Review follow-up M: Grim Harvest fires in turn-based combat

The doc's item M claimed all three mage subclass spell hooks were dead on the
turn path because applyMageSubclassSpellHooks had one caller. It has three.
resolveTurnSpell has called it since 5cd343a, so Empowered Evocation and
Overchannel always worked -- they only move mods.SpellPreDamage, which
resolveTurnSpell returns as EnemyDamage.

Grim Harvest was the real defect, with a narrower cause: the hook wrote
mods.GrimHarvestSlot into a local CombatModifiers that resolveTurnSpell
discarded, because turnSpellOutcome had no field to carry it out. A Necromancy
Mage who killed with a spell in a manual fight never healed.

The stash can't ride on fight-start mods the way auto-resolve's does -- the
spell is cast mid-fight and the turn engine rebuilds combatants every round --
so it rides on the casting seat's ActorStatuses, like ArmedAbility. Each
damaging cast overwrites it; snapshotActor carries it across commit().

grimHarvestHeal also scanned for the *first* spell_cast event to ask whether
the spell landed the killing blow. Auto-resolve casts once, pre-combat, so
first == last there. A turn-based mage casts every round, so a non-lethal
opening cantrip vetoed the heal the killing spell had earned. Now scans for the
last spell_cast -- provably identical on the auto-resolve path, so the golden
corpus does not move.

Balance: a caster buff on the manual surface only, and the one the subclass was
written to have. Auto-resolve already paid it out.

Claude-Session: https://claude.ai/code/session_017mEwUmmS7aQTP2NQXj6rUa
This commit is contained in:
prosolis
2026-07-10 08:25:56 -07:00
parent a59a544fff
commit c34e740008
7 changed files with 296 additions and 16 deletions

View File

@@ -310,6 +310,13 @@ type turnSpellOutcome struct {
PlayerHeal int
EnemySkip bool
Desc string
// GrimHarvestSlot / GrimHarvestNecrotic are the Necromancy Mage's kill-heal
// stash for this cast, zero for everyone else. Unlike the fields above they
// outlive the casting round: the caller parks them on the seat's
// ActorStatuses and the close-out decides whether the cast was lethal.
GrimHarvestSlot int
GrimHarvestNecrotic bool
}
// resolveTurnSpell resolves a spell as a turn-based player action, reusing the
@@ -344,6 +351,8 @@ func resolveTurnSpell(c *DnDCharacter, spell SpellDefinition, slotLevel int, ene
out.EnemyDamage = mods.SpellPreDamage
out.EnemySkip = mods.SpellEnemySkipFirst
out.Desc = mods.SpellPreDamageDesc
out.GrimHarvestSlot = mods.GrimHarvestSlot
out.GrimHarvestNecrotic = mods.GrimHarvestNecrotic
if out.Desc == "" {
out.Desc = spell.Name
}