mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 08:21:02 +02:00
155 lines
6.3 KiB
Lua
155 lines
6.3 KiB
Lua
-- Parity test: A/START are never handled mid-step (#286).
|
|
-- Self-contained: run via `luajit tests/parity_midstep_buttons.lua`; also
|
|
-- dofile'd by tests/run_tests.lua's aggregator.
|
|
--
|
|
-- Oracle: home/overworld.asm OverworldLoop reads wWalkCounter and, when it
|
|
-- is nonzero ("the player sprite has not yet completed the walking
|
|
-- animation"), jumps straight to .moveAhead -- JoypadOverworld, and with
|
|
-- it the START check, the A check, and every direction initiation, only
|
|
-- ever runs while the player stands on a tile.
|
|
--
|
|
-- The port ran handleInput() every frame regardless of player.moving, so a
|
|
-- mid-step A/START press pushed its TextBox/StartMenu right there and
|
|
-- froze Red between tiles, mid-animation (#286: running up to Nurse Joy
|
|
-- and mashing A stops him half off the tile).
|
|
--
|
|
-- Second oracle, engine/joypad.asm _Joypad: hJoyPressed is
|
|
-- (hJoyLast ^ hJoyInput) & hJoyInput, and hJoyLast only advances on an
|
|
-- explicit `call Joypad`. vblank's per-frame ReadJoypad writes hJoyInput
|
|
-- alone, and the mid-step path never calls Joypad, so hJoyLast is FROZEN
|
|
-- for the whole animation. A button pressed mid-step and still held when
|
|
-- the step lands therefore reads as a fresh press at the next poll; one
|
|
-- released before the step lands is genuinely lost. The port used to drop
|
|
-- both, which on the Cycling Road roll made START a coin flip (#525).
|
|
--
|
|
-- The invariant: while a step is in progress, A and START change nothing
|
|
-- (no TextBox, no StartMenu, the step completes). On the landing frame a
|
|
-- still-held A or START is acted on, a released one is not.
|
|
|
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
|
if not _G.love then _G.love = require("tests.love_stub") end
|
|
local Data = require("src.core.Data")
|
|
if not (Data.maps and Data.maps.PALLET_TOWN) then Data:load() end
|
|
local S = require("tests.harness").suite("parity midstep buttons")
|
|
local check, eq = S.check, S.eq
|
|
|
|
require("src.render.Font").load(Data)
|
|
local Game = require("src.core.Game")
|
|
local Input = require("src.core.Input")
|
|
local StateStack = require("src.core.StateStack")
|
|
local Renderer = require("src.render.Renderer")
|
|
local SaveData = require("src.core.SaveData")
|
|
local OW = require("src.world.OverworldController")
|
|
|
|
Game.data = Data
|
|
Game.input = Input; Input:init()
|
|
Game.renderer = Renderer; Renderer:init()
|
|
Game.stack = StateStack
|
|
StateStack:init()
|
|
|
|
-- PALLET_TOWN (6,9) facing down: open grass, several free tiles south
|
|
Game.save = SaveData.newGame()
|
|
Game.stack:push(OW, "PALLET_TOWN", 6, 9, "down")
|
|
local ow = Game.stack:top()
|
|
|
|
local function step(pressedBtn)
|
|
-- the real driver: Game:step promotes pressQueue edges via Input:step()
|
|
-- (which also expires them) before stack:update
|
|
if pressedBtn then table.insert(Input.pressQueue, pressedBtn) end
|
|
Input:step()
|
|
ow:update(1 / 60)
|
|
end
|
|
|
|
-- A synthetic pressQueue inject has no source entry, so Input:step sets
|
|
-- state[btn] = true and nothing ever clears it (src/core/Input.lua) -- the
|
|
-- harness models a HELD button. Most cases below want a tap, so release it
|
|
-- explicitly; the held cases are called out where they matter.
|
|
local function tap(btn)
|
|
step(btn)
|
|
Input.state[btn] = false
|
|
end
|
|
|
|
-- start a step south (held direction, like hJoyHeld)
|
|
Input.state.down = true
|
|
step()
|
|
Input.state.down = false
|
|
check(ow.player.moving, "held direction starts a step")
|
|
local startY = ow.player.cellY
|
|
|
|
-- spy on interact(): a mid-step A press must not even reach it
|
|
local interactCalls = 0
|
|
local baseInteract = ow.interact
|
|
ow.interact = function(self, ...)
|
|
interactCalls = interactCalls + 1
|
|
return baseInteract(self, ...)
|
|
end
|
|
|
|
-- mid-step A press: nothing may happen (the original acts on nothing here)
|
|
tap("a")
|
|
eq(interactCalls, 0, "mid-step A never reaches interact()")
|
|
check(Game.stack:top() == ow, "mid-step A pushes no TextBox")
|
|
check(ow.player.moving, "mid-step A does not interrupt the step")
|
|
|
|
-- mid-step START press: no start menu either
|
|
tap("start")
|
|
check(Game.stack:top() == ow, "mid-step START opens no menu")
|
|
check(ow.player.moving, "mid-step START does not interrupt the step")
|
|
|
|
-- run the step out: the player lands on the next tile, unfrozen
|
|
local guard = 0
|
|
while ow.player.moving and guard < 60 do step(); guard = guard + 1 end
|
|
eq(ow.player.cellY, startY + 1, "the step completes onto the next tile")
|
|
|
|
-- the issue's actual repro ("press A quickly/early" running up to Nurse
|
|
-- Joy): start another step and press A on its FINAL mid-step frame, then
|
|
-- RELEASE it before the step lands. hJoyLast is frozen through the
|
|
-- animation, so the next poll sees the button already up and computes no
|
|
-- edge (engine/joypad.asm) -- this press really is lost.
|
|
Input.state.down = true
|
|
step()
|
|
Input.state.down = false
|
|
check(ow.player.moving, "second step starts")
|
|
guard = 0
|
|
while ow.player.moving and guard < 60 do
|
|
guard = guard + 1
|
|
if guard == (ow.player.stepFramesCur or 16) - 1 then
|
|
tap("a") -- the last frame before landing, released immediately
|
|
else
|
|
step()
|
|
end
|
|
end
|
|
check(not ow.player.moving, "the second step completes")
|
|
step() -- the landing frame, where a still-held button would be polled
|
|
eq(interactCalls, 0, "a mid-step A released before landing is still lost")
|
|
check(Game.stack:top() == ow, "the released last-frame A pushes no TextBox")
|
|
|
|
-- ...but a mid-step A that is STILL HELD when the step lands is delivered
|
|
-- on the landing frame, because hJoyLast never advanced (#525). Nothing
|
|
-- happens mid-step either way: the poll is deferred, not the action.
|
|
Input.state.down = true
|
|
step()
|
|
Input.state.down = false
|
|
check(ow.player.moving, "third step starts")
|
|
step("a") -- pressed mid-step and left held
|
|
eq(interactCalls, 0, "the held A still does nothing mid-step")
|
|
check(ow.player.moving, "the held A does not interrupt the step")
|
|
guard = 0
|
|
while ow.player.moving and guard < 60 do step(); guard = guard + 1 end
|
|
eq(interactCalls, 0, "still nothing while the step runs out")
|
|
step() -- landing frame
|
|
eq(interactCalls, 1, "a held mid-step A is polled on the landing frame")
|
|
Input.state.a = false
|
|
|
|
-- standing on the tile again, START and A work as always
|
|
interactCalls = 0
|
|
tap("start")
|
|
check(Game.stack:top() ~= ow, "START opens the start menu on a tile")
|
|
while Game.stack:top() do Game.stack:pop() end
|
|
Game.stack:push(OW, "PALLET_TOWN", 6, 9, "down")
|
|
ow = Game.stack:top()
|
|
interactCalls = 0 -- OW is a singleton: the spy survives the re-push
|
|
step("a")
|
|
eq(interactCalls, 1, "A on a tile runs interact() (the gate is movement-only)")
|
|
|
|
S.finish()
|