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
312 lines
13 KiB
Lua
312 lines
13 KiB
Lua
-- SPRITEMOVEDATA_POKEMON ($16), the row every overworld mon object stands on.
|
|
--
|
|
-- luajit tests/gen2_ow_bounce_test.lua (ROM-free; the cache section SKIPs
|
|
-- without a gold cache)
|
|
--
|
|
-- data/sprites/map_objects.asm:181-187 gives the row SPRITEMOVEFN_BOUNCE and
|
|
-- OBJECT_ACTION_BOUNCE. SetFacingBounce increments OBJECT_STEP_FRAME once a
|
|
-- frame, masks it to four bits and reads bit 3: set means FACING_STEP_UP_0,
|
|
-- clear falls into SetFacingFreezeBounce and FACING_STEP_DOWN_0
|
|
-- (engine/overworld/map_object_action.asm:184-202). Those are tiles $00..$03
|
|
-- and $04..$07 of the mon's menu icon (data/sprites/facings.asm:43-72), i.e.
|
|
-- the two 16x16 halves of the 16x32 sheet extractIcons already writes.
|
|
--
|
|
-- Both halves of #1748 are pinned here: the animation itself, and the sheet
|
|
-- being two frames deep so the animation has a second pose to reach.
|
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
|
|
|
local S = require("tests.harness").suite("gen2 ow bounce")
|
|
local check, eq = S.check, S.eq
|
|
|
|
love = require("tests.love_stub")
|
|
|
|
local NPC = require("src.world.gen2.Npc")
|
|
local SpriteRenderer = require("src.render.SpriteRenderer")
|
|
local World = require("src.world.gen2.World")
|
|
|
|
-- constants/map_object_constants.asm:135-151
|
|
local SPRITEMOVEDATA_STILL = 0x01
|
|
local SPRITEMOVEDATA_STANDING_DOWN = 0x06
|
|
local SPRITEMOVEDATA_POKEMON = 0x16
|
|
|
|
-- trueColor short-circuits SpriteRenderer past the OBP bake, so :draw here
|
|
-- needs no canvases (the same trick tests/gen2_big_object_test.lua uses).
|
|
local MON_SHEET = {
|
|
id = "SPRITE_JIGGLYPUFF", frames = 2, trueColor = true,
|
|
spriteType = "POKEMON_SPRITE", walker = false,
|
|
image = "assets/generated/icons/gen2/jigglypuff.png",
|
|
}
|
|
local DOLL_SHEET = {
|
|
id = "SPRITE_FAIRY", frames = 6, trueColor = true,
|
|
spriteType = "WALKING_SPRITE", walker = true,
|
|
image = "assets/generated/sprites/fairy.png",
|
|
}
|
|
|
|
local function build(movement, sheet)
|
|
return NPC.new("TEST_MAP",
|
|
{ index = 1, movement = movement, x = 4, y = 4 }, sheet or MON_SHEET)
|
|
end
|
|
|
|
-- ---- the movement byte -----------------------------------------------------
|
|
eq(NPC.MOVE.POKEMON, SPRITEMOVEDATA_POKEMON,
|
|
"SPRITEMOVEDATA_POKEMON is $16 in map_object_constants.asm")
|
|
|
|
-- Without the seam every block below it raises rather than reporting, which
|
|
-- turns a whole suite of answers into one traceback.
|
|
local HAS_BOUNCE = check(type(NPC.bounceFrame) == "function",
|
|
"NPC:bounceFrame is the seam OBJECT_ACTION_BOUNCE draws through")
|
|
if not HAS_BOUNCE then
|
|
NPC.bounceFrame = function() return nil end
|
|
end
|
|
|
|
do
|
|
local mon = build(SPRITEMOVEDATA_POKEMON)
|
|
check(mon.bouncing == true, "a $16 object is built bouncing")
|
|
check(mon.fixedFacing == true,
|
|
"and keeps FIXED_FACING, which the $16 row's flags1 byte carries")
|
|
eq(mon.facing, "down", "its `db DOWN ; facing` column is DOWN")
|
|
eq(select(1, NPC.patternFor(SPRITEMOVEDATA_POKEMON)), "stand",
|
|
"MovementFunction_Bouncing parks it on STEP_TYPE_STANDING: no walking")
|
|
end
|
|
|
|
-- ---- eight frames down, eight frames up ------------------------------------
|
|
-- `inc a / and %00001111 / ld [hl], a / and %00001000` is a sixteen-frame
|
|
-- cycle with an eight-frame dwell on each pose, and the counter is stepped
|
|
-- BEFORE the bit is read -- so the spawn frame is pose 0 and so are the seven
|
|
-- after it.
|
|
do
|
|
local mon = build(SPRITEMOVEDATA_POKEMON)
|
|
eq(mon.bounceStep, 0, "OBJECT_STEP_FRAME starts at zero")
|
|
local seen, wrong = { mon:bounceFrame() }, nil
|
|
for i = 1, 31 do
|
|
mon:update()
|
|
seen[i + 1] = mon:bounceFrame()
|
|
end
|
|
for i = 0, 31 do
|
|
local want = math.floor(i / 8) % 2
|
|
if seen[i + 1] ~= want and not wrong then
|
|
wrong = ("frame %d drew pose %s, wanted %d")
|
|
:format(i, tostring(seen[i + 1]), want)
|
|
end
|
|
end
|
|
check(wrong == nil, "two full cycles run 0,0..0,1,1..1 eight frames apiece"
|
|
.. (wrong and (" -- " .. wrong) or ""))
|
|
eq(seen[1], 0, "the spawn frame is FacingStepDown0, the icon's first half")
|
|
eq(seen[9], 1, "the ninth is FacingStepUp0, its second half")
|
|
eq(seen[17], 0, "and the seventeenth is back to the first")
|
|
eq(mon.bounceStep, 31 % 16, "the counter wrapped at sixteen, not at eight")
|
|
end
|
|
|
|
-- ---- the frozen column -----------------------------------------------------
|
|
-- OBJECT_ACTION_BOUNCE's second entry is SetFacingFreezeBounce, which writes
|
|
-- FACING_STEP_DOWN_0 and never touches OBJECT_STEP_FRAME. So a mon held for a
|
|
-- conversation pins its FIRST pose and resumes on the phase it left.
|
|
do
|
|
local mon = build(SPRITEMOVEDATA_POKEMON)
|
|
for _ = 1, 10 do mon:update() end
|
|
eq(mon:bounceFrame(), 1, "ten frames in, the mon is on the up pose")
|
|
eq(mon.bounceStep, 10, "with OBJECT_STEP_FRAME at 10")
|
|
|
|
mon.frozen = true
|
|
local pinned = true
|
|
for _ = 1, 40 do
|
|
mon:update()
|
|
if mon:bounceFrame() ~= 0 then pinned = false end
|
|
end
|
|
check(pinned, "frozen, it holds FacingStepDown0 for the whole conversation")
|
|
eq(mon.bounceStep, 10,
|
|
"and SetFacingFreezeBounce leaves the step counter where it was")
|
|
|
|
mon.frozen = false
|
|
mon:update()
|
|
eq(mon.bounceStep, 11, "released, the counter carries on from 10")
|
|
eq(mon:bounceFrame(), 1, "so the bounce resumes on the phase it froze at")
|
|
end
|
|
|
|
-- ---- every other movement byte is still ------------------------------------
|
|
-- Only the $16 row carries OBJECT_ACTION_BOUNCE; the Clefairy, Charizard and
|
|
-- Pidgeot dolls are SPRITEMOVEDATA_STANDING_DOWN and take
|
|
-- OBJECT_ACTION_STAND (data/sprites/map_objects.asm:53-59).
|
|
do
|
|
for _, movement in ipairs({ SPRITEMOVEDATA_STILL, SPRITEMOVEDATA_STANDING_DOWN,
|
|
NPC.MOVE.WANDER, NPC.MOVE.SPINRANDOM_SLOW, NPC.MOVE.BIGDOLL }) do
|
|
local npc = build(movement, DOLL_SHEET)
|
|
check(not npc.bouncing,
|
|
("movement $%02x does not bounce"):format(movement))
|
|
check(npc:bounceFrame() == nil,
|
|
("and $%02x asks for no frame override"):format(movement))
|
|
end
|
|
|
|
-- The other half of it: a mon SHEET on a still row stays still. Gold has no
|
|
-- such object, but a mod or a variablesprite slot can make one.
|
|
local still = build(SPRITEMOVEDATA_STILL, MON_SHEET)
|
|
for _ = 1, 40 do still:update() end
|
|
check(still:bounceFrame() == nil,
|
|
"a POKEMON_SPRITE sheet on a STILL row never bounces either")
|
|
end
|
|
|
|
-- ---- the frame reaches the renderer ----------------------------------------
|
|
-- NPC:draw is where the pose is spent: SpriteRenderer:draw's trailing
|
|
-- frameOverride argument. A bounce the draw call drops looks exactly like no
|
|
-- bounce at all.
|
|
do
|
|
local mon = build(SPRITEMOVEDATA_POKEMON)
|
|
local doll = build(SPRITEMOVEDATA_STANDING_DOWN, DOLL_SHEET)
|
|
local seen = {}
|
|
local function recorder(who)
|
|
return { draw = function(_, _, _, _, _, _, _, _, _, _, frameOverride)
|
|
seen[who] = { override = frameOverride, count = (seen[who]
|
|
and seen[who].count or 0) + 1 }
|
|
end }
|
|
end
|
|
mon.sprite, doll.sprite = recorder("mon"), recorder("doll")
|
|
|
|
mon:draw(0, 0, 1)
|
|
doll:draw(0, 0, 1)
|
|
eq(seen.mon.override, 0, "the mon's first draw overrides to frame 0")
|
|
check(seen.doll.override == nil, "and the doll's overrides to nothing")
|
|
|
|
for _ = 1, 8 do mon:update() end
|
|
mon:draw(0, 0, 1)
|
|
eq(seen.mon.override, 1, "eight frames later it overrides to frame 1")
|
|
end
|
|
|
|
-- ---- a one-frame sheet has nowhere to bounce to ----------------------------
|
|
-- SpriteRenderer builds one quad per frame and :draw only honours an override
|
|
-- it has a quad for, so the extractor half of #1748 is load bearing: with
|
|
-- `frames = 1` the up pose is simply not on the sheet.
|
|
do
|
|
local one = SpriteRenderer.new({
|
|
id = "ONE", frames = 1, trueColor = true, image = MON_SHEET.image }, "one")
|
|
eq(one.frameCount, 1, "a frames = 1 def builds one quad")
|
|
check(one.frames[1] == nil, "so there is no second frame to override to")
|
|
|
|
local two = SpriteRenderer.new(MON_SHEET, "two")
|
|
eq(two.frameCount, 2, "a frames = 2 def builds both")
|
|
check(two.frames[1] ~= nil, "and frame 1 is a real quad")
|
|
eq(two:getFrameGeometry(1).y, two.frameHeight,
|
|
"which is the lower 16x16 of the 16x32 icon sheet")
|
|
end
|
|
|
|
-- ---- World:load repairs a stale cache --------------------------------------
|
|
-- The extractor fix only reaches a player who re-imports, and there is no
|
|
-- non-file lever that would force one. World:load back-fills the SpriteMons
|
|
-- rows on the way past, keyed on `source` so a mod's own POKEMON_SPRITE row is
|
|
-- left exactly as the mod wrote it.
|
|
do
|
|
local sprites = {
|
|
SPRITE_JIGGLYPUFF = { id = "SPRITE_JIGGLYPUFF", frames = 1,
|
|
source = "ROM:SpriteMons[20]", spriteType = "POKEMON_SPRITE",
|
|
image = "assets/generated/icons/gen2/jigglypuff.png" },
|
|
SPRITE_CHRIS = { id = "SPRITE_CHRIS", frames = 12, walker = true,
|
|
source = "ROM:OverworldSprites[0]",
|
|
image = "assets/generated/sprites/chris.png" },
|
|
SPRITE_MOD_MON = { id = "SPRITE_MOD_MON", frames = 1,
|
|
source = "MOD:pocket-monsters", spriteType = "POKEMON_SPRITE",
|
|
image = "mods/pocket-monsters/mon.png" },
|
|
}
|
|
local world = World.new({ data = {
|
|
gen2Maps = { TEST_MAP = { width = 1, height = 1 } },
|
|
gen2Tilesets = {},
|
|
gen2Sprites = sprites,
|
|
} })
|
|
-- load goes on to tilesets, palettes and map art this stub has none of; the
|
|
-- back-fill sits in its first dozen lines, so let the rest fall over.
|
|
pcall(world.load, world)
|
|
eq(sprites.SPRITE_JIGGLYPUFF.frames, 2,
|
|
"a pre-#1748 SpriteMons row is back-filled to two frames on load")
|
|
eq(sprites.SPRITE_CHRIS.frames, 12,
|
|
"an OverworldSprites row is not touched")
|
|
eq(sprites.SPRITE_MOD_MON.frames, 1,
|
|
"and a mod's own POKEMON_SPRITE row keeps the frame count it declared")
|
|
end
|
|
|
|
-- ---- the day-care pair -----------------------------------------------------
|
|
-- GetMonSprite's .BreedMon1 / .BreedMon2 arms have no sprites.lua row to
|
|
-- back-fill (engine/overworld/overworld.asm:279-305), so the def World builds
|
|
-- by hand has to carry the second frame itself.
|
|
do
|
|
local world = World.new({ data = { gen2Icons = {
|
|
species = { PIKACHU = "ICON_PIKACHU" },
|
|
icons = { ICON_PIKACHU = {
|
|
image = "assets/generated/icons/gen2/pikachu.png" } },
|
|
} } })
|
|
local def = world:breedmonSpriteDef("PIKACHU")
|
|
check(def ~= nil, "a deposited PIKACHU builds a day-care sprite def")
|
|
eq(def and def.frames, 2, "two frames, like every other SpriteMons row")
|
|
eq(def and def.spriteType, "POKEMON_SPRITE", "and it is a mon sheet")
|
|
end
|
|
|
|
-- ---- the cache -------------------------------------------------------------
|
|
-- Same default every other gen2 suite uses, so a run with no GOLD_CACHE set
|
|
-- still reads the cache instead of skipping silently.
|
|
local cache = os.getenv("GOLD_CACHE")
|
|
or ((os.getenv("HOME") or "") .. "/Library/Application Support/LOVE/gold-dev/gold")
|
|
local function loadCache(name)
|
|
local chunk = loadfile(cache .. "/data/generated/" .. name .. ".lua")
|
|
return chunk and chunk() or nil
|
|
end
|
|
|
|
local sprites = loadCache("sprites")
|
|
if not sprites then
|
|
check(true, "no GOLD_CACHE: the extracted rows are not checked (SKIP)")
|
|
else
|
|
local monRows, thin = 0, {}
|
|
for id, def in pairs(sprites) do
|
|
if type(def) == "table" and type(def.source) == "string"
|
|
and def.source:find("^ROM:SpriteMons") then
|
|
monRows = monRows + 1
|
|
if (def.frames or 1) < 2 then thin[#thin + 1] = id end
|
|
end
|
|
end
|
|
check(monRows > 0, "the cache carries SpriteMons rows at all")
|
|
if #thin > 0 then
|
|
check(true, ("cache predates #1748 (%d thin rows, first %s) : World:load"
|
|
.. " back-fills them, a re-import writes them (SKIP)")
|
|
:format(#thin, thin[1]))
|
|
else
|
|
check(true, ("all %d SpriteMons rows are two frames deep"):format(monRows))
|
|
end
|
|
|
|
-- The objects the reporter was looking at: every $16 object on every map has
|
|
-- to name a sheet that the bounce can actually flip.
|
|
local maps = loadCache("maps")
|
|
if not maps then
|
|
check(true, "no maps.lua: the $16 objects are not checked (SKIP)")
|
|
else
|
|
local bouncers, slots, unresolved, oneFrame = 0, 0, {}, {}
|
|
for mapId, def in pairs(maps) do
|
|
if type(def) == "table" then
|
|
for _, obj in ipairs(def.objects or {}) do
|
|
if obj.movement == SPRITEMOVEDATA_POKEMON then
|
|
bouncers = bouncers + 1
|
|
if type(obj.sprite) == "number" then
|
|
-- The day-care pair: GetMonSprite's .BreedMon arms resolve those
|
|
-- two bytes to the deposited species' icon at spawn time, so
|
|
-- there is no sprites.lua row for them to name.
|
|
slots = slots + 1
|
|
elseif not sprites[obj.sprite] then
|
|
unresolved[#unresolved + 1] = mapId
|
|
elseif (sprites[obj.sprite].frames or 1) < 2 then
|
|
oneFrame[#oneFrame + 1] = mapId
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
check(bouncers > 0, ("the cache carries %d SPRITEMOVEDATA_POKEMON objects")
|
|
:format(bouncers))
|
|
check(#unresolved == 0, "every named one of them names a sheet the cache has"
|
|
.. (unresolved[1] and (" (%s does not)"):format(unresolved[1]) or ""))
|
|
check(slots == 2, ("and %d of them are the two day-care slots"):format(slots))
|
|
if #oneFrame > 0 then
|
|
check(true, ("%d of them sit on a pre-#1748 row : World:load back-fills"
|
|
.. " them (SKIP)"):format(#oneFrame))
|
|
else
|
|
check(true, "and every one of those sheets is two frames deep")
|
|
end
|
|
end
|
|
end
|
|
|
|
S.finish()
|