diff --git a/src/battle/BattleState.lua b/src/battle/BattleState.lua index f0a5403f..6cee785a 100644 --- a/src/battle/BattleState.lua +++ b/src/battle/BattleState.lua @@ -145,6 +145,13 @@ function BattleState:statusHUDVisible() self) ~= false end +function BattleState:moveGridNavigation() + if self:wideLayout() then return true end + if not Runtime.wantsHook("battle.move_grid_navigation") then return false end + return Runtime.call("battle.move_grid_navigation", function() return false end, + self) == true +end + local Rulesets = { gen1_faithful = require("src.battle.rulesets.gen1_faithful"), modern_clean = require("src.battle.rulesets.modern_clean"), @@ -1994,7 +2001,7 @@ function BattleState:update(dt) -- The widescreen layout lays the four slots out as a 2x2 grid, so all -- four directions navigate it; nil means no direction was pressed and -- A / B / SELECT below behave the same in either layout. - local grid = self:wideLayout() + local grid = self:moveGridNavigation() and WideBattle.navigate(self.moveIndex, #moves, input) if grid then self.moveIndex = grid @@ -2044,7 +2051,7 @@ function BattleState:update(dt) local moves = self.mimicMoves -- the copy menu shares the widescreen move grid, so it navigates the -- same way there (the classic layout keeps the vertical list) - local grid = self:wideLayout() + local grid = self:moveGridNavigation() and WideBattle.navigate(self.mimicIndex, #moves, input) if grid then self.mimicIndex = grid diff --git a/src/ui/PartyMenu.lua b/src/ui/PartyMenu.lua index 9882c67e..33df1d36 100644 --- a/src/ui/PartyMenu.lua +++ b/src/ui/PartyMenu.lua @@ -107,6 +107,23 @@ PartyMenu.iconFrames = { PIKACHU = { rest = 0, alt = 3 }, -- Yellow: PikachuSprite tile 0 <-> 12 } +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 + -- Which 16x16 frame of `name`'s sheet to draw; `ih` (sheet pixel -- height) only matters for the fallback, which keeps the old uniform -- behavior for icons outside the table (BALL/HELIX y-bob instead). @@ -307,6 +324,13 @@ function PartyMenu:close() if self.game.stack:top() == self then self.game.stack:pop() end 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 + function PartyMenu:update(dt) -- icon animation counter; 320 = a whole cycle at every HP speed self.blink = ((self.blink or 0) + 1) % 320 @@ -531,7 +555,18 @@ function PartyMenu:update(dt) return end - 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, #party, direction) + end + if grid then + self.index = grid + self.game.partyMenuSavedIndex = self.index + elseif input:wasPressed("up") then self.index = self.index > 1 and self.index - 1 or math.max(1, #party) self.game.partyMenuSavedIndex = self.index -- HandlePartyMenuInput #768 elseif input:wasPressed("down") then diff --git a/tests/mod_qol_hooks_tests.lua b/tests/mod_qol_hooks_tests.lua index 6fd63347..dfa425b2 100644 --- a/tests/mod_qol_hooks_tests.lua +++ b/tests/mod_qol_hooks_tests.lua @@ -12,6 +12,7 @@ local Zoom = require("src.render.Zoom") local ListMenu = require("src.ui.ListMenu") local NamingScreen = require("src.ui.NamingScreen") local TextBox = require("src.render.TextBox") +local PartyMenu = require("src.ui.PartyMenu") local Player = require("src.world.Player") local Music = require("src.core.Music") @@ -192,6 +193,41 @@ do "battle status HUD returns when the hook is removed") end +-- ------- grid navigation ownership (alternate menu renderers) + +do + local BattleState = require("src.battle.BattleState") + local battle = { wideLayout = function() return false end } + check(not BattleState.moveGridNavigation(battle), + "classic move navigation stays a list without a mod") + local unsub = wrap("battle.move_grid_navigation", function() return true end) + check(BattleState.moveGridNavigation(battle), + "a mod can opt the classic move menu into grid navigation") + unsub() + battle.wideLayout = function() return true end + check(BattleState.moveGridNavigation(battle), + "the native wide move grid remains enabled without a mod") + + local game = { + save = { party = { {}, {}, {}, {}, {}, {} } }, + input = { wasPressed = function(_, key) return key == "down" end }, + } + local field = PartyMenu.new(game) + unsub = wrap("ui.party.grid_navigation", function() return true end) + field:update(0) + check(field.index == 2, + "field party navigation remains a native list when the hook is active") + game.partyMenuSavedIndex = nil + local menu = PartyMenu.new(game, { battle = {} }) + menu:update(0) + check(menu.index == 3 and game.partyMenuSavedIndex == 3, + "a battle party can follow and remember a companion grid") + unsub() + menu:update(0) + check(menu.index == 4, + "removing the hook restores native list navigation immediately") +end + -- ------- music.volume (distance / indoor muffling) do