fix testsZ

This commit is contained in:
bryanthaboi
2026-08-13 06:05:27 -04:00
parent 62b9f04191
commit 52e36ad7e4
15 changed files with 148 additions and 48 deletions
+4 -1
View File
@@ -266,7 +266,10 @@ function OakSpeech.new(game, onDone)
-- RedSprite: the walking sprite the pic shrinks into (frame 0 = -- RedSprite: the walking sprite the pic shrinks into (frame 0 =
-- standing, facing down) -- standing, facing down)
local playerSprites = (game.data.field and game.data.field.playerSprites) or {} local playerSprites = (game.data.field and game.data.field.playerSprites) or {}
local red = game.data.sprites and game.data.sprites[playerSprites.walk or "SPRITE_RED"] or game.data.sprites.SPRITE_RED -- The fallback has to read the same guarded table: reaching for
-- game.data.sprites.SPRITE_RED after the `and` already found it nil threw.
local sprites = game.data.sprites or {}
local red = sprites[playerSprites.walk or "SPRITE_RED"] or sprites.SPRITE_RED
self.walkSheet = tryImage(red and red.image) self.walkSheet = tryImage(red and red.image)
return self return self
end end
+14 -3
View File
@@ -3517,10 +3517,21 @@ end
function OverworldState:onStepComplete() function OverworldState:onStepComplete()
local p = self.player local p = self.player
local suppressWildEncounter = self.wildEncounterGraceSteps > 0 -- Defaulted: a state built without the constructor (a mod harness, a test
if suppressWildEncounter then -- fixture) reaches this before :234 ever ran, and nil > 0 threw the step.
self.wildEncounterGraceSteps = self.wildEncounterGraceSteps - 1 local grace = self.wildEncounterGraceSteps or 0
if grace > 0 then
self.wildEncounterGraceSteps = grace - 1
end end
-- TryDoWildEncounter's first guard is `ld a, [wNPCMovementScriptPointerTable
-- Num] / and a / ret nz` (engine/battle/wild_encounters.asm:3-9): a step the
-- player did not take never rolls, which is why Oak's escort walks to the lab
-- through Pallet's grass without being jumped.
local runner = self.runner
local scripted = (runner and runner.isRunning and runner:isRunning())
or #(self.scriptMoves or {}) > 0
or self.engaging or self.emote or self.teleportOut
local suppressWildEncounter = grace > 0 or scripted and true or false
self.todSteps = (self.todSteps or 0) + 1 self.todSteps = (self.todSteps or 0) + 1
-- UpdatePikachuHappinessAndMood rides the step counter (poison.asm) -- UpdatePikachuHappinessAndMood rides the step counter (poison.asm)
require("src.world.PikachuFollower").onStep(Game.save) require("src.world.PikachuFollower").onStep(Game.save)
+2 -1
View File
@@ -3946,7 +3946,8 @@ function World:rollEncounter(kind, terrain, tables, vanilla)
local ctx = { local ctx = {
mapId = map and map.id, mapId = map and map.id,
terrain = terrain, terrain = terrain,
rng = love.math.random, -- Same guard World:rockRandom uses: a headless suite has no love global.
rng = (love and love.math and love.math.random) or math.random,
kind = kind, kind = kind,
daytime = self.daytime, daytime = self.daytime,
environment = map and map.def and map.def.environment, environment = map and map.def and map.def.environment,
+5
View File
@@ -252,10 +252,15 @@ do
local outcome local outcome
screen.onDone = function(result) outcome = result end screen.onDone = function(result) outcome = result end
-- Battle lines end in `prompt` and PromptButton waits on A/B with no
-- countdown (home/joypad.asm:383-412), so the drain presses.
local function drain(cap) local function drain(cap)
for _ = 1, (cap or 3000) do for _ = 1, (cap or 3000) do
local waiting = (screen.messageTimer or 0) > 0
if waiting then Input:overlayPressed("a") end
Input:step() Input:step()
screen:update(1 / 60) screen:update(1 / 60)
if waiting then Input:overlayReleased("a") end
if screen.phase == "menu" or screen.phase == "done" then return true end if screen.phase == "menu" or screen.phase == "done" then return true end
end end
return false return false
+6
View File
@@ -159,10 +159,16 @@ local function newScreen(opts)
return screen, battle, player, save, pushed return screen, battle, player, save, pushed
end end
-- Battle lines end in `prompt`, and PromptButton waits on A or B with no
-- frame countdown (home/joypad.asm:383-412), so a drain has to press like a
-- player does rather than wait for a timer that never runs out.
local function runToMenu(screen, cap) local function runToMenu(screen, cap)
for _ = 1, (cap or 3000) do for _ = 1, (cap or 3000) do
local waiting = (screen.messageTimer or 0) > 0
if waiting then Input:overlayPressed("a") end
Input:step() Input:step()
screen:update(1 / 60) screen:update(1 / 60)
if waiting then Input:overlayReleased("a") end
if screen.phase == "menu" then return true end if screen.phase == "menu" then return true end
end end
return false return false
+6
View File
@@ -157,10 +157,16 @@ local function newBattleOverWorld(inventory)
return screen, battle, world, save, pushed, player return screen, battle, world, save, pushed, player
end end
-- Battle lines end in `prompt`, and PromptButton waits on A or B with no
-- frame countdown (home/joypad.asm:383-412), so a drain has to press like a
-- player does rather than wait for a timer that never runs out.
local function runToMenu(screen, cap) local function runToMenu(screen, cap)
for _ = 1, (cap or 3000) do for _ = 1, (cap or 3000) do
local waiting = (screen.messageTimer or 0) > 0
if waiting then Input:overlayPressed("a") end
Input:step() Input:step()
screen:update(1 / 60) screen:update(1 / 60)
if waiting then Input:overlayReleased("a") end
if screen.phase == "menu" then return true end if screen.phase == "menu" then return true end
end end
return false return false
+39 -21
View File
@@ -262,10 +262,21 @@ local function run(screen, frames)
end end
end end
-- One frame of a drain. Battle lines end in `prompt` and PromptButton waits
-- on A or B with no countdown (home/joypad.asm:383-412); the stats box waits
-- the same way (engine/battle/core.asm:7069), so a drain presses rather than
-- idling for a timer that never runs out.
local function drainStep(screen)
local waiting = (screen.messageTimer or 0) > 0 or screen.phase == "stats-box"
if waiting then Input:overlayPressed("a") end
Input:step()
screen:update(1 / 60)
if waiting then Input:overlayReleased("a") end
end
local function runToMenu(screen, cap) local function runToMenu(screen, cap)
for _ = 1, (cap or 3000) do for _ = 1, (cap or 3000) do
Input:step() drainStep(screen)
screen:update(1 / 60)
if screen.phase == "menu" then return true end if screen.phase == "menu" then return true end
end end
return false return false
@@ -366,8 +377,7 @@ do
local crawled, restarted = false, false local crawled, restarted = false, false
for _ = 1, 3000 do for _ = 1, 3000 do
Input:step() drainStep(screen)
screen:update(1 / 60)
if screen.shownLevel == 5 and (screen.shownExp or 0) > startExp then if screen.shownLevel == 5 and (screen.shownExp or 0) > startExp then
crawled = true crawled = true
end end
@@ -404,8 +414,7 @@ do
local maxBefore = player.maxHp local maxBefore = player.maxHp
screen:submit({ kind = "move", move = "TACKLE" }) screen:submit({ kind = "move", move = "TACKLE" })
for _ = 1, 3000 do for _ = 1, 3000 do
Input:step() drainStep(screen)
screen:update(1 / 60)
if screen.phase == "done" then break end if screen.phase == "done" then break end
end end
check(player.maxHp > maxBefore, "the level-up raised the maximum") check(player.maxHp > maxBefore, "the level-up raised the maximum")
@@ -601,8 +610,7 @@ do
local function runToPhase(screen, phase, cap) local function runToPhase(screen, phase, cap)
for _ = 1, (cap or 4000) do for _ = 1, (cap or 4000) do
Input:step() drainStep(screen)
screen:update(1 / 60)
if screen.phase == phase then return true end if screen.phase == phase then return true end
end end
return false return false
@@ -622,9 +630,15 @@ do
noBattle.enemy.hp = 1 noBattle.enemy.hp = 1
local noTurn = noBattle.turn local noTurn = noBattle.turn
noScreen:submit({ kind = "move", move = "TACKLE" }) noScreen:submit({ kind = "move", move = "TACKLE" })
check(runToPhase(noScreen, "ask-shift"), "the KO stops on OfferSwitch") -- The `para` in BattleText_EnemyIsAboutToUseWillPlayerChangeMon splits the
-- offer (data/text/battle.asm:222-231): the incoming mon is named on an
-- earlier page, and only the last one carries the yes/no box (#1158).
check(runToPhase(noScreen, "shift-intro"), "the KO stops on OfferSwitch")
check((noScreen.message or ""):find("is about to use"), check((noScreen.message or ""):find("is about to use"),
"with BattleText_EnemyIsAboutToUseWillPlayerChangeMon") "with BattleText_EnemyIsAboutToUseWillPlayerChangeMon")
check(runToPhase(noScreen, "ask-shift"), "and its pages reach the question")
check((noScreen.message or ""):find("change POK"),
"whose last page is the one YesNoBox opens over")
-- NO falls through to the send-out with nothing switched and nothing spent. -- NO falls through to the send-out with nothing switched and nothing spent.
noScreen.messageTimer = 0 noScreen.messageTimer = 0
@@ -1083,8 +1097,7 @@ do
-- not ring is anything from the faint onward. -- not ring is anything from the faint onward.
local latched, rangAfterFaint, sawExpLine = false, false, false local latched, rangAfterFaint, sawExpLine = false, false, false
for _ = 1, 3000 do for _ = 1, 3000 do
Input:step() drainStep(screen)
screen:update(1 / 60)
if screen.lowHealthAlarmDisabled then latched = true end if screen.lowHealthAlarmDisabled then latched = true end
if latched and siren then rangAfterFaint = true end if latched and siren then rangAfterFaint = true end
if latched and (screen.message or ""):find("EXP") then sawExpLine = true end if latched and (screen.message or ""):find("EXP") then sawExpLine = true end
@@ -1155,7 +1168,9 @@ do
eq(screen.phase, "refuse-move", "the disabled row is refused") eq(screen.phase, "refuse-move", "the disabled row is refused")
eq(screen.message, "The move is DISABLED!", "with BattleText_TheMoveIsDisabled") eq(screen.message, "The move is DISABLED!", "with BattleText_TheMoveIsDisabled")
eq(wild.hp, before, "and the enemy got no free turn") eq(wild.hp, before, "and the enemy got no free turn")
run(screen, 120) -- Both refusal lines end in `prompt` (data/text/battle.asm:315-323), so the
-- list comes back on a press, not on a timer.
tap("a")
eq(screen.phase, "moves", "the list comes back") eq(screen.phase, "moves", "the list comes back")
-- The same for a spent row. -- The same for a spent row.
@@ -1168,7 +1183,7 @@ do
eq(screen.message, "There's no PP left for this move!", eq(screen.message, "There's no PP left for this move!",
"with BattleText_TheresNoPPLeftForThisMove") "with BattleText_TheresNoPPLeftForThisMove")
eq(wild.hp, before, "and still no enemy turn") eq(wild.hp, before, "and still no enemy turn")
run(screen, 120) tap("a")
eq(screen.phase, "moves", "and the list comes back again") eq(screen.phase, "moves", "and the list comes back again")
end end
@@ -1221,8 +1236,7 @@ end
-- so the question is reached by pages rather than by one button. -- so the question is reached by pages rather than by one button.
local function runToPhase(screen, phase, cap) local function runToPhase(screen, phase, cap)
for _ = 1, (cap or 900) do for _ = 1, (cap or 900) do
Input:step() drainStep(screen)
screen:update(1 / 60)
if screen.phase == phase then return true end if screen.phase == phase then return true end
end end
return false return false
@@ -1236,11 +1250,15 @@ do
check(runToPhase(screen, "ask-forget"), "and its pages run into the question") check(runToPhase(screen, "ask-forget"), "and its pages run into the question")
eq(screen.message, "move to make room\nfor EMBER?", eq(screen.message, "move to make room\nfor EMBER?",
"whose last page is the one YesNoBox opens over") "whose last page is the one YesNoBox opens over")
run(screen, 60)
local tap = tapper(screen) local tap = tapper(screen)
-- The question's own `prompt` is read first and YesNoBox opens after it,
-- exactly as OfferSwitch does (engine/battle/core.asm:3298-3305), so the
-- answer is the second press, not the first.
tap("a")
eq(screen.phase, "ask-forget", "the last page holds until it is read")
tap("b") tap("b")
eq(screen.phase, "stop-learning", "NO there asks whether to stop learning") eq(screen.phase, "stop-learning", "NO there asks whether to stop learning")
run(screen, 60) tap("a")
tap("b") tap("b")
eq(screen.phase, "learn-intro", "and NO to THAT reprints the ask") eq(screen.phase, "learn-intro", "and NO to THAT reprints the ask")
check(runToPhase(screen, "ask-forget"), "which is a loop, not an exit") check(runToPhase(screen, "ask-forget"), "which is a loop, not an exit")
@@ -1251,8 +1269,8 @@ end
do do
local screen, lead = learnScreen() local screen, lead = learnScreen()
check(runToPhase(screen, "ask-forget"), "the pages reach the question") check(runToPhase(screen, "ask-forget"), "the pages reach the question")
run(screen, 60)
local tap = tapper(screen) local tap = tapper(screen)
tap("a") -- read the question
tap("a") -- YES tap("a") -- YES
eq(screen.phase, "choose-forget", "YES opens the picker") eq(screen.phase, "choose-forget", "YES opens the picker")
tap("a") -- slot 1 tap("a") -- slot 1
@@ -1263,9 +1281,9 @@ end
do do
local screen, lead = learnScreen() local screen, lead = learnScreen()
check(runToPhase(screen, "ask-forget"), "the pages reach the question") check(runToPhase(screen, "ask-forget"), "the pages reach the question")
run(screen, 60)
local tap = tapper(screen) local tap = tapper(screen)
tap("a") tap("a") -- read the question
tap("a") -- YES
eq(screen.phase, "choose-forget", "the picker is up") eq(screen.phase, "choose-forget", "the picker is up")
screen.forgetIndex = 4 screen.forgetIndex = 4
tap("a") tap("a")
@@ -1273,7 +1291,7 @@ do
eq(screen.message, "HM moves can't be\nforgotten now.", eq(screen.message, "HM moves can't be\nforgotten now.",
"with MoveCantForgetHMText") "with MoveCantForgetHMText")
eq(screen.forgetIndex, 1, "and `jr .loop` puts the cursor back on slot 1") eq(screen.forgetIndex, 1, "and `jr .loop` puts the cursor back on slot 1")
run(screen, 60) tap("a")
eq(screen.phase, "choose-forget", "the picker is still up after the line") eq(screen.phase, "choose-forget", "the picker is still up after the line")
eq(lead.moves[4].id, "SURF", "and SURF is still there") eq(lead.moves[4].id, "SURF", "and SURF is still there")
end end
+10 -5
View File
@@ -356,11 +356,14 @@ local optionsGame, optionsInput = newGame(Save.newGame())
local options = OptionsMenu.new(optionsGame, { local options = OptionsMenu.new(optionsGame, {
options = Save.defaultOptions(), options = Save.defaultOptions(),
}) })
-- The cart's seven value rows, then the port's audio, speed and display -- The cart's seven value rows, then CONTROLS, the port's audio, speed and
-- rows, then CANCEL -- which is what makes this screen scroll. -- display rows, the three touch rows and CANCEL -- which is what makes this
check("sixteen rows", #OptionsMenu.ROWS, 16) -- screen scroll. The touch three are gated to mobile by buildRows; ROWS
-- itself carries every descriptor.
check("twenty rows", #OptionsMenu.ROWS, 20)
check("the cart's rows come first", OptionsMenu.ROWS[7].key, "frame") check("the cart's rows come first", OptionsMenu.ROWS[7].key, "frame")
check("then the port's audio group", OptionsMenu.ROWS[8].key, "musicVol") check("then the rebind screen", OptionsMenu.ROWS[8].id, "controls")
check("then the port's audio group", OptionsMenu.ROWS[9].key, "musicVol")
check("last row is CANCEL", OptionsMenu.ROWS[#OptionsMenu.ROWS].cancel, true) check("last row is CANCEL", OptionsMenu.ROWS[#OptionsMenu.ROWS].cancel, true)
check("starts on TEXT SPEED", options:row().key, "textSpeed") check("starts on TEXT SPEED", options:row().key, "textSpeed")
check("default text speed", options.options.textSpeed, "MID") check("default text speed", options.options.textSpeed, "MID")
@@ -401,7 +404,9 @@ local exiting = OptionsMenu.new(exitGame, {
options = Save.defaultOptions(), options = Save.defaultOptions(),
onDone = function(o) savedOptions = o end, onDone = function(o) savedOptions = o end,
}) })
exiting.index = #OptionsMenu.ROWS -- The screen's own rows, not ROWS: buildRows drops the touch three off a
-- desktop, so the raw descriptor count overshoots CANCEL.
exiting.index = #exiting.rows
exitInput:press("a") exitInput:press("a")
exiting:update(0) exiting:update(0)
check("CANCEL leaves", savedOptions ~= nil, true) check("CANCEL leaves", savedOptions ~= nil, true)
+5 -2
View File
@@ -17,14 +17,17 @@ local Nests = require("src.core.gen2.Nests")
-- Landmark indices from constants/landmark_constants.asm: Johto runs below -- Landmark indices from constants/landmark_constants.asm: Johto runs below
-- PALLET_TOWN ($2e), Kanto from it up to ROUTE_28. -- PALLET_TOWN ($2e), Kanto from it up to ROUTE_28.
local DATA = { local DATA = {
maps = { -- gen2Maps, matching Game2's key for the Gold map table.
gen2Maps = {
ROUTE_29 = { landmark = 2 }, -- Johto ROUTE_29 = { landmark = 2 }, -- Johto
ILEX_FOREST = { landmark = 11 }, -- Johto ILEX_FOREST = { landmark = 11 }, -- Johto
ROUTE_1 = { landmark = 0x2f }, -- Kanto ROUTE_1 = { landmark = 0x2f }, -- Kanto
UNION_CAVE_1F = { landmark = 9 }, -- Johto UNION_CAVE_1F = { landmark = 9 }, -- Johto
NOWHERE = { }, -- no landmark at all NOWHERE = { }, -- no landmark at all
}, },
encounters = { -- Game2 loads the encounter tables under gen2Encounters, not `encounters`:
-- the flat name is Gen 1's and Gen2Compat only maps it for mods.
gen2Encounters = {
grass = { grass = {
ROUTE_29 = { slots = { ROUTE_29 = { slots = {
MORN = { { species = "PIDGEY" }, { species = "SENTRET" } }, MORN = { { species = "PIDGEY" }, { species = "SENTRET" } },
+8 -5
View File
@@ -417,22 +417,25 @@ check("only the written byte is stored", memCount, 1)
local _, _, _, loadReport = Save.load("gold") local _, _, _, loadReport = Save.load("gold")
check("load reports a clean save", Save.emptyReport(loadReport), true) check("load reports a clean save", Save.emptyReport(loadReport), true)
-- A second write backs the first up, so the previous file is always recoverable. -- A second write backs the first up, so the previous file is always
-- recoverable. The file lives at saves/<version>/<slot>.lua since Gold grew
-- launcher slots (#1107); the flat save_gold.lua is only the migration source.
local SLOT = "saves/gold/slot1.lua"
written.player.money = 5555 written.player.money = 5555
Save.save(written) Save.save(written)
check("backup written", files["save_gold.lua.bak"] ~= nil, true) check("backup written", files[SLOT .. ".bak"] ~= nil, true)
check("new value loads", Save.load("gold").player.money, 5555) check("new value loads", Save.load("gold").player.money, 5555)
-- A corrupt main file falls back to the backup rather than losing the game. -- A corrupt main file falls back to the backup rather than losing the game.
files["save_gold.lua"] = "this is not a lua table" files[SLOT] = "this is not a lua table"
local recoveredSave, how = Save.load("gold") local recoveredSave, how = Save.load("gold")
check("recovered from the backup", recoveredSave ~= nil, true) check("recovered from the backup", recoveredSave ~= nil, true)
check("recovery reported", how, "bak") check("recovery reported", how, "bak")
check("backup held the previous money", recoveredSave.player.money, 4321) check("backup held the previous money", recoveredSave.player.money, 4321)
-- A staged .tmp is preferred over the backup: it is the newer of the two. -- A staged .tmp is preferred over the backup: it is the newer of the two.
files["save_gold.lua"] = nil files[SLOT] = nil
files["save_gold.lua.tmp"] = files["save_gold.lua.bak"] files[SLOT .. ".tmp"] = files[SLOT .. ".bak"]
local staged, stagedHow = Save.load("gold") local staged, stagedHow = Save.load("gold")
check("recovered from the staged copy", staged ~= nil, true) check("recovered from the staged copy", staged ~= nil, true)
check("staged recovery reported", stagedHow, "tmp") check("staged recovery reported", stagedHow, "tmp")
+2 -1
View File
@@ -3315,7 +3315,8 @@ local function grassLatchChecks()
check(hp.jumping, "and it is a jump") check(hp.jumping, "and it is a jump")
check(not hp.grassShake, "no rustle spawns for the airborne cells") check(not hp.grassShake, "no rustle spawns for the airborne cells")
check(not hp.inGrass, "and IN_GRASS is clear for the whole hop") check(not hp.inGrass, "and IN_GRASS is clear for the whole hop")
for _ = 1, PlayerMod.STEP_FRAMES + 1 do hopGrass:step() end -- A hop clears two cells, so it runs two step-times, not one (#1165).
for _ = 1, PlayerMod.STEP_FRAMES * 2 + 1 do hopGrass:step() end
check(hp.inGrass, "the landing tile latches it once the hop ends") check(hp.inGrass, "the landing tile latches it once the hop ends")
end end
grassLatchChecks() grassLatchChecks()
@@ -48,10 +48,15 @@ function fs.load(path)
end end
function fs.getDirectoryItems(path) function fs.getDirectoryItems(path)
local items = {} local items = {}
-- -print plus a basename in Lua, not -printf: that is a GNU extension and
-- BSD find (macOS) fails the whole call, which silently emptied the listing
-- and left the probe mod undiscovered.
local pipe = io.popen("find " .. quote(full(path)) local pipe = io.popen("find " .. quote(full(path))
.. " -mindepth 1 -maxdepth 1 -printf '%f\\n' 2>/dev/null") .. " -mindepth 1 -maxdepth 1 -print 2>/dev/null")
if pipe then if pipe then
for item in pipe:lines() do items[#items + 1] = item end for item in pipe:lines() do
items[#items + 1] = item:match("[^/]+$") or item
end
pipe:close() pipe:close()
end end
table.sort(items) table.sort(items)
+12 -3
View File
@@ -21,6 +21,7 @@ local Net = require("src.link.Net")
local Pokemon = require("src.pokemon.Pokemon") local Pokemon = require("src.pokemon.Pokemon")
local Protocol = require("src.link.Protocol") local Protocol = require("src.link.Protocol")
local Runtime = require("src.mods.Runtime") local Runtime = require("src.mods.Runtime")
local Session = require("src.link.Session")
local S = require("tests.harness").suite("mod link") local S = require("tests.harness").suite("mod link")
local check, eq = S.check, S.eq local check, eq = S.check, S.eq
@@ -529,11 +530,19 @@ local function linkGame(name, species, data)
return { data = data or Data, save = save, stack = stack, input = mkInput() } return { data = data or Data, save = save, stack = stack, input = mkInput() }
end end
-- LinkState talks to a Session, not to a raw transport (src/link/LinkState.lua
-- :75 wraps every backend the same way), so a loopback end has to be wrapped
-- here too or :update reaches for a getStatus the transport does not have.
local function linkSession(transport, role)
return Session.new(transport, { role = role, kind = "link" })
end
-- two paired states, host already listening and guest already dialling -- two paired states, host already listening and guest already dialling
local function pairStates(gameA, gameB) local function pairStates(gameA, gameB)
local netA, netB = Net.loopbackPair() local netA, netB = Net.loopbackPair()
local host, guest = LinkState.new(gameA), LinkState.new(gameB) local host, guest = LinkState.new(gameA), LinkState.new(gameB)
host.net, guest.net = netA, netB host.net = linkSession(netA, "host")
guest.net = linkSession(netB, "guest")
host.stage, guest.stage = "hosting", "joining" host.stage, guest.stage = "hosting", "joining"
gameA.stack:push(host) gameA.stack:push(host)
gameB.stack:push(guest) gameB.stack:push(guest)
@@ -573,7 +582,7 @@ check(host.trade.strict, "a v2 verdict unpacks strictly")
local gameOld = linkGame("RED", "PIDGEY") local gameOld = linkGame("RED", "PIDGEY")
local oldNet, peerNet = Net.loopbackPair() local oldNet, peerNet = Net.loopbackPair()
local v1guest = LinkState.new(gameOld) local v1guest = LinkState.new(gameOld)
v1guest.net = oldNet v1guest.net = linkSession(oldNet, "guest")
v1guest.stage = "joining" v1guest.stage = "joining"
gameOld.stack:push(v1guest) gameOld.stack:push(v1guest)
v1guest:update(1 / 60) v1guest:update(1 / 60)
@@ -591,7 +600,7 @@ check(not v1guest.trade.strict, "the v1 path keeps the old unpack rules")
local gameLone = linkGame("RED", "PIDGEY") local gameLone = linkGame("RED", "PIDGEY")
local loneNet, silentNet = Net.loopbackPair() local loneNet, silentNet = Net.loopbackPair()
local v1host = LinkState.new(gameLone) local v1host = LinkState.new(gameLone)
v1host.net = loneNet v1host.net = linkSession(loneNet, "host")
v1host.stage = "hosting" v1host.stage = "hosting"
gameLone.stack:push(v1host) gameLone.stack:push(v1host)
v1host:update(1 / 60) v1host:update(1 / 60)
+16
View File
@@ -64,6 +64,17 @@ local function freshGame(mapX, mapY)
worldViewSize = function() return 160, 144 end, worldViewSize = function() return 160, 144 end,
setSGBZones = function() end, setSGBZones = function() end,
} }
-- OverworldState is a singleton and :enter does not clear the movement
-- latches, so a scenario that runs after a few thousand other checks can
-- inherit a half-finished script and never walk. Reset them here: this
-- suite asserts a frame budget, so it has to start from a known state.
OverworldState.scriptMoves = {}
OverworldState.pendingScripts = {}
OverworldState.emote = nil
OverworldState.engaging = false
OverworldState.teleportOut = nil
OverworldState.transitioning = nil
OverworldState.wildEncounterGraceSteps = 0
StateStack:push(OverworldState, "PALLET_TOWN", mapX, mapY, "up") StateStack:push(OverworldState, "PALLET_TOWN", mapX, mapY, "up")
Game.overworld = OverworldState Game.overworld = OverworldState
return pressed return pressed
@@ -114,6 +125,11 @@ end
-- keeps running straight into the lab. -- keeps running straight into the lab.
-- ===================================================================== -- =====================================================================
scenario(function() scenario(function()
-- The trigger tile is in Pallet's north grass, so the escort walk rolls for
-- wild encounters as it goes. Pin the stream: run standalone this suite got
-- one draw sequence and run inside tests/run_tests.lua another, and one of
-- them dropped a battle on top of the escort and ate the frame budget.
math.randomseed(require("tests.harness").SEED)
GameVersion.set("red") GameVersion.set("red")
local pressed = freshGame(8, 2) local pressed = freshGame(8, 2)
local played = {} local played = {}
+12 -4
View File
@@ -2788,15 +2788,23 @@ do
press("down") press("down")
eq(om.index, 25, "cursor reaches CONTROLS") eq(om.index, 25, "cursor reaches CONTROLS")
press("down") press("down")
eq(om.index, 26, "CANCEL stays the fixed final row") eq(om.index, 26, "cursor reaches DATE FORMAT")
eq(om.scroll, 21, "CANCEL keeps the last option boxes on screen") press("down")
eq(om.index, 27, "cursor reaches TIME FORMAT")
press("down")
-- CANCEL is appended after the descriptor list rather than living in it, so
-- it lands one past #rows and the window holds the last six boxes. Counted
-- off #rows so the next row added here is not read as a wrap bug.
local cancelRow = #om.rows + 1
eq(om.index, cancelRow, "CANCEL stays the fixed final row")
eq(om.scroll, cancelRow - 5, "CANCEL keeps the last option boxes on screen")
om:draw() -- smoke: scrolled layout draws under the headless stub om:draw() -- smoke: scrolled layout draws under the headless stub
press("a") press("a")
check(popped, "A on CANCEL closes the options menu") check(popped, "A on CANCEL closes the options menu")
local om2 = OptionsMenu.new(og) local om2 = OptionsMenu.new(og)
OInput.pressed = { up = true }; om2:update(1 / 60); OInput.pressed = {} OInput.pressed = { up = true }; om2:update(1 / 60); OInput.pressed = {}
eq(om2.index, 26, "up from the top wraps to CANCEL") eq(om2.index, cancelRow, "up from the top wraps to CANCEL")
eq(om2.scroll, 21, "wrapping to CANCEL scrolls to the tail") eq(om2.scroll, cancelRow - 5, "wrapping to CANCEL scrolls to the tail")
-- headless-safe: no love.audio, setters only update internal state -- headless-safe: no love.audio, setters only update internal state
require("src.core.Music").applyOptions(og.save.options) require("src.core.Music").applyOptions(og.save.options)
require("src.core.Sound").applyOptions(og.save.options) require("src.core.Sound").applyOptions(og.save.options)