diff --git a/src/components/frontend/opord/OpordGame.tsx b/src/components/frontend/opord/OpordGame.tsx index 746c941..73c27d6 100644 --- a/src/components/frontend/opord/OpordGame.tsx +++ b/src/components/frontend/opord/OpordGame.tsx @@ -135,7 +135,7 @@ function buildBriefingRows( "text", `Movement from the line of departure to the objective takes ${scenario.enRouteMin} minutes.`, ); - push(rows, "text", `On abort, return to ${scenario.rallyPoint} and await further orders.`); + push(rows, "text", `On abort, ${scenario.abortAction}.`); push(rows, "text", `Resupply is scheduled at H+${scenario.resupplyOffsetMin} minutes.`); push(rows, "text", `Exfiltration no later than H+${scenario.exfilOffsetMin} minutes.`); push( diff --git a/src/lib/opord-recall.ts b/src/lib/opord-recall.ts index 3f3cb40..9aa95c6 100644 --- a/src/lib/opord-recall.ts +++ b/src/lib/opord-recall.ts @@ -231,6 +231,8 @@ export interface OpordScenario { readonly exfilOffsetMin: number; /** Egress azimuth in degrees (1-359); never equals the main axis azimuth. */ readonly withdrawalAzimuth: number; + /** Element action on abort; one of the abortActionBank templates. */ + readonly abortAction: string; } /** Formats a 6-digit grid Arma map style: three digits, hyphen, three digits, e.g. "034-672". */ @@ -243,6 +245,25 @@ export function cardinalFromAzimuth(azimuth: number): string { return CARDINAL_DIRECTIONS[Math.round(azimuth / 45) % 8]; } +// Shared by scenario generation (stores one entry as abortAction) and question +// building (offers the rest as distractors); every template reads only fields +// the elite briefing renders. +function abortActionBank( + scenario: Pick< + OpordScenario, + "rallyPoint" | "ldPhaseLine" | "otherPhaseLines" | "withdrawalAzimuth" | "casevacLz" | "friendlyUnit" + >, +): readonly string[] { + return [ + `Return to ${scenario.rallyPoint} and await further orders`, + `Retire to phase line ${scenario.otherPhaseLines[1]} and hold`, + `Withdraw on azimuth ${pad3(scenario.withdrawalAzimuth)} to ${scenario.rallyPoint}`, + `Regroup at ${scenario.casevacLz} and await transport`, + `Fall back to ${scenario.friendlyUnit} and consolidate`, + `Hold short of phase line ${scenario.ldPhaseLine} and await orders`, + ]; +} + export function generateOpordScenario(seed: number): OpordScenario { const rng = mulberry32(seed); const operationName = `${pick(rng, OPERATION_ADJECTIVES)} ${pick(rng, OPERATION_NOUNS)}`; @@ -264,6 +285,23 @@ export function generateOpordScenario(seed: number): OpordScenario { // azimuth and its cardinal direction, so the two questions never collide. const wrappedWithdrawal = (azimuth + pick(rng, [120, 150, 180, 210])) % 360; const withdrawalAzimuth = wrappedWithdrawal === 0 ? 359 : wrappedWithdrawal; + const enemyVehicles = pickInt(rng, 1, 4); + const enemySquads = pickInt(rng, 1, 3); + const enemyMortars = pickInt(rng, 1, 3); + const friendlyUnit = pick(rng, FRIENDLY_UNITS); + const transport = pick(rng, TRANSPORT_OPTIONS); + const rallyPoint = `RALLY POINT ${pick(rng, RALLY_POINT_NAMES)}`; + const aircraftCallsign = `${pick(rng, AIRCRAFT_CALLSIGNS)} ${pick(rng, [1, 2, 3, 4])}`; + const airSupportOffsetMin = pick(rng, [15, 20, 30, 45]); + const enRouteMin = pickInt(rng, 18, 42); + const checkInMinutes = pick(rng, [10, 15, 20, 30]); + const casevacLz = `LZ ${pick(rng, LZ_NAMES)}`; + const ldPhaseLine = phaseLines[0]; + const otherPhaseLines = phaseLines.slice(1, 3); + const abortAction = pick( + rng, + abortActionBank({ rallyPoint, ldPhaseLine, otherPhaseLines, withdrawalAzimuth, casevacLz, friendlyUnit }), + ); return { seed, @@ -273,26 +311,27 @@ export function generateOpordScenario(seed: number): OpordScenario { azimuth, distanceKm, hHour, - ldPhaseLine: phaseLines[0], - otherPhaseLines: phaseLines.slice(1, 3), + ldPhaseLine, + otherPhaseLines, codeword, abortCodeword, callsign, frequency, - enemyVehicles: pickInt(rng, 1, 4), - enemySquads: pickInt(rng, 1, 3), - enemyMortars: pickInt(rng, 1, 3), - friendlyUnit: pick(rng, FRIENDLY_UNITS), - transport: pick(rng, TRANSPORT_OPTIONS), - rallyPoint: `RALLY POINT ${pick(rng, RALLY_POINT_NAMES)}`, - aircraftCallsign: `${pick(rng, AIRCRAFT_CALLSIGNS)} ${pick(rng, [1, 2, 3, 4])}`, - airSupportOffsetMin: pick(rng, [15, 20, 30, 45]), - enRouteMin: pickInt(rng, 18, 42), - checkInMinutes: pick(rng, [10, 15, 20, 30]), - casevacLz: `LZ ${pick(rng, LZ_NAMES)}`, + enemyVehicles, + enemySquads, + enemyMortars, + friendlyUnit, + transport, + rallyPoint, + aircraftCallsign, + airSupportOffsetMin, + enRouteMin, + checkInMinutes, + casevacLz, resupplyOffsetMin, exfilOffsetMin, withdrawalAzimuth, + abortAction, }; } @@ -569,12 +608,8 @@ export function buildOpordQuestions( choiceQuestion( "abort-rally", `If the abort codeword "${scenario.abortCodeword}" is broadcast, what does your element do?`, - `Return to ${scenario.rallyPoint} and await further orders`, - [ - "Continue the mission as planned", - "Mark the objective with smoke and hold in place", - `Request close air support from ${scenario.aircraftCallsign}`, - ], + scenario.abortAction, + abortActionBank(scenario).filter((action) => action !== scenario.abortAction), rng, ), "callsign-net": () =>