diff --git a/src/ui/BagMenu.lua b/src/ui/BagMenu.lua index 3d169c0b..1205dde6 100644 --- a/src/ui/BagMenu.lua +++ b/src/ui/BagMenu.lua @@ -372,6 +372,8 @@ function BagMenu.new(game, opts) list = ListMenu.new(game, "ITEMS", buildItems(game), { kind = "bag", footer = ("¥%d"):format(game.save.money), + -- B returns to the start menu when the bag was opened from it + onCancel = opts.onCancel, -- SELECT reorders items like the original bag (swap_items.asm) onSelectKey = function(item, l) if not item then return end diff --git a/src/ui/OptionsMenu.lua b/src/ui/OptionsMenu.lua index e5c1e815..fe7f8463 100644 --- a/src/ui/OptionsMenu.lua +++ b/src/ui/OptionsMenu.lua @@ -328,7 +328,8 @@ local function buildRows(game) return rows end -function OptionsMenu.new(game) +function OptionsMenu.new(game, opts) + opts = opts or {} local rows = buildRows(game) local hooked = Runtime.call("ui.options.rows", sameRows, game, rows) if type(hooked) == "table" then @@ -337,8 +338,8 @@ function OptionsMenu.new(game) Logger.error("ui.options.rows returned %s; keeping the vanilla rows", type(hooked)) end - return setmetatable({ game = game, rows = rows, index = 1, scroll = 0 }, - OptionsMenu) + return setmetatable({ game = game, rows = rows, index = 1, scroll = 0, + onCancel = opts.onCancel }, OptionsMenu) end function OptionsMenu:update(dt) @@ -361,9 +362,11 @@ function OptionsMenu:update(dt) changed = row.step(self.game, dir) and true or false elseif input:wasPressed("a") then -- CANCEL self.game.stack:pop() + if self.onCancel then self.onCancel() end end elseif input:wasPressed("b") or input:wasPressed("start") then self.game.stack:pop() + if self.onCancel then self.onCancel() end end if changed and self.game.writeOptions then self.game:writeOptions() diff --git a/src/ui/PlayerPC.lua b/src/ui/PlayerPC.lua index 1f11aebe..06a21f53 100644 --- a/src/ui/PlayerPC.lua +++ b/src/ui/PlayerPC.lua @@ -160,9 +160,12 @@ end function PlayerPC.new(game) game.save.pcItems = game.save.pcItems or {} return Menu.new(game, { - { label = "WITHDRAW ITEM", onSelect = function() withdraw(game) end }, - { label = "DEPOSIT ITEM", onSelect = function() deposit(game) end }, - { label = "TOSS ITEM", onSelect = function() toss(game) end }, + -- keepOpen so B in the item lists returns here instead of dropping the + -- whole PC session (players_pc.asm re-shows the PC menu); same pattern + -- as BoxMenu's rows + { label = "WITHDRAW ITEM", keepOpen = true, onSelect = function() withdraw(game) end }, + { label = "DEPOSIT ITEM", keepOpen = true, onSelect = function() deposit(game) end }, + { label = "TOSS ITEM", keepOpen = true, onSelect = function() toss(game) end }, { label = "LOG OFF" }, -- silent PC session (BIT_NO_MENU_BUTTON_SOUND); players_pc.asm -- PlayersPCMenu TextBoxBorder (0,0) b=8 c=14 → 16x10 diff --git a/src/ui/PokedexMenu.lua b/src/ui/PokedexMenu.lua index b5bceb2d..7902c93c 100644 --- a/src/ui/PokedexMenu.lua +++ b/src/ui/PokedexMenu.lua @@ -9,7 +9,8 @@ function PokedexMenu:sgbPalettes(game) return require("src.render.PaletteFX").wholeNamed(game.data, "BROWNMON") end -function PokedexMenu.new(game) +function PokedexMenu.new(game, opts) + opts = opts or {} local dex = game.save.pokedex or { seen = {}, owned = {} } local byDex = {} for species, def in pairs(game.data.pokemon) do @@ -47,6 +48,7 @@ function PokedexMenu.new(game) local list = ListMenu.new(game, "POKéDEX", items, { footer = ("SEEN %d OWNED %d"):format(seen, owned), pageJump = true, -- Left/Right page jumps like the original + onCancel = opts.onCancel, -- B returns to the start menu when opened from it onChoose = function(item) if not item.value then return end -- the DATA / CRY / AREA / QUIT choice (engine/menus/pokedex.asm diff --git a/src/ui/StartMenu.lua b/src/ui/StartMenu.lua index aa1cc93a..7a0db47a 100644 --- a/src/ui/StartMenu.lua +++ b/src/ui/StartMenu.lua @@ -19,10 +19,15 @@ function StartMenu.new(game) local flags = game.save.flags or {} local items = {} + -- vanilla start submenus return here on B (RedisplayStartMenu): the + -- generic Menu pops the start menu when a row is selected, so each + -- submenu gets an onCancel that re-opens it + local function reopen() Screens.push(game, "StartMenu") end + -- POKéDEX: only after Oak hands it over if flags.EVENT_GOT_POKEDEX then table.insert(items, { label = "POKéDEX", onSelect = function() - Screens.push(game, "PokedexMenu") + Screens.push(game, "PokedexMenu", { onCancel = reopen }) end }) end @@ -30,17 +35,17 @@ function StartMenu.new(game) -- an empty party; selecting it then just no-ops) table.insert(items, { label = "POKéMON", onSelect = function() if #game.save.party == 0 then return end - Screens.push(game, "PartyMenu") + Screens.push(game, "PartyMenu", { onCancel = reopen }) end }) table.insert(items, { label = "ITEM", onSelect = function() - Screens.push(game, "BagMenu") + Screens.push(game, "BagMenu", { onCancel = reopen }) end }) -- the player's name opens the trainer card (StartMenu_TrainerInfo) table.insert(items, { label = game.save.player.name or "RED", onSelect = function() - Screens.push(game, "TrainerCard") + Screens.push(game, "TrainerCard", { onCancel = reopen }) end }) -- SAVE shows the player/badges/dex/time panel then asks to confirm @@ -74,7 +79,7 @@ function StartMenu.new(game) end }) table.insert(items, { label = "OPTION", onSelect = function() - Screens.push(game, "OptionsMenu") + Screens.push(game, "OptionsMenu", { onCancel = reopen }) end }) -- LINK needs a party diff --git a/src/ui/TrainerCard.lua b/src/ui/TrainerCard.lua index 7493d2c5..0ef3d33b 100644 --- a/src/ui/TrainerCard.lua +++ b/src/ui/TrainerCard.lua @@ -30,8 +30,10 @@ local function quads16(img, count, stride, x0, y0) return q end -function TrainerCard.new(game) - local self = setmetatable({ game = game }, TrainerCard) +function TrainerCard.new(game, opts) + opts = opts or {} + local self = setmetatable({ game = game, onCancel = opts.onCancel }, + TrainerCard) local img = tryImage("assets/generated/trainer_card/badges.png") if img then -- badges.2bpp is 8 stacked [face, badge] pairs (DrawBadges FaceBadgeTiles) @@ -66,8 +68,12 @@ end function TrainerCard:update(dt) local input = self.game.input + -- either button dismisses the card back to the start menu + -- (StartMenu_TrainerInfo: WaitForTextScrollButtonPress then + -- RedisplayStartMenu) if input:wasPressed("a") or input:wasPressed("b") then self.game.stack:pop() + if self.onCancel then self.onCancel() end end end diff --git a/tests/mod_ui_tests.lua b/tests/mod_ui_tests.lua index 4b079f2d..4199e79b 100644 --- a/tests/mod_ui_tests.lua +++ b/tests/mod_ui_tests.lua @@ -185,6 +185,83 @@ check(#menu.items == #VANILLA_START and menu.items[3].label == "ITEM", check(logged("ui.start_menu.items returned"), "the degrade is logged") hooks:removeOwner("bad") +-- ------- issue #270: B in a start submenu returns to the start menu +-- (RedisplayStartMenu), not to the overworld. The generic Menu pops the +-- start menu when a row is selected, so every vanilla submenu must carry +-- an onCancel that re-opens it. +local function gameForSubmenus() + local game = startGame() + game.save.inventory = {} + game.save.money = 0 + game.save.pcItems = { POTION = 1 } + game.data.items = { POTION = { name = "POTION" } } + game.data.pokemon = {} + game.data.constants = {} + game.data.rulesets = {} + game.modStatus = { available = {} } + return game +end + +local MenuClass = require("src.ui.Menu") +for _, rowLabel in ipairs({ "POKéDEX", "POKéMON", "ITEM", "RED", "OPTION" }) do + local game = gameForSubmenus() + local start = Screens.push(game, "StartMenu") + for i, item in ipairs(start.items) do + if item.label == rowLabel then start.index = i end + end + press(start, "a") + local sub = game.stack:top() + check(sub ~= start, rowLabel .. " opens a submenu over the overworld") + press(sub, "b") + local top = game.stack:top() + check(top ~= nil and top ~= sub and getmetatable(top) == MenuClass + and top.items[1].label == "POKéDEX", + "B from " .. rowLabel .. " re-opens the start menu") + check(#game.stack.states == 1, + "B from " .. rowLabel .. " leaves exactly the start menu on the stack") + check(top.index == start.index, + "the re-opened start menu restores the " .. rowLabel .. " cursor row") +end + +-- the trainer card also dismisses on A, back to the start menu +local cardGame = gameForSubmenus() +local cardStart = Screens.push(cardGame, "StartMenu") +for i, item in ipairs(cardStart.items) do + if item.label == "RED" then cardStart.index = i end +end +press(cardStart, "a") +press(cardGame.stack:top(), "a") +check(getmetatable(cardGame.stack:top()) == MenuClass + and cardGame.stack:top().items[1].label == "POKéDEX", + "A on the trainer card also returns to the start menu") + +-- the options CANCEL row takes the same path as B +local optCancelGame = gameForSubmenus() +local optStart = Screens.push(optCancelGame, "StartMenu") +for i, item in ipairs(optStart.items) do + if item.label == "OPTION" then optStart.index = i end +end +press(optStart, "a") +local optMenu = optCancelGame.stack:top() +optMenu.index = #optMenu.rows + 1 -- CANCEL +press(optMenu, "a") +check(getmetatable(optCancelGame.stack:top()) == MenuClass + and optCancelGame.stack:top().items[1].label == "POKéDEX", + "the options CANCEL row returns to the start menu") + +-- the PC session (players_pc.asm): B in an item list returns to the PC's +-- root menu, which stays on the stack via keepOpen rows +local pcGame = gameForSubmenus() +local pcRoot = Screens.push(pcGame, "PlayerPC") +press(pcRoot, "a") -- WITHDRAW ITEM +check(#pcGame.stack.states == 2, + "WITHDRAW ITEM keeps the PC root menu underneath") +press(pcGame.stack:top(), "b") +check(pcGame.stack:top() == pcRoot, + "B in the withdraw list returns to the PC root menu") +press(pcRoot, "b") +check(#pcGame.stack.states == 0, "B on the PC root logs off to the overworld") + -- ------- ui.options.rows and the descriptor refactor local OptionsMenu = require("src.ui.OptionsMenu") local function optGame()