Mission Deployment Flow
How a player goes from the hub to an active in-engine run. Three stops: hub planet selector → mission board → play screen. Each stop owns a single piece of state and hands the result to the next via useSessionStore.
End-to-end path
- Hub (
src/metagame/screens/HubScreen.tsx). Player swipes the planet ring to pickPLANET_ORDER[planetIndex]and chooses a ship viaselectedShipId. The LAUNCH CTA today callsassembleRunDefdirectly and routes to/games/starship-survivors/play— the mission board sits on/games/starship-survivors/boardas a parallel entry point reachable via direct link / dev navigation. The board path is fully wired even though the hub button skips it. - Mission Board (
src/metagame/screens/MissionBoardScreen.tsx). Renders two postings side-by-side using<MedPanel variant="card">. Each card shows faction, difficulty pill, location (planet name fromPLANETS), positive objective headline, flavor blurb, payout line, posted-by tag, and an ACCEPT button. - Play (
src/starship-survivors/screens/GameScreen.tsx). ReadsrunDeffromuseSessionStore. IfrunDefisnullon mount (refresh wiped session state), redirects to/. The only valid way to arrive here is with a freshly-assembledrunDef.
Posting selection — 2-of-N, deterministic per seed
pickTwoPostings(seed = Date.now()) in src/starship-survivors/data/mission-postings.ts returns two distinct MissionPosting records from the static MISSION_POSTINGS table (one entry per planet in PLANET_ORDER, 11 total). The selection is deterministic for any given seed — same seed always yields the same pair. The board uses useState(() => pickTwoPostings()) so the pair is locked on mount and doesn’t reroll on re-render. The hash mixes the seed bits (((seed ^ (seed >>> 16)) * 0x45d9f3b) ^ ...) to avoid clustering at small seed values, then derives indexB from (count - 1) space and shifts it past indexA to guarantee distinctness.
A second helper, pickTwoPostingsForPlanet(planetId, seed), biases one of the two cards to a specific planet for when the hub routes through the board for a known planet selection. Card A comes from the planet-matching subset; card B comes from postings on other planets. Both picks remain deterministic per seed so refresh/back-button doesn’t reroll.
What the postings differ on
Each MissionPosting carries five gameplay-relevant fields plus pure flavor:
faction— flavor only. One of ten in-universe authors (Cygnus Freight Co., Outer Rim Salvage, Sol Defense Authority, etc.).difficulty— one of five tiers (Routine/Standard/Hazardous/Critical/Black Flag). Drives the difficulty pill color and the extraction timer viaEXTRACTION_TIMER_BY_DIFFICULTY.objectiveLabel— one of four positively-framed types (Explore/Find/Protect/Battle). Each maps throughMISSION_OBJECTIVE_TUNINGto distinct engine knobs:Explore— baseline.enemyCountMult = 1.0,weaponBoxCount = 0.Find— calmer, more loot.0.85enemies,2weapon boxes.Protect— mild pressure plus a defensive weapon.1.15,1box.Battle— pure pressure, no extra loot.1.4,0boxes.
extractionTimerSeconds— derived from difficulty (3 min Routine → 7 min Black Flag, +60s per tier). Surfaced byengine/rendering/mission-timer-hud.tsas theSURVIVE M:SS until extractionbanner.payoutLine/objectiveBlurb/postedBy— flavor strings rendered on the card; not consumed by the engine.
Accept handler — assembleRunDef and the bridge
handleAccept(posting) in MissionBoardScreen does three things:
- Calls
assembleRunDef({ shipId: selectedShipId, planetIndex: posting.planetId })fromsrc/starship-survivors/services/assembleRunService.ts. - Stores the result via
setRunDef(runDef)onuseSessionStore. - Navigates to
/games/starship-survivors/play.
assembleRunDef merges player selections into a complete RunDefinition:
- Looks up
selectedShipIdagainstuseInventoryStore.getState().currentStar(shipId)to determine star tier, thengetShipDef(shipId, star)for hull stats. - Maps ship stats to engine combat stats via
toShipCombatStats(shipDef)and meta stats viatoShipMetaStats(shipDef). - Pulls the planet at
params.planetIndexfromPLANETS, setsnode.biomefromplanet.biome, and setsnode.levelConfigfromLEVEL_PRESETS[planet.levelPreset]unless explicitly overridden. - Composes
worldKnobsby multiplyingbaseKnobs.enemyCountMult × planet.enemyCountMult × RARITY_SCALE[shipDef.rarity] × challengeDiffMult(Challenge Mode is1.5×across HP, damage, and count; plus2×reward mult since the run is twice as long). Common ships scale to0.5×, legendary to1.0×— the hidden rarity scale stays the legendary baseline at the top. - Wires
weaponPool = null(no chapter gating today) and zeroes facilities/supplyLevel(buildings disabled). - Returns a
RunDefinitionv2 withnode,ship,context,spawn,player,timingblocks.
The mission board’s accept handler today does not plumb missionObjective or extractionTimerSeconds into the AssembleParams — assembleRunService.ts is wired to read missionObjective and apply MISSION_OBJECTIVE_TUNING when the caller passes it, but MissionBoardScreen.handleAccept only passes { shipId, planetIndex }. Per-posting objective tuning is data-ready but the launch path doesn’t connect it yet.
Engine handoff
GameScreen reads runDef from the session store and passes it to the engine bridge (src/starship-survivors/engine/bridge.ts). The bridge writes runDef.node.timerSeconds into game.missionTimer, which the mission-timer HUD surfaces as the survive-until-extraction banner. The spawn loop reads runDef.node.weaponBoxCount (defaulted from MISSION_OBJECTIVE_TUNING when objective is plumbed through; otherwise from the caller-supplied node override). Run-end transitions to the matrix dissolve (end-level-dissolve.ts, 2.0s, fade-to-black in the final 0.5s) and then to the post-run reward/artifact pick overlay.
See also
- Assemble Run — full
RunDefinitionassembly contract. - Mission Objectives — per-objective engine tuning detail.
- Challenge Mode — the 2× length, 1.5× difficulty variant gated per planet.