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 = ` +

${(this.params && this.params.title) || 'Phone Messages'}

+

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 = ` +
+
+
+
+ + + + +
+
85%
+
+ +
+ +
+ + +
+ +
+ + + + +
+ + + +
+ +
+ +
+ `; + + // Get references to important elements + this.messagesList = document.getElementById('messages-list'); + this.messageDetail = document.getElementById('message-detail'); + this.senderName = document.getElementById('sender-name'); + this.messageTime = document.getElementById('message-time'); + this.messageContent = document.getElementById('message-content'); + this.messageActions = document.getElementById('message-actions'); + this.phoneObservations = document.getElementById('phone-observations'); + + // Control buttons + this.prevBtn = document.getElementById('prev-btn'); + this.nextBtn = document.getElementById('next-btn'); + this.playBtn = document.getElementById('play-btn'); + this.stopBtn = document.getElementById('stop-btn'); + this.backBtn = document.getElementById('back-btn'); + + // Voice controls + this.voiceControls = document.getElementById('voice-controls'); + this.voiceSelect = document.getElementById('voice-select'); + this.refreshVoicesBtn = document.getElementById('refresh-voices-btn'); + + // Populate messages + this.populateMessages(); + + // Populate observations + this.populateObservations(); + } + + populateMessages() { + if (!this.phoneData.messages || this.phoneData.messages.length === 0) { + this.messagesList.innerHTML = ` +
+

No messages found

+
+ `; + return; + } + + this.messagesList.innerHTML = ''; + + this.phoneData.messages.forEach((message, index) => { + const messageElement = document.createElement('div'); + messageElement.className = `message-item ${message.type || 'text'}`; + messageElement.dataset.index = index; + + const icon = message.type === 'voice' ? '🎤' : '💬'; + const preview = message.type === 'voice' + ? (message.text ? message.text.substring(0, 50) + '...' : 'Voice message') + : (message.text || 'No text content'); + + messageElement.innerHTML = ` +
${icon}
+
+
${message.sender || 'Unknown'}
+
${preview}
+
${message.timestamp || 'Unknown time'}
+
+
+ `; + + this.messagesList.appendChild(messageElement); + }); + } + + populateObservations() { + // Get observations from the original object data + const observations = this.params?.observations || this.phoneData?.observations; + + if (observations) { + this.phoneObservations.innerHTML = ` +
+

Observations:

+

${observations}

+
+ `; + } else { + this.phoneObservations.innerHTML = ''; + } + } + + setupEventListeners() { + // Message list clicks + this.addEventListener(this.messagesList, 'click', (event) => { + const messageItem = event.target.closest('.message-item'); + if (messageItem) { + const index = parseInt(messageItem.dataset.index); + this.showMessageDetail(index); + } + }); + + // Control buttons + this.addEventListener(this.prevBtn, 'click', () => { + this.previousMessage(); + }); + + this.addEventListener(this.nextBtn, 'click', () => { + this.nextMessage(); + }); + + this.addEventListener(this.playBtn, 'click', () => { + this.playCurrentMessage(); + }); + + this.addEventListener(this.stopBtn, 'click', () => { + this.stopCurrentMessage(); + }); + + this.addEventListener(this.backBtn, 'click', () => { + this.showMessageList(); + }); + + // Voice selector + this.addEventListener(this.voiceSelect, 'change', (event) => { + this.handleVoiceSelection(event.target.value); + }); + + // Refresh voices button + this.addEventListener(this.refreshVoicesBtn, 'click', () => { + this.refreshVoices(); + }); + + // Notebook button (in minigame controls) + const notebookBtn = document.getElementById('minigame-notebook'); + if (notebookBtn) { + this.addEventListener(notebookBtn, 'click', () => { + this.addToNotebook(); + }); + } + + // Keyboard controls + this.addEventListener(document, 'keydown', (event) => { + this.handleKeyPress(event); + }); + } + + handleKeyPress(event) { + if (!this.gameState.isActive) return; + + switch(event.key) { + case 'ArrowLeft': + event.preventDefault(); + this.previousMessage(); + break; + case 'ArrowRight': + event.preventDefault(); + this.nextMessage(); + break; + case ' ': + event.preventDefault(); + if (this.phoneData.isPlaying) { + this.stopCurrentMessage(); + } else { + this.playCurrentMessage(); + } + break; + case 'Escape': + event.preventDefault(); + this.showMessageList(); + break; + } + } + + showMessageDetail(index) { + if (index < 0 || index >= this.phoneData.messages.length) return; + + this.phoneData.currentMessageIndex = index; + const message = this.phoneData.messages[index]; + + // Update message detail view + this.senderName.textContent = message.sender || 'Unknown'; + this.messageTime.textContent = message.timestamp || 'Unknown time'; + + // Format message content based on type + if (message.type === 'voice') { + this.messageContent.innerHTML = ` +
+
+
+ Audio +
+
Transcript:
${message.voice || message.text || 'No transcript available'} +
+
+ `; + } else { + this.messageContent.textContent = message.text || 'No text content'; + } + + // Set up actions based on message type + this.setupMessageActions(message); + + // Add click listener for audio controls if it's a voice message + if (message.type === 'voice') { + const audioControls = this.messageContent.querySelector('.audio-controls'); + if (audioControls) { + this.addEventListener(audioControls, 'click', () => { + this.toggleCurrentMessage(); + }); + } + } + + // Show detail view + this.messagesList.style.display = 'none'; + this.messageDetail.style.display = 'block'; + + // Mark as read + message.read = true; + this.updateMessageStatus(index); + } + + setupMessageActions(message) { + this.messageActions.innerHTML = ''; + + // Hide all action buttons - we only use the inline audio controls now + this.playBtn.style.display = 'none'; + this.stopBtn.style.display = 'none'; + this.prevBtn.style.display = 'none'; + this.nextBtn.style.display = 'none'; + + // Show a note if this is a voice message but speech is not available + if (message.type === 'voice' && message.voice && !this.speechAvailable) { + const note = document.createElement('div'); + note.className = 'voice-note'; + note.style.cssText = 'color: #666; font-size: 10px; text-align: center; margin-top: 10px; font-family: "Courier New", monospace;'; + note.textContent = 'Voice playback not available on this system'; + this.messageActions.appendChild(note); + } + } + + handleVoiceSelection(voiceName) { + if (!voiceName) { + // Auto-select best voice + this.selectBestVoice(); + return; + } + + const voices = this.phoneData.speechSynthesis.getVoices(); + const selectedVoice = voices.find(voice => voice.name === voiceName); + + if (selectedVoice) { + this.selectedVoice = selectedVoice; + console.log('User selected voice:', selectedVoice.name, selectedVoice.lang); + this.showSuccess(`Voice changed to: ${selectedVoice.name}`, false, 2000); + } + } + + refreshVoices() { + console.log('Refreshing voices...'); + this.showSuccess("Refreshing voices...", false, 1000); + + // Force voice reload + this.setupVoiceSelection(); + + // Also try to trigger voiceschanged event + if (this.phoneData.speechSynthesis) { + // Create a temporary utterance to trigger voice loading + const tempUtterance = new SpeechSynthesisUtterance(''); + tempUtterance.volume = 0; + this.phoneData.speechSynthesis.speak(tempUtterance); + this.phoneData.speechSynthesis.cancel(); + } + } + + addToNotebook() { + // Check if there are any messages + if (!this.phoneData.messages || this.phoneData.messages.length === 0) { + this.showFailure("No messages to add to notebook", false, 2000); + return; + } + + // Create comprehensive notebook content for all messages + const notebookContent = this.formatAllMessagesForNotebook(); + const notebookTitle = `Phone Messages - ${this.params?.title || 'Phone'}`; + const notebookObservations = this.params?.observations || `Phone messages from ${this.params?.title || 'phone'}`; + + // Check if notes minigame is available + if (window.startNotesMinigame) { + // Store the phone state globally so we can return to it + const phoneState = { + messages: this.phoneData.messages, + currentMessageIndex: this.phoneData.currentMessageIndex, + selectedVoice: this.selectedVoice, + speechAvailable: this.speechAvailable, + voiceSettings: this.voiceSettings, + params: this.params + }; + + window.pendingPhoneReturn = phoneState; + + // Create a phone messages item for the notes minigame + const phoneMessagesItem = { + scenarioData: { + type: 'phone_messages', + name: notebookTitle, + text: notebookContent, + observations: notebookObservations, + important: true // Mark as important since it's from a phone + } + }; + + // Start notes minigame - it will handle returning to phone via returnToPhoneAfterNotes + window.startNotesMinigame( + phoneMessagesItem, + notebookContent, + notebookObservations, + null, // Let notes minigame auto-navigate to the newly added note + false, // Don't auto-add to inventory + false // Don't auto-close + ); + + this.showSuccess("Added all messages to notebook", false, 2000); + } else { + this.showFailure("Notebook not available", false, 2000); + } + } + + formatAllMessagesForNotebook() { + let content = `Phone Messages Log\n`; + content += `Source: ${this.params?.title || 'Phone'}\n`; + content += `Total Messages: ${this.phoneData.messages.length}\n`; + content += `Date: ${new Date().toLocaleString()}\n\n`; + content += `${'='.repeat(50)}\n\n`; + + this.phoneData.messages.forEach((message, index) => { + content += `Message ${index + 1}:\n`; + content += `${'-'.repeat(20)}\n`; + content += `From: ${message.sender}\n`; + content += `Time: ${message.timestamp}\n`; + content += `Type: ${message.type === 'voice' ? 'Voice Message' : 'Text Message'}\n`; + content += `Status: ${message.read ? 'Read' : 'Unread'}\n\n`; + + if (message.type === 'voice') { + // For voice messages, show audio icon and transcript + content += `🎵 [Audio Message]\n`; + content += `Transcript: ${message.voice || message.text || 'No transcript available'}\n\n`; + } else { + // For text messages, show the content + content += `${message.text || 'No text content'}\n\n`; + } + }); + + content += `${'='.repeat(50)}\n`; + content += `End of Phone Messages Log`; + + return content; + } + + showMessageList() { + this.messageDetail.style.display = 'none'; + this.messagesList.style.display = 'block'; + this.stopCurrentMessage(); + } + + previousMessage() { + if (this.phoneData.currentMessageIndex > 0) { + this.phoneData.currentMessageIndex--; + this.showMessageDetail(this.phoneData.currentMessageIndex); + } + } + + nextMessage() { + if (this.phoneData.currentMessageIndex < this.phoneData.messages.length - 1) { + this.phoneData.currentMessageIndex++; + this.showMessageDetail(this.phoneData.currentMessageIndex); + } + } + + playCurrentMessage() { + const message = this.phoneData.messages[this.phoneData.currentMessageIndex]; + + if (!message || message.type !== 'voice' || !message.voice) { + this.showFailure("No voice message to play", false, 2000); + return; + } + + if (this.phoneData.isPlaying) { + this.stopCurrentMessage(); + return; + } + + // Check if speech synthesis is available + if (!this.speechAvailable || !this.phoneData.speechSynthesis) { + this.showFailure("Voice playback not available on this system. Text is displayed above.", false, 3000); + return; + } + + // Stop any current speech + this.phoneData.speechSynthesis.cancel(); + + // Create new utterance + this.phoneData.currentUtterance = new SpeechSynthesisUtterance(message.voice); + + // Configure voice settings + this.phoneData.currentUtterance.rate = this.voiceSettings.rate; + this.phoneData.currentUtterance.pitch = this.voiceSettings.pitch; + this.phoneData.currentUtterance.volume = this.voiceSettings.volume; + + // Set the selected voice if available + if (this.selectedVoice) { + this.phoneData.currentUtterance.voice = this.selectedVoice; + } + + // Set up event handlers + this.phoneData.currentUtterance.onstart = () => { + this.phoneData.isPlaying = true; + this.updatePlayButtonIcon(); + }; + + this.phoneData.currentUtterance.onend = () => { + this.phoneData.isPlaying = false; + this.updatePlayButtonIcon(); + }; + + this.phoneData.currentUtterance.onerror = (event) => { + console.error('Speech synthesis error:', event); + this.phoneData.isPlaying = false; + this.updatePlayButtonIcon(); + this.speechAvailable = false; // Mark as unavailable for future attempts + + // Show a more helpful error message + let errorMessage = "Voice playback failed. "; + if (event.error === 'synthesis-failed') { + errorMessage += "This is common on Linux systems. The text is displayed above."; + } else { + errorMessage += "The text is displayed above."; + } + this.showFailure(errorMessage, false, 4000); + }; + + // Start speaking + try { + this.phoneData.speechSynthesis.speak(this.phoneData.currentUtterance); + } catch (error) { + console.error('Failed to start speech synthesis:', error); + this.phoneData.isPlaying = false; + this.updatePlayButtonIcon(); + this.speechAvailable = false; + this.showFailure("Voice playback not supported on this system. Text is displayed above.", false, 3000); + } + } + + stopCurrentMessage() { + if (this.phoneData.isPlaying) { + this.phoneData.speechSynthesis.cancel(); + this.phoneData.isPlaying = false; + this.updatePlayButtonIcon(); + } + } + + updatePlayButtonIcon() { + const playButton = this.messageContent.querySelector('.play-button'); + if (playButton) { + playButton.textContent = this.phoneData.isPlaying ? '⏹' : '▶'; + } + } + + toggleCurrentMessage() { + if (this.phoneData.isPlaying) { + this.stopCurrentMessage(); + } else { + this.playCurrentMessage(); + } + } + + updateMessageStatus(index) { + const messageItems = this.messagesList.querySelectorAll('.message-item'); + if (messageItems[index]) { + const statusElement = messageItems[index].querySelector('.message-status'); + if (statusElement) { + statusElement.className = 'message-status read'; + } + } + } + + start() { + // Call parent start + super.start(); + + console.log("Phone messages minigame started"); + + // Show message list initially + this.showMessageList(); + } + + cleanup() { + // Stop any playing speech + this.stopCurrentMessage(); + + // Call parent cleanup (handles event listeners) + super.cleanup(); + } +} + +// Function to return to phone after notes minigame (similar to container pattern) +export function returnToPhoneAfterNotes() { + console.log('Returning to phone after notes minigame'); + + // Check if there's a pending phone return + if (window.pendingPhoneReturn) { + const phoneState = window.pendingPhoneReturn; + + // Clear the pending return state + window.pendingPhoneReturn = null; + + // Start the phone minigame with the stored state + if (window.MinigameFramework) { + window.MinigameFramework.startMinigame('phone-messages', null, { + title: phoneState.params?.title || 'Phone Messages', + messages: phoneState.messages, + observations: phoneState.params?.observations, + onComplete: (success, result) => { + console.log('Phone messages minigame completed:', success, result); + } + }); + } + } else { + console.warn('No pending phone return state found'); + } +} diff --git a/js/systems/interactions.js b/js/systems/interactions.js index e07eda7..af060f8 100644 --- a/js/systems/interactions.js +++ b/js/systems/interactions.js @@ -253,6 +253,55 @@ export function handleObjectInteraction(sprite) { message += `Observations: ${data.observations}\n`; } + // For phone type objects, use the phone messages minigame + if (data.type === 'phone' && (data.text || data.voice)) { + console.log('Phone object detected:', { type: data.type, text: data.text, voice: data.voice }); + // Start the phone messages minigame + if (window.MinigameFramework) { + // Initialize the framework if not already done + if (!window.MinigameFramework.mainGameScene && window.game) { + window.MinigameFramework.init(window.game); + } + + const messages = []; + + // Add text message if available + if (data.text) { + messages.push({ + type: 'text', + sender: data.sender || 'Unknown', + text: data.text, + timestamp: data.timestamp || 'Unknown time', + read: false + }); + } + + // Add voice message if available + if (data.voice) { + messages.push({ + type: 'voice', + sender: data.sender || 'Unknown', + text: data.text || null, // text is optional for voice messages + voice: data.voice, + timestamp: data.timestamp || 'Unknown time', + read: false + }); + } + + const minigameParams = { + title: data.name || 'Phone Messages', + messages: messages, + observations: data.observations, + onComplete: (success, result) => { + console.log('Phone messages minigame completed:', success, result); + } + }; + + window.MinigameFramework.startMinigame('phone-messages', null, minigameParams); + return; // Exit early since minigame handles the interaction + } + } + if (data.readable && data.text) { message += `Text: ${data.text}\n`; diff --git a/scenarios/ceo_exfil.json b/scenarios/ceo_exfil.json index 6a43df5..f1e84ba 100644 --- a/scenarios/ceo_exfil.json +++ b/scenarios/ceo_exfil.json @@ -13,7 +13,9 @@ "name": "Reception Phone", "takeable": false, "readable": true, - "text": "Voicemail: 'Security breach detected in server room. Changed access code to 4829. - IT Team'", + "voice": "Hi, this is the IT Team. Security breach detected in server room. Changed access code to 4829.", + "sender": "IT Team", + "timestamp": "2:15 AM", "observations": "The reception phone's message light is blinking urgently" }, { @@ -216,6 +218,8 @@ "takeable": false, "readable": true, "text": "Recent calls: 'Offshore Bank', 'Unknown', 'Data Buyer'", + "sender": "Call Log", + "timestamp": "Last 24 hours", "observations": "The CEO's phone shows suspicious recent calls" } ] diff --git a/test-phone-minigame.html b/test-phone-minigame.html new file mode 100644 index 0000000..23ec78b --- /dev/null +++ b/test-phone-minigame.html @@ -0,0 +1,174 @@ + + + + + + Phone Messages Minigame Test + + + + + + +
+

Phone Messages Minigame Test

+ +
+

Test Instructions:

+ +
+ + + + + +
+
+ + + + +