combat: revive the caster sustained-cantrip floor in the turn engine

The arcane-blaster "sustained floor" passives (CantripPerRound, DamageBonus,
FlatDmgStart from casterBlasterFloor) were built for the swing-based engine
(SimulateCombat, combat_engine.go:590). But every live expedition auto-resolves
through the turn engine (autoDriveCombat -> session -> combat_turn_engine),
where casters autocast every turn and never weapon-swing -- so CantripPerRound
never fired and DamageBonus was inert. Casters fought at bare cantrip dice
(~4d10~=22 at L20) instead of their intended floor, in sim AND in prod. This is
why every caster damage dial read as a dead lever across the whole rebaseline.

Fix (combat_cmd.go): bridge the already-computed CantripPerRound into the
turn-engine damage-cantrip cast, hit-gated (only lift a cast that already
connected, so the ~35% miss variance survives and the floor isn't a guaranteed
flat hammer). Self-targeting: only Mage/Sorcerer/Warlock carry a nonzero
CantripPerRound -- martials swing (untouched), cleric/bard/druid have floor 0.

Tuning (dnd_passives.go): casterCantripBase 9 -> 3, now a live, class-specific
lever. Mage/Sorcerer take base 3; Warlock passes 0 (its bare-dice cantrip plus
a structural edge already lands it mid-band, so an added floor overshoots).
Removed the dead casterHPPerLevel rider (it inflated the truncation-fraction
denominator without adding startable HP -- a bug).

Also lands the deterministic-seeding infra (sim_seed.go + simIntN/simFloat64
threading) used to read these deltas out of the process-seed noise; prod is
byte-identical (unseeded -> package rand).

Confirmation (expedition-sim, L20 T5 dragons_lair+abyss_portal, n=250):
casters now in the 35-45 floor -- sorcerer 39, mage 38, warlock 36; martial
leaders undisturbed (rogue 68, druid 66, ranger 65, fighter 64, ... paladin 55).
This commit is contained in:
prosolis
2026-07-17 16:17:41 -07:00
parent 6e2782ac48
commit 1f62a8e842
13 changed files with 281 additions and 31 deletions

View File

@@ -3,7 +3,6 @@ package plugin
import (
"fmt"
"log/slog"
"math/rand/v2"
"maunium.net/go/mautrix/id"
)
@@ -128,7 +127,7 @@ func applyMageSubclassSpellHooks(c *DnDCharacter, spell SpellDefinition, slotLev
// applySpellDamageAttack — Fire Bolt, Inflict Wounds, Chill Touch, etc.
// Roll d20 + spell attack vs enemy AC; nat 20 doubles dice damage.
func applySpellDamageAttack(spell SpellDefinition, atk int, mods *CombatModifiers, enemy *CombatStats, slot, charLevel int) {
roll := 1 + rand.IntN(20)
roll := 1 + simIntN(20)
isCrit := roll == 20
isFumble := roll == 1
if isFumble || (!isCrit && roll+atk < enemy.AC) {
@@ -158,7 +157,7 @@ func applySpellDamageAttack(spell SpellDefinition, atk int, mods *CombatModifier
// future multi-enemy combat (Phase 11+) but is not consulted here.
func applySpellDamageSave(spell SpellDefinition, dc int, c *DnDCharacter, mods *CombatModifiers, enemy *CombatStats, slot int) {
saveMod := enemySpellSaveMod(enemy)
saveRoll := 1 + rand.IntN(20)
saveRoll := 1 + simIntN(20)
saved := saveRoll+saveMod >= dc
dmg := rollSpellDamageDice(spell, slot, c.Level)
if saved {
@@ -182,7 +181,7 @@ func applySpellDamageAuto(spell SpellDefinition, mods *CombatModifiers, slot, ch
}
total := 0
for i := 0; i < darts; i++ {
total += 1 + rand.IntN(4) + 1
total += 1 + simIntN(4) + 1
}
mods.SpellPreDamage += total
mods.SpellPreDamageDesc = fmt.Sprintf("Magic Missile (%d darts, %d dmg)", darts, total)
@@ -223,7 +222,7 @@ func enemySpellSaveMod(enemy *CombatStats) int {
// double damage (5e: paralyzed creatures auto-crit on melee hits).
func applySpellControl(spell SpellDefinition, dc int, mods *CombatModifiers, enemy *CombatStats, slot int) {
saveMod := enemySpellSaveMod(enemy)
saveRoll := 1 + rand.IntN(20)
saveRoll := 1 + simIntN(20)
if saveRoll+saveMod >= dc {
mods.SpellPreDamageDesc = spell.Name + " — resisted"
return
@@ -382,7 +381,7 @@ func rollTurnSpellHeal(c *DnDCharacter, spell SpellDefinition, slotLevel int) in
if supreme {
heal += faces
} else {
heal += 1 + rand.IntN(faces)
heal += 1 + simIntN(faces)
}
}
heal += abilityModifier(c.WIS)
@@ -418,7 +417,7 @@ func rollSpellDamageDice(spell SpellDefinition, slot, charLevel int) int {
}
total := flat
for i := 0; i < dice; i++ {
total += 1 + rand.IntN(faces)
total += 1 + simIntN(faces)
}
if total < 1 {
total = 1