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

  1. Hub (src/metagame/screens/HubScreen.tsx). Player swipes the planet ring to pick PLANET_ORDER[planetIndex] and chooses a ship via selectedShipId. The LAUNCH CTA today calls assembleRunDef directly and routes to /games/starship-survivors/play — the mission board sits on /games/starship-survivors/board as a parallel entry point reachable via direct link / dev navigation. The board path is fully wired even though the hub button skips it.
  2. 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 from PLANETS), positive objective headline, flavor blurb, payout line, posted-by tag, and an ACCEPT button.
  3. Play (src/starship-survivors/screens/GameScreen.tsx). Reads runDef from useSessionStore. If runDef is null on mount (refresh wiped session state), redirects to /. The only valid way to arrive here is with a freshly-assembled runDef.

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 via EXTRACTION_TIMER_BY_DIFFICULTY.
  • objectiveLabel — one of three positively-framed types (Explore / Find / Protect). Currently flavor-only; the per-objective engine tuning was removed in Phase 0.
  • extractionTimerSeconds — derived from difficulty (3 min Routine → 7 min Black Flag, +60s per tier). Surfaced by engine/rendering/mission-timer-hud.ts as the SURVIVE M:SS until extraction banner.
  • 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:

  1. Calls assembleRunDef({ shipId: selectedShipId, planetIndex: posting.planetId }) from src/starship-survivors/services/assembleRunService.ts.
  2. Stores the result via setRunDef(runDef) on useSessionStore.
  3. Navigates to /games/starship-survivors/play.

assembleRunDef merges player selections into a complete RunDefinition:

  • Looks up selectedShipId against useInventoryStore.getState().currentStar(shipId) to determine star tier, then getShipDef(shipId, star) for hull stats.
  • Maps ship stats to engine combat stats via toShipCombatStats(shipDef) and meta stats via toShipMetaStats(shipDef).
  • Pulls the planet at params.planetIndex from PLANETS, sets node.biome from planet.biome, and sets node.levelConfig from LEVEL_PRESETS[planet.levelPreset] unless explicitly overridden.
  • Composes worldKnobs by multiplying baseKnobs.enemyCountMult × planet.enemyCountMult × RARITY_SCALE[shipDef.rarity] × challengeDiffMult (Challenge Mode is 1.5× across HP, damage, and count; plus 2× reward mult since the run is twice as long). Common ships scale to 0.5×, legendary to 1.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 RunDefinition v2 with node, ship, context, spawn, player, timing blocks.

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