feat(opord): dynamic abort contingency with six options
This commit is contained in:
parent
53dec0b127
commit
d8ee14ee8d
2 changed files with 55 additions and 20 deletions
|
|
@ -135,7 +135,7 @@ function buildBriefingRows(
|
||||||
"text",
|
"text",
|
||||||
`Movement from the line of departure to the objective takes ${scenario.enRouteMin} minutes.`,
|
`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", `Resupply is scheduled at H+${scenario.resupplyOffsetMin} minutes.`);
|
||||||
push(rows, "text", `Exfiltration no later than H+${scenario.exfilOffsetMin} minutes.`);
|
push(rows, "text", `Exfiltration no later than H+${scenario.exfilOffsetMin} minutes.`);
|
||||||
push(
|
push(
|
||||||
|
|
|
||||||
|
|
@ -231,6 +231,8 @@ export interface OpordScenario {
|
||||||
readonly exfilOffsetMin: number;
|
readonly exfilOffsetMin: number;
|
||||||
/** Egress azimuth in degrees (1-359); never equals the main axis azimuth. */
|
/** Egress azimuth in degrees (1-359); never equals the main axis azimuth. */
|
||||||
readonly withdrawalAzimuth: number;
|
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". */
|
/** 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];
|
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 {
|
export function generateOpordScenario(seed: number): OpordScenario {
|
||||||
const rng = mulberry32(seed);
|
const rng = mulberry32(seed);
|
||||||
const operationName = `${pick(rng, OPERATION_ADJECTIVES)} ${pick(rng, OPERATION_NOUNS)}`;
|
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.
|
// azimuth and its cardinal direction, so the two questions never collide.
|
||||||
const wrappedWithdrawal = (azimuth + pick(rng, [120, 150, 180, 210])) % 360;
|
const wrappedWithdrawal = (azimuth + pick(rng, [120, 150, 180, 210])) % 360;
|
||||||
const withdrawalAzimuth = wrappedWithdrawal === 0 ? 359 : wrappedWithdrawal;
|
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 {
|
return {
|
||||||
seed,
|
seed,
|
||||||
|
|
@ -273,26 +311,27 @@ export function generateOpordScenario(seed: number): OpordScenario {
|
||||||
azimuth,
|
azimuth,
|
||||||
distanceKm,
|
distanceKm,
|
||||||
hHour,
|
hHour,
|
||||||
ldPhaseLine: phaseLines[0],
|
ldPhaseLine,
|
||||||
otherPhaseLines: phaseLines.slice(1, 3),
|
otherPhaseLines,
|
||||||
codeword,
|
codeword,
|
||||||
abortCodeword,
|
abortCodeword,
|
||||||
callsign,
|
callsign,
|
||||||
frequency,
|
frequency,
|
||||||
enemyVehicles: pickInt(rng, 1, 4),
|
enemyVehicles,
|
||||||
enemySquads: pickInt(rng, 1, 3),
|
enemySquads,
|
||||||
enemyMortars: pickInt(rng, 1, 3),
|
enemyMortars,
|
||||||
friendlyUnit: pick(rng, FRIENDLY_UNITS),
|
friendlyUnit,
|
||||||
transport: pick(rng, TRANSPORT_OPTIONS),
|
transport,
|
||||||
rallyPoint: `RALLY POINT ${pick(rng, RALLY_POINT_NAMES)}`,
|
rallyPoint,
|
||||||
aircraftCallsign: `${pick(rng, AIRCRAFT_CALLSIGNS)} ${pick(rng, [1, 2, 3, 4])}`,
|
aircraftCallsign,
|
||||||
airSupportOffsetMin: pick(rng, [15, 20, 30, 45]),
|
airSupportOffsetMin,
|
||||||
enRouteMin: pickInt(rng, 18, 42),
|
enRouteMin,
|
||||||
checkInMinutes: pick(rng, [10, 15, 20, 30]),
|
checkInMinutes,
|
||||||
casevacLz: `LZ ${pick(rng, LZ_NAMES)}`,
|
casevacLz,
|
||||||
resupplyOffsetMin,
|
resupplyOffsetMin,
|
||||||
exfilOffsetMin,
|
exfilOffsetMin,
|
||||||
withdrawalAzimuth,
|
withdrawalAzimuth,
|
||||||
|
abortAction,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -569,12 +608,8 @@ export function buildOpordQuestions(
|
||||||
choiceQuestion(
|
choiceQuestion(
|
||||||
"abort-rally",
|
"abort-rally",
|
||||||
`If the abort codeword "${scenario.abortCodeword}" is broadcast, what does your element do?`,
|
`If the abort codeword "${scenario.abortCodeword}" is broadcast, what does your element do?`,
|
||||||
`Return to ${scenario.rallyPoint} and await further orders`,
|
scenario.abortAction,
|
||||||
[
|
abortActionBank(scenario).filter((action) => action !== scenario.abortAction),
|
||||||
"Continue the mission as planned",
|
|
||||||
"Mark the objective with smoke and hold in place",
|
|
||||||
`Request close air support from ${scenario.aircraftCallsign}`,
|
|
||||||
],
|
|
||||||
rng,
|
rng,
|
||||||
),
|
),
|
||||||
"callsign-net": () =>
|
"callsign-net": () =>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue