mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-19 04:06:10 +02:00
CLOSES #223, CLOSES #233, CLOSES #236, CLOSES #240, CLOSES #241, CLOSES #249, CLOSES #252, CLOSES #255, CLOSES #257, CLOSES #258, CLOSES #263, CLOSES #265, CLOSES #274, CLOSES #275, CLOSES #276, CLOSES #279, CLOSES #280, CLOSES #282, CLOSES #283, CLOSES #287, CLOSES #291, CLOSES #292, CLOSES #293, CLOSES #301, CLOSES #304, CLOSES #315, CLOSES #316, CLOSES #317, CLOSES #321, CLOSES #322, CLOSES #330
This commit is contained in:
@@ -0,0 +1,224 @@
|
||||
-- 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
|
||||
Reference in New Issue
Block a user