Phase 5: four real jobs, radio, and work found in the field

Mission types. Four of them, differing in pacing and risk rather than in the
noun on the board: a quick supply drop, a retrieval whose danger is all on the
return leg, a long stationary intel transmission in the open, and a cheap survey
whose payoff is the map. Retrieval carries cargo that impacts damage, and pays
for what arrives rather than what you set off with, which makes the drive home a
different problem from the drive out.

Radio. Chatter framing the player as an undercover driver being talked at by
people busy elsewhere. Every line is triggered by something actually happening
in the sim — crossing a border, a road you personally made notorious, a car past
saving, or the front genuinely drifting. The enemy-has-other-priorities line
fires off measured drift, so the brief's ambient-drift-is-the-mechanism idea is
said out loud without ever showing a number. Topics carry cooldowns and never
repeat a sentence back to back; a test caught that they could.

Field work. Past the line, with nothing in hand, a wounded stranger or a contact
or a strippable wreck occasionally turns up. Deliberately not on a board: no
route preview, no comparison, no shopping between them. Take the detour in front
of you or drive on. They never appear behind your own lines, where a safe errand
would defeat the point.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
dejvino
2026-08-07 13:35:37 +02:00
co-authored by Claude Opus 5
parent 90dc18fd17
commit 06c7555188
11 changed files with 890 additions and 50 deletions
+14 -2
View File
@@ -1,6 +1,17 @@
import type { Base } from '../sim/bases';
import type { HeatLevel } from '../sim/heat';
import type { Offer } from '../sim/quests';
import { MISSION_SHAPE, type MissionType, type Offer } from '../sim/quests';
/**
* What each job actually asks of you, in the words the people asking would use.
* The point is that the four read as different *risks*, not different nouns.
*/
const BRIEFING: Record<MissionType, string> = {
supply: 'drop the load and come back — quick work at the far end',
retrieval: 'collect and carry it home; it pays for what arrives intact',
intel: 'sit still and transmit, a long minute in the open',
recon: 'eyes on the area — you will come back knowing the roads',
};
/**
* The quest board. Deliberately words, not numbers: the player is reading a
@@ -67,10 +78,11 @@ export function createBoard() {
(offer, i) => `
<li>
<span class="key">[${i + 1}]</span>
${offer.type === 'recon' ? 'Survey' : 'Supply run'} —
${MISSION_SHAPE[offer.type].label} —
${offer.novel ? '<span class="novel">new target</span>' : 'known target'}
· ${(offer.route.length / 1000).toFixed(1)} km
· ${offer.reward.toFixed(2)} parts
<div class="meta">${BRIEFING[offer.type]}</div>
<div class="meta">${describe(offer)}</div>
</li>`,
)
+9 -5
View File
@@ -1,8 +1,8 @@
import { SUBSYSTEMS, type CarCondition } from '../sim/car';
import type { HeatLevel } from '../sim/heat';
import type { MissionType } from '../sim/quests';
import { MISSION_SHAPE, type MissionType } from '../sim/quests';
import type { Control } from '../sim/regions';
import { WORK_SECONDS } from '../sim/quests';
const BAR_WIDTH = 12;
@@ -31,6 +31,7 @@ export interface HudModel {
stage: 'travel' | 'working' | 'return';
novel: boolean;
worked: number;
cargo: number;
distance: number;
/** Radians from the car's nose to the target, positive to the left. */
bearing: number;
@@ -57,13 +58,16 @@ const CONTROL_WORDS: Record<Control, string> = {
};
function questLines(quest: NonNullable<HudModel['quest']>): string[] {
const label = quest.type === 'recon' ? 'Survey' : 'Supply run';
const shape = MISSION_SHAPE[quest.type];
const label = shape.label;
const kind = quest.novel ? 'new target' : 'known target';
if (quest.stage === 'return') {
return [`${label} done — return to any base`];
// Only retrieval has anything aboard that can be spoiled on the way home.
const cargo = shape.cargoHome ? ` · cargo ${(quest.cargo * 100).toFixed(0)}%` : '';
return [`${label} done — return to any base${cargo}`];
}
if (quest.stage === 'working') {
return [`${label} — working ${quest.worked.toFixed(1)}/${WORK_SECONDS}s (hold still)`];
return [`${label} — ${shape.working} ${quest.worked.toFixed(1)}/${shape.work}s (hold still)`];
}
return [
`${label} (${kind})`,
+10
View File
@@ -45,6 +45,8 @@ export interface MinimapView {
/** Car heading in radians, matching the world's yaw convention. */
heading: number;
objective: { x: number; z: number } | null;
/** A field contact waiting somewhere, if one has turned up. */
opportunity: { x: number; z: number } | null;
/** Elapsed time, for ageing observations. */
now: number;
}
@@ -133,6 +135,14 @@ export function createMinimap(roads: RoadNetwork, bases: Base[], worldExtent: nu
ctx.stroke();
}
// --- A field contact: marked, but not the same as an assigned target ---
if (view.opportunity) {
ctx.beginPath();
ctx.arc(px(view.opportunity.x), px(view.opportunity.z), 3.5, 0, Math.PI * 2);
ctx.fillStyle = '#cf6bd6';
ctx.fill();
}
// --- The car, as an arrow, so heading is readable at a glance ---
const cx = px(view.x);
const cz = px(view.z);