data
PURPOSE — Read-only catalog of game content (weapons, enemies, bosses, artifacts, ships, mods, passives, planets) and the shared schemas everything downstream consumes. Pure TypeScript with no runtime side effects beyond module-load asserts; importable in a Vitest test environment without touching window, document, localStorage, or any store. The foundation everything else builds on — engine and metagame read from data, never the reverse.
OWNS
- Per-domain item registries built from the one-file-per-item pattern: each domain folder holds an
_types.ts(item type definition), anindex.ts(assembles the array and lookup map, runs module-load invariant asserts, and exposes both), and one source-of-truth file per item. - The five primary content registries — artifacts, weapons, enemies, mods, passives — and the boss registry that self-registers per-file via top-level mutation of a shared map.
- Lookup maps from item id to definition for every registry, built at module load by walking the array and indexing into a
Record<string, Def>. - Top-level lookup tables that hold rosters / palettes / tuning curves and frequently re-export per-domain folders: ships and hull classes, planet roster, nebula archetypes, level presets, terrain shapes, theme palette tokens, weapon icons, hull hitboxes, ship rarity metadata, kill streaks, pull rates and configs, boss scaling curves, reward cards, reward types, progress key constants, prologue script, run modifiers, economy constants.
- Schema / contract types that define the shape of data consumed elsewhere: the run definition (engine input), the mission result (engine output), per-level configuration, the persistent player save format with versioning, run history records, currency / resource id enums, per-ship and per-planet progression curves, per-planet challenge definitions.
- Enemy type generation: an 8 × 5 rarity cube produced by walking the base archetype list against the rarity multiplier table, plus a hand-authored boss-roster patch list pushed onto the cube to give faction-specific bodies their own typeIds, plus the hull-shape map that routes typeId → polygon archetype for the renderer.
- Spawn pool tables banded by run progress (default and per-planet variants), with elite-leader pickers and per-set leader archetype lists.
- The save migration runner: a version-keyed migration map, a current-version constant, and a runner that throws if no migration is registered for an intermediate version (no silent fallback).
- Boss scaling math: per-stat dimension configs (
uncapped/soft_cap/hard_cap), the dimension resolver, and the default scaling config for all bosses. - Run-level progression sequences: the fixed 5-step normal arc and the 10-step challenge arc, plus the
LevelKindresolver and final-level test, plus sealed-arena geometry and boss-kind HP / damage multipliers. - Run history persistence:
localStorage-backed FIFO buffer of compact run summaries, per-node personal-best keys for tier / kills / events / level, the new-account spawn dampener curve, run averages, and personal-best extractors. The single boundary file indata/that toucheslocalStorage— it lives here because the schema is the canonical run-history record shape, not because it does any game logic. - Color tokens (raw + semantic), rarity palette, font stacks, VFX trait library, and a number-formatter utility.
- Constants for tier color palettes, rarity multipliers, runtime-vs-data tier index bridging, flat-bonus percentage ramps by tier, per-artifact flat-bonus stat mappings.
READS FROM — nothing internal. Data is the leaves of the dependency tree. The purity rule (in data/README.md) forbids importing from ../engine/, ../stores/, ../services/, ../screens/, ../components/, or any browser API at module load. The only outbound imports are intra-domain (top-level weapons.ts re-exports the weapons/ folder; the artifact effect-def field references an engine effect type via a import('...') type-only reference).
PUSHES TO — none. Read-only by contract. Every consumer pulls from the exposed maps / arrays / helper functions; the data layer never mutates external state, never fires signals, never writes to stores, never calls into the engine.
DOES NOT
- Mutate any state at runtime. The only writes that happen at module load are populating lookup maps from arrays and running invariant asserts.
- Perform I/O, network calls, Supabase round-trips, or async work at module load.
- Import browser APIs (
window,document,localStorage,navigator,fetch) except in the one file whose schema explicitly is thelocalStorage-backed run-history record — and even there the storage access is gated behind try/catch so the surrounding module load can’t fail. - Hold runtime game state (active bullets, live enemies, ship instance) — that lives in the engine and stores.
- Decide which enemy spawns next, which weapon fires, which artifact procs — those are engine concerns that read from these tables.
- Render anything, play audio, or queue VFX — only defines the palette tokens, VFX trait library, and per-item color metadata the renderers consume.
- Validate gameplay legality (e.g. “can this ship equip this weapon”) — only the engine / metagame enforces those rules using the read-only metadata exposed here.
- Define implementation behavior for artifacts, mods, passives, or weapon archetypes — those handlers live in
engine/; this layer only carries the named numeric params and ids. - Resolve player progression, account state, or mission selection — only defines the contract shapes those systems pass through.
Signals fired / Signals watched — none. Read-only data has no event surface.
Entry points
WEAPONS,WEAPON_MAP,WEAPON_ORDER— array, id-to-spec lookup, and ordered id list for the weapon roster (including legendary fusions appended at the end).LEGENDARY_WEAPONS,LEGENDARY_PAIR_MAP,getLegendaryForPair,pairKey,sortTagPair— fusion-result resolution.RARITY_COLORS(weapons) — UI hex colors per rarity tier.getExtraProjectiles— bonus-projectile resolver from the horizontal upgrade count, table-driven per weapon id.getEffectiveLevel,getWeaponStatAtLevel,getSteppedStatAtLevel,getProbabilisticSteppedStat,getVfxTier,getWeaponDamageMult,LEGENDARY_DAMAGE_BASELINE_MULT— per-weapon stat-pipeline helpers.ENEMY_WEAPON_STATS— enemy-side weapon stat table.resolveWeaponRarity— flattens stored rarity to'common' | 'legendary'.ENEMY_TYPES,ENEMY_TYPE_MAP— the full 8 × 5 archetype-rarity cube plus the appended boss-roster bodies, indexed by typeId.SPAWN_POOLS,getSpawnPool,getSpawnPoolForSet,pickEliteForSet,pickEliteForProgress,SET_SWARM_ARCHETYPE— progress-banded spawn-pool resolvers, including per-planet variant pools.RARITY_TINTS,RARITY_MULTS,RARITY_ORDER,ENEMY_TIER_COLORS,PERSONALITY_PROFILES— enemy rarity scaling and presentation metadata.ENEMY_COLLISION_SCALE,getEnemyCollisionRadius— collision-radius helper.HULL_SHAPE_MAP— typeId-to-polygon-archetype routing for the renderer, with explicit entries per rarity per archetype plus per-boss-roster passthrough.ARTIFACT_DEFS,ARTIFACT_MAP— array and lookup of artifact definitions across the three categories (unique,stat,weapon).ARTIFACT_TIER_NAMES,ARTIFACT_TIER_MAX,ARTIFACT_TIER_COLORS,ARTIFACT_TIER_COLOR_BY_IDX,FLAT_BONUS_PCT_BY_TIER,getTierValuesAt,getTierLabelAt,ARTIFACT_FLAT_BONUSES,getArtifactFlatBonus,getArtifactFlatBonusValueAt— runtime-tier-to-data-tier bridging, per-tier flat-bonus ramp resolution, per-artifact stat-mapping lookup.MOD_TEMPLATES,MOD_TEMPLATES_BY_ID— array (frozen) and lookup of the mod template catalog.RARITY_MULTIPLIER,RARITY_ORDER,RARITY_COLORS(mods),DROP_RARITY,nextRarity,shape,rectCells,iterateFilledCells,filledCellCount,validateShape— mod-shape primitives and rarity scaling.PASSIVES,getPassiveValue,getPassiveDescription,resolvePassive,RARITY_INDEX— passive-by-id lookup and rarity-aware value resolution.BOSS_DEFS,bossDefKind,pickRandomBossId— boss registry mutated by per-file self-registration, plus the kind classifier and pool picker.HULL_CLASSES,getShipDef,toShipCombatStats,toShipMetaStats— ship/hull roster lookup and stat extraction.PLANETS,PLANET_ORDER,PlanetDef— planet roster.DEFAULT_RUN,validateRunDef,RunDefinition(and its sub-interfaces —ShipCombatStats,ShipMetaStats,FacilityBonuses,WorldKnobs,SessionBonuses,CodexBonuses,MissionObjective,BossConfig,VisionConfig,EventPoolConfig,StartingWeapon) — the engine input contract and its assertion-based validator.EVENT_KILL_THRESHOLD— per-run kill gate before sub-events spawn.MissionResult— the engine output contract consumed by the results screen.LevelConfig,DEFAULT_LEVEL_CONFIG,DEFAULT_SPAWN_ZONES,DEFAULT_PALETTE,Hub,Spoke,PrecomputedWorld,ZoneType,PatternType,SpokeStyleId,HubStyleId,TerrainTypeId,HubParticleType,LanternPresetId,WorldMode,LevelTrigger,TriggerType,TriggerPayload,ColorPalette,SpawnZoneConfig,ZonePool,ZonePoolEntry,SpokeFlowConfig,ScriptedHub,ScriptedSpoke— per-level configuration, defaults, hub/spoke runtime types, and the trigger contract.RUN_LEVEL_SEQUENCE,CHALLENGE_LEVEL_SEQUENCE,LevelKind,resolveLevelKind,isFinalLevel,SEALED_ARENA_HALF_SIZE,SEALED_ARENA_WALL_THICKNESS,BOSS_KIND_HP_MULT_MINI,BOSS_KIND_HP_MULT_BOSS,BOSS_KIND_DAMAGE_MULT_MINI,BOSS_KIND_DAMAGE_MULT_BOSS— run-level progression sequence helpers plus arena geometry and boss-kind multipliers.BossScalingConfig,ScalingDimension,CapType,ResolvedBossScaling,computeDimension,resolveBossScaling,DEFAULT_BOSS_SCALING,BASE_ENRAGE_TIMER_SEC— per-stat scaling resolver with three cap behaviors.SaveBlob,CURRENT_SAVE_VERSION,runSaveMigrations— persistent save schema and version-gated migration runner.RunHistorySummary,saveRunToHistory,getRunHistory,getNewAccountSpawnMult,getRunAverages,getPersonalBests,getTierPB/saveTierPB,getKillsPB/saveKillsPB,getEventsPB/saveEventsPB,getLevelPB/saveLevelPB— local run-history record, FIFO save, new-account dampener, averages / PBs, and strictly-improving per-node personal-best persistence.PROGRESS_KEYS,ProgressKey— cumulative-stat tracking keys used by the achievement system and rookie-week curve.SP,THEME,RARITY_COLORS(theme),VFX_TRAITS,formatNum,RarityColor,VfxTrait— palette tokens, semantic UI theme, rarity colors used app-wide, named VFX trait library, and the K/M number formatter.- Top-level re-exports —
weapons.ts,artifacts.ts,enemies.ts,passives.ts,mod-templates.tsare barrels that re-export from their per-domain folders so older consumers keep importing from the flat path while new code may use either.
Pattern notes
- Every primary content domain follows the one-file-per-item registry pattern:
_types.tsdefines the item type,_helpers.ts(optional, currently only inweapons/) holds shared math,index.tsimports each item module, builds the array and lookup map, runs any module-load invariants, and exports both, and each<item-id>.tsexports a single named const matching the item type. Per-item files are the source of truth — no item data lives inindex.ts. - Module-load
throws for invariant assertion are encouraged and used in practice (mods validate shape, assert id uniqueness, and assert the expected template count). They catch contract violations before any test or build step touches the data. - Lookup maps are built imperatively in a tight loop right after the array literal —
for (const x of ARRAY) MAP[x.id] = x;— and are exported as plainRecord<string, Def>. The mod map is additionallyObject.freezed. - Top-level files split into two flavors: registries / lookup tables (
ships.ts,planet-config.ts,nebula-archetypes.ts,level-presets.ts,terrain-shapes.ts,theme.ts,weapon-icons.ts,hull-hitboxes.ts,ships-v4-rarity.ts,kill-streaks.ts,pull-rates.ts,pull-config.ts,boss-scaling.ts,reward-cards.ts,reward-types.ts,progress-keys.ts,prologue-config.ts,modifiers.ts,economy.ts) and schemas / contracts (run-config.ts,mission-result.ts,level-config.ts,save-schema.ts,save-migrations.ts,run-history.ts,resources.ts,ship-progression.ts,planet-progression.ts,challenges.ts). New top-level files should pick a flavor before being added and group with their peers. - Top-level files like
weapons.ts,artifacts.ts,enemies.ts,passives.ts,mod-templates.tsare re-export barrels — the actual definitions live in the per-domain folders, but older import paths keep working. New code may import from either. - The boss registry inverts the pattern: instead of
index.tsimporting definitions and building the map, each per-boss file is imported for side effect and then explicitly assigned into the sharedBOSS_DEFSmap via top-level mutation. The lookup is read-mostly afterward. - Enemy types are generated, not enumerated: 8 base archetypes × 5 rarities are produced by
buildEnemyTypes()at module load, walkingRARITY_ORDERand applyingRARITY_MULTSto each base. Archetype-specific tuning (AOE, blast, field, sniper, burst) is grafted on inside the loop usingif (base.archetype === ...)branches. Faction-specific boss bodies are then appended viaENEMY_TYPES.push(...BOSS_ENEMY_TYPES). - Spawn pools are stored as banded arrays keyed by progress min/max, walked in reverse to find the latest band the progress value satisfies. Per-planet variants live in
VARIANT_POOL_MAPand override the default; sets that aren’t in the map fall back toSPAWN_POOLS(orCITY_SPAWN_POOLSfor the city set). - Curve-based scaling (boss-scaling, weapon scaling) uses tent-pole keyframe arrays of
{ time, value }pairs that the engine lerps between. Out-of-bounds inputs clamp to the nearest pole, not throw. - The save migrations file enforces “no silent fallback”: if the runner encounters a
save_versionbelowCURRENT_SAVE_VERSIONwith no registered migration, it throws. The migration map is aRecord<number, Migration>where each key is the from-version and the function returns the next-version blob. - Run-level progression resolves a
LevelKindenum via a fixed sequence array indexed bycurrentLevel - 1, clamped to the final entry for out-of-bounds inputs so dev mode never crashes the engine. Challenge mode swaps the sequence array. RunDefinitionis the single source of truth for the engine input contract — every field is required (no optionals except a handful of explicitly marked dev/debug knobs), andvalidateRunDefis a flat assertion list that throws on missing / out-of-range fields rather than coercing.MissionResultmirrors the same shape on the way out: a frozen contract with a__versiondiscriminant and a__contractname, every field required, consumed by the results screen and downstream progression / economy systems.- The artifact tier system has a 5-tier runtime ladder (
common, uncommon, rare, epic, legendary) but data files carry a 4-tupletiersarray (uncommon → legendary);getTierValuesAtbridges the index gap by clamping the runtime tier into the data range. Differentiation between runtime common and uncommon comes from the linear flat-bonus ramp inFLAT_BONUS_PCT_BY_TIER, not from per-tier ability values. ARTIFACT_FLAT_BONUSESis a parallel map alongsideARTIFACT_MAPthat maps artifact id → flat-stat-bonus descriptor. The runtime stat palette is intentionally small (weaponDamagePct,fireRatePct,maxSpeed,magnetRange,luck,damageReduction), so artifacts in the same archetype share a stat — the repeats across the table are intentional.- The
run-history.tsfile is the single exception to the “no browser APIs” purity rule, because its entire purpose is to define and persist the run-history schema inlocalStorage. AlllocalStorageaccess is wrapped in try/catch so failures degrade silently rather than crash module load. Personal-best writes are strictly improving — they return false on stale value, invalid input, or storage failure. - Rarity ordering is canonical:
['common', 'uncommon', 'rare', 'epic', 'legendary']is used everywhere (enemies, mods, ships, artifacts) as the source of truth for index-to-name resolution. WoW-style hex colors are reused across most rarity-color maps (artifacts, mod drops, weapons), with theme.ts’sRARITY_COLORSbeing a separate green/blue/epic/legend palette for the older 4-tier system. - Tier-tagged additions to the spawn-pool tables (Sprinter, Spitter, Burner, Suppressor at ticks 48–52, Brute / Wisp / Lurker / Bombardier at ticks 25–26) reuse existing archetype behaviors and polygon shapes; the
HULL_SHAPE_MAPcarries explicit per-archetype-per-rarity entries plus a backfill for the t25/t26 cohort that previously relied on a polygon fallback. - The data layer is the contract — engine and metagame call into it, never the reverse. If a file in
data/needs to import fromengine/orstores/, the layering is broken and the file belongs inservices/instead.