Files
gen1recomp/src/ui/ListMenu.lua
T
2026-07-23 15:39:35 -04:00

172 lines
5.8 KiB
Lua

-- Generic full-screen scrollable list: items are { label=..., right=...,
-- value=... }; onChoose(item) / onCancel(). Used by the bag, shops, the
-- box and the Pokédex.
local Font = require("src.render.Font")
local Theme = require("src.ui.Theme")
local ListMenu = {}
ListMenu.__index = ListMenu
ListMenu.isOpaque = true
-- SGB: generic whole-screen palette (SET_PAL_GENERIC)
function ListMenu:sgbPalettes(game)
return require("src.render.PaletteFX").wholeNamed(game.data, "MEWMON")
end
local ROWS = 7
function ListMenu.new(game, title, items, opts)
opts = opts or {}
local self = setmetatable({}, ListMenu)
self.game = game
self.title = title
self.items = items
self.index = 1
self.scroll = 0
self.onChoose = opts.onChoose
self.onCancel = opts.onCancel
self.footer = opts.footer
self.pageJump = opts.pageJump -- Left/Right move a page at a time
self.onSelectKey = opts.onSelectKey -- SELECT pressed on an item
-- scripted mode (the old man tutorial): update() runs the script
-- every frame INSTEAD of reading input -- DisplayListMenuID's old-man
-- branch (home/list_menu.asm:65-80) never calls HandleMenuInput
self.script = opts.script
-- shop mode: the footer becomes the clerk's line in a framed bottom
-- text box, a money box sits top-right, and the list shortens to
-- clear them (DisplayPokemartDialogue_'s screen)
self.dialogue = opts.dialogue
self.money = opts.money -- () -> current money for the box
self.rows = opts.dialogue and 4 or ROWS
return self
end
function ListMenu:update(dt)
if self.script then
self.script(self)
return
end
local input = self.game.input
if #self.items == 0 then
if input:wasPressed("a") or input:wasPressed("b") then
self.game.stack:pop()
if self.onCancel then self.onCancel() end
end
return
end
if input:wasPressed("up") then
self.index = math.max(1, self.index - 1)
elseif input:wasPressed("down") then
self.index = math.min(#self.items, self.index + 1)
elseif self.pageJump and input:wasPressed("left") then
self.index = math.max(1, self.index - self.rows)
elseif self.pageJump and input:wasPressed("right") then
self.index = math.min(#self.items, self.index + self.rows)
elseif self.onSelectKey and input:wasPressed("select") then
self.onSelectKey(self.items[self.index], self)
elseif input:wasPressed("b") then
self.game.stack:pop()
if self.onCancel then self.onCancel() end
return
elseif input:wasPressed("a") then
local item = self.items[self.index]
if self.onChoose then
self.onChoose(item, self)
end
return
end
if self.index - self.scroll > self.rows then
self.scroll = self.index - self.rows
end
if self.index - self.scroll < 1 then self.scroll = self.index - 1 end
end
-- remove current item (e.g. consumed); keeps cursor valid
function ListMenu:removeCurrent()
table.remove(self.items, self.index)
self.index = math.max(1, math.min(self.index, #self.items))
end
function ListMenu:close()
local top = self.game.stack:top()
if top == self then self.game.stack:pop() end
end
function ListMenu:draw()
love.graphics.setColor(1, 1, 1, 1)
love.graphics.rectangle("fill", 0, 0, 160, 144)
love.graphics.setColor(0, 0, 0, 1)
Font.draw(self.title, 8, 4)
if #self.items == 0 then
Font.draw("Nothing here.", 16, 64)
end
for row = 1, self.rows do
local i = self.scroll + row
local item = self.items[i]
if not item then break end
local y = 8 + row * 16
Font.draw(item.label, 16, y)
if item.ball then -- the Pokédex owned-ball marker tile
local bx = 16 + (#item.label + 1) * 8 + 3
local by = y + 3
love.graphics.circle("fill", bx, by, 3.5)
love.graphics.setColor(1, 1, 1, 1)
love.graphics.rectangle("fill", bx - 3.5, by - 0.5, 7, 1)
love.graphics.circle("fill", bx, by, 1.2)
love.graphics.setColor(0, 0, 0, 1)
end
if item.right then
Font.draw(item.right, 160 - 8 - Font.width(item.right), y)
end
if i == self.index then
-- hollowIndex: a chosen row keeps the hollow '▷' left behind by
-- pokered's PlaceUnfilledArrowMenuCursor (the old man demo's
-- auto A-press, home/list_menu.asm:89-91)
Font.drawCode((self.swapIndex == i or self.hollowIndex == i)
and Theme.cursorHollow or Theme.cursor, 8, y)
end
if self.swapIndex == i and i ~= self.index then
Font.drawCode(Theme.cursorHollow, 8, y) -- ▷ marks the item being moved
end
end
if self.dialogue then
-- money box (DisplayTextBoxID MONEY_BOX, hlcoord 11,0): the amount
-- right-aligned on its middle row
Font.drawBox(11, 0, 9, 3)
love.graphics.setColor(0, 0, 0, 1)
local money = ("¥%d"):format(self.money and self.money() or 0)
Font.draw(money, 152 - Font.width(money), 8)
-- the clerk's line in the standard bottom text box; long prompts
-- wrap and keep their last two lines, like the GB's scrolled box
Font.drawBox(0, 12, 20, 6)
love.graphics.setColor(0, 0, 0, 1)
if self.footer then
local flat = {}
for _, page in ipairs(require("src.render.TextBox").paginate(self.footer)) do
for _, line in ipairs(page) do flat[#flat + 1] = line end
end
local y = 112
for i = math.max(1, #flat - 1), #flat do
Font.draw(flat[i], 8, y)
y = y + 16
end
end
elseif self.footer then
-- PC deposit/withdraw footers use "\n"; draw the last two lines so
-- long item names are not clipped at the screen edge (#115).
local flat = {}
for _, page in ipairs(require("src.render.TextBox").paginate(self.footer)) do
for _, line in ipairs(page) do flat[#flat + 1] = line end
end
local y = (#flat >= 2) and 120 or 136
for i = math.max(1, #flat - 1), #flat do
Font.draw(flat[i], 8, y)
y = y + 16
end
end
love.graphics.setColor(1, 1, 1, 1)
end
return ListMenu