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 byplanetId.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)returningmilestone * 2, which mirrors the server formula inclaim_tier_milestone.
READS FROM
bootstrap_playerRPC response shape viaTierRecordRow[]andTierClaimRow[]passed toloadFromBootstrap.finalize_runRPC result viaupdateFromRunResult(planetId, highestTier).claim_tier_milestoneRPC outcome viaaddClaim(planetId, milestone).
PUSHES TO
- Nothing. This is a passive cache. Consumers subscribe via
useTierStoreselectors. 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_playeron each app open. - Does not compute or grant gems. Gem rewards are awarded server-side by
claim_tier_milestone;getTierMilestoneGemsis a display/preview helper only. - Does not validate milestone eligibility against the server.
canClaimMilestoneis 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 viaMath.max).claims[planetId]gains a milestone number after a successful claim RPC.getNextMilestonereturns the smallest unclaimed multiple of 5 between 5 and 1000, defaulting back to 5 if all are claimed.canClaimMilestoneflips true whenhighestTier >= nextMilestone.
Entry points
useTierStore— Zustand hook. Default selector pattern across the metagame UI.useTierStore.getState().loadFromBootstrap(records, claims)— called afterbootstrap_playerresolves.useTierStore.getState().updateFromRunResult(planetId, highestTier)— called by the run-finalization handler.useTierStore.getState().addClaim(planetId, milestone)— called afterclaim_tier_milestonesucceeds.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.
updateFromRunResultis monotonic: it never lowers a record, only raises it viaMath.max.addClaimclones the innerSetbefore 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.