import { createBaseScenario, formatTemp } from '~~/shared/learn/scenario' import type { ModuleDef } from '~~/shared/learn/types' function gradientArt(colors: string[]): string { const stops = colors .map((color, idx) => ``) .join('') const svg = `${stops}` return `data:image/svg+xml;utf8,${encodeURIComponent(svg)}` } export const learnModules: ModuleDef[] = [ { id: 'normalize', title: 'Normalize · Fundamentals', subtitle: 'Alphabet, ATIS, METAR & Radio Check', art: '/img/learn/modules/img10.jpeg', lessons: [ { id: 'icao-alphabet', title: 'ICAO Alphabet & Numbers', desc: 'Spell letters and digits clearly', keywords: ['Alphabet', 'Numbers', 'Normalize'], 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: '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 } ] }, { id: 'arc', title: 'ARC Decision Tree', subtitle: 'From clearance call to departure', art: '/img/learn/modules/img2.jpeg', lessons: [ { 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: '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} (${scenario.squawkWords})` ], generate: createBaseScenario }, { id: 'pushback', title: 'Push & Start Readback', desc: 'Acknowledge the pushback clearance', keywords: ['Ground', 'Pushback'], hints: [ 'Repeat the runway direction and QNH, call sign at the end.', 'QNH may be just the number or "QNH xxxx".' ], 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/Gate: ${scenario.stand}`, `Ground: ${scenario.groundFreq} (${scenario.frequencyWords.GND})` ], 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 => [ `Tower: ${scenario.towerFreq} (${scenario.frequencyWords.TWR})`, `Wind: ${scenario.wind} (${scenario.windWords})` ], 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: 'Frequenz', 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: '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: '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: 'center-handoff', title: 'Center Handoff', desc: 'Acknowledge the handoff to center', keywords: ['Center', 'Handoff'], hints: [ 'Repeat the frequency exactly (with or without the decimal point).', 'Place the call sign at the end.' ], fields: [ { key: 'ctr-freq', label: 'Frequenz', expected: scenario => scenario.centerFreq, alternatives: scenario => [ scenario.centerFreq, scenario.centerFreq.replace('.', ''), scenario.frequencyWords.CTR ], width: 'md' }, { key: 'ctr-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: 'ctr-freq', width: 'md' }, { type: 'text', text: ', ' }, { type: 'field', key: 'ctr-callsign', width: 'lg' } ], defaultFrequency: 'CTR', phrase: scenario => `${scenario.radioCall}, contact center ${scenario.centerFreq}.`, info: scenario => [ `Center: ${scenario.centerFreq} (${scenario.frequencyWords.CTR})`, `Next unit: Center` ], generate: createBaseScenario } ] } ] export default learnModules