mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-10-08 09:42:55 -04:00
592 lines
24 KiB
Lua
592 lines
24 KiB
Lua
#!/usr/bin/env luajit
|
|
-- Gen 3 MODS entry + FRLG mod-manager shell (docs/compose/spec/game3-mods-menu.md).
|
|
-- ROM-free: pure option rows, start-menu gating, ManagerState row models, and
|
|
-- the game3.stack push/close path. No GBA cache required.
|
|
|
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
|
local function romTextKey(n, i, j) return j and (n .. "[" .. i .. "][" .. j .. "]") or (n .. "[" .. i .. "]") end
|
|
local function romTextPlain(key) return key 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) return romTextKey(n, i, j) end,
|
|
count = function() return 0 end, list = function() return {} end,
|
|
lazy = function(map) return setmetatable({}, { __index = function(_, k) return map[k] end }) end,
|
|
}
|
|
|
|
local love = _G.love or require("tests.love_stub")
|
|
_G.love = love
|
|
|
|
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. OPTION row is always present with installed count")
|
|
local Rows = require("src.ui.game3.option_rows")
|
|
local function optionRow(ctx)
|
|
for _, row in ipairs(Rows.build(ctx)) do
|
|
if row.id == "mods" then return row end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
local vanillaCtx = {
|
|
options = { musicVol = 7, sfxVol = 7, musicFilter = 0, buttonMode = 0 },
|
|
game = { modStatus = { available = {} } },
|
|
}
|
|
local modsRow = optionRow(vanillaCtx)
|
|
check(modsRow ~= nil, "mods row present on vanilla install")
|
|
check(modsRow and modsRow.activate ~= nil and modsRow.step == nil,
|
|
"mods row is activate-only")
|
|
check(modsRow and tostring(modsRow.value(vanillaCtx)):find("0"),
|
|
"vanilla value reports 0 INSTALLED (got " ..
|
|
tostring(modsRow and modsRow.value(vanillaCtx)) .. ")")
|
|
|
|
local withModsCtx = {
|
|
options = vanillaCtx.options,
|
|
game = {
|
|
modStatus = {
|
|
available = {
|
|
{ id = "a", name = "A", category = "TWEAK", enabled = true, state = "loaded" },
|
|
{ id = "b", name = "B", category = "TWEAK", enabled = true, state = "loaded" },
|
|
},
|
|
},
|
|
},
|
|
}
|
|
local modsRow2 = optionRow(withModsCtx)
|
|
check(modsRow2 and tostring(modsRow2.value(withModsCtx)):find("2"),
|
|
"value reports 2 INSTALLED")
|
|
|
|
local orderHasMods = false
|
|
for _, id in ipairs(Rows.ORDER) do
|
|
if id == "mods" then orderHasMods = true end
|
|
end
|
|
check(orderHasMods, "mods listed in Rows.ORDER")
|
|
|
|
print("[test] 2. START menu MODS gate (normal field only)")
|
|
local StartMenu = require("src.ui.game3.start_menu")
|
|
local function entryIds()
|
|
local out = {}
|
|
for _, e in ipairs(StartMenu.ENTRIES or {}) do out[#out + 1] = e.id end
|
|
return out
|
|
end
|
|
local function hasId(list, id)
|
|
for _, v in ipairs(list) do
|
|
if v == id then return true end
|
|
end
|
|
return false
|
|
end
|
|
|
|
StartMenu._game = { modStatus = { available = {} } }
|
|
StartMenu.show({ session = { bag = {}, party = {} }, game = StartMenu._game })
|
|
local ids = entryIds()
|
|
check(not hasId(ids, "mods"), "vanilla START has no MODS row")
|
|
check(hasId(ids, "option") and hasId(ids, "exit"), "vanilla START keeps OPTION/EXIT")
|
|
StartMenu.close(true)
|
|
|
|
StartMenu._game = {
|
|
modStatus = {
|
|
available = {
|
|
{ id = "a", name = "A", category = "TWEAK", enabled = true, state = "loaded" },
|
|
},
|
|
},
|
|
}
|
|
StartMenu.show({ session = { bag = {}, party = {} }, game = StartMenu._game })
|
|
ids = entryIds()
|
|
check(hasId(ids, "mods"), "START shows MODS when mods are discovered")
|
|
local oi, mi, ei
|
|
for i, id in ipairs(ids) do
|
|
if id == "option" then oi = i end
|
|
if id == "mods" then mi = i end
|
|
if id == "exit" then ei = i end
|
|
end
|
|
check(oi and mi and ei and oi < mi and mi < ei, "MODS sits after OPTION, before EXIT")
|
|
StartMenu.close(true)
|
|
|
|
print("[test] 3. Manager list rows come from ManagerState.modRows")
|
|
local ManagerState = require("src.mods.ManagerState")
|
|
local statusStub = withModsCtx.game.modStatus
|
|
local modsStub = {
|
|
status = function() return statusStub end,
|
|
}
|
|
local mgrGame = {
|
|
modStatus = statusStub,
|
|
mods = modsStub,
|
|
save = { options = {} },
|
|
stack = { pop = function() end },
|
|
input = { wasPressed = function() return false end },
|
|
}
|
|
local mgr = ManagerState.new(mgrGame)
|
|
mgr:refresh()
|
|
local listRows = mgr:modRows()
|
|
local sawHeader, sawA = false, false
|
|
for _, row in ipairs(listRows) do
|
|
if row.header then sawHeader = true end
|
|
if row.mod and row.mod.id == "a" then sawA = true end
|
|
end
|
|
check(sawHeader and sawA, "modRows groups by category and lists mods")
|
|
check(mgr:rowsForScreen() ~= nil, "rowsForScreen exposes the model to Gen 3 chrome")
|
|
|
|
print("[test] 4. Toggle path still uses ManagerState.resolveToggle")
|
|
local r = ManagerState.resolveToggle({
|
|
a = { id = "a", dependencySpecs = {}, conflictSpecs = {} },
|
|
}, "a", true, {})
|
|
check(r.apply.a == true, "resolveToggle enable lands on the target")
|
|
check(#r.alsoEnable == 0, "no phantom cascade for a leaf mod")
|
|
|
|
print("[test] 5. ModManager pushes/pops game3.stack")
|
|
local Stack = require("src.ui.game3.stack")
|
|
local ModManager = require("src.ui.game3.mod_manager")
|
|
local function stubGame(available)
|
|
local status = { available = available or statusStub.available }
|
|
return {
|
|
modStatus = status,
|
|
mods = { status = function() return status end },
|
|
save = { options = {} },
|
|
options = {},
|
|
input = { wasPressed = function() return false end },
|
|
writeOptions = function() end,
|
|
returnToTitle = function() end,
|
|
}
|
|
end
|
|
Stack.clear()
|
|
check(not ModManager.isOpen(), "closed before show")
|
|
local openerDepthAfterShow
|
|
do
|
|
-- opener stays under the manager so B-close can return to it
|
|
Stack.push("start", StartMenu, { hideBelow = false })
|
|
ModManager.show({ game = stubGame() })
|
|
openerDepthAfterShow = Stack.depth()
|
|
end
|
|
check(ModManager.isOpen(), "open after show")
|
|
check(Stack.has("mod_manager"), "mod_manager layer on the game3 stack")
|
|
check(openerDepthAfterShow == 2, "opener still on the stack under the manager")
|
|
ModManager.draw()
|
|
check(true, "draw with FRLG chrome did not raise")
|
|
ModManager.handleInput({ wasPressed = function() return false end })
|
|
check(ModManager.isOpen(), "inert input keeps the manager open")
|
|
-- B at the manager root is ManagerState:goBack → proxy stack:pop → close
|
|
ModManager.handleInput({
|
|
wasPressed = function(_, btn) return btn == "b" end,
|
|
})
|
|
check(not ModManager.isOpen(), "B at root closes the manager")
|
|
check(not Stack.has("mod_manager"), "close pops the manager layer")
|
|
check(Stack.has("start") and Stack.depth() == 1,
|
|
"stack depth returns to the opener")
|
|
Stack.clear()
|
|
|
|
print("[test] 6. START confirm and OPTION activate open the manager")
|
|
StartMenu._game = stubGame()
|
|
StartMenu.show({ session = { bag = {}, party = {} }, game = StartMenu._game })
|
|
local modsIndex
|
|
for i, e in ipairs(StartMenu.ENTRIES) do
|
|
if e.id == "mods" then modsIndex = i end
|
|
end
|
|
check(modsIndex ~= nil, "MODS entry present before confirm")
|
|
StartMenu.cursor = modsIndex
|
|
StartMenu.confirm()
|
|
check(ModManager.isOpen() and Stack.has("mod_manager"),
|
|
"START confirm on MODS opens the manager")
|
|
ModManager.close()
|
|
StartMenu.close(true)
|
|
|
|
local activateRow = optionRow(withModsCtx)
|
|
local activated = false
|
|
local actGame = stubGame()
|
|
activateRow.activate({ game = actGame, session = {}, options = vanillaCtx.options })
|
|
check(ModManager.isOpen(), "OPTION activate opens the manager")
|
|
ModManager.close()
|
|
check(not ModManager.isOpen(), "manager closed after OPTION path")
|
|
|
|
print("[test] 7. Link / Union / Safari start lists stay retail")
|
|
package.loaded["src.core.game3.link"] = { link = {}, inLinkRoom = function() return true end }
|
|
StartMenu.show({ session = { bag = {}, party = {} }, game = stubGame() })
|
|
check(not hasId(entryIds(), "mods"), "link start menu has no MODS")
|
|
StartMenu.close(true)
|
|
package.loaded["src.core.game3.link"] = nil
|
|
|
|
package.loaded["src.core.game3.map"] = { current = "FR_UNION_ROOM" }
|
|
StartMenu.show({ session = { bag = {}, party = {}, map = "FR_UNION_ROOM" }, game = stubGame() })
|
|
check(not hasId(entryIds(), "mods"), "union room start menu has no MODS")
|
|
StartMenu.close(true)
|
|
package.loaded["src.core.game3.map"] = nil
|
|
|
|
package.loaded["src.core.game3.safari"] = { isActive = function() return true end }
|
|
StartMenu.show({ session = { bag = {}, party = {} }, game = stubGame() })
|
|
check(not hasId(entryIds(), "mods"), "safari start menu has no MODS")
|
|
StartMenu.close(true)
|
|
package.loaded["src.core.game3.safari"] = nil
|
|
|
|
print("[test] 8. Nested NamingScreen / QuantityBox are bridged, not dropped")
|
|
local NamingScreen = require("src.ui.NamingScreen")
|
|
local QuantityBox = require("src.ui.QuantityBox")
|
|
local named, qtyDone = nil, nil
|
|
local g = stubGame()
|
|
ModManager.show({ game = g })
|
|
local mproxy = ModManager._mgr.game
|
|
mproxy.stack:push(NamingScreen.new(mproxy, {
|
|
title = "NAME?", maxLen = 10,
|
|
onDone = function(name) named = name end,
|
|
}))
|
|
check(named == nil and require("src.ui.game3.naming").isOpen(),
|
|
"NamingScreen routes to Gen 3 naming modal")
|
|
require("src.ui.game3.naming").close("ALPHA")
|
|
check(named == "ALPHA", "naming onDone delivers the typed name")
|
|
|
|
mproxy.stack:push(QuantityBox.new(mproxy, {
|
|
max = 9, start = 3,
|
|
onDone = function(q) qtyDone = q end,
|
|
}))
|
|
check(ModManager._prompt and ModManager._prompt.kind == "qty",
|
|
"QuantityBox becomes an FRLG qty prompt")
|
|
ModManager.handleInput({ wasPressed = function(_, b) return b == "a" end })
|
|
check(qtyDone == 3, "qty prompt confirms the current value")
|
|
ModManager.close()
|
|
|
|
print("[test] 9. APPLY & RESTART restarts the process like Gen 1/2 (#2691)")
|
|
local restarts = 0
|
|
local realHostShell = package.loaded["src.core.HostShell"]
|
|
package.loaded["src.core.HostShell"] = { restart = function() restarts = restarts + 1 end }
|
|
local returned = false
|
|
local g2 = stubGame()
|
|
g2.returnToTitle = function() returned = true end
|
|
ModManager.show({ game = g2 })
|
|
ModManager._mgr:restartGame()
|
|
check(restarts == 1, "restartGame reaches HostShell.restart")
|
|
check(not returned, "restartGame does not soft-return to the Gen 3 title")
|
|
check(not ModManager.isOpen(), "restart closes the manager (open flag not stuck)")
|
|
|
|
local Game3 = require("src.core.Game3")
|
|
check(type(Game3.restartWithMods) == "function", "Game3 defines restartWithMods")
|
|
local g3r = stubGame()
|
|
g3r.restartWithMods = Game3.restartWithMods
|
|
local titled = false
|
|
g3r.returnToTitle = function() titled = true end
|
|
restarts = 0
|
|
ModManager.show({ game = g3r })
|
|
ModManager._mgr:restartGame()
|
|
check(restarts == 1 and not titled, "Game3:restartWithMods goes through HostShell.restart")
|
|
check(not ModManager.isOpen(), "Game3 restart path closes the manager")
|
|
package.loaded["src.core.HostShell"] = realHostShell
|
|
|
|
ModManager.show({ game = stubGame() })
|
|
check(ModManager.isOpen(), "manager can re-open after APPLY & RESTART")
|
|
ModManager.close()
|
|
|
|
print("[test] 9b. Title-screen manager (no save yet) writes the live Game3.options (#2691)")
|
|
do
|
|
local SaveData = require("src.core.SaveData")
|
|
local status = { available = {
|
|
{ id = "a", name = "A", category = "TWEAK", enabled = true, state = "loaded" },
|
|
{ id = "b", name = "B", category = "TWEAK", enabled = true, state = "loaded" },
|
|
} }
|
|
local written
|
|
local gt = {
|
|
modStatus = status,
|
|
mods = {
|
|
status = function() return status end,
|
|
setEnabled = function(_, id, en)
|
|
for _, m in ipairs(status.available) do
|
|
if m.id == id then m.enabled = en end
|
|
end
|
|
return true
|
|
end,
|
|
},
|
|
save = nil,
|
|
options = {},
|
|
input = { wasPressed = function() return false end },
|
|
writeOptions = function(self) written = self.options end,
|
|
}
|
|
ModManager.show({ game = gt })
|
|
local m = ModManager._mgr
|
|
m:commitToggle({ a = false })
|
|
check(SaveData.modEnabled(gt.options, "a", m:enableScope()) == false,
|
|
"toggle from the title mirrors into Game3.options")
|
|
m:moveOrder(m.byId.b, -1)
|
|
local order = SaveData.modOrder(gt.options) or {}
|
|
check(order[1] == "b", "load-order move from the title lands in Game3.options")
|
|
check(written == gt.options, "persistOptions writes the live Game3.options table")
|
|
ModManager.close()
|
|
end
|
|
|
|
print("[test] 10. Options scroll uses ManagerState's 0-based window")
|
|
do
|
|
local g3 = stubGame({
|
|
{ id = "o1", name = "O1", category = "TWEAK", enabled = true, state = "loaded" },
|
|
{ id = "o2", name = "O2", category = "TWEAK", enabled = true, state = "loaded" },
|
|
{ id = "o3", name = "O3", category = "TWEAK", enabled = true, state = "loaded" },
|
|
{ id = "o4", name = "O4", category = "TWEAK", enabled = true, state = "loaded" },
|
|
{ id = "o5", name = "O5", category = "TWEAK", enabled = true, state = "loaded" },
|
|
{ id = "o6", name = "O6", category = "TWEAK", enabled = true, state = "loaded" },
|
|
})
|
|
ModManager.show({ game = g3 })
|
|
local m = ModManager._mgr
|
|
m.screen = "options"
|
|
m.optionRows = {
|
|
{ id = "a", label = "A", value = function() return "1" end },
|
|
{ id = "b", label = "B", value = function() return "2" end },
|
|
{ id = "c", label = "C", value = function() return "3" end },
|
|
{ id = "d", label = "D", value = function() return "4" end },
|
|
{ id = "e", label = "E", value = function() return "5" end },
|
|
{ id = "f", label = "F", value = function() return "6" end },
|
|
}
|
|
m.cursor = 5
|
|
m.scroll = 1
|
|
ModManager.draw()
|
|
check(true, "options draw with 0-based scroll did not raise")
|
|
check((m.scroll or 0) + 1 == 2, "options first row is scroll+1 (0-based scroll)")
|
|
ModManager.close()
|
|
end
|
|
|
|
print("[test] 11. List view is a sticky 7-row window (not ManagerState's 11)")
|
|
do
|
|
local many = {}
|
|
for i = 1, 15 do
|
|
many[i] = {
|
|
id = "m" .. i, name = "M" .. i, category = "TWEAK",
|
|
enabled = true, state = "loaded",
|
|
}
|
|
end
|
|
ModManager.show({ game = stubGame(many) })
|
|
local m = ModManager._mgr
|
|
-- ManagerState clamps to LIST_ROWS=11; we must stay on a 7-row follow
|
|
for step = 1, 8 do
|
|
m:moveCursor(1)
|
|
end
|
|
ModManager.draw()
|
|
local first = ModManager._viewFirst or 1
|
|
check(m.cursor >= first and m.cursor <= first + 6,
|
|
"cursor stays inside the 7-row view (cursor=" .. tostring(m.cursor) ..
|
|
" first=" .. tostring(first) .. ")")
|
|
check(first >= 1 and first <= m.cursor,
|
|
"view scrolls one row at a time, cursor never parks off screen")
|
|
ModManager.close()
|
|
end
|
|
|
|
print("[test] 11b. Overlays print inside their own frames (#2690)")
|
|
do
|
|
local Chrome = require("src.ui.game3.chrome")
|
|
local FrlgFont = require("src.ui.game3.frlg_font")
|
|
local log = {}
|
|
local real = {
|
|
std = Chrome.stdFrame, fixed = Chrome.fixedStdFrame, user = Chrome.userFrame,
|
|
dlg = Chrome.dialogueFrame, draw = FrlgFont.draw, glyph = FrlgFont.drawGlyph,
|
|
}
|
|
Chrome.stdFrame = function(l, t, w, h) log[#log + 1] = { frame = true, l = l, t = t, w = w, h = h } end
|
|
Chrome.fixedStdFrame = function() end
|
|
Chrome.userFrame = function() end
|
|
Chrome.dialogueFrame = function()
|
|
local l, t, w, h = Chrome.dialogueWindow()
|
|
log[#log + 1] = { frame = true, dlg = true, l = l, t = t, w = w, h = h }
|
|
end
|
|
FrlgFont.draw = function(s, x, y) log[#log + 1] = { s = tostring(s), x = x, y = y } return 0 end
|
|
FrlgFont.drawGlyph = function(id, x, y) log[#log + 1] = { glyph = id, x = x, y = y } return 0 end
|
|
|
|
local function inside(e, f)
|
|
return f and e.x >= f.l * 8 and e.x < (f.l + f.w) * 8
|
|
and e.y >= f.t * 8 and e.y + FrlgFont.GLYPH_HEIGHT <= (f.t + f.h) * 8
|
|
end
|
|
local function find(s)
|
|
for _, e in ipairs(log) do if e.s == s then return e end end
|
|
end
|
|
|
|
local function layout(label, setup, expect, confirm)
|
|
for i = #log, 1, -1 do log[i] = nil end
|
|
ModManager.show({ game = stubGame() })
|
|
local m = ModManager._mgr
|
|
setup(m)
|
|
ModManager.draw()
|
|
local msgFrame, ynFrame
|
|
for _, e in ipairs(log) do
|
|
if e.frame and e.l == 21 then ynFrame = e
|
|
elseif e.frame and e.l == 2 then msgFrame = e end
|
|
end
|
|
check(msgFrame ~= nil, label .. ": message frame drawn")
|
|
local prevY
|
|
for _, s in ipairs(expect) do
|
|
local e = find(s)
|
|
check(e and inside(e, msgFrame), label .. ": '" .. s .. "' inside the message frame"
|
|
.. (e and (" (y=" .. e.y .. ")") or " (missing)"))
|
|
if e and prevY then
|
|
check(e.y - prevY >= 14, label .. ": message rows at least 14px apart")
|
|
end
|
|
prevY = e and e.y
|
|
end
|
|
if confirm then
|
|
check(ynFrame ~= nil and ynFrame.w == 6 and ynFrame.h == 4,
|
|
label .. ": YES/NO std frame at left 21")
|
|
local yes, no = find("YES"), find("NO")
|
|
check(yes and no and inside(yes, ynFrame) and inside(no, ynFrame),
|
|
label .. ": YES/NO printed inside the YES/NO frame")
|
|
check(ynFrame and msgFrame and (ynFrame.t + ynFrame.h < msgFrame.t - 1
|
|
or msgFrame.l + msgFrame.w < ynFrame.l - 1),
|
|
label .. ": YES/NO frame clears the message frame border")
|
|
check(ynFrame and ynFrame.t - 1 > 5, label .. ": YES/NO frame clears the title frame border"
|
|
.. (ynFrame and (" (top=" .. ynFrame.t .. ")") or ""))
|
|
local arrow
|
|
for _, e in ipairs(log) do
|
|
if e.glyph and ynFrame and e.x == ynFrame.l * 8 then arrow = e end
|
|
end
|
|
check(arrow and yes and arrow.y == yes.y, label .. ": cursor sits on YES")
|
|
elseif confirm == false then
|
|
check(ynFrame == nil, label .. ": no YES/NO frame for an OK notice")
|
|
end
|
|
check(msgFrame and msgFrame.t - 1 > 5, label .. ": message frame clears the title frame border"
|
|
.. (msgFrame and (" (top=" .. msgFrame.t .. ")") or ""))
|
|
for _, e in ipairs(log) do
|
|
if e.s and e.y then
|
|
check(e.y + FrlgFont.GLYPH_HEIGHT <= 160, label .. ": '" .. e.s .. "' on screen")
|
|
end
|
|
end
|
|
ModManager._prompt = nil
|
|
ModManager.close()
|
|
end
|
|
|
|
layout("restart", function(m)
|
|
m:openConfirm({ "RESTART NOW?" }, function() end)
|
|
end, { "RESTART NOW?" }, true)
|
|
layout("not made for", function(m)
|
|
m:openConfirm({ "NOT MADE FOR", "THIS GAME.", "TRY IT ANYWAY?" }, function() end)
|
|
end, { "NOT MADE FOR", "THIS GAME.", "TRY IT ANYWAY?" }, true)
|
|
layout("experimental", function(m)
|
|
m:openConfirm({ "EXPERIMENTAL MOD", "THIS MOD IS MARKED", "EXPERIMENTAL.", "ENABLE ANYWAY?" },
|
|
function() end)
|
|
end, { "EXPERIMENTAL MOD", "THIS MOD IS MARKED", "EXPERIMENTAL.", "ENABLE ANYWAY?" }, true)
|
|
layout("blocked", function(m)
|
|
m:openBlocked({ missing = { "dep" }, conflicts = { "zz" }, badVersion = {} })
|
|
end, { "NEEDS dep", "NOT INSTALLED", "CONFLICTS WITH", "zz", "DISABLE IT FIRST" }, false)
|
|
layout("blocked long", function(m)
|
|
m:openBlocked({ missing = { "d1", "d2", "d3", "d4" }, conflicts = {}, badVersion = {} })
|
|
end, { "NEEDS d1", "NOT INSTALLED", "NEEDS d2" }, false)
|
|
layout("qty", function(m)
|
|
m.game.stack:push(QuantityBox.new(m.game, { max = 9, start = 4, onDone = function() end }))
|
|
end, { "HOW MANY?" }, nil)
|
|
local qtyValue = find("4")
|
|
local qtyFrame
|
|
for _, e in ipairs(log) do
|
|
if e.frame and e.l == 21 then qtyFrame = e end
|
|
end
|
|
check(qtyFrame and qtyValue and inside(qtyValue, qtyFrame),
|
|
"qty: value printed inside its own std frame")
|
|
|
|
local wraps = 0
|
|
local realWrap = FrlgFont.wrap
|
|
FrlgFont.wrap = function(...) wraps = wraps + 1 return realWrap(...) end
|
|
ModManager.show({ game = stubGame() })
|
|
ModManager._mgr:openConfirm({ "EXPERIMENTAL MOD", "THIS MOD IS MARKED", "EXPERIMENTAL.", "ENABLE ANYWAY?" },
|
|
function() end)
|
|
ModManager.draw()
|
|
check(wraps > 0, "overlay: first draw wraps the message lines")
|
|
local afterFirst = wraps
|
|
ModManager.draw()
|
|
ModManager.draw()
|
|
check(wraps == afterFirst, "overlay: later frames reuse the wrapped layout (wraps=" .. wraps .. ")")
|
|
ModManager._mgr.game.stack:push(QuantityBox.new(ModManager._mgr.game, { max = 9, start = 4, onDone = function() end }))
|
|
ModManager.draw()
|
|
afterFirst = wraps
|
|
ModManager.draw()
|
|
check(wraps == afterFirst, "qty prompt: later frames reuse the wrapped layout")
|
|
ModManager._prompt = nil
|
|
ModManager.close()
|
|
FrlgFont.wrap = realWrap
|
|
|
|
Chrome.stdFrame, Chrome.fixedStdFrame, Chrome.userFrame = real.std, real.fixed, real.user
|
|
Chrome.dialogueFrame, FrlgFont.draw, FrlgFont.drawGlyph = real.dlg, real.draw, real.glyph
|
|
end
|
|
|
|
print("[test] 11c. Keypad icons fall back to button words without a keypad sheet (Ruby/Sapphire)")
|
|
do
|
|
local FrlgFont = require("src.ui.game3.frlg_font")
|
|
local real = { sync = FrlgFont.sync, draw = FrlgFont.draw, measure = FrlgFont.measure, keypad = FrlgFont._keypad }
|
|
local texts = {}
|
|
FrlgFont.draw = function(s, x, y, opts) texts[#texts + 1] = { s = tostring(s), opts = opts } return 0 end
|
|
FrlgFont.measure = function(s) return 6 * #tostring(s) end
|
|
|
|
FrlgFont.sync = function() return { nativeLayout = "rs" } end
|
|
check(FrlgFont.hasKeypadIcons() == false, "rs: no keypad icon sheet")
|
|
local colors = { fg = { 1, 1, 1, 1 } }
|
|
local ok, w = pcall(FrlgFont.drawKeypadIcon, 0x00, 10, 2, { colors = colors, small = true })
|
|
check(ok, "rs: drawKeypadIcon does not raise " .. tostring(not ok and w or ""))
|
|
check(texts[1] and texts[1].s == "A " and texts[1].opts.colors == colors and texts[1].opts.small == true,
|
|
"rs: A_BUTTON draws the word 'A' in the caller's colors and face")
|
|
check(w == 12, "rs: drawKeypadIcon returns the word width")
|
|
check(FrlgFont.keypadIconWidth(0x0A) == 6 * #"UP/DN ", "rs: measure uses the word width")
|
|
|
|
FrlgFont.sync = function() return nil end
|
|
FrlgFont._keypad = nil
|
|
check(FrlgFont.keypadIconWidth(0x04) == 6 * #"START " and FrlgFont._keypad == false,
|
|
"missing sheet on a FRLG cache: measure-only caller gets the word width before any draw")
|
|
FrlgFont._keypad = false
|
|
texts = {}
|
|
ok = pcall(FrlgFont.drawKeypadIcon, 0x04, 0, 0)
|
|
check(ok and texts[1] and texts[1].s == "START ", "missing sheet on a FRLG cache: START word, no raise")
|
|
FrlgFont._keypad = { getDimensions = function() return 128, 32 end }
|
|
check(FrlgFont.hasKeypadIcons() == true, "frlg with a sheet keeps the icons")
|
|
check(FrlgFont.keypadIconWidth(0x04) == FrlgFont.KEYPAD_ICONS[0x04].w, "frlg: icon width unchanged")
|
|
|
|
FrlgFont.draw, FrlgFont.measure = real.draw, real.measure
|
|
local font = { fg = FrlgFont._fg, widths = FrlgFont._widths, quads = FrlgFont._quads, icon = FrlgFont.drawKeypadIcon }
|
|
FrlgFont._fg, FrlgFont._widths, FrlgFont._quads = font.fg or {}, font.widths or {}, font.quads or {}
|
|
local seen = {}
|
|
FrlgFont.drawKeypadIcon = function(id, _, _, opts)
|
|
seen[#seen + 1] = { opts = opts, colors = opts and opts.colors, fg = opts and opts.colors and opts.colors.fg }
|
|
return FrlgFont.KEYPAD_ICONS[id].w
|
|
end
|
|
local white = { fg = { 1, 1, 1, 1 } }
|
|
FrlgFont.draw("{A_BUTTON}{B_BUTTON}", 0, 0, { colors = white })
|
|
FrlgFont.draw("{START_BUTTON}", 0, 0, { colors = white })
|
|
check(#seen == 3 and seen[1].opts == seen[2].opts and seen[2].opts == seen[3].opts
|
|
and seen[1].colors == seen[3].colors and seen[1].opts ~= nil,
|
|
"draw: icon opts/colors tables are reused across icons and calls (no per-icon allocation)")
|
|
check(seen[1] and seen[1].fg == white.fg, "draw: icon colors carry the caller's fg")
|
|
FrlgFont._fg, FrlgFont._widths, FrlgFont._quads, FrlgFont.drawKeypadIcon = font.fg, font.widths, font.quads, font.icon
|
|
|
|
FrlgFont.sync, FrlgFont._keypad = real.sync, real.keypad
|
|
|
|
local msrc = io.open("src/ui/game3/mod_manager.lua", "r"):read("*a")
|
|
check(not msrc:find("KEY_WORDS", 1, true) and not msrc:find("_keypadOk", 1, true),
|
|
"mod manager has no per-screen keypad fallback")
|
|
end
|
|
|
|
print("[test] 11d. Named colors map to the RS font palette slots on Ruby/Sapphire only")
|
|
do
|
|
local FrlgFont = require("src.ui.game3.frlg_font")
|
|
local P, C = FrlgFont.STDPAL, FrlgFont.COLOR
|
|
FrlgFont.applyColorSlots(true)
|
|
check(C.DARK_GRAY.fg == P[1] and C.DARK_GRAY.shadow == P[8], "rs: DARK_GRAY is DARK_GREY/LIGHT_GREY")
|
|
check(C.RED.fg == P[2] and C.RED.shadow == P[8], "rs: RED is slot 2")
|
|
check(C.BLUE.fg == P[4] and C.GREEN.fg == P[3], "rs: BLUE slot 4, GREEN slot 3")
|
|
check(C.WHITE.fg == P[12] and C.WHITE.shadow == P[1], "rs: WHITE slot 12 over DARK_GREY")
|
|
check(FrlgFont.COLOR_IDS.RED == 2 and FrlgFont.COLOR_IDS.BLUE == 4 and FrlgFont.COLOR_IDS.WHITE == 12,
|
|
"rs: {COLOR} names use pokeruby charmap ids")
|
|
FrlgFont.applyColorSlots(false)
|
|
check(C.DARK_GRAY.fg == P[2] and C.DARK_GRAY.shadow == P[3], "frlg: DARK_GRAY back to 2/3")
|
|
check(C.RED.fg == P[4] and C.RED.shadow == P[5] and C.BLUE.fg == P[8] and C.BLUE.shadow == P[3],
|
|
"frlg: RED 4/5, BLUE 8/3")
|
|
check(C.MALE.fg == P[9] and C.FEMALE.fg == P[5] and C.WHITE.fg == P[1] and C.WHITE.shadow == P[2],
|
|
"frlg: gender and WHITE slots unchanged")
|
|
check(FrlgFont.COLOR_IDS.RED == 4 and FrlgFont.COLOR_IDS.BLUE == 8 and FrlgFont.COLOR_IDS.WHITE == 1
|
|
and FrlgFont.COLOR_IDS.DARK_GREY == nil, "frlg: COLOR_IDS restored")
|
|
end
|
|
|
|
print("[test] 12. New manager module does not draw with Gen 1 Font/Theme")
|
|
local src = io.open("src/ui/game3/mod_manager.lua", "r"):read("*a")
|
|
check(not src:find('require("src.render.Font")', 1, true),
|
|
"no src.render.Font require")
|
|
check(not src:find('require("src.ui.Theme")', 1, true),
|
|
"no src.ui.Theme require")
|
|
-- Match Gen 1 Font.draw, not FrlgFont.draw (shared substring).
|
|
check(not src:find("[^%w]Font%.draw"), "no Gen 1 Font.draw call")
|
|
|
|
if failed > 0 then
|
|
print(string.format("[FAIL] %d check(s) failed", failed))
|
|
os.exit(1)
|
|
end
|
|
print("ALL TESTS PASSED")
|
|
os.exit(0)
|