dont log users id on transmission

This commit is contained in:
itsrubberduck
2026-02-13 18:44:34 +01:00
parent b73746d843
commit a915af4398
7 changed files with 584 additions and 40 deletions

View File

@@ -1,5 +1,5 @@
// shared/composables/flightlab/useFlightLabAudio.ts
import { ref } from 'vue'
import { ref, computed } from 'vue'
import type { FlightLabSound } from '../../data/flightlab/types'
import { getReadabilityProfile, createNoiseGenerators } from '../../utils/radioEffects'
import type { PizzicatoLite as PizzicatoLiteType } from '../../utils/pizzicatoLite'
@@ -13,6 +13,10 @@ export function useFlightLabAudio() {
let speechQueue: Promise<void> = Promise.resolve()
let pizzicato: PizzicatoLiteType | null = null
// --- Replay Cache ---
const lastSpokenAudio = ref<{ base64: string; mime: string; readability: number; text: string } | null>(null)
const canReplay = computed(() => lastSpokenAudio.value !== null && !isSpeaking.value)
function getContext(): AudioContext {
if (!audioContext.value) {
audioContext.value = new AudioContext()
@@ -125,6 +129,13 @@ export function useFlightLabAudio() {
})
if (res.success && res.audio?.base64) {
// Cache for replay
lastSpokenAudio.value = {
base64: res.audio.base64,
mime: res.audio.mime,
readability: options?.readability ?? 5,
text,
}
await playWithRadioEffects(res.audio.base64, res.audio.mime, options?.readability ?? 5)
}
} catch (e) {
@@ -179,6 +190,31 @@ export function useFlightLabAudio() {
stopNoise.forEach((fn: () => void) => fn())
}
async function replayLastMessage(): Promise<void> {
if (!lastSpokenAudio.value || isSpeaking.value) return
const { base64, mime, readability } = lastSpokenAudio.value
return new Promise((resolve) => {
speechQueue = speechQueue.then(async () => {
isSpeaking.value = true
try {
await playWithRadioEffects(base64, mime, readability)
} catch (e) {
console.error('[FlightLabAudio] Replay error:', e)
} finally {
isSpeaking.value = false
resolve()
}
}).catch(() => {
isSpeaking.value = false
resolve()
})
})
}
function clearReplayCache() {
lastSpokenAudio.value = null
}
function stopAllSounds() {
for (const [id] of activeSounds.value) {
stopAmbientSound(id)
@@ -200,8 +236,12 @@ export function useFlightLabAudio() {
return {
isSpeaking,
canReplay,
lastSpokenAudio,
preloadSounds,
speakAtcMessage,
replayLastMessage,
clearReplayCache,
handlePhaseSounds,
playAmbientSound,
stopAmbientSound,

View File

@@ -1,6 +1,6 @@
// shared/composables/flightlab/useFlightLabEngine.ts
import { ref, computed } from 'vue'
import type { FlightLabPhase, FlightLabScenario, FlightLabButton } from '../../data/flightlab/types'
import { ref, computed, watch } from 'vue'
import type { FlightLabPhase, FlightLabScenario, FlightLabButton, FlightLabTelemetryState, SimConditionGroup, SimCondition } from '../../data/flightlab/types'
export function useFlightLabEngine(scenario: FlightLabScenario) {
const currentPhaseId = ref(scenario.phases[0]?.id ?? '')
@@ -8,6 +8,20 @@ export function useFlightLabEngine(scenario: FlightLabScenario) {
const history = ref<Array<{ phaseId: string; buttonId: string; timestamp: number }>>([])
const startedAt = ref(Date.now())
// --- Auto-Advance / SimConnect ---
const autoAdvanceEnabled = ref(false)
const currentTelemetry = ref<FlightLabTelemetryState | null>(null)
const showingHelpMessage = ref(false)
const helpMessageText = ref<string | null>(null)
const conditionsMet = ref(false)
let conditionInterval: ReturnType<typeof setInterval> | null = null
let helpTimeout: ReturnType<typeof setTimeout> | null = null
let helpMessageSpoken = false
// Callback for TTS when help message triggers
let onHelpMessage: ((text: string) => void) | null = null
const phasesMap = computed(() => {
const map = new Map<string, FlightLabPhase>()
for (const phase of scenario.phases) {
@@ -38,6 +52,120 @@ export function useFlightLabEngine(scenario: FlightLabScenario) {
const isFinished = computed(() => currentPhaseId.value === 'end')
/** Whether the current phase has sim conditions that can be monitored */
const hasSimConditions = computed(() => {
const phase = currentPhase.value
return !!(phase?.simConditions && phase.simConditionNextPhase)
})
// --- Condition Evaluation ---
function evaluateCondition(condition: SimCondition, telemetry: FlightLabTelemetryState): boolean {
const actual = telemetry[condition.variable]
const expected = condition.value
switch (condition.operator) {
case '>': return (actual as number) > (expected as number)
case '<': return (actual as number) < (expected as number)
case '>=': return (actual as number) >= (expected as number)
case '<=': return (actual as number) <= (expected as number)
case '==': return actual === expected
case '!=': return actual !== expected
default: return false
}
}
function evaluateConditions(group: SimConditionGroup, telemetry: FlightLabTelemetryState): boolean {
if (group.logic === 'AND') {
return group.conditions.every(c => evaluateCondition(c, telemetry))
} else {
return group.conditions.some(c => evaluateCondition(c, telemetry))
}
}
// --- Condition Monitoring ---
function startConditionMonitoring() {
stopConditionMonitoring()
const phase = currentPhase.value
if (!phase?.simConditions || !phase.simConditionNextPhase) return
conditionsMet.value = false
showingHelpMessage.value = false
helpMessageText.value = null
helpMessageSpoken = false
// Check every 500ms
conditionInterval = setInterval(() => {
if (!autoAdvanceEnabled.value || isPaused.value) return
if (!currentTelemetry.value || !phase.simConditions) return
const met = evaluateConditions(phase.simConditions, currentTelemetry.value)
conditionsMet.value = met
if (met && phase.simConditionNextPhase) {
stopConditionMonitoring()
goToPhase(phase.simConditionNextPhase)
}
}, 500)
// Help timeout
const timeoutMs = phase.simConditionTimeoutMs ?? 20000
if (phase.simConditionHelpMessage) {
helpTimeout = setTimeout(() => {
if (!conditionsMet.value && autoAdvanceEnabled.value) {
showingHelpMessage.value = true
helpMessageText.value = phase.simConditionHelpMessage ?? null
if (!helpMessageSpoken && helpMessageText.value && onHelpMessage) {
helpMessageSpoken = true
onHelpMessage(helpMessageText.value)
}
}
}, timeoutMs)
}
}
function stopConditionMonitoring() {
if (conditionInterval) {
clearInterval(conditionInterval)
conditionInterval = null
}
if (helpTimeout) {
clearTimeout(helpTimeout)
helpTimeout = null
}
}
function dismissHelpMessage() {
showingHelpMessage.value = false
helpMessageText.value = null
}
// --- Telemetry ---
function updateTelemetry(data: FlightLabTelemetryState) {
currentTelemetry.value = { ...data, timestamp: Date.now() }
}
function toggleAutoAdvance() {
autoAdvanceEnabled.value = !autoAdvanceEnabled.value
if (autoAdvanceEnabled.value && hasSimConditions.value) {
startConditionMonitoring()
} else {
stopConditionMonitoring()
conditionsMet.value = false
showingHelpMessage.value = false
helpMessageText.value = null
}
}
function setOnHelpMessage(fn: (text: string) => void) {
onHelpMessage = fn
}
// --- Phase navigation ---
function selectOption(button: FlightLabButton) {
if (isPaused.value) return
history.value.push({
@@ -51,15 +179,31 @@ export function useFlightLabEngine(scenario: FlightLabScenario) {
function goToPhase(phaseId: string) {
if (phasesMap.value.has(phaseId)) {
stopConditionMonitoring()
conditionsMet.value = false
showingHelpMessage.value = false
helpMessageText.value = null
currentPhaseId.value = phaseId
}
}
// Watch for phase changes to start condition monitoring
watch(currentPhaseId, () => {
if (autoAdvanceEnabled.value && hasSimConditions.value) {
startConditionMonitoring()
}
})
function restart() {
stopConditionMonitoring()
currentPhaseId.value = scenario.phases[0]?.id ?? ''
isPaused.value = false
history.value = []
startedAt.value = Date.now()
conditionsMet.value = false
showingHelpMessage.value = false
helpMessageText.value = null
currentTelemetry.value = null
}
function pause() { isPaused.value = true }
@@ -81,6 +225,10 @@ export function useFlightLabEngine(scenario: FlightLabScenario) {
}
}
function cleanup() {
stopConditionMonitoring()
}
return {
// State
currentPhaseId,
@@ -91,6 +239,13 @@ export function useFlightLabEngine(scenario: FlightLabScenario) {
isFinished,
startedAt,
scenario,
// Auto-Advance State
autoAdvanceEnabled,
currentTelemetry,
showingHelpMessage,
helpMessageText,
conditionsMet,
hasSimConditions,
// Actions
selectOption,
goToPhase,
@@ -99,5 +254,12 @@ export function useFlightLabEngine(scenario: FlightLabScenario) {
resume,
skipForward,
skipBack,
// Auto-Advance Actions
updateTelemetry,
toggleAutoAdvance,
dismissHelpMessage,
evaluateConditions,
setOnHelpMessage,
cleanup,
}
}

View File

@@ -150,6 +150,17 @@ export const takeoffEddf: FlightLabScenario = {
{ id: 'engine-spool', action: 'play', volume: 0.5, loop: false },
{ id: 'engine-cruise', action: 'play', volume: 0.4, loop: true },
],
simConditions: {
conditions: [
{ variable: 'TURB_ENG_N1_1', operator: '>=', value: 85 },
{ variable: 'TURB_ENG_N1_2', operator: '>=', value: 85 },
{ variable: 'BRAKE_PARKING_POSITION', operator: '==', value: false },
],
logic: 'AND',
},
simConditionTimeoutMs: 20000,
simConditionHelpMessage: 'Schub-Hebel nach vorne schieben bis N1 bei etwa 85 Prozent. Dann die Parkbremse loesen.',
simConditionNextPhase: 'takeoff_roll',
},
{
id: 'engines_loud_comfort',
@@ -184,6 +195,16 @@ export const takeoffEddf: FlightLabScenario = {
{ id: 'runway-rumble', action: 'play', volume: 0.5, loop: true },
{ id: 'wind-low', action: 'play', volume: 0.3, loop: true },
],
simConditions: {
conditions: [
{ variable: 'AIRSPEED_INDICATED', operator: '>=', value: 140 },
{ variable: 'SIM_ON_GROUND', operator: '==', value: true },
],
logic: 'AND',
},
simConditionTimeoutMs: 45000,
simConditionHelpMessage: 'Warte bis die Geschwindigkeit 140 Knoten erreicht. Schau auf den Speed-Tape links am Display.',
simConditionNextPhase: 'rotation',
},
{
id: 'roll_rumble_explain',
@@ -228,6 +249,16 @@ export const takeoffEddf: FlightLabScenario = {
{ id: 'wind-low', action: 'crossfade', volume: 0.0 },
{ id: 'wind-high', action: 'play', volume: 0.35, loop: true },
],
simConditions: {
conditions: [
{ variable: 'SIM_ON_GROUND', operator: '==', value: false },
{ variable: 'PLANE_PITCH_DEGREES', operator: '>', value: 5 },
],
logic: 'AND',
},
simConditionTimeoutMs: 15000,
simConditionHelpMessage: 'Sidestick sanft nach hinten ziehen bis die Nase hochgeht.',
simConditionNextPhase: 'gear_retract',
},
{
id: 'rotation_belly_explain',
@@ -252,6 +283,15 @@ export const takeoffEddf: FlightLabScenario = {
sounds: [
{ id: 'gear-retract', action: 'play', volume: 0.6, loop: false },
],
simConditions: {
conditions: [
{ variable: 'GEAR_HANDLE_POSITION', operator: '==', value: false },
],
logic: 'AND',
},
simConditionTimeoutMs: 10000,
simConditionHelpMessage: 'Fahrwerk-Hebel nach oben schieben. Der Hebel ist links neben dem Mitteldisplay.',
simConditionNextPhase: 'climb',
},
{
id: 'gear_explain',
@@ -277,6 +317,16 @@ export const takeoffEddf: FlightLabScenario = {
{ id: 'engine-cruise', action: 'crossfade', volume: 0.3 },
{ id: 'wind-high', action: 'crossfade', volume: 0.25 },
],
simConditions: {
conditions: [
{ variable: 'PLANE_ALTITUDE', operator: '>', value: 2000 },
{ variable: 'VERTICAL_SPEED', operator: '>', value: 1000 },
],
logic: 'AND',
},
simConditionTimeoutMs: 30000,
simConditionHelpMessage: 'Steigrate erhoehen auf etwa 2000 Fuss pro Minute. Sidestick leicht nach hinten halten.',
simConditionNextPhase: 'climb_high',
},
{
id: 'climb_height_info',
@@ -313,6 +363,15 @@ export const takeoffEddf: FlightLabScenario = {
{ id: 'almost_there', label: 'Fast geschafft!', icon: 'mdi-flag-checkered', next: 'leveloff', type: 'primary' },
],
sounds: [],
simConditions: {
conditions: [
{ variable: 'PLANE_ALTITUDE', operator: '>=', value: 9800 },
],
logic: 'AND',
},
simConditionTimeoutMs: 60000,
simConditionHelpMessage: 'Weiter steigen bis 10.000 Fuss. Halte die Nase leicht oben.',
simConditionNextPhase: 'leveloff',
},
// --- Phase 8: Level-off & Debrief ---
@@ -332,6 +391,17 @@ export const takeoffEddf: FlightLabScenario = {
{ id: 'wind-high', action: 'crossfade', volume: 0.15 },
{ id: 'chime', action: 'play', volume: 0.3, loop: false },
],
simConditions: {
conditions: [
{ variable: 'VERTICAL_SPEED', operator: '<', value: 500 },
{ variable: 'VERTICAL_SPEED', operator: '>', value: -500 },
{ variable: 'PLANE_ALTITUDE', operator: '>=', value: 9500 },
],
logic: 'AND',
},
simConditionTimeoutMs: 20000,
simConditionHelpMessage: 'Nase etwas senken zum Geradeausflug. Steigrate auf nahe Null bringen.',
simConditionNextPhase: 'debrief',
},
{
id: 'debrief',

View File

@@ -16,6 +16,40 @@ export interface FlightLabButton {
instructorAlert?: string
}
// --- MSFS SimConnect Telemetry ---
/** MSFS2020 SimConnect variable state — keys match SimConnect naming */
export interface FlightLabTelemetryState {
AIRSPEED_INDICATED: number // knots
GROUND_VELOCITY: number // knots
VERTICAL_SPEED: number // feet per minute
PLANE_ALTITUDE: number // feet MSL
PLANE_PITCH_DEGREES: number // degrees
TURB_ENG_N1_1: number // percent (0-100), engine 1
TURB_ENG_N1_2: number // percent (0-100), engine 2
SIM_ON_GROUND: boolean
GEAR_HANDLE_POSITION: boolean // true = down, false = up
FLAPS_HANDLE_INDEX: number // 0-4 for A320
BRAKE_PARKING_POSITION: boolean // true = set, false = released
AUTOPILOT_MASTER: boolean
timestamp?: number
}
export type SimConditionOperator = '>' | '<' | '>=' | '<=' | '==' | '!='
export interface SimCondition {
variable: keyof FlightLabTelemetryState
operator: SimConditionOperator
value: number | boolean
}
export interface SimConditionGroup {
conditions: SimCondition[]
logic: 'AND' | 'OR'
}
// --- Phase ---
export interface FlightLabPhase {
id: string
atcMessage: string
@@ -24,8 +58,18 @@ export interface FlightLabPhase {
sounds?: FlightLabSound[]
instructorNote?: string
autoAdvanceAfterTTS?: boolean
/** SimConnect conditions for auto-advance (when sim data available) */
simConditions?: SimConditionGroup
/** How long to wait (ms) before showing help if conditions not met. Default 20000 */
simConditionTimeoutMs?: number
/** Help message spoken via TTS when timeout reached */
simConditionHelpMessage?: string
/** Phase to advance to when conditions are met */
simConditionNextPhase?: string
}
// --- Scenario ---
export interface FlightLabScenario {
id: string
title: string
@@ -38,6 +82,8 @@ export interface FlightLabScenario {
phases: FlightLabPhase[]
}
// --- Session / WebSocket ---
export type FlightLabRole = 'instructor' | 'participant'
export interface FlightLabSessionState {