feat(opord): add garrison, ROE, intel, and secondary objectives with tier scaling
This commit is contained in:
parent
d8ee14ee8d
commit
50b9b7e46c
3 changed files with 251 additions and 9 deletions
|
|
@ -131,8 +131,10 @@ export function OpordArena({
|
||||||
<span className="font-medium text-foreground">Study</span>
|
<span className="font-medium text-foreground">Study</span>
|
||||||
<span>
|
<span>
|
||||||
The order covers the objective grid, azimuth, H-HOUR, phase lines, codewords,
|
The order covers the objective grid, azimuth, H-HOUR, phase lines, codewords,
|
||||||
callsign, and radio frequency. It is rendered as an image with copy and context
|
callsign, and radio frequency. Veteran tiers add garrison tasks, rules of
|
||||||
menus blocked: there is no text to select, and "Begin recall" starts the quiz
|
engagement, and a secondary objective; elite adds intelligence targets and a
|
||||||
|
tertiary objective. It is rendered as an image with copy and context menus
|
||||||
|
blocked: there is no text to select, and "Begin recall" starts the quiz
|
||||||
whenever you are ready.
|
whenever you are ready.
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -89,11 +89,17 @@ function buildBriefingRows(
|
||||||
};
|
};
|
||||||
|
|
||||||
const rows: BriefingRow[] = [];
|
const rows: BriefingRow[] = [];
|
||||||
|
let sectionNo = 0;
|
||||||
|
const header = (label: string) => {
|
||||||
|
sectionNo += 1;
|
||||||
|
push(rows, "header", `${sectionNo}. ${label}`);
|
||||||
|
};
|
||||||
|
|
||||||
push(rows, "subtitle", "COPY 1 OF 1 · POLARIS TASK FORCE");
|
push(rows, "subtitle", "COPY 1 OF 1 · POLARIS TASK FORCE");
|
||||||
push(rows, "title", `OPERATION ${scenario.operationName}`);
|
push(rows, "title", `OPERATION ${scenario.operationName}`);
|
||||||
push(rows, "subtitle", `OPERATIONS ORDER NO. ${String(scenario.seed).slice(-4).padStart(4, "0")}`);
|
push(rows, "subtitle", `OPERATIONS ORDER NO. ${String(scenario.seed).slice(-4).padStart(4, "0")}`);
|
||||||
rows.push({ kind: "rule" });
|
rows.push({ kind: "rule" });
|
||||||
push(rows, "header", "1. SITUATION");
|
header("SITUATION");
|
||||||
const enemyStrength =
|
const enemyStrength =
|
||||||
difficulty === "recruit"
|
difficulty === "recruit"
|
||||||
? `${scenario.enemyVehicles} vehicle(s) and ${scenario.enemySquads} infantry squad(s)`
|
? `${scenario.enemyVehicles} vehicle(s) and ${scenario.enemySquads} infantry squad(s)`
|
||||||
|
|
@ -103,24 +109,57 @@ function buildBriefingRows(
|
||||||
"text",
|
"text",
|
||||||
`Enemy: ${enemyStrength} dug in on objective ${scenario.objective} at grid ${formatOpordGrid(scenario.grid)}.`,
|
`Enemy: ${enemyStrength} dug in on objective ${scenario.objective} at grid ${formatOpordGrid(scenario.grid)}.`,
|
||||||
);
|
);
|
||||||
|
if (difficulty !== "recruit") {
|
||||||
|
push(
|
||||||
|
rows,
|
||||||
|
"text",
|
||||||
|
`Enemy garrison: ${scenario.garrisonCount} personnel reported across Buildings ${scenario.garrisonBuildings[0]} and ${scenario.garrisonBuildings[1]}.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
push(rows, "text", `Friendly: ${scenario.friendlyUnit} in support aboard ${scenario.transport}.`);
|
push(rows, "text", `Friendly: ${scenario.friendlyUnit} in support aboard ${scenario.transport}.`);
|
||||||
push(rows, "header", "2. MISSION");
|
header("MISSION");
|
||||||
push(
|
push(
|
||||||
rows,
|
rows,
|
||||||
"text",
|
"text",
|
||||||
`At H-HOUR ${scenario.hHour}, seize objective ${scenario.objective} (grid ${formatOpordGrid(scenario.grid)}), ${scenario.distanceKm} km on azimuth ${String(scenario.azimuth).padStart(3, "0")}.`,
|
`At H-HOUR ${scenario.hHour}, seize objective ${scenario.objective} (grid ${formatOpordGrid(scenario.grid)}), ${scenario.distanceKm} km on azimuth ${String(scenario.azimuth).padStart(3, "0")}.`,
|
||||||
);
|
);
|
||||||
push(rows, "header", "3. EXECUTION");
|
header("EXECUTION");
|
||||||
push(
|
push(
|
||||||
rows,
|
rows,
|
||||||
"text",
|
"text",
|
||||||
`Line of departure is phase line ${scenario.ldPhaseLine}. Subsequent phase lines: ${scenario.otherPhaseLines.length > 0 ? scenario.otherPhaseLines.join(", ") : "none assigned"}.`,
|
`Line of departure is phase line ${scenario.ldPhaseLine}. Subsequent phase lines: ${scenario.otherPhaseLines.length > 0 ? scenario.otherPhaseLines.join(", ") : "none assigned"}.`,
|
||||||
);
|
);
|
||||||
|
if (difficulty !== "recruit") {
|
||||||
|
push(rows, "text", `${scenario.maneuverElement} moves in and secures Building ${scenario.securedBuilding}.`);
|
||||||
|
push(
|
||||||
|
rows,
|
||||||
|
"text",
|
||||||
|
`${scenario.overwatchCallsign} moves to ${scenario.overwatchPosition} and assumes overwatch of ${scenario.overwatchTarget}.`,
|
||||||
|
);
|
||||||
|
push(rows, "text", `Secondary objective: neutralize ${scenario.secondaryObjective}.`);
|
||||||
|
}
|
||||||
|
if (difficulty === "elite") {
|
||||||
|
push(rows, "text", `Tertiary objective: neutralize ${scenario.tertiaryObjective}.`);
|
||||||
|
}
|
||||||
|
if (difficulty !== "recruit") {
|
||||||
|
header("RULES OF ENGAGEMENT");
|
||||||
|
push(rows, "text", `Enemy uniforms mirror ours: positive ID requires ${scenario.enemyUniformMark}.`);
|
||||||
|
push(rows, "text", `Civilian presence ${scenario.civilianStatus}.`);
|
||||||
|
}
|
||||||
|
if (difficulty === "elite") {
|
||||||
|
header("INTELLIGENCE");
|
||||||
|
push(
|
||||||
|
rows,
|
||||||
|
"text",
|
||||||
|
`Priority collection target: ${scenario.intelItem}, held in Building ${scenario.intelBuilding}.`,
|
||||||
|
);
|
||||||
|
push(rows, "text", `Priority enemy asset identified: ${scenario.priorityAsset}.`);
|
||||||
|
}
|
||||||
if (difficulty === "recruit") {
|
if (difficulty === "recruit") {
|
||||||
push(rows, "header", "4. SERVICE SUPPORT");
|
header("SERVICE SUPPORT");
|
||||||
push(rows, "text", `Element callsign ${scenario.callsign} monitors ${scenario.frequency} MHz.`);
|
push(rows, "text", `Element callsign ${scenario.callsign} monitors ${scenario.frequency} MHz.`);
|
||||||
} else {
|
} else {
|
||||||
push(rows, "header", "4. COORDINATION");
|
header("COORDINATION");
|
||||||
push(rows, "text", `Element callsign ${scenario.callsign} monitors ${scenario.frequency} MHz.`);
|
push(rows, "text", `Element callsign ${scenario.callsign} monitors ${scenario.frequency} MHz.`);
|
||||||
push(
|
push(
|
||||||
rows,
|
rows,
|
||||||
|
|
@ -129,7 +168,7 @@ function buildBriefingRows(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (difficulty === "elite") {
|
if (difficulty === "elite") {
|
||||||
push(rows, "header", "5. TIMING & CONTINGENCY");
|
header("TIMING & CONTINGENCY");
|
||||||
push(
|
push(
|
||||||
rows,
|
rows,
|
||||||
"text",
|
"text",
|
||||||
|
|
@ -145,7 +184,7 @@ function buildBriefingRows(
|
||||||
);
|
);
|
||||||
push(rows, "text", `Casualty evacuation lifts from ${scenario.casevacLz}.`);
|
push(rows, "text", `Casualty evacuation lifts from ${scenario.casevacLz}.`);
|
||||||
}
|
}
|
||||||
push(rows, "header", difficulty === "elite" ? "6. COMMAND & SIGNAL" : "5. COMMAND & SIGNAL");
|
header("COMMAND & SIGNAL");
|
||||||
rows.push({ kind: "kv", label: "AUTHENTICATION CODEWORD:", value: scenario.codeword });
|
rows.push({ kind: "kv", label: "AUTHENTICATION CODEWORD:", value: scenario.codeword });
|
||||||
rows.push({ kind: "kv", label: "ABORT CODEWORD:", value: scenario.abortCodeword });
|
rows.push({ kind: "kv", label: "ABORT CODEWORD:", value: scenario.abortCodeword });
|
||||||
if (difficulty === "elite") {
|
if (difficulty === "elite") {
|
||||||
|
|
|
||||||
|
|
@ -233,6 +233,34 @@ export interface OpordScenario {
|
||||||
readonly withdrawalAzimuth: number;
|
readonly withdrawalAzimuth: number;
|
||||||
/** Element action on abort; one of the abortActionBank templates. */
|
/** Element action on abort; one of the abortActionBank templates. */
|
||||||
readonly abortAction: string;
|
readonly abortAction: string;
|
||||||
|
/** e.g. "Fire Team ALPHA"; friendly maneuver element tasked in the veteran+ execution annex. */
|
||||||
|
readonly maneuverElement: string;
|
||||||
|
/** Building number (10-99) the maneuver element secures; distinct from every other building number. */
|
||||||
|
readonly securedBuilding: number;
|
||||||
|
/** e.g. "ROMEO 2"; overwatch element named in the veteran+ execution annex. */
|
||||||
|
readonly overwatchCallsign: string;
|
||||||
|
/** e.g. "the water tower"; where the overwatch element sets in (veteran+). */
|
||||||
|
readonly overwatchPosition: string;
|
||||||
|
/** e.g. "the northern approach"; what the overwatch element covers (veteran+). */
|
||||||
|
readonly overwatchTarget: string;
|
||||||
|
/** Enemy garrison headcount reported across two buildings (veteran+). */
|
||||||
|
readonly garrisonCount: number;
|
||||||
|
/** The two garrison building numbers, both distinct from all other building numbers. */
|
||||||
|
readonly garrisonBuildings: readonly [number, number];
|
||||||
|
/** e.g. "a red armband"; positive-identification mark distinguishing enemy uniforms (veteran+). */
|
||||||
|
readonly enemyUniformMark: string;
|
||||||
|
/** e.g. "possible on the objective"; rendered as "Civilian presence ${...}" (veteran+). */
|
||||||
|
readonly civilianStatus: string;
|
||||||
|
/** e.g. "the shipment manifests"; priority collection target (elite). */
|
||||||
|
readonly intelItem: string;
|
||||||
|
/** Building number (10-99) holding the collection target, distinct from all others (elite). */
|
||||||
|
readonly intelBuilding: number;
|
||||||
|
/** e.g. "two field guns"; identified enemy priority asset (elite). */
|
||||||
|
readonly priorityAsset: string;
|
||||||
|
/** e.g. "the comms relay"; secondary objective task (veteran+). */
|
||||||
|
readonly secondaryObjective: string;
|
||||||
|
/** e.g. "the mortar position"; tertiary objective task, distinct from secondary (elite). */
|
||||||
|
readonly tertiaryObjective: 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". */
|
||||||
|
|
@ -248,6 +276,57 @@ export function cardinalFromAzimuth(azimuth: number): string {
|
||||||
// Shared by scenario generation (stores one entry as abortAction) and question
|
// Shared by scenario generation (stores one entry as abortAction) and question
|
||||||
// building (offers the rest as distractors); every template reads only fields
|
// building (offers the rest as distractors); every template reads only fields
|
||||||
// the elite briefing renders.
|
// the elite briefing renders.
|
||||||
|
const FIRE_TEAMS = ["ALPHA", "BRAVO", "CHARLIE", "DELTA"] as const;
|
||||||
|
const OVERWATCH_CALLSIGNS = ["ROMEO", "SIERRA", "TANGO", "YANKEE"] as const;
|
||||||
|
const OVERWATCH_POSITIONS = [
|
||||||
|
"the church bell tower",
|
||||||
|
"the water tower",
|
||||||
|
"the grain silo",
|
||||||
|
"the parking structure",
|
||||||
|
"the rooftop terrace",
|
||||||
|
"Hill 140",
|
||||||
|
] as const;
|
||||||
|
const OVERWATCH_TARGETS = [
|
||||||
|
"the northern approach",
|
||||||
|
"the eastern avenue",
|
||||||
|
"the rail underpass",
|
||||||
|
"the main street junction",
|
||||||
|
"the western courtyard",
|
||||||
|
"the market square",
|
||||||
|
] as const;
|
||||||
|
const UNIFORM_MARKS = [
|
||||||
|
"a red armband",
|
||||||
|
"a white brassard",
|
||||||
|
"an orange chevron",
|
||||||
|
"a blue shoulder patch",
|
||||||
|
"a yellow pennant",
|
||||||
|
] as const;
|
||||||
|
const CIVILIAN_STATUS = [
|
||||||
|
"not reported",
|
||||||
|
"possible on the objective",
|
||||||
|
"confirmed sheltering in place",
|
||||||
|
"evacuated before H-HOUR",
|
||||||
|
] as const;
|
||||||
|
const INTEL_ITEMS = [
|
||||||
|
"the shipment manifests",
|
||||||
|
"the commander's map case",
|
||||||
|
"the Signals Intelligence kit",
|
||||||
|
"the comms shack hard drive",
|
||||||
|
] as const;
|
||||||
|
const PRIORITY_ASSETS = [
|
||||||
|
"two field guns",
|
||||||
|
"a troop transport vehicle",
|
||||||
|
"a mobile SAM launcher",
|
||||||
|
"a fuel bladder farm",
|
||||||
|
] as const;
|
||||||
|
const OBJECTIVE_TASKS = [
|
||||||
|
"the comms relay",
|
||||||
|
"the mortar position",
|
||||||
|
"the ammunition cache",
|
||||||
|
"the fuel depot",
|
||||||
|
"the vehicle park",
|
||||||
|
] as const;
|
||||||
|
|
||||||
function abortActionBank(
|
function abortActionBank(
|
||||||
scenario: Pick<
|
scenario: Pick<
|
||||||
OpordScenario,
|
OpordScenario,
|
||||||
|
|
@ -303,6 +382,13 @@ export function generateOpordScenario(seed: number): OpordScenario {
|
||||||
abortActionBank({ rallyPoint, ldPhaseLine, otherPhaseLines, withdrawalAzimuth, casevacLz, friendlyUnit }),
|
abortActionBank({ rallyPoint, ldPhaseLine, otherPhaseLines, withdrawalAzimuth, casevacLz, friendlyUnit }),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const buildings: number[] = [];
|
||||||
|
while (buildings.length < 4) {
|
||||||
|
const candidate = pickInt(rng, 10, 99);
|
||||||
|
if (!buildings.includes(candidate)) buildings.push(candidate);
|
||||||
|
}
|
||||||
|
const secondaryObjective = pick(rng, OBJECTIVE_TASKS);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
seed,
|
seed,
|
||||||
operationName,
|
operationName,
|
||||||
|
|
@ -332,6 +418,20 @@ export function generateOpordScenario(seed: number): OpordScenario {
|
||||||
exfilOffsetMin,
|
exfilOffsetMin,
|
||||||
withdrawalAzimuth,
|
withdrawalAzimuth,
|
||||||
abortAction,
|
abortAction,
|
||||||
|
maneuverElement: `Fire Team ${pick(rng, FIRE_TEAMS)}`,
|
||||||
|
securedBuilding: buildings[0],
|
||||||
|
overwatchCallsign: `${pick(rng, OVERWATCH_CALLSIGNS)} ${pick(rng, [2, 3, 4, 6])}`,
|
||||||
|
overwatchPosition: pick(rng, OVERWATCH_POSITIONS),
|
||||||
|
overwatchTarget: pick(rng, OVERWATCH_TARGETS),
|
||||||
|
garrisonCount: pickInt(rng, 6, 24),
|
||||||
|
garrisonBuildings: [buildings[1], buildings[2]],
|
||||||
|
enemyUniformMark: pick(rng, UNIFORM_MARKS),
|
||||||
|
civilianStatus: pick(rng, CIVILIAN_STATUS),
|
||||||
|
intelItem: pick(rng, INTEL_ITEMS),
|
||||||
|
intelBuilding: buildings[3],
|
||||||
|
priorityAsset: pick(rng, PRIORITY_ASSETS),
|
||||||
|
secondaryObjective,
|
||||||
|
tertiaryObjective: pick(rng, OBJECTIVE_TASKS.filter((task) => task !== secondaryObjective)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -551,6 +651,73 @@ export function buildOpordQuestions(
|
||||||
.map((name) => `LZ ${name}`),
|
.map((name) => `LZ ${name}`),
|
||||||
rng,
|
rng,
|
||||||
),
|
),
|
||||||
|
garrisonCount: () =>
|
||||||
|
choiceQuestion(
|
||||||
|
"garrison-count",
|
||||||
|
"How many garrisoned personnel are reported on the objective?",
|
||||||
|
`${scenario.garrisonCount}`,
|
||||||
|
[scenario.garrisonCount - 4, scenario.garrisonCount - 2, scenario.garrisonCount + 2, scenario.garrisonCount + 4]
|
||||||
|
.filter((value) => value >= 1 && value !== scenario.garrisonCount)
|
||||||
|
.slice(0, 3)
|
||||||
|
.map(String),
|
||||||
|
rng,
|
||||||
|
),
|
||||||
|
uniformMark: () =>
|
||||||
|
choiceQuestion(
|
||||||
|
"uniform-mark",
|
||||||
|
"Which distinguishing mark identifies enemy personnel?",
|
||||||
|
scenario.enemyUniformMark,
|
||||||
|
shuffle(rng, UNIFORM_MARKS.filter((mark) => mark !== scenario.enemyUniformMark)).slice(0, 3),
|
||||||
|
rng,
|
||||||
|
),
|
||||||
|
civilianStatus: () =>
|
||||||
|
choiceQuestion(
|
||||||
|
"civilian-status",
|
||||||
|
"What civilian presence is reported for the objective?",
|
||||||
|
scenario.civilianStatus,
|
||||||
|
shuffle(rng, CIVILIAN_STATUS.filter((status) => status !== scenario.civilianStatus)).slice(0, 3),
|
||||||
|
rng,
|
||||||
|
),
|
||||||
|
secondaryObjective: () =>
|
||||||
|
choiceQuestion(
|
||||||
|
"secondary-objective",
|
||||||
|
"What is the element's secondary objective?",
|
||||||
|
`Neutralize ${scenario.secondaryObjective}`,
|
||||||
|
shuffle(rng, OBJECTIVE_TASKS.filter((task) => task !== scenario.secondaryObjective))
|
||||||
|
.slice(0, 3)
|
||||||
|
.map((task) => `Neutralize ${task}`),
|
||||||
|
rng,
|
||||||
|
),
|
||||||
|
tertiaryObjective: () =>
|
||||||
|
choiceQuestion(
|
||||||
|
"tertiary-objective",
|
||||||
|
"What is the element's tertiary objective?",
|
||||||
|
`Neutralize ${scenario.tertiaryObjective}`,
|
||||||
|
shuffle(rng, OBJECTIVE_TASKS.filter((task) => task !== scenario.tertiaryObjective))
|
||||||
|
.slice(0, 3)
|
||||||
|
.map((task) => `Neutralize ${task}`),
|
||||||
|
rng,
|
||||||
|
),
|
||||||
|
securedBuilding: () =>
|
||||||
|
choiceQuestion(
|
||||||
|
"secured-building",
|
||||||
|
`Which building does ${scenario.maneuverElement} secure?`,
|
||||||
|
`Building ${scenario.securedBuilding}`,
|
||||||
|
[
|
||||||
|
`Building ${scenario.intelBuilding}`,
|
||||||
|
`Building ${scenario.garrisonBuildings[0]}`,
|
||||||
|
`Building ${scenario.garrisonBuildings[1]}`,
|
||||||
|
],
|
||||||
|
rng,
|
||||||
|
),
|
||||||
|
priorityAsset: () =>
|
||||||
|
choiceQuestion(
|
||||||
|
"priority-asset",
|
||||||
|
"Which enemy priority asset was identified on the objective?",
|
||||||
|
scenario.priorityAsset,
|
||||||
|
shuffle(rng, PRIORITY_ASSETS.filter((asset) => asset !== scenario.priorityAsset)).slice(0, 3),
|
||||||
|
rng,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Relational questions link two rendered briefing elements instead of
|
// Relational questions link two rendered briefing elements instead of
|
||||||
|
|
@ -683,6 +850,26 @@ export function buildOpordQuestions(
|
||||||
).slice(0, 3),
|
).slice(0, 3),
|
||||||
rng,
|
rng,
|
||||||
),
|
),
|
||||||
|
"overwatch-post": () =>
|
||||||
|
choiceQuestion(
|
||||||
|
"overwatch-post",
|
||||||
|
`From which position does ${scenario.overwatchCallsign} overwatch ${scenario.overwatchTarget}?`,
|
||||||
|
scenario.overwatchPosition,
|
||||||
|
shuffle(rng, OVERWATCH_POSITIONS.filter((position) => position !== scenario.overwatchPosition)).slice(0, 3),
|
||||||
|
rng,
|
||||||
|
),
|
||||||
|
"intel-link": () =>
|
||||||
|
choiceQuestion(
|
||||||
|
"intel-link",
|
||||||
|
`From where is ${scenario.intelItem} recovered?`,
|
||||||
|
`Building ${scenario.intelBuilding}`,
|
||||||
|
[
|
||||||
|
`Building ${scenario.securedBuilding}`,
|
||||||
|
`Building ${scenario.garrisonBuildings[0]}`,
|
||||||
|
`Building ${scenario.garrisonBuildings[1]}`,
|
||||||
|
],
|
||||||
|
rng,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
const tier = OPORD_DIFFICULTIES[difficulty];
|
const tier = OPORD_DIFFICULTIES[difficulty];
|
||||||
|
|
@ -712,6 +899,10 @@ export function buildOpordQuestions(
|
||||||
recallBuilders.distance,
|
recallBuilders.distance,
|
||||||
recallBuilders.squads,
|
recallBuilders.squads,
|
||||||
recallBuilders.mortars,
|
recallBuilders.mortars,
|
||||||
|
recallBuilders.garrisonCount,
|
||||||
|
recallBuilders.uniformMark,
|
||||||
|
recallBuilders.civilianStatus,
|
||||||
|
recallBuilders.secondaryObjective,
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
recallBuilders.codeword,
|
recallBuilders.codeword,
|
||||||
|
|
@ -727,6 +918,13 @@ export function buildOpordQuestions(
|
||||||
recallBuilders.mortars,
|
recallBuilders.mortars,
|
||||||
recallBuilders.checkIn,
|
recallBuilders.checkIn,
|
||||||
recallBuilders.casevac,
|
recallBuilders.casevac,
|
||||||
|
recallBuilders.garrisonCount,
|
||||||
|
recallBuilders.uniformMark,
|
||||||
|
recallBuilders.civilianStatus,
|
||||||
|
recallBuilders.secondaryObjective,
|
||||||
|
recallBuilders.tertiaryObjective,
|
||||||
|
recallBuilders.securedBuilding,
|
||||||
|
recallBuilders.priorityAsset,
|
||||||
];
|
];
|
||||||
const relationalPool: readonly (() => OpordQuestion)[] =
|
const relationalPool: readonly (() => OpordQuestion)[] =
|
||||||
difficulty === "veteran"
|
difficulty === "veteran"
|
||||||
|
|
@ -737,6 +935,7 @@ export function buildOpordQuestions(
|
||||||
relationalBuilders["pl-sequence"],
|
relationalBuilders["pl-sequence"],
|
||||||
relationalBuilders["transport-link"],
|
relationalBuilders["transport-link"],
|
||||||
relationalBuilders["callsign-net"],
|
relationalBuilders["callsign-net"],
|
||||||
|
relationalBuilders["overwatch-post"],
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
relationalBuilders["azimuth-cardinal"],
|
relationalBuilders["azimuth-cardinal"],
|
||||||
|
|
@ -750,6 +949,8 @@ export function buildOpordQuestions(
|
||||||
relationalBuilders["resupply-time"],
|
relationalBuilders["resupply-time"],
|
||||||
relationalBuilders["exfil-time"],
|
relationalBuilders["exfil-time"],
|
||||||
relationalBuilders["withdrawal-cardinal"],
|
relationalBuilders["withdrawal-cardinal"],
|
||||||
|
relationalBuilders["overwatch-post"],
|
||||||
|
relationalBuilders["intel-link"],
|
||||||
];
|
];
|
||||||
const relationalCount = difficulty === "recruit" ? 0 : difficulty === "veteran" ? 5 : 8;
|
const relationalCount = difficulty === "recruit" ? 0 : difficulty === "veteran" ? 5 : 8;
|
||||||
const relational = shuffle(rng, relationalPool).slice(0, relationalCount);
|
const relational = shuffle(rng, relationalPool).slice(0, relationalCount);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue