Merge pull request #412 from johnjohto/fix-wide-option-battle-screens

Keep wide layout through battle menus
This commit is contained in:
bryanthaboi
2026-07-29 13:26:54 -04:00
committed by GitHub
3 changed files with 82 additions and 15 deletions
+9 -8
View File
@@ -38,15 +38,16 @@ BattleState.letterboxWhite = true
-- BATTLE LAYOUT: the classic 160x144 arrangement, or the widescreen one on -- BATTLE LAYOUT: the classic 160x144 arrangement, or the widescreen one on
-- a 304x144 surface (src/battle/WideBattle.lua). Only the composition -- a 304x144 surface (src/battle/WideBattle.lua). Only the composition
-- differs; every battler, queue and animation below is shared. The wide -- differs; every battler, queue and animation below is shared. Menus and
-- layout is live only while this battle is the state being drawn on top -- -- prompts pushed during a wide battle keep its wide canvas, while drawing
-- a party menu or bag pushed over it is a 160x144 screen, so the surface -- their classic 160px UI centred within it (Game:draw).
-- goes back with it and the battle underneath is not drawn at all. function BattleState:isWideBattleLayout()
function BattleState:wideLayout()
local options = self.game and self.game.save and self.game.save.options local options = self.game and self.game.save and self.game.save.options
if not options or options.battleLayout ~= "wide" then return false end return options and options.battleLayout == "wide" or false
local stack = self.game.stack end
return (stack and stack.top and stack:top()) == self
function BattleState:wideLayout()
return self:isWideBattleLayout()
end end
-- Renderer:setUISize asks the top state for its surface before anything draws -- Renderer:setUISize asks the top state for its surface before anything draws
+65 -7
View File
@@ -244,35 +244,93 @@ end
-- exactly as the owning state computed it -- exactly as the owning state computed it
local function sameZones(_, zones) return zones end local function sameZones(_, zones) return zones end
-- A wide battle owns the surface until it leaves the stack. The party,
-- bag, choice and text states it opens still draw their original 160px UI,
-- but the canvas must not snap to 160px between those states.
function Game.wideBattleInStack(stack)
for i = #(stack and stack.states or {}), 1, -1 do
local state = stack.states[i]
if state and state.isWideBattleLayout and state:isWideBattleLayout() then
return state
end
end
return nil
end
-- Shift classic SGB zones to the centred UI. A full-width base zone extends
-- into both margins, keeping the canvas' paper color continuous; narrower
-- sprite and status zones move with the classic UI content.
local function centerClassicZones(zones, offset)
if not zones or offset == 0 then return zones end
local shifted = {}
for i, zone in ipairs(zones) do
local copy = {}
for key, value in pairs(zone) do copy[key] = value end
if copy.x == 0 and copy.w == Renderer.WIDTH then
copy.w = copy.w + offset * 2
else
copy.x = (copy.x or 0) + offset
end
shifted[i] = copy
end
return shifted
end
function Game:draw() function Game:draw()
-- the UI canvas clears transparent when the overworld's world pass -- the UI canvas clears transparent when the overworld's world pass
-- shows through beneath it; opaque full-screen states get the classic -- shows through beneath it; opaque full-screen states get the classic
-- white clear -- white clear
local base = self.stack:visibleBase() local base = self.stack:visibleBase()
local worldBelow = self.stack.states[base] == self.overworld local worldBelow = self.stack.states[base] == self.overworld
-- The UI surface is resolved once, before any state draws: the top state -- A wide battle holds its 304px surface through every menu or prompt it
-- may want more than the Game Boy's 160x144 (the widescreen battle layout -- opens. States that do not draw the wide battle composition are centred
-- asks for 304x144). Anything else keeps the classic surface, so a menu -- in that surface below, so their classic coordinates and hit testing stay
-- pushed over a wide battle brings the screen straight back to 160x144. -- unchanged. Outside a battle, including the title screen, the option is
-- intentionally inactive because it is a battle-layout setting.
local top = self.stack:top() local top = self.stack:top()
if top and top.uiSize then local wideBattle = Game.wideBattleInStack(self.stack)
local classicOffset = 0
if wideBattle and wideBattle.uiSize then
Renderer:setUISize(wideBattle:uiSize())
classicOffset = math.floor((select(1, Renderer:uiSize()) - Renderer.WIDTH) / 2)
elseif top and top.uiSize then
Renderer:setUISize(top:uiSize()) Renderer:setUISize(top:uiSize())
else else
Renderer:setUISize(Renderer.WIDTH, Renderer.HEIGHT) Renderer:setUISize(Renderer.WIDTH, Renderer.HEIGHT)
end end
Renderer:beginFrame(worldBelow) Renderer:beginFrame(worldBelow)
self.stack:draw() for i = self.stack:visibleBase(), #self.stack.states do
local state = self.stack.states[i]
local wideState = state and state.isWideBattleLayout
and state:isWideBattleLayout()
if state and state.draw then
if classicOffset ~= 0 and not wideState then
love.graphics.push()
love.graphics.translate(classicOffset, 0)
state:draw()
love.graphics.pop()
else
state:draw()
end
end
end
-- SGB colorization: the topmost state that knows its palette owns the -- SGB colorization: the topmost state that knows its palette owns the
-- screen (overlays like text boxes inherit from what's beneath them); -- screen (overlays like text boxes inherit from what's beneath them);
-- the overworld's world pass colors each visible map area separately -- the overworld's world pass colors each visible map area separately
local zones, worldZones local zones, worldZones, zoneOwner
for i = #self.stack.states, 1, -1 do for i = #self.stack.states, 1, -1 do
local s = self.stack.states[i] local s = self.stack.states[i]
if s.sgbPalettes then if s.sgbPalettes then
zones = s:sgbPalettes(self) zones = s:sgbPalettes(self)
zoneOwner = s
break break
end end
end end
if classicOffset ~= 0 and zoneOwner
and not (zoneOwner.isWideBattleLayout
and zoneOwner:isWideBattleLayout()) then
zones = centerClassicZones(zones, classicOffset)
end
-- 14's render.zones: weather/lighting overlays and custom colorization -- 14's render.zones: weather/lighting overlays and custom colorization
-- recolor or add zones before the blit -- recolor or add zones before the blit
if ModRuntime.wantsHook("render.zones") then if ModRuntime.wantsHook("render.zones") then
+8
View File
@@ -6,12 +6,20 @@ package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.modkit") local T = require("tests.modkit")
local WideBattle = require("src.battle.WideBattle") local WideBattle = require("src.battle.WideBattle")
local Renderer = require("src.render.Renderer") local Renderer = require("src.render.Renderer")
local Game = require("src.core.Game")
T.eq(WideBattle.WIDTH, 304, "the wide layout runs on a 304px native surface") T.eq(WideBattle.WIDTH, 304, "the wide layout runs on a 304px native surface")
T.eq(WideBattle.HEIGHT, 144, "the wide surface keeps the native height") T.eq(WideBattle.HEIGHT, 144, "the wide surface keeps the native height")
T.eq(WideBattle.FIELD_BOTTOM, 104, T.eq(WideBattle.FIELD_BOTTOM, 104,
"the lower 40 rows are the message / command windows") "the lower 40 rows are the message / command windows")
local wide = { isWideBattleLayout = function() return true end }
local normal = { isWideBattleLayout = function() return false end }
T.eq(Game.wideBattleInStack({ states = { normal, wide, normal } }), wide,
"a wide battle remains the surface owner under a classic overlay")
T.eq(Game.wideBattleInStack({ states = { normal } }), nil,
"a classic stack keeps the normal surface")
-- move grid: slots are laid out 1 2 / 3 4 -- move grid: slots are laid out 1 2 / 3 4
T.eq(WideBattle.moveGridIndex(1, 4, "right"), 2, "RIGHT crosses the row") T.eq(WideBattle.moveGridIndex(1, 4, "right"), 2, "RIGHT crosses the row")
T.eq(WideBattle.moveGridIndex(2, 4, "left"), 1, "LEFT crosses the row") T.eq(WideBattle.moveGridIndex(2, 4, "left"), 1, "LEFT crosses the row")