mirror of
https://github.com/OpenSquawk/OpenSquawk
synced 2026-08-05 00:46:00 +08:00
feat(pfd): realistic styling, flight inertia, and improved training exercises
- Darken all tape backgrounds to match real A320 PFD (#16181f/#1c1e26) - Speed/altitude readouts green (#19e34a) instead of cyan - Attitude indicator: sky/ground gradients, W-shaped aircraft symbol - Speed tape: cyan target zone (not red), VFE/min-speed red bands - Altitude tape: ticks on left side toward attitude indicator - Flight physics: 3x slower pitch (smoothed, tau 2s), halved roll rate, stronger speed-pitch coupling for realistic 150t inertia - Pitch/bank exercises use normal flight ranges (±10° bank, ±3-4° pitch) - Multi-phase speed exercise: explain → coarse hold 5s → fine hold 8s - ATC messages emphasize small inputs, patience, and anticipation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -65,7 +65,7 @@ const tapeInnerHeight = computed(() => props.height - 4)
|
||||
y="0"
|
||||
:width="width"
|
||||
:height="height"
|
||||
fill="#8f9198"
|
||||
fill="#16181f"
|
||||
rx="1"
|
||||
/>
|
||||
<rect
|
||||
@@ -73,27 +73,27 @@ const tapeInnerHeight = computed(() => props.height - 4)
|
||||
:y="tapeInnerY"
|
||||
:width="tapeInnerWidth"
|
||||
:height="tapeInnerHeight"
|
||||
fill="#93959c"
|
||||
stroke="#d2d4da"
|
||||
fill="#1c1e26"
|
||||
stroke="#3a3d48"
|
||||
stroke-width="0.8"
|
||||
/>
|
||||
|
||||
<!-- Scrolling tape (clipped) -->
|
||||
<g :clip-path="`url(#${clipId})`">
|
||||
<g v-for="mark in marks" :key="mark.alt">
|
||||
<!-- Tick mark -->
|
||||
<!-- Tick mark (left side, toward attitude indicator) -->
|
||||
<line
|
||||
:x1="width - 10"
|
||||
x1="1"
|
||||
:y1="mark.y"
|
||||
:x2="width - 1"
|
||||
x2="10"
|
||||
:y2="mark.y"
|
||||
stroke="#f4f6fb"
|
||||
stroke-width="1"
|
||||
/>
|
||||
<!-- Label -->
|
||||
<!-- Label (right of ticks) -->
|
||||
<text
|
||||
v-if="mark.isLabel"
|
||||
x="4"
|
||||
x="14"
|
||||
:y="mark.y + 4"
|
||||
fill="#f4f6fb"
|
||||
font-size="12"
|
||||
@@ -112,14 +112,14 @@ const tapeInnerHeight = computed(() => props.height - 4)
|
||||
:width="readoutBoxWidth"
|
||||
:height="readoutBoxHeight"
|
||||
fill="#02040b"
|
||||
stroke="#2fe5ff"
|
||||
stroke="#19e34a"
|
||||
stroke-width="1.4"
|
||||
rx="1.5"
|
||||
/>
|
||||
<text
|
||||
:x="width / 2"
|
||||
:y="centerY + 5"
|
||||
fill="#2fe5ff"
|
||||
fill="#19e34a"
|
||||
font-size="14"
|
||||
font-weight="bold"
|
||||
text-anchor="middle"
|
||||
|
||||
@@ -84,12 +84,12 @@ const dotRadius = computed(() => 4)
|
||||
<circle :cx="cx" :cy="cy" :r="size * 0.46" />
|
||||
</clipPath>
|
||||
<linearGradient :id="skyGradId" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#22a7eb" />
|
||||
<stop offset="0%" stop-color="#0a4a8a" />
|
||||
<stop offset="100%" stop-color="#22a7eb" />
|
||||
</linearGradient>
|
||||
<linearGradient :id="groundGradId" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#b26113" />
|
||||
<stop offset="100%" stop-color="#b26113" />
|
||||
<stop offset="100%" stop-color="#5a2e08" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
@@ -184,7 +184,7 @@ const dotRadius = computed(() => 4)
|
||||
:fill="YELLOW"
|
||||
/>
|
||||
|
||||
<!-- Fixed aircraft reference symbol -->
|
||||
<!-- Fixed aircraft reference symbol (W-shape) -->
|
||||
<g>
|
||||
<!-- Left wing -->
|
||||
<rect
|
||||
@@ -195,6 +195,16 @@ const dotRadius = computed(() => 4)
|
||||
:fill="YELLOW"
|
||||
rx="1"
|
||||
/>
|
||||
<!-- Left wing tip (descending) -->
|
||||
<line
|
||||
:x1="cx - wingSpan - dotRadius"
|
||||
:y1="cy"
|
||||
:x2="cx - wingSpan - dotRadius"
|
||||
:y2="cy + 5"
|
||||
:stroke="YELLOW"
|
||||
stroke-width="3"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
<!-- Right wing -->
|
||||
<rect
|
||||
:x="cx + dotRadius"
|
||||
@@ -204,6 +214,16 @@ const dotRadius = computed(() => 4)
|
||||
:fill="YELLOW"
|
||||
rx="1"
|
||||
/>
|
||||
<!-- Right wing tip (descending) -->
|
||||
<line
|
||||
:x1="cx + wingSpan + dotRadius"
|
||||
:y1="cy"
|
||||
:x2="cx + wingSpan + dotRadius"
|
||||
:y2="cy + 5"
|
||||
:stroke="YELLOW"
|
||||
stroke-width="3"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
<!-- Center dot -->
|
||||
<circle
|
||||
:cx="cx"
|
||||
|
||||
@@ -89,7 +89,7 @@ const readoutBoxHeight = 18
|
||||
y="0"
|
||||
:width="width"
|
||||
:height="height"
|
||||
fill="#8f9198"
|
||||
fill="#16181f"
|
||||
rx="1"
|
||||
/>
|
||||
<rect
|
||||
@@ -97,8 +97,8 @@ const readoutBoxHeight = 18
|
||||
y="1.5"
|
||||
:width="width - 3"
|
||||
:height="height - 3"
|
||||
fill="#94969d"
|
||||
stroke="#d4d6dc"
|
||||
fill="#1c1e26"
|
||||
stroke="#3a3d48"
|
||||
stroke-width="0.8"
|
||||
/>
|
||||
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
<rect
|
||||
@@ -104,22 +108,59 @@ const tapeInnerHeight = computed(() => props.height - 4)
|
||||
:y="tapeInnerY"
|
||||
:width="tapeInnerWidth"
|
||||
:height="tapeInnerHeight"
|
||||
fill="#93959c"
|
||||
stroke="#d2d4da"
|
||||
fill="#1c1e26"
|
||||
stroke="#3a3d48"
|
||||
stroke-width="0.8"
|
||||
/>
|
||||
|
||||
<!-- Scrolling tape (clipped) -->
|
||||
<g :clip-path="`url(#${clipId})`">
|
||||
<!-- Target hold zone -->
|
||||
<!-- VFE overspeed band (red) -->
|
||||
<rect
|
||||
x="1"
|
||||
:y="0"
|
||||
:width="width - 2"
|
||||
:height="Math.max(0, speedToY(vfeSpeed))"
|
||||
fill="rgba(239, 40, 40, 0.45)"
|
||||
/>
|
||||
<line
|
||||
v-if="speedToY(vfeSpeed) > 0"
|
||||
x1="1"
|
||||
:y1="speedToY(vfeSpeed)"
|
||||
:x2="width - 1"
|
||||
:y2="speedToY(vfeSpeed)"
|
||||
stroke="#ef4444"
|
||||
stroke-width="1.5"
|
||||
/>
|
||||
|
||||
<!-- Minimum speed band (red) -->
|
||||
<rect
|
||||
v-if="speedToY(minSpeed) < height"
|
||||
x="1"
|
||||
:y="speedToY(minSpeed)"
|
||||
:width="width - 2"
|
||||
:height="Math.max(0, height - speedToY(minSpeed))"
|
||||
fill="rgba(239, 40, 40, 0.45)"
|
||||
/>
|
||||
<line
|
||||
v-if="speedToY(minSpeed) < height"
|
||||
x1="1"
|
||||
:y1="speedToY(minSpeed)"
|
||||
:x2="width - 1"
|
||||
:y2="speedToY(minSpeed)"
|
||||
stroke="#ef4444"
|
||||
stroke-width="1.5"
|
||||
/>
|
||||
|
||||
<!-- Target hold zone (cyan) -->
|
||||
<g v-if="targetZone">
|
||||
<rect
|
||||
x="1"
|
||||
:y="targetZone.top"
|
||||
:width="width - 2"
|
||||
:height="targetZone.height"
|
||||
fill="rgba(239, 68, 68, 0.22)"
|
||||
stroke="rgba(248, 113, 113, 0.95)"
|
||||
fill="rgba(45, 212, 255, 0.18)"
|
||||
stroke="rgba(45, 229, 255, 0.8)"
|
||||
stroke-width="1.2"
|
||||
/>
|
||||
<line
|
||||
@@ -127,7 +168,7 @@ const tapeInnerHeight = computed(() => 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"
|
||||
/>
|
||||
<line
|
||||
@@ -135,7 +176,7 @@ const tapeInnerHeight = computed(() => 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"
|
||||
/>
|
||||
</g>
|
||||
@@ -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"
|
||||
/>
|
||||
<text
|
||||
:x="width / 2"
|
||||
:y="centerY + 5"
|
||||
fill="#2fe5ff"
|
||||
fill="#19e34a"
|
||||
font-size="15"
|
||||
font-weight="bold"
|
||||
text-anchor="middle"
|
||||
|
||||
@@ -68,8 +68,8 @@ const channelShape = computed(() => {
|
||||
<!-- Airbus-style VS channel -->
|
||||
<polygon
|
||||
:points="channelShape"
|
||||
fill="#8f9198"
|
||||
stroke="#d2d4da"
|
||||
fill="#1c1e26"
|
||||
stroke="#3a3d48"
|
||||
stroke-width="0.8"
|
||||
/>
|
||||
|
||||
|
||||
79
docs/plans/2026-02-21-pfd-realism-design.md
Normal file
79
docs/plans/2026-02-21-pfd-realism-design.md
Normal file
@@ -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.
|
||||
453
docs/plans/2026-02-21-pfd-realism-implementation.md
Normal file
453
docs/plans/2026-02-21-pfd-realism-implementation.md
Normal file
@@ -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
|
||||
<!-- Overspeed band (red, above VFE) -->
|
||||
<rect
|
||||
x="1"
|
||||
:y="0"
|
||||
:width="width - 2"
|
||||
:height="Math.max(0, speedToY(vfeSpeed))"
|
||||
fill="rgba(239, 40, 40, 0.45)"
|
||||
/>
|
||||
<line
|
||||
x1="1" :y1="speedToY(vfeSpeed)"
|
||||
:x2="width - 1" :y2="speedToY(vfeSpeed)"
|
||||
stroke="#ef4444" stroke-width="1.5"
|
||||
/>
|
||||
<!-- Min speed band (red, below min) -->
|
||||
<rect
|
||||
x="1"
|
||||
:y="speedToY(minSpeed)"
|
||||
:width="width - 2"
|
||||
:height="height - speedToY(minSpeed)"
|
||||
fill="rgba(239, 40, 40, 0.45)"
|
||||
/>
|
||||
<line
|
||||
x1="1" :y1="speedToY(minSpeed)"
|
||||
:x2="width - 1" :y2="speedToY(minSpeed)"
|
||||
stroke="#ef4444" stroke-width="1.5"
|
||||
/>
|
||||
```
|
||||
|
||||
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.
|
||||
@@ -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<StickInput>) {
|
||||
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 ---
|
||||
|
||||
@@ -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<string, string> = {
|
||||
'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',
|
||||
|
||||
@@ -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' },
|
||||
],
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user