diff --git a/src/ui/Menu.lua b/src/ui/Menu.lua index feb578f7..027fa7e7 100644 --- a/src/ui/Menu.lua +++ b/src/ui/Menu.lua @@ -19,7 +19,16 @@ function Menu.new(game, items, opts) self.tx = opts.tx or 10 self.ty = opts.ty or 0 self.tw = opts.tw or 10 - self.th = opts.th or (#items * 2 + 2) + self.rowStep = opts.rowStep or 2 + -- maxVisible: cap the box to this many rows and scroll the rest instead + -- of growing past it (e.g. the start menu, whose row count varies with + -- save state and mod hooks); nil/unset keeps every caller's old + -- behavior of sizing the box to fit all items. + self.maxVisible = opts.maxVisible + self.scroll = 0 + local visible = (self.maxVisible and math.min(self.maxVisible, #items)) + or #items + self.th = opts.th or (visible * self.rowStep + 2) self.cancelable = opts.cancelable ~= false -- Whether START closes the menu. In pokered a menu responds only to the -- keys in its wMenuWatchedKeys mask; the common PAD_A | PAD_B (and the @@ -31,9 +40,25 @@ function Menu.new(game, items, opts) -- BIT_NO_MENU_BUTTON_SOUND (wMiscFlags): the PC session runs its -- menus silent (home/window.asm HandleMenuInput_) self.noSound = opts.noSound or false + self:clampScroll() return self end +-- keeps self.index inside the visible [scroll+1, scroll+maxVisible] window; +-- callers that move self.index directly (e.g. restoring a saved cursor +-- position) should call this afterwards to scroll it into view +function Menu:clampScroll() + if not (self.maxVisible and #self.items > self.maxVisible) then + self.scroll = 0 + return + end + if self.index - self.scroll > self.maxVisible then + self.scroll = self.index - self.maxVisible + elseif self.index - self.scroll < 1 then + self.scroll = self.index - 1 + end +end + function Menu:update(dt) local input = self.game.input if input:wasPressed("up") then @@ -61,15 +86,29 @@ function Menu:update(dt) self.game.stack:pop() if self.onCancel then self.onCancel() end end + self:clampScroll() end function Menu:draw() Font.drawBox(self.tx, self.ty, self.tw, self.th) love.graphics.setColor(0, 0, 0, 1) - for i, item in ipairs(self.items) do - Font.draw(item.label, (self.tx + 2) * 8, (self.ty + i * 2 - 1) * 8) + local visible = (self.maxVisible and math.min(self.maxVisible, #self.items)) + or #self.items + for row = 1, visible do + local item = self.items[self.scroll + row] + if not item then break end + Font.draw(item.label, (self.tx + 2) * 8, + (self.ty + row * self.rowStep - (self.rowStep - 1)) * 8) + end + local cursorRow = self.index - self.scroll + Font.drawCode(Theme.cursor, (self.tx + 1) * 8, + (self.ty + cursorRow * self.rowStep - (self.rowStep - 1)) * 8) + -- moreArrow ($EE): the same "more below" glyph OptionRows/ManagerState + -- use, sat on the bottom border like TextBox's page-advance cursor + if self.maxVisible and self.scroll + self.maxVisible < #self.items then + Font.drawCode(Theme.moreArrow, (self.tx + self.tw - 2) * 8, + (self.ty + self.th - 2) * 8) end - Font.drawCode(Theme.cursor, (self.tx + 1) * 8, (self.ty + self.index * 2 - 1) * 8) love.graphics.setColor(1, 1, 1, 1) end diff --git a/src/ui/StartMenu.lua b/src/ui/StartMenu.lua index 35eb5640..aa1cc93a 100644 --- a/src/ui/StartMenu.lua +++ b/src/ui/StartMenu.lua @@ -7,6 +7,7 @@ local Font = require("src.render.Font") local Logger = require("src.core.Logger") local Menu = require("src.ui.Menu") +local Renderer = require("src.render.Renderer") local Runtime = require("src.mods.Runtime") local Screens = require("src.ui.Screens") @@ -117,11 +118,20 @@ function StartMenu.new(game) -- the start menu's mask is PAD_DOWN | PAD_UP | PAD_START | PAD_B | PAD_A -- (engine/menus/draw_start_menu.asm), so START closes it back to the -- overworld -- unlike most menus, whose masks omit PAD_START. + -- + -- item count isn't fixed: POKéDEX/LINK/MODS come and go with save state, + -- and mods can append their own rows through the hook above, so the + -- double-spaced box (the original's style) can grow past the 18-tile + -- canvas. Cap it at however many rows actually fit and scroll the rest, + -- with Menu's moreArrow showing while there's more below. + local rowStep = 2 + local maxVisible = math.floor((Renderer.HEIGHT / 8 - 2) / rowStep) local menu = Menu.new(game, items, - { tx = 9, ty = 0, tw = 11, th = #items * 2 + 2, startCloses = true }) + { tx = 9, ty = 0, tw = 11, maxVisible = maxVisible, startCloses = true }) -- the cursor position survives closing the menu -- (wBattleAndStartSavedMenuItem, home/start_menu.asm) menu.index = math.min(game.save.startMenuIndex or 1, #items) + menu:clampScroll() local baseUpdate = menu.update menu.update = function(self, dt) baseUpdate(self, dt)