Files
gen1recomp/tests/game3_pokedex_card_chrome_test.lua
bryanthaboi dee935a4ea CLOSES #2430
2026-09-23 11:58:32 -04:00

276 lines
12 KiB
Lua

#!/usr/bin/env luajit
package.path = "./?.lua;./?/init.lua;" .. package.path
require("tests.game3_cache").requireData("game3_pokedex_card_chrome_test")
local failed = 0
local function check(cond, msg)
if cond then
print("[ok] " .. msg)
else
failed = failed + 1
print("[FAIL] " .. msg)
end
end
local function eq(a, b, msg)
check(a == b, string.format("%s (%s == %s)", msg, tostring(a), tostring(b)))
end
require("src.core.GameVersion").set("firered")
local playedSe = {}
package.loaded["src.core.game3.audio"] = {
playSe = function(id) playedSe[#playedSe + 1] = id end,
playCry = function() end,
playSong = function() end,
stopAll = function() end,
}
local SE = require("src.core.game3.se_ids")
local PokedexChrome = require("src.ui.game3.pokedex_chrome")
local Pokedex = require("src.ui.game3.pokedex")
print("[test] 0. PokedexChrome exposes the pret entry-card chrome")
local hasCard = type(PokedexChrome.dataCardLayout) == "function"
and type(PokedexChrome.areaCardLayout) == "function"
and type(PokedexChrome.cardTilemap) == "function"
and type(PokedexChrome.composeCard) == "function"
and type(PokedexChrome.cardSheet) == "function"
check(hasCard, "card layout / tilemap / compose / sheet API present")
check(PokedexChrome.CARD_TILES == nil, "no tile rows live in source")
check(PokedexChrome.CARD_PALETTE == nil, "no palette table lives in source")
if hasCard then
print("[test] 1. Data card layout matches DexScreen_DexPageZoomEffectFrame(3, 6)")
local L = PokedexChrome.dataCardLayout()
eq(L.left, 0, "data left")
eq(L.top, 2, "data top")
eq(L.width, 28, "data width")
eq(L.height, 14, "data height")
eq(L.divTile, 11, "data divider tile row")
eq(L.x, 0, "data x")
eq(L.y, 16, "data y")
eq(L.w, 240, "data w")
eq(L.h, 128, "data h")
eq(L.borderLeft, 1, "data border left")
eq(L.borderRight, 237, "data border right")
eq(L.borderTop, 17, "data border top")
eq(L.borderBottom, 141, "data border bottom")
eq(L.borderThickness, 2, "data border thickness")
eq(L.dividerY, 88, "data divider y")
eq(L.upperY, 19, "data upper interior y")
eq(L.upperH, 69, "data upper interior height")
eq(L.lowerY, 96, "data lower interior y")
eq(L.lowerH, 45, "data lower interior height")
print("[test] 2. Area card layout has no divider and a full-height interior")
local A = PokedexChrome.areaCardLayout()
eq(A.divTile, nil, "area divider tile row is nil")
eq(A.dividerY, nil, "area divider y is nil")
eq(A.y, 16, "area y")
eq(A.h, 128, "area h")
eq(A.upperY, 19, "area interior y")
eq(A.upperH, 122, "area interior height")
print("[test] 3. composeCard blits a cached tile sheet through the tilemap")
local function synthetic_sheet(tag)
local px = {}
for sy = 0, 31 do
for sx = 0, 63 do
local tile = math.floor(sy / 8) * 8 + math.floor(sx / 8)
px[#px + 1] = string.char(tile, sx % 8 + tag, sy % 8, 255)
end
end
return table.concat(px)
end
local sheet = synthetic_sheet(0)
local card = PokedexChrome.composeCard(L, sheet)
check(type(card) == "string" and #card == 240 * 160 * 4, "data card is 240x160 RGBA")
local function at(buf, x, y)
local o = (y * 240 + x) * 4
return buf:byte(o + 1), buf:byte(o + 2), buf:byte(o + 3), buf:byte(o + 4)
end
local function px_eq(buf, x, y, t, sx, sy, msg)
local r, gch, b, a = at(buf, x, y)
check(r == t and gch == sx and b == sy and a == 255,
string.format("%s (%d,%d) = tile %s px %s,%s", msg, x, y, tostring(r), tostring(gch), tostring(b)))
end
px_eq(card, 5, 3, 0, 5, 3, "backdrop above the card is tile 0")
px_eq(card, 100, 155, 0, 4, 3, "backdrop below the card is tile 0")
px_eq(card, 1, 18, 4, 1, 2, "top-left corner")
px_eq(card, 233, 18, 4, 6, 2, "top-right corner is mirrored")
px_eq(card, 42, 17, 5, 2, 1, "top edge")
px_eq(card, 42, 30, 1, 2, 6, "upper interior")
px_eq(card, 42, 89, 8, 2, 1, "divider row")
px_eq(card, 42, 100, 2, 2, 4, "lower interior")
px_eq(card, 3, 90, 7, 3, 2, "left edge at divider")
px_eq(card, 236, 90, 7, 3, 2, "right edge at divider is mirrored")
px_eq(card, 2, 138, 10, 2, 2, "bottom-left corner")
px_eq(card, 42, 141, 11, 2, 5, "bottom edge")
local areaCard = PokedexChrome.composeCard(A, sheet)
px_eq(areaCard, 42, 89, 1, 2, 1, "area card has no divider row")
px_eq(areaCard, 1, 138, 4, 1, 5, "area bottom-left corner is tile 4 upside down")
px_eq(areaCard, 233, 138, 4, 6, 5, "area bottom-right corner is tile 4 rotated")
px_eq(areaCard, 42, 141, 5, 2, 2, "area bottom edge is tile 5 upside down")
print("[test] 4. Missing or short sheet composes nothing, sheet follows the dex mode")
eq(PokedexChrome.composeCard(L, nil), nil, "no sheet, no card")
eq(PokedexChrome.composeCard(L, "short"), nil, "truncated sheet, no card")
local PokedexData = require("src.core.game3.pokedex_data")
local realUnlocked = PokedexData.isNationalUnlocked
local natSheet = synthetic_sheet(100)
PokedexChrome._installed = true
PokedexChrome._sheets = {}
eq((PokedexChrome.cardSheet()), nil, "cache without the asset yields no sheet")
PokedexChrome._sheets = { kanto = sheet, national = natSheet }
PokedexData.isNationalUnlocked = function() return false end
eq(select(2, PokedexChrome.cardSheet()), "kanto", "kanto dex uses the kanto sheet")
PokedexData.isNationalUnlocked = function() return true end
eq(select(2, PokedexChrome.cardSheet()), "national", "national dex uses the national sheet")
PokedexChrome._sheets = { kanto = sheet }
eq(select(2, PokedexChrome.cardSheet()), "kanto", "national dex without its sheet falls back to kanto")
PokedexData.isNationalUnlocked = realUnlocked
local drawn = { rectangle = 0, draw = 0 }
PokedexChrome._sheets = {}
love = { graphics = {
setColor = function() end,
rectangle = function() drawn.rectangle = drawn.rectangle + 1 end,
draw = function() drawn.draw = drawn.draw + 1 end,
} }
local okMissing = pcall(PokedexChrome.drawDataCardBg)
love = nil
check(not okMissing, "drawDataCardBg raises when the cache has no dex tile sheet")
check(drawn.draw == 0, "nothing is drawn without the sheet")
PokedexChrome._installed = false
print("[test] 4b. Importer bakes a 4bpp sheet the UI can read back")
local Extractor = require("src.import.gba.pokedex_chrome_extract")
local gfx = {}
for i = 1, 64 do gfx[i] = 0 end
gfx[33] = 0x21
local bakedPal = { [0] = 0, [1] = 0x001F, [2] = 0x03E0 }
local baked, bw, bh = Extractor.bakeTileSheet(gfx, bakedPal)
eq(bw, 64, "baked sheet is 8 tiles wide")
eq(bh, 8, "two tiles bake to one row")
eq(#baked, 64 * 8 * 4, "baked sheet is RGBA")
eq(baked:sub(8 * 4 + 1, 8 * 4 + 4), string.char(255, 0, 0, 255), "low nibble is the left pixel")
eq(baked:sub(9 * 4 + 1, 9 * 4 + 4), string.char(0, 255, 0, 255), "high nibble is the right pixel")
eq(baked:sub(1, 4), string.char(0, 0, 0, 255), "index 0 is opaque backdrop")
local Versions = require("src.import.gba.versions")
check(type(Versions.POKEDEX_BG_TILES) == "table"
and Versions.POKEDEX_BG_TILES.kanto and Versions.POKEDEX_BG_TILES.national, "both dex sheets have ROM offsets")
local fake = {}
local fakeCache = { read = function(_, rel) return fake[rel] end }
for _, f in ipairs({ "manifest.lua", "entries.lua", "categories.lua", "orders.lua", "area_markers.lua" }) do
fake["root/pokemon/pokedex/" .. f] = string.rep("x", 32)
end
eq(Extractor.ready(fakeCache, "root"), false, "a cache without the tile sheets is not ready")
fake["root/pokemon/pokedex/dex_tiles_kanto.rgba"] = string.rep("x", 64)
fake["root/pokemon/pokedex/dex_tiles_national.rgba"] = string.rep("x", 64)
for _, f in ipairs({ Extractor.CHROME_FILE, "map_kanto.rgba", "mini_page.rgba",
"blit_wide_ellipse.rgba", "marker_0.rgba", "cat_icon_grassland.rgba" }) do
fake["root/pokemon/pokedex/" .. f] = string.rep("x", 64)
end
eq(Extractor.ready(fakeCache, "root"), false, "a cache without the dex page assets is not ready")
fake["root/pokemon/pokedex/" .. Extractor.PAPER_BG_FILE] =
string.rep("x", Extractor.PAPER_BG_W * Extractor.PAPER_BG_H * 4)
for _, f in ipairs({ "1.rgba", "bulbasaur.rgba", Extractor.FOOTPRINT_QUESTION_MARK_FILE }) do
fake["root/pokemon/pokedex/" .. Extractor.FOOTPRINT_SUB .. "/" .. f] =
string.rep("x", Extractor.FOOTPRINT_W * Extractor.FOOTPRINT_H * 4)
end
eq(Extractor.ready(fakeCache, "root"), true, "tile sheets and chrome graphics complete the cache")
print("[test] 5. Data card tilemap lays the pret tiles")
local g = PokedexChrome.cardTilemap(L)
eq(g[2][0].tile, 4, "top-left corner tile")
eq(g[2][29].tile, 4, "top-right corner tile")
check(g[2][29].flipH == true, "top-right corner is H-flipped")
eq(g[2][1].tile, 5, "top edge tile")
eq(g[17][0].tile, 10, "bottom-left corner tile")
eq(g[17][1].tile, 11, "bottom edge tile")
eq(g[3][0].tile, 6, "left edge above divider")
eq(g[10][0].tile, 6, "left edge still above divider at row 10")
eq(g[11][0].tile, 7, "left edge at divider")
eq(g[12][0].tile, 9, "left edge below divider")
eq(g[16][0].tile, 9, "left edge below divider at last interior row")
check(g[11][29].flipH == true, "right edge at divider is H-flipped")
eq(g[3][1].tile, 1, "upper interior tile")
eq(g[11][1].tile, 8, "divider interior tile")
eq(g[12][1].tile, 2, "lower interior tile")
eq(g[1], nil, "nothing drawn above the card")
eq(g[18], nil, "nothing drawn below the card")
print("[test] 6. Area card tilemap uses flipped tile 4/5 edges and one interior")
local ga = PokedexChrome.cardTilemap(A)
eq(ga[17][0].tile, 4, "area bottom-left corner is tile 4")
check(ga[17][0].flipV == true, "area bottom-left corner is V-flipped")
check(ga[17][29].flipH == true and ga[17][29].flipV == true, "area bottom-right corner is HV-flipped")
eq(ga[17][1].tile, 5, "area bottom edge is tile 5")
check(ga[17][1].flipV == true, "area bottom edge is V-flipped")
eq(ga[11][1].tile, 1, "area interior is unbroken at the data card's divider row")
eq(ga[16][1].tile, 1, "area interior runs to the last row")
eq(ga[3][0].tile, 6, "area left edge")
end
print("[test] 7. Card draws are love-free no-ops headless")
check(love == nil, "no love in this harness")
check(pcall(PokedexChrome.drawDataCardBg), "drawDataCardBg is safe without love")
check(pcall(PokedexChrome.drawAreaCardBg), "drawAreaCardBg is safe without love")
print("[test] 8. Control info matches DexScreen_DrawMonDexPage(justRegistered)")
check(type(Pokedex.controlInfoForDataPage) == "function", "Pokedex.controlInfoForDataPage present")
if type(Pokedex.controlInfoForDataPage) == "function" then
local regCry, regInfo = Pokedex.controlInfoForDataPage("registration")
eq(regCry, nil, "registration prints no CRY hint")
eq(regInfo, "{A_BUTTON}NEXT", "registration prints gText_Next")
local dataCry, dataInfo = Pokedex.controlInfoForDataPage("data")
eq(dataCry, "{START_BUTTON}CRY", "browse prints gText_Cry")
eq(dataInfo, "{A_BUTTON}NEXT DATA {B_BUTTON}CANCEL", "browse prints gText_NextDataCancel")
end
print("[test] 9. Species map to their own habitat category page")
check(type(Pokedex.categoryForSpecies) == "function", "Pokedex.categoryForSpecies present")
if type(Pokedex.categoryForSpecies) == "function" then
eq(Pokedex.categoryForSpecies(19), "grassland", "Rattata is a grassland mon")
eq(Pokedex.categoryForSpecies(10), "forest", "Caterpie is a forest mon")
eq(Pokedex.categoryForSpecies(41), "cave", "Zubat is a cave mon")
eq(Pokedex.categoryForSpecies(144), "rare", "Articuno is a rare mon")
end
print("[test] 10. Registration close runs _onClose once and plays no SE")
local calls = 0
playedSe = {}
Pokedex.showRegistration(19, { onDone = function() calls = calls + 1 end })
eq(Pokedex.screen, "registration", "screen is registration")
eq(Pokedex.subScreenPrev, "category_grid", "header comes from the category page")
eq(Pokedex.currentCategory, "grassland", "header category is the species' habitat")
eq(Pokedex._onDone, nil, "no dead _onDone field is set")
check(type(Pokedex._onClose) == "function", "_onClose holds the callback")
local pressed = {}
local fakeInput = { wasPressed = function(_, b) return pressed[b] == true end }
pressed.a = true
Pokedex.handleInput(fakeInput)
eq(calls, 1, "A on the registration card fires the callback once")
eq(Pokedex.open, false, "registration card closed")
pressed.a = false
Pokedex.handleInput(fakeInput)
eq(calls, 1, "callback is not fired twice")
for _, id in ipairs(playedSe) do
check(id ~= SE.SE_FLEE, "close did not play SE_FLEE (pokedex_screen.c:1045)")
end
check(#playedSe == 0, "close plays no SE at all (" .. #playedSe .. " played)")
print("")
if failed > 0 then
print("FAILED: " .. failed)
os.exit(1)
end
print("ALL PASS")