assembleRunService

PURPOSE

Assembles a complete RunDefinition from metagame selections — ship choice, planet, optional mission/node overrides, and Challenge Mode flag. This is the single bridge function between the React metagame shell and the game engine: every entry into a run goes through assembleRunDef. It produces a frozen, fully-populated snapshot the engine can consume verbatim, with no further metagame lookups required during the run.

OWNS

  • The assembleRunDef(params) factory function.
  • The AssembleParams interface (caller-facing API).
  • The RARITY_SCALE table — hidden ship-rarity difficulty scale used to soften runs for common ships and leave legendary as the unmodified baseline.
  • Composition order: ship lookup → combat/meta stat derivation → node merge → biome/levelPreset wiring from planet → world-knob multiplication → context assembly → final RunDefinition shape.

READS FROM

  • useInventoryStore — calls currentStar(shipId) to derive ship star from inventory XP (defaults to 1 if zero/undefined).
  • getShipDef(shipId, star) from data/ships — looks up the hull definition. Throws if shipId is not in the roster.
  • toShipCombatStats(shipDef) and toShipMetaStats(shipDef) from data/ships — map ship direct stats onto engine ShipCombatStats / ShipMetaStats shapes.
  • PLANETS from data/planet-config — looks up biome, enemyCountMult, and levelPreset by planetIndex.
  • LEVEL_PRESETS from data/level-presets — resolves the planet’s named levelPreset to a concrete LevelConfig.
  • DEFAULT_RUN from data/run-config — base template providing node, spawn, player, timing, and context defaults including worldKnobs, facilities, and bonuses.
  • ShipRarity typedef from data/ships — keys the RARITY_SCALE map.

PUSHES TO

  • The return value (a RunDefinition) is handed to the engine bridge by call sites — there is no direct push from inside this module.
  • Call sites that consume the return value: LevelSelectScreen, MissionBoardScreen, HubScreen, routes.tsx (game route default), DevKeys, ShipPlaygroundScreen.

DOES NOT

  • Does not mutate any store. Reads useInventoryStore once via getState(); no subscribe, no write.
  • Does not consult useArtifactUnlocksStore for run assembly. The starting-artifact feature was removed (Tick 67); context.startingArtifactId is always null. The store is still used elsewhere to gate artifact eligibility in-run.
  • Does not apply mod-grid stat bonuses — the ship-upgrades feature was removed; ship stats flow straight from shipDef through toShipCombatStats.
  • Does not apply chapter-based content gating. weaponPool, upgradePool, levelupRarityCap, and weaponCacheRarityCap are all set to null (engine uses full pools); eventsUnlocked and starsUnlocked are hard-coded true; eventTier is hard-coded 3.
  • Does not apply facility bonuses. Buildings are disabled — facilities is a shallow copy of DEFAULT_RUN.context.facilities (default zeros) and supplyLevel is 0.
  • Does not validate planetIndex beyond ?? 0. If the index falls outside PLANETS, biome and level-preset wiring are skipped silently and node.biome falls back to whatever DEFAULT_RUN.node provides; the multipliers default to 1.0.
  • Does not seed RNG, generate IDs, or assign mission IDs — those come in via params.node or stay at DEFAULT_RUN.node values.

Signals

  • Throws (propagated from getShipDef) when params.shipId is not in the ship roster. There is no try/catch in this service.
  • No telemetry, console logging, or Sentry instrumentation here. Diagnostic surfacing is the caller’s responsibility.

Entry points

The exported surface:

  • assembleRunDef(params: AssembleParams): RunDefinition — single factory function.
  • AssembleParams interface:
    • shipId: string — hull class name (e.g. 'Bulwark', 'dart_common'); v4 bare-hull naming.
    • node?: Partial<typeof DEFAULT_RUN.node> — optional node overrides shallow-merged onto DEFAULT_RUN.node. If biome or levelConfig are present on the override, planet-derived values are skipped for that field.
    • unlockPools?: unknown — accepted but currently unused (typed unknown); kept in the API surface for forward-compat with progression-gated content pools.
    • planetIndex?: number — defaults to 0. Drives biome, enemyCountMult, level preset, and context.planetId.
    • isChallenge?: boolean — Challenge Mode toggle. Defaults false. When true: rewardMult × 2.0 and enemyCountMult / enemyHpMult / enemyDamageMult × 1.5. Plumbed to context.isChallenge and into worldKnobs.

RunDefinition shape returned (top-level):

  • version: 2 — schema version literal.
  • node — merged node config (id, seed, heat, missionType, missionId, biome, timerSeconds, objective, boss, vision, events, isRareSignal, levelConfig?, weaponBoxCount, artifactBoxCount, bossDefId?).
  • ship{ id, color, accent, combatStats, metaStats, weaponSlotCount, nonWeaponSlotCount, startingWeapons } derived from shipDef. startingWeapons is shallow-cloned.
  • context{ ...DEFAULT_RUN.context, weaponPool: null, upgradePool: null, levelupRarityCap: null, weaponCacheRarityCap: null, eventsUnlocked: true, starsUnlocked: true, eventTier: 3, facilities, planetId, isChallenge, supplyLevel: 0, worldKnobs, startingArtifactId: null }.
  • spawn, player, timing — shallow copies of the corresponding DEFAULT_RUN blocks.

Pattern notes

  • Mission tuning composition is multiplicative and layered. worldKnobs is computed as:
    • enemyCountMult = base × planet.enemyCountMult × rarityScale × challengeDiffMult
    • enemyHpMult = base × rarityScale × challengeDiffMult
    • enemyDamageMult = base × rarityScale × challengeDiffMult
    • rewardMult = base × challengeRewardMult (2.0 in Challenge, 1.0 otherwise)
    • rarityScale is also stored on worldKnobs so hardcoded melee/charger damage call sites can read it directly (they bypass enemy.damageMult).
  • RARITY_SCALE table: common: 0.5, uncommon: 0.6, rare: 0.7, epic: 0.8, legendary: 1.0. Legendary is the unmodified baseline; lower rarities scale enemy count, HP, and damage down. Applied at assembly so the entire run is scaled by ship rarity.
  • Override-vs-default precedence for node: node fields supplied in params.node win against DEFAULT_RUN.node. Planet-derived biome and levelConfig are only applied if the override did not specify them — this is checked against params.node?.biome / params.node?.levelConfig, not against the merged result, so an explicit undefined in the override still allows the planet fallback.
  • Defensive defaults are intentional only at the metagame boundary. Inside the engine the assembled RunDefinition is treated as authoritative — there is no re-defaulting downstream.
  • Star derivation: ship star comes from useInventoryStore.currentStar(shipId) with a || 1 fallback. A return of 0, undefined, or null collapses to star 1 before getShipDef is called.
  • Shallow copies, not deep clones. facilities, spawn, player, timing, startingWeapons, and context are all spread one level. Nested objects (e.g. inside node.objective, node.boss, worldKnobs source) share references with DEFAULT_RUN after assembly — engine code must treat the snapshot as read-only.
  • Tick 67 cleanup is reflected here. The starting-artifact picker was removed; startingArtifactId is hardcoded null and the hub Artifacts tab is queued for follow-up cleanup to become a read-only catalog.
  • No suspense, no async. Assembly is fully synchronous — store reads happen via getState() so this can be invoked from event handlers, route loaders, or dev shortcuts without await.