mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-10-04 08:21:40 -04:00
236 lines
10 KiB
Lua
236 lines
10 KiB
Lua
-- 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
|