Merge pull request #1055 from AverageConsumer/codex/mod-grid-navigation-hooks

Mod API: allow alternate battle grids to own navigation
This commit is contained in:
bryanthaboi
2026-08-10 17:08:24 -04:00
committed by GitHub
3 changed files with 81 additions and 3 deletions
+9 -2
View File
@@ -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
+36 -1
View File
@@ -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
+36
View File
@@ -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