#!/usr/bin/env luajit -- ChangePokemonNickname special: open naming after TO_BLACK, apply nick, clear fade. package.path = "./?.lua;./?/init.lua;" .. package.path require("tests.game3_cache").mountOrSkip("game3_nickname_test") local failed = 0 local function check(cond, msg) if cond then print("[ok] " .. msg) else failed = failed + 1 print("[FAIL] " .. msg) end end print("[test] 1. Special id matches pret specials.inc") local Std = require("src.core.game3.scripting.stdscripts") check(Std.SPECIAL.ChangePokemonNickname == 158, "ChangePokemonNickname = 158") check(Std.SPECIAL.BufferMonNickname == 124, "BufferMonNickname = 124") print("[test] 2. Handler registered") local Natives = require("src.core.game3.scripting.natives") check(Natives.ALLOW["special:158"] ~= nil, "special:158 handler") check(Natives.ALLOW["special:124"] ~= nil, "special:124 handler") print("[test] 3. ChangePokemonNickname opens naming + fades in + sets nick") local Fade = require("src.ui.game3.fade") local Naming = require("src.ui.game3.naming") -- Stub Stack so Naming.open works headless package.loaded["src.ui.game3.stack"] = { push = function() end, pop = function() end, busy = function() return false end, drawOrder = function() return {} end, } -- Force re-require naming with stubs package.loaded["src.ui.game3.naming"] = nil local romTextReal = package.loaded["src.core.game3.rom_text"] local ROM_TEXT = { gText_PkmnsNickname = "'s nickname?" } local function romTextKey(n, i, j) return j and (n .. "[" .. i .. "][" .. j .. "]") or (n .. "[" .. i .. "]") end local function romTextPlain(key, ctx) return ((ROM_TEXT[key] or key):gsub("{PLAYER}", ctx and ctx.playerName or "")) end package.loaded["src.core.game3.rom_text"] = { plain = romTextPlain, box = romTextPlain, ascii = romTextPlain, has = function() return true end, key = romTextKey, at = function(n, i, j, ctx) return romTextPlain(romTextKey(n, i, j), ctx) end, count = function() return 0 end, list = function() return {} end, lazy = function(map) return setmetatable({}, { __index = function(_, k) return map[k] end }) end, } Naming = require("src.ui.game3.naming") package.loaded["src.core.game3.rom_text"] = romTextReal local mon = { species = 1, nickname = "", name = "BULBASAUR" } package.loaded["src.core.game3.runtime"] = { getSession = function() return { party = { mon } } end, isActive = function() return true end, } Fade.begin(Fade.MODE.TO_BLACK, 1) -- Finish TO_BLACK instantly Fade.t = 16 Fade.active = false local opened = false local finished = false local ctx = { getVar = function(_, id) if id == 0x8004 then return 0 end return 0 end, mode = "bytecode", status = "running", } local adapters = { openNaming = function(opts, done) opened = true check(opts.template == "NICKNAME", "template NICKNAME") check(opts.maxLen == 10, "maxLen 10") check(type(opts.title) == "string" and opts.title:find("nickname"), "title has nickname") -- Mimic field adapter: fade from black after open Naming.open(opts) if (Fade.t or 0) > 0 then Fade.begin(Fade.MODE.FROM_BLACK, 1) end -- Confirm a name Naming.close("SPROUT") if done then done("SPROUT") end end, log = print, } local yielded = Natives.special(ctx, 158, adapters) check(opened, "openNaming invoked for special 158") check(yielded == true or finished or mon.nickname == "SPROUT", "special yielded or applied") -- Poll until native finishes local guard = 0 while ctx.nativePoll and not ctx.nativePoll() and guard < 10 do guard = guard + 1 end if ctx.nativePoll and ctx.nativePoll() then finished = true end check(mon.nickname == "SPROUT", "party mon nickname SPROUT (got " .. tostring(mon.nickname) .. ")") check(finished or mon.nickname == "SPROUT", "native wait completed") opened = false mon.nickname = "" Natives.special(ctx, 159, { log = function() end, chooseParty = function(_, done) done(0) end }) check(not opened, "special 159 does not open the naming keyboard") check(mon.nickname == "", "special 159 leaves the nickname alone") mon.nickname = "SPROUT" print("[test] 4. Fade-from-black after TO_BLACK cover") Fade.t = 16 Fade.active = false Fade.begin(Fade.MODE.FROM_BLACK, 1) check(Fade.active == true and Fade.t == 16 and Fade._dir == -1, "FROM_BLACK starts covered") print("[test] 5. Naming screen button highlights and layout parity") local NamingChrome = require("src.ui.game3.naming_chrome") local expectedKeys = { "page_swap_button_glow", "back_button_glow", "ok_button_glow", "cursor", "page_swap_frame", } local keySet = {} for _, k in ipairs(NamingChrome.KEYS or {}) do keySet[k] = true end for _, key in ipairs(expectedKeys) do check(keySet[key] == true, "NamingChrome includes key " .. key) end -- Check Naming screen layout coordinates match pokefirered Naming.open({ template = "NICKNAME", species = 1, maxLen = 10, title = Naming.monTitle("BULBASAUR") }) check(Naming.isOpen(), "Naming screen is open") -- Verify species icon positioning at (56, 40) local st = Naming._state check(st ~= nil, "Naming internal state is present") check(st.template == "NICKNAME", "state template is NICKNAME") check(st.species == 1, "state species is 1") -- Verify right button selection index mapping (1 = Page Swap, 2 = Back, 3 = OK) st.btn = 1 check(st.btn == 1, "btn 1 is Page Swap") st.btn = 2 check(st.btn == 2, "btn 2 is Back") st.btn = 3 check(st.btn == 3, "btn 3 is OK") -- Verify button cursor geometry matches pokefirered 32x13 pill coordinates check(Naming.L.btnCursorX == 188, "btnCursorX is 188") check(Naming.L.btnCursorY[1] == 77, "Page Swap cursor Y is 77") check(Naming.L.btnCursorY[2] == 106, "Back cursor Y is 106") check(Naming.L.btnCursorY[3] == 128, "OK cursor Y is 128") -- Verify Gfx.drawUi suppresses Fade.draw during Naming local Gfx = require("src.core.game3.gfx") check(Naming.isOpen(), "Naming is open during UI render check") Naming.close() check(not Naming.isOpen(), "Naming is closed after close()") print("[test] 6. Caught Pokémon nickname prompt and naming modal flow") local Battle = require("src.core.game3.battle") local BattleUi = require("src.core.game3.battle.ui") local Choice = require("src.ui.game3.choice") local Message = require("src.ui.game3.message") local Catching = require("src.core.game3.battle.catching") -- Setup mock input helper local mockInput = { _pressed = {}, press = function(self, key) self._pressed[key] = true end, clear = function(self) self._pressed = {} end, isDown = function(self, key) return self._pressed[key] == true end, wasPressed = function(self, key) if self._pressed[key] then self._pressed[key] = false return true end return false end, } function mockInput:consume(key) return self:wasPressed(key) end -- Scenario A: Caught Pokémon -> Yes to Nickname -> Type nickname -> Confirmed local testParty = { { species = 1, hp = 20, maxHp = 20, level = 5 } } local testSession = { name = "RED", party = testParty, dex = {} } local caughtMon = { species = 25, name = "PIKACHU", level = 3, hp = 10, maxHp = 10, nickname = "" } local catchResParty = { success = true, location = "party", firstTimeCaught = false, mon = caughtMon, } Battle.start({ wild = true, playerParty = testParty, foe = { species = 25, level = 3, hp = 10, maxHp = 10, mon = caughtMon }, headless = false, session = testSession, }) check(Battle.isActive(), "Battle active for catch nickname test") -- Trigger post catch flow Battle.startPostCatchFlow(catchResParty) check(Battle._phase == "catch_nickname_prompt", "Phase is catch_nickname_prompt") -- Message says "Give a nickname to the\ncaptured PIKACHU?" local foundPrompt = false for _, line in ipairs(BattleUi._log or {}) do if line:find("Give a nickname") and line:find("PIKACHU") then foundPrompt = true end end check(foundPrompt, "Prompt log has 'Give a nickname to the captured PIKACHU?'") -- Advance typewriter message to trigger yes/no Choice box while Message.isOpen() and not (Choice.active) do Message.tick() BattleUi.pump() end check(Choice.active == true, "Choice Yes/No is active") -- Select YES (index 0 / A button) mockInput:clear() mockInput:press("a") Battle.update(1 / 60, { input = mockInput }) check(Naming.isOpen(), "Naming screen opened after choosing YES") check(Battle._phase == "catch_naming", "Battle phase is catch_naming") check(Naming._state.template == "CAUGHT_MON", "Naming template is CAUGHT_MON") check(Naming._state.species == 25, "Naming species is 25 (Pikachu)") -- Complete naming with nickname "SPARKY" Naming.close("SPARKY") check(not Naming.isOpen(), "Naming screen closed") check(caughtMon.nickname == "SPARKY", "caughtMon nickname is now SPARKY") check(Battle._phase == "ending", "Battle transitioned to ending phase after naming") -- Scenario B: Caught Pokémon -> No to Nickname local caughtPidgey = { species = 16, name = "PIDGEY", level = 3, hp = 8, maxHp = 8, nickname = "" } local catchResNoNick = { success = true, location = "party", firstTimeCaught = false, mon = caughtPidgey, } Battle.start({ wild = true, playerParty = testParty, foe = { species = 16, level = 3, hp = 8, maxHp = 8, mon = caughtPidgey }, headless = false, session = testSession, }) Battle.startPostCatchFlow(catchResNoNick) while Message.isOpen() and not Choice.active do Message.tick() BattleUi.pump() end check(Choice.active == true, "Choice Yes/No is active for Pidgey") -- Select NO (index 1 / down + A button or B button) mockInput:clear() mockInput:press("b") Battle.update(1 / 60, { input = mockInput }) check(not Naming.isOpen(), "Naming screen NOT opened when NO selected") check(caughtPidgey.nickname == "", "caughtPidgey nickname remains default/empty") check(Battle._phase == "ending", "Battle transitioned to ending phase immediately") -- Scenario C: Caught Pokémon sent to PC -> Yes to Nickname -> no PC transfer message local caughtCaterpie = { species = 10, name = "CATERPIE", level = 3, hp = 6, maxHp = 6, nickname = "" } local catchResPc = { success = true, location = "pc", firstTimeCaught = false, mon = caughtCaterpie, } Battle.start({ wild = true, playerParty = testParty, foe = { species = 10, level = 3, hp = 6, maxHp = 6, mon = caughtCaterpie }, headless = false, session = testSession, }) Battle.startPostCatchFlow(catchResPc) while Message.isOpen() and not Choice.active do Message.tick() BattleUi.pump() end mockInput:clear() mockInput:press("a") Battle.update(1 / 60, { input = mockInput }) check(Naming.isOpen(), "Naming screen opened for PC-bound Caterpie") Naming.close("SLUGGY") check(caughtCaterpie.nickname == "SLUGGY", "Caterpie nicknamed SLUGGY") -- pokefirered/src/battle_script_commands.c:9853 check(Battle._phase == "ending", "a nicknamed catch skips the PC transfer message") local foundTransfer = false for _, line in ipairs(BattleUi._log or {}) do if line:find("transferred") then foundTransfer = true end end check(not foundTransfer, "trygivecaughtmonnick jumps past printfromtable gCaughtMonStringIds") print("[test] 9. species name install retries until the cache is mounted") do local Pokemon = require("src.core.game3.pokemon") local saved = { install = Pokemon.install, name = Pokemon.name, names = Pokemon._names, warned = Pokemon._installWarned } local rt = package.loaded["src.core.game3.runtime"] local bare = { species = 25, nickname = "" } package.loaded["src.core.game3.runtime"] = { getSession = function() return { party = { bare } } end, isActive = function() return true end, } local attempts, mounted = 0, false Pokemon._names, Pokemon._installWarned = nil, nil Pokemon.install = function() attempts = attempts + 1 if not mounted then error("cache not mounted") end Pokemon._names = { [25] = "PIKACHU" } end Pokemon.name = function(sp) return Pokemon._names and Pokemon._names[sp] or "" end local realPrint, printed = print, 0 print = function(msg) if tostring(msg):find("install failed", 1, true) then printed = printed + 1 else realPrint(msg) end end local out = {} local function buffer() local ctx = { specialVars = { [0x8004] = 0 }, stringVars = {} } Natives.ALLOW["special:124"](ctx, { setStringVar = function(i, t) out[i] = t end }) return out[1] end local first = buffer() local second = buffer() mounted = true local third = buffer() print = realPrint check(first == "" and second == "", "no name while the cache is unmounted") check(third == "PIKACHU", "the name loads once the cache mounts") check(attempts == 3, "every lookup retries the install until it succeeds") check(printed == 1, "the install failure is logged once") Pokemon.install, Pokemon.name = saved.install, saved.name Pokemon._names, Pokemon._installWarned = saved.names, saved.warned package.loaded["src.core.game3.runtime"] = rt end if failed == 0 then print("\nAll game3 nickname tests passed.") os.exit(0) else print("\n" .. failed .. " nickname test(s) failed.") os.exit(1) end