Merge pull request #1378 from syybott/experiment/fixed-extended-world-alignment

This commit is contained in:
bryanthaboi
2026-08-16 22:04:28 -04:00
committed by GitHub
16 changed files with 726 additions and 33 deletions
+27
View File
@@ -88,6 +88,33 @@ function BattleState:wantsFillScale()
return options and options.battleFit == "fill" or false return options and options.battleFit == "fill" or false
end end
-- EXTENDED HUD configurations are admitted one at a time after their own
-- placement and screenshot review. FIXED supports the three authored battle
-- backgrounds; FILL uses one adaptive presentation stored as WHITE: stock
-- battles retain the paper field required by Gen 1 back sprites, while arena
-- providers may replace it with their own scene. Only the HUD moves to window
-- space.
function BattleState:extendedHUD()
local options = self.game and self.game.save and self.game.save.options
local bg = options and options.battleBg
return self:wideLayout()
and options and options.battleHud == "extended"
and ((options.battleFit == "fixed"
and (bg == "world" or bg == "white" or bg == "black"))
or (options.battleFit == "fill" and bg == "white"))
end
function BattleState:extendedWorldHUD()
local options = self.game and self.game.save and self.game.save.options
return self:extendedHUD() and options
and options.battleFit == "fixed" and options.battleBg == "world"
end
function BattleState:extendedBlackHUD()
local options = self.game and self.game.save and self.game.save.options
return self:extendedHUD() and options and options.battleBg == "black"
end
-- BATTLE BG: what fills the screen AROUND the battle -- the letterbox voids -- BATTLE BG: what fills the screen AROUND the battle -- the letterbox voids
-- that grow as the window gets bigger or the view is zoomed out. The battle -- that grow as the window gets bigger or the view is zoomed out. The battle
-- screen itself is untouched: it keeps its white paper field in every mode. -- screen itself is untouched: it keeps its white paper field in every mode.
+51 -8
View File
@@ -92,6 +92,26 @@ local function levelAt(battle, battler, x, y)
end end
end end
local function battleIsTopState(battle)
local stack = battle.game and battle.game.stack
return not (stack and stack.top) or stack:top() == battle
end
local function anchorHUD(battle, x, y, w, h, anchor)
if not battle:extendedHUD() or not battleIsTopState(battle) then return end
local renderer = battle.game and battle.game.renderer
if not (renderer and renderer.setBattleUIAnchor) then return end
x = x + (battle.extendedHUDOffsetX or 0)
y = y + (battle.extendedHUDOffsetY or 0)
local x2 = math.min(WideBattle.WIDTH, x + w)
local y2 = math.min(WideBattle.HEIGHT, y + h)
x, y = math.max(0, x), math.max(0, y)
w, h = x2 - x, y2 - y
if w > 0 and h > 0 then
renderer:setBattleUIAnchor(x, y, w, h, anchor)
end
end
-- One side's status box: name and level on the first line, a long HP bar -- One side's status box: name and level on the first line, a long HP bar
-- under it, and the numeric HP on the player's box only (the foe's exact -- under it, and the numeric HP on the player's box only (the foe's exact
-- HP is never shown, like the original). -- HP is never shown, like the original).
@@ -114,6 +134,7 @@ local function drawStatusPanel(battle, battler, x, y, player)
Font.draw(("%3d/%3d"):format(shownHP(battler), battler.mon.stats.hp), Font.draw(("%3d/%3d"):format(shownHP(battler), battler.mon.stats.hp),
x + tw * 8 - 64, y + 24) x + tw * 8 - 64, y + 24)
end end
anchorHUD(battle, x, y, tw * 8, th * 8, player and "bottom" or "top")
end end
-- the party ball rows DrawAllPokeballs puts up with the intro text, moved -- the party ball rows DrawAllPokeballs puts up with the intro text, moved
@@ -144,7 +165,6 @@ local function drawHUDs(battle, slide)
and not battle.showPlayerBack and slide == 0 then and not battle.showPlayerBack and slide == 0 then
drawStatusPanel(battle, battle.player, 184, 56, true) drawStatusPanel(battle, battle.player, 184, 56, true)
end end
drawIntroBalls(battle)
end end
local function drawMessageBox(battle) local function drawMessageBox(battle)
@@ -268,6 +288,8 @@ local function drawTextArea(battle)
else else
Font.drawBox(0, 13, 38, 5) Font.drawBox(0, 13, 38, 5)
end end
anchorHUD(battle, 0, WideBattle.FIELD_BOTTOM,
WideBattle.WIDTH, WideBattle.HEIGHT - WideBattle.FIELD_BOTTOM, "bottom")
end end
-- Battle animations are authored in the original 160px coordinate space. -- Battle animations are authored in the original 160px coordinate space.
@@ -309,17 +331,23 @@ end
-- The whole 304x144 composition for one frame. -- The whole 304x144 composition for one frame.
function WideBattle.draw(battle) function WideBattle.draw(battle)
local g = love.graphics local g = love.graphics
local renderer = battle.game and battle.game.renderer
local extendedHUD = battle:extendedHUD() and renderer
and renderer.beginBattleHUDPass
and renderer.endBattleHUDPass
-- The field is the display mode's paper. Under a forced-mono mode the -- The field is the display mode's paper. Under a forced-mono mode the
-- whole surface is remapped downstream (WideBattle.zones), so the field -- whole surface is remapped downstream (WideBattle.zones), so the field
-- goes down as DMG white and comes out of that pass as the mode's paper; -- goes down as DMG white and comes out of that pass as the mode's paper;
-- painting the resolved shade there would run it through the remap twice -- painting the resolved shade there would run it through the remap twice
-- and land a shade off the letterbox the renderer fills around it. -- and land a shade off the letterbox the renderer fills around it.
if monoMode() then if not (extendedHUD and battle:extendedWorldHUD()) then
g.setColor(1, 1, 1, 1) if monoMode() then
else g.setColor(1, 1, 1, 1)
g.setColor(PaletteFX.paperShade(battle.data)) else
g.setColor(PaletteFX.paperShade(battle.data))
end
g.rectangle("fill", 0, 0, WideBattle.WIDTH, WideBattle.HEIGHT)
end end
g.rectangle("fill", 0, 0, WideBattle.WIDTH, WideBattle.HEIGHT)
-- AskName clears the field the same way the classic layout does -- AskName clears the field the same way the classic layout does
if battle.blankForAskName then return end if battle.blankForAskName then return end
@@ -346,6 +374,7 @@ function WideBattle.draw(battle)
inRegion(160 + sx, sy, 144, WideBattle.FIELD_BOTTOM, 136 + sx, sy, inRegion(160 + sx, sy, 144, WideBattle.FIELD_BOTTOM, 136 + sx, sy,
function() battle:drawPicsLayer(slide, 0, 0, "enemy", true) end) function() battle:drawPicsLayer(slide, 0, 0, "enemy", true) end)
battle.wideRegion = nil battle.wideRegion = nil
drawIntroBalls(battle)
-- A battle sets rWY to 0 (engine/battle/core.asm), so the window the -- A battle sets rWY to 0 (engine/battle/core.asm), so the window the
-- shakes move IS the whole screen: PredefShakeScreenHorizontally, -- shakes move IS the whole screen: PredefShakeScreenHorizontally,
@@ -359,12 +388,26 @@ function WideBattle.draw(battle)
if sx == 0 and sy == 0 then return fn() end if sx == 0 and sy == 0 then return fn() end
g.push() g.push()
g.translate(sx, sy) g.translate(sx, sy)
battle.extendedHUDOffsetX, battle.extendedHUDOffsetY = sx, sy
fn() fn()
battle.extendedHUDOffsetX, battle.extendedHUDOffsetY = nil, nil
g.pop() g.pop()
end end
shaken(function() drawHUDs(battle, slide) end)
drawAnimationLayer(battle) drawAnimationLayer(battle)
shaken(function() drawTextArea(battle) end)
if extendedHUD then
local previous = renderer:beginBattleHUDPass()
shaken(function() drawHUDs(battle, slide) end)
shaken(function() drawTextArea(battle) end)
if fx and fx.flash and fx.flash > 0 and battle.frame % 4 < 2 then
g.setColor(1, 1, 1, 0.85)
g.rectangle("fill", 0, 0, WideBattle.WIDTH, WideBattle.HEIGHT)
end
renderer:endBattleHUDPass(previous)
else
shaken(function() drawHUDs(battle, slide) end)
shaken(function() drawTextArea(battle) end)
end
if fx and fx.flash and fx.flash > 0 and battle.frame % 4 < 2 then if fx and fx.flash and fx.flash > 0 and battle.frame % 4 < 2 then
g.setColor(1, 1, 1, 0.85) g.setColor(1, 1, 1, 0.85)
+44 -10
View File
@@ -306,12 +306,32 @@ function Game.worldBgBattleDim(stack)
for i = #(stack and stack.states or {}), 1, -1 do for i = #(stack and stack.states or {}), 1, -1 do
local state = stack.states[i] local state = stack.states[i]
if state and state.bgMode and state:bgMode() == "world" then if state and state.bgMode and state:bgMode() == "world" then
-- The extended fixed HUD intentionally exposes the live world across
-- the whole physical window. Keep this as a world-backed battle (zero
-- is non-nil, so scaling and overlay holds remain active), but do not
-- paint the standard dim veil around the native battle rectangle.
if state.extendedWorldHUD and state:extendedWorldHUD() then
return 0
end
return state.BG_WORLD_DIM or 0.55 return state.BG_WORLD_DIM or 0.55
end end
end end
return nil return nil
end end
-- Does the stack contain the opt-in fixed Extended WORLD battle? Renderer
-- uses this separately from battleDim: the world remains the surround, while
-- the native-width battle field receives a paper backing from top to bottom.
function Game.extendedWorldHUDInStack(stack)
for i = #(stack and stack.states or {}), 1, -1 do
local state = stack.states[i]
if state and state.extendedWorldHUD and state:extendedWorldHUD() then
return true
end
end
return false
end
-- Is a BATTLE BG "world" battle composing itself over the live map right now? -- Is a BATTLE BG "world" battle composing itself over the live map right now?
-- Same whole-stack walk as worldBgBattleDim, asked for a different reason: the -- Same whole-stack walk as worldBgBattleDim, asked for a different reason: the
-- dark-cave shade shift (wMapPalOffset) must not reach a frame a battle is -- dark-cave shade shift (wMapPalOffset) must not reach a frame a battle is
@@ -404,13 +424,13 @@ function Game.uiAnchorsHeldInStack(stack)
end end
-- Where Game:draw starts drawing this frame. Normally the topmost opaque -- Where Game:draw starts drawing this frame. Normally the topmost opaque
-- state (StateStack:visibleBase) -- but BATTLE BG "world" composes the battle -- state (StateStack:visibleBase) -- but an opaque menu pushed over a WIDE
-- over the LIVE map, and an opaque state pushed on top of it (the party menu, -- battle must not prevent that battle from drawing. Native WIDE battles own
-- the bag) becomes that base, cutting the overworld -- and with it the world -- the 304x144 surround around a centred classic menu, while external arena
-- pass -- out of the frame entirely. The backdrop the battle established -- providers establish their window-sized scene from BattleState:draw. If the
-- then collapses to endFrame's flat black clear for as long as the menu is -- menu becomes the draw base, neither owner runs and the menu's white field
-- up. So a world-bg battle keeps the frame starting from underneath itself -- replaces the whole presentation. BATTLE BG "world" additionally needs the
-- until it leaves the stack, the same hold uiFill and the dim already use. -- overworld below the battle, as before.
-- --
-- Only the START of the draw moves. The clear stays keyed to the real -- Only the START of the draw moves. The clear stays keyed to the real
-- visibleBase, so the menu still gets its opaque canvas and draws exactly as -- visibleBase, so the menu still gets its opaque canvas and draws exactly as
@@ -421,9 +441,13 @@ function Game.drawBaseInStack(stack, visibleBase)
local states = stack and stack.states or {} local states = stack and stack.states or {}
for i = visibleBase - 1, 1, -1 do for i = visibleBase - 1, 1, -1 do
local state = states[i] local state = states[i]
if state and state.bgMode and state:bgMode() == "world" then local worldBattle = state and state.bgMode and state:bgMode() == "world"
local wideBattle = state and state.isWideBattleLayout
and state:isWideBattleLayout()
if worldBattle or wideBattle then
-- restart the search from under the battle: the highest opaque state at -- restart the search from under the battle: the highest opaque state at
-- or below it (the overworld), not the menu sitting over it -- or below it (the battle itself for white/black WIDE, the overworld for
-- a non-opaque world-backed battle), not the menu sitting over it
for j = i, 1, -1 do for j = i, 1, -1 do
if states[j].isOpaque then return j end if states[j].isOpaque then return j end
end end
@@ -433,6 +457,14 @@ function Game.drawBaseInStack(stack, visibleBase)
return visibleBase return visibleBase
end end
-- A classic overlay above the approved world-backed extended HUD paints only
-- its centred area. Keep the wider owner surface transparent so its margins
-- continue to reveal the world instead of becoming an opaque white sheet.
function Game.uiCanvasTransparent(worldBelow, worldDrawn, wideBattle)
return worldBelow or (worldDrawn and wideBattle ~= nil
and wideBattle.extendedHUD and wideBattle:extendedHUD())
end
-- Shift classic SGB zones to the centred UI. A full-width base zone extends -- Shift classic SGB zones to the centred UI. A full-width base zone extends
-- into both margins, keeping the canvas' paper color continuous; narrower -- into both margins, keeping the canvas' paper color continuous; narrower
-- sprite and status zones move with the classic UI content. -- sprite and status zones move with the classic UI content.
@@ -487,6 +519,7 @@ function Game:draw()
-- the stack for the same reason as uiFill above -- a prompt opened during -- the stack for the same reason as uiFill above -- a prompt opened during
-- the battle must not drop the dim for a frame. -- the battle must not drop the dim for a frame.
Renderer.battleDim = Game.worldBgBattleDim(self.stack) Renderer.battleDim = Game.worldBgBattleDim(self.stack)
Renderer.extendedWorldBand = Game.extendedWorldHUDInStack(self.stack)
-- ...and for the same reason the UI's own scale has to know the world is -- ...and for the same reason the UI's own scale has to know the world is
-- still the backdrop while an opaque menu covers it. Renderer:uiScale -- still the backdrop while an opaque menu covers it. Renderer:uiScale
-- steps the UI down with the survey zoom only while a world is behind it, -- steps the UI down with the survey zoom only while a world is behind it,
@@ -504,7 +537,8 @@ function Game:draw()
-- menu to its top right, and the whole UI steps down with the zoom. -- menu to its top right, and the whole UI steps down with the zoom.
Renderer.uiCentered = not Game.dynamicUI(self.save) Renderer.uiCentered = not Game.dynamicUI(self.save)
Renderer.uiAnchorHold = Game.uiAnchorsHeldInStack(self.stack) Renderer.uiAnchorHold = Game.uiAnchorsHeldInStack(self.stack)
Renderer:beginFrame(worldBelow) Renderer:beginFrame(Game.uiCanvasTransparent(
worldBelow, worldDrawn, wideBattle))
for i = drawFrom, #self.stack.states do for i = drawFrom, #self.stack.states do
local state = self.stack.states[i] local state = self.stack.states[i]
local wideState = state and state.isWideBattleLayout local wideState = state and state.isWideBattleLayout
+5
View File
@@ -239,6 +239,11 @@ function SaveData.defaultOptions()
-- scale the battle surface to the window so it fills vertically. See -- scale the battle surface to the window so it fills vertically. See
-- BattleState:wantsFillScale. -- BattleState:wantsFillScale.
battleFit = "fixed", battleFit = "fixed",
-- BATTLE HUD: STANDARD keeps every wide-battle element inside the native
-- 304x144 surface. EXTENDED is opt-in window-space placement for selected
-- wide layouts; unsupported combinations deliberately fall back to the
-- standard composition.
battleHud = "standard",
-- BATTLE BG: what fills the screen behind and around the battle. -- BATTLE BG: what fills the screen behind and around the battle.
-- "white" = the display mode's paper shade (the classic look), -- "white" = the display mode's paper shade (the classic look),
-- "black" = plain black bars, "world" = the frozen overworld showing -- "black" = plain black bars, "world" = the frozen overworld showing
+64 -7
View File
@@ -138,15 +138,72 @@ local function coreRows(opts, hooks)
ladder(opts, "battleStyle", ladder(opts, "battleStyle",
{ { "shift", "SHIFT" }, { "set", "SET" } }, "shift")) { { "shift", "SHIFT" }, { "set", "SET" } }, "shift"))
add(Strings("BATTLE LAYOUT"), add(Strings("BATTLE LAYOUT"),
ladder(opts, "battleLayout", function()
{ { "og", "OG" }, { "wide", "WIDE" } }, "og")) return opts.battleLayout == "wide" and Strings("WIDE") or Strings("OG")
end,
function()
opts.battleLayout = opts.battleLayout == "wide" and "og" or "wide"
if opts.battleLayout ~= "wide" then
opts.battleHud = "standard"
elseif opts.battleFit == "fill" and opts.battleHud == "extended" then
opts.battleBg = "white"
end
return true
end)
add(Strings("BATTLE SIZE"), add(Strings("BATTLE SIZE"),
ladder(opts, "battleFit", function()
{ { "fixed", "FIXED" }, { "fill", "FILL" } }, "fixed")) return opts.battleFit == "fill" and Strings("FILL") or Strings("FIXED")
end,
function()
opts.battleFit = opts.battleFit == "fill" and "fixed" or "fill"
if opts.battleFit == "fill" and opts.battleLayout == "wide"
and opts.battleHud == "extended" then
opts.battleBg = "white"
end
return true
end)
add(Strings("BATTLE HUD"),
function()
return opts.battleLayout == "wide" and opts.battleHud == "extended"
and Strings("EXTENDED")
or Strings("STANDARD")
end,
function()
if opts.battleLayout ~= "wide" then
opts.battleHud = "standard"
return false
end
opts.battleHud = opts.battleHud == "extended" and "standard" or "extended"
if opts.battleHud == "extended" and opts.battleFit == "fill" then
opts.battleBg = "white"
end
return true
end)
add(Strings("BATTLE BG"), add(Strings("BATTLE BG"),
ladder(opts, "battleBg", function()
{ { "white", "WHITE" }, { "black", "BLACK" }, { "world", "WORLD" } }, if opts.battleLayout == "wide" and opts.battleFit == "fill"
"white")) and opts.battleHud == "extended" then
opts.battleBg = "white"
return Strings("AUTO")
end
if opts.battleBg == "black" then return Strings("BLACK") end
if opts.battleBg == "world" then return Strings("WORLD") end
return Strings("WHITE")
end,
function(dir)
if opts.battleLayout == "wide" and opts.battleFit == "fill"
and opts.battleHud == "extended" then
opts.battleBg = "white"
return false
end
local order = { "white", "black", "world" }
local cur = 1
for i, mode in ipairs(order) do
if opts.battleBg == mode then cur = i break end
end
opts.battleBg = order[wrapIndex(cur - 1 + (dir or 1), #order) + 1]
return true
end)
add(Strings("UI LAYOUT"), add(Strings("UI LAYOUT"),
ladder(opts, "uiLayout", ladder(opts, "uiLayout",
{ { "centered", "CENTERED" }, { "dynamic", "DYNAMIC" } }, "centered")) { { "centered", "CENTERED" }, { "dynamic", "DYNAMIC" } }, "centered"))
+92 -6
View File
@@ -93,6 +93,7 @@ function Renderer:init()
-- reason -- worldViewSize() already works in drawable pixels. -- reason -- worldViewSize() already works in drawable pixels.
self.uiWidth, self.uiHeight = self.WIDTH, self.HEIGHT self.uiWidth, self.uiHeight = self.WIDTH, self.HEIGHT
self.canvas = PixelCanvas.new(self.uiWidth, self.uiHeight, "nearest") self.canvas = PixelCanvas.new(self.uiWidth, self.uiHeight, "nearest")
self.battleHUDCanvas = nil
self.worldCanvas = nil self.worldCanvas = nil
self.worldActive = false self.worldActive = false
-- tilt mode only: a transparent overlay canvas the size of the world -- tilt mode only: a transparent overlay canvas the size of the world
@@ -201,10 +202,38 @@ function Renderer:setUISize(w, h)
w, h = math.floor(w), math.floor(h) w, h = math.floor(w), math.floor(h)
if w == self.uiWidth and h == self.uiHeight and self.canvas then return end if w == self.uiWidth and h == self.uiHeight and self.canvas then return end
if self.canvas and self.canvas.release then self.canvas:release() end if self.canvas and self.canvas.release then self.canvas:release() end
if self.battleHUDCanvas and self.battleHUDCanvas.release then
self.battleHUDCanvas:release()
end
self.battleHUDCanvas = nil
self.uiWidth, self.uiHeight = w, h self.uiWidth, self.uiHeight = w, h
self.canvas = PixelCanvas.new(w, h, "nearest") self.canvas = PixelCanvas.new(w, h, "nearest")
end end
-- Transparent native-pixel surface for an extended WIDE battle HUD. The
-- battle scene remains in `canvas`; endFrame places registered HUD regions
-- afterward in physical-window space.
function Renderer:beginBattleHUDPass()
local w, h = self:uiSize()
if not self.battleHUDCanvas
or self.battleHUDCanvas:getWidth() ~= w
or self.battleHUDCanvas:getHeight() ~= h then
if self.battleHUDCanvas and self.battleHUDCanvas.release then
self.battleHUDCanvas:release()
end
self.battleHUDCanvas = PixelCanvas.new(w, h, "nearest")
end
local previous = love.graphics.getCanvas and love.graphics.getCanvas()
or self.canvas
love.graphics.setCanvas(self.battleHUDCanvas)
love.graphics.clear(0, 0, 0, 0)
return previous
end
function Renderer:endBattleHUDPass(previous)
love.graphics.setCanvas(previous or self.canvas)
end
-- LOVE-unit draw scales endFrame uses for the UI blit: integer framebuffer -- LOVE-unit draw scales endFrame uses for the UI blit: integer framebuffer
-- scale (fitScale) divided by each axis's unit→pixel factor, so a GB pixel -- scale (fitScale) divided by each axis's unit→pixel factor, so a GB pixel
-- lands on fitScale() whole PHYSICAL pixels on both axes once LOVE applies -- lands on fitScale() whole PHYSICAL pixels on both axes once LOVE applies
@@ -670,6 +699,17 @@ end
-- centred letterbox. Declared during the element's own draw, in UI-canvas -- centred letterbox. Declared during the element's own draw, in UI-canvas
-- pixels, and consumed by endFrame this frame only. -- pixels, and consumed by endFrame this frame only.
-- anchor: "bottom" | "topright" | "topleft" | "bottomright" -- anchor: "bottom" | "topright" | "topleft" | "bottomright"
local function addUIAnchor(renderer, x, y, w, h, anchor, windowClamped,
canvas, extract)
renderer.uiAnchors = renderer.uiAnchors or {}
renderer.uiAnchors[#renderer.uiAnchors + 1] = {
x = x, y = y, w = w, h = h, anchor = anchor,
windowClamped = windowClamped and true or false,
canvas = canvas,
extract = extract ~= false,
}
end
function Renderer:setUIAnchor(x, y, w, h, anchor) function Renderer:setUIAnchor(x, y, w, h, anchor)
-- UI LAYOUT = CENTERED (uiCentered, set per frame by Game:draw from -- UI LAYOUT = CENTERED (uiCentered, set per frame by Game:draw from
-- save.options.uiLayout): every element stays where it was drawn in the -- save.options.uiLayout): every element stays where it was drawn in the
@@ -683,9 +723,16 @@ function Renderer:setUIAnchor(x, y, w, h, anchor)
-- battle -- keeps every element inside it, so the box blits where it was -- battle -- keeps every element inside it, so the box blits where it was
-- drawn in the canvas instead of being pulled to the window edge. -- drawn in the canvas instead of being pulled to the window edge.
if self.uiAnchorHold then return end if self.uiAnchorHold then return end
self.uiAnchors = self.uiAnchors or {} addUIAnchor(self, x, y, w, h, anchor, false, self.canvas, true)
self.uiAnchors[#self.uiAnchors + 1] = end
{ x = x, y = y, w = w, h = h, anchor = anchor }
-- Battle-owned window-space placement. Unlike ordinary UI anchors this is
-- intentionally allowed while BattleState holds general dialogue/menu
-- anchors inside the battle surface. Callers must gate it to an explicit
-- battle HUD mode.
function Renderer:setBattleUIAnchor(x, y, w, h, anchor)
addUIAnchor(self, x, y, w, h, anchor, true,
self.battleHUDCanvas or self.canvas, false)
end end
-- zones: optional list of SGB palette regions (see PaletteFX) in -- zones: optional list of SGB palette regions (see PaletteFX) in
@@ -798,6 +845,8 @@ function Renderer:endFrame(zones, worldZones)
-- is the pack's off-white (255,239,255), which a hardcoded 1,1,1 framed in -- is the pack's off-white (255,239,255), which a hardcoded 1,1,1 framed in
-- a visibly brighter border. -- a visibly brighter border.
local clearR, clearG, clearB = 0, 0, 0 local clearR, clearG, clearB = 0, 0, 0
local extendedBlackBand = false
local bandR, bandG, bandB = 1, 1, 1
if not self.worldActive then if not self.worldActive then
local ok, Game = pcall(require, "src.core.Game") local ok, Game = pcall(require, "src.core.Game")
local stack = ok and Game and Game.stack local stack = ok and Game and Game.stack
@@ -824,7 +873,15 @@ function Renderer:endFrame(zones, worldZones)
-- screen stays black (src/core/FaithfulRes.lua); the paper surround -- screen stays black (src/core/FaithfulRes.lua); the paper surround
-- painted the whole phone white on New Game and in battle (#864), so -- painted the whole phone white on New Game and in battle (#864), so
-- the lock keeps the default black bars. -- the lock keeps the default black bars.
if state and state.letterboxWhite if state and state.extendedBlackHUD and state:extendedBlackHUD()
and not FaithfulRes.scaleCap() then
-- Extended/Black keeps the author's black surround, but extends the
-- fixed battle's paper field vertically through the physical window.
-- The band uses the exact centred fixed-width composition bounds, so
-- only vertical black bars remain at the sides.
extendedBlackBand = true
bandR, bandG, bandB = PaletteFX.paperShade(Game and Game.data)
elseif state and state.letterboxWhite
and not (state.bgMode and state:bgMode() == "black") and not (state.bgMode and state:bgMode() == "black")
and not FaithfulRes.scaleCap() then and not FaithfulRes.scaleCap() then
clearR, clearG, clearB = PaletteFX.paperShade(Game and Game.data) clearR, clearG, clearB = PaletteFX.paperShade(Game and Game.data)
@@ -832,6 +889,10 @@ function Renderer:endFrame(zones, worldZones)
end end
love.graphics.setColor(clearR, clearG, clearB, 1) love.graphics.setColor(clearR, clearG, clearB, 1)
love.graphics.rectangle("fill", 0, 0, ww, wh) love.graphics.rectangle("fill", 0, 0, ww, wh)
if extendedBlackBand then
love.graphics.setColor(bandR, bandG, bandB, 1)
love.graphics.rectangle("fill", uox, 0, uvpw, wh)
end
love.graphics.setColor(1, 1, 1, 1) love.graphics.setColor(1, 1, 1, 1)
-- render.letterbox: SGB borders / custom void art in the bars around the -- render.letterbox: SGB borders / custom void art in the bars around the
-- 160x144 (or world) blit. Drawn after the clear and before the game -- 160x144 (or world) blit. Drawn after the clear and before the game
@@ -974,6 +1035,22 @@ function Renderer:endFrame(zones, worldZones)
love.graphics.setColor(1, 1, 1, 1) love.graphics.setColor(1, 1, 1, 1)
end end
-- Extended/WORLD keeps the frozen world as the physical surround, but stock
-- Gen 1 back sprites rely on the battle's paper shade for visible highlights.
-- Back the exact fixed-width composition from physical top to bottom so only
-- the left and right sides expose the world. The battle canvas and detached
-- HUD remain transparent layers composited afterward.
-- A worldOverride is an arena provider's completed scene (for example,
-- StadiumBattleFX/Dramaless). It replaces the stock paper-backed battle
-- field, so never cover it with the native back-sprite fallback.
if self.extendedWorldBand and not self.worldOverride
and not FaithfulRes.scaleCap() then
local ok, Game = pcall(require, "src.core.Game")
love.graphics.setColor(PaletteFX.paperShade(ok and Game and Game.data))
love.graphics.rectangle("fill", uox, 0, uvpw, wh)
love.graphics.setColor(1, 1, 1, 1)
end
-- UI: anchored regions against their screen edges, the rest in the classic -- UI: anchored regions against their screen edges, the rest in the classic
-- centred letterbox. With nothing anchored this is the single blit it has -- centred letterbox. With nothing anchored this is the single blit it has
-- always been. -- always been.
@@ -996,14 +1073,23 @@ function Renderer:endFrame(zones, worldZones)
if a.anchor == "bottom" then if a.anchor == "bottom" then
dx = uox + a.x * Ux -- horizontally it stays with the letterbox dx = uox + a.x * Ux -- horizontally it stays with the letterbox
dy = wh - gapB - dh dy = wh - gapB - dh
elseif a.anchor == "top" then
dx = uox + a.x * Ux -- horizontally it stays with the letterbox
dy = a.y * Uy
elseif a.anchor == "topright" then elseif a.anchor == "topright" then
dx = ww - gapR - dw dx = ww - gapR - dw
dy = a.y * Uy dy = a.y * Uy
else -- unknown anchor: leave it where it is else -- unknown anchor: leave it where it is
dx, dy = uox + a.x * Ux, uoy + a.y * Uy dx, dy = uox + a.x * Ux, uoy + a.y * Uy
end end
if a.windowClamped then
dx = math.max(0, math.min(math.max(0, ww - dw), dx))
dy = math.max(0, math.min(math.max(0, wh - dh), dy))
end
placed[#placed + 1] = { a = a, dx = dx, dy = dy, dw = dw, dh = dh } placed[#placed + 1] = { a = a, dx = dx, dy = dy, dw = dw, dh = dh }
rest = subtractRect(rest, uox + a.x * Ux, uoy + a.y * Uy, dw, dh) if a.extract then
rest = subtractRect(rest, uox + a.x * Ux, uoy + a.y * Uy, dw, dh)
end
end end
for _, r in ipairs(rest) do for _, r in ipairs(rest) do
blit(self.canvas, Ux, Uy, zones, Ux, Uy, uox, uoy, r[1], r[2], r[3], r[4]) blit(self.canvas, Ux, Uy, zones, Ux, Uy, uox, uoy, r[1], r[2], r[3], r[4])
@@ -1012,7 +1098,7 @@ function Renderer:endFrame(zones, worldZones)
-- shift the draw origin so canvas pixel (a.x, a.y) lands on (dx, dy). -- shift the draw origin so canvas pixel (a.x, a.y) lands on (dx, dy).
-- The zone scissors are computed from the same origin, so an SGB -- The zone scissors are computed from the same origin, so an SGB
-- region travels with the element instead of staying in the letterbox. -- region travels with the element instead of staying in the letterbox.
blit(self.canvas, Ux, Uy, zones, Ux, Uy, blit(p.a.canvas or self.canvas, Ux, Uy, zones, Ux, Uy,
p.dx - p.a.x * Ux, p.dy - p.a.y * Uy, p.dx, p.dy, p.dw, p.dh) p.dx - p.a.x * Ux, p.dy - p.a.y * Uy, p.dx, p.dy, p.dw, p.dh)
end end
end end
+42 -1
View File
@@ -180,6 +180,11 @@ local function buildRows(game)
step = function(g) step = function(g)
local o = g.save.options local o = g.save.options
o.battleLayout = o.battleLayout == "wide" and "og" or "wide" o.battleLayout = o.battleLayout == "wide" and "og" or "wide"
if o.battleLayout ~= "wide" then
o.battleHud = "standard"
elseif o.battleFit == "fill" and o.battleHud == "extended" then
o.battleBg = "white"
end
return true return true
end }, end },
-- FIXED keeps the classic integer-scaled letterbox -- a GB pixel is a -- FIXED keeps the classic integer-scaled letterbox -- a GB pixel is a
@@ -195,6 +200,31 @@ local function buildRows(game)
step = function(g) step = function(g)
local o = g.save.options local o = g.save.options
o.battleFit = o.battleFit == "fill" and "fixed" or "fill" o.battleFit = o.battleFit == "fill" and "fixed" or "fill"
if o.battleFit == "fill" and o.battleLayout == "wide"
and o.battleHud == "extended" then
o.battleBg = "white"
end
return true
end },
{ id = "battleHud", label = Strings("BATTLE HUD"),
value = function(g)
local o = g.save.options
return o.battleLayout == "wide" and o.battleHud == "extended"
and Strings("EXTENDED")
or Strings("STANDARD")
end,
step = function(g)
local o = g.save.options
-- The extended HUD is a widescreen-only composition. Keep OG locked
-- to the author's standard HUD even if an older save says otherwise.
if o.battleLayout ~= "wide" then
o.battleHud = "standard"
return false
end
o.battleHud = o.battleHud == "extended" and "standard" or "extended"
if o.battleHud == "extended" and o.battleFit == "fill" then
o.battleBg = "white"
end
return true return true
end }, end },
-- What sits behind and around the battle. WHITE is the classic paper -- What sits behind and around the battle. WHITE is the classic paper
@@ -203,13 +233,24 @@ local function buildRows(game)
-- shows through everywhere the battle does not paint). -- shows through everywhere the battle does not paint).
{ id = "battleBg", label = Strings("BATTLE BG"), { id = "battleBg", label = Strings("BATTLE BG"),
value = function(g) value = function(g)
local m = g.save.options.battleBg local o = g.save.options
if o.battleLayout == "wide" and o.battleFit == "fill"
and o.battleHud == "extended" then
o.battleBg = "white"
return Strings("AUTO")
end
local m = o.battleBg
if m == "black" then return Strings("BLACK") end if m == "black" then return Strings("BLACK") end
if m == "world" then return Strings("WORLD") end if m == "world" then return Strings("WORLD") end
return Strings("WHITE") return Strings("WHITE")
end, end,
step = function(g, dir) step = function(g, dir)
local o = g.save.options local o = g.save.options
if o.battleLayout == "wide" and o.battleFit == "fill"
and o.battleHud == "extended" then
o.battleBg = "white"
return false
end
local order = { "white", "black", "world" } local order = { "white", "black", "world" }
local cur = 1 local cur = 1
for i, m in ipairs(order) do if o.battleBg == m then cur = i break end end for i, m in ipairs(order) do if o.battleBg == m then cur = i break end end
@@ -0,0 +1,36 @@
local U = require("tests.drivers.util")
local OUT = os.getenv("SHOT_DIR") or "battle-hud-layout-lock"
local BEFORE = OUT .. "/battle_hud_wide_extended.png"
local AFTER = OUT .. "/battle_hud_og_locked_standard.png"
return function(game)
os.remove(BEFORE)
os.remove(AFTER)
local options = game.save.options
options.battleLayout = "wide"
options.battleHud = "extended"
local menu = require("src.ui.Screens").push(game, "OptionsMenu")
local layoutRow, hudRow
for _, row in ipairs(menu.rows) do
if row.id == "battleLayout" then layoutRow = row end
if row.id == "battleHud" then hudRow = row end
end
assert(layoutRow and hudRow, "battle layout/HUD rows are present")
menu.index = 6
menu.scroll = 3
assert(hudRow.value(game) == "EXTENDED", "WIDE displays EXTENDED")
assert(U.shot(game, BEFORE), "WIDE/EXTENDED screenshot was written")
layoutRow.step(game, 1)
assert(options.battleLayout == "og", "layout switched to OG")
assert(options.battleHud == "standard", "OG normalized HUD to STANDARD")
assert(hudRow.value(game) == "STANDARD", "OG displays STANDARD")
assert(U.shot(game, AFTER), "OG/STANDARD screenshot was written")
print("[driver] BATTLE_HUD_LAYOUT_LOCK_PASS")
game.driverDone = true
end
@@ -0,0 +1,53 @@
-- Visual and behavioral acceptance for the adaptive BATTLE BG menu rule.
return function(game)
local U = dofile("tests/drivers/util.lua")
local DIR = os.getenv("SHOT_DIR") or "/tmp/shots"
love.window.setMode(1920, 1080, { resizable = true })
U.wait(3)
local options = game.save.options
options.battleLayout = "wide"
options.battleFit = "fixed"
options.battleHud = "extended"
options.battleBg = "black"
local menu = require("src.ui.Screens").push(game, "OptionsMenu")
local fitRow, bgRow
local bgIndex
for i, row in ipairs(menu.rows) do
if row.id == "battleFit" then fitRow = row end
if row.id == "battleBg" then bgRow, bgIndex = row, i end
end
assert(fitRow and bgRow and bgIndex, "battle size/background rows are present")
fitRow.step(game, 1)
assert(options.battleFit == "fill", "battle size switched to FILL")
assert(options.battleBg == "white", "FILL + EXTENDED normalized background to WHITE")
assert(bgRow.value(game) == "AUTO", "adaptive background is labeled AUTO")
assert(bgRow.step(game, 1) == false, "AUTO background row is locked")
assert(options.battleBg == "white", "locked AUTO retains the WHITE value")
menu.index = bgIndex
menu.scroll = math.max(0, bgIndex - 5)
U.wait(2)
local autoPath = DIR .. "/fill_extended_auto_menu.png"
os.remove(autoPath)
local ok = U.shot(game, autoPath)
fitRow.step(game, -1)
assert(options.battleFit == "fixed", "battle size switched back to FIXED")
assert(bgRow.value(game) == "WHITE", "FIXED exposes the stored WHITE choice")
assert(bgRow.step(game, 1) == true and options.battleBg == "black",
"FIXED can select BLACK")
assert(bgRow.step(game, 1) == true and options.battleBg == "world",
"FIXED can select WORLD")
U.wait(2)
local fixedPath = DIR .. "/fixed_extended_background_choices.png"
os.remove(fixedPath)
ok = U.shot(game, fixedPath) and ok
U.log(ok and "FILL_EXTENDED_AUTO_MENU_PASS"
or "FILL_EXTENDED_AUTO_MENU_FAIL")
love.event.quit(ok and 0 or 1)
end
@@ -0,0 +1,64 @@
-- Visual acceptance driver for WIDE + FILL + EXTENDED + WHITE.
-- The full physical-window backing remains white while the four battle HUD
-- panels move to their approved window anchors.
return function(game)
local U = dofile("tests/drivers/util.lua")
local DIR = os.getenv("SHOT_DIR") or "/tmp/shots"
local BattleState = require("src.battle.BattleState")
local Pokemon = require("src.pokemon.Pokemon")
love.window.setMode(2048, 1152, { resizable = true })
U.wait(3)
local options = game.save.options
options.battleLayout = "wide"
options.battleFit = "fill"
options.battleHud = "extended"
options.battleBg = "white"
game.save.party = { Pokemon.new(game.data, "PIKACHU", 100) }
U.teleport(game, "ROUTE_1", 5, 5, "down")
U.wait(60)
local battle = BattleState.newWild(game, "PIDGEY", 3,
{ onFinish = function() end })
game.overworld:pushBattle(battle)
U.wait(360)
battle.introSlide = 0
battle.introBalls = nil
battle.showEnemyTrainer = false
battle.showPlayerBack = false
battle.enemySendingOut = false
battle.sendingOut = false
battle.phase = "menu"
battle.menuIndex = 1
U.wait(2)
assert(battle:extendedHUD(), "FILL/WHITE activates the approved extended HUD")
assert(not battle:extendedWorldHUD(), "FILL/WHITE does not use FIXED's paper band")
local path = DIR .. "/fill_extended_white_separate_layer.png"
os.remove(path)
local ok = U.shot(game, path)
love.window.setMode(960, 540, { resizable = true })
U.wait(5)
local smallPath = DIR .. "/fill_extended_white_small_16x9.png"
os.remove(smallPath)
ok = U.shot(game, smallPath) and ok
love.window.setMode(2048, 1152, { resizable = true })
U.wait(5)
local NamingScreen = require("src.ui.NamingScreen")
game.stack:push(NamingScreen.new(game, {
title = "NICKNAME?", maxLen = 10, onDone = function() end,
}))
U.wait(5)
local overlayPath = DIR .. "/fill_extended_white_naming_overlay.png"
os.remove(overlayPath)
ok = U.shot(game, overlayPath) and ok
U.log(ok and "FILL_EXTENDED_WHITE_PASS" or "FILL_EXTENDED_WHITE_FAIL")
love.event.quit(ok and 0 or 1)
end
@@ -0,0 +1,64 @@
local U = require("tests.drivers.util")
local OUT = os.getenv("SHOT_DIR") or "fixed-extended-black"
local FULL = OUT .. "/fixed_extended_black_full.png"
local SMALL = OUT .. "/fixed_extended_black_small_16x9.png"
local OVERLAY = OUT .. "/fixed_extended_black_overlay.png"
return function(game)
local BattleState = require("src.battle.BattleState")
local Pokemon = require("src.pokemon.Pokemon")
os.remove(FULL)
os.remove(SMALL)
os.remove(OVERLAY)
love.window.setMode(2048, 1152, { resizable = true })
U.wait(3)
local options = game.save.options
options.battleLayout = "wide"
options.battleFit = "fixed"
options.battleHud = "extended"
options.battleBg = "black"
game.save.party = { Pokemon.new(game.data, "PIKACHU", 100) }
U.teleport(game, "ROUTE_1", 5, 5, "down")
U.wait(60)
local battle = BattleState.newWild(game, "PIDGEY", 3,
{ onFinish = function() end })
game.overworld:pushBattle(battle)
U.wait(360)
battle.introSlide = 0
battle.introBalls = nil
battle.showEnemyTrainer = false
battle.showPlayerBack = false
battle.enemySendingOut = false
battle.sendingOut = false
battle.phase = "menu"
battle.menuIndex = 1
U.wait(2)
assert(battle:extendedHUD(), "BLACK activates the approved extended HUD")
assert(battle:extendedBlackHUD(), "BLACK activates the white vertical battle band")
assert(not battle:extendedWorldHUD(), "BLACK remains separate from WORLD")
assert(U.shot(game, FULL), "full black-background screenshot was written")
love.window.setMode(960, 540, { resizable = true })
U.wait(10)
assert(U.shot(game, SMALL), "small black-background screenshot was written")
love.window.setMode(2048, 1152, { resizable = true })
U.wait(10)
battle.blankForAskName = true
local naming = require("src.ui.NamingScreen").new(game, {
title = "NICKNAME?", maxLen = 10, onDone = function() end,
})
game.stack:push(naming)
U.wait(10)
assert(U.shot(game, OVERLAY), "black-background overlay screenshot was written")
print("[driver] FIXED_EXTENDED_BLACK_PASS")
love.event.quit(0)
end
@@ -0,0 +1,65 @@
local U = require("tests.drivers.util")
local OUT = os.getenv("SHOT_DIR") or "fixed-extended-white"
local FULL = OUT .. "/fixed_extended_white_full.png"
local SMALL = OUT .. "/fixed_extended_white_small_16x9.png"
local OVERLAY = OUT .. "/fixed_extended_white_overlay.png"
return function(game)
local BattleState = require("src.battle.BattleState")
local Pokemon = require("src.pokemon.Pokemon")
os.remove(FULL)
os.remove(SMALL)
os.remove(OVERLAY)
love.window.setMode(2048, 1152, { resizable = true })
U.wait(3)
local options = game.save.options
options.battleLayout = "wide"
options.battleFit = "fixed"
options.battleHud = "extended"
options.battleBg = "white"
game.save.party = { Pokemon.new(game.data, "PIKACHU", 100) }
U.teleport(game, "ROUTE_1", 5, 5, "down")
U.wait(60)
local battle = BattleState.newWild(game, "PIDGEY", 3,
{ onFinish = function() end })
game.overworld:pushBattle(battle)
U.wait(360)
battle.introSlide = 0
battle.introBalls = nil
battle.showEnemyTrainer = false
battle.showPlayerBack = false
battle.enemySendingOut = false
battle.sendingOut = false
battle.phase = "menu"
battle.menuIndex = 1
U.wait(2)
assert(battle:extendedHUD(), "WHITE activates the approved extended HUD")
assert(not battle:extendedWorldHUD(), "WHITE keeps its opaque paper field")
assert(U.shot(game, FULL), "full WHITE screenshot was written")
love.window.setMode(960, 540, { resizable = true })
U.wait(10)
assert(U.shot(game, SMALL), "small WHITE screenshot was written")
love.window.setMode(2048, 1152, { resizable = true })
U.wait(10)
battle.blankForAskName = true
local naming = require("src.ui.NamingScreen").new(game, {
title = "NICKNAME?",
maxLen = 10,
initial = "",
onDone = function() end,
})
game.stack:push(naming)
U.wait(10)
assert(U.shot(game, OVERLAY), "WHITE overlay screenshot was written")
print("[driver] FIXED_EXTENDED_WHITE_PASS")
love.event.quit(0)
end
@@ -0,0 +1,44 @@
-- Visual regression coverage for Professor Oak's scripted Yellow capture Bag
-- over the WIDE + FIXED + EXTENDED + WORLD composition.
return function(game)
local U = dofile("tests/drivers/util.lua")
local DIR = os.getenv("SHOT_DIR") or "/tmp/shots"
local BattleState = require("src.battle.BattleState")
local Pokemon = require("src.pokemon.Pokemon")
love.window.setMode(2048, 1152, { resizable = true })
U.wait(3)
local options = game.save.options
options.battleLayout = "wide"
options.battleFit = "fixed"
options.battleHud = "extended"
options.battleBg = "world"
game.save.party = { Pokemon.new(game.data, "PIKACHU", 20) }
U.teleport(game, "ROUTE_1", 5, 5, "down")
U.wait(30)
local demo = BattleState.newWild(game, "CHARMANDER", 5)
demo:makeOldManDemo("PROF.OAK")
demo.onFinish = function() end
game.overworld:pushBattle(demo)
for _ = 1, 100 do
if demo.phase == "menu" and (demo.demoTimer or 0) > 5 then break end
U.tap(game, "a")
U.wait(4)
end
for _ = 1, 180 do
if game.stack:top() ~= demo then break end
U.wait(1)
end
U.wait(3)
local path = DIR .. "/fixed_extended_world_oak_charmander_bag.png"
os.remove(path)
local ok = game.stack:top() ~= demo and U.shot(game, path)
U.log(ok and "FIXED_EXTENDED_WORLD_BAG_PASS"
or "FIXED_EXTENDED_WORLD_BAG_FAIL")
love.event.quit(ok and 0 or 1)
end
@@ -0,0 +1,65 @@
-- Visual acceptance driver for the first EXTENDED HUD configuration only:
-- WIDE + FIXED + EXTENDED + WORLD.
-- POKEPORT_DRIVER=tests/drivers/fixed_extended_world_hud_test.lua \
-- POKEPORT_IDENTITY=fixed-extended-world POKEPORT_TOUCH=0 \
-- SHOT_DIR=/tmp/shots love .
return function(game)
local U = dofile("tests/drivers/util.lua")
local DIR = os.getenv("SHOT_DIR") or "/tmp/shots"
local BattleState = require("src.battle.BattleState")
local Pokemon = require("src.pokemon.Pokemon")
-- Match the 16:9 acceptance screenshot so the fixed 304x144 surface has
-- measurable space above and below it.
love.window.setMode(2048, 1152, { resizable = true })
U.wait(3)
local options = game.save.options
options.battleLayout = "wide"
options.battleFit = "fixed"
options.battleHud = "extended"
options.battleBg = "world"
game.save.party = { Pokemon.new(game.data, "PIKACHU", 100) }
U.teleport(game, "ROUTE_1", 5, 5, "down")
U.wait(60)
local battle = BattleState.newWild(game, "PIDGEY", 3,
{ onFinish = function() end })
game.overworld:pushBattle(battle)
U.wait(360)
battle.introSlide = 0
battle.introBalls = nil
battle.showEnemyTrainer = false
battle.showPlayerBack = false
battle.enemySendingOut = false
battle.sendingOut = false
battle.phase = "menu"
battle.menuIndex = 1
U.wait(2)
local path = DIR .. "/fixed_extended_world_separate_layer.png"
os.remove(path)
local ok = U.shot(game, path)
love.window.setMode(960, 540, { resizable = true })
U.wait(5)
local smallPath = DIR .. "/fixed_extended_world_small_16x9.png"
os.remove(smallPath)
ok = U.shot(game, smallPath) and ok
love.window.setMode(2048, 1152, { resizable = true })
U.wait(5)
local NamingScreen = require("src.ui.NamingScreen")
game.stack:push(NamingScreen.new(game, {
title = "NICKNAME?", maxLen = 10, onDone = function() end,
}))
U.wait(5)
local overlayPath = DIR .. "/fixed_extended_world_naming_overlay.png"
os.remove(overlayPath)
ok = U.shot(game, overlayPath) and ok
U.log(ok and "FIXED_EXTENDED_WORLD_PASS" or "FIXED_EXTENDED_WORLD_FAIL")
love.event.quit(ok and 0 or 1)
end
+9 -1
View File
@@ -92,6 +92,11 @@ local menu = { isOpaque = true } -- PartyMenu / ListMenu
local whiteBattle = setmetatable( local whiteBattle = setmetatable(
{ game = { save = { options = { battleBg = "white" } } } }, { game = { save = { options = { battleBg = "white" } } } },
{ __index = BattleState }) { __index = BattleState })
local wideWhiteBattle = setmetatable(
{ game = { save = { options = {
battleBg = "white", battleLayout = "wide",
} } } },
{ __index = BattleState })
local function stack(...) return { states = { ... }, local function stack(...) return { states = { ... },
visibleBase = function(self) visibleBase = function(self)
for i = #self.states, 1, -1 do for i = #self.states, 1, -1 do
@@ -111,7 +116,10 @@ T.eq(s2:visibleBase(), 1, "the battle alone already drew from the overworld")
T.eq(Game.drawBaseInStack(s2, s2:visibleBase()), 1, "and still does") T.eq(Game.drawBaseInStack(s2, s2:visibleBase()), 1, "and still does")
local s3 = stack(overworld, whiteBattle, menu) local s3 = stack(overworld, whiteBattle, menu)
T.eq(Game.drawBaseInStack(s3, s3:visibleBase()), 3, T.eq(Game.drawBaseInStack(s3, s3:visibleBase()), 3,
"a white-bg battle has no map to hold, so nothing moves") "a classic white-bg battle has no presentation to hold, so nothing moves")
local s3wide = stack(overworld, wideWhiteBattle, menu)
T.eq(Game.drawBaseInStack(s3wide, s3wide:visibleBase()), 2,
"an opaque WIDE battle still draws beneath its classic menu")
local s4 = stack(overworld, menu) local s4 = stack(overworld, menu)
T.eq(Game.drawBaseInStack(s4, s4:visibleBase()), 2, T.eq(Game.drawBaseInStack(s4, s4:visibleBase()), 2,
"and a menu outside a battle is untouched") "and a menu outside a battle is untouched")
@@ -77,6 +77,7 @@ local function battleWith(fx, sprites)
statusHUDVisible = function() return true end, statusHUDVisible = function() return true end,
bottomUIVisible = function() return true end, bottomUIVisible = function() return true end,
caughtMarkerVisible = function() return false end, caughtMarkerVisible = function() return false end,
extendedHUD = function() return false end,
} }
end end