mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 00:10:56 +02:00
190 lines
8.5 KiB
Lua
190 lines
8.5 KiB
Lua
-- Parity test, the Pallet Town intro cutscene (Oak escort) and the
|
|
-- Oak-speech shrink assets.
|
|
-- Self-contained: run via `luajit tests/parity_intro.lua`; also dofile'd
|
|
-- by tests/run_tests.lua's aggregator.
|
|
--
|
|
-- Sources: scripts/PalletTown.asm, engine/overworld/auto_movement.asm
|
|
-- (PalletMovementScriptPointerTable, RLEList_ProfOakWalkToLab,
|
|
-- RLEList_PlayerWalkToLab), engine/overworld/pathfinding.asm
|
|
-- (FindPathToPlayer), engine/movie/oak_speech/oak_speech.asm.
|
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
|
if not _G.love then _G.love = require("tests.love_stub") end
|
|
local Data = require("src.core.Data")
|
|
if not (Data.maps and Data.maps.PALLET_TOWN) then Data:load() end
|
|
local S = require("tests.harness").suite("parity intro")
|
|
local check, eq = S.check, S.eq
|
|
|
|
local mapScripts = require("data.scripts.init")
|
|
local pallet = mapScripts.get("PALLET_TOWN")
|
|
check(pallet and pallet.escort, "PALLET_TOWN exposes the escort tables")
|
|
local escort = pallet.escort
|
|
|
|
local function joined(t) return table.concat(t, ",") end
|
|
|
|
-- =====================================================================
|
|
-- (A) Oak's walk to the player: FindPathToPlayer from his object spot
|
|
-- (8,5) to one tile below the player (hNPCPlayerYDistance decremented
|
|
-- before the predef). Reducing the greater remaining axis each step
|
|
-- (ties go to X) yields a strict zigzag.
|
|
-- =====================================================================
|
|
eq(joined(escort.oakApproach(10)), "up,right,up,right,up",
|
|
"Oak approach to the left tile (player at 10,1)")
|
|
eq(joined(escort.oakApproach(11)), "right,up,right,up,right,up",
|
|
"Oak approach to the right tile (player at 11,1)")
|
|
|
|
-- walking the approach from (8,5) must land exactly on (playerX, 2)
|
|
for _, px in ipairs({ 10, 11 }) do
|
|
local cx, cy = 8, 5
|
|
local D = { up = { 0, -1 }, down = { 0, 1 }, left = { -1, 0 }, right = { 1, 0 } }
|
|
for _, dir in ipairs(escort.oakApproach(px)) do
|
|
cx, cy = cx + D[dir][1], cy + D[dir][2]
|
|
end
|
|
check(cx == px and cy == 2,
|
|
("Oak's approach ends below the player at (%d,2)"):format(px))
|
|
end
|
|
|
|
-- =====================================================================
|
|
-- (B) The escort to the lab. Oak: RLEList_ProfOakWalkToLab = DOWN x5,
|
|
-- LEFT, DOWN x5, RIGHT x3, UP (the NPC_CHANGE_FACING tail is a march-
|
|
-- in-place beat, not a step). Player: RLEList_PlayerWalkToLab plays in
|
|
-- reverse buffer order = DOWN x6, LEFT, DOWN x5, RIGHT x3, UP x2, and
|
|
-- the last UP is consumed by the door-warp frame -- so the realized
|
|
-- player path is Oak's path with one extra leading DOWN.
|
|
-- =====================================================================
|
|
eq(joined(escort.oakSteps),
|
|
"down,down,down,down,down,left,down,down,down,down,down,right,right,right,up",
|
|
"Oak's walk-to-lab movement (RLEList_ProfOakWalkToLab)")
|
|
eq(#escort.oakSteps, 15, "Oak takes 15 steps")
|
|
eq(joined(escort.playerSteps),
|
|
"down," ..
|
|
"down,down,down,down,down,left,down,down,down,down,down,right,right,right,up",
|
|
"player's realized walk (RLEList_PlayerWalkToLab reversed, warp eats the 17th press)")
|
|
eq(#escort.playerSteps, 16, "player takes 16 real steps")
|
|
|
|
-- both start one apart and stay in lockstep; both paths end on the lab
|
|
-- door at (12,11)
|
|
do
|
|
local D = { up = { 0, -1 }, down = { 0, 1 }, left = { -1, 0 }, right = { 1, 0 } }
|
|
local ox, oy = 10, 2 -- Oak, below the player on the left tile
|
|
local px, py = 10, 1 -- player
|
|
for i = 1, #escort.playerSteps do
|
|
local od = escort.oakSteps[i]
|
|
if od then ox, oy = ox + D[od][1], oy + D[od][2] end
|
|
local pd = escort.playerSteps[i]
|
|
px, py = px + D[pd][1], py + D[pd][2]
|
|
if i < #escort.playerSteps then
|
|
check(math.abs(ox - px) + math.abs(oy - py) == 1,
|
|
("beat %d: player stays exactly one tile behind Oak"):format(i))
|
|
end
|
|
end
|
|
check(ox == 12 and oy == 11, "Oak's walk ends on the lab door (12,11)")
|
|
check(px == 12 and py == 11, "player's walk ends on the lab door (12,11)")
|
|
end
|
|
|
|
-- the door tile is the real Pallet lab door and resolves to the lab's
|
|
-- second warp, the (5,11) mat the walk-in starts from
|
|
local MapLoader = require("src.world.MapLoader")
|
|
local Warp = require("src.world.Warp")
|
|
local town = MapLoader.load(Data, "PALLET_TOWN")
|
|
local w = town:warpAtCell(12, 11)
|
|
check(w and w.def.destMap == "OAKS_LAB", "(12,11) is the Oak's Lab door warp")
|
|
local dm, dx, dy = Warp.destination(Data, { destMap = "OAKS_LAB", destWarp = 2 })
|
|
check(dm == "OAKS_LAB" and dx == 5 and dy == 11,
|
|
"the door warps onto the lab mat at (5,11)")
|
|
|
|
-- the walk-in cast: door Oak at (5,10), desk Oak at (5,2), both
|
|
-- initially hidden; Pallet's Oak object at (8,5), initially hidden
|
|
local function obj(mapId, name)
|
|
for _, o in ipairs(Data.maps[mapId].objects) do
|
|
if o.name == name then return o end
|
|
end
|
|
end
|
|
local oak2 = obj("OAKS_LAB", "OAKSLAB_OAK2")
|
|
check(oak2 and oak2.x == 5 and oak2.y == 10 and oak2.hidden and oak2.index == 8,
|
|
"OAKSLAB_OAK2 hides at the door (5,10)")
|
|
local oak1 = obj("OAKS_LAB", "OAKSLAB_OAK1")
|
|
check(oak1 and oak1.x == 5 and oak1.y == 2 and oak1.hidden and oak1.index == 5,
|
|
"OAKSLAB_OAK1 hides behind the desk (5,2)")
|
|
local poak = obj("PALLET_TOWN", "PALLETTOWN_OAK")
|
|
check(poak and poak.x == 8 and poak.y == 5 and poak.hidden and poak.index == 1,
|
|
"PALLETTOWN_OAK hides at (8,5)")
|
|
|
|
-- =====================================================================
|
|
-- (C) Oak speech shrink: the extracted ShrinkPic1/ShrinkPic2 manifest
|
|
-- and the sounds/music the sequence uses.
|
|
-- =====================================================================
|
|
local oakGfx = Data.field.oakSpeech
|
|
check(oakGfx and oakGfx.shrink1 and oakGfx.shrink2,
|
|
"field.oakSpeech lists both shrink frames")
|
|
if oakGfx then
|
|
for _, key in ipairs({ "shrink1", "shrink2" }) do
|
|
local fh = io.open(oakGfx[key], "rb")
|
|
check(fh ~= nil, ("%s exists on disk (%s)"):format(key, tostring(oakGfx[key])))
|
|
if fh then fh:close() end
|
|
end
|
|
end
|
|
check(Data.audio.sfx and Data.audio.sfx.Shrink ~= nil, "SFX_SHRINK is extracted")
|
|
check(Data.audio.songs and Data.audio.songs.Music_Routes2 ~= nil
|
|
and Data.audio.songs.Music_MeetProfOak ~= nil,
|
|
"Routes2 + MeetProfOak songs are extracted")
|
|
|
|
-- =====================================================================
|
|
-- (D) Lab walk-in with UP held: a Delay3 emote queued from Oak's entry
|
|
-- onDone must not leave a one-frame handleInput gap, or the held press
|
|
-- walks an extra tile and PlayerEntryMovementRLE (up x8) lands on desk
|
|
-- Oak at (5,2) instead of (5,3).
|
|
-- =====================================================================
|
|
do
|
|
local SaveData = require("src.core.SaveData")
|
|
local Game = require("src.core.Game")
|
|
local StateStack = require("src.core.StateStack")
|
|
local OverworldState = require("src.world.OverworldController")
|
|
local Commands = require("src.script.Commands")
|
|
local prev = { data = Game.data, save = Game.save, stack = Game.stack,
|
|
input = Game.input, renderer = Game.renderer,
|
|
overworld = Game.overworld }
|
|
Game.data = Data
|
|
Game.save = SaveData.newGame(Data)
|
|
Game.save.player.name = "RED"
|
|
Game.save.objectToggles = { OAKS_LAB = { OAKSLAB_OAK2 = true } }
|
|
StateStack:init()
|
|
Game.stack = StateStack
|
|
Game.input = {
|
|
isDown = function(_, b) return b == "up" end,
|
|
wasPressed = function() return false end,
|
|
step = function() end, state = {}, pressQueue = {},
|
|
}
|
|
Game.renderer = {
|
|
beginWorldPass = function() end, endWorldPass = function() end,
|
|
beginUIPass = function() end, endUIPass = function() end,
|
|
worldViewSize = function() return 160, 144 end,
|
|
setSGBZones = function() end,
|
|
}
|
|
StateStack:push(OverworldState, "OAKS_LAB", 5, 11, "up")
|
|
local ow = OverworldState
|
|
Game.overworld = ow
|
|
local ctx = { save = Game.save, game = Game, overworld = ow }
|
|
local finished = false
|
|
local function swapOaks()
|
|
Commands.hide_object(ctx, "OAKS_LAB", "OAKSLAB_OAK2")
|
|
Commands.show_object(ctx, "OAKS_LAB", "OAKSLAB_OAK1")
|
|
ow.emote = { frames = 3, onDone = function()
|
|
ow:scriptMove(ow.player, "up", 8, function() finished = true end)
|
|
end }
|
|
end
|
|
ow:scriptMove(ow:npcByIndex(8), "up", 3, swapOaks)
|
|
for _ = 1, 600 do
|
|
ow:update(1)
|
|
if finished then break end
|
|
end
|
|
check(finished, "held-UP lab walk-in completes")
|
|
eq(ow.player.cellX, 5, "held-UP walk-in x stays in the aisle")
|
|
eq(ow.player.cellY, 3, "held-UP walk-in ends at (5,3), not on desk Oak")
|
|
local oak1 = ow:npcByIndex(5)
|
|
check(oak1 and not (oak1.cellX == ow.player.cellX and oak1.cellY == ow.player.cellY),
|
|
"player does not stack on desk Oak after walk-in")
|
|
for k, v in pairs(prev) do Game[k] = v end
|
|
end
|
|
|
|
S.finish()
|