The goals of this quick analyses:
- Create a conversion tool between OSR monsters and Suldokars wake
- Create a few combat curves for Suldokar's wake to help with creature design
- Explore player/boss survivability with endrolls
Two combat simulators were written quick and dirty in R
- assume 1d6 damage as a baseline for comparisons between DCC and Suldokar's Wake
- roll attack and count number of attacks
- if attack succeeds, roll damage (incorporating the special success die upgrade for SW)
- count number of attacks until death
- ignore crit, fumbles
This is a simplified analyses and really represents an upper bound on combat. You would expect actual combat to take less turns due to:
- vulnerabilities
- armour piercing bullets
- burst / autofire and other weapon rules
- enemies with multiple attacks
- higher damage dies (d8, d10)
- critical hit dice 1ed6 exploding damage
- critical hit dice effects (table in issue 3)
The chart also presents the median. Remember, 50% of the time the combat will be faster. It's entirely possible to one-shot a boss. For example, a d8 die with a special success stepped up to 10 with a high roll against a bulk 6 enemy boss. The player then calls for an endroll (50% chance the boss goes to an injured state)
## common setup
library(dplyr)
library(ggplot2)
n <- 15000 ## number of simulated combat scenarios per variable
The OSR one uses ascending AC from 10. It does a D20 roll against AC (must exceed to hit). No modifier.
## OSR simulator
dd_run <- function(ac, hp, weapon_damage) {
df <-bind_rows(
replicate(n, data.frame(case=c("dd"), hp=c(hp), ac=c(ac), weapon_damage=c(weapon_damage)), simplify = FALSE)
)
hit_until_dead <- function(ac, hp, weapon_damage) {
cur_hp <- hp
attacks <- 0
while(cur_hp > 0) {
if (sample(1:20, 1, replace=T) >= ac) {
attacks = attacks + 1
cur_hp = cur_hp - sample(1:weapon_damage, 1, T)
} else {
#print("miss")
attacks = attacks + 1
}
}
return(attacks)
}
return(df %>% rowwise() %>% mutate(hits = hit_until_dead(ac, hp, weapon_damage)))
}
The Suldokar's Wake simulator is a little more complicated. There are more variables to track:
sw_run <- function(bulk, combat_rating, weapon_damage, armoured, er_threshold, chance_endroll, major) {
df <-bind_rows(
replicate(n, data.frame(case=c("sw"), bulk=c(bulk), combat_rating = c(combat_rating), armoured=c(armoured), weapon_damage=c(weapon_damage),
er_threshold = c(er_threshold), chance_endroll = c(chance_endroll), major=c(major)), simplify = FALSE)
)
hit_until_end <- function(bulk, combat_rating, weapon_damage, armoured, er_threshold, chance_endroll, major, harm_fn) {
harm <- 0
attacks <- 0
alive <- TRUE
while(alive) {
hitroll <- sample(1:20, 1, replace=T)
if(hitroll > 15 || hitroll <= combat_rating) {
attacks = attacks + 1
if(armoured) {
if(hitroll > 15) { ## step up damage die
harm = harm + min(sample(1:weapon_damage+2, 2, T))
} else {
harm = harm + min(sample(1:weapon_damage, 2, T))
}
} else {
if(hitroll > 15) { ## step up damage die
harm = harm + sample(1:weapon_damage+2, 1, T)
} else {
harm = harm + sample(1:weapon_damage, 1, T)
}
}
if(harm >= 20) {
alive = FALSE
} else {
if(major) {
if(harm > bulk && harm >= er_threshold) {
shouldEndroll = sample(1:100, 1, T)
if(shouldEndroll <= chance_endroll) {
endroll <- sample(1:20, 1, T)
if (endroll > harm) {
harm = 0
} else {
alive = FALSE
}
}
}
} else {
if (harm > bulk)
alive = FALSE
}
}
} else {
attacks = attacks + 1
}
}
return(attacks)
}
return(df %>% rowwise() %>% mutate(hits = hit_until_end(bulk, combat_rating, weapon_damage, armoured, er_threshold, chance_endroll, major)))
}
When fighting non-boss, non-players it's a simple check if the hitroll > 15 or <= combat_rating, do damage, and if the cummulative damage exceeds bulk then it's dead.
Someone with a Combat Rating 2, attacking a 2 bulk creature, with 1d6 damage die, that is unarmoured looks like the following:
sw_sum(2, 2, 6, FALSE, 20, 100, FALSE)
Only the first four inputs matter.
Two new variables come into play:
er_threshold
: the threshold at which to start calling for an end_roll *chance_endroll
: for a given opportunity where harm > er_threshold, probability that the end_roll is called for. If the endroll succeeds, harm goes back to 0
Method based on survivability in combat:
- lookup OSR monster HP (convert from HitDice as you need to) and lookup AC
- find number of hits until it dies
- lookup the number of hits on the suldokar's wake chart on the y-axis
- you may need to map to armoured
- hit a curve (pick an appropriate combat rating, or an average value)
- drop down to the x-axis to figure out bulk
Because PH harm is fixed, we don't have the same range as a buff, high level D&D style creature. Things you may want to consider for even tankier enemies:
- resistences
- giving a modifier on harm rolls (maybe this armoured tank like construct gets +5 or +10 on it's end_rolls)
- stepping up the harm pool to 24, 30, higher and using higher dice.
Here we generate some tables:
- waiting until player/boss hits 20 harm, no end roll called for
- various scenarios for end roll via
er_threshold
andchance_endroll
For example, if er_threshold is at 10 and chance_endroll
is at 50% it means the following:
- every time they take damage and their harm is above 10, there is a 50% chance the simulation will call for an endroll
- if the endroll succeeds, arm is reduced back to 0
- if the endroll fails, put in injured/end state
Remember: this is a simplified analyses representing an upperbound that is looking at mediansi. Combat can be quite lethal! Look at the example in the appendix of Issue 3 where an enemy with two attacks gets a lucky crit with an exploding die on one of the PCs.
These are medians, so 50% of the time. Unlucky dice by the players on their end_roll can put them in an injured state early. Mind you, they are still tough as they can use Gunta to turn the clean roll into a normal roll; as well as various
Below is the median with some analyses on the effect of calling for an endroll. In general, calling for an endroll starting around harm 10 will lower the number of attacks by 2-3 on average.
It can be a little swingy when we look at quantiles:
case | 0.10 percentile attacks | 0.25 percentile_attacks |
---|---|---|
d6 armoured | 5 | 7 |
d6 unarmoued | 4 | 6 |
d8 armoured | 4 | 6 |
d8 unarmoued | 3 | 5 |
d10 armoued | 4 | 5 |
d10 unarmoued | 3 | 4 |
Again: this is an upper bound, if you factor in crits, armour piercing, burst/auto, you would expect those number of attacks to go down.
Armoured:
er_threshold | chance_endroll | n | median_attacks | mean_attacks |
---|---|---|---|---|
10 | 0 | 20000 | 12 | 12.37185 |
10 | 100 | 20000 | 9 | 11.32275 |
10 | 25 | 20000 | 11 | 11.83950 |
10 | 50 | 20000 | 10 | 11.59295 |
10 | 75 | 20000 | 9 | 11.36855 |
13 | 100 | 20000 | 9 | 11.37700 |
13 | 25 | 20000 | 11 | 11.96395 |
13 | 50 | 20000 | 10 | 11.64085 |
13 | 75 | 20000 | 10 | 11.44420 |
15 | 100 | 20000 | 10 | 11.29380 |
15 | 25 | 20000 | 11 | 11.94015 |
15 | 50 | 20000 | 11 | 11.67740 |
15 | 75 | 20000 | 10 | 11.51155 |
unarmoured:
er_threshold | chance_endroll | n | median_attacks | mean_attacks |
---|---|---|---|---|
10 | 0 | 20000 | 9 | 9.84820 |
10 | 25 | 20000 | 9 | 9.44010 |
10 | 50 | 20000 | 8 | 9.12245 |
10 | 75 | 20000 | 7 | 9.02790 |
10 | 100 | 20000 | 7 | 8.91115 |
13 | 25 | 20000 | 9 | 9.47790 |
13 | 50 | 20000 | 8 | 9.26965 |
13 | 75 | 20000 | 8 | 9.05585 |
13 | 100 | 20000 | 7 | 8.86650 |
15 | 25 | 20000 | 9 | 9.59115 |
15 | 50 | 20000 | 8 | 9.32790 |
15 | 75 | 20000 | 8 | 9.16945 |
15 | 100 | 20000 | 8 | 8.94960 |
Armoured:
er_threshold | chance_endroll | n | median_attacks | mean_attacks |
---|---|---|---|---|
10 | 0 | 20000 | 10 | 10.54795 |
10 | 25 | 20000 | 9 | 10.07630 |
10 | 50 | 20000 | 8 | 9.77245 |
10 | 75 | 20000 | 8 | 9.58775 |
10 | 100 | 20000 | 8 | 9.51535 |
13 | 25 | 20000 | 9 | 10.22020 |
13 | 50 | 20000 | 9 | 9.93965 |
13 | 75 | 20000 | 8 | 9.66580 |
13 | 100 | 20000 | 8 | 9.60910 |
15 | 25 | 20000 | 10 | 10.33490 |
15 | 50 | 20000 | 9 | 9.98940 |
15 | 75 | 20000 | 9 | 9.74660 |
15 | 100 | 20000 | 8 | 9.58485 |
Unarmoured:
er_threshold | chance_endroll | n | median_attacks | mean_attacks |
---|---|---|---|---|
10 | 0 | 20000 | 8 | 8.29980 |
10 | 25 | 20000 | 7 | 7.91085 |
10 | 50 | 20000 | 7 | 7.60175 |
10 | 75 | 20000 | 6 | 7.42600 |
10 | 100 | 20000 | 6 | 7.23565 |
13 | 25 | 20000 | 7 | 7.99820 |
13 | 50 | 20000 | 7 | 7.71005 |
13 | 75 | 20000 | 6 | 7.50150 |
13 | 100 | 20000 | 6 | 7.29695 |
15 | 25 | 20000 | 7 | 8.02650 |
15 | 50 | 20000 | 7 | 7.80835 |
15 | 75 | 20000 | 7 | 7.60130 |
15 | 100 | 20000 | 6 | 7.35970 |
Armoured:
er_threshold | chance_endroll | n | median_hits | mean_hits |
---|---|---|---|---|
10 | 0 | 20000 | 9 | 9.35685 |
10 | 25 | 20000 | 8 | 8.85140 |
10 | 50 | 20000 | 7 | 8.59005 |
10 | 75 | 20000 | 7 | 8.34590 |
10 | 100 | 20000 | 7 | 8.30160 |
13 | 25 | 20000 | 8 | 8.97210 |
13 | 50 | 20000 | 8 | 8.72400 |
13 | 75 | 20000 | 7 | 8.42790 |
13 | 100 | 20000 | 7 | 8.26890 |
15 | 25 | 20000 | 8 | 9.02640 |
15 | 50 | 20000 | 8 | 8.79330 |
15 | 75 | 20000 | 8 | 8.57940 |
15 | 100 | 20000 | 7 | 8.35305 |
Unarmoured:
er_threshold | chance_endroll | n | median_hits | mean_hits |
---|---|---|---|---|
10 | 0 | 20000 | 7 | 7.20475 |
10 | 25 | 20000 | 6 | 6.90545 |
10 | 50 | 20000 | 6 | 6.54085 |
10 | 75 | 20000 | 5 | 6.36240 |
10 | 100 | 20000 | 5 | 6.14810 |
13 | 25 | 20000 | 6 | 6.92405 |
13 | 50 | 20000 | 6 | 6.66190 |
13 | 75 | 20000 | 6 | 6.50830 |
13 | 100 | 20000 | 5 | 6.25840 |
15 | 25 | 20000 | 6 | 6.97195 |
15 | 50 | 20000 | 6 | 6.77085 |
15 | 75 | 20000 | 6 | 6.56035 |
15 | 100 | 20000 | 6 | 6.32255 |