From 9b22b06d08072e08abd426a730cf3bbd22695dfc Mon Sep 17 00:00:00 2001 From: Michael Mainguy Date: Tue, 11 Nov 2025 16:57:12 -0600 Subject: [PATCH] Fix critical bug: Repeating voice messages now stop when resources recover MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added stateKey field to VoiceMessage interface to explicitly link messages with warning states, preventing infinite repeat loops. Bug fixes: - Messages check warning state exists before re-queuing - clearWarningState() now purges matching messages from queue - Proper cleanup when resources increase above thresholds Changes to voiceAudioSystem.ts: - VoiceMessage interface: Added stateKey field - queueMessage(): Added stateKey parameter - handleStatusChange(): Pass state keys when queueing warnings - update(): Validate state exists before re-queuing repeating messages - clearWarningState(): Filter queue to remove messages with matching stateKey Also includes explosion audio improvements: - Use onEndedObservable instead of setTimeout for audio cleanup - Adjusted explosion force and duration parameters 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- public/levels/1.json | 0 public/levels/directory.json | 0 src/environment/asteroids/explosionManager.ts | 20 ++++----- src/environment/asteroids/rockFactory.ts | 5 +-- src/ship/voiceAudioSystem.ts | 42 +++++++++++++------ 5 files changed, 42 insertions(+), 25 deletions(-) create mode 100644 public/levels/1.json create mode 100644 public/levels/directory.json diff --git a/public/levels/1.json b/public/levels/1.json new file mode 100644 index 0000000..e69de29 diff --git a/public/levels/directory.json b/public/levels/directory.json new file mode 100644 index 0000000..e69de29 diff --git a/src/environment/asteroids/explosionManager.ts b/src/environment/asteroids/explosionManager.ts index 6b38424..124051d 100644 --- a/src/environment/asteroids/explosionManager.ts +++ b/src/environment/asteroids/explosionManager.ts @@ -34,18 +34,19 @@ export class ExplosionManager { // Default configuration private static readonly DEFAULT_CONFIG: Required = { - duration: 1000, - explosionForce: 5, + duration: 2000, + explosionForce: 10, frameRate: 60 }; constructor(scene: Scene, config?: ExplosionConfig) { this.scene = scene; this.config = { ...ExplosionManager.DEFAULT_CONFIG, ...config }; + debugLog(this.config); this._debrisBaseMesh = MeshBuilder.CreateIcoSphere( 'debrisBase', { - radius: .2, + radius: 1, subdivisions: 2 }, DefaultScene.MainScene ); @@ -130,14 +131,13 @@ export class ExplosionManager { // Attach spatial sound to the node sound.spatial.attach(explosionNode); sound.play(); - - // Cleanup after explosion duration (synchronized with visual effect) - setTimeout(() => { - if (sound.spatial) { - sound.spatial.detach(); - } + sound.onEndedObservable.addOnce(() => { + //Cleanup after sound ends. + sound.spatial.detach(); explosionNode.dispose(); - }, this.config.duration); + }) + + } catch (error) { debugLog("ExplosionManager: Error playing explosion audio", error); explosionNode.dispose(); diff --git a/src/environment/asteroids/rockFactory.ts b/src/environment/asteroids/rockFactory.ts index ddf302b..2bc1fb7 100644 --- a/src/environment/asteroids/rockFactory.ts +++ b/src/environment/asteroids/rockFactory.ts @@ -48,8 +48,8 @@ export class RockFactory { this._orbitCenter = new PhysicsAggregate(node, PhysicsShapeType.SPHERE, {radius: .1, mass: 1000}, DefaultScene.MainScene ); this._orbitCenter.body.setMotionType(PhysicsMotionType.ANIMATED); this._explosionManager = new ExplosionManager(DefaultScene.MainScene, { - duration: 800, - explosionForce: 20.0, + duration: 2000, + explosionForce: 150.0, frameRate: 60 }); await this._explosionManager.initialize(); @@ -102,7 +102,6 @@ export class RockFactory { const body = agg.body; const constraint = new DistanceConstraint(Vector3.Distance(position, this._orbitCenter.body.transformNode.position), DefaultScene.MainScene); body.addConstraint(this._orbitCenter.body, constraint); - body.setLinearDamping(0) body.setMotionType(PhysicsMotionType.DYNAMIC); body.setCollisionCallbackEnabled(true); diff --git a/src/ship/voiceAudioSystem.ts b/src/ship/voiceAudioSystem.ts index 9afbb56..5c392fc 100644 --- a/src/ship/voiceAudioSystem.ts +++ b/src/ship/voiceAudioSystem.ts @@ -21,6 +21,7 @@ interface VoiceMessage { 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) + stateKey?: string; // Warning state key (e.g., "warning_fuel") to track ownership } /** @@ -130,19 +131,19 @@ export class VoiceAudioSystem { this._warningStates.add(`danger_${statusType}`); // Clear warning state if it exists (danger supersedes warning) this.clearWarningState(`warning_${statusType}`); - this.queueMessage(['danger', statusType], VoiceMessagePriority.HIGH, false, 2000); + this.queueMessage(['danger', statusType], VoiceMessagePriority.HIGH, false, 2000, `danger_${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, 4000); + this.queueMessage(['warning', statusType], VoiceMessagePriority.NORMAL, false, 4000, `warning_${statusType}`); } // 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}`); - this.queueMessage([statusType, 'empty'], VoiceMessagePriority.HIGH, false); + this.queueMessage([statusType, 'empty'], VoiceMessagePriority.HIGH, false, 0, `empty_${statusType}`); } } @@ -154,7 +155,8 @@ export class VoiceAudioSystem { sounds: string[], priority: VoiceMessagePriority = VoiceMessagePriority.NORMAL, interrupt: boolean = false, - repeatInterval: number = 0 + repeatInterval: number = 0, + stateKey?: string ): void { if (!this._audioEngine) { debugLog('VoiceAudioSystem: Cannot queue message - audio not initialized'); @@ -165,7 +167,8 @@ export class VoiceAudioSystem { sounds, priority, interrupt, - repeatInterval: repeatInterval > 0 ? repeatInterval : undefined + repeatInterval: repeatInterval > 0 ? repeatInterval : undefined, + stateKey }; // If interrupt flag is set, stop current playback and clear queue @@ -224,12 +227,17 @@ export class VoiceAudioSystem { // 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(); + // Only re-queue if the warning state still exists + if (!this._currentMessage.stateKey || this._warningStates.has(this._currentMessage.stateKey)) { + // 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`); + // Re-queue the message for repeat + this._queue.push({ ...this._currentMessage }); + debugLog(`VoiceAudioSystem: Message re-queued for repeat in ${this._currentMessage.repeatInterval}ms`); + } else { + debugLog(`VoiceAudioSystem: Message NOT re-queued - warning state '${this._currentMessage.stateKey}' cleared`); + } } this._isPlaying = false; @@ -311,12 +319,22 @@ export class VoiceAudioSystem { } /** - * Clear a specific warning state (allows warning to re-trigger) + * Clear a specific warning state and remove matching messages from queue */ public clearWarningState(key: string): void { if (this._warningStates.has(key)) { this._warningStates.delete(key); - debugLog(`VoiceAudioSystem: Cleared warning state '${key}'`); + + // Remove all messages with this state key from queue + const originalLength = this._queue.length; + this._queue = this._queue.filter(msg => msg.stateKey !== key); + + const removed = originalLength - this._queue.length; + if (removed > 0) { + debugLog(`VoiceAudioSystem: Cleared warning state '${key}' and purged ${removed} queued message(s)`); + } else { + debugLog(`VoiceAudioSystem: Cleared warning state '${key}'`); + } } }