Start Menu

The key words MUST, MUST NOT, REQUIRED, SHOULD, MAY, and OPTIONAL below are used as in RFC 2119.

This is the runtime behavior reference. For the player-facing view with a live visual preview, see the Start Menu journey page.

The start menu is a PSO-style, non-pausing overlay with an L-shaped backdrop: a left strip plus a bottom strip, leaving the upper-right of the screen clear so the player can still see the world. It is a single autoload CanvasLayer (PsoStartMenu) driven by one Mode enum; exactly one Mode is active at a time, held in _mode. Input routing and drawing both dispatch on that mode. This page is the behavior reference the planned three-way split (see Implemented by) MUST preserve.

Opening and closing

The menu is toggled with Start / ESC via toggle(), which calls open() when closed and close() when open. open() resets _mode to MAIN and clears every per-mode cursor (_menu_idx, _sub_idx, the equip / palette / mag indices, scroll, and the one-shot action message) so reopening always lands on a clean main menu rather than resuming in a sub-mode the player had backed out of. is_open() reports the current state.

Because it is an autoload, the menu MUST be available globally — it works in both the field and the city without each scene instantiating it. It is registered with process_mode = PROCESS_MODE_ALWAYS and it MUST NOT pause the game: opening it pushes a modal flag (GameState.push_modal()) but does not set the tree paused. As a direct consequence, the player can still walk around with the analog stick while the menu is open — _unhandled_input explicitly lets the four movement actions pass through unconsumed, while the d-pad / right stick navigate the menu. Every other gameplay input (camera, interact, quick-weapon, palette) is consumed while the menu is open.

Opening is gated: from the closed state the menu only opens on pause when there is no other overlay on the SceneManager overlay stack and the current scene is a gameplay scene. While open, the joypad Start button always closes the menu regardless of mode; ESC / pause closes only from MAIN and otherwise acts as Back into the active mode handler.

Confirm / interact precedence

A single confirm/action press can be bound to more than one consumer (a world interaction such as a teleporter / telepipe / NPC / pickup, a palette / free action, and the menu's own Accept can share a physical button under some control schemes). The field input path MUST resolve each press through exactly one consumer, in this fixed priority order (highest first):

#ConditionOutcome
1 An active modal / the Start Menu is open (GameState.is_gameplay_blocked() is true) The confirm press MUST be consumed by the modal. World interaction and palette/free actions MUST NOT fire. Movement MUST stay enabled (it runs from the ungated _physics_process, not the input path).
2 No modal, and a world-interaction prompt (teleporter / telepipe / NPC / pickup) is in range when interact is pressed The world interaction MUST take precedence over and CONSUME the press ahead of any palette/free action bound to the same button — the palette action MUST NOT also fire on that press (cross-ref issue #423).
3 Neither 1 nor 2 applies A palette / free action (and only then) fires. Inside the city the combat palette stays suppressed regardless.

Closing the menu MUST restore world interaction on the next frame with no buffered confirm leaking through (the press that closed the menu MUST NOT also trigger a world interaction or palette action). Movement is NOT suppressed while the menu is open — the analog stick keeps the player walking (tester Sazonde99); only confirm/interact/palette are gated. This row table makes the world > palette ordering explicit and is consistent with the invariant above ("movement input MUST pass through while open, all other gameplay input MUST be consumed").

Modes (views)

MAIN

The menu spine. Up / Down move the cursor over the dynamically built label list; Left / Right flip the four info pages (also bound to LB / RB); Accept enters the highlighted sub-view via _enter_sub; Back / Cancel closes the menu. The label list comes from _get_menu_labels()Items, Equip, optionally Techs, then Palette, Mags, Quest, System — so its size is the wrap bound for the cursor.

ITEMS (and ITEMS_MOVE)

Lists the inventory in storage order. Accept on a row opens an item modal offering Equip / Unequip (when the item is equippable), Use, Drop (disabled for equipped gear), and Sort; choices are greyed PSO-style when unavailable. Use calls Inventory.use_item and writes a one-shot result line (restored HP / PP, learned a disk, telepipe placed, etc.). Sort offers Auto (rebuild in canonical order) or Manual, and Manual enters ITEMS_MOVE, a relocate sub-mode where Up / Down pick the destination row and Accept drops the held item there (Back cancels the move). The move origin is tracked by item id so the cursor survives a list that reorders under it.

The items list MUST respect its panel bounds: it scrolls by whole rows and clips to the inner panel, so a row is never painted past the panel border (the list must not "pop out" the bottom of the UI). Only fully-visible rows render; the visible count follows from the panel height and row metrics, with ▲ / ▼ more cues shown while content extends above / below the window. Row padding matches the 2D shop lists (the same name-font size and row height) so the inventory does not read as cramped relative to the shops.

A disabled inventory row MUST read as disabled and offer no Use — the visual marker and the action agree (the grey-tier marker == action rule; see Equip Legality). A row is disabled when the item is permanently class-unusable (ShopNav.sell_cannot_use — gear the class can't equip, or a disk the class can never learn → the marker) or temporarily useless (ShopNav.sell_disabled — a technique disk already known at this level or higher, or below the required player level → greyed, no ). In both cases the row's usable flag is false, so the item modal MUST NOT offer Use (only Drop / Sort remain): buying five Lv.1 Foie disks and learning one leaves the other four greyed and un-usable. The disabled style MUST be clearly distinct from a normal row — a faded text colour and a dimmed icon, not a near-normal shade.

EQUIP (and EQUIP_PICK)

Lists the character's equipment slots — a Weapon slot, the armor Frame slot, and one Unit slot per the equipped frame instance's own unit-slot count (a per-instance rolled value, not the resource's max_slots — see Inventory; so the unit slots are built dynamically, and an unarmored character shows none). Accept on a slot enters EQUIP_PICK, which lists the inventory items that fit that slot (_get_equip_candidates); Accept there equips the chosen item via _do_equip and returns to EQUIP, Back returns without changing the loadout. Equipping a weapon or mag refreshes the player's held model; changing the armor frame to a different item unequips every currently-equipped unit (not only those beyond the new frame's capacity) — see the equipping-gear contract under Inventory.

TECHS

Lists the character's techniques; Accept casts a learned technique when the player is in the field (routes to the player node's _cast_technique). This view MAY be absent: see Techs gating below.

PALETTE (and PALETTE_PICK)

Edits the action palette. PALETTE shows the three slots of the current page; Up / Down move between the three slots, Left / Right flip palette pages, Accept enters PALETTE_PICK for the highlighted slot (seeding the picker cursor to the slot's current action). PALETTE_PICK is a two-column grid of assignable actions (combat / recovery on the left, techniques on the right) navigated with all four directions, with edge wrapping that crosses columns; Accept assigns the action to the slot (when available) and returns to PALETTE, Back returns without assigning.

MAGS (and MAG_FEED)

Lists the character's mags. Accept on a mag enters MAG_FEED, which lists feedable inventory items; Accept feeds the selected item to the mag (_do_feed_magMagManager.feed_mag, consuming one item on success), Back returns to MAGS.

QUEST

Read-only view of the current quest's info / objectives. It has no sub-navigation — its only input is Back to MAIN (_input_back).

SYSTEM (and OPTIONS, DEBUG)

A fixed four-item list (SYSTEM_LABELS): Save (writes the save and plays the saved SFX), Return to Title (saves, closes the menu, and navigates to the title scene — leaving the open state entirely; the in-memory session teardown, including the city-hub position cache, is delegated to the title scene's SessionManager.reset_all_state() — see Session Model), Options (enters OPTIONS), and Debug (enters DEBUG). Back returns to MAIN. Player-facing settings and developer tooling are separated: everyday settings live under OPTIONS, and all debug toggles and cheats live under DEBUG. Each submenu's Back MUST return to SYSTEM with the cursor restored to the row that opened it (Options for OPTIONS, Debug for DEBUG).

OPTIONS

An adjustable settings list (_get_options_list) of player-facing settings only: Music Volume and SFX Volume are stepped with Left / Right; the remaining rows are toggles or actions fired with Accept — Controls: Reconfigure (closes the menu, clears the input config, and pushes the input-select scene as an overlay), On-Screen Controls, camera-rotation / camera-Y options, and Auto-Sort Inventory. OPTIONS MUST NOT contain debug toggles or cheats; those belong to DEBUG. Back returns to SYSTEM with the cursor restored to the Options row.

DEBUG

A developer-tools list (_get_debug_list) reached from System → Debug, dispatched by _toggle_debug. Row 0 is Unlock All Missions, a one-shot action (not a toggle); the remaining rows are the DebugConfig boolean flags (floor collision, gate dots, hitboxes, combo timing, time/room, frame profiler, player position, equip-all, reveal-map), fired ON/OFF with Accept. Up / Down navigate; Accept fires the highlighted row; Back returns to SYSTEM with the cursor on the Debug row.

Unlock All Missions (GameState.unlock_all_missions()) MUST mark every real quest complete — appending each quest id from QuestLoader.list_quests() to GameState.completed_missions, skipping the manifest sentinel and hello_quest (which are not missions) — so the guild counter, whose availability gates read is_mission_completed for parent_quest / required_quests, surfaces the whole roster as available. It MUST be idempotent (re-running adds nothing) and MUST return the count of newly cleared missions. It affects mission availability only; it MUST NOT unlock difficulty tiers (that axis is governed by Difficulty Unlock). Beta/disabled quests stay hard-locked by the guild's own disabled flag regardless. The menu action MUST persist the result via SaveManager.save_game() and report the count in the description line.

Techs gating

The Techs view MAY be absent. _can_use_techs() looks up the active character's class and returns false when its race is CAST; _get_menu_labels() (and the parallel _get_menu_descs()) omit the Techs entry in that case. Therefore, for a CAST character the main menu MUST NOT offer Techs, and for any non-CAST character it MUST. Because the label list is the cursor's wrap bound, the same gate that hides the label also keeps the cursor from ever landing on a missing row.

Persistence across an area transition

Because the menu is an autoload CanvasLayer, it survives a full area transition (SceneManager.goto_scenechange_scene_to_file): the node is not freed and rebuilt with the scene. Its canvas, however, only repaints on input or open (queue_redraw), so a transition that fires a redraw mid-rebuild can repaint at a moment a freshly load()ed visual is transiently unavailable.

Therefore every menu visual that is drawn while a view is open MUST be cached on the persistent autoload rather than re-loaded inside the draw call, so a redraw can never silently skip it. The action-palette icons already follow this (_get_action_icon_icon_cache). The Palette HUD-preview background image (palette_bg.png / palette_bg_r.png) MUST likewise be cached (_get_palette_bg_pal_bg_cache); the renderer MUST NOT load() it inline per draw. Nulls MUST NOT be cached, preserving recovery after a late pack mount.

Additionally, the menu MUST re-issue a redraw on the area-load signal (SceneManager.scene_changed) when it is open, so a Palette page that was open across the warp repaints in place from the cache. The player MUST NOT have to exit and re-enter the page to recover the background. The redraw handler MUST be guarded on the open state so overlay pushes (which also emit scene_changed) do not trigger spurious repaints on a closed menu.

Data contract

A set of pure data helpers feeds both rendering (the _draw_* family) and input (the _input_* router), so the two halves never disagree about what a view contains:

These helpers are locked by test_start_menu_data() in scripts/tools/test_runner.gd, which asserts the category mappings, the fits-slot results, and the invariant that Techs is present in the labels exactly when _can_use_techs() is true. They are the regression net for the split below, and the split MUST keep them behaving identically.

Invariants

Implemented by

This 2,279-line autoload is slated to split by concern; this page is the contract that split MUST preserve:

The Mode enum, the transition rules in the diagram, and the data contract are the seams the split crosses; whichever unit ends up owning each piece, the contract above MUST hold.