mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-18 01:42:42 +00:00
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:
@@ -443,6 +443,15 @@ type combatState struct {
|
||||
enemyFearImmune bool // fear_immune: player control spells (enemy-skip) fizzle against this enemy
|
||||
enemyAtkBuff int // ally_buff: flat, accumulating bonus to the enemy's attack damage
|
||||
|
||||
// Amendment (T6 Custodian) — an in-combat Layer-2 hook resolved at round end
|
||||
// by applyBossInCombatRoundEnd. enemyRewindHP is the boss's round-3 HP
|
||||
// snapshot (0 until captured); enemyRewindUsed gates the once-only rewind
|
||||
// that restores it to that snapshot when the boss crosses into phase 2. The
|
||||
// soft midnight timer past round 20 rides enemyAtkBuff. Round-tripped through
|
||||
// CombatStatuses; zero/false for every non-Custodian fight.
|
||||
enemyRewindHP int
|
||||
enemyRewindUsed bool
|
||||
|
||||
round int
|
||||
events []CombatEvent
|
||||
|
||||
|
||||
@@ -333,6 +333,10 @@ func renderEvent(e CombatEvent, playerName, enemyName string, result CombatResul
|
||||
return pickRand(narrativeSurvive)
|
||||
case "phylactery_rebirth":
|
||||
return pickRand(narrativePhylacteryRebirth)
|
||||
case "amendment_rewind":
|
||||
return pickRand(narrativeAmendmentRewind)
|
||||
case "midnight_toll":
|
||||
return fmt.Sprintf(pickRand(narrativeMidnightToll), e.Damage)
|
||||
case "stat_drain":
|
||||
return fmt.Sprintf(pickRand(narrativeStatDrain), e.Damage)
|
||||
case "debuff":
|
||||
@@ -772,6 +776,22 @@ var narrativePhylacteryRebirth = []string{
|
||||
"💀 That should have been the end of him. A Verse still hums somewhere in the cathedral, and Valdris simply *begins again*.",
|
||||
}
|
||||
|
||||
// narrativeAmendmentRewind fires when the Custodian rewinds itself to its round-3
|
||||
// HP snapshot — the once-only Amendment. Each line reads as time being undone so
|
||||
// the mechanic (front-loaded burst is partly refunded) teaches itself over a run.
|
||||
var narrativeAmendmentRewind = []string{
|
||||
"🕰️ The Custodian raises a hand and *edits the last few minutes out of the record.* Wounds close in reverse; the clock-golem stands as it did rounds ago.",
|
||||
"🕰️ \"That entry is amended.\" The damage you dealt simply un-happens — the Custodian rewinds to where it was and resumes, unhurried.",
|
||||
"🕰️ Verdigris rings spin backward. Time you spent hurting it is refunded to the golem; it returns to its round-three self and keeps working.",
|
||||
}
|
||||
|
||||
// narrativeMidnightToll fires past round 20 — the soft closing-time timer, the
|
||||
// Custodian's Attack climbing each round a stalled fight refuses to end.
|
||||
var narrativeMidnightToll = []string{
|
||||
"🔔 A bell tolls somewhere above. Closing time — the Custodian's swings come harder. (+%d attack)",
|
||||
"🔔 The hour is nearly spent, and so is its patience; each blow lands with more weight now. (+%d attack)",
|
||||
}
|
||||
|
||||
var narrativeStatDrain = []string{
|
||||
"🩸 The enemy saps your strength — your swings feel heavier, weaker. (-%d hit damage)",
|
||||
"🩸 Something drains out of your limbs. Your hits won't bite as deep now. (-%d damage)",
|
||||
|
||||
@@ -232,6 +232,17 @@ type CombatStatuses struct {
|
||||
EnemyRevealNext bool `json:"enemy_reveal_next,omitempty"`
|
||||
EnemyFearImmune bool `json:"enemy_fear_immune,omitempty"`
|
||||
EnemyAtkBuff int `json:"enemy_atk_buff,omitempty"`
|
||||
|
||||
// Amendment (Tier-6 postgame, The Custodian of the Last Hour). An in-combat
|
||||
// Layer-2 hook resolved at round end (applyBossInCombatRoundEnd), not proc-
|
||||
// armed: EnemyRewindHP snapshots the boss's HP at the end of round 3 (0 until
|
||||
// captured); when the boss then crosses into phase 2 the hook restores it to
|
||||
// that snapshot exactly once and sets EnemyRewindUsed. Both round-trip through
|
||||
// combatState so the once-only rewind survives a suspend/resume. Zero/false
|
||||
// for every other enemy. The soft midnight timer past round 20 rides the
|
||||
// existing EnemyAtkBuff, so it needs no field of its own.
|
||||
EnemyRewindHP int `json:"enemy_rewind_hp,omitempty"`
|
||||
EnemyRewindUsed bool `json:"enemy_rewind_used,omitempty"`
|
||||
}
|
||||
|
||||
// applyBuffDelta folds one resolved buff (the result of a !cast / !consume
|
||||
|
||||
@@ -370,6 +370,9 @@ func resumeTurnEngine(sess *CombatSession, players []*Combatant, enemy *Combatan
|
||||
enemyRevealNext: sess.Statuses.EnemyRevealNext,
|
||||
enemyFearImmune: sess.Statuses.EnemyFearImmune,
|
||||
enemyAtkBuff: sess.Statuses.EnemyAtkBuff,
|
||||
// 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)
|
||||
@@ -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...)
|
||||
}
|
||||
|
||||
@@ -7,18 +7,26 @@ package plugin
|
||||
// is the bespoke, per-boss stuff the plan promised — mechanics that read the
|
||||
// *run* the player took to reach the boss, not just the fight in front of them.
|
||||
//
|
||||
// This file holds the PRE-COMBAT half of that seam: adjustments derived once
|
||||
// from run state and folded into the boss's live Combatant before the fight
|
||||
// resolves. It must be a PURE, IDEMPOTENT function of run state, because the
|
||||
// turn engine rebuilds the enemy from the bestiary every single round
|
||||
// (partyCombatantsForSession) — the boss's numbers are re-derived on every
|
||||
// !attack. That is fine as long as the inputs are frozen for the duration of
|
||||
// the fight, which they are: the boss room is terminal, so no more nodes are
|
||||
// walked once the fight begins.
|
||||
// It holds BOTH halves of that seam:
|
||||
//
|
||||
// In-combat Layer-2 hooks (Inversion Stitch, Amendment, Two Hearts) are a
|
||||
// separate seam — a new MonsterAbility.Effect case in applyAbility, shared by
|
||||
// both engines — and are not in this file.
|
||||
// - PRE-COMBAT (applyBossRunModifiers / seedBossRunStatuses): adjustments
|
||||
// derived once from run state and folded into the boss's live Combatant (or
|
||||
// the fresh session) before the fight resolves. applyBossRunModifiers must be
|
||||
// a PURE, IDEMPOTENT function of run state, because the turn engine rebuilds
|
||||
// the enemy from the bestiary every single round (partyCombatantsForSession)
|
||||
// — the boss's numbers are re-derived on every !attack. That is fine as long
|
||||
// as the inputs are frozen for the fight, which they are: the boss room is
|
||||
// terminal, so no more nodes are walked once the fight begins.
|
||||
//
|
||||
// - IN-COMBAT (applyBossInCombatRoundEnd): round-boundary mechanics that read
|
||||
// and mutate the live combatState — HP snapshots, once-only rewinds, escalating
|
||||
// timers. Called from the turn engine's stepRoundEnd after the round's damage
|
||||
// has settled. Any state it spends round-trips through CombatStatuses so a
|
||||
// suspend/resume can't replay it.
|
||||
//
|
||||
// The remaining planned in-combat mechanics (Inversion Stitch, Two Hearts) are
|
||||
// still a separate seam — a new MonsterAbility.Effect case in applyAbility — and
|
||||
// are not in this file yet.
|
||||
|
||||
// applyBossRunModifiers folds any pre-combat Layer-2 mechanic for bossID into
|
||||
// the freshly-built enemy Combatant, reading the run the player took to get
|
||||
@@ -168,3 +176,88 @@ func seedBossRunStatuses(sess *CombatSession, bossID string, enemyMaxHP int, run
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ── Amendment — The Custodian of the Last Hour (The Last Meridian) ───────────
|
||||
//
|
||||
// The Custodian has concluded its contract is complete and is dismantling the
|
||||
// hours on its way out — including, once, the last few minutes of its own fight.
|
||||
// Its true durability is HIDDEN in the opening rounds: it snapshots its HP at the
|
||||
// end of round 3, and the first time the party knocks it into phase 2 it *rewinds
|
||||
// itself* to that snapshot — once. Front-loaded burst is partially refunded (the
|
||||
// damage past the snapshot is undone), so sustained builds that can grind through
|
||||
// the extra pool shine over glass-cannon alpha strikes. A soft "midnight timer"
|
||||
// then leans on stalls: every round past round 20 the Custodian's Attack climbs,
|
||||
// so a fight that can't close still resolves before the clock runs out.
|
||||
//
|
||||
// This is the first IN-COMBAT Layer-2 mechanic. Unlike the pre-combat hooks it
|
||||
// reads/mutates the live combatState at round end, and its once-only state
|
||||
// (EnemyRewindHP snapshot + EnemyRewindUsed) round-trips through CombatStatuses so
|
||||
// a suspend/resume can't hand the boss a second rewind. Dispatched by bestiary ID,
|
||||
// so it is a no-op for every other enemy.
|
||||
const (
|
||||
// custodianSnapshotRound is the round whose end HP the Custodian rewinds to.
|
||||
// Snapshotting late enough that a normal party has committed real damage, but
|
||||
// early enough that the refund still matters, is the whole point — the fight's
|
||||
// true length is concealed until the rewind fires.
|
||||
custodianSnapshotRound = 3
|
||||
|
||||
// custodianPhaseTwoFrac must track The Custodian's PhaseTwoAt in
|
||||
// postgame_zone_defs.go (0.45): the rewind is meant to fire exactly as the
|
||||
// party crosses the boss into phase 2. Kept as a local constant because the
|
||||
// turn engine resolves off the bestiary template + session, not the
|
||||
// ZoneDefinition, so the threshold isn't otherwise in reach at round end.
|
||||
custodianPhaseTwoFrac = 0.45
|
||||
|
||||
// midnightAtkStep / midnightAtkCap: the soft closing-time timer. Every round
|
||||
// past midnightTimerAfter adds midnightAtkStep to the boss's Attack (via the
|
||||
// shared EnemyAtkBuff, which enemyAttackStat already folds in), capped so a
|
||||
// truly stalled fight still ends without the number becoming meaningless.
|
||||
midnightTimerAfter = 20
|
||||
midnightAtkStep = 2
|
||||
midnightAtkCap = 40
|
||||
)
|
||||
|
||||
// applyBossInCombatRoundEnd resolves the round-boundary in-combat Layer-2
|
||||
// mechanics for the enemy the turn engine is fighting. It runs after the round's
|
||||
// damage has settled and only while the enemy still stands. Dispatched by
|
||||
// bestiary ID; a no-op for every enemy that isn't a hooked T6 boss.
|
||||
func applyBossInCombatRoundEnd(st *combatState, bossID string, enemyMaxHP int) {
|
||||
if st == nil {
|
||||
return
|
||||
}
|
||||
switch bossID {
|
||||
case "boss_custodian":
|
||||
applyAmendment(st, enemyMaxHP)
|
||||
}
|
||||
}
|
||||
|
||||
// applyAmendment resolves the Custodian's Amendment (round-3 snapshot + once-only
|
||||
// phase-2 rewind) and its soft midnight timer, given the round that just finished
|
||||
// (st.round, pre-increment) and the boss's party-scaled MaxHP.
|
||||
func applyAmendment(st *combatState, enemyMaxHP int) {
|
||||
// Snapshot the boss's HP at the end of round 3 — the concealed "true length".
|
||||
if st.round == custodianSnapshotRound && st.enemyRewindHP == 0 {
|
||||
st.enemyRewindHP = st.enemyHP
|
||||
}
|
||||
// The first time the party crosses the boss into phase 2, rewind to the
|
||||
// snapshot — once. Guarded on snapshot > current so a party that never got
|
||||
// below the round-3 HP (and a fight bursted into phase 2 before the snapshot
|
||||
// was even taken, EnemyRewindHP == 0) can't be handed a free heal.
|
||||
phaseTwo := int(custodianPhaseTwoFrac * float64(enemyMaxHP))
|
||||
if !st.enemyRewindUsed && st.enemyRewindHP > st.enemyHP && st.enemyHP <= phaseTwo {
|
||||
st.enemyHP = min(enemyMaxHP, st.enemyRewindHP)
|
||||
st.enemyRewindUsed = true
|
||||
st.events = append(st.events, CombatEvent{
|
||||
Round: st.round, Phase: CombatPhaseRoundEnd, Actor: "enemy", Action: "amendment_rewind",
|
||||
PlayerHP: st.playerHP, EnemyHP: st.enemyHP,
|
||||
})
|
||||
}
|
||||
// Soft midnight timer: closing time leans on a stalled fight.
|
||||
if st.round >= midnightTimerAfter && st.enemyAtkBuff < midnightAtkCap {
|
||||
st.enemyAtkBuff = min(midnightAtkCap, st.enemyAtkBuff+midnightAtkStep)
|
||||
st.events = append(st.events, CombatEvent{
|
||||
Round: st.round, Phase: CombatPhaseRoundEnd, Actor: "enemy", Action: "midnight_toll",
|
||||
Damage: midnightAtkStep, PlayerHP: st.playerHP, EnemyHP: st.enemyHP,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,3 +199,168 @@ func approxEq(a, b float64) bool {
|
||||
d := a - b
|
||||
return d < 1e-9 && d > -1e-9
|
||||
}
|
||||
|
||||
// custodianState builds a live combatState seated against a Custodian-shaped
|
||||
// enemy (MaxHP given), at the given round and current enemy HP, for direct
|
||||
// applyAmendment tests. Uses the real turn engine so the embedded actor/roster
|
||||
// are fully formed.
|
||||
func custodianState(t *testing.T, round, enemyHP, enemyMaxHP int) *combatState {
|
||||
t.Helper()
|
||||
sess := turnSession(CombatPhaseRoundEnd, 10000, enemyHP)
|
||||
sess.Round = round
|
||||
sess.EnemyID = "boss_custodian"
|
||||
p := basePlayer()
|
||||
e := baseEnemy()
|
||||
e.Stats.MaxHP = enemyMaxHP
|
||||
te := resumeTurnEngine(sess, []*Combatant{&p}, &e, combatSessionStepRNG(sess, enemySeat))
|
||||
te.st.enemyHP = enemyHP
|
||||
return te.st
|
||||
}
|
||||
|
||||
func countEvents(events []CombatEvent, action string) int {
|
||||
n := 0
|
||||
for _, ev := range events {
|
||||
if ev.Action == action {
|
||||
n++
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func TestApplyAmendment_SnapshotAtRoundThree(t *testing.T) {
|
||||
// Before round 3: no snapshot.
|
||||
st := custodianState(t, 2, 400, 500)
|
||||
applyAmendment(st, 500)
|
||||
if st.enemyRewindHP != 0 {
|
||||
t.Errorf("round 2 snapshot = %d, want 0 (not yet)", st.enemyRewindHP)
|
||||
}
|
||||
// End of round 3: snapshot the current HP.
|
||||
st = custodianState(t, 3, 380, 500)
|
||||
applyAmendment(st, 500)
|
||||
if st.enemyRewindHP != 380 {
|
||||
t.Errorf("round 3 snapshot = %d, want 380", st.enemyRewindHP)
|
||||
}
|
||||
// A later round does not re-snapshot over the captured value.
|
||||
st.round = 5
|
||||
st.enemyHP = 200
|
||||
applyAmendment(st, 500)
|
||||
if st.enemyRewindHP != 380 {
|
||||
t.Errorf("post-capture snapshot = %d, want it frozen at 380", st.enemyRewindHP)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyAmendment_RewindOnceAtPhaseTwo(t *testing.T) {
|
||||
// Snapshot 400; boss now at 200, below the 0.45*500=225 phase-two line.
|
||||
st := custodianState(t, 6, 200, 500)
|
||||
st.enemyRewindHP = 400
|
||||
applyAmendment(st, 500)
|
||||
if st.enemyHP != 400 {
|
||||
t.Errorf("post-rewind HP = %d, want restored to snapshot 400", st.enemyHP)
|
||||
}
|
||||
if !st.enemyRewindUsed {
|
||||
t.Error("rewind did not mark itself used")
|
||||
}
|
||||
if countEvents(st.events, "amendment_rewind") != 1 {
|
||||
t.Errorf("amendment_rewind events = %d, want 1", countEvents(st.events, "amendment_rewind"))
|
||||
}
|
||||
// A second crossing does not rewind again.
|
||||
st.enemyHP = 150
|
||||
mark := len(st.events)
|
||||
applyAmendment(st, 500)
|
||||
if st.enemyHP != 150 {
|
||||
t.Errorf("second-crossing HP = %d, want left at 150 (rewind spent)", st.enemyHP)
|
||||
}
|
||||
if len(st.events) != mark {
|
||||
t.Error("a spent rewind emitted another event")
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyAmendment_NoRewindAbovePhaseTwo(t *testing.T) {
|
||||
// Boss above the phase-two line: no rewind even with a snapshot in hand.
|
||||
st := custodianState(t, 6, 300, 500) // 300 > 225
|
||||
st.enemyRewindHP = 450
|
||||
applyAmendment(st, 500)
|
||||
if st.enemyHP != 300 || st.enemyRewindUsed {
|
||||
t.Errorf("HP=%d used=%v; want no rewind above phase two", st.enemyHP, st.enemyRewindUsed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyAmendment_NoRewindWithoutSnapshot(t *testing.T) {
|
||||
// Bursted into phase two before round 3: no snapshot, so no free heal.
|
||||
st := custodianState(t, 2, 100, 500)
|
||||
applyAmendment(st, 500)
|
||||
if st.enemyHP != 100 || st.enemyRewindUsed {
|
||||
t.Errorf("HP=%d used=%v; want no rewind without a snapshot", st.enemyHP, st.enemyRewindUsed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyAmendment_MidnightTimer(t *testing.T) {
|
||||
// Before round 20: no attack climb.
|
||||
st := custodianState(t, 19, 300, 500)
|
||||
applyAmendment(st, 500)
|
||||
if st.enemyAtkBuff != 0 {
|
||||
t.Errorf("round 19 atk buff = %d, want 0", st.enemyAtkBuff)
|
||||
}
|
||||
// Round 20+: +2 per round, capped.
|
||||
st = custodianState(t, 20, 300, 500)
|
||||
applyAmendment(st, 500)
|
||||
if st.enemyAtkBuff != midnightAtkStep {
|
||||
t.Errorf("round 20 atk buff = %d, want %d", st.enemyAtkBuff, midnightAtkStep)
|
||||
}
|
||||
if countEvents(st.events, "midnight_toll") != 1 {
|
||||
t.Errorf("midnight_toll events = %d, want 1", countEvents(st.events, "midnight_toll"))
|
||||
}
|
||||
// The cap holds against a truly endless stall.
|
||||
st.enemyAtkBuff = midnightAtkCap
|
||||
mark := len(st.events)
|
||||
applyAmendment(st, 500)
|
||||
if st.enemyAtkBuff != midnightAtkCap {
|
||||
t.Errorf("capped atk buff = %d, want %d", st.enemyAtkBuff, midnightAtkCap)
|
||||
}
|
||||
if len(st.events) != mark {
|
||||
t.Error("midnight timer emitted a toll after hitting the cap")
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyBossInCombatRoundEnd_DispatchesOnlyCustodian(t *testing.T) {
|
||||
// A non-Custodian enemy is untouched even at a rewind-eligible HP.
|
||||
st := custodianState(t, 6, 200, 500)
|
||||
st.enemyRewindHP = 400
|
||||
applyBossInCombatRoundEnd(st, "boss_seraphel", 500)
|
||||
if st.enemyHP != 200 || st.enemyRewindUsed {
|
||||
t.Errorf("HP=%d used=%v; want no-op for a non-Custodian boss", st.enemyHP, st.enemyRewindUsed)
|
||||
}
|
||||
// The Custodian id does resolve the hook.
|
||||
applyBossInCombatRoundEnd(st, "boss_custodian", 500)
|
||||
if st.enemyHP != 400 || !st.enemyRewindUsed {
|
||||
t.Errorf("HP=%d used=%v; want the rewind for the Custodian", st.enemyHP, st.enemyRewindUsed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTurnEngine_AmendmentRoundTrips(t *testing.T) {
|
||||
// Drive a real Custodian round-end and confirm the once-only rewind is
|
||||
// captured through CombatStatuses (a suspend/resume can't replay it).
|
||||
sess := turnSession(CombatPhaseRoundEnd, 10000, 200)
|
||||
sess.Round = 6
|
||||
sess.EnemyID = "boss_custodian"
|
||||
sess.EnemyHPMax = 500
|
||||
sess.Statuses.EnemyRewindHP = 400 // snapshot taken earlier in the fight
|
||||
p := basePlayer()
|
||||
e := baseEnemy()
|
||||
e.Stats.MaxHP = 500
|
||||
te := resumeTurnEngine(sess, []*Combatant{&p}, &e, combatSessionStepRNG(sess, enemySeat))
|
||||
if _, err := te.step(PlayerAction{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
te.commit()
|
||||
|
||||
if sess.EnemyHP != 400 {
|
||||
t.Errorf("committed EnemyHP = %d, want the 400 rewind pool", sess.EnemyHP)
|
||||
}
|
||||
if !sess.Statuses.EnemyRewindUsed {
|
||||
t.Error("EnemyRewindUsed did not persist through commit")
|
||||
}
|
||||
if countEvents(sess.TurnLog, "amendment_rewind") != 1 {
|
||||
t.Errorf("amendment_rewind events = %d, want 1", countEvents(sess.TurnLog, "amendment_rewind"))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user