feat(live-atc): teach the rare events on demand

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.
This commit is contained in:
itsrubberduck
2026-07-27 09:59:56 +02:00
parent 67541a48b6
commit f852c8f2d5
3 changed files with 62 additions and 1 deletions

View File

@@ -21,6 +21,8 @@ const prerecEnabled = defineModel<boolean>('prerecEnabled', { required: true })
const prerecSeconds = defineModel<number>('prerecSeconds', { required: true })
const aiTrafficEnabled = defineModel<boolean>('aiTrafficEnabled', { required: true })
const autoTuneEnabled = defineModel<boolean>('autoTuneEnabled', { required: true })
const forceRto = defineModel<boolean>('forceRto', { required: true })
const forceGoAround = defineModel<boolean>('forceGoAround', { required: true })
/**
* Shown while AI traffic is on. These are deliberate v1 boundaries from the
@@ -170,6 +172,42 @@ const AI_TRAFFIC_LIMITS = [
</v-expand-transition>
</div>
<!-- Both of these happen on their own roughly once in five hundred
flights, which is the point of them you cannot practise reacting
to something you were told to expect. These switches are for
teaching them deliberately, so they apply to the NEXT flight rather
than the one in progress. -->
<div class="pt-2 border-t border-white/10 space-y-3">
<div>
<p class="text-xs uppercase tracking-[0.3em] text-white/40">Special scenarios</p>
<p class="text-[11px] text-white/50 mt-1">
Force a rare event on the next flight instead of waiting for it. Both
otherwise occur by chance, about once in five hundred flights.
</p>
</div>
<v-switch
v-model="forceRto"
color="amber"
inset
label="Rejected take-off (Tower cancels)"
hide-details
/>
<v-switch
v-model="forceGoAround"
color="amber"
inset
label="Go-around (Tower breaks off the approach)"
hide-details
/>
<v-expand-transition>
<p v-if="forceRto || forceGoAround" class="text-[11px] text-amber-300/80">
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.
</p>
</v-expand-transition>
</div>
<div class="pt-2 border-t border-white/10 space-y-3">
<div>
<p class="text-xs uppercase tracking-[0.3em] text-white/40">Voice input</p>

View File

@@ -36,6 +36,13 @@ export interface LiveAtcSessionDeps {
isRecording: Ref<boolean>
/** Settings toggle: dial in the new frequency automatically after a handoff. */
autoTuneEnabled: Ref<boolean>
/**
* 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<boolean>
forceGoAround: Ref<boolean>
bridgeConnected: Ref<boolean>
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

View File

@@ -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,