Mission Objectives
Every mission posting on the board carries an objective label — a one-word, positively framed verb that tells the player what the contract is about. The three labels collapse what used to be seven cosmetic synonyms (Survey/Investigate → Explore, Recover → Find, Escort → Protect) into a small set. Per-objective engine tuning was removed in Phase 0; the labels are currently flavor-only.
The extraction timer is an orthogonal axis driven by the posting’s difficulty, not its objective. Difficulty owns “how long do I survive”; objective owns “what does the field feel like”.
Naming clash, read carefully. “Mission objective” means two different things in code. The player-facing label (
MissionObjectiveLabelindata/mission-postings.ts) is one ofExplore | Find | Protect. The engine-sideMissionObjectiveinterface (data/run-config.ts) is a{ type, count }pair wheretypeis a lowercase string like'survive_timer'or'explore', andcountis a beacon/kill quota. The two taxonomies live side by side; the lowercase one is the active win-condition the engine evaluates, the title-case one is the board-card flavor.
The three labels
Source of truth: src/starship-survivors/data/mission-postings.ts.
| Label | Feel |
|---|---|
| Explore | Baseline. Scout / observe. |
| Find | Locate-and-retrieve. |
| Protect | Hold or escort an asset. |
Extraction timer per difficulty
Source of truth: EXTRACTION_TIMER_BY_DIFFICULTY in data/mission-postings.ts (lines 143–149). The 4-minute Standard tier is the spec default; surrounding tiers fan out by ±60 seconds per step so the curve is monotonic with difficulty.
| Difficulty | Timer (seconds) | Timer (minutes) |
|---|---|---|
| Routine | 180 | 3:00 |
| Standard | 240 | 4:00 |
| Hazardous | 300 | 5:00 |
| Critical | 360 | 6:00 |
| Black Flag | 420 | 7:00 |
This value is written onto each posting at module load (extractionTimerSeconds: EXTRACTION_TIMER_BY_DIFFICULTY[difficulty]) so postings stay in sync with the table.
The TypeScript record is exhaustive over MissionDifficulty — adding a new difficulty tier without a matching entry fails the build, which is how we keep the curve honest.
Posting → run assembly
The translation from “card the player tapped” to “engine config the run loop reads” runs through a couple of files:
- Mission board pick.
pickTwoPostings(seed)/pickTwoPostingsForPlanet(planetId, seed)indata/mission-postings.tsreturn two distinctMissionPostingrecords to theMissionBoardScreen. - Player accepts.
MissionBoardScreenresolves the planet name viaPLANETS[posting.planetId].nameand hands the chosen posting to the run-start handler. - Engine config built.
services/assembleRunService.tsassembleRunDef({ shipId, node, planetIndex, isChallenge })produces aRunDefinition. Thenodeoverrides are merged ontoDEFAULT_RUN.node(line 56–59), so the posting’sextractionTimerSecondslands onrunDef.node.timerSeconds, and the per-objectiveweaponBoxCountlands onrunDef.node.weaponBoxCount. - Engine boots.
engine/bridge.tsreadsrunDef.node.timerSecondsand writes it intogame.missionTimerat run start;engine/rendering/mission-timer-hud.tssurfaces it as the “SURVIVE M:SS until extraction” banner. The same bridge readsrunDef.node.weaponBoxCountand places that many crates in the world. Enemy count flows throughrunDef.context.worldKnobs.enemyCountMult, which the spawn director consumes wave by wave.
The posting’s objectiveLabel itself is not stored on RunDefinition — the engine doesn’t care what the contract said, it cares about the levers the contract set.
Engine-side MissionObjective (separate concept)
data/run-config.ts defines a different MissionObjective interface:
export interface MissionObjective {
/** Objective type: 'explore' | 'survive_timer' | 'kill_target' | 'collect' | 'escort' | 'defend' */
type: string;
/** Number of beacons/items/kills required (0 = N/A for survive_timer) */
count: number;
}This lives on RunDefinition.node.objective and is the win condition the engine evaluates each frame. The default run uses { type: 'survive_timer', count: 0 } — survive until the extraction timer hits zero. Postings don’t currently override this; every accepted contract runs on the survive-timer win condition regardless of its title-case objectiveLabel. The lowercase type taxonomy is older and broader than the four-label set, kept around for future objective variants (kill-target, escort, defend) that would change the win check.
Related
- mission-board — UX layer that picks pairs and renders the card
- mission-postings — full catalogue of the 11 postings
- planets — supplies the per-planet
enemyCountMultthat stacks with objective tuning - run-assembly — full pipeline from metagame selection to
RunDefinition