/** * Audio Manager - Centralized Audio System for Gooner Training Academy * Handles multiple audio categories with volume controls and fade effects */ class AudioManager { constructor(dataManager, game = null) { this.dataManager = dataManager; this.game = game; // Reference to game instance for state checking this.audioEnabled = true; // Global flag to disable all audio this.isPlaylistMode = false; // New playlist mode flag this.currentPlaylistAudio = null; // Current playlist audio element this.playlistQueue = []; // Queue of audio files to play this.currentPlaylistIndex = 0; // Current position in playlist this.audioContext = null; this.masterGain = null; // Audio category configurations - simplified to just background audio this.categories = { background: { volume: 0.7, enabled: true, currentAudio: null, fadeTimeout: null } }; this.masterVolume = 0.7; this.isInitialized = false; this.audioLibrary = {}; this.init(); } async init() { console.log('AudioManager initializing...'); // Check if we're in a problematic browser environment const isElectron = window.electronAPI !== undefined; const isFileProtocol = window.location.protocol === 'file:'; if (!isElectron && isFileProtocol) { console.warn('โš ๏ธ Browser file:// mode detected - audio may not work due to CORS restrictions'); console.warn('๐Ÿ’ก For full audio support, run: npm start (Electron desktop app)'); // Option to disable audio in browser mode const disableAudio = localStorage.getItem('disableBrowserAudio') === 'true'; if (disableAudio) { console.log('๐Ÿ”‡ Audio disabled for browser mode'); this.isInitialized = true; return; } } // Load saved settings this.loadSettings(); // Initialize Web Audio API for better control try { this.audioContext = new (window.AudioContext || window.webkitAudioContext)(); this.masterGain = this.audioContext.createGain(); this.masterGain.connect(this.audioContext.destination); this.masterGain.gain.value = this.masterVolume; } catch (error) { console.warn('Web Audio API not available, falling back to HTML5 audio:', error); } // Discover available audio files await this.discoverAudioLibrary(); this.isInitialized = true; console.log('AudioManager initialized successfully'); console.log('Available audio categories:', Object.keys(this.audioLibrary)); } // Clean up corrupted audio data (paths with URL encoding issues) async cleanupCorruptedAudioData() { if (!window.game || !window.game.dataManager) { return; } const customAudio = window.game.dataManager.get('customAudio') || { background: [], ambient: [] }; let hasCorruption = false; // Check for and fix corrupted paths ['background', 'ambient'].forEach(category => { if (customAudio[category]) { customAudio[category] = customAudio[category].filter(audioFile => { // Remove files with corrupted paths (containing URL encoding artifacts) if (audioFile.path && audioFile.path.includes('%08')) { console.log(`Removing corrupted audio file: ${audioFile.path}`); hasCorruption = true; return false; } return true; }); } }); // Remove any references to old 'effects' category if (customAudio.effects) { console.log('Removing deprecated effects category'); delete customAudio.effects; hasCorruption = true; } if (hasCorruption) { console.log('๐Ÿงน Cleaned up corrupted audio data'); window.game.dataManager.set('customAudio', customAudio); } } async discoverAudioLibrary() { console.log('๐ŸŽต Discovering audio library from user-managed files...'); // Clean up any corrupted audio data first await this.cleanupCorruptedAudioData(); // Instead of hardcoded files, use the dynamic audio management system // This integrates with the existing "Manage Audio" menu that users can control // Initialize the library structure this.audioLibrary = { background: { general: [] // Single category for all background audio } }; // Get audio files from the existing audio management system if (window.game && window.game.dataManager) { const customAudio = window.game.dataManager.get('customAudio') || { background: [], ambient: [] }; // Combine all audio categories into a single "background" category const allAudioFiles = [ ...(customAudio.background || []), ...(customAudio.ambient || []) ]; // Add user's audio files to the library (only if they actually exist) const validFiles = []; for (const audioFile of allAudioFiles) { if (audioFile.enabled !== false) { // Only include enabled files // Debug path corruption issue console.log(`Processing audio file:`, audioFile); console.log(`Path before adding:`, audioFile.path); // Fix path corruption - decode any URL encoding and normalize path let cleanPath = audioFile.path; if (cleanPath.includes('%')) { try { cleanPath = decodeURIComponent(cleanPath); console.log(`Decoded path: ${cleanPath}`); } catch (e) { console.warn(`Failed to decode path: ${cleanPath}`, e); } } // For Electron with absolute Windows paths, keep them as-is // Don't convert backslashes to forward slashes for Windows absolute paths if (window.electronAPI && cleanPath.match(/^[A-Za-z]:\\/)) { // Windows absolute path - convert backslashes to forward slashes for web compatibility cleanPath = cleanPath.replace(/\\/g, '/'); console.log(`Normalized Windows path: ${cleanPath}`); } // For Electron, validate file exists before adding if (window.electronAPI && window.electronAPI.fs) { try { const exists = await window.electronAPI.fs.existsSync(cleanPath.replace(/\//g, '\\')); if (!exists) { console.warn(`โš ๏ธ File does not exist, skipping: ${cleanPath}`); continue; } } catch (error) { console.warn(`โš ๏ธ Could not verify file existence: ${cleanPath}`, error); continue; } } const audioFileObj = { name: audioFile.name || 'Unnamed Audio', path: cleanPath, element: null }; this.audioLibrary.background.general.push(audioFileObj); validFiles.push(audioFileObj); console.log(`Added user audio file: ${cleanPath}`); } } // Update storage to remove non-existent files if (validFiles.length !== allAudioFiles.length && window.game && window.game.dataManager) { console.log(`๐Ÿงน Removing ${allAudioFiles.length - validFiles.length} non-existent files from storage`); const updatedCustomAudio = { background: validFiles.filter(f => f.path.includes('/background/')), ambient: validFiles.filter(f => f.path.includes('/ambient/')) }; window.game.dataManager.set('customAudio', updatedCustomAudio); } console.log(`๐ŸŽต Loaded ${validFiles.length} user audio files`); } // If no user audio is available, create empty structure if (this.audioLibrary.background.general.length === 0) { console.log('๐ŸŽต No user audio files found.'); console.log('๐ŸŽต To add background audio:'); console.log(' 1. Go to the main menu'); console.log(' 2. Click "Manage Audio"'); console.log(' 3. Import audio files using the "Import" buttons'); console.log(' 4. Audio will automatically play during tasks'); } console.log('๐ŸŽต Audio library discovery completed:', this.audioLibrary); } loadSettings() { const savedSettings = this.dataManager.get('audioSettings') || {}; // Load master volume this.masterVolume = savedSettings.masterVolume || 0.7; // Load category settings for (const category in this.categories) { if (savedSettings.categories && savedSettings.categories[category]) { this.categories[category] = { ...this.categories[category], ...savedSettings.categories[category] }; } } } saveSettings() { const settings = { masterVolume: this.masterVolume, categories: this.categories }; this.dataManager.set('audioSettings', settings); } // Master volume control setMasterVolume(volume) { this.masterVolume = Math.max(0, Math.min(1, volume)); if (this.masterGain) { this.masterGain.gain.value = this.masterVolume; } this.saveSettings(); } getMasterVolume() { return this.masterVolume; } // Category volume control setCategoryVolume(category, volume) { if (this.categories[category]) { this.categories[category].volume = Math.max(0, Math.min(1, volume)); // Update currently playing audio if any if (this.categories[category].currentAudio) { this.categories[category].currentAudio.volume = this.categories[category].volume * this.masterVolume; } this.saveSettings(); } } getCategoryVolume(category) { return this.categories[category]?.volume || 0; } // Enable/disable categories setCategoryEnabled(category, enabled) { if (this.categories[category]) { this.categories[category].enabled = enabled; // Stop currently playing audio if disabled if (!enabled && this.categories[category].currentAudio) { this.stopCategory(category); } this.saveSettings(); } } isCategoryEnabled(category) { return this.categories[category]?.enabled || false; } // Play audio from library async playAudio(category, subcategory, fileName = null, options = {}) { console.log(`PlayAudio called: ${category}/${subcategory}/${fileName || 'random'}`); // Check if audio is globally disabled if (!this.audioEnabled) { console.log('Audio globally disabled - blocking playback'); return null; } // Check if we're in a skip audio state (for consequences) if (this.game && this.game.gameState && this.game.gameState.skipAudioPlayed) { console.log('Skip audio played flag set - blocking background audio during consequence transition'); return null; } if (!this.isInitialized) { console.warn('AudioManager not initialized yet'); return null; } // Check if we're in browser mode and warn about limitations const isElectron = window.electronAPI !== undefined; if (!isElectron) { console.warn('Running in browser mode - audio may have limitations due to file:// restrictions'); } // Check if the game is still running - prevent audio after game ends if (this.game && this.game.gameState && !this.game.gameState.isRunning) { console.log(`Game is not running - blocking audio playback for ${category}/${subcategory}`); return null; } if (!this.isCategoryEnabled(category)) { console.log(`Audio category ${category} is disabled`); return null; } const categoryLib = this.audioLibrary[category]; if (!categoryLib || !categoryLib[subcategory]) { console.warn(`Audio not found: ${category}/${subcategory}. Available:`, Object.keys(this.audioLibrary)); console.warn(`Category structure:`, categoryLib); return null; } const audioFiles = categoryLib[subcategory]; if (audioFiles.length === 0) { console.warn(`No audio files available in ${category}/${subcategory}`); return null; } // Select audio file let audioFile; if (fileName) { audioFile = audioFiles.find(f => f.name === fileName); if (!audioFile) { console.warn(`Specific audio file not found: ${fileName}. Available:`, audioFiles.map(f => f.name)); return null; } } else { // Random selection audioFile = audioFiles[Math.floor(Math.random() * audioFiles.length)]; } console.log(`Selected audio file: ${audioFile.path}`); // Validate the audio file path if (!audioFile.path || audioFile.path.trim() === '') { console.error(`Invalid audio file path: ${audioFile.path}`); return null; } // Stop any currently playing audio in this category immediately and synchronously if (this.categories[category].currentAudio) { const existingAudio = this.categories[category].currentAudio; existingAudio.pause(); existingAudio.currentTime = 0; existingAudio._stopped = true; // Mark as stopped to prevent further playback console.log(`[src debug] Stopping previous audio:`, existingAudio.src); // Don't clear src as it causes corruption - just pause and reset time // Also remove from DOM if it exists if (existingAudio.parentNode) { existingAudio.parentNode.removeChild(existingAudio); } this.categories[category].currentAudio = null; console.log(`Forcibly stopped existing audio in category: ${category}`); } // Clear any pending fade timeout for this category if (this.categories[category].fadeTimeout) { clearTimeout(this.categories[category].fadeTimeout); this.categories[category].fadeTimeout = null; } // Create and configure audio element const audio = new Audio(); audio._stopped = false; // Flag to prevent orphaned playback // Set the source with proper error handling try { console.log(`Setting audio source to: ${audioFile.path}`); console.log(`Audio file object:`, audioFile); // Debug: Check if path is already corrupted if (audioFile.path.includes('%')) { console.error(`Path already contains URL encoding: ${audioFile.path}`); } // For Electron, construct proper file:// URL for absolute paths let audioSrc = audioFile.path; if (window.electronAPI && audioFile.path.match(/^[A-Za-z]:\//)) { // Absolute Windows path - convert to file:// URL audioSrc = `file:///${audioFile.path}`; console.log(`Converted to file URL: ${audioSrc}`); } else if (window.electronAPI && !audioFile.path.startsWith('file://')) { // Relative path in Electron - make it relative to app root audioSrc = audioFile.path; console.log(`Using relative path: ${audioSrc}`); } console.log(`[src debug] Setting audio.src to:`, audioSrc); audio.src = audioSrc; console.log(`[src debug] audio.src after assignment:`, audio.src); } catch (error) { console.error(`Error setting audio source: ${audioFile.path}`, error); return null; } audio.volume = this.categories[category].volume * this.masterVolume; audio.loop = options.loop || false; console.log(`Audio volume set to: ${audio.volume} (category: ${this.categories[category].volume}, master: ${this.masterVolume})`); // Add comprehensive error handling with retry logic audio.addEventListener('error', (e) => { console.error(`Audio file failed to load: ${audioFile.path}`, e); console.error('Error details:', { error: e.target.error, networkState: e.target.networkState, readyState: e.target.readyState, src: e.target.src, srcAttribute: audio.getAttribute('src'), srcProperty: audio.src, errorCode: e.target.error?.code, errorMessage: e.target.error?.message }); // Clean up failed audio element if (this.categories[category].currentAudio === audio) { this.categories[category].currentAudio = null; } // If this was a specific file request and it failed, don't retry if (fileName) { console.log(`Specific audio file ${fileName} failed, not retrying`); return; } // For browser mode with file:// restrictions, disable retries to prevent spam const isElectron = window.electronAPI !== undefined; if (!isElectron && window.location.protocol === 'file:') { console.warn('Browser file:// mode detected - disabling audio retries due to CORS restrictions'); return; } // For random selection, try a different file if available const remainingFiles = audioFiles.filter(f => f !== audioFile); if (remainingFiles.length > 0) { console.log(`Retrying with different audio file from ${category}/${subcategory}`); // Try again with a different random file after a short delay setTimeout(() => { // Check if game is still running before retrying if (!window.game || !window.game.running) { console.log('Game not running - canceling audio retry'); return; } this.playAudio(category, subcategory, null, options); }, 500); } }); audio.addEventListener('loadstart', () => { console.log(`Started loading: ${audioFile.path}`); }); audio.addEventListener('canplay', () => { if (audio._stopped) { console.log(`Audio ready but stopped - not playing: ${audioFile.path}`); return; } console.log(`Audio ready to play: ${audioFile.path}`); }); audio.addEventListener('loadeddata', () => { console.log(`Audio data loaded: ${audioFile.path}`); }); // Set up event handlers audio.addEventListener('ended', () => { if (this.categories[category].currentAudio === audio) { this.categories[category].currentAudio = null; } }); // Store reference this.categories[category].currentAudio = audio; // Use a small delay before attempting to play to ensure audio element is ready try { // First, load the audio audio.load(); // Small delay to ensure the audio element is properly initialized await new Promise(resolve => setTimeout(resolve, 50)); // Check if audio was stopped while loading if (audio._stopped) { console.log(`Audio was stopped while loading - not playing: ${audioFile.path}`); return null; } await audio.play(); console.log(`Playing audio: ${category}/${subcategory}/${audioFile.name}`); // Handle fade in if (options.fadeIn) { this.fadeIn(audio, options.fadeIn); } return audio; } catch (error) { console.error(`Failed to play audio: ${audioFile.path}`, error); console.error(`Play error details:`, { name: error.name, message: error.message, audioSrc: audio.src, audioReadyState: audio.readyState, audioNetworkState: audio.networkState }); this.categories[category].currentAudio = null; return null; } } // Stop audio in specific category stopCategory(category, fadeOut = 0) { if (!this.categories[category] || !this.categories[category].currentAudio) { return; } const audio = this.categories[category].currentAudio; console.log(`Stopping audio in category: ${category} with fadeOut: ${fadeOut}ms`); if (fadeOut > 0) { this.fadeOut(audio, fadeOut, () => { audio.pause(); audio.currentTime = 0; // Reset to beginning console.log(`[src debug] Clearing src of stopped audio:`, audio.src); audio.src = ''; console.log(`[src debug] src after clearing:`, audio.src); // Also remove from DOM if it exists if (audio.parentNode) { audio.parentNode.removeChild(audio); } this.categories[category].currentAudio = null; console.log(`Audio stopped in category: ${category}`); }); } else { audio.pause(); audio.currentTime = 0; // Reset to beginning audio._stopped = true; // Mark as stopped to prevent further playback console.log(`[src debug] Stopping audio:`, audio.src); // Don't clear src as it causes corruption - just pause and reset time // Also remove from DOM if it exists if (audio.parentNode) { audio.parentNode.removeChild(audio); } this.categories[category].currentAudio = null; console.log(`Audio immediately stopped in category: ${category}`); } // Clear any pending fade timeout if (this.categories[category].fadeTimeout) { clearTimeout(this.categories[category].fadeTimeout); this.categories[category].fadeTimeout = null; } } // Immediately stop all audio without fade stopAllImmediate() { console.log('Stopping all audio immediately...'); // Stop playlist first this.stopPlaylist(); // Globally disable audio to prevent any new playback this.audioEnabled = false; for (const category in this.categories) { if (this.categories[category].currentAudio) { const audio = this.categories[category].currentAudio; audio.pause(); audio.currentTime = 0; audio._stopped = true; // Mark as stopped to prevent further playback // Don't clear src as it causes corruption - just pause and reset // Also remove from DOM if it exists if (audio.parentNode) { audio.parentNode.removeChild(audio); } this.categories[category].currentAudio = null; console.log(`Forcibly stopped audio in category: ${category}`); } // Clear any pending fade timeouts if (this.categories[category].fadeTimeout) { clearTimeout(this.categories[category].fadeTimeout); this.categories[category].fadeTimeout = null; } } } // Re-enable audio (call when game starts) enableAudio() { console.log('Audio re-enabled'); this.audioEnabled = true; // Clear any stopped audio elements to prevent orphaned events for (const category in this.categories) { if (this.categories[category].currentAudio && this.categories[category].currentAudio._stopped) { // Remove stopped audio elements completely const stoppedAudio = this.categories[category].currentAudio; if (stoppedAudio.parentNode) { stoppedAudio.parentNode.removeChild(stoppedAudio); } this.categories[category].currentAudio = null; console.log(`Cleared stopped audio element in category: ${category}`); } } } // Start continuous background audio playlist startBackgroundPlaylist() { if (!this.audioEnabled) { console.log('Audio disabled - not starting playlist'); return; } // Get background audio files const backgroundFiles = this.audioLibrary.background?.general || []; if (backgroundFiles.length === 0) { console.log('No background audio files available for playlist'); return; } // Shuffle the playlist this.playlistQueue = [...backgroundFiles].sort(() => Math.random() - 0.5); this.currentPlaylistIndex = 0; this.isPlaylistMode = true; console.log(`๐ŸŽต Starting background audio playlist with ${this.playlistQueue.length} tracks`); this.playNextInPlaylist(); } // Play next track in playlist playNextInPlaylist() { if (!this.isPlaylistMode || !this.audioEnabled) { return; } if (this.playlistQueue.length === 0) { console.log('Playlist is empty'); return; } // Stop current playlist audio if playing if (this.currentPlaylistAudio) { this.currentPlaylistAudio.pause(); this.currentPlaylistAudio.remove(); } // Get next track (loop back to start if at end) if (this.currentPlaylistIndex >= this.playlistQueue.length) { this.currentPlaylistIndex = 0; // Re-shuffle for variety this.playlistQueue = [...this.playlistQueue].sort(() => Math.random() - 0.5); console.log('๐Ÿ”„ Playlist completed, reshuffling and restarting'); } const audioFile = this.playlistQueue[this.currentPlaylistIndex]; console.log(`๐ŸŽต Playing playlist track ${this.currentPlaylistIndex + 1}/${this.playlistQueue.length}: ${audioFile.name}`); // Create audio element this.currentPlaylistAudio = new Audio(); const audio = this.currentPlaylistAudio; // Set up event handlers audio.addEventListener('ended', () => { console.log(`๐ŸŽต Track ended: ${audioFile.name}`); this.currentPlaylistIndex++; // Small delay before next track setTimeout(() => this.playNextInPlaylist(), 500); }); audio.addEventListener('error', (e) => { console.error(`๐ŸŽต Playlist track failed: ${audioFile.name}`, e); this.currentPlaylistIndex++; // Try next track after error setTimeout(() => this.playNextInPlaylist(), 1000); }); // Set source and play try { // Use the same file URL conversion logic as the main playAudio method let audioSrc = audioFile.path; if (window.electronAPI && audioFile.path.match(/^[A-Za-z]:\//)) { // Absolute Windows path - convert to file:// URL audioSrc = `file:///${audioFile.path}`; console.log(`๐ŸŽต Converted to file URL: ${audioSrc}`); } else if (window.electronAPI && !audioFile.path.startsWith('file://')) { // Relative path in Electron - make it relative to app root audioSrc = audioFile.path; console.log(`๐ŸŽต Using relative path: ${audioSrc}`); } audio.src = audioSrc; audio.volume = this.getMasterVolume() * (this.getCategoryVolume('background') || 0.7); audio.play().then(() => { console.log(`๐ŸŽต Now playing: ${audioFile.name}`); }).catch(error => { console.error(`๐ŸŽต Failed to play playlist track: ${audioFile.name}`, error); this.currentPlaylistIndex++; setTimeout(() => this.playNextInPlaylist(), 1000); }); } catch (error) { console.error(`๐ŸŽต Error setting up playlist track: ${audioFile.name}`, error); this.currentPlaylistIndex++; setTimeout(() => this.playNextInPlaylist(), 1000); } } // Pause playlist pausePlaylist() { if (this.currentPlaylistAudio && !this.currentPlaylistAudio.paused) { this.currentPlaylistAudio.pause(); console.log('๐ŸŽต Playlist paused'); } } // Resume playlist resumePlaylist() { if (this.currentPlaylistAudio && this.currentPlaylistAudio.paused) { this.currentPlaylistAudio.play().then(() => { console.log('๐ŸŽต Playlist resumed'); }).catch(error => { console.error('๐ŸŽต Failed to resume playlist', error); // Try next track if current one fails this.currentPlaylistIndex++; setTimeout(() => this.playNextInPlaylist(), 500); }); } } // Stop playlist completely stopPlaylist() { this.isPlaylistMode = false; if (this.currentPlaylistAudio) { this.currentPlaylistAudio.pause(); this.currentPlaylistAudio.src = ''; this.currentPlaylistAudio.remove(); this.currentPlaylistAudio = null; } this.playlistQueue = []; this.currentPlaylistIndex = 0; console.log('๐ŸŽต Playlist stopped'); } // Stop all audio stopAll(fadeOut = 0) { for (const category in this.categories) { this.stopCategory(category, fadeOut); } } // Fade effects fadeIn(audio, duration) { const targetVolume = audio.volume; audio.volume = 0; const steps = 20; const stepSize = targetVolume / steps; const stepDelay = duration / steps; let currentStep = 0; const fadeInterval = setInterval(() => { currentStep++; audio.volume = Math.min(targetVolume, stepSize * currentStep); if (currentStep >= steps) { clearInterval(fadeInterval); audio.volume = targetVolume; } }, stepDelay); } fadeOut(audio, duration, callback = null) { const startVolume = audio.volume; const steps = 20; const stepSize = startVolume / steps; const stepDelay = duration / steps; let currentStep = 0; const fadeInterval = setInterval(() => { currentStep++; audio.volume = Math.max(0, startVolume - (stepSize * currentStep)); if (currentStep >= steps) { clearInterval(fadeInterval); audio.volume = 0; if (callback) callback(); } }, stepDelay); } // Get current playing status isPlaying(category) { return this.categories[category]?.currentAudio && !this.categories[category].currentAudio.paused; } getCurrentlyPlaying() { const playing = {}; for (const [category, config] of Object.entries(this.categories)) { if (this.isPlaying(category)) { playing[category] = { src: config.currentAudio.src, currentTime: config.currentAudio.currentTime, duration: config.currentAudio.duration }; } } return playing; } // Audio library info getAvailableAudio() { const available = {}; for (const [category, subcategories] of Object.entries(this.audioLibrary)) { available[category] = {}; for (const [subcategory, files] of Object.entries(subcategories)) { available[category][subcategory] = files.map(f => f.name); } } return available; } // Preview audio (for settings menu) async previewAudio(category, subcategory, fileName) { // Stop any currently playing preview this.stopCategory('preview'); // Play audio with temporary category const audio = await this.playAudio(category, subcategory, fileName); if (audio) { // Auto-stop after 3 seconds for preview setTimeout(() => { if (audio && !audio.paused) { audio.pause(); } }, 3000); } return audio; } // Test audio playback - useful for debugging async testAudio() { console.log('Testing audio system...'); console.log('Audio library:', this.audioLibrary); // Test a teasing audio file if (this.audioLibrary.tasks && this.audioLibrary.tasks.teasing && this.audioLibrary.tasks.teasing.length > 0) { console.log('Testing teasing audio...'); await this.playAudio('tasks', 'teasing'); } else { console.log('No teasing audio files found'); } } // Convenient methods for specific audio types - now all use background audio playTaskAudio(intensity = 'general', options = {}) { return this.playBackgroundAudio(options); } playPunishmentAudio(type = 'general', options = {}) { return this.playBackgroundAudio(options); } playRewardAudio(options = {}) { return this.playBackgroundAudio(options); } playInstructionAudio(instruction, options = {}) { return this.playBackgroundAudio(options); } // Main background audio playing method playBackgroundAudio(options = {}) { return this.playAudio('background', 'general', null, options); } // Enhanced playAudio with fallback support - simplified since we only have one category async playAudioWithFallback(category, primarySubcategory, fallbackSubcategory, fileName = null, options = {}) { try { return await this.playAudio(category, primarySubcategory, fileName, options); } catch (error) { console.log(`Audio playback failed for ${category}/${primarySubcategory}`); return null; } } // Scan audio directories for files async scanAudioDirectories() { console.log('๐Ÿ” Scanning audio directories...'); const scannedFiles = []; // Simplified directory structure - only background and ambient const audioDirectories = [ 'audio/background', 'audio/ambient' ]; // For Electron environment, we can use the existing readAudioDirectory API if (window.electronAPI && window.electronAPI.readAudioDirectory) { try { for (const directory of audioDirectories) { console.log(`Scanning directory: ${directory}`); // Get the full path to the directory const appPath = await window.electronAPI.getAppPath(); const fullPath = await window.electronAPI.pathJoin(appPath, directory); // Check if directory exists before trying to read it const dirExists = await window.electronAPI.fileExists(fullPath); if (!dirExists) { console.log(`Directory does not exist: ${fullPath}`); continue; } const files = await window.electronAPI.readAudioDirectory(fullPath); for (const file of files) { scannedFiles.push({ name: file.name, path: file.path, title: file.title || file.name, directory: directory, enabled: true }); } } console.log(`๐Ÿ” Found ${scannedFiles.length} audio files in directories`); // Add scanned files to the audio library if they're not already in customAudio if (scannedFiles.length > 0) { await this.addScannedFilesToLibrary(scannedFiles); } return scannedFiles; } catch (error) { console.error('Error scanning audio directories:', error); return []; } } else { console.warn('Directory scanning not available - requires Electron environment'); console.log('๐Ÿ’ก To use directory scanning:'); console.log(' 1. Run the app with: npm start'); console.log(' 2. Or manually add audio files via "Manage Audio" menu'); return []; } } // Add scanned files to the audio library async addScannedFilesToLibrary(scannedFiles) { if (!window.game || !window.game.dataManager) { console.warn('DataManager not available for adding scanned files'); return; } const customAudio = window.game.dataManager.get('customAudio') || { background: [], ambient: [] }; let addedCount = 0; for (const file of scannedFiles) { // Check if file is already in the library const existsInBackground = customAudio.background.some(f => f.path === file.path); const existsInAmbient = customAudio.ambient.some(f => f.path === file.path); if (!existsInBackground && !existsInAmbient) { // Determine which category to add to based on directory let category = 'background'; // Default category if (file.directory.includes('ambient')) { category = 'ambient'; } // Note: Since we removed effects, tasks, etc., everything else goes to background customAudio[category].push({ name: file.name, path: file.path, enabled: true, source: 'directory_scan' }); addedCount++; console.log(`Added ${file.name} to ${category} category`); } } if (addedCount > 0) { // Save updated audio library window.game.dataManager.set('customAudio', customAudio); // Refresh the audio library to include new files await this.discoverAudioLibrary(); console.log(`โœ… Added ${addedCount} new audio files to library`); // Show success message to user if (window.game && window.game.flashMessageManager) { window.game.flashMessageManager.show( `Audio Scan Complete: Added ${addedCount} audio files to library`, 'success' ); } } else { console.log('โ„น๏ธ No new audio files found during scan'); if (window.game && window.game.flashMessageManager) { window.game.flashMessageManager.show( 'Audio scan complete - no new files found', 'info' ); } } } // Refresh audio library - useful when user adds new audio files async refreshAudioLibrary() { console.log('๐ŸŽต Refreshing audio library...'); await this.discoverAudioLibrary(); console.log('๐ŸŽต Audio library refreshed'); } // Get settings for UI getSettings() { return { masterVolume: this.masterVolume, categories: { ...this.categories }, available: this.getAvailableAudio(), isInitialized: this.isInitialized }; } // Public method to trigger directory scan (for UI buttons) async triggerDirectoryScan() { console.log('๐Ÿ” Triggering audio directory scan from UI...'); return await this.scanAudioDirectories(); } }