mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-25 15:01:11 +02:00
1905261c5b
CLOSES #1483, CLOSES #1610, CLOSES #1615, CLOSES #1646, CLOSES #1649, CLOSES #1651, CLOSES #1653, CLOSES #1656, CLOSES #1683, CLOSES #1685, CLOSES #1686, CLOSES #1687, CLOSES #1688, CLOSES #1689, CLOSES #1690, CLOSES #1693, CLOSES #1694, CLOSES #1695, CLOSES #1696, CLOSES #1702, CLOSES #1704, CLOSES #1705, CLOSES #1706, CLOSES #1707, CLOSES #1708, CLOSES #1710, CLOSES #1711, CLOSES #1712, CLOSES #1713, CLOSES #1716, CLOSES #1717, CLOSES #1718, CLOSES #1719, CLOSES #1720, CLOSES #1721, CLOSES #1725, CLOSES #1732, CLOSES #1745, CLOSES #1748, CLOSES #1749, CLOSES #1751, CLOSES #1754
273 lines
10 KiB
Lua
273 lines
10 KiB
Lua
-- #1713: a ledge hop advanced only every other logic frame, so the whole map
|
|
-- scrolled at 30Hz in 2px lurches and the sprite bobbed against its own arc.
|
|
--
|
|
-- POKEPORT_IDENTITY=gold-dev POKEPORT_GAME=gold POKEPORT_TOUCH=0 \
|
|
-- POKEPORT_DRIVER=tests/drivers/gold_ledge_hop_bug1713_test.lua \
|
|
-- POKEPORT_SHOT_DIR=/tmp/gold-ledge \
|
|
-- perl -e 'alarm 240; exec @ARGV' \
|
|
-- python3 -c "import pty; pty.spawn(['love','.'])"
|
|
--
|
|
-- jump_step is STEP_WALK (pokegold/engine/overworld/movement.asm:595-597), so
|
|
-- StepFunction_PlayerJump runs the `db 0, 2, 8, 2` StepVectors row as two
|
|
-- 8-frame beats (engine/overworld/map_objects.asm:365-381, :1163-1200) and
|
|
-- UpdateJumpPosition walks its 16-entry arc one entry a frame (:1796-1817).
|
|
-- We render at twice that resolution, so the hop owes 1px and half an arc
|
|
-- entry on every one of its 32 frames.
|
|
--
|
|
-- No POKEPORT_SPEED here on purpose: the whole subject is per-frame motion,
|
|
-- and fast-forward scales only the logic clock.
|
|
local U = require("tests.drivers.util")
|
|
|
|
local Player = require("src.world.gen2.Player")
|
|
local Permissions = require("src.world.gen2.Permissions")
|
|
|
|
-- ROUTE_30's ledge run at y=10 (data/generated/maps.lua, TILESET_JOHTO
|
|
-- collision $a3 COLL_HOP_DOWN): open ground two cells above it, a wall in
|
|
-- between, floor on the landing. Re-derived by scanning if a re-import moves.
|
|
local MAP = "ROUTE_30"
|
|
local LEDGE = { x = 4, y = 10 }
|
|
|
|
return function(game)
|
|
local out = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/gold-ledge"
|
|
local fails, lines = 0, {}
|
|
|
|
local function claim(ok, text)
|
|
if not ok then fails = fails + 1 end
|
|
lines[#lines + 1] = (ok and "PASS " or "FAIL ") .. text
|
|
return ok
|
|
end
|
|
|
|
local function stop()
|
|
for _, line in ipairs(lines) do U.log(line) end
|
|
while true do coroutine.yield() end
|
|
end
|
|
|
|
U.wait(45)
|
|
local world = game.world
|
|
if not (world and world.map) then
|
|
U.log("FAIL the gold world never booted, nothing to look at")
|
|
while true do coroutine.yield() end
|
|
end
|
|
|
|
claim(Player.STEP_FRAMES == 16, "a walk is 16 logic frames per cell")
|
|
claim(type(world.tryLedgeJump) == "function",
|
|
"World:tryLedgeJump is wired for the refused step to fall into")
|
|
|
|
-- ---- find a ledge with room to run up to it ------------------------------
|
|
world:setMap(MAP, LEDGE.x, LEDGE.y - 2, "down")
|
|
U.wait(10)
|
|
world.noWildEncounters = true
|
|
local map = world.map
|
|
|
|
local function hopsDown(x, y)
|
|
if not map:inBounds(x, y) then return false end
|
|
local facings = Permissions.ledgeFacings(map:cellCollision(x, y))
|
|
return facings ~= nil and facings.down == true
|
|
end
|
|
local function walkable(x, y)
|
|
return map:inBounds(x, y) and map:isWalkable(x, y)
|
|
end
|
|
-- A usable ledge: two free cells to walk down through, a refused cell in the
|
|
-- middle (that refusal is what becomes the jump) and a free landing.
|
|
local function usable(x, y)
|
|
return hopsDown(x, y) and not walkable(x, y + 1) and walkable(x, y + 2)
|
|
and walkable(x, y - 1) and walkable(x, y - 2)
|
|
end
|
|
|
|
local target
|
|
if usable(LEDGE.x, LEDGE.y) then target = { x = LEDGE.x, y = LEDGE.y } end
|
|
if not target then
|
|
local def = world.maps[MAP]
|
|
for y = 0, (def.height or 0) * 2 - 1 do
|
|
for x = 0, (def.width or 0) * 2 - 1 do
|
|
if not target and usable(x, y) then target = { x = x, y = y } end
|
|
end
|
|
end
|
|
end
|
|
claim(target ~= nil,
|
|
("%s still has a hop-down ledge with a run-up to it"):format(MAP))
|
|
if not target then
|
|
U.log("nothing to hop off; stopping rather than parking you at a wall")
|
|
stop()
|
|
end
|
|
if target.x ~= LEDGE.x or target.y ~= LEDGE.y then
|
|
U.log(("note: using the ledge at (%d,%d), not the (%d,%d) in the header")
|
|
:format(target.x, target.y, LEDGE.x, LEDGE.y))
|
|
end
|
|
local START = { x = target.x, y = target.y - 2 }
|
|
|
|
local function press(dir)
|
|
table.insert(game.input.pressQueue, dir)
|
|
game.input.state[dir] = true
|
|
coroutine.yield()
|
|
end
|
|
|
|
-- Back to the top of the run-up. The settle first is load bearing: a
|
|
-- setMap on top of a hop that is still in the air carries the leftover jump
|
|
-- into the new position and the next run measures nonsense.
|
|
local function reset()
|
|
game.input.state.down = false
|
|
for _ = 1, 120 do
|
|
if not (world.player and world.player.moving) then break end
|
|
coroutine.yield()
|
|
end
|
|
world:setMap(MAP, START.x, START.y, "down")
|
|
world.noWildEncounters = true
|
|
U.wait(8)
|
|
end
|
|
|
|
-- ---- the run-up and the hop, measured ------------------------------------
|
|
--
|
|
-- Nobody can count pixels per frame by eye, and this is the whole bug: the
|
|
-- per-frame travel of the camera-following position, plus the arc riding on
|
|
-- top of it. Every sample is "pixels travelled == frames elapsed", never a
|
|
-- difference between two driver frames -- the fixed-step loop can run two
|
|
-- logic steps for one yield, and a delta would read that as a lurch.
|
|
reset()
|
|
local p = world.player
|
|
local hopRows, takeoff = {}, 0
|
|
local walkSeen, walkWrong = 0, 0
|
|
for _ = 1, 240 do
|
|
press("down")
|
|
if p.jumping then
|
|
if #hopRows == 0 then takeoff = p.cellY * 16 end
|
|
hopRows[#hopRows + 1] = { n = p.progress, py = p.py,
|
|
off = p.spriteYOffset or 0 }
|
|
elseif #hopRows > 0 then
|
|
break
|
|
elseif p.moving and (p.progress or 0) > 0 then
|
|
walkSeen = walkSeen + 1
|
|
if p.py - p.cellY * 16 ~= p.progress then walkWrong = walkWrong + 1 end
|
|
end
|
|
end
|
|
game.input.state.down = false
|
|
|
|
local wrong, lurch, last = 0, 0, 0
|
|
for _, r in ipairs(hopRows) do
|
|
if r.py - takeoff ~= r.n then wrong = wrong + 1 end
|
|
if r.n > last then last = r.n end
|
|
end
|
|
-- The old quantum, named directly: an even number of pixels on an odd frame.
|
|
for _, r in ipairs(hopRows) do
|
|
if (r.py - takeoff) % 2 == 0 and r.n % 2 == 1 then lurch = lurch + 1 end
|
|
end
|
|
|
|
claim(#hopRows > 0, "walking down into the ledge started a hop")
|
|
claim(last >= 30,
|
|
("the hop ran its full 32 frames (the last one sampled was %d)")
|
|
:format(last))
|
|
claim(p.cellY == target.y + 2,
|
|
("it landed two cells down at y=%d (the player is at y=%d)")
|
|
:format(target.y + 2, p.cellY))
|
|
claim(p.py - takeoff == 32,
|
|
("and 32 pixels down, which is two cells (it moved %d)")
|
|
:format(p.py - takeoff))
|
|
claim(wrong == 0,
|
|
("on every one of the %d frames sampled the player had moved a pixel per"
|
|
.. " frame elapsed (%d had not)"):format(#hopRows, wrong))
|
|
claim(lurch == 0,
|
|
("so no frame of it stood still waiting to lurch two (%d did)")
|
|
:format(lurch))
|
|
claim(walkSeen > 0 and walkWrong == 0,
|
|
("the two ordinary steps before it move a pixel a frame too, on all %d"
|
|
.. " frames sampled (%d wrong)"):format(walkSeen, walkWrong))
|
|
|
|
-- The composed screen position: the camera-following py plus the sprite
|
|
-- offset. The armed frame is skipped because the first frame after it is
|
|
-- the take-off, and rising there is the jump starting.
|
|
local air, back = {}, 0
|
|
for _, r in ipairs(hopRows) do
|
|
if r.n >= 1 then air[#air + 1] = r.py + r.off end
|
|
end
|
|
for i = 2, #air do
|
|
if air[i] < air[i - 1] then back = back + 1 end
|
|
end
|
|
claim(back == 0,
|
|
("after the take-off the sprite never reversed against its own arc"
|
|
.. " (%d frames did)"):format(back))
|
|
|
|
-- ---- one frame per run, so the strip really is consecutive ---------------
|
|
--
|
|
-- A capture costs the driver an unknown number of frames, so shooting four
|
|
-- frames in one hop would skip some. Four identical run-ups, each shot on a
|
|
-- different frame, gives a strip that can be flipped through.
|
|
-- One run-up, ending either on the frame asked for or on the landing. The
|
|
-- fixed-step loop can occasionally run two logic steps for one driver yield,
|
|
-- so the shot is taken at the first frame AT OR PAST the one asked for, and
|
|
-- the frame it actually landed on is reported rather than assumed.
|
|
local function hopRun(frame, path)
|
|
reset()
|
|
local airborne = false
|
|
for _ = 1, 240 do
|
|
press("down")
|
|
local pl = world.player
|
|
if pl.jumping then
|
|
airborne = true
|
|
if frame and pl.progress >= frame then
|
|
local at, py = pl.progress, pl.py
|
|
game.input.state.down = false
|
|
U.shot(game, path)
|
|
return at, py
|
|
end
|
|
elseif airborne then
|
|
break
|
|
end
|
|
end
|
|
game.input.state.down = false
|
|
end
|
|
|
|
reset()
|
|
U.shot(game, out .. "/01-standing.png")
|
|
local strip = {}
|
|
for i, frame in ipairs({ 12, 13, 14, 15 }) do
|
|
local at, py = hopRun(frame, ("%s/02-strip-%d.png"):format(out, i))
|
|
strip[#strip + 1] = { at = at or -1, py = py or -1 }
|
|
end
|
|
hopRun(4, out .. "/03-rising.png")
|
|
hopRun(16, out .. "/04-peak.png")
|
|
hopRun(28, out .. "/05-falling.png")
|
|
hopRun()
|
|
U.wait(20)
|
|
U.shot(game, out .. "/06-landed.png")
|
|
|
|
local consecutive = true
|
|
local shown = {}
|
|
for i, s in ipairs(strip) do
|
|
shown[i] = ("%d"):format(s.at)
|
|
if i > 1 and (s.at - strip[i - 1].at ~= 1
|
|
or s.py - strip[i - 1].py ~= 1) then
|
|
consecutive = false
|
|
end
|
|
end
|
|
claim(consecutive,
|
|
("the strip really is four consecutive frames, %s -- if this is FAIL the"
|
|
.. " images are not comparable"):format(table.concat(shown, ", ")))
|
|
|
|
for _, line in ipairs(lines) do U.log(line) end
|
|
U.log(("%d checks, %d failed"):format(#lines, fails))
|
|
if fails > 0 then
|
|
U.log("something above is FAIL, so do not spend time watching the replay")
|
|
end
|
|
|
|
-- ---- the replay, live ----------------------------------------------------
|
|
U.log("shots are in " .. out .. "; 02-strip-* are consecutive frames of the")
|
|
U.log("same hop, so the ground should shift one pixel between each, and")
|
|
U.log("03 / 04 / 05 are the rise, the top of the arc and the drop.")
|
|
U.log("the replay walks down into the ledge three times, then hands over.")
|
|
U.wait(120)
|
|
for _ = 1, 3 do
|
|
hopRun()
|
|
U.wait(60)
|
|
end
|
|
|
|
U.log("two ordinary steps down, then the hop: the map should scroll at the")
|
|
U.log("same speed through all three, and the sprite should rise, hang and")
|
|
U.log("fall once. a stutter, a 2px lurch, or the sprite bobbing back up")
|
|
U.log("during the rise is the old behaviour.")
|
|
U.log("the near miss to watch for: a smooth hop that clears only one cell,")
|
|
U.log("or one that takes twice as long as it should -- that is the two-cell")
|
|
U.log("span applied without the doubled duration.")
|
|
U.log("gen 1 is the reference; a Red ledge hop already looked like this.")
|
|
while true do coroutine.yield() end
|
|
end
|