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 four positively-framed types (Explore / Find / Protect / Battle). Each maps through MISSION_OBJECTIVE_TUNING to distinct engine knobs:
    • Explore — baseline. enemyCountMult = 1.0, weaponBoxCount = 0.
    • Find — calmer, more loot. 0.85 enemies, 2 weapon boxes.
    • Protect — mild pressure plus a defensive weapon. 1.15, 1 box.
    • Battle — pure pressure, no extra loot. 1.4, 0 boxes.
  • 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 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 AssembleParamsassembleRunService.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