From f852c8f2d5fedf6c2c7f11e5642583c734a4adc5 Mon Sep 17 00:00:00 2001 From: itsrubberduck Date: Mon, 27 Jul 2026 09:59:56 +0200 Subject: [PATCH] feat(live-atc): teach the rare events on demand MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A rejected take-off and a go-around both happen by chance about once in five hundred flights, which is the point of them — you cannot practise reacting to something you were told to expect. That also made them impossible to teach. Two switches under Special scenarios force either on the next flight. They are read when the session is created rather than watched, so flipping one mid-flight applies to the following one — which is how an instructor sets it up without the pilot seeing it coming. Only a switched-on value is sent: the backend reads the variable's absence as "leave it to chance", so sending false would pin the roll off instead of leaving it alone. Styled amber rather than the usual cyan, and the consequence is spelled out while either is armed: neither scenario continues to the stand. --- .../live-atc/cockpit/SettingsSheet.vue | 38 +++++++++++++++++++ app/composables/useLiveAtcSession.ts | 16 +++++++- app/pages/live-atc.vue | 9 +++++ 3 files changed, 62 insertions(+), 1 deletion(-) diff --git a/app/components/live-atc/cockpit/SettingsSheet.vue b/app/components/live-atc/cockpit/SettingsSheet.vue index d381e65..ed3c14e 100644 --- a/app/components/live-atc/cockpit/SettingsSheet.vue +++ b/app/components/live-atc/cockpit/SettingsSheet.vue @@ -21,6 +21,8 @@ const prerecEnabled = defineModel('prerecEnabled', { required: true }) const prerecSeconds = defineModel('prerecSeconds', { required: true }) const aiTrafficEnabled = defineModel('aiTrafficEnabled', { required: true }) const autoTuneEnabled = defineModel('autoTuneEnabled', { required: true }) +const forceRto = defineModel('forceRto', { required: true }) +const forceGoAround = defineModel('forceGoAround', { required: true }) /** * Shown while AI traffic is on. These are deliberate v1 boundaries from the @@ -170,6 +172,42 @@ const AI_TRAFFIC_LIMITS = [ + +
+
+

Special scenarios

+

+ Force a rare event on the next flight instead of waiting for it. Both + otherwise occur by chance, about once in five hundred flights. +

+
+ + + +

+ Applies when you start the next flight. A rejected take-off ends with + the aircraft stopped on the runway; a go-around sends you back to + Approach — neither continues to the stand. +

+
+
+

Voice input

diff --git a/app/composables/useLiveAtcSession.ts b/app/composables/useLiveAtcSession.ts index 0181a6c..dd44ff4 100644 --- a/app/composables/useLiveAtcSession.ts +++ b/app/composables/useLiveAtcSession.ts @@ -36,6 +36,13 @@ export interface LiveAtcSessionDeps { isRecording: Ref /** Settings toggle: dial in the new frequency automatically after a handoff. */ autoTuneEnabled: Ref + /** + * Special-scenario switches. Read once when the session is created, so + * toggling one mid-flight applies to the next flight — which is how an + * instructor sets one up without the pilot seeing it coming. + */ + forceRto: Ref + forceGoAround: Ref bridgeConnected: Ref bridgePosition: Ref<{ lat: number; lon: number } | null> /** ?token=… from the route — auths the frequency-sim-control channel (design §4). */ @@ -65,7 +72,8 @@ export function useLiveAtcSession( const { state, freq, speech, radioBackend, api, config, prefetchAtisAudio, - isRecording, autoTuneEnabled, bridgeConnected, bridgePosition, bridgeToken, + isRecording, autoTuneEnabled, forceRto, forceGoAround, + bridgeConnected, bridgePosition, bridgeToken, persistSelectedPlan, maybeShowFirstRunHelp, } = deps @@ -696,6 +704,12 @@ export function useLiveAtcSession( handoff_freq: v.handoff_freq || '131.150', } + // Special scenarios. Only sent when switched on: the backend treats the + // variable's absence as "leave it to chance", and sending false would pin + // the roll off rather than leaving it alone. + if (forceRto.value) backendVariables.force_rto = true + if (forceGoAround.value) backendVariables.force_go_around = true + // VFR flights identify by aircraft registration, not an airline callsign. // Assign a German D-registration and its abbreviated form (D-EMIL -> D-IL, // first letter + last two), which ATC uses after the first call. Mirror it diff --git a/app/pages/live-atc.vue b/app/pages/live-atc.vue index 734c08c..3752535 100644 --- a/app/pages/live-atc.vue +++ b/app/pages/live-atc.vue @@ -142,6 +142,8 @@ v-model:prerec-seconds="prerecSeconds" v-model:ai-traffic-enabled="aiTrafficEnabled" v-model:auto-tune-enabled="autoTuneEnabled" + v-model:force-rto="forceRto" + v-model:force-go-around="forceGoAround" @set-theme="setPmTheme" /> @@ -321,6 +323,11 @@ const aiTrafficEnabled = ref(false) // working the radio is part of what is being practised, so turning it over to // the aircraft has to be a deliberate choice. const autoTuneEnabled = ref(false) +// Special scenarios: force a rare intervention on the next flight rather than +// waiting for its ~0.2% chance. Read when the session is created, so toggling +// mid-flight affects the next one — which is also how you teach them. +const forceRto = ref(false) +const forceGoAround = ref(false) // ── Bug Report ─────────────────────────────────────────────────────────────── // Owned here rather than by the dialog: the HUD button starts the screenshot @@ -599,6 +606,8 @@ const session = useLiveAtcSession(engine, { prefetchAtisAudio, isRecording, autoTuneEnabled, + forceRto, + forceGoAround, bridgeConnected, bridgePosition, bridgeToken,