From 6f75a64c461070b07af1f89ad97fcf2473694eb8 Mon Sep 17 00:00:00 2001 From: bryanthaboi Date: Thu, 6 Aug 2026 07:11:16 -0400 Subject: [PATCH] deleting stale tests --- tests/parity_faint_cry_bug709.lua | 29 ++- tests/parity_fly_anim.lua | 91 ------- tests/parity_gift_atomicity.lua | 185 --------------- tests/parity_marowak_ball.lua | 188 --------------- tests/parity_midstep_buttons.lua | 154 ------------ tests/parity_move_swap.lua | 129 ---------- tests/parity_shift_exp_share.lua | 206 ---------------- tests/parity_starter_dex.lua | 117 --------- tests/parity_surf_clears_bike_bug846.lua | 187 --------------- tests/parity_switch_cursor_reset.lua | 49 ---- tests/parity_trainer_victory_text.lua | 198 ---------------- tests/parity_wardens_house_bug535.lua | 127 ---------- tests/parity_yellow_old_man.lua | 290 ----------------------- 13 files changed, 19 insertions(+), 1931 deletions(-) delete mode 100644 tests/parity_fly_anim.lua delete mode 100644 tests/parity_gift_atomicity.lua delete mode 100644 tests/parity_marowak_ball.lua delete mode 100644 tests/parity_midstep_buttons.lua delete mode 100644 tests/parity_move_swap.lua delete mode 100644 tests/parity_shift_exp_share.lua delete mode 100644 tests/parity_starter_dex.lua delete mode 100644 tests/parity_surf_clears_bike_bug846.lua delete mode 100644 tests/parity_switch_cursor_reset.lua delete mode 100644 tests/parity_trainer_victory_text.lua delete mode 100644 tests/parity_wardens_house_bug535.lua delete mode 100644 tests/parity_yellow_old_man.lua diff --git a/tests/parity_faint_cry_bug709.lua b/tests/parity_faint_cry_bug709.lua index 45f7e3b0..bf61f36c 100644 --- a/tests/parity_faint_cry_bug709.lua +++ b/tests/parity_faint_cry_bug709.lua @@ -14,6 +14,15 @@ package.path = "./?.lua;./?/init.lua;" .. package.path local T = require("tests.modkit") + +-- Scoped suite, not the module-level counters: run_tests.lua dofiles this +-- file in its own process, and T.finish ends in os.exit, which took the +-- parent runner down with it. The run still exited 0, so it read as a pass +-- while every alphabetically later parity file and the three tiers chained +-- after them silently never ran. S.finish raises instead, which is what the +-- rest of the parity files do. modkit does not re-export suite, so it comes +-- off the shared harness it wraps. +local S = T.harness.suite("parity faint cry bug709") local Data = T.fixtures.fresh() local Font = require("src.render.Font") Font.load(Data) @@ -70,10 +79,10 @@ do battle.playVictoryMusic = function() end battle:onFaint(battle.player) pump(battle, 1) - T.eq(cries[1], "FIXMON_A", "the player mon's faint plays its species cry") - T.eq(#cries, 1, "no other cry on the player faint") + S.eq(cries[1], "FIXMON_A", "the player mon's faint plays its species cry") + S.eq(#cries, 1, "no other cry on the player faint") for _, name in ipairs(sfx) do - T.check(name ~= "Faint_Fall", + S.check(name ~= "Faint_Fall", "the player faint never plays Faint_Fall (#709)") end end @@ -87,18 +96,18 @@ do battle.playVictoryMusic = function() end battle:onFaint(battle.enemy) pump(battle, 2) - T.eq(#cries, 0, "the enemy faint plays no species cry") + S.eq(#cries, 0, "the enemy faint plays no species cry") local fall, thud = false, false for i, name in ipairs(sfx) do if name == "Faint_Fall" then - T.check(not fall, "Faint_Fall plays once") + S.check(not fall, "Faint_Fall plays once") fall = true - T.check(not thud, "Faint_Fall precedes Faint_Thud") + S.check(not thud, "Faint_Fall precedes Faint_Thud") elseif name == "Faint_Thud" then thud = true end end - T.check(fall and thud, "trainer enemy faint plays Faint_Fall and Faint_Thud") + S.check(fall and thud, "trainer enemy faint plays Faint_Fall and Faint_Thud") end -- enemy faint, wild battle: no faint sfx at all (victory music only) @@ -110,11 +119,11 @@ do battle.playVictoryMusic = function() end battle:onFaint(battle.enemy) pump(battle) - T.eq(#cries, 0, "the wild enemy faint plays no species cry") + S.eq(#cries, 0, "the wild enemy faint plays no species cry") for _, name in ipairs(sfx) do - T.check(name ~= "Faint_Fall" and name ~= "Faint_Thud", + S.check(name ~= "Faint_Fall" and name ~= "Faint_Thud", "the wild enemy faint plays no faint sfx (.wild_win)") end end -T.finish("parity faint cry bug709") +S.finish() diff --git a/tests/parity_fly_anim.lua b/tests/parity_fly_anim.lua deleted file mode 100644 index e3e14ef1..00000000 --- a/tests/parity_fly_anim.lua +++ /dev/null @@ -1,91 +0,0 @@ --- Parity: the Fly overworld animation (#702). --- --- Oracle: engine/overworld/player_animations.asm. Departure --- (_LeaveMapAnim .flyAnimation) flaps the bird in place for 8 x Delay3, --- plays SFX_FLY, flies FlyAnimationScreenCoords1 up and off to the right --- (12 pairs, 3 frames each), waits 40 frames, then exits over the --- top-left along FlyAnimationScreenCoords2 (11 pairs). Arrival --- (EnterMapAnim .flyAnimation) plays SFX_FLY again and swoops in along --- FlyAnimationEnterScreenCoords (12 pairs), and only then does --- LoadPlayerSpriteGraphics bring the player back. --- --- Self-contained: `luajit tests/parity_fly_anim.lua`; also globbed by --- tests/run_tests.lua. -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 fly anim (#702)") -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() -Game.save = SaveData.newGame() - --- record SFX without touching the audio backend -local plays = {} -local Sound = require("src.core.Sound") -local realPlay = Sound.play -Sound.play = function(_, key) plays[#plays + 1] = key end - -local function popAll() while Game.stack:top() do Game.stack:pop() end end -local function frame() - Input.pressed = {} - StateStack:update(1 / 60) -end -local function frames(n) for _ = 1, n do frame() end end - -Game.stack:push(OW, "ROUTE_17", 4, 10, "down") -local ow = Game.stack:top() - -ow:flyTo("PALLET_TOWN") -check(ow.flyAnim ~= nil, "the bird lead-in starts on FLY") -eq(ow.flyAnim and ow.flyAnim.phase, "flap", "the bird flaps in place first") -eq(ow.player.inputLocked, true, "input is locked for the flight") -eq(#plays, 0, "no SFX during the in-place flap") - -frames(23) -eq(ow.flyAnim and ow.flyAnim.phase, "flap", "still flapping 23 frames in") -frame() -eq(ow.flyAnim and ow.flyAnim.phase, "path1", - "the up-right path starts after 8 x Delay3") -eq(plays[#plays], "Fly", "SFX_FLY plays as the bird takes off") - -frames(36) -eq(ow.flyAnim and ow.flyAnim.phase, "hold", - "the bird parks off screen after the 12-pair path") -frames(40) -eq(ow.flyAnim and ow.flyAnim.phase, "path2", - "the top-left exit follows the 40-frame beat") -frames(33) -check(ow.flyAnim == nil, "the departure ends after the 11-pair exit") - --- the warp transition runs its fade out/in; the map switches inside it -local guard = 0 -while ow.map.id == "ROUTE_17" and guard < 400 do - guard = guard + 1 - frame() -end -eq(ow.map.id, "PALLET_TOWN", "the warp lands in Pallet Town") -check(ow.flyArrive ~= nil, "the landing swoop starts on arrival") -eq(plays[#plays], "Fly", "SFX_FLY plays again for the landing") -eq(ow.player.inputLocked, true, "input stays locked for the swoop") - -frames(35) -check(ow.flyArrive ~= nil, "the swoop is still flying 35 frames in") -frame() -check(ow.flyArrive == nil, "the swoop ends after the 12-pair path") -eq(ow.player.inputLocked, false, "and hands input back") - -Sound.play = realPlay -S.finish() diff --git a/tests/parity_gift_atomicity.lua b/tests/parity_gift_atomicity.lua deleted file mode 100644 index 96e6e394..00000000 --- a/tests/parity_gift_atomicity.lua +++ /dev/null @@ -1,185 +0,0 @@ --- Parity test, gift atomicity: a mon handed over by give_pokemon and the --- event that closes its offer must land in the same script step, so a --- script torn down between the two cannot hand the gift out twice (#426). --- --- asm sources: --- pokeyellow scripts/Route24.asm (Route24CooltrainerM4Text: CheckEvent --- EVENT_54F -> YesNoChoice -> GivePokemon -> `jp nc, TextScriptEnd` --- (party + box full leaves the event clear so the offer repeats) -> --- PrintText Route24Text_515e3 -> SetEvent EVENT_54F) --- pokeyellow scripts/CeruleanMelaniesHouse.asm (same shape plus predef --- HideObject TOGGLE_CERULEAN_BULBASAUR, then SetEvent --- EVENT_GOT_BULBASAUR_IN_CERULEAN) --- pokeyellow scripts/VermilionCity_2.asm (CheckEvent / SetEvent --- EVENT_GOT_SQUIRTLE_FROM_OFFICER_JENNY) --- scripts/CeladonMansionRoofHouse.asm (Eevee ball: GivePokemon with no --- confirm, HideObject on success) --- On hardware the event write trails the received text because no step in --- between can abort. The port yields there (AskName, NamingScreen, the --- text box) and wraps every row in the script.command mod hook, so the --- write is hoisted ahead of the text: the event is only read at script --- entry, and the failed-give path still leaves it clear. --- --- Self-contained: run via `luajit tests/parity_gift_atomicity.lua`; also --- dofile'd by tests/run_tests.lua's aggregator. -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 gift atomicity") -local check, eq = S.check, S.eq - -local Commands = require("src.script.Commands") -local Events = require("src.mods.Events") -local Flags = require("src.script.Flags") -local Game = require("src.core.Game") -local Hooks = require("src.mods.Hooks") -local Input = require("src.core.Input") -local Logger = require("src.core.Logger") -local Runtime = require("src.mods.Runtime") -local SaveData = require("src.core.SaveData") -local ScriptRunner = require("src.script.ScriptRunner") -local StateStack = require("src.core.StateStack") - -Game.data = Data -Game.input = Input; Input:init() -Game.stack = StateStack; StateStack:init() -Game.save = SaveData.newGame() -require("src.render.Font").load(Data) - -local gifts = require("data.scripts.yellow_gifts") -local eevee = require("data.scripts.celadon_eevee") - --- === 1) row-order audit: on every gift site the carry guard follows --- give_pokemon immediately and the bookkeeping (event, and the --- HideObject that clears a ball or a pen mon) comes before any --- received text === -local function audit(label, rows) - local give - for i, row in ipairs(rows) do - if row[1] == "give_pokemon" then give = i break end - end - if not give then - check(false, label .. ": has a give_pokemon row") - return - end - eq(rows[give + 1] and rows[give + 1][1], "jump_if_false", - label .. ": carry guard sits right after give_pokemon") - local flag, text, hide - for i = give + 2, #rows do - local name = rows[i][1] - if name == "set_flag" and not flag then flag = i end - if name == "hide_object" and not hide then hide = i end - if (name == "show_text" or name == "ask") and not text then text = i end - if name == "jump" and rows[i][2] ~= nil and text then break end - end - eq(flag, give + 2, label .. ": event write is the first row past the guard") - check(text and flag < text, - label .. ": event write precedes the received text") - if hide then - check(hide < text, label .. ": HideObject precedes the received text") - end -end - --- the two function-form scripts build their rows per talk; run them with --- the gift branch's preconditions and keep what they hand the runner -local function capture(fn, save) - local rows - local ow = { runner = { run = function(_, r) rows = r end } } - fn({ save = save }, ow, { def = {}, facePlayer = function() end }, - function() end) - return rows or {} -end - -audit("Route 24 Damian", - gifts.ROUTE_24.talk.TEXT_ROUTE24_COOLTRAINER_M4) -audit("Melanie's BULBASAUR", - capture(gifts.CERULEAN_MELANIES_HOUSE.talk - .TEXT_CERULEANMELANIESHOUSE_MELANIE, - { flags = {}, pikachuHappiness = 200 })) -audit("Officer Jenny's SQUIRTLE", - capture(gifts.VERMILION_CITY.talk.TEXT_VERMILIONCITY_OFFICER_JENNY, - { flags = {}, inventory = { THUNDERBADGE = 1 } })) -audit("Celadon EEVEE ball", - eevee.talk.TEXT_CELADONMANSION_ROOF_HOUSE_EEVEE_POKEBALL) - --- === harness: run a row list headless, A-mashing through the yes/no, --- the nickname prompt and every text box, recording show_text ids --- (Yellow's gift text is not in a Red cache, so show_text takes --- its literal-id fallback: the ids are still what we assert on) === -local shown = {} -local origShow = Commands.show_text -Commands.show_text = function(ctx, textId, subs) - shown[#shown + 1] = textId - return origShow(ctx, textId, subs) -end - -local function runRows(rows) - shown = {} - StateStack:init() - local ow = { map = { id = "ROUTE_24", def = { label = "ROUTE_24" } }, - npcs = {}, entities = {} } - local r = ScriptRunner.new(Game, ow) - r:run(rows, { npc = { def = {}, facePlayer = function() end }, - overworld = ow }) - local guard = 0 - while r:isRunning() and guard < 3000 do - guard = guard + 1 - Input.pressed = { a = true } - StateStack:update(1 / 60) - r:update() - end - Input.pressed = {} - return not r:isRunning() -end - -local DAMIAN = gifts.ROUTE_24.talk.TEXT_ROUTE24_COOLTRAINER_M4 - --- === 2) plain accept: one CHARMANDER, EVENT_54F set, and the next talk --- is Damian's after-text only === -Game.save = SaveData.newGame() -check(runRows(DAMIAN), "Damian gift script completes") -eq(#Game.save.party, 1, "CHARMANDER joins the party") -eq(Game.save.party[1].species, "CHARMANDER", "gift species is CHARMANDER") -check(Flags.get(Game.save, "EVENT_54F"), "accepting sets EVENT_54F") -check(runRows(DAMIAN), "post-gift talk completes") -eq(table.concat(shown, ","), "_Route24DamianText4", - "a closed offer shows only the after-text") -eq(#Game.save.party, 1, "no second CHARMANDER") - --- === 3) the regression itself: every row runs inside the script.command --- hook, and a mod that mishandles the row after the give (the --- reporter was running a third-party UI mod) tears the coroutine --- down mid-gift -- here by sending the pc at a label that is not --- there. The mon is already in the party, so EVENT_54F has to be --- set by then or the next talk re-runs the whole offer === -local savedEvents, savedHooks, savedErrors = - Runtime.events, Runtime.hooks, Runtime.errors -local hooks = Hooks.new() -Runtime.install(Events.new(), hooks, {}) -local remove = hooks:wrap("script.command", function(nextFn, _, name, args) - if name == "show_text" and args[1] == "_Route24DamianText2" then - return "no_such_label" - end - return nextFn() -end, 0, "t") - -Game.save = SaveData.newGame() -local origError = Logger.error -- the tear-down logs; the test expects it -Logger.error = function() end -runRows(DAMIAN) -Logger.error = origError -eq(#Game.save.party, 1, "the killed script still handed the CHARMANDER over") -check(Flags.get(Game.save, "EVENT_54F"), - "EVENT_54F survives a tear-down after the give") - -remove() -Runtime.install(savedEvents, savedHooks, savedErrors) - -check(runRows(DAMIAN), "talk after the tear-down completes") -eq(table.concat(shown, ","), "_Route24DamianText4", - "the interrupted gift is not offered again") -eq(#Game.save.party, 1, "still exactly one CHARMANDER") - -S.finish() diff --git a/tests/parity_marowak_ball.lua b/tests/parity_marowak_ball.lua deleted file mode 100644 index d385db42..00000000 --- a/tests/parity_marowak_ball.lua +++ /dev/null @@ -1,188 +0,0 @@ --- Parity test: a ball thrown at the POKEMON_TOWER_6F RESTLESS SOUL is --- always dodged, scope or no scope. --- --- ItemUseBall reaches the $10 "can't be caught" anim data by TWO --- independent routes (engine/items/item_effects.asm): --- --- :149-153 callfar IsGhostBattle / ld b, $10 / jp z, .setAnimData --- :166-175 .notOldManBattle -- wCurMap == POKEMON_TOWER_6F and --- wEnemyMonSpecies2 == RESTLESS_SOUL -> the same $10 --- --- The port only had the first, as the scope-less disguise flag --- self.ghost. Once the SILPH_SCOPE revealed the MAROWAK the battle was --- an ordinary wild one, so throwBall ran the capture roll and a MASTER --- BALL caught it outright. That result is "caught", not "win" or the --- POKE DOLL escape, so PokemonTower6F's script never set --- EVENT_BEAT_GHOST_MAROWAK and the (10,16) trigger re-fired forever --- (#444). The map+species half sits BEFORE .loop, hence before the --- MASTER_BALL shortcut, so even a Master Ball is dodged. --- --- Run-away parity is the other side of this: only IsGhostBattle grants --- the free escape (engine/battle/core.asm TryRunningFromBattle), so a --- revealed MAROWAK keeps normal flee rolls and self.ghost stays the sole --- gate there. --- --- Self-contained; run via `luajit tests/parity_marowak_ball.lua`. -package.path = "./?.lua;./?/init.lua;" .. package.path -if not _G.love then _G.love = require("tests.love_stub") end -local S = require("tests.harness").suite("parity marowak ball") -local check, eq = S.check, S.eq - -local BattleState = require("src.battle.BattleState") - --- ---- 1. the 6F script arms noCatch with and without the scope ----------- -do - local realTextBox = package.loaded["src.render.TextBox"] - local realBattleState = package.loaded["src.battle.BattleState"] - package.loaded["src.render.TextBox"] = { - new = function(_, text, done) return { text = text, done = done } end, - } - local made = {} - package.loaded["src.battle.BattleState"] = { - newWild = function(_, species, level) - local b = { species = species, level = level, ghost = false } - b.makeGhost = function(self) self.ghost = true end - -- the scope's branch (#492): disguised on entry, but IsGhostBattle - -- false, which is exactly the state the dodge below has to survive - b.makeUnveiledGhost = function(self) self.scopeReveal = true end - made[#made + 1] = b - return b - end, - } - - local tower = dofile("data/scripts/story3.lua").POKEMON_TOWER_6F - local function trigger(inventory) - local pushed = {} - local game = { - save = { inventory = inventory, flags = {} }, - data = { text = {} }, - stack = { push = function(_, box) pushed[#pushed + 1] = box end }, - } - local ow = { - player = {}, - scriptMove = function() end, - afterBattle = function() end, - } - check(tower.onStep(game, ow, 10, 16), "the trigger fires on (10,16)") - pushed[1].done() - return made[#made] - end - - local noScope = trigger({}) - check(noScope.ghost, "without the scope the battle is still disguised") - check(noScope.noCatch, "and noCatch is set") - - local withScope = trigger({ SILPH_SCOPE = 1 }) - check(not withScope.ghost, "with the scope IsGhostBattle is false") - check(withScope.scopeReveal, "and the unveil plays instead (#492)") - check(withScope.noCatch, - "but noCatch survives it -- balls are dodged either way") - - package.loaded["src.render.TextBox"] = realTextBox - package.loaded["src.battle.BattleState"] = realBattleState -end - --- ---- 2. throwBall takes the dodge branch on noCatch alone --------------- -local realSound = package.loaded["src.core.Sound"] -package.loaded["src.core.Sound"] = { play = function() end } - --- A real BattleState minus the pieces the decision does not touch: the --- capture roll and the ball chain record that they were reached, which is --- exactly the bug (a MASTER BALL catching the revealed MAROWAK). -local function throw(flags, ball) - local self = setmetatable({ - kind = "wild", - ghost = flags.ghost or false, - noCatch = flags.noCatch or false, - queue = {}, - rolled = false, - chained = false, - enemyMoved = false, - turnEnded = false, - data = { items = { MASTER_BALL = { name = "MASTER BALL" }, - POKE_BALL = { name = "POKé BALL" } }, - text = {} }, - game = { save = { player = { name = "RED" } } }, - player = {}, - enemy = {}, - }, BattleState) - self.ballDef = function() return nil end - self.catchAttempt = function(s) s.rolled = true return false, 3 end - self.ballChain = function(s) s.chained = true end - self.enemyAction = function() return {} end - self.executeAction = function(s) s.enemyMoved = true end - self.endOfTurn = function(s) s.turnEnded = true end - self:throwBall(ball) - -- the whole outcome lives in the act() closure throwBall queues, and - -- that closure queues more rows, so drain like updateQueue does: run - -- each fn row once, with nextInsert pointing at it. - local ran = {} - local more = true - while more do - more = false - for i, row in ipairs(self.queue) do - if row.fn and not ran[row] then - ran[row] = true - self.nextInsert = i - row.fn() - more = true - break - end - end - end - local texts = {} - for _, row in ipairs(self.queue) do - if row.text then texts[#texts + 1] = tostring(row.text) end - end - self.texts = table.concat(texts, "|") - return self -end - -local function assertDodge(b, label) - check(not b.rolled, label .. ": no capture roll") - check(not b.chained, label .. ": no wobble chain") - check(b.texts:find("It dodged the", 1, true) ~= nil, - label .. ": ItemUseBallText00 line 1") - check(b.texts:find("can't be caught", 1, true) ~= nil, - label .. ": ItemUseBallText00 line 2") - check(b.enemyMoved, label .. ": the turn is spent, the foe moves") - check(b.turnEnded, label .. ": and the turn ends") -end - -assertDodge(throw({ ghost = true }, "POKE_BALL"), "IsGhostBattle exit") -assertDodge(throw({ noCatch = true }, "POKE_BALL"), ".notOldManBattle exit") --- the regression itself: revealed by the scope, so ghost is false -assertDodge(throw({ noCatch = true }, "MASTER_BALL"), "MASTER BALL") - -do - local plain = throw({}, "MASTER_BALL") - check(plain.rolled, - "an ordinary wild mon still rolls -- the guard is not global") -end - --- The dodged toss keeps the arc the thrown ball picked (TossBallAnimation --- reads wCurItem), so the Master Ball flicker is not lost. -do - local b = throw({ noCatch = true }, "MASTER_BALL") - local anim - for _, row in ipairs(b.queue) do - if row.anim then anim = row.anim break end - end - eq("ULTRATOSS_ANIM", anim, "a dodged MASTER BALL still tosses as ULTRATOSS") -end - -package.loaded["src.core.Sound"] = realSound - --- ---- 3. noCatch grants no free escape ---------------------------------- -do - local function roll(flags) - local b = { ghost = flags.ghost or false, noCatch = flags.noCatch or false, - runAttempts = 1, rng = function() return 255 end } - return BattleState.runRollVanilla(b, 10, 100) - end - check(roll({ ghost = true }), "IsGhostBattle still always escapes") - check(not roll({ noCatch = true }), - "a revealed MAROWAK takes the normal flee roll") -end - -S.finish() diff --git a/tests/parity_midstep_buttons.lua b/tests/parity_midstep_buttons.lua deleted file mode 100644 index 04031b9d..00000000 --- a/tests/parity_midstep_buttons.lua +++ /dev/null @@ -1,154 +0,0 @@ --- 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() diff --git a/tests/parity_move_swap.lua b/tests/parity_move_swap.lua deleted file mode 100644 index 7f4c0996..00000000 --- a/tests/parity_move_swap.lua +++ /dev/null @@ -1,129 +0,0 @@ --- Parity / regression for #73: Gen 1 fight-menu SELECT reorders moves. --- --- Select marks a slot, move the cursor, Select (or A) swaps. Defaults: --- Tab / either Shift / gamepad Back. Self-contained; also picked up by --- tests/run_tests.lua's parity_* glob. -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.pokemon and Data.pokemon.RATTATA) then Data:load() end -local TypeChart = require("src.battle.TypeChart") -TypeChart.load(Data) - -local Pokemon = require("src.pokemon.Pokemon") -local BattleState = require("src.battle.BattleState") -local Input = require("src.core.Input") -local S = require("tests.harness").suite("parity move swap") -local check, eq = S.check, S.eq - -local function freshGame() - local mon = Pokemon.new(Data, "NIDORAN_M", 8) - mon.moves = { - { id = "TACKLE", pp = 35 }, - { id = "LEER", pp = 30 }, - { id = "HORN_ATTACK", pp = 25 }, - { id = "POISON_STING", pp = 35 }, - } - return { - data = Data, - input = Input, - save = { - party = { mon }, - player = { name = "RED" }, - inventory = {}, - options = {}, - pokedex = { seen = {}, owned = {} }, - flags = {}, - money = 0, - }, - stack = { push = function() end, pop = function() end, top = function() end }, - } -end - -local function tapKey(battle, key) - Input:keypressed(key) - Input:step() - battle:update(0) - Input:keyreleased(key) -end - -local function tapPad(battle, button) - Input:gamepadpressed(nil, button) - Input:step() - battle:update(0) - Input:gamepadreleased(nil, button) -end - --- Default Select sources all edge the logical select button. -do - Input:init() - for _, key in ipairs({ "tab", "rshift", "lshift" }) do - Input:reset() - Input:keypressed(key) - Input:step() - check(Input:wasPressed("select"), key .. " maps to select") - end - Input:reset() - Input:gamepadpressed(nil, "back") - Input:step() - check(Input:wasPressed("select"), "gamepad back maps to select") -end - --- Fight menu: Select, move, Select swaps slots 1 and 2. -do - Input:init() - local game = freshGame() - local battle = BattleState.newWild(game, "PIDGEY", 5) - battle.phase = "moveSelect" - battle.moveIndex = 1 - battle.moveSwapIndex = nil - local a = battle.player.curMoves[1].id - local b = battle.player.curMoves[2].id - tapKey(battle, "tab") - eq(battle.moveSwapIndex, 1, "first Select marks the current slot") - tapKey(battle, "down") - eq(battle.moveIndex, 2, "cursor moved to slot 2") - tapKey(battle, "tab") - check(battle.moveSwapIndex == nil, "second Select clears the mark") - eq(battle.player.curMoves[1].id, b, "slot 1 holds the former slot 2 move") - eq(battle.player.curMoves[2].id, a, "slot 2 holds the former slot 1 move") - eq(battle.player.mon.moves[1].id, b, "party moves table stays in sync") -end - --- Same reorder via gamepad Back (SDL "back" = controller Select/View). -do - Input:init() - local game = freshGame() - local battle = BattleState.newWild(game, "PIDGEY", 5) - battle.phase = "moveSelect" - battle.moveIndex = 1 - battle.moveSwapIndex = nil - local a = battle.player.curMoves[1].id - local b = battle.player.curMoves[2].id - tapPad(battle, "back") - tapPad(battle, "dpdown") - tapPad(battle, "back") - eq(battle.player.curMoves[1].id, b, "pad Select swaps slot 1") - eq(battle.player.curMoves[2].id, a, "pad Select swaps slot 2") -end - --- A confirms a pending swap (bag-style), without starting the turn. -do - Input:init() - local game = freshGame() - local battle = BattleState.newWild(game, "PIDGEY", 5) - battle.phase = "moveSelect" - battle.moveIndex = 1 - battle.moveSwapIndex = nil - local a = battle.player.curMoves[1].id - local b = battle.player.curMoves[2].id - tapKey(battle, "tab") - tapKey(battle, "down") - tapKey(battle, "z") -- A - eq(battle.phase, "moveSelect", "A completes a pending swap without attacking") - eq(battle.player.curMoves[1].id, b, "A-confirm swapped slot 1") - eq(battle.player.curMoves[2].id, a, "A-confirm swapped slot 2") -end - -S.finish() diff --git a/tests/parity_shift_exp_share.lua b/tests/parity_shift_exp_share.lua deleted file mode 100644 index 601af1a7..00000000 --- a/tests/parity_shift_exp_share.lua +++ /dev/null @@ -1,206 +0,0 @@ --- Parity test: the SHIFT free switch hands the WHOLE exp share to the mon --- coming in (#275). EnemySendOutFirstMon zeroes wPartyGainExpFlags and --- wPartyFoughtCurrentEnemyFlags before jumping to SwitchPlayerMon, which sets --- only the incoming mon's bit (engine/battle/core.asm:1436-1443, 2424-2433); --- GiveExperiencePoints divides by the set bits (experience.asm:295-300), so a --- leftover flag halves the payout. The reset was never ported. -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.pokemon and Data.pokemon.RATTATA) then Data:load() end -local TypeChart = require("src.battle.TypeChart") -TypeChart.load(Data) - -local Pokemon = require("src.pokemon.Pokemon") -local BattleState = require("src.battle.BattleState") -local Experience = require("src.battle.Experience") -local Screens = require("src.ui.Screens") -local S = require("tests.harness").suite("parity shift exp share") -local check, eq = S.check, S.eq - --- Minimal game stub: what BattleState.newTrainer / enemyMonFainted touch. --- battleStyle is per-scenario, so the caller sets it. -local function freshGame(style) - return { - data = Data, - save = { - party = { - Pokemon.new(Data, "BULBASAUR", 50), - Pokemon.new(Data, "SQUIRTLE", 40), - }, - player = { name = "RED" }, - inventory = {}, - options = { battleStyle = style }, - pokedex = { seen = {}, owned = {} }, - flags = {}, - money = 0, - }, - stack = { push = function() end, pop = function() end, top = function() end }, - } -end - --- Drain the queue, running act rows and answering the SHIFT prompt. `yes` --- picks YES (the free switch) or NO; `pick` is the party mon the battle --- PartyMenu would hand back. Text rows are collected in order so the exp --- line can be read the way the player reads it. -local function pump(b, yes, pick, seen) - local origPush = Screens.push - Screens.push = function(_, id, opts) - if id == "PartyMenu" and opts and opts.onSwitch and pick then - opts.onSwitch(pick) - end - end - local ok, err = pcall(function() - local n = 0 - while #b.queue > 0 and n < 500 do - n = n + 1 - local item = table.remove(b.queue, 1) - if item.fn then - b.nextInsert = 0 - item.fn() - elseif item.text then - seen[#seen + 1] = item.text - if item.choice and item.text:find("change POKéMON", 1, true) then - item.choice(yes) - end - end - end - end) - Screens.push = origPush - return ok, err -end - --- the number _ExpPointsText prints (wExpAmountGained), out of the port's --- "%s gained\n%d EXP. Points!" row -local function expLine(seen) - for _, t in ipairs(seen) do - local n = t:match("gained\n(%d+) EXP%. Points!") - if n then return tonumber(n), t end - end - return nil -end - --- OPP_YOUNGSTER 1 is RATTATA 11 / EKANS 11 in both versions: two slots, so --- there is a second mon to KO after the switch. -local YOUNGSTER, ROSTER = "OPP_YOUNGSTER", 1 - --- Set up the fight at the moment the first enemy mon drops, with the lead the --- only participant (as markParticipant left it), so the caller only has to pump. -local function atFirstKO(style) - local Game = freshGame(style) - local b = BattleState.newTrainer(Game, YOUNGSTER, ROSTER) - b.enemyParty[1].hp = 0 - b.enemyIndex = 1 - b.enemy.mon = b.enemyParty[1] - b.participants = { [Game.save.party[1]] = true } - b:enemyMonFainted() - return Game, b -end - --- KO whatever is out now and read back the exp line for it. -local function koAndRead(b) - local before = {} - for i, mon in ipairs(b.game.save.party) do before[i] = mon.exp end - local seen = {} - b.enemy.mon.hp = 0 - -- updateQueue zeroes this before every act row it runs; calling - -- enemyMonFainted straight from the test has to do the same, or the *Next - -- inserters index past the end of the drained queue and leave a hole - b.nextInsert = 0 - b:enemyMonFainted() - local ok, err = pump(b, false, nil, seen) - local delta = {} - for i, mon in ipairs(b.game.save.party) do delta[i] = mon.exp - before[i] end - return ok, err, seen, delta -end - -do - local Game, b = atFirstKO("shift") - eq(#b.enemyParty, 2, "OPP_YOUNGSTER roster " .. ROSTER .. " has two mons") - local lead, reserve = Game.save.party[1], Game.save.party[2] - - -- KO one: the SHIFT prompt, answered YES with the reserve picked. - local seen = {} - local ok, err = pump(b, true, reserve, seen) - check(ok, "the SHIFT switch pumped without error: " .. tostring(err)) - check(b.player.mon == reserve, "the free switch put the reserve on the field") - check(b.enemy.mon.hp > 0, "the foe's second mon is out") - - -- The participant set is the mechanism; the exp number below is the symptom. - check(b.participants[reserve] == true, "the switch-in is a participant") - check(b.participants[lead] == nil, - "the mon that was out when the foe fainted is no longer one (#275)") - - -- KO two: the reserve fights alone, so it must be paid as a single - -- participant. - local ok2, err2, seen2, delta = koAndRead(b) - check(ok2, "the second KO pumped without error: " .. tostring(err2)) - - local foeDef = Data.pokemon[b.enemyParty[2].species] - local solo = Experience.gainFor(foeDef, b.enemyParty[2].level, true, 1, nil, - Data.constants) - local halved = Experience.gainFor(foeDef, b.enemyParty[2].level, true, 2, nil, - Data.constants) - check(solo > halved, - "the two divisors are distinguishable for this foe (" .. - solo .. " vs " .. halved .. ")") - - local shown, line = expLine(seen2) - check(shown ~= nil, "the KO printed an EXP. Points! line") - eq(shown, solo, "the switch-in is paid a whole share, not a split one (#275)") - check(shown ~= halved, - "the printed number is not the two-way split (" .. tostring(line) .. ")") - eq(delta[2], solo, "the reserve's exp rose by exactly that share") - eq(delta[1], 0, "the mon left behind is paid nothing for a KO it missed") - - local lines = 0 - for _, t in ipairs(seen2) do - if t:find("EXP%. Points!") then lines = lines + 1 end - end - eq(lines, 1, "exactly one mon is announced as gaining exp") -end - --- Control: SET style has no free switch, so the lead fights both mons and is --- paid a whole share for each. Pin it here: the SHIFT switch-in above must --- earn the same number. -do - local Game, b = atFirstKO("set") - local seen = {} - local ok = pump(b, false, nil, seen) - check(ok, "SET style pumped without error") - check(b.player.mon == Game.save.party[1], "SET style never offered a switch") - - local ok2, _, seen2, delta = koAndRead(b) - check(ok2, "the SET second KO pumped without error") - local foeDef = Data.pokemon[b.enemyParty[2].species] - local solo = Experience.gainFor(foeDef, b.enemyParty[2].level, true, 1, nil, - Data.constants) - local shown = expLine(seen2) - eq(shown, solo, "SET style pays the lead a whole share") - eq(delta[1], solo, "and the lead's exp rises by it") -end - --- The path the reset must NOT touch: the party-menu SwitchPlayerMon --- (core.asm:2424-2433, from PartyMenuOrRockOrRun) sets the incoming mon's bit --- without zeroing the flag bytes, which is the exp-share trick every player --- uses: send a weak mon in, switch it straight out, it still splits the KO. -do - local Game = freshGame("shift") - local b = BattleState.newTrainer(Game, YOUNGSTER, ROSTER) - local lead, reserve = Game.save.party[1], Game.save.party[2] - b.participants = { [lead] = true } - b:resolveSwitch(reserve) - local n = 0 - while #b.queue > 0 and n < 200 do - n = n + 1 - local item = table.remove(b.queue, 1) - if item.fn then b.nextInsert = 0; item.fn() end - end - check(b.player.mon == reserve, "the voluntary switch went through") - check(b.participants[reserve] == true, "the mon coming in participates") - check(b.participants[lead] == true, - "a VOLUNTARY switch keeps the outgoing mon flagged (the exp share)") -end - -S.finish() diff --git a/tests/parity_starter_dex.lua b/tests/parity_starter_dex.lua deleted file mode 100644 index 49935232..00000000 --- a/tests/parity_starter_dex.lua +++ /dev/null @@ -1,117 +0,0 @@ --- Parity: Oak's lab starter-ball Pokédex preview (#110). --- pret StarterDex (engine/events/starter_dex.asm) temporarily sets the --- owned bits so ShowPokedexData prints the full entry before the player --- has caught anything. Also: English R/B prints only the kind string --- (no " POKéMON" suffix -- that clipped "LIZARD" to "LIZARD POKé"). -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 starter dex") -local check, eq = S.check, S.eq - -local Font = require("src.render.Font") -Font.load(Data) - -local DexEntryMenu = require("src.ui.DexEntryMenu") -local SaveData = require("src.core.SaveData") -local mapScripts = require("data.scripts.init") - -local function fakeGame() - return { - data = Data, - save = SaveData.newGame(), - input = { wasPressed = function() return false end }, - stack = { pop = function() end }, - } -end - -local function drawCapture(menu) - local drawn = {} - local saved = Font.draw - Font.draw = function(text, x, y) - drawn[#drawn + 1] = { text = tostring(text), x = x, y = y } - return Font.width(text) - end - menu:draw() - Font.draw = saved - return drawn -end - -local function findText(drawn, needle) - for _, d in ipairs(drawn) do - if d.text == needle or d.text:find(needle, 1, true) then return d end - end - return nil -end - --- === 1) unowned entry without forceOwned stays "Data unknown." === -do - local game = fakeGame() - game.save.pokedex = { seen = {}, owned = {} } - local menu = DexEntryMenu.new(game, "CHARMANDER") - local drawn = drawCapture(menu) - check(findText(drawn, "Data unknown."), - "unowned Charmander shows Data unknown without forceOwned") - check(not findText(drawn, "Obviously prefers"), - "unowned Charmander hides description without forceOwned") - check(not findText(drawn, "HT "), - "unowned Charmander hides height without forceOwned") -end - --- === 2) forceOwned shows full entry without mutating save === -do - local game = fakeGame() - game.save.pokedex = { seen = {}, owned = {} } - local menu = DexEntryMenu.new(game, { species = "CHARMANDER", forceOwned = true }) - check(menu.forceOwned, "forceOwned flag sticks on the menu") - local drawn = drawCapture(menu) - check(findText(drawn, "Obviously prefers"), - "forceOwned Charmander shows dex description") - check(findText(drawn, "HT "), - "forceOwned Charmander shows height") - check(not findText(drawn, "Data unknown."), - "forceOwned Charmander does not show Data unknown") - check(not game.save.pokedex.owned.CHARMANDER, - "forceOwned preview does not mark Charmander owned") -end - --- === 3) kind is the bare English string (no POKéMON suffix) === -do - local game = fakeGame() - game.save.pokedex = { seen = {}, owned = { CHARMANDER = true } } - local menu = DexEntryMenu.new(game, "CHARMANDER") - local drawn = drawCapture(menu) - local kind = findText(drawn, "LIZARD") - check(kind and kind.text == "LIZARD", - "kind draws as LIZARD only (English R/B PlaceString)") - check(not findText(drawn, "POKéMON"), - "kind line does not append POKéMON") - check(kind.x + Font.width(kind.text) <= 160, - "LIZARD kind fits on-screen (no clip)") -end - --- === 4) Oak's lab starter scripts request forceOwned === -do - local balls = { - "TEXT_OAKSLAB_CHARMANDER_POKE_BALL", - "TEXT_OAKSLAB_SQUIRTLE_POKE_BALL", - "TEXT_OAKSLAB_BULBASAUR_POKE_BALL", - } - for _, textId in ipairs(balls) do - local script = mapScripts.talkScript("OAKS_LAB", textId) - local found - for _, row in ipairs(script) do - if row[1] == "push_screen" and row[2] == "DexEntryMenu" then - found = row[3] - break - end - end - check(type(found) == "table" and found.forceOwned == true - and type(found.species) == "string", - textId .. " pushes DexEntryMenu with forceOwned") - end -end - -S.finish() diff --git a/tests/parity_surf_clears_bike_bug846.lua b/tests/parity_surf_clears_bike_bug846.lua deleted file mode 100644 index 8b8e1ea0..00000000 --- a/tests/parity_surf_clears_bike_bug846.lua +++ /dev/null @@ -1,187 +0,0 @@ --- Parity test: getting on SURF ends the bike (#846). --- --- pokered keeps walking / biking / surfing in ONE state byte, --- wWalkBikeSurfState. ItemUseSurfboard (engine/items/item_effects.asm) --- copies the old state aside, refuses when it is already 2, and on a --- successful mount does `ld a, 2 / ld [wWalkBikeSurfState], a ; change --- player state to surfing` followed by PlayDefaultMusic -- the bike state --- is overwritten, so no bike can survive a surf: not its 8-frame step --- cadence, not its theme. The port splits that byte into two independent --- flags (Game.save.onBike and player.surfing) and nothing used to clear --- the first when the second went up, so a player who surfed off the bike --- paddled at bike speed with Music_BikeRiding still playing. --- --- The mirror direction is explicit in the same asm file: ItemUseBicycle --- opens `ld a, [wWalkBikeSurfState] / cp 2 ; is the player surfing? / --- jp z, ItemUseNotTime`, so the bag cannot re-raise the bike on water. --- --- Self-contained; run via `luajit tests/parity_surf_clears_bike_bug846.lua`. -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 surf clears bike") -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 ItemEffects = require("src.inventory.ItemEffects") -local Music = require("src.core.Music") -local Pokemon = require("src.pokemon.Pokemon") -local Renderer = require("src.render.Renderer") -local SaveData = require("src.core.SaveData") -local StateStack = require("src.core.StateStack") -local OW = require("src.world.OverworldController") - -Game.data = Data -Game.input = Input; Input:init() -Game.renderer = Renderer; Renderer:init() -Game.stack = StateStack; StateStack:init() -Game.save = SaveData.newGame() -Game.overworld = OW - --- tests/parity_bills_pc.lua swaps the OverworldController chunk's TextBox --- upvalue for a stub and never puts it back, and run_tests.lua runs every --- parity suite from one file: point it back at the real module so trySurf --- pushes a real box here (same guard as parity_field_move_layering.lua). -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 -setUpvalue(OW.trySurf, "TextBox", require("src.render.TextBox")) - -local function frame(btns) - Input.pressed = {} - for _, b in ipairs(btns or {}) do Input.pressed[b] = true; Input.state[b] = true end - StateStack:update(1 / 60) - for _, b in ipairs(btns or {}) do Input.state[b] = false end -end - -local function popAll() while Game.stack:top() do Game.stack:pop() end end - -local function pushOW(mapId, x, y, facing) - popAll() - Game.stack:push(OW, mapId, x, y, facing) - return Game.stack:top() -end - -local function mkMon(species, ...) - local m = Pokemon.new(Data, species, 20) - m.moves = {} - for _, id in ipairs({ ... }) do m.moves[#m.moves + 1] = { id = id, pp = 15 } end - return m -end - --- Music.play no-ops on the headless audio stub, so the song the bike/surf --- override actually resolves to is only observable by intercepting it --- (the Music.playMap stub pattern in parity_seam_walk_anim.lua) -local playedSong -local realPlay = Music.play -Music.play = function(_, song) playedSong = song end - -local bikeSong = Music.special(Data, "bike") -local surfSong = Music.special(Data, "surf") -check(bikeSong ~= nil and surfSong ~= nil and bikeSong ~= surfSong, - "the bike and surf themes are two distinct songs") - --- ===================================================================== --- control: on land, onBike really does buy the halved step cadence, so --- the assertion after the mount below is not vacuous --- ===================================================================== -local ow = pushOW("PALLET_TOWN", 5, 6, "up") -local p = ow.player -eq(p.stepFrames, 16, "a walking step is 16 frames") -eq(p.bikeStepFrames, 8, "the bicycle doubles walking speed (8 frames)") -Game.save.onBike = true -p.turnTimer = 0 -p.stepFramesCur = nil -eq(p:tryMove("up", ow.map, ow.entities), "moved", "riding north out of the spawn cell") -eq(p.stepFramesCur, p.bikeStepFrames, "onBike hands out the bike cadence on land") - --- ===================================================================== --- the mount: bike state is gone the moment the got-on text closes, and --- the first step onto the water is a WALK-length step, not a bike one --- ===================================================================== -ow = pushOW("PALLET_TOWN", 4, 13, "down") -p = ow.player -p.surfing = false -Game.save.onBike = true -Game.save.party = { mkMon("SQUIRTLE", "SURF") } --- HM03's badge gate (FieldDefaults hmBadges): without it partyKnows("SURF") --- refuses and trySurf never prints anything -Game.save.inventory.SOULBADGE = true -check(ow:partyKnows("SURF") ~= nil, "the party can use SURF here") -check(ow.map:isWaterCell(4, 14), "Pallet's south shore faces water at (4,14)") - --- what is playing when the mount starts: the bike override over the --- outdoor Pallet theme -Music.playMap(Data, "PALLET_TOWN", true, false) -eq(playedSong, bikeSong, "the bike theme plays while riding through Pallet") - -p.stepFramesCur = nil -ow:trySurf(4, 14, nil) -local box = Game.stack:top() -check(box ~= nil and box.pages ~= nil, "SURF prints _SurfingGotOnText") -local guard = 0 -while Game.stack:top() == box and guard < 400 do - guard = guard + 1 - frame({ "a" }) -end -check(Game.stack:top() ~= box, "the got-on text closes") - -eq(p.surfing, true, "the mount raises the surf state") -eq(Game.save.onBike, false, - "ItemUseSurfboard writes surfing OVER the bike state, so the bike ends (#846)") -eq(playedSong, surfSong, - "PlayDefaultMusic after the mount picks the surf theme, not the bike theme") - --- the blink carries the mount forward onto the water; let that scripted --- step land (it is queued through scriptMove, which drives the entity --- directly and never touches Player:tryMove) -guard = 0 -while (Game.stack:top() ~= ow or p.moving or #ow.scriptMoves > 0) and guard < 240 do - guard = guard + 1 - frame({}) -end -eq(Game.stack:top(), ow, "the mount ends back on the map") -eq(p.cellY, 14, "the mount steps forward onto the water") - --- the symptom in #846: the first paddled step the player takes. It runs --- through Player:tryMove, which reads Game.save.onBike for its step --- length -- a stale bike flag paddles at 8 frames per cell. -check(ow.map:isWaterCell(4, 15), "the next cell south is water too") -p.turnTimer = 0 -p.stepFramesCur = nil -eq(p:tryMove("down", ow.map, ow.entities), "moved", "paddling south from (4,14)") -eq(p.stepFramesCur, p.stepFrames, - "the paddled step uses the walk cadence, the exact symptom in #846") -check(p.stepFramesCur ~= p.bikeStepFrames, "no bike cadence survives onto the water") - --- ===================================================================== --- the mirror hole: the bag cannot put the bike back on under a surfer --- (ItemUseBicycle's `cp 2` -> jp z, ItemUseNotTime), or the bug returns --- by another route --- ===================================================================== -local save = SaveData.newGame() -local surfingOw = { player = { surfing = true } } -local result, msgs = ItemEffects.use(Data, save, "BICYCLE", nil, false, nil, surfingOw) -eq(result, "failed", "the BICYCLE is refused while surfing") -check(result ~= "bicycle", "a surfing BICYCLE never reaches the mount path") -check(msgs and msgs[1] and msgs[1]:find("isn't the", 1, true) ~= nil, - "the surfing BICYCLE refusal uses the OAK 'not the time' text") - -local landOw = { player = { surfing = false } } -eq((ItemEffects.use(Data, save, "BICYCLE", nil, false, nil, landOw)), "bicycle", - "the BICYCLE still mounts normally on land") - -Music.play = realPlay -popAll() -S.finish() diff --git a/tests/parity_switch_cursor_reset.lua b/tests/parity_switch_cursor_reset.lua deleted file mode 100644 index ba3a1f2e..00000000 --- a/tests/parity_switch_cursor_reset.lua +++ /dev/null @@ -1,49 +0,0 @@ --- Parity: a player send-out zeroes both battle cursors (#737). SendOutMon --- (engine/battle/core.asm:1733-1735) clears wBattleAndStartSavedMenuItem and, --- with the same hli/hl pair, wPlayerMoveListIndex behind it (wram.asm:242-244), --- so the menu reopens on FIGHT and the move list on the first slot. -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 then Data:load() end -local TypeChart = require("src.battle.TypeChart") -TypeChart.load(Data) -local Pokemon = require("src.pokemon.Pokemon") -local SaveData = require("src.core.SaveData") -local BattleState = require("src.battle.BattleState") -local S = require("tests.harness").suite("parity switch cursor reset") -local eq = S.eq - -local pressed = {} -local save = SaveData.newGame() -save.party = { - Pokemon.new(Data, "BULBASAUR", 10), - Pokemon.new(Data, "PIDGEY", 10), -} -local game = { - data = Data, - save = save, - input = { - wasPressed = function(_, key) return pressed[key] == true end, - isDown = function(_, key) return pressed[key] == true end, - }, - stack = { push = function() end, pop = function() end, top = function() end }, -} -local battle = BattleState.newWild(game, "RATTATA", 3) -battle.phase = "menu" -battle.menuIndex = 4 -battle.moveIndex = 3 - -battle:resolveSwitch(save.party[2]) -for i = 1, 4000 do - if battle.phase == "menu" then break end - pressed.a = (i % 4 == 0) - battle:update(1 / 60) - pressed.a = nil -end - -eq(battle.moveIndex, 1, "the move cursor is back on the first slot") -eq(battle.menuIndex, 1, "the battle menu is back on FIGHT") - -S.finish() diff --git a/tests/parity_trainer_victory_text.lua b/tests/parity_trainer_victory_text.lua deleted file mode 100644 index 45cf8446..00000000 --- a/tests/parity_trainer_victory_text.lua +++ /dev/null @@ -1,198 +0,0 @@ --- Parity: the beaten trainer's own loss line prints ON the battle screen, --- between the pic scrolling back in and the prize money (#282). --- TrainerBattleVictory (engine/battle/core.asm:915-949) runs TrainerDefeatedText, --- ScrollTrainerPicAfterBattle, PrintEndBattleText, then MoneyForWinningText. --- The scroll (scroll_draw_trainer_pic.asm:1-31) rewrites tilemap columns only, --- so the pokeball row ClearSprites emptied does not come back with the pic. -package.path = "./?.lua;./?/init.lua;" .. package.path -if not _G.love then _G.love = require("tests.love_stub") end -local S = require("tests.harness").suite("parity trainer victory text") -local check, eq = S.check, S.eq - -local Data = require("src.core.Data") -if not Data.maps then Data:load() end -local Font = require("src.render.Font") -Font.load(Data) - -local BattleState = require("src.battle.BattleState") -local Pokemon = require("src.pokemon.Pokemon") -local SaveData = require("src.core.SaveData") -local Sound = require("src.core.Sound") -local Music = require("src.core.Music") - -Sound.playCry = function() end -Sound.play = function() end -Sound.playMove = function() end -Sound.playMoveCry = function() end -Sound.stopLoop = function() end -Music.playBattle = function() end -Music.play = function() end - -local press = {} -local function makeGame(party) - local save = SaveData.newGame() - save.party = party - local stack = { states = {} } - function stack:push(state) self.states[#self.states + 1] = state end - function stack:pop() return table.remove(self.states) end - function stack:top() return self.states[#self.states] end - -- isDown as well as wasPressed: battle text collapses PrintLetterDelay - -- while A or B is held, and the typing path reads it every frame - return { data = Data, save = save, stack = stack, - input = { wasPressed = function(_, b) return press[b] == true end, - isDown = function(_, b) return press[b] == true end } } -end - --- A held: updateQueue only reads the button once a page is typed out, so an --- early press is ignored and the queue drains at a player's pace. -local function step(battle) - press.a = true - battle:update(1 / 60) - press.a = false -end - --- Fight a YOUNGSTER, wipe its party, and record every message row in the order --- it reached the screen plus what the foe's pic slot was doing at the time. -local function fightAndWin(endBattleText) - local game = makeGame({ Pokemon.new(Data, "BULBASAUR", 60) }) - local battle = BattleState.newTrainer(game, "OPP_YOUNGSTER", 1) - battle.endBattleText = endBattleText - local result, resultAt - battle.onFinish = function(r) result = r end - battle:enter() - for _ = 1, 500 do - step(battle) - if battle.phase == "menu" then break end - end - - -- the KO itself, through the real faint path (onFaint queues the slide, - -- the faint text and the enemyMonFainted act) - for _, mon in ipairs(battle.enemyParty) do mon.hp = 0 end - battle.enemy.mon.hp = 0 - battle.phase = "messages" - battle.nextInsert = 0 - battle:onFaint(battle.enemy) - - local pages, foeOffAt, foeShownAt = {}, {}, {} - local ballRowsSeen, frame, foeMax, foeSteps = 0, 0, 0, 0 - local realRow = battle.drawBallRow - battle.drawBallRow = function() ballRowsSeen = ballRowsSeen + 1 end - local lastOff = battle:picOffset("foe") - for f = 1, 2000 do - frame = f - step(battle) - local cur = battle.current - local text = cur and cur.text - if text and pages[#pages] ~= text then - pages[#pages + 1] = text - foeOffAt[text] = battle:picOffset("foe") - foeShownAt[text] = battle.showEnemyTrainer and true or false - end - local off = battle:picOffset("foe") - if off > foeMax then foeMax = off end - -- count only the inward frames; the jump from 0 to 64 is the program - -- being armed off-screen, not a step of the scroll - if off < lastOff then foeSteps = foeSteps + 1 end - lastOff = off - -- drawHUDs is the only place a ball row can come from; sample it while - -- the beaten trainer is back on screen - if battle.showEnemyTrainer then pcall(battle.drawHUDs, battle, 0) end - if result then resultAt = f break end - end - battle.drawBallRow = realRow - return { - battle = battle, pages = pages, result = result, resultAt = resultAt, - foeOffAt = foeOffAt, foeShownAt = foeShownAt, ballRowsSeen = ballRowsSeen, - frames = frame, foeMax = foeMax, foeSteps = foeSteps, - } -end - -local function indexOf(pages, fragment) - for i, p in ipairs(pages) do - if p:find(fragment, 1, true) then return i end - end - return nil -end - --- ------------------------------------------------- the full victory order -local LOSS = "What a total\nwaste of time!" -local run = fightAndWin(LOSS) - -eq(run.result, "win", "the battle resolves as a win") -local defeated = indexOf(run.pages, "defeated") -local loss = indexOf(run.pages, "waste of time") -local money = indexOf(run.pages, "for winning") -check(defeated ~= nil, "TrainerDefeatedText prints (\"RED defeated YOUNGSTER!\")") -check(loss ~= nil, - "the trainer's own EndBattleText prints INSIDE the battle (#282)") -check(money ~= nil, "MoneyForWinningText prints") -check(defeated and loss and defeated < loss, - "the defeat line comes before the trainer's loss line") -check(loss and money and loss < money, - "PrintEndBattleText comes before MoneyForWinningText (core.asm:942-949)") -check(money == #run.pages, - "the prize money is the LAST thing on the battle screen") - --- the pic is back, at rest, with no ball row beside it -if loss then - local text = run.pages[loss] - eq(run.foeShownAt[text], true, - "the beaten trainer's pic is on screen for his loss line") - eq(run.foeOffAt[text], 16, - "the pic has come to rest two tiles right of the battle slot " - .. "(_ScrollTrainerPicAfterBattle ends at hlcoord 14,0)") -end -eq(run.ballRowsSeen, 0, - "no pokeball row comes back with the pic (ClearSprites emptied that OAM; " - .. "_ScrollTrainerPicAfterBattle only rewrites tilemap columns)") -eq(run.battle.introBalls, nil, "the DrawAllPokeballs window stays closed") - --- The pic is on screen well before the LAST page: the plain act() this used to --- ride appended to the end of the queue, so the trainer flashed up one row --- before finish() popped the battle. -if money then - eq(run.foeShownAt[run.pages[money]], true, - "the trainer's pic is already back for the money line, not flashed up " - .. "one row before the battle pops (#282)") -end - --- and it really scrolls rather than popping into place: 64px off the right --- edge, then 2px a frame down to the resting 16 -eq(run.foeMax, 64, "the scroll-in starts 8 tiles off the right edge") -eq(run.foeSteps, 24, - "it takes 24 frames to walk in (six 4-frame columns, " - .. "scroll_draw_trainer_pic.asm:1-31)") - --- ------------------------------------------------------ ordering vs onFinish --- finish() pops the battle, so anything the overworld pushes afterwards is a --- second screen cut. Every trainer-victory row must be consumed before it. -check(run.resultAt ~= nil and run.resultAt >= run.frames, - "onFinish fires only once the whole sequence has drained") - --- ------------------------------------------------------------- \f pages --- Five EndBattleTexts carry a `para` (e.g. _Route9Youngster1EndBattleText). --- BattleState:startMessage only splits \n and \v, so an unsplit \f would --- render as a garbage glyph instead of starting a new page. -local para = fightAndWin("Oh well.\fI give up!") -check(indexOf(para.pages, "Oh well.") ~= nil, - "a \\f EndBattleText prints its first page") -check(indexOf(para.pages, "I give up!") ~= nil, - "a \\f EndBattleText prints its second page") -local p1, p2 = indexOf(para.pages, "Oh well."), indexOf(para.pages, "I give up!") -check(p1 and p2 and p2 == p1 + 1, "the two pages are consecutive rows") -for _, page in ipairs(para.pages) do - check(page:find("\f", 1, true) == nil, - "no page still carries a raw \\f: " .. (page:gsub("\n", " / "))) -end - --- --------------------------------------------------- scripted battles --- Commands.start_battle never sets endBattleText; those scripts print their --- own follow-up, so the sequence must simply skip the row. -local none = fightAndWin(nil) -eq(none.result, "win", "a battle with no EndBattleText still resolves") -local d2, m2 = indexOf(none.pages, "defeated"), indexOf(none.pages, "for winning") -check(d2 and m2 and d2 < m2, - "defeat text then money, with nothing between them") -eq(m2, #none.pages, "the prize money is still last") - -S.finish() diff --git a/tests/parity_wardens_house_bug535.lua b/tests/parity_wardens_house_bug535.lua deleted file mode 100644 index 7ab69ee5..00000000 --- a/tests/parity_wardens_house_bug535.lua +++ /dev/null @@ -1,127 +0,0 @@ --- Regression (#535): after handing over the GOLD TEETH and receiving --- HM04, every later talk to the Warden must still say something. --- --- data/scripts/story.lua's TEXT_WARDENSHOUSE_WARDEN pointed the --- EVENT_GOT_HM04 branch (row 3, jump_if_true) at the same silent-end jump --- the give-then-thank fallthrough uses (row 13), so ScriptRunner's pc ran --- straight past the end of the row list with zero show_text calls -- the --- Warden went mute on every visit after the trade. pokered's .got_item --- branch (scripts/WardensHouse.asm) instead prints .HM04ExplanationText --- (text/WardensHouse.asm: "HM04 teaches STRENGTH ... SECRET HOUSE in --- SAFARI ZONE") on every subsequent talk. -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 wardens house (#535)") -local check, eq = S.check, S.eq - -local Commands = require("src.script.Commands") -local Flags = require("src.script.Flags") -local Game = require("src.core.Game") -local Input = require("src.core.Input") -local SaveData = require("src.core.SaveData") -local ScriptRunner = require("src.script.ScriptRunner") -local StateStack = require("src.core.StateStack") - -Game.data = Data -Game.input = Input; Input:init() -Game.stack = StateStack; StateStack:init() -require("src.render.Font").load(Data) - -local story = require("data.scripts.story") -local script = story.WARDENS_HOUSE.talk.TEXT_WARDENSHOUSE_WARDEN - --- instrument show_text the way parity_gift_atomicity.lua does, to record --- exactly which text ids actually printed -local shown = {} --- forward EVERY argument: the 4th is extraOpts, which is how Commands.ask --- hands down its `choice` callback. A wrapper that stops at `subs` silently --- turns every ask in the script back into a plain show_text -- no YES/NO box, --- and ctx.lastCheck left holding whatever the previous check_* put there. -local origShow = Commands.show_text -Commands.show_text = function(ctx, textId, ...) - shown[#shown + 1] = textId - return origShow(ctx, textId, ...) -end - --- `button` drives the whole conversation: both A and B page a text box, and --- on the YES/NO box A takes the cursor's default (YES) while B snaps to NO --- and answers false (ChoiceBox:update, .choseSecondMenuItem). So holding A --- runs the yes branch and holding B runs the no branch, with no reaching --- into the choice box from the test. -local function runScript(button) - shown = {} - StateStack:init() - local ow = { map = { id = "WARDENS_HOUSE", def = { label = "WardensHouse" } }, - npcs = {}, entities = {} } - local r = ScriptRunner.new(Game, ow) - r:run(script, { npc = { def = {}, facePlayer = function() end }, - overworld = ow }) - local guard = 0 - while r:isRunning() and guard < 3000 do - guard = guard + 1 - Input.pressed = { [button or "a"] = true } - StateStack:update(1 / 60) - r:update() - end - Input.pressed = {} - return not r:isRunning() -end - --- === 1) first talk, holding the GOLD TEETH: gives HM04, sets the flag === -Game.save = SaveData.newGame() -Game.save.inventory.GOLD_TEETH = 1 -check(runScript(), "give-the-teeth talk completes") -eq(table.concat(shown, ","), - "_WardensHouseWardenGaveTheGoldTeethText,_WardensHouseWardenThanksText," - .. "_WardensHouseWardenReceivedHM04Text", - "handing over the teeth shows the give/thanks/received sequence, nothing after") -check(Flags.get(Game.save, "EVENT_GOT_HM04"), "EVENT_GOT_HM04 is set") -check(Flags.get(Game.save, "EVENT_GAVE_GOLD_TEETH"), "EVENT_GAVE_GOLD_TEETH is set") -check(Game.save.inventory.HM_STRENGTH ~= nil, "HM04 (Strength) lands in the bag") -check(not Game.save.inventory.GOLD_TEETH, "the GOLD TEETH is taken") - --- === 2) the regression itself: every later talk, once EVENT_GOT_HM04 is --- set, must print the explanation text instead of nothing === -check(runScript(), "post-gift talk completes") -eq(table.concat(shown, ","), "_WardensHouseWardenHM04ExplanationText", - "every subsequent talk now prints the HM04/Safari Zone explanation (#535)") - --- run it again to confirm this is not a one-shot: it repeats every visit -check(runScript(), "a third talk completes") -eq(table.concat(shown, ","), "_WardensHouseWardenHM04ExplanationText", - "the explanation text repeats on every later talk, not just the first") - --- === 3) no GOLD TEETH yet: the gibberish question, then a YES/NO, then the --- warden's answer -- Gibberish2 on yes, Gibberish3 on no (#645). --- The port used to stop dead after the question. === -Game.save = SaveData.newGame() -check(runScript("a"), "empty-handed talk completes on yes") -eq(table.concat(shown, ","), - "_WardensHouseWardenGibberish1Text,_WardensHouseWardenGibberish2Text", - "answering YES gets the warden's reply, not silence (#645)") -check(not Flags.get(Game.save, "EVENT_GOT_HM04"), "no HM04 yet") - -Game.save = SaveData.newGame() -check(runScript("b"), "empty-handed talk completes on no") -eq(table.concat(shown, ","), - "_WardensHouseWardenGibberish1Text,_WardensHouseWardenGibberish3Text", - "and answering NO gets the other reply (#645)") - --- the question is asked, not just printed: `ask` is what puts the YES/NO box --- up, so a future edit that downgrades it back to show_text fails here -local askRow -for _, row in ipairs(script) do - if row[2] == "_WardensHouseWardenGibberish1Text" then askRow = row[1] end -end -eq(askRow, "ask", "the gibberish line is asked with a YES/NO, not just shown") - --- neither answer touches the teeth trade -check(not Flags.get(Game.save, "EVENT_GAVE_GOLD_TEETH"), - "and neither answer hands over teeth the player does not have") - -Commands.show_text = origShow - -S.finish() diff --git a/tests/parity_yellow_old_man.lua b/tests/parity_yellow_old_man.lua deleted file mode 100644 index 60ce0319..00000000 --- a/tests/parity_yellow_old_man.lua +++ /dev/null @@ -1,290 +0,0 @@ --- Parity (#617): Yellow's Viridian old man is the OLD_MAN2 at (18,9), --- not the Red/Blue OLD_MAN at (17,5), and his dialog has no yes/no --- choice -- the apology speech runs the RATTATA demo battle straight --- away, the post-battle line is the losing-my-touch text, and he walks --- off and hides. --- --- Oracle: pokeyellow scripts/OaksLab.asm (OaksLabOakGivesPokedexScript: --- HideObject TOGGLE_LYING_OLD_MAN / ShowObject TOGGLE_OLD_MAN_2), --- scripts/ViridianCity.asm (ViridianCityCheckWaitingOldMan, --- ViridianCityOldMan2Text, ...InitialCatchTrainingScript, --- ...PostInitialCatchTraining) and scripts/ViridianCity_2.asm --- (ViridianCityPrintOldManText). The Red/Blue "Are you in a hurry?" --- script was running against Yellow's text: YES printed the TimeIsMoney --- alias (_ViridianCityOldManLosingMyTouchText) and NO ran the demo -- --- every talk, forever. --- --- Self-contained: `luajit tests/parity_yellow_old_man.lua`; also globbed --- by tests/run_tests.lua. -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 GameVersion = require("src.core.GameVersion") -local SaveData = require("src.core.SaveData") -local ScriptRunner = require("src.script.ScriptRunner") -local TextBox = require("src.render.TextBox") -local BattleState = require("src.battle.BattleState") -local Pokemon = require("src.pokemon.Pokemon") - -local S = require("tests.harness").suite("parity Yellow old man (#617)") -local check, eq = S.check, S.eq - -local oldVersion = GameVersion.get() - -local MAP = "VIRIDIAN_CITY" -local SLEEPER = "VIRIDIANCITY_OLD_MAN_SLEEPY" -local WALKER = "VIRIDIANCITY_OLD_MAN" -local OLD_MAN2 = "VIRIDIANCITY_OLD_MAN2" -local DONE_FLAG = "EVENT_COMPLETED_CATCH_TRAINING" - --- The Yellow wiring must be attached before anything else caches the --- map-script registry: data.scripts.init branches on GameVersion at --- load, so flip it first (this file owns its own process when run --- standalone). Under tests/run_tests.lua the registry is already --- cached with the Red wiring, so attach the Yellow modules directly --- afterwards -- attachBase merges per TEXT constant and replaces hooks, --- which is a no-op on a fresh process and the fix on a shared one. -GameVersion.set("yellow") -local mapScripts = require("data.scripts.init") -local MapScripts = require("src.script.MapScripts") -MapScripts.attachBase(MAP, - require("data.scripts.yellow_viridian_old_man").VIRIDIAN_CITY) -MapScripts.attachBase("OAKS_LAB", - require("data.scripts.oaks_lab_yellow")) -local oldManMod = require("data.scripts.yellow_viridian_old_man") - --- ------------------------------------------------------- the demo species --- The catch demo is a RATTATA in Yellow (SetupBattle sets wCurOpponent --- = RATTATA) but the Yellow manifest inherited Red's WEEDLE; the runtime --- override in Data:applyVersionedFieldData repairs old caches. Kept --- active until the end of this file so the demo-battle assertions below --- run against the Yellow value; restored before S.finish() like --- parity_yellow_trades does for its trades table. -local originalOldManBattle = Data.field.oldManBattle - or { species = "WEEDLE", level = 5 } -- the fixture carries no oldManBattle -local originalTrades = Data.field.trades -eq(originalOldManBattle.species, "WEEDLE", - "Red/Blue's old man still demos a Weedle") -GameVersion.set("yellow") -Data:applyVersionedFieldData() -eq(Data.field.oldManBattle.species, "RATTATA", - "Yellow's old man demos a Rattata (#617)") - -local manifestFile = assert(io.open("tools/rom_manifest_yellow.json", "r")) -local yellowManifest = manifestFile:read("*a") -manifestFile:close() -check(yellowManifest:find('"species": "RATTATA"', 1, true) ~= nil, - "the Yellow manifest stamps RATTATA for fresh imports") -local redManifestFile = assert(io.open("tools/rom_manifest.json", "r")) -local redManifest = redManifestFile:read("*a") -redManifestFile:close() -check(redManifest:find('"species": "WEEDLE"', 1, true) ~= nil, - "and the Red/Blue manifest keeps WEEDLE") - --- ------------------------------------------------------- the Pokedex swap --- OaksLabOakGivesPokedexScript shows TOGGLE_OLD_MAN_2 (the tutorial old --- man standing on the sleeper's cell), never the Red/Blue walker -local oaksRows = mapScripts.talkScript("OAKS_LAB", "TEXT_OAKSLAB_OAK1") -check(type(oaksRows) == "table", - "the Yellow OaksLab Oak talk resolves to rows") -local sawSleepHide, sawOldMan2Show, sawOldManShow = false, false, false -for _, row in ipairs(oaksRows or {}) do - if row[1] == "hide_object" and row[3] == SLEEPER then sawSleepHide = true end - if row[1] == "show_object" and row[3] == OLD_MAN2 then sawOldMan2Show = true end - if row[1] == "show_object" and row[3] == WALKER then sawOldManShow = true end -end -check(sawSleepHide, "the Pokédex hand-over hides the lying old man") -check(sawOldMan2Show, "it shows OLD_MAN2 on the sleeper's cell") -check(not sawOldManShow, "it never shows the Red/Blue OLD_MAN (#617)") - --- both Yellow gamblers default hidden (toggle OFF), like pokeyellow --- data/maps/toggleable_objects.asm. OLD_MAN2 only exists in a Yellow --- import -- a Red-imported checkout carries just OLD_MAN -- so the --- dataset checks tolerate its absence and the Yellow manifest carries --- the OLD_MAN2 default instead. -local walkerDef, oldMan2Def -if Data.maps[MAP] then - for _, o in ipairs(Data.maps[MAP].objects or {}) do - if o.name == WALKER then walkerDef = o end - if o.name == OLD_MAN2 then oldMan2Def = o end - end -end -check(walkerDef == nil or walkerDef.hidden == true, - "VIRIDIANCITY_OLD_MAN defaults hidden in Yellow") -check(oldMan2Def == nil or oldMan2Def.hidden == true, - "VIRIDIANCITY_OLD_MAN2 defaults hidden in Yellow") -local om2Name = yellowManifest:find('"name": "VIRIDIANCITY_OLD_MAN2"', 1, true) -local om2Hidden = om2Name and yellowManifest:sub( - math.max(1, om2Name - 40), om2Name):find('"hidden": true', 1, true) -check(om2Hidden ~= nil, - "the Yellow manifest ships OLD_MAN2 with the toggle OFF") - --- ------------------------------------------------------- script registry -local talk = mapScripts.talkScript(MAP, "TEXT_VIRIDIANCITY_OLD_MAN2") -check(type(talk) == "function", - "TEXT_VIRIDIANCITY_OLD_MAN2 resolves to the Yellow handler") -check(type(mapScripts.talkScript(MAP, "TEXT_VIRIDIANCITY_OLD_MAN")) == "table", - "the Red/Blue OLD_MAN talk is still registered (unreachable in Yellow)") -local hooks = mapScripts.get(MAP) -check(hooks and type(hooks.onEnter) == "function", - "VIRIDIAN_CITY.onEnter is the Yellow swap") -check(hooks and type(hooks.onStep) == "function", - "VIRIDIAN_CITY.onStep chains the gym lock and sleeper gate") -check(oldManMod.VIRIDIAN_CITY and oldManMod.VIRIDIAN_CITY.talk - and oldManMod.VIRIDIAN_CITY.talk.TEXT_VIRIDIANCITY_OLD_MAN2 == talk, - "the handler is the module's own, not a leftover merge") - --- ------------------------------------------------------- completed branch -do - local pushed = {} - local game = { - data = Data, - save = SaveData.newGame(), - stack = { push = function(_, s) pushed[#pushed + 1] = s end }, - } - game.save.flags.EVENT_COMPLETED_CATCH_TRAINING = true - local done = false - talk(game, nil, {}, function() done = true end) - eq(#pushed, 1, "a second talk only prints one box") - eq(getmetatable(pushed[1]), TextBox, "the losing-my-touch line, in a box") - pushed[1].onDone() - check(done, "closing it hands input back") -end - --- ------------------------------- the initial tutorial, end to end --- Needs real species in the dataset (the fixture carries only FIX_*); --- the engine's old-man demo machinery itself is parity_J's territory. -if Data.pokemon.RATTATA and Data.pokemon.PIKACHU then -do - require("src.render.Font").load(Data) - local pushed = {} - local save = SaveData.newGame() - save.party = { Pokemon.new(Data, "PIKACHU", 12) } - local moves = {} - local man = { def = { index = 8, name = OLD_MAN2 } } - local ow = { - map = { id = MAP, def = { label = "ViridianCity" } }, - npcs = { man }, entities = { man }, - player = { cellX = 19, cellY = 9, facing = "left" }, - scriptMove = function(_, _, dir, _, cb) moves[#moves + 1] = dir; cb() end, - npcByIndex = function(_, i) if i == 8 then return man end end, - } - local game = { - data = Data, - save = save, - stack = { push = function(_, s) pushed[#pushed + 1] = s end }, - } - local runner = ScriptRunner.new(game, ow) - ow.runner = runner - local done = false - talk(game, ow, man, function() done = true end) - - eq(#pushed, 1, "the initial talk opens the apology speech") - eq(getmetatable(pushed[1]), TextBox, "in a text box") - pushed[1].onDone() -- A: the apology closes, the demo battle starts - - eq(#pushed, 2, "the demo battle starts with no choice in between") - local battle = pushed[2] - check(battle and battle.demo, "it is the old-man demo battle") - eq(battle and battle.enemy and battle.enemy.mon.species, "RATTATA", - "the demo is a RATTATA in Yellow (#617)") - check(battle and battle.demoFails, - "the initial training throw breaks out, never catches (#636)") - eq(save.flags[DONE_FLAG], nil, "the flag is still clear mid-demo") - battle.onFinish() -- the battle ends, the post-battle text prints - - eq(save.flags[DONE_FLAG], true, "EVENT_COMPLETED_CATCH_TRAINING is set") - eq(#pushed, 3, "the losing-my-touch line follows the demo") - pushed[3].onDone() -- A: the old man walks off - - eq(#moves, 6, "with the player on (19,9) he walks down 6 tiles") - check(moves[1] == "down" and moves[6] == "down", - "all six steps are the ViridianCityOldManMovementData2 walk") - eq(save.objectToggles[MAP] and save.objectToggles[MAP][OLD_MAN2], false, - "TOGGLE_OLD_MAN_2 hides once the walk finishes") - check(done, "and the talk hands input back") -end - --- ---------------------------------- side talk: player not on (19,9) cell -do - local pushed = {} - local save = SaveData.newGame() - save.party = { Pokemon.new(Data, "PIKACHU", 12) } - local moves = {} - local man = { def = { index = 8, name = OLD_MAN2 } } - local pika = { def = { index = 99, name = "PIKACHU_FOLLOWER" }, - pikachuFollower = true } - local ow = { - map = { id = MAP, def = { label = "ViridianCity" } }, - npcs = { man, pika }, entities = { man, pika }, - player = { cellX = 18, cellY = 8, facing = "down" }, - scriptMove = function(_, _, dir, _, cb) moves[#moves + 1] = dir; cb() end, - npcByIndex = function(_, i) if i == 8 then return man elseif i == 99 then return pika end end, - } - local game = { - data = Data, - save = save, - stack = { push = function(_, s) pushed[#pushed + 1] = s end }, - } - local runner = ScriptRunner.new(game, ow) - ow.runner = runner - talk(game, ow, man, function() end) - pushed[1].onDone() - pushed[2].onFinish() - pushed[3].onDone() - eq(moves[1], "right", "Pikachu steps aside first (ViridianCityMovePikachu)") - eq(moves[2], "right", "then the old man turns right one tile") - eq(#moves, 2, "and no more") -end -else - check(true, "fixture dataset: demo-battle flow skipped (no RATTATA)") -end - --- --------------------------------------------------------- the (19,9) step -do - local pushed = {} - local save = SaveData.newGame() - local man = { def = { index = 8, name = OLD_MAN2 } } - local ow = { - map = { id = MAP, def = { label = "ViridianCity" } }, - npcs = { man }, entities = { man }, - player = { cellX = 19, cellY = 9, facing = "down" }, - scriptMove = function(_, _, _, _, cb) cb() end, - npcByIndex = function() end, - } - local game = { - data = Data, - save = save, - stack = { push = function(_, s) pushed[#pushed + 1] = s end }, - } - local runner = ScriptRunner.new(game, ow) - ow.runner = runner - - check(not hooks.onStep(game, ow, 5, 5), - "off the trigger cell the step passes through") - check(hooks.onStep(game, ow, 19, 9), - "pre-Pokedex the sleeper gate owns (19,9)") - eq(#pushed, 1, "with the sleepy text box") - check(save.flags[DONE_FLAG] ~= true, "the tutorial is not running") - - save.flags.EVENT_GOT_POKEDEX = true - check(hooks.onStep(game, ow, 19, 9), - "with the Pokedex, (19,9) starts the tutorial") - eq(man.facing, "right", "the old man faces the player") - eq(ow.player.facing, "left", "and the player turns to face him") - eq(#pushed, 2, "the apology box is up") - check(save.flags[DONE_FLAG] ~= true, - "no flag until the demo battle actually runs") - - save.flags.EVENT_COMPLETED_CATCH_TRAINING = true - check(not hooks.onStep(game, ow, 19, 9), - "once the tutorial is done the cell is quiet again") -end - -Data.field.trades = originalTrades -Data.field.oldManBattle = originalOldManBattle -GameVersion.set(oldVersion) - -S.finish()