From c8e035d3322dddfb1bed4ef275235b2eaa81a658 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 31 Jul 2026 01:44:17 +0000 Subject: [PATCH] Add graphics performance tier for low-end devices Introduce an OPTIONS -> PERFORMANCE setting that scales the port's optional presentation extras down for weaker hardware, so older/lower-end devices can run the game smoothly. The tier governs the three heaviest non-faithful extras -- the 3D TILT, the GBC FX post-process shader, and survey ZOOM (which renders connected neighbor maps) -- plus a hard FPS ceiling. It never touches game logic, which is fixed-step off dt, so every tier plays identically. - src/core/Performance.lua: tiers (auto/high/balanced/low), a conservative device auto-detect (ARM handhelds -> low, phones -> balanced, normal desktops -> high), per-tier caps, and the option-row cycle. Zero requires, like GameVersion. - Game:applyOptions clamps the *live* presentation state against the tier without rewriting stored options, so a lower tier hides the player's TILT/GBC FX/ZOOM/FPS choices and a higher tier restores them exactly. - Zoom.offsetRange floors the range at FIT when survey is disallowed, so the option row, hotkey, and mouse wheel all stop at close-up on LOW. - New save.options.performance default "auto"; OPTIONS row heads the display group and re-applies live. - Tests: tests/engine/performance_tiers.lua (ROM-free); mod_ui_tests row golden updated for the spliced row. AUTO resolves to HIGH on a normal desktop and on every options.lua that predates the option, so the common case is unchanged. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01Q6bFAiQyZ5jDmewsbB4LG9 --- README.md | 11 ++ docs/anbernic-rg34xxsp.md | 7 ++ docs/new-features.md | 35 +++++++ src/core/Game.lua | 17 ++++ src/core/Performance.lua | 156 +++++++++++++++++++++++++++++ src/core/SaveData.lua | 5 + src/render/Zoom.lua | 10 ++ src/ui/OptionsMenu.lua | 16 +++ tests/engine/performance_tiers.lua | 113 +++++++++++++++++++++ tests/mod_ui_tests.lua | 30 +++--- 10 files changed, 386 insertions(+), 14 deletions(-) create mode 100644 src/core/Performance.lua create mode 100644 tests/engine/performance_tiers.lua diff --git a/README.md b/README.md index a7291e32..f000776b 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,17 @@ supported out of the box. COLORS, TILT, ZOOM, GBC FX, and VOID FILL are also in the Options menu and persist in `options.lua`. +### Low-end devices + +**OPTIONS → PERFORMANCE** scales the port's optional extras for weaker +hardware: **HIGH** (everything on), **BALANCED** (no 3D tilt or GBC FX), +**LOW** (also no survey zoom, FPS capped), or **AUTO** — the default, which +picks a tier from your device (ARM handhelds → LOW, phones → BALANCED, +normal desktops → HIGH, unchanged). It only scales presentation; the +fixed-step game logic is identical on every tier, and a lower tier hides +your tilt/zoom/GBC-FX preferences without forgetting them. Details in +[docs/new-features.md](docs/new-features.md#performance-tier-low-end-devices). + ### Rulesets **OPTIONS → RULESET** picks which set of Gen 1 battle behaviors to run. diff --git a/docs/anbernic-rg34xxsp.md b/docs/anbernic-rg34xxsp.md index 4081f4bc..441090ab 100644 --- a/docs/anbernic-rg34xxsp.md +++ b/docs/anbernic-rg34xxsp.md @@ -65,6 +65,13 @@ display option — COLORS, TILT, ZOOM, VOID FILL, MAX FPS — works normally. If your device turns out to handle the pass, launch with `POKEPORT_GBCFX=1` to put the row back. +**PERFORMANCE defaults to LOW here.** The OPTIONS → PERFORMANCE tier defaults +to AUTO, which reads this device as an ARM Linux handheld and resolves to +**LOW**: the 3D tilt and survey zoom stay off and the frame rate is capped, +so the overworld runs smoothly on the H700 out of the box. Bump it to +BALANCED or HIGH from OPTIONS if you want the extras and your device keeps +up; see [Performance tier](new-features.md#performance-tier-low-end-devices). + The pack bundles the LÖVE 11.5 aarch64 runtime from [PortMaster](https://portmaster.games/), so the device does not need a separate `love_11.5` runtime download on first launch. The launcher resolves diff --git a/docs/new-features.md b/docs/new-features.md index 42d72087..f6dd6461 100644 --- a/docs/new-features.md +++ b/docs/new-features.md @@ -137,6 +137,41 @@ effect, `=1` forces it available. The Anbernic handheld pack exports `0` from its launcher because the device reports `"Linux"` while its GPU is in the phone class (see [Anbernic RG34XXSP](anbernic-rg34xxsp.md)). +## Performance tier (low-end devices) + +The Options **PERFORMANCE** row scales the port's optional presentation +extras down for weaker hardware. The extras it governs are the three +heaviest things the port adds on top of the original -- the whole-screen 3D +**TILT** (transforms the entire map as a ground plane), the **GBC FX** +post-process shader (a fullscreen pass), and survey **ZOOM** (zooming out +renders the connected neighbor maps, a lot of extra overdraw) -- plus a hard +FPS ceiling. None of this touches game logic, which is fixed-step off `dt` +(`src/core/FixedStep.lua`), so every tier plays identically; they differ +only in how much eye-candy the renderer is allowed to do. + +| Tier | TILT | GBC FX | Survey ZOOM | Extra FPS ceiling | +| ------------ | ---- | ------ | ----------- | ----------------- | +| **HIGH** | on | on | on | none | +| **BALANCED** | off | off | on | none | +| **LOW** | off | off | off | 60 | +| **AUTO** | picks a default from the device (below) ||| + +- **AUTO** (the default) reads the device once at boot: ARM Linux handhelds + (e.g. the RG34XXSP) resolve to **LOW**, phones/tablets and very-low-core + desktops to **BALANCED**, and everything else -- a normal desktop, and + every existing `options.lua` that predates this option -- to **HIGH**, + so the common case is unchanged. See `src/core/Performance.detect`. +- AUTO only chooses the *default*; all four tiers are selectable, so a + wrong guess is one row away from being overridden. +- The clamps are applied **live** against your stored options and never + rewrite them (`Game:applyOptions`), so a lower tier hides your TILT / GBC + FX / ZOOM without forgetting them -- raising the tier restores exactly + what you had. (This is why the TILT / GBC FX / ZOOM rows still show your + saved choice on a clamped tier: it's your preference, waiting for a tier + that can afford it.) +- Persisted as `save.options.performance` (`auto` | `high` | `balanced` | + `low`); unit-tested in `tests/engine/performance_tiers.lua`. + ## Peer-to-peer link play (lua-enet) Trades and link battles connect two copies of the game directly over diff --git a/src/core/Game.lua b/src/core/Game.lua index 694e5c72..5492f7c5 100644 --- a/src/core/Game.lua +++ b/src/core/Game.lua @@ -618,6 +618,23 @@ function Game:applyOptions(opts) -- normalizes a nil/garbage cap to the 60 default, so old saves with no -- fpsCap key pace at the standard rate (issue #88) require("src.core.FrameCap").applyOptions(opts) + -- Scale the optional presentation extras to the device's performance + -- tier. Every heavy feature was just applied from the stored options + -- above; here we clamp the *live* state down for a weaker device without + -- rewriting what the player saved, so raising the tier later restores + -- their exact TILT / GBC FX / ZOOM / MAX FPS choices. A HIGH tier (the + -- default on a normal desktop, and every options.lua predating this + -- option) clamps nothing, so it is a no-op for the common case. + local caps = require("src.core.Performance").applyOptions(opts) + if not caps.tilt then require("src.render.Tilt").setLevel(0) end + if not caps.gbcfx then require("src.render.GBCFX").setLevel(0) end + local Zoom = require("src.render.Zoom") + Zoom.allowSurvey = caps.survey + if not caps.survey and Zoom.offset < 0 then Zoom.offset = 0 end + if caps.fpsMax then + local FrameCap = require("src.core.FrameCap") + if FrameCap.current > caps.fpsMax then FrameCap.apply(caps.fpsMax) end + end Input:applyBindings(opts.bindings) -- heal soft-bricked APK installs that already saved gbcfx > 0 (#136) if gbcCleared then self:writeOptions() end diff --git a/src/core/Performance.lua b/src/core/Performance.lua new file mode 100644 index 00000000..a3d2945a --- /dev/null +++ b/src/core/Performance.lua @@ -0,0 +1,156 @@ +-- Graphics performance tier: one knob that scales the port's optional, +-- non-faithful presentation extras down for weaker hardware. +-- +-- The heavy extras are all things the Game Boy never had and this port +-- adds on top: the whole-screen 3D TILT (transforms the entire map as a +-- ground plane), the GBC FX post-process shader (a fullscreen pass), and +-- survey ZOOM (zooming out renders the connected neighbor maps -- a lot of +-- extra overdraw). A hard FPS ceiling caps present cost on top of that. +-- None of this touches game logic, which is fixed-step off dt +-- (src/core/FixedStep.lua), so every tier plays identically; they differ +-- only in how much optional eye-candy the renderer is allowed to do. +-- +-- The tier is persisted as save.options.performance: +-- auto pick a default from the device (see detect()) +-- high everything on -- the historical behavior +-- balanced no TILT, no GBC FX (kept: survey zoom, colors, uncapped FPS) +-- low no TILT, no GBC FX, no survey zoom, FPS capped +-- +-- AUTO only chooses the *default* for the current device; every tier is +-- selectable in OPTIONS, so a heuristic that guesses wrong is one row away +-- from being overridden. The clamps are applied live in Game:applyOptions +-- against the stored options without rewriting them, so raising the tier +-- restores exactly the TILT / GBC FX / ZOOM / FPS the player had. +-- +-- Zero requires: loads during love.conf and under plain Lua for tools and +-- tests, the same way src/core/GameVersion.lua does. + +local Performance = {} + +-- Option-row order (auto first, then most to least capable). +Performance.TIERS = { "auto", "high", "balanced", "low" } + +Performance.LABELS = { + auto = "AUTO", + high = "HIGH", + balanced = "BALANCED", + low = "LOW", +} + +-- What each concrete tier permits. `auto` is resolved to one of these +-- before caps are read, so it has no row here. fpsMax = false means no +-- extra ceiling (the player's own MAX FPS still applies). +Performance.CAPS = { + high = { tilt = true, gbcfx = true, survey = true, fpsMax = false }, + balanced = { tilt = false, gbcfx = false, survey = true, fpsMax = false }, + low = { tilt = false, gbcfx = false, survey = false, fpsMax = 60 }, +} + +-- Live resolved tier (never "auto"); Game:applyOptions sets it and the +-- renderer / options row read it back. Defaults to the unclamped tier so +-- a pre-boot launcher behaves exactly as it always did. +Performance.tier = "high" + +local function loveOS() + if love and love.system and love.system.getOS then + return love.system.getOS() + end + return nil +end + +local function processorCount() + if love and love.system and love.system.getProcessorCount then + return love.system.getProcessorCount() + end + return nil +end + +-- CPU architecture via LuaJIT's jit.arch when present: "arm"/"arm64" on +-- phones and PortMaster handhelds, "x86"/"x64" on desktops. nil under a +-- plain-Lua tool or test with no jit table, which resolves to HIGH (no +-- clamping) so tooling is never surprised. +local function cpuArch() + return (jit and jit.arch) or nil +end + +-- Default tier for the current device. Deliberately conservative: only +-- the platforms that are reliably weak drop below HIGH, and AUTO is always +-- overridable, so a wrong guess costs one OPTIONS row. A normal +-- multi-core desktop -- the common case, and every existing install whose +-- options.lua predates this option -- resolves to HIGH and behaves exactly +-- as before. +function Performance.detect() + local os = loveOS() + local arch = cpuArch() + local isArm = arch == "arm" or arch == "arm64" + local cores = processorCount() + + -- PortMaster-style ARM Linux handhelds (e.g. the RG34XXSP the project + -- already ships a build for): the weakest target here. + if isArm and os ~= "Android" and os ~= "iOS" then + return "low" + end + -- Phones and tablets: GBC FX is already force-disabled here (issue #136); + -- balanced additionally drops the 3D tilt, the heaviest remaining extra. + if os == "Android" or os == "iOS" then + return "balanced" + end + -- Desktop: a dual-core (or single-core) box is the low-end line. + if cores and cores <= 2 then + return "balanced" + end + return "high" +end + +-- Fold a stored option value to a concrete tier, resolving "auto" (and any +-- hand-edited garbage) through detect(). +function Performance.resolve(value) + if value == "high" or value == "balanced" or value == "low" then + return value + end + return Performance.detect() +end + +-- Normalize a stored value to a valid option id (keeps "auto"); a bad value +-- degrades to auto so a corrupt options.lua still lands on something sane. +function Performance.normalize(value) + for _, id in ipairs(Performance.TIERS) do + if value == id then return id end + end + return "auto" +end + +-- Row label for a stored value: the tier's own name (AUTO / HIGH / ...). +function Performance.label(value) + return Performance.LABELS[Performance.normalize(value)] +end + +-- Concrete caps for a stored option value (resolving auto). +function Performance.caps(value) + return Performance.CAPS[Performance.resolve(value)] +end + +-- Resolve the tier for these options, record it live, and return its caps. +-- Called from Game:applyOptions, which clamps the live presentation modules +-- against the returned caps. +function Performance.applyOptions(opts) + local tier = Performance.resolve(opts and opts.performance) + Performance.tier = tier + return Performance.CAPS[tier] +end + +-- Cycle the OPTIONS row: auto -> high -> balanced -> low -> auto (dir -1 +-- reverses). Lua's `%` is non-negative for a positive modulus, so a +-- negative dir wraps correctly. +function Performance.cycle(value, dir) + value = Performance.normalize(value) + local i = 1 + for idx, id in ipairs(Performance.TIERS) do + if id == value then i = idx break end + end + local n = #Performance.TIERS + local nextIdx = (i - 1 + (dir or 1)) % n + 1 + return Performance.TIERS[nextIdx] +end + +return Performance diff --git a/src/core/SaveData.lua b/src/core/SaveData.lua index c597c361..f9a9c48e 100644 --- a/src/core/SaveData.lua +++ b/src/core/SaveData.lua @@ -241,6 +241,11 @@ function SaveData.defaultOptions() videoMode = "windowed", -- hard render frame-rate cap; render-only pacing (issue #88, FrameCap.lua) fpsCap = 60, + -- graphics performance tier: auto | high | balanced | low. "auto" + -- picks a default from the device (ARM handhelds/phones drop the heavy + -- extras); scales TILT / GBC FX / survey ZOOM / FPS but never game + -- logic. See src/core/Performance.lua. + performance = "auto", -- Per-pipeline display levels, keyed by render_pipelines id (see -- src/render/Pipelines.lua). A level for a mod that is not installed -- is kept rather than pruned, so re-enabling the mod restores the mode diff --git a/src/render/Zoom.lua b/src/render/Zoom.lua index 55a5f567..13b27cad 100644 --- a/src/render/Zoom.lua +++ b/src/render/Zoom.lua @@ -10,6 +10,13 @@ local Zoom = {} Zoom.offset = 0 +-- Survey zoom (zooming out past FIT, which renders connected neighbor maps) +-- is the port's most expensive optional extra. The performance tier sets +-- this false on LOW hardware (Game:applyOptions); offsetRange then floors +-- the range at FIT so the option row, hotkey, and mouse wheel all stop at +-- close-up. Nil/true keeps the historical full range. +Zoom.allowSurvey = true + -- legal offset range for a given fit scale (vanilla: survey at 1 px/world -- through 2× fit). zoom.range may widen or shrink the window. function Zoom.offsetRange(S) @@ -21,6 +28,9 @@ function Zoom.offsetRange(S) hi = math.floor(tonumber(hi) or S) if lo > hi then lo, hi = hi, lo end end + -- LOW performance tier: no survey (negative offsets), even if a mod's + -- zoom.range widened it. == false so nil/true stays permissive. + if Zoom.allowSurvey == false and lo < 0 then lo = 0 end return lo, hi end diff --git a/src/ui/OptionsMenu.lua b/src/ui/OptionsMenu.lua index 6ca776c3..c54cf32e 100644 --- a/src/ui/OptionsMenu.lua +++ b/src/ui/OptionsMenu.lua @@ -20,6 +20,7 @@ local GameSpeed = require("src.core.GameSpeed") local GameVersion = require("src.core.GameVersion") local VideoMode = require("src.core.VideoMode") local FrameCap = require("src.core.FrameCap") +local Performance = require("src.core.Performance") local Logger = require("src.core.Logger") local Runtime = require("src.mods.Runtime") local OptionRows = require("src.ui.OptionRows") @@ -202,6 +203,21 @@ local function buildRows(game) require("src.core.Music").setFilterLevel(o.musicFilter) return true end }, + -- Heads the port's display group: one tier that scales the heavy extras + -- (TILT / GBC FX / survey ZOOM) and the FPS ceiling for weaker devices. + -- AUTO picks a default from the hardware; every tier is overridable. + -- Re-applies live so the extras clamp (or, on a higher tier, restore to + -- the player's stored TILT / GBC FX / ZOOM) the moment the row changes. + { id = "performance", label = Strings("PERFORMANCE"), + value = function(g) + return Strings(Performance.label(g.save.options.performance)) + end, + step = function(g, dir) + local o = g.save.options + o.performance = Performance.cycle(o.performance, dir) + g:applyOptions(o) + return true + end }, { id = "colors", label = Strings("COLORS"), value = function(g) return PaletteFX.modeLabel(g.save.options.colors or "gbc") diff --git a/tests/engine/performance_tiers.lua b/tests/engine/performance_tiers.lua new file mode 100644 index 00000000..befdeb2f --- /dev/null +++ b/tests/engine/performance_tiers.lua @@ -0,0 +1,113 @@ +-- Graphics performance tier (src/core/Performance.lua): the AUTO device +-- heuristic, tier normalization/labels, per-tier caps, the option-row +-- cycle, and applyOptions recording the live tier. Pure logic over +-- stubbed love/jit globals, so it needs no ROM and runs in the T2 tier. + +package.path = "./?.lua;./?/init.lua;" .. package.path + +local T = require("tests.harness") +local Performance = require("src.core.Performance") + +-- detect() reads the live `love` and `jit` globals; swap them per scenario +-- and restore afterward so nothing leaks between checks (or into luajit). +local realLove, realJit = love, jit + +local function device(os, arch, cores) + love = { + system = { + getOS = function() return os end, + getProcessorCount = function() return cores end, + }, + } + jit = arch and { arch = arch } or nil +end + +local function restore() + love, jit = realLove, realJit +end + +-- ---------------------------------------------------------------- detect +device("Linux", "arm64", 4) +T.eq(Performance.detect(), "low", "ARM64 Linux handheld -> low") + +device("Linux", "arm", 2) +T.eq(Performance.detect(), "low", "32-bit ARM Linux handheld -> low") + +device("Android", "arm64", 8) +T.eq(Performance.detect(), "balanced", "Android phone -> balanced (not low)") + +device("iOS", "arm64", 6) +T.eq(Performance.detect(), "balanced", "iOS -> balanced") + +device("OS X", "x64", 8) +T.eq(Performance.detect(), "high", "8-core desktop -> high") + +device("Windows", "x64", 2) +T.eq(Performance.detect(), "balanced", "dual-core desktop -> balanced") + +device("Windows", "x86", 1) +T.eq(Performance.detect(), "balanced", "single-core desktop -> balanced") + +-- No love, no jit (a plain-Lua tool/test): nothing looks weak -> high, so +-- tooling that runs applyOptions is never surprised by a clamp. +love, jit = nil, nil +T.eq(Performance.detect(), "high", "no love/jit -> high (no clamping)") +restore() + +-- ---------------------------------------------------------------- resolve +T.eq(Performance.resolve("high"), "high", "resolve passes concrete high") +T.eq(Performance.resolve("balanced"), "balanced", "resolve passes balanced") +T.eq(Performance.resolve("low"), "low", "resolve passes low") + +device("Linux", "arm64", 4) +T.eq(Performance.resolve("auto"), "low", "resolve auto -> detect (handheld)") +T.eq(Performance.resolve(nil), "low", "resolve nil -> detect") +T.eq(Performance.resolve("bogus"), "low", "resolve garbage -> detect") +restore() + +-- --------------------------------------------------------------- normalize +T.eq(Performance.normalize("auto"), "auto", "normalize keeps auto") +T.eq(Performance.normalize("low"), "low", "normalize keeps low") +T.eq(Performance.normalize("xyz"), "auto", "normalize garbage -> auto") +T.eq(Performance.normalize(nil), "auto", "normalize nil -> auto") + +-- ------------------------------------------------------------------- label +T.eq(Performance.label("low"), "LOW", "label low") +T.eq(Performance.label("balanced"), "BALANCED", "label balanced") +T.eq(Performance.label(nil), "AUTO", "label nil -> AUTO") + +-- -------------------------------------------------------------------- caps +local hi, lo = Performance.CAPS.high, Performance.CAPS.low +T.check(hi.tilt and hi.gbcfx and hi.survey and not hi.fpsMax, "high caps: all on, no fps ceiling") +T.check((not lo.tilt) and (not lo.gbcfx) and (not lo.survey), "low caps: heavy extras off") +T.eq(lo.fpsMax, 60, "low caps: FPS ceiling of 60") +local bal = Performance.CAPS.balanced +T.check((not bal.tilt) and (not bal.gbcfx) and bal.survey and not bal.fpsMax, + "balanced caps: no tilt/gbcfx, survey kept, no fps ceiling") + +device("Linux", "arm64", 4) +T.eq(Performance.caps("auto"), Performance.CAPS.low, "caps(auto) resolves through detect") +restore() + +-- ------------------------------------------------------------- applyOptions +local caps = Performance.applyOptions({ performance = "low" }) +T.eq(caps, Performance.CAPS.low, "applyOptions returns the resolved caps") +T.eq(Performance.tier, "low", "applyOptions records the live tier") + +Performance.applyOptions({ performance = "high" }) +T.eq(Performance.tier, "high", "applyOptions high tier") + +device("Linux", "arm64", 4) +Performance.applyOptions(nil) +T.eq(Performance.tier, "low", "applyOptions(nil) resolves auto to the device tier") +restore() + +-- ------------------------------------------------------------------- cycle +T.eq(Performance.cycle("auto", 1), "high", "cycle auto -> high") +T.eq(Performance.cycle("high", 1), "balanced", "cycle high -> balanced") +T.eq(Performance.cycle("balanced", 1), "low", "cycle balanced -> low") +T.eq(Performance.cycle("low", 1), "auto", "cycle low wraps to auto") +T.eq(Performance.cycle("auto", -1), "low", "cycle back auto -> low") +T.eq(Performance.cycle("high", -1), "auto", "cycle back high -> auto") + +T.finish("performance_tiers") diff --git a/tests/mod_ui_tests.lua b/tests/mod_ui_tests.lua index f1aad5fd..acc34e9b 100644 --- a/tests/mod_ui_tests.lua +++ b/tests/mod_ui_tests.lua @@ -282,7 +282,8 @@ local function optGame() end local om = OptionsMenu.new(optGame()) local WANT_IDS = { "textSpeed", "animations", "battleStyle", "battleLayout", - "ruleset", "musicVol", "sfxVol", "musicFilter", "colors", + "ruleset", "musicVol", "sfxVol", "musicFilter", + "performance", "colors", "tilt", "gbcfx", "zoom", "voidFill", "videoMode", "fpsCap", "speed", "mods", "controls" } check(#om.rows == #WANT_IDS, "vanilla options row count (plus MODS/CONTROLS)") @@ -320,35 +321,36 @@ check(om.game.save.options.musicVol == 6, "music volume steps down") for _ = 1, 10 do om.rows[6].step(om.game, -1) end check(om.game.save.options.musicVol == 0, "music volume clamps at 0") --- ZOOM / VOID FILL rows +-- ZOOM / VOID FILL rows (indices shifted +1 by the PERFORMANCE row spliced +-- in ahead of COLORS) local Zoom = require("src.render.Zoom") local TileRenderer = require("src.render.TileRenderer") om.game.save.options.zoom = 0 Zoom.offset = 0 -check(om.rows[12].value(om.game) == "FIT", "ZOOM row shows FIT at offset 0") -om.rows[12].step(om.game, 1) +check(om.rows[13].value(om.game) == "FIT", "ZOOM row shows FIT at offset 0") +om.rows[13].step(om.game, 1) check(om.game.save.options.zoom == 1 and Zoom.offset == 1, "ZOOM row steps to IN1") -om.rows[13].step(om.game, 1) +om.rows[14].step(om.game, 1) check(om.game.save.options.voidFill == "water" and TileRenderer.voidFill == "water", "VOID FILL row cycles TREES → WATER") -om.rows[13].step(om.game, 1) +om.rows[14].step(om.game, 1) check(om.game.save.options.voidFill == "black", "VOID FILL steps to BLACK") -om.rows[13].step(om.game, 1) +om.rows[14].step(om.game, 1) check(om.game.save.options.voidFill == "trees", "VOID FILL wraps to TREES") -- the MAX FPS row cycles the render-cap steps and shows the value plain om.game.save.options.fpsCap = nil -check(om.rows[15].value(om.game) == "60", +check(om.rows[16].value(om.game) == "60", "MAX FPS row defaults to 60 with no saved cap") -om.rows[15].step(om.game, 1) +om.rows[16].step(om.game, 1) check(om.game.save.options.fpsCap == 75, "MAX FPS steps up from 60 to 75") -check(om.rows[15].value(om.game) == "75", "the MAX FPS row renders the cap") +check(om.rows[16].value(om.game) == "75", "the MAX FPS row renders the cap") om.game.save.options.fpsCap = 160 -om.rows[15].step(om.game, 1) +om.rows[16].step(om.game, 1) check(om.game.save.options.fpsCap == 30, "MAX FPS wraps past the ceiling to 30") -om.rows[15].step(om.game, -1) +om.rows[16].step(om.game, -1) check(om.game.save.options.fpsCap == 160, "MAX FPS wraps back down to the ceiling") -- ------- FrameCap normalize / cycle (issue #88) @@ -378,7 +380,7 @@ check(FrameCap.current == 60, "FrameCap.applyOptions defaults a missing key to 6 -- the MODS row is the manager's discoverable home local mgGame = optGame() om = OptionsMenu.new(mgGame) -om.rows[17].activate(mgGame) +om.rows[18].activate(mgGame) check(getmetatable(mgGame.stack:top()) == ManagerState, "the MODS row opens the manager") check(mgGame.stack:top().screenId == "ManagerState", @@ -388,7 +390,7 @@ check(mgGame.stack:top().screenId == "ManagerState", local BindingsMenu = require("src.ui.BindingsMenu") local cbGame = optGame() om = OptionsMenu.new(cbGame) -om.rows[18].activate(cbGame) +om.rows[19].activate(cbGame) local bm = cbGame.stack:top() check(getmetatable(bm) == BindingsMenu, "the CONTROLS row opens the rebind list")