From 051b90aaa8c50f607e0761a17a21f399171d955b Mon Sep 17 00:00:00 2001 From: "Z. Cliffe Schreuders" Date: Mon, 13 Oct 2025 18:48:14 +0100 Subject: [PATCH] Add Phone Messages Minigame: Introduce a new minigame for interacting with phone messages, including voice and text message playback, voice selection, and observation display. Update index.html to include the new CSS file and integrate the minigame into the existing framework. Add test page for functionality and enhance interaction logic for phone objects in the game. --- assets/mini-games/audio.png | Bin 0 -> 207 bytes css/minigames.css | 2 +- css/phone.css | 447 +++++++++ index.html | 1 + js/core/game.js | 9 + js/minigames/framework/minigame-manager.js | 20 +- js/minigames/index.js | 25 +- js/minigames/notes/notes-minigame.js | 9 + js/minigames/phone/phone-messages-minigame.js | 920 ++++++++++++++++++ js/systems/interactions.js | 49 + scenarios/ceo_exfil.json | 6 +- test-phone-minigame.html | 174 ++++ 12 files changed, 1656 insertions(+), 6 deletions(-) create mode 100644 assets/mini-games/audio.png create mode 100644 css/phone.css create mode 100644 js/minigames/phone/phone-messages-minigame.js create mode 100644 test-phone-minigame.html diff --git a/assets/mini-games/audio.png b/assets/mini-games/audio.png new file mode 100644 index 0000000000000000000000000000000000000000..0f813105f2a5939bad9e556677b1c0f4f4d12a78 GIT binary patch literal 207 zcmeAS@N?(olHy`uVBq!ia0vp^)<7)4!3HE(__4bIDaPU;cPEB*=VV?2Ih~#^jv*eM zcc*L=J0QTraCp6xuHDh3FKew*HRo-aYEsyJ(OR8RK`1FpNL_v2@22GiUgewW_v{dP zbxKL5e@&R!Owau@Yrk$f(RV2=MR3)Z6&6GVyY{gj)r0i3VciI7T6oaR$pUXO@ GgeCySi%$#y literal 0 HcmV?d00001 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:

+
    +
  • Reception Phone: Contains a voicemail from IT Team about server room access code
  • +
  • CEO Phone: Shows recent call log with suspicious contacts
  • +
  • Controls: Use arrow keys to navigate, spacebar to play/stop voice messages, escape to go back
  • +
  • Voice Messages: Uses Web Speech API to play back voice content
  • +
+
+ + + + + +
+
+ + + + +