PURPOSE
Zustand store that holds the player’s v4 hull-keyed ship collection. Each owned hull is a single record (not per-instance) — duplicate pulls grant XP toward the same hull, and star level is derived from cumulative XP. Acts as the client-side cache; Supabase player_entities is the backup.
OWNS
ships: Record<string, ShipInstance>— owned hulls keyed byhull_class.ShipInstanceshape:{ shipId: string; xp: number }. First pull =xp 0; each subsequent pull =+1 xp.STARTER_HULLSconstant:Industria_Towncar,Junkrats_Tank,Solaris_Cargo— granted on fresh accounts and on reset.makeStarter()helper that builds the starter inventory map.
READS FROM
../data/ships—HULL_CLASSESarray, used to validatehull_classarguments ongrantShipPull.../data/ship-progression—starFromXpfunction, used to derive star level from cumulative XP.zustand—createfactory.
PUSHES TO
- Nothing direct. The store is a pure in-memory cache; persistence to Supabase
player_entitiesis handled by external save/load layers that callloadInventoryto hydrate and readshipsto serialize.
DOES NOT
- Does not persist to Supabase or any storage. No I/O, no async work.
- Does not track per-ship installed mods — removed in v5.64+; mods are account-wide in
useModGridStore(seedocs/superpowers/specs/2026-04-18-mod-merge-system-master-plan.md). - Does not store per-instance ship records — v4 collapsed duplicates into a single record per
hull_class. - Does not count duplicate pulls as separate ships;
getShipCountreturns 1 or 0 only. - Does not silently accept unknown hulls —
grantShipPullthrows onhull_classnot inHULL_CLASSES.
Signals
grantShipPull(hull_class)returns{ unlocked: boolean; xpGained: number }—unlocked: trueon first pull (xpGained0),unlocked: falseon subsequent pulls (xpGained1). Callers can branch onunlockedfor unlock VFX vs. star-up checks.- All selectors are synchronous getters reading from
get(); safe to call inside React renders or game-loop ticks.
Entry points
useInventoryStore— the Zustand hook; the only export consumers use to read or mutate state.grantShipPull(hull_class)— sole mutation path for pulls. First call creates a record atxp 0; subsequent calls incrementxpby 1.loadInventory(ships)— replacesshipsmap wholesale (hydration from Supabase backup).resetToStarter()— replacesshipswithmakeStarter()output.- Selectors:
isUnlocked,currentXp,currentStar,ownsShip,getShipCount,ownedShipIds. STARTER_HULLS,ShipInstance,InventoryState— exported for external consumers and tests.
Pattern notes
- v4 hull-keyed model:
shipIdfield onShipInstanceequals thehull_classmap key — redundant by design for legacy call-site compatibility. - Crash-on-bad-data: unknown
hull_classthrows rather than silently no-op’ing. - Immutable updates use spread on both
shipsmap and the individualShipInstance. ownsShipis an explicit alias ofisUnlocked— kept for readability at call sites.getShipCountreturns0 | 1only; legacy call sites that counted duplicates need to readcurrentXpinstead.- Star level is never stored — always derived via
starFromXp(xp)so progression-curve changes apply retroactively without migration. - No subscription/middleware (persist, devtools) attached at the store level — persistence is external.