mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 16:31:05 +02:00
225 lines
9.7 KiB
Lua
225 lines
9.7 KiB
Lua
-- Driver: the Pewter gym guide leaves without walking through you (#241).
|
|
-- scripts/PewterCity.asm:133 snaps him to map 16/22 (cell 12,18 after the +4
|
|
-- border offset) and runs MovementData_PewterGymGuyExit, five NPC_MOVEMENT_RIGHT,
|
|
-- then hides him and puts him back on his object_event spawn. The port retraced
|
|
-- the 41-step escort route instead, first step LEFT into the player's own cell.
|
|
-- POKEPORT_DRIVER=tests/drivers/pewter_guide_exit_bug241_test.lua SHOT_DIR=/tmp/shots love .
|
|
return function(game)
|
|
local U = dofile("tests/drivers/util.lua")
|
|
local DIR = os.getenv("SHOT_DIR") or "/tmp/shots"
|
|
local Flags = require("src.script.Flags")
|
|
local TextBox = require("src.render.TextBox")
|
|
local mapScripts = require("data.scripts.init")
|
|
|
|
local MAP = "PEWTER_CITY"
|
|
local NPC = "PEWTERCITY_YOUNGSTER"
|
|
local TEXT = "TEXT_PEWTERCITY_YOUNGSTER"
|
|
-- engine/events/pewter_guys.asm PewterGymGuyCoords: (35,17) is the plain
|
|
-- east-exit trigger, the one with no head-start pause.
|
|
local TRIG = { x = 35, y = 17 }
|
|
local PARK = { x = 11, y = 18 } -- where the escort leaves the player
|
|
local DROP = { x = 12, y = 18 } -- where it leaves the guide
|
|
|
|
local function check(label, ok)
|
|
U.log(ok and "PASS" or "FAIL", label)
|
|
return ok
|
|
end
|
|
|
|
-- ---- state the escort needs --------------------------------------------
|
|
-- Brock beaten permanently HideObject's him (PewterGym.asm .gymVictory),
|
|
-- so a stale save would show an empty street and read as a broken script.
|
|
Flags.clear(game.save, "EVENT_BEAT_BROCK")
|
|
game.save.objectToggles = game.save.objectToggles or {}
|
|
game.save.objectToggles[MAP] = nil
|
|
game.save.player = game.save.player or {}
|
|
game.save.player.name = game.save.player.name or "RED"
|
|
|
|
-- ---- preconditions the eye cannot check --------------------------------
|
|
local t = game.data.text
|
|
for _, key in ipairs({ "_PewterCityYoungsterYoureATrainerFollowMeText",
|
|
"_PewterCityYoungsterGoTakeOnBrockText" }) do
|
|
check(key .. " resolves to a string",
|
|
type(t[key]) == "string" and t[key] ~= "")
|
|
end
|
|
|
|
local script = mapScripts.get(MAP)
|
|
local escort = script and script.escort
|
|
check("PEWTER_CITY exposes the escort tables", escort ~= nil)
|
|
check(("PewterGymGuyCoords has a plan for the trigger tile (%d,%d)")
|
|
:format(TRIG.x, TRIG.y),
|
|
escort ~= nil and escort.playerPlan(TRIG.x, TRIG.y) ~= nil)
|
|
|
|
local spawn
|
|
for _, o in ipairs(game.data.maps[MAP].objects or {}) do
|
|
if o.name == NPC then spawn = o end
|
|
end
|
|
check(NPC .. " has an object_event", spawn ~= nil)
|
|
check("it spawns on (35,16) as in data/maps/objects/PewterCity.asm",
|
|
spawn ~= nil and spawn.x == 35 and spawn.y == 16)
|
|
|
|
-- ---- dry-run the escort and the exit before anything is on screen ------
|
|
-- Same shape as the parity test: the whole callback chain runs inside one
|
|
-- call because the stubbed TextBox fires its continuation immediately.
|
|
local talk = mapScripts.talkScript(MAP, TEXT)
|
|
check("the youngster's talk handler is the escort", type(talk) == "function")
|
|
local Music = require("src.core.Music")
|
|
local realPlay, realPlayMap = Music.play, Music.playMap
|
|
local realNew = TextBox.new
|
|
local D = { up = { 0, -1 }, down = { 0, 1 }, left = { -1, 0 }, right = { 1, 0 } }
|
|
local moves, dryGuy, dryPlayer
|
|
if type(talk) == "function" then
|
|
Music.play, Music.playMap = function() end, function() end
|
|
TextBox.new = function(_, s, done) return { text = s, done = done } end
|
|
local ok, err = pcall(function()
|
|
moves = {}
|
|
dryGuy = { cellX = spawn.x, cellY = spawn.y, facing = "down", moving = false }
|
|
dryPlayer = { cellX = TRIG.x, cellY = TRIG.y, facing = "left" }
|
|
local mockGame = {
|
|
data = { text = {} },
|
|
save = { flags = {} },
|
|
stack = { push = function(_, box) if box.done then box.done() end end },
|
|
}
|
|
local mockOw = {
|
|
scriptMoves = {},
|
|
runner = { isRunning = function() return false end },
|
|
player = dryPlayer,
|
|
npcByIndex = function(_, i) return (i == 5) and dryGuy or nil end,
|
|
scriptMove = function(_, ent, dir, tiles, onDone)
|
|
local v = D[dir]
|
|
for _ = 1, (tiles or 1) do
|
|
ent.cellX, ent.cellY = ent.cellX + v[1], ent.cellY + v[2]
|
|
end
|
|
ent.facing = dir
|
|
moves[#moves + 1] = { who = (ent == dryGuy) and "guy" or "player", dir = dir }
|
|
if onDone then onDone() end
|
|
end,
|
|
}
|
|
talk(mockGame, mockOw, dryGuy, nil)
|
|
end)
|
|
Music.play, Music.playMap = realPlay, realPlayMap
|
|
TextBox.new = realNew
|
|
check("the dry run completed", ok)
|
|
if not ok then U.log("dry run error:", tostring(err)) end
|
|
end
|
|
|
|
if moves and dryGuy and dryPlayer then
|
|
local lastPlayer = 0
|
|
for i, m in ipairs(moves) do if m.who == "player" then lastPlayer = i end end
|
|
local exit = {}
|
|
for i = lastPlayer + 1, #moves do exit[#exit + 1] = moves[i].dir end
|
|
U.log("planned exit:", table.concat(exit, ", "),
|
|
("then snap to (%d,%d) facing %s")
|
|
:format(dryGuy.cellX, dryGuy.cellY, tostring(dryGuy.facing)))
|
|
check(("the escort parks the player on (%d,%d)"):format(PARK.x, PARK.y),
|
|
dryPlayer.cellX == PARK.x and dryPlayer.cellY == PARK.y)
|
|
check("MovementData_PewterGymGuyExit is five steps", #exit == 5)
|
|
local allRight = #exit == 5
|
|
for _, d in ipairs(exit) do if d ~= "right" then allRight = false end end
|
|
check("every exit step is RIGHT, away from the player", allRight)
|
|
check("the guide is snapped back to his spawn (35,16) facing down",
|
|
dryGuy.cellX == spawn.x and dryGuy.cellY == spawn.y
|
|
and dryGuy.facing == "down")
|
|
-- a stale target reserves the vacated cell forever in
|
|
-- OverworldState:npcAtCell (OverworldController.lua:1451)
|
|
check("the snap clears the move target so (17,18) is not reserved",
|
|
dryGuy.targetX == nil and dryGuy.targetY == nil
|
|
and dryGuy.moving == false)
|
|
end
|
|
|
|
-- ---- park below the trigger tile ---------------------------------------
|
|
-- (35,18) is open road under the trigger and the youngster is on (35,16), so
|
|
-- stepping up onto (35,17) puts the player between them and fires the script.
|
|
U.teleport(game, MAP, TRIG.x, TRIG.y + 1, "up")
|
|
U.wait(10)
|
|
local ow = game.overworld
|
|
|
|
for _, cell in ipairs({ { PARK.x, PARK.y }, { DROP.x, DROP.y },
|
|
{ 13, 18 }, { 14, 18 }, { 15, 18 },
|
|
{ 16, 18 }, { 17, 18 } }) do
|
|
check(("PEWTER_CITY (%d,%d) is walkable gym road"):format(cell[1], cell[2]),
|
|
ow.map:isWalkableCell(cell[1], cell[2]))
|
|
end
|
|
check("PEWTER_CITY (18,18) is the fence that ends the road",
|
|
not ow.map:isWalkableCell(18, 18))
|
|
check("PEWTER_CITY (17,17) is wall, so (17,18) is a dead-end pocket",
|
|
not ow.map:isWalkableCell(17, 17))
|
|
|
|
local function guideNpc()
|
|
for _, n in ipairs(game.overworld.npcs or {}) do
|
|
if n.def and n.def.name == NPC then return n end
|
|
end
|
|
end
|
|
check("the youngster is on the map", guideNpc() ~= nil)
|
|
if not ow.map:isWalkableCell(TRIG.x, TRIG.y + 1) then
|
|
-- a map edit moved the road: degrade to any free neighbour of the
|
|
-- trigger tile rather than leaving the player facing a wall
|
|
for _, s in ipairs({ { 0, 1, "up" }, { 1, 0, "left" }, { -1, 0, "right" } }) do
|
|
local cx, cy = TRIG.x + s[1], TRIG.y + s[2]
|
|
if ow.map:isWalkableCell(cx, cy) and not ow:npcAtCell(cx, cy) then
|
|
U.log("approach cell blocked, standing on", cx, cy, "facing", s[3])
|
|
U.teleport(game, MAP, cx, cy, s[3])
|
|
U.wait(10)
|
|
ow = game.overworld
|
|
break
|
|
end
|
|
end
|
|
end
|
|
if U.shot(game, DIR .. "/bug241_0_before.png") then
|
|
U.log("captured", DIR .. "/bug241_0_before.png")
|
|
end
|
|
|
|
-- ---- step onto the trigger and ride out the escort ----------------------
|
|
-- About 41 lockstep tiles. A is only pressed while a TextBox is up, so a
|
|
-- stray press can never re-arm the escort by talking to him again.
|
|
U.hold(game, "up", 24)
|
|
|
|
local function isBox()
|
|
return getmetatable(game.stack:top()) == TextBox
|
|
end
|
|
local function boxText()
|
|
local top = game.stack:top()
|
|
if getmetatable(top) ~= TextBox then return "" end
|
|
local parts = {}
|
|
for _, page in ipairs(top.pages or {}) do
|
|
if type(page) == "table" then
|
|
for _, line in ipairs(page) do parts[#parts + 1] = tostring(line) end
|
|
end
|
|
end
|
|
return table.concat(parts, " ")
|
|
end
|
|
|
|
local arrived = false
|
|
for _ = 1, 1200 do
|
|
local parked = ow.player.cellX == PARK.x and ow.player.cellY == PARK.y
|
|
if parked and isBox() and #ow.scriptMoves == 0 then arrived = true break end
|
|
if isBox() and #ow.scriptMoves == 0 then U.tap(game, "a") end
|
|
U.wait(2)
|
|
end
|
|
check("the escort walked and the last box is up", arrived)
|
|
U.log(("player is on (%d,%d)"):format(ow.player.cellX, ow.player.cellY))
|
|
local guide = guideNpc()
|
|
if guide then
|
|
U.log(("guide is on (%d,%d) facing %s")
|
|
:format(guide.cellX, guide.cellY, tostring(guide.facing)))
|
|
end
|
|
check(("the escort parked the player on (%d,%d)"):format(PARK.x, PARK.y),
|
|
ow.player.cellX == PARK.x and ow.player.cellY == PARK.y)
|
|
check(("the guide is standing beside him on (%d,%d)"):format(DROP.x, DROP.y),
|
|
guide ~= nil and guide.cellX == DROP.x and guide.cellY == DROP.y)
|
|
U.log("box reads:", (boxText():gsub("%s+", " ")))
|
|
U.wait(20)
|
|
if U.shot(game, DIR .. "/bug241_1_atgym.png") then
|
|
U.log("captured", DIR .. "/bug241_1_atgym.png")
|
|
end
|
|
|
|
-- ---- hand off, then stay out of the way --------------------------------
|
|
U.log("The youngster has walked you to the PEWTER GYM door and is on your right.")
|
|
U.log("Press A to close the box and watch him go: five steps RIGHT, away from")
|
|
U.log("you, then he blinks out and turns up again in the north-east of town")
|
|
U.log("facing down. He must never step onto your cell (#241).")
|
|
|
|
while true do
|
|
coroutine.yield()
|
|
end
|
|
end
|