PURPOSE
Pure functions for mod-grid placement validation and stat aggregation. Layer between mod-template data and any caller (UI, store, or future server-side validation) that needs to ask “does this mod fit here?” or “what is the total stat bonus from these mods?“. No store reads, no side effects — all inputs are passed in explicitly. Supports the v5.64+ global 4×4 Mod Grid with Survivor.io-style merge, where instances carry { uid, templateId, rarity } and stats are computed as template.stats × RARITY_MULTIPLIER.
OWNS
PlacedModinterface — extendsModInstancewithcol/rowgrid coordinates for UI consumption.canPlace— bounds + overlap check for a candidate template at a target cell, honoring per-template cell masks (L/T/S/Z shapes have empty corners) and an optionalignoreUidto allow self-replacement.computeOccupancy— produces agridRows × gridColsboolean matrix marking which cells are filled by any placed mod.computeEquippedStatBonus— sums rarity-multiplied stat blocks across an array of equipped instances, returning aModStatBlock.statsForInstance— returns the rarity-multiplied stat block for a single instance (used for tooltip deltas).- Internal
fillSethelper that builds the"col,row"string set of cells a template occupies at a given origin.
READS FROM
../data/mod-templatesfor the typesModStatBlock,ModTemplate,ModInstance,ModRarityand the valueRARITY_MULTIPLIER.- Caller-supplied
Record<string, ModTemplate>template lookup — the service never imports a template registry directly. - Caller-supplied
placed: PlacedMod[]arrays, mod-instance arrays, and grid dimension arguments.
PUSHES TO
Nothing. Pure functions only — outputs are returned as new objects (Set<string>, boolean[][], ModStatBlock). No global state mutation, no events, no logs.
DOES NOT
- Does not read from
modGridStoreor any Zustand store. - Does not import the template registry; callers pass the
templatesrecord so the service stays reusable for server-side validation. - Does not mutate its input arrays or the supplied templates record.
- Does not handle drag/drop, animation, snapping, or any UI concern.
- Does not persist or load snapshots — that responsibility lives in
modGridStore. - Does not compute rotation —
template.cellsis consumed as-is.
Signals
None. The module is signal-free: callers invoke functions and use the returned values directly.
Entry points
canPlace(placed, templates, template, col, row, gridCols, gridRows, ignoreUid?)— validates a prospective placement.computeOccupancy(placed, templates, gridCols, gridRows)— builds a boolean grid for UI highlighting.computeEquippedStatBonus(mods, templates)— total stat bonus from a set of equipped mods.statsForInstance(mod, templates)— per-instance rarity-multiplied stats.- Re-exports the types
ModStatKey,ModInstance,ModRarityfor downstream convenience.
Pattern notes
- Two divergent missing-template policies coexist by design:
canPlaceandcomputeOccupancyandstatsForInstancethrow on missing templates because they describe an in-progress state that must be coherent;computeEquippedStatBonussilently skips unknowntemplateIds to tolerate inventories carrying mods whose templates were deleted in a prior patch, matchingmodGridStore::loadFromSnapshot. - Cell masks are the authoritative shape source. Bounds and overlap checks iterate
template.cellsand skip falsy entries so non-rectangular shapes (L/T/S/Z) correctly leave their empty corners free. - Occupancy uses
[row][col]indexing while placement and fill helpers consume(col, row)arguments — the asymmetry is preserved at the boundary; callers reading the matrix must usegrid[row][col]. - Stat aggregation iterates
Object.entries(tpl.stats)and guardstypeof v === 'number', defending against future non-numeric stat fields. ignoreUidoncanPlaceexists to let the caller validate a “move” by ignoring the mover’s current footprint.- The
fillSetcell key is the literal string${col},${row}— keep that format if any caller ever needs to match against it externally.