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

332 lines
16 KiB
Lua

#!/usr/bin/env luajit
-- pokefirered/src/player_pc.c:151 BedroomPC, pokefirered/src/player_pc.c:100 gNewGamePCItems
package.path = "./?.lua;./?/init.lua;" .. package.path
require("tests.game3_cache").stubSpeciesNames()
require("tests.fixture_data.game3_items").install()
package.loaded["src.core.game3.rom_text"] = {
plain = function(key) return key end, box = function(key) return key end,
ascii = function(key) return key end, has = function() return true end,
key = function(n, i, j) return j and (n .. "[" .. i .. "][" .. j .. "]") or (n .. "[" .. i .. "]") end,
at = function(n, i, j) return j and (n .. "[" .. i .. "][" .. j .. "]") or (n .. "[" .. i .. "]") end,
count = function() return 0 end, list = function() return {} end,
lazy = function(map) return setmetatable({}, { __index = function(_, k) return map[k] end }) end,
}
local failed = 0
local function check(cond, msg)
if cond then
print("[ok] " .. msg)
else
failed = failed + 1
print("[FAIL] " .. msg)
end
end
package.loaded["src.ui.game3.stack"] = {
push = function() end,
pop = function() end,
busy = function() return false end,
drawOrder = function() return {} end,
}
package.loaded["src.core.game3.audio"] = {
playSe = function() end,
playCry = function() end,
}
print("[test] 1. BedroomPC special id + handler")
local Std = require("src.core.game3.scripting.stdscripts")
check(Std.SPECIAL.BedroomPC == 249, "BedroomPC = 249 (got " .. tostring(Std.SPECIAL.BedroomPC) .. ")")
local Natives = require("src.core.game3.scripting.natives")
check(Natives.ALLOW["special:249"] ~= nil, "special:249 handler registered")
print("[test] 2. special 249 yields into openPc with bedroom opts")
local gotOpts, gotDone
local ctx = { mode = "bytecode", status = "running", specialVars = {} }
local PcAnim = require("src.core.game3.pc_anim")
local mtSets = {}
PcAnim.setter(function(x, y, mid, imp)
mtSets[#mtSets + 1] = { x = x, y = y, mid = mid, imp = imp }
end)
local realPlayer = package.loaded["src.core.game3.player"]
package.loaded["src.core.game3.player"] = { cellX = 1, cellY = 2, facing = "up" }
local adapters = {
openPc = function(done, opts)
gotDone = done
gotOpts = opts
end,
log = function() end,
}
local yielded = Natives.special(ctx, 249, adapters)
check(yielded == true, "special 249 yields")
check(type(gotOpts) == "table" and gotOpts.bedroom == true, "openPc got bedroom=true")
check(ctx.nativePoll and ctx.nativePoll() == false, "native wait pending while PC open")
if gotDone then gotDone() end
check(ctx.nativePoll and ctx.nativePoll() == true, "native wait finishes on close")
local last = mtSets[#mtSets]
check(ctx.specialVars[0x8004] == 1, "ShutDownPC sets VAR_0x8004 = 1")
check(last and last.x == 1 and last.y == 1 and last.mid == 0x28F and last.imp == true,
"bedroom close swaps PC at (1,1) to PlayersPCOff 0x28F")
print("[test] 3. host openPc routes bedroom into player_pc with closeOnExit")
do
local shown
local realPc = package.loaded["src.ui.game3.pc_menu"]
local realRt = package.loaded["src.core.game3.runtime"]
package.loaded["src.ui.game3.pc_menu"] = { show = function(o) shown = o end }
package.loaded["src.core.game3.runtime"] = {
isActive = function() return true end,
getSession = function() return {} end,
}
local okA, Adapters = pcall(require, "src.core.game3.scripting.adapters")
local okH, host = false, nil
if okA then
okH, host = pcall(Adapters.host, nil, {}, nil)
end
check(okH and host and host.openPc, "Adapters.host builds openPc")
if okH and host and host.openPc then
host.openPc(function() end, { bedroom = true })
check(shown and shown.startMode == "player_pc" and shown.closeOnExit == true,
"bedroom -> startMode player_pc, closeOnExit")
shown = nil
host.openPc(function() end)
check(shown and shown.startMode == nil and not shown.closeOnExit, "no opts -> root hub")
end
package.loaded["src.ui.game3.pc_menu"] = realPc
package.loaded["src.core.game3.runtime"] = realRt
end
print("[test] 4. PcMenu bedroom mode: starts in player_pc, B and TURN OFF close")
local PcMenu = require("src.ui.game3.pc_menu")
local function input(btn)
return { wasPressed = function(_, b) return b == btn end }
end
local closed = 0
PcMenu.show({ session = {}, onClose = function() closed = closed + 1 end,
startMode = "player_pc", closeOnExit = true })
check(PcMenu.mode == "player_pc", "starts in player_pc (got " .. tostring(PcMenu.mode) .. ")")
check(PcMenu._status == "gText_WhatWouldYouLikeToDo", "status What would you like to do?")
PcMenu.handleInput(input("b"))
check(not PcMenu.isOpen() and closed == 1, "B closes bedroom PC and fires onClose")
PcMenu.show({ session = {}, onClose = function() closed = closed + 1 end,
startMode = "player_pc", closeOnExit = true })
PcMenu.handleInput(input("up"))
check(PcMenu.cursor == 1, "top menu does not wrap up")
PcMenu.handleInput(input("down"))
PcMenu.handleInput(input("down"))
PcMenu.handleInput(input("down"))
check(PcMenu.cursor == 3, "cursor on TURN OFF, no wrap down")
PcMenu.handleInput(input("a"))
check(not PcMenu.isOpen() and closed == 2, "TURN OFF closes bedroom PC")
PcMenu.show({ session = {}, onClose = function() closed = closed + 1 end })
check(PcMenu.mode == "root", "default show opens root hub")
PcMenu.cursor = 2
PcMenu.handleInput(input("a"))
check(PcMenu.mode == "player_pc", "hub -> player_pc")
PcMenu.handleInput(input("b"))
check(PcMenu.isOpen() and PcMenu.mode == "root" and closed == 2, "hub player_pc B returns to root")
PcMenu.close()
print("[test] 5. New game seeds PC POTION; withdraw moves it to bag; save round-trip")
local Schema = require("src.core.game3.save_schema_firered")
local Bag = require("src.core.game3.bag")
local Storage = require("src.core.game3.storage")
local session = Schema.newGame({ rngSeed = 1 })
local it = session.storage and session.storage.items and session.storage.items[1]
check(it and it.id == 13 and it.qty == 1, "newGame storage.items[1] = POTION x1")
check(session.storage and #session.storage.items == 1, "exactly one PC item")
local saved = Schema.toSaveTable(session)
check(type(saved.storage) == "table" and saved.storage.items[1] and saved.storage.items[1].id == 13,
"toSaveTable writes storage")
local restored = Schema.fromSaveTable(saved)
check(restored.storage and restored.storage.items[1] and restored.storage.items[1].id == 13,
"fromSaveTable restores storage")
local legacy = Schema.fromSaveTable({ map = "FR_PALLET_TOWN", x = 1, y = 1 })
check(legacy.storage and legacy.storage.items[1] and legacy.storage.items[1].id == 13, "legacy save without storage gets seeded POTION")
check(Storage.withdrawItem(session, 1, 1) == true, "withdrawItem POTION ok")
check(Bag.has(session.bag, 13, 1), "bag has POTION")
check(#session.storage.items == 0, "PC storage empty after withdraw")
print("[test] 6. cart labels, descriptions, MAILBOX, no TOSS")
local function joined(list)
local t = {}
for i, a in ipairs(list) do t[i] = a.label end
return table.concat(t, "/")
end
check(joined(PcMenu.TOP_ACTIONS) == "sMenuActions_TopMenu[0]/sMenuActions_TopMenu[1]/sMenuActions_TopMenu[2]",
"top menu labels (got " .. joined(PcMenu.TOP_ACTIONS) .. ")")
check(joined(PcMenu.ITEM_STORAGE_ACTIONS) == "sMenuActions_ItemPc[0]/sMenuActions_ItemPc[1]/sMenuActions_ItemPc[2]",
"item storage labels (got " .. joined(PcMenu.ITEM_STORAGE_ACTIONS) .. ")")
local seLog = {}
package.loaded["src.core.game3.audio"].playSe = function(id) seLog[#seLog + 1] = id end
local s6 = { bag = Bag.new(), storage = Storage.new() }
s6.storage.items = {} -- clear to test empty WITHDRAW message
local closed6 = 0
PcMenu.show({ session = s6, startMode = "player_pc", closeOnExit = true,
onClose = function() closed6 = closed6 + 1 end })
check(#seLog == 0, "bedroom PC open plays no SE_PC_LOGIN")
PcMenu.handleInput(input("a"))
check(PcMenu.mode == "item_storage" and PcMenu.cursor == 1
and PcMenu._status == "sItemStorageActionDescriptionPtrs[0]", "ITEM STORAGE -> submenu, WITHDRAW desc")
PcMenu.handleInput(input("up"))
check(PcMenu.cursor == 1, "submenu does not wrap up")
PcMenu.handleInput(input("down"))
check(PcMenu._status == "sItemStorageActionDescriptionPtrs[1]", "DEPOSIT desc")
PcMenu.handleInput(input("down"))
check(PcMenu._status == "sItemStorageActionDescriptionPtrs[2]", "CANCEL desc")
PcMenu.handleInput(input("down"))
check(PcMenu.cursor == 3, "submenu does not wrap down")
PcMenu.handleInput(input("up"))
PcMenu.handleInput(input("up"))
PcMenu.handleInput(input("a"))
check(PcMenu.mode == "msg" and PcMenu._status == "gText_ThereAreNoItems", "empty WITHDRAW -> There are no items.")
PcMenu.handleInput(input("a"))
check(PcMenu.mode == "item_storage" and PcMenu.cursor == 1
and PcMenu._status == "sItemStorageActionDescriptionPtrs[0]", "no-items message returns to submenu")
PcMenu.handleInput(input("b"))
check(PcMenu.mode == "player_pc" and PcMenu.cursor == 1
and PcMenu._status == "gText_WhatWouldYouLikeToDo", "B in submenu returns to top menu")
PcMenu.handleInput(input("down"))
PcMenu.handleInput(input("a"))
check(PcMenu.mode == "msg" and PcMenu._status == "gText_TheresNoMailHere", "MAILBOX -> There's no MAIL here.")
PcMenu.handleInput(input("a"))
check(PcMenu.mode == "player_pc" and PcMenu.cursor == 1
and PcMenu._status == "gText_WhatWouldYouLikeToDo", "mail message returns to top menu")
s6.storage.items[1] = { id = 13, qty = 1 }
PcMenu.handleInput(input("a"))
PcMenu.handleInput(input("a"))
local ItemPc = require("src.ui.game3.item_pc")
-- pokefirered/src/player_pc.c:378
check(PcMenu.mode == "item_pc" and ItemPc.isOpen(), "WITHDRAW ITEM opens the item PC")
for _ = 1, 30 do ItemPc.handleInput(input()) end
check(ItemPc._fx == nil and ItemPc.mode == "list", "the PC screen turn-on effect finishes")
ItemPc.handleInput(input("a"))
check(ItemPc.mode == "submenu", "A on POTION opens WITHDRAW / GIVE / CANCEL")
ItemPc.handleInput(input("a"))
check(ItemPc.mode == "result" and ItemPc.resultText == "gText_WithdrewQuantItem", "a single POTION skips the quantity")
ItemPc.handleInput(input("a"))
check(Bag.has(s6.bag, 13, 1) and #s6.storage.items == 0, "POTION withdrawn")
ItemPc.handleInput(input("b"))
for _ = 1, 30 do if ItemPc.isOpen() then ItemPc.handleInput(input()) end end
check(not ItemPc.isOpen() and PcMenu.mode == "item_storage" and PcMenu.cursor == 1,
"B turns the item PC off and returns to ITEM STORAGE")
PcMenu.handleInput(input("b"))
PcMenu.handleInput(input("b"))
check(not PcMenu.isOpen() and closed6 == 1, "B at top menu turns the PC off")
package.loaded["src.core.game3.audio"].playSe = function() end
print("[test] 7. AnimatePcTurnOn flicker / AnimatePcTurnOff")
mtSets = {}
local actx = { specialVars = { [0x8004] = 1 } }
check(Natives.special(actx, Std.SPECIAL.AnimatePcTurnOn, { log = function() end }) == false,
"AnimatePcTurnOn does not block the script")
local frames = {}
for f = 1, 40 do
local before = #mtSets
PcAnim.update()
if #mtSets > before then frames[#frames + 1] = f end
end
local mids = {}
for i, s in ipairs(mtSets) do mids[i] = string.format("%X@%d,%d", s.mid, s.x, s.y) end
check(table.concat(mids, " ") == "28A@1,1 28F@1,1 28A@1,1 28F@1,1 28A@1,1",
"flicker on/off/on/off/on (got " .. table.concat(mids, " ") .. ")")
check(table.concat(frames, ",") == "7,13,19,25,31", "flicker every 6 frames (got " .. table.concat(frames, ",") .. ")")
check(PcAnim.task == nil, "turn-on task ends after 5 swaps")
mtSets = {}
Natives.special({ specialVars = { [0x8004] = 0 } }, Std.SPECIAL.AnimatePcTurnOff, { log = function() end })
check(mtSets[1] and mtSets[1].mid == 0x062, "center PC turn off uses Building_PCOff 0x062")
package.loaded["src.core.game3.player"] = { cellX = 5, cellY = 5, facing = "left" }
mtSets = {}
PcAnim.turnOff({ specialVars = { [0x8004] = 0 } })
check(mtSets[1] and mtSets[1].x == 4 and mtSets[1].y == 4, "facing west targets (x-1, y-1)")
package.loaded["src.core.game3.player"] = realPlayer
PcAnim.setter(nil)
do
local realMap = package.loaded["src.core.game3.map"]
local realTs = package.loaded["src.core.game3.tileset_native"]
package.loaded["src.core.game3.map"] = { currentDef = function() return { pair = "players_house" } end }
package.loaded["src.core.game3.tileset_native"] = {
ready = function() return true end,
get = function() return { midToSlot = { [0x28F] = 7 } } end,
}
check(PcAnim.drawable(0x28F) == true, "drawable: mid in the pair atlas")
check(PcAnim.drawable(0x28A) == false, "drawable: mid missing from atlas would draw the black void slot")
package.loaded["src.core.game3.map"] = realMap
package.loaded["src.core.game3.tileset_native"] = realTs
end
do
local NativePack = require("src.import.gba.native_pack")
local grids = {
FR_PLAYERS_HOUSE_2F = { pair = "player_house", cells = { { mid = 0x280 }, { mid = 0x28F } } },
SEVII_ONE_ISLAND_POKECENTER = { pair = "network", cells = { { mid = 0x062 } } },
FR_PALLET_TOWN = { pair = "pallet_outdoor", cells = { { mid = 0x001 } } },
}
local function has(list, mid)
for _, m in ipairs(list) do if m == mid then return true end end
return false
end
local ph = NativePack.collectMidsForPair(grids, {}, "player_house")
check(has(ph, 0x28F) and has(ph, 0x28A), "atlas mids: bedroom PC off 0x28F brings PlayersPCOn 0x28A")
local net = NativePack.collectMidsForPair(grids, {}, "network")
check(has(net, 0x062) and has(net, 0x063), "atlas mids: center PC off 0x062 brings Building_PCOn 0x063")
local pal = NativePack.collectMidsForPair(grids, {}, "pallet_outdoor")
check(not has(pal, 0x063) and not has(pal, 0x28A), "atlas mids: no PC on mids without a PC off mid")
end
print("[test] 8. one PC table: session.storage, legacy pc migration, sidecar")
local ng = Schema.newGame({ rngSeed = 2 })
check(ng.pc == nil, "newGame has no session.pc")
check(Schema.toSaveTable(ng).pc == nil, "save table writes no pc")
local mig = Schema.fromSaveTable({ map = "FR_PALLET_TOWN", x = 1, y = 1,
pc = { items = { { id = 13, qty = 2 } }, mons = { { species = 16, level = 4 } } } })
check(mig.pc == nil and mig.storage and mig.storage.items[1] and mig.storage.items[1].id == 13
and mig.storage.items[1].qty == 2, "legacy pc.items migrated into storage")
check(mig.storage and Storage.countTotalMons(mig.storage) == 1 and mig.storage.boxes[1].mons[1].species == 16,
"legacy pc.mons migrated into box 1")
local both = Schema.fromSaveTable({
storage = { items = { { id = 17, qty = 1 } }, boxes = { [1] = { mons = { [1] = { species = 25 } } } } },
pc = { items = { { id = 13, qty = 2 } }, mons = { { species = 16 } } },
})
check(#both.storage.items == 1 and both.storage.items[1].id == 17, "storage items win over stale pc items")
check(Storage.countTotalMons(both.storage) == 1 and both.storage.boxes[1].mons[1].species == 25,
"no duplicate box mons from stale pc.mons")
local okB, Bridge = pcall(require, "src.core.game3.bridge")
check(okB, "bridge loads")
if okB then
local sc = { pc = { items = { { id = 13, qty = 9 } } } }
Bridge.storageToSidecar(sc, { storage = mig.storage })
check(sc.pc == nil and sc.storage and sc.storage.items[1].qty == 2 and sc.storage.boxes[1].mons[1].species == 16,
"sidecar keeps storage and drops pc")
local back = Storage.restore(sc.storage, sc.pc)
check(back and back.items[1].id == 13 and Storage.countTotalMons(back) == 1, "sidecar storage restores")
local legacySc = Storage.restore(nil, { items = { { id = 13, qty = 1 } } })
check(legacySc and legacySc.items[1].id == 13, "sidecar with only pc migrates")
end
local okC, Catching = pcall(require, "src.core.game3.battle.catching")
local okD, Dex = pcall(require, "src.core.game3.dex")
if okC and okD then
local cs = { name = "RED", id = 1, dex = Dex.new(), party = {} }
for i = 1, 6 do cs.party[i] = { species = 1, level = 5, hp = 20, maxHp = 20 } end
local okS, res = pcall(Catching.storeCaught, cs, { species = 16, mon = { species = 16, level = 4, hp = 5, maxHp = 18 } }, 4)
check(okS and res.location == "pc" and cs.pc == nil and Storage.countTotalMons(cs.storage) == 1,
"catch with full party writes only session.storage")
else
check(false, "catching/dex load")
end
if failed == 0 then
print("\nAll game3 bedroom PC tests passed.")
os.exit(0)
else
print("\n" .. failed .. " bedroom PC test(s) failed.")
os.exit(1)
end