PURPOSE
Zustand store backing the account-wide Mod Grid metagame system. Maintains the player’s mod inventory, the 4x4 grid of equipped mods, and credit-purchased cell unlocks. Implements merge-to-upgrade economy: three inventory copies sharing (templateId, rarity) fuse into one copy at the next rarity tier. Equipped mods never participate in merges. Provides placement validation against shape footprints, rotations, and unlocked cells. Persists snapshots to Supabase via debounced RPC.
OWNS
MOD_GRID_SCHEMA_VERSIONconstant (currently 3) used to invalidate incompatible persisted snapshots.ModGridSnapshotshape:schemaVersion,inventory(unplacedModInstance[]),equipped(record of uid toEquippedMod),purchasedCells(string keys of credit-bought cells).EquippedModinterface:templateId,col,row,rotation(0..3),raritydriving run-assemble multipliers.useModGridStoreZustand store: starter snapshot is empty inventory, empty equipped, empty purchasedCells.uidCountermodule-local incrementing counter;nextUid()generates ids of the formm<timestamp36><counter36>._persistTimermodule-local debounce handle (300ms) for Supabase writes.__resetModGridUidCounter()testing helper.
READS FROM
MOD_TEMPLATES_BY_IDfrom../data/modsfor template lookup during validation, placement, and load-time pruning._typesexports:ModTemplate,ModInstance,ModRarity,DROP_RARITY,nextRarity,resolveShape.../data/mods/grid-unlock:cellKey,cellCost,isFreeCell,nextPurchasableCelldefine cell pricing and unlock sequencing.useWalletStoreforspendCreditsduring cell purchase.- Dynamic import of
@metagame/services/supabase(invokeRpc) duringpersistModGrid.
PUSHES TO
- Supabase RPC
save_mod_gridwith payload{ schemaVersion, inventory, equipped_v2, purchasedCells, equipped }. The legacyequippedfield is a uid →{col, row}map kept for older servers;equipped_v2is the full record current clients read. useWalletStore.getState().spendCredits(cost)duringpurchaseCellwhen unlocking non-free cells.console.warnon RPC failure (fire-and-forget; not rethrown).
DOES NOT
- Does not gate cells by captain level. Cell unlocks are credit-purchased through the wallet.
- Does not store free cells (the upper-left 2x2) in
purchasedCells; freeness is computed viaisFreeCell. - Does not count equipped mods toward merges. Only inventory entries are eligible.
- Does not allow merging at legendary rarity (
nextRarityreturns null). - Does not support out-of-order cell purchases. Only the cell returned by
nextPurchasableCellmay be bought. - Does not synchronously block on Supabase writes. Persistence is debounced and dispatched asynchronously.
- Does not preserve snapshots whose
schemaVersionmismatchesMOD_GRID_SCHEMA_VERSION; mismatched snapshots reset to starter. - Does not generate uids that collide between inventory and equipped sets (transitions move uids between collections rather than duplicating).
- Does not validate rotation inputs as a tagged type beyond modulo normalization in
setRotation.
Signals
equipFromInventory(uid, col, row, rotation)returns false if the uid is missing, its template is unknown, orcanPlacerejects the candidate.moveEquipped(uid, col, row)returns false if the uid is not equipped or the new position failscanPlace(ignoring the moving uid).setRotation(uid, rotation)normalizes input to 0..3 and returns false when the rotated footprint no longer fits.rotateEquipped(uid)advances rotation by +1 (90 degrees CW) viasetRotation.unequip(uid)returns false when the uid is not equipped; otherwise re-adds an inventory entry carrying the original rarity.merge(uids)returns the new uid on success, null when any uid is missing, templateIds differ, rarities differ, or the rarity cannot advance.purchaseCell(col, row)returns false on out-of-bounds, already-unlocked, non-sequential, zero-cost, or insufficient-credit conditions.canPlacethrows iftemplateIdis unknown; otherwise returns boolean for bounds, unlocked-cell coverage, and overlap against other equipped mods.addDropthrows on unknowntemplateId; otherwise returns the generated uid.
Entry points
useModGridStore— Zustand hook used by metagame UI (mod grid screen, inventory pane, merge interactions).persistModGrid(snap)— exported debounced writer for callers that want to persist a snapshot directly without going through the store action.MOD_GRID_SCHEMA_VERSION— exported constant for migration and validation checks elsewhere.EquippedMod,ModGridSnapshot,ModGridState— exported types consumed by UI components and load pipelines.loadFromSnapshot(snap)— rehydrates the store from a partial snapshot, pruning unknown templates and rejecting mismatched schema versions.resetToStarter()— restores the empty starter snapshot.persist()— action wrapper that callspersistModGridwith the current state.__resetModGridUidCounter()— test-only counter reset.
Pattern notes
- Inventory and equipped uids form disjoint pools; transitions (
equipFromInventory,unequip) move the uid between collections rather than copying. - Inventory grouping (
inventoryStacks) orders bytemplateIdascending and rarity in the explicit sequence common, uncommon, rare, epic, legendary. canPlaceperforms two passes: a bounds-and-unlocked-cell sweep over the filled footprint cells, then an overlap sweep against every other equipped mod’s resolved shape (skippingignoreUidto support in-place rotation and moves).- Cell unlock pricing tiers come from
grid-unlock.ts: the 2x2 corner is free, the cells completing the 3x3 cost 1,000 credits, and the seven outer-rim cells cost 10,000 credits. Sequencing is enforced vianextPurchasableCell. persistModGriddebounces at 300ms and writes both the modernequipped_v2map and a legacyequipped(uid → col/row only) map in the same payload for backward compatibility with older servers.loadFromSnapshotprunes both inventory entries and equipped entries whosetemplateIdno longer exists inMOD_TEMPLATES_BY_ID, preventing crashes after template removals.- Rarity advancement is delegated to
nextRarity; legendary rarity intentionally has no successor and merges stop there. - The persistence path is fire-and-forget: failures are logged via
console.warnand never propagated to callers. uidCounteris wrapped to unsigned 32-bit (>>> 0) to bound growth across long sessions; the timestamp prefix preserves uniqueness across resets.