Files
gen1recomp/tests/game3_fame_screen_test.lua
bryanthaboi d920f7acdb clean up
2026-09-22 17:05:12 -04:00

380 lines
14 KiB
Lua

#!/usr/bin/env luajit
-- pokefirered/src/fame_checker.c:729
package.path = "./?.lua;./?/init.lua;" .. package.path
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
local Cache = require("tests.game3_cache")
local cacheRoot = Cache.mount("trainers.lua")
if not cacheRoot then
print("[skip] game3_fame_screen: " .. tostring(Cache.reason))
os.exit(0)
end
print("[info] FireRed cache at " .. cacheRoot)
local FameChecker = require("src.core.game3.fame_checker")
local Ui = require("src.ui.game3.fame_checker")
local Stack = require("src.ui.game3.stack")
local PERSON = FameChecker.PERSON
local PICK = FameChecker.PICKSTATE
local NSLOT = FameChecker.NUM_FLAVOR_TEXTS
local function press(key)
return { wasPressed = function(_, k) return k == key end }
end
local function newSession()
return { modData = {}, name = "RED", rivalName = "BLUE" }
end
local function openWith(session, opts)
Stack.clear()
Ui.show(session, opts)
end
local function labels()
local out = {}
for i, row in ipairs(Ui.rows()) do out[i] = row.label end
return out
end
print("[test] 1. the list carries the drawn people and CANCEL")
do
local s = newSession()
openWith(s)
local rows = Ui.rows()
eq(#rows, 2, "a new game lists OAK and CANCEL")
eq(rows[1].person, PERSON.OAK, "OAK is the first row")
check(rows[2].cancel == true, "the last row is CANCEL")
-- pokefirered/src/strings.c:128 gFameCheckerText_Cancel
eq(rows[2].label, "CANCEL", "the CANCEL label")
FameChecker.updatePickState(PERSON.BROCK, PICK.COLORED, s)
FameChecker.updatePickState(PERSON.MISTY, PICK.SILHOUETTE, s)
rows = Ui.rows()
eq(#rows, 4, "two more people join the list")
eq(rows[2].person, PERSON.BROCK, "BROCK is row 2")
eq(rows[3].person, PERSON.MISTY, "MISTY is row 3")
eq(rows[2].label, "BROCK", "BROCK's list label comes from gTrainers")
eq(rows[3].label, "MISTY", "MISTY's list label comes from gTrainers")
check(rows[4].cancel == true, "CANCEL stays last")
local names = table.concat(labels(), ",")
check(not names:find("KOGA"), "an undrawn person is not listed (" .. names .. ")")
end
print("[test] 2. the non-trainer names")
do
local s = newSession()
FameChecker.fullyUnlock(s)
openWith(s)
-- pokefirered/src/strings.c:1272
eq(Ui.personName(PERSON.OAK), "OAK", "OAK")
eq(Ui.personName(PERSON.DAISY), "DAISY", "DAISY")
eq(Ui.personName(PERSON.BILL), "BILL", "BILL")
eq(Ui.personName(PERSON.MRFUJI), "FUJI", "FUJI")
eq(Ui.personName(PERSON.GIOVANNI), "GIOVANNI", "GIOVANNI comes from gTrainers")
eq(#Ui.rows(), FameChecker.NUM_PERSONS + 1, "a fully unlocked checker lists all 16 plus CANCEL")
end
print("[test] 3. the per-person page shows only the unlocked slots")
do
local s = newSession()
FameChecker.updatePickState(PERSON.BROCK, PICK.COLORED, s)
FameChecker.setFlavorText(PERSON.BROCK, 1, s)
FameChecker.setFlavorText(PERSON.BROCK, 4, s)
openWith(s)
Ui.handleInput(press("down"))
eq(Ui.selectedPerson(), PERSON.BROCK, "the cursor is on BROCK")
local icons = Ui.icons(PERSON.BROCK)
local unlocked = {}
for slot = 0, NSLOT - 1 do
if icons[slot].unlocked then unlocked[#unlocked + 1] = slot end
end
eq(#unlocked, 2, "two of the six panels are unlocked")
eq(unlocked[1], 1, "slot 1 is unlocked")
eq(unlocked[2], 4, "slot 4 is unlocked")
-- pokefirered/src/fame_checker.c:265 sFameCheckerArrayNpcGraphicsIds
eq(icons[1].graphicsId, 80, "slot 1 draws OBJ_EVENT_GFX_BROCK")
eq(icons[4].graphicsId, 30, "slot 4 draws OBJ_EVENT_GFX_BALDING_MAN")
eq(icons[0].graphicsId, nil, "a locked slot has no icon graphic")
eq(icons[2].graphicsId, nil, "a locked slot has no icon graphic")
check(Ui.personHasUnlockedPanels(PERSON.BROCK), "BROCK has unlocked panels")
check(not Ui.personHasUnlockedPanels(PERSON.KOGA), "KOGA has none")
end
print("[test] 4. the pick mode picture")
do
local s = newSession()
FameChecker.updatePickState(PERSON.BROCK, PICK.COLORED, s)
FameChecker.updatePickState(PERSON.MISTY, PICK.SILHOUETTE, s)
openWith(s)
-- pokefirered/src/fame_checker.c:171 sFameCheckerTrainerPicIdxs
local source, path = Ui.portraitSource(PERSON.BROCK)
eq(source, "trainer", "BROCK uses CreateTrainerPicSprite")
check(path:find("/trainers/front/116%.rgba$") ~= nil,
"TRAINER_PIC_LEADER_BROCK is pic 116 (" .. tostring(path) .. ")")
local f = io.open(path, "rb")
check(f ~= nil, "the trainer pic is in the cache today")
if f then
local bytes = f:read("*a")
f:close()
eq(#bytes, 64 * 64 * 4, "it is a 64x64 RGBA sprite")
end
-- pokefirered/src/fame_checker.c:1347
for _, p in ipairs({ PERSON.OAK, PERSON.DAISY, PERSON.BILL, PERSON.MRFUJI }) do
local src2, path2 = Ui.portraitSource(p)
eq(src2, "art", "person " .. p .. " uses the Fame Checker's own art")
check(path2:find("/fame_checker/" .. p .. "%.rgba$") ~= nil,
"person " .. p .. " reads " .. tostring(path2))
end
eq(Ui.portraitSource(FameChecker.NUM_PERSONS), nil, "person 16 has no picture")
-- pokefirered/src/fame_checker.c:1374 sSilhouettePalette
eq(FameChecker.pickState(s, PERSON.MISTY), PICK.SILHOUETTE,
"MISTY is drawn as a silhouette")
eq(FameChecker.pickState(s, PERSON.BROCK), PICK.COLORED, "BROCK is drawn coloured")
end
print("[test] 5. the art and text missing fallback")
do
local s = newSession()
FameChecker.updatePickState(PERSON.BROCK, PICK.COLORED, s)
FameChecker.setFlavorText(PERSON.BROCK, 1, s)
openWith(s)
Ui.handleInput(press("down"))
Ui.handleInput(press("start"))
check(Ui.pickMode, "START put BROCK in pick mode")
local pack = Ui.pack()
if pack == nil then
print("[info] no fame_checker pack in this cache; checking the fallback")
eq(Ui.flavorText(PERSON.BROCK, 1), nil, "no flavour text without the pack")
eq(Ui.pickModeText(PERSON.BROCK), nil, "no name or quote text without the pack")
eq(Ui.messageText(), "BROCK", "pick mode falls back to the name-only list entry")
Ui.handleInput(press("start"))
check(not Ui.pickMode, "START left pick mode again")
Ui.handleInput(press("a"))
eq(Ui.mode, "flavor", "the page still opens")
-- pokefirered/src/fame_checker.c:944
eq(Ui.messageText(), nil, "a locked icon leaves the message box empty")
Ui.handleInput(press("right"))
eq(Ui.iconCursor, 1, "the cursor is on the unlocked icon")
eq(Ui.messageText(), "BROCK", "the page falls back to the name-only entry")
else
print("[info] fame_checker pack present; checking the real text")
local text = Ui.flavorText(PERSON.BROCK, 1)
check(type(text) == "string" and #text > 0, "BROCK slot 1 has flavour text")
check(type(Ui.pickModeText(PERSON.BROCK)) == "string", "BROCK has a name string")
local msg = Ui.messageText()
check(type(msg) == "string" and #msg > 0, "pick mode prints it")
end
end
print("[test] 6. START enters and leaves pick mode")
do
local s = newSession()
FameChecker.updatePickState(PERSON.BROCK, PICK.COLORED, s)
openWith(s)
Ui.handleInput(press("down"))
check(not Ui.pickMode, "pick mode starts off")
Ui.handleInput(press("start"))
check(Ui.pickMode, "START enters pick mode")
Ui.handleInput(press("start"))
check(not Ui.pickMode, "START leaves it again")
Ui.handleInput(press("start"))
check(Ui.pickMode, "START enters it once more")
Ui.handleInput(press("b"))
check(not Ui.pickMode, "B leaves pick mode instead of closing")
check(Ui.isOpen(), "the screen is still open")
Ui.handleInput(press("down"))
check(Ui.selectedRow().cancel == true, "the cursor walked down to CANCEL")
Ui.handleInput(press("start"))
check(not Ui.pickMode, "START over CANCEL does nothing")
end
print("[test] 7. A opens the flavour text page, B backs out, CANCEL closes")
do
local s = newSession()
FameChecker.updatePickState(PERSON.KOGA, PICK.SILHOUETTE, s)
openWith(s)
Ui.handleInput(press("down"))
eq(Ui.selectedPerson(), PERSON.KOGA, "KOGA is row 2")
Ui.handleInput(press("a"))
eq(Ui.mode, "top", "A does nothing for a person with no unlocked panels")
FameChecker.setFlavorText(PERSON.KOGA, 0, s)
Ui.handleInput(press("a"))
eq(Ui.mode, "flavor", "A opens the page once a panel is unlocked")
Ui.handleInput(press("b"))
eq(Ui.mode, "top", "B returns to the list")
check(Ui.isOpen(), "and leaves the screen open")
Ui.handleInput(press("down"))
check(Ui.selectedRow().cancel == true, "the cursor is on CANCEL")
Ui.handleInput(press("a"))
check(not Ui.isOpen(), "A on CANCEL closes the Fame Checker")
check(not Stack.has("fame_checker"), "and pops the UI stack layer")
end
print("[test] 8. B and SELECT close from the top menu")
do
local s = newSession()
openWith(s)
Ui.handleInput(press("b"))
check(not Ui.isOpen(), "B closes from the top menu")
-- pokefirered/src/fame_checker.c:741
openWith(s)
Ui.handleInput(press("select"))
check(not Ui.isOpen(), "SELECT closes when opened from the field")
openWith(s, { fromBag = true })
Ui.handleInput(press("select"))
check(Ui.isOpen(), "SELECT does nothing when opened from the bag")
Ui.close()
end
print("[test] 9. the icon cursor wraps the way pret wraps it")
do
local s = newSession()
FameChecker.updatePickState(PERSON.BROCK, PICK.COLORED, s)
for slot = 0, NSLOT - 1 do FameChecker.setFlavorText(PERSON.BROCK, slot, s) end
openWith(s)
Ui.handleInput(press("down"))
Ui.handleInput(press("a"))
eq(Ui.mode, "flavor", "the page is open")
eq(Ui.iconCursor, 0, "it starts on slot 0")
-- pokefirered/src/fame_checker.c:889
Ui.handleInput(press("right"))
eq(Ui.iconCursor, 1, "right moves along the row")
Ui.handleInput(press("right"))
eq(Ui.iconCursor, 2, "right again")
Ui.handleInput(press("right"))
eq(Ui.iconCursor, 0, "right off the end of the row wraps back to its start")
Ui.handleInput(press("left"))
eq(Ui.iconCursor, 2, "left off the start wraps to the end of the row")
Ui.handleInput(press("down"))
eq(Ui.iconCursor, 5, "down moves to the second row")
Ui.handleInput(press("down"))
eq(Ui.iconCursor, 2, "down again comes back to the first row")
Ui.handleInput(press("up"))
eq(Ui.iconCursor, 5, "up is the same move as down")
Ui.handleInput(press("left"))
eq(Ui.iconCursor, 4, "left on the second row")
Ui.handleInput(press("right"))
eq(Ui.iconCursor, 5, "right back")
Ui.handleInput(press("b"))
Ui.handleInput(press("down"))
eq(Ui.iconCursor, 0, "moving the list cursor resets the icon cursor")
Ui.close()
end
print("[test] 10. the UI help line tracks pret's three states")
do
local s = newSession()
FameChecker.updatePickState(PERSON.BROCK, PICK.COLORED, s)
openWith(s)
Ui.handleInput(press("down"))
-- pokefirered/src/strings.c:1270 gFameCheckerText_PickScreenUI
eq(Ui.helpText(), "{START_BUTTON}PICK {DPAD_UPDOWN}SELECT {B_BUTTON}CANCEL",
"a person with no unlocked panels shows PrintUIHelp(1)")
FameChecker.setFlavorText(PERSON.BROCK, 0, s)
-- pokefirered/src/strings.c:1269 gFameCheckerText_MainScreenUI
eq(Ui.helpText(), "{START_BUTTON}PICK {DPAD_UPDOWN}SELECT {A_BUTTON}OK",
"once a panel is unlocked it shows PrintUIHelp(0)")
Ui.handleInput(press("start"))
eq(Ui.helpText(), "{START_BUTTON}PICK {DPAD_UPDOWN}SELECT {B_BUTTON}CANCEL",
"pick mode shows PrintUIHelp(1)")
Ui.handleInput(press("start"))
Ui.handleInput(press("a"))
-- pokefirered/src/strings.c:1271 gFameCheckerText_FlavorTextUI
eq(Ui.helpText(), "{DPAD_ANY}PICK {A_BUTTON}READ {B_BUTTON}CANCEL",
"the flavour text page shows PrintUIHelp(2)")
Ui.close()
end
print("[test] 11. the CANCEL row describes itself")
do
local s = newSession()
openWith(s)
Ui.handleInput(press("down"))
-- pokefirered/src/strings.c:601 gFameCheckerText_FameCheckerWillBeClosed
eq(Ui.messageText(), "The FAME CHECKER will be closed.",
"PrintCancelDescription runs on the CANCEL row")
Ui.handleInput(press("up"))
eq(Ui.messageText(), nil, "a person row leaves the message box empty")
Ui.close()
end
print("[test] 12. the list scrolls the way pret's ListMenu scrolls")
do
local s = newSession()
for p = PERSON.BROCK, PERSON.BLAINE do
FameChecker.updatePickState(p, PICK.COLORED, s)
end
openWith(s)
eq(#Ui.rows(), 9, "eight drawn people plus CANCEL")
local function row() return Ui.cursor - 1 - Ui.scroll end
eq(Ui.scroll, 0, "the list starts unscrolled")
eq(row(), 0, "and on its first row")
-- pokefirered/src/list_menu.c:438 ListMenuUpdateSelectedRowIndexAndScrollOffset
for i = 1, 3 do
Ui.handleInput(press("down"))
eq(Ui.scroll, 0, "down " .. i .. " does not scroll yet")
eq(row(), i, "down " .. i .. " walks the cursor to row " .. i)
end
Ui.handleInput(press("down"))
eq(Ui.cursor, 5, "the fourth down selects the fifth entry")
eq(Ui.scroll, 1, "and scrolls the list instead of the cursor")
eq(row(), 3, "the cursor holds on row 3 going down")
for _ = 1, 4 do Ui.handleInput(press("down")) end
eq(Ui.cursor, 9, "eight downs reach CANCEL")
eq(Ui.scroll, 4, "the list is scrolled to its end")
eq(row(), 4, "and the cursor drops to the last visible row")
Ui.handleInput(press("down"))
eq(Ui.cursor, 9, "down on the last row does not wrap")
for _ = 1, 3 do Ui.handleInput(press("up")) end
eq(Ui.cursor, 6, "three ups walk back up the window")
eq(Ui.scroll, 4, "without scrolling")
eq(row(), 1, "the cursor holds on row 1 going up")
Ui.handleInput(press("up"))
eq(Ui.cursor, 5, "the fourth up selects the fifth entry")
eq(Ui.scroll, 3, "and scrolls the list back")
eq(row(), 1, "the cursor stays on row 1")
for _ = 1, 8 do Ui.handleInput(press("up")) end
eq(Ui.cursor, 1, "eight more ups reach the top")
eq(Ui.scroll, 0, "with the list unscrolled")
Ui.handleInput(press("up"))
eq(Ui.cursor, 1, "up on the first row does not wrap")
Ui.close()
end
if failed == 0 then
print("PASS game3_fame_screen")
os.exit(0)
end
print("FAIL game3_fame_screen failures=" .. failed)
os.exit(1)