Files
gen1recomp/src/ui/StartMenu.lua
T

236 lines
10 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- The START menu (engine/menus/start_menu.asm): entries appear as they
-- become usable -- POKéDEX once Oak gives it, POKéMON once you have any,
-- SAVE with a confirmation, plus ITEM / OPTION / QUIT. The built
-- item list runs through the ui.start_menu.items hook before the menu
-- opens, so mods insert or remove rows without patching this file.
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")
local Strings = require("src.core.Strings")
local Theme = require("src.ui.Theme")
local StartMenu = {}
local function sameItems(_, items) return items end
function StartMenu.new(game)
local flags = game.save.flags or {}
local items = {}
local menu
-- vanilla start submenus return here on B (RedisplayStartMenu): the
-- generic Menu pops the start menu when a row is selected, so each
-- submenu gets an onCancel that re-opens it
local function reopen() Screens.push(game, "StartMenu") end
-- POKéDEX: only after Oak hands it over
if flags.EVENT_GOT_POKEDEX then
table.insert(items, { label = Strings("POKéDEX"), onSelect = function()
Screens.push(game, "PokedexMenu", { onCancel = reopen })
end })
end
-- POKéMON is always listed (draw_start_menu.asm prints it even with
-- an empty party; selecting it then just no-ops)
table.insert(items, { label = Strings("POKéMON"),
keepOpen = #game.save.party == 0, onSelect = function()
if #game.save.party == 0 then return end
Screens.push(game, "PartyMenu", { onCancel = reopen })
end })
-- StartMenu_Item draws LIST_MENU_BOX over the still-drawn START menu and
-- only redisplays it on the way out (start_sub_menus.asm:302-329) #1745
table.insert(items, { label = Strings("ITEM"), keepOpen = true,
onSelect = function()
Screens.push(game, "BagMenu", { onClose = function()
if menu and game.stack:top() == menu then game.stack:pop() end
end })
end })
-- the player's name opens the trainer card (StartMenu_TrainerInfo)
table.insert(items, { label = game.save.player.name or "RED",
onSelect = function()
Screens.push(game, "TrainerCard", { onCancel = reopen })
end })
-- SAVE shows the player/badges/dex/time panel then asks to confirm
-- (PrintSaveScreenText); StartMenu_SaveReset never clears the START menu
-- box, so it stays on screen beside the panel (start_sub_menus.asm:641-647)
table.insert(items, { label = Strings("SAVE"), keepOpen = true,
onSelect = function()
local TextBox = require("src.render.TextBox")
local badges = require("src.inventory.Badges").count(game.data, game.save)
local owned = 0
for _ in pairs(game.save.pokedex and game.save.pokedex.owned or {}) do
owned = owned + 1
end
-- the panel is a static snapshot; the cart prints it once
-- (main_menu.asm:390-401)
local t = math.floor(game.save.playTime or 0)
-- PrintSaveScreenText draws its own border at hlcoord 4,0 (b=8, c=$e) and
-- leaves it up under the prompt -- engine/menus/main_menu.asm:381-405
local panel
panel = {
-- the panel overlaps the kept-open START menu box (start_sub_menus.asm:
-- 641-647), so neither can be docked to a screen edge on its own
holdsUIAnchors = true,
delay = 0,
update = function()
-- ld c, 30 / jp DelayFrames: the bare panel holds before the
-- prompt (main_menu.asm:404-405)
panel.delay = panel.delay + 1
if panel.delay == 30 then panel.openPrompt() end
end,
draw = function()
Font.drawBox(4, 0, 16, 10)
love.graphics.setColor(0, 0, 0, 1)
Font.draw(Strings("PLAYER"), 5 * 8, 2 * 8)
Font.draw(game.save.player.name or "RED", 12 * 8, 2 * 8)
Font.draw(Strings("BADGES"), 5 * 8, 4 * 8)
Font.draw(("%2d"):format(badges), 17 * 8, 4 * 8)
Font.draw(Strings("POKéDEX"), 5 * 8, 6 * 8)
Font.draw(("%3d"):format(owned), 16 * 8, 6 * 8)
Font.draw(Strings("TIME"), 5 * 8, 8 * 8)
Font.draw(("%3d:%02d"):format(math.floor(t / 3600),
math.floor(t / 60) % 60), 13 * 8, 8 * 8)
love.graphics.setColor(1, 1, 1, 1)
end,
}
local function closePanel()
if game.stack:top() == panel then game.stack:pop() end
-- SaveMenu returns into HoldTextDisplayOpen, not RedisplayStartMenu
-- (start_sub_menus.asm:645-647): the kept-open START menu goes too
if menu and game.stack:top() == menu then game.stack:pop() end
end
panel.openPrompt = function()
game.stack:push(TextBox.new(game,
Strings("Would you like to\nSAVE the game?"), nil, {
-- SaveTheGame_YesOrNo pins its TWO_OPTION_MENU at hlcoord 0, 7 rather
-- than the shared right-hand one -- engine/menus/save.asm:186-192
choiceBox = Theme.saveBox,
choice = function(yes)
if not yes then closePanel() return end
-- SaveMenu .save (engine/menus/save.asm:164-181): "Now saving..."
-- is a bare PlaceString held by DelayFrames 120, then GameSavedText,
-- which ends in `done` and so never reaches TX_PROMPT_BUTTON.
-- Neither page takes a button press (#765); the second waits on
-- SFX_SAVE (PlaySoundWaitForCurrent + WaitForSoundToFinish) and then
-- DelayFrames 30. The write itself is invisible either side of the
-- "Now saving..." hold, so it stays on that box's onDone.
game.stack:push(TextBox.new(game, Strings("Now saving..."), function()
game:writeSave()
game.stack:push(TextBox.new(game,
Strings("%s saved\nthe game!", game.save.player.name or "RED"),
closePanel, { auto = {
sound = function()
return require("src.core.Sound").play(game.data, "Save")
end,
delay = 30,
} }))
end, { auto = { delay = 120 } }))
end,
}))
end
game.stack:push(panel)
end })
table.insert(items, { label = Strings("OPTION"), onSelect = function()
Screens.push(game, "OptionsMenu", { onCancel = reopen })
end })
-- the manager's pause-menu entry (18-mod-manager-ux): gated on at least
-- one discovered mod so a vanilla install's menu is unchanged
local status = game.modStatus
if status and #(status.available or {}) > 0 then
table.insert(items, { label = Strings("MODS"), onSelect = function()
Screens.push(game, "ManagerState")
end })
end
-- the original's EXIT just closed the menu (CloseStartMenu); with a
-- window close button covering that, QUIT instead power-cycles back
-- to the title after a confirm (defaultNo guards accidental quits)
table.insert(items, { label = Strings("QUIT"), onSelect = function()
local TextBox = require("src.render.TextBox")
game.stack:push(TextBox.new(game, Strings("RETURN TO MAIN\nMENU?"), nil, {
defaultNo = true,
choice = function(yes)
if yes then game:returnToTitle() end
end,
}))
end })
local hooked = Runtime.call("ui.start_menu.items", sameItems, game, items)
if type(hooked) == "table" then
items = hooked
else
Logger.error("ui.start_menu.items returned %s; keeping the vanilla items",
type(hooked))
end
-- 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/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)
menu = Menu.new(game, items,
-- the START menu hugs the top-right corner of the SCREEN, not of a
-- centred letterbox: at 10,0 x 10 (engine/menus/draw_start_menu.asm:5-12)
-- it is already flush with the top and right of the 20x18 grid, so the
-- anchor keeps it flush when the view is zoomed out and the letterbox no
-- longer fills the window
{ tx = 10, ty = 0, tw = 10, maxVisible = maxVisible, startCloses = true,
anchor = "topright" })
-- the cursor position survives closing the menu
-- (wBattleAndStartSavedMenuItem, home/start_menu.asm)
menu.index = math.min(game.startMenuIndex or 1, #items)
menu:clampScroll()
local baseUpdate = menu.update
menu.update = function(self, dt)
baseUpdate(self, dt)
game.startMenuIndex = self.index
end
-- inside the Safari Zone the start menu also shows remaining steps and
-- SAFARI BALLs (PrintSafariZoneSteps, engine/overworld/player_state.asm:
-- 219-224): a 9x5 border at the top-left with "steps/500" and "BALL×xx".
-- It opens with `cp SAFARI_ZONE_EAST / ret c`, so only the nine interior
-- maps ($D9..$E1) get it -- SAFARI_ZONE_GATE is $9C and falls under that
-- early out, and used to show "502/500" while the player was still
-- standing at the counter (#540). Same map set as the step counter's
-- (FieldDefaults safari.stepMaps via OverworldState:inSafariStepZone).
local ow = game.overworld
if game.save.safari and ow and ow.map and ow.inSafariStepZone
and ow:inSafariStepZone() then
local baseDraw = menu.draw
menu.draw = function(self)
baseDraw(self)
local safari = game.save.safari
Font.drawBox(0, 0, 9, 5)
love.graphics.setColor(0, 0, 0, 1)
Font.draw(("%3d"):format(math.floor(safari.steps or 0)), 8, 8)
Font.draw("/500", 32, 8)
Font.draw(Strings("BALL"), 8, 24)
-- engine/overworld/player_state.asm:236-249, home/print_num.asm:205-217
local digits = tostring(math.max(0, math.floor(safari.balls or 0)))
local bx = (7 - #digits) * 8
Font.draw("\xc3\x97", bx, 24)
Font.draw(digits, bx + 8, 24)
love.graphics.setColor(1, 1, 1, 1)
end
end
return menu
end
return StartMenu