import { createBaseScenario, createScenarioSeries, formatTemp } from '~~/shared/learn/scenario'
import type { 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)}`
}
const fundamentalsLessons = [
{
id: 'icao-alphabet',
title: 'ICAO Alphabet & Numbers',
desc: 'Spell letters and digits clearly',
keywords: ['Alphabet', 'Numbers', 'Basics'],
hints: [
'Spell each letter with its ICAO name (e.g. Delta, Lima, Hotel).',
'Use ATC numbers: tree, fower, fife, niner.'
],
fields: [
{
key: 'letters',
label: 'Letters',
expected: scenario => scenario.callsignNato,
placeholder: 'Delta Lima Hotel',
width: 'xl',
threshold: 0.9
},
{
key: 'digits',
label: 'Numbers',
expected: scenario => scenario.flightNumberWords,
placeholder: 'one two three',
width: 'lg',
threshold: 0.88
}
],
readback: [
{ type: 'text', text: 'Call sign: ' },
{ type: 'field', key: 'letters', width: 'lg' },
{ type: 'text', text: ' · ' },
{ type: 'field', key: 'digits', width: 'md' }
],
defaultFrequency: 'DEL',
phrase: scenario => `${scenario.callsignNato} ${scenario.flightNumberWords}`,
info: scenario => [
`Callsign: ${scenario.callsign}`,
`Radio call: ${scenario.radioCall}`,
`ICAO spelling: ${scenario.callsignNato}`,
`Flight number spoken: ${scenario.flightNumberWords}`
],
generate: createBaseScenario
},
{
id: 'radio-call',
title: 'Radio Call Sign',
desc: 'Say the spoken and ICAO call sign',
keywords: ['Basics', 'Callsign'],
hints: [
'Use the airline telephony plus digits for the spoken version.',
'ICAO format keeps the three-letter code with the numbers.'
],
fields: [
{
key: 'radio-call-spoken',
label: 'Spoken Call Sign',
expected: scenario => scenario.radioCall,
alternatives: scenario => [
scenario.radioCall,
`${scenario.airlineCall} ${scenario.flightNumber}`,
`${scenario.airlineCall} ${scenario.flightNumberWords}`
],
width: 'lg'
},
{
key: 'radio-call-icao',
label: 'ICAO Identifier',
expected: scenario => scenario.callsign,
alternatives: scenario => [
scenario.callsign,
`${scenario.airlineCode}${scenario.flightNumber}`
],
width: 'md'
}
],
readback: [
{ type: 'text', text: 'Call sign ' },
{ type: 'field', key: 'radio-call-spoken', width: 'lg' },
{ type: 'text', text: ' · ICAO ' },
{ type: 'field', key: 'radio-call-icao', width: 'md' }
],
defaultFrequency: 'DEL',
phrase: scenario => `${scenario.radioCall}`,
info: scenario => [
`Spoken call sign: ${scenario.radioCall}`,
`ICAO identifier: ${scenario.callsign}`
],
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 a single letter.',
'Order: runway – wind – visibility – temperature – dew point – QNH.'
],
fields: [
{
key: 'atis-code',
label: 'ATIS',
expected: scenario => scenario.atisCode,
alternatives: scenario => [
scenario.atisCode,
scenario.atisCode.toLowerCase(),
`Information ${scenario.atisCode}`
],
placeholder: '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.atisSummary.visibility,
alternatives: scenario => [scenario.visibility, scenario.visibilityWords],
width: 'sm'
},
{
key: 'atis-temp',
label: 'Temperature',
expected: scenario => scenario.atisSummary.temperature,
alternatives: scenario => [scenario.temperatureWords],
width: 'sm'
},
{
key: 'atis-dew',
label: 'Dew point',
expected: scenario => scenario.atisSummary.dewpoint,
alternatives: scenario => [scenario.dewpointWords],
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, runway, wind, visibility, temperature, dew point, and QNH.',
'Visibility: four digits or 9999 for ≥10 km; QNH as a four-digit value.'
],
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: '9999',
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: 'Report readability',
keywords: ['Ground', 'Comms'],
hints: [
'Reply with "Readability" followed by the number.',
'Always finish the check with your call sign.'
],
fields: [
{
key: 'rc-callsign',
label: 'Callsign',
expected: scenario => scenario.radioCall,
alternatives: scenario => [
scenario.radioCall,
`${scenario.airlineCall} ${scenario.flightNumber}`,
scenario.callsign
],
placeholder: 'Lufthansa one two three',
width: 'lg'
},
{
key: 'rc-readability',
label: 'Readability',
expected: scenario => scenario.readabilityWord,
alternatives: scenario => [scenario.readability.toString(), scenario.readabilityWord],
placeholder: 'five',
width: 'sm'
}
],
readback: [
{ type: 'text', text: 'This is ' },
{ type: 'field', key: 'rc-callsign', width: 'lg' },
{ type: 'text', text: ', readability ' },
{ type: 'field', key: 'rc-readability', width: 'sm' }
],
defaultFrequency: 'GND',
phrase: scenario => `${scenario.airport.name} Ground, ${scenario.radioCall}, radio check on ${scenario.groundFreq}.`,
info: scenario => [
`Frequency: ${scenario.groundFreq} (${scenario.frequencyWords.GND})`,
`Expected response: Readability ${scenario.readability} (${scenario.readabilityWord})`
],
generate: createBaseScenario
}
]
const readbackLessons = [
{
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`
],
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: '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: runway – cleared for takeoff – call sign.',
'Wind information can be omitted if it was not given.'
],
fields: [
{
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: '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: [
'Lead with the runway, then "cleared to land".',
'Keep the call sign at the end.'
],
fields: [
{
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: '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 = [
{
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 => `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 => `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 = [
{
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: runway – cleared for takeoff – call sign.'
],
fields: [
{
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: '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: [
'Lead with the runway, then "cleared to land".'
],
fields: [
{
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: '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 · Mandatory Acknowledgements',
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