Files
gen1recomp/tests/engine/options_submenu_marquee_test.lua
T
2026-08-26 12:04:06 -04:00

153 lines
6.0 KiB
Lua

-- The OPTION screen's grouped pages and its scrolling text.
--
-- The flat row list outgrew the four-box viewport, so the battle, audio,
-- video and speed rows collapse into one opener each. Grouping happens after
-- the ui.options.rows hook and only builds self.view, so self.rows stays the
-- flat list a mod reads and edits.
--
-- Text longer than its line used to run off the 160px panel with no clipping
-- (COLORS showed "1-A (Default) (B"); the highlighted row now scrolls instead.
-- luajit tests/engine/options_submenu_marquee_test.lua
package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.modkit")
local OptionsMenu = require("src.ui.OptionsMenu")
local Marquee = require("src.ui.Marquee")
local function stubGame()
local stack = { items = {} }
function stack:push(s) self.items[#self.items + 1] = s; return s end
function stack:pop() self.items[#self.items] = nil end
function stack:top() return self.items[#self.items] end
return {
data = { rulesets = { gen1_faithful = { name = "GEN 1" } }, constants = {} },
save = { options = {} },
stack = stack,
modStatus = { available = {} },
}
end
local function ids(rows)
local out = {}
for i, row in ipairs(rows) do out[i] = row.id end
return table.concat(out, ",")
end
local function find(rows, id)
for i, row in ipairs(rows) do if row.id == id then return row, i end end
end
-- ------------------------------------------------------------- grouping
local game = stubGame()
local menu = OptionsMenu.new(game)
T.check(find(menu.rows, "battleBg"), "self.rows keeps the flat list for mods")
T.check(find(menu.rows, "musicVol"), "self.rows keeps the audio rows too")
for _, id in ipairs({ "battleBg", "speedBattle", "uiLayout", "tilt", "zoom",
"voidFill", "colors", "shaderfx", "shaderfx2",
"musicVol", "videoMode" }) do
T.check(not find(menu.view, id), id .. " moved onto a page")
end
for _, spec in ipairs({
{ "group.battle", 6 }, { "group.audio", 3 }, { "group.video", 5 },
{ "group.speed", 4 }, { "group.graphics", 4 }, { "group.extras", 3 },
}) do
local row = find(menu.view, spec[1])
T.check(row, spec[1] .. " has an opener row")
T.eq(row.group, true, spec[1] .. " is marked as a group")
end
-- Six battle rows on this build (PIKACHU VOL is Yellow-only, so AUDIO is 3).
T.eq(select(1, find(menu.view, "group.battle")).value(game), "6 OPTIONS",
"BATTLE OPTIONS counts its six rows")
-- The top level is explicitly ordered, groups and singles interleaved.
T.eq(ids(menu.view):gsub("group%.", ""),
"speed,video,graphics,audio,performance,ruleset,battle,extras,mods," ..
"controls,dateFormat,timeFormat",
"the top level reads in the order ORDER names")
T.check(not find(menu.view, "textSpeed"), "TEXT SPEED moved onto the SPEED page")
T.check(#menu.view < #menu.rows, "the view is shorter than the flat list")
-- A row in no group is untouched and keeps its order.
T.check(find(menu.view, "ruleset"), "RULESET stays on the top level")
T.check(find(menu.view, "performance"), "so does PERFORMANCE")
T.check(find(menu.view, "mods"), "and MODS")
-- ------------------------------------------------------------- submenus
local graphics = find(menu.view, "group.graphics")
graphics.activate(game)
T.eq(ids(game.stack:top().view), "colors,uiLetterbox,shaderfx,shaderfx2",
"GRAPHICS carries COLORS, UI LETTERBOX and both SHADER FX slots")
game.stack:pop()
local extras = find(menu.view, "group.extras")
extras.activate(game)
T.eq(ids(game.stack:top().view), "tilt,zoom,voidFill",
"EXTRAS carries TILT, ZOOM and VOID FILL")
game.stack:pop()
local video = find(menu.view, "group.video")
video.activate(game)
T.eq(ids(game.stack:top().view):match("^uiLayout"), "uiLayout",
"UI LAYOUT heads the VIDEO page")
game.stack:pop()
local speed = find(menu.view, "group.speed")
speed.activate(game)
T.eq(ids(game.stack:top().view),
"textSpeed,speedOverworld,speedBattle,speedMenu",
"TEXT SPEED heads the SPEED page")
game.stack:pop()
local battle, at = find(menu.view, "group.battle")
menu.index = at
battle.activate(game)
local sub = game.stack:top()
T.check(sub ~= menu, "opening a group pushes a screen")
T.eq(ids(sub.view),
"animations,battleStyle,battleLayout,battleFit,battleHud,battleBg",
"the page carries exactly its six battle rows, in order")
T.eq(sub.sub, true, "the page knows it is a submenu")
T.eq(sub.onCancel, nil, "BACK out of a page never fires the caller's close hook")
-- focusRow reaches a grouped row by opening its page.
local game2 = stubGame()
local menu2 = OptionsMenu.new(game2)
local landed = menu2:focusRow("battleHud")
T.check(landed and landed ~= menu2, "focusRow opens the page a grouped row lives on")
T.eq(landed.view[landed.index].id, "battleHud", "and lands the cursor on it")
T.eq(menu2:focusRow("ruleset"), menu2, "an ungrouped row focuses in place")
T.eq(menu2.view[menu2.index].id, "ruleset", "on the row asked for")
T.eq(menu2:focusRow("nope"), nil, "an unknown id focuses nothing")
-- ------------------------------------------------------------- marquee
-- 16 characters fit beside a value's x=24 before the box frame at x=152;
-- the label's x=16 fits 17.
local long = "1-A (Default) (BGB)"
T.check(#long > 16, "the COLORS value really is too long for the line")
-- 19 characters, 16 fit: three steps of overflow. Hold 1.0s, one char per
-- 0.25s, hold 1.0s on the tail, then repeat -- a 2.75s cycle.
T.eq(Marquee.at(long, 16, 0), long:sub(1, 16), "it holds at the start")
T.eq(Marquee.at(long, 16, 1.3), long:sub(2, 17), "then steps one char")
T.eq(Marquee.at(long, 16, 1.6), long:sub(3, 18), "then another")
T.eq(Marquee.at(long, 16, 2.4), long:sub(4, 19), "then holds on the tail")
T.eq(Marquee.at(long, 16, 2.8), long:sub(1, 16), "before wrapping back")
T.eq(Marquee.at(long, 16, 2.4):sub(-4), "BGB)",
"so the tail the panel used to clip becomes readable")
for _, at2 in ipairs({ 0, 1.3, 1.6, 2.4, 2.8 }) do
T.eq(#Marquee.at(long, 16, at2), 16,
"every frame fills the line exactly")
end
T.eq(Marquee.at("SHORT", 16, 2.0), "SHORT",
"text that fits is never scrolled")
T.finish("options submenu + marquee")