PURPOSE

Zustand store that serializes reward presentation. Two parallel surfaces coexist: a legacy popup queue (sequential RewardItem overlays for simple grants like prologue beats) and the new phase-driven RewardBatch pipeline that drives RewardOrchestrator and HomeResolveController through reveal → collect → await_surface → resolve → milestones. Acts as the single declarative state for both surfaces — timers, animation promises, and skip logic live in the orchestrator, never in this store.

OWNS

  • queue: RewardItem[] — popup queue, FIFO after priority-sort on enqueue.
  • isShowing: boolean — popup overlay visibility flag; true while queue is non-empty.
  • phase: RewardPhase — pipeline state machine; one of idle | reveal | collect | await_surface | resolve | milestones. Starts at idle.
  • activeBatch: RewardBatch | null — the batch currently being presented through the phase pipeline; null when idle.
  • RewardItem shape: { id, label, icon, amount, priority, source }. id auto-generated via _idCounter (module-level, monotonic, prefix rq_).
  • REWARD_PRIORITY constant — sort weights { gems: 6, tickets: 5, ship: 4, credits: 1 }. Higher wins.
  • PHASE_ORDER module constant — linear advancement order ['reveal','collect','await_surface','resolve','milestones'].
  • makeRewardItem(label, icon, amount, priority, source) helper that auto-IDs a RewardItem.

READS FROM

  • zustandcreate factory.
  • ../data/reward-typesRewardBatch and RewardPhase types only. No runtime dependency.

PUSHES TO

  • Nothing direct. The store is purely declarative state. Consumers subscribe via useRewardQueueStore selectors:
    • RewardPopup (in src/metagame/components/) renders the front of queue and calls dismiss() on tap or auto-dismiss timeout.
    • RewardOrchestrator (in src/starship-survivors/components/) watches phase + activeBatch and drives phase transitions via setPhase / finishCurrent / deferCurrentResolve.
    • HomeResolveController (hub-only) watches for phase === 'resolve', animates progress bars, calls surfaceReady() on mount.
    • reward-finalizers.ts calls enqueue(batch) after committing domain mutations and locking display overrides.

DOES NOT

  • Does not run timers, animations, or async work. The orchestrator owns the runtime.
  • Does not persist anything. State is in-memory only; resets on reload.
  • Does not coalesce or dedupe items in the popup queue — every call to enqueue(items[]) appends after priority-sort.
  • Does not check item content or batch source — sorting is by priority field only; phase advancement is by PHASE_ORDER only.
  • Does not drive the resolve phase logic — resolve is handled by HomeResolveController; the orchestrator’s case 'resolve' is a no-op.
  • Does not currently implement deferred-resolve backlog: surfaceReady() is a stub no-op, and deferCurrentResolve() discards activeBatch and returns to idle rather than queuing for later replay.
  • Does not enforce that a popup queue and an active batch are mutually exclusive — both surfaces can be active simultaneously.

Signals

  • enqueue(items) is polymorphic on its argument:
    • RewardItem[] — sorts by priority (highest first) and appends to queue; sets isShowing: true. Empty array is a no-op.
    • RewardBatch — sets activeBatch and forces phase: 'reveal'. Skips the popup queue entirely.
  • dismiss() slices the front of queue and recomputes isShowing from remaining length — once queue empties, the overlay hides on the next dismiss.
  • current() returns the front item or null — selector-style read, not a subscription target.
  • finishCurrent() clears activeBatch and resets phase to idle.
  • resolveNextPhase(batch, currentPhase) is a pure module function returning the next phase or null if the batch is exhausted. Used by RewardOrchestrator.advanceOrFinish.

Entry points

  • useRewardQueueStore — Zustand hook; the only export consumers use to read or mutate state.
  • enqueue(items) — sole admission point. reward-finalizers.ts is the canonical caller for RewardBatch; legacy/prologue paths pass RewardItem[].
  • dismiss() — called by RewardPopup on tap or auto-dismiss.
  • clear() — drops the entire popup queue; hides overlay.
  • setPhase(phase) / finishCurrent() / deferCurrentResolve() — pipeline transitions called by RewardOrchestrator and HomeResolveController.
  • surfaceReady() — called by HomeResolveController on hub mount (currently no-op).
  • makeRewardItem(...) — used by PrologueScreen and similar simple grant sites that construct RewardItem directly.
  • REWARD_PRIORITY, resolveNextPhase, RewardItem — exported for external consumers.

Pattern notes

  • Two coexisting surfaces by design: legacy popup queue for simple immediate grants, phase pipeline for the new batch-presentation flow. Both share the store but never interact.
  • enqueue polymorphism via Array.isArrayRewardItem[] vs RewardBatch is the discriminator.
  • Module-level _idCounter is the sole source of RewardItem.id — IDs are unique within a session but not across reloads.
  • The phase state machine is declarative-only: the orchestrator reads it, runs side effects, then writes the next state. The store never advances itself.
  • PHASE_ORDER is the single source of phase sequence; resolveNextPhase is the only function that interprets it.
  • await_surface and defer_until_hub (the RewardBatch.presentation.resolvePolicy) are the seams for deferring resolve animations until HubScreen is mounted — but the backlog/replay machinery is stubbed (surfaceReady no-op, deferCurrentResolve drops the batch).
  • No subscription/middleware (persist, devtools) attached at the store level.
  • Crash-on-bad-data does not apply here: unknown phases or empty batches fall through silently — phase advancement returns null for out-of-range indices and finishes the batch.