mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 08:21:02 +02:00
c8e035d332
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q6bFAiQyZ5jDmewsbB4LG9
114 lines
3.7 KiB
Lua
114 lines
3.7 KiB
Lua
-- Overworld survey zoom: integer pixels-per-world-pixel scales stepped
|
||
-- by the mouse wheel, Options ZOOM row, or hotkey `4`. Stored as an
|
||
-- offset from the window fit scale S so a resize keeps the relative
|
||
-- zoom. Persisted as save.options.zoom (default 0 = FIT).
|
||
-- Spec: docs/new-features.md (survey zoom)
|
||
|
||
local Runtime = require("src.mods.Runtime")
|
||
|
||
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)
|
||
S = math.max(1, math.floor(tonumber(S) or 1))
|
||
local lo, hi = 1 - S, S
|
||
if Runtime.wantsHook("zoom.range") then
|
||
lo, hi = Runtime.call("zoom.range", function(a, b) return a, b end, lo, hi, S)
|
||
lo = math.floor(tonumber(lo) or (1 - 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
|
||
|
||
-- effective scale s' = S + offset, clamped to the (possibly modded) range.
|
||
-- Vanilla stays in [1, 2*S]. A zoom.range wrapper that lowers `lo` below
|
||
-- 1-S permits sub-1 survey scales so the whole region can fit on screen.
|
||
function Zoom.scale(S)
|
||
local lo, hi = Zoom.offsetRange(S)
|
||
local s = S + Zoom.offset
|
||
local minScale = S + lo
|
||
local maxScale = math.max(minScale, S + hi)
|
||
if s < minScale then s = minScale end
|
||
if s > maxScale then s = maxScale end
|
||
if s < 0.25 then s = 0.25 end
|
||
return s
|
||
end
|
||
|
||
function Zoom.clampOffset(offset, S)
|
||
local lo, hi = Zoom.offsetRange(S)
|
||
offset = math.floor(tonumber(offset) or 0)
|
||
if offset < lo then return lo end
|
||
if offset > hi then return hi end
|
||
return offset
|
||
end
|
||
|
||
function Zoom.step(delta, S)
|
||
Zoom.offset = Zoom.clampOffset(Zoom.offset + delta, S)
|
||
return Zoom.offset
|
||
end
|
||
|
||
-- Advance one zoom level toward max close-up, then wrap to full survey.
|
||
-- Returns the new offset.
|
||
function Zoom.cycle(S)
|
||
local lo, hi = Zoom.offsetRange(S)
|
||
local next = Zoom.offset + 1
|
||
if next > hi then next = lo end
|
||
Zoom.offset = next
|
||
return Zoom.offset
|
||
end
|
||
|
||
function Zoom.reset()
|
||
Zoom.offset = 0
|
||
end
|
||
|
||
function Zoom.applyOptions(opts)
|
||
Zoom.offset = math.floor(tonumber(opts and opts.zoom) or 0)
|
||
end
|
||
|
||
-- FIT / OUT1 / OUT2 / … / IN1 / IN2 / …
|
||
function Zoom.offsetLabel(offset)
|
||
offset = math.floor(tonumber(offset) or 0)
|
||
if offset == 0 then return "FIT" end
|
||
if offset < 0 then return "OUT" .. tostring(-offset) end
|
||
return "IN" .. tostring(offset)
|
||
end
|
||
|
||
-- world pixels covered by a w x h letterbox viewport at fit scale S
|
||
-- (legacy GB-framed size; prefer fillViewSize for the live world pass)
|
||
function Zoom.viewSize(S, w, h)
|
||
local s = Zoom.scale(S)
|
||
return math.ceil(w * S / s), math.ceil(h * S / s)
|
||
end
|
||
|
||
-- world pixels needed to fill a ww x wh window at the current zoom scale
|
||
-- (fills letterbox "black voids" with more map, phones, tall windows)
|
||
function Zoom.fillViewSize(s, ww, wh)
|
||
return math.ceil(ww / s), math.ceil(wh / s)
|
||
end
|
||
|
||
-- zoom input is honored only while free-roaming the overworld
|
||
function Zoom.gateOK(top, overworld)
|
||
if top == nil or top ~= overworld then return false end
|
||
if top.transitioning then return false end
|
||
if top.runner and top.runner.isRunning and top.runner:isRunning() then
|
||
return false
|
||
end
|
||
return true
|
||
end
|
||
|
||
return Zoom
|