From 881670db91599296592c11c6c700cdce6a06e2d3 Mon Sep 17 00:00:00 2001 From: sanjinpepic Date: Sun, 16 Aug 2026 20:01:49 +0200 Subject: [PATCH] Clear drainHold once the HP-bar drain actually finishes stepHPDrain counts drainHold down to 0 as the last step of every phase (pixel slide, HP-number step, closing frames) but never let go of the field afterward, so it sat at 0 -- not nil -- for the rest of the battle. BattleSafety.inspect uses drainHold ~= nil as its settled-presentation gate for checkpoint capture, so the very first HP change in a battle permanently refused every checkpoint after it with battle_phase_busy, even once the bar had long since caught up. Only nil the field when the whole drain is actually over (bar pixel, HP number and the closing-frame hold all settled), not on every mid-sequence 0 -- a fresh HP change still needs drainHold to read as busy so BattleSafety keeps refusing captures until that one settles too. --- src/battle/BattleState.lua | 9 +++++++++ tests/engine/battle_checkpoint_boundary.lua | 21 +++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/src/battle/BattleState.lua b/src/battle/BattleState.lua index dee46a19..36e9bf5e 100644 --- a/src/battle/BattleState.lua +++ b/src/battle/BattleState.lua @@ -1103,6 +1103,15 @@ function BattleState:stepHPDrain() if not b.shownPx then b.shownPx = targetPx end if (b.drainHold or 0) > 0 then b.drainHold = b.drainHold - 1 + -- Once the count runs out with nothing left pending (bar and + -- number already on the final total), the drain is over, not just + -- between steps: leave the field at 0 and BattleSafety.inspect + -- reads it as still mid-animation for the rest of the battle, + -- since drainHold ~= nil is its settled-presentation gate. + if b.drainHold <= 0 and b.shownPx == targetPx and b.shownHP == goal + and not b.draining then + b.drainHold = nil + end busy = true elseif b.shownPx ~= targetPx then -- .barAnimationLoop redraws the bar one pixel at a time, `ld c, 2 / diff --git a/tests/engine/battle_checkpoint_boundary.lua b/tests/engine/battle_checkpoint_boundary.lua index c9bfea05..318c460c 100644 --- a/tests/engine/battle_checkpoint_boundary.lua +++ b/tests/engine/battle_checkpoint_boundary.lua @@ -131,4 +131,25 @@ T.same(Checkpoint.inspect(game), { canCapture = true, canRestore = true, kind = "overworld", }, "settled overworld remains supported") +-- drainHold gates capture (see the refused() case above) exactly because it +-- marks an HP bar mid-animation. Once stepHPDrain settles the bar it must +-- let go of that gate too, or the very first drain of a battle leaves the +-- checkpoint contract refused for everything after it. +do + local game3, _, battle3 = makeGame() + battle3.enemy.mon.hp = battle3.enemy.mon.hp - 5 + local frames = 0 + while battle3:stepHPDrain() and frames < 10000 do + frames = frames + 1 + end + T.eq(battle3.enemy.shownHP, battle3.enemy.mon.hp, + "the HP bar settles on the new total") + T.eq(battle3.enemy.drainHold, nil, + "drainHold releases the checkpoint gate once the bar finishes draining") + local capability = Checkpoint.inspect(game3) + T.check(capability.canCapture == true, + "a checkpoint is capturable again after the drain settles: " + .. tostring(capability.reason)) +end + T.finish()