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
AssembleParamsinterface (caller-facing API). - The
RARITY_SCALEtable — 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
RunDefinitionshape.
READS FROM
useInventoryStore— callscurrentStar(shipId)to derive ship star from inventory XP (defaults to 1 if zero/undefined).getShipDef(shipId, star)fromdata/ships— looks up the hull definition. Throws ifshipIdis not in the roster.toShipCombatStats(shipDef)andtoShipMetaStats(shipDef)fromdata/ships— map ship direct stats onto engineShipCombatStats/ShipMetaStatsshapes.PLANETSfromdata/planet-config— looks up biome,enemyCountMult, andlevelPresetbyplanetIndex.LEVEL_PRESETSfromdata/level-presets— resolves the planet’s namedlevelPresetto a concreteLevelConfig.DEFAULT_RUNfromdata/run-config— base template providingnode,spawn,player,timing, andcontextdefaults includingworldKnobs,facilities, andbonuses.ShipRaritytypedef fromdata/ships— keys theRARITY_SCALEmap.
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
useInventoryStoreonce viagetState(); no subscribe, no write. - Does not consult
useArtifactUnlocksStorefor run assembly. The starting-artifact feature was removed (Tick 67);context.startingArtifactIdis alwaysnull. 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
shipDefthroughtoShipCombatStats. - Does not apply chapter-based content gating.
weaponPool,upgradePool,levelupRarityCap, andweaponCacheRarityCapare all set tonull(engine uses full pools);eventsUnlockedandstarsUnlockedare hard-codedtrue;eventTieris hard-coded3. - Does not apply facility bonuses. Buildings are disabled —
facilitiesis a shallow copy ofDEFAULT_RUN.context.facilities(default zeros) andsupplyLevelis0. - Does not validate
planetIndexbeyond?? 0. If the index falls outsidePLANETS, biome and level-preset wiring are skipped silently andnode.biomefalls back to whateverDEFAULT_RUN.nodeprovides; the multipliers default to1.0. - Does not seed RNG, generate IDs, or assign mission IDs — those come in via
params.nodeor stay atDEFAULT_RUN.nodevalues.
Signals
- Throws (propagated from
getShipDef) whenparams.shipIdis 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.AssembleParamsinterface: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 ontoDEFAULT_RUN.node. IfbiomeorlevelConfigare present on the override, planet-derived values are skipped for that field.unlockPools?: unknown— accepted but currently unused (typedunknown); kept in the API surface for forward-compat with progression-gated content pools.planetIndex?: number— defaults to0. Drives biome,enemyCountMult, level preset, andcontext.planetId.isChallenge?: boolean— Challenge Mode toggle. Defaultsfalse. Whentrue:rewardMult× 2.0 andenemyCountMult/enemyHpMult/enemyDamageMult× 1.5. Plumbed tocontext.isChallengeand intoworldKnobs.
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 fromshipDef.startingWeaponsis 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 correspondingDEFAULT_RUNblocks.
Pattern notes
- Mission tuning composition is multiplicative and layered.
worldKnobsis computed as:enemyCountMult = base × planet.enemyCountMult × rarityScale × challengeDiffMultenemyHpMult = base × rarityScale × challengeDiffMultenemyDamageMult = base × rarityScale × challengeDiffMultrewardMult = base × challengeRewardMult(2.0 in Challenge, 1.0 otherwise)rarityScaleis also stored onworldKnobsso hardcoded melee/charger damage call sites can read it directly (they bypassenemy.damageMult).
RARITY_SCALEtable: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.nodewin againstDEFAULT_RUN.node. Planet-derivedbiomeandlevelConfigare only applied if the override did not specify them — this is checked againstparams.node?.biome/params.node?.levelConfig, not against the merged result, so an explicitundefinedin the override still allows the planet fallback. - Defensive defaults are intentional only at the metagame boundary. Inside the engine the assembled
RunDefinitionis treated as authoritative — there is no re-defaulting downstream. - Star derivation: ship star comes from
useInventoryStore.currentStar(shipId)with a|| 1fallback. A return of0,undefined, ornullcollapses to star 1 beforegetShipDefis called. - Shallow copies, not deep clones.
facilities,spawn,player,timing,startingWeapons, andcontextare all spread one level. Nested objects (e.g. insidenode.objective,node.boss,worldKnobssource) share references withDEFAULT_RUNafter assembly — engine code must treat the snapshot as read-only. - Tick 67 cleanup is reflected here. The starting-artifact picker was removed;
startingArtifactIdis hardcodednulland 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.