mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 08:21:02 +02:00
46bd0f6709
BATTLE SIZE "fixed" draws the battle as a discrete letterbox rather than
filling the window, and BATTLE BG "world" composes it over the live map.
Everything the battle then opens broke out of that composition, because
each piece of the frame's geometry was read off a fact about THIS FRAME
instead of about the battle:
* Renderer:uiScale follows the survey zoom only while a world is behind
the UI, gated on worldActive -- this frame's world pass. PartyMenu and
ListMenu are opaque, so pushing one makes StateStack:visibleBase skip
the map, no world pass runs, and the menu loses the step-down and blits
a whole integer scale larger than the battle it just covered. Held
with uiWorldHold, the same whole-stack rule uiFill and the battle dim
already use. ("fill" hid this: it overrides the scale outright.)
* Game:draw started the frame at visibleBase, so that same opaque menu
cut the overworld -- and the world pass with it -- out of the frame
entirely, collapsing a "world" backdrop to endFrame's flat black clear.
A world-bg battle now keeps the frame starting from underneath itself
(drawBaseInStack). 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 before.
* worldZones was keyed to that same clear base, so it came out nil for a
frame whose world pass HAD run -- dropping endFrame's world blit onto
the UI zone list instead, smearing the party menu's own HP-bar palettes
across a world-canvas-sized image. Keyed to whether the map drew.
* endFrame's letterbox clear read letterboxWhite off visibleBase alone,
so an opaque menu over a BG "white" battle flipped its surround to
black the same way. Same whole-stack hold.
* ChoiceBox bottom-anchored unconditionally, docking it to the WINDOW
edge. That is only right when it rides the dialogue box beneath it,
which is anchored there too; TextBox now passes the anchor and nothing
else does, so the battle's switch offer and the shop/PC confirms stay
over the screen that pushed them.
* TextBox anchors likewise: a battle is a self-contained SCREEN, not the
window, and pokered prints its text box in the same 160x144 tilemap as
the HUD. The caught-mon nickname prompt was landing a whole letterbox
below the blanked battle field it is printed on. BattleState.holdsUI-
Anchors holds setUIAnchor off while a battle is in the stack; the
overworld's own dialogue box still docks to the screen edge.
179 lines
8.2 KiB
Lua
179 lines
8.2 KiB
Lua
-- BATTLE SIZE "fixed" + BATTLE BG "world": the battle draws as a fixed
|
|
-- letterbox over the map, stepped down with the survey zoom. The PKMN and
|
|
-- ITEM menus it opens have to stay that size, and its YES/NO prompt has to
|
|
-- stay inside it.
|
|
--
|
|
-- Both broke the same way -- by reading a fact about THIS FRAME instead of
|
|
-- about the battle:
|
|
--
|
|
-- * Renderer:uiScale follows the zoom only while a world is behind the UI,
|
|
-- gated on worldActive (beginWorldPass set it this frame). A "world"-bg
|
|
-- battle is non-opaque so the map keeps drawing under it -- but PartyMenu
|
|
-- and ListMenu ARE opaque, so pushing one makes StateStack:visibleBase
|
|
-- skip the map, the world pass never runs, and the menu loses the
|
|
-- step-down and blits a whole integer scale larger than the battle it
|
|
-- just covered. ("fill" hid this: it overrides the scale outright.)
|
|
--
|
|
-- * ChoiceBox bottom-anchored unconditionally, which docks it to the
|
|
-- WINDOW's bottom edge. That is right only when it is riding the
|
|
-- dialogue box below it, which is anchored there too. The battle draws
|
|
-- its own text inside the battle canvas, so the switch offer's YES/NO was
|
|
-- the only piece of that prompt flung to the window edge -- further off
|
|
-- the smaller the fixed battle is drawn.
|
|
-- luajit tests/engine/battle_fixed_menu_scale.lua
|
|
|
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
|
|
|
local T = require("tests.modkit")
|
|
local Renderer = require("src.render.Renderer")
|
|
local Zoom = require("src.render.Zoom")
|
|
local TextBox = require("src.render.TextBox")
|
|
local ChoiceBox = require("src.ui.ChoiceBox")
|
|
|
|
-- ------------------------------------------------- the UI scale holds its size
|
|
|
|
-- pin the window so the scales below are exact numbers rather than whatever
|
|
-- the runner happens to be sized at (this suite also runs under real LOVE)
|
|
local g = love.graphics
|
|
local realDims, realPixelDims = g.getDimensions, g.getPixelDimensions
|
|
g.getDimensions = function() return 640, 576 end
|
|
g.getPixelDimensions = function() return 640, 576 end
|
|
|
|
-- 640x576 fits the 160x144 classic surface at exactly 4x
|
|
T.eq(Renderer:fitScale(), 4, "the fixture window fits the classic surface at 4x")
|
|
|
|
local function uiScaleWith(offset, worldActive, hold)
|
|
local oldOffset, oldActive, oldHold =
|
|
Zoom.offset, Renderer.worldActive, Renderer.uiWorldHold
|
|
Zoom.offset, Renderer.worldActive, Renderer.uiWorldHold =
|
|
offset, worldActive, hold
|
|
local s = Renderer:uiScale()
|
|
Zoom.offset, Renderer.worldActive, Renderer.uiWorldHold =
|
|
oldOffset, oldActive, oldHold
|
|
return s
|
|
end
|
|
|
|
T.eq(uiScaleWith(0, true, false), 4, "unzoomed, the UI is the fit scale")
|
|
T.eq(uiScaleWith(-2, true, false), 2,
|
|
"zoomed out over a live world pass, the UI steps down with it")
|
|
T.eq(uiScaleWith(-2, false, false), 4,
|
|
"with no world behind it at all (title screen), the zoom is ignored")
|
|
|
|
-- the fix: the party menu / bag ended the world pass, but the battle under
|
|
-- them is still drawn over the map, so the surface must not grow
|
|
T.eq(uiScaleWith(-2, false, true), 2,
|
|
"an opaque menu over a world-bg battle keeps the battle's stepped-down scale")
|
|
T.eq(uiScaleWith(0, false, true), 4,
|
|
"and the hold changes nothing when the player never zoomed out")
|
|
|
|
g.getDimensions, g.getPixelDimensions = realDims, realPixelDims
|
|
|
|
-- the hold is the same whole-stack answer the dim already uses, so a menu
|
|
-- opened over the battle cannot drop it for a frame (Game:draw wires
|
|
-- uiWorldHold to worldBgBattleDim ~= nil; battle_fit_option covers the scan)
|
|
local Game = require("src.core.Game")
|
|
local BattleState = require("src.battle.BattleState")
|
|
-- isOpaque false is what "world" actually does to a live battle (BattleState
|
|
-- drops it per-instance at start, so the class default stays opaque for every
|
|
-- other battle) -- and it is the whole reason the map draws underneath
|
|
local worldBattle = setmetatable(
|
|
{ game = { save = { options = { battleBg = "world" } } }, isOpaque = false },
|
|
{ __index = BattleState })
|
|
T.check(Game.worldBgBattleDim({ states = { {}, worldBattle, {} } }) ~= nil,
|
|
"the stack scan the hold reads still finds the battle under an opaque menu")
|
|
|
|
-- --------------------------------------------- the backdrop holds under menus
|
|
|
|
-- The battle establishes the surround too, and the same opaque menu used to
|
|
-- take that over: it becomes visibleBase, the overworld stops drawing, and
|
|
-- the world the battle was composed over collapses to a flat black clear.
|
|
local overworld = { isOpaque = true }
|
|
local menu = { isOpaque = true } -- PartyMenu / ListMenu
|
|
local whiteBattle = setmetatable(
|
|
{ game = { save = { options = { battleBg = "white" } } } },
|
|
{ __index = BattleState })
|
|
local function stack(...) return { states = { ... },
|
|
visibleBase = function(self)
|
|
for i = #self.states, 1, -1 do
|
|
if self.states[i].isOpaque then return i end
|
|
end
|
|
return 1
|
|
end } end
|
|
|
|
local s = stack(overworld, worldBattle, menu)
|
|
T.eq(s:visibleBase(), 3, "the menu is the topmost opaque state, as before")
|
|
T.eq(Game.drawBaseInStack(s, s:visibleBase()), 1,
|
|
"but the frame still starts at the overworld, so the map keeps drawing")
|
|
|
|
-- unchanged everywhere else
|
|
local s2 = stack(overworld, worldBattle)
|
|
T.eq(s2:visibleBase(), 1, "the battle alone already drew from the overworld")
|
|
T.eq(Game.drawBaseInStack(s2, s2:visibleBase()), 1, "and still does")
|
|
local s3 = stack(overworld, whiteBattle, menu)
|
|
T.eq(Game.drawBaseInStack(s3, s3:visibleBase()), 3,
|
|
"a white-bg battle has no map to hold, so nothing moves")
|
|
local s4 = stack(overworld, menu)
|
|
T.eq(Game.drawBaseInStack(s4, s4:visibleBase()), 2,
|
|
"and a menu outside a battle is untouched")
|
|
T.eq(Game.drawBaseInStack(stack(overworld), 1), 1, "a lone base is safe")
|
|
T.eq(Game.drawBaseInStack(nil, 1), 1, "and so is no stack at all")
|
|
|
|
-- ------------------------------------------- the YES/NO stays with its screen
|
|
|
|
-- a bare choice box -- the battle's switch offer, a shop or PC confirm -- has
|
|
-- no anchored dialogue box under it to ride
|
|
T.eq(ChoiceBox.new({}, function() end).anchor, nil,
|
|
"a bare choice box does not anchor itself to the window edge")
|
|
T.eq(ChoiceBox.new({}, function() end, { defaultNo = true }).anchor, nil,
|
|
"and neither does one that only asked to start on NO")
|
|
|
|
-- ...but the one a dialogue box opens does, because that box is anchored too
|
|
local game = { save = { player = {} }, data = { text = {} } }
|
|
game.stack = {
|
|
states = {},
|
|
push = function(self, s) table.insert(self.states, s) end,
|
|
pop = function(self) return table.remove(self.states) end,
|
|
top = function(self) return self.states[#self.states] end,
|
|
}
|
|
game.input = {
|
|
wasPressed = function() return false end,
|
|
isDown = function() return false end,
|
|
}
|
|
|
|
local box = TextBox.new(game, "Shall we heal\nyour POKEMON?", nil,
|
|
{ choice = function() end })
|
|
game.stack:push(box)
|
|
for _ = 1, 600 do
|
|
if box.done then break end
|
|
box:update(1 / 60)
|
|
end
|
|
T.check(box.done, "the question finished typing")
|
|
box:update(1 / 60) -- the update after done is the one that pushes the choice
|
|
T.eq(#game.stack.states, 2, "the dialogue box opened its YES/NO")
|
|
T.eq(game.stack:top().anchor, "bottom",
|
|
"a choice box riding a dialogue box shares its bottom anchor")
|
|
|
|
-- ...and inside a battle even THAT one stays put, because the battle is the
|
|
-- screen: the caught-mon nickname prompt prints on the blanked battle field
|
|
-- (BattleState.blankForAskName), and docking it to the window edge drops it a
|
|
-- whole letterbox below what it is printed on.
|
|
T.eq(BattleState.holdsUIAnchors, true, "a battle composes its own screen")
|
|
T.eq(Game.uiAnchorsHeldInStack(stack(overworld, worldBattle)), true,
|
|
"so the anchors are held while it is up")
|
|
T.eq(Game.uiAnchorsHeldInStack(stack(overworld, worldBattle, {})), true,
|
|
"including for the text box and YES/NO it opens above itself")
|
|
T.eq(Game.uiAnchorsHeldInStack(stack(overworld)), false,
|
|
"the overworld's own dialogue box still docks to the screen edge")
|
|
T.eq(Game.uiAnchorsHeldInStack(nil), false, "and no stack is safe")
|
|
|
|
Renderer.uiAnchors = nil
|
|
Renderer.uiAnchorHold = true
|
|
Renderer:setUIAnchor(0, 96, 160, 48, "bottom")
|
|
T.eq(Renderer.uiAnchors, nil, "a held anchor never reaches the frame")
|
|
Renderer.uiAnchorHold = false
|
|
Renderer:setUIAnchor(0, 96, 160, 48, "bottom")
|
|
T.eq(#(Renderer.uiAnchors or {}), 1, "and an unheld one still does")
|
|
Renderer.uiAnchors = nil
|
|
|
|
T.finish("battle fixed menu scale")
|