PURPOSE

Hydrated runtime cache of per-planet tier progression and milestone-claim state. Tier is the 4-minute mission difficulty escalation counter (mirrors game._currentLevel). Each planet tracks its own personal best tier and the set of milestone rewards already collected. Supabase is the source of truth; this store is populated on app open and patched from RPC responses.

OWNS

  • records: Record<number, number> — personal-best tier keyed by planetId.
  • claims: Record<number, Set<number>> — set of claimed milestone numbers per planet.
  • Selector logic for highest-tier lookup, next-unclaimed-milestone resolution, and claim-eligibility checks.
  • Mutation actions: updateFromRunResult, addClaim, loadFromBootstrap.
  • The reward formula getTierMilestoneGems(milestone) returning milestone * 2, which mirrors the server formula in claim_tier_milestone.

READS FROM

  • bootstrap_player RPC response shape via TierRecordRow[] and TierClaimRow[] passed to loadFromBootstrap.
  • finalize_run RPC result via updateFromRunResult(planetId, highestTier).
  • claim_tier_milestone RPC outcome via addClaim(planetId, milestone).

PUSHES TO

  • Nothing. This is a passive cache. Consumers subscribe via useTierStore selectors. Callers (RPC layer / screens) invoke the mutation actions after server confirmation.

DOES NOT

  • Does not make network calls. No Supabase client, no fetch.
  • Does not persist to localStorage or IndexedDB. State is rebuilt from bootstrap_player on each app open.
  • Does not compute or grant gems. Gem rewards are awarded server-side by claim_tier_milestone; getTierMilestoneGems is a display/preview helper only.
  • Does not validate milestone eligibility against the server. canClaimMilestone is an optimistic UI gate; the RPC is authoritative.
  • Does not track active-run tier. Active tier lives in the engine (game._currentLevel); only the post-run highest tier lands here.
  • Does not handle multi-account state. The store is reset implicitly by re-hydrating from bootstrap.

Signals

  • records[planetId] changes when a finalized run reports a new high tier (monotonic via Math.max).
  • claims[planetId] gains a milestone number after a successful claim RPC.
  • getNextMilestone returns the smallest unclaimed multiple of 5 between 5 and 1000, defaulting back to 5 if all are claimed.
  • canClaimMilestone flips true when highestTier >= nextMilestone.

Entry points

  • useTierStore — Zustand hook. Default selector pattern across the metagame UI.
  • useTierStore.getState().loadFromBootstrap(records, claims) — called after bootstrap_player resolves.
  • useTierStore.getState().updateFromRunResult(planetId, highestTier) — called by the run-finalization handler.
  • useTierStore.getState().addClaim(planetId, milestone) — called after claim_tier_milestone succeeds.
  • getTierMilestoneGems(milestone) — pure export for previewing milestone reward sizes.

Pattern notes

  • Zustand 5 create<TierState> with selectors and actions colocated.
  • Milestones are multiples of 5, walked iteratively up to 1000 — bounded loop, no recursion.
  • updateFromRunResult is monotonic: it never lowers a record, only raises it via Math.max.
  • addClaim clones the inner Set before mutation to preserve referential change semantics for Zustand subscribers.
  • Server shape interfaces (TierRecordRow, TierClaimRow) use snake_case to match Supabase row payloads; in-store shape is camelCase / numeric maps.
  • Authority model is documented in the file header: Supabase truth, store cache. No optimistic writes without RPC confirmation.