import type { AirlineData, AirportData, Frequency, FrequencyType, Scenario } from './types' const natoMap: Record = { A: 'Alpha', B: 'Bravo', C: 'Charlie', D: 'Delta', E: 'Echo', F: 'Foxtrot', G: 'Golf', H: 'Hotel', I: 'India', J: 'Juliett', K: 'Kilo', L: 'Lima', M: 'Mike', N: 'November', O: 'Oscar', P: 'Papa', Q: 'Quebec', R: 'Romeo', S: 'Sierra', T: 'Tango', U: 'Uniform', V: 'Victor', W: 'Whiskey', X: 'X-ray', Y: 'Yankee', Z: 'Zulu' } const atcNumberWords: Record = { '0': 'zero', '1': 'one', '2': 'two', '3': 'tree', '4': 'four', '5': 'fife', '6': 'six', '7': 'seven', '8': 'eight', '9': 'niner' } const runwaySuffixWords: Record = { L: 'left', R: 'right', C: 'center' } const readabilityScale = [ { level: 1, word: 'one', description: 'Unreadable' }, { level: 2, word: 'two', description: 'Barely readable' }, { level: 3, word: 'three', description: 'Readable with difficulty' }, { level: 4, word: 'four', description: 'Readable' }, { level: 5, word: 'five', description: 'Perfectly readable' } ] const airlines: AirlineData[] = [ { code: 'DLH', call: 'Lufthansa' }, { code: 'BAW', call: 'Speedbird' }, { code: 'AFR', call: 'Air France' }, { code: 'KLM', call: 'KLM' }, { code: 'SWR', call: 'Swiss' }, { code: 'EZY', call: 'Easyjet' } ] const airportsData: AirportData[] = [ { icao: 'EDDF', name: 'Frankfurt/Main', city: 'Frankfurt', runways: ['25C', '25R', '07C', '07R', '18'], stands: ['V155', 'A12', 'B24', 'G5', 'H43', 'L21'], sids: ['ANEKI 7S', 'TOBAK 5Q', 'OBOKA 6N'], transitions: ['ANEKI', 'TOBAK', 'OBOKA'], approaches: ['ILS Z 25C', 'ILS Y 07C'], taxi: ['N3 U4', 'V A', 'N7 K', 'S V12'], freqs: { atis: '126.350', delivery: '121.900', ground: '121.800', tower: '118.700', departure: '125.350', approach: '120.800', center: '134.200' }, transLevel: 'FL070' }, { icao: 'EDDM', name: 'Munich', city: 'Munich', runways: ['26R', '26L', '08R', '08L'], stands: ['211', '214', '302', 'N16', 'H45'], sids: ['OBAXA 3S', 'MERSI 6S', 'TULSI 5M'], transitions: ['OBAXA', 'MERSI', 'TULSI'], approaches: ['ILS Z 26R', 'ILS Y 08R'], taxi: ['L4 N3', 'P3 W2', 'S1 D2'], freqs: { atis: '122.130', delivery: '121.775', ground: '121.800', tower: '118.700', departure: '129.050', approach: '120.800', center: '133.700' }, transLevel: 'FL070' }, { icao: 'EHAM', name: 'Amsterdam Schiphol', city: 'Amsterdam', runways: ['24', '36L', '18C', '09'], stands: ['D14', 'E22', 'F8', 'H4'], sids: ['SUGOL 2S', 'ANDIK 2S', 'ARNEM 2V'], transitions: ['SUGOL', 'ANDIK', 'ARNEM'], approaches: ['ILS Z 24', 'ILS Y 18C'], taxi: ['A5 B2', 'K1 V3', 'W4 S8'], freqs: { atis: '136.050', delivery: '121.800', ground: '121.900', tower: '119.220', departure: '123.875', approach: '121.200', center: '135.050' }, transLevel: 'FL060' } ] const atisLetters = 'ABCDEFGHJKLMNOPQRSTUVWXYZ'.split('') function choice(items: T[]): T { return items[Math.floor(Math.random() * items.length)] } function randInt(min: number, max: number): number { return Math.floor(Math.random() * (max - min + 1)) + min } function randomFlightNumber(): string { const length = Math.random() < 0.5 ? 3 : 4 let value = '' for (let i = 0; i < length; i++) { value += String(randInt(0, 9)) } if (value.startsWith('0')) value = '1' + value.slice(1) return value } function generateSquawk(): string { let code = '' for (let i = 0; i < 4; i++) { code += String(randInt(0, 7)) } return code } function digitsToWords(value: string): string { return value .split('') .map(char => atcNumberWords[char] ?? char) .join(' ') .replace(/\s+/g, ' ') .trim() } function lettersToNato(value: string): string { return value .toUpperCase() .split('') .map(char => natoMap[char] ?? char) .join(' ') } function runwayToWords(runway: string): string { const digits = runway.replace(/[^0-9]/g, '').padStart(2, '0') const suffix = runway.replace(/[0-9]/g, '') const base = digits .split('') .map(char => atcNumberWords[char] ?? char) .join(' ') const suffixWord = suffix ? runwaySuffixWords[suffix] ?? suffix.toLowerCase() : '' return suffixWord ? `${base} ${suffixWord}` : base } function frequencyToSpeech(freq: string): string { const [intPart, decimalPart] = freq.split('.') const intWords = digitsToWords(intPart) if (!decimalPart) return intWords const trimmed = decimalPart.replace(/0+$/, '') const decWords = trimmed ? digitsToWords(trimmed) : 'zero' return `${intWords} decimal ${decWords}` } export function formatTemp(temp: number): string { const prefix = temp < 0 ? 'M' : '' return `${prefix}${Math.abs(temp).toString().padStart(2, '0')}` } function temperatureToWords(temp: number): string { const prefix = temp < 0 ? 'minus' : 'plus' return `${prefix} ${digitsToWords(Math.abs(temp).toString())}` } function qnhToWords(qnh: number): string { return digitsToWords(qnh.toString()) } function windToWords(direction: number, speed: number): string { const dir = direction.toString().padStart(3, '0') const spd = speed.toString().padStart(2, '0') return `${dir .split('') .map(char => atcNumberWords[char] ?? char) .join(' ')} degrees at ${spd .split('') .map(char => atcNumberWords[char] ?? char) .join(' ')} knots` } function visibilityToWords(vis: string): string { if (vis === '9999') return 'ten kilometres or more' const numeric = Number(vis) if (!Number.isNaN(numeric)) { if (numeric >= 1000) { const km = Math.round(numeric / 1000) return `${digitsToWords(km.toString())} kilometres` } return `${digitsToWords(vis)} metres` } return vis } function altitudeToWords(value: number): string { const thousands = Math.floor(value / 1000) const remainder = value % 1000 let words = thousands ? `${digitsToWords(thousands.toString())} thousand` : '' if (remainder) { words = `${words} ${digitsToWords(remainder.toString())}`.trim() } return words.trim() } export function createBaseScenario(): Scenario { const airport = choice(airportsData) const possibleDestinations = airportsData.filter(a => a.icao !== airport.icao) const destination = choice(possibleDestinations) const airline = choice(airlines) const flightNumber = randomFlightNumber() const callsign = `${airline.code}${flightNumber}` const runway = choice(airport.runways) const stand = choice(airport.stands) const taxiRoute = choice(airport.taxi) const sid = choice(airport.sids) const transition = choice(airport.transitions) const approach = choice(airport.approaches) const altitude = choice([4000, 5000, 6000, 7000]) const climbAltitude = altitude + 2000 const squawk = generateSquawk() const qnh = randInt(984, 1032) const windDirection = randInt(0, 35) * 10 const windSpeed = randInt(3, 18) const windDirectionStr = windDirection.toString().padStart(3, '0') const windSpeedStr = windSpeed.toString().padStart(2, '0') const visibility = choice(['9999', '9000', '8000', '6000']) const cloud = choice(['SKC', 'FEW020', 'SCT025', 'BKN030']) const temperature = randInt(-3, 28) const dewpoint = Math.max(temperature - randInt(2, 6), -10) const atisCode = choice(atisLetters) const remarks = choice(['NOSIG', 'BECMG 4000', 'TEMPO -SHRA']) const now = new Date() const minute = Math.floor(now.getUTCMinutes() / 5) * 5 const timestamp = `${now.getUTCDate().toString().padStart(2, '0')}${now .getUTCHours() .toString() .padStart(2, '0')}${minute.toString().padStart(2, '0')}Z` const metarWindGroup = `${windDirectionStr}${windSpeedStr}KT` const tempGroup = `${formatTemp(temperature)}/${formatTemp(dewpoint)}` const metar = `${airport.icao} ${timestamp} ${metarWindGroup} ${visibility} ${cloud} ${tempGroup} Q${qnh .toString() .padStart(4, '0')} ${remarks}` const frequencies: Frequency[] = [ { type: 'ATIS', label: 'ATIS', value: airport.freqs.atis }, { type: 'DEL', label: 'Delivery', value: airport.freqs.delivery }, { type: 'GND', label: 'Ground', value: airport.freqs.ground }, { type: 'TWR', label: 'Tower', value: airport.freqs.tower }, { type: 'DEP', label: 'Departure', value: airport.freqs.departure }, { type: 'APP', label: 'Approach', value: airport.freqs.approach }, { type: 'CTR', label: 'Center', value: airport.freqs.center } ] const frequencyWords = frequencies.reduce((acc, freq) => { acc[freq.type] = frequencyToSpeech(freq.value) return acc }, {} as Record) const readability = choice(readabilityScale) const atisText = `${airport.name} information ${atisCode}, time ${timestamp.slice(2, 4)}${timestamp.slice(4, 6)}, runway ${ runwayToWords(runway) } in use, wind ${windToWords(windDirection, windSpeed)}, visibility ${visibilityToWords(visibility)}, temperature ${ temperatureToWords(temperature) }, dew point ${temperatureToWords(dewpoint)}, QNH ${qnh}, transition level ${airport.transLevel.replace('FL', '')}.` return { callsign, airlineCode: airline.code, airlineCall: airline.call, radioCall: `${airline.call} ${digitsToWords(flightNumber)}`, callsignNato: lettersToNato(airline.code), flightNumber, flightNumberWords: digitsToWords(flightNumber), airport, destination, runway, runwayWords: runwayToWords(runway), stand, taxiRoute, sid, transition, approach, altitudes: { initial: altitude, climb: climbAltitude, initialWords: altitudeToWords(altitude), climbWords: altitudeToWords(climbAltitude) }, squawk, squawkWords: digitsToWords(squawk), qnh, qnhWords: qnhToWords(qnh), atisCode, atisText, atisSummary: { runway, wind: `${windDirectionStr}/${windSpeedStr}`, visibility, temperature: `${temperature}°C`, dewpoint: `${dewpoint}°C`, qnh: `QNH ${qnh}` }, wind: `${windDirectionStr}/${windSpeedStr}`, windWords: windToWords(windDirection, windSpeed), visibility, visibilityWords: visibilityToWords(visibility), temperature, temperatureWords: temperatureToWords(temperature), dewpoint, dewpointWords: temperatureToWords(dewpoint), metar, metarSegments: { wind: metarWindGroup, visibility, temp: tempGroup, qnh: `Q${qnh.toString().padStart(4, '0')}` }, readability: readability.level, readabilityWord: readability.word, readabilityPhrase: `Readability ${readability.word}`, frequencies, frequencyWords, atisFreq: airport.freqs.atis, deliveryFreq: airport.freqs.delivery, groundFreq: airport.freqs.ground, towerFreq: airport.freqs.tower, departureFreq: airport.freqs.departure, approachFreq: airport.freqs.approach, centerFreq: airport.freqs.center, transLevel: airport.transLevel, remarks } }