diff --git a/docs/mod-api-gen2-compat.md b/docs/mod-api-gen2-compat.md index e412b4d2..1994d8a9 100644 --- a/docs/mod-api-gen2-compat.md +++ b/docs/mod-api-gen2-compat.md @@ -512,8 +512,9 @@ gains a field instead of the name gaining a prefix. id under Gen 1's `name` key, which is the one payload difference the numeric flag space forces. - *Menus (`src/ui/gen2/`):* `ui.start_menu.items`, `ui.title_menu.items`, - `ui.options.rows`, `ui.party.submenu`, `ui.naming.grid`, `ui.pc.items`, - `ui.list_menu`, `transition.style`. `ui.list_menu` covers Gold's script + `ui.options.rows`, `ui.party.submenu`, `ui.party.grid_navigation`, + `ui.naming.grid`, `ui.pc.items`, `ui.list_menu`, `transition.style`. + `ui.list_menu` covers Gold's script menus (`ScriptMenu.lua`); the `Chrome.List` widget the START and title menus draw with does not raise it yet, so those two are composed through their own hooks only. diff --git a/src/ui/gen2/BattleState.lua b/src/ui/gen2/BattleState.lua index 9254d2b9..c4a4de4e 100644 --- a/src/ui/gen2/BattleState.lua +++ b/src/ui/gen2/BattleState.lua @@ -2290,6 +2290,7 @@ function BattleState:openParty(forced) -- :2702; engine/pokemon/party_menu.asm:660-679). Only the voluntary list -- carries BattleMonMenu; PickPartyMonInBattle has no submenu. prompt = forced and "which" or "choose", + battle = true, battleSubmenu = not forced, onCancel = function() stack:pop() @@ -2762,6 +2763,7 @@ function BattleState:openShiftParty() self.phase = "submenu" Screens.push(self.game, "Gen2PartyMenu", { prompt = "which", + battle = true, onCancel = function() stack:pop() self.phase = "resolving" @@ -3135,6 +3137,7 @@ function BattleState:useOnPartyMon(itemId, action) self.phase = "submenu" Screens.push(self.game, "Gen2PartyMenu", { prompt = "useItem", + battle = true, party = self.battle.party or (self.save and self.save.party), onCancel = function() stack:pop() diff --git a/src/ui/gen2/PartyMenu.lua b/src/ui/gen2/PartyMenu.lua index 107c05a7..6cae3190 100644 --- a/src/ui/gen2/PartyMenu.lua +++ b/src/ui/gen2/PartyMenu.lua @@ -76,6 +76,23 @@ local BATTLE_SUBMENU_LEFT, BATTLE_SUBMENU_TOP = 11, 11 -- HP bar is 6 tiles wide (48px) in the party list. +local function gridIndex(index, count, direction) + if count < 1 then return nil end + local row, col = math.floor((index - 1) / 2), (index - 1) % 2 + if direction == "left" or direction == "right" then + local other = row * 2 + (1 - col) + 1 + return other <= count and other or index + end + local step = direction == "up" and -1 or direction == "down" and 1 + if not step then return nil end + local rows = math.ceil(count / 2) + for offset = 1, rows do + local other = ((row + step * offset) % rows) * 2 + col + 1 + if other <= count then return other end + end + return index +end + function PartyMenu:wantsFillScale() return true end function PartyMenu:drawsWidescreen() return true end @@ -114,6 +131,7 @@ function PartyMenu.new(game, opts) self.wantsSubmenu = opts.submenu == true -- BattleMenu_PKMN's `callfar BattleMonMenu` (engine/battle/core.asm:4810). self.wantsBattleSubmenu = opts.battleSubmenu == true + self.battle = opts.battle == true self.submenu = nil -- The held slot while SwitchPartyMons' second pick is open; nil otherwise. self.switchFrom = nil @@ -146,6 +164,13 @@ function PartyMenu:isCancel() return self.index > #self.party end +function PartyMenu:gridNavigation() + if not self.battle + or not Runtime.wantsHook("ui.party.grid_navigation") then return false end + return Runtime.call("ui.party.grid_navigation", function() return false end, + self) == true +end + -- ------------------------------------------------------------- mon submenu -- GetMonSubmenuItems, in its own order: every field move the mon knows first, @@ -477,7 +502,18 @@ function PartyMenu:update(_dt) return end local total = self:count() - if input:wasPressed("up") then + local grid + if self:gridNavigation() then + local direction = input:wasPressed("left") and "left" + or input:wasPressed("right") and "right" + or input:wasPressed("up") and "up" + or input:wasPressed("down") and "down" + grid = gridIndex(self.index, #self.party, direction) + end + if grid then + self.index = grid + self:storeCursor() + elseif input:wasPressed("up") then self.index = self.index > 1 and self.index - 1 or total elseif input:wasPressed("down") then self.index = self.index < total and self.index + 1 or 1 diff --git a/tests/engine/gate_gen2_mod_api.lua b/tests/engine/gate_gen2_mod_api.lua index 68cff2e8..0da5fc1e 100644 --- a/tests/engine/gate_gen2_mod_api.lua +++ b/tests/engine/gate_gen2_mod_api.lua @@ -397,7 +397,8 @@ local GEN2_HOOKS = { "world.tod", "map.palette", "fieldmove.eligibility", -- menus and the battle intro "ui.start_menu.items", "ui.title_menu.items", "ui.options.rows", - "ui.party.submenu", "ui.naming.grid", "ui.pc.items", "ui.list_menu", + "ui.party.submenu", "ui.party.grid_navigation", "ui.naming.grid", + "ui.pc.items", "ui.list_menu", "transition.style", -- battle "battle.damage", "battle.crit", "battle.accuracy", "battle.turn_order", diff --git a/tests/mod_qol_hooks_tests.lua b/tests/mod_qol_hooks_tests.lua index d4b82643..d065c079 100644 --- a/tests/mod_qol_hooks_tests.lua +++ b/tests/mod_qol_hooks_tests.lua @@ -14,6 +14,7 @@ local NamingScreen = require("src.ui.NamingScreen") local TextBox = require("src.render.TextBox") local ChoiceBox = require("src.ui.ChoiceBox") local PartyMenu = require("src.ui.PartyMenu") +local Gen2PartyMenu = require("src.ui.gen2.PartyMenu") local Player = require("src.world.Player") local Music = require("src.core.Music") @@ -295,6 +296,16 @@ do menu:update(0) check(menu.index == 4, "removing the hook restores native list navigation immediately") + + local gold = Gen2PartyMenu.new(game, { battle = true }) + unsub = wrap("ui.party.grid_navigation", function() return true end) + gold:update(0) + check(gold.index == 3, + "a Gold battle party can follow the same companion grid") + unsub() + gold:update(0) + check(gold.index == 4, + "Gold restores native party list navigation without the hook") end -- ------- music.volume (distance / indoor muffling)