Files
OpenSquawk/shared/data/learnModules.ts
2026-02-27 13:03:04 +01:00

3350 lines
107 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { createBaseScenario, createScenarioSeries, digitsToWords, formatTemp, lettersToNato } from '~~/shared/learn/scenario'
import type { Lesson, ModuleDef, Scenario } from '~~/shared/learn/types'
function gradientArt(colors: string[]): string {
const stops = colors
.map((color, idx) => `<stop offset="${Math.round((idx / Math.max(colors.length - 1, 1)) * 100)}%" stop-color="${color}"/>`)
.join('')
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 240"><defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1">${stops}</linearGradient></defs><rect fill="url(#g)" width="400" height="240"/></svg>`
return `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`
}
function randInt(min: number, max: number): number {
return Math.floor(Math.random() * (max - min + 1)) + min
}
function sample<T>(values: readonly T[]): T {
return values[randInt(0, values.length - 1)]!
}
const identifierCharacters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
function randomIdentifier(length: number): string {
let value = ''
for (let i = 0; i < length; i++) {
value += identifierCharacters[randInt(0, identifierCharacters.length - 1)]
}
return value
}
function toPhoneticBlocks(value: string): string {
return value
.toUpperCase()
.split('')
.map(char => {
if (/[A-Z]/.test(char)) {
return lettersToNato(char)
}
if (/[0-9]/.test(char)) {
return digitsToWords(char)
}
return char
})
.join(' ')
.replace(/\s+/g, ' ')
.trim()
}
function createExtendedIdentifierScenario(length = 10): Scenario {
const scenario = createBaseScenario()
const identifier = randomIdentifier(length)
scenario.phoneticCode = identifier
scenario.phoneticCodeWords = toPhoneticBlocks(identifier)
return scenario
}
const taxiPrefixes = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'R', 'S', 'T', 'U', 'V', 'W', 'Y', 'Z']
const taxiNumbers = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '14', '15', '16']
function randomTaxiWaypoint(): string {
const prefix = sample(taxiPrefixes)
const number = sample(taxiNumbers)
return `${prefix}${number}`
}
function createComplexTaxiScenario(): Scenario {
const scenario = createBaseScenario()
const segments: string[] = []
const total = randInt(5, 8)
while (segments.length < total) {
const next = randomTaxiWaypoint()
if (segments[segments.length - 1] === next) continue
segments.push(next)
}
scenario.taxiRoute = segments.join(' ')
return scenario
}
const fundamentalsLessons: Lesson[] = [
{
id: 'icao-alphabet',
title: 'Decode ICAO Letters & Numbers',
desc: 'Turn phonetic blocks into identifiers',
keywords: ['Alphabet', 'Numbers', 'Basics'],
hints: [
'Convert the NATO letters and ATC numbers back into characters.',
'Write the identifier exactly as you would enter it into the FMS.'
],
fields: [
{
key: 'icao-code',
label: 'Identifier',
expected: scenario => scenario.phoneticCode,
alternatives: scenario => {
const spelled = scenario.phoneticCodeWords
const upper = scenario.phoneticCode.toUpperCase()
const lower = upper.toLowerCase()
const spacedUpper = upper.split('').join(' ')
const spacedLower = lower.split('').join(' ')
return Array.from(
new Set([
upper,
lower,
spelled,
spelled.toLowerCase(),
spacedUpper,
spacedLower
])
)
},
placeholder: 'Enter identifier',
width: 'lg'
}
],
readback: [
{ type: 'text', text: 'Identifier ' },
{ type: 'field', key: 'icao-code', width: 'lg' }
],
defaultFrequency: 'DEL',
phrase: scenario => scenario.phoneticCodeWords,
info: scenario => [
`Heard: ${scenario.phoneticCodeWords}`,
`Identifier: ${scenario.phoneticCode}`
],
generate: createBaseScenario
},
{
id: 'icao-marathon',
title: 'ICAO Marathon Drill',
desc: 'Copy extended identifiers with up to ten characters',
keywords: ['Alphabet', 'Numbers', 'Advanced'],
hints: [
'Scan for natural breaks every two to three characters to keep long identifiers manageable.',
'Say the full sequence back without pauses, including letters and digits exactly as transmitted.'
],
fields: [
{
key: 'icao-long-code',
label: 'Identifier',
expected: scenario => scenario.phoneticCode,
alternatives: scenario => {
const value = scenario.phoneticCode
const upper = value.toUpperCase()
const lower = upper.toLowerCase()
const spacedUpper = upper.split('').join(' ')
const spacedLower = lower.split('').join(' ')
const spelled = scenario.phoneticCodeWords
return Array.from(
new Set([upper, lower, spacedUpper, spacedLower, spelled, spelled.toLowerCase()])
)
},
placeholder: 'Enter long identifier',
width: 'xl'
}
],
readback: [
{ type: 'text', text: 'Identifier ' },
{ type: 'field', key: 'icao-long-code', width: 'xl' }
],
defaultFrequency: 'DEL',
phrase: scenario => scenario.phoneticCodeWords,
info: scenario => [
`Heard: ${scenario.phoneticCodeWords}`,
`Identifier: ${scenario.phoneticCode}`
],
generate: () => createExtendedIdentifierScenario(10)
},
{
id: 'radio-call',
title: 'Radio Call Sign',
desc: 'Say the spoken or ICAO call sign confidently',
keywords: ['Basics', 'Callsign'],
hints: [
'Use the airline telephony plus digits when talking to ATC.',
'Readbacks can use either the spoken or ICAO format.'
],
fields: [
{
key: 'radio-call',
label: 'Call sign',
expected: scenario => scenario.radioCall,
alternatives: scenario => {
const variants = new Set<string>()
const add = (value?: string) => {
if (!value) return
variants.add(value)
variants.add(value.toLowerCase())
}
add(scenario.radioCall)
add(`${scenario.airlineCall} ${scenario.flightNumber}`)
add(`${scenario.airlineCall} ${scenario.flightNumberWords}`)
add(scenario.callsign)
add(`${scenario.airlineCode} ${scenario.flightNumber}`)
add(`${scenario.callsignNato} ${scenario.flightNumberWords}`)
return Array.from(variants)
},
width: 'xl'
}
],
readback: [
{ type: 'text', text: 'Call sign ' },
{ type: 'field', key: 'radio-call', width: 'xl' }
],
defaultFrequency: 'DEL',
phrase: scenario => `${scenario.radioCall}`,
info: scenario => [
`Spoken: ${scenario.radioCall}`,
`ICAO: ${scenario.callsign}`,
`Phonetic: ${scenario.callsignNato} ${scenario.flightNumberWords}`
],
generate: createBaseScenario
},
{
id: 'atis',
title: 'Understand the ATIS',
desc: 'Extract the identifier and key data from the ATIS',
keywords: ['ATIS', 'Weather'],
hints: [
'Remember the ATIS identifier as its NATO word (e.g. Information Yankee).',
'Order: runway wind visibility temperature dew point QNH; copy the numbers as digits.'
],
fields: [
{
key: 'atis-code',
label: 'ATIS',
expected: scenario => scenario.atisCode,
alternatives: scenario => [
scenario.atisCode,
scenario.atisCode.toLowerCase(),
scenario.atisCodeWord,
scenario.atisCodeWord.toLowerCase(),
`Information ${scenario.atisCode}`,
`information ${scenario.atisCode.toLowerCase()}`,
`Information ${scenario.atisCodeWord}`,
`information ${scenario.atisCodeWord.toLowerCase()}`
],
placeholder: 'Enter ATIS letter',
width: 'xs',
threshold: 0.9
},
{
key: 'atis-runway',
label: 'Runway',
expected: scenario => scenario.runway,
alternatives: scenario => [scenario.runway.replace(/^0/, ''), scenario.runwayWords],
width: 'sm'
},
{
key: 'atis-wind',
label: 'Wind',
expected: scenario => scenario.wind,
alternatives: scenario => [
scenario.wind,
`${scenario.wind}KT`,
scenario.windWords
],
width: 'md'
},
{
key: 'atis-visibility',
label: 'Visibility',
expected: scenario => scenario.visibility,
alternatives: scenario => {
const values = new Set<string>()
const add = (value?: string) => {
if (!value) return
values.add(value)
values.add(value.toLowerCase())
}
add(scenario.visibility)
add(scenario.atisSummary.visibility)
add(scenario.visibilityWords)
const numeric = Number(scenario.visibility)
if (!Number.isNaN(numeric)) {
if (numeric >= 1000) {
const precise = (numeric / 1000).toFixed(1).replace(/\.0$/, '')
add(precise)
add(`${precise}km`)
add(`${precise} km`)
}
if (numeric >= 1000) {
const rounded = Math.round(numeric / 1000)
add(rounded.toString())
add(`${rounded}km`)
add(`${rounded} km`)
}
}
if (scenario.visibility === '9999') {
add('10')
add('10km')
add('10 km')
}
return Array.from(values)
},
width: 'sm'
},
{
key: 'atis-temp',
label: 'Temperature',
expected: scenario => scenario.temperature.toString(),
alternatives: scenario => {
const value = scenario.temperature
const base = value.toString()
const plus = value > 0 ? `+${base}` : base
const c = `${base}°C`
const cSpaced = `${base} °C`
const shortC = value > 0 ? `${plus}C` : `${base}C`
const options = new Set<string>([
base,
plus,
c,
cSpaced,
shortC,
scenario.temperatureWords,
formatTemp(value)
])
options.add(`${plus}°C`)
options.add(`${plus} °C`)
options.add(scenario.temperatureWords.toLowerCase())
return Array.from(options)
},
width: 'sm'
},
{
key: 'atis-dew',
label: 'Dew point',
expected: scenario => scenario.dewpoint.toString(),
alternatives: scenario => {
const value = scenario.dewpoint
const base = value.toString()
const plus = value > 0 ? `+${base}` : base
const c = `${base}°C`
const cSpaced = `${base} °C`
const shortC = value > 0 ? `${plus}C` : `${base}C`
const options = new Set<string>([
base,
plus,
c,
cSpaced,
shortC,
scenario.dewpointWords,
formatTemp(value)
])
options.add(`${plus}°C`)
options.add(`${plus} °C`)
options.add(scenario.dewpointWords.toLowerCase())
return Array.from(options)
},
width: 'sm'
},
{
key: 'atis-qnh',
label: 'QNH',
expected: scenario => scenario.qnh.toString(),
alternatives: scenario => [`QNH ${scenario.qnh}`, scenario.qnhWords],
width: 'sm'
}
],
readback: [
{ type: 'text', text: 'Information ' },
{ type: 'field', key: 'atis-code', width: 'xs' },
{ type: 'text', text: ', runway ' },
{ type: 'field', key: 'atis-runway', width: 'sm' },
{ type: 'text', text: ', wind ' },
{ type: 'field', key: 'atis-wind', width: 'md' },
{ type: 'text', text: ', visibility ' },
{ type: 'field', key: 'atis-visibility', width: 'sm' },
{ type: 'text', text: ', temperature ' },
{ type: 'field', key: 'atis-temp', width: 'sm' },
{ type: 'text', text: ', dew point ' },
{ type: 'field', key: 'atis-dew', width: 'sm' },
{ type: 'text', text: ', QNH ' },
{ type: 'field', key: 'atis-qnh', width: 'sm' }
],
defaultFrequency: 'ATIS',
phrase: scenario => scenario.atisText,
info: () => [
'Note the identifier (NATO word), runway, wind, visibility, temperature, dew point, and QNH.',
'Temperatures and dew point can be recorded as plain numbers; visibility may be metres or kilometres.'
],
generate: createBaseScenario
},
{
id: 'metar',
title: 'Decode the METAR',
desc: 'Extract the raw METAR values',
keywords: ['METAR', 'Weather'],
hints: [
'Read the METAR in blocks: wind visibility clouds temperature QNH.',
'The temperature block looks like 18/10; negative values start with M.'
],
fields: [
{
key: 'metar-wind',
label: 'Wind',
expected: scenario => scenario.metarSegments.wind,
alternatives: scenario => [
scenario.metarSegments.wind,
`${scenario.wind.replace('/', '')}KT`,
scenario.wind
],
width: 'md'
},
{
key: 'metar-vis',
label: 'Visibility',
expected: scenario => scenario.metarSegments.visibility,
alternatives: scenario => [scenario.visibility],
placeholder: 'Enter visibility value',
width: 'sm',
inputmode: 'numeric'
},
{
key: 'metar-temp',
label: 'Temp/Dew',
expected: scenario => scenario.metarSegments.temp,
alternatives: scenario => [
`${formatTemp(scenario.temperature)}/${formatTemp(scenario.dewpoint)}`
],
width: 'md'
},
{
key: 'metar-qnh',
label: 'QNH',
expected: scenario => scenario.metarSegments.qnh,
alternatives: scenario => [`Q${scenario.qnh}`, scenario.qnh.toString()],
width: 'sm'
}
],
readback: [
{ type: 'text', text: 'Wind ' },
{ type: 'field', key: 'metar-wind', width: 'md' },
{ type: 'text', text: ', visibility ' },
{ type: 'field', key: 'metar-vis', width: 'sm' },
{ type: 'text', text: ', temperature ' },
{ type: 'field', key: 'metar-temp', width: 'md' },
{ type: 'text', text: ', QNH ' },
{ type: 'field', key: 'metar-qnh', width: 'sm' }
],
defaultFrequency: 'ATIS',
phrase: scenario => scenario.metar,
info: scenario => [
`METAR: ${scenario.metar}`,
`Interpretation: Wind ${scenario.metarSegments.wind}, visibility ${scenario.visibilityWords}, temperature ${scenario.temperature}°C, QNH ${scenario.qnh}`
],
generate: createBaseScenario
},
{
id: 'radio-check',
title: 'Radio Check',
desc: 'Acknowledge a requested radio check',
keywords: ['Ground', 'Comms'],
hints: [
'Reply with "Readability" plus the number you hear.',
'Finish with your call sign; no need to repeat the frequency.'
],
fields: [
{
key: 'rc-callsign',
label: 'Callsign',
expected: scenario => scenario.radioCall,
alternatives: scenario => {
const variants = new Set<string>()
const add = (value?: string) => {
if (!value) return
variants.add(value)
variants.add(value.toLowerCase())
}
add(scenario.radioCall)
add(`${scenario.airlineCall} ${scenario.flightNumber}`)
add(`${scenario.airlineCall} ${scenario.flightNumberWords}`)
add(scenario.callsign)
add(`${scenario.airlineCode} ${scenario.flightNumber}`)
add(`${scenario.callsignNato} ${scenario.flightNumberWords}`)
return Array.from(variants)
},
placeholder: 'Enter call sign',
width: 'lg'
},
{
key: 'rc-readability',
label: 'Readability',
expected: scenario => scenario.readabilityWord,
alternatives: scenario => [
scenario.readability.toString(),
scenario.readabilityWord,
scenario.readabilityWord.toLowerCase()
],
placeholder: 'Enter readability (1-5)',
width: 'sm'
}
],
readback: [
{ type: 'text', text: 'Readability ' },
{ type: 'field', key: 'rc-readability', width: 'sm' },
{ type: 'text', text: ', ' },
{ type: 'field', key: 'rc-callsign', width: 'lg' }
],
defaultFrequency: 'GND',
phrase: scenario => `${scenario.radioCall}, ${scenario.airport.name} Ground, say radio check on ${scenario.groundFreq}.`,
info: scenario => [
`Request: ${scenario.airport.name} Ground on ${scenario.groundFreq} (${scenario.frequencyWords.GND})`,
`Respond: Readability ${scenario.readability} (${scenario.readabilityWord}) with your call sign`
],
generate: createBaseScenario
},
{
id: 'frequency-change',
title: 'Frequency Change Readback',
desc: 'Confirm a handoff instruction',
keywords: ['Comms', 'Frequency'],
hints: [
'Repeat the station and frequency before your call sign.',
'State the frequency as digits or with "decimal".'
],
fields: [
{
key: 'freq-contact-frequency',
label: 'Frequency',
expected: scenario => scenario.handoff.frequency,
threshold: 0.99,
alternatives: scenario => {
const freq = scenario.handoff.frequency
const trimmed = freq.replace(/\.?0+$/, '')
const comma = freq.replace('.', ',')
const trimmedComma = trimmed.replace('.', ',')
const spaced = freq.replace('.', ' ')
const trimmedSpaced = trimmed.replace('.', ' ')
const words = scenario.handoff.frequencyWords
const pointWords = words.replace('decimal', 'point')
const compact = freq.replace('.', '')
const trimmedCompact = trimmed.replace('.', '')
const values = new Set<string>([
freq,
trimmed,
comma,
trimmedComma,
spaced,
trimmedSpaced,
words,
words.toLowerCase(),
pointWords,
pointWords.toLowerCase(),
compact,
trimmedCompact
])
return Array.from(values)
},
placeholder: 'Enter contact frequency',
width: 'md'
}
],
readback: [
{ type: 'text', text: scenario => `Contact ${scenario.handoff.facility} on ` },
{ type: 'field', key: 'freq-contact-frequency', width: 'md' },
{ type: 'text', text: scenario => `, ${scenario.radioCall}` }
],
defaultFrequency: 'TWR',
phrase: scenario => `${scenario.radioCall}, contact ${scenario.handoff.facility} on ${scenario.handoff.frequencyWords}.`,
info: scenario => [
`Station: ${scenario.handoff.facility}`,
`Frequency: ${scenario.handoff.frequency} (${scenario.handoff.frequencyWords})`
],
generate: createBaseScenario
}
]
const readbackLessons: Lesson[] = [
{
id: 'clearance-readback',
title: 'Clearance Readback',
desc: 'Read back the clearance in full',
keywords: ['Delivery', 'Readback'],
hints: [
'Remember the order: destination SID runway altitude squawk.',
'Speak altitude and squawk digits clearly.'
],
fields: [
{
key: 'clr-dest',
label: 'Destination',
expected: scenario => scenario.destination.city,
alternatives: scenario => [scenario.destination.icao, scenario.destination.name],
width: 'md'
},
{
key: 'clr-sid',
label: 'SID',
expected: scenario => scenario.sid,
width: 'lg'
},
{
key: 'clr-runway',
label: 'Runway',
expected: scenario => scenario.runway,
alternatives: scenario => [scenario.runway.replace(/^0/, ''), scenario.runwayWords],
width: 'sm'
},
{
key: 'clr-alt',
label: 'Initial Altitude',
expected: scenario => scenario.altitudes.initialWords,
alternatives: scenario => [scenario.altitudes.initial.toString(), `${scenario.altitudes.initial} feet`],
width: 'md'
},
{
key: 'clr-squawk',
label: 'Squawk',
expected: scenario => scenario.squawkWords,
alternatives: scenario => [scenario.squawk, scenario.squawk.split('').join(' ')],
width: 'md'
}
],
readback: [
{ type: 'text', text: scenario => `${scenario.radioCall} cleared ` },
{ type: 'field', key: 'clr-dest', width: 'md' },
{ type: 'text', text: ' via ' },
{ type: 'field', key: 'clr-sid', width: 'lg' },
{ type: 'text', text: ', runway ' },
{ type: 'field', key: 'clr-runway', width: 'sm' },
{ type: 'text', text: ', climb ' },
{ type: 'field', key: 'clr-alt', width: 'md' },
{ type: 'text', text: ', squawk ' },
{ type: 'field', key: 'clr-squawk', width: 'md' }
],
defaultFrequency: 'DEL',
phrase: scenario => `${scenario.radioCall}, cleared to ${scenario.destination.city} via ${scenario.sid}, runway ${scenario.runway}, climb ${scenario.altitudes.initial} feet, squawk ${scenario.squawk}.`,
info: scenario => [
`SID: ${scenario.sid} (${scenario.transition})`,
`Initial Altitude: ${scenario.altitudes.initial} ft (${scenario.altitudes.initialWords})`,
`Squawk: ${scenario.squawk}`
],
generate: createBaseScenario
},
{
id: 'clearance-amendment',
title: 'Amended Clearance',
desc: 'Confirm the updated SID, transition and runway',
keywords: ['Delivery', 'Amendment'],
hints: [
'Start with "Amended clearance" followed by SID and transition.',
'Include the runway and end with your call sign.'
],
fields: [
{
key: 'clr-amend-sid',
label: 'SID',
expected: scenario => scenario.sid,
width: 'lg'
},
{
key: 'clr-amend-transition',
label: 'Transition',
expected: scenario => scenario.transition,
width: 'md'
},
{
key: 'clr-amend-runway',
label: 'Runway',
expected: scenario => scenario.runway,
alternatives: scenario => [scenario.runway.replace(/^0/, ''), scenario.runwayWords],
width: 'sm'
},
{
key: 'clr-amend-callsign',
label: 'Callsign',
expected: scenario => scenario.radioCall,
alternatives: scenario => [
scenario.radioCall,
`${scenario.airlineCall} ${scenario.flightNumber}`,
scenario.callsign
],
width: 'lg'
}
],
readback: [
{ type: 'text', text: 'Amended clearance: ' },
{ type: 'field', key: 'clr-amend-sid', width: 'lg' },
{ type: 'text', text: ' ' },
{ type: 'field', key: 'clr-amend-transition', width: 'md' },
{ type: 'text', text: ' departure, runway ' },
{ type: 'field', key: 'clr-amend-runway', width: 'sm' },
{ type: 'text', text: ', ' },
{ type: 'field', key: 'clr-amend-callsign', width: 'lg' }
],
defaultFrequency: 'DEL',
phrase: scenario => `${scenario.radioCall}, amended clearance: ${scenario.sid} ${scenario.transition} departure, runway ${scenario.runway}.`,
info: scenario => [
`SID: ${scenario.sid}`,
`Transition: ${scenario.transition}`,
`Runway: ${scenario.runway}`
],
generate: createBaseScenario
},
{
id: 'pushback',
title: 'Pushback Clearance',
desc: 'Acknowledge the pushback approval',
keywords: ['Ground', 'Pushback'],
hints: [
'Repeat the direction you must face and the QNH.',
'Keep the call sign at the end.'
],
fields: [
{
key: 'push-runway',
label: 'Runway',
expected: scenario => scenario.runway,
alternatives: scenario => [scenario.runway.replace(/^0/, ''), scenario.runwayWords],
width: 'sm'
},
{
key: 'push-qnh',
label: 'QNH',
expected: scenario => scenario.qnh.toString(),
alternatives: scenario => [`QNH ${scenario.qnh}`, scenario.qnhWords],
width: 'sm'
},
{
key: 'push-callsign',
label: 'Callsign',
expected: scenario => scenario.radioCall,
alternatives: scenario => [
scenario.radioCall,
`${scenario.airlineCall} ${scenario.flightNumber}`,
scenario.callsign
],
width: 'lg'
}
],
readback: [
{ type: 'text', text: 'Push and start approved, facing runway ' },
{ type: 'field', key: 'push-runway', width: 'sm' },
{ type: 'text', text: ', QNH ' },
{ type: 'field', key: 'push-qnh', width: 'sm' },
{ type: 'text', text: ', ' },
{ type: 'field', key: 'push-callsign', width: 'lg' }
],
defaultFrequency: 'GND',
phrase: scenario => `${scenario.radioCall}, push and start approved, facing runway ${scenario.runway}. QNH ${scenario.qnh}.`,
info: scenario => [
`Stand: ${scenario.stand}`,
`Ground frequency: ${scenario.groundFreq} (${scenario.frequencyWords.GND})`
],
generate: createBaseScenario
},
{
id: 'pushback-delay',
title: 'Pushback Delay Acknowledgement',
desc: 'Confirm delayed push and the expected taxi route',
keywords: ['Ground', 'Pushback', 'Exception'],
hints: [
'Read back the delay before the taxi route.',
'Close with the call sign.'
],
fields: [
{
key: 'push-delay',
label: 'Delay',
expected: scenario => scenario.pushDelayWords,
alternatives: scenario => [
scenario.pushDelayWords,
`${scenario.pushDelayMinutes} minutes`,
scenario.pushDelayMinutes.toString()
],
width: 'md'
},
{
key: 'push-route',
label: 'Taxi Route',
expected: scenario => scenario.taxiRoute,
alternatives: scenario => [scenario.taxiRoute, `via ${scenario.taxiRoute}`],
width: 'lg'
},
{
key: 'push-delay-callsign',
label: 'Callsign',
expected: scenario => scenario.radioCall,
alternatives: scenario => [
scenario.radioCall,
`${scenario.airlineCall} ${scenario.flightNumber}`,
scenario.callsign
],
width: 'lg'
}
],
readback: [
{ type: 'text', text: 'Push and start approved in ' },
{ type: 'field', key: 'push-delay', width: 'md' },
{ type: 'text', text: ' minutes, expect taxi via ' },
{ type: 'field', key: 'push-route', width: 'lg' },
{ type: 'text', text: ', ' },
{ type: 'field', key: 'push-delay-callsign', width: 'lg' }
],
defaultFrequency: 'GND',
phrase: scenario => `${scenario.radioCall}, push and start approved in ${scenario.pushDelayMinutes} minutes, expect taxi via ${scenario.taxiRoute}.`,
info: scenario => [
`Delay: ${scenario.pushDelayMinutes} minutes (${scenario.pushDelayWords})`,
`Taxi route: ${scenario.taxiRoute}`
],
generate: createBaseScenario
},
{
id: 'taxi',
title: 'Taxi Readback',
desc: 'Read back the taxi clearance with hold short',
keywords: ['Ground', 'Taxi'],
hints: [
'Repeat the route exactly as received, including the hold short.',
'Finish with the call sign.'
],
fields: [
{
key: 'taxi-runway',
label: 'Runway',
expected: scenario => scenario.runway,
alternatives: scenario => [scenario.runway.replace(/^0/, ''), scenario.runwayWords],
width: 'sm'
},
{
key: 'taxi-route',
label: 'Route',
expected: scenario => scenario.taxiRoute,
alternatives: scenario => [scenario.taxiRoute, `via ${scenario.taxiRoute}`],
width: 'lg'
},
{
key: 'taxi-hold',
label: 'Hold Short',
expected: scenario => scenario.runway,
alternatives: scenario => [scenario.runway.replace(/^0/, ''), scenario.runwayWords],
width: 'sm'
},
{
key: 'taxi-callsign',
label: 'Callsign',
expected: scenario => scenario.radioCall,
alternatives: scenario => [
scenario.radioCall,
`${scenario.airlineCall} ${scenario.flightNumber}`,
scenario.callsign
],
width: 'lg'
}
],
readback: [
{ type: 'text', text: 'Taxi to runway ' },
{ type: 'field', key: 'taxi-runway', width: 'sm' },
{ type: 'text', text: ' via ' },
{ type: 'field', key: 'taxi-route', width: 'lg' },
{ type: 'text', text: ', holding short runway ' },
{ type: 'field', key: 'taxi-hold', width: 'sm' },
{ type: 'text', text: ', ' },
{ type: 'field', key: 'taxi-callsign', width: 'lg' }
],
defaultFrequency: 'GND',
phrase: scenario => `${scenario.radioCall}, taxi to runway ${scenario.runway} via ${scenario.taxiRoute}, hold short runway ${scenario.runway}.`,
info: scenario => [
`Taxi route: ${scenario.taxiRoute}`,
`Hold short: ${scenario.runway}`
],
generate: createBaseScenario
},
{
id: 'taxi-complex',
title: 'Complex Taxi Route',
desc: 'Handle long taxi instructions with multiple waypoints',
keywords: ['Ground', 'Taxi', 'Advanced'],
hints: [
'Copy the entire route, including every waypoint in the order delivered.',
'Group waypoints into small chunks so you can read the route back smoothly.'
],
fields: [
{
key: 'taxi-complex-runway',
label: 'Runway',
expected: scenario => scenario.runway,
alternatives: scenario => [scenario.runway.replace(/^0/, ''), scenario.runwayWords],
width: 'sm'
},
{
key: 'taxi-complex-route',
label: 'Route',
expected: scenario => scenario.taxiRoute,
alternatives: scenario => [scenario.taxiRoute, `via ${scenario.taxiRoute}`],
width: 'xl'
},
{
key: 'taxi-complex-hold',
label: 'Hold Short',
expected: scenario => scenario.runway,
alternatives: scenario => [scenario.runway.replace(/^0/, ''), scenario.runwayWords],
width: 'sm'
},
{
key: 'taxi-complex-callsign',
label: 'Callsign',
expected: scenario => scenario.radioCall,
alternatives: scenario => [
scenario.radioCall,
`${scenario.airlineCall} ${scenario.flightNumber}`,
scenario.callsign
],
width: 'lg'
}
],
readback: [
{ type: 'text', text: 'Taxi to runway ' },
{ type: 'field', key: 'taxi-complex-runway', width: 'sm' },
{ type: 'text', text: ' via ' },
{ type: 'field', key: 'taxi-complex-route', width: 'xl' },
{ type: 'text', text: ', holding short runway ' },
{ type: 'field', key: 'taxi-complex-hold', width: 'sm' },
{ type: 'text', text: ', ' },
{ type: 'field', key: 'taxi-complex-callsign', width: 'lg' }
],
defaultFrequency: 'GND',
phrase: scenario => `${scenario.radioCall}, taxi to runway ${scenario.runway} via ${scenario.taxiRoute}, hold short runway ${scenario.runway}.`,
info: scenario => [
`Taxi route: ${scenario.taxiRoute}`,
`Hold short: ${scenario.runway}`
],
generate: createComplexTaxiScenario
},
{
id: 'lineup',
title: 'Line-up Clearance',
desc: 'Acknowledge line up and wait',
keywords: ['Tower', 'Line Up'],
hints: [
'Repeat the runway, then say "line up and wait".',
'Place the call sign at the end.'
],
fields: [
{
key: 'lineup-runway',
label: 'Runway',
expected: scenario => scenario.runway,
alternatives: scenario => [scenario.runway.replace(/^0/, ''), scenario.runwayWords],
width: 'sm'
},
{
key: 'lineup-callsign',
label: 'Callsign',
expected: scenario => scenario.radioCall,
alternatives: scenario => [
scenario.radioCall,
`${scenario.airlineCall} ${scenario.flightNumber}`,
scenario.callsign
],
width: 'lg'
}
],
readback: [
{ type: 'text', text: 'Runway ' },
{ type: 'field', key: 'lineup-runway', width: 'sm' },
{ type: 'text', text: ', line up and wait, ' },
{ type: 'field', key: 'lineup-callsign', width: 'lg' }
],
defaultFrequency: 'TWR',
phrase: scenario => `${scenario.radioCall}, line up and wait runway ${scenario.runway}.`,
info: scenario => [
`Tower: ${scenario.towerFreq} (${scenario.frequencyWords.TWR})`,
`Line-up runway: ${scenario.runway}`
],
generate: createBaseScenario
},
{
id: 'takeoff',
title: 'Takeoff Clearance',
desc: 'Acknowledge the takeoff clearance',
keywords: ['Tower', 'Departure'],
hints: [
'Order: wind runway cleared for takeoff call sign.',
'Write the wind as direction/speed, e.g. 030/12.'
],
fields: [
{
key: 'tko-wind',
label: 'Wind',
expected: scenario => scenario.wind,
alternatives: scenario => [
scenario.wind,
`${scenario.wind}KT`,
scenario.windWords
],
width: 'md'
},
{
key: 'tko-runway',
label: 'Runway',
expected: scenario => scenario.runway,
alternatives: scenario => [scenario.runway.replace(/^0/, ''), scenario.runwayWords],
width: 'sm'
},
{
key: 'tko-callsign',
label: 'Callsign',
expected: scenario => scenario.radioCall,
alternatives: scenario => [
scenario.radioCall,
`${scenario.airlineCall} ${scenario.flightNumber}`,
scenario.callsign
],
width: 'lg'
}
],
readback: [
{ type: 'text', text: 'Wind ' },
{ type: 'field', key: 'tko-wind', width: 'md' },
{ type: 'text', text: ', runway ' },
{ type: 'field', key: 'tko-runway', width: 'sm' },
{ type: 'text', text: ', cleared for takeoff, ' },
{ type: 'field', key: 'tko-callsign', width: 'lg' }
],
defaultFrequency: 'TWR',
phrase: scenario => `${scenario.radioCall}, wind ${scenario.windWords}, runway ${scenario.runway}, cleared for takeoff.`,
info: scenario => [
`Wind: ${scenario.wind} (${scenario.windWords})`,
`Runway: ${scenario.runway}`
],
generate: createBaseScenario
},
{
id: 'climb-instruction',
title: 'Climb & Direct',
desc: 'Read back the climb instruction in full',
keywords: ['Departure', 'Climb'],
hints: [
'Start with "Climb", then the altitude and any direct clearance.',
'Repeat the call sign at the end.'
],
fields: [
{
key: 'climb-alt',
label: 'Altitude',
expected: scenario => scenario.altitudes.climbWords,
alternatives: scenario => [
scenario.altitudes.climbWords,
scenario.altitudes.climb.toString(),
`${scenario.altitudes.climb} feet`
],
width: 'md'
},
{
key: 'climb-direct',
label: 'Direct',
expected: scenario => scenario.transition,
width: 'md'
},
{
key: 'climb-callsign',
label: 'Callsign',
expected: scenario => scenario.radioCall,
alternatives: scenario => [
scenario.radioCall,
`${scenario.airlineCall} ${scenario.flightNumber}`,
scenario.callsign
],
width: 'lg'
}
],
readback: [
{ type: 'text', text: 'Climb ' },
{ type: 'field', key: 'climb-alt', width: 'md' },
{ type: 'text', text: ', direct ' },
{ type: 'field', key: 'climb-direct', width: 'md' },
{ type: 'text', text: ', ' },
{ type: 'field', key: 'climb-callsign', width: 'lg' }
],
defaultFrequency: 'DEP',
phrase: scenario => `${scenario.radioCall}, climb ${scenario.altitudes.climb} feet, proceed direct ${scenario.transition}.`,
info: scenario => [
`Climb: ${scenario.altitudes.climb} ft (${scenario.altitudes.climbWords})`,
`Direct to: ${scenario.transition}`
],
generate: createBaseScenario
},
{
id: 'climb-alt-route',
title: 'Climb & Reroute',
desc: 'Confirm a new altitude and routing',
keywords: ['Center', 'Climb'],
hints: [
'Mention the altitude first, then the direct routing.',
'End the readback with your call sign.'
],
fields: [
{
key: 'climb-alt-route-alt',
label: 'Altitude',
expected: scenario => scenario.altitudes.climbWords,
alternatives: scenario => [
scenario.altitudes.climbWords,
scenario.altitudes.climb.toString(),
`${scenario.altitudes.climb} feet`
],
width: 'md'
},
{
key: 'climb-alt-route-direct',
label: 'Direct',
expected: scenario => scenario.transition,
width: 'md'
},
{
key: 'climb-alt-route-callsign',
label: 'Callsign',
expected: scenario => scenario.radioCall,
alternatives: scenario => [
scenario.radioCall,
`${scenario.airlineCall} ${scenario.flightNumber}`,
scenario.callsign
],
width: 'lg'
}
],
readback: [
{ type: 'text', text: 'Climb ' },
{ type: 'field', key: 'climb-alt-route-alt', width: 'md' },
{ type: 'text', text: ', proceed direct ' },
{ type: 'field', key: 'climb-alt-route-direct', width: 'md' },
{ type: 'text', text: ', ' },
{ type: 'field', key: 'climb-alt-route-callsign', width: 'lg' }
],
defaultFrequency: 'CTR',
phrase: scenario => `${scenario.radioCall}, climb to ${scenario.altitudes.climb} feet, proceed direct ${scenario.transition}.`,
info: scenario => [
`Requested altitude: ${scenario.altitudes.climb} ft`,
`New routing: direct ${scenario.transition}`
],
generate: createBaseScenario
},
{
id: 'departure-handoff',
title: 'Departure Handoff',
desc: 'Switch to departure',
keywords: ['Departure', 'Handoff'],
hints: [
'Repeat the frequency exactly, either as digits or spoken form.',
'Append the call sign at the end.'
],
fields: [
{
key: 'dep-freq',
label: 'Frequency',
expected: scenario => scenario.departureFreq,
alternatives: scenario => [
scenario.departureFreq,
scenario.departureFreq.replace('.', ''),
scenario.frequencyWords.DEP
],
width: 'md'
},
{
key: 'dep-callsign',
label: 'Callsign',
expected: scenario => scenario.radioCall,
alternatives: scenario => [
scenario.radioCall,
`${scenario.airlineCall} ${scenario.flightNumber}`,
scenario.callsign
],
width: 'lg'
}
],
readback: [
{ type: 'text', text: 'Contact departure ' },
{ type: 'field', key: 'dep-freq', width: 'md' },
{ type: 'text', text: ', ' },
{ type: 'field', key: 'dep-callsign', width: 'lg' }
],
defaultFrequency: 'DEP',
phrase: scenario => `${scenario.radioCall}, contact departure ${scenario.departureFreq}.`,
info: scenario => [
`Departure: ${scenario.departureFreq} (${scenario.frequencyWords.DEP})`,
`Tower handoff after departure.`
],
generate: createBaseScenario
},
{
id: 'center-handoff',
title: 'Center Handoff',
desc: 'Confirm the handoff to center',
keywords: ['Enroute', 'Handoff'],
hints: [
'Repeat the frequency and include your call sign.',
'You can say the digits or the spoken version.'
],
fields: [
{
key: 'center-freq',
label: 'Frequency',
expected: scenario => scenario.centerFreq,
alternatives: scenario => [
scenario.centerFreq,
scenario.centerFreq.replace('.', ''),
scenario.frequencyWords.CTR
],
width: 'md'
},
{
key: 'center-callsign',
label: 'Callsign',
expected: scenario => scenario.radioCall,
alternatives: scenario => [
scenario.radioCall,
`${scenario.airlineCall} ${scenario.flightNumber}`,
scenario.callsign
],
width: 'lg'
}
],
readback: [
{ type: 'text', text: 'Contact center ' },
{ type: 'field', key: 'center-freq', width: 'md' },
{ type: 'text', text: ', ' },
{ type: 'field', key: 'center-callsign', width: 'lg' }
],
defaultFrequency: 'CTR',
phrase: scenario => `${scenario.radioCall}, contact Center ${scenario.centerFreq}.`,
info: scenario => [
`Center: ${scenario.centerFreq} (${scenario.frequencyWords.CTR})`,
`Expect to report level or route as required.`
],
generate: createBaseScenario
},
{
id: 'descent-clearance',
title: 'Descent Clearance',
desc: 'Read back the STAR, transition and QNH',
keywords: ['Approach', 'Descent', 'Readback'],
hints: [
'State the STAR and transition in order.',
'Include the QNH and end with your call sign.'
],
fields: [
{
key: 'descent-star',
label: 'STAR',
expected: scenario => scenario.arrivalStar,
width: 'lg'
},
{
key: 'descent-transition',
label: 'Transition',
expected: scenario => scenario.arrivalTransition,
width: 'md'
},
{
key: 'descent-qnh',
label: 'QNH',
expected: scenario => scenario.arrivalQnh.toString(),
alternatives: scenario => [
scenario.arrivalQnh.toString(),
`QNH ${scenario.arrivalQnh}`,
scenario.arrivalQnhWords
],
width: 'sm'
},
{
key: 'descent-callsign',
label: 'Callsign',
expected: scenario => scenario.radioCall,
alternatives: scenario => [
scenario.radioCall,
`${scenario.airlineCall} ${scenario.flightNumber}`,
scenario.callsign
],
width: 'lg'
}
],
readback: [
{ type: 'text', text: 'Descend via ' },
{ type: 'field', key: 'descent-star', width: 'lg' },
{ type: 'text', text: ' ' },
{ type: 'field', key: 'descent-transition', width: 'md' },
{ type: 'text', text: ', QNH ' },
{ type: 'field', key: 'descent-qnh', width: 'sm' },
{ type: 'text', text: ', ' },
{ type: 'field', key: 'descent-callsign', width: 'lg' }
],
defaultFrequency: 'APP',
phrase: scenario => `${scenario.radioCall}, descend via ${scenario.arrivalStar} ${scenario.arrivalTransition}, QNH ${scenario.arrivalQnh}.`,
info: scenario => [
`STAR: ${scenario.arrivalStar}`,
`Transition: ${scenario.arrivalTransition}`,
`Arrival QNH: ${scenario.arrivalQnh} (${scenario.arrivalQnhWords})`
],
generate: createBaseScenario
},
{
id: 'approach-vector',
title: 'Approach Vector',
desc: 'Read back heading, altitude and speed',
keywords: ['Approach', 'Vector'],
hints: [
'Heading, then altitude, then speed restriction.',
'Use ATC number words for the speed and altitude.'
],
fields: [
{
key: 'vector-heading',
label: 'Heading',
expected: scenario => scenario.vectorHeading,
alternatives: scenario => [
scenario.vectorHeading,
scenario.vectorHeadingWords
],
width: 'md'
},
{
key: 'vector-alt',
label: 'Altitude',
expected: scenario => scenario.altitudes.initialWords,
alternatives: scenario => [
scenario.altitudes.initialWords,
scenario.altitudes.initial.toString(),
`${scenario.altitudes.initial} feet`
],
width: 'md'
},
{
key: 'vector-speed',
label: 'Speed',
expected: scenario => scenario.speedRestrictionWords,
alternatives: scenario => [
scenario.speedRestrictionWords,
`${scenario.speedRestriction} knots`,
scenario.speedRestriction.toString()
],
width: 'md'
},
{
key: 'vector-callsign',
label: 'Callsign',
expected: scenario => scenario.radioCall,
alternatives: scenario => [
scenario.radioCall,
`${scenario.airlineCall} ${scenario.flightNumber}`,
scenario.callsign
],
width: 'lg'
}
],
readback: [
{ type: 'text', text: 'Heading ' },
{ type: 'field', key: 'vector-heading', width: 'md' },
{ type: 'text', text: ', descend to ' },
{ type: 'field', key: 'vector-alt', width: 'md' },
{ type: 'text', text: ', reduce speed ' },
{ type: 'field', key: 'vector-speed', width: 'md' },
{ type: 'text', text: ', ' },
{ type: 'field', key: 'vector-callsign', width: 'lg' }
],
defaultFrequency: 'APP',
phrase: scenario => `${scenario.radioCall}, turn left heading ${scenario.vectorHeading}, descend to ${scenario.altitudes.initial} feet, reduce speed ${scenario.speedRestriction}.`,
info: scenario => [
`Heading: ${scenario.vectorHeading} (${scenario.vectorHeadingWords})`,
`Altitude: ${scenario.altitudes.initial} ft`,
`Speed restriction: ${scenario.speedRestriction} (${scenario.speedRestrictionWords})`
],
generate: createBaseScenario
},
{
id: 'approach-clearance',
title: 'Approach Clearance',
desc: 'Confirm the approach type and runway',
keywords: ['Approach', 'Readback'],
hints: [
'Say "Cleared" followed by the approach.',
'Repeat the runway and finish with your call sign.'
],
fields: [
{
key: 'app-type',
label: 'Approach',
expected: scenario => scenario.approach,
width: 'lg'
},
{
key: 'app-runway',
label: 'Runway',
expected: scenario => scenario.arrivalRunway,
alternatives: scenario => [
scenario.arrivalRunway.replace(/^0/, ''),
scenario.arrivalRunwayWords
],
width: 'sm'
},
{
key: 'app-callsign',
label: 'Callsign',
expected: scenario => scenario.radioCall,
alternatives: scenario => [
scenario.radioCall,
`${scenario.airlineCall} ${scenario.flightNumber}`,
scenario.callsign
],
width: 'lg'
}
],
readback: [
{ type: 'text', text: 'Cleared ' },
{ type: 'field', key: 'app-type', width: 'lg' },
{ type: 'text', text: ' approach runway ' },
{ type: 'field', key: 'app-runway', width: 'sm' },
{ type: 'text', text: ', ' },
{ type: 'field', key: 'app-callsign', width: 'lg' }
],
defaultFrequency: 'APP',
phrase: scenario => `${scenario.radioCall}, cleared ${scenario.approach} approach runway ${scenario.arrivalRunway}, report established.`,
info: scenario => [
`Approach: ${scenario.approach}`,
`Runway: ${scenario.arrivalRunway} (${scenario.arrivalRunwayWords})`
],
generate: createBaseScenario
},
{
id: 'approach-contact',
title: 'Approach Contact',
desc: 'Switch from center to approach',
keywords: ['Approach', 'Handoff'],
hints: [
'Repeat the frequency and add your call sign.',
'You can pronounce the frequency as numbers or using "decimal".'
],
fields: [
{
key: 'app-contact-freq',
label: 'Frequency',
expected: scenario => scenario.approachFreq,
alternatives: scenario => [
scenario.approachFreq,
scenario.approachFreq.replace('.', ''),
scenario.frequencyWords.APP
],
width: 'md'
},
{
key: 'app-contact-callsign',
label: 'Callsign',
expected: scenario => scenario.radioCall,
alternatives: scenario => [
scenario.radioCall,
`${scenario.airlineCall} ${scenario.flightNumber}`,
scenario.callsign
],
width: 'lg'
}
],
readback: [
{ type: 'text', text: 'Contact approach ' },
{ type: 'field', key: 'app-contact-freq', width: 'md' },
{ type: 'text', text: ', ' },
{ type: 'field', key: 'app-contact-callsign', width: 'lg' }
],
defaultFrequency: 'APP',
phrase: scenario => `${scenario.radioCall}, contact Approach ${scenario.approachFreq}.`,
info: scenario => [
`Approach: ${scenario.approachFreq} (${scenario.frequencyWords.APP})`
],
generate: createBaseScenario
},
{
id: 'tower-contact',
title: 'Tower Handoff',
desc: 'Confirm the switch to tower when number one',
keywords: ['Tower', 'Handoff'],
hints: [
'Repeat the frequency and mention "when number one".',
'End with the call sign.'
],
fields: [
{
key: 'tower-contact-freq',
label: 'Frequency',
expected: scenario => scenario.towerFreq,
alternatives: scenario => [
scenario.towerFreq,
scenario.towerFreq.replace('.', ''),
scenario.frequencyWords.TWR
],
width: 'md'
},
{
key: 'tower-contact-callsign',
label: 'Callsign',
expected: scenario => scenario.radioCall,
alternatives: scenario => [
scenario.radioCall,
`${scenario.airlineCall} ${scenario.flightNumber}`,
scenario.callsign
],
width: 'lg'
}
],
readback: [
{ type: 'text', text: 'Contact tower ' },
{ type: 'field', key: 'tower-contact-freq', width: 'md' },
{ type: 'text', text: ' when number one, ' },
{ type: 'field', key: 'tower-contact-callsign', width: 'lg' }
],
defaultFrequency: 'GND',
phrase: scenario => `${scenario.radioCall}, contact Tower ${scenario.towerFreq} when number one.`,
info: scenario => [
`Tower frequency: ${scenario.towerFreq} (${scenario.frequencyWords.TWR})`
],
generate: createBaseScenario
},
{
id: 'landing-clearance',
title: 'Landing Clearance',
desc: 'Read back the landing clearance',
keywords: ['Tower', 'Landing', 'Readback'],
hints: [
'Order: wind runway cleared to land call sign.',
'Write the wind as direction/speed, e.g. 260/08.'
],
fields: [
{
key: 'landing-wind',
label: 'Wind',
expected: scenario => scenario.arrivalWind,
alternatives: scenario => [
scenario.arrivalWind,
`${scenario.arrivalWind}KT`,
scenario.arrivalWindWords
],
width: 'md'
},
{
key: 'landing-runway',
label: 'Runway',
expected: scenario => scenario.arrivalRunway,
alternatives: scenario => [
scenario.arrivalRunway.replace(/^0/, ''),
scenario.arrivalRunwayWords
],
width: 'sm'
},
{
key: 'landing-callsign',
label: 'Callsign',
expected: scenario => scenario.radioCall,
alternatives: scenario => [
scenario.radioCall,
`${scenario.airlineCall} ${scenario.flightNumber}`,
scenario.callsign
],
width: 'lg'
}
],
readback: [
{ type: 'text', text: 'Wind ' },
{ type: 'field', key: 'landing-wind', width: 'md' },
{ type: 'text', text: ', runway ' },
{ type: 'field', key: 'landing-runway', width: 'sm' },
{ type: 'text', text: ', cleared to land, ' },
{ type: 'field', key: 'landing-callsign', width: 'lg' }
],
defaultFrequency: 'TWR',
phrase: scenario => `${scenario.radioCall}, wind ${scenario.arrivalWindWords}, runway ${scenario.arrivalRunway} cleared to land.`,
info: scenario => [
`Wind: ${scenario.arrivalWind} (${scenario.arrivalWindWords})`,
`Runway: ${scenario.arrivalRunway}`
],
generate: createBaseScenario
},
{
id: 'vacate',
title: 'Vacate Instruction',
desc: 'Confirm runway exit and ground frequency',
keywords: ['Tower', 'Arrival'],
hints: [
'Repeat the taxi route for vacating the runway.',
'Include the ground frequency before your call sign.'
],
fields: [
{
key: 'vacate-route',
label: 'Taxi Route',
expected: scenario => scenario.arrivalTaxiRoute,
alternatives: scenario => [
scenario.arrivalTaxiRoute,
`via ${scenario.arrivalTaxiRoute}`
],
width: 'lg'
},
{
key: 'vacate-freq',
label: 'Ground Frequency',
expected: scenario => scenario.groundFreq,
alternatives: scenario => [
scenario.groundFreq,
scenario.groundFreq.replace('.', ''),
scenario.frequencyWords.GND
],
width: 'md'
},
{
key: 'vacate-callsign',
label: 'Callsign',
expected: scenario => scenario.radioCall,
alternatives: scenario => [
scenario.radioCall,
`${scenario.airlineCall} ${scenario.flightNumber}`,
scenario.callsign
],
width: 'lg'
}
],
readback: [
{ type: 'text', text: 'Vacate via ' },
{ type: 'field', key: 'vacate-route', width: 'lg' },
{ type: 'text', text: ', contact ground ' },
{ type: 'field', key: 'vacate-freq', width: 'md' },
{ type: 'text', text: ', ' },
{ type: 'field', key: 'vacate-callsign', width: 'lg' }
],
defaultFrequency: 'TWR',
phrase: scenario => `${scenario.radioCall}, vacate via ${scenario.arrivalTaxiRoute}, contact Ground ${scenario.groundFreq}.`,
info: scenario => [
`Taxi route: ${scenario.arrivalTaxiRoute}`,
`Ground: ${scenario.groundFreq} (${scenario.frequencyWords.GND})`
],
generate: createBaseScenario
},
{
id: 'taxi-in-readback',
title: 'Taxi-In Readback',
desc: 'Acknowledge taxi instructions to the stand',
keywords: ['Ground', 'Taxi', 'Arrival'],
hints: [
'Repeat the stand and the full route.',
'End with your call sign.'
],
fields: [
{
key: 'taxi-in-stand',
label: 'Stand',
expected: scenario => scenario.arrivalStand,
width: 'sm'
},
{
key: 'taxi-in-route',
label: 'Route',
expected: scenario => scenario.arrivalTaxiRoute,
alternatives: scenario => [
scenario.arrivalTaxiRoute,
`via ${scenario.arrivalTaxiRoute}`
],
width: 'lg'
},
{
key: 'taxi-in-callsign',
label: 'Callsign',
expected: scenario => scenario.radioCall,
alternatives: scenario => [
scenario.radioCall,
`${scenario.airlineCall} ${scenario.flightNumber}`,
scenario.callsign
],
width: 'lg'
}
],
readback: [
{ type: 'text', text: 'Taxi to stand ' },
{ type: 'field', key: 'taxi-in-stand', width: 'sm' },
{ type: 'text', text: ' via ' },
{ type: 'field', key: 'taxi-in-route', width: 'lg' },
{ type: 'text', text: ', ' },
{ type: 'field', key: 'taxi-in-callsign', width: 'lg' }
],
defaultFrequency: 'GND',
phrase: scenario => `${scenario.radioCall}, taxi to stand ${scenario.arrivalStand} via ${scenario.arrivalTaxiRoute}.`,
info: scenario => [
`Stand: ${scenario.arrivalStand}`,
`Taxi route: ${scenario.arrivalTaxiRoute}`
],
generate: createBaseScenario
},
{
id: 'mayday-vector',
title: 'MAYDAY Vector Readback',
desc: 'Confirm emergency heading, altitude and QNH',
keywords: ['Emergency', 'Mayday'],
hints: [
'Acknowledge the heading, altitude, direct routing and QNH.',
'Keep the call sign at the end even in emergencies.'
],
fields: [
{
key: 'mayday-heading',
label: 'Heading',
expected: scenario => scenario.emergencyHeading,
alternatives: scenario => [
scenario.emergencyHeading,
scenario.emergencyHeadingWords
],
width: 'md'
},
{
key: 'mayday-alt',
label: 'Altitude',
expected: scenario => scenario.altitudes.initialWords,
alternatives: scenario => [
scenario.altitudes.initialWords,
scenario.altitudes.initial.toString(),
`${scenario.altitudes.initial} feet`
],
width: 'md'
},
{
key: 'mayday-dest',
label: 'Direct',
expected: scenario => scenario.destination.city,
alternatives: scenario => [
scenario.destination.city,
scenario.destination.icao,
scenario.destination.name
],
width: 'md'
},
{
key: 'mayday-qnh',
label: 'QNH',
expected: scenario => scenario.qnh.toString(),
alternatives: scenario => [`QNH ${scenario.qnh}`, scenario.qnhWords],
width: 'sm'
},
{
key: 'mayday-callsign',
label: 'Callsign',
expected: scenario => scenario.radioCall,
alternatives: scenario => [
scenario.radioCall,
`${scenario.airlineCall} ${scenario.flightNumber}`,
scenario.callsign
],
width: 'lg'
}
],
readback: [
{ type: 'text', text: 'Heading ' },
{ type: 'field', key: 'mayday-heading', width: 'md' },
{ type: 'text', text: ', climb ' },
{ type: 'field', key: 'mayday-alt', width: 'md' },
{ type: 'text', text: ', direct ' },
{ type: 'field', key: 'mayday-dest', width: 'md' },
{ type: 'text', text: ' when able, QNH ' },
{ type: 'field', key: 'mayday-qnh', width: 'sm' },
{ type: 'text', text: ', ' },
{ type: 'field', key: 'mayday-callsign', width: 'lg' }
],
defaultFrequency: 'CTR',
phrase: scenario => `${scenario.radioCall}, roger MAYDAY, fly heading ${scenario.emergencyHeading}, climb ${scenario.altitudes.initial} feet, direct ${scenario.destination.city} when able, QNH ${scenario.qnh}.`,
info: scenario => [
`Emergency heading: ${scenario.emergencyHeading} (${scenario.emergencyHeadingWords})`,
`Initial altitude: ${scenario.altitudes.initial} ft`,
`QNH: ${scenario.qnh}`
],
generate: createBaseScenario
},
{
id: 'go-around-instruction',
title: 'Go-Around Instruction',
desc: 'Read back the published missed approach instructions',
keywords: ['Missed Approach', 'Readback'],
hints: [
'Acknowledge the missed approach, altitude and frequency.',
'State the call sign last even when workload is high.'
],
fields: [
{
key: 'goaround-missed',
label: 'Missed Approach',
expected: scenario => scenario.missedApproach,
alternatives: scenario => [
scenario.missedApproach,
'published missed approach'
],
width: 'lg'
},
{
key: 'goaround-alt',
label: 'Altitude',
expected: scenario => scenario.altitudes.initialWords,
alternatives: scenario => [
scenario.altitudes.initialWords,
scenario.altitudes.initial.toString(),
`${scenario.altitudes.initial} feet`
],
width: 'md'
},
{
key: 'goaround-freq',
label: 'Approach Frequency',
expected: scenario => scenario.approachFreq,
alternatives: scenario => [
scenario.approachFreq,
scenario.approachFreq.replace('.', ''),
scenario.frequencyWords.APP
],
width: 'md'
},
{
key: 'goaround-callsign',
label: 'Callsign',
expected: scenario => scenario.radioCall,
alternatives: scenario => [
scenario.radioCall,
`${scenario.airlineCall} ${scenario.flightNumber}`,
scenario.callsign
],
width: 'lg'
}
],
readback: [
{ type: 'text', text: 'Going around, ' },
{ type: 'field', key: 'goaround-missed', width: 'lg' },
{ type: 'text', text: ', climb ' },
{ type: 'field', key: 'goaround-alt', width: 'md' },
{ type: 'text', text: ', contact approach ' },
{ type: 'field', key: 'goaround-freq', width: 'md' },
{ type: 'text', text: ', ' },
{ type: 'field', key: 'goaround-callsign', width: 'lg' }
],
defaultFrequency: 'APP',
phrase: scenario => `${scenario.radioCall}, roger go-around, fly published missed approach, climb ${scenario.altitudes.initial} feet, contact Approach ${scenario.approachFreq}.`,
info: scenario => [
`Missed approach: ${scenario.missedApproach}`,
`Altitude: ${scenario.altitudes.initial} ft`,
`Approach frequency: ${scenario.approachFreq} (${scenario.frequencyWords.APP})`
],
generate: createBaseScenario
}
]
const decisionTreeLessons: Lesson[] = [
{
id: 'clearance-contact',
title: 'Delivery: Initial contact',
desc: 'Contact clearance delivery',
keywords: ['Delivery', 'Clearance'],
hints: [
'Order: unit, call sign, ATIS, destination, stand, request.',
'Say the ATIS as a single letter.'
],
fields: [
{
key: 'cd-atis',
label: 'ATIS',
expected: scenario => scenario.atisCode,
alternatives: scenario => [
scenario.atisCode,
scenario.atisCode.toLowerCase(),
`Information ${scenario.atisCode}`
],
width: 'xs',
threshold: 0.9
},
{
key: 'cd-dest',
label: 'Destination',
expected: scenario => scenario.destination.city,
alternatives: scenario => [scenario.destination.icao, scenario.destination.name],
width: 'md'
},
{
key: 'cd-stand',
label: 'Stand/Gate',
expected: scenario => scenario.stand,
width: 'sm'
}
],
readback: [
{
type: 'text',
text: scenario => `${scenario.airport.city} Delivery, ${scenario.radioCall}, information `
},
{ type: 'field', key: 'cd-atis', width: 'xs' },
{ type: 'text', text: ', IFR to ' },
{ type: 'field', key: 'cd-dest', width: 'md' },
{ type: 'text', text: ', stand ' },
{ type: 'field', key: 'cd-stand', width: 'sm' },
{ type: 'text', text: ', request clearance.' }
],
defaultFrequency: 'DEL',
phrase: scenario => `${scenario.airport.city} Delivery, ${scenario.radioCall}, information ${scenario.atisCode}, IFR to ${scenario.destination.city}, stand ${scenario.stand}, request clearance.`,
info: scenario => [
`ATIS: ${scenario.atisCode}`,
`Destination: ${scenario.destination.city} (${scenario.destination.icao})`,
`Stand/Gate: ${scenario.stand}`
],
generate: createBaseScenario
},
{
id: 'pushback-ready',
title: 'Ready for Pushback',
desc: 'Call ground when ready for push and start',
keywords: ['Ground', 'Pushback'],
hints: [
'Address the ground unit, include your stand, then say "ready for push and start".',
'Finish with the call sign.'
],
fields: [
{
key: 'pushready-callsign',
label: 'Callsign',
expected: scenario => scenario.radioCall,
alternatives: scenario => [
scenario.radioCall,
`${scenario.airlineCall} ${scenario.flightNumber}`,
scenario.callsign
],
width: 'lg'
},
{
key: 'pushready-stand',
label: 'Stand',
expected: scenario => scenario.stand,
width: 'sm'
}
],
readback: [
{ type: 'text', text: scenario => `${scenario.airport.city} Ground, ` },
{ type: 'field', key: 'pushready-callsign', width: 'lg' },
{ type: 'text', text: ', stand ' },
{ type: 'field', key: 'pushready-stand', width: 'sm' },
{ type: 'text', text: ', ready for push and start.' }
],
defaultFrequency: 'GND',
phrase: scenario => `${scenario.airport.city} Ground, ${scenario.radioCall}, stand ${scenario.stand}, ready for push and start.`,
info: scenario => [
`Stand: ${scenario.stand}`,
`Next expected clearance: pushback approval`
],
generate: createBaseScenario
},
{
id: 'taxi-request',
title: 'Taxi Request',
desc: 'Request taxi from ground when ready',
keywords: ['Ground', 'Taxi'],
hints: [
'Address the unit before your call sign and stand.',
'Finish with "request taxi".'
],
fields: [
{
key: 'taxi-req-callsign',
label: 'Callsign',
expected: scenario => scenario.radioCall,
alternatives: scenario => [
scenario.radioCall,
`${scenario.airlineCall} ${scenario.flightNumber}`,
scenario.callsign
],
width: 'lg'
},
{
key: 'taxi-req-stand',
label: 'Stand',
expected: scenario => scenario.stand,
width: 'sm'
}
],
readback: [
{ type: 'text', text: scenario => `${scenario.airport.city} Ground, ` },
{ type: 'field', key: 'taxi-req-callsign', width: 'lg' },
{ type: 'text', text: ', stand ' },
{ type: 'field', key: 'taxi-req-stand', width: 'sm' },
{ type: 'text', text: ', request taxi.' }
],
defaultFrequency: 'GND',
phrase: scenario => `${scenario.airport.city} Ground, ${scenario.radioCall}, stand ${scenario.stand}, request taxi.`,
info: scenario => [
`Stand: ${scenario.stand}`,
`Likely taxi route: ${scenario.taxiRoute}`
],
generate: createBaseScenario
},
{
id: 'lineup-request',
title: 'Ready for Departure',
desc: 'Advise tower you are ready for departure',
keywords: ['Tower', 'Departure'],
hints: [
'Include the runway and keep the message short.',
'Finish with your call sign.'
],
fields: [
{
key: 'lineup-req-runway',
label: 'Runway',
expected: scenario => scenario.runway,
alternatives: scenario => [scenario.runway.replace(/^0/, ''), scenario.runwayWords],
width: 'sm'
},
{
key: 'lineup-req-callsign',
label: 'Callsign',
expected: scenario => scenario.radioCall,
alternatives: scenario => [
scenario.radioCall,
`${scenario.airlineCall} ${scenario.flightNumber}`,
scenario.callsign
],
width: 'lg'
}
],
readback: [
{ type: 'text', text: scenario => `${scenario.airport.city} Tower, ` },
{ type: 'field', key: 'lineup-req-callsign', width: 'lg' },
{ type: 'text', text: ', ready for departure runway ' },
{ type: 'field', key: 'lineup-req-runway', width: 'sm' }
],
defaultFrequency: 'TWR',
phrase: scenario => `${scenario.airport.city} Tower, ${scenario.radioCall}, ready for departure runway ${scenario.runway}.`,
info: scenario => [
`Runway in use: ${scenario.runway}`,
`Expect line-up or takeoff clearance`
],
generate: createBaseScenario
},
{
id: 'departure-checkin',
title: 'Departure Check-in',
desc: 'Initial call to departure',
keywords: ['Departure', 'Check-in'],
hints: [
'Address the unit first, then state the call sign.',
'Repeat the altitude and SID exactly as received.'
],
fields: [
{
key: 'depcheck-callsign',
label: 'Callsign',
expected: scenario => scenario.radioCall,
alternatives: scenario => [
scenario.radioCall,
`${scenario.airlineCall} ${scenario.flightNumber}`,
scenario.callsign
],
width: 'lg'
},
{
key: 'depcheck-alt',
label: 'Altitude',
expected: scenario => scenario.altitudes.initialWords,
alternatives: scenario => [
scenario.altitudes.initialWords,
scenario.altitudes.initial.toString(),
`${scenario.altitudes.initial} feet`
],
width: 'md'
},
{
key: 'depcheck-sid',
label: 'SID',
expected: scenario => scenario.sid,
width: 'lg'
}
],
readback: [
{ type: 'text', text: scenario => `${scenario.airport.city} Departure, ` },
{ type: 'field', key: 'depcheck-callsign', width: 'lg' },
{ type: 'text', text: ', passing ' },
{ type: 'field', key: 'depcheck-alt', width: 'md' },
{ type: 'text', text: ', on SID ' },
{ type: 'field', key: 'depcheck-sid', width: 'lg' }
],
defaultFrequency: 'DEP',
phrase: scenario => `${scenario.airport.city} Departure, ${scenario.radioCall}, passing ${scenario.altitudes.initialWords}, on SID ${scenario.sid}.`,
info: scenario => [
`Initial altitude: ${scenario.altitudes.initial} ft (${scenario.altitudes.initialWords})`,
`SID: ${scenario.sid}`
],
generate: createBaseScenario
},
{
id: 'unable-direct',
title: 'Unable Direct',
desc: 'Decline a direct routing and keep the SID',
keywords: ['Departure', 'Exception'],
hints: [
'Say "Unable direct" followed by the waypoint.',
'Finish with the call sign.'
],
fields: [
{
key: 'unable-transition',
label: 'Waypoint',
expected: scenario => scenario.transition,
width: 'md'
},
{
key: 'unable-callsign',
label: 'Callsign',
expected: scenario => scenario.radioCall,
alternatives: scenario => [
scenario.radioCall,
`${scenario.airlineCall} ${scenario.flightNumber}`,
scenario.callsign
],
width: 'lg'
}
],
readback: [
{ type: 'field', key: 'unable-callsign', width: 'lg' },
{ type: 'text', text: ' unable direct ' },
{ type: 'field', key: 'unable-transition', width: 'md' }
],
defaultFrequency: 'DEP',
phrase: scenario => `${scenario.radioCall}, unable direct ${scenario.transition}.`,
info: scenario => [
`Requested direct: ${scenario.transition}`,
`Climb continues on SID`
],
generate: createBaseScenario
},
{
id: 'alt-accept',
title: 'Accept Alternate Instruction',
desc: 'Acknowledge ATCs alternative plan',
keywords: ['Interrupt', 'Compliance'],
hints: [
'Simply respond with your call sign followed by "wilco".',
'Keep it concise to show you comply.'
],
fields: [
{
key: 'alt-accept-callsign',
label: 'Callsign',
expected: scenario => scenario.radioCall,
alternatives: scenario => [
scenario.radioCall,
`${scenario.airlineCall} ${scenario.flightNumber}`,
scenario.callsign
],
width: 'lg'
}
],
readback: [
{ type: 'field', key: 'alt-accept-callsign', width: 'lg' },
{ type: 'text', text: ' wilco.' }
],
defaultFrequency: 'CTR',
phrase: scenario => `${scenario.radioCall}, alternative instruction acknowledged with "wilco".`,
info: () => [
'Use "wilco" to indicate you will comply with the new instruction.'
],
generate: createBaseScenario
},
{
id: 'alt-reject',
title: 'Reject Alternate Instruction',
desc: 'Request a different solution when unable',
keywords: ['Interrupt', 'Request'],
hints: [
'State "negative" then what you request instead.',
'Keep the intent short, e.g. "request vectors" or destination.'
],
fields: [
{
key: 'alt-reject-callsign',
label: 'Callsign',
expected: scenario => scenario.radioCall,
alternatives: scenario => [
scenario.radioCall,
`${scenario.airlineCall} ${scenario.flightNumber}`,
scenario.callsign
],
width: 'lg'
},
{
key: 'alt-reject-intent',
label: 'Requested Intent',
expected: scenario => scenario.emergencyIntent,
width: 'lg'
}
],
readback: [
{ type: 'field', key: 'alt-reject-callsign', width: 'lg' },
{ type: 'text', text: ' negative, request ' },
{ type: 'field', key: 'alt-reject-intent', width: 'lg' }
],
defaultFrequency: 'CTR',
phrase: scenario => `${scenario.radioCall}, negative, request ${scenario.emergencyIntent}.`,
info: scenario => [
`Requested intent: ${scenario.emergencyIntent}`
],
generate: createBaseScenario
},
{
id: 'continue-approach',
title: 'Continue Approach',
desc: 'Acknowledge a late landing clearance instruction',
keywords: ['Tower', 'Exception'],
hints: [
'Keep the response short: continuing approach plus call sign.'
],
fields: [
{
key: 'continue-callsign',
label: 'Callsign',
expected: scenario => scenario.radioCall,
alternatives: scenario => [
scenario.radioCall,
`${scenario.airlineCall} ${scenario.flightNumber}`,
scenario.callsign
],
width: 'lg'
}
],
readback: [
{ type: 'text', text: 'Continuing approach, ' },
{ type: 'field', key: 'continue-callsign', width: 'lg' }
],
defaultFrequency: 'TWR',
phrase: scenario => `${scenario.radioCall}, continue approach, expect late landing clearance.`,
info: scenario => [`Runway in use: ${scenario.arrivalRunway}`],
generate: createBaseScenario
},
{
id: 'approach-established',
title: 'Approach Established',
desc: 'Report established on the localizer',
keywords: ['Approach', 'Report'],
hints: [
'State the runway after saying you are established.',
'Finish with the call sign.'
],
fields: [
{
key: 'app-est-runway',
label: 'Runway',
expected: scenario => scenario.arrivalRunway,
alternatives: scenario => [
scenario.arrivalRunway.replace(/^0/, ''),
scenario.arrivalRunwayWords
],
width: 'sm'
},
{
key: 'app-est-callsign',
label: 'Callsign',
expected: scenario => scenario.radioCall,
alternatives: scenario => [
scenario.radioCall,
`${scenario.airlineCall} ${scenario.flightNumber}`,
scenario.callsign
],
width: 'lg'
}
],
readback: [
{ type: 'field', key: 'app-est-callsign', width: 'lg' },
{ type: 'text', text: ' established localizer ' },
{ type: 'field', key: 'app-est-runway', width: 'sm' }
],
defaultFrequency: 'APP',
phrase: scenario => `${scenario.radioCall}, established localizer runway ${scenario.arrivalRunway}.`,
info: scenario => [
`Runway: ${scenario.arrivalRunway} (${scenario.arrivalRunwayWords})`
],
generate: createBaseScenario
},
{
id: 'taxi-in-request',
title: 'Taxi-In Request',
desc: 'Ask ground for taxi to stand after vacating',
keywords: ['Ground', 'Taxi-In'],
hints: [
'Announce runway vacated, then request taxi to your stand.',
'Include the stand number if known.'
],
fields: [
{
key: 'taxiin-callsign',
label: 'Callsign',
expected: scenario => scenario.radioCall,
alternatives: scenario => [
scenario.radioCall,
`${scenario.airlineCall} ${scenario.flightNumber}`,
scenario.callsign
],
width: 'lg'
},
{
key: 'taxiin-stand',
label: 'Stand',
expected: scenario => scenario.arrivalStand,
width: 'sm'
}
],
readback: [
{ type: 'field', key: 'taxiin-callsign', width: 'lg' },
{ type: 'text', text: ' runway vacated, request taxi to stand ' },
{ type: 'field', key: 'taxiin-stand', width: 'sm' }
],
defaultFrequency: 'GND',
phrase: scenario => `${scenario.radioCall}, runway vacated, request taxi to stand ${scenario.arrivalStand}.`,
info: scenario => [
`Arrival stand: ${scenario.arrivalStand}`,
`Expect taxi route: ${scenario.arrivalTaxiRoute}`
],
generate: createBaseScenario
},
{
id: 'mayday',
title: 'MAYDAY Call',
desc: 'Declare an emergency and state intentions',
keywords: ['Emergency', 'MAYDAY'],
hints: [
'Say "MAYDAY" three times, then the call sign.',
'State the problem and your intentions clearly.'
],
fields: [
{
key: 'mayday-callsign',
label: 'Callsign',
expected: scenario => scenario.radioCall,
alternatives: scenario => [
scenario.radioCall,
`${scenario.airlineCall} ${scenario.flightNumber}`,
scenario.callsign
],
width: 'lg'
},
{
key: 'mayday-problem',
label: 'Problem',
expected: scenario => scenario.emergencyProblem,
width: 'lg'
},
{
key: 'mayday-intent',
label: 'Intentions',
expected: scenario => scenario.emergencyIntent,
width: 'lg'
}
],
readback: [
{ type: 'text', text: 'MAYDAY MAYDAY MAYDAY, ' },
{ type: 'field', key: 'mayday-callsign', width: 'lg' },
{ type: 'text', text: ', ' },
{ type: 'field', key: 'mayday-problem', width: 'lg' },
{ type: 'text', text: ', intentions ' },
{ type: 'field', key: 'mayday-intent', width: 'lg' }
],
defaultFrequency: 'CTR',
phrase: scenario => `${scenario.radioCall}, Emergency scenario: ${scenario.emergencyProblem}. Declare MAYDAY with intentions ${scenario.emergencyIntent}.`,
info: scenario => [
`Problem: ${scenario.emergencyProblem}`,
`Intentions: ${scenario.emergencyIntent}`
],
generate: createBaseScenario
},
{
id: 'pan-pan',
title: 'PAN PAN Call',
desc: 'Request priority for an urgent situation',
keywords: ['Emergency', 'PAN'],
hints: [
'Say "PAN PAN PAN" three times, then the call sign.',
'Describe the problem and end with "request priority".'
],
fields: [
{
key: 'pan-callsign',
label: 'Callsign',
expected: scenario => scenario.radioCall,
alternatives: scenario => [
scenario.radioCall,
`${scenario.airlineCall} ${scenario.flightNumber}`,
scenario.callsign
],
width: 'lg'
},
{
key: 'pan-problem',
label: 'Problem',
expected: scenario => scenario.emergencyProblem,
width: 'lg'
}
],
readback: [
{ type: 'text', text: 'PAN PAN PAN, ' },
{ type: 'field', key: 'pan-callsign', width: 'lg' },
{ type: 'text', text: ', ' },
{ type: 'field', key: 'pan-problem', width: 'lg' },
{ type: 'text', text: ', request priority.' }
],
defaultFrequency: 'CTR',
phrase: scenario => `${scenario.radioCall}, Urgent situation: ${scenario.emergencyProblem}. Declare PAN to obtain priority.`,
info: scenario => [
`Problem: ${scenario.emergencyProblem}`,
'Ensure ATC knows you require priority handling.'
],
generate: createBaseScenario
},
{
id: 'go-around',
title: 'Go-Around Call',
desc: 'Declare a go-around with the missed approach',
keywords: ['Tower', 'Missed Approach', 'Exception'],
hints: [
'State your call sign, then "going around".',
'Include the missed approach instructions.'
],
fields: [
{
key: 'goaround-callsign',
label: 'Callsign',
expected: scenario => scenario.radioCall,
alternatives: scenario => [
scenario.radioCall,
`${scenario.airlineCall} ${scenario.flightNumber}`,
scenario.callsign
],
width: 'lg'
},
{
key: 'goaround-missed',
label: 'Missed Approach',
expected: scenario => scenario.missedApproach,
width: 'lg'
}
],
readback: [
{ type: 'field', key: 'goaround-callsign', width: 'lg' },
{ type: 'text', text: ' going around, ' },
{ type: 'field', key: 'goaround-missed', width: 'lg' }
],
defaultFrequency: 'TWR',
phrase: scenario => `${scenario.radioCall}, going around, ${scenario.missedApproach}.`,
info: scenario => [
`Missed approach: ${scenario.missedApproach}`
],
generate: createBaseScenario
},
{
id: 'tcas-ra',
title: 'TCAS RA Call',
desc: 'Report a TCAS resolution advisory',
keywords: ['Emergency', 'TCAS'],
hints: [
'Say your call sign followed by "TCAS RA, deviating".'
],
fields: [
{
key: 'tcas-callsign',
label: 'Callsign',
expected: scenario => scenario.radioCall,
alternatives: scenario => [
scenario.radioCall,
`${scenario.airlineCall} ${scenario.flightNumber}`,
scenario.callsign
],
width: 'lg'
}
],
readback: [
{ type: 'field', key: 'tcas-callsign', width: 'lg' },
{ type: 'text', text: ' TCAS RA, deviating.' }
],
defaultFrequency: 'CTR',
phrase: scenario => `${scenario.radioCall} experiences a TCAS RA. Report it immediately.`,
info: () => [
'Inform ATC that you are following the RA.',
'Expect to report when clear of conflict.'
],
generate: createBaseScenario
},
{
id: 'tcas-resume',
title: 'TCAS Clear of Conflict',
desc: 'Report returning to your clearance after an RA',
keywords: ['Emergency', 'TCAS'],
hints: [
'State your call sign followed by "clear of conflict, returning to clearance".'
],
fields: [
{
key: 'tcas-resume-callsign',
label: 'Callsign',
expected: scenario => scenario.radioCall,
alternatives: scenario => [
scenario.radioCall,
`${scenario.airlineCall} ${scenario.flightNumber}`,
scenario.callsign
],
width: 'lg'
}
],
readback: [
{ type: 'field', key: 'tcas-resume-callsign', width: 'lg' },
{ type: 'text', text: ' clear of conflict, returning to clearance.' }
],
defaultFrequency: 'CTR',
phrase: scenario => `${scenario.radioCall}, report clear of conflict and returning to clearance.`,
info: () => [
'Advise ATC once the RA is resolved so normal control can resume.'
],
generate: createBaseScenario
},
{
id: 'standby',
title: 'Standby Call',
desc: 'Pause the exchange when busy',
keywords: ['Interrupt'],
hints: [
'Say your call sign followed by "standby" to pause the conversation briefly.'
],
fields: [
{
key: 'standby-callsign',
label: 'Callsign',
expected: scenario => scenario.radioCall,
alternatives: scenario => [
scenario.radioCall,
`${scenario.airlineCall} ${scenario.flightNumber}`,
scenario.callsign
],
width: 'lg'
}
],
readback: [
{ type: 'field', key: 'standby-callsign', width: 'lg' },
{ type: 'text', text: ' standby.' }
],
defaultFrequency: 'CTR',
phrase: scenario => `${scenario.radioCall}, standby.`,
info: () => [
'Use sparingly when workload requires a short pause before responding.'
],
generate: createBaseScenario
}
]
const fullFlightScenario = createScenarioSeries()
export function seedFullFlightScenario(scenario: Scenario | null) {
if (scenario) {
fullFlightScenario.setScenario(scenario)
} else {
fullFlightScenario.setScenario(null)
}
}
export function peekFullFlightScenario(): Scenario | null {
return fullFlightScenario.peek()
}
const makeFullFlightGenerator = (reset = false) => () => {
if (reset) {
fullFlightScenario.reset()
}
return fullFlightScenario()
}
const fullFlightLessons: Lesson[] = [
{
id: 'full-clearance-contact',
title: 'Delivery Contact',
desc: 'Start the flight with clearance delivery',
keywords: ['Delivery', 'Flow'],
hints: [
'Use the full opening: unit, call sign, ATIS, destination, stand, request.'
],
fields: [
{
key: 'full-cd-atis',
label: 'ATIS',
expected: scenario => scenario.atisCode,
alternatives: scenario => [
scenario.atisCode,
scenario.atisCode.toLowerCase(),
`Information ${scenario.atisCode}`
],
width: 'xs',
threshold: 0.9
},
{
key: 'full-cd-dest',
label: 'Destination',
expected: scenario => scenario.destination.city,
alternatives: scenario => [scenario.destination.icao, scenario.destination.name],
width: 'md'
},
{
key: 'full-cd-stand',
label: 'Stand',
expected: scenario => scenario.stand,
width: 'sm'
}
],
readback: [
{
type: 'text',
text: scenario => `${scenario.airport.city} Delivery, ${scenario.radioCall}, information `
},
{ type: 'field', key: 'full-cd-atis', width: 'xs' },
{ type: 'text', text: ', IFR to ' },
{ type: 'field', key: 'full-cd-dest', width: 'md' },
{ type: 'text', text: ', stand ' },
{ type: 'field', key: 'full-cd-stand', width: 'sm' },
{ type: 'text', text: ', request clearance.' }
],
defaultFrequency: 'DEL',
phrase: scenario => `${scenario.airport.city} Delivery, ${scenario.radioCall}, information ${scenario.atisCode}, IFR to ${scenario.destination.city}, stand ${scenario.stand}, request clearance.`,
info: scenario => [
`ATIS: ${scenario.atisCode}`,
`Destination: ${scenario.destination.city} (${scenario.destination.icao})`,
`Stand: ${scenario.stand}`
],
generate: makeFullFlightGenerator(true)
},
{
id: 'full-clearance-readback',
title: 'Clearance Readback',
desc: 'Confirm the IFR clearance in full',
keywords: ['Delivery', 'Readback', 'Flow'],
hints: [
'Order: destination SID runway altitude squawk.'
],
fields: [
{
key: 'full-clr-dest',
label: 'Destination',
expected: scenario => scenario.destination.city,
alternatives: scenario => [scenario.destination.icao, scenario.destination.name],
width: 'md'
},
{
key: 'full-clr-sid',
label: 'SID',
expected: scenario => scenario.sid,
width: 'lg'
},
{
key: 'full-clr-runway',
label: 'Runway',
expected: scenario => scenario.runway,
alternatives: scenario => [scenario.runway.replace(/^0/, ''), scenario.runwayWords],
width: 'sm'
},
{
key: 'full-clr-alt',
label: 'Initial Altitude',
expected: scenario => scenario.altitudes.initialWords,
alternatives: scenario => [
scenario.altitudes.initial.toString(),
`${scenario.altitudes.initial} feet`
],
width: 'md'
},
{
key: 'full-clr-squawk',
label: 'Squawk',
expected: scenario => scenario.squawkWords,
alternatives: scenario => [scenario.squawk, scenario.squawk.split('').join(' ')],
width: 'md'
}
],
readback: [
{ type: 'text', text: scenario => `${scenario.radioCall} cleared ` },
{ type: 'field', key: 'full-clr-dest', width: 'md' },
{ type: 'text', text: ' via ' },
{ type: 'field', key: 'full-clr-sid', width: 'lg' },
{ type: 'text', text: ', runway ' },
{ type: 'field', key: 'full-clr-runway', width: 'sm' },
{ type: 'text', text: ', climb ' },
{ type: 'field', key: 'full-clr-alt', width: 'md' },
{ type: 'text', text: ', squawk ' },
{ type: 'field', key: 'full-clr-squawk', width: 'md' }
],
defaultFrequency: 'DEL',
phrase: scenario => `${scenario.radioCall}, cleared to ${scenario.destination.city} via ${scenario.sid}, runway ${scenario.runway}, climb ${scenario.altitudes.initial} feet, squawk ${scenario.squawk}.`,
info: scenario => [
`SID: ${scenario.sid} (${scenario.transition})`,
`Initial altitude: ${scenario.altitudes.initial} ft`,
`Squawk: ${scenario.squawk} (${scenario.squawkWords})`
],
generate: makeFullFlightGenerator()
},
{
id: 'full-pushback',
title: 'Push & Start',
desc: 'Acknowledge the pushback clearance',
keywords: ['Ground', 'Pushback', 'Flow'],
hints: [
'Repeat the runway direction and QNH, call sign at the end.'
],
fields: [
{
key: 'full-push-runway',
label: 'Runway',
expected: scenario => scenario.runway,
alternatives: scenario => [scenario.runway.replace(/^0/, ''), scenario.runwayWords],
width: 'sm'
},
{
key: 'full-push-qnh',
label: 'QNH',
expected: scenario => scenario.qnh.toString(),
alternatives: scenario => [`QNH ${scenario.qnh}`, scenario.qnhWords],
width: 'sm'
},
{
key: 'full-push-callsign',
label: 'Callsign',
expected: scenario => scenario.radioCall,
alternatives: scenario => [
scenario.radioCall,
`${scenario.airlineCall} ${scenario.flightNumber}`,
scenario.callsign
],
width: 'lg'
}
],
readback: [
{ type: 'text', text: 'Push and start approved, facing runway ' },
{ type: 'field', key: 'full-push-runway', width: 'sm' },
{ type: 'text', text: ', QNH ' },
{ type: 'field', key: 'full-push-qnh', width: 'sm' },
{ type: 'text', text: ', ' },
{ type: 'field', key: 'full-push-callsign', width: 'lg' }
],
defaultFrequency: 'GND',
phrase: scenario => `${scenario.radioCall}, push and start approved, facing runway ${scenario.runway}. QNH ${scenario.qnh}.`,
info: scenario => [
`Stand: ${scenario.stand}`,
`Ground frequency: ${scenario.groundFreq} (${scenario.frequencyWords.GND})`
],
generate: makeFullFlightGenerator()
},
{
id: 'full-taxi',
title: 'Taxi Clearance',
desc: 'Read back the taxi clearance on the same scenario',
keywords: ['Ground', 'Taxi', 'Flow'],
hints: [
'Repeat the runway and full taxi route, including the hold short.'
],
fields: [
{
key: 'full-taxi-runway',
label: 'Runway',
expected: scenario => scenario.runway,
alternatives: scenario => [scenario.runway.replace(/^0/, ''), scenario.runwayWords],
width: 'sm'
},
{
key: 'full-taxi-route',
label: 'Route',
expected: scenario => scenario.taxiRoute,
alternatives: scenario => [scenario.taxiRoute, `via ${scenario.taxiRoute}`],
width: 'lg'
},
{
key: 'full-taxi-hold',
label: 'Hold Short',
expected: scenario => scenario.runway,
alternatives: scenario => [scenario.runway.replace(/^0/, ''), scenario.runwayWords],
width: 'sm'
},
{
key: 'full-taxi-callsign',
label: 'Callsign',
expected: scenario => scenario.radioCall,
alternatives: scenario => [
scenario.radioCall,
`${scenario.airlineCall} ${scenario.flightNumber}`,
scenario.callsign
],
width: 'lg'
}
],
readback: [
{ type: 'text', text: 'Taxi to runway ' },
{ type: 'field', key: 'full-taxi-runway', width: 'sm' },
{ type: 'text', text: ' via ' },
{ type: 'field', key: 'full-taxi-route', width: 'lg' },
{ type: 'text', text: ', holding short runway ' },
{ type: 'field', key: 'full-taxi-hold', width: 'sm' },
{ type: 'text', text: ', ' },
{ type: 'field', key: 'full-taxi-callsign', width: 'lg' }
],
defaultFrequency: 'GND',
phrase: scenario => `${scenario.radioCall}, taxi to runway ${scenario.runway} via ${scenario.taxiRoute}, hold short runway ${scenario.runway}.`,
info: scenario => [
`Taxi route: ${scenario.taxiRoute}`,
`Hold short: runway ${scenario.runway}`
],
generate: makeFullFlightGenerator()
},
{
id: 'full-tower-contact',
title: 'Tower Handoff',
desc: 'Confirm the switch to tower when number one',
keywords: ['Tower', 'Handoff', 'Flow'],
hints: [
'Read back the frequency, mention "when number one" and end with the call sign.'
],
fields: [
{
key: 'full-tower-freq',
label: 'Frequency',
expected: scenario => scenario.towerFreq,
alternatives: scenario => [
scenario.towerFreq,
scenario.towerFreq.replace('.', ''),
scenario.frequencyWords.TWR
],
width: 'md'
},
{
key: 'full-tower-callsign',
label: 'Callsign',
expected: scenario => scenario.radioCall,
alternatives: scenario => [
scenario.radioCall,
`${scenario.airlineCall} ${scenario.flightNumber}`,
scenario.callsign
],
width: 'lg'
}
],
readback: [
{ type: 'text', text: 'Contact tower ' },
{ type: 'field', key: 'full-tower-freq', width: 'md' },
{ type: 'text', text: ' when number one, ' },
{ type: 'field', key: 'full-tower-callsign', width: 'lg' }
],
defaultFrequency: 'GND',
phrase: scenario => `${scenario.radioCall}, contact Tower ${scenario.towerFreq} when number one.`,
info: scenario => [
`Tower frequency: ${scenario.towerFreq} (${scenario.frequencyWords.TWR})`
],
generate: makeFullFlightGenerator()
},
{
id: 'full-lineup',
title: 'Line Up and Wait',
desc: 'Acknowledge the line-up clearance',
keywords: ['Tower', 'Line Up', 'Flow'],
hints: [
'Repeat the runway and add "line up and wait".'
],
fields: [
{
key: 'full-lineup-runway',
label: 'Runway',
expected: scenario => scenario.runway,
alternatives: scenario => [scenario.runway.replace(/^0/, ''), scenario.runwayWords],
width: 'sm'
},
{
key: 'full-lineup-callsign',
label: 'Callsign',
expected: scenario => scenario.radioCall,
alternatives: scenario => [
scenario.radioCall,
`${scenario.airlineCall} ${scenario.flightNumber}`,
scenario.callsign
],
width: 'lg'
}
],
readback: [
{ type: 'text', text: 'Runway ' },
{ type: 'field', key: 'full-lineup-runway', width: 'sm' },
{ type: 'text', text: ', line up and wait, ' },
{ type: 'field', key: 'full-lineup-callsign', width: 'lg' }
],
defaultFrequency: 'TWR',
phrase: scenario => `${scenario.radioCall}, line up and wait runway ${scenario.runway}.`,
info: scenario => [
`Tower: ${scenario.towerFreq} (${scenario.frequencyWords.TWR})`,
`Line-up: runway ${scenario.runway}`
],
generate: makeFullFlightGenerator()
},
{
id: 'full-takeoff',
title: 'Takeoff Clearance',
desc: 'Acknowledge the takeoff clearance',
keywords: ['Tower', 'Departure', 'Flow'],
hints: [
'Order: wind runway cleared for takeoff call sign.',
'Write the wind as direction/speed, e.g. 030/12.'
],
fields: [
{
key: 'full-tko-wind',
label: 'Wind',
expected: scenario => scenario.wind,
alternatives: scenario => [
scenario.wind,
`${scenario.wind}KT`,
scenario.windWords
],
width: 'md'
},
{
key: 'full-tko-runway',
label: 'Runway',
expected: scenario => scenario.runway,
alternatives: scenario => [scenario.runway.replace(/^0/, ''), scenario.runwayWords],
width: 'sm'
},
{
key: 'full-tko-callsign',
label: 'Callsign',
expected: scenario => scenario.radioCall,
alternatives: scenario => [
scenario.radioCall,
`${scenario.airlineCall} ${scenario.flightNumber}`,
scenario.callsign
],
width: 'lg'
}
],
readback: [
{ type: 'text', text: 'Wind ' },
{ type: 'field', key: 'full-tko-wind', width: 'md' },
{ type: 'text', text: ', runway ' },
{ type: 'field', key: 'full-tko-runway', width: 'sm' },
{ type: 'text', text: ', cleared for takeoff, ' },
{ type: 'field', key: 'full-tko-callsign', width: 'lg' }
],
defaultFrequency: 'TWR',
phrase: scenario => `${scenario.radioCall}, wind ${scenario.windWords}, runway ${scenario.runway}, cleared for takeoff.`,
info: scenario => [
`Wind: ${scenario.wind} (${scenario.windWords})`
],
generate: makeFullFlightGenerator()
},
{
id: 'full-departure-handoff',
title: 'Handoff to Departure',
desc: 'Switch to departure after takeoff',
keywords: ['Departure', 'Handoff', 'Flow'],
hints: [
'Read back the frequency exactly and include your call sign.'
],
fields: [
{
key: 'full-dep-freq',
label: 'Frequency',
expected: scenario => scenario.departureFreq,
alternatives: scenario => [
scenario.departureFreq,
scenario.departureFreq.replace('.', ''),
scenario.frequencyWords.DEP
],
width: 'md'
},
{
key: 'full-dep-callsign',
label: 'Callsign',
expected: scenario => scenario.radioCall,
alternatives: scenario => [
scenario.radioCall,
`${scenario.airlineCall} ${scenario.flightNumber}`,
scenario.callsign
],
width: 'lg'
}
],
readback: [
{ type: 'text', text: 'Contact departure ' },
{ type: 'field', key: 'full-dep-freq', width: 'md' },
{ type: 'text', text: ', ' },
{ type: 'field', key: 'full-dep-callsign', width: 'lg' }
],
defaultFrequency: 'DEP',
phrase: scenario => `${scenario.radioCall}, contact departure ${scenario.departureFreq}.`,
info: scenario => [
`Departure frequency: ${scenario.departureFreq} (${scenario.frequencyWords.DEP})`
],
generate: makeFullFlightGenerator()
},
{
id: 'full-departure-checkin',
title: 'Departure Check-in',
desc: 'Call departure with altitude and SID',
keywords: ['Departure', 'Flow'],
hints: [
'Address departure first, then your call sign, altitude and SID.'
],
fields: [
{
key: 'full-depcheck-callsign',
label: 'Callsign',
expected: scenario => scenario.radioCall,
alternatives: scenario => [
scenario.radioCall,
`${scenario.airlineCall} ${scenario.flightNumber}`,
scenario.callsign
],
width: 'lg'
},
{
key: 'full-depcheck-alt',
label: 'Altitude',
expected: scenario => scenario.altitudes.initialWords,
alternatives: scenario => [
scenario.altitudes.initialWords,
scenario.altitudes.initial.toString(),
`${scenario.altitudes.initial} feet`
],
width: 'md'
},
{
key: 'full-depcheck-sid',
label: 'SID',
expected: scenario => scenario.sid,
width: 'lg'
}
],
readback: [
{ type: 'text', text: scenario => `${scenario.airport.city} Departure, ` },
{ type: 'field', key: 'full-depcheck-callsign', width: 'lg' },
{ type: 'text', text: ', passing ' },
{ type: 'field', key: 'full-depcheck-alt', width: 'md' },
{ type: 'text', text: ', on SID ' },
{ type: 'field', key: 'full-depcheck-sid', width: 'lg' }
],
defaultFrequency: 'DEP',
phrase: scenario => `${scenario.airport.city} Departure, ${scenario.radioCall}, passing ${scenario.altitudes.initialWords}, on SID ${scenario.sid}.`,
info: scenario => [
`Initial altitude: ${scenario.altitudes.initial} ft`,
`SID: ${scenario.sid}`
],
generate: makeFullFlightGenerator()
},
{
id: 'full-climb',
title: 'Climb & Direct',
desc: 'Confirm the climb and direct instruction',
keywords: ['Departure', 'Climb', 'Flow'],
hints: [
'Start with "Climb", then the altitude and any direct clearance.'
],
fields: [
{
key: 'full-climb-alt',
label: 'Altitude',
expected: scenario => scenario.altitudes.climbWords,
alternatives: scenario => [
scenario.altitudes.climbWords,
scenario.altitudes.climb.toString(),
`${scenario.altitudes.climb} feet`
],
width: 'md'
},
{
key: 'full-climb-direct',
label: 'Direct',
expected: scenario => scenario.transition,
width: 'md'
},
{
key: 'full-climb-callsign',
label: 'Callsign',
expected: scenario => scenario.radioCall,
alternatives: scenario => [
scenario.radioCall,
`${scenario.airlineCall} ${scenario.flightNumber}`,
scenario.callsign
],
width: 'lg'
}
],
readback: [
{ type: 'text', text: 'Climb ' },
{ type: 'field', key: 'full-climb-alt', width: 'md' },
{ type: 'text', text: ', direct ' },
{ type: 'field', key: 'full-climb-direct', width: 'md' },
{ type: 'text', text: ', ' },
{ type: 'field', key: 'full-climb-callsign', width: 'lg' }
],
defaultFrequency: 'DEP',
phrase: scenario => `${scenario.radioCall}, climb ${scenario.altitudes.climb} feet, proceed direct ${scenario.transition}.`,
info: scenario => [
`Climb altitude: ${scenario.altitudes.climb} ft (${scenario.altitudes.climbWords})`,
`Waypoint: ${scenario.transition}`
],
generate: makeFullFlightGenerator()
},
{
id: 'full-descent',
title: 'Descent Clearance',
desc: 'Read back STAR, transition and QNH',
keywords: ['Approach', 'Descent', 'Flow'],
hints: [
'Mention the STAR, transition and QNH before the call sign.'
],
fields: [
{
key: 'full-descent-star',
label: 'STAR',
expected: scenario => scenario.arrivalStar,
width: 'lg'
},
{
key: 'full-descent-transition',
label: 'Transition',
expected: scenario => scenario.arrivalTransition,
width: 'md'
},
{
key: 'full-descent-qnh',
label: 'QNH',
expected: scenario => scenario.arrivalQnh.toString(),
alternatives: scenario => [
scenario.arrivalQnh.toString(),
`QNH ${scenario.arrivalQnh}`,
scenario.arrivalQnhWords
],
width: 'sm'
},
{
key: 'full-descent-callsign',
label: 'Callsign',
expected: scenario => scenario.radioCall,
alternatives: scenario => [
scenario.radioCall,
`${scenario.airlineCall} ${scenario.flightNumber}`,
scenario.callsign
],
width: 'lg'
}
],
readback: [
{ type: 'text', text: 'Descend via ' },
{ type: 'field', key: 'full-descent-star', width: 'lg' },
{ type: 'text', text: ' ' },
{ type: 'field', key: 'full-descent-transition', width: 'md' },
{ type: 'text', text: ', QNH ' },
{ type: 'field', key: 'full-descent-qnh', width: 'sm' },
{ type: 'text', text: ', ' },
{ type: 'field', key: 'full-descent-callsign', width: 'lg' }
],
defaultFrequency: 'APP',
phrase: scenario => `${scenario.radioCall}, descend via ${scenario.arrivalStar} ${scenario.arrivalTransition}, QNH ${scenario.arrivalQnh}.`,
info: scenario => [
`STAR: ${scenario.arrivalStar}`,
`Transition: ${scenario.arrivalTransition}`,
`Arrival QNH: ${scenario.arrivalQnh}`
],
generate: makeFullFlightGenerator()
},
{
id: 'full-approach',
title: 'Approach Clearance',
desc: 'Confirm approach type and runway',
keywords: ['Approach', 'Readback', 'Flow'],
hints: [
'Say "Cleared" followed by the approach and runway.'
],
fields: [
{
key: 'full-app-type',
label: 'Approach',
expected: scenario => scenario.approach,
width: 'lg'
},
{
key: 'full-app-runway',
label: 'Runway',
expected: scenario => scenario.arrivalRunway,
alternatives: scenario => [scenario.arrivalRunway.replace(/^0/, ''), scenario.arrivalRunwayWords],
width: 'sm'
},
{
key: 'full-app-callsign',
label: 'Callsign',
expected: scenario => scenario.radioCall,
alternatives: scenario => [
scenario.radioCall,
`${scenario.airlineCall} ${scenario.flightNumber}`,
scenario.callsign
],
width: 'lg'
}
],
readback: [
{ type: 'text', text: 'Cleared ' },
{ type: 'field', key: 'full-app-type', width: 'lg' },
{ type: 'text', text: ' approach runway ' },
{ type: 'field', key: 'full-app-runway', width: 'sm' },
{ type: 'text', text: ', ' },
{ type: 'field', key: 'full-app-callsign', width: 'lg' }
],
defaultFrequency: 'APP',
phrase: scenario => `${scenario.radioCall}, cleared ${scenario.approach} approach runway ${scenario.arrivalRunway}, report established.`,
info: scenario => [
`Approach: ${scenario.approach}`,
`Runway: ${scenario.arrivalRunway} (${scenario.arrivalRunwayWords})`
],
generate: makeFullFlightGenerator()
},
{
id: 'full-landing',
title: 'Landing Clearance',
desc: 'Confirm the landing clearance',
keywords: ['Tower', 'Landing', 'Flow'],
hints: [
'Order: wind runway cleared to land call sign.',
'Write the wind as direction/speed, e.g. 260/08.'
],
fields: [
{
key: 'full-landing-wind',
label: 'Wind',
expected: scenario => scenario.arrivalWind,
alternatives: scenario => [
scenario.arrivalWind,
`${scenario.arrivalWind}KT`,
scenario.arrivalWindWords
],
width: 'md'
},
{
key: 'full-landing-runway',
label: 'Runway',
expected: scenario => scenario.arrivalRunway,
alternatives: scenario => [scenario.arrivalRunway.replace(/^0/, ''), scenario.arrivalRunwayWords],
width: 'sm'
},
{
key: 'full-landing-callsign',
label: 'Callsign',
expected: scenario => scenario.radioCall,
alternatives: scenario => [
scenario.radioCall,
`${scenario.airlineCall} ${scenario.flightNumber}`,
scenario.callsign
],
width: 'lg'
}
],
readback: [
{ type: 'text', text: 'Wind ' },
{ type: 'field', key: 'full-landing-wind', width: 'md' },
{ type: 'text', text: ', runway ' },
{ type: 'field', key: 'full-landing-runway', width: 'sm' },
{ type: 'text', text: ', cleared to land, ' },
{ type: 'field', key: 'full-landing-callsign', width: 'lg' }
],
defaultFrequency: 'TWR',
phrase: scenario => `${scenario.radioCall}, wind ${scenario.arrivalWindWords}, runway ${scenario.arrivalRunway} cleared to land.`,
info: scenario => [
`Wind: ${scenario.arrivalWind} (${scenario.arrivalWindWords})`
],
generate: makeFullFlightGenerator()
},
{
id: 'full-taxi-in',
title: 'Taxi-In Clearance',
desc: 'Read back taxi instructions to the stand',
keywords: ['Ground', 'Taxi', 'Arrival'],
hints: [
'Repeat the stand and full route, end with the call sign.'
],
fields: [
{
key: 'full-taxiin-stand',
label: 'Stand',
expected: scenario => scenario.arrivalStand,
width: 'sm'
},
{
key: 'full-taxiin-route',
label: 'Route',
expected: scenario => scenario.arrivalTaxiRoute,
alternatives: scenario => [scenario.arrivalTaxiRoute, `via ${scenario.arrivalTaxiRoute}`],
width: 'lg'
},
{
key: 'full-taxiin-callsign',
label: 'Callsign',
expected: scenario => scenario.radioCall,
alternatives: scenario => [
scenario.radioCall,
`${scenario.airlineCall} ${scenario.flightNumber}`,
scenario.callsign
],
width: 'lg'
}
],
readback: [
{ type: 'text', text: 'Taxi to stand ' },
{ type: 'field', key: 'full-taxiin-stand', width: 'sm' },
{ type: 'text', text: ' via ' },
{ type: 'field', key: 'full-taxiin-route', width: 'lg' },
{ type: 'text', text: ', ' },
{ type: 'field', key: 'full-taxiin-callsign', width: 'lg' }
],
defaultFrequency: 'GND',
phrase: scenario => `${scenario.radioCall}, taxi to stand ${scenario.arrivalStand} via ${scenario.arrivalTaxiRoute}.`,
info: scenario => [
`Stand: ${scenario.arrivalStand}`,
`Taxi route: ${scenario.arrivalTaxiRoute}`
],
generate: makeFullFlightGenerator()
}
]
export const learnModules: ModuleDef[] = [
{
id: 'normalize',
title: 'Fundamentals · Basics',
subtitle: 'Alphabet, Call Signs, ATIS & METAR',
art: '/img/learn/modules/img14.jpeg',
lessons: fundamentalsLessons
},
{
id: 'arc',
title: 'Readbacks · Essential Calls',
subtitle: 'Clearances, taxi, approach & landing',
art: '/img/learn/modules/img11.jpeg',
lessons: readbackLessons
},
{
id: 'decision-tree',
title: 'ATC · Advanced Calls',
subtitle: 'Requests, contingencies & interrupts',
art: '/img/learn/modules/img10.jpeg',
lessons: decisionTreeLessons
},
{
id: 'full-flight',
title: 'Full Flight · Gate to Gate',
subtitle: 'One linked scenario from clearance to taxi-in',
art: '/img/learn/modules/img6.jpeg',
lessons: fullFlightLessons,
meta: {
flightPlan: true,
briefingArt: '/img/learn/missions/full-flight/briefing-hero.png'
}
}
]
export default learnModules