/** * Sound Manager for RTS Game * Handles loading and playing sound effects * * Features: * - Async sound loading * - Volume control * - Enable/disable toggle * - Multiple concurrent sounds * - Fallback for unsupported formats */ class SoundManager { constructor() { this.sounds = {}; this.enabled = true; this.volume = 0.5; this.loaded = false; this.audioContext = null; // Initialize Audio Context (for better browser support) try { this.audioContext = new (window.AudioContext || window.webkitAudioContext)(); } catch (e) { console.warn('[SoundManager] Web Audio API not supported, using HTML5 Audio fallback'); } } /** * Load all game sounds */ async loadAll() { console.log('[SoundManager] Loading sounds...'); const soundFiles = { 'unit_fire': '/static/sounds/fire.wav', 'explosion': '/static/sounds/explosion.wav', 'build_complete': '/static/sounds/build.wav', 'unit_ready': '/static/sounds/ready.wav', }; const loadPromises = Object.entries(soundFiles).map(([name, url]) => this.loadSound(name, url) ); await Promise.all(loadPromises); this.loaded = true; console.log(`[SoundManager] Loaded ${Object.keys(this.sounds).length} sounds`); } /** * Load a single sound file */ async loadSound(name, url) { try { const audio = new Audio(url); audio.preload = 'auto'; audio.volume = this.volume; // Wait for audio to be ready await new Promise((resolve, reject) => { audio.addEventListener('canplaythrough', resolve, { once: true }); audio.addEventListener('error', reject, { once: true }); audio.load(); }); this.sounds[name] = audio; console.log(`[SoundManager] Loaded: ${name}`); } catch (e) { console.warn(`[SoundManager] Failed to load sound: ${name}`, e); } } /** * Play a sound effect * @param {string} name - Sound name * @param {number} volumeMultiplier - Optional volume multiplier (0.0-1.0) */ play(name, volumeMultiplier = 1.0) { if (!this.enabled || !this.loaded || !this.sounds[name]) { return; } try { // Clone the audio element to allow multiple concurrent plays const audio = this.sounds[name].cloneNode(); audio.volume = this.volume * volumeMultiplier; // Play and auto-cleanup audio.play().catch(e => { // Ignore play errors (user interaction required, etc.) console.debug(`[SoundManager] Play failed: ${name}`, e.message); }); // Remove after playing to free memory audio.addEventListener('ended', () => { audio.src = ''; audio.remove(); }); } catch (e) { console.debug(`[SoundManager] Error playing sound: ${name}`, e); } } /** * Toggle sound on/off */ toggle() { this.enabled = !this.enabled; console.log(`[SoundManager] Sound ${this.enabled ? 'enabled' : 'disabled'}`); return this.enabled; } /** * Set volume (0.0 - 1.0) */ setVolume(volume) { this.volume = Math.max(0, Math.min(1, volume)); // Update volume for all loaded sounds Object.values(this.sounds).forEach(audio => { audio.volume = this.volume; }); console.log(`[SoundManager] Volume set to ${Math.round(this.volume * 100)}%`); } /** * Check if sounds are loaded */ isLoaded() { return this.loaded; } /** * Get sound status */ getStatus() { return { enabled: this.enabled, volume: this.volume, loaded: this.loaded, soundCount: Object.keys(this.sounds).length, }; } } // Export for use in game.js window.SoundManager = SoundManager;