PURPOSE
Hydrated runtime cache of per-planet XP and reward claim state. Supabase is the source of truth; this Zustand store mirrors it for synchronous UI reads. Populated from bootstrap_player on app open and updated from finalize_run / claim_planet_reward RPC responses.
OWNS
xp: Record<number, number>— cumulative XP per planet, keyed by numericplanet_id.claims: Record<number, Set<number>>— set of claimed reward levels per planet.- Derived selectors:
getXp,getLevel,isRewardClaimed,canClaimReward,hasAnyClaim. - Mutators:
setXp(absolute replace),addXp(optimistic local add),addClaim,loadFromBootstrap(full hydrate).
READS FROM
getLevelForXpfrom../data/planet-progression— maps cumulative XP to a level (0–10) for a given planet.PlanetIdtype from../data/planet-config.- Server-shaped rows
PlanetXpRowandPlanetRewardClaimRowsupplied by the bootstrap caller.
PUSHES TO
- Nothing directly. The store is a passive cache. UI components (
PlanetProgressBar,PlanetTrackViewer) subscribe and re-render on state changes.
DOES NOT
- Does not call Supabase or any network API.
- Does not persist to localStorage or any local cache.
- Does not compute or award XP —
runProgressionServicecallssetXp/addXpafter the server returns. - Does not validate XP totals or reject claim levels — trusts the caller.
- Does not handle authentication, multi-account swapping, or store reset on logout.
- Does not own the XP-to-level curve — defers to
getLevelForXp.
Signals
xpmap updates onsetXp,addXp, andloadFromBootstrap.claimsmap updates onaddClaimandloadFromBootstrap.addClaimclones the existingSetbefore insertion so subscribers see a new reference.setXpandaddXpspreadstate.xpto produce a new outer object on every write.getXpdefaults missing planets to0.getLeveldoes the same before delegating togetLevelForXp.canClaimRewardreturnsfalseif the requested level exceeds the planet’s current level, elsetrueiff the level is not in the claims set.hasAnyClaimscans levels1..currentLeveland returnstrueon the first unclaimed level.
Entry points
usePlanetProgressStore— the Zustand hook; default export pattern is the named hook.loadFromBootstrap(xpRows, claimRows)— wholesale replace of both maps frombootstrap_playerpayload.setXp(planetId, total)— server-confirmed XP write afterfinalize_run.addXp(planetId, amount)— optimistic pre-server XP bump.addClaim(planetId, level)— record a successfulclaim_planet_reward.- Read-only selectors are called directly off the store inside components.
Pattern notes
- Numeric
planet_idkeys are used internally;PlanetId(the typed union) is accepted at the surface and coerced via object indexing. claimsvalues areSet<number>rather than arrays — guarantees uniqueness and O(1)haslookups for reward checks.loadFromBootstraprebuilds the entireSetper planet from row stream; later duplicate rows in the same payload are absorbed bySetsemantics.- Authority model is server-truth + runtime mirror; there is no merge/conflict logic. The latest server payload wins via
setXp(replace) and any localaddXpis overwritten on the next server confirmation. - All derived state is computed on read inside selectors; no memoization. Callers re-derive levels on every read.
PlanetXpRowandPlanetRewardClaimRoware exported so the bootstrap service can type its parse step against the same shape.