more buggies CLOSES #960, CLOSES #961, CLOSES #968, CLOSES #995, CLOSES #1006, CLOSES #1009, CLOSES #1013, CLOSES #1021, CLOSES #1031, CLOSES #1044, CLOSES #1049, CLOSES #1050, CLOSES #1045,,

This commit is contained in:
bryanthaboi
2026-08-10 17:06:24 -04:00
parent 7fa758602a
commit c3855cebec
20 changed files with 1837 additions and 70 deletions
@@ -248,7 +248,7 @@ return function(game)
-- ---- case 2: B-cancel settles on the OLD form's colours -----------------
local karp = startEvo("MAGIKARP", "GYARADOS")
U.wait(24)
U.wait(100) -- past the 80-frame pre-animLoop delay before the poll starts
U.hold(game, "b", 20) -- evolution.asm Evolution_CheckForCancel
if not waitFor(function() return findText("stopped evolving") ~= nil end, 240) then
check(false, "holding B prints \"stopped evolving\"")
@@ -1,13 +1,14 @@
-- Driver: cancel an evolution with the B button (#213).
--
-- pokered engine/pokemon/evos_moves.asm polls hJoyHeld during the pic
-- flash: holding B aborts the evolution (the mon keeps its species and
-- pokered engine/movie/evolution.asm polls hJoy5 during the pic flash: a
-- fresh B press aborts the evolution (the mon keeps its species and
-- _StoppedEvolvingText prints). Trade evolutions (wLinkState ==
-- LINK_STATE_TRADING) skip that poll and cannot be cancelled.
--
-- Case 1 (level path, cancelable): open EvolutionState directly, wait a
-- few frames into the flash (t well under FLASH_FRAMES=220), hold B, and
-- assert the mon stays CATERPIE with "stopped evolving" text on screen.
-- Case 1 (level path, cancelable): open EvolutionState directly, wait past
-- the 80-frame pre-animLoop delay (still well under FLASH_FRAMES=220),
-- press B, and assert the mon stays CATERPIE with the "stopped evolving"
-- text on screen.
-- Case 1b: after cancel, checkParty with no level-ups must not re-offer;
-- a subsequent level-up set must offer again (EvolveAfterBattle parity).
-- Case 2 (control): let the flash run to completion with no input and
@@ -80,11 +81,11 @@ return function(game)
Evolution.evolve(game, mon, "METAPOD", function() done1 = true end)
if not waitFor(evoTop, 300) then error("EvolutionState never opened (case1)") end
U.wait(20) -- into the flash, well under FLASH_FRAMES=220
U.wait(100) -- past the 80-frame pre-animLoop delay, still under 220
U.log("case1 flash", "t=", top().t, "species=", mon.species)
U.shot(game, DIR .. "/evo213_1_evolving.png")
U.hold(game, "b", 20) -- Gen1 hJoyHeld B-cancel
U.hold(game, "b", 20) -- Gen1 hJoy5 B-cancel
-- the flash aborts: EvolutionState is no longer the top (the stopped
-- text overlays it and then pops it)
@@ -118,7 +119,7 @@ return function(game)
if not waitFor(evoTop, 300) then
error("EvolutionState never opened after level-up re-offer")
end
U.wait(20)
U.wait(100) -- past the same 80-frame pre-animLoop delay
U.hold(game, "b", 20) -- cancel so case 2 stays independent
if not waitFor(function() return not evoTop() end, 240) then
error("level-up re-offer did not abort on B")
@@ -0,0 +1,484 @@
-- Ear check for the PC, bump, door/stairs and battle menu SFX (#960, #961, #1044, #1045).
-- POKEPORT_DRIVER=tests/drivers/menu_sfx_bug960_bug961_bug1044_bug1045_test.lua POKEPORT_IDENTITY=sfx960 POKEPORT_TOUCH=0 POKEPORT_VERSION=red SHOT_DIR=/tmp/shots love .
return function(game)
local U = dofile("tests/drivers/util.lua")
local DIR = os.getenv("SHOT_DIR") or "/tmp/shots"
local BattleState = require("src.battle.BattleState")
local Boxes = require("src.pokemon.Boxes")
local Menu = require("src.ui.Menu")
local Pokemon = require("src.pokemon.Pokemon")
local Sound = require("src.core.Sound")
local Strings = require("src.core.Strings")
local Timing = require("src.core.Timing")
local FADE = Timing.WARP_FADE_OUT
local pass, fail = 0, 0
local function check(label, ok)
if ok then pass = pass + 1 else fail = fail + 1 end
U.log(ok and "PASS" or "FAIL", label)
return ok
end
-- every cue this run makes, forwarded so playback is untouched; the frame
local cues = {}
local realPlay = Sound.play
Sound.play = function(data, name)
cues[#cues + 1] = { name = name, frame = U.frame() }
return realPlay(data, name)
end
local function since(mark)
local names = {}
for i = mark + 1, #cues do names[#names + 1] = cues[i].name end
return #names > 0 and table.concat(names, ", ") or "nothing"
end
local function heard(mark, want)
for i = mark + 1, #cues do
if cues[i].name == want then return cues[i] end
end
return nil
end
local function countOf(mark, want)
local n = 0
for i = mark + 1, #cues do if cues[i].name == want then n = n + 1 end end
return n
end
-- ---- what the ear cannot check -----------------------------------------
local opts = game.save.options or {}
local sfxVol, musicVol = opts.sfxVol or 0, opts.musicVol or 0
if sfxVol == 0 then
U.log("FAIL SFX volume is 0. Every line below is about a sound that is or")
U.log(" is not there, and at 0 none of them are. Set SFX to 7 in OPTION")
U.log(" and start over, or this run proves nothing at all.")
end
check(("sfx volume %d/7, music volume %d/7"):format(sfxVol, musicVol),
sfxVol > 0)
-- an unresolved key is silent in exactly the way these bugs were
local sfx = (game.data.audio or {}).sfx or {}
for _, key in ipairs({ "Turn_On_PC", "Turn_Off_PC", "Enter_PC", "Collision",
"Save", "Go_Inside", "Go_Outside", "Press_AB" }) do
check("sfx " .. key .. " is in the generated audio", sfx[key] ~= nil)
end
-- positions come from data/events/hidden_events.asm and data/maps/objects/*.asm
local extras = game.data.field.hiddenExtras or {}
local pcTiles = extras.pcTiles or {}
local bedroomPC = (pcTiles.REDS_HOUSE_2F or {})[1]
local centerPC = (pcTiles.VIRIDIAN_POKECENTER or {})[1]
check("REDS_HOUSE_2F carries the OpenRedsPC tile", bedroomPC ~= nil)
check("VIRIDIAN_POKECENTER carries a PC tile", centerPC ~= nil)
local function warpTo(mapId, destMap)
for _, w in ipairs((game.data.maps[mapId] or {}).warps or {}) do
if w.destMap == destMap then return w end
end
return nil
end
local houseDoor = warpTo("PALLET_TOWN", "REDS_HOUSE_1F")
local houseExit = warpTo("REDS_HOUSE_1F", "LAST_MAP")
local houseStairs = warpTo("REDS_HOUSE_1F", "REDS_HOUSE_2F")
local centerDoor = warpTo("VIRIDIAN_CITY", "VIRIDIAN_POKECENTER")
check("PALLET_TOWN has the door into REDS_HOUSE_1F", houseDoor ~= nil)
check("REDS_HOUSE_1F has an exit mat and the stairs up",
houseExit ~= nil and houseStairs ~= nil)
check("VIRIDIAN_CITY has the door into its POKéMON CENTER", centerDoor ~= nil)
-- ---- helpers ------------------------------------------------------------
local DIRS = { up = { 0, -1 }, down = { 0, 1 },
left = { -1, 0 }, right = { 1, 0 } }
local ORDER = { "down", "up", "left", "right" }
local function pressStep(dir)
table.insert(game.input.pressQueue, dir)
game.input.state[dir] = true
U.wait(1)
end
local function stand(mapId, x, y, facing)
U.teleport(game, mapId, x, y, facing)
U.wait(15)
return game.overworld
end
-- the cell you stand on to face (cx, cy) from `dir`, i.e. one step back
local function cellBehind(cx, cy, dir)
local d = DIRS[dir]
return cx - d[1], cy - d[2]
end
-- first walkable free neighbour of (cx, cy), plus the facing that looks at
local function approach(ow, cx, cy)
for _, dir in ipairs(ORDER) do
local sx, sy = cellBehind(cx, cy, dir)
if ow.map:isWalkableCell(sx, sy) and not ow:npcAtCell(sx, sy) then
return sx, sy, dir
end
end
end
-- any free walkable cell on the map with a solid one next to it, so the
local function findWall(ow)
local map = ow.map
for cy = 0, map.heightCells - 1 do
for cx = 0, map.widthCells - 1 do
if map:isWalkableCell(cx, cy) and not map:warpAtCell(cx, cy)
and not ow:npcAtCell(cx, cy) then
for _, d in ipairs(ORDER) do
local dd = DIRS[d]
if not map:isWalkableCell(cx + dd[1], cy + dd[2]) then
return cx, cy, d
end
end
end
end
end
end
local function npcNamed(ow, name)
for _, n in ipairs(ow.npcs or {}) do
if n.def and n.def.name == name then return n end
end
end
-- PlayMapChangeSound (home/overworld.asm:690) plays before GBFadeOutToBlack,
local function takeDoor(dir, want, label)
local mark = #cues
local from = game.overworld.map.id
local cue, switched
for _ = 1, 300 do
if cue or switched then
game.input.state[dir] = false
U.wait(1)
else
pressStep(dir)
end
cue = cue or heard(mark, want)
local ow = game.overworld
if not switched and ow and ow.map.id ~= from then switched = U.frame() end
if cue and switched then break end
end
game.input.state[dir] = false
U.wait(50) -- PlayerStepOutFromDoor walks off the mat before anything else
if not (cue and switched) then
check(("%s: %s played and the map changed"):format(label, want), false)
U.log(" cues on the way through:", since(mark))
return
end
check(("%s: %s fired %d frames before the map switched (the fade is %d)")
:format(label, want, switched - cue.frame, FADE),
switched - cue.frame >= FADE - 8)
end
-- CollisionCheckOnLand (home/overworld.asm): a sprite in the way takes the
local function bumpInto(dir, label)
local mark = #cues
for _ = 1, 10 do pressStep(dir) end
game.input.state[dir] = false
U.wait(24) -- past the 16-frame bumpCooldown, so the next bump is its own
check(label .. " rings Collision", heard(mark, "Collision") ~= nil)
end
local function topIs(class)
local top = game.stack:top()
return top ~= nil and getmetatable(top) == class
end
local function rowIndex(menu, label)
for i, item in ipairs(menu.items or {}) do
if item.label == label then return i end
end
end
-- rows come and go with save state and the ui.pc.items hook, so pick each
local function choose(menu, label)
local i = rowIndex(menu, label)
if not i then
check("the menu has a " .. label .. " row", false)
return false
end
menu.index = i
menu:clampScroll()
U.wait(2)
U.tap(game, "a")
U.wait(26)
return true
end
local function mash(cond, tries)
for _ = 1, tries or 40 do
if cond() then return true end
U.tap(game, "a")
U.wait(6)
end
return cond()
end
game.save.party = { Pokemon.new(game.data, "BULBASAUR", 20) }
game.save.party[1].moves = {
{ id = "TACKLE", pp = 35, maxPP = 35 },
{ id = "VINE_WHIP", pp = 10, maxPP = 10 },
}
Boxes.ensure(game.save)
game.save.currentBox = 1
-- ---- the door into the house, from outside (#961) -----------------------
if houseDoor then
local sx, sy = cellBehind(houseDoor.x, houseDoor.y, "up")
stand("PALLET_TOWN", sx, sy, "up")
takeDoor("up", "Go_Inside", "walking into RED's house")
end
-- walking into MOM, and into a wall (#960); teleport is only a fallback
local ow = game.overworld
if ow.map.id ~= "REDS_HOUSE_1F" and houseExit then
ow = stand("REDS_HOUSE_1F", houseExit.x, houseExit.y, "up")
end
local mom = npcNamed(ow, "REDSHOUSE1F_MOM")
check("MOM is loaded on REDS_HOUSE_1F", mom ~= nil)
if mom then
local sx, sy, dir = approach(ow, mom.cellX, mom.cellY)
if sx then
ow = stand("REDS_HOUSE_1F", sx, sy, dir)
-- the teleport rebuilt the npc list, so pin her on the state we bump
local pinned = npcNamed(ow, "REDSHOUSE1F_MOM")
if pinned then pinned.frozen = true end
U.shot(game, DIR .. "/bug960_mom.png")
bumpInto(dir, "walking into MOM")
else
check("MOM has a free cell to be walked into from", false)
end
-- the control: a wall bump was audible before #960 too, so silence here
local wx, wy, wdir = findWall(game.overworld)
if wdir then
ow = stand("REDS_HOUSE_1F", wx, wy, wdir)
bumpInto(wdir, "walking into the wall to the " .. wdir)
else
check("REDS_HOUSE_1F has a wall to bump into", false)
end
end
-- ---- the stairs up (#961) -----------------------------------------------
if houseStairs then
local sx, sy = cellBehind(houseStairs.x, houseStairs.y, "up")
stand("REDS_HOUSE_1F", sx, sy, "up")
-- the destination decides the cue here, not the tile underfoot, so an
takeDoor("up", "Go_Inside", "taking the stairs up")
end
-- ---- the bedroom PC (#960) ---------------------------------------------
if bedroomPC then
-- OpenRedsPC's hidden_event is gated on SPRITE_FACING_UP, so the cell
local sx, sy = cellBehind(bedroomPC.x, bedroomPC.y, "up")
ow = stand("REDS_HOUSE_2F", sx, sy, "up")
check("the cell below the bedroom PC is walkable",
ow.map:isWalkableCell(sx, sy))
local mark = #cues
U.tap(game, "a")
U.wait(26)
check("A on the bedroom PC opens a menu", topIs(Menu))
check("...and turns it on with Turn_On_PC", heard(mark, "Turn_On_PC") ~= nil)
U.shot(game, DIR .. "/bug960_bedroom_pc.png")
if topIs(Menu) then
mark = #cues
choose(game.stack:top(), Strings("LOG OFF"))
check("LOG OFF on the bedroom PC rings Turn_Off_PC (#960)",
heard(mark, "Turn_Off_PC") ~= nil)
U.log(" cues:", since(mark))
check("...and the PC closed", game.stack:top() == game.overworld)
end
-- B out of the same menu is ExitPlayerPC's other entry and rings it too
U.tap(game, "a")
U.wait(26)
if topIs(Menu) then
local mark2 = #cues
U.tap(game, "b")
U.wait(26)
check("backing out of the bedroom PC with B rings it as well",
heard(mark2, "Turn_Off_PC") ~= nil)
end
end
-- ---- back out of the house, to the street (#961) ------------------------
if houseExit then
local sx, sy = cellBehind(houseExit.x, houseExit.y, "down")
ow = stand("REDS_HOUSE_1F", sx, sy, "down")
if not ow.map:isWalkableCell(sx, sy) then
U.log(("(%d, %d) is blocked; using the other half of the mat")
:format(sx, sy))
ow = stand("REDS_HOUSE_1F", sx + 1, sy, "down")
end
takeDoor("down", "Go_Outside", "stepping out onto the street")
end
-- ---- into the POKéMON CENTER, for the PC main menu -----------------------
if centerDoor then
local sx, sy = cellBehind(centerDoor.x, centerDoor.y, "up")
stand("VIRIDIAN_CITY", sx, sy, "up")
takeDoor("up", "Go_Inside", "walking into the POKéMON CENTER")
end
-- ---- the PC main menu: Enter_PC, and the silence under it (#960) --------
if centerPC then
local sx, sy = cellBehind(centerPC.x, centerPC.y, "up")
ow = stand("VIRIDIAN_POKECENTER", sx, sy, "up")
for _, n in ipairs(ow.npcs or {}) do n.frozen = true end -- the GENTLEMAN walks
local mark = #cues
U.tap(game, "a")
U.wait(26)
check("A on the Center PC opens the PC main menu", topIs(Menu))
check("...with Turn_On_PC", heard(mark, "Turn_On_PC") ~= nil)
local mine = (game.save.player.name or "RED") .. "'s PC"
if topIs(Menu) then
mark = #cues
choose(game.stack:top(), mine)
check(mine .. " rings Enter_PC (#960)", heard(mark, "Enter_PC") ~= nil)
check("...and the item PC opened", topIs(Menu))
-- BIT_USING_GENERIC_PC: reached this way, ExitPlayerPC is silent and
mark = #cues
U.tap(game, "b")
U.wait(26)
check("backing out of it again is silent, as the ROM is",
heard(mark, "Turn_Off_PC") == nil)
U.log(" cues:", since(mark))
end
-- ---- CHANGE BOX (#1044) ----------------------------------------------
-- the box PC reads SOMEONE'S PC until EVENT_MET_BILL (pokemon_pc.asm)
local flags = game.save.flags or {}
local boxPC = (flags.EVENT_MET_BILL or flags.EVENT_GOT_SS_TICKET)
and "BILL'S PC" or Strings("SOMEONE'S PC")
if topIs(Menu) then
mark = #cues
choose(game.stack:top(), boxPC)
check(boxPC .. " rings Enter_PC too", heard(mark, "Enter_PC") ~= nil)
check("...and the box menu opened", topIs(Menu))
end
if topIs(Menu) and rowIndex(game.stack:top(), Strings("CHANGE BOX")) then
choose(game.stack:top(), Strings("CHANGE BOX"))
U.shot(game, DIR .. "/bug1044_change_box.png")
local before = game.save.currentBox
U.tap(game, "down") -- BOX 1 is the current one, so move off it
U.wait(8)
mark = #cues
-- A picks the box, then the "data will be saved" prompt and its YES
U.tap(game, "a")
U.wait(20)
mash(function() return game.save.currentBox ~= before end, 40)
U.wait(60) -- the 15-frame answer hold, then the write
check(("CHANGE BOX switched box %s -> %s")
:format(tostring(before), tostring(game.save.currentBox)),
game.save.currentBox ~= before)
check("...and rang the SAVE jingle (#1044)", heard(mark, "Save") ~= nil)
U.log(" cues:", since(mark))
end
-- ---- LOG OFF from the main menu --------------------------------------
for _ = 1, 6 do
if game.stack:top() == game.overworld then break end
U.tap(game, "b")
U.wait(20)
end
U.tap(game, "a")
U.wait(26)
if topIs(Menu) and rowIndex(game.stack:top(), Strings("LOG OFF")) then
local mark2 = #cues
choose(game.stack:top(), Strings("LOG OFF"))
check("LOG OFF on the PC main menu rings Turn_Off_PC",
heard(mark2, "Turn_Off_PC") ~= nil)
end
for _ = 1, 8 do
if game.stack:top() == game.overworld then break end
U.tap(game, "b")
U.wait(20)
end
end
-- the battle menu click (#1045) already landed in HEAD 12c2677; confirmation only
ow = stand("ROUTE_1", 5, 5, "down")
if not ow.map:isWalkableCell(5, 5) then
local fx, fy
for cy = 0, ow.map.heightCells - 1 do
for cx = 0, ow.map.widthCells - 1 do
if ow.map:isWalkableCell(cx, cy) then fx, fy = cx, cy break end
end
if fx then break end
end
if fx then ow = stand("ROUTE_1", fx, fy, "down") end
end
local battle = BattleState.newWild(game, "SNORLAX", 50)
battle.onFinish = function(result) ow:afterBattle(result, battle) end
-- SPLASH so the foe's turn can never end the run under the menu presses
battle.enemy.mon.moves = { { id = "SPLASH", pp = 40, maxPP = 40 } }
battle.enemy.curMoves = battle.enemy.mon.moves
ow:pushBattle(battle)
U.wait(220) -- the send-out intro runs before the menu is reachable
mash(function() return battle.phase == "menu" end, 60)
check("the wild SNORLAX battle reached its FIGHT menu",
battle.phase == "menu")
-- one press, then long enough for the click to be its own sound
local function click(btn, label, want)
local mark = #cues
U.tap(game, btn)
U.wait(28)
local n = countOf(mark, "Press_AB")
check(("%s: %s gave %d click%s, expected %d")
:format(label, btn:upper(), n, n == 1 and "" or "s", want),
n == want)
end
if battle.phase == "menu" then
click("a", "A on FIGHT (#1045a)", 1)
check("...and the move list opened", battle.phase == "moveSelect")
U.shot(game, DIR .. "/bug1045_move_list.png")
click("b", "B out of the move list (#1045c)", 1)
check("...and the FIGHT menu came back", battle.phase == "menu")
click("a", "A on FIGHT again", 1)
click("a", "A on a move (#1045b)", 1)
mash(function() return battle.phase == "menu" end, 120)
if battle.phase == "menu" then
battle.menuIndex = 4 -- RUN shares the FIGHT/PKMN/ITEM call site
U.wait(4)
click("a", "A on RUN", 1)
end
end
U.log(("machine checks: %d passed, %d failed"):format(pass, fail))
-- ---- over to you --------------------------------------------------------
if centerPC then
local sx, sy = cellBehind(centerPC.x, centerPC.y, "up")
ow = stand("VIRIDIAN_POKECENTER", sx, sy, "up")
for _, n in ipairs(ow.npcs or {}) do n.frozen = true end
end
U.log("Everything above has been pressed once already; you are parked at the")
U.log("Viridian POKéMON CENTER PC to do it again by ear.")
U.log("A opens the PC main menu. RED's PC clicks in on the two-note ENTER PC")
U.log("chirp and LOG OFF closes with the descending power-down. B out of RED's")
U.log("PC is silent on purpose; a power-down there is the near miss, not a pass.")
U.log("SOMEONE'S PC, CHANGE BOX, any other box, YES: the SAVE jingle rings")
U.log("after the box has changed, the same jingle the SAVE menu plays. A")
U.log("jingle before the switch, or none at all, is #1044 back.")
U.log("The bedroom PC upstairs at home is the other half of #960: it beeps on,")
U.log("and LOG OFF or B rings the power-down there. That one is not silent.")
U.log("The COOLTRAINER at (4,3) and the NURSE at (3,1) are pinned; walk into")
U.log("either and it thuds like a wall. Walking into a wall is the control --")
U.log("it always thudded, so a silent wall means the device, not the fix.")
U.log(("Walk out over the exit mat at the bottom of the room: the door sound")
.. (" starts as the screen begins to darken, %d frames ahead of")
:format(FADE))
U.log("Viridian City. One that lands on the new map, or after it, is #961.")
U.log("In any battle, A on FIGHT/PKMN/ITEM/RUN, A on a move and B out of the")
U.log("move list all click. Silence on any of the three is #1045.")
U.log("Shots: " .. DIR .. "/bug960_*.png, bug1044_*.png, bug1045_*.png")
-- keeps naming cues with their frame after the hand-off, so a door sound
local reported = #cues
while true do
if #cues > reported then
for i = reported + 1, #cues do
U.log("cue", cues[i].name, "frame", cues[i].frame)
end
reported = #cues
end
coroutine.yield()
end
end
@@ -0,0 +1,410 @@
-- Pikachu stays in its ball until the rival fight (#1009) and clears (4,3) for the rival (#1021); the Route 15 leg only instruments #920.
-- pokeyellow scripts/OaksLab.asm, OaksLab_2.asm.
return function(game)
local U = dofile("tests/drivers/util.lua")
local GameVersion = require("src.core.GameVersion")
local MapScripts = require("src.script.MapScripts")
local Commands = require("src.script.Commands")
local PF = require("src.world.PikachuFollower")
local Pokemon = require("src.pokemon.Pokemon")
local Bag = require("src.inventory.Bag")
local BattleState = require("src.battle.BattleState")
local SHOT_DIR = os.getenv("SHOT_DIR") or "/tmp/shots"
local LAB = "OAKS_LAB"
local RIVAL = 1 -- OAKSLAB_RIVAL, object index 1
local GIFT = { x = 5, y = 3 } -- where OaksLabRLE_PlayerWalksToOak ends
local function check(label, ok)
U.log(ok and "PASS" or "FAIL", label)
return ok
end
local function idle()
while true do coroutine.yield() end
end
local function ow() return game.overworld end
local function follower() return PF.current(game.overworld) end
local function ctx()
return { game = game, save = game.save, overworld = game.overworld }
end
local function where(npc)
if not npc then return "gone" end
return "(" .. npc.cellX .. "," .. npc.cellY .. ") facing "
.. tostring(npc.facing)
end
-- one player step; false when refused, so a caller can route around a map edit
local function stepOnce(dir)
local p = ow().player
local x0, y0 = p.cellX, p.cellY
for _ = 1, 60 do
table.insert(game.input.pressQueue, dir)
game.input.state[dir] = true
coroutine.yield()
p = ow().player
if p.cellX ~= x0 or p.cellY ~= y0 then break end
end
game.input.state[dir] = false
for _ = 1, 40 do
if not ow().player.moving then break end
U.wait(1)
end
U.wait(3)
p = ow().player
return p.cellX ~= x0 or p.cellY ~= y0
end
-- ---------------------------------------------------------------- checks
if not check("running the Yellow cache (POKEPORT_VERSION=yellow)",
GameVersion.isYellow()) then
U.log("Red and Blue have no follower and no Yellow lab script, so every")
U.log("line below would fail for the wrong reason.")
idle()
end
check("SPRITE_PIKACHU resolves in the sprite table",
game.data.sprites ~= nil and game.data.sprites.SPRITE_PIKACHU ~= nil)
check("PikachuFollower.oaksLabMakeWay exists",
type(PF.oaksLabMakeWay) == "function")
check("the pikachu_make_way verb exists",
type(Commands.pikachu_make_way) == "function")
check("and it blocks the runner while the walk plays",
Commands.meta.pikachu_make_way ~= nil
and Commands.meta.pikachu_make_way.blocking == true)
local lab = require("data.scripts.oaks_lab_yellow")
local oakRows = lab.talk.TEXT_OAKSLAB_OAK1
local function rowIndex(pred)
for i, r in ipairs(oakRows) do
if pred(r) then return i end
end
return nil
end
local iGramps = rowIndex(function(r)
return r[1] == "show_text" and r[2] == "_OaksLabRivalGrampsText"
end)
local iMakeWay = rowIndex(function(r) return r[1] == "pikachu_make_way" end)
local iShow = rowIndex(function(r)
return r[1] == "show_object" and r[3] == "OAKSLAB_RIVAL"
end)
check("Oak's parcel branch has a make-way row", iMakeWay ~= nil)
-- the callfar sits after the GRAMPS text and before ShowObject; a row on
check("it sits between the GRAMPS text and the rival's ShowObject",
iGramps ~= nil and iMakeWay ~= nil and iShow ~= nil
and iGramps < iMakeWay and iMakeWay < iShow)
local sfx = game.save.options and game.save.options.sfxVol
if sfx == 0 then
U.log("sfxVol is 0. Pikachu's cry as it bursts out of the ball will be")
U.log("silent and a mute run reads exactly like a missing cry -- turn")
U.log("the sound back up before judging the escape scene.")
end
-- leg 1: in the ball. Level 30 only so the lab battle ends fast
game.save.flags = game.save.flags or {}
local flags = game.save.flags
flags.EVENT_GOT_STARTER = true
flags.EVENT_CHOSE_PIKACHU = true
flags.EVENT_FOLLOWED_OAK_INTO_LAB = true
flags.EVENT_FOLLOWED_OAK_INTO_LAB_2 = true
flags.EVENT_BATTLED_RIVAL_IN_OAKS_LAB = nil
flags.EVENT_OAK_ASKED_TO_CHOOSE_MON = true
flags.EVENT_GOT_POKEDEX = nil
flags.EVENT_OAK_GOT_PARCEL = nil
game.save.party = { Pokemon.new(game.data, "PIKACHU", 30) }
game.save.player.name = "bryan"
game.save.onBike = false
game.save.pikachuInBall = true
U.teleport(game, LAB, GIFT.x, GIFT.y, "up")
U.wait(10)
Commands.show_object(ctx(), LAB, "OAKSLAB_OAK1")
U.wait(5)
-- asked after the teleport: the registry only fills on first require
check("the Yellow lab module is the one bound to the map",
MapScripts.talkScript(LAB, "TEXT_OAKSLAB_OAK1") == oakRows)
check("straight after the gift there is no follower on the map",
follower() == nil)
-- save compat: nil pikachuInBall falls back to the rival-fight flag, not false
game.save.pikachuInBall = nil
U.wait(10)
check("a pre-#1009 save before the rival fight still has none",
follower() == nil)
flags.EVENT_BATTLED_RIVAL_IN_OAKS_LAB = true
U.wait(20)
check("a pre-#1009 save past the rival fight keeps its follower",
follower() ~= nil)
flags.EVENT_BATTLED_RIVAL_IN_OAKS_LAB = nil
game.save.pikachuInBall = true
U.wait(20)
check("and the in-ball byte puts it away again", follower() == nil)
U.shot(game, SHOT_DIR .. "/bug1009_in_ball.png")
U.log("captured", SHOT_DIR .. "/bug1009_in_ball.png",
"- player alone at the gift spot")
-- --------------------------------------------- leg 2: out of the ball
Commands.show_object(ctx(), LAB, "OAKSLAB_RIVAL")
U.wait(5)
-- OaksLabRivalChallengesPlayerScript fires from y >= 6 with the starter held
local walkedClean = true
for _ = 1, 10 do
if ow().player.cellY >= 6 then break end
if follower() then walkedClean = false end
if not stepOnce("down") then
if not stepOnce("left") then break end
end
end
check("crossed the lab to the door row with no follower behind",
walkedClean and follower() == nil)
check("reached the row the rival challenges from", ow().player.cellY >= 6)
-- the escape only spawns the companion once the overworld is back on top
local sawBattle, escapedAt, spawnCell = false, nil, nil
for _ = 1, 2500 do
local o = game.overworld
local top = game.stack:top()
if top ~= o then
if getmetatable(top) == BattleState then sawBattle = true end
U.tap(game, "a")
U.wait(3)
else
local npc = follower()
if npc and not escapedAt then
escapedAt = U.frame()
spawnCell = { x = npc.cellX, y = npc.cellY, facing = npc.facing }
U.shot(game, SHOT_DIR .. "/bug1009_escaped.png")
end
if escapedAt and not o.runner:isRunning() and #o.scriptMoves == 0 then
break
end
U.wait(2)
end
end
check("the rival battle actually ran", sawBattle)
check("the escape scene put a follower on the map", escapedAt ~= nil)
check("EVENT_BATTLED_RIVAL_IN_OAKS_LAB is set",
flags.EVENT_BATTLED_RIVAL_IN_OAKS_LAB == true)
check("the ball is open (save.pikachuInBall false, not nil)",
game.save.pikachuInBall == false)
if spawnCell then
local p = ow().player
U.log("it appeared at", spawnCell.x, spawnCell.y, "with the player at",
p.cellX, p.cellY, "facing", p.facing)
-- OaksLabPikachuEscapesPokeballScript faces the player up and uses spawn
check("it burst out on the cell behind the player, not beside him",
spawnCell.x == p.cellX and spawnCell.y == p.cellY + 1)
U.log("captured", SHOT_DIR .. "/bug1009_escaped.png")
end
-- leg 3: Oak's .DeliverParcelText needs parcel held, no balls, no Pokedex
flags.EVENT_BATTLED_RIVAL_IN_OAKS_LAB = true
flags.EVENT_PALLET_AFTER_GETTING_POKEBALLS = nil
flags.EVENT_BEAT_ROUTE22_RIVAL_1ST_BATTLE = nil
flags.EVENT_GOT_POKEDEX = nil
game.save.pikachuInBall = false
game.save.inventory = game.save.inventory or {}
game.save.inventory.POKE_BALL = nil
if game.save.pokedex then game.save.pokedex.owned = {} end
Bag.add(game.save, "OAKS_PARCEL", 1, game.data)
-- the SPRITE_FACING_LEFT case of TryApplyPikachuMovementData
U.teleport(game, LAB, 4, 3, "right")
U.wait(10)
Commands.show_object(ctx(), LAB, "OAKSLAB_OAK1")
-- he walked out after the lab battle; ShowObject brings him back mid-scene
Commands.hide_object(ctx(), LAB, "OAKSLAB_RIVAL")
U.wait(5)
check("the follower is back for the parcel scene", follower() ~= nil)
stepOnce("right")
-- Oak stands on (5,2) and blocks, so this only turns the player up
U.hold(game, "up", 10)
U.wait(10)
local p = ow().player
local pika = follower()
U.log("player at", p.cellX, p.cellY, "facing", p.facing,
"| Pikachu at", where(pika))
check("the player is below Oak on row 3", p.cellY == 3 and p.facing == "up")
local onRivalCell = pika ~= nil and pika.cellX == 4 and pika.cellY == 3
check("Pikachu is standing on the rival's landing cell (4,3)", onRivalCell)
if not onRivalCell then
U.log("Without it on (4,3) the movement data does not apply and the")
U.log("scene below proves nothing about #1021.")
end
U.shot(game, SHOT_DIR .. "/bug1021_before.png")
-- the rival's own walk sits in the same scriptMoves list, so ask only
local function stillWalking(o, npc)
if npc.moving then return true end
for _, mv in ipairs(o.scriptMoves) do
if mv.entity == npc then return true end
end
return false
end
-- end pose snapshotted at walk stop; the idle roll turns it later (Func_fc803)
U.tap(game, "a")
local leftCell, rivalSeen, rivalArrived, midShot = nil, nil, nil, false
local settled
local trace, last = {}, nil
for f = 1, 1200 do
local o = game.overworld
local top = game.stack:top()
local npc = follower()
if npc then
local cell = npc.cellX .. "," .. npc.cellY
if cell ~= last then
last = cell
trace[#trace + 1] = "f" .. f .. " " .. cell
end
if not leftCell and not (npc.cellX == 4 and npc.cellY == 3) then
leftCell = f
end
if leftCell and not settled and not stillWalking(o, npc) then
settled = { x = npc.cellX, y = npc.cellY, facing = npc.facing }
end
end
local rival = o:npcByIndex(RIVAL)
if rival and not rivalSeen then rivalSeen = f end
if rival and not rivalArrived and rival.cellX == 4 and rival.cellY == 3
and not rival.moving then
rivalArrived = f
end
if leftCell and rival and rival.moving and not midShot then
midShot = true
U.shot(game, SHOT_DIR .. "/bug1021_stepped_aside.png")
end
if rivalArrived and settled then break end
if top ~= o then U.tap(game, "a") end
U.wait(2)
end
U.log("Pikachu trace:", table.concat(trace, " | "))
check("Pikachu moved off (4,3)", leftCell ~= nil)
check("the rival came in and reached (4,3)", rivalArrived ~= nil)
if leftCell and rivalSeen then
-- the callfar runs before ShowObject: the cell is clear before the rival
check("it started clearing the cell before the rival appeared",
leftCell <= rivalSeen)
end
if settled then
U.log("the walk ended with Pikachu at", settled.x, settled.y,
"facing", settled.facing)
end
-- OaksLabPikachuMovementData2: STEP_DOWN, STEP_RIGHT, LOOK_UP
check("it ended one below the player looking up",
settled ~= nil and settled.x == 5 and settled.y == 4
and settled.facing == "up")
U.shot(game, SHOT_DIR .. "/bug1021_rival_in_place.png")
-- leg 4: nothing is fixed for #920; this only records ow.npcs and trail.ledgeHop
local eastMap = game.data.maps.FUCHSIA_CITY
and game.data.maps.FUCHSIA_CITY.connections
and game.data.maps.FUCHSIA_CITY.connections.east
eastMap = eastMap and eastMap.map or "ROUTE_15"
U.log("Fuchsia's east connection is", eastMap)
U.teleport(game, "FUCHSIA_CITY", 10, 12, "down")
U.wait(10)
local city = ow().map
local row = nil
for y = 0, city.heightCells - 1 do
if city:isWalkableCell(city.widthCells - 1, y)
and city:isWalkableCell(city.widthCells - 2, y) then
row = row or y
end
end
if row then
U.log("crossing the east seam on row", row)
U.teleport(game, "FUCHSIA_CITY", city.widthCells - 2, row, "right")
U.wait(10)
for _ = 1, 6 do
if ow().map.id == eastMap then break end
if not stepOnce("right") then break end
end
end
-- the seam row is blocked (gate rebuild, mod): drop in on the route itself
local function dropOnRoute()
U.teleport(game, eastMap, 4, 8, "right")
U.wait(10)
local m = ow().map
if m:isWalkableCell(4, 8) then return end
for y = 0, m.heightCells - 1 do
for x = 0, 9 do
if m:isWalkableCell(x, y) then
U.teleport(game, eastMap, x, y, "right")
U.wait(10)
return
end
end
end
end
if ow().map.id ~= eastMap then
U.log("could not walk the seam; dropping straight on to", eastMap)
dropOnRoute()
end
check("standing on " .. eastMap, ow().map.id == eastMap)
local worstGap, lostAt, steps = 0, nil, 0
local back = { x = ow().player.cellX, y = ow().player.cellY }
for i = 1, 10 do
if not stepOnce("right") then
if not stepOnce("down") then break end
end
local o = game.overworld
if o.map.id ~= eastMap then
-- the route's gate is a warp, and an arrival respawn is not the stall
U.log("step", i, "walked into", o.map.id, "- stepping back out")
U.teleport(game, eastMap, back.x, back.y, "left")
U.wait(10)
break
end
steps = i
back.x, back.y = o.player.cellX, o.player.cellY
local npc = follower()
local p2 = o.player
local hop = o.pikachuTrail and o.pikachuTrail.ledgeHop
if npc then
local gap = math.abs(npc.cellX - p2.cellX)
+ math.abs(npc.cellY - p2.cellY)
if gap > worstGap then worstGap = gap end
U.log("step", i, "player", p2.cellX, p2.cellY, "| Pikachu", where(npc),
"| gap", gap, "| ledgeHop", tostring(hop))
else
lostAt = lostAt or i
U.log("step", i, "player", p2.cellX, p2.cellY,
"| Pikachu is not in ow.npcs at all | ledgeHop", tostring(hop))
end
end
U.log("walked", steps, "steps east on", eastMap)
check("the follower stayed in ow.npcs the whole way", lostAt == nil)
check("it never fell more than two cells behind", worstGap <= 2)
if lostAt then
U.log("it dropped out of ow.npcs on step", lostAt,
"- that is the shape #920 would take")
end
-- an arrival parks the follower under the player (#863), so walk one step
stepOnce("left")
U.wait(20)
U.shot(game, SHOT_DIR .. "/bug920_east_of_fuchsia.png")
U.log("Six shots, in story order. bug1009_in_ball: the player alone in the")
U.log("lab with the starter already in the party. bug1009_escaped: the cry,")
U.log("then Pikachu on the cell behind him. bug1021_stepped_aside: it walks")
U.log("down and right on the GRAMPS text while the rival is still off the")
U.log("map, ending below the player looking up. The near miss to watch for")
U.log("is the rival arriving first and Pikachu shuffling around him after,")
U.log("or the walk playing under the box instead of after it.")
U.log("Talk to Pikachu here for the control case: it answers with a bubble")
U.log("and a cry, which is the same sprite and the same audio path.")
U.log("#920 is unfixed. The pad is yours on Route 15; the step log above is")
U.log("what the triage wants from a stall -- whether it is still in ow.npcs")
U.log("and whether trail.ledgeHop stayed set after a ledge.")
idle()
end