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), orPROLOGUE_BEATS.lengthwhen finished.completedBeats—Set<PrologueBeatId>of beat ids the player has cleared.hubReveal— currentHubRevealLevel('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_BEATSand thePrologueBeatId/HubRevealLeveltypes fromsrc/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 toprologueComplete,currentBeatIndex,completeBeat, andgetCurrentBeatto drive the prologue flow.HubScreen(src/metagame/screens/HubScreen.tsx) subscribes toprologueCompleteto control hub gating.V32Shelland other hub-side surfaces readhubRevealto 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_RULESfromprologue-config; consumers interprethubRevealagainst those rules. - Does not start the rookie-boost timer on
completePrologue(skip path); the timer only starts when the final beat is cleared viacompleteBeat. - Does not contain navigation, networking, animation, or telemetry logic.
- Does not validate that the supplied
beatIdexists inPROLOGUE_BEATS; if missing, falls back tocurrentBeatIndex + 1.
Signals
completeBeat(beatId)— adds the beat tocompletedBeats. If the resolved next index reachesPROLOGUE_BEATS.length, also setsprologueComplete=true, advancescurrentBeatIndexto the end, switcheshubRevealto'hub_lite', and stampsrookieBoostStartedAttoDate.now()if still 0. Otherwise just advancescurrentBeatIndex.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; convertscompletedBeats: string[]into aSet<PrologueBeatId>.reset()— fresh-player state:prologueComplete=false,currentBeatIndex=0, emptycompletedBeats,hubReveal='locked',rookieBoostStartedAt=0. Intended for tests.
Entry points
- Hook export:
useOnboardingStore(Zustandcreate-d store). - Imported by:
src/metagame/screens/PrologueScreen.tsxsrc/metagame/screens/HubScreen.tsx
- Bootstrap wiring lives outside this file; the store exposes
loadFromBootstrapas 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 explicitreset()or a bootstrap payload withprologueComplete=false. This is deliberate so a missed hydration falls open rather than locking the hub. - Reveal progression:
locked→ (final beat) →hub_lite→ (further unlocks viasetHubReveal) →hub_mid→hub_full. The store only auto-advances tohub_lite; deeper reveals are pushed by external code. getCurrentBeat()returnsnullwhen the prologue is complete or the index has overrunPROLOGUE_BEATS, so consumers can branch on a single nullable read.completedBeatsis aSetfor O(1) membership checks viaisBeatComplete; rebuilt as a newSeton each mutation to keep Zustand selectors stable.- Selector functions live on the store object itself (
isBeatComplete,getCurrentBeat) rather than being external helpers — they read viaget()and are safe to invoke from components.