From bf45bb51d6fbab151714b03e54c4a9a9fbdc19b3 Mon Sep 17 00:00:00 2001 From: bryanthaboi Date: Tue, 29 Sep 2026 16:00:57 -0400 Subject: [PATCH] CLOSES #2557 --- src/world/OverworldController.lua | 3 +- tests/drivers/poison_center_flash_2557.lua | 84 ++++++++++++++++++++++ tests/engine/poison_flash_hold_bug2557.lua | 71 ++++++++++++++++++ 3 files changed, 157 insertions(+), 1 deletion(-) create mode 100644 tests/drivers/poison_center_flash_2557.lua create mode 100644 tests/engine/poison_flash_hold_bug2557.lua diff --git a/src/world/OverworldController.lua b/src/world/OverworldController.lua index c21e2a1b..7bdcc5d3 100644 --- a/src/world/OverworldController.lua +++ b/src/world/OverworldController.lua @@ -1697,7 +1697,8 @@ function OverworldState:handleInput() -- the edge outright made START a coin flip on the Cycling Road roll, -- where the pull below re-arms a step on the single idle frame in -- bikeStepFrames (#525). - if self.player.moving then + -- engine/gfx/screen_effects.asm:7-8 + if self.player.moving or (self.poisonFlash or 0) > 0 then local held = self.joyLatch if not held then held = {}; self.joyLatch = held end if input:wasPressed("a") then held.a = true end diff --git a/tests/drivers/poison_center_flash_2557.lua b/tests/drivers/poison_center_flash_2557.lua new file mode 100644 index 00000000..156d0d0c --- /dev/null +++ b/tests/drivers/poison_center_flash_2557.lua @@ -0,0 +1,84 @@ +-- engine/events/poison.asm:93 / engine/gfx/screen_effects.asm:2 (#2557) +return function(game) + local U = dofile("tests/drivers/util.lua") + local Pokemon = require("src.pokemon.Pokemon") + local shotDir = os.getenv("POKEPORT_SHOT_DIR") or ".bazinga/BSA/09-29-26-01-userreported/shots" + local fails = 0 + local function check(label, ok) + U.log(ok and "PASS" or "FAIL", label) + if not ok then fails = fails + 1 end + return ok + end + + game.save.party = { Pokemon.new(game.data, "CHARIZARD", 50) } + game.save.party[1].status = "PSN" + game.save.player.name = "bryan" + game.save.usedPokecenter = true + + local function scenario(label, shots) + U.log("=== " .. label) + U.teleport(game, "VIRIDIAN_POKECENTER", 3, 7, "up") + game.save.poisonSteps = 0 + local ow = game.overworld + local p = ow.player + local hpBefore = game.save.party[1].hp + local flashFrames, shotTaken = 0, false + for _ = 1, 400 do + table.insert(game.input.pressQueue, "up") + game.input.state.up = true + if p.moving and p.targetY == 3 and not game.input.state.a then + table.insert(game.input.pressQueue, "a") + game.input.state.a = true + end + coroutine.yield() + if (ow.poisonFlash or 0) > 0 and game.stack:top() == ow then + flashFrames = flashFrames + 1 + if shots and not shotTaken then + shotTaken = true + game.input.state.up = false + U.still(game, shotDir .. "/2557_01_flash_before_nurse_talk.png") + end + end + if game.stack:top() ~= ow then break end + end + game.input.state.up = false + game.input.state.a = false + check(label .. ": 4th step landed at (3,3)", p.cellY == 3 and not p.moving) + check(label .. ": the 4th step cost 1 HP", game.save.party[1].hp == hpBefore - 1) + check(label .. ": flash plays 4 frames before the nurse talk", flashFrames == 4) + check(label .. ": nurse dialogue opened after the flash", game.stack:top() ~= ow) + check(label .. ": nothing frozen under the text box", (ow.poisonFlash or 0) == 0) + + local deferred, balls = 0, false + for tick = 1, 1200 do + if game.stack:top() ~= ow and tick % 4 == 0 then + table.insert(game.input.pressQueue, "a") + end + coroutine.yield() + game.input.state.a = false + if game.stack:top() == ow and (ow.poisonFlash or 0) > 0 then + deferred = deferred + 1 + end + if ow.healAnim and ow.healAnim.lit >= 1 then + balls = true + break + end + end + check(label .. ": machine started", balls) + check(label .. ": no poison flash after the nurse text", deferred == 0) + if shots and balls then + U.still(game, shotDir .. "/2557_02_no_flash_at_balls.png") + end + for _ = 1, 400 do + if game.stack:top() == ow and not ow.healAnim and not ow.emote then break end + if game.stack:top() ~= ow then table.insert(game.input.pressQueue, "a") end + coroutine.yield() + game.input.state.a = false + end + game.save.party[1].status = "PSN" + end + + scenario("count_pass", false) + scenario("shot_pass", true) + love.event.quit(fails == 0 and 0 or 1) +end diff --git a/tests/engine/poison_flash_hold_bug2557.lua b/tests/engine/poison_flash_hold_bug2557.lua new file mode 100644 index 00000000..821ce6b7 --- /dev/null +++ b/tests/engine/poison_flash_hold_bug2557.lua @@ -0,0 +1,71 @@ +-- engine/gfx/screen_effects.asm:2-12, home/overworld.asm:317 (#2557) + +package.path = "./?.lua;./?/init.lua;" .. package.path +if not _G.love then _G.love = require("tests.love_stub") end + +local T = require("tests.modkit") +local check, eq = T.check, T.eq + +T.fixtures.fresh() + +local OW = require("src.world.OverworldController") + +local function setUpvalue(fn, name, val) + local i = 1 + while true do + local n = debug.getupvalue(fn, i) + if not n then return false end + if n == name then debug.setupvalue(fn, i, val); return true end + i = i + 1 + end +end + +local down = { a = true } +local pressed = {} +local input = { + wasPressed = function(_, k) return pressed[k] == true end, + isDown = function(_, k) return down[k] == true end, +} +local stack = { top = function() return nil end } +local game = { input = input, stack = stack, data = {}, save = {} } +setUpvalue(OW.handleInput, "Game", game) +setUpvalue(OW.poisonFlashLive, "Game", game) + +local interacts = 0 +local ow = setmetatable({ + player = { moving = false, facing = "up" }, + poisonFlash = 4, + dirHeld = function() return false end, + interact = function() interacts = interacts + 1 end, +}, { __index = OW }) +stack.top = function() return ow end + +pressed.a = true +ow:handleInput() +eq(interacts, 0, "A pressed during the poison flash does not interact") +check(ow.joyLatch and ow.joyLatch.a, "and is latched for the poll after the flash") + +pressed.a = false +for i = 3, 1, -1 do + ow:tickPoisonFlash() + eq(ow.poisonFlash, i, "flash frame counts down") + ow:handleInput() + eq(interacts, 0, "held A still waits out flash frame " .. i) +end +check(ow.joyLatch and ow.joyLatch.a, "latch survives the whole flash") + +ow:tickPoisonFlash() +eq(ow.poisonFlash, 0, "flash spent") +ow:handleInput() +eq(interacts, 1, "the still-held A talks on the first poll after the flash") +eq(ow.joyLatch, nil, "and the latch is consumed") + +down.a = false +ow.poisonFlash = 2 +ow.joyLatch = nil +pressed.a = true +ow:handleInput() +pressed.a = false +ow.poisonFlash = 0 +ow:handleInput() +eq(interacts, 1, "an A released before the poll is lost, as on the cart")