fix(classroom): improve VATSIM Essentials lessons

Radio Check: remove readability field (pilot can't know it before
controller responds), focus on initiating the check call.
Say Again: make expected answer concrete ("all before [SID]") instead
of subjective, improve hints to guide the answer.
Monitor vs Contact: simplify by replacing huge initial-call field with
a yes/no "do you transmit?" field.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
itsrubberduck
2026-02-15 15:06:40 +01:00
parent 8b20dc9131
commit aac357cf38

View File

@@ -3257,6 +3257,513 @@ const fullFlightLessons = [
}
]
/* ────────────────────────────────────────────────────────────
* ABNORMAL COMMUNICATIONS
* ──────────────────────────────────────────────────────────── */
const abnormalCommsLessons = [
{
id: 'conditional-crossing',
title: 'Conditional Runway Crossing',
desc: 'Cross a runway behind specific traffic',
keywords: ['Conditional', 'Runway', 'Crossing'],
hints: [
'Conditional clearances always start AND end with the condition — "behind" appears twice.',
'Identify the traffic type before the runway and intersection.',
'Never cross until the condition is met — the departing traffic must be clear.'
],
fields: [
{
key: 'traffic-type',
label: 'Traffic',
expected: () => 'departing 737',
alternatives: () => ['departing 737', 'Departing 737', 'departing Boeing 737'],
placeholder: 'e.g. departing 737',
width: 'md' as const
},
{
key: 'runway',
label: 'Runway',
expected: (scenario: Scenario) => scenario.runway,
alternatives: (scenario: Scenario) => [scenario.runway, scenario.runway.replace(/^0/, '')],
placeholder: 'e.g. 25C',
width: 'sm' as const
},
{
key: 'intersection',
label: 'Intersection',
expected: (scenario: Scenario) => scenario.taxiRoute.split(' ')[0],
placeholder: 'e.g. A5',
width: 'sm' as const
}
],
readback: [
{ type: 'text' as const, text: 'Behind ' },
{ type: 'field' as const, key: 'traffic-type', width: 'md' as const },
{ type: 'text' as const, text: ', cross runway ' },
{ type: 'field' as const, key: 'runway', width: 'sm' as const },
{ type: 'text' as const, text: ' at ' },
{ type: 'field' as const, key: 'intersection', width: 'sm' as const },
{ type: 'text' as const, text: (scenario: Scenario) => `, behind, ${scenario.radioCall}` }
],
defaultFrequency: 'GND' as const,
phrase: (scenario: Scenario) =>
`${scenario.radioCall}, behind the departing 737, cross runway ${scenario.runway} at ${scenario.taxiRoute.split(' ')[0]}, behind.`,
info: (scenario: Scenario) => [
`Traffic: departing 737`,
`Runway: ${scenario.runway}`,
`Intersection: ${scenario.taxiRoute.split(' ')[0]}`,
'Conditional clearances must include the condition at the start AND end of the readback.'
],
generate: createBaseScenario
},
{
id: 'complex-holding',
title: 'Complex Holding Pattern',
desc: 'Copy a full holding clearance with all parameters',
keywords: ['Holding', 'Pattern', 'EFC'],
hints: [
'Holding clearances have six parts: direction, fix, inbound course, turn direction, leg time, and EFC.',
'Right turns are standard — only non-standard (left) turns are explicitly stated.',
'EFC is "expect further clearance" — copy the Zulu time carefully.'
],
fields: [
{
key: 'direction',
label: 'Direction',
expected: () => 'east',
alternatives: () => ['east', 'west', 'north', 'south', 'northeast', 'northwest', 'southeast', 'southwest'],
threshold: 0.8,
placeholder: 'e.g. east',
width: 'sm' as const
},
{
key: 'fix',
label: 'Fix',
expected: (scenario: Scenario) => scenario.holdingFix,
placeholder: 'e.g. TOBAK',
width: 'sm' as const
},
{
key: 'inbound-course',
label: 'Inbound course',
expected: (scenario: Scenario) => scenario.holdingInbound,
placeholder: 'e.g. 270',
width: 'sm' as const,
inputmode: 'numeric' as const
},
{
key: 'turn-direction',
label: 'Turn direction',
expected: (scenario: Scenario) => scenario.holdingTurn,
alternatives: (scenario: Scenario) => [scenario.holdingTurn, scenario.holdingTurn === 'right' ? 'right' : 'left'],
placeholder: 'e.g. right',
width: 'sm' as const
},
{
key: 'leg-time',
label: 'Leg time',
expected: (scenario: Scenario) => scenario.holdingLegTime,
placeholder: 'e.g. 1.5',
width: 'sm' as const
},
{
key: 'efc-time',
label: 'EFC time',
expected: (scenario: Scenario) => scenario.holdingEfc,
placeholder: 'e.g. 1435',
width: 'sm' as const,
inputmode: 'numeric' as const
}
],
readback: [
{ type: 'text' as const, text: 'Hold ' },
{ type: 'field' as const, key: 'direction', width: 'sm' as const },
{ type: 'text' as const, text: ' of ' },
{ type: 'field' as const, key: 'fix', width: 'sm' as const },
{ type: 'text' as const, text: ', inbound course ' },
{ type: 'field' as const, key: 'inbound-course', width: 'sm' as const },
{ type: 'text' as const, text: ', ' },
{ type: 'field' as const, key: 'turn-direction', width: 'sm' as const },
{ type: 'text' as const, text: ' turns, ' },
{ type: 'field' as const, key: 'leg-time', width: 'sm' as const },
{ type: 'text' as const, text: ' minute legs, expect further clearance ' },
{ type: 'field' as const, key: 'efc-time', width: 'sm' as const },
{ type: 'text' as const, text: (scenario: Scenario) => `, ${scenario.radioCall}` }
],
defaultFrequency: 'CTR' as const,
phrase: (scenario: Scenario) =>
`${scenario.radioCall}, hold east of ${scenario.holdingFix}, inbound course ${scenario.holdingInbound}, ${scenario.holdingTurn} turns, ${scenario.holdingLegTime} minute legs, expect further clearance ${scenario.holdingEfc}.`,
info: (scenario: Scenario) => [
`Fix: ${scenario.holdingFix}`,
`Inbound course: ${scenario.holdingInbound}`,
`Turn: ${scenario.holdingTurn}, Leg: ${scenario.holdingLegTime} min`,
`EFC: ${scenario.holdingEfc}Z`
],
generate: createBaseScenario
},
{
id: 'amended-departure',
title: 'Amended Departure Clearance',
desc: 'Copy a full re-clearance with SID, altitude, frequency and squawk',
keywords: ['Amended', 'Clearance', 'Departure'],
hints: [
'An amended clearance replaces the original — copy every element.',
'Note "climb via SID except maintain" — the initial altitude caps the SID.',
'Departure frequency and squawk may differ from the original clearance.'
],
fields: [
{
key: 'destination',
label: 'Destination',
expected: (scenario: Scenario) => scenario.destination.name,
placeholder: 'e.g. Munich',
width: 'md' as const
},
{
key: 'sid',
label: 'SID',
expected: (scenario: Scenario) => scenario.sid,
placeholder: 'e.g. TOBAK 5Q',
width: 'md' as const
},
{
key: 'altitude',
label: 'Initial altitude',
expected: (scenario: Scenario) => scenario.altitudes.initial.toString(),
placeholder: 'e.g. 5000',
width: 'sm' as const,
inputmode: 'numeric' as const
},
{
key: 'expect-alt',
label: 'Expect altitude',
expected: (scenario: Scenario) => scenario.altitudes.climb.toString(),
placeholder: 'e.g. 7000',
width: 'sm' as const,
inputmode: 'numeric' as const
},
{
key: 'dep-freq',
label: 'Departure freq',
expected: (scenario: Scenario) => scenario.departureFreq,
placeholder: 'e.g. 125.350',
width: 'md' as const
},
{
key: 'squawk',
label: 'Squawk',
expected: (scenario: Scenario) => scenario.squawk,
placeholder: 'e.g. 4521',
width: 'sm' as const,
inputmode: 'numeric' as const
}
],
readback: [
{ type: 'text' as const, text: 'Amended clearance, cleared to ' },
{ type: 'field' as const, key: 'destination', width: 'md' as const },
{ type: 'text' as const, text: ' via ' },
{ type: 'field' as const, key: 'sid', width: 'md' as const },
{ type: 'text' as const, text: ' departure, climb via SID except maintain ' },
{ type: 'field' as const, key: 'altitude', width: 'sm' as const },
{ type: 'text' as const, text: ', expect ' },
{ type: 'field' as const, key: 'expect-alt', width: 'sm' as const },
{ type: 'text' as const, text: ' ten minutes after departure, departure frequency ' },
{ type: 'field' as const, key: 'dep-freq', width: 'md' as const },
{ type: 'text' as const, text: ', squawk ' },
{ type: 'field' as const, key: 'squawk', width: 'sm' as const },
{ type: 'text' as const, text: (scenario: Scenario) => `, ${scenario.radioCall}` }
],
defaultFrequency: 'DEL' as const,
phrase: (scenario: Scenario) =>
`${scenario.radioCall}, amended clearance: Cleared to ${scenario.destination.name} via ${scenario.sid} departure, climb via SID except maintain ${scenario.altitudes.initial}, expect ${scenario.altitudes.climb} ten minutes after departure, departure frequency ${scenario.departureFreq}, squawk ${scenario.squawk}.`,
info: (scenario: Scenario) => [
`Destination: ${scenario.destination.name}`,
`SID: ${scenario.sid}`,
`Initial: ${scenario.altitudes.initial}, Expect: ${scenario.altitudes.climb}`,
`Dep freq: ${scenario.departureFreq}, Squawk: ${scenario.squawk}`
],
generate: createBaseScenario
},
{
id: 'speed-then-altitude',
title: 'Speed Then Altitude',
desc: 'Execute a sequential speed-then-descend instruction',
keywords: ['Speed', 'Descent', 'Sequential'],
hints: [
'"Then" means sequential — reduce speed FIRST, begin descent SECOND.',
'Read back both values in the correct order to confirm you understood the sequence.',
'Do not descend until the target speed is reached.'
],
fields: [
{
key: 'speed',
label: 'Speed (kt)',
expected: (scenario: Scenario) => scenario.speedRestriction.toString(),
placeholder: 'e.g. 210',
width: 'sm' as const,
inputmode: 'numeric' as const
},
{
key: 'level',
label: 'Flight level',
expected: (scenario: Scenario) => scenario.approachAltitude.toString(),
placeholder: 'e.g. 5000',
width: 'sm' as const,
inputmode: 'numeric' as const
}
],
readback: [
{ type: 'text' as const, text: 'Reduce speed ' },
{ type: 'field' as const, key: 'speed', width: 'sm' as const },
{ type: 'text' as const, text: ' knots, then descend and maintain flight level ' },
{ type: 'field' as const, key: 'level', width: 'sm' as const },
{ type: 'text' as const, text: (scenario: Scenario) => `, ${scenario.radioCall}` }
],
defaultFrequency: 'APP' as const,
phrase: (scenario: Scenario) =>
`${scenario.radioCall}, reduce speed ${scenario.speedRestriction} knots, then descend and maintain flight level ${scenario.approachAltitude}.`,
info: (scenario: Scenario) => [
`Speed: ${scenario.speedRestriction} knots`,
`Descend to: FL${scenario.approachAltitude}`,
'"Then" = sequential: speed first, descent second.'
],
generate: createBaseScenario
},
{
id: 'immediate-traffic',
title: 'Immediate Turn for Traffic',
desc: 'Execute an immediate heading change for traffic avoidance',
keywords: ['Immediate', 'Traffic', 'Avoidance'],
hints: [
'IMMEDIATELY means begin the turn while reading back — do not wait.',
'Traffic calls include clock position, distance, direction and altitude.',
'Comply first, then read back — safety overrides normal sequencing.'
],
fields: [
{
key: 'turn-direction',
label: 'Turn direction',
expected: () => 'left',
alternatives: () => ['left'],
placeholder: 'e.g. left',
width: 'sm' as const
},
{
key: 'heading',
label: 'Heading',
expected: (scenario: Scenario) => scenario.vectorHeading,
placeholder: 'e.g. 270',
width: 'sm' as const,
inputmode: 'numeric' as const
}
],
readback: [
{ type: 'text' as const, text: 'IMMEDIATELY turning left heading ' },
{ type: 'field' as const, key: 'heading', width: 'sm' as const },
{ type: 'text' as const, text: (scenario: Scenario) => `, ${scenario.radioCall}` }
],
defaultFrequency: 'APP' as const,
phrase: (scenario: Scenario) =>
`${scenario.radioCall}, turn left IMMEDIATELY heading ${scenario.vectorHeading}, traffic 12 o'clock, 3 miles, opposite direction, same level.`,
info: (scenario: Scenario) => [
`Turn: left heading ${scenario.vectorHeading}`,
'Traffic: 12 o\'clock, 3 miles, opposite direction, same level',
'IMMEDIATELY = execute while reading back.'
],
generate: createBaseScenario
},
{
id: 'multiple-crossing-restrictions',
title: 'Multiple Crossing Restrictions',
desc: 'Copy a STAR descent with two crossing fixes and altitudes',
keywords: ['STAR', 'Crossing', 'Restrictions'],
hints: [
'Each crossing fix has its own altitude restriction — copy them separately.',
'Restrictions can be "at", "at or above", or "at or below" — the wording matters.',
'Read back each fix and restriction in the order given.'
],
fields: [
{
key: 'fix1',
label: 'Fix 1',
expected: (scenario: Scenario) => scenario.crossingFix1,
placeholder: 'e.g. ANEKI',
width: 'sm' as const
},
{
key: 'restriction1',
label: 'Restriction',
expected: (scenario: Scenario) => scenario.crossingRestriction1,
alternatives: (scenario: Scenario) => [scenario.crossingRestriction1],
threshold: 0.8,
placeholder: 'e.g. at or above',
width: 'md' as const
},
{
key: 'alt1',
label: 'Altitude 1',
expected: (scenario: Scenario) => scenario.crossingAlt1,
placeholder: 'e.g. 10000',
width: 'sm' as const,
inputmode: 'numeric' as const
},
{
key: 'fix2',
label: 'Fix 2',
expected: (scenario: Scenario) => scenario.crossingFix2,
placeholder: 'e.g. TOBAK',
width: 'sm' as const
},
{
key: 'alt2',
label: 'Altitude 2',
expected: (scenario: Scenario) => scenario.crossingAlt2,
placeholder: 'e.g. 6000',
width: 'sm' as const,
inputmode: 'numeric' as const
}
],
readback: [
{ type: 'text' as const, text: 'Descend via the STAR, cross ' },
{ type: 'field' as const, key: 'fix1', width: 'sm' as const },
{ type: 'text' as const, text: ' ' },
{ type: 'field' as const, key: 'restriction1', width: 'md' as const },
{ type: 'text' as const, text: ' ' },
{ type: 'field' as const, key: 'alt1', width: 'sm' as const },
{ type: 'text' as const, text: ', cross ' },
{ type: 'field' as const, key: 'fix2', width: 'sm' as const },
{ type: 'text' as const, text: ' at ' },
{ type: 'field' as const, key: 'alt2', width: 'sm' as const },
{ type: 'text' as const, text: (scenario: Scenario) => `, ${scenario.radioCall}` }
],
defaultFrequency: 'CTR' as const,
phrase: (scenario: Scenario) =>
`${scenario.radioCall}, descend via the STAR, cross ${scenario.crossingFix1} ${scenario.crossingRestriction1} ${scenario.crossingAlt1}, cross ${scenario.crossingFix2} at ${scenario.crossingAlt2}.`,
info: (scenario: Scenario) => [
`Fix 1: ${scenario.crossingFix1}${scenario.crossingRestriction1} ${scenario.crossingAlt1}`,
`Fix 2: ${scenario.crossingFix2} — at ${scenario.crossingAlt2}`,
'Read back each crossing restriction separately.'
],
generate: createBaseScenario
},
{
id: 'tcas-ra-override',
title: 'TCAS RA Override',
desc: 'Reject an ATC instruction due to a TCAS Resolution Advisory',
keywords: ['TCAS', 'RA', 'Override'],
hints: [
'A TCAS RA always takes priority over ATC instructions — you MUST follow the RA.',
'Tell ATC "Unable, TCAS RA" to reject their instruction.',
'Once clear of conflict, report back: "Clear of conflict, returning to [assigned level]".'
],
fields: [
{
key: 'rejection',
label: 'Rejection',
expected: () => 'Unable, TCAS RA',
alternatives: () => ['Unable, TCAS RA', 'Unable TCAS RA', 'unable, TCAS RA', 'unable TCAS RA'],
threshold: 0.75,
placeholder: 'e.g. Unable, TCAS RA',
width: 'lg' as const
},
{
key: 'resolution',
label: 'Resolution report',
expected: (scenario: Scenario) => `Clear of conflict, returning to flight level ${scenario.altitudes.climb}`,
alternatives: (scenario: Scenario) => [
`Clear of conflict, returning to flight level ${scenario.altitudes.climb}`,
`Clear of conflict, returning to FL${Math.round(scenario.altitudes.climb / 100)}`,
`clear of conflict, returning to flight level ${scenario.altitudes.climb}`,
],
threshold: 0.6,
placeholder: 'e.g. Clear of conflict, returning to ...',
width: 'xl' as const
}
],
readback: [
{ type: 'field' as const, key: 'rejection', width: 'lg' as const },
{ type: 'text' as const, text: ' · ' },
{ type: 'field' as const, key: 'resolution', width: 'xl' as const },
{ type: 'text' as const, text: (scenario: Scenario) => `, ${scenario.radioCall}` }
],
defaultFrequency: 'CTR' as const,
phrase: (scenario: Scenario) =>
`${scenario.radioCall}, descend flight level ${scenario.altitudes.initial}. [TCAS commands: CLIMB, CLIMB NOW]`,
info: (scenario: Scenario) => [
'ATC says descend — but your TCAS RA commands CLIMB.',
'TCAS RA always overrides ATC. Follow the RA, then report clear of conflict.',
`Return to: flight level ${scenario.altitudes.climb}`
],
generate: createBaseScenario
},
{
id: 'late-runway-change',
title: 'Late Runway Change',
desc: 'Cancel approach and accept vectors to a new runway',
keywords: ['Runway Change', 'Vectors', 'Approach'],
hints: [
'Read back the cancelled runway, the new heading, the new runway, and the new altitude.',
'Confirm which runway is cancelled and which is the new assignment.',
'Late changes happen — stay calm and copy all four elements.'
],
fields: [
{
key: 'old-runway',
label: 'Old runway',
expected: (scenario: Scenario) => scenario.arrivalRunway,
alternatives: (scenario: Scenario) => [scenario.arrivalRunway, scenario.arrivalRunway.replace(/^0/, '')],
placeholder: 'e.g. 25C',
width: 'sm' as const
},
{
key: 'heading',
label: 'Heading',
expected: (scenario: Scenario) => scenario.vectorHeading,
placeholder: 'e.g. 180',
width: 'sm' as const,
inputmode: 'numeric' as const
},
{
key: 'new-runway',
label: 'New runway',
expected: (scenario: Scenario) => scenario.runway,
alternatives: (scenario: Scenario) => [scenario.runway, scenario.runway.replace(/^0/, '')],
placeholder: 'e.g. 07R',
width: 'sm' as const
},
{
key: 'altitude',
label: 'Altitude',
expected: (scenario: Scenario) => scenario.approachAltitude.toString(),
placeholder: 'e.g. 4000',
width: 'sm' as const,
inputmode: 'numeric' as const
}
],
readback: [
{ type: 'text' as const, text: 'Cancel approach runway ' },
{ type: 'field' as const, key: 'old-runway', width: 'sm' as const },
{ type: 'text' as const, text: ', turn right heading ' },
{ type: 'field' as const, key: 'heading', width: 'sm' as const },
{ type: 'text' as const, text: ', vectors runway ' },
{ type: 'field' as const, key: 'new-runway', width: 'sm' as const },
{ type: 'text' as const, text: ', descend ' },
{ type: 'field' as const, key: 'altitude', width: 'sm' as const },
{ type: 'text' as const, text: (scenario: Scenario) => `, ${scenario.radioCall}` }
],
defaultFrequency: 'APP' as const,
phrase: (scenario: Scenario) =>
`${scenario.radioCall}, cancel approach runway ${scenario.arrivalRunway}, turn right heading ${scenario.vectorHeading}, vectors runway ${scenario.runway}, descend ${scenario.approachAltitude}.`,
info: (scenario: Scenario) => [
`Cancelled: runway ${scenario.arrivalRunway}`,
`New: runway ${scenario.runway}, heading ${scenario.vectorHeading}`,
`Descend to: ${scenario.approachAltitude}`
],
generate: createBaseScenario
}
]
/* ────────────────────────────────────────────────────────────
* VATSIM ESSENTIALS
* ──────────────────────────────────────────────────────────── */
@@ -3329,12 +3836,12 @@ const vatsimEssentialsLessons = [
{
id: 'radio-check-readability',
title: 'Radio Check & Readability',
desc: 'Initiate a radio check and confirm readability',
desc: 'Initiate a radio check and read back the controller readability response',
keywords: ['Radio Check', 'Readability', 'Basics'],
hints: [
'Format: "[Station], [Callsign], radio check [frequency]".',
'Readability scale: 1 = unreadable, 2 = barely readable, 3 = readable with difficulty, 4 = readable, 5 = perfectly readable.',
'Confirm with: "Readability [number], [Callsign]".'
'You initiate a radio check, controller responds with your readability.',
'Readability scale: 1 = unreadable, 2 = barely, 3 = with difficulty, 4 = readable, 5 = perfectly readable.',
'Your readback confirms you received their assessment.'
],
fields: [
{
@@ -3360,34 +3867,20 @@ const vatsimEssentialsLessons = [
],
placeholder: 'e.g. 121.900',
width: 'md' as const
},
{
key: 'readability',
label: 'Readability',
expected: (scenario: Scenario) => scenario.readability.toString(),
alternatives: (scenario: Scenario) => [
scenario.readability.toString(),
scenario.readabilityWord,
],
placeholder: '1-5',
width: 'xs' as const,
inputmode: 'numeric' as const
}
],
readback: [
{ type: 'field' as const, key: 'station', width: 'lg' as const },
{ type: 'text' as const, text: (scenario: Scenario) => `, ${scenario.radioCall}, radio check ` },
{ type: 'field' as const, key: 'frequency', width: 'md' as const },
{ type: 'text' as const, text: ' · Readability ' },
{ type: 'field' as const, key: 'readability', width: 'xs' as const },
],
defaultFrequency: 'DEL',
phrase: (scenario: Scenario) =>
`${scenario.radioCall}, ${scenario.readabilityPhrase}.`,
`You want to check your radio on ${scenario.airport.city} Delivery, frequency ${scenario.deliveryFreq}. Initiate the radio check call.`,
info: (scenario: Scenario) => [
`Station: ${scenario.airport.city} Delivery`,
`Frequency: ${scenario.deliveryFreq}`,
`Readability: ${scenario.readability}${scenario.readabilityWord}`,
'Format: "[Station], [Callsign], radio check [frequency]"',
],
generate: createBaseScenario
},
@@ -3398,24 +3891,24 @@ const vatsimEssentialsLessons = [
keywords: ['Say Again', 'Comms', 'Recovery'],
hints: [
'Never just say "say again" — always specify WHAT you missed.',
'Use: "Say again [element]" or "Say again all before [word]" or "Say again all after [word]".',
'Common targets: clearance limit, routing, squawk code, altitude.'
'Standard form: "Say again all before [last word you heard clearly]".',
'Here the clearance limit and routing are garbled. You heard the SID and squawk clearly.'
],
fields: [
{
key: 'say-again-target',
label: 'Say again target',
expected: () => 'clearance limit and routing',
alternatives: () => [
expected: (scenario: Scenario) => `all before ${scenario.sid}`,
alternatives: (scenario: Scenario) => [
`all before ${scenario.sid}`,
`all before ${scenario.sid} departure`,
'clearance limit and routing',
'clearance limit',
'routing',
'all before squawk',
'destination and routing',
'all before departure frequency',
'all before departure',
],
threshold: 0.55,
placeholder: 'e.g. clearance limit and routing',
placeholder: 'e.g. all before [word]',
width: 'xl' as const
}
],
@@ -3428,8 +3921,8 @@ const vatsimEssentialsLessons = [
`${scenario.radioCall}, cleared to... [garbled] ...${scenario.sid} departure, squawk ${scenario.squawkWords}.`,
info: (scenario: Scenario) => [
'The clearance limit (destination) and routing were garbled.',
`You heard: SID "${scenario.sid}" and squawk "${scenario.squawk}"`,
'Request specifically what you missed — not "say again all".'
`You heard clearly: "${scenario.sid} departure, squawk ${scenario.squawk}"`,
`Best response: "Say again all before ${scenario.sid}"`,
],
generate: createBaseScenario
},
@@ -3446,7 +3939,7 @@ const vatsimEssentialsLessons = [
fields: [
{
key: 'action',
label: 'Action',
label: 'What did ATC say?',
expected: () => 'contact',
alternatives: () => ['contact', 'Contact'],
placeholder: 'contact or monitor',
@@ -3473,17 +3966,12 @@ const vatsimEssentialsLessons = [
width: 'md' as const
},
{
key: 'initial-call',
label: 'Initial call',
expected: (scenario: Scenario) =>
`${scenario.airport.city} Tower, ${scenario.radioCall}, ${scenario.altitudes.initialWords} feet, information ${scenario.atisCodeWord}`,
alternatives: (scenario: Scenario) => [
`${scenario.airport.city} Tower, ${scenario.radioCall}, ${scenario.altitudes.initial} feet, information ${scenario.atisCode}`,
`${scenario.airport.city} Tower, ${scenario.radioCall}, ${scenario.altitudes.initial}, information ${scenario.atisCode}`,
],
threshold: 0.55,
placeholder: 'Full initial call on new frequency',
width: 'xl' as const
key: 'should-call',
label: 'Do you transmit?',
expected: () => 'yes',
alternatives: () => ['yes', 'Yes', 'YES'],
placeholder: 'yes or no',
width: 'sm' as const
}
],
readback: [
@@ -3492,16 +3980,16 @@ const vatsimEssentialsLessons = [
{ type: 'field' as const, key: 'facility', width: 'lg' as const },
{ type: 'text' as const, text: ' ' },
{ type: 'field' as const, key: 'freq', width: 'md' as const },
{ type: 'text' as const, text: (scenario: Scenario) => `, ${scenario.radioCall} · ` },
{ type: 'field' as const, key: 'initial-call', width: 'xl' as const },
{ type: 'text' as const, text: (scenario: Scenario) => `, ${scenario.radioCall} · Transmit on new freq? ` },
{ type: 'field' as const, key: 'should-call', width: 'sm' as const },
],
defaultFrequency: 'GND',
phrase: (scenario: Scenario) =>
`${scenario.radioCall}, contact ${scenario.airport.city} Tower ${scenario.towerFreq}.`,
info: (scenario: Scenario) => [
'"Contact" means: switch AND call in with your position + ATIS.',
`Station: ${scenario.airport.city} Tower on ${scenario.towerFreq}`,
`Your altitude: ${scenario.altitudes.initial} ft, ATIS: ${scenario.atisCode}`,
info: () => [
'"Contact" = switch AND call in. "Monitor" = switch and LISTEN ONLY.',
'ATC said "contact" → you MUST make an initial call after switching.',
'If ATC had said "monitor" → just switch and listen, do NOT transmit.',
],
generate: createBaseScenario
},