diff --git a/app/components/flightlab/pfd/PfdAltitudeTape.vue b/app/components/flightlab/pfd/PfdAltitudeTape.vue index d1f767f..f5f55ba 100644 --- a/app/components/flightlab/pfd/PfdAltitudeTape.vue +++ b/app/components/flightlab/pfd/PfdAltitudeTape.vue @@ -65,7 +65,7 @@ const tapeInnerHeight = computed(() => props.height - 4) y="0" :width="width" :height="height" - fill="#8f9198" + fill="#16181f" rx="1" /> props.height - 4) :y="tapeInnerY" :width="tapeInnerWidth" :height="tapeInnerHeight" - fill="#93959c" - stroke="#d2d4da" + fill="#1c1e26" + stroke="#3a3d48" stroke-width="0.8" /> - + - + props.height - 4) :width="readoutBoxWidth" :height="readoutBoxHeight" fill="#02040b" - stroke="#2fe5ff" + stroke="#19e34a" stroke-width="1.4" rx="1.5" /> 4) - + - + @@ -184,7 +184,7 @@ const dotRadius = computed(() => 4) :fill="YELLOW" /> - + 4) :fill="YELLOW" rx="1" /> + + 4) :fill="YELLOW" rx="1" /> + + diff --git a/app/components/flightlab/pfd/PfdSpeedTape.vue b/app/components/flightlab/pfd/PfdSpeedTape.vue index 9a0dcb5..dfc44e2 100644 --- a/app/components/flightlab/pfd/PfdSpeedTape.vue +++ b/app/components/flightlab/pfd/PfdSpeedTape.vue @@ -8,10 +8,14 @@ const props = withDefaults(defineProps<{ speed: number speedTrend?: number targetRange?: SpeedTargetRange + vfeSpeed?: number + minSpeed?: number width?: number height?: number }>(), { speedTrend: 0, + vfeSpeed: 340, + minSpeed: 130, width: 70, height: 300, }) @@ -96,7 +100,7 @@ const tapeInnerHeight = computed(() => props.height - 4) y="0" :width="width" :height="height" - fill="#8f9198" + fill="#16181f" rx="1" /> props.height - 4) :y="tapeInnerY" :width="tapeInnerWidth" :height="tapeInnerHeight" - fill="#93959c" - stroke="#d2d4da" + fill="#1c1e26" + stroke="#3a3d48" stroke-width="0.8" /> - + + + + + + + + + props.height - 4) :y1="targetZone.top" :x2="width - 1" :y2="targetZone.top" - stroke="rgba(254, 202, 202, 0.95)" + stroke="rgba(45, 229, 255, 0.5)" stroke-width="1" /> props.height - 4) :y1="targetZone.bottom" :x2="width - 1" :y2="targetZone.bottom" - stroke="rgba(254, 202, 202, 0.95)" + stroke="rgba(45, 229, 255, 0.5)" stroke-width="1" /> @@ -171,7 +212,7 @@ const tapeInnerHeight = computed(() => props.height - 4) :y1="centerY" :x2="width / 2" :y2="trendLineEndY" - stroke="#30e3ff" + stroke="#19e34a" stroke-width="2" opacity="0.8" /> @@ -184,14 +225,14 @@ const tapeInnerHeight = computed(() => props.height - 4) :width="readoutBoxWidth" :height="readoutBoxHeight" fill="#02040b" - stroke="#2fe5ff" + stroke="#19e34a" stroke-width="1.4" rx="1.5" /> { diff --git a/docs/plans/2026-02-21-pfd-realism-design.md b/docs/plans/2026-02-21-pfd-realism-design.md new file mode 100644 index 0000000..3efeb93 --- /dev/null +++ b/docs/plans/2026-02-21-pfd-realism-design.md @@ -0,0 +1,79 @@ +# PFD Realism & Training Improvements + +## Context + +Comparing the simulator PFD to the real Airbus A320 PFD reveals styling inaccuracies in the elements we use. Additionally, the flight physics react too quickly (no inertia), and the speed-hold exercise doesn't teach energy management effectively. Pitch/bank exercises use values outside normal flight envelopes. + +## 1. PFD Styling Corrections + +### Attitude Indicator +- Sky: gradient `#0a4a8a` (top) → `#22a7eb` (horizon) instead of flat `#22a7eb` +- Ground: gradient `#b26113` (horizon) → `#5a2e08` (bottom) instead of flat `#b26113` +- Aircraft symbol: make W-shaped (small droop at wing ends) + +### Speed Tape +- Background: `#16181f` instead of `#93959c` +- Inner rect: `#1c1e26`, border `#3a3d48` instead of `#93959c`/`#d2d4da` +- Readout text/border: green `#19e34a` instead of cyan `#2fe5ff` + +### Altitude Tape +- Same dark background as speed tape +- Readout: green instead of cyan +- Tick marks on LEFT side (toward attitude indicator), labels right of ticks + +### Vertical Speed +- Channel fill: darken to match tape style +- Already uses green — correct + +### Heading Indicator +- Background: dark (`#16181f`) instead of `#8f9198`/`#94969d` +- Inner: `#1c1e26` with border `#3a3d48` +- Yellow readout stays (correct for selected heading on Airbus) + +## 2. Realistic Flight Handling + +### Pitch Inertia +- `PITCH_RATE_PER_G_DELTA`: 3.5 → 1.2 (3x slower response) +- Add exponential smoothing on pitch rate (tau ~2s) so changes ramp up gradually + +### Roll Inertia +- `MAX_ROLL_RATE`: 15 → 7 deg/s +- `ROLL_RETURN_RATE`: 5 → 2 deg/s + +### Speed-Pitch Coupling +- `SPEED_PITCH_COUPLING`: 1.2 → 2.0 (pitch changes affect speed more, forcing planning) + +## 3. Training Exercise Improvements + +### Pitch/Bank: Normal Ranges +- `horizon_intro` bank target: -20° → -10° (tolerance 8°) +- `horizon_roll_right` bank target: 20° → 10° (tolerance 8°) +- `pitch_intro` target: 10° → 4° (tolerance 3°) +- `pitch_down` target: -5° → -3° (tolerance 3°) +- ATC messages emphasize small, gentle inputs + +### Speed Tape: Correct Color Semantics +- Target hold zone: cyan/turquoise (NOT red — red = danger on real PFD) +- Add VFE (overspeed) red band at top (~340 kts) +- Add minimum speed red band at bottom (~130 kts) +- Red bands only visible when approaching dangerous speeds + +### Speed Exercise: Multi-Phase +Split single `speed_hold_pitch` into 4 phases: + +1. **`speed_explain`** — Explanation only. "Beim Airbus kontrollierst du Geschwindigkeit mit Pitch. Nase hoch = langsamer, Nase runter = schneller." Button: "Verstanden". + +2. **`speed_set_throttle`** — Set throttle to 70% (existing phase, unchanged). + +3. **`speed_hold_coarse`** — Coarse hold: speed 215-235 kts, hold 5s. Wide tolerance, first success. + +4. **`speed_hold_fine`** — Fine hold: speed 225 ±5 kts, hold 8s. ATC emphasizes tiny corrections only. + +Update `goalNextPhase` mapping accordingly: +- `speed_intro` → `speed_explain` (was → `speed_hold_pitch`) +- `speed_explain` → `speed_set_throttle` (new, button-driven) +- `speed_set_throttle` → `speed_hold_coarse` (new) +- `speed_hold_coarse` → `speed_hold_fine` (new) +- `speed_hold_fine` → `alt_intro` (was `speed_hold_pitch` → `alt_intro`) + +Also update `mainPhaseIds` to include new speed phases for progress tracking. diff --git a/docs/plans/2026-02-21-pfd-realism-implementation.md b/docs/plans/2026-02-21-pfd-realism-implementation.md new file mode 100644 index 0000000..4ff310b --- /dev/null +++ b/docs/plans/2026-02-21-pfd-realism-implementation.md @@ -0,0 +1,453 @@ +# PFD Realism & Training Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Make the PFD visually match the real Airbus A320, add realistic flight inertia, and improve training exercises with normal-range targets and a multi-phase speed lesson. + +**Architecture:** Pure frontend changes across 3 layers: SVG styling in Vue SFC components, physics constants + smoothing in the FBW composable, and phase data in the learn-pfd scenario definition + engine. + +**Tech Stack:** Vue 3 SFC (SVG), TypeScript composables, Node test runner (tsx) + +--- + +### Task 1: Darken Speed Tape colors + +**Files:** +- Modify: `app/components/flightlab/pfd/PfdSpeedTape.vue` + +**Step 1: Update background colors** + +Change outer rect fill from `#8f9198` to `#16181f`. +Change inner rect fill from `#93959c` to `#1c1e26`, stroke from `#d2d4da` to `#3a3d48`. + +**Step 2: Update readout colors to green** + +Change readout box stroke from `#2fe5ff` to `#19e34a`. +Change readout text fill from `#2fe5ff` to `#19e34a`. +Change speed trend arrow stroke from `#30e3ff` to `#19e34a`. + +**Step 3: Change target zone to cyan (not red)** + +The target hold zone must NOT be red — red means danger on a real PFD. Change to cyan/turquoise: +- Target zone fill: `rgba(239, 68, 68, 0.22)` → `rgba(45, 212, 255, 0.18)` +- Target zone stroke: `rgba(248, 113, 113, 0.95)` → `rgba(45, 229, 255, 0.8)` +- Top/bottom lines: `rgba(254, 202, 202, 0.95)` → `rgba(45, 229, 255, 0.5)` + +**Step 4: Add VFE (overspeed) and minimum speed red bands** + +Add two new props: `vfeSpeed` (default 340) and `minSpeed` (default 130). + +Add red bands above VFE and below minSpeed, rendered inside the clipped tape group (before the scrolling marks): + +```html + + + + + + +``` + +The red bands only appear when they scroll into the visible tape area. At 220 kts neither will be visible, which is correct — they only show when approaching dangerous speeds. + +**Step 4: Verify visually** + +Run: `bun run dev` +Navigate to `/flightlab/medienstationen/learn-pfd`, advance to speed phase. +Confirm: dark background, green readout, visible target zone. + +**Step 5: Commit** + +``` +git add app/components/flightlab/pfd/PfdSpeedTape.vue +git commit -m "style(pfd): darken speed tape and use green readout colors" +``` + +--- + +### Task 2: Darken Altitude Tape + fix tick side + +**Files:** +- Modify: `app/components/flightlab/pfd/PfdAltitudeTape.vue` + +**Step 1: Update background colors** + +Same as speed tape: outer `#16181f`, inner `#1c1e26` with stroke `#3a3d48`. + +**Step 2: Update readout colors to green** + +Readout box stroke: `#2fe5ff` → `#19e34a`. +Readout text fill: `#2fe5ff` → `#19e34a`. + +**Step 3: Move tick marks to LEFT side** + +Change tick x1 from `width - 10` to `1`. +Change tick x2 from `width - 1` to `10`. +Move labels: x from `4` to `14`, keep `text-anchor="start"`. + +**Step 4: Verify visually** + +Confirm: dark background, green readout, ticks on left side with labels to the right of ticks. + +**Step 5: Commit** + +``` +git add app/components/flightlab/pfd/PfdAltitudeTape.vue +git commit -m "style(pfd): darken altitude tape, green readout, ticks on left" +``` + +--- + +### Task 3: Darken Heading Indicator + +**Files:** +- Modify: `app/components/flightlab/pfd/PfdHeadingIndicator.vue` + +**Step 1: Update background colors** + +Outer rect fill: `#8f9198` → `#16181f`. +Inner rect fill: `#94969d` → `#1c1e26`, stroke: `#d4d6dc` → `#3a3d48`. + +**Step 2: Verify visually** + +Yellow readout stays — already correct for Airbus selected heading. + +**Step 3: Commit** + +``` +git add app/components/flightlab/pfd/PfdHeadingIndicator.vue +git commit -m "style(pfd): darken heading indicator background" +``` + +--- + +### Task 4: Darken Vertical Speed channel + +**Files:** +- Modify: `app/components/flightlab/pfd/PfdVerticalSpeed.vue` + +**Step 1: Update channel fill** + +Channel polygon fill: `#8f9198` → `#1c1e26`, stroke: `#d2d4da` → `#3a3d48`. + +**Step 2: Verify visually** + +Green vector and labels should remain unchanged and visible against dark bg. + +**Step 3: Commit** + +``` +git add app/components/flightlab/pfd/PfdVerticalSpeed.vue +git commit -m "style(pfd): darken vertical speed channel" +``` + +--- + +### Task 5: Add sky/ground gradients to Attitude Indicator + +**Files:** +- Modify: `app/components/flightlab/pfd/PfdAttitudeIndicator.vue` + +**Step 1: Update gradient stops** + +Sky gradient: change both stops from `#22a7eb` to: +- stop offset="0%": `#0a4a8a` +- stop offset="100%": `#22a7eb` + +Ground gradient: change both stops from `#b26113` to: +- stop offset="0%": `#b26113` +- stop offset="100%": `#5a2e08` + +**Step 2: Make aircraft symbol W-shaped** + +Replace the two wing rectangles and center dot with a path that has a slight droop at wing tips. Change the wing construction: + +Current wings are flat rectangles at `cy`. Add small downward offsets at the outer wing ends to create the characteristic W-shape: + +Replace left wing rect with a polygon: +``` +points: `${cx - wingSpan - dotRadius},${cy + 3} ${cx - dotRadius - 2},${cy - wingThickness/2} ${cx - dotRadius},${cy - wingThickness/2} ${cx - dotRadius},${cy + wingThickness/2} ${cx - dotRadius - 2},${cy + wingThickness/2}` +``` + +Simpler approach: keep rects but add small "wing tip" lines descending from each wing end: +- Left tip: line from `(cx - wingSpan - dotRadius, cy)` to `(cx - wingSpan - dotRadius, cy + 5)` +- Right tip: line from `(cx + wingSpan + dotRadius, cy)` to `(cx + wingSpan + dotRadius, cy + 5)` + +Both lines yellow, stroke-width 3, stroke-linecap round. + +**Step 3: Verify visually** + +Confirm gradient visible in sky (darker blue at top) and ground (darker brown at bottom). W-shape visible on aircraft symbol. + +**Step 4: Commit** + +``` +git add app/components/flightlab/pfd/PfdAttitudeIndicator.vue +git commit -m "style(pfd): add sky/ground gradients and W-shaped aircraft symbol" +``` + +--- + +### Task 6: Add pitch inertia smoothing to FBW engine + +**Files:** +- Modify: `shared/composables/flightlab/useAirbusFBW.ts` + +**Step 1: Update constants** + +```typescript +const MAX_ROLL_RATE = 7 // was 15 +const ROLL_RETURN_RATE = 2 // was 5 +const PITCH_RATE_PER_G_DELTA = 1.2 // was 3.5 +const SPEED_PITCH_COUPLING = 2.0 // was 1.2 +``` + +**Step 2: Add smoothed pitch rate state** + +Add a `smoothedPitchRate` variable (initialized to 0) before the `tick` function: + +```typescript +let smoothedPitchRate = 0 +const PITCH_SMOOTH_TAU = 2.0 // seconds — exponential smoothing time constant +``` + +**Step 3: Apply exponential smoothing in tick** + +In the pitch section of `tick()`, replace: +```typescript +const pitchRate = gDelta * PITCH_RATE_PER_G_DELTA +state.pitch += pitchRate * dt +``` + +With: +```typescript +const rawPitchRate = gDelta * PITCH_RATE_PER_G_DELTA +const alpha = 1 - Math.exp(-dt / PITCH_SMOOTH_TAU) +smoothedPitchRate += (rawPitchRate - smoothedPitchRate) * alpha +state.pitch += smoothedPitchRate * dt +``` + +**Step 4: Reset smoothedPitchRate in reset()** + +Add `smoothedPitchRate = 0` to the `reset()` function. + +**Step 5: Verify by running dev** + +Run: `bun run dev` +Navigate to `/flightlab/medienstationen/learn-pfd`. Pull stick back — pitch should ramp up slowly over ~2-3 seconds instead of responding instantly. + +**Step 6: Commit** + +``` +git add shared/composables/flightlab/useAirbusFBW.ts +git commit -m "feat(flightlab): add realistic inertia to pitch/roll/speed response" +``` + +--- + +### Task 7: Update pitch/bank exercise targets to normal ranges + +**Files:** +- Modify: `shared/data/flightlab/learn-pfd.ts` + +**Step 1: Update horizon_intro phase** + +Change `interactionGoal` target from `-20` to `-10`, tolerance from `15` to `8`. +Update `atcMessage` to: `'Das hier ist der künstliche Horizont. Blau ist der Himmel, braun ist der Boden. Genau wie draußen. Beweg jetzt den Stick ganz sanft nach links — nur ein kleines bisschen. Im echten Flugzeug macht man immer nur kleine, sanfte Bewegungen.'` +Update `goalHint` to: `'Drück den Stick ganz leicht nach links. Nur ein kleiner Ausschlag reicht.'` + +**Step 2: Update horizon_roll_right phase** + +Change `interactionGoal` target from `20` to `10`, tolerance from `15` to `8`. +Update `atcMessage` to: `'Perfekt! Jetzt ganz sanft in die andere Richtung — Stick leicht nach rechts.'` +Update `goalHint` to: `'Stick ganz leicht nach rechts. Kleine, sanfte Bewegungen.'` + +**Step 3: Update pitch_intro phase** + +Change `interactionGoal` target from `10` to `4`, tolerance from `5` to `3`. +Update `atcMessage` to: `'Jetzt kommt was Wichtiges. Zieh den Stick ganz leicht nach hinten. Nur ein kleines bisschen. Das Besondere beim Airbus: Wenn du loslässt, hält er die Lage. Und das Flugzeug reagiert langsam — du musst immer vorausdenken.'` +Update `goalHint` to: `'Zieh den Stick nur ganz leicht nach hinten. Beim echten Airbus macht man nur minimale Bewegungen.'` + +**Step 4: Update pitch_down phase** + +Change `interactionGoal` target from `-5` to `-3`, tolerance from `5` to `3`. +Update `atcMessage` to: `'Gut gemacht! Jetzt drück den Stick ganz leicht nach vorne. Auch hier gilt: kleine Bewegungen, und Geduld — das Flugzeug braucht ein paar Sekunden bis es reagiert.'` +Update `goalHint` to: `'Stick ganz leicht nach vorne drücken und warten. Das Flugzeug reagiert mit Verzögerung.'` + +**Step 5: Commit** + +``` +git add shared/data/flightlab/learn-pfd.ts +git commit -m "feat(flightlab): use normal-range targets for pitch/bank exercises" +``` + +--- + +### Task 8: Add multi-phase speed exercise + +**Files:** +- Modify: `shared/data/flightlab/learn-pfd.ts` +- Modify: `shared/composables/flightlab/useLearnPfdEngine.ts` + +**Step 1: Replace speed_hold_pitch with 3 new phases in learn-pfd.ts** + +Remove the existing `speed_hold_pitch` phase object. Insert 3 new phase objects after `speed_intro`: + +```typescript +// --- Phase 7a: Speed Explain --- +{ + id: 'speed_explain', + atcMessage: 'Bevor wir loslegen, eine wichtige Regel: Beim Airbus kontrollierst du die Geschwindigkeit mit der Nase. Nase hoch — du wirst langsamer. Nase runter — du wirst schneller. Der Schub bleibt dabei immer gleich. Das nennt man Energie-Management.', + explanation: 'Im echten Flug hält der Autopilot den Schub konstant. Der Pilot steuert die Geschwindigkeit über den Pitch.', + visibleElements: ['attitude', 'speedTape'], + layoutMode: 'split', + buttons: [ + { id: 'understood', label: 'Verstanden', icon: 'mdi-check', next: 'speed_set_throttle', type: 'primary' }, + ], +}, + +// --- Phase 7b: Speed Set Throttle (existing speed_intro content, target stays) --- +// speed_intro already handles throttle setting — keep as is + +// --- Phase 7c: Speed Hold Coarse --- +{ + id: 'speed_hold_coarse', + atcMessage: 'Gut, Schub steht. Jetzt versuch die Geschwindigkeit in den türkisen Bereich zu bringen. Nase ganz leicht hoch oder runter — und dann warten. Das Flugzeug braucht mehrere Sekunden bis sich die Speed ändert.', + explanation: 'Erste Annäherung: Bringe die Speed grob in den Zielbereich.', + visibleElements: ['attitude', 'speedTape'], + layoutMode: 'split', + interactionGoal: { parameter: 'speed', target: 225, tolerance: 10, holdMs: 5000 }, + speedTargetRange: { min: 215, max: 235 }, + goalTimeoutMs: 30000, + goalHint: 'Korrigiere nur mit ganz kleinen Pitch-Bewegungen. Die Speed ändert sich langsam — hab Geduld.', + autoAdvanceAfterTTS: false, + buttons: [ + { id: 'skip_coarse', label: 'Weiter', icon: 'mdi-arrow-right', next: 'speed_hold_fine', type: 'primary' }, + ], +}, + +// --- Phase 7d: Speed Hold Fine --- +{ + id: 'speed_hold_fine', + atcMessage: 'Sehr gut! Jetzt genauer. Halte die Geschwindigkeit möglichst exakt auf 225 Knoten. Nur winzige Korrekturen — und immer ein paar Sekunden warten bevor du nachkorrigierst.', + explanation: 'Feinsteuerung: Präzise Speed-Kontrolle mit minimalen Pitch-Änderungen.', + visibleElements: ['attitude', 'speedTape'], + layoutMode: 'split', + interactionGoal: { parameter: 'speed', target: 225, tolerance: 5, holdMs: 8000 }, + speedTargetRange: { min: 220, max: 230 }, + goalTimeoutMs: 40000, + goalHint: 'Ganz kleine Pitch-Korrekturen. Warte nach jeder Korrektur 3-4 Sekunden bevor du nachjustierst.', + autoAdvanceAfterTTS: false, + buttons: [ + { id: 'skip_fine', label: 'Weiter', icon: 'mdi-arrow-right', next: 'alt_intro', type: 'primary' }, + ], +}, +``` + +**Step 2: Update speed_intro phase** + +Change the `speed_intro` button's `next` from `'speed_hold_pitch'` to `'speed_explain'`. + +**Step 3: Update goalNextPhase in useLearnPfdEngine.ts** + +Replace: +```typescript +'speed_intro': 'speed_hold_pitch', +'speed_hold_pitch': 'alt_intro', +``` + +With: +```typescript +'speed_intro': 'speed_explain', +'speed_set_throttle': 'speed_hold_coarse', +'speed_hold_coarse': 'speed_hold_fine', +'speed_hold_fine': 'alt_intro', +``` + +Note: `speed_explain` → `speed_set_throttle` is button-driven (not goal-driven), so no entry needed in `goalNextPhase`. But `speed_set_throttle` is actually `speed_intro` with the throttle goal. Wait — looking at the existing flow: + +- `speed_intro` has goal: throttle 70%. On goal met → `goalNextPhase['speed_intro']` fires. +- So `speed_intro` goal-advances to `speed_explain` (explanation screen). +- `speed_explain` has no goal, button-advances to `speed_set_throttle`. + +Actually this is wrong — `speed_intro` IS the throttle-setting phase. The flow should be: + +- `speed_intro` (throttle 70% goal) → goal met → `speed_explain` +- `speed_explain` (no goal, button) → `speed_hold_coarse` +- `speed_hold_coarse` (speed ±10, 5s) → goal met → `speed_hold_fine` +- `speed_hold_fine` (speed ±5, 8s) → goal met → `alt_intro` + +Fix `speed_explain` button next to `'speed_hold_coarse'` (not `speed_set_throttle`). + +Updated goalNextPhase: +```typescript +'speed_intro': 'speed_explain', +'speed_hold_coarse': 'speed_hold_fine', +'speed_hold_fine': 'alt_intro', +``` + +**Step 4: Update mainPhaseIds** + +Replace `'speed_hold_pitch'` with `'speed_explain', 'speed_hold_coarse', 'speed_hold_fine'` in the `mainPhaseIds` array: + +```typescript +const mainPhaseIds = ['welcome', 'horizon_intro', 'pitch_intro', 'speed_intro', 'speed_explain', 'speed_hold_coarse', 'speed_hold_fine', 'alt_intro', 'vs_intro', 'heading_intro', 'combined', 'free_practice', 'end'] +``` + +**Step 5: Verify the full flow** + +Run: `bun run dev` +Walk through all phases from welcome to end. Verify: +- Speed explain shows text + "Verstanden" button +- Coarse hold has wide band, 5s hold requirement +- Fine hold has narrow band, 8s hold requirement +- Progress bar advances smoothly through new phases + +**Step 6: Commit** + +``` +git add shared/data/flightlab/learn-pfd.ts shared/composables/flightlab/useLearnPfdEngine.ts +git commit -m "feat(flightlab): multi-phase speed exercise with progressive difficulty" +``` + +--- + +### Task 9: Final visual verification + +**Step 1: Full walkthrough** + +Run: `bun run dev` +Navigate to `/flightlab/medienstationen/learn-pfd`. +Walk through every phase and verify: +- All tapes have dark backgrounds +- Green readouts on speed and altitude +- Yellow heading readout +- Attitude has sky/ground gradients +- Aircraft symbol has W-shape +- Flight physics feel sluggish and realistic +- Pitch/bank exercises use small targets +- Speed exercise teaches energy management in 3 steps + +**Step 2: Commit any final tweaks** + +If color adjustments needed for contrast/readability on dark backgrounds, adjust and commit. diff --git a/shared/composables/flightlab/useAirbusFBW.ts b/shared/composables/flightlab/useAirbusFBW.ts index b95a499..4a27b9d 100644 --- a/shared/composables/flightlab/useAirbusFBW.ts +++ b/shared/composables/flightlab/useAirbusFBW.ts @@ -24,13 +24,14 @@ const MAX_PITCH_UP = 30 const MAX_PITCH_DOWN = -15 const MAX_BANK = 67 const BANK_NEUTRAL_LIMIT = 33 -const MAX_ROLL_RATE = 15 -const ROLL_RETURN_RATE = 5 +const MAX_ROLL_RATE = 7 +const ROLL_RETURN_RATE = 2 const MAX_G_PULL = 2.5 const MIN_G_PUSH = -1.0 const NEUTRAL_G = 1.0 -const PITCH_RATE_PER_G_DELTA = 3.5 +const PITCH_RATE_PER_G_DELTA = 1.2 +const PITCH_SMOOTH_TAU = 2.0 // seconds — exponential smoothing time constant const IDLE_THRUST = 2000 const MAX_THRUST = 50000 @@ -39,7 +40,7 @@ const MASS = 150000 const GRAVITY = 32.174 const KT_TO_FPS = 1.68781 const TRIM_PITCH_DEG = 2 -const SPEED_PITCH_COUPLING = 1.2 +const SPEED_PITCH_COUPLING = 2.0 const INITIAL_SPEED = 220 const INITIAL_ALTITUDE = 5000 @@ -63,6 +64,7 @@ export function useAirbusFBW() { let animFrame: number | null = null let lastTime: number | null = null let running = false + let smoothedPitchRate = 0 function updateInput(newInput: Partial) { if (newInput.pitch !== undefined) input.pitch = clamp(newInput.pitch, -1, 1) @@ -84,6 +86,7 @@ export function useAirbusFBW() { state.throttlePercent = 50 state.onGround = false lastTime = null + smoothedPitchRate = 0 } function tick(timestamp: number) { @@ -126,8 +129,10 @@ export function useAirbusFBW() { const currentG = speedFps > 50 ? 1 + (state.pitch * Math.PI / 180) * speedFps / GRAVITY * 0.01 : 1 const gDelta = targetG - currentG - const pitchRate = gDelta * PITCH_RATE_PER_G_DELTA - state.pitch += pitchRate * dt + const rawPitchRate = gDelta * PITCH_RATE_PER_G_DELTA + const alpha = 1 - Math.exp(-dt / PITCH_SMOOTH_TAU) + smoothedPitchRate += (rawPitchRate - smoothedPitchRate) * alpha + state.pitch += smoothedPitchRate * dt state.pitch = clamp(state.pitch, MAX_PITCH_DOWN, MAX_PITCH_UP) // --- Speed --- diff --git a/shared/composables/flightlab/useLearnPfdEngine.ts b/shared/composables/flightlab/useLearnPfdEngine.ts index ac1d319..60cf1d7 100644 --- a/shared/composables/flightlab/useLearnPfdEngine.ts +++ b/shared/composables/flightlab/useLearnPfdEngine.ts @@ -3,15 +3,16 @@ import { ref, computed, watch } from 'vue' import type { LearnPfdPhase, LearnPfdScenario, FlightLabButton, PfdInteractionGoal, PfdElement, PfdLayoutMode } from '../../data/flightlab/types' import type { FlightState } from './useAirbusFBW' -const mainPhaseIds = ['welcome', 'horizon_intro', 'pitch_intro', 'speed_intro', 'speed_hold_pitch', 'alt_intro', 'vs_intro', 'heading_intro', 'combined', 'free_practice', 'end'] +const mainPhaseIds = ['welcome', 'horizon_intro', 'pitch_intro', 'speed_intro', 'speed_explain', 'speed_hold_coarse', 'speed_hold_fine', 'alt_intro', 'vs_intro', 'heading_intro', 'combined', 'free_practice', 'end'] const goalNextPhase: Record = { 'horizon_intro': 'horizon_roll_right', 'horizon_roll_right': 'pitch_intro', 'pitch_intro': 'pitch_down', 'pitch_down': 'speed_intro', - 'speed_intro': 'speed_hold_pitch', - 'speed_hold_pitch': 'alt_intro', + 'speed_intro': 'speed_explain', + 'speed_hold_coarse': 'speed_hold_fine', + 'speed_hold_fine': 'alt_intro', 'alt_intro': 'vs_intro', 'vs_intro': 'heading_intro', 'heading_intro': 'combined', diff --git a/shared/data/flightlab/learn-pfd.ts b/shared/data/flightlab/learn-pfd.ts index d7bbc37..4e4678e 100644 --- a/shared/data/flightlab/learn-pfd.ts +++ b/shared/data/flightlab/learn-pfd.ts @@ -16,13 +16,13 @@ const phases: LearnPfdPhase[] = [ // --- Phase 2: Horizon Intro (bank left) --- { id: 'horizon_intro', - atcMessage: 'Das hier ist der künstliche Horizont. Blau ist der Himmel, braun ist der Boden. Genau wie draußen. Beweg jetzt den Stick nach links — und schau was passiert.', + atcMessage: 'Das hier ist der künstliche Horizont. Blau ist der Himmel, braun ist der Boden. Genau wie draußen. Beweg jetzt den Stick ganz sanft nach links — nur ein kleines bisschen. Im echten Flugzeug macht man immer nur kleine, sanfte Bewegungen.', explanation: 'Der künstliche Horizont zeigt dir die Lage des Flugzeugs im Raum.', visibleElements: ['attitude'], layoutMode: 'model-focus', - interactionGoal: { parameter: 'bankAngle', target: -20, tolerance: 15, holdMs: 1500 }, + interactionGoal: { parameter: 'bankAngle', target: -10, tolerance: 8, holdMs: 1500 }, goalTimeoutMs: 20000, - goalHint: 'Versuch den Stick nach links zu drücken. Du solltest sehen, wie sich der Horizont dreht.', + goalHint: 'Drück den Stick ganz leicht nach links. Nur ein kleiner Ausschlag reicht.', autoAdvanceAfterTTS: false, buttons: [ { id: 'skip_horizon', label: 'Weiter', icon: 'mdi-arrow-right', next: 'pitch_intro', type: 'primary' }, @@ -32,12 +32,12 @@ const phases: LearnPfdPhase[] = [ // --- Phase 3: Horizon Roll Right --- { id: 'horizon_roll_right', - atcMessage: 'Perfekt! Du hast das Flugzeug nach links geneigt. Jetzt andersrum — Stick nach rechts.', + atcMessage: 'Perfekt! Jetzt ganz sanft in die andere Richtung — Stick leicht nach rechts.', visibleElements: ['attitude'], layoutMode: 'model-focus', - interactionGoal: { parameter: 'bankAngle', target: 20, tolerance: 15, holdMs: 1500 }, + interactionGoal: { parameter: 'bankAngle', target: 10, tolerance: 8, holdMs: 1500 }, goalTimeoutMs: 15000, - goalHint: 'Stick nach rechts drücken, bis der Horizont sich nach links neigt.', + goalHint: 'Stick ganz leicht nach rechts. Kleine, sanfte Bewegungen.', buttons: [ { id: 'skip_roll', label: 'Weiter', icon: 'mdi-arrow-right', next: 'pitch_intro', type: 'primary' }, ], @@ -46,13 +46,13 @@ const phases: LearnPfdPhase[] = [ // --- Phase 4: Pitch Intro (pitch up) --- { id: 'pitch_intro', - atcMessage: 'Jetzt kommt was Wichtiges. Zieh den Stick leicht nach hinten. Die Nase geht hoch. Und das Besondere beim Airbus: Wenn du loslässt, hält er die Lage. Probier es aus.', + atcMessage: 'Jetzt kommt was Wichtiges. Zieh den Stick ganz leicht nach hinten. Nur ein kleines bisschen. Das Besondere beim Airbus: Wenn du loslässt, hält er die Lage. Und das Flugzeug reagiert langsam — du musst immer vorausdenken.', explanation: 'Beim Airbus steuert der Stick nicht direkt die Ruder, sondern wie stark das Flugzeug steigen oder sinken soll.', visibleElements: ['attitude'], layoutMode: 'model-focus', - interactionGoal: { parameter: 'pitch', target: 10, tolerance: 5, holdMs: 2000 }, + interactionGoal: { parameter: 'pitch', target: 4, tolerance: 3, holdMs: 2000 }, goalTimeoutMs: 20000, - goalHint: 'Zieh den Stick langsam nach hinten. Du siehst die braune Fläche größer werden — das heißt, die Nase zeigt nach oben.', + goalHint: 'Zieh den Stick nur ganz leicht nach hinten. Beim echten Airbus macht man nur minimale Bewegungen.', buttons: [ { id: 'skip_pitch', label: 'Weiter', icon: 'mdi-arrow-right', next: 'pitch_down', type: 'primary' }, ], @@ -61,18 +61,18 @@ const phases: LearnPfdPhase[] = [ // --- Phase 5: Pitch Down --- { id: 'pitch_down', - atcMessage: 'Gut gemacht! Jetzt drück den Stick leicht nach vorne. Die Nase geht runter.', + atcMessage: 'Gut gemacht! Jetzt drück den Stick ganz leicht nach vorne. Auch hier gilt: kleine Bewegungen, und Geduld — das Flugzeug braucht ein paar Sekunden bis es reagiert.', visibleElements: ['attitude'], layoutMode: 'model-focus', - interactionGoal: { parameter: 'pitch', target: -5, tolerance: 5, holdMs: 1500 }, + interactionGoal: { parameter: 'pitch', target: -3, tolerance: 3, holdMs: 1500 }, goalTimeoutMs: 15000, - goalHint: 'Stick nach vorne drücken. Die blaue Fläche wird größer.', + goalHint: 'Stick ganz leicht nach vorne drücken und warten. Das Flugzeug reagiert mit Verzögerung.', buttons: [ { id: 'skip_pitch_down', label: 'Weiter', icon: 'mdi-arrow-right', next: 'speed_intro', type: 'primary' }, ], }, - // --- Phase 6: Speed Intro --- + // --- Phase 6: Speed Intro (set throttle) --- { id: 'speed_intro', atcMessage: 'Links erscheint jetzt das Speed Tape. Erster Schritt: Setz den Schubhebel sauber auf siebzig Prozent.', @@ -83,23 +83,53 @@ const phases: LearnPfdPhase[] = [ goalTimeoutMs: 20000, goalHint: 'Schubhebel auf die 70%-Marke setzen und kurz halten.', buttons: [ - { id: 'skip_speed', label: 'Weiter', icon: 'mdi-arrow-right', next: 'speed_hold_pitch', type: 'primary' }, + { id: 'skip_speed', label: 'Weiter', icon: 'mdi-arrow-right', next: 'speed_explain', type: 'primary' }, ], }, - // --- Phase 7: Hold Speed With Pitch --- + // --- Phase 7a: Speed Explain --- { - id: 'speed_hold_pitch', - atcMessage: 'Jetzt kommt der wichtige Teil: Halte die Geschwindigkeit im roten Bereich. Der Schub bleibt bei siebzig Prozent — du steuerst das nur mit Pitch. Nase leicht hoch macht langsamer, Nase leicht runter macht schneller.', - explanation: 'Energie-Management: Mit konstantem Schub kontrollierst du die Geschwindigkeit über den Pitchwinkel.', + id: 'speed_explain', + atcMessage: 'Bevor wir loslegen, eine wichtige Regel: Beim Airbus kontrollierst du die Geschwindigkeit mit der Nase. Nase hoch — du wirst langsamer. Nase runter — du wirst schneller. Der Schub bleibt dabei immer gleich. Das nennt man Energie-Management.', + explanation: 'Im echten Flug hält der Autopilot den Schub konstant. Der Pilot steuert die Geschwindigkeit über den Pitch.', visibleElements: ['attitude', 'speedTape'], layoutMode: 'split', - interactionGoal: { parameter: 'speed', target: 225, tolerance: 8, holdMs: 3000 }, - speedTargetRange: { min: 217, max: 233 }, - goalTimeoutMs: 25000, - goalHint: 'Halte den Schub bei 70%. Korrigiere nur mit Pitch, bis die Speed-Zahl im roten Band bleibt.', buttons: [ - { id: 'skip_speed_hold', label: 'Weiter', icon: 'mdi-arrow-right', next: 'alt_intro', type: 'primary' }, + { id: 'understood', label: 'Verstanden', icon: 'mdi-check', next: 'speed_hold_coarse', type: 'primary' }, + ], + }, + + // --- Phase 7b: Speed Hold Coarse --- + { + id: 'speed_hold_coarse', + atcMessage: 'Gut, Schub steht. Jetzt versuch die Geschwindigkeit in den türkisen Bereich zu bringen. Nase ganz leicht hoch oder runter — und dann warten. Das Flugzeug braucht mehrere Sekunden bis sich die Speed ändert.', + explanation: 'Erste Annäherung: Bringe die Speed grob in den Zielbereich.', + visibleElements: ['attitude', 'speedTape'], + layoutMode: 'split', + interactionGoal: { parameter: 'speed', target: 225, tolerance: 10, holdMs: 5000 }, + speedTargetRange: { min: 215, max: 235 }, + goalTimeoutMs: 30000, + goalHint: 'Korrigiere nur mit ganz kleinen Pitch-Bewegungen. Die Speed ändert sich langsam — hab Geduld.', + autoAdvanceAfterTTS: false, + buttons: [ + { id: 'skip_coarse', label: 'Weiter', icon: 'mdi-arrow-right', next: 'speed_hold_fine', type: 'primary' }, + ], + }, + + // --- Phase 7c: Speed Hold Fine --- + { + id: 'speed_hold_fine', + atcMessage: 'Sehr gut! Jetzt genauer. Halte die Geschwindigkeit möglichst exakt auf 225 Knoten. Nur winzige Korrekturen — und immer ein paar Sekunden warten bevor du nachkorrigierst.', + explanation: 'Feinsteuerung: Präzise Speed-Kontrolle mit minimalen Pitch-Änderungen.', + visibleElements: ['attitude', 'speedTape'], + layoutMode: 'split', + interactionGoal: { parameter: 'speed', target: 225, tolerance: 5, holdMs: 8000 }, + speedTargetRange: { min: 220, max: 230 }, + goalTimeoutMs: 40000, + goalHint: 'Ganz kleine Pitch-Korrekturen. Warte nach jeder Korrektur 3-4 Sekunden bevor du nachjustierst.', + autoAdvanceAfterTTS: false, + buttons: [ + { id: 'skip_fine', label: 'Weiter', icon: 'mdi-arrow-right', next: 'alt_intro', type: 'primary' }, ], },