mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-15 07:41:21 +02:00
fix testsZ
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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" } },
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user