diff --git a/src/ui/StartMenu.lua b/src/ui/StartMenu.lua index f4c6d5c8..65f01166 100644 --- a/src/ui/StartMenu.lua +++ b/src/ui/StartMenu.lua @@ -70,6 +70,9 @@ function StartMenu.new(game) -- leaves it up under the prompt -- engine/menus/main_menu.asm:381-405 local panel panel = { + -- the panel overlaps the kept-open START menu box (start_sub_menus.asm: + -- 641-647), so neither can be docked to a screen edge on its own + holdsUIAnchors = true, delay = 0, update = function() -- ld c, 30 / jp DelayFrames: the bare panel holds before the diff --git a/src/ui/TitleState.lua b/src/ui/TitleState.lua index 6abe6074..5c48711d 100644 --- a/src/ui/TitleState.lua +++ b/src/ui/TitleState.lua @@ -43,6 +43,28 @@ local function withWhiteOf(pal, ref) return { ref[1], pal[2], pal[3], pal[4] } end +-- Every drawn box, not just the topmost state's: DisplayContinueGameInfo +-- leaves the menu box up behind the info window (main_menu.asm:36-39), so both +-- are on screen and both need the overlay below. +local function titleUiBoxes(game) + local stack = game and game.stack + local states = stack and stack.states + if not states then + local top = stack and stack.top and stack:top() + local box = top and top.titleUiBox + return box and { box } or {} + end + local boxes = {} + for i = (stack.visibleBase and stack:visibleBase() or 1), #states do + local state = states[i] + local shown = not stack.renderVisible or stack:renderVisible(state) + if shown and state and state.titleUiBox then + boxes[#boxes + 1] = state.titleUiBox + end + end + return boxes +end + function TitleState:sgbPalettes(game) local P = require("src.render.PaletteFX") local z @@ -65,16 +87,14 @@ function TitleState:sgbPalettes(game) P.zone(P.pal(game.data, "MEWMON"), 0, 10, 19, 17), } end - local top = game.stack and game.stack:top() - local box = top and top.titleUiBox - if box then - -- A DMG-grays zone, not the trueColor opt-out: through the shade-remap - -- shader GRAYS is the identity for the box's four shades, so SGB / - -- ADVANCED / OG modes keep #133's white paper and black ink exactly, - -- while effectiveColors still substitutes the mono and inverted display - -- modes -- a trueColor rect skipped the shader entirely, leaving the - -- main menu and CONTINUE info box a raw white hole over a CLASSIC - -- pea-green title instead of matching it like the START menu does (#870). + -- A DMG-grays zone, not the trueColor opt-out: through the shade-remap + -- shader GRAYS is the identity for the box's four shades, so SGB / + -- ADVANCED / OG modes keep #133's white paper and black ink exactly, + -- while effectiveColors still substitutes the mono and inverted display + -- modes -- a trueColor rect skipped the shader entirely, leaving the + -- main menu and CONTINUE info box a raw white hole over a CLASSIC + -- pea-green title instead of matching it like the START menu does (#870). + for _, box in ipairs(titleUiBoxes(game)) do z[#z + 1] = P.zone(P.GRAYS, box[1], box[2], box[3], box[4]) end return z[3] and z or nil @@ -175,18 +195,19 @@ end local function replayObjSprite(game, image, quad, x, y) local P = require("src.render.PaletteFX") if not P.usesSpriteObp() then return end - local top = game.stack and game.stack:top() - local box = top and top.titleUiBox - if box then + local boxes = titleUiBoxes(game) + if boxes[1] then local w, h if quad then w, h = select(3, quad:getViewport()) else w, h = image:getDimensions() end - if x < (box[3] + 1) * 8 and x + w > box[1] * 8 - and y < (box[4] + 1) * 8 and y + h > box[2] * 8 then - return + for _, box in ipairs(boxes) do + if x < (box[3] + 1) * 8 and x + w > box[1] * 8 + and y < (box[4] + 1) * 8 and y + h > box[2] * 8 then + return + end end end P.markUiSpriteRedraw(image, quad, x, y) diff --git a/tests/engine/title_zone_seams.lua b/tests/engine/title_zone_seams.lua index edee845f..e416c8f6 100644 --- a/tests/engine/title_zone_seams.lua +++ b/tests/engine/title_zone_seams.lua @@ -236,6 +236,32 @@ do same(z[2].colors[3], BLUE_LOGO1[3], "Blue LOGO1's other inks stay put") end +-- #133's grays overlay follows every box on screen, not just the topmost +-- state's: DisplayContinueGameInfo leaves the menu box up behind the info +-- window (main_menu.asm:36-39), and reading only the top left the menu box +-- on the raw LOGO2 / LOGO1 bands -- blue rows over a red EXIT GAME row. +do + PaletteFX.mode = "gbc" + GameVersion.set("red") + local stack = { + states = { { isOpaque = true }, + { titleUiBox = { 0, 0, 12, 9 } }, + { titleUiBox = { 4, 7, 19, 16 } } }, + visibleBase = function() return 1 end, + top = function(self) return self.states[#self.states] end, + } + local z = title:sgbPalettes({ data = romPack(RED_LOGO1), stack = stack }) + eq(#z, 5, "both open boxes get an overlay zone") + eq(z[4].x, 0, "the menu box keeps its overlay while the info window is up") + eq(z[4].y, 0, "at the menu box's own origin") + eq(z[5].x, 32, "and the info window's sits on top of it") + eq(z[5].y, 56, "at hlcoord 4,7") + + stack.states[3] = nil + z = title:sgbPalettes({ data = romPack(RED_LOGO1), stack = stack }) + eq(#z, 4, "the menu alone is still one overlay, as before") +end + PaletteFX.mode = savedMode GameVersion.set(savedVersion) diff --git a/tests/engine/ui_layout_option.lua b/tests/engine/ui_layout_option.lua index 7620018d..eba56e80 100644 --- a/tests/engine/ui_layout_option.lua +++ b/tests/engine/ui_layout_option.lua @@ -67,6 +67,37 @@ T.eq(anchorsAfter({ centered = false, hold = true }), 0, "a battle still holds the anchors while DYNAMIC is on") T.eq(anchorsAfter({ centered = true, hold = true }), 0, "and with it off") +-- --------------------------------------------------------- the save panel + +-- PrintSaveScreenText prints at hlcoord 4,0 over the kept-open START menu box +-- at 9,0 (start_sub_menus.asm:641-647); docking the menu alone split it (#1619). +do + local StartMenu = require("src.ui.StartMenu") + local DataFx = T.fixtures.load() + require("src.render.Font").load(DataFx) + local pushed = {} + local panelGame = { + data = DataFx, save = SaveData.newGame(), + stack = { states = pushed, + push = function(_, s) pushed[#pushed + 1] = s end, + pop = function() end, + top = function() return pushed[#pushed] end }, + } + panelGame.save.player.name = "RED" + local menu = StartMenu.new(panelGame) + pushed[#pushed + 1] = menu + local saveRow + for _, item in ipairs(menu.items) do + if tostring(item.label):match("SAVE") then saveRow = item end + end + T.check(saveRow ~= nil, "the START menu carries a SAVE row") + T.eq(Game.uiAnchorsHeldInStack({ states = pushed }), false, + "the START menu alone still docks") + saveRow.onSelect() + T.eq(Game.uiAnchorsHeldInStack({ states = pushed }), true, + "the SAVE panel holds the anchors so it cannot be split from the menu") +end + -- ------------------------------------------------- the scale half of it -- CENTERED is a FIXED letterbox, so the UI must not follow the survey zoom