feat(live-atc): drive ATIS letter, runway, QNH and wind from the resolved report

The broadcast and the flow variables used to read different sources: the audio
came from VATSIM while `information` was genATIS() — a random A-Z letter — and
the runway came from genRunway(), a hard-coded list unrelated to the airport.
So the pilot could hear "information Q" and be expected to call "information T".

Both now read the same AtisReport. The frequency list backfills the letter and
broadcast text from it, so an ATIS station always announces an information
letter even when no VATSIM controller is online. QNH and surface wind come from
the same observation, so the controller stops contradicting the ATIS.

A synthesised ATIS is labelled "Simulated ATIS" in the frequency picker rather
than being credited to VATSIM.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
itsrubberduck
2026-07-26 19:40:40 +02:00
parent bcf9a9621c
commit 8b62b8e3aa
4 changed files with 140 additions and 42 deletions

View File

@@ -55,6 +55,10 @@ export interface AtisReport {
runwayDep: string | null
runwayArr: string | null
runwaySource: 'atis' | 'wind' | 'main' | null
/** Observed QNH, so the flow quotes the same pressure the ATIS just read. */
qnhHpa: number | null
/** Observed surface wind as "260/14", or null without an observation. */
surfaceWind: string | null
observedAt: string | null
stations: AtisStation[]
}
@@ -382,6 +386,8 @@ export function resolveAtisReport(input: ResolveAtisInput): AtisReport {
runwaySource: fromText.dep || fromText.arr
? 'atis'
: byWind.dep?.source ?? byWind.arr?.source ?? null,
qnhHpa: observation?.qnhHpa ?? null,
surfaceWind: formatSurfaceWind(observation),
observedAt,
stations: liveStations,
}
@@ -413,6 +419,8 @@ export function resolveAtisReport(input: ResolveAtisInput): AtisReport {
runwayDep: dep?.designator ?? null,
runwayArr: arr?.designator ?? null,
runwaySource: dep?.source ?? arr?.source ?? null,
qnhHpa: observation?.qnhHpa ?? null,
surfaceWind: formatSurfaceWind(observation),
observedAt,
stations: [{
frequency: silentStation?.frequency || input.atisFrequency || FREQUENCY_UNKNOWN,
@@ -424,6 +432,20 @@ export function resolveAtisReport(input: ResolveAtisInput): AtisReport {
}
}
/** Surface wind in the "260/14" form the flows use, or null when unobserved. */
function formatSurfaceWind(observation: MetarObservation | null): string | null {
if (!observation || observation.windKt === null) return null
const direction = observation.windVariable
? 'VRB'
: observation.windDeg === null ? null : pad3(observation.windDeg)
if (direction === null) return null
return `${direction}/${pad2(observation.windKt)}`
}
function pad3(value: number): string {
return String(value).padStart(3, '0')
}
/**
* Absolute time for a METAR day-of-month stamp, resolved against `now`.
* A report near a month boundary belongs to the previous month.