mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 08:21:02 +02:00
411 lines
16 KiB
Lua
411 lines
16 KiB
Lua
-- Pikachu stays in its ball until the rival fight (#1009) and clears (4,3) for the rival (#1021); the Route 15 leg only instruments #920.
|
|
-- pokeyellow scripts/OaksLab.asm, OaksLab_2.asm.
|
|
return function(game)
|
|
local U = dofile("tests/drivers/util.lua")
|
|
local GameVersion = require("src.core.GameVersion")
|
|
local MapScripts = require("src.script.MapScripts")
|
|
local Commands = require("src.script.Commands")
|
|
local PF = require("src.world.PikachuFollower")
|
|
local Pokemon = require("src.pokemon.Pokemon")
|
|
local Bag = require("src.inventory.Bag")
|
|
local BattleState = require("src.battle.BattleState")
|
|
|
|
local SHOT_DIR = os.getenv("SHOT_DIR") or "/tmp/shots"
|
|
local LAB = "OAKS_LAB"
|
|
local RIVAL = 1 -- OAKSLAB_RIVAL, object index 1
|
|
local GIFT = { x = 5, y = 3 } -- where OaksLabRLE_PlayerWalksToOak ends
|
|
|
|
local function check(label, ok)
|
|
U.log(ok and "PASS" or "FAIL", label)
|
|
return ok
|
|
end
|
|
local function idle()
|
|
while true do coroutine.yield() end
|
|
end
|
|
local function ow() return game.overworld end
|
|
local function follower() return PF.current(game.overworld) end
|
|
local function ctx()
|
|
return { game = game, save = game.save, overworld = game.overworld }
|
|
end
|
|
local function where(npc)
|
|
if not npc then return "gone" end
|
|
return "(" .. npc.cellX .. "," .. npc.cellY .. ") facing "
|
|
.. tostring(npc.facing)
|
|
end
|
|
|
|
-- one player step; false when refused, so a caller can route around a map edit
|
|
local function stepOnce(dir)
|
|
local p = ow().player
|
|
local x0, y0 = p.cellX, p.cellY
|
|
for _ = 1, 60 do
|
|
table.insert(game.input.pressQueue, dir)
|
|
game.input.state[dir] = true
|
|
coroutine.yield()
|
|
p = ow().player
|
|
if p.cellX ~= x0 or p.cellY ~= y0 then break end
|
|
end
|
|
game.input.state[dir] = false
|
|
for _ = 1, 40 do
|
|
if not ow().player.moving then break end
|
|
U.wait(1)
|
|
end
|
|
U.wait(3)
|
|
p = ow().player
|
|
return p.cellX ~= x0 or p.cellY ~= y0
|
|
end
|
|
|
|
-- ---------------------------------------------------------------- checks
|
|
if not check("running the Yellow cache (POKEPORT_VERSION=yellow)",
|
|
GameVersion.isYellow()) then
|
|
U.log("Red and Blue have no follower and no Yellow lab script, so every")
|
|
U.log("line below would fail for the wrong reason.")
|
|
idle()
|
|
end
|
|
check("SPRITE_PIKACHU resolves in the sprite table",
|
|
game.data.sprites ~= nil and game.data.sprites.SPRITE_PIKACHU ~= nil)
|
|
check("PikachuFollower.oaksLabMakeWay exists",
|
|
type(PF.oaksLabMakeWay) == "function")
|
|
check("the pikachu_make_way verb exists",
|
|
type(Commands.pikachu_make_way) == "function")
|
|
check("and it blocks the runner while the walk plays",
|
|
Commands.meta.pikachu_make_way ~= nil
|
|
and Commands.meta.pikachu_make_way.blocking == true)
|
|
|
|
local lab = require("data.scripts.oaks_lab_yellow")
|
|
local oakRows = lab.talk.TEXT_OAKSLAB_OAK1
|
|
local function rowIndex(pred)
|
|
for i, r in ipairs(oakRows) do
|
|
if pred(r) then return i end
|
|
end
|
|
return nil
|
|
end
|
|
local iGramps = rowIndex(function(r)
|
|
return r[1] == "show_text" and r[2] == "_OaksLabRivalGrampsText"
|
|
end)
|
|
local iMakeWay = rowIndex(function(r) return r[1] == "pikachu_make_way" end)
|
|
local iShow = rowIndex(function(r)
|
|
return r[1] == "show_object" and r[3] == "OAKSLAB_RIVAL"
|
|
end)
|
|
check("Oak's parcel branch has a make-way row", iMakeWay ~= nil)
|
|
-- the callfar sits after the GRAMPS text and before ShowObject; a row on
|
|
check("it sits between the GRAMPS text and the rival's ShowObject",
|
|
iGramps ~= nil and iMakeWay ~= nil and iShow ~= nil
|
|
and iGramps < iMakeWay and iMakeWay < iShow)
|
|
|
|
local sfx = game.save.options and game.save.options.sfxVol
|
|
if sfx == 0 then
|
|
U.log("sfxVol is 0. Pikachu's cry as it bursts out of the ball will be")
|
|
U.log("silent and a mute run reads exactly like a missing cry -- turn")
|
|
U.log("the sound back up before judging the escape scene.")
|
|
end
|
|
|
|
-- leg 1: in the ball. Level 30 only so the lab battle ends fast
|
|
game.save.flags = game.save.flags or {}
|
|
local flags = game.save.flags
|
|
flags.EVENT_GOT_STARTER = true
|
|
flags.EVENT_CHOSE_PIKACHU = true
|
|
flags.EVENT_FOLLOWED_OAK_INTO_LAB = true
|
|
flags.EVENT_FOLLOWED_OAK_INTO_LAB_2 = true
|
|
flags.EVENT_BATTLED_RIVAL_IN_OAKS_LAB = nil
|
|
flags.EVENT_OAK_ASKED_TO_CHOOSE_MON = true
|
|
flags.EVENT_GOT_POKEDEX = nil
|
|
flags.EVENT_OAK_GOT_PARCEL = nil
|
|
game.save.party = { Pokemon.new(game.data, "PIKACHU", 30) }
|
|
game.save.player.name = "bryan"
|
|
game.save.onBike = false
|
|
game.save.pikachuInBall = true
|
|
|
|
U.teleport(game, LAB, GIFT.x, GIFT.y, "up")
|
|
U.wait(10)
|
|
Commands.show_object(ctx(), LAB, "OAKSLAB_OAK1")
|
|
U.wait(5)
|
|
|
|
-- asked after the teleport: the registry only fills on first require
|
|
check("the Yellow lab module is the one bound to the map",
|
|
MapScripts.talkScript(LAB, "TEXT_OAKSLAB_OAK1") == oakRows)
|
|
|
|
check("straight after the gift there is no follower on the map",
|
|
follower() == nil)
|
|
|
|
-- save compat: nil pikachuInBall falls back to the rival-fight flag, not false
|
|
game.save.pikachuInBall = nil
|
|
U.wait(10)
|
|
check("a pre-#1009 save before the rival fight still has none",
|
|
follower() == nil)
|
|
flags.EVENT_BATTLED_RIVAL_IN_OAKS_LAB = true
|
|
U.wait(20)
|
|
check("a pre-#1009 save past the rival fight keeps its follower",
|
|
follower() ~= nil)
|
|
flags.EVENT_BATTLED_RIVAL_IN_OAKS_LAB = nil
|
|
game.save.pikachuInBall = true
|
|
U.wait(20)
|
|
check("and the in-ball byte puts it away again", follower() == nil)
|
|
|
|
U.shot(game, SHOT_DIR .. "/bug1009_in_ball.png")
|
|
U.log("captured", SHOT_DIR .. "/bug1009_in_ball.png",
|
|
"- player alone at the gift spot")
|
|
|
|
-- --------------------------------------------- leg 2: out of the ball
|
|
Commands.show_object(ctx(), LAB, "OAKSLAB_RIVAL")
|
|
U.wait(5)
|
|
|
|
-- OaksLabRivalChallengesPlayerScript fires from y >= 6 with the starter held
|
|
local walkedClean = true
|
|
for _ = 1, 10 do
|
|
if ow().player.cellY >= 6 then break end
|
|
if follower() then walkedClean = false end
|
|
if not stepOnce("down") then
|
|
if not stepOnce("left") then break end
|
|
end
|
|
end
|
|
check("crossed the lab to the door row with no follower behind",
|
|
walkedClean and follower() == nil)
|
|
check("reached the row the rival challenges from", ow().player.cellY >= 6)
|
|
|
|
-- the escape only spawns the companion once the overworld is back on top
|
|
local sawBattle, escapedAt, spawnCell = false, nil, nil
|
|
for _ = 1, 2500 do
|
|
local o = game.overworld
|
|
local top = game.stack:top()
|
|
if top ~= o then
|
|
if getmetatable(top) == BattleState then sawBattle = true end
|
|
U.tap(game, "a")
|
|
U.wait(3)
|
|
else
|
|
local npc = follower()
|
|
if npc and not escapedAt then
|
|
escapedAt = U.frame()
|
|
spawnCell = { x = npc.cellX, y = npc.cellY, facing = npc.facing }
|
|
U.shot(game, SHOT_DIR .. "/bug1009_escaped.png")
|
|
end
|
|
if escapedAt and not o.runner:isRunning() and #o.scriptMoves == 0 then
|
|
break
|
|
end
|
|
U.wait(2)
|
|
end
|
|
end
|
|
|
|
check("the rival battle actually ran", sawBattle)
|
|
check("the escape scene put a follower on the map", escapedAt ~= nil)
|
|
check("EVENT_BATTLED_RIVAL_IN_OAKS_LAB is set",
|
|
flags.EVENT_BATTLED_RIVAL_IN_OAKS_LAB == true)
|
|
check("the ball is open (save.pikachuInBall false, not nil)",
|
|
game.save.pikachuInBall == false)
|
|
if spawnCell then
|
|
local p = ow().player
|
|
U.log("it appeared at", spawnCell.x, spawnCell.y, "with the player at",
|
|
p.cellX, p.cellY, "facing", p.facing)
|
|
-- OaksLabPikachuEscapesPokeballScript faces the player up and uses spawn
|
|
check("it burst out on the cell behind the player, not beside him",
|
|
spawnCell.x == p.cellX and spawnCell.y == p.cellY + 1)
|
|
U.log("captured", SHOT_DIR .. "/bug1009_escaped.png")
|
|
end
|
|
|
|
-- leg 3: Oak's .DeliverParcelText needs parcel held, no balls, no Pokedex
|
|
flags.EVENT_BATTLED_RIVAL_IN_OAKS_LAB = true
|
|
flags.EVENT_PALLET_AFTER_GETTING_POKEBALLS = nil
|
|
flags.EVENT_BEAT_ROUTE22_RIVAL_1ST_BATTLE = nil
|
|
flags.EVENT_GOT_POKEDEX = nil
|
|
game.save.pikachuInBall = false
|
|
game.save.inventory = game.save.inventory or {}
|
|
game.save.inventory.POKE_BALL = nil
|
|
if game.save.pokedex then game.save.pokedex.owned = {} end
|
|
Bag.add(game.save, "OAKS_PARCEL", 1, game.data)
|
|
|
|
-- the SPRITE_FACING_LEFT case of TryApplyPikachuMovementData
|
|
U.teleport(game, LAB, 4, 3, "right")
|
|
U.wait(10)
|
|
Commands.show_object(ctx(), LAB, "OAKSLAB_OAK1")
|
|
-- he walked out after the lab battle; ShowObject brings him back mid-scene
|
|
Commands.hide_object(ctx(), LAB, "OAKSLAB_RIVAL")
|
|
U.wait(5)
|
|
check("the follower is back for the parcel scene", follower() ~= nil)
|
|
stepOnce("right")
|
|
-- Oak stands on (5,2) and blocks, so this only turns the player up
|
|
U.hold(game, "up", 10)
|
|
U.wait(10)
|
|
|
|
local p = ow().player
|
|
local pika = follower()
|
|
U.log("player at", p.cellX, p.cellY, "facing", p.facing,
|
|
"| Pikachu at", where(pika))
|
|
check("the player is below Oak on row 3", p.cellY == 3 and p.facing == "up")
|
|
local onRivalCell = pika ~= nil and pika.cellX == 4 and pika.cellY == 3
|
|
check("Pikachu is standing on the rival's landing cell (4,3)", onRivalCell)
|
|
if not onRivalCell then
|
|
U.log("Without it on (4,3) the movement data does not apply and the")
|
|
U.log("scene below proves nothing about #1021.")
|
|
end
|
|
U.shot(game, SHOT_DIR .. "/bug1021_before.png")
|
|
|
|
-- the rival's own walk sits in the same scriptMoves list, so ask only
|
|
local function stillWalking(o, npc)
|
|
if npc.moving then return true end
|
|
for _, mv in ipairs(o.scriptMoves) do
|
|
if mv.entity == npc then return true end
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- end pose snapshotted at walk stop; the idle roll turns it later (Func_fc803)
|
|
U.tap(game, "a")
|
|
local leftCell, rivalSeen, rivalArrived, midShot = nil, nil, nil, false
|
|
local settled
|
|
local trace, last = {}, nil
|
|
for f = 1, 1200 do
|
|
local o = game.overworld
|
|
local top = game.stack:top()
|
|
local npc = follower()
|
|
if npc then
|
|
local cell = npc.cellX .. "," .. npc.cellY
|
|
if cell ~= last then
|
|
last = cell
|
|
trace[#trace + 1] = "f" .. f .. " " .. cell
|
|
end
|
|
if not leftCell and not (npc.cellX == 4 and npc.cellY == 3) then
|
|
leftCell = f
|
|
end
|
|
if leftCell and not settled and not stillWalking(o, npc) then
|
|
settled = { x = npc.cellX, y = npc.cellY, facing = npc.facing }
|
|
end
|
|
end
|
|
local rival = o:npcByIndex(RIVAL)
|
|
if rival and not rivalSeen then rivalSeen = f end
|
|
if rival and not rivalArrived and rival.cellX == 4 and rival.cellY == 3
|
|
and not rival.moving then
|
|
rivalArrived = f
|
|
end
|
|
if leftCell and rival and rival.moving and not midShot then
|
|
midShot = true
|
|
U.shot(game, SHOT_DIR .. "/bug1021_stepped_aside.png")
|
|
end
|
|
if rivalArrived and settled then break end
|
|
if top ~= o then U.tap(game, "a") end
|
|
U.wait(2)
|
|
end
|
|
|
|
U.log("Pikachu trace:", table.concat(trace, " | "))
|
|
check("Pikachu moved off (4,3)", leftCell ~= nil)
|
|
check("the rival came in and reached (4,3)", rivalArrived ~= nil)
|
|
if leftCell and rivalSeen then
|
|
-- the callfar runs before ShowObject: the cell is clear before the rival
|
|
check("it started clearing the cell before the rival appeared",
|
|
leftCell <= rivalSeen)
|
|
end
|
|
if settled then
|
|
U.log("the walk ended with Pikachu at", settled.x, settled.y,
|
|
"facing", settled.facing)
|
|
end
|
|
-- OaksLabPikachuMovementData2: STEP_DOWN, STEP_RIGHT, LOOK_UP
|
|
check("it ended one below the player looking up",
|
|
settled ~= nil and settled.x == 5 and settled.y == 4
|
|
and settled.facing == "up")
|
|
U.shot(game, SHOT_DIR .. "/bug1021_rival_in_place.png")
|
|
|
|
-- leg 4: nothing is fixed for #920; this only records ow.npcs and trail.ledgeHop
|
|
local eastMap = game.data.maps.FUCHSIA_CITY
|
|
and game.data.maps.FUCHSIA_CITY.connections
|
|
and game.data.maps.FUCHSIA_CITY.connections.east
|
|
eastMap = eastMap and eastMap.map or "ROUTE_15"
|
|
U.log("Fuchsia's east connection is", eastMap)
|
|
|
|
U.teleport(game, "FUCHSIA_CITY", 10, 12, "down")
|
|
U.wait(10)
|
|
local city = ow().map
|
|
local row = nil
|
|
for y = 0, city.heightCells - 1 do
|
|
if city:isWalkableCell(city.widthCells - 1, y)
|
|
and city:isWalkableCell(city.widthCells - 2, y) then
|
|
row = row or y
|
|
end
|
|
end
|
|
if row then
|
|
U.log("crossing the east seam on row", row)
|
|
U.teleport(game, "FUCHSIA_CITY", city.widthCells - 2, row, "right")
|
|
U.wait(10)
|
|
for _ = 1, 6 do
|
|
if ow().map.id == eastMap then break end
|
|
if not stepOnce("right") then break end
|
|
end
|
|
end
|
|
-- the seam row is blocked (gate rebuild, mod): drop in on the route itself
|
|
local function dropOnRoute()
|
|
U.teleport(game, eastMap, 4, 8, "right")
|
|
U.wait(10)
|
|
local m = ow().map
|
|
if m:isWalkableCell(4, 8) then return end
|
|
for y = 0, m.heightCells - 1 do
|
|
for x = 0, 9 do
|
|
if m:isWalkableCell(x, y) then
|
|
U.teleport(game, eastMap, x, y, "right")
|
|
U.wait(10)
|
|
return
|
|
end
|
|
end
|
|
end
|
|
end
|
|
if ow().map.id ~= eastMap then
|
|
U.log("could not walk the seam; dropping straight on to", eastMap)
|
|
dropOnRoute()
|
|
end
|
|
check("standing on " .. eastMap, ow().map.id == eastMap)
|
|
|
|
local worstGap, lostAt, steps = 0, nil, 0
|
|
local back = { x = ow().player.cellX, y = ow().player.cellY }
|
|
for i = 1, 10 do
|
|
if not stepOnce("right") then
|
|
if not stepOnce("down") then break end
|
|
end
|
|
local o = game.overworld
|
|
if o.map.id ~= eastMap then
|
|
-- the route's gate is a warp, and an arrival respawn is not the stall
|
|
U.log("step", i, "walked into", o.map.id, "- stepping back out")
|
|
U.teleport(game, eastMap, back.x, back.y, "left")
|
|
U.wait(10)
|
|
break
|
|
end
|
|
steps = i
|
|
back.x, back.y = o.player.cellX, o.player.cellY
|
|
local npc = follower()
|
|
local p2 = o.player
|
|
local hop = o.pikachuTrail and o.pikachuTrail.ledgeHop
|
|
if npc then
|
|
local gap = math.abs(npc.cellX - p2.cellX)
|
|
+ math.abs(npc.cellY - p2.cellY)
|
|
if gap > worstGap then worstGap = gap end
|
|
U.log("step", i, "player", p2.cellX, p2.cellY, "| Pikachu", where(npc),
|
|
"| gap", gap, "| ledgeHop", tostring(hop))
|
|
else
|
|
lostAt = lostAt or i
|
|
U.log("step", i, "player", p2.cellX, p2.cellY,
|
|
"| Pikachu is not in ow.npcs at all | ledgeHop", tostring(hop))
|
|
end
|
|
end
|
|
U.log("walked", steps, "steps east on", eastMap)
|
|
check("the follower stayed in ow.npcs the whole way", lostAt == nil)
|
|
check("it never fell more than two cells behind", worstGap <= 2)
|
|
if lostAt then
|
|
U.log("it dropped out of ow.npcs on step", lostAt,
|
|
"- that is the shape #920 would take")
|
|
end
|
|
-- an arrival parks the follower under the player (#863), so walk one step
|
|
stepOnce("left")
|
|
U.wait(20)
|
|
U.shot(game, SHOT_DIR .. "/bug920_east_of_fuchsia.png")
|
|
|
|
U.log("Six shots, in story order. bug1009_in_ball: the player alone in the")
|
|
U.log("lab with the starter already in the party. bug1009_escaped: the cry,")
|
|
U.log("then Pikachu on the cell behind him. bug1021_stepped_aside: it walks")
|
|
U.log("down and right on the GRAMPS text while the rival is still off the")
|
|
U.log("map, ending below the player looking up. The near miss to watch for")
|
|
U.log("is the rival arriving first and Pikachu shuffling around him after,")
|
|
U.log("or the walk playing under the box instead of after it.")
|
|
U.log("Talk to Pikachu here for the control case: it answers with a bubble")
|
|
U.log("and a cry, which is the same sprite and the same audio path.")
|
|
U.log("#920 is unfixed. The pad is yours on Route 15; the step log above is")
|
|
U.log("what the triage wants from a stall -- whether it is still in ow.npcs")
|
|
U.log("and whether trail.ledgeHop stayed set after a ledge.")
|
|
|
|
idle()
|
|
end
|