-- 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.takeWarp = function(_, def) 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.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")