Port timing/parity fixes, seamless battle transitions, faithful-res lock, and zoom-aware UI anchoring

Ports from a downstream fork, hand-surgered hunk-by-hunk to exclude the
fork's randomizer/pokescript work and to skip a FixedStep jitter-tolerance
attempt that never fixed the stutter it targeted.

- src/core/Timing.lua: hardware-accurate frame-delay catalog ported from
  pret/pokered, feeding BattleState:waitNext, EffectRegistry's miss/crit
  beats, TextBox/ChoiceBox scroll and prompt holds, and the battle
  silhouette slide/shake/blink/faint timings.
- Seamless battle transitions: Renderer:drawBattleWipe replaces the old
  160x144-only cascade with one wipe drawn over the whole surface at any
  zoom or window size; BattleTransition's per-style frame lengths are
  corrected against pokered-c's derivation; Transition.battleReturn adds
  the post-battle GBFadeInFromWhite the port never had.
- BATTLE SIZE / BATTLE BG options (BattleState:wantsFillScale/bgMode,
  Game.fillScaleInStack/worldBgBattleDim): battle surface can fill the
  window instead of the fixed integer letterbox, and the area around it
  can show white/black/the dimmed overworld instead of only white.
- src/core/FaithfulRes.lua: locks the window to an exact 160x144 multiple.
- Zoom-aware UI anchoring: Renderer:uiScale steps the UI down with survey
  zoom (gated to worldActive so the title/intro never shrink);
  Renderer:setUIAnchor lets TextBox, ChoiceBox, and an opted-in Menu
  (the START menu) pin themselves to a screen edge instead of the
  zoomed-out letterbox.
This commit is contained in:
spiritsnails
2026-08-01 15:52:34 -06:00
parent 5e89e35e02
commit aedc63c40d
23 changed files with 2515 additions and 114 deletions
+49
View File
@@ -19,6 +19,7 @@ local TileRenderer = require("src.render.TileRenderer")
local GameSpeed = require("src.core.GameSpeed")
local GameVersion = require("src.core.GameVersion")
local VideoMode = require("src.core.VideoMode")
local FaithfulRes = require("src.core.FaithfulRes")
local FrameCap = require("src.core.FrameCap")
local Performance = require("src.core.Performance")
local Logger = require("src.core.Logger")
@@ -156,6 +157,40 @@ local function buildRows(game)
o.battleLayout = o.battleLayout == "wide" and "og" or "wide"
return true
end },
-- FIXED keeps the classic integer-scaled letterbox -- a GB pixel is a
-- whole number of screen pixels and the battle is the same size at any
-- zoom. FILL scales the battle surface to the window so it fills
-- vertically; that needs a fractional scale, so pixels stop being evenly
-- sized. Battle only: the overworld is untouched either way.
{ id = "battleFit", label = Strings("BATTLE SIZE"),
value = function(g)
return g.save.options.battleFit == "fill" and Strings("FILL")
or Strings("FIXED")
end,
step = function(g)
local o = g.save.options
o.battleFit = o.battleFit == "fill" and "fixed" or "fill"
return true
end },
-- What sits behind and around the battle. WHITE is the classic paper
-- field; BLACK swaps it for black bars; WORLD leaves the frozen overworld
-- visible underneath, dimmed (the battle stops being opaque, so the map
-- shows through everywhere the battle does not paint).
{ id = "battleBg", label = Strings("BATTLE BG"),
value = function(g)
local m = g.save.options.battleBg
if m == "black" then return Strings("BLACK") end
if m == "world" then return Strings("WORLD") end
return Strings("WHITE")
end,
step = function(g, dir)
local o = g.save.options
local order = { "white", "black", "world" }
local cur = 1
for i, m in ipairs(order) do if o.battleBg == m then cur = i break end end
o.battleBg = order[(cur - 1 + (dir or 1)) % #order + 1]
return true
end },
{ id = "ruleset", label = Strings("RULESET"),
value = function(g) return rulesetName(g) end,
step = function(g, dir)
@@ -298,6 +333,20 @@ local function buildRows(game)
VideoMode.apply(o.videoMode)
return true
end },
-- Lock the window to an exact 160x144 multiple, so the surface IS the
-- Game Boy screen with no letterbox at all. Sits next to VIDEO MODE
-- because it overrides it: holding an exact size means dropping
-- fullscreen.
{ id = "faithfulRes", label = Strings("FAITHFUL RES"),
value = function(g)
return FaithfulRes.label(g.save.options.faithfulRes)
end,
step = function(g, dir)
local o = g.save.options
o.faithfulRes = FaithfulRes.cycle(o.faithfulRes, dir)
FaithfulRes.apply(o.faithfulRes)
return true
end },
-- hard render cap (issue #88): bounds the present rate so a
-- driver-forced vsync-off run cannot spin at thousands of FPS. Logic
-- is fixed-step off dt, so this touches presentation only.