mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-25 23:11:15 +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
370 lines
15 KiB
Lua
370 lines
15 KiB
Lua
-- #1708: Gold's fishing pose and rod. The cast is reached the way a player
|
|
-- reaches it -- walk to the shore, START, PACK, KEY ITEMS, OLD ROD, USE -- once
|
|
-- per facing, and the pose row and rod tile are sampled out of the real draw.
|
|
-- FacingFishDown/Up/Left/Right ../pokegold/data/sprites/facings.asm:122-152;
|
|
-- LoadFishingGFX ../pokegold/engine/events/fishing_gfx.asm:1-24.
|
|
-- No POKEPORT_SPEED: it scales the logic clock only, so the sampled frames and
|
|
-- the rendered ones drift apart and the rod is read off the wrong frame.
|
|
-- POKEPORT_IDENTITY=gold-dev POKEPORT_GAME=gold POKEPORT_TOUCH=0 \
|
|
-- POKEPORT_DRIVER=tests/drivers/gold_fishing_rod_bug1708_test.lua \
|
|
-- POKEPORT_SHOT_DIR=/tmp/gold-fishing \
|
|
-- perl -e 'alarm 420; exec @ARGV' \
|
|
-- python3 -c "import pty; pty.spawn(['love','.'])"
|
|
local U = require("tests.drivers.util")
|
|
|
|
local Assets = require("src.render.Assets")
|
|
local Map = require("src.world.gen2.Map")
|
|
local PackMenu = require("src.ui.gen2.PackMenu")
|
|
local Permissions = require("src.world.gen2.Permissions")
|
|
local SpriteRenderer = require("src.render.SpriteRenderer")
|
|
local StartMenu = require("src.ui.gen2.StartMenu")
|
|
|
|
local ROD_ITEM = "OLD_ROD"
|
|
local FACINGS = { "down", "up", "left", "right" }
|
|
local DELTA = { up = { 0, -1 }, down = { 0, 1 }, left = { -1, 0 }, right = { 1, 0 } }
|
|
|
|
-- The loose rod OAM, `db y, x, attr, tile`, off the sprite's top-left
|
|
-- (data/sprites/facings.asm:122-152). Read here a second time so a typo in the
|
|
-- engine's copy of the table is a mismatch rather than a shared mistake.
|
|
local ROD_OAM = {
|
|
down = { dx = 0, dy = 16, tile = 0xfc, flip = false },
|
|
up = { dx = 0, dy = -8, tile = 0xfc, flip = false },
|
|
left = { dx = -8, dy = 5, tile = 0xfd, flip = true },
|
|
right = { dx = 16, dy = 5, tile = 0xfd, flip = false },
|
|
}
|
|
-- LoadFishingGFX's three destinations, vTiles $02 / $06 / $0a, which are the
|
|
-- bottom tile rows of the down / up / left standing frames; right is left
|
|
-- x-flipped (fishing_gfx.asm:13-18, facings.asm:143-152).
|
|
local POSE_ROW = { down = 0, up = 1, left = 2, right = 2 }
|
|
local POSE_FLIP = { down = false, up = false, left = false, right = true }
|
|
-- Four rows of two tiles: the three pose rows then $fc/$fd.
|
|
local SHEET_W, SHEET_H, ROD_ROW_Y = 16, 32, 24
|
|
|
|
-- Ponds with a shore on all four sides. Route 28's has no object events at
|
|
-- all (../pokegold/maps/Route28.asm:29), so nothing walks into a shot and no
|
|
-- trainer's line of sight crosses one; the other two are the fallbacks if its
|
|
-- blocks are ever edited (../pokegold/maps/Route28.blk).
|
|
local MAPS = { "ROUTE_28", "VIRIDIAN_CITY", "ROUTE_43" }
|
|
|
|
return function(game)
|
|
local out = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/gold-fishing"
|
|
local failures = 0
|
|
|
|
local function check(label, condition, detail)
|
|
if condition then
|
|
U.log("PASS", label)
|
|
else
|
|
failures = failures + 1
|
|
U.log("FAIL", label, detail ~= nil and tostring(detail) or "")
|
|
end
|
|
return condition and true or false
|
|
end
|
|
|
|
local function tap(btn, frames)
|
|
game.input.pressQueue[#game.input.pressQueue + 1] = btn
|
|
game.input.state[btn] = true
|
|
U.wait(2)
|
|
game.input.state[btn] = false
|
|
U.wait(frames or 6)
|
|
end
|
|
|
|
U.wait(45)
|
|
local world = game.world
|
|
assert(world and world.map, "gold world did not boot")
|
|
local save = game.save
|
|
|
|
-- ---- the halves of the drawing, before anything is on screen -------------
|
|
-- A cache with no sheet, a sheet of the wrong size and a draw branch that
|
|
-- never fires all look identical on screen: a plain standing player.
|
|
|
|
local sheet = world.fishingSheet
|
|
if not check("the cache carries LoadFishingGFX's sheet (world.fishingSheet)",
|
|
type(sheet) == "string", sheet) then
|
|
U.log("no emotes/fishing.png in this cache, so nothing below can draw.")
|
|
U.log("re-import the cart first:")
|
|
U.log(" POKEPORT_IDENTITY=gold-dev POKEPORT_GAME=gold POKEPORT_IMPORT_ONLY=1 \\")
|
|
U.log(" POKEPORT_IMPORT_ROM=/path/to/gold.gbc love .")
|
|
while true do coroutine.yield() end
|
|
end
|
|
|
|
local okImg, image = pcall(Assets.image, sheet)
|
|
check("and it loads: " .. tostring(sheet), okImg and image ~= nil)
|
|
if okImg and image then
|
|
local w, h = image:getDimensions()
|
|
check(("it is %dx%d, the four rows the quads are written against")
|
|
:format(SHEET_W, SHEET_H),
|
|
w == SHEET_W and h == SHEET_H, ("%dx%d"):format(w, h))
|
|
if h ~= SHEET_H then
|
|
U.log("a taller sheet moves the rod row, and both draws then sample")
|
|
U.log("the wrong pixels -- Gen 1 shipped exactly that as #321.")
|
|
end
|
|
end
|
|
|
|
local Player = require("src.world.gen2.Player")
|
|
check("Player:drawFishing exists (the branch that reads player.fishing)",
|
|
type(Player.drawFishing) == "function")
|
|
check("SpriteRenderer:drawTile exists (it bakes the OBJ palette; a raw "
|
|
.. "love.graphics.draw would leave the rod in DMG greys)",
|
|
type(SpriteRenderer.drawTile) == "function")
|
|
|
|
local rodDef = game.data and game.data.items and game.data.items[ROD_ITEM]
|
|
check("OLD ROD is a real item in this cache", rodDef ~= nil)
|
|
check("and it lives in the KEY ITEMS pocket",
|
|
rodDef ~= nil and rodDef.pocket == "KEY_ITEM",
|
|
rodDef and rodDef.pocket)
|
|
|
|
-- ---- pick the pond ------------------------------------------------------
|
|
-- Shores are re-derived from the generated blocks every run, so a re-extract
|
|
-- that shifts the water degrades to another cell instead of parking the
|
|
-- player at a wall.
|
|
|
|
local function shores(mapId)
|
|
local def = world.maps and world.maps[mapId]
|
|
local tileset = def and world.tilesets and world.tilesets[def.tileset]
|
|
if not (def and tileset) then return nil end
|
|
local probe = Map.new(def, tileset)
|
|
local taken = {}
|
|
for _, obj in ipairs(def.objects or {}) do
|
|
taken[obj.x .. "," .. obj.y] = true
|
|
end
|
|
local function land(cx, cy)
|
|
if not probe:inBounds(cx, cy) then return false end
|
|
if taken[cx .. "," .. cy] then return false end
|
|
local coll = probe:cellCollision(cx, cy)
|
|
return Permissions.isWalkable(coll) and not Permissions.isWater(coll)
|
|
end
|
|
local found = {}
|
|
for _, facing in ipairs(FACINGS) do
|
|
local d = DELTA[facing]
|
|
for cy = 0, probe.heightCells - 1 do
|
|
for cx = 0, probe.widthCells - 1 do
|
|
if not found[facing] and land(cx, cy)
|
|
and probe:inBounds(cx + d[1], cy + d[2])
|
|
and Permissions.isWater(probe:cellCollision(cx + d[1], cy + d[2]))
|
|
and land(cx - d[1], cy - d[2]) then
|
|
found[facing] = { x = cx, y = cy }
|
|
end
|
|
end
|
|
end
|
|
if not found[facing] then return nil end
|
|
end
|
|
return found
|
|
end
|
|
|
|
local MAP, SHORE
|
|
for _, id in ipairs(MAPS) do
|
|
SHORE = shores(id)
|
|
if SHORE then MAP = id break end
|
|
end
|
|
if not check("a pond with all four shores and a step of room behind each",
|
|
MAP ~= nil, table.concat(MAPS, ", ")) then
|
|
while true do coroutine.yield() end
|
|
end
|
|
U.log("fishing on " .. MAP .. ", from " ..
|
|
("down (%d,%d), up (%d,%d), left (%d,%d), right (%d,%d)"):format(
|
|
SHORE.down.x, SHORE.down.y, SHORE.up.x, SHORE.up.y,
|
|
SHORE.left.x, SHORE.left.y, SHORE.right.x, SHORE.right.y))
|
|
|
|
-- ---- the bag, and a pond that never bites -------------------------------
|
|
-- FISHGROUP_NONE is .FishNoFish: the cast, the pose and the rod are the same
|
|
-- as a bite's, and every run ends on "Not even a nibble!" back on the map
|
|
-- rather than in a battle nobody asked for. Put back before the hand-off.
|
|
|
|
save.inventory = { [ROD_ITEM] = 1 }
|
|
save.bagOrder = { ROD_ITEM }
|
|
local mapDef = world.maps[MAP]
|
|
local realGroup = mapDef.fishGroup
|
|
mapDef.fishGroup = "FISHGROUP_NONE"
|
|
|
|
-- ---- what the sprite actually drew --------------------------------------
|
|
|
|
local watch = nil
|
|
local realDrawTile = SpriteRenderer.drawTile
|
|
SpriteRenderer.drawTile = function(self, path, x, y, flip, quad)
|
|
if watch and path == sheet then
|
|
local qx, qy, qw, qh = 0, 0, 0, 0
|
|
if quad and quad.getViewport then qx, qy, qw, qh = quad:getViewport() end
|
|
watch[#watch + 1] = { x = x, y = y, flip = flip and true or false,
|
|
qx = qx, qy = qy, qw = qw, qh = qh }
|
|
end
|
|
return realDrawTile(self, path, x, y, flip, quad)
|
|
end
|
|
|
|
local function lastOfSize(calls, w, h)
|
|
local hit
|
|
for _, c in ipairs(calls) do
|
|
if c.qw == w and c.qh == h then hit = c end
|
|
end
|
|
return hit
|
|
end
|
|
|
|
-- ---- walk to a shore ----------------------------------------------------
|
|
|
|
local function goToShore(facing)
|
|
local cell, d = SHORE[facing], DELTA[facing]
|
|
world:setMap(MAP, cell.x - d[1], cell.y - d[2], facing)
|
|
U.wait(24)
|
|
U.hold(game, facing, 26)
|
|
U.wait(10)
|
|
local p = world.player
|
|
if p.cellX ~= cell.x or p.cellY ~= cell.y then
|
|
world:setMap(MAP, cell.x, cell.y, facing)
|
|
U.wait(24)
|
|
p = world.player
|
|
end
|
|
local fx, fy = p.cellX + d[1], p.cellY + d[2]
|
|
check(("%s: standing at (%d,%d) with water in front")
|
|
:format(facing, p.cellX, p.cellY),
|
|
p.facing == facing and world.map:inBounds(fx, fy)
|
|
and Permissions.isWater(world.map:cellCollision(fx, fy)),
|
|
("facing %s"):format(tostring(p.facing)))
|
|
end
|
|
|
|
-- ---- START, PACK, KEY ITEMS, OLD ROD, USE --------------------------------
|
|
|
|
local function castRod(facing)
|
|
tap("start")
|
|
local menu = game.stack:top()
|
|
if not check(("%s: START opened the menu"):format(facing),
|
|
getmetatable(menu) == StartMenu) then
|
|
return false
|
|
end
|
|
for _ = 1, #menu.items do
|
|
local row = menu.list and menu.list:current()
|
|
if row and row.value == "pack" then break end
|
|
tap("down", 4)
|
|
end
|
|
tap("a", 10)
|
|
local pack = game.stack:top()
|
|
if not check(("%s: and the PACK is open"):format(facing),
|
|
getmetatable(pack) == PackMenu) then
|
|
return false
|
|
end
|
|
for _ = 1, 4 do
|
|
if pack:pocket().id == "KEY_ITEM" then break end
|
|
tap("right", 6)
|
|
end
|
|
check(("%s: on the KEY ITEMS pocket"):format(facing),
|
|
pack:pocket().id == "KEY_ITEM", pack:pocket().id)
|
|
for _ = 1, 20 do
|
|
local row = pack.rows[pack.index]
|
|
if row and row.id == ROD_ITEM then break end
|
|
tap("down", 4)
|
|
end
|
|
local row = pack.rows[pack.index]
|
|
if not check(("%s: cursor on the OLD ROD"):format(facing),
|
|
row ~= nil and row.id == ROD_ITEM, row and row.id) then
|
|
return false
|
|
end
|
|
tap("a", 8)
|
|
local sub = pack.submenu
|
|
check(("%s: the item submenu opened on USE"):format(facing),
|
|
sub ~= nil and sub.rows[sub.index] == "use",
|
|
sub and sub.rows[sub.index])
|
|
tap("a", 10)
|
|
return true
|
|
end
|
|
|
|
-- ---- one facing, end to end ---------------------------------------------
|
|
|
|
local function castAndSample(facing, index)
|
|
goToShore(facing)
|
|
if not castRod(facing) then return end
|
|
|
|
for _ = 1, 60 do
|
|
if world.fishing then break end
|
|
U.wait(1)
|
|
end
|
|
check(("%s: the rod is out (world.fishing)"):format(facing),
|
|
world.fishing ~= nil)
|
|
check(("%s: the pose is up (player.fishing)"):format(facing),
|
|
world.player.fishing == true)
|
|
check(("%s: and it drew from the fishing sheet"):format(facing),
|
|
world.player.fishSheet == sheet, world.player.fishSheet)
|
|
check(("%s: the PACK closed, so the cast is on screen"):format(facing),
|
|
getmetatable(game.stack:top()) ~= PackMenu)
|
|
|
|
watch = {}
|
|
U.wait(6)
|
|
local calls = watch
|
|
watch = nil
|
|
local pose = lastOfSize(calls, 16, 8)
|
|
local rod = lastOfSize(calls, 8, 8)
|
|
check(("%s: a 16x8 pose row was drawn"):format(facing), pose ~= nil)
|
|
check(("%s: and the 8x8 rod tile beside it"):format(facing), rod ~= nil)
|
|
if pose and rod then
|
|
check(("%s: the pose row is sheet row %d")
|
|
:format(facing, POSE_ROW[facing]),
|
|
pose.qx == 0 and pose.qy == POSE_ROW[facing] * 8,
|
|
("%d,%d"):format(pose.qx, pose.qy))
|
|
check(("%s: the pose is%s x-flipped")
|
|
:format(facing, POSE_FLIP[facing] and "" or " not"),
|
|
pose.flip == POSE_FLIP[facing])
|
|
local oam = ROD_OAM[facing]
|
|
check(("%s: the rod tile is $%02x on the sheet's rod row")
|
|
:format(facing, oam.tile),
|
|
rod.qy == ROD_ROW_Y and rod.qx == (oam.tile - 0xfc) * 8,
|
|
("%d,%d"):format(rod.qx, rod.qy))
|
|
check(("%s: the rod tile is%s x-flipped")
|
|
:format(facing, oam.flip and "" or " not"),
|
|
rod.flip == oam.flip)
|
|
-- The pose row sits at the sprite's bottom tile row, so the OAM offsets
|
|
-- are read back relative to it.
|
|
local drop = math.max(0, (world.player.sprite.frameHeight or 16) - 8)
|
|
check(("%s: the rod sits at OAM offset %d,%d from the sprite")
|
|
:format(facing, oam.dx, oam.dy),
|
|
rod.x - pose.x == oam.dx and rod.y - pose.y == oam.dy - drop,
|
|
("%d,%d"):format(rod.x - pose.x, rod.y - pose.y + drop))
|
|
end
|
|
U.shot(game, ("%s/%02d-fishing-%s.png"):format(out, index, facing))
|
|
|
|
-- PutTheRodAway: the pose comes down with the verdict box, not later.
|
|
local down = nil
|
|
for frame = 1, 300 do
|
|
if world.textbox then tap("a", 2) else U.wait(1) end
|
|
if not down and world.player.fishing == nil then down = frame end
|
|
if down and not world.textbox then break end
|
|
end
|
|
check(("%s: the pose came down with the box"):format(facing), down ~= nil)
|
|
watch = {}
|
|
U.wait(8)
|
|
local after = #watch
|
|
watch = nil
|
|
check(("%s: and nothing draws the rod afterwards"):format(facing),
|
|
after == 0, after)
|
|
end
|
|
|
|
for i, facing in ipairs(FACINGS) do
|
|
castAndSample(facing, i)
|
|
end
|
|
|
|
SpriteRenderer.drawTile = realDrawTile
|
|
mapDef.fishGroup = realGroup
|
|
|
|
-- ---- hand off on a live cast --------------------------------------------
|
|
|
|
goToShore("right")
|
|
castRod("right")
|
|
U.wait(20)
|
|
|
|
U.log(("%d shot(s) in %s, one per facing (#1708)."):format(#FACINGS, out))
|
|
U.log("in each one the player's lower half is the two-handed fishing pose,")
|
|
U.log("not the standing legs, and one thin dark rod line runs from the hands")
|
|
U.log("into the water: straight down below the feet facing down, straight up")
|
|
U.log("above the head facing up, and a short stroke starting a tile clear of")
|
|
U.log("the sprite and sloping away from it facing left or right.")
|
|
U.log("the near miss to look for is the pose swapping in with no rod, or the")
|
|
U.log("rod as a garbage block, which is the sheet and the quads disagreeing;")
|
|
U.log("the other is a rod in flat DMG greys while the player is in colour.")
|
|
U.log("the pond bit on nothing for the four shots; the group is back now, so")
|
|
U.log("this last cast can bite. the controls are yours.")
|
|
if failures > 0 then
|
|
U.log(("%d check(s) failed above -- read those before looking at the shots")
|
|
:format(failures))
|
|
end
|
|
|
|
while true do
|
|
coroutine.yield()
|
|
end
|
|
end
|