PageSourceSearch

https://ilmenit.github.io/pressure/js/sound-manager.js

js ilmenit.github.io collected 2026-10-03 09:20:28 UTC 19,862 bytes, 550 lines download raw bytes

1/**
2 * Sound Manager for the game
3 * Implements the Observer pattern to respond to game events with sounds
4 * Enhanced to use simulation context filtering
5 */
6class SoundManager {
7    constructor(events) {
8        this.events = events || window.gameEvents;
9        this.sounds = {};
10        this.loadedSounds = 0;
11        this.totalSounds = 0;
12        this.isSoundEnabled = true;
13        this.isMuted = false;
14        this.isReady = false;
15        
16        // Load sound preferences first
17        this.loadSoundPreferences();
18        
19        // Initialize and load sounds
20        this.initSounds();
21        
22        // Set up event listeners after a short delay to ensure game is initialized
23        setTimeout(() => this.setupEventListeners(), 100);
24    }
25    
26    /**
27     * Initialize sound objects with error handling
28     */
29    initSounds() {
30        const soundFiles = {
31            // Menu and UI sounds
32            buttonClick: 'assets/sounds/button-click.mp3',
33            menuButtonClick: 'assets/sounds/menu-button-click.mp3',
34            
35            // Game sounds
36            tokenSelect: 'assets/sounds/token-select.mp3',
37            player1Move: 'assets/sounds/player1-move.mp3',
38            player2Move: 'assets/sounds/player2-move.mp3',
39            player1Capture: 'assets/sounds/player1-capture.mp3',
40            player2Capture: 'assets/sounds/player2-capture.mp3',
41            
42            // Result sounds
43            matchWin: 'assets/sounds/match_win.mp3',
44            matchLose: 'assets/sounds/match-lose.mp3',
45            winTournament: 'assets/sounds/win-tournament.mp3'
46        };
47        
48        this.totalSounds = Object.keys(soundFiles).length;
49        
50        // Create and load all sound objects
51        Object.entries(soundFiles).forEach(([name, path]) => {
52            try {
53                const audio = new Audio();
54                
55                // Add event listeners for loading and errors
56                audio.addEventListener('canplaythrough', () => {
57                    this.loadedSounds++;
58                    if (this.events && this.loadedSounds === this.totalSounds) {
59                        this.isReady = true;
60                        this.events.emit('sound:allLoaded', {
61                            soundCount: this.totalSounds
62                        });
63                    }
64                }, { once: true });
65                
66                audio.addEventListener('error', (e) => {
67                    console.warn(`Failed to load sound ${name} from ${path}:`, e);
68                });
69                
70                // Set source and load
71                audio.src = path;
72                audio.volume = 0.5;
73                audio.load();
74                
75                this.sounds[name] = audio;
76            } catch (e) {
77                console.error(`Failed to create audio object for ${name}:`, e);
78            }
79        });
80    }
81    
82    /**
83     * Set up event listeners for game events
84     */
85    setupEventListeners() {
86        if (!this.events) return;
87        
88        // Define event to sound mappings for game events - using onReal to filter simulations
89        const eventSoundMap = {
90            // UI interactions - these are always real anyway
91            'ui:tokenSelected': (data) => {
92                // If the UIManager has already validated this token, use that information
93                if (data && data.isValidToken === true) {
94                    return 'tokenSelect';
95                }
96                
97                // Otherwise, perform our own validation
98                if (data && data.position && window.game) {
99                    const row = data.position.row;
100                    const col = data.position.col;
101                    const token = window.game.board.getTokenAt(row, col);
102                    
103                    if (token && 
104                        token.color === window.game.currentPlayer && 
105                        token.isActive && 
106                        !token.isCaptured) {
107                        return 'tokenSelect';
108                    }
109                }
110                return null;
111            },
112            
113            'ui:winModalShown': data => {
114                // In tournament mode, check if the winner is the player
115                if (window.game && window.game.isTournamentMode && window.game.tournamentManager) {
116                    const playerColor = window.game.tournamentManager.playerColor;
117                    return data.winner === playerColor ? 'matchWin' : 'matchLose';
118                }
119                // In standard mode, use the default behavior (white = player)
120                return data.winner === 'white' ? 'matchWin' : 'matchLose';
121            },
122            
123            // Move execution - now using onReal events
124            'move:executed': (data) => {
125                // Tournament mode - check who's moving based on player color
126                if (window.game && window.game.isTournamentMode && window.game.tournamentManager) {
127                    const playerColor = window.game.tournamentManager.playerColor;
128                    return data.player === playerColor ? 'player1Move' : 'player2Move';
129                }
130                
131                // Standard mode - use default logic (white = player1, black = player2)
132                return data.player === 'white' ? 'player1Move' : 'player2Move';
133            },
134            
135            // Token events - capture sounds
136            'token:captured': (data) => {
137                // Play sound based on the color of the captured token
138                if (data && data.color) {
139                    // In tournament mode, check if the captured token belongs to player or AI
140                    if (window.game && window.game.isTournamentMode && window.game.tournamentManager) {
141                        const playerColor = window.game.tournamentManager.playerColor;
142                        // If the captured token belongs to the player, play player1Capture
143                        // If it belongs to the AI, play player2Capture
144                        return data.color === playerColor ? 'player1Capture' : 'player2Capture';
145                    }
146                    
147                    // In standard mode, use the default logic
148                    return data.color === 'white' ? 'player1Capture' : 'player2Capture';
149                }
150                
151                // Fallback
152                return 'player1Capture';
153            },
154                        
155            // Game state
156            'game:over': data => {
157                // In tournament mode, check if the winner is the player
158                if (window.game && window.game.isTournamentMode && window.game.tournamentManager) {
159                    const playerColor = window.game.tournamentManager.playerColor;
160                    return data.winner === playerColor ? 'matchWin' : 'matchLose';
161                }
162                
163                // In standard mode, use the default behavior (white = player)
164                return data.winner === 'white' ? 'matchWin' : 'matchLose';
165            },
166            
167            // Tournament events
168            'tournament:completed': 'winTournament',
169            'tournament:gameEnded': data => {
170                // Check if the winner is the player
171                if (window.game && window.game.tournamentManager) {
172                    const playerColor = window.game.tournamentManager.playerColor;
173                    return data.winner === playerColor ? 'matchWin' : 'matchLose';
174                }
175                // Fallback to default if tournament manager not available
176                return data.winner === 'white' ? 'matchWin' : 'matchLose';
177            },
178        };
179        
180        // Add tutorial events if they exist
181        if (typeof window.tutorialService !== 'undefined') {
182            eventSoundMap['tutorial:completed'] = 'winTournament';
183            eventSoundMap['tutorial:stepCompleted'] = 'tokenSelect';
184        }
185        
186        // Register listeners based on mapping - always use onReal for gameplay events
187        Object.entries(eventSoundMap).forEach(([eventName, soundNameOrFn]) => {
188            // Gameplay events should use onReal to filter out simulations
189            if (eventName.startsWith('move:') || 
190                eventName === 'token:captured' || 
191                eventName === 'game:over') {
192                
193                this.events.onReal(eventName, data => {
194                    try {
195                        const soundName = typeof soundNameOrFn === 'function' 
196                            ? soundNameOrFn(data) 
197                            : soundNameOrFn;
198                        
199                        // Only play if soundName is not null
200                        if (soundName) {
201                            this.playSound(soundName);
202                        }
203                    } catch (e) {
204                        console.warn(`Error playing sound for event ${eventName}:`, e);
205                    }
206                });
207            } else {
208                // UI events are already real (not simulated) so can use regular on
209                this.events.on(eventName, data => {
210                    try {
211                        const soundName = typeof soundNameOrFn === 'function' 
212                            ? soundNameOrFn(data) 
213                            : soundNameOrFn;
214                        
215                        // Only play if soundName is not null
216                        if (soundName) {
217                            this.playSound(soundName);
218                        }
219                    } catch (e) {
220                        console.warn(`Error playing sound for event ${eventName}:`, e);
221                    }
222                });
223            }
224        });
225        
226        // Add button click sounds
227        this.addButtonClickSounds();
228    }
229    
230	/**
231	 * Add button click sounds to various UI elements
232	 * Fixed version that prevents duplicates
233	 */
234	addButtonClickSounds() {
235		// Flag to track if document handler is already added
236		if (this._documentHandlerAdded) return;
237		this._documentHandlerAdded = true;
238
239		// Add direct click listeners to all buttons
240		const addButtonSounds = () => {
241			// Find all buttons in the document
242			const allButtons = document.querySelectorAll('button:not([data-sound-added])');
243
244			allButtons.forEach(button => {
245				// Skip buttons that already have sound handlers
246				if (button.hasAttribute('data-sound-added')) return;
247
248				// Add the sound handler
249				button.addEventListener('click', (e) => {
250					// Skip sound toggle button to avoid double sounds
251					if (button.id === 'sound-toggle-btn' || button.closest('#sound-toggle')) return;
252
253					// Determine which sound to play based on context
254					if (button.closest('#main-menu') ||
255						button.classList.contains('large-btn') ||
256						button.closest('.tournament-controls')) {
257						this.playSound('menuButtonClick');
258					} else {
259						this.playSound('buttonClick');
260					}
261
262					// Mark event as having played a sound
263					e._soundPlayed = true;
264				}, true); // Use capture to ensure we get the event before stopPropagation
265
266				// Mark the button as having a sound handler
267				button.setAttribute('data-sound-added', 'true');
268			});
269		};
270
271		// Initial application of sound handlers
272		addButtonSounds();
273
274		// Add event delegation for dynamically added elements
275		document.addEventListener('click', e => {
276			// Skip if sound already played for this event or if clicked on/within element with sound handler
277			if (e._soundPlayed || e.target.closest('[data-sound-added]')) return;
278
279			// Handle cell clicks (but not token clicks)
280			const cell = e.target.closest('.cell');
281			const token = e.target.closest('.token');
282
283			if (cell && !token && !cell.hasAttribute('data-sound-added')) {
284				cell.setAttribute('data-sound-added', 'true');
285				this.playSound('buttonClick');
286				return;
287			}
288
289			// Handle ladder opponent clicks in tournament mode
290			const ladderOpponent = e.target.closest('.ladder-opponent');
291			if (ladderOpponent && !ladderOpponent.hasAttribute('data-sound-added')) {
292				ladderOpponent.setAttribute('data-sound-added', 'true');
293				this.playSound('menuButtonClick');
294				return;
295			}
296		});
297
298		// Use MutationObserver to detect new buttons
299		const observer = new MutationObserver((mutations) => {
300			let shouldAddSounds = false;
301
302			// Check if relevant nodes were added
303			for (const mutation of mutations) {
304				if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
305					for (const node of mutation.addedNodes) {
306						if (node.nodeType === 1) { // Element node
307							shouldAddSounds = true;
308							break;
309						}
310					}
311					if (shouldAddSounds) break;
312				}
313			}
314
315			if (shouldAddSounds) {
316				addButtonSounds();
317			}
318		});
319
320		// Start observing document body
321		observer.observe(document.body, {
322			childList: true,
323			subtree: true
324		});
325	}
326
327    /**
328     * Play a sound if sound is enabled
329     * @param {string} soundName - Name of the sound to play
330     */
331    playSound(soundName) {
332        if (!this.isSoundEnabled || this.isMuted || !this.isReady) return;
333        
334        const sound = this.sounds[soundName];
335        if (sound) {
336            try {
337                // Try to use the original sound object (more efficient)
338                if (sound.paused || sound.ended) {
339                    sound.currentTime = 0;
340                    const playPromise = sound.play();
341                    
342                    if (playPromise !== undefined) {
343                        playPromise.catch(e => {
344                            console.warn(`Failed to play sound ${soundName} (original):`, e);
345                            this.playClonedSound(sound, soundName);
346                        });
347                    }
348                } else {
349                    // Sound is already playing, use a clone
350                    this.playClonedSound(sound, soundName);
351                }
352            } catch (e) {
353                console.warn(`Failed to play sound ${soundName}:`, e);
354            }
355        }
356    }
357    
358    /**
359     * Play a cloned sound for overlapping sounds
360     * @param {HTMLAudioElement} sound - Original sound
361     * @param {string} soundName - Sound name for debug
362     */
363    playClonedSound(sound, soundName) {
364        try {
365            const soundClone = sound.cloneNode();
366            soundClone.volume = sound.volume;
367            
368            const playPromise = soundClone.play();
369            if (playPromise !== undefined) {
370                playPromise.catch(e => {
371                    console.warn(`Failed to play sound ${soundName} (clone):`, e);
372                });
373            }
374        } catch (e) {
375            console.warn(`Failed to clone sound ${soundName}:`, e);
376        }
377    }
378    
379    /**
380     * Enable or disable all sounds
381     * @param {boolean} enabled - Whether sound should be enabled
382     */
383    setSoundEnabled(enabled) {
384        this.isSoundEnabled = enabled;
385        
386        // Save preference
387        this.saveSoundPreferences();
388        
389        // Emit event
390        if (this.events) {
391            this.events.emit('sound:enabledChanged', {
392                enabled: this.isSoundEnabled
393            });
394        }
395        
396        // Update UI toggle if it exists
397        this.updateSoundToggleUI();
398    }
399    
400    /**
401     * Toggle sound enabled state
402     * @returns {boolean} - New sound enabled state
403     */
404    toggleSound() {
405        this.setSoundEnabled(!this.isSoundEnabled);
406        return this.isSoundEnabled;
407    }
408    
409    /**
410     * Mute or unmute all sounds (temporary)
411     * @param {boolean} muted - Whether sound should be muted
412     */
413    setMuted(muted) {
414        this.isMuted = muted;
415        
416        // Emit event
417        if (this.events) {
418            this.events.emit('sound:mutedChanged', {
419                muted: this.isMuted
420            });
421        }
422    }
423    
424    /**
425     * Toggle mute state
426     * @returns {boolean} - New muted state
427     */
428    toggleMute() {
429        this.setMuted(!this.isMuted);
430        return this.isMuted;
431    }
432    
433    /**
434     * Set volume for all sounds
435     * @param {number} volume - Volume level (0 to 1)
436     */
437    setVolume(volume) {
438        // Clamp volume between 0 and 1
439        const clampedVolume = Math.min(Math.max(volume, 0), 1);
440        
441        // Set volume for all sounds
442        Object.values(this.sounds).forEach(sound => {
443            sound.volume = clampedVolume;
444        });
445        
446        // Emit event
447        if (this.events) {
448            this.events.emit('sound:volumeChanged', {
449                volume: clampedVolume
450            });
451        }
452    }
453    
454    /**
455     * Save sound preferences to localStorage
456     */
457    saveSoundPreferences() {
458        try {
459            localStorage.setItem('pressure_sound_enabled', this.isSoundEnabled.toString());
460        } catch (e) {
461            console.warn('Failed to save sound preferences:', e);
462        }
463    }
464    
465    /**
466     * Load sound preferences from localStorage
467     */
468    loadSoundPreferences() {
469        try {
470            const soundEnabled = localStorage.getItem('pressure_sound_enabled');
471            if (soundEnabled !== null) {
472                this.isSoundEnabled = soundEnabled === 'true';
473            }
474        } catch (e) {
475            console.warn('Failed to load sound preferences:', e);
476        }
477    }
478    
479    /**
480     * Update the sound toggle UI elements
481     */
482    updateSoundToggleUI() {
483        // Update floating sound toggle
484        const soundOn = document.getElementById('sound-on');
485        const soundOff = document.getElementById('sound-off');
486        
487        if (soundOn && soundOff) {
488            soundOn.style.display = this.isSoundEnabled ? 'inline' : 'none';
489            soundOff.style.display = this.isSoundEnabled ? 'none' : 'inline';
490        }
491        
492        // Update settings toggle button
493        const soundToggleBtn = document.getElementById('sound-toggle-btn');
494        if (soundToggleBtn) {
495            soundToggleBtn.textContent = `Sound: ${this.isSoundEnabled ? 'ON' : 'OFF'}`;
496        }
497    }
498    
499    /**
500     * Add floating sound toggle to the UI
501     */
502    addSoundToggleToUI() {
503        // Don't add if it already exists
504        if (document.getElementById('sound-toggle')) return;
505        
506        // Create sound toggle button
507        const soundToggle = document.createElement('div');
508        soundToggle.id = 'sound-toggle';
509        soundToggle.className = 'sound-toggle';
510        soundToggle.innerHTML = `
511            <span id="sound-on" ${!this.isSoundEnabled ? 'style="display:none"' : ''}>&#128266;</span>
512            <span id="sound-off" ${this.isSoundEnabled ? 'style="display:none"' : ''}>&#128263;</span>
513            `;
514        
515        // Add click event listener
516        soundToggle.addEventListener('click', () => {
517            const isEnabled = this.toggleSound();
518            
519            // Play sound when enabling
520            if (isEnabled) {
521                this.playSound('buttonClick');
522            }
523        });
524        
525        // Add to document
526        document.body.appendChild(soundToggle);
527    }
528}
529
530// Create sound manager instance when DOM is loaded
531document.addEventListener('DOMContentLoaded', () => {
532    // Wait for event system to be available
533    const initSoundManager = () => {
534        if (window.gameEvents) {
535            window.soundManager = new SoundManager(window.gameEvents);
536            
537            // Add sound toggle to UI after a short delay to ensure DOM is fully ready
538            setTimeout(() => {
539                if (window.soundManager) {
540                    window.soundManager.addSoundToggleToUI();
541                }
542            }, 500);
543        } else {
544            // Retry after a short delay
545            setTimeout(initSoundManager, 100);
546        }
547    };
548    
549    initSoundManager();
550});

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.