mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-15 07:41:21 +02:00
fix testsZ
This commit is contained in:
@@ -252,10 +252,15 @@ do
|
||||
local outcome
|
||||
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)
|
||||
for _ = 1, (cap or 3000) do
|
||||
local waiting = (screen.messageTimer or 0) > 0
|
||||
if waiting then Input:overlayPressed("a") end
|
||||
Input:step()
|
||||
screen:update(1 / 60)
|
||||
if waiting then Input:overlayReleased("a") end
|
||||
if screen.phase == "menu" or screen.phase == "done" then return true end
|
||||
end
|
||||
return false
|
||||
|
||||
@@ -159,10 +159,16 @@ local function newScreen(opts)
|
||||
return screen, battle, player, save, pushed
|
||||
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)
|
||||
for _ = 1, (cap or 3000) do
|
||||
local waiting = (screen.messageTimer or 0) > 0
|
||||
if waiting then Input:overlayPressed("a") end
|
||||
Input:step()
|
||||
screen:update(1 / 60)
|
||||
if waiting then Input:overlayReleased("a") end
|
||||
if screen.phase == "menu" then return true end
|
||||
end
|
||||
return false
|
||||
|
||||
@@ -157,10 +157,16 @@ local function newBattleOverWorld(inventory)
|
||||
return screen, battle, world, save, pushed, player
|
||||
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)
|
||||
for _ = 1, (cap or 3000) do
|
||||
local waiting = (screen.messageTimer or 0) > 0
|
||||
if waiting then Input:overlayPressed("a") end
|
||||
Input:step()
|
||||
screen:update(1 / 60)
|
||||
if waiting then Input:overlayReleased("a") end
|
||||
if screen.phase == "menu" then return true end
|
||||
end
|
||||
return false
|
||||
|
||||
@@ -262,10 +262,21 @@ local function run(screen, frames)
|
||||
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)
|
||||
for _ = 1, (cap or 3000) do
|
||||
Input:step()
|
||||
screen:update(1 / 60)
|
||||
drainStep(screen)
|
||||
if screen.phase == "menu" then return true end
|
||||
end
|
||||
return false
|
||||
@@ -366,8 +377,7 @@ do
|
||||
|
||||
local crawled, restarted = false, false
|
||||
for _ = 1, 3000 do
|
||||
Input:step()
|
||||
screen:update(1 / 60)
|
||||
drainStep(screen)
|
||||
if screen.shownLevel == 5 and (screen.shownExp or 0) > startExp then
|
||||
crawled = true
|
||||
end
|
||||
@@ -404,8 +414,7 @@ do
|
||||
local maxBefore = player.maxHp
|
||||
screen:submit({ kind = "move", move = "TACKLE" })
|
||||
for _ = 1, 3000 do
|
||||
Input:step()
|
||||
screen:update(1 / 60)
|
||||
drainStep(screen)
|
||||
if screen.phase == "done" then break end
|
||||
end
|
||||
check(player.maxHp > maxBefore, "the level-up raised the maximum")
|
||||
@@ -601,8 +610,7 @@ do
|
||||
|
||||
local function runToPhase(screen, phase, cap)
|
||||
for _ = 1, (cap or 4000) do
|
||||
Input:step()
|
||||
screen:update(1 / 60)
|
||||
drainStep(screen)
|
||||
if screen.phase == phase then return true end
|
||||
end
|
||||
return false
|
||||
@@ -622,9 +630,15 @@ do
|
||||
noBattle.enemy.hp = 1
|
||||
local noTurn = noBattle.turn
|
||||
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"),
|
||||
"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.
|
||||
noScreen.messageTimer = 0
|
||||
@@ -1083,8 +1097,7 @@ do
|
||||
-- not ring is anything from the faint onward.
|
||||
local latched, rangAfterFaint, sawExpLine = false, false, false
|
||||
for _ = 1, 3000 do
|
||||
Input:step()
|
||||
screen:update(1 / 60)
|
||||
drainStep(screen)
|
||||
if screen.lowHealthAlarmDisabled then latched = true end
|
||||
if latched and siren then rangAfterFaint = 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.message, "The move is DISABLED!", "with BattleText_TheMoveIsDisabled")
|
||||
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")
|
||||
|
||||
-- The same for a spent row.
|
||||
@@ -1168,7 +1183,7 @@ do
|
||||
eq(screen.message, "There's no PP left for this move!",
|
||||
"with BattleText_TheresNoPPLeftForThisMove")
|
||||
eq(wild.hp, before, "and still no enemy turn")
|
||||
run(screen, 120)
|
||||
tap("a")
|
||||
eq(screen.phase, "moves", "and the list comes back again")
|
||||
end
|
||||
|
||||
@@ -1221,8 +1236,7 @@ end
|
||||
-- so the question is reached by pages rather than by one button.
|
||||
local function runToPhase(screen, phase, cap)
|
||||
for _ = 1, (cap or 900) do
|
||||
Input:step()
|
||||
screen:update(1 / 60)
|
||||
drainStep(screen)
|
||||
if screen.phase == phase then return true end
|
||||
end
|
||||
return false
|
||||
@@ -1236,11 +1250,15 @@ do
|
||||
check(runToPhase(screen, "ask-forget"), "and its pages run into the question")
|
||||
eq(screen.message, "move to make room\nfor EMBER?",
|
||||
"whose last page is the one YesNoBox opens over")
|
||||
run(screen, 60)
|
||||
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")
|
||||
eq(screen.phase, "stop-learning", "NO there asks whether to stop learning")
|
||||
run(screen, 60)
|
||||
tap("a")
|
||||
tap("b")
|
||||
eq(screen.phase, "learn-intro", "and NO to THAT reprints the ask")
|
||||
check(runToPhase(screen, "ask-forget"), "which is a loop, not an exit")
|
||||
@@ -1251,8 +1269,8 @@ end
|
||||
do
|
||||
local screen, lead = learnScreen()
|
||||
check(runToPhase(screen, "ask-forget"), "the pages reach the question")
|
||||
run(screen, 60)
|
||||
local tap = tapper(screen)
|
||||
tap("a") -- read the question
|
||||
tap("a") -- YES
|
||||
eq(screen.phase, "choose-forget", "YES opens the picker")
|
||||
tap("a") -- slot 1
|
||||
@@ -1263,9 +1281,9 @@ end
|
||||
do
|
||||
local screen, lead = learnScreen()
|
||||
check(runToPhase(screen, "ask-forget"), "the pages reach the question")
|
||||
run(screen, 60)
|
||||
local tap = tapper(screen)
|
||||
tap("a")
|
||||
tap("a") -- read the question
|
||||
tap("a") -- YES
|
||||
eq(screen.phase, "choose-forget", "the picker is up")
|
||||
screen.forgetIndex = 4
|
||||
tap("a")
|
||||
@@ -1273,7 +1291,7 @@ do
|
||||
eq(screen.message, "HM moves can't be\nforgotten now.",
|
||||
"with MoveCantForgetHMText")
|
||||
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(lead.moves[4].id, "SURF", "and SURF is still there")
|
||||
end
|
||||
|
||||
@@ -356,11 +356,14 @@ local optionsGame, optionsInput = newGame(Save.newGame())
|
||||
local options = OptionsMenu.new(optionsGame, {
|
||||
options = Save.defaultOptions(),
|
||||
})
|
||||
-- The cart's seven value rows, then the port's audio, speed and display
|
||||
-- rows, then CANCEL -- which is what makes this screen scroll.
|
||||
check("sixteen rows", #OptionsMenu.ROWS, 16)
|
||||
-- The cart's seven value rows, then CONTROLS, the port's audio, speed and
|
||||
-- display rows, the three touch rows and CANCEL -- which is what makes this
|
||||
-- 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("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("starts on TEXT SPEED", options:row().key, "textSpeed")
|
||||
check("default text speed", options.options.textSpeed, "MID")
|
||||
@@ -401,7 +404,9 @@ local exiting = OptionsMenu.new(exitGame, {
|
||||
options = Save.defaultOptions(),
|
||||
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")
|
||||
exiting:update(0)
|
||||
check("CANCEL leaves", savedOptions ~= nil, true)
|
||||
|
||||
@@ -17,14 +17,17 @@ local Nests = require("src.core.gen2.Nests")
|
||||
-- Landmark indices from constants/landmark_constants.asm: Johto runs below
|
||||
-- PALLET_TOWN ($2e), Kanto from it up to ROUTE_28.
|
||||
local DATA = {
|
||||
maps = {
|
||||
-- gen2Maps, matching Game2's key for the Gold map table.
|
||||
gen2Maps = {
|
||||
ROUTE_29 = { landmark = 2 }, -- Johto
|
||||
ILEX_FOREST = { landmark = 11 }, -- Johto
|
||||
ROUTE_1 = { landmark = 0x2f }, -- Kanto
|
||||
UNION_CAVE_1F = { landmark = 9 }, -- Johto
|
||||
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 = {
|
||||
ROUTE_29 = { slots = {
|
||||
MORN = { { species = "PIDGEY" }, { species = "SENTRET" } },
|
||||
|
||||
@@ -417,22 +417,25 @@ check("only the written byte is stored", memCount, 1)
|
||||
local _, _, _, loadReport = Save.load("gold")
|
||||
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
|
||||
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)
|
||||
|
||||
-- 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")
|
||||
check("recovered from the backup", recoveredSave ~= nil, true)
|
||||
check("recovery reported", how, "bak")
|
||||
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.
|
||||
files["save_gold.lua"] = nil
|
||||
files["save_gold.lua.tmp"] = files["save_gold.lua.bak"]
|
||||
files[SLOT] = nil
|
||||
files[SLOT .. ".tmp"] = files[SLOT .. ".bak"]
|
||||
local staged, stagedHow = Save.load("gold")
|
||||
check("recovered from the staged copy", staged ~= nil, true)
|
||||
check("staged recovery reported", stagedHow, "tmp")
|
||||
|
||||
@@ -3315,7 +3315,8 @@ local function grassLatchChecks()
|
||||
check(hp.jumping, "and it is a jump")
|
||||
check(not hp.grassShake, "no rustle spawns for the airborne cells")
|
||||
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")
|
||||
end
|
||||
grassLatchChecks()
|
||||
|
||||
@@ -48,10 +48,15 @@ function fs.load(path)
|
||||
end
|
||||
function fs.getDirectoryItems(path)
|
||||
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))
|
||||
.. " -mindepth 1 -maxdepth 1 -printf '%f\\n' 2>/dev/null")
|
||||
.. " -mindepth 1 -maxdepth 1 -print 2>/dev/null")
|
||||
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()
|
||||
end
|
||||
table.sort(items)
|
||||
|
||||
@@ -21,6 +21,7 @@ local Net = require("src.link.Net")
|
||||
local Pokemon = require("src.pokemon.Pokemon")
|
||||
local Protocol = require("src.link.Protocol")
|
||||
local Runtime = require("src.mods.Runtime")
|
||||
local Session = require("src.link.Session")
|
||||
|
||||
local S = require("tests.harness").suite("mod link")
|
||||
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() }
|
||||
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
|
||||
local function pairStates(gameA, gameB)
|
||||
local netA, netB = Net.loopbackPair()
|
||||
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"
|
||||
gameA.stack:push(host)
|
||||
gameB.stack:push(guest)
|
||||
@@ -573,7 +582,7 @@ check(host.trade.strict, "a v2 verdict unpacks strictly")
|
||||
local gameOld = linkGame("RED", "PIDGEY")
|
||||
local oldNet, peerNet = Net.loopbackPair()
|
||||
local v1guest = LinkState.new(gameOld)
|
||||
v1guest.net = oldNet
|
||||
v1guest.net = linkSession(oldNet, "guest")
|
||||
v1guest.stage = "joining"
|
||||
gameOld.stack:push(v1guest)
|
||||
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 loneNet, silentNet = Net.loopbackPair()
|
||||
local v1host = LinkState.new(gameLone)
|
||||
v1host.net = loneNet
|
||||
v1host.net = linkSession(loneNet, "host")
|
||||
v1host.stage = "hosting"
|
||||
gameLone.stack:push(v1host)
|
||||
v1host:update(1 / 60)
|
||||
|
||||
@@ -64,6 +64,17 @@ local function freshGame(mapX, mapY)
|
||||
worldViewSize = function() return 160, 144 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")
|
||||
Game.overworld = OverworldState
|
||||
return pressed
|
||||
@@ -114,6 +125,11 @@ end
|
||||
-- keeps running straight into the lab.
|
||||
-- =====================================================================
|
||||
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")
|
||||
local pressed = freshGame(8, 2)
|
||||
local played = {}
|
||||
|
||||
+12
-4
@@ -2788,15 +2788,23 @@ do
|
||||
press("down")
|
||||
eq(om.index, 25, "cursor reaches CONTROLS")
|
||||
press("down")
|
||||
eq(om.index, 26, "CANCEL stays the fixed final row")
|
||||
eq(om.scroll, 21, "CANCEL keeps the last option boxes on screen")
|
||||
eq(om.index, 26, "cursor reaches DATE FORMAT")
|
||||
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
|
||||
press("a")
|
||||
check(popped, "A on CANCEL closes the options menu")
|
||||
local om2 = OptionsMenu.new(og)
|
||||
OInput.pressed = { up = true }; om2:update(1 / 60); OInput.pressed = {}
|
||||
eq(om2.index, 26, "up from the top wraps to CANCEL")
|
||||
eq(om2.scroll, 21, "wrapping to CANCEL scrolls to the tail")
|
||||
eq(om2.index, cancelRow, "up from the top wraps to CANCEL")
|
||||
eq(om2.scroll, cancelRow - 5, "wrapping to CANCEL scrolls to the tail")
|
||||
-- headless-safe: no love.audio, setters only update internal state
|
||||
require("src.core.Music").applyOptions(og.save.options)
|
||||
require("src.core.Sound").applyOptions(og.save.options)
|
||||
|
||||
Reference in New Issue
Block a user