mirror of
https://github.com/OpenSquawk/OpenSquawk
synced 2026-08-05 00:46:00 +08:00
engine thr mehr respektieren
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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 ---
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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' },
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user