adventure: route round-end concentration kill through enemyDown (P8)

The T6 Valdris phylactery rebirth (and the pre-existing survive_at_1
one-shot) live in enemyDown, on the premise that it is the single death
chokepoint. It isn't: the round-end concentration pulse ended the fight
on a raw enemyHP<=0 read, so a cleric's Spirit Guardians landing the
lethal blow robbed a revive-armed boss of its rebirth — exactly the
cleric-party arm P8 is tuned around. Route that win-check through
enemyDown so the boss gets its chance to stand back up. Regression test
covers both the armed (revives) and charge-less (still wins) paths.

Claude-Session: https://claude.ai/code/session_0156WqjgsbmSY2U8eQ3Kkb1s
This commit is contained in:
prosolis
2026-07-16 08:41:58 -07:00
parent 686434f8e3
commit db13ed75b9
2 changed files with 60 additions and 1 deletions

View File

@@ -842,7 +842,12 @@ func (te *turnEngine) stepRoundEnd() {
Round: st.round, Phase: CombatPhaseRoundEnd, Actor: "player", Action: "concentration_tick", Round: st.round, Phase: CombatPhaseRoundEnd, Actor: "player", Action: "concentration_tick",
Damage: st.concentrationDmg, PlayerHP: st.playerHP, EnemyHP: st.enemyHP, Seat: i, Damage: st.concentrationDmg, PlayerHP: st.playerHP, EnemyHP: st.enemyHP, Seat: i,
}) })
if st.enemyHP <= 0 { // Route the kill through enemyDown, not a raw HP read: a boss that cheats
// death (survive_at_1) or holds a phylactery rebirth (T6 Valdris) must get
// that chance even when the lethal blow is a lingering concentration pulse.
// enemyDown restores its HP and returns false, so the next seat's pulse (or
// the following round) resolves against the revived pool.
if enemyDown(st, CombatPhaseRoundEnd) {
te.finish(CombatStatusWon) te.finish(CombatStatusWon)
return return
} }

View File

@@ -275,3 +275,57 @@ func TestTurnEngine_CommitPersistsSeatZeroNotTheCursor(t *testing.T) {
t.Error("seat 1's consumed Lucky reroll leaked onto the session row") t.Error("seat 1's consumed Lucky reroll leaked onto the session row")
} }
} }
// A lingering concentration pulse that lands the killing blow must still give a
// revive-armed boss (survive_at_1 / T6 Valdris's phylactery rebirth) its chance
// to stand back up — the round-end tick routes the kill through enemyDown, not a
// raw enemyHP<=0 read. Regression for the concentration-bypass gap found in the
// P8 Layer-2 review.
func TestTurnEngine_ConcentrationKillHonorsRebirth(t *testing.T) {
// A charged rebirth: the pulse drops the enemy, a charge spends, it revives.
sess := turnSession(CombatPhaseRoundEnd, 500, 30)
p := basePlayer()
e := baseEnemy()
te := resumeTurnEngine(sess, []*Combatant{&p}, &e, combatSessionStepRNG(sess, enemySeat))
te.st.concentrationDmg = 100 // lethal against 30 HP
te.st.enemyReviveCharges = 1
te.st.enemyReviveHP = 40
if _, err := te.step(PlayerAction{}); err != nil {
t.Fatal(err)
}
te.commit()
if !sess.IsActive() {
t.Fatalf("a concentration kill ended the fight (%q) while a rebirth charge was armed", sess.Status)
}
if sess.EnemyHP != 40 {
t.Errorf("revived EnemyHP = %d, want the 40-HP revive pool", sess.EnemyHP)
}
if sess.Statuses.EnemyReviveCharges != 0 {
t.Errorf("post-revive charges = %d, want 0 (one spent)", sess.Statuses.EnemyReviveCharges)
}
rebirths := 0
for _, ev := range sess.TurnLog {
if ev.Action == "phylactery_rebirth" {
rebirths++
}
}
if rebirths != 1 {
t.Errorf("phylactery_rebirth events = %d, want 1", rebirths)
}
// With no charge left, the same pulse ends the fight cleanly (the win path
// is not broken by the enemyDown routing).
mortal := turnSession(CombatPhaseRoundEnd, 500, 30)
p2 := basePlayer()
e2 := baseEnemy()
te2 := resumeTurnEngine(mortal, []*Combatant{&p2}, &e2, combatSessionStepRNG(mortal, enemySeat))
te2.st.concentrationDmg = 100
if _, err := te2.step(PlayerAction{}); err != nil {
t.Fatal(err)
}
te2.commit()
if mortal.Status != CombatStatusWon {
t.Errorf("charge-less concentration kill status = %q, want %q", mortal.Status, CombatStatusWon)
}
}