Skip to content

Start Menu Carousel — design

Restyle the start menu’s mode picker as a carousel (reusing the shop’s established hologram-carousel visual/interaction language), remove the inline ship-picker row from the start menu to declutter it, and turn ShipConfigPanel’s single-ship display into a carousel you can page left/right through to choose a ship, when opened pre-run.

The start menu (ui/start_menu.gd) currently shows a vertical VBoxContainer list of mode cards (collapsed to a single “SURVIVAL” card while V01_CRYSTALS_ONLY = true), followed by a HBoxContainer row of 4 static ship-picker tiles (_make_ship_button), then footer buttons including “Ship” which opens ui/ship_config_panel.gdShipConfigPanel today only ever displays meta.selected_ship (the ship already picked on the row above it); there’s no way to switch ships from inside that panel.

ui/meta_shop_panel.gd already has a shipped, validated carousel (design: docs/superpowers/specs/2026-07-02-shop-carousel-hologram-design.md) — hologram-bracket cards, one centered/full-scale item with dimmed/scaled-down neighbors, live-follow drag + tween-on-release, MenuNav.is_left/right D-pad stepping. That carousel is not a shared/extracted component (confirmed by reading the file) — it’s private state and methods on MetaShopPanel itself, ported there from a tools/shop_lab/ prototype rather than pulled out into a reusable class. This design follows the same precedent: port the pattern into start_menu.gd and ship_config_panel.gd independently, don’t extract a shared carousel class. Three carousels with copy-adapted logic is consistent with this codebase’s existing bias toward one-off ports over speculative abstraction, and avoids inventing a generic data-source/delegate interface no other caller needs yet.

  • No change to ShopCategories, MetaState, ShipBonuses, or any upgrade/loadout data model — this is a rendering/navigation change to two screens, not a data change.
  • No change to meta_shop_panel.gd itself — it’s the reference pattern, not something this work touches.
  • No ship-switching from the pause-menu path into ShipConfigPanel (sim != null). Switching hulls mid-run isn’t a mechanic this game has (ShipBonuses.apply_base() only runs at _new_run()), so the carousel/left-right paging is gated to the pre-run (sim == null) case only. Mid-run, the panel keeps today’s exact behavior: a single static ship + live weapon/drone loadout, no paging, no left/right input handled.
  • No change to the mode-picker’s actual mode list, V01_CRYSTALS_ONLY gating, or which modes exist — purely how the (currently 1, later up to 4) mode cards are laid out and navigated.
  • Not reintroducing anything from the reverted “glassy ambient”/Gemini-glass-texture shop restyles mentioned in the shop-carousel design — hologram brackets only.
Section titled “1. Mode picker → carousel (ui/start_menu.gd)”

Replace the VBoxContainer of full-width mode cards with a hologram-bracket carousel, following the shop’s pattern: one centered mode card at CENTER_SCALE, immediate neighbors dimmed/scaled at SIDE_SCALE/SIDE_ALPHA, positions computed fresh each frame from get_viewport().get_visible_rect().size() (not hardcoded — this is the exact bug the shop carousel had to fix for real device aspect ratios). With V01_CRYSTALS_ONLY still true there’s only one card, so it always renders center-only (mirrors the shop carousel’s existing 1-item fallback) — no special-casing needed for “only one mode” beyond what the ported pattern already handles. Card content (title, description, accent color, leading _AccentDot) is unchanged; only the container/positioning/nav changes.

MenuNav.is_left/right steps the carousel (debounced, NAV_DEBOUNCE_MS = 200, same as today); touch drag and tap-a-side-card behave as in the shop carousel; confirm (ui_accept/JOY_BUTTON_A/tap-center) calls _choose(mode) exactly as today — activation logic is unchanged, only how the centered item is determined changes (carousel index, not _sel grid focus).

2. Remove the inline ship-picker row (ui/start_menu.gd)

Section titled “2. Remove the inline ship-picker row (ui/start_menu.gd)”

Delete _make_ship_button, the ship_row/ship_label construction, and _pick_ship() from start_menu.gd entirely — ship selection moves to ShipConfigPanel (below). The “Ship” footer button (ship_config_requested signal) is unchanged; it’s now the only way to reach ship selection, from either the start menu or the pause menu.

Section titled “3. ShipConfigPanel becomes carousel-capable, pre-run only”

open_config(meta, sim) already receives sim (null pre-run, live mid-run) — gate the new paging behavior on sim == null:

  • Pre-run (sim == null): the centered ship becomes pageable. Left/right (MenuNav.is_left/right, touch drag, tap-a-side-ship) cycles through ShipBonuses.ORDER the same way the start menu’s old row did, updating _ship_thumb, _title_lbl, _bonus_lbl, the weapon-slot ring (_weapon_cards, still built from ShipBonuses.weapon_slots_for(ship_id) with all-empty-but-Blade views since there’s no live sim), and locked/unlocked state (dimmed + “LOCKED” tag, ported from the old _make_ship_button’s locked treatment) — all per-ship content already exists today, this just re-triggers building it for whichever ship_id is centered instead of only ever meta.selected_ship. Confirming the centered ship (ui_accept/tap) calls the existing _pick_ship-equivalent persistence path (meta.selected_ship = ship_id; MetaStore.save_state(meta), ported from the deleted start_menu.gd method) and stays open on that ship (browsing further is still possible; “Back” is the only way to leave).
  • Mid-run (sim != null): unchanged from today — one ship (whichever is already equipped), live weapon/drone data, no left/right handling, no locked-ship dimming/paging.

Given this panel isn’t a multi-item swarm of small tiles (it’s one large hero ship with a ring of weapon cards around it), the carousel treatment here means paging the centered hero display itself — not shrinking to a 3-up hologram-bracket layout like the shop or mode picker. Side neighbors are NOT rendered as visible dimmed ships (there’s no room once the weapon ring is laid out around the center); the paging animation is a cross-fade/slide of the whole hero+ring composition between ship A and ship B, reusing the shop carousel’s ANIM_DURATION = 0.3/Tween.TRANS_CUBIC timing for consistency, not its 3-up slot layout.

Screen Confirm Left/Right Back
Mode carousel (start menu) _choose(mode) (unchanged) steps carousel n/a (footer buttons only)
Ship carousel (pre-run ShipConfigPanel) persist meta.selected_ship, stay open pages to next/prev ship close() (unchanged)
Ship detail (mid-run ShipConfigPanel) n/a no-op (not handled) close() (unchanged)

Start menu — kept: test_shop_entry_emits_shop_requested (footer button behavior, untouched by the carousel change). Removed/rewritten: any assertion that walks _cards/box.get_children() expecting a flat vertical list of mode cards followed by a ship-tile row (none currently exist beyond the one kept test, per tests/test_start_menu.gd being only 12 lines today — this file grows, it isn’t gutting an existing suite). New: carousel center/side scale+alpha for the mode list (mirroring the shop carousel’s own carousel tests), MenuNav.is_left/right steps it, confirm activates the centered mode, 1-item case renders center-only without erroring, no ship-picker nodes exist anywhere in the built tree.

ShipConfigPanel — kept exactly as-is (all assert real behavior this design doesn’t touch): test_open_pre_run_shows_default_hull_weapon_slots_only_blade_owned, test_config_panel_card_count_follows_hull_weapon_slots, test_open_mid_run_shows_live_owned_weapons, test_close_emits_closed_and_hides, test_shows_the_selected_ships_name_and_bonus, test_drone_row_shows_all_eventual_slots, test_open_config_shows_the_current_level. New: pre-run left/right pages to the next/prev ShipBonuses.ORDER entry and rebuilds thumb/title/bonus/weapon-ring/drone-row for it; paging wraps at both ends; confirming a centered locked ship does NOT persist meta.selected_ship (mirrors the old _pick_ship guard); confirming an owned ship DOES persist it and calls MetaStore.save_state; mid-run (sim != null) left/right input is a no-op — _weapon_cards/_ship_thumb don’t change and no ship switches.

Render/UI-only change across two files (ui/start_menu.gd, ui/ship_config_panel.gd), no /sim files touched — the pinned determinism baseline should hold by construction, verify per the standard bh-dev-chunk ritual anyway. Given both screens are used identically across iPhone, iPad, Apple TV, and Mac, and the shop carousel needed a real on-device pass to catch its centering bug, include a real-device check (Mac export at minimum, bh-deploy to the Apple TV if this session’s cadence continues) before calling this done — not just a headless test pass.