mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 08:21:02 +02:00
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:
@@ -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
|
||||
@@ -0,0 +1,153 @@
|
||||
-- A level-up evolution survives the B held from the level-up box (#968, #1031); a fresh press still cancels (#290, #213).
|
||||
-- pokered engine/movie/evolution.asm EvolveMon, Evolution_CheckForCancel.
|
||||
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local T = require("tests.harness")
|
||||
local check, eq = T.check, T.eq
|
||||
love = love or require("tests.love_stub")
|
||||
|
||||
-- TextBox and EvolutionState both require Sound inside update, so seeding
|
||||
package.loaded["src.core.Sound"] = {
|
||||
play = function() end,
|
||||
playCry = function() end,
|
||||
}
|
||||
|
||||
local Fixtures = require("tests.modkit.fixtures")
|
||||
local Evolution = require("src.pokemon.Evolution")
|
||||
local EvolutionState = require("src.ui.EvolutionState")
|
||||
local Input = require("src.core.Input")
|
||||
local Pokemon = require("src.pokemon.Pokemon")
|
||||
local StateStack = require("src.core.StateStack")
|
||||
local TextBox = require("src.render.TextBox")
|
||||
|
||||
local Data = Fixtures.fresh()
|
||||
require("src.render.Font").load(Data)
|
||||
|
||||
-- FIXMON_A evolves into FIXMON_B at 16 (tests/fixture_data/pokemon.lua)
|
||||
local EVO_LEVEL = 16
|
||||
-- x is the default keyboard B (src/core/Input.lua DEFAULT_BINDINGS)
|
||||
local B_KEY = "x"
|
||||
|
||||
local function newGame()
|
||||
local game = { data = Data }
|
||||
local mon = Pokemon.new(Data, "FIXMON_A", EVO_LEVEL)
|
||||
game.save = {
|
||||
party = { mon },
|
||||
player = { name = "RED", id = 1 },
|
||||
options = { textSpeed = 5 },
|
||||
flags = {},
|
||||
pokedex = { seen = {}, owned = {} },
|
||||
}
|
||||
game.stack = setmetatable({}, { __index = StateStack })
|
||||
game.stack:init()
|
||||
game.input = Input
|
||||
Input:init()
|
||||
return game, mon
|
||||
end
|
||||
|
||||
-- one fixed step, in Game:step's order: promote the queued edges, then
|
||||
local function step(game)
|
||||
game.input:step()
|
||||
game.stack:update(1 / 60)
|
||||
end
|
||||
|
||||
-- the post-battle sequence: grew-to-level box, then Evolution.checkParty
|
||||
local function levelUpBox(game, mon)
|
||||
game.stack:push(TextBox.new(game, "FIXMON A grew\nto level 16!",
|
||||
function() Evolution.checkParty(game, nil, { [mon] = true }) end))
|
||||
end
|
||||
|
||||
-- one B edge on the box, still held when the movie takes over: the bug's handoff
|
||||
local function dismissWithB(game, mon)
|
||||
levelUpBox(game, mon)
|
||||
local box = game.stack:top()
|
||||
for _ = 1, 900 do
|
||||
if box.done then break end
|
||||
step(game)
|
||||
end
|
||||
if not box.done then return nil, "the level-up text never finished typing" end
|
||||
Input:keypressed(B_KEY)
|
||||
step(game)
|
||||
local top = game.stack:top()
|
||||
if getmetatable(top) ~= EvolutionState then
|
||||
return nil, "the evolution screen never opened"
|
||||
end
|
||||
return top
|
||||
end
|
||||
|
||||
local function textOf(box)
|
||||
local out = {}
|
||||
for _, page in ipairs(box.pages) do
|
||||
for _, line in ipairs(page) do out[#out + 1] = line end
|
||||
end
|
||||
return table.concat(out, " ")
|
||||
end
|
||||
|
||||
-- B held out of the text box: the movie must run to the end and evolve.
|
||||
do
|
||||
local game, mon = newGame()
|
||||
local evo, why = dismissWithB(game, mon)
|
||||
if check(evo ~= nil, "the level-up box handed off to the movie: " .. tostring(why)) then
|
||||
check(Input:isDown("b"),
|
||||
"B is still physically down as the movie starts, which is what "
|
||||
.. "the old isDown poll cancelled on")
|
||||
eq(evo.cancelable, true,
|
||||
"and this is a cancelable level-up evolution, so the movie really "
|
||||
.. "is reading the button (#290)")
|
||||
-- never released: no second edge ever reaches the movie
|
||||
for _ = 1, 400 do
|
||||
if evo.done then break end
|
||||
step(game)
|
||||
end
|
||||
check(Input:isDown("b"), "B was held for the whole movie")
|
||||
eq(evo.canceled, false, "the held B did not cancel the evolution")
|
||||
eq(mon.species, "FIXMON_B", "the mon actually evolved")
|
||||
eq(mon.stats.hp, require("src.pokemon.Stats")
|
||||
.calc(Data.pokemon.FIXMON_B, EVO_LEVEL, mon.dvs, mon.statExp).hp,
|
||||
"and Evolution.apply recalculated its stats on the new species")
|
||||
local top = game.stack:top()
|
||||
check(getmetatable(top) == TextBox and textOf(top):find("evolved into"),
|
||||
"the congratulations text is what closes the movie")
|
||||
end
|
||||
end
|
||||
|
||||
-- A deliberate fresh press after the 80-frame delay still cancels.
|
||||
do
|
||||
local game, mon = newGame()
|
||||
local evo = assert(dismissWithB(game, mon))
|
||||
Input:keyreleased(B_KEY)
|
||||
for _ = 1, 400 do
|
||||
if evo.t > 80 then break end
|
||||
step(game)
|
||||
end
|
||||
check(evo.t > 80 and not evo.done,
|
||||
"the movie is past the DelayFrames window and still running")
|
||||
Input:keypressed(B_KEY)
|
||||
step(game)
|
||||
eq(evo.canceled, true, "a fresh B press cancels the evolution (#213)")
|
||||
eq(mon.species, "FIXMON_A", "and the mon keeps its species")
|
||||
local top = game.stack:top()
|
||||
check(getmetatable(top) == TextBox and textOf(top):find("stopped evolving"),
|
||||
"_StoppedEvolvingText prints instead of the congratulations")
|
||||
end
|
||||
|
||||
-- A press inside the 80 frames is not polled at all, so the mon still
|
||||
do
|
||||
local game, mon = newGame()
|
||||
local evo = assert(dismissWithB(game, mon))
|
||||
Input:keyreleased(B_KEY)
|
||||
for _ = 1, 10 do step(game) end
|
||||
Input:keypressed(B_KEY)
|
||||
step(game)
|
||||
Input:keyreleased(B_KEY)
|
||||
check(evo.t <= 80, "the press landed inside the delay window")
|
||||
eq(evo.canceled, false, "a B press during the delay is never polled")
|
||||
for _ = 1, 400 do
|
||||
if evo.done then break end
|
||||
step(game)
|
||||
end
|
||||
eq(mon.species, "FIXMON_B", "so the evolution still completes")
|
||||
end
|
||||
|
||||
T.finish()
|
||||
@@ -0,0 +1,102 @@
|
||||
-- Nurse Joy bows between the two closing lines (#995).
|
||||
-- pokered engine/events/pokecenter.asm.
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local T = require("tests.modkit")
|
||||
local Data = T.fixtures.load()
|
||||
|
||||
-- the ROM-extracted strings the fixture text table does not carry
|
||||
Data.text._PokemonFightingFitText = "Thank you for\nwaiting.\fYour POKéMON are\nfighting fit!"
|
||||
Data.text._PokemonCenterFarewellText = "We hope to see\nyou again!"
|
||||
|
||||
local OW = require("src.world.OverworldController")
|
||||
|
||||
local function setUpvalue(fn, name, val)
|
||||
local i = 1
|
||||
while true do
|
||||
local n = debug.getupvalue(fn, i)
|
||||
if not n then return false end
|
||||
if n == name then debug.setupvalue(fn, i, val); return true end
|
||||
i = i + 1
|
||||
end
|
||||
end
|
||||
|
||||
local pushed = {}
|
||||
local stackStub = { push = function(_, item) pushed[#pushed + 1] = item end }
|
||||
local textBoxStub = {
|
||||
new = function(_, text, onDone, opts)
|
||||
return { text = text, onDone = onDone, opts = opts }
|
||||
end,
|
||||
}
|
||||
local fakeGame = { data = Data, stack = stackStub }
|
||||
T.check(setUpvalue(OW.finishNurseHeal, "TextBox", textBoxStub),
|
||||
"TextBox upvalue on finishNurseHeal")
|
||||
T.check(setUpvalue(OW.finishNurseHeal, "Game", fakeGame),
|
||||
"Game upvalue on finishNurseHeal")
|
||||
|
||||
local FIT = Data.text._PokemonFightingFitText
|
||||
local BYE = Data.text._PokemonCenterFarewellText
|
||||
|
||||
local player = { cellX = 3, cellY = 5 }
|
||||
local faced
|
||||
local function newNurse()
|
||||
faced = 0
|
||||
return {
|
||||
facing = "down",
|
||||
facePlayer = function(self) faced = faced + 1; self.facing = "down" end,
|
||||
}
|
||||
end
|
||||
|
||||
local fakeSelf
|
||||
local function reset()
|
||||
pushed = {}
|
||||
fakeSelf = setmetatable({ player = player }, { __index = OW })
|
||||
end
|
||||
|
||||
-- === with the nurse on the counter: fit line, bow, farewell
|
||||
reset()
|
||||
local nurse = newNurse()
|
||||
local finished = 0
|
||||
fakeSelf:finishNurseHeal(BYE, function() finished = finished + 1 end, nurse)
|
||||
T.eq(#pushed, 1, "the fighting-fit line goes up on its own")
|
||||
T.eq(pushed[1].text, FIT, "first box is exactly the fighting-fit text")
|
||||
T.check(pushed[1].text:find(BYE, 1, true) == nil,
|
||||
"the farewell is no longer merged into it with a page break (#995)")
|
||||
|
||||
pushed[1].onDone()
|
||||
T.eq(#pushed, 1, "the farewell waits for the bow")
|
||||
T.eq(nurse.facing, "up", "image index $14: the nurse bows")
|
||||
T.check(fakeSelf.emote ~= nil, "the bow is a world hold, not a text pause")
|
||||
local hold = fakeSelf.emote or {}
|
||||
T.eq(hold.npc, nurse, "the hold is anchored on the nurse")
|
||||
T.eq(hold.frames, 20, "DelayFrames $14 is 20 frames")
|
||||
T.eq(hold.bubble, false, "no emotion bubble is drawn over her")
|
||||
T.check(not hold.skippable, "the bow cannot be skipped with A/B")
|
||||
T.eq(finished, 0, "the pokecenter is still busy during the bow")
|
||||
|
||||
-- OverworldState:update counts emote.frames down and then calls onDone
|
||||
if hold.onDone then hold.onDone() end
|
||||
T.eq(#pushed, 2, "the farewell follows the bow")
|
||||
local farewell = pushed[2] or {}
|
||||
T.eq(farewell.text, BYE, "second box is the farewell text")
|
||||
T.eq(nurse.facing, "up", "she is still bowed while the farewell prints")
|
||||
|
||||
if farewell.onDone then farewell.onDone() end
|
||||
T.eq(nurse.facing, "down", "the trailing UpdateSprites faces her back")
|
||||
T.eq(faced, 1, "she is turned back exactly once")
|
||||
T.eq(finished, 1, "control returns to the player once, after the farewell")
|
||||
|
||||
-- === no nurse sprite (the Yellow/rest-stop callers): no bow, same text
|
||||
reset()
|
||||
finished = 0
|
||||
fakeSelf:finishNurseHeal(BYE, function() finished = finished + 1 end)
|
||||
T.eq(pushed[1].text, FIT, "npc-less caller still opens with the fit line")
|
||||
pushed[1].onDone()
|
||||
T.check(fakeSelf.emote == nil, "nothing to bow, so no world hold")
|
||||
T.eq(#pushed, 2, "the farewell follows immediately")
|
||||
farewell = pushed[2] or {}
|
||||
T.eq(farewell.text, BYE, "npc-less caller still closes with the farewell")
|
||||
if farewell.onDone then farewell.onDone() end
|
||||
T.eq(finished, 1, "npc-less caller returns control once")
|
||||
|
||||
T.finish("nurse_bow_bug995")
|
||||
@@ -0,0 +1,149 @@
|
||||
-- Oak's aide quotes the REQUIREMENT, not your current count (#1006).
|
||||
-- pokered engine/events/oaks_aide.asm .notEnoughOwnedMons.
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local T = require("tests.modkit")
|
||||
local Data = T.fixtures.load()
|
||||
|
||||
-- the ROM-extracted strings the fixture text table does not carry; labels
|
||||
-- and wording match pokered data/text/text_1.asm
|
||||
Data.text._OaksAideHiText =
|
||||
"Hi! Remember me?\nI'm PROF.OAK's\vAIDE!\fIf you caught " ..
|
||||
"{NUM:hOaksAideRequirement, 1, 3}\nkinds of POKéMON,\vI'm supposed to\v" ..
|
||||
"give you an\v{RAM:wOaksAideRewardItemName}!\fSo, {PLAYER}! Have\n" ..
|
||||
"you caught at\vleast {NUM:hOaksAideRequirement, 1, 3} kinds of\vPOKéMON?"
|
||||
Data.text._OaksAideUhOhText =
|
||||
"Let's see...\nUh-oh! You have\vcaught only " ..
|
||||
"{NUM:hOaksAideNumMonsOwned, 1, 3}\vkinds of POKéMON!\fYou need " ..
|
||||
"{NUM:hOaksAideRequirement, 1, 3} kinds\nif you want the\v" ..
|
||||
"{RAM:wOaksAideRewardItemName}."
|
||||
Data.text._OaksAideComeBackText =
|
||||
"Oh. I see.\fWhen you get {NUM:hOaksAideRequirement, 1, 3}\nkinds, come " ..
|
||||
"back\vfor {RAM:wOaksAideRewardItemName}."
|
||||
Data.text._OaksAideHereYouGoText =
|
||||
"Great! You have\ncaught {NUM:hOaksAideNumMonsOwned, 1, 3} kinds \v" ..
|
||||
"of POKéMON!\vCongratulations!\fHere you go!"
|
||||
Data.text._OaksAideGotItemText =
|
||||
"{PLAYER} got the\n{RAM:wOaksAideRewardItemName}!"
|
||||
-- the two rewards this suite drives (Route2Gate / Route11Gate2F pass them
|
||||
Data.items.HM_FLASH = { id = "HM_FLASH", index = 196, name = "HM FLASH" }
|
||||
Data.items.ITEMFINDER = { id = "ITEMFINDER", index = 6, name = "ITEMFINDER" }
|
||||
|
||||
local SaveData = require("src.core.SaveData")
|
||||
|
||||
local pushed = {}
|
||||
local realTextBox = package.loaded["src.render.TextBox"]
|
||||
-- story4's push/ask require TextBox lazily, so a package.loaded stub is
|
||||
package.loaded["src.render.TextBox"] = {
|
||||
new = function(_, text, onDone, opts)
|
||||
return { text = text, onDone = onDone, opts = opts }
|
||||
end,
|
||||
}
|
||||
|
||||
local story4 = dofile("data/scripts/story4.lua")
|
||||
local ROUTE_11 = story4.ROUTE_11_GATE_2F.talk.TEXT_ROUTE11GATE2F_OAKS_AIDE
|
||||
local ROUTE_2 = story4.ROUTE_2_GATE.talk.TEXT_ROUTE2GATE_OAKS_AIDE
|
||||
T.check(type(ROUTE_11) == "function" and type(ROUTE_2) == "function",
|
||||
"both aides are wired to the shared oaksAide handler")
|
||||
|
||||
local game = {
|
||||
data = Data,
|
||||
save = SaveData.newGame(),
|
||||
stack = { push = function(_, box) pushed[#pushed + 1] = box end },
|
||||
}
|
||||
|
||||
local function reset(ownedCount)
|
||||
game.save = SaveData.newGame()
|
||||
game.save.player.name = "RED"
|
||||
local owned = {}
|
||||
for i = 1, ownedCount do owned["SPECIES_" .. i] = true end
|
||||
game.save.pokedex = { seen = {}, owned = owned }
|
||||
pushed = {}
|
||||
end
|
||||
local function lastText()
|
||||
return tostring(pushed[#pushed] and pushed[#pushed].text)
|
||||
end
|
||||
local function has(fragment)
|
||||
return lastText():find(fragment, 1, true) ~= nil
|
||||
end
|
||||
local function held(id)
|
||||
return game.save.inventory[id] or 0
|
||||
end
|
||||
-- A press on the box that is up
|
||||
local function dismiss()
|
||||
local box = pushed[#pushed]
|
||||
if box and box.onDone then box.onDone() end
|
||||
end
|
||||
|
||||
-- === the aide asks for his own threshold, whatever the player owns
|
||||
reset(12)
|
||||
local done = false
|
||||
ROUTE_11(game, {}, {}, function() done = true end)
|
||||
local offer = pushed[1]
|
||||
T.check(offer.opts and offer.opts.choice ~= nil,
|
||||
"the aide's opener is the YesNoChoice question")
|
||||
T.check(has("least 30 kinds"), "opener asks for the aide's 30 kinds")
|
||||
T.check(has("give you an\vITEMFINDER"), "opener names the reward item")
|
||||
T.check(not has("{NUM"), "no placeholder survives into the opener")
|
||||
|
||||
-- === YES with too few kinds: both decimals are filled, and differently
|
||||
offer.opts.choice(true)
|
||||
T.check(has("caught only 12"), "Uh-oh line reports the kinds actually owned")
|
||||
T.check(has("You need 30 kinds"), "Uh-oh line then states the requirement")
|
||||
T.check(not has("You need 12 kinds"),
|
||||
"the requirement is not overwritten by the owned count (#1006)")
|
||||
T.check(has("want the\vITEMFINDER"), "Uh-oh line still names the reward")
|
||||
dismiss()
|
||||
T.check(done, "the Uh-oh branch completes the talk")
|
||||
T.eq(held("ITEMFINDER"), 0, "no reward below the threshold")
|
||||
T.check(not game.save.flags.EVENT_GOT_ITEMFINDER,
|
||||
"the aide can still be asked again")
|
||||
|
||||
-- === the threshold tracks the aide, not a constant: Route 2 wants 10
|
||||
reset(3)
|
||||
ROUTE_2(game, {}, {}, function() end)
|
||||
pushed[1].opts.choice(true)
|
||||
T.check(has("caught only 3"), "Route 2 Uh-oh reports 3 kinds owned")
|
||||
T.check(has("You need 10 kinds"), "Route 2 states its own 10-kind threshold")
|
||||
T.check(has("want the\vHM FLASH"), "Route 2 names the HM FLASH reward")
|
||||
|
||||
-- === NO: ComeBackText quotes the requirement, nothing is given
|
||||
reset(12)
|
||||
done = false
|
||||
ROUTE_11(game, {}, {}, function() done = true end)
|
||||
pushed[1].opts.choice(false)
|
||||
T.check(has("When you get 30"), "come-back line quotes the requirement")
|
||||
T.check(has("back\vfor ITEMFINDER"), "come-back line names the reward")
|
||||
dismiss()
|
||||
T.check(done, "declining completes the talk")
|
||||
T.eq(held("ITEMFINDER"), 0, "declining gives nothing")
|
||||
|
||||
-- === YES at the threshold: HereYouGo carries the OWNED count, then the
|
||||
reset(30)
|
||||
done = false
|
||||
ROUTE_11(game, {}, {}, function() done = true end)
|
||||
pushed[1].opts.choice(true)
|
||||
T.check(has("caught 30 kinds"), "congratulation line carries the owned count")
|
||||
dismiss()
|
||||
T.check(has("RED got the\nITEMFINDER!"), "the item line names player and item")
|
||||
dismiss()
|
||||
T.check(done, "the reward branch completes the talk")
|
||||
T.eq(held("ITEMFINDER"), 1, "ITEMFINDER lands in the bag")
|
||||
T.check(game.save.flags.EVENT_GOT_ITEMFINDER, "the aide's event flag is set")
|
||||
|
||||
-- === repeat visit: the explanation text, no second ITEMFINDER
|
||||
pushed = {}
|
||||
done = false
|
||||
ROUTE_11(game, {}, {}, function() done = true end)
|
||||
T.eq(#pushed, 1, "a served player gets exactly one box")
|
||||
T.check(pushed[1].opts == nil or pushed[1].opts.choice == nil,
|
||||
"the repeat line is not a question")
|
||||
T.eq(held("ITEMFINDER"), 1, "no second ITEMFINDER")
|
||||
|
||||
if realTextBox ~= nil then
|
||||
package.loaded["src.render.TextBox"] = realTextBox
|
||||
else
|
||||
package.loaded["src.render.TextBox"] = nil
|
||||
end
|
||||
|
||||
T.finish("oaks_aide_requirement_bug1006")
|
||||
@@ -0,0 +1,113 @@
|
||||
-- Yellow's starter Pikachu gets the nickname prompt (#1013).
|
||||
-- pokeyellow scripts/OaksLab.asm OaksLabPlayerReceivesPikachuScript.
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local T = require("tests.modkit")
|
||||
local Data = T.fixtures.load()
|
||||
|
||||
local pushed = {}
|
||||
local realTextBox = package.loaded["src.render.TextBox"]
|
||||
-- Commands requires TextBox at load time; stub it before the first require
|
||||
package.loaded["src.render.TextBox"] = {
|
||||
new = function(_, text, onDone, opts)
|
||||
return { text = text, onDone = onDone, opts = opts }
|
||||
end,
|
||||
}
|
||||
local Commands = require("src.script.Commands")
|
||||
local SaveData = require("src.core.SaveData")
|
||||
|
||||
-- === the lab scene: one PIKACHU gift row, and no skipNickname on it
|
||||
local lab = dofile("data/scripts/oaks_lab_yellow.lua")
|
||||
local ball = lab.talk.TEXT_OAKSLAB_EEVEE_POKE_BALL
|
||||
T.check(type(ball) == "function", "the Eevee ball builds its rows per playthrough")
|
||||
|
||||
local captured
|
||||
local function buildScene(cellX, cellY)
|
||||
captured = nil
|
||||
local game = { data = Data, save = SaveData.newGame() }
|
||||
game.save.flags.EVENT_OAK_ASKED_TO_CHOOSE_MON = true
|
||||
local ow = {
|
||||
player = { cellX = cellX, cellY = cellY },
|
||||
runner = { run = function(_, rows) captured = rows end },
|
||||
}
|
||||
ball(game, ow, { def = {} }, function() end)
|
||||
return captured or {}
|
||||
end
|
||||
|
||||
local function indexOf(rows, verb, arg)
|
||||
for i, row in ipairs(rows) do
|
||||
if row[1] == verb and (arg == nil or row[2] == arg) then return i end
|
||||
end
|
||||
end
|
||||
|
||||
-- the shove branch (player on the table row, py == 4) and the plain
|
||||
for _, spot in ipairs({ { 9, 4 }, { 5, 6 } }) do
|
||||
local rows = buildScene(spot[1], spot[2])
|
||||
local where = ("from (%d,%d)"):format(spot[1], spot[2])
|
||||
local gives = 0
|
||||
for _, row in ipairs(rows) do
|
||||
if row[1] == "give_pokemon" then
|
||||
gives = gives + 1
|
||||
T.eq(row[2], "PIKACHU", "the gift species is PIKACHU " .. where)
|
||||
T.eq(row[3], 5, "the gift is level 5 " .. where)
|
||||
T.check(row[4] == nil,
|
||||
"no skipNickname: AskName is left to run " .. where .. " (#1013)")
|
||||
end
|
||||
end
|
||||
T.eq(gives, 1, "exactly one give_pokemon row " .. where)
|
||||
|
||||
local give = indexOf(rows, "give_pokemon")
|
||||
local received = indexOf(rows, "show_text", "_OaksLabReceivedText")
|
||||
local got = indexOf(rows, "set_flag", "EVENT_GOT_STARTER")
|
||||
T.check(received and give and received < give,
|
||||
"the received line prints before the mon is added " .. where)
|
||||
T.check(give and got and give < got,
|
||||
"AddPartyMon runs before SetEvent EVENT_GOT_STARTER " .. where)
|
||||
end
|
||||
|
||||
-- === nothing in the Yellow lab names a Kanto starter (#1014): only a mod
|
||||
local source = assert(io.open("data/scripts/oaks_lab_yellow.lua", "r"))
|
||||
local text = source:read("*a")
|
||||
source:close()
|
||||
for _, species in ipairs({ "CHARMANDER", "SQUIRTLE", "BULBASAUR" }) do
|
||||
T.check(not text:find(species, 1, true),
|
||||
"the Yellow lab script never names " .. species)
|
||||
end
|
||||
|
||||
-- === give_pokemon offers AskName with a runner and no skipNickname
|
||||
local function giveThrough(skipNickname)
|
||||
pushed = {}
|
||||
local game = { data = Data, save = SaveData.newGame(),
|
||||
stack = { push = function(_, box) pushed[#pushed + 1] = box end } }
|
||||
local runner = {
|
||||
yield = function() return coroutine.yield() end,
|
||||
resume = function(self, ...) coroutine.resume(self.co, ...) end,
|
||||
}
|
||||
local ctx = { game = game, save = game.save, runner = runner }
|
||||
runner.co = coroutine.create(function()
|
||||
Commands.give_pokemon(ctx, "FIXMON_A", 5, skipNickname)
|
||||
end)
|
||||
local ok, err = coroutine.resume(runner.co)
|
||||
T.check(ok, "give_pokemon runs cleanly: " .. tostring(err))
|
||||
return game.save
|
||||
end
|
||||
|
||||
local save = giveThrough(nil)
|
||||
T.eq(#pushed, 1, "a plain gift puts one box up")
|
||||
T.check(tostring(pushed[1].text):find("nickname", 1, true) ~= nil,
|
||||
"that box is AskName's question")
|
||||
T.check(pushed[1].opts and pushed[1].opts.choice ~= nil,
|
||||
"AskName is a YES/NO, not a plain box")
|
||||
T.eq(#save.party, 1, "the gift joined the party")
|
||||
|
||||
save = giveThrough(true)
|
||||
T.eq(#pushed, 0, "skipNickname suppresses the prompt")
|
||||
T.eq(#save.party, 1, "the gift still joined the party")
|
||||
|
||||
if realTextBox ~= nil then
|
||||
package.loaded["src.render.TextBox"] = realTextBox
|
||||
else
|
||||
package.loaded["src.render.TextBox"] = nil
|
||||
end
|
||||
|
||||
T.finish("oaks_lab_yellow_starter_bug1013")
|
||||
@@ -118,8 +118,9 @@ for _, item in ipairs(menu.items) do
|
||||
if item.label == "PROF.OAK's PC" then oak = item end
|
||||
end
|
||||
T.check(oak ~= nil, "PROF.OAK's PC is offered once the Pokédex is had")
|
||||
plays = {} -- drop the menu's Turn_On_PC; the session's jingle is what counts
|
||||
oak.onSelect()
|
||||
-- drop the menu's Turn_On_PC and the row's Enter_PC
|
||||
plays = {}
|
||||
T.eq(pushed[2].kind, "text", "selection opens the access text")
|
||||
T.check(tostring(pushed[2].text):find("Accessed", 1, true) ~= nil,
|
||||
"first session box is the access text")
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
-- Parity: the Fan Club Chairman asks before telling his story (#1050).
|
||||
-- pokered scripts/PokemonFanClub.asm PokemonFanClubChairmanText.
|
||||
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 Fan Club chairman")
|
||||
local check, eq = S.check, S.eq
|
||||
|
||||
local Game = require("src.core.Game")
|
||||
local Input = require("src.core.Input")
|
||||
local StateStack = require("src.core.StateStack")
|
||||
local SaveData = require("src.core.SaveData")
|
||||
local ScriptRunner = require("src.script.ScriptRunner")
|
||||
local Commands = require("src.script.Commands")
|
||||
local Flags = require("src.script.Flags")
|
||||
local ChoiceBox = require("src.ui.ChoiceBox")
|
||||
local mapScripts = require("data.scripts.init")
|
||||
|
||||
Game.data = Data
|
||||
Game.input = Input; Input:init()
|
||||
Game.stack = StateStack; StateStack:init()
|
||||
Game.save = SaveData.newGame()
|
||||
require("src.render.Font").load(Data)
|
||||
|
||||
local MAP, TEXT = "POKEMON_FAN_CLUB", "TEXT_POKEMONFANCLUB_CHAIRMAN"
|
||||
local script = mapScripts.talkScript(MAP, TEXT)
|
||||
check(type(script) == "table", "the chairman is a row list")
|
||||
eq(#ScriptRunner.validate(script), 0,
|
||||
"the rows validate cleanly (labels resolve after the renumbering)")
|
||||
|
||||
-- === the intro is the question, not a plain box ===
|
||||
local asks = 0
|
||||
for _, row in ipairs(type(script) == "table" and script or {}) do
|
||||
if row[1] == "ask" then
|
||||
asks = asks + 1
|
||||
eq(row[2], "_PokemonFanClubChairmanIntroText",
|
||||
"the YesNoChoice rides the intro text (#1050)")
|
||||
end
|
||||
end
|
||||
eq(asks, 1, "exactly one question, right after the intro")
|
||||
|
||||
-- === harness: run the talk script headless, recording show_text ids ===
|
||||
local shown = {}
|
||||
local origShow = Commands.show_text
|
||||
-- forward extraOpts: Commands.ask rides show_text's 4th argument
|
||||
Commands.show_text = function(ctx, textId, subs, ...)
|
||||
table.insert(shown, textId)
|
||||
return origShow(ctx, textId, subs, ...)
|
||||
end
|
||||
|
||||
-- pressFn returns the Input.pressed table for this frame (default: A)
|
||||
local function runScript(pressFn)
|
||||
shown = {}
|
||||
local ow = { map = { id = MAP, def = { label = MAP } },
|
||||
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 = pressFn and pressFn() or { a = true }
|
||||
StateStack:update(1 / 60)
|
||||
r:update()
|
||||
end
|
||||
Input.pressed = {}
|
||||
return not r:isRunning()
|
||||
end
|
||||
|
||||
local function shownIs(want, msg)
|
||||
eq(table.concat(shown, ","), table.concat(want, ","), msg)
|
||||
end
|
||||
|
||||
-- press B while the YES/NO box is up, A otherwise: the NO answer
|
||||
local function declines()
|
||||
if getmetatable(StateStack:top()) == ChoiceBox then return { b = true } end
|
||||
return { a = true }
|
||||
end
|
||||
|
||||
local function held(id) return Game.save.inventory[id] or 0 end
|
||||
|
||||
-- === 1) YES: story, voucher, received line, explanation ===
|
||||
Game.save = SaveData.newGame()
|
||||
check(runScript(), "chairman script completes on YES")
|
||||
shownIs({ "_PokemonFanClubChairmanIntroText",
|
||||
"_PokemonFanClubChairmanStoryText",
|
||||
"_PokemonFanClubReceivedBikeVoucherText",
|
||||
"_PokemonFanClubExplainBikeVoucherText" },
|
||||
"YES hears the RAPIDASH story out and collects the voucher")
|
||||
eq(held("BIKE_VOUCHER"), 1, "the BIKE VOUCHER is in the bag")
|
||||
check(Flags.get(Game.save, "EVENT_RECEIVED_BIKE_VOUCHER"),
|
||||
"EVENT_GOT_BIKE_VOUCHER is set")
|
||||
|
||||
-- === 2) NO: the brush-off, and the voucher stays with the chairman ===
|
||||
Game.save = SaveData.newGame()
|
||||
check(runScript(declines), "chairman script completes on NO")
|
||||
shownIs({ "_PokemonFanClubChairmanIntroText", "_PokemonFanClubNoStoryText" },
|
||||
"NO skips the story and the gift (#1050)")
|
||||
eq(held("BIKE_VOUCHER"), 0, "declining leaves the voucher unclaimed")
|
||||
check(not Flags.get(Game.save, "EVENT_RECEIVED_BIKE_VOUCHER"),
|
||||
"declining leaves the event clear, so he can be asked again")
|
||||
|
||||
-- === 3) asking again after NO still works, and YES then pays out ===
|
||||
check(runScript(), "second visit completes")
|
||||
shownIs({ "_PokemonFanClubChairmanIntroText",
|
||||
"_PokemonFanClubChairmanStoryText",
|
||||
"_PokemonFanClubReceivedBikeVoucherText",
|
||||
"_PokemonFanClubExplainBikeVoucherText" },
|
||||
"a player who said NO can come back for the voucher")
|
||||
eq(held("BIKE_VOUCHER"), 1, "the voucher arrives on the second visit")
|
||||
|
||||
-- === 4) served player: .nothingleft, no question at all ===
|
||||
check(runScript(), "post-voucher script completes")
|
||||
shownIs({ "_PokemonFanClubChairFinalText" },
|
||||
"with the voucher collected he only reminisces")
|
||||
eq(held("BIKE_VOUCHER"), 1, "no second voucher")
|
||||
|
||||
Commands.show_text = origShow
|
||||
S.finish()
|
||||
@@ -0,0 +1,135 @@
|
||||
-- Parity: the Silph Co. 7F worker's LAPRAS gift offers the nickname prompt (#1049).
|
||||
-- pokered scripts/SilphCo7F.asm SilphCo7FSilphWorkerM1Text.
|
||||
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 Silph LAPRAS")
|
||||
local check, eq = S.check, S.eq
|
||||
|
||||
local Game = require("src.core.Game")
|
||||
local Input = require("src.core.Input")
|
||||
local StateStack = require("src.core.StateStack")
|
||||
local SaveData = require("src.core.SaveData")
|
||||
local ScriptRunner = require("src.script.ScriptRunner")
|
||||
local Commands = require("src.script.Commands")
|
||||
local Flags = require("src.script.Flags")
|
||||
local Pokemon = require("src.pokemon.Pokemon")
|
||||
local Boxes = require("src.pokemon.Boxes")
|
||||
local mapScripts = require("data.scripts.init")
|
||||
|
||||
Game.data = Data
|
||||
Game.input = Input; Input:init()
|
||||
Game.stack = StateStack; StateStack:init()
|
||||
Game.save = SaveData.newGame()
|
||||
require("src.render.Font").load(Data)
|
||||
|
||||
local MAP, TEXT = "SILPH_CO_7F", "TEXT_SILPHCO7F_SILPH_WORKER_M1"
|
||||
|
||||
-- === 1) the worker is command rows, so the run carries a ScriptRunner ===
|
||||
local script = mapScripts.talkScript(MAP, TEXT)
|
||||
check(type(script) == "table",
|
||||
"the LAPRAS worker is a row list, not a bare callback (#1049)")
|
||||
eq(#ScriptRunner.validate(script), 0, "the rows validate cleanly")
|
||||
local gives = 0
|
||||
for _, row in ipairs(type(script) == "table" and script or {}) do
|
||||
if row[1] == "give_pokemon" then
|
||||
gives = gives + 1
|
||||
eq(row[2], "LAPRAS", "the gift species is LAPRAS")
|
||||
eq(row[3], 15, "the gift is level 15 (lb bc, LAPRAS, 15)")
|
||||
check(row[4] == nil, "no skipNickname: AskName is left to run")
|
||||
end
|
||||
end
|
||||
eq(gives, 1, "exactly one give_pokemon row")
|
||||
|
||||
-- === harness: run the talk script headless, A every frame, recording show_text ids
|
||||
local shown = {}
|
||||
local origShow = Commands.show_text
|
||||
-- forward extraOpts: Commands.ask rides show_text's 4th argument
|
||||
Commands.show_text = function(ctx, textId, subs, ...)
|
||||
table.insert(shown, textId)
|
||||
return origShow(ctx, textId, subs, ...)
|
||||
end
|
||||
|
||||
local function runScript()
|
||||
shown = {}
|
||||
local ow = { map = { id = MAP, def = { label = MAP } },
|
||||
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 = { a = true }
|
||||
StateStack:update(1 / 60)
|
||||
r:update()
|
||||
end
|
||||
Input.pressed = {}
|
||||
return not r:isRunning()
|
||||
end
|
||||
|
||||
local function shownIs(want, msg)
|
||||
eq(table.concat(shown, ","), table.concat(want, ","), msg)
|
||||
end
|
||||
|
||||
-- === 2) the gift itself: thanks, nickname prompt, GotMonText, blurb ===
|
||||
Game.save = SaveData.newGame()
|
||||
check(runScript(), "LAPRAS gift script completes")
|
||||
shownIs({ "_SilphCo7FSilphWorkerM1HaveThisPokemonText",
|
||||
"_DoYouWantToNicknameText", "_GotMonText",
|
||||
"_SilphCo7FSilphWorkerM1LaprasDescriptionText" },
|
||||
"thanks, nickname prompt, got-mon line, then the LAPRAS blurb")
|
||||
eq(#Game.save.party, 1, "LAPRAS joins the party")
|
||||
local lapras = Game.save.party[1] or {}
|
||||
eq(lapras.species, "LAPRAS", "gift species is LAPRAS")
|
||||
eq(lapras.level, 15, "LAPRAS is level 15")
|
||||
eq(lapras.nickname, "AAAAAAAAAA",
|
||||
"the nickname prompt reaches the NamingScreen (A-mash)")
|
||||
check(Flags.get(Game.save, "EVENT_GOT_LAPRAS"), "BIT_GOT_LAPRAS is set")
|
||||
check(Game.save.pokedex.owned.LAPRAS, "LAPRAS is registered owned")
|
||||
|
||||
-- === 3) after the gift he worries about the PRESIDENT, and only after
|
||||
check(runScript(), "post-gift script completes")
|
||||
shownIs({ "_SilphCo7FSilphWorkerM1IsOurPresidentOkText" },
|
||||
"before Giovanni: the worried line, and no second LAPRAS")
|
||||
eq(#Game.save.party, 1, "no second LAPRAS")
|
||||
|
||||
Flags.set(Game.save, "EVENT_BEAT_SILPH_CO_GIOVANNI")
|
||||
check(runScript(), "post-Giovanni script completes")
|
||||
shownIs({ "_SilphCo7FSilphWorkerM1SavedText" },
|
||||
"after Giovanni: saved at last")
|
||||
|
||||
-- === 4) party full, box has room: SendNewMonToBox still asks the name ===
|
||||
Game.save = SaveData.newGame()
|
||||
for i = 1, 6 do Game.save.party[i] = Pokemon.new(Data, "PIDGEY", 5) end
|
||||
check(runScript(), "full-party gift script completes")
|
||||
shownIs({ "_SilphCo7FSilphWorkerM1HaveThisPokemonText",
|
||||
"_DoYouWantToNicknameText", "_SentToBoxText", "_GotMonText",
|
||||
"_SilphCo7FSilphWorkerM1LaprasDescriptionText" },
|
||||
"full party: nickname, sent-to-box, got-mon line, blurb")
|
||||
local boxed = false
|
||||
for _, box in ipairs(Boxes.ensure(Game.save)) do
|
||||
for _, m in ipairs(box) do
|
||||
if m.species == "LAPRAS" then boxed = true end
|
||||
end
|
||||
end
|
||||
check(boxed, "full-party LAPRAS lands in a box")
|
||||
check(Flags.get(Game.save, "EVENT_GOT_LAPRAS"), "full-party gift sets the flag")
|
||||
|
||||
-- === 5) party AND every box full: BoxIsFullText, no got-mon line, flag
|
||||
Game.save = SaveData.newGame()
|
||||
for i = 1, 6 do Game.save.party[i] = Pokemon.new(Data, "PIDGEY", 5) end
|
||||
Boxes.ensure(Game.save)
|
||||
for b = 1, Boxes.COUNT do
|
||||
for s = 1, Boxes.CAPACITY do Game.save.boxes[b][s] = { species = "PIDGEY" } end
|
||||
end
|
||||
check(runScript(), "full-everything script completes")
|
||||
shownIs({ "_SilphCo7FSilphWorkerM1HaveThisPokemonText", "_BoxIsFullText" },
|
||||
"no room: the box-full line, never a got-mon line for a mon you lack")
|
||||
check(not Flags.get(Game.save, "EVENT_GOT_LAPRAS"),
|
||||
"a failed give leaves BIT_GOT_LAPRAS clear, so the gift stays claimable")
|
||||
|
||||
Commands.show_text = origShow
|
||||
S.finish()
|
||||
Reference in New Issue
Block a user