diff --git a/assets/mini-games/audio.png b/assets/mini-games/audio.png new file mode 100644 index 0000000..0f81310 Binary files /dev/null and b/assets/mini-games/audio.png differ diff --git a/css/minigames.css b/css/minigames.css index ceae475..ed6ba0f 100644 --- a/css/minigames.css +++ b/css/minigames.css @@ -189,4 +189,4 @@ background: linear-gradient(90deg, #2ecc71, #27ae60); transition: width 0.3s ease; border-radius: 5px; -} \ No newline at end of file +} diff --git a/css/phone.css b/css/phone.css new file mode 100644 index 0000000..06381dd --- /dev/null +++ b/css/phone.css @@ -0,0 +1,447 @@ +/* Phone Messages Minigame Styles */ +.phone-messages-container { + display: flex; + flex-direction: column; + height: 500px; + width: 100%; + max-width: 400px; + margin: 0 auto; + background: #a0a0ad; + border-radius: 20px; + border: 3px solid #333; + box-shadow: 0 0 20px rgba(0, 255, 0, 0.3); + overflow: hidden; + font-family: 'Courier New', monospace; +} + +.phone-screen { + flex: 1; + background: #5fcf69; + display: flex; + flex-direction: column; + position: relative; + color: #000; + margin: 10px; + border-radius: 15px; + border: 2px solid #333; +} + +.phone-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 15px; + background: rgba(0, 0, 0, 0.1); + border-bottom: 1px solid #333; + color: #000; +} + +.signal-bars { + display: flex; + gap: 2px; + align-items: end; +} + +.signal-bars .bar { + width: 3px; + background: #000; + border-radius: 1px; +} + +.signal-bars .bar:nth-child(1) { height: 4px; } +.signal-bars .bar:nth-child(2) { height: 6px; } +.signal-bars .bar:nth-child(3) { height: 8px; } +.signal-bars .bar:nth-child(4) { height: 10px; } + +.battery { + color: #000; + font-size: 10px; + font-family: 'Courier New', monospace; + font-weight: bold; +} + +.messages-list { + flex: 1; + overflow-y: auto; + padding: 10px; + color: #000; +} + +.message-item { + display: flex; + align-items: center; + padding: 12px; + margin-bottom: 8px; + background: rgba(0, 0, 0, 0.1); + border: 1px solid rgba(0, 0, 0, 0.3); + border-radius: 8px; + cursor: pointer; + transition: all 0.3s ease; + position: relative; + color: #000; +} + +.message-item:hover { + background: rgba(0, 0, 0, 0.2); + border-color: rgba(0, 0, 0, 0.5); + transform: translateX(5px); +} + +.message-item.voice { + border-left: 4px solid #ff6b35; +} + +.message-item.text { + border-left: 4px solid #000; +} + +.message-preview { + flex: 1; + min-width: 0; +} + +.message-sender { + font-weight: bold; + color: #000; + font-size: 12px; + margin-bottom: 4px; + font-family: 'Courier New', monospace; +} + +.message-text { + color: #333; + font-size: 11px; + line-height: 1.3; + margin-bottom: 4px; + font-family: 'Courier New', monospace; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.message-time { + color: #666; + font-size: 10px; + font-family: 'Courier New', monospace; +} + +.message-status { + width: 8px; + height: 8px; + border-radius: 50%; + margin-left: 8px; +} + +.message-status.unread { + background: #000; + box-shadow: 0 0 6px rgba(0, 0, 0, 0.6); +} + +.message-status.read { + background: #666; +} + +.no-messages { + display: flex; + align-items: center; + justify-content: center; + height: 200px; + color: #666; + font-size: 12px; + font-family: 'Courier New', monospace; +} + +.message-detail { + flex: 1; + display: flex; + flex-direction: column; + padding: 15px; + color: #000; +} + +.message-header { + display: flex; + align-items: center; + margin-bottom: 15px; + padding-bottom: 10px; + border-bottom: 1px solid #333; +} + +.back-btn { + background: #333; + color: #5fcf69; + border: 1px solid #555; + padding: 8px 12px; + border-radius: 5px; + cursor: pointer; + font-family: 'Courier New', monospace; + font-size: 11px; + margin-right: 15px; + transition: all 0.3s ease; + font-weight: bold; +} + +.back-btn:hover { + background: #555; + border-color: #5fcf69; + box-shadow: 0 0 5px rgba(95, 207, 105, 0.5); +} + +.message-info { + flex: 1; +} + +.sender { + display: block; + color: #000; + font-weight: bold; + font-size: 14px; + margin-bottom: 4px; + font-family: 'Courier New', monospace; +} + +.timestamp { + color: #666; + font-size: 11px; + font-family: 'Courier New', monospace; +} + +.message-content { + flex: 1; + background: rgba(0, 0, 0, 0.1); + padding: 15px; + border-radius: 8px; + border: 1px solid #333; + color: #000; + font-size: 12px; + line-height: 1.5; + font-family: 'Courier New', monospace; + white-space: pre-wrap; + overflow-y: auto; + margin-bottom: 15px; +} + +/* Voice message display styling */ +.voice-message-display { + display: flex; + flex-direction: column; + align-items: center; + gap: 15px; +} + +.audio-controls { + display: flex; + align-items: center; + gap: 10px; + cursor: pointer; + transition: transform 0.2s ease; + padding: 5px; + border-radius: 8px; +} + +.audio-controls:hover { + transform: scale(1.25); + background: rgba(0, 0, 0, 0.1); +} + +.audio-sprite { + height: 32px; + width: auto; + image-rendering: pixelated; + image-rendering: -moz-crisp-edges; + image-rendering: crisp-edges; + image-rendering: -webkit-optimize-contrast; + filter: contrast(1.2) saturate(1.1); +} + +.play-button { + background: #5fcf69; + color: #000; + border-radius: 50%; + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + font-size: 14px; + font-weight: bold; + font-family: 'Courier New', monospace; + border: 2px solid #333; +} + +.transcript { + text-align: center; + background: rgba(0, 0, 0, 0.1); + padding: 10px; + border-radius: 5px; + border: 1px solid #333; + width: 100%; + font-family: 'Courier New', monospace; + font-size: 11px; + line-height: 1.4; +} + +.transcript strong { + color: #000; + font-weight: bold; +} + +/* Phone observations styling */ +.phone-observations { + margin-top: 20px; + padding: 15px; + background: #f0f0f0; + border-radius: 8px; + border: 2px solid #333; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.observations-content h4 { + margin: 0 0 8px 0; + color: #000; + font-size: 12px; + font-weight: bold; + font-family: 'Courier New', monospace; +} + +.observations-content p { + margin: 0; + color: #000; + font-size: 11px; + line-height: 1.4; + font-family: 'Courier New', monospace; +} + +.message-actions { + display: flex; + gap: 10px; + justify-content: center; +} + +.phone-controls { + display: flex; + justify-content: center; + gap: 15px; + padding: 15px; + background: rgba(0, 0, 0, 0.1); + border-top: 1px solid #333; +} + +.control-btn { + background: #333; + color: #5fcf69; + border: 1px solid #555; + padding: 10px 15px; + border-radius: 8px; + cursor: pointer; + font-family: 'Courier New', monospace; + font-size: 11px; + transition: all 0.3s ease; + min-width: 80px; + font-weight: bold; +} + +.control-btn:hover { + background: #555; + border-color: #5fcf69; + box-shadow: 0 0 10px rgba(95, 207, 105, 0.5); + transform: translateY(-1px); +} + +.control-btn:active { + background: #666; + transform: translateY(0px); +} + +.control-btn:disabled { + background: #222; + color: #666; + border-color: #444; + cursor: not-allowed; +} + +/* Voice playback note styling */ +.voice-note { + color: #666 !important; + font-size: 10px !important; + text-align: center !important; + margin-top: 10px !important; + font-family: 'Courier New', monospace !important; + background: rgba(0, 0, 0, 0.1); + padding: 5px; + border-radius: 3px; + border: 1px solid #333; +} + +/* Voice controls styling */ +.voice-controls { + display: flex; + align-items: center; + justify-content: center; + gap: 10px; + padding: 10px 15px; + background: rgba(0, 0, 0, 0.1); + border-top: 1px solid #333; + font-family: 'Courier New', monospace; + font-size: 11px; +} + +.voice-controls label { + color: #000; + font-weight: bold; +} + +.voice-select { + background: #333; + color: #5fcf69; + border: 1px solid #555; + padding: 5px 8px; + border-radius: 4px; + font-family: 'Courier New', monospace; + font-size: 10px; + min-width: 200px; + cursor: pointer; + font-weight: bold; +} + +.voice-select:hover { + border-color: #5fcf69; + box-shadow: 0 0 5px rgba(95, 207, 105, 0.3); +} + +.voice-select:focus { + outline: none; + border-color: #5fcf69; + box-shadow: 0 0 5px rgba(95, 207, 105, 0.5); +} + +.voice-select option { + background: #333; + color: #5fcf69; + padding: 5px; + font-weight: bold; +} + + +/* Scrollbar styling for phone interface */ +.messages-list::-webkit-scrollbar, +.message-content::-webkit-scrollbar { + width: 6px; +} + +.messages-list::-webkit-scrollbar-track, +.message-content::-webkit-scrollbar-track { + background: rgba(0, 0, 0, 0.1); + border-radius: 3px; +} + +.messages-list::-webkit-scrollbar-thumb, +.message-content::-webkit-scrollbar-thumb { + background: #333; + border-radius: 3px; +} + +.messages-list::-webkit-scrollbar-thumb:hover, +.message-content::-webkit-scrollbar-thumb:hover { + background: #555; +} diff --git a/index.html b/index.html index 41da8fb..f1f25eb 100644 --- a/index.html +++ b/index.html @@ -39,6 +39,7 @@ + diff --git a/js/core/game.js b/js/core/game.js index 388117e..25f7c23 100644 --- a/js/core/game.js +++ b/js/core/game.js @@ -516,6 +516,15 @@ export function create() { // Set up input handling this.input.on('pointerdown', (pointer) => { + // Check if a minigame is currently running - if so, don't process main game clicks + if (window.MinigameFramework && window.MinigameFramework.currentMinigame) { + console.log('Minigame is running, ignoring main game click', { + currentMinigame: window.MinigameFramework.currentMinigame, + minigameType: window.MinigameFramework.currentMinigame.constructor.name + }); + return; + } + // Convert screen coordinates to world coordinates const worldX = this.cameras.main.scrollX + pointer.x; const worldY = this.cameras.main.scrollY + pointer.y; diff --git a/js/minigames/framework/minigame-manager.js b/js/minigames/framework/minigame-manager.js index c8a9450..b4c12bf 100644 --- a/js/minigames/framework/minigame-manager.js +++ b/js/minigames/framework/minigame-manager.js @@ -9,7 +9,7 @@ export const MinigameFramework = { init(gameScene) { this.mainGameScene = gameScene; - console.log("MinigameFramework initialized"); + console.log("MinigameFramework initialized with main game scene:", gameScene); }, startMinigame(sceneType, container, params) { @@ -32,11 +32,25 @@ export const MinigameFramework = { this.mainGameScene.input.mouse.enabled = false; this.mainGameScene.input.keyboard.enabled = false; this.gameInputDisabled = true; - console.log('Disabled main game input for minigame'); + console.log('Disabled main game input for minigame', { + sceneType: sceneType, + mainGameScene: this.mainGameScene, + inputDisabled: true + }); } else { this.gameInputDisabled = false; - console.log('Keeping main game input enabled for minigame'); + console.log('Keeping main game input enabled for minigame', { + sceneType: sceneType, + mainGameScene: this.mainGameScene, + inputDisabled: false + }); } + } else { + console.warn('Cannot disable main game input - no main game scene or input available', { + sceneType: sceneType, + mainGameScene: this.mainGameScene, + hasInput: this.mainGameScene ? !!this.mainGameScene.input : false + }); } // Use provided container or create one diff --git a/js/minigames/index.js b/js/minigames/index.js index 9de4bfd..57004da 100644 --- a/js/minigames/index.js +++ b/js/minigames/index.js @@ -10,6 +10,7 @@ export { BluetoothScannerMinigame, startBluetoothScannerMinigame } from './bluet export { BiometricsMinigame, startBiometricsMinigame } from './biometrics/biometrics-minigame.js'; export { LockpickSetMinigame, startLockpickSetMinigame } from './lockpick/lockpick-set-minigame.js'; export { ContainerMinigame, startContainerMinigame, returnToContainerAfterNotes } from './container/container-minigame.js'; +export { PhoneMessagesMinigame, returnToPhoneAfterNotes } from './phone/phone-messages-minigame.js'; // Initialize the global minigame framework for backward compatibility import { MinigameFramework } from './framework/minigame-manager.js'; @@ -18,6 +19,23 @@ import { LockpickingMinigamePhaser } from './lockpicking/lockpicking-game-phaser // Make the framework available globally window.MinigameFramework = MinigameFramework; +// Add global helper functions for debugging +window.restartMinigame = () => { + if (window.MinigameFramework) { + window.MinigameFramework.restartCurrentMinigame(); + } else { + console.log('MinigameFramework not available'); + } +}; + +window.closeMinigame = () => { + if (window.MinigameFramework) { + window.MinigameFramework.forceCloseMinigame(); + } else { + console.log('MinigameFramework not available'); + } +}; + // Import the dusting minigame import { DustingMinigame } from './dusting/dusting-game.js'; @@ -36,6 +54,9 @@ import { LockpickSetMinigame, startLockpickSetMinigame } from './lockpick/lockpi // Import the container minigame import { ContainerMinigame, startContainerMinigame, returnToContainerAfterNotes } from './container/container-minigame.js'; +// Import the phone messages minigame +import { PhoneMessagesMinigame, returnToPhoneAfterNotes } from './phone/phone-messages-minigame.js'; + // Register minigames MinigameFramework.registerScene('lockpicking', LockpickingMinigamePhaser); // Use Phaser version as default MinigameFramework.registerScene('lockpicking-phaser', LockpickingMinigamePhaser); // Keep explicit phaser name @@ -45,6 +66,7 @@ MinigameFramework.registerScene('bluetooth-scanner', BluetoothScannerMinigame); MinigameFramework.registerScene('biometrics', BiometricsMinigame); MinigameFramework.registerScene('lockpick-set', LockpickSetMinigame); MinigameFramework.registerScene('container', ContainerMinigame); +MinigameFramework.registerScene('phone-messages', PhoneMessagesMinigame); // Make minigame functions available globally window.startNotesMinigame = startNotesMinigame; @@ -53,4 +75,5 @@ window.startBluetoothScannerMinigame = startBluetoothScannerMinigame; window.startBiometricsMinigame = startBiometricsMinigame; window.startLockpickSetMinigame = startLockpickSetMinigame; window.startContainerMinigame = startContainerMinigame; -window.returnToContainerAfterNotes = returnToContainerAfterNotes; \ No newline at end of file +window.returnToContainerAfterNotes = returnToContainerAfterNotes; +window.returnToPhoneAfterNotes = returnToPhoneAfterNotes; \ No newline at end of file diff --git a/js/minigames/notes/notes-minigame.js b/js/minigames/notes/notes-minigame.js index 425aaeb..9b084e7 100644 --- a/js/minigames/notes/notes-minigame.js +++ b/js/minigames/notes/notes-minigame.js @@ -739,6 +739,15 @@ export function startNotesMinigame(item, noteContent, observationText, navigateT window.returnToContainerAfterNotes(); }, 100); } + + // Check if we need to return to phone after notes minigame + if (window.pendingPhoneReturn && window.returnToPhoneAfterNotes) { + console.log('Returning to phone after notes minigame'); + // Small delay to ensure notes minigame cleanup completes + setTimeout(() => { + window.returnToPhoneAfterNotes(); + }, 100); + } } }; diff --git a/js/minigames/phone/phone-messages-minigame.js b/js/minigames/phone/phone-messages-minigame.js new file mode 100644 index 0000000..794187c --- /dev/null +++ b/js/minigames/phone/phone-messages-minigame.js @@ -0,0 +1,920 @@ +import { MinigameScene } from '../framework/base-minigame.js'; + +export class PhoneMessagesMinigame extends MinigameScene { + constructor(container, params) { + super(container, params); + + // Ensure params is an object with default values + const safeParams = params || {}; + + // Initialize phone-specific state + this.phoneData = { + messages: safeParams.messages || [], + currentMessageIndex: 0, + isPlaying: false, + speechSynthesis: window.speechSynthesis, + currentUtterance: null + }; + + // Set up speech synthesis + this.setupSpeechSynthesis(); + } + + setupSpeechSynthesis() { + // Check if speech synthesis is available + if (!this.phoneData.speechSynthesis) { + console.warn('Speech synthesis not available'); + this.speechAvailable = false; + return; + } + + // Check if speech synthesis is actually working on this platform + this.speechAvailable = true; + this.voiceSettings = { + rate: 0.9, + pitch: 1.0, + volume: 0.8 + }; + + // Set up voice selection + this.setupVoiceSelection(); + + // Test speech synthesis availability + try { + const testUtterance = new SpeechSynthesisUtterance(''); + testUtterance.volume = 0; + testUtterance.onerror = (event) => { + console.warn('Speech synthesis test failed:', event.error); + this.speechAvailable = false; + }; + this.phoneData.speechSynthesis.speak(testUtterance); + } catch (error) { + console.warn('Speech synthesis not supported:', error); + this.speechAvailable = false; + } + } + + setupVoiceSelection() { + // Wait for voices to load - Chromium often needs this + const voices = this.phoneData.speechSynthesis.getVoices(); + console.log('Initial voices count:', voices.length); + + if (voices.length === 0) { + console.log('No voices loaded yet, waiting for voiceschanged event...'); + this.phoneData.speechSynthesis.addEventListener('voiceschanged', () => { + console.log('Voices changed event fired, voices count:', this.phoneData.speechSynthesis.getVoices().length); + this.selectBestVoice(); + }); + + // Fallback: try again after a delay (Chromium sometimes needs this) + setTimeout(() => { + const delayedVoices = this.phoneData.speechSynthesis.getVoices(); + console.log('Delayed voices count:', delayedVoices.length); + if (delayedVoices.length > 0) { + this.selectBestVoice(); + } + }, 1000); + } else { + this.selectBestVoice(); + } + } + + selectBestVoice() { + const voices = this.phoneData.speechSynthesis.getVoices(); + console.log('Available voices:', voices.map(v => ({ name: v.name, lang: v.lang, default: v.default }))); + + // Prefer modern, natural-sounding voices (updated for your system) + const preferredVoices = [ + // High-quality neural voices (best quality) + 'Microsoft Zira Desktop', + 'Microsoft David Desktop', + 'Microsoft Hazel Desktop', + 'Microsoft Susan Desktop', + 'Microsoft Mark Desktop', + 'Microsoft Catherine Desktop', + 'Microsoft Linda Desktop', + 'Microsoft Richard Desktop', + + // Google Cloud voices (very high quality) + 'Google UK English Female', + 'Google UK English Male', + 'Google US English Female', + 'Google US English Male', + 'Google Australian English Female', + 'Google Australian English Male', + 'Google Canadian English Female', + 'Google Canadian English Male', + + // macOS voices (high quality) + 'Alex', + 'Samantha', + 'Victoria', + 'Daniel', + 'Moira', + 'Tessa', + 'Karen', + 'Lee', + 'Rishi', + 'Veena', + 'Fiona', + 'Susan', + 'Tom', + 'Allison', + 'Ava', + 'Fred', + 'Junior', + 'Kathy', + 'Princess', + 'Ralph', + 'Vicki', + 'Whisper', + 'Zarvox', + + // Amazon Polly voices (if available) + 'Joanna', + 'Matthew', + 'Amy', + 'Brian', + 'Emma', + 'Joey', + 'Justin', + 'Kendra', + 'Kimberly', + 'Salli', + + // IBM Watson voices (if available) + 'en-US_AllisonVoice', + 'en-US_MichaelVoice', + 'en-US_EmilyVoice', + 'en-US_HenryVoice', + 'en-US_KevinVoice', + 'en-US_LisaVoice', + 'en-US_OliviaVoice', + + // Avoid robotic voices - these are typically lower quality + 'Andy', + 'klatt', + 'Robosoft', + 'male1', + 'male2', + 'male3', + 'female1', + 'female2', + 'female3' + ]; + + // Find the best available voice + let selectedVoice = null; + + // Get all English voices + const englishVoices = voices.filter(voice => + voice.lang.startsWith('en') || voice.lang === 'en-US' || voice.lang === 'en-GB' + ); + + console.log('English voices found:', englishVoices.length); + + // First, try to find a preferred high-quality voice + for (const preferredName of preferredVoices) { + selectedVoice = englishVoices.find(voice => voice.name === preferredName); + if (selectedVoice) { + console.log('Found preferred voice:', selectedVoice.name); + break; + } + } + + // If no preferred voice found, look for high-quality indicators in voice names + if (!selectedVoice) { + const qualityIndicators = [ + 'neural', 'cloud', 'desktop', 'premium', 'enhanced', 'natural', + 'Microsoft', 'Google', 'Amazon', 'IBM', 'Watson', 'Polly' + ]; + + // Look for voices with quality indicators + for (const indicator of qualityIndicators) { + selectedVoice = englishVoices.find(voice => + voice.name.toLowerCase().includes(indicator.toLowerCase()) + ); + if (selectedVoice) { + console.log('Found quality voice by indicator:', selectedVoice.name, 'indicator:', indicator); + break; + } + } + } + + // If still no good voice, avoid obviously robotic voices + if (!selectedVoice) { + const avoidPatterns = [ + 'andy', 'klatt', 'robosoft', 'male1', 'male2', 'male3', 'female1', 'female2', 'female3', + 'ricishaymax', 'ricishay', 'max', 'min', 'robot', 'synthetic', 'tts', 'speech', + 'voice', 'synthesizer', 'engine', 'system', 'default', 'basic', 'simple', + 'generic', 'standard', 'built-in', 'builtin', 'internal', 'system' + ]; + + selectedVoice = englishVoices.find(voice => { + const name = voice.name.toLowerCase(); + return !avoidPatterns.some(pattern => name.includes(pattern)); + }); + + if (selectedVoice) { + console.log('Found non-robotic voice:', selectedVoice.name); + } + } + + // Last resort: use default or first available + if (!selectedVoice) { + selectedVoice = englishVoices.find(voice => voice.default) || + englishVoices[0] || + voices.find(voice => voice.default) || + voices[0]; + console.log('Using fallback voice:', selectedVoice?.name); + } + + if (selectedVoice) { + this.selectedVoice = selectedVoice; + console.log('Selected voice:', selectedVoice.name, selectedVoice.lang); + } else { + console.warn('No suitable voice found'); + } + + // Populate voice selector + this.populateVoiceSelector(voices); + } + + populateVoiceSelector(voices) { + if (!this.voiceSelect) return; + + // Clear existing options except the first one + this.voiceSelect.innerHTML = ''; + + // Get English voices and sort them by quality + const englishVoices = voices.filter(voice => + voice.lang.startsWith('en') || voice.lang === 'en-US' || voice.lang === 'en-GB' + ); + + // Sort voices by quality (preferred voices first, then by name) + const sortedVoices = englishVoices.sort((a, b) => { + const aName = a.name.toLowerCase(); + const bName = b.name.toLowerCase(); + + // Quality indicators (higher priority) + const qualityIndicators = ['microsoft', 'google', 'amazon', 'ibm', 'watson', 'polly', 'neural', 'cloud', 'desktop', 'premium', 'enhanced', 'natural']; + const aHasQuality = qualityIndicators.some(indicator => aName.includes(indicator)); + const bHasQuality = qualityIndicators.some(indicator => bName.includes(indicator)); + + if (aHasQuality && !bHasQuality) return -1; + if (!aHasQuality && bHasQuality) return 1; + + // Avoid robotic voices (lower priority) + const roboticPatterns = [ + 'andy', 'klatt', 'robosoft', 'male1', 'male2', 'male3', 'female1', 'female2', 'female3', + 'ricishaymax', 'ricishay', 'max', 'min', 'robot', 'synthetic', 'tts', 'speech', + 'voice', 'synthesizer', 'engine', 'system', 'default', 'basic', 'simple', + 'generic', 'standard', 'built-in', 'builtin', 'internal', 'system' + ]; + const aIsRobotic = roboticPatterns.some(pattern => aName.includes(pattern)); + const bIsRobotic = roboticPatterns.some(pattern => bName.includes(pattern)); + + if (aIsRobotic && !bIsRobotic) return 1; + if (!aIsRobotic && bIsRobotic) return -1; + + // Alphabetical by name + return aName.localeCompare(bName); + }); + + // Add voices to selector (limit to first 20 to avoid overwhelming the dropdown) + const voicesToShow = sortedVoices.slice(0, 20); + + voicesToShow.forEach(voice => { + const option = document.createElement('option'); + option.value = voice.name; + + // Add quality indicator to display name + let displayName = voice.name; + const qualityIndicators = ['microsoft', 'google', 'amazon', 'ibm', 'watson', 'polly', 'neural', 'cloud', 'desktop', 'premium', 'enhanced', 'natural']; + const hasQuality = qualityIndicators.some(indicator => voice.name.toLowerCase().includes(indicator)); + + if (hasQuality) { + displayName = `⭐ ${voice.name}`; + } + + option.textContent = `${displayName} (${voice.lang})`; + if (voice === this.selectedVoice) { + option.selected = true; + } + this.voiceSelect.appendChild(option); + }); + + // Show voice controls if we have voices and speech is available + if (englishVoices.length > 0 && this.speechAvailable) { + this.voiceControls.style.display = 'flex'; + console.log('Voice controls shown with', englishVoices.length, 'English voices'); + } else { + console.log('Voice controls hidden - English voices:', englishVoices.length, 'Speech available:', this.speechAvailable); + } + } + + init() { + // Call parent init to set up basic UI structure + super.init(); + + // Customize the header + this.headerElement.innerHTML = ` +
Review messages and listen to voicemails
+ `; + + // Add notebook button to minigame controls (before cancel button) + if (this.controlsElement) { + const notebookBtn = document.createElement('button'); + notebookBtn.className = 'minigame-button'; + notebookBtn.id = 'minigame-notebook'; + notebookBtn.innerHTML = '📝 Add to Notebook'; + this.controlsElement.appendChild(notebookBtn); + + // Change cancel button text to "Close" + const cancelBtn = document.getElementById('minigame-cancel'); + if (cancelBtn) { + cancelBtn.innerHTML = 'Close'; + } + } + + // Set up the phone interface + this.setupPhoneInterface(); + + // Set up event listeners + this.setupEventListeners(); + } + + setupPhoneInterface() { + // Create the phone interface + this.gameContainer.innerHTML = ` + + +${observations}
+