mission-postings.ts
Static job-posting data for the in-universe mission board. One posting per planet in PLANET_ORDER, plus the deterministic 2-card picker used by MissionBoardScreen. UI-free: planet names are resolved at render time through the PLANETS map.
Role in the system
Stage 1 of the mission-board UX overhaul (audit issue #5). Postings are positively framed — they describe what the operative will do, never a loss condition. The card surface shows planet name, hiring faction, difficulty pill, positive objective label; the in-run HUD surfaces “SURVIVE M:SS until extraction” using the timer derived here.
Downstream consumers:
MissionBoardScreen— renders the two-card pair.assembleRunService.assembleRunDef— readsmissionObjectiveto applyMISSION_OBJECTIVE_TUNING.engine/bridge.ts— consumesrunDef.node.weaponBoxCount(spawn loop) andrunDef.node.timerSeconds(mission timer).engine/rendering/mission-timer-hud.ts— renders the survive banner fromgame.missionTimer.
Union types
| Type | Members | Notes |
|---|---|---|
MissionFaction | 10 string literals | Hiring authorities (e.g. Cygnus Freight Co., Sol Defense Authority, Voidstar Research Div.). Used as flavor copy and postedBy context. |
MissionObjectiveLabel | Explore | Find | Protect | Battle | Stage 7 collapsed 7 cosmetic verbs to 4 vibes with engine tuning. |
MissionDifficulty | Routine | Standard | Hazardous | Critical | Black Flag | Drives extraction timer and ordering. |
Objective label collapse mapping (historical, baked into the postings):
- Survey, Investigate →
Explore - Recover →
Find - Patrol →
Battle - Escort →
Protect
Objective tuning contract
MissionObjectiveTuning exposes two real engine levers per label:
enemyCountMult— multiplier layered on top of planet × rarity × challenge mults (multiplicative composition).weaponBoxCount— number of weapon boxes placed at run start; caller-suppliedparams.node.weaponBoxCountwins over this default inassembleRunService.
The MISSION_OBJECTIVE_TUNING: Record<MissionObjectiveLabel, MissionObjectiveTuning> map is the canonical source. Values are conservative (1.0×–1.4× enemyCountMult, 0–2 boxes) so objective tuning never dominates the existing difficulty stack — it produces vibe, not difficulty.
Per-label intent:
Explore— baseline, nothing extra.Find— calmer + more loot.Protect— mild pressure + a weapon to defend with.Battle— pure pressure, no extra loot.
Extraction-timer contract
EXTRACTION_TIMER_BY_DIFFICULTY: Record<MissionDifficulty, number> maps each difficulty to a survive-time-in-seconds. The map is declared exhaustive over MissionDifficulty — adding a new difficulty without adding an entry fails the TypeScript build.
Curve shape: monotonic with difficulty, fanning out by ±60s per step around the 4-minute standard. The chosen timer is copied into MissionPosting.extractionTimerSeconds, which engine/bridge.ts writes into game.missionTimer at run start.
MissionPosting interface
| Field | Type | Notes |
|---|---|---|
id | string | Stable unique identifier (posting-N). |
planetId | PlanetId | Key into the PLANETS map; resolved to a display name at render time. |
faction | MissionFaction | Hiring authority. |
objectiveLabel | MissionObjectiveLabel | Drives tuning via MISSION_OBJECTIVE_TUNING. |
objectiveBlurb | string | One-line in-universe flavor, positively framed. |
difficulty | MissionDifficulty | Drives extraction-timer lookup. |
payoutLine | string | Prefixed with PAYOUT:. Pure UI copy, no engine effect. |
postedBy | string | In-universe author tag at card bottom. |
extractionTimerSeconds | number | Mirror of EXTRACTION_TIMER_BY_DIFFICULTY[difficulty]. |
Authoring rule: keep loss conditions out of objectiveBlurb. The card surface stays positive; failure is only ever expressed at run-time, not at offer-time.
MISSION_POSTINGS table
A MissionPosting[] with one entry per PLANET_ORDER index. Difficulty loosely tracks planet progression (Routine in early indices, Black Flag at the late end). Each posting references PLANET_ORDER[N] directly, so adding/removing planets requires re-authoring or pruning entries here.
Selection helpers
Both helpers use a stable bit-mixing hash on the input seed (Math.abs(((seed ^ (seed >>> 16)) * 0x45d9f3b) ^ ((seed ^ (seed >>> 16)) >>> 16))) so the same seed always produces the same pair. Both default seed = Date.now() so the board varies per page visit when callers omit the seed.
pickTwoPostings(seed?)
Returns [MissionPosting, MissionPosting] of two distinct entries from MISSION_POSTINGS. Index B is derived by a second hash mod (count - 1) and shifted past index A to guarantee distinctness — no rejection loop.
pickTwoPostingsForPlanet(planetId, seed?)
Biased variant used when the player taps LAUNCH on a specific planet in the hub. Contract:
- At least one of the two cards has
posting.planetId === planetId. - The other card is drawn from postings whose
iddiffers from card A — does not constrain its planet. - Edge case — zero matching postings: defensive fallback to
pickTwoPostings(seed). Should not occur because the authored table covers everyPLANET_ORDERentry. - Edge case — only one posting in the entire table: returns
[cardA, cardA](pathological; defensive only).
Selection is deterministic for (planetId, seed) so refresh/back-button on the board route does not reroll the offers.
Authoring a new posting
- Add a
PlanetIdtoPLANET_ORDER(inplanet-config.ts) — postings index against this array. - Append a
MissionPostingliteral at the matchingMISSION_POSTINGSindex. UsePLANET_ORDER[N]forplanetId. - Choose
objectiveLabelfrom the four vibe types; the engine tuning attaches automatically throughMISSION_OBJECTIVE_TUNING. - Choose
difficultyand referenceEXTRACTION_TIMER_BY_DIFFICULTY[difficulty]forextractionTimerSeconds— never hard-code a number. - Keep
objectiveBlurbpositively framed (what the operative does, not what they avoid). payoutLineuses thePAYOUT: …prefix convention.
If a new MissionDifficulty is needed, also extend EXTRACTION_TIMER_BY_DIFFICULTY — the Record<MissionDifficulty, number> typing will otherwise fail the build.
Related
code/data/planet-config— source ofPLANET_ORDERand thePLANETSmap.code/services/assembleRunService— consumesmissionObjectiveand the tuning record.code/engine/bridge— consumesrunDef.node.timerSecondsandrunDef.node.weaponBoxCount.code/engine/rendering/mission-timer-hud— surfaces the extraction-timer banner.code/screens/MissionBoardScreen— renders the 2-card output of the pickers.
EXTRACT-CANDIDATE
- The objective→tuning mapping (
MISSION_OBJECTIVE_TUNING) is currently colocated with posting data but is a pure engine-tuning contract. If a second consumer needs the tuning without the posting data, extract intodata/mission-objective-tuning.tssoassembleRunServicecan depend on the smaller module. - The bit-mixing hash (
((x ^ (x >>> 16)) * k) ^ ((x ^ (x >>> 16)) >>> 16)) is duplicated across both pickers with different constants. If a third deterministic picker is added (e.g. weekly featured posting), extract a sharedmixSeed(seed, salt)helper. EXTRACTION_TIMER_BY_DIFFICULTYcould move to a shareddifficulty-config.tsif other systems (ship rewards, telemetry buckets) start keying off the same difficulty enum.