PURPOSE

Zustand store tracking new-player prologue progress and the progressive hub-reveal state. Drives the prologue beat flow for fresh players and gates which surfaces of the metagame hub are visible. Existing players bypass the system entirely by defaulting to prologueComplete=true and hubReveal='hub_full'.

OWNS

  • prologueComplete — boolean flag, true once the prologue tunnel is done.
  • currentBeatIndex — numeric index of the active prologue beat (0..N-1), or PROLOGUE_BEATS.length when finished.
  • completedBeatsSet<PrologueBeatId> of beat ids the player has cleared.
  • hubReveal — current HubRevealLevel ('locked' | 'hub_lite' | 'hub_mid' | 'hub_full').
  • rookieBoostStartedAt — epoch ms when the rookie-boost window opened (0 = not started).
  • Mutators: completeBeat, completePrologue, setHubReveal, loadFromBootstrap, reset.
  • Selectors: isBeatComplete, getCurrentBeat.

READS FROM

  • PROLOGUE_BEATS and the PrologueBeatId / HubRevealLevel types from src/starship-survivors/data/prologue-config.ts. Looks up beat definitions by id to find the next index and to return the current beat.
  • Bootstrap payload (server-supplied shape consumed by loadFromBootstrap): prologueComplete, currentBeatIndex, completedBeats: string[], hubReveal, rookieBoostStartedAt.

PUSHES TO

  • PrologueScreen (src/metagame/screens/PrologueScreen.tsx) subscribes to prologueComplete, currentBeatIndex, completeBeat, and getCurrentBeat to drive the prologue flow.
  • HubScreen (src/metagame/screens/HubScreen.tsx) subscribes to prologueComplete to control hub gating.
  • V32Shell and other hub-side surfaces read hubReveal to decide visibility of progressively unlocked panels.

DOES NOT

  • Does not persist state itself — relies on bootstrap to hydrate and on whatever upstream service writes the canonical record.
  • Does not consult HUB_REVEAL_RULES from prologue-config; consumers interpret hubReveal against those rules.
  • Does not start the rookie-boost timer on completePrologue (skip path); the timer only starts when the final beat is cleared via completeBeat.
  • Does not contain navigation, networking, animation, or telemetry logic.
  • Does not validate that the supplied beatId exists in PROLOGUE_BEATS; if missing, falls back to currentBeatIndex + 1.

Signals

  • completeBeat(beatId) — adds the beat to completedBeats. If the resolved next index reaches PROLOGUE_BEATS.length, also sets prologueComplete=true, advances currentBeatIndex to the end, switches hubReveal to 'hub_lite', and stamps rookieBoostStartedAt to Date.now() if still 0. Otherwise just advances currentBeatIndex.
  • completePrologue() — force-finishes the prologue: prologueComplete=true, currentBeatIndex=PROLOGUE_BEATS.length, hubReveal='hub_full'. Skip / existing-player path.
  • setHubReveal(level) — direct setter for the reveal stage.
  • loadFromBootstrap(data) — hydrates all fields; converts completedBeats: string[] into a Set<PrologueBeatId>.
  • reset() — fresh-player state: prologueComplete=false, currentBeatIndex=0, empty completedBeats, hubReveal='locked', rookieBoostStartedAt=0. Intended for tests.

Entry points

  • Hook export: useOnboardingStore (Zustand create-d store).
  • Imported by:
    • src/metagame/screens/PrologueScreen.tsx
    • src/metagame/screens/HubScreen.tsx
  • Bootstrap wiring lives outside this file; the store exposes loadFromBootstrap as the hydration entry point.

Pattern notes

  • Default in-memory state is the existing-player profile (prologueComplete=true, hubReveal='hub_full'). New players are only reachable by an explicit reset() or a bootstrap payload with prologueComplete=false. This is deliberate so a missed hydration falls open rather than locking the hub.
  • Reveal progression: locked → (final beat) → hub_lite → (further unlocks via setHubReveal) → hub_midhub_full. The store only auto-advances to hub_lite; deeper reveals are pushed by external code.
  • getCurrentBeat() returns null when the prologue is complete or the index has overrun PROLOGUE_BEATS, so consumers can branch on a single nullable read.
  • completedBeats is a Set for O(1) membership checks via isBeatComplete; rebuilt as a new Set on each mutation to keep Zustand selectors stable.
  • Selector functions live on the store object itself (isBeatComplete, getCurrentBeat) rather than being external helpers — they read via get() and are safe to invoke from components.