mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 00:10:56 +02:00
198 lines
7.4 KiB
Lua
198 lines
7.4 KiB
Lua
-- Yellow's follower has to trail exactly one cell behind over a long walk
|
|
-- (#410). ROUTE_1 column x=0 is 36 cells of plain path (tile $2c: no
|
|
-- grass, no ledge row, no object on it, per the generated ROUTE_1 blocks
|
|
-- and pokeyellow data/maps/objects/Route1.asm), so 32 steps north measure
|
|
-- the gap with nothing else moving. Never add POKEPORT_SPEED here: it
|
|
-- scales the logic clock only, and the gap is a timing measurement. No
|
|
-- POKEPORT_IDENTITY either: the Yellow cache lives in the default save dir.
|
|
-- POKEPORT_DRIVER=tests/drivers/pikachu_follow_distance_bug410_test.lua POKEPORT_TOUCH=0 POKEPORT_VERSION=yellow love .
|
|
return function(game)
|
|
local U = dofile("tests/drivers/util.lua")
|
|
local Pokemon = require("src.pokemon.Pokemon")
|
|
local PikachuFollower = require("src.world.PikachuFollower")
|
|
|
|
local MAP = "ROUTE_1"
|
|
local START = { x = 0, y = 34 }
|
|
local STEPS = 32
|
|
|
|
local function check(label, ok)
|
|
U.log(ok and "PASS" or "FAIL", label)
|
|
return ok
|
|
end
|
|
|
|
-- ShouldPikachuSpawn wants the lab gift and a healthy party Pikachu; the
|
|
-- level 100 lead plus a long REPEL is belt and braces, since the column
|
|
-- below carries no grass tile and cannot roll an encounter anyway
|
|
game.save.party = { Pokemon.new(game.data, "PIKACHU", 100) }
|
|
game.save.flags = game.save.flags or {}
|
|
game.save.flags.EVENT_GOT_STARTER = true
|
|
game.save.onBike = false
|
|
game.save.repelSteps = 9999
|
|
game.save.player.name = "bryan"
|
|
|
|
U.teleport(game, MAP, START.x, START.y, "up")
|
|
U.wait(10)
|
|
|
|
local ow = game.overworld
|
|
local map = ow.map
|
|
|
|
-- the run has to be walkable, grass-free and warp-free the whole way; a
|
|
-- later map edit degrades to the longest column that still is, rather
|
|
-- than walking the player into a fence for 500 frames
|
|
local function runLength(cx, fromY)
|
|
local n = 0
|
|
local y = fromY
|
|
while y >= 0 and map:isWalkableCell(cx, y) and not map:isGrassCell(cx, y)
|
|
and not map:warpAtCell(cx, y) do
|
|
n = n + 1
|
|
y = y - 1
|
|
end
|
|
return n
|
|
end
|
|
|
|
if runLength(START.x, START.y) < STEPS + 1 then
|
|
local best, bestX, bestY = 0, START.x, START.y
|
|
for cx = 0, map.widthCells - 1 do
|
|
for cy = map.heightCells - 1, 0, -1 do
|
|
local n = runLength(cx, cy)
|
|
if n > best then best, bestX, bestY = n, cx, cy end
|
|
end
|
|
end
|
|
U.log(("column %d is short (%d cells); walking column %d from y=%d (%d cells)")
|
|
:format(START.x, runLength(START.x, START.y), bestX, bestY, best))
|
|
START.x, START.y = bestX, bestY
|
|
STEPS = math.min(STEPS, best - 1)
|
|
U.teleport(game, MAP, START.x, START.y, "up")
|
|
U.wait(10)
|
|
ow = game.overworld
|
|
map = ow.map
|
|
end
|
|
check(("a straight %d step run exists at column %d"):format(STEPS, START.x),
|
|
STEPS >= 30 and runLength(START.x, START.y) >= STEPS + 1)
|
|
|
|
local function follower()
|
|
for _, n in ipairs(ow.npcs or {}) do
|
|
if n.pikachuFollower then return n end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
check("the follower spawned on " .. MAP, follower() ~= nil)
|
|
|
|
-- The far > 6 snap teleports the follower onto its goal and hides any
|
|
-- drift the walk built up, so a broken run would read as a clean one.
|
|
-- PikachuFollower.update is looked up on the module table at every call
|
|
-- site, so wrapping the field here counts snaps without touching the
|
|
-- engine: a cell that changes across the call while the follower is not
|
|
-- mid-step is the snap and nothing else (a normal step lands its cell
|
|
-- inside NPC:update, which OverworldState runs before this).
|
|
local snaps, fastCommits = 0, 0
|
|
local realUpdate = PikachuFollower.update
|
|
PikachuFollower.update = function(g, o)
|
|
local npc = follower()
|
|
local bx, by, bmoving
|
|
if npc then bx, by, bmoving = npc.cellX, npc.cellY, npc.moving end
|
|
realUpdate(g, o)
|
|
if npc and not bmoving and not npc.moving
|
|
and (npc.cellX ~= bx or npc.cellY ~= by) then
|
|
snaps = snaps + 1
|
|
end
|
|
if npc and not bmoving and npc.moving
|
|
and (npc.stepFrames or 16) < (o.player.stepFramesCur or 16) then
|
|
fastCommits = fastCommits + 1
|
|
end
|
|
end
|
|
|
|
-- The distance sampled is the settled one: while a step is in flight the
|
|
-- follower's committed cell is targetX/Y, which is where it will stand
|
|
-- when the player's own landing frame is over. Raw cell distance is
|
|
-- kept alongside it so a report of 1 cannot come from reading the wrong
|
|
-- field; it reads 2 all the way through a held walk, because both
|
|
-- sprites are then mid-step, and settles to 1 the moment input stops.
|
|
local function gap()
|
|
local p = ow.player
|
|
local npc = follower()
|
|
if not npc then return -1, -1 end
|
|
local pxc = p.targetX or p.cellX
|
|
local pyc = p.targetY or p.cellY
|
|
local nx = npc.targetX or npc.cellX
|
|
local ny = npc.targetY or npc.cellY
|
|
return math.abs(pxc - nx) + math.abs(pyc - ny),
|
|
math.abs(p.cellX - npc.cellX) + math.abs(p.cellY - npc.cellY)
|
|
end
|
|
|
|
local series, raws = {}, {}
|
|
local prevX, prevY = ow.player.cellX, ow.player.cellY
|
|
local frames = 0
|
|
while #series < STEPS and frames < STEPS * 40 do
|
|
table.insert(game.input.pressQueue, "up")
|
|
game.input.state.up = true
|
|
frames = frames + 1
|
|
coroutine.yield()
|
|
local p = ow.player
|
|
if p.cellX ~= prevX or p.cellY ~= prevY then
|
|
prevX, prevY = p.cellX, p.cellY
|
|
local g, r = gap()
|
|
series[#series + 1] = g
|
|
raws[#raws + 1] = r
|
|
end
|
|
end
|
|
game.input.state.up = false
|
|
U.wait(20) -- let the last follow step land before the final reading
|
|
|
|
check(("all %d steps completed (%d recorded)"):format(STEPS, #series),
|
|
#series == STEPS)
|
|
|
|
local maxGap, badSteps = 0, 0
|
|
for _, g in ipairs(series) do
|
|
if g > maxGap then maxGap = g end
|
|
if g ~= 1 then badSteps = badSteps + 1 end
|
|
end
|
|
local function avg(from, to)
|
|
local sum, n = 0, 0
|
|
for i = from, to do
|
|
if series[i] then sum = sum + series[i] n = n + 1 end
|
|
end
|
|
return n > 0 and sum / n or 0
|
|
end
|
|
local head, tail = avg(1, 8), avg(#series - 7, #series)
|
|
local finalGap, finalRaw = gap()
|
|
|
|
local function compact(list)
|
|
local out, row = {}, {}
|
|
for i, g in ipairs(list) do
|
|
row[#row + 1] = (g >= 0 and g < 10) and tostring(g) or ("[" .. g .. "]")
|
|
if i % 40 == 0 then out[#out + 1] = table.concat(row) row = {} end
|
|
end
|
|
if #row > 0 then out[#out + 1] = table.concat(row) end
|
|
return out
|
|
end
|
|
for _, row in ipairs(compact(series)) do U.log("gap per step:", row) end
|
|
for _, row in ipairs(compact(raws)) do U.log("raw cell gap: ", row) end
|
|
|
|
check("the gap is 1 on every step", badSteps == 0 and #series > 0)
|
|
check(("max gap is 1 (saw %d)"):format(maxGap), maxGap == 1)
|
|
check(("final gap is 1 (saw %d)"):format(finalGap), finalGap == 1)
|
|
check(("Pikachu came to rest one cell behind (saw %d)"):format(finalRaw),
|
|
finalRaw == 1)
|
|
check(("no upward trend (first 8 avg %.2f, last 8 avg %.2f)")
|
|
:format(head, tail), tail <= head)
|
|
check(("the far > 6 snap never fired (%d)"):format(snaps), snaps == 0)
|
|
U.log("fast (half length) follow steps committed:", fastCommits)
|
|
|
|
PikachuFollower.update = realUpdate
|
|
|
|
local SHOT_DIR = os.getenv("SHOT_DIR") or "/tmp/shots"
|
|
if U.shot(game, SHOT_DIR .. "/bug410_follow_distance.png") then
|
|
U.log("captured", SHOT_DIR .. "/bug410_follow_distance.png")
|
|
end
|
|
|
|
U.log("Pikachu has just walked 32 cells up ROUTE_1 and should be standing")
|
|
U.log("one cell below you, close enough to touch. Walk on and it stays")
|
|
U.log("there; the bug left a visible cell of daylight between you.")
|
|
|
|
while true do
|
|
coroutine.yield()
|
|
end
|
|
end
|