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) => ``)
.join('')
const 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(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()
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()
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([
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([
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()
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: () => ['1', '2', '3', '4', '5', 'one', 'two', 'three', 'four', 'five', 'wun', 'too', 'tree', 'fife'],
placeholder: '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([
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: [
'In the readback, state the runway first — even though the controller said it last.',
'ICAO standard: runway leads the readback, then "line up and wait", then your call sign.'
],
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 ATC’s 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: [
'In the readback, state the runway first — even though the controller said it last.',
'ICAO standard: runway leads the readback, then "line up and wait", then your call sign.'
],
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