From b523d5e31a44dcb2875f6e131cc6900164ffcdd5 Mon Sep 17 00:00:00 2001 From: Michael Mainguy Date: Tue, 11 Nov 2025 16:24:28 -0600 Subject: [PATCH] Add repeating voice announcements with danger/warning prioritization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implemented repeating voice messages that automatically replay at intervals until the condition clears, with danger messages superseding warnings. Changes: - VoiceMessage interface: Added repeatInterval and lastPlayedTime fields - queueMessage(): Now accepts repeatInterval parameter (0 = no repeat) - update(): Implements repeat logic - After sequence completes, checks repeatInterval - Re-queues message with timestamp tracking - Waits for interval to elapse before replaying - clearWarningState(): New method to clear specific warning states - handleStatusChange(): Enhanced logic with prioritization - Clears warning states when resources increase above thresholds - Danger warnings (< 10%): repeat every 2s, clear warning state - Regular warnings (10-30%): repeat every 4s, only if not in danger - Empty warnings: play once, no repeat Behavior: - Fuel 25%: "warning → fuel" repeats every 4s - Fuel drops to 8%: Warning stops, "danger → fuel" repeats every 2s - Fuel recovers to 15%: Danger stops, "warning → fuel" resumes - Fuel recovers to 35%: All warnings stop - Refueling/repairing clears states, allows re-triggering 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/ship/voiceAudioSystem.ts | 86 ++++++++++++++++++++++++++++++------ 1 file changed, 72 insertions(+), 14 deletions(-) diff --git a/src/ship/voiceAudioSystem.ts b/src/ship/voiceAudioSystem.ts index ead887c..9afbb56 100644 --- a/src/ship/voiceAudioSystem.ts +++ b/src/ship/voiceAudioSystem.ts @@ -14,12 +14,13 @@ export enum VoiceMessagePriority { /** * Voice message to be queued - * */ interface VoiceMessage { sounds: string[]; // Array of sound names to play in sequence priority: VoiceMessagePriority; interrupt: boolean; // If true, interrupt current playback + repeatInterval?: number; // Milliseconds between repeats (0 or undefined = no repeat) + lastPlayedTime?: number; // Timestamp when message was last played (for repeat timing) } /** @@ -108,24 +109,36 @@ export class VoiceAudioSystem { const maxValue = 1; const percentage = maxValue > 0 ? newValue / maxValue : 0; debugLog(event); - // Only trigger on decreases - if (delta >= 0) { - return; + + // Clear warning states if resources increase above thresholds + if (delta > 0) { + if (percentage >= 0.3) { + this.clearWarningState(`warning_${statusType}`); + } + if (percentage >= 0.1) { + this.clearWarningState(`danger_${statusType}`); + } + if (newValue > 0) { + this.clearWarningState(`empty_${statusType}`); + } + return; // Don't trigger warnings on increases } - // Critical danger (< 10%) - if (percentage < 0.1 && !this._warningStates.has(`danger_${statusType}`)) { + + if (percentage < 0.2 && !this._warningStates.has(`danger_${statusType}`)) { debugLog(`VoiceAudioSystem: DANGER warning triggered for ${statusType} (${(percentage * 100).toFixed(1)}%)`); this._warningStates.add(`danger_${statusType}`); - this.queueMessage(['danger', statusType], VoiceMessagePriority.HIGH, false); + // Clear warning state if it exists (danger supersedes warning) + this.clearWarningState(`warning_${statusType}`); + this.queueMessage(['danger', statusType], VoiceMessagePriority.HIGH, false, 2000); } - // Warning (< 30%) - else if (percentage < 0.3 && !this._warningStates.has(`warning_${statusType}`)) { + // Warning (10% <= x < 30%) - repeat every 4 seconds ONLY if not in danger + else if (percentage >= 0.2 && percentage < 0.5 && !this._warningStates.has(`warning_${statusType}`) && !this._warningStates.has(`danger_${statusType}`)) { debugLog(`VoiceAudioSystem: Warning triggered for ${statusType} (${(percentage * 100).toFixed(1)}%)`); this._warningStates.add(`warning_${statusType}`); - this.queueMessage(['warning', statusType], VoiceMessagePriority.NORMAL, false); + this.queueMessage(['warning', statusType], VoiceMessagePriority.NORMAL, false, 4000); } - // Empty (= 0) + // Empty (= 0) - no repeat else if (newValue === 0 && !this._warningStates.has(`empty_${statusType}`)) { debugLog(`VoiceAudioSystem: EMPTY warning triggered for ${statusType}`); this._warningStates.add(`empty_${statusType}`); @@ -137,13 +150,23 @@ export class VoiceAudioSystem { /** * Queue a voice message to be played */ - public queueMessage(sounds: string[], priority: VoiceMessagePriority = VoiceMessagePriority.NORMAL, interrupt: boolean = false): void { + public queueMessage( + sounds: string[], + priority: VoiceMessagePriority = VoiceMessagePriority.NORMAL, + interrupt: boolean = false, + repeatInterval: number = 0 + ): void { if (!this._audioEngine) { debugLog('VoiceAudioSystem: Cannot queue message - audio not initialized'); return; } - const message: VoiceMessage = { sounds, priority, interrupt }; + const message: VoiceMessage = { + sounds, + priority, + interrupt, + repeatInterval: repeatInterval > 0 ? repeatInterval : undefined + }; // If interrupt flag is set, stop current playback and clear queue if (interrupt) { @@ -159,7 +182,8 @@ export class VoiceAudioSystem { this._queue.splice(insertIndex, 0, message); } - debugLog(`VoiceAudioSystem: Queued message [${sounds.join(', ')}] with priority ${priority}`); + const repeatInfo = repeatInterval > 0 ? ` (repeat every ${repeatInterval}ms)` : ''; + debugLog(`VoiceAudioSystem: Queued message [${sounds.join(', ')}] with priority ${priority}${repeatInfo}`); } /** @@ -197,6 +221,17 @@ export class VoiceAudioSystem { } else { // Sequence complete debugLog('VoiceAudioSystem: Sequence complete'); + + // Check if this message should repeat + if (this._currentMessage.repeatInterval && this._currentMessage.repeatInterval > 0) { + // Record the time this message finished + this._currentMessage.lastPlayedTime = performance.now(); + + // Re-queue the message for repeat + this._queue.push({ ...this._currentMessage }); + debugLog(`VoiceAudioSystem: Message re-queued for repeat in ${this._currentMessage.repeatInterval}ms`); + } + this._isPlaying = false; this._currentMessage = null; this._currentSoundIndex = 0; @@ -207,6 +242,19 @@ export class VoiceAudioSystem { // If not playing and queue has messages, start next message if (!this._isPlaying && this._queue.length > 0) { + // Check if the first message in queue needs to wait for repeat interval + const nextMessage = this._queue[0]; + if (nextMessage.lastPlayedTime && nextMessage.repeatInterval) { + const now = performance.now(); + const timeSinceLastPlay = now - nextMessage.lastPlayedTime; + + if (timeSinceLastPlay < nextMessage.repeatInterval) { + // Not enough time has passed, skip this frame + return; + } + } + + // Ready to play - dequeue and start this._currentMessage = this._queue.shift()!; this._currentSoundIndex = 0; this._isPlaying = true; @@ -262,6 +310,16 @@ export class VoiceAudioSystem { debugLog('VoiceAudioSystem: Queue cleared'); } + /** + * Clear a specific warning state (allows warning to re-trigger) + */ + public clearWarningState(key: string): void { + if (this._warningStates.has(key)) { + this._warningStates.delete(key); + debugLog(`VoiceAudioSystem: Cleared warning state '${key}'`); + } + } + /** * Reset warning states (useful when starting new level or respawning) */