J3 D8-c: concentration ×3 multiplier in spellExpectedDamage

`spellExpectedDamage` (`dnd_class_balance.go`) now multiplies expected
damage by 3 when `sp.Concentration && sp.Effect ∈ {EffectDamageSave,
EffectDamageAuto}`. Attack-roll concentration excluded by design —
hex-style cases ride per-hit mods, not the score. Closes the picker
gap that walked past heat_metal / spike_growth / flaming_sphere /
cloud_of_daggers in favour of one-shot blasts of similar slot level.

Measured on n=5000 d8c corpus vs d8prereq baseline (sim_results,
gitignored): per-class means all within ±2.4pp (1σ ≈ 2.2pp); macro
leaderboard unchanged. Real picker swap lands at T3 manor_blackspire
— bard +11pp, mage +10pp, sorcerer +9pp, druid +5pp. T4/T5 caster
wall unchanged → confirms HP+AC is the binding constraint past T3
(D8-d-fix territory), not picker quality.

Side discovery: cleric flat / warlock −6pp manor — spirit_guardians-
style AOE-save concentration spells appear to resolve once in
SimulateCombat / turn-engine instead of re-ticking per round. The
multiplier is correct in expectation; the engine under-delivers.
Filed as separate follow-up; D8-c stays.

Plan §8 D8-c marked SHIPPED.
This commit is contained in:
prosolis
2026-05-28 08:14:13 -07:00
parent 63ad423b79
commit d7ced471a1
3 changed files with 117 additions and 16 deletions

View File

@@ -272,9 +272,19 @@ func spellExpectedDamage(s SpellDefinition, slot, charLevel int) float64 {
}
avgFace := (float64(faces) + 1) / 2
avg := float64(dice)*avgFace + float64(flat)
// Auto-damage (Magic Missile) doesn't roll to hit — count its
// expected-on-table value at face. Attack/save spells roll, and the
// engine will resolve hit chance at cast time.
// Concentration damage spells (heat_metal, spirit_guardians,
// flaming_sphere, call_lightning, spike_growth, cloud_of_daggers, …)
// re-tick each round while concentration holds. Without this factor
// the picker scores them as one-shots and they lose to higher-tier
// blasts on every comparison. Conservative ×3 = roughly the median
// fight length the picker can hope to keep concentration up; tier-
// scaled would be more correct but adds noise here.
// EffectDamageAttack is excluded — single-target attack-roll spells
// aren't generally concentration; the rare ones (hex-style) get
// their lift from mods, not this score.
if s.Concentration && (s.Effect == EffectDamageSave || s.Effect == EffectDamageAuto) {
avg *= 3
}
return avg
}