Tick residuals after each move in Gen 1 mode

In pokered, MainInBattleLoop calls HandlePoisonBurnLeechSeed right
after every Execute*Move (core.asm:426-464), so a seeded, poisoned or
burned mon takes its residual before the slower side acts. The port
ran the whole sweep in endOfTurn, which made leech seed behave like
Gen 3+ and never showed the drain animation (#784).

The residual sweep now runs per action under the gen1_faithful
ruleset, gated by a new residualAfterMove flag; modern_clean keeps
the end of round sweep. The leech seed drain plays the ABSORB
animation from the healing side, the way the original flips
hWhoseTurn before PlayMoveAnimation. Item, ball, failed run and
ghost-fear turns still tick the player's residual, matching
ExecutePlayerMoveDone.
This commit is contained in:
johnjohto
2026-08-04 12:06:21 -04:00
parent 1820f411ae
commit ba8ac3d143
5 changed files with 237 additions and 3 deletions
+75 -1
View File
@@ -1889,6 +1889,9 @@ function BattleState:update(dt)
self:act(function()
self:executeAction(self.enemy, self.player, self:enemyAction())
end)
-- the scared turn still ticks the player's residual (PrintGhostText
-- -> ExecutePlayerMoveDone, core.asm:3056, 3275-3279)
self:queueResidual(self.player, self.enemy)
self:act(function() self:endOfTurn() end)
elseif choice == "fight" then
-- After the menu: own trapping/Bide or foe Wrap skips the move
@@ -2358,6 +2361,48 @@ function BattleState:resolveSwitch(newMon)
self:act(function() self:endOfTurn() end)
end
-- Gen 1 calls HandlePoisonBurnLeechSeed right after the acting side's
-- move (core.asm:426-464), so the drain lands before the slower mon acts;
-- the modern ruleset sweeps residuals at end of round instead (Gen 3+).
local function residualAfterMove(battle)
local ruleset = battle.ruleset
return not ruleset or ruleset.residualAfterMove ~= false
end
-- HandlePoisonBurnLeechSeed for one side, run right after its action.
-- Skipped when the action settled the battle (a Teleport escape rets
-- before the call), when an AI switch swapped the side out mid-action, or
-- when the move already knocked the opponent out (core.asm:423-425,
-- 452-454) -- the same bypass the end-of-round sweep applies.
function BattleState:residualFor(b, opp)
if self.result then return end
if self.player ~= b and self.enemy ~= b then return end
if b.mon.hp <= 0 or opp.mon.hp <= 0 then return end
local msgs = Status.residual(b, opp, self)
for _, m in ipairs(msgs) do self:sayNext(prefixEnemy(m, b)) end
if b.leechSeeded and b.mon.hp > 0 then
-- the drain plays the ABSORB animation from the healing side
-- (core.asm:506-517 flips hWhoseTurn before PlayMoveAnimation)
self:animNext("ABSORB", opp.isPlayer)
end
if #msgs > 0 then self:drainNext() end -- poison/burn/seed HP moved
self.sideToxic = self.sideToxic or {}
if b.toxicCounter then
self.sideToxic[b.isPlayer and "player" or "enemy"] = b.toxicCounter
end
if b.mon.hp <= 0 then
self:onFaint(b)
end
end
-- append one side's residual to the queue under Gen 1 timing; a no-op
-- under the modern ruleset, whose sweep runs in endOfTurn instead
function BattleState:queueResidual(b, opp)
if residualAfterMove(self) then
self:act(function() self:residualFor(b, opp) end)
end
end
function BattleState:endOfTurn()
-- the same ret: a decided battle never reaches HandlePoisonBurnLeechSeed
-- or CheckNumAttacksLeft (core.asm:417-421, 456-460), so the residual
@@ -2377,6 +2422,9 @@ function BattleState:endOfTurn()
-- that sets the flag also zeroes the counter, so a stale value is
-- unobservable (a switch or cure downgrades Toxic to plain poison).
self.sideToxic = self.sideToxic or {}
-- Gen 1 timing already ran each side's residual right after its move
-- (see executeAction); the end-of-round sweep is the modern ruleset's
local sweep = not residualAfterMove(self)
-- a battler whose opponent was already knocked out by a move this turn
-- skips its own residual (HandlePoisonBurnLeechSeed is bypassed when the
-- move faints the target); snapshot before residual so one side's
@@ -2386,7 +2434,7 @@ function BattleState:endOfTurn()
for _, pair in ipairs({ { self.player, self.enemy, "player", enemyAlive },
{ self.enemy, self.player, "enemy", playerAlive } }) do
local b, opp, side, oppAlive = pair[1], pair[2], pair[3], pair[4]
if b.mon.hp > 0 and oppAlive then
if sweep and b.mon.hp > 0 and oppAlive then
local msgs = Status.residual(b, opp, self)
for _, m in ipairs(msgs) do self:sayNext(prefixEnemy(m, b)) end
if #msgs > 0 then self:drainNext() end -- poison/burn/seed HP moved
@@ -2397,6 +2445,9 @@ function BattleState:endOfTurn()
self:onFaint(b)
end
end
-- the Haze move-forfeit only covers the turn Haze was used; if the
-- cured mon had already moved, drop the flag before next turn
b.skipMove = nil
-- CheckNumAttacksLeft (core.asm:683-697): a trapping counter that
-- hit 0 this turn releases its bit only now, at the end of the turn
if b.trappingTurns and b.trappingTurns <= 0 then
@@ -3181,6 +3232,12 @@ function BattleState:executeAction(user, target, action)
run()
-- after announce/anim/effect text (pokered DrawHUDsAndHPBars)
self:actNext(function() self:syncShownStatus() end)
-- MainInBattleLoop calls HandlePoisonBurnLeechSeed right after each
-- Execute*Move (core.asm:426-464): the acting side's poison/burn/leech
-- seed ticks before the slower mon acts, not at end of round
if residualAfterMove(self) then
self:actNext(function() self:residualFor(user, target) end)
end
end
-- Sleep / confusion onomatopoeia from Check*StatusConditions
@@ -4245,6 +4302,9 @@ function BattleState:tryRun()
self:act(function()
self:executeAction(self.enemy, self.player, self:enemyAction())
end)
-- a failed escape loses the turn (core.asm:1572): the player's
-- residual still ticks, same as an item turn
self:queueResidual(self.player, self.enemy)
self:act(function() self:endOfTurn() end)
end
end
@@ -4267,6 +4327,11 @@ function BattleState:itemUsed(messages)
self:act(function()
self:executeAction(self.enemy, self.player, self:enemyAction())
end)
-- the item spends the player's move, but its residual still ticks:
-- ExecutePlayerMove rets early on wActionResultOrTookBattleTurn and
-- MainInBattleLoop calls HandlePoisonBurnLeechSeed anyway
-- (core.asm:3086-3088, 3275-3279)
self:queueResidual(self.player, self.enemy)
self:act(function() self:endOfTurn() end)
end
@@ -4448,6 +4513,9 @@ function BattleState:throwBall(ball)
self:act(function()
self:executeAction(self.enemy, self.player, self:enemyAction())
end)
-- a thrown ball spends the turn like an item: the player's residual
-- still ticks (core.asm:3275-3279)
self:queueResidual(self.player, self.enemy)
self:act(function() self:endOfTurn() end)
return
end
@@ -4466,6 +4534,9 @@ function BattleState:throwBall(ball)
self:act(function()
self:executeAction(self.enemy, self.player, self:enemyAction())
end)
-- a thrown ball spends the turn like an item: the player's residual
-- still ticks (core.asm:3275-3279)
self:queueResidual(self.player, self.enemy)
self:act(function() self:endOfTurn() end)
return
end
@@ -4492,6 +4563,9 @@ function BattleState:throwBall(ball)
self:act(function()
self:executeAction(self.enemy, self.player, self:enemyAction())
end)
-- a thrown ball spends the turn like an item: the player's residual
-- still ticks (core.asm:3275-3279)
self:queueResidual(self.player, self.enemy)
self:act(function() self:endOfTurn() end)
end
end)