mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-09-26 13:33:27 -04:00
332 lines
14 KiB
Lua
332 lines
14 KiB
Lua
-- Regression coverage for #540 "Safari Zone: wrong start tile / step count,
|
|
-- black leaving-early dialogue, wrong early-leave tile" (T2, ROM-free).
|
|
--
|
|
-- pret/pokered scripts/SafariZoneGate.asm:
|
|
-- .success (186-198) ends `ld a, PAD_UP / ld c, 3 /
|
|
-- SafariZoneEntranceAutoWalk`, so paying walks the player up through the
|
|
-- gate's north warp instead of leaving him at the counter. Two of those
|
|
-- steps happen with EVENT_IN_SAFARI_ZONE already set, and home/overworld.asm
|
|
-- :307-310 charges every step against wSafariSteps, which is why the game
|
|
-- starts at 502 but reads 500/500 on arrival. The port only counts steps on
|
|
-- the nine interior maps, so the script pays those two itself.
|
|
-- SafariZoneGateSafariZoneWorker1LeavingEarlyText (227-254): YES prints the
|
|
-- return-balls text and auto-walks `PAD_DOWN, c = 3` down to the counter row,
|
|
-- NO prints "Good Luck!" and walks back up through the warp.
|
|
--
|
|
-- The leaving-early prompt must be QUEUED, never pushed: onEnter runs at the
|
|
-- arriving warp Transition's midpoint and Transition:finish pops the top state
|
|
-- on the same frame, so a box pushed there is swallowed (or, on an older
|
|
-- build, drawn over a screen still faded to black).
|
|
|
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
|
|
|
local T = require("tests.harness")
|
|
local MapScripts = require("src.script.MapScripts")
|
|
local ScriptRunner = require("src.script.ScriptRunner")
|
|
|
|
-- ---- stubs for the two UI modules safari.lua requires lazily. A pushed
|
|
-- box runs its continuation immediately, so a whole prompt chain resolves
|
|
-- inside the push that started it.
|
|
|
|
local answer = true
|
|
package.loaded["src.render.TextBox"] = {
|
|
new = function(_, text, done, opts)
|
|
local box = { text = text, opts = opts }
|
|
box.done = function()
|
|
if opts and opts.choice then
|
|
opts.choice(answer)
|
|
elseif done then
|
|
done()
|
|
end
|
|
end
|
|
return box
|
|
end,
|
|
soundOpts = function(_, sound, opts)
|
|
opts = opts or {}
|
|
opts.auto = { sound = sound, wait = true }
|
|
return opts
|
|
end,
|
|
}
|
|
package.loaded["src.ui.ChoiceBox"] = {
|
|
new = function(_, cb) return { done = function() cb(answer) end } end,
|
|
}
|
|
|
|
local contribution = dofile("data/scripts/safari.lua")
|
|
local gate = contribution.SAFARI_ZONE_GATE
|
|
T.check(gate ~= nil, "the contribution still carries SAFARI_ZONE_GATE")
|
|
|
|
local problems = MapScripts.validateContribution(gate)
|
|
T.eq(#problems, 0, "safari contribution validates cleanly")
|
|
for _, p in ipairs(problems) do T.check(false, "unexpected finding: " .. p) end
|
|
|
|
-- ---- a minimal game/overworld pair
|
|
|
|
local NORTH_WARP = { x = 4, y = 0, destMap = "SAFARI_ZONE_CENTER", destWarp = 2 }
|
|
|
|
local function newWorld(cellX, cellY)
|
|
local w = {
|
|
pushed = {}, moves = {}, warps = {}, queued = {},
|
|
player = { cellX = cellX, cellY = cellY },
|
|
}
|
|
w.map = {
|
|
id = "SAFARI_ZONE_GATE",
|
|
warpAtCell = function(_, cx, cy)
|
|
if cy == 0 and (cx == 3 or cx == 4) then
|
|
return { index = cx == 3 and 3 or 4, def = NORTH_WARP }
|
|
end
|
|
end,
|
|
}
|
|
w.scriptMove = function(_, entity, dir, tiles, onDone)
|
|
w.moves[#w.moves + 1] = { entity = entity, dir = dir, tiles = tiles,
|
|
onDone = onDone }
|
|
end
|
|
w.refreshStandingOnWarp = function()
|
|
w.standingOnWarp = w.map:warpAtCell(w.player.cellX, w.player.cellY) ~= nil
|
|
end
|
|
w.takeWarp = function(_, def)
|
|
T.check(w.standingOnWarp, "scripted admission refreshes source warp flag before takeWarp")
|
|
w.warps[#w.warps + 1] = def
|
|
end
|
|
w.startWarpTo = function(_, mapId, x, y, facing)
|
|
w.warps[#w.warps + 1] = { startWarpTo = mapId, x = x, y = y,
|
|
facing = facing }
|
|
end
|
|
w.queueScript = function(_, rows) w.queued[#w.queued + 1] = rows end
|
|
return w
|
|
end
|
|
|
|
local function newGame(world, money)
|
|
local game = {
|
|
data = {
|
|
text = {},
|
|
maps = { SAFARI_ZONE_CENTER = {
|
|
warps = { { x = 14, y = 25 }, { x = 15, y = 25 } } } },
|
|
},
|
|
save = { player = { name = "RED" }, money = money or 3000 },
|
|
}
|
|
game.stack = { push = function(_, state)
|
|
world.pushed[#world.pushed + 1] = state
|
|
if state.done then state.done() end
|
|
end }
|
|
return game
|
|
end
|
|
|
|
-- ---- paying walks into the zone and pays the two gate steps
|
|
|
|
answer = true
|
|
local ow = newWorld(4, 2)
|
|
local game = newGame(ow)
|
|
T.eq(gate.onStep(game, ow, 4, 2), true, "the trigger cell claims the step")
|
|
-- SafariZoneGateDefaultScript displays TEXT_..._WORKER1_1 before dispatching
|
|
-- to the join prompt (scripts/SafariZoneGate.asm:22-24) (#2205)
|
|
T.check(ow.pushed[1] and ow.pushed[1].text:find("SAFARI ZONE"),
|
|
"the worker welcomes you before the join prompt")
|
|
T.eq(ow.player.facing, "right", "SPRITE_FACING_RIGHT is written on both branches")
|
|
local joinBox = ow.pushed[2]
|
|
T.check(joinBox ~= nil and joinBox.opts ~= nil and joinBox.opts.choice ~= nil,
|
|
"the YES/NO rides over the still-visible join text")
|
|
T.check(joinBox and joinBox.opts and joinBox.opts.money ~= nil
|
|
and joinBox.opts.moneyWithChoice == true,
|
|
"MONEY_BOX goes up with the YES/NO")
|
|
T.check(game.save.safari ~= nil, "paying starts the game")
|
|
T.eq(game.save.safari.steps, 502, "wSafariSteps is written as 502")
|
|
T.eq(game.save.safari.balls, 30, "SAFARI_BALLS_RECEIVED is 30")
|
|
T.eq(game.save.money, 2500, "the ¥500 fee is taken")
|
|
|
|
-- .MakePaymentText carries sound_get_item_1 between the "received 30 SAFARI
|
|
-- BALLs!" page and the PA page (scripts/SafariZoneGate.asm:213-217), and the
|
|
-- MONEY box is redrawn with the reduced total (:181-183) (#2205)
|
|
local paidBox = ow.pushed[3]
|
|
T.check(paidBox ~= nil and paidBox.text:find("SAFARI BALLs"),
|
|
"the payment text is its own box")
|
|
T.check(paidBox and paidBox.opts and paidBox.opts.auto
|
|
and paidBox.opts.auto.sound == "Get_Item1",
|
|
"the Get_Item1 fanfare holds the box the balls were handed over in")
|
|
T.check(paidBox and paidBox.opts and paidBox.opts.money ~= nil,
|
|
"the MONEY box stays up through the payment text")
|
|
local paBox = ow.pushed[4]
|
|
T.check(paBox ~= nil and paBox.text:find("PA"), "the PA line follows it")
|
|
T.check(paBox and paBox.opts and paBox.opts.money ~= nil,
|
|
"and the MONEY box is still up for it")
|
|
for _, box in ipairs(ow.pushed) do
|
|
T.check(not box.text:find("Good Luck"),
|
|
"\"Good Luck!\" belongs to the leaving script, never the entry path")
|
|
end
|
|
|
|
T.eq(#ow.moves, 1, "the payment text is followed by the entrance auto-walk")
|
|
T.eq(ow.moves[1].dir, "up", "SafariZoneEntranceAutoWalk walks PAD_UP")
|
|
T.eq(ow.moves[1].tiles, 2, "two cells reach the north warp row")
|
|
T.eq(#ow.warps, 0, "the warp is not taken until the walk lands on it")
|
|
ow.player.cellY = 0
|
|
ow.moves[1].onDone()
|
|
T.eq(game.save.safari.steps, 500,
|
|
"the two gate steps are charged, so the counter reads 500/500 on arrival")
|
|
T.eq(ow.warps[1], NORTH_WARP,
|
|
"a scripted step skips CheckWarpsNoCollision, so the script takes the warp")
|
|
|
|
-- the LEFT trigger cell auto-walks one step right to the counter first:
|
|
-- wCoordIndex is 1-based (home/map_objects.asm:107-121), so `cp 1` is (3,2)
|
|
-- (scripts/SafariZoneGate.asm:31-40) (#2205)
|
|
answer = true
|
|
local leftCell = newWorld(3, 2)
|
|
local leftCellGame = newGame(leftCell)
|
|
T.eq(gate.onStep(leftCellGame, leftCell, 3, 2), true, "the left cell triggers too")
|
|
T.eq(leftCellGame.save.safari, nil, "nothing is paid until the walk finishes")
|
|
T.eq(#leftCell.moves, 1, "the welcome text is followed by the walk right")
|
|
T.eq(leftCell.moves[1].dir, "right", "PAD_RIGHT, c = 1")
|
|
T.eq(leftCell.moves[1].tiles, 1, "one cell reaches (4,2)")
|
|
leftCell.moves[1].onDone()
|
|
T.check(leftCellGame.save.safari ~= nil, "the join prompt runs from (4,2)")
|
|
|
|
-- talking to the worker from anywhere else has no warp above the player, so
|
|
-- the auto-walk stays out of the way and he walks in himself
|
|
answer = true
|
|
local far = newWorld(2, 4)
|
|
local farGame = newGame(far)
|
|
farGame.save.safari = nil
|
|
gate.talk.TEXT_SAFARIZONEGATE_SAFARI_ZONE_WORKER1(farGame, far, nil, nil)
|
|
T.check(farGame.save.safari ~= nil, "the talk path still starts the game")
|
|
T.eq(#far.moves, 0, "no auto-walk from a cell with no warp above it")
|
|
|
|
-- declining walks the player back off the trigger cell, unchanged
|
|
answer = false
|
|
local no = newWorld(4, 2)
|
|
local noGame = newGame(no)
|
|
gate.onStep(noGame, no, 4, 2)
|
|
T.eq(noGame.save.safari, nil, "declining starts no game")
|
|
T.eq(no.moves[1] and no.moves[1].dir, "down", "declining walks you back down")
|
|
|
|
-- ---- leaving early: queued, never pushed
|
|
|
|
local back = newWorld(4, 0)
|
|
local backGame = newGame(back)
|
|
backGame.save.safari = { balls = 7, steps = 300 }
|
|
gate.onEnter(backGame, back)
|
|
T.eq(#back.pushed, 0,
|
|
"onEnter pushes nothing: the arriving Transition pops the top state on the "
|
|
.. "same frame")
|
|
T.eq(#back.queued, 1, "the leaving-early prompt is queued for an idle frame")
|
|
|
|
local rows = back.queued[1]
|
|
T.eq(#ScriptRunner.validate(rows), 0, "the queued rows validate")
|
|
|
|
local function runRows(script, yes)
|
|
local pc, texts, out = 1, {}, { fields = {}, moves = {}, warps = {} }
|
|
local lastCheck = nil
|
|
while pc <= #script do
|
|
local row = script[pc]
|
|
local verb = row[1]
|
|
local jump = nil
|
|
if verb == "ask" then
|
|
texts[#texts + 1] = row[2]
|
|
lastCheck = yes
|
|
elseif verb == "show_text" then
|
|
texts[#texts + 1] = row[2]
|
|
elseif verb == "jump_if_false" then
|
|
if not lastCheck then jump = row[2] end
|
|
elseif verb == "jump" then
|
|
jump = row[2]
|
|
elseif verb == "set_field" then
|
|
out.fields[#out.fields + 1] = { key = row[2], value = row[3] }
|
|
elseif verb == "move_player" then
|
|
out.moves[#out.moves + 1] = { dir = row[2], tiles = row[3] }
|
|
elseif verb == "warp" then
|
|
out.warps[#out.warps + 1] = { map = row[2], x = row[3], y = row[4],
|
|
facing = row[5] }
|
|
end
|
|
if jump == "end" then break end
|
|
if jump then
|
|
local target
|
|
for i, r in ipairs(script) do
|
|
if r[1] == "label" and r[2] == jump then target = i break end
|
|
end
|
|
T.check(target ~= nil, "jump target '" .. tostring(jump) .. "' exists")
|
|
pc = target
|
|
else
|
|
pc = pc + 1
|
|
end
|
|
end
|
|
out.texts = texts
|
|
return out
|
|
end
|
|
|
|
local yes = runRows(rows, true)
|
|
T.eq(yes.texts[1], "_SafariZoneGateSafariZoneWorker1LeavingEarlyText",
|
|
"the worker asks first")
|
|
T.eq(yes.texts[2], "_SafariZoneGateSafariZoneWorker1ReturnSafariBallsText",
|
|
"YES takes the leftover balls back")
|
|
-- .leaving_early prints the return-balls text and nothing else; the good-haul
|
|
-- sign-off belongs to the game-over branch (scripts/SafariZoneGate.asm:85-99)
|
|
T.eq(yes.texts[3], nil, "no good-haul sign-off on the leaving-early branch")
|
|
T.eq(#yes.fields, 1, "the game state is cleared once")
|
|
T.eq(yes.fields[1].key, "safari", "save.safari is the field cleared")
|
|
T.eq(yes.fields[1].value, nil, "set_field with no value assigns nil")
|
|
T.eq(#yes.moves, 1, "YES ends with the exit auto-walk")
|
|
T.eq(yes.moves[1].dir, "down", "SafariZoneEntranceAutoWalk walks PAD_DOWN")
|
|
T.eq(yes.moves[1].tiles, 3, "three cells reach the counter row")
|
|
T.eq(#yes.warps, 0, "the YES branch never warps back into the zone")
|
|
|
|
local stay = runRows(rows, false)
|
|
T.eq(stay.texts[2], "_SafariZoneGateSafariZoneWorker1GoodLuckText",
|
|
"NO wishes you good luck")
|
|
T.eq(#stay.moves, 0, "NO does not walk you to the counter")
|
|
T.eq(#stay.warps, 1, "NO puts you back in the zone")
|
|
T.eq(stay.warps[1].map, "SAFARI_ZONE_CENTER", "back through the entrance")
|
|
T.eq(stay.warps[1].x, 15, "the right-hand warp column comes back on column 15")
|
|
T.eq(stay.warps[1].y, 25, "the entrance row of SAFARI_ZONE_CENTER")
|
|
|
|
-- the left-hand column pairs with the left-hand destination warp
|
|
local left = newWorld(3, 0)
|
|
local leftGame = newGame(left)
|
|
leftGame.save.safari = { balls = 7, steps = 300 }
|
|
gate.onEnter(leftGame, left)
|
|
local leftStay = runRows(left.queued[1], false)
|
|
T.eq(leftStay.warps[1].x, 14, "the left-hand warp column comes back on 14")
|
|
|
|
-- ---- the game-over return: EVENT_SAFARI_GAME_OVER takes the other branch
|
|
-- (scripts/SafariZoneGate.asm:79-94) (#2206)
|
|
|
|
local over = newWorld(4, 0)
|
|
local overGame = newGame(over)
|
|
overGame.save.safariGameOver = true
|
|
gate.onEnter(overGame, over)
|
|
T.eq(#over.pushed, 0, "the game-over branch queues rather than pushes too")
|
|
T.eq(#over.queued, 1, "the sign-off script is queued")
|
|
T.eq(overGame.save.safariGameOver, nil,
|
|
"CheckAndResetEvent clears the flag, so the next arrival asks normally")
|
|
local overRows = over.queued[1]
|
|
T.eq(#ScriptRunner.validate(overRows), 0, "the queued rows validate")
|
|
local overOut = runRows(overRows, true)
|
|
T.eq(overOut.texts[1], "_SafariZoneGateSafariZoneWorker1GoodHaulComeAgainText",
|
|
"the worker signs off with the good-haul text")
|
|
T.eq(#overOut.moves, 1, "then the exit auto-walk runs")
|
|
T.eq(overOut.moves[1].dir, "down", "PAD_DOWN")
|
|
T.eq(overOut.moves[1].tiles, 3, "c = 3, from the warp row down to the counter")
|
|
T.eq(#overOut.warps, 0, "the game-over branch never warps back into the zone")
|
|
|
|
-- wDestinationWarpID $3 is warp_event 4,0
|
|
-- (data/maps/objects/SafariZoneGate.asm:12), the walk's START, not its end
|
|
local FieldDefaults = require("src.world.FieldDefaults")
|
|
local exitWarp = FieldDefaults.fieldValue(nil, "safari", "exitWarp")
|
|
T.check(exitWarp ~= nil, "the safari exit warp is defined")
|
|
T.eq(exitWarp and exitWarp.map, "SAFARI_ZONE_GATE", "it lands in the gate")
|
|
T.eq(exitWarp and exitWarp.x, 4, "on the right-hand north warp column")
|
|
T.eq(exitWarp and exitWarp.y, 0, "on the warp row, not the counter row")
|
|
|
|
-- no game running, or arriving from the town side, asks nothing
|
|
local idle = newWorld(4, 0)
|
|
local idleGame = newGame(idle)
|
|
gate.onEnter(idleGame, idle)
|
|
T.eq(#idle.queued, 0, "no safari game, no prompt")
|
|
|
|
local south = newWorld(4, 5)
|
|
local southGame = newGame(south)
|
|
southGame.save.safari = { balls = 7, steps = 300 }
|
|
gate.onEnter(southGame, south)
|
|
T.eq(#south.queued, 0, "arriving from Fuchsia asks nothing")
|
|
|
|
package.loaded["src.render.TextBox"] = nil
|
|
package.loaded["src.ui.ChoiceBox"] = nil
|
|
|
|
T.finish("safari_gate_bug540")
|