This commit is contained in:
bryanthaboi
2026-08-14 11:41:54 -04:00
parent b6388013ec
commit 4395792226
4 changed files with 142 additions and 3 deletions
+8 -2
View File
@@ -178,7 +178,8 @@ M.PALLET_TOWN = {
end
end
local function enterLab()
local function enterLab(oak)
if oak then oak.stepFrames = nil end
Commands.hide_object(ctx, "PALLET_TOWN", "PALLETTOWN_OAK")
Commands.show_object(ctx, "OAKS_LAB", "OAKSLAB_OAK2")
ow.doorWarp = true
@@ -187,12 +188,17 @@ M.PALLET_TOWN = {
end
local function walkToLab(oak)
-- lockstep half runs Oak on the player's own frames per cell
-- engine/overworld/movement.asm:737 (DoScriptedNPCMovement)
local i = 0
if oak then
oak.stepFrames = ow.player.stepFramesCur or ow.player.stepFrames
end
local function tick()
i = i + 1
local playerStep = escort.playerSteps[i]
if not playerStep then
enterLab()
enterLab(oak)
return
end
if oak and escort.oakSteps[i] then
+6 -1
View File
@@ -446,7 +446,7 @@ local function pewterGymEscort(game, ow)
end
local function afterWalk()
if guy then guy.facing = "left" end
if guy then guy.stepFrames, guy.facing = nil, "left" end
Music.playMap(game.data, "PEWTER_CITY")
push(game, t._PewterCityYoungsterGoTakeOnBrockText
or "Go take on BROCK\nat the GYM first!", walkHome)
@@ -469,6 +469,11 @@ local function pewterGymEscort(game, ow)
end
local function beginWalk()
-- the escort runs the youngster on the player's own frames per cell
-- engine/overworld/movement.asm:737 (DoScriptedNPCMovement)
if guy then
guy.stepFrames = ow.player.stepFramesCur or ow.player.stepFrames
end
Music.play(game.data, "Music_MuseumGuy")
if guy and head > 0 then
local h = 0
+40
View File
@@ -0,0 +1,40 @@
-- Driver: Oak's Pallet Town escort. Logs the player/Oak separation the
-- whole way down and shoots the walk mid-street, so the formation is
-- checkable by eye as well as by number (the escort holds one cell,
-- ~16px; a desynced Oak drifts off by a cell per two steps).
return function(game)
local U = dofile("tests/drivers/util.lua")
local DIR = os.getenv("SHOT_DIR") or "/tmp/shots"
U.teleport(game, "PALLET_TOWN", 10, 3, "up")
local ow = game.overworld
local function oak()
for _, n in ipairs(ow.npcs or {}) do
if n.def and n.def.name == "PALLETTOWN_OAK" then return n end
end
end
U.hold(game, "up", 40)
local worst, samples = 0, 0
for _ = 1, 1200 do
if game.stack:top() ~= ow then U.tap(game, "a") end
local o = oak()
if o and #ow.scriptMoves > 0 and ow.map.id == "PALLET_TOWN" then
local d = math.abs(o.px - ow.player.px) + math.abs(o.py - ow.player.py)
if samples > 0 or d <= 20 then
samples = samples + 1
if d > worst then worst = d end
if samples == 60 then U.shot(game, DIR .. "/escort_midwalk.png") end
if samples % 24 == 1 then
U.log(("t=%d player=(%d,%d) oak=(%d,%d) dist=%dpx")
:format(samples, ow.player.cellX, ow.player.cellY,
o.cellX, o.cellY, d))
end
end
end
if ow.map.id == "OAKS_LAB" then break end
U.wait(2)
end
U.log(("ESCORT worst separation: %dpx over %d samples"):format(worst, samples))
U.log("map:", ow.map.id, "flag:",
tostring(game.save.flags.EVENT_FOLLOWED_OAK_INTO_LAB))
love.event.quit()
end
+88
View File
@@ -0,0 +1,88 @@
-- Parity test: escort lockstep cadence. An NPC walking the player to a
-- destination moves under DoScriptedNPCMovement (engine/overworld/
-- movement.asm:737), whose wScriptedNPCWalkCounter is 8 ticks of 2px --
-- the player's own frames per cell, not MoveSprite's doubled NPC walk.
-- Self-contained: run via `luajit tests/parity_escort_lockstep.lua`; also
-- dofile'd by tests/run_tests.lua's aggregator.
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 escort lockstep")
local check, eq = S.check, S.eq
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 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"
StateStack:init()
Game.stack = StateStack
Game.input = {
isDown = function() return false 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, "PALLET_TOWN", 10, 5, "down")
local ow = OverworldState
Game.overworld = ow
-- A PALLET_TOWN object parked one cell ahead of the player, then walked
-- in the paired-step pattern the escorts use: the NPC's step is queued
-- without a callback and the player's carries the chain.
local function runEscort(sync, steps)
local npc = ow.npcs[1]
local p = ow.player
p.cellX, p.cellY, p.px, p.py = 10, 5, 160, 80
p.moving, p.progress, p.targetX, p.targetY = false, 0, nil, nil
npc.cellX, npc.cellY, npc.px, npc.py = 10, 4, 160, 64
npc.moving, npc.progress, npc.targetX, npc.targetY = false, 0, nil, nil
ow.scriptMoves = {}
npc.stepFrames = sync and (p.stepFramesCur or p.stepFrames) or nil
local worstDrift, done, i = 0, false, 0
local function tick()
i = i + 1
if i > steps then done = true; return end
ow:scriptMove(npc, "down", 1)
ow:scriptMove(p, "down", 1, tick)
end
tick()
-- NPCs update before updateScriptMoves and the player after, so a
-- paired step leaves the NPC one frame (1px) behind for its whole cell
for _ = 1, 60 * steps do
ow:update(1)
local drift = math.abs((p.py - npc.py) - 16)
if drift > worstDrift then worstDrift = drift end
if done then break end
end
npc.stepFrames = nil
return { done = done, drift = worstDrift,
npcY = npc.cellY, playerY = p.cellY }
end
local synced = runEscort(true, 6)
check(synced.done, "synced escort finishes")
check(synced.drift <= 2,
("synced NPC holds formation (worst drift %dpx)"):format(synced.drift))
eq(synced.playerY - synced.npcY, 1, "synced NPC stays one cell ahead")
-- The default NPC walk is MoveSprite's, half the player's rate: without
-- the sync the escort desyncs, which is the bug this guards.
local plain = runEscort(false, 6)
check(plain.drift >= 16,
("unsynced NPC lags a cell or more (worst drift %dpx)"):format(plain.drift))
for k, v in pairs(prev) do Game[k] = v end
S.finish()