adventure: T6 Amendment (Custodian) + in-combat round-end boss seam (P8)

First in-combat Layer-2 mechanic. applyBossInCombatRoundEnd is a new
round-boundary seam called from the turn engine's stepRoundEnd, the
counterpart to the pre-combat applyBossRunModifiers. The Custodian snapshots
its HP at end of round 3 and rewinds to it once on the phase-2 crossing
(refunding front-loaded burst); a soft midnight timer past round 20 climbs
its Attack via the existing enemyAtkBuff. New state (EnemyRewindHP/Used)
round-trips through CombatStatuses so a suspend/resume can't replay the
rewind. Sim A/B (n=120 L20 party): last_meridian fighter 65->37.5% clear,
a clean -27.5pp swing attributable to the mechanic; shipped as-is per the
opt-in-endgame difficulty call.

Claude-Session: https://claude.ai/code/session_0156WqjgsbmSY2U8eQ3Kkb1s
This commit is contained in:
prosolis
2026-07-16 09:20:29 -07:00
parent db13ed75b9
commit 189a44e1eb
6 changed files with 323 additions and 12 deletions

View File

@@ -370,7 +370,10 @@ func resumeTurnEngine(sess *CombatSession, players []*Combatant, enemy *Combatan
enemyRevealNext: sess.Statuses.EnemyRevealNext,
enemyFearImmune: sess.Statuses.EnemyFearImmune,
enemyAtkBuff: sess.Statuses.EnemyAtkBuff,
rng: rng,
// Amendment (T6 Custodian) — round-3 snapshot + once-only rewind.
enemyRewindHP: sess.Statuses.EnemyRewindHP,
enemyRewindUsed: sess.Statuses.EnemyRewindUsed,
rng: rng,
}
order := turnOrder(sess, sess.Round, players, enemy)
sess.Statuses.TurnIdx = turnIdxForPhase(order, sess.Statuses.TurnIdx, sess.Phase)
@@ -879,6 +882,14 @@ func (te *turnEngine) stepRoundEnd() {
Damage: st.enemyRegen, PlayerHP: st.playerHP, EnemyHP: st.enemyHP,
})
}
// Tier-6 in-combat Layer-2 boss hooks (Amendment): round-3 HP snapshot +
// once-only phase-2 rewind + soft midnight timer, resolved on the round that
// just finished. A no-op for every enemy but the hooked bosses, and only
// while the enemy still stands, so it is safe to call unconditionally here
// after the round's damage has settled.
if st.enemyHP > 0 {
applyBossInCombatRoundEnd(st, te.sess.EnemyID, te.enemy.Stats.MaxHP)
}
st.round++
// Initiative is re-rolled each round, so the next round's order is derived
// here — off st.round, since commit has not yet pushed it onto the session.
@@ -939,6 +950,8 @@ func (te *turnEngine) commit() {
s.EnemyRevealNext = st.enemyRevealNext
s.EnemyFearImmune = st.enemyFearImmune
s.EnemyAtkBuff = st.enemyAtkBuff
s.EnemyRewindHP = st.enemyRewindHP
s.EnemyRewindUsed = st.enemyRewindUsed
te.sess.TurnLog = append(te.sess.TurnLog, st.events...)
}