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 by hull_class.
  • ShipInstance shape: { shipId: string; xp: number }. First pull = xp 0; each subsequent pull = +1 xp.
  • STARTER_HULLS constant: Industria_Towncar, Junkrats_Tank, Solaris_Cargo — granted on fresh accounts and on reset.
  • makeStarter() helper that builds the starter inventory map.

READS FROM

  • ../data/shipsHULL_CLASSES array, used to validate hull_class arguments on grantShipPull.
  • ../data/ship-progressionstarFromXp function, used to derive star level from cumulative XP.
  • zustandcreate factory.

PUSHES TO

  • Nothing direct. The store is a pure in-memory cache; persistence to Supabase player_entities is handled by external save/load layers that call loadInventory to hydrate and read ships to 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 (see docs/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; getShipCount returns 1 or 0 only.
  • Does not silently accept unknown hulls — grantShipPull throws on hull_class not in HULL_CLASSES.

Signals

  • grantShipPull(hull_class) returns { unlocked: boolean; xpGained: number }unlocked: true on first pull (xpGained 0), unlocked: false on subsequent pulls (xpGained 1). Callers can branch on unlocked for 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 at xp 0; subsequent calls increment xp by 1.
  • loadInventory(ships) — replaces ships map wholesale (hydration from Supabase backup).
  • resetToStarter() — replaces ships with makeStarter() output.
  • Selectors: isUnlocked, currentXp, currentStar, ownsShip, getShipCount, ownedShipIds.
  • STARTER_HULLS, ShipInstance, InventoryState — exported for external consumers and tests.

Pattern notes

  • v4 hull-keyed model: shipId field on ShipInstance equals the hull_class map key — redundant by design for legacy call-site compatibility.
  • Crash-on-bad-data: unknown hull_class throws rather than silently no-op’ing.
  • Immutable updates use spread on both ships map and the individual ShipInstance.
  • ownsShip is an explicit alias of isUnlocked — kept for readability at call sites.
  • getShipCount returns 0 | 1 only; legacy call sites that counted duplicates need to read currentXp instead.
  • 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.