engine thr mehr respektieren

This commit is contained in:
itsrubberduck
2026-02-20 23:55:45 +01:00
parent 34df91188d
commit b83ccf2c7c
6 changed files with 52 additions and 34 deletions

View File

@@ -102,11 +102,11 @@ function initScene() {
scene.background = new THREE.Color(0x1a5fb4)
scene.fog = new THREE.Fog(0x62a0ea, 30, 80)
// Camera — view from front-left, slightly above
// Aircraft nose points -Z, so camera at +Z looks at front
// Camera — chase-style view from behind and slightly above
// Aircraft nose points -Z, so +Z is behind the aircraft
camera = new THREE.PerspectiveCamera(50, width / height, 0.1, 200)
camera.position.set(-4, 3, -6)
camera.lookAt(0, 0, 0)
camera.position.set(0, 2.6, 8)
camera.lookAt(0, 0.2, -2.2)
// Lights
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6)

View File

@@ -65,7 +65,7 @@
<div v-else class="flex-1 flex gap-2 p-3">
<!-- Throttle (left side, vertical slider) -->
<div class="w-20 flex flex-col items-center gap-2">
<span class="text-[10px] uppercase tracking-widest text-white/30">Thrust</span>
<span class="text-[10px] uppercase tracking-widest text-white/30">Thrust (max 70%)</span>
<div
ref="throttleTrack"
class="flex-1 w-16 rounded-2xl border border-white/10 bg-[#0b1328]/90 relative overflow-hidden cursor-pointer"
@@ -77,12 +77,12 @@
<!-- Fill -->
<div
class="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-amber-500/80 to-amber-400/40 transition-[height] duration-75"
:style="{ height: `${throttle * 100}%` }"
:style="{ height: `${throttleUiPercent}%` }"
/>
<!-- Handle -->
<div
class="absolute left-1/2 -translate-x-1/2 w-12 h-3 rounded-full bg-white/80 border border-white/30 shadow-lg"
:style="{ bottom: `calc(${throttle * 100}% - 6px)` }"
:style="{ bottom: `calc(${throttleUiPercent}% - 6px)` }"
/>
<!-- Label -->
<div class="absolute bottom-2 left-0 right-0 text-center">
@@ -144,13 +144,14 @@
</template>
<script setup lang="ts">
import { ref, onBeforeUnmount } from 'vue'
import { ref, computed, onBeforeUnmount } from 'vue'
import { useFlightLabSync } from '~~/shared/composables/flightlab/useFlightLabSync'
definePageMeta({ layout: false })
useHead({ title: 'FlightLab - Stick Input' })
const sync = useFlightLabSync()
const MAX_TRAINING_THROTTLE = 0.7
// --- Connection state ---
const sessionCodeInput = ref('')
@@ -161,9 +162,10 @@ const connectionError = ref('')
// --- Stick state ---
const stickX = ref(0) // -1 (left) to +1 (right) = roll
const stickY = ref(0) // -1 (forward/push/nose down) to +1 (back/pull/nose up)
const throttle = ref(0) // 0 (idle) to 1 (TOGA)
const throttle = ref(0) // 0 (idle) to 0.7 (training max)
const stickActive = ref(false)
const throttleActive = ref(false)
const throttleUiPercent = computed(() => (throttle.value / MAX_TRAINING_THROTTLE) * 100)
// --- Refs ---
const stickPad = ref<HTMLElement | null>(null)
@@ -253,7 +255,7 @@ function updateThrottlePosition(e: PointerEvent) {
const rect = track.getBoundingClientRect()
// Bottom = 0, top = 1
const rawThrottle = 1 - (e.clientY - rect.top) / rect.height
throttle.value = Math.max(0, Math.min(1, rawThrottle))
throttle.value = Math.max(0, Math.min(MAX_TRAINING_THROTTLE, rawThrottle))
}
// --- Cleanup ---

View File

@@ -16,6 +16,7 @@ interface FlightLabSession {
telemetryUserIds: Set<string>
}
const GLOBAL_SESSION_CODE = 'GLOBAL'
const sessions = new Map<string, FlightLabSession>()
// Map userId → session code for telemetry routing
@@ -30,11 +31,27 @@ flightlabTelemetryStore.subscribe((userId, data) => {
broadcastToSession(session, { type: 'telemetry', data })
})
function generateCode(): string {
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789' // No I,O,0,1 for readability
let code = ''
for (let i = 0; i < 4; i++) code += chars[Math.floor(Math.random() * chars.length)]
return sessions.has(code) ? generateCode() : code
function getOrCreateGlobalSession(scenarioId = 'takeoff-eddf'): FlightLabSession {
const existing = sessions.get(GLOBAL_SESSION_CODE)
if (existing) {
existing.scenarioId = scenarioId
return existing
}
const session: FlightLabSession = {
code: GLOBAL_SESSION_CODE,
scenarioId,
currentPhaseId: 'welcome',
isPaused: false,
startedAt: Date.now(),
participantConnected: false,
instructorConnected: false,
history: [],
peers: new Map(),
telemetryUserIds: new Set(),
}
sessions.set(GLOBAL_SESSION_CODE, session)
return session
}
function broadcastToSession(session: FlightLabSession, event: any, excludePeerId?: string) {
@@ -81,26 +98,17 @@ export default defineWebSocketHandler({
switch (data.type) {
case 'create-session': {
const code = generateCode()
const session: FlightLabSession = {
code,
scenarioId: data.scenarioId ?? 'takeoff-eddf',
currentPhaseId: 'welcome',
isPaused: false,
startedAt: Date.now(),
participantConnected: false,
instructorConnected: true,
history: [],
peers: new Map([[peerId, { role: 'instructor', peer }]]),
telemetryUserIds: new Set(),
}
sessions.set(code, session)
peer.send(JSON.stringify({ type: 'session-created', code, state: getSessionState(session) }))
const session = getOrCreateGlobalSession(data.scenarioId ?? 'takeoff-eddf')
session.peers.set(peerId, { role: 'instructor', peer })
session.instructorConnected = true
peer.send(JSON.stringify({ type: 'session-created', code: session.code, state: getSessionState(session) }))
broadcastToSession(session, { type: 'peer-joined', role: 'instructor' }, peerId)
break
}
case 'join-session': {
const session = sessions.get(data.code?.toUpperCase())
const requestedCode = typeof data.code === 'string' ? data.code.toUpperCase() : null
const session = requestedCode ? sessions.get(requestedCode) : getOrCreateGlobalSession(data.scenarioId ?? 'takeoff-eddf')
if (!session) {
peer.send(JSON.stringify({ type: 'error', message: 'Session nicht gefunden' }))
return

View File

@@ -38,6 +38,7 @@ const DRAG_COEFFICIENT = 0.03
const MASS = 150000
const GRAVITY = 32.174
const KT_TO_FPS = 1.68781
const SPEED_PITCH_COUPLING = 0.5
const INITIAL_SPEED = 220
const INITIAL_ALTITUDE = 5000
@@ -130,8 +131,9 @@ export function useAirbusFBW() {
// --- Speed ---
const speedFps2 = state.speed * KT_TO_FPS
const pitchRad = state.pitch * Math.PI / 180
const drag = DRAG_COEFFICIENT * speedFps2 * speedFps2
const climbPenalty = Math.sin(state.pitch * Math.PI / 180) * MASS * GRAVITY * 0.3
const climbPenalty = Math.sin(pitchRad) * MASS * GRAVITY * SPEED_PITCH_COUPLING
const netForce = thrust - drag - climbPenalty
const acceleration = netForce / MASS
const speedDelta = (acceleration / KT_TO_FPS) * dt

View File

@@ -100,6 +100,11 @@ export function useFlightLabSync() {
send({ type: 'join-session', code: code.toUpperCase(), role: joinRole })
}
async function joinGlobalSession(joinRole: FlightLabRole = 'participant', scenarioId = 'learn-pfd') {
await connect()
send({ type: 'join-session', role: joinRole, scenarioId })
}
/** Subscribe this session to receive telemetry for the given userId */
function subscribeTelemetry(userId: string) {
send({ type: 'subscribe-telemetry', userId })
@@ -145,6 +150,7 @@ export function useFlightLabSync() {
remoteState,
createSession,
joinSession,
joinGlobalSession,
subscribeTelemetry,
sendParticipantAction,
sendInstructorCommand,

View File

@@ -75,13 +75,13 @@ const phases: LearnPfdPhase[] = [
// --- Phase 6: Speed Intro ---
{
id: 'speed_intro',
atcMessage: 'Links erscheint jetzt das Speed Tape. Das zeigt dir wie schnell du fliegst, in Knoten. Schieb den Schubhebel nach vorne — und schau was mit der Geschwindigkeit passiert.',
atcMessage: 'Links erscheint jetzt das Speed Tape. Das zeigt dir wie schnell du fliegst, in Knoten. Schieb den Schubhebel auf etwa siebzig Prozent nach vorne — und schau was mit der Geschwindigkeit passiert.',
explanation: 'Das Speed Tape zeigt die angezeigte Fluggeschwindigkeit (IAS) in Knoten.',
visibleElements: ['attitude', 'speedTape'],
layoutMode: 'split',
interactionGoal: { parameter: 'speed', target: 280, tolerance: 30, holdMs: 2000 },
goalTimeoutMs: 20000,
goalHint: 'Schieb den Schubhebel weiter nach vorne. Die Zahl auf dem Speed Tape sollte steigen.',
goalHint: 'Schieb den Schubhebel bis ungefähr siebzig Prozent. Die Zahl auf dem Speed Tape sollte steigen.',
buttons: [
{ id: 'skip_speed', label: 'Weiter', icon: 'mdi-arrow-right', next: 'alt_intro', type: 'primary' },
],