mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-18 11:44:42 +02:00
308 lines
11 KiB
Lua
308 lines
11 KiB
Lua
-- Gold's START menu (engine/menus/start_menu.asm StartMenu).
|
|
--
|
|
-- The box is right-aligned -- menu_coords 10, 0, SCREEN_WIDTH - 1,
|
|
-- SCREEN_HEIGHT - 1 -- and grows to fit however many entries are currently
|
|
-- unlocked. #DEX and POKEGEAR only appear once the player owns them, so early
|
|
-- in the game the menu is short; the ASM builds wMenuItemsList each time it
|
|
-- opens for exactly that reason (.SetUpMenuItems).
|
|
--
|
|
-- With MENU ACCOUNT on, a second box at the bottom-left describes the
|
|
-- highlighted entry (.MenuDesc), which is why every item carries two lines of
|
|
-- description text here.
|
|
--
|
|
-- The cursor position is remembered between openings
|
|
-- (wBattleMenuCursorPosition), so reopening the menu lands where you left it.
|
|
--
|
|
-- The assembled list runs through the ui.start_menu.items hook before the menu
|
|
-- opens, exactly as the Gen 1 port's does (src/ui/StartMenu.lua), so mods
|
|
-- insert, drop or reorder rows without patching this file. It is the SAME
|
|
-- hook name and the same (game, items) payload: Gold's POKEGEAR row is simply
|
|
-- one more entry in the list the hook receives.
|
|
|
|
local Chrome = require("src.ui.gen2.Chrome")
|
|
local Logger = require("src.core.Logger")
|
|
local Runtime = require("src.mods.Runtime")
|
|
local Sound = require("src.core.Sound")
|
|
|
|
local StartMenu = {}
|
|
StartMenu.__index = StartMenu
|
|
-- Not opaque: the overworld keeps drawing underneath, the way the real menu
|
|
-- slides a window over the map.
|
|
StartMenu.isOpaque = false
|
|
|
|
-- SFX_MENU on open, SFX_READ_TEXT_2 (the click) on select
|
|
-- (StartMenu_Show's `ld de, SFX_MENU / call PlaySFX` and .Select's
|
|
-- PlayClickSFX). By LABEL, not index: the old numeric ids (2 and 3) landed
|
|
-- on whatever the sfx table had there -- the item-get jingle on every menu
|
|
-- open -- and a repointed table would drift again.
|
|
|
|
-- STARTMENUITEM_* in the order .SetUpMenuItems appends them. `need` is the
|
|
-- save flag that unlocks the entry; nil means always shown.
|
|
--
|
|
-- Labels are the ASM's own strings, with the compression bytes expanded the
|
|
-- way the cart expands them at print time: "#DEX" is POKé + DEX (seven tiles),
|
|
-- while "<POKE>GEAR" is <PO><KE>GEAR -- the <PO> and <KE> glyphs ($70/$71) are
|
|
-- one tile each, so that row is six tiles wide, not eight. That difference is
|
|
-- exactly what kept POKéGEAR hanging off the menu box's right edge.
|
|
local ITEMS = {
|
|
{
|
|
id = "pokedex", label = "POKéDEX", need = "pokedex",
|
|
desc = { "POKéMON", "database" },
|
|
},
|
|
{
|
|
id = "pokemon", label = "POKéMON", need = "party",
|
|
desc = { "Party <PK><MN>", "status" },
|
|
},
|
|
{
|
|
id = "pack", label = "PACK", need = "pack",
|
|
desc = { "Contains", "items" },
|
|
},
|
|
{
|
|
id = "pokegear", label = "<PO><KE>GEAR", need = "pokegear",
|
|
desc = { "Trainer's", "key device" },
|
|
},
|
|
{
|
|
-- The player's own name is the label (.StatusString is "<PLAYER>").
|
|
id = "status", label = nil,
|
|
desc = { "Your own", "status" },
|
|
},
|
|
{
|
|
id = "save", label = "SAVE",
|
|
desc = { "Save your", "progress" },
|
|
},
|
|
{
|
|
id = "option", label = "OPTION",
|
|
desc = { "Change", "settings" },
|
|
},
|
|
{
|
|
-- The mod manager's discoverable home, exactly as the Gen 1 start menu
|
|
-- carries it: the row only appears once at least one mod has been
|
|
-- discovered, so a vanilla install's menu is the cart's.
|
|
id = "mods", label = "MODS", need = "mods",
|
|
desc = { "Installed", "add-ons" },
|
|
},
|
|
{
|
|
-- The cart's EXIT just closed the menu (CloseStartMenu). A window with a
|
|
-- close button already covers that, so -- exactly as the Gen 1 port does
|
|
-- (src/ui/StartMenu.lua) -- this row is QUIT and power-cycles back to the
|
|
-- title after a confirmation that defaults to NO.
|
|
id = "quit", label = "QUIT",
|
|
desc = { "Return to", "the title" },
|
|
},
|
|
}
|
|
|
|
-- The confirmation's yes/no box. A bare YesNoBox lands at YesNoMenuHeader's
|
|
-- own menu_coords 10, 5, 15, 9, which is exactly where the start menu is, so
|
|
-- this uses the other position the cart already places one at:
|
|
-- SaveTheGame_yesorno's `lb bc, 0, 7`, a 6x5 box at (0,7) clear of the menu.
|
|
-- Labels sit at (2,8) and (2,10) -- border + 1, plus a column for
|
|
-- STATICMENU_CURSOR and no extra row because STATICMENU_NO_TOP_SPACING is set.
|
|
local YESNO_X, YESNO_Y, YESNO_W, YESNO_H = 0, 7, 6, 5
|
|
|
|
-- Persisted across openings, like wBattleMenuCursorPosition.
|
|
StartMenu.lastIndex = 1
|
|
|
|
-- ui.start_menu.items identity: an unhooked build hands its own list back.
|
|
local function sameItems(_, items) return items end
|
|
|
|
-- opts: save, onChoose(id), onClose(), unlocked (override table for tests)
|
|
function StartMenu.new(game, opts)
|
|
opts = opts or {}
|
|
local self = setmetatable({}, StartMenu)
|
|
self.game = game
|
|
self.save = opts.save or (game and game.save)
|
|
self.onChoose = opts.onChoose
|
|
self.onClose = opts.onClose
|
|
self.unlocked = opts.unlocked
|
|
local items = self:visibleItems()
|
|
-- Unguarded, like the Gen 1 site: the menu is built once per opening, not
|
|
-- per frame, so there is nothing here worth a wantsHook fast path. A hook
|
|
-- that answers with anything but a table is degraded to the vanilla list
|
|
-- rather than emptying the player's menu.
|
|
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
|
|
self.items = items
|
|
local options = (self.save and self.save.options) or {}
|
|
self.showDescription = options.menuAccount ~= false
|
|
|
|
self.list = Chrome.List.new({
|
|
items = self.items,
|
|
-- GetMenuTextStartCoord: the box's left/top plus one for the border, plus
|
|
-- one more for the cursor column (STATICMENU_CURSOR) and one more for the
|
|
-- top spacing this menu does not opt out of -- so (10,0) becomes (12,2),
|
|
-- and Chrome.List puts the cursor at x - 1 = 11.
|
|
x = 12, y = 2, spacing = 2,
|
|
-- Two rows per entry inside an 18-row screen leaves room for eight, which
|
|
-- is exactly the vanilla count. A mod that adds a row scrolls rather than
|
|
-- drawing off the bottom of the frame; Chrome.List puts the ▼ hint on.
|
|
rows = math.min(#self.items, 8),
|
|
wrap = true,
|
|
startAccepts = true,
|
|
index = math.min(StartMenu.lastIndex, math.max(1, #self.items)),
|
|
onChoose = function(value, index) self:choose(value, index) end,
|
|
onCancel = function() self:close() end,
|
|
})
|
|
return self
|
|
end
|
|
|
|
-- Which entries the player has unlocked. Derived from the save so the menu
|
|
-- grows as the game does; `unlocked` overrides it wholesale for tests.
|
|
-- .SetUpMenuItems tests two ENGINE flag bits, and both arrive as `setflag`
|
|
-- ids in save.engineFlags (constants/engine_flags.asm const order, via
|
|
-- World:setEngineFlag): `bit STATUSFLAGS_POKEDEX_F, [wStatusFlags]` is
|
|
-- ENGINE_POKEDEX = 11, written by Oak at Mr. Pokemon's house, and
|
|
-- `bit POKEGEAR_OBTAINED_F, [wPokegearFlags]` is ENGINE_POKEGEAR = 4,
|
|
-- written by Mom on the way out the door.
|
|
local ENGINE_POKEGEAR, ENGINE_POKEDEX = 4, 11
|
|
|
|
function StartMenu:availability()
|
|
if self.unlocked then return self.unlocked end
|
|
local save = self.save or {}
|
|
local inventory = save.inventory or {}
|
|
local engine = save.engineFlags or {}
|
|
local status = self.game and self.game.modStatus
|
|
return {
|
|
mods = status ~= nil and #(status.available or {}) > 0,
|
|
-- ENGINE_POKEDEX first; pokedexReceived stays as the test/driver override.
|
|
pokedex = engine[ENGINE_POKEDEX] == true or save.pokedexReceived == true,
|
|
party = #(save.party or {}) > 0,
|
|
-- The PACK exists from the start; the cart gates it on nothing.
|
|
pack = true,
|
|
pokegear = engine[ENGINE_POKEGEAR] == true
|
|
or (inventory.POKEGEAR or 0) > 0 or save.pokegearReceived == true,
|
|
}
|
|
end
|
|
|
|
function StartMenu:visibleItems()
|
|
local available = self:availability()
|
|
local playerName = (self.save and self.save.player and self.save.player.name)
|
|
or "GOLD"
|
|
local out = {}
|
|
for _, item in ipairs(ITEMS) do
|
|
if not item.need or available[item.need] then
|
|
out[#out + 1] = {
|
|
label = item.label or playerName,
|
|
value = item.id,
|
|
desc = item.desc,
|
|
}
|
|
end
|
|
end
|
|
return out
|
|
end
|
|
|
|
function StartMenu:playSfx(name)
|
|
local data = self.game and self.game.data
|
|
local audio = data and data.audio
|
|
if not audio then return end
|
|
if audio.sfx and audio.sfx[name] then Sound.play(data, name) end
|
|
end
|
|
|
|
function StartMenu:enter()
|
|
self:playSfx("Sfx_Menu")
|
|
end
|
|
|
|
-- index comes from Chrome.List (src/ui/gen2/Chrome.lua:346) so a hook-injected
|
|
-- entry can be found by position: a mod's row carries an onSelect callback and
|
|
-- no `value`, the way the Gen 1 menu's rows do (src/ui/StartMenu.lua:30), and
|
|
-- without this arm it falls off the end of the id chain in Game2:switch.
|
|
function StartMenu:choose(id, index)
|
|
StartMenu.lastIndex = self.list.index
|
|
local item = index and self.items[index]
|
|
if item and item.onSelect and item.value == nil then
|
|
self:playSfx("Sfx_ReadText2")
|
|
item.onSelect(self.game)
|
|
return
|
|
end
|
|
self:playSfx("Sfx_ReadText2")
|
|
if id == "quit" then
|
|
-- Ask before throwing away everything since the last save. NO is the
|
|
-- default, the way the Gen 1 port's QUIT is.
|
|
self.phase = "confirm"
|
|
self.confirmChoice = 2
|
|
return
|
|
end
|
|
if self.onChoose then self.onChoose(id) end
|
|
end
|
|
|
|
function StartMenu:confirmQuit()
|
|
self.phase = nil
|
|
if self.onQuit then
|
|
self.onQuit()
|
|
elseif self.game and self.game.returnToTitle then
|
|
self.game:returnToTitle()
|
|
end
|
|
end
|
|
|
|
function StartMenu:close()
|
|
StartMenu.lastIndex = self.list.index
|
|
if self.onClose then self.onClose() end
|
|
end
|
|
|
|
function StartMenu:update(_dt)
|
|
local input = self.game and self.game.input
|
|
if not input then return end
|
|
if self.phase == "confirm" then
|
|
if input:wasPressed("up") or input:wasPressed("down") then
|
|
self.confirmChoice = self.confirmChoice == 1 and 2 or 1
|
|
elseif input:wasPressed("a") then
|
|
if self.confirmChoice == 1 then
|
|
self:confirmQuit()
|
|
else
|
|
self.phase = nil
|
|
end
|
|
elseif input:wasPressed("b") or input:wasPressed("start") then
|
|
self.phase = nil
|
|
end
|
|
return
|
|
end
|
|
-- START closes the menu as well as opening it.
|
|
if input:wasPressed("start") then
|
|
self:close()
|
|
return
|
|
end
|
|
self.list:update(input)
|
|
end
|
|
|
|
function StartMenu:draw()
|
|
-- AutomaticGetMenuBottomCoord: bottom = top + 2 * items + 1, so the box is
|
|
-- two rows per entry plus its two border rows. A menu that a mod has grown
|
|
-- past the screen scrolls instead of overflowing (Chrome.List draws the ▼
|
|
-- hint when there is more below).
|
|
local height = math.min(#self.items * 2 + 2, Chrome.SCREEN_H)
|
|
Chrome.box(10, 0, 10, height)
|
|
self.list:draw()
|
|
|
|
if self.phase == "confirm" then
|
|
Chrome.textbox(0, 12, 18, 4)
|
|
Chrome.print("Return to the", 1, 14)
|
|
Chrome.print("title screen?", 1, 16)
|
|
Chrome.box(YESNO_X, YESNO_Y, YESNO_W, YESNO_H)
|
|
Chrome.print("YES", YESNO_X + 2, YESNO_Y + 1)
|
|
Chrome.print("NO", YESNO_X + 2, YESNO_Y + 3)
|
|
Chrome.cursor(YESNO_X + 1,
|
|
YESNO_Y + (self.confirmChoice == 1 and 1 or 3))
|
|
return
|
|
end
|
|
|
|
if not self.showDescription then return end
|
|
local item = self.list:current()
|
|
local desc = item and item.desc
|
|
if not desc then return end
|
|
-- ._DrawMenuAccount ClearBox (0,13) 5 rows by 10, .PrintMenuAccount decoord
|
|
-- 0, 14 and the desc's `next` steps two rows (start_menu.asm:366-382).
|
|
local G = love.graphics
|
|
G.setColor(1, 1, 1, 1)
|
|
G.rectangle("fill", 0, 13 * 8, 10 * 8, 5 * 8)
|
|
Chrome.print(desc[1] or "", 0, 14)
|
|
Chrome.print(desc[2] or "", 0, 16)
|
|
end
|
|
|
|
StartMenu.ITEMS = ITEMS
|
|
|
|
return StartMenu
|