mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-17 11:11:10 +02:00
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:
+27
-4
@@ -3,6 +3,7 @@
|
||||
local Font = require("src.render.Font")
|
||||
local Theme = require("src.ui.Theme")
|
||||
local Strings = require("src.core.Strings")
|
||||
local Timing = require("src.core.Timing")
|
||||
|
||||
local ChoiceBox = {}
|
||||
ChoiceBox.__index = ChoiceBox
|
||||
@@ -25,6 +26,19 @@ end
|
||||
|
||||
function ChoiceBox:update(dt)
|
||||
local input = self.game.input
|
||||
-- Both branches of DisplayTwoOptionMenu hold 15 frames with the menu still
|
||||
-- on screen before TwoOptionMenu_RestoreScreenTiles hands control back
|
||||
-- (engine/menus/text_box.asm:322-323, :333-334).
|
||||
if self.pending ~= nil then
|
||||
self.holdFrames = self.holdFrames - 1
|
||||
if self.holdFrames <= 0 then
|
||||
local yes = self.pending
|
||||
self.pending = nil
|
||||
self.game.stack:pop()
|
||||
self.onChoose(yes)
|
||||
end
|
||||
return
|
||||
end
|
||||
if input:wasPressed("up") or input:wasPressed("down") then
|
||||
self.index = self.index == 1 and 2 or 1
|
||||
elseif input:wasPressed("a") then
|
||||
@@ -32,19 +46,28 @@ function ChoiceBox:update(dt)
|
||||
if not self.noSound then
|
||||
require("src.core.Sound").play(self.game.data, "Press_AB")
|
||||
end
|
||||
self.game.stack:pop()
|
||||
self.onChoose(self.index == 1)
|
||||
self.pending = (self.index == 1)
|
||||
self.holdFrames = Timing.YES_NO_ANSWER
|
||||
elseif input:wasPressed("b") then
|
||||
if not self.noSound then
|
||||
require("src.core.Sound").play(self.game.data, "Press_AB")
|
||||
end
|
||||
self.game.stack:pop()
|
||||
self.onChoose(false)
|
||||
-- .choseSecondMenuItem writes wCurrentMenuItem = 1 before the hold, so
|
||||
-- the cursor visibly snaps to NO for those 15 frames
|
||||
self.index = 2
|
||||
self.pending = false
|
||||
self.holdFrames = Timing.YES_NO_ANSWER
|
||||
end
|
||||
end
|
||||
|
||||
function ChoiceBox:draw()
|
||||
local tx, ty, tw, th = self.tx, self.ty, self.tw, self.th
|
||||
-- rides the same bottom anchor as the dialogue box it sits above, so the
|
||||
-- pair travels together (the anchor keeps each element's gap from the edge)
|
||||
local r = self.game and self.game.renderer
|
||||
if r and r.setUIAnchor then
|
||||
r:setUIAnchor(tx * 8, ty * 8, tw * 8, th * 8, "bottom")
|
||||
end
|
||||
Font.drawBox(tx, ty, tw, th)
|
||||
love.graphics.setColor(0, 0, 0, 1)
|
||||
Font.draw(Strings("YES"), (tx + 2) * 8, (ty + 1) * 8)
|
||||
|
||||
@@ -33,6 +33,10 @@ local IntroMovie = {}
|
||||
IntroMovie.__index = IntroMovie
|
||||
IntroMovie.isOpaque = true
|
||||
|
||||
-- Same as the title screen: full-bleed art, no world behind it, no player zoom
|
||||
-- to respect, so fill the window rather than sit at the fixed integer scale.
|
||||
function IntroMovie:wantsFillScale() return true end
|
||||
|
||||
-- SGB intro palettes: the splash uses PalPacket_GameFreakIntro (logo
|
||||
-- GAMEFREAK, falling star columns RED/VIRIDIAN/BLUEMON), the attract
|
||||
-- fight PalPacket_NidorinoIntro (PURPLEMON letterbox, BLACK bars)
|
||||
|
||||
@@ -50,6 +50,9 @@ function Menu.new(game, items, opts)
|
||||
-- only menus whose real mask includes PAD_START -- the start menu
|
||||
-- (engine/menus/draw_start_menu.asm) -- opt in here.
|
||||
self.startCloses = opts.startCloses or false
|
||||
-- screen-edge anchor for this menu (see Menu:draw); nil keeps it in the
|
||||
-- classic centred letterbox
|
||||
self.anchor = opts.anchor
|
||||
self.onCancel = opts.onCancel
|
||||
-- BIT_NO_MENU_BUTTON_SOUND (wMiscFlags): the PC session runs its
|
||||
-- menus silent (home/window.asm HandleMenuInput_)
|
||||
@@ -104,6 +107,14 @@ function Menu:update(dt)
|
||||
end
|
||||
|
||||
function Menu:draw()
|
||||
-- opts.anchor opts a menu out of the centred letterbox and onto a screen
|
||||
-- edge (the START menu asks for "topright"). Only menus that ask for it
|
||||
-- move; every other menu is placed exactly as before.
|
||||
local r = self.anchor and self.game and self.game.renderer
|
||||
if r and r.setUIAnchor then
|
||||
r:setUIAnchor(self.tx * 8, self.ty * 8,
|
||||
self.tw * 8, self.th * 8, self.anchor)
|
||||
end
|
||||
Font.drawBox(self.tx, self.ty, self.tw, self.th)
|
||||
love.graphics.setColor(0, 0, 0, 1)
|
||||
local visible = (self.maxVisible and math.min(self.maxVisible, #self.items))
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -133,7 +133,12 @@ function StartMenu.new(game)
|
||||
local rowStep = 2
|
||||
local maxVisible = math.floor((Renderer.HEIGHT / 8 - 2) / rowStep)
|
||||
local menu = Menu.new(game, items,
|
||||
{ tx = 9, ty = 0, tw = 11, maxVisible = maxVisible, startCloses = true })
|
||||
-- the START menu hugs the top-right corner of the SCREEN, not of a
|
||||
-- centred letterbox: at 9,0 x 11 it is already flush with the top and
|
||||
-- right of the 20x18 grid, so the anchor keeps it flush when the view
|
||||
-- is zoomed out and the letterbox no longer fills the window
|
||||
{ tx = 9, ty = 0, tw = 11, maxVisible = maxVisible, startCloses = true,
|
||||
anchor = "topright" })
|
||||
-- the cursor position survives closing the menu
|
||||
-- (wBattleAndStartSavedMenuItem, home/start_menu.asm)
|
||||
menu.index = math.min(game.save.startMenuIndex or 1, #items)
|
||||
|
||||
@@ -14,6 +14,12 @@ local TitleState = {}
|
||||
TitleState.__index = TitleState
|
||||
TitleState.isOpaque = true
|
||||
|
||||
-- Fill the window (aspect preserved, bars on the long axis) instead of sitting
|
||||
-- at the fixed integer scale. The title screen is a full-bleed picture with no
|
||||
-- world behind it, so a small centred box in a large window is just wasted
|
||||
-- glass -- and unlike the overworld it has no zoom the player chose to respect.
|
||||
function TitleState:wantsFillScale() return true end
|
||||
|
||||
-- SGB title zones (PalPacket_Titlescreen): the logo rows get LOGO2,
|
||||
-- the version-ribbon band LOGO1, the rest MEWMON.
|
||||
--
|
||||
|
||||
Reference in New Issue
Block a user