/** * Interactive Task Manager - Handles advanced task types with user interaction * Supports mini-games, interactive elements, progress tracking, and dynamic content */ class InteractiveTaskManager { constructor(gameInstance) { this.game = gameInstance; this.currentInteractiveTask = null; this.taskContainer = null; this.isInteractiveTaskActive = false; // Initialize TTS voice manager (will be loaded when needed) this.voiceManager = null; this.ttsEnabled = false; this.initializeTTS(); // Task type registry this.taskTypes = new Map(); this.registerBuiltInTaskTypes(); } /** * Initialize TTS system */ async initializeTTS() { try { // Check if VoiceManager is available globally if (window.VoiceManager) { this.voiceManager = new window.VoiceManager(); if (this.voiceManager.isSupported()) { await this.voiceManager.initialize(); this.ttsEnabled = true; console.log('🎤 TTS initialized with voice:', this.voiceManager.getVoiceInfo().name); } else { console.warn('TTS not supported in this environment'); } } else { console.log('VoiceManager not available, TTS disabled'); } } catch (error) { console.error('Failed to initialize TTS:', error); } } /** * Register all built-in interactive task types */ registerBuiltInTaskTypes() { // Rhythm tasks removed per user request // Input Tasks // Focus/Concentration Tasks this.registerTaskType('focus-hold', { name: 'Focus Hold', description: 'Maintain focus on a target for set duration', handler: this.createFocusTask.bind(this), validator: this.validateFocusTask.bind(this) }); // Scenario Builder Tasks this.registerTaskType('scenario-adventure', { name: 'Choose Your Own Adventure', description: 'Interactive storylines with meaningful choices', handler: this.createScenarioTask.bind(this), validator: this.validateScenarioTask.bind(this) }); // Mirror tasks this.registerTaskType('mirror-task', { name: 'Webcam Mirror Task', description: 'Use webcam as mirror for self-viewing tasks', handler: this.createMirrorTask.bind(this), validator: this.validateMirrorTask.bind(this) }); } /** * Register a new task type */ registerTaskType(typeId, config) { this.taskTypes.set(typeId, { id: typeId, name: config.name, description: config.description, handler: config.handler, validator: config.validator, cleanup: config.cleanup || (() => {}) }); console.log(`Registered interactive task type: ${typeId}`); } /** * Check if a task is interactive */ isInteractiveTask(task) { return task.hasOwnProperty('interactiveType') && this.taskTypes.has(task.interactiveType); } /** * Display an interactive task */ async displayInteractiveTask(task) { if (!this.isInteractiveTask(task)) { console.warn('Task is not interactive:', task); return false; } this.currentInteractiveTask = task; this.isInteractiveTaskActive = true; // Handle task image for interactive tasks const taskImage = document.getElementById('task-image'); if (taskImage && task.image) { taskImage.src = task.image; taskImage.onerror = () => { console.warn('Interactive task image failed to load:', task.image); // Create placeholder if image fails taskImage.src = this.createPlaceholderImage('Interactive Task'); }; } else if (taskImage) { taskImage.src = this.createPlaceholderImage('Interactive Task'); } // Get the task container this.taskContainer = document.querySelector('.task-display-container'); // Add interactive container const interactiveContainer = document.createElement('div'); interactiveContainer.id = 'interactive-task-container'; interactiveContainer.className = 'interactive-task-container'; // Insert after task text const taskTextContainer = document.querySelector('.task-text-container'); taskTextContainer.parentNode.insertBefore(interactiveContainer, taskTextContainer.nextSibling); // Get task type config and create the interactive element const taskType = this.taskTypes.get(task.interactiveType); if (taskType && taskType.handler) { await taskType.handler(task, interactiveContainer); } // Hide normal action buttons and show interactive controls this.showInteractiveControls(); return true; } /** * Create placeholder image for interactive tasks */ createPlaceholderImage(text) { const canvas = document.createElement('canvas'); canvas.width = 400; canvas.height = 300; const ctx = canvas.getContext('2d'); // Create gradient background const gradient = ctx.createLinearGradient(0, 0, 400, 300); gradient.addColorStop(0, '#667eea'); gradient.addColorStop(1, '#764ba2'); ctx.fillStyle = gradient; ctx.fillRect(0, 0, 400, 300); // Add text ctx.fillStyle = 'white'; ctx.font = '24px Arial'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(text, 200, 150); return canvas.toDataURL(); } /** * Show interactive controls and hide standard buttons */ showInteractiveControls() { const actionButtons = document.querySelector('.action-buttons'); const originalButtons = actionButtons.querySelectorAll('button:not(.interactive-btn)'); // Hide original buttons originalButtons.forEach(btn => btn.style.display = 'none'); // Add interactive control buttons const interactiveControls = document.createElement('div'); interactiveControls.className = 'interactive-controls'; interactiveControls.innerHTML = ` `; actionButtons.appendChild(interactiveControls); // Add event listeners document.getElementById('interactive-complete-btn').addEventListener('click', () => { console.log('🎯 Complete Challenge button clicked!'); console.log('🎯 Current task:', this.game?.gameState?.currentTask?.id); console.log('🎯 Is scenario mode:', window.gameModeManager?.isScenarioMode()); console.log('🎯 Current interactive task:', this.currentInteractiveTask); this.completeInteractiveTask(); }); document.getElementById('interactive-give-up-btn').addEventListener('click', () => this.giveUpInteractiveTask()); } /** * Validate and complete interactive task */ async completeInteractiveTask() { console.log('🔍 CompleteInteractiveTask called - currentInteractiveTask:', this.currentInteractiveTask); if (!this.currentInteractiveTask) { console.log('❌ No current interactive task found!'); return; } // Check if game is still running - prevent completion after game has ended if (!this.game.gameState.isRunning) { console.log('Interactive task completion cancelled - game no longer running'); return; } console.log(`🔍 Attempting to complete interactive task:`, this.currentInteractiveTask); console.log(`🔍 Task interactiveType: ${this.currentInteractiveTask.interactiveType}`); console.log(`🔍 Task isInterruption: ${this.currentInteractiveTask.isInterruption}`); console.log(`🔍 Task scenarioState:`, this.currentInteractiveTask.scenarioState); const taskType = this.taskTypes.get(this.currentInteractiveTask.interactiveType); console.log(`🔍 Found task type:`, taskType); if (taskType && taskType.validator) { console.log(`🔍 Calling validator for ${this.currentInteractiveTask.interactiveType}`); const isValid = await taskType.validator(this.currentInteractiveTask); if (isValid) { // Task completed successfully this.showFeedback('success', 'Challenge completed successfully! 🎉'); setTimeout(() => { // Double-check game is still running before proceeding if (this.game.gameState.isRunning) { // Check if this was a focus interruption task BEFORE cleanup const isInterruption = this.currentInteractiveTask && this.currentInteractiveTask.isInterruption; console.log(`🔍 Is interruption task: ${isInterruption}`); // For interruption tasks, don't fully cleanup - we need the container if (isInterruption) { // Just stop audio and clear task reference, but keep container if (this.game && this.game.audioManager) { console.log('Interruption task cleanup - stopping audio only'); this.game.audioManager.stopCategory('tasks', 0); this.game.audioManager.stopCategory('punishments', 0); } this.currentInteractiveTask = null; } else { this.cleanupInteractiveTask(); } if (isInterruption) { console.log(`🧘 Resuming scenario after interruption completion`); this.resumeScenarioAfterInterruption(); } else { console.log(`🎯 Completing regular task`); this.game.completeTask(); } } }, 1500); } else { // Task failed validation this.showFeedback('error', 'Not quite right. Try again! 🤔'); this.enableInteractiveControls(); } } } /** * Give up on interactive task (shows warning first) */ giveUpInteractiveTask() { // Check if game is still running if (!this.game.gameState.isRunning) { console.log('Interactive task give up cancelled - game no longer running'); return; } // Show rude warning popup instead of immediate action this.showGiveUpWarning(); } /** * Show rude warning popup when user tries to give up */ showGiveUpWarning() { // Create warning modal const modal = document.createElement('div'); modal.id = 'give-up-warning-modal'; modal.className = 'modal-overlay'; const rudeMessages = [ "Really? You're going to quit like a pathetic little coward?", "What a weak, spineless loser. Can't handle a simple challenge?", "Typical. Running away the moment things get interesting.", "How embarrassing. Even a child could do better than this.", "Such a disappointment. I expected more from you, but clearly I was wrong.", "Pathetic. You're proving just how worthless and weak you really are.", "Giving up already? What a sad, miserable failure you are.", "Can't even finish what you started. How utterly shameful." ]; const randomMessage = rudeMessages[Math.floor(Math.random() * rudeMessages.length)]; modal.innerHTML = ` `; document.body.appendChild(modal); // Add event listeners document.getElementById('confirm-give-up').addEventListener('click', () => { this.confirmGiveUp(); document.body.removeChild(modal); }); document.getElementById('cancel-give-up').addEventListener('click', () => { document.body.removeChild(modal); }); // Add styles for the warning modal this.addGiveUpWarningStyles(); } /** * Actually give up after confirmation */ confirmGiveUp() { // Double-check game is still running before proceeding if (this.game.gameState.isRunning) { this.cleanupInteractiveTask(); this.game.skipTask(); } } /** * Show hint for current task */ showHint() { if (!this.currentInteractiveTask || !this.currentInteractiveTask.hint) { this.showFeedback('info', 'No hint available for this challenge.'); return; } this.showFeedback('info', `💡 Hint: ${this.currentInteractiveTask.hint}`); } /** * Show feedback message */ showFeedback(type, message, duration = 3000) { // Create or update feedback element let feedback = document.getElementById('interactive-feedback'); if (!feedback) { feedback = document.createElement('div'); feedback.id = 'interactive-feedback'; feedback.className = 'interactive-feedback'; const container = document.getElementById('interactive-task-container'); if (container) { container.appendChild(feedback); } else { // If no interactive container, try to append to task display const taskDisplay = document.querySelector('.task-display-container') || document.querySelector('.task-display') || document.body; if (taskDisplay) { taskDisplay.appendChild(feedback); } else { console.warn('No suitable container found for feedback message'); return; // Exit early if no container available } } } feedback.className = `interactive-feedback feedback-${type}`; feedback.textContent = message; feedback.style.display = 'block'; // Auto-hide after specified duration for non-error messages if (type !== 'error') { setTimeout(() => { if (feedback) feedback.style.display = 'none'; }, duration); } } /** * Enable/disable interactive controls */ enableInteractiveControls(enabled = true) { const completeBtn = document.getElementById('interactive-complete-btn'); if (completeBtn) { completeBtn.disabled = !enabled; } } /** * Clean up interactive task elements */ cleanupInteractiveTask() { // Stop any audio that might be playing before cleanup if (this.game && this.game.audioManager) { this.game.audioManager.stopCategory('tasks', 0); this.game.audioManager.stopCategory('punishments', 0); } // Stop any ongoing TTS narration if (this.voiceManager) { this.stopTTS(); } // Remove interactive container const container = document.getElementById('interactive-task-container'); if (container) { container.remove(); } // Remove interactive controls const controls = document.querySelector('.interactive-controls'); if (controls) { controls.remove(); } // Show original buttons const actionButtons = document.querySelector('.action-buttons'); const originalButtons = actionButtons.querySelectorAll('button:not(.interactive-btn)'); originalButtons.forEach(btn => btn.style.display = ''); // Run task-specific cleanup if (this.currentInteractiveTask) { const taskType = this.taskTypes.get(this.currentInteractiveTask.interactiveType); if (taskType && taskType.cleanup) { taskType.cleanup(); } } this.currentInteractiveTask = null; this.isInteractiveTaskActive = false; } // ============================================================================= // TASK TYPE IMPLEMENTATIONS // ============================================================================= // Rhythm task implementations removed per user request // ============================================================================= // TASK TYPE IMPLEMENTATIONS - Focus, Scenario, and Mirror tasks only // ============================================================================= async createFocusTask(task, container) { console.log('📝 Creating text input task'); // Create text input interface container.innerHTML = `

✍️ ${task.text}

${task.story}

Minimum ${task.minLength || 10} characters required
`; // Style the container const taskContainer = container.querySelector('.text-input-task'); taskContainer.style.cssText = ` background: rgba(40, 40, 40, 0.95); border-radius: 15px; padding: 25px; margin: 15px 0; border: 2px solid rgba(76, 175, 80, 0.4); box-shadow: 0 5px 15px rgba(76, 175, 80, 0.2); `; // Style the textarea const textarea = container.querySelector('#task-text-input'); textarea.style.cssText = ` width: 100%; padding: 15px; border-radius: 8px; border: 2px solid rgba(76, 175, 80, 0.3); background: rgba(30, 30, 30, 0.8); color: white; font-size: 16px; resize: vertical; margin: 10px 0; `; // Style the submit button const submitBtn = container.querySelector('#submit-text-btn'); submitBtn.style.cssText = ` background: linear-gradient(135deg, #4caf50, #45a049); color: white; border: none; padding: 15px 30px; font-size: 16px; border-radius: 8px; cursor: pointer; margin-top: 15px; width: 100%; `; // Add validation and submission logic const minLength = task.minLength || 10; textarea.addEventListener('input', () => { const length = textarea.value.length; const reqDiv = container.querySelector('.text-requirements'); if (length >= minLength) { submitBtn.disabled = false; submitBtn.style.opacity = '1'; reqDiv.style.color = '#4caf50'; reqDiv.textContent = `✓ Requirement met (${length} characters)`; } else { submitBtn.disabled = true; submitBtn.style.opacity = '0.6'; reqDiv.style.color = '#f44336'; reqDiv.textContent = `Need ${minLength - length} more characters`; } }); submitBtn.addEventListener('click', () => { console.log('📝 Text input submitted:', textarea.value); submitBtn.disabled = true; submitBtn.textContent = '✓ Submitted'; submitBtn.style.background = '#4caf50'; // Enable completion controls setTimeout(() => { this.enableInteractiveControls(true); }, 1500); }); console.log('📝 Text input task created successfully'); } async validateTextInputTask(task) { const textarea = document.getElementById('task-text-input'); const submitBtn = document.getElementById('submit-text-btn'); if (!textarea || !submitBtn) return false; // Check if task was submitted (button disabled and text changed) return submitBtn.disabled && submitBtn.textContent.includes('✓'); } async createSliderTask(task, container) { console.log('🎚️ Creating slider challenge task'); // Create slider task interface container.innerHTML = `

🎚️ ${task.text}

${task.story}

Target: ${task.targetValue || 50}${task.valueUnit || '%'}

Time Limit: ${task.duration || 60} seconds

Move the slider to reach the exact target value!

${task.startValue || 0} Target: ${task.targetValue || 50}
`; // Add task-specific styling const styles = document.createElement('style'); styles.textContent = ` .slider-task { max-width: 600px; margin: 0 auto; padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 15px; color: white; } .slider-container { background: rgba(255,255,255,0.1); padding: 20px; border-radius: 10px; margin: 20px 0; } .slider-input { width: 100%; height: 20px; margin: 15px 0; appearance: none; background: linear-gradient(to right, #ff4757, #ffa502, #2ed573); border-radius: 10px; outline: none; } .slider-input::-webkit-slider-thumb { appearance: none; width: 30px; height: 30px; background: white; border-radius: 50%; cursor: pointer; box-shadow: 0 0 10px rgba(0,0,0,0.3); } .slider-display { display: flex; justify-content: space-between; font-size: 18px; font-weight: bold; } .current-value { color: #ffa502; } .target-value { color: #2ed573; } .timer-progress { margin: 15px 0; } .progress-bar { width: 100%; height: 10px; background: rgba(255,255,255,0.2); border-radius: 5px; overflow: hidden; position: relative; } .progress-bar::after { content: ''; position: absolute; top: 0; left: 0; height: 100%; background: linear-gradient(90deg, #2ed573, #ffa502); border-radius: 5px; transition: width 0.1s linear; } `; document.head.appendChild(styles); // Get elements const slider = document.getElementById('challenge-slider'); const currentValueSpan = document.getElementById('current-value'); const startBtn = document.getElementById('start-slider-btn'); const submitBtn = document.getElementById('submit-slider-btn'); const timerProgress = document.querySelector('.timer-progress'); const progressBar = document.getElementById('slider-progress-bar'); const timerText = document.getElementById('slider-timer-text'); let isActive = false; let timeLeft = task.duration || 60; let timerInterval = null; // Update slider display slider.addEventListener('input', (e) => { currentValueSpan.textContent = e.target.value; // Visual feedback based on accuracy const target = task.targetValue || 50; const current = parseInt(e.target.value); const accuracy = Math.abs(target - current); const tolerance = task.tolerance || 5; if (accuracy <= tolerance) { currentValueSpan.style.color = '#2ed573'; // Green for close } else if (accuracy <= tolerance * 2) { currentValueSpan.style.color = '#ffa502'; // Orange for medium } else { currentValueSpan.style.color = '#ff4757'; // Red for far } }); // Start challenge startBtn.addEventListener('click', () => { if (isActive) return; isActive = true; startBtn.style.display = 'none'; submitBtn.style.display = 'block'; timerProgress.style.display = 'block'; // Start timer const totalTime = task.duration || 60; timerInterval = setInterval(() => { timeLeft--; timerText.textContent = `${timeLeft}s`; // Update progress bar const progress = ((totalTime - timeLeft) / totalTime) * 100; progressBar.style.setProperty('--progress', `${progress}%`); progressBar.querySelector('::after') && (progressBar.style.background = `linear-gradient(90deg, transparent ${progress}%, #2ed573 ${progress}%)`); if (timeLeft <= 0) { clearInterval(timerInterval); this.completeSliderTask(task, false, 'Time ran out!'); } }, 1000); }); // Submit answer submitBtn.addEventListener('click', () => { if (!isActive) return; clearInterval(timerInterval); const target = task.targetValue || 50; const current = parseInt(slider.value); const accuracy = Math.abs(target - current); const tolerance = task.tolerance || 5; const success = accuracy <= tolerance; const message = success ? `Perfect! You hit ${current}, target was ${target}` : `Close! You hit ${current}, target was ${target} (off by ${accuracy})`; this.completeSliderTask(task, success, message); }); console.log('✅ Slider challenge task interface created'); } async validateSliderTask(task) { console.log('🎚️ Validating slider task'); return true; // Validation handled in interface } completeSliderTask(task, success, message) { console.log(`🎚️ Slider task completed: ${success ? 'Success' : 'Failed'}`); // Show completion feedback const container = this.taskContainer; if (container) { container.innerHTML = `
${success ? '🎯' : '❌'}

${success ? 'Perfect Aim!' : 'Nice Try!'}

${message}

`; } // Mark task as completed this.isInteractiveTaskActive = false; this.currentInteractiveTask = null; } async createRhythmTask(task, container) { console.log('🥁 Creating rhythm challenge task'); // Create rhythm task interface container.innerHTML = `

🥁 ${task.text}

${task.story}

Duration: ${task.duration || 60} seconds

Target BPM: ${task.targetBPM || 120}

Tap the beat button to match the rhythm!

0
BPM
0%
Accuracy
`; // Add rhythm-specific styling const styles = document.createElement('style'); styles.textContent = ` .rhythm-task { max-width: 700px; margin: 0 auto; padding: 20px; background: linear-gradient(135deg, #e74c3c 0%, #8e44ad 100%); border-radius: 15px; color: white; } .rhythm-display { display: flex; justify-content: space-around; margin: 20px 0; } .bpm-meter, .accuracy-meter { background: rgba(255,255,255,0.1); padding: 15px; border-radius: 10px; text-align: center; min-width: 120px; } .current-bpm, .accuracy-value { font-size: 24px; font-weight: bold; color: #ffa502; } .bpm-label, .accuracy-label { font-size: 12px; opacity: 0.8; margin-top: 5px; } .beat-button-container { text-align: center; margin: 20px 0; } .beat-button { width: 200px; height: 200px; border-radius: 50%; background: linear-gradient(135deg, #ff6b6b, #ffa500); border: none; color: white; font-size: 18px; font-weight: bold; cursor: pointer; transition: all 0.1s ease; box-shadow: 0 10px 20px rgba(0,0,0,0.3); } .beat-button:hover:not(:disabled) { transform: scale(1.05); } .beat-button:active { transform: scale(0.95); background: linear-gradient(135deg, #ff4757, #ff6348); } .beat-button:disabled { opacity: 0.5; cursor: not-allowed; } .metronome-display { text-align: center; margin: 20px 0; } .metronome-dot { width: 20px; height: 20px; background: #2ed573; border-radius: 50%; margin: 0 auto; transition: all 0.1s ease; opacity: 0.3; } .metronome-dot.beat { opacity: 1; transform: scale(1.5); background: #fff; } .timer-progress { margin: 15px 0; } .progress-bar { width: 100%; height: 10px; background: rgba(255,255,255,0.2); border-radius: 5px; overflow: hidden; position: relative; } .progress-bar::after { content: ''; position: absolute; top: 0; left: 0; height: 100%; background: linear-gradient(90deg, #e74c3c, #ffa502); border-radius: 5px; transition: width 0.1s linear; } `; document.head.appendChild(styles); // Initialize rhythm tracking const beatButton = document.getElementById('beat-button'); const startBtn = document.getElementById('start-rhythm-btn'); const currentBpmSpan = document.getElementById('current-bpm'); const accuracySpan = document.getElementById('accuracy-value'); const timerProgress = document.querySelector('.timer-progress'); const progressBar = document.getElementById('rhythm-progress-bar'); const timerText = document.getElementById('rhythm-timer-text'); const metronomeDot = document.querySelector('.metronome-dot'); let isActive = false; let timeLeft = task.duration || 60; let timerInterval = null; let metronomeInterval = null; let tapTimes = []; let targetBPM = task.targetBPM || 120; let currentBPM = 0; let accuracy = 0; // Start challenge startBtn.addEventListener('click', () => { if (isActive) return; isActive = true; startBtn.style.display = 'none'; beatButton.disabled = false; timerProgress.style.display = 'block'; // Start metronome const beatInterval = 60000 / targetBPM; metronomeInterval = setInterval(() => { metronomeDot.classList.add('beat'); setTimeout(() => metronomeDot.classList.remove('beat'), 100); }, beatInterval); // Start timer const totalTime = task.duration || 60; timerInterval = setInterval(() => { timeLeft--; timerText.textContent = `${timeLeft}s`; // Update progress bar const progress = ((totalTime - timeLeft) / totalTime) * 100; progressBar.style.setProperty('--progress', `${progress}%`); if (timeLeft <= 0) { clearInterval(timerInterval); clearInterval(metronomeInterval); this.completeRhythmTask(task, accuracy >= 70, `Challenge complete! Final accuracy: ${accuracy}%`); } }, 1000); }); // Handle beat taps beatButton.addEventListener('click', () => { if (!isActive) return; const now = Date.now(); tapTimes.push(now); // Keep only recent taps (last 10 or within 5 seconds) tapTimes = tapTimes.filter(time => now - time < 5000).slice(-10); if (tapTimes.length >= 2) { // Calculate BPM from tap intervals const intervals = []; for (let i = 1; i < tapTimes.length; i++) { intervals.push(tapTimes[i] - tapTimes[i-1]); } const avgInterval = intervals.reduce((a, b) => a + b) / intervals.length; currentBPM = Math.round(60000 / avgInterval); // Calculate accuracy const bpmDiff = Math.abs(currentBPM - targetBPM); const maxDiff = targetBPM * 0.3; // 30% tolerance accuracy = Math.max(0, Math.round(100 * (1 - bpmDiff / maxDiff))); // Update display currentBpmSpan.textContent = currentBPM; accuracySpan.textContent = `${accuracy}%`; // Color feedback if (accuracy >= 80) { currentBpmSpan.style.color = '#2ed573'; // Green accuracySpan.style.color = '#2ed573'; } else if (accuracy >= 60) { currentBpmSpan.style.color = '#ffa502'; // Orange accuracySpan.style.color = '#ffa502'; } else { currentBpmSpan.style.color = '#ff4757'; // Red accuracySpan.style.color = '#ff4757'; } } }); console.log('✅ Rhythm challenge task interface created'); } async validateRhythmTask(task) { console.log('🥁 Validating rhythm task'); return true; // Validation handled in interface } completeRhythmTask(task, success, message) { console.log(`🥁 Rhythm task completed: ${success ? 'Success' : 'Failed'}`); // Show completion feedback const container = this.taskContainer; if (container) { container.innerHTML = `
${success ? '🎵' : '🥁'}

${success ? 'Perfect Rhythm!' : 'Keep Practicing!'}

${message}

`; } // Mark task as completed this.isInteractiveTaskActive = false; this.currentInteractiveTask = null; } async createChoiceTask(task, container) { // Implementation for choice tasks console.log('Choice task created'); } async validateChoiceTask(task) { return true; // Placeholder } async createSpeedTask(task, container) { // Implementation for speed tasks console.log('Speed task created'); } async validateSpeedTask(task) { return true; // Placeholder } async createFocusTask(task, container) { console.log('🧘 Creating focus task'); // Create focus task interface with integrated video player container.innerHTML = `

🧘 ${task.text}

${task.story}

${task.instructions || 'Hold the position and focus for the required time'}
${task.duration || 60} seconds
`; // Style the container const taskContainer = container.querySelector('.focus-task'); taskContainer.style.cssText = ` background: rgba(40, 40, 40, 0.95); border-radius: 15px; padding: 25px; margin: 15px 0; text-align: center; border: 2px solid rgba(103, 58, 183, 0.4); box-shadow: 0 5px 15px rgba(103, 58, 183, 0.2); `; // Style the timer display const timerDisplay = container.querySelector('#focus-timer-display'); timerDisplay.style.cssText = ` font-size: 48px; font-weight: bold; color: #673ab7; margin: 20px 0; `; // Style the start button const startBtn = container.querySelector('#start-focus-btn'); startBtn.style.cssText = ` background: linear-gradient(135deg, #673ab7, #5e35b1); color: white; border: none; padding: 15px 30px; font-size: 18px; border-radius: 8px; cursor: pointer; margin: 20px 0; `; // Style the progress bar container const progressContainer = container.querySelector('#focus-progress'); progressContainer.style.cssText = ` margin: 20px 0; width: 100%; `; const progressBar = container.querySelector('#progress-bar'); progressBar.style.cssText = ` width: 0%; height: 8px; background: linear-gradient(90deg, #673ab7, #9c27b0); border-radius: 4px; transition: width 1s ease; margin: 10px 0; `; // Style the volume slider const volumeSlider = container.querySelector('#focus-video-volume'); if (volumeSlider) { volumeSlider.style.cssText = ` width: 200px; height: 6px; background: rgba(255, 255, 255, 0.3); border-radius: 3px; outline: none; cursor: pointer; `; } // Initialize video management for focus session this.initializeFocusVideos(); // Add focus session logic const duration = task.duration || 60; startBtn.addEventListener('click', () => { console.log('🧘 Starting focus session'); // Track focus session start for XP (5 XP per minute) if (this.game && this.game.trackFocusSession) { this.game.trackFocusSession(true); } // Hide start button and show progress startBtn.style.display = 'none'; progressContainer.style.display = 'block'; // Show and start video player const videoContainer = container.querySelector('#focus-video-container'); if (videoContainer) { videoContainer.style.display = 'block'; this.setupFocusVolumeControl(); this.startFocusVideoPlayback(); } let timeLeft = duration; const progressText = container.querySelector('#progress-text'); const focusInterval = setInterval(() => { const elapsed = duration - timeLeft; const progressPercent = (elapsed / duration) * 100; // Update progress bar progressBar.style.width = `${progressPercent}%`; // Update timer display timerDisplay.textContent = `${timeLeft}s`; progressText.textContent = `Hold position... ${timeLeft}s remaining`; timeLeft--; if (timeLeft < 0) { clearInterval(focusInterval); // Track focus session end for XP if (this.game && this.game.trackFocusSession) { this.game.trackFocusSession(false); } // Focus session completed timerDisplay.textContent = 'Complete!'; progressText.textContent = '✓ Focus session completed'; progressBar.style.width = '100%'; progressBar.style.background = '#4caf50'; // Stop video playback this.stopFocusVideoPlayback(); console.log('🧘 Focus session completed'); // Enable completion controls setTimeout(() => { this.enableInteractiveControls(true); }, 1500); } }, 1000); }); console.log('🧘 Focus task created successfully'); } /** * Setup volume control for focus videos */ setupFocusVolumeControl() { const volumeSlider = document.getElementById('focus-video-volume'); const volumeDisplay = document.getElementById('focus-volume-display'); if (!volumeSlider || !volumeDisplay) { console.warn('🧘 ⚠️ Volume controls not found'); return; } // Get initial volume from video manager settings or default const initialVolume = (window.videoPlayerManager?.settings?.volume || 0.5) * 100; volumeSlider.value = initialVolume; volumeDisplay.textContent = `${Math.round(initialVolume)}%`; // Handle volume changes volumeSlider.addEventListener('input', (e) => { const volume = parseInt(e.target.value); const volumeDecimal = volume / 100; // Update display volumeDisplay.textContent = `${volume}%`; // Update video player volume const videoPlayer = document.getElementById('focus-video-player'); if (videoPlayer) { videoPlayer.volume = volumeDecimal; console.log(`🧘 🔊 Volume changed to: ${volume}%`); } // Save to video manager settings if available if (window.videoPlayerManager && window.videoPlayerManager.settings) { window.videoPlayerManager.settings.volume = volumeDecimal; console.log(`🧘 💾 Saved volume setting: ${volume}%`); } }); console.log(`🧘 🎛️ Volume control setup complete, initial volume: ${Math.round(initialVolume)}%`); } /** * Initialize video system for focus sessions */ initializeFocusVideos() { // Initialize with new FocusVideoPlayer try { this.focusVideoPlayer = new FocusVideoPlayer('#focus-video-container'); console.log('🧘 📺 FocusVideoPlayer initialized'); // Initialize video library from video manager if (window.videoPlayerManager) { this.focusVideoPlayer.initializeVideoLibrary(window.videoPlayerManager); } else { console.warn('🧘 ⚠️ Video manager not available, focus videos disabled'); } } catch (error) { console.error('🧘 ❌ Failed to initialize FocusVideoPlayer:', error); // Fallback to legacy implementation this.initializeLegacyFocusVideos(); } } /** * Legacy focus video initialization (fallback) */ initializeLegacyFocusVideos() { this.focusVideoLibrary = []; this.focusVideoPlayer = null; this.focusVideoIndex = 0; this.focusVideoActive = false; // Get all available videos from video manager if (window.videoPlayerManager && window.videoPlayerManager.videoLibrary) { const library = window.videoPlayerManager.videoLibrary; // Combine all video categories into one pool this.focusVideoLibrary = [ ...(library.task || []), ...(library.reward || []), ...(library.punishment || []), ...(library.background || []) ]; console.log(`🧘 🎬 Initialized legacy focus video library with ${this.focusVideoLibrary.length} videos`); if (this.focusVideoLibrary.length === 0) { console.warn('🧘 ⚠️ No videos found in any category, focus videos will be disabled'); } } else { console.warn('🧘 ⚠️ Video manager not available, focus videos disabled'); } } /** * Start continuous video playback for focus session */ async startFocusVideoPlayback() { // Try new FocusVideoPlayer first if (this.focusVideoPlayer && typeof this.focusVideoPlayer.startFocusSession === 'function') { console.log('🧘 📺 Starting focus session with FocusVideoPlayer'); this.focusVideoPlayer.startFocusSession(); return; } // Fallback to legacy implementation console.log('🧘 📹 Using legacy focus video implementation'); this.startLegacyFocusVideoPlayback(); } /** * Legacy focus video playback (fallback) */ async startLegacyFocusVideoPlayback() { if (this.focusVideoLibrary.length === 0) { console.warn('🧘 ⚠️ No videos available for focus session, hiding video container'); const videoContainer = document.getElementById('focus-video-container'); if (videoContainer) { videoContainer.style.display = 'none'; } return; } this.focusVideoActive = true; this.focusVideoPlayer = document.getElementById('focus-video-player'); if (!this.focusVideoPlayer) { console.error('🧘 ❌ Focus video player element not found'); return; } // Set up video ended event to play next video automatically this.focusVideoPlayer.addEventListener('ended', () => { if (this.focusVideoActive) { console.log('🧘 🎬 Video ended, playing next...'); setTimeout(() => this.playNextFocusVideo(), 500); // Small delay before next video } else { console.log('🧘 📹 Video ended after session completed, not playing next'); } }); // Set up error handling this.focusVideoPlayer.addEventListener('error', (e) => { // Don't handle errors if the focus session is no longer active if (!this.focusVideoActive) { console.log('🧘 📹 Video error after session ended, ignoring'); return; } const error = this.focusVideoPlayer.error; let errorMessage = 'Unknown video error'; if (error) { switch (error.code) { case 1: errorMessage = 'Video loading aborted'; break; case 2: errorMessage = 'Network error'; break; case 3: errorMessage = 'Video decoding error'; break; case 4: errorMessage = 'Video format not supported'; break; default: errorMessage = `Video error code: ${error.code}`; } } console.warn(`🧘 ⚠️ Video error (${errorMessage}), skipping to next video`); if (this.focusVideoActive) { // Update video info to show error const videoInfo = document.getElementById('video-info'); if (videoInfo) { videoInfo.textContent = `Error: ${errorMessage}, loading next...`; } setTimeout(() => this.playNextFocusVideo(), 1000); } }); // Set up loading start this.focusVideoPlayer.addEventListener('loadstart', () => { const videoInfo = document.getElementById('video-info'); if (videoInfo) { videoInfo.textContent = 'Loading video...'; } }); // Start playing the first video this.playNextFocusVideo(); } /** * Check if video format is supported */ isVideoFormatSupported(videoPath) { const extension = videoPath.toLowerCase().split('.').pop(); const supportedFormats = ['mp4', 'webm', 'ogg', 'avi', 'mov', 'mkv']; // Check basic extension support if (!supportedFormats.includes(extension)) { console.warn(`🧘 ⚠️ Potentially unsupported video format: .${extension}`); return false; } // For .mov files, check if browser can play them if (extension === 'mov') { const testVideo = document.createElement('video'); const canPlay = testVideo.canPlayType('video/quicktime'); if (canPlay === '') { console.warn('🧘 ⚠️ Browser may not support .mov files'); return false; } } return true; } /** * Play the next random video in the focus session */ async playNextFocusVideo() { if (!this.focusVideoActive || this.focusVideoLibrary.length === 0) { return; } // Try up to 5 times to find a compatible video let attempts = 0; let videoFile, videoPath; do { // Pick a random video const randomIndex = Math.floor(Math.random() * this.focusVideoLibrary.length); videoFile = this.focusVideoLibrary[randomIndex]; // Get video path (handle both string and object formats) if (typeof videoFile === 'string') { videoPath = videoFile; } else if (videoFile.path) { videoPath = videoFile.path; } else if (videoFile.name) { videoPath = videoFile.name; } else { console.warn('🧘 ⚠️ Invalid video file format:', videoFile); attempts++; continue; } // Check if format is supported if (this.isVideoFormatSupported(videoPath)) { break; // Found a compatible video } attempts++; } while (attempts < 5); if (attempts >= 5) { console.warn('🧘 ⚠️ Could not find compatible video after 5 attempts'); const videoInfo = document.getElementById('video-info'); if (videoInfo) { videoInfo.textContent = 'No compatible videos found'; } return; } try { // Construct full path const fullPath = this.getVideoFilePath(videoPath); console.log(`🧘 🎬 Playing focus video: ${videoPath}`); // Update video info display const videoInfo = document.getElementById('video-info'); if (videoInfo) { const fileName = videoPath.split('/').pop().split('\\').pop(); videoInfo.textContent = `Playing: ${fileName}`; } // Set video source and play this.focusVideoPlayer.src = fullPath; this.focusVideoPlayer.muted = false; // Ensure video is not muted // Get volume from slider if available, otherwise use default const volumeSlider = document.getElementById('focus-video-volume'); let volume = 0.5; // default if (volumeSlider) { volume = parseInt(volumeSlider.value) / 100; } else { volume = window.videoPlayerManager?.settings?.volume || 0.5; } this.focusVideoPlayer.volume = volume; console.log(`🧘 🎬 Video volume set to: ${Math.round(volume * 100)}%`); // Add load event to handle successful loading const handleLoad = () => { const fileName = videoPath.split('/').pop().split('\\').pop(); console.log(`🧘 🎬 Video loaded successfully: ${fileName}`); this.focusVideoPlayer.removeEventListener('loadeddata', handleLoad); }; this.focusVideoPlayer.addEventListener('loadeddata', handleLoad); try { await this.focusVideoPlayer.play(); const fileName = videoPath.split('/').pop().split('\\').pop(); console.log(`🧘 🎬 Video playing: ${fileName}`); } catch (playError) { console.warn('🧘 ⚠️ Could not autoplay video:', playError); // Still might work with user interaction, so don't skip immediately } } catch (error) { console.error('🧘 ❌ Error setting up focus video:', error); // Try another video after a short delay setTimeout(() => { if (this.focusVideoActive) { this.playNextFocusVideo(); } }, 1000); } } /** * Stop focus video playback */ stopFocusVideoPlayback() { console.log('🧘 🎬 Stopping focus video playback...'); // End focus session tracking for XP (if still active) if (this.game && this.game.trackFocusSession) { this.game.trackFocusSession(false); } // Try new FocusVideoPlayer first if (this.focusVideoPlayer && typeof this.focusVideoPlayer.stopFocusSession === 'function') { console.log('🧘 📺 Stopping focus session with FocusVideoPlayer'); this.focusVideoPlayer.stopFocusSession(); return; } // Fallback to legacy implementation this.stopLegacyFocusVideoPlayback(); } /** * Legacy focus video stop (fallback) */ stopLegacyFocusVideoPlayback() { console.log('🧘 📹 Using legacy focus video stop'); this.focusVideoActive = false; if (this.focusVideoPlayer) { // Pause and clear the video this.focusVideoPlayer.pause(); this.focusVideoPlayer.src = ''; this.focusVideoPlayer.load(); // Reset the video element // Update video info const videoInfo = document.getElementById('video-info'); if (videoInfo) { videoInfo.textContent = 'Video session completed'; } // Hide video container const videoContainer = document.getElementById('focus-video-container'); if (videoContainer) { videoContainer.style.display = 'none'; } } console.log('🧘 🎬 Legacy focus video playback stopped'); } /** * Get proper file path for videos */ getVideoFilePath(videoPath) { // If it's already a full path or data URL, use as-is if (videoPath.startsWith('http') || videoPath.startsWith('data:') || videoPath.startsWith('file:')) { return videoPath; } // For desktop app, construct proper file path if (window.electronAPI) { // Handle both forward and back slashes and ensure proper file:// protocol const normalizedPath = videoPath.replace(/\\/g, '/'); // Remove any leading slashes to avoid file://// const cleanPath = normalizedPath.replace(/^\/+/, ''); return `file:///${cleanPath}`; } // For web version, assume relative path in videos directory if (!videoPath.startsWith('videos/')) { return `videos/${videoPath}`; } return videoPath; } async validateFocusTask(task) { const timerDisplay = document.getElementById('focus-timer-display'); const progressText = document.getElementById('progress-text'); console.log(`🧘 Validating focus task...`); console.log(`🧘 Timer display found: ${!!timerDisplay}, text: "${timerDisplay?.textContent}"`); console.log(`🧘 Progress text found: ${!!progressText}, text: "${progressText?.textContent}"`); if (!timerDisplay || !progressText) { console.log(`🧘 ❌ Missing required elements for focus validation`); return false; } // Check if focus session was completed const isComplete = timerDisplay.textContent === 'Complete!' && progressText.textContent.includes('✓'); console.log(`🧘 Focus task validation result: ${isComplete ? '✅ VALID' : '❌ INVALID'}`); return isComplete; } // ============================================================================= // SCENARIO BUILDER IMPLEMENTATION // ============================================================================= /** * Create a choose your own adventure scenario task */ async createScenarioTask(task, container) { const scenario = task.interactiveData || {}; container.innerHTML = `

📖 ${scenario.title || 'Interactive Scenario'}

Step 1 of ${this.getScenarioStepCount(scenario)}
`; // Initialize scenario state this.initializeScenario(task, scenario); } initializeScenario(task, scenario) { // Safety check for scenario data if (!scenario) { console.error('❌ Cannot initialize scenario - scenario data is undefined for task:', task.id); console.error('Task object:', task); return; } if (!scenario.steps) { console.error('❌ Cannot initialize scenario - no steps found in scenario for task:', task.id); console.error('Scenario object:', scenario); return; } // Scenario state tracking task.scenarioState = { currentStep: 'start', stepNumber: 1, totalSteps: this.getScenarioStepCount(scenario), choices: [], completed: false, outcome: null }; // Start the scenario this.displayScenarioStep(task, scenario, 'start'); // Setup TTS controls this.setupTTSControls(); } /** * Setup TTS control event handlers */ setupTTSControls() { const ttsToggle = document.getElementById('tts-toggle'); const ttsStop = document.getElementById('tts-stop'); const ttsInfo = document.getElementById('tts-info'); // TTS toggle button if (ttsToggle) { ttsToggle.addEventListener('click', () => { const newState = this.toggleTTS(); ttsToggle.textContent = `🎤 ${newState ? 'TTS On' : 'TTS Off'}`; ttsToggle.className = `tts-control ${newState ? 'enabled' : 'disabled'}`; this.updateTTSInfo(); }); } // TTS stop button if (ttsStop) { ttsStop.addEventListener('click', () => { this.stopTTS(); }); } // Display current TTS info this.updateTTSInfo(); } /** * Update TTS information display */ updateTTSInfo() { const ttsInfo = document.getElementById('tts-info'); if (!ttsInfo) return; const info = this.getTTSInfo(); if (info.available) { const voiceInfo = info.voiceInfo; ttsInfo.innerHTML = `🎤 Voice: ${voiceInfo.name} (${voiceInfo.platform})`; } else { ttsInfo.innerHTML = '🎤 TTS not available'; } } displayScenarioStep(task, scenario, stepId) { // Add scenario styles when displaying scenario steps this.addScenarioStyles(); const step = scenario.steps[stepId]; if (!step) { console.error('Scenario step not found:', stepId); return; } // Stop any previous TTS narration before displaying new step if (this.voiceManager) { this.stopTTS(); } // Check if scenario interface elements exist (safety check) const storyEl = document.getElementById('scenario-story'); const choicesEl = document.getElementById('scenario-choices'); const stepEl = document.getElementById('scenario-step'); if (!storyEl || !choicesEl || !stepEl) { console.warn('Scenario interface not available - cannot display step:', stepId); console.log('Available elements:', { storyEl: !!storyEl, choicesEl: !!choicesEl, stepEl: !!stepEl }); // If this is an ending step and interface is gone, complete the task instead if (step.type === 'ending') { console.log('🎭 Ending step reached with no UI - completing task'); if (this.game && this.game.gameState && this.game.gameState.isRunning) { this.cleanupInteractiveTask(); this.game.completeTask(); } } return; } // Update step counter stepEl.textContent = task.scenarioState.stepNumber; // Display story text with dynamic content const storyText = this.processScenarioText(step.story, task.scenarioState); storyEl.innerHTML = `
${storyText}
`; // Speak the story text using TTS this.speakScenarioText(step.story, task.scenarioState, { onComplete: () => { // Story narration complete - could add visual feedback here if needed } }); // Clear previous choices choicesEl.innerHTML = ''; if (step.type === 'choice') { // Display choices step.choices.forEach((choice, index) => { const isAvailable = this.isChoiceAvailable(choice, task.scenarioState); const choiceBtn = document.createElement('button'); choiceBtn.className = `scenario-choice ${choice.type || 'normal'} ${!isAvailable ? 'disabled' : ''}`; choiceBtn.disabled = !isAvailable; choiceBtn.innerHTML = `
${choice.text}
${choice.preview || ''}
${choice.requirements ? `
${choice.requirements}
` : ''} `; if (isAvailable) { choiceBtn.addEventListener('click', () => { this.makeScenarioChoice(task, scenario, choice, index); }); } choicesEl.appendChild(choiceBtn); }); } else if (step.type === 'action') { console.log('📸 Action step detected, checking if photography step...'); // Check if this is a photography task first const isPhotoStep = this.isPhotographyStep(step); const hasWebcamManager = !!this.game.webcamManager; console.log('📸 Is photography step:', isPhotoStep, 'Has webcam manager:', hasWebcamManager); if (isPhotoStep && hasWebcamManager) { console.log('📸 Creating CAMERA BUTTON for photography step'); // For photography tasks, show camera button instead of timer const cameraBtn = document.createElement('button'); cameraBtn.className = 'scenario-camera-btn'; // Get dynamic photo requirements const photoRequirements = this.getPhotoRequirements(step); console.log('📸 Photo requirements for button:', photoRequirements); cameraBtn.innerHTML = `
${step.actionText || 'Take Photos'}
${photoRequirements.description}
`; cameraBtn.addEventListener('click', () => { console.log('📸 Camera button clicked!'); this.startPhotographySession(task, scenario, step); }); choicesEl.appendChild(cameraBtn); console.log('📸 Camera button added to DOM'); } else { console.log('📸 Creating ACTION BUTTON for non-photography step'); // For non-photography tasks, show regular action button with timer const actionBtn = document.createElement('button'); actionBtn.className = 'scenario-action'; actionBtn.innerHTML = `
${step.actionText}
${step.duration || 30}s
`; actionBtn.addEventListener('click', () => { this.startScenarioAction(task, scenario, step); }); choicesEl.appendChild(actionBtn); } } else if (step.type === 'mirror-action') { // Handle mirror-based action steps - create the mirror task directly this.createMirrorTask(task, choicesEl); } else if (step.type === 'text') { // Handle text-only steps that just need a continue button const continueBtn = document.createElement('button'); continueBtn.className = 'scenario-continue'; continueBtn.innerHTML = `
Continue
`; continueBtn.addEventListener('click', () => { const nextStep = step.next || step.nextStep || this.getDefaultNextStep(task.scenarioState); task.scenarioState.currentStep = nextStep; task.scenarioState.stepNumber++; this.displayScenarioStep(task, scenario, nextStep); }); choicesEl.appendChild(continueBtn); } else if (step.type === 'ending') { // Display ending and completion task.scenarioState.completed = true; task.scenarioState.outcome = step.outcome; // Stop any ongoing TTS narration when scenario ends if (this.voiceManager) { this.stopTTS(); } const endingDiv = document.createElement('div'); endingDiv.className = 'scenario-ending'; endingDiv.innerHTML = `
${this.processScenarioText(step.endingText, task.scenarioState)}
${this.getOutcomeText(step.outcome)}
`; choicesEl.appendChild(endingDiv); // Enable completion button setTimeout(() => { this.enableInteractiveControls(true); }, 2000); } // Apply scenario effects (audio, visual effects, etc.) this.applyScenarioEffects(step, task.scenarioState); } makeScenarioChoice(task, scenario, choice, choiceIndex) { console.log(`🎯 makeScenarioChoice called: "${choice.text}" (choice ${choiceIndex})`); console.log(`🎯 Current step: ${task.scenarioState.currentStep}, Step #${task.scenarioState.stepNumber}`); // Record the choice task.scenarioState.choices.push({ step: task.scenarioState.currentStep, choice: choiceIndex, text: choice.text }); // Apply choice effects this.applyChoiceEffects(choice, task.scenarioState); console.log(`🧘 Checking for focus interruption before proceeding to: ${choice.nextStep || 'default'}`); // Check for focus-hold interruption if (this.shouldTriggerFocusInterruption()) { console.log(`🧘 ⚡ INTERRUPTION TRIGGERED! Pausing scenario progress.`); this.triggerFocusInterruption(task, scenario, choice); return; // Don't proceed to next step yet } console.log(`🎯 No interruption - proceeding to next step`); // Move to next step const nextStep = choice.nextStep || this.getDefaultNextStep(task.scenarioState); task.scenarioState.currentStep = nextStep; task.scenarioState.stepNumber++; console.log(`🎯 Moving to step: ${nextStep} (Step #${task.scenarioState.stepNumber})`); // Display next step this.displayScenarioStep(task, scenario, nextStep); } startScenarioAction(task, scenario, step) { const duration = step.duration || 30; const timerEl = document.getElementById('action-timer'); let timeLeft = duration; // Disable the action button const actionBtn = document.querySelector('.scenario-action'); actionBtn.disabled = true; actionBtn.classList.add('active'); const countdown = setInterval(() => { timeLeft--; timerEl.textContent = `${timeLeft}s`; if (timeLeft <= 0) { clearInterval(countdown); // Action completed actionBtn.classList.remove('active'); actionBtn.classList.add('completed'); // Apply action effects this.applyActionEffects(step, task.scenarioState); // Move to next step setTimeout(() => { const nextStep = step.nextStep || step.next || this.getDefaultNextStep(task.scenarioState); task.scenarioState.currentStep = nextStep; task.scenarioState.stepNumber++; this.displayScenarioStep(task, scenario, nextStep); }, 1500); } }, 1000); } startScenarioMirrorAction(task, scenario, step) { console.log('🪞 Starting scenario mirror action'); // Create mirror interface for scenario step const choicesEl = document.getElementById('scenario-choices'); if (!choicesEl) return; // Clear existing content choicesEl.innerHTML = ''; // Create mirror task interface const mirrorContainer = document.createElement('div'); mirrorContainer.className = 'scenario-mirror-container'; mirrorContainer.innerHTML = `

🪞 Mirror Session

${step.story || 'Complete the mirror task'}

${step.mirrorInstructions || 'Look at yourself while completing the task'}
${step.mirrorTaskText || ''}
`; choicesEl.appendChild(mirrorContainer); // Add click handler for mirror button const mirrorBtn = document.getElementById('scenario-mirror-btn'); mirrorBtn.addEventListener('click', async () => { console.log('🪞 Opening webcam for scenario mirror task'); // Disable button while starting mirrorBtn.disabled = true; mirrorBtn.textContent = '🪞 Opening...'; try { // Use webcam manager to start mirror mode if (this.game && this.game.webcamManager) { await this.game.webcamManager.startMirrorMode(); // Start countdown timer with slight delay to ensure overlay is ready setTimeout(() => { this.startMirrorTimer(task, scenario, step); }, 100); } else { console.error('Webcam manager not available'); mirrorBtn.disabled = false; mirrorBtn.textContent = '🪞 Try Again'; } } catch (error) { console.error('Failed to start mirror mode:', error); mirrorBtn.disabled = false; mirrorBtn.textContent = '🪞 Try Again'; } }); } startMirrorTimer(task, scenario, step) { const duration = step.duration || 60; console.log('🪞 Starting mirror timer for', duration, 'seconds'); // Look for timer elements in the mirror overlay first const overlay = document.getElementById('mirror-overlay'); let timerEl, timeEl, progressBar; if (overlay) { timerEl = overlay.querySelector('#mirror-timer'); timeEl = overlay.querySelector('#mirror-time'); progressBar = overlay.querySelector('#mirror-progress-bar'); console.log('🪞 Found overlay elements:', {timerEl: !!timerEl, timeEl: !!timeEl, progressBar: !!progressBar}); } // Fallback to scenario elements if overlay elements not found if (!timerEl || !timeEl) { timerEl = document.getElementById('mirror-timer'); timeEl = document.getElementById('mirror-time'); progressBar = document.getElementById('mirror-progress-bar'); console.log('🪞 Using fallback elements:', {timerEl: !!timerEl, timeEl: !!timeEl, progressBar: !!progressBar}); } if (!timerEl || !timeEl) { console.error('Timer elements not found in overlay or scenario'); return; } // Show timer and progress bar timerEl.style.display = 'block'; let timeLeft = duration; // Update time display timeEl.textContent = timeLeft; // Style and reset the progress bar if found if (progressBar) { progressBar.style.cssText = ` width: 0%; height: 100%; background: linear-gradient(90deg, #8a2be2, #9c27b0); border-radius: 4px; transition: width 1s ease; `; // Style the progress container const progressContainer = progressBar.parentElement; if (progressContainer) { progressContainer.style.cssText = ` width: 100%; background: rgba(255, 255, 255, 0.1); border-radius: 4px; margin: 10px 0; height: 8px; overflow: hidden; `; } } // Update scenario button state if it exists const mirrorBtn = document.getElementById('scenario-mirror-btn'); if (mirrorBtn) { mirrorBtn.textContent = '🪞 Mirror Active'; mirrorBtn.disabled = true; } // Prevent camera closure during session if (this.game && this.game.webcamManager) { this.game.webcamManager.preventClose = true; } const countdown = setInterval(() => { timeLeft--; timeEl.textContent = timeLeft; // Update progress bar if (progressBar) { const elapsed = duration - timeLeft; const progressPercent = (elapsed / duration) * 100; progressBar.style.width = `${progressPercent}%`; } if (timeLeft <= 0) { clearInterval(countdown); // Timer completed - allow camera closure if (this.game && this.game.webcamManager) { this.game.webcamManager.preventClose = false; } // Update progress bar to complete if (progressBar) { progressBar.style.width = '100%'; progressBar.style.background = '#4caf50'; } // Timer completed mirrorBtn.textContent = '🪞 Completed'; mirrorBtn.classList.add('completed'); timeEl.textContent = 'Complete!'; // Apply step effects this.applyActionEffects(step, task.scenarioState); // Move to next step setTimeout(() => { const nextStep = step.next || step.nextStep || this.getDefaultNextStep(task.scenarioState); task.scenarioState.currentStep = nextStep; task.scenarioState.stepNumber++; this.displayScenarioStep(task, scenario, nextStep); }, 1500); } }, 1000); } startStandaloneMirrorTimer(task, container) { const duration = task.duration || 60; const timerSection = container.querySelector('#mirror-timer-section'); const progressBar = container.querySelector('#standalone-mirror-progress-bar'); const timeDisplay = container.querySelector('#standalone-mirror-time'); const startBtn = container.querySelector('#start-mirror-btn'); if (!timerSection || !progressBar || !timeDisplay) return; // Show timer section timerSection.style.display = 'block'; let timeLeft = duration; // Style the progress bar progressBar.style.cssText = ` width: 0%; height: 10px; background: linear-gradient(90deg, #8a2be2, #9c27b0); border-radius: 5px; transition: width 1s ease; `; // Style the progress container const progressContainer = progressBar.parentElement; progressContainer.style.cssText = ` width: 100%; background: rgba(255, 255, 255, 0.1); border-radius: 5px; margin: 20px 0; border: 1px solid rgba(138, 43, 226, 0.3); `; // Style the timer display timeDisplay.style.cssText = ` font-size: 36px; font-weight: bold; color: #8a2be2; margin: 15px 0; `; // Update button state startBtn.textContent = '🪞 Mirror Active'; startBtn.disabled = true; startBtn.style.opacity = '0.6'; // Prevent camera closure during session if (this.game && this.game.webcamManager) { this.game.webcamManager.preventClose = true; } const countdown = setInterval(() => { timeLeft--; timeDisplay.textContent = `${timeLeft}s`; // Update progress bar const elapsed = duration - timeLeft; const progressPercent = (elapsed / duration) * 100; progressBar.style.width = `${progressPercent}%`; if (timeLeft <= 0) { clearInterval(countdown); // Timer completed - allow camera closure if (this.game && this.game.webcamManager) { this.game.webcamManager.preventClose = false; } // Update progress bar to complete progressBar.style.width = '100%'; progressBar.style.background = '#4caf50'; // Timer completed timeDisplay.textContent = 'Complete!'; timeDisplay.style.color = '#4caf50'; // Update timer label const timerLabel = container.querySelector('.timer-label'); if (timerLabel) { timerLabel.textContent = '✓ Mirror session completed successfully!'; timerLabel.style.color = '#4caf50'; } console.log('🪞 Standalone mirror task completed'); // Complete the task setTimeout(() => { this.completeMirrorTask(task); }, 1500); } }, 1000); } applyChoiceEffects(choice, state) { // Effects system removed } applyActionEffects(step, state) { // Effects system removed } applyScenarioEffects(step, state) { // Could add visual effects, audio changes, etc. based on step properties if (step.bgColor) { document.querySelector('.interactive-task-container').style.background = step.bgColor; } } // Scenario stats system removed - counters no longer used getOutcomeText(outcome) { const outcomes = { 'success': '🎉 Scenario Completed Successfully!', 'partial': '⚡ Partially Successful', 'failure': '🔥 Overwhelmed by Desire', 'denied': '🚫 Denied and Frustrated', 'reward': '🏆 Earned a Special Reward', 'punishment': '⛓️ Earned Punishment' }; return outcomes[outcome] || 'Scenario Complete'; } isChoiceAvailable(choice, state) { // Counter-based conditions removed - all choices are available return true; } processScenarioText(text, state) { // Counter system removed - return text as-is return text .replace(/\{arousal\}/g, 'your state') .replace(/\{control\}/g, 'your focus') .replace(/\{intensity\}/g, 'the level'); } /** * Speak scenario text using TTS with cross-platform voice */ speakScenarioText(text, state, options = {}) { if (!this.ttsEnabled || !text) { return; } // Process the text to replace placeholders const processedText = this.processScenarioText(text, state); // Clean up HTML tags and excessive whitespace for better speech const cleanText = processedText .replace(/<[^>]*>/g, ' ') // Remove HTML tags .replace(/\s+/g, ' ') // Collapse whitespace .trim(); if (!cleanText) { return; } // Default TTS settings optimized for scenario narration const ttsOptions = { rate: options.rate || 0.9, // Slightly slower for dramatic effect pitch: options.pitch || 1.1, // Slightly higher for feminine voice volume: options.volume || 0.8, // Not too loud onStart: () => { // Optional: could add subtle visual indicator }, onEnd: () => { if (options.onComplete) options.onComplete(); }, onError: (error) => { console.warn('TTS error:', error.message || error); if (options.onError) options.onError(error); } }; // Speak the text this.voiceManager.speak(cleanText, ttsOptions); } /** * Stop any current TTS playback */ stopTTS() { if (this.voiceManager) { this.voiceManager.stop(); } } /** * Toggle TTS on/off */ toggleTTS() { this.ttsEnabled = !this.ttsEnabled; if (!this.ttsEnabled) { this.stopTTS(); } return this.ttsEnabled; } /** * Get current TTS voice information */ getTTSInfo() { if (!this.voiceManager) { return { available: false, reason: 'Voice manager not initialized' }; } return { available: this.ttsEnabled, voiceInfo: this.voiceManager.getVoiceInfo(), supported: this.voiceManager.isSupported() }; } getScenarioStepCount(scenario) { return Object.keys(scenario.steps || {}).length; } /** * Check if a focus interruption should be triggered */ shouldTriggerFocusInterruption() { const chance = window.game?.gameState?.focusInterruptionChance || 0; console.log(`🧘 Focus interruption chance setting: ${chance}%`); if (chance <= 0) { console.log(`🧘 Focus interruption disabled (chance is ${chance}%)`); return false; } const random = Math.random() * 100; const shouldInterrupt = random < chance; console.log(`🧘 Focus interruption check: ${random.toFixed(1)}% vs ${chance}% = ${shouldInterrupt ? 'INTERRUPT' : 'continue'}`); return shouldInterrupt; } /** * Trigger a focus-hold interruption during scenario */ triggerFocusInterruption(task, scenario, pendingChoice) { console.log('🧘 ⚡ === FOCUS INTERRUPTION TRIGGERED ==='); console.log(`🧘 Scenario: ${scenario.id || scenario.title || 'unknown'}, Current Step: ${task.scenarioState.currentStep}`); console.log(`🧘 Pending Choice: "${pendingChoice.text}" -> ${pendingChoice.nextStep}`); // Show brief notification this.showFeedback('info', '⚡ Focus interruption triggered! Prepare to concentrate...', 2000); // Store the scenario state for resumption const resumeData = { task: task, scenario: scenario, pendingChoice: pendingChoice, originalContainer: this.currentContainer }; // Store resume data for later this.scenarioResumeData = resumeData; console.log(`🧘 Stored resume data for later:`, resumeData); // Create interruption message const container = document.getElementById('interactive-task-container'); if (!container) { console.error('🧘 ❌ Interactive task container not found - cannot show interruption'); return; } console.log(`🧘 Found container, showing focus interruption interface`); container.innerHTML = `

🧘 Focus Interruption

Your mind needs to focus. Complete this concentration task before continuing your adventure...

`; // Create a focus-hold task const focusTask = this.createFocusInterruptionTask(); // Set this as the current interactive task so validation works this.currentInteractiveTask = focusTask; console.log(`🧘 Set focus interruption as current task:`, focusTask); // Insert focus task into the interruption content setTimeout(() => { const focusContainer = container.querySelector('#focus-interruption-content'); if (focusContainer) { this.createFocusTask(focusTask, focusContainer); } }, 100); } /** * Create a focus-hold task for interruption */ createFocusInterruptionTask() { const durations = [30, 45, 60, 90]; // Different durations in seconds const instructions = [ 'Hold the position and maintain focus on the screen', 'Focus on your breathing and stay perfectly still', 'Clear your mind and concentrate on the task at hand', 'Hold steady and let your arousal build without release', 'Maintain perfect focus while staying on the edge' ]; return { id: 'focus-interruption', text: '🧘 Focus Training Interruption', story: '⚡ Your adventure is interrupted by a moment requiring deep concentration. Complete this focus challenge to continue your journey.', instructions: instructions[Math.floor(Math.random() * instructions.length)], duration: durations[Math.floor(Math.random() * durations.length)], interactiveType: 'focus-hold', isInterruption: true }; } /** * Resume scenario after focus interruption */ resumeScenarioAfterInterruption() { console.log('🧘 === RESUMING SCENARIO AFTER INTERRUPTION ==='); if (!this.scenarioResumeData) { console.warn('⚠️ No scenario resume data found'); return; } console.log('🧘 Found resume data:', this.scenarioResumeData); // Show success message this.showFeedback('success', '✨ Focus completed! Returning to your adventure...', 2000); const { task, scenario, pendingChoice } = this.scenarioResumeData; console.log(`🧘 Resuming scenario: ${scenario.id}`); console.log(`🧘 Continuing with choice: "${pendingChoice.text}" -> ${pendingChoice.nextStep}`); // Small delay to show the success message before continuing setTimeout(() => { // First, restore the scenario interface since interruption replaced it console.log(`🧘 Restoring scenario interface...`); const container = document.getElementById('interactive-task-container'); if (container) { console.log(`🧘 Container found, calling createScenarioTask...`); this.createScenarioTask(task, container); // Wait a moment for the interface to be created setTimeout(() => { console.log(`🧘 Checking if scenario elements are now available...`); const storyEl = document.getElementById('scenario-story'); const choicesEl = document.getElementById('scenario-choices'); const stepEl = document.getElementById('scenario-step'); console.log(`🧘 Elements found: story=${!!storyEl}, choices=${!!choicesEl}, step=${!!stepEl}`); // Move to next step (the original choice processing) const nextStep = pendingChoice.nextStep || this.getDefaultNextStep(task.scenarioState); task.scenarioState.currentStep = nextStep; task.scenarioState.stepNumber++; console.log(`🧘 ✅ Scenario resumed - moved to step: ${nextStep} (Step #${task.scenarioState.stepNumber})`); // Display next step this.displayScenarioStep(task, scenario, nextStep); // Clear resume data this.scenarioResumeData = null; console.log(`🧘 Resume data cleared - scenario flow restored`); }, 100); } else { console.error(`🧘 ❌ Container not found for scenario restoration`); } }, 1000); } getDefaultNextStep(state) { // Simple logic to determine next step based on state // Can be overridden by specific choice nextStep properties return `step${state.stepNumber + 1}`; } /** * Validate scenario task completion */ async validateScenarioTask(task) { console.log('🔍 ValidateScenarioTask called with task:', task); console.log('🔍 Task scenarioState:', task.scenarioState); console.log('🔍 Task scenarioState.completed:', task.scenarioState?.completed); // Check if scenario is marked as completed if (task.scenarioState && task.scenarioState.completed) { console.log('✅ Scenario task validation: PASSED (completed = true)'); return true; } // For photography steps, check if photos have been taken const currentStep = task.scenarioState?.currentStep; if (currentStep && task.interactiveData?.steps) { const step = task.interactiveData.steps[currentStep]; if (step && this.isPhotographyStep(step)) { console.log('📸 Validating photography step completion'); // Photography steps are considered complete when photos are taken // The webcam manager should have already handled completion return true; } } console.log('❌ Scenario task validation: FAILED'); return false; } /** * Create mirror task - opens webcam for self-viewing */ async createMirrorTask(task, container) { console.log('🪞 Creating mirror task'); // Create mirror task interface container.innerHTML = `

🪞 Mirror Task

${task.story || 'Use the webcam to look at yourself and complete the task'}

${task.mirrorInstructions || task.story || 'Complete the task while looking at yourself'}
${task.mirrorTaskText || ''}
`; // Style the mirror task const mirrorContainer = container.querySelector('.mirror-task-container'); // Add event listener for the start button const startBtn = container.querySelector('#start-mirror-btn'); // Add hover effect startBtn.addEventListener('mouseenter', () => { startBtn.style.transform = 'translateY(-2px) scale(1.05)'; startBtn.style.boxShadow = '0 5px 15px rgba(138, 43, 226, 0.5)'; }); startBtn.addEventListener('mouseleave', () => { startBtn.style.transform = 'translateY(0) scale(1)'; startBtn.style.boxShadow = '0 3px 10px rgba(138, 43, 226, 0.3)'; }); // Add click handler to start mirror mode startBtn.addEventListener('click', () => { if (this.game.webcamManager) { const mirrorData = { instructions: task.mirrorInstructions || 'Complete your task while looking at yourself', taskText: task.mirrorTaskText || task.story, task: task }; // Start mirror mode this.game.webcamManager.startMirrorMode(mirrorData); // Start the timer and progress bar this.startStandaloneMirrorTimer(task, container); } else { console.warn('Webcam not available - falling back to regular task completion'); if (this.game && this.game.showNotification) { this.game.showNotification('Webcam not available. Task completed automatically.', 'warning'); } setTimeout(() => { this.completeMirrorTask(task); }, 2000); } }); return true; } /** * Validate mirror task completion */ async validateMirrorTask(task) { return true; // Mirror tasks are completed when webcam mirror is closed } /** * Check if a scenario step involves photography */ isPhotographyStep(step) { if (!step) return false; // Check for explicit photography step markers if (step.type === 'action' && step.duration === 0) { console.log('📸 Photography step detected: explicit 0 duration'); return true; // Steps with 0 duration are photo steps } // For dress-up scenarios, all action steps should be photography steps const task = this.game?.gameState?.currentTask; console.log('📸 Checking photography step detection for task:', task?.id, 'step type:', step.type); if (task && task.id === 'scenario-dress-up-photo') { const isAction = step.type === 'action'; console.log('📸 Dress-up scenario detected, is action step:', isAction); return isAction; } const photoKeywords = ['photo', 'photograph', 'camera', 'picture', 'pose', 'submissive_photo', 'dress.*photo']; const textToCheck = `${step.actionText || ''} ${step.story || ''}`.toLowerCase(); console.log('📸 Keyword detection - text to check:', textToCheck); const result = photoKeywords.some(keyword => { if (keyword.includes('.*')) { // Handle regex patterns const regex = new RegExp(keyword); return regex.test(textToCheck); } return textToCheck.includes(keyword); }); console.log('📸 Photography step detection result:', result); return result; } /** * Start photography session for scenario step */ async startPhotographySession(task, scenario, step) { console.log('📸 Starting photography session for scenario step'); console.log('📸 Task:', task?.id, 'Step:', step?.nextStep || 'no-next-step'); console.log('📸 Step details:', { type: step?.type, actionText: step?.actionText }); // Determine photo requirements from step const photoRequirements = this.getPhotoRequirements(step); console.log('📸 Photo requirements:', photoRequirements); const sessionType = this.getSessionTypeFromStep(step); console.log('📸 Session type:', sessionType); // Set up event listener for photo session completion const handlePhotoCompletion = (event) => { console.log('📸 EVENT TRIGGERED - Photo session completed - handling scenario progression'); console.log('📸 Event detail:', event.detail); console.log('📸 Task state:', { id: task?.id, stepNumber: task?.scenarioState?.stepNumber }); console.log('📸 Current step:', task?.scenarioState?.currentStep); // Remove this specific event listener document.removeEventListener('photoSessionComplete', handlePhotoCompletion); // Progress to next scenario step console.log('📸 About to handle photography step completion'); this.handlePhotographyStepCompletion(task, scenario, step); }; // Add the event listener document.addEventListener('photoSessionComplete', handlePhotoCompletion); console.log('📸 Event listener set up, starting photo session...'); const success = await this.game.webcamManager.startPhotoSessionWithProgress(sessionType, { task: task, scenario: scenario, step: step, requirements: photoRequirements }); console.log('📸 Photo session start result:', success); if (!success) { console.log('📸 Camera not available - removing event listener'); document.removeEventListener('photoSessionComplete', handlePhotoCompletion); this.showFeedback('error', 'Camera not available. Please complete the task manually.'); } } /** * Handle completion of a photography step in scenario */ handlePhotographyStepCompletion(task, scenario, step) { console.log('📸 ENTERING handlePhotographyStepCompletion'); console.log('📸 Task:', task?.id); console.log('📸 Step:', step); console.log('📸 Step nextStep:', step?.nextStep); console.log('📸 Current scenario state:', task?.scenarioState); // Check if this step has a specific nextStep if (step.nextStep) { console.log(`📸 Auto-progressing to next step: ${step.nextStep}`); console.log('📸 Setting up auto-progression with 1500ms delay...'); // Add a small delay to show completion feedback setTimeout(() => { console.log('📸 TIMEOUT EXECUTING - updating scenario state'); console.log('📸 Previous step:', task.scenarioState.currentStep); console.log('📸 Previous step number:', task.scenarioState.stepNumber); task.scenarioState.currentStep = step.nextStep; task.scenarioState.stepNumber++; console.log('📸 New step:', task.scenarioState.currentStep); console.log('📸 New step number:', task.scenarioState.stepNumber); console.log('📸 About to call displayScenarioStep...'); this.displayScenarioStep(task, scenario, step.nextStep); console.log('📸 displayScenarioStep called'); }, 1500); } else { console.log('📸 No nextStep found - enabling manual progression'); console.log('📸 Photo step completed - enabling manual progression'); // Show completion message and enable manual progression this.showFeedback('success', '📸 Photo task completed! Click Complete to continue.'); // Enable the complete button for manual progression const completeBtn = document.getElementById('interactive-complete-btn'); console.log('📸 Looking for complete button:', !!completeBtn); if (completeBtn) { completeBtn.disabled = false; completeBtn.style.display = 'block'; console.log('📸 Complete button enabled for manual progression'); } else { console.log('📸 WARNING: Complete button not found!'); } } // Show success feedback this.showFeedback('success', '📸 Photo session completed successfully!'); } /** * Get photo requirements from step */ getPhotoRequirements(step) { // First check if step has explicit photoCount property if (step.photoCount) { return { count: step.photoCount, description: `Take ${step.photoCount} photos to complete this task` }; } // Check if this is a scenario with dynamic photo count based on state if (this.currentInteractiveTask && this.currentInteractiveTask.scenarioState) { const state = this.currentInteractiveTask.scenarioState; const photoCount = this.calculateDynamicPhotoCount(state, step); return { count: photoCount, description: `Take ${photoCount} photos to complete this task` }; } // Otherwise parse step content to determine how many photos are needed const actionText = step.actionText?.toLowerCase() || ''; const story = step.story?.toLowerCase() || ''; // Look for photo count hints in the text if (actionText.includes('series') || story.includes('multiple') || story.includes('several')) { return { count: 3, description: 'Take 3 photos in different poses' }; } else if (actionText.includes('photo session') || story.includes('photo shoot')) { return { count: 5, description: 'Complete photo session (5 photos)' }; } else { return { count: 1, description: 'Take 1 photo to complete task' }; } } /** * Calculate dynamic photo count based on scenario state */ calculateDynamicPhotoCount(state, step) { // Counter system removed // Counter system removed // Base photo count let photoCount = 1; // Increase photo count based on arousal level photoCount += 1; // Static photo count // Adjust based on control level if (control <= 20) { photoCount += 2; // Very low control = need more photos for submission } else if (control <= 40) { photoCount += 1; // Low control = need extra photos } // Cap at reasonable limits photoCount = Math.max(1, Math.min(6, photoCount)); console.log(`📸 Dynamic photo count: ${photoCount}`); return photoCount; } /** * Get session type from scenario step */ getSessionTypeFromStep(step) { const content = `${step.actionText || ''} ${step.story || ''}`.toLowerCase(); if (content.includes('submissive') || content.includes('pose')) { return 'submissive_poses'; } else if (content.includes('dress') || content.includes('costume')) { return 'dress_up_photos'; } else if (content.includes('humil') || content.includes('degrad')) { return 'humiliation_photos'; } else { return 'general_photography'; } } /** * Offer webcam option for photography tasks */ async offerWebcamOption(task) { return new Promise((resolve) => { // Create modal dialog const modal = document.createElement('div'); modal.id = 'webcam-offer-modal'; modal.innerHTML = ` `; document.body.appendChild(modal); this.addWebcamModalStyles(); // Handle button clicks document.getElementById('use-webcam-btn').addEventListener('click', () => { modal.remove(); resolve(true); }); document.getElementById('skip-webcam-btn').addEventListener('click', () => { modal.remove(); resolve(false); }); }); } /** * Start webcam photo session for photography task */ async startWebcamPhotoSession(task) { console.log('📸 Starting webcam photo session for task:', task.interactiveType); const sessionType = this.game.webcamManager.getSessionTypeFromTask(task); const success = await this.game.webcamManager.startPhotoSession(sessionType, task); if (success) { console.log('✅ Webcam photo session started successfully'); return true; } else { console.log('❌ Webcam photo session failed to start, falling back to text mode'); // Fall back to regular interactive task display return this.displayRegularInteractiveTask(task); } } /** * Handle photo session completion (multiple photos) */ handlePhotoSessionCompletion(detail) { console.log('🎉 Photo session completion handled:', detail.sessionType, `(${detail.photos.length} photos)`); // Show task completion message if (this.game.showNotification) { this.game.showNotification(`Photography completed! ${detail.photos.length} photos taken 📸`, 'success', 3000); } // Progress the scenario to next step (don't end the game) if (this.currentInteractiveTask && detail.taskData) { const { task, scenario, step } = detail.taskData; // Apply step effects based on photo count (more photos = more arousal/less control) if (step.effects) { this.applyStepEffects(step.effects, task.scenarioState, detail.photos.length); } // Move to next step const nextStep = step.nextStep || 'completion'; setTimeout(() => { this.displayScenarioStep(task, scenario, nextStep); }, 1000); } } /** * Apply step effects with photo count modifiers */ applyStepEffects(effects, state, photoCount = 1) { // Effects system removed } /** * Handle photo completion from webcam (single photo - keep for compatibility) */ handlePhotoCompletion(detail) { console.log('📸 Photo completion handled:', detail.sessionType); // Show task completion message if (this.game.showNotification) { this.game.showNotification('Photography task completed! 📸', 'success', 3000); } // Mark current task as completed after a delay (but don't use completeInteractiveTask to avoid container issues) setTimeout(() => { // Check if game is still running before completing if (this.currentInteractiveTask && this.game.gameState.isRunning) { this.cleanupInteractiveTask(); this.game.completeTask(); } }, 2000); } /** * Resume from camera back to task */ resumeFromCamera() { console.log('📱 Resuming from camera to task interface'); // Check if game is still running if (!this.game.gameState.isRunning) { console.log('Resume from camera cancelled - game no longer running'); return; } // Show completion message and progress to next task if (this.currentInteractiveTask) { // Mark task as completed without trying to show feedback in missing containers this.cleanupInteractiveTask(); // Show success notification through game system instead if (this.game && this.game.showNotification) { this.game.showNotification('Photography task completed successfully! 📸', 'success', 3000); } // Complete the task directly setTimeout(() => { // Double-check game is still running if (this.game.gameState.isRunning) { this.game.completeTask(); } }, 1000); } } /** * Complete mirror task from webcam */ completeMirrorTask(taskData) { console.log('🪞 Mirror task completed through webcam'); // Check if game is still running if (!this.game.gameState.isRunning) { console.log('Mirror task completion cancelled - game no longer running'); return; } // Show completion message if (this.game && this.game.showNotification) { this.game.showNotification('Mirror task completed! 🪞', 'success', 3000); } // Clean up any interactive task if (this.currentInteractiveTask) { this.cleanupInteractiveTask(); } // Complete the task setTimeout(() => { if (this.game.gameState.isRunning) { this.game.completeTask(); } }, 1000); } /** * Handle mirror task abandonment */ abandonMirrorTask() { console.log('❌ Mirror task abandoned by user'); // Check if game is still running if (!this.game.gameState.isRunning) { console.log('Mirror task abandonment cancelled - game no longer running'); return; } // Show abandonment message if (this.game && this.game.showNotification) { this.game.showNotification('Mirror task abandoned. You gave up like a quitter! 😤', 'error', 4000); } // Clean up any interactive task if (this.currentInteractiveTask) { this.cleanupInteractiveTask(); } // Mark as failure and end task setTimeout(() => { if (this.game.gameState.isRunning) { // Add penalty points or mark as failure if (this.game.gameState.stats) { this.game.gameState.stats.failures = (this.game.gameState.stats.failures || 0) + 1; this.game.gameState.stats.abandonments = (this.game.gameState.stats.abandonments || 0) + 1; } // End task as failure this.game.failTask('Mirror task abandoned'); } }, 1000); } /** * Display regular interactive task (fallback from webcam) */ async displayRegularInteractiveTask(task) { // Continue with regular interactive task display // (This is the original displayInteractiveTask logic) // Handle task image for interactive tasks const taskImage = document.getElementById('task-image'); if (taskImage && task.image) { console.log(`🖼️ Setting interactive task image: ${task.image}`); taskImage.src = task.image; taskImage.onerror = () => { console.log('Interactive task image failed to load:', task.image); taskImage.src = this.createPlaceholderImage('Interactive Task'); }; } else if (taskImage) { console.log(`⚠️ No image provided for interactive task, creating placeholder`); taskImage.src = this.createPlaceholderImage('Interactive Task'); } // Continue with rest of task display logic... const taskContainer = this.taskContainer || document.querySelector('.task-display-container'); // Add interactive container const interactiveContainer = document.createElement('div'); interactiveContainer.id = 'interactive-task-container'; interactiveContainer.innerHTML = `

Interactive Task: ${task.interactiveType}

${task.text}

`; if (taskContainer) { taskContainer.appendChild(interactiveContainer); } // Bind completion button document.getElementById('complete-photo-task').addEventListener('click', () => { this.completeInteractiveTask(); }); return true; } /** * Add styles for webcam modal */ addWebcamModalStyles() { if (document.getElementById('webcam-modal-styles')) return; const styles = document.createElement('style'); styles.id = 'webcam-modal-styles'; styles.textContent = ` #webcam-offer-modal .modal-overlay { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0, 0, 0, 0.8); display: flex; align-items: center; justify-content: center; z-index: 9999; } #webcam-offer-modal .modal-content { background: #2a2a2a; padding: 30px; border-radius: 10px; max-width: 500px; text-align: center; color: white; box-shadow: 0 4px 20px rgba(0,0,0,0.5); } #webcam-offer-modal h3 { margin-bottom: 20px; color: #ff6b6b; } #webcam-offer-modal .task-preview { background: #3a3a3a; padding: 15px; border-radius: 5px; margin: 20px 0; font-style: italic; } #webcam-offer-modal .modal-buttons { margin: 20px 0; } #webcam-offer-modal .modal-buttons button { margin: 0 10px; padding: 12px 24px; border: none; border-radius: 5px; cursor: pointer; font-size: 16px; transition: all 0.3s; } #webcam-offer-modal .btn-primary { background: #ff6b6b; color: white; } #webcam-offer-modal .btn-secondary { background: #6c757d; color: white; } #webcam-offer-modal .modal-buttons button:hover { transform: translateY(-2px); box-shadow: 0 4px 8px rgba(0,0,0,0.3); } #webcam-offer-modal .privacy-note { color: #aaa; margin-top: 15px; } `; document.head.appendChild(styles); } /** * Add styles for scenario interface elements */ addScenarioStyles() { if (document.getElementById('scenario-styles')) return; const styles = document.createElement('style'); styles.id = 'scenario-styles'; styles.textContent = ` /* Scenario camera button styles */ .scenario-camera-btn { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border: none; border-radius: 16px; padding: 20px 25px; margin: 15px 0; width: 100%; cursor: pointer; transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); font-size: 16px; position: relative; overflow: hidden; box-shadow: 0 8px 20px rgba(102, 126, 234, 0.3); border: 2px solid rgba(255, 255, 255, 0.1); } .scenario-camera-btn::before { content: ''; position: absolute; top: 0; left: -100%; width: 100%; height: 100%; background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent); transition: left 0.6s; } .scenario-camera-btn:hover { background: linear-gradient(135deg, #5a67d8 0%, #6b46c1 100%); transform: translateY(-3px) scale(1.02); box-shadow: 0 12px 28px rgba(102, 126, 234, 0.4); border-color: rgba(255, 255, 255, 0.2); } .scenario-camera-btn:hover::before { left: 100%; } .scenario-camera-btn:active { transform: translateY(-1px) scale(1.01); box-shadow: 0 6px 16px rgba(102, 126, 234, 0.3); } .camera-text { font-weight: bold; margin-bottom: 8px; font-size: 18px; display: flex; align-items: center; justify-content: center; gap: 10px; text-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); } .camera-text::before { content: '📷'; font-size: 24px; filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3)); animation: cameraGlow 2s ease-in-out infinite alternate; } .camera-info { font-size: 14px; opacity: 0.95; font-weight: 500; text-align: center; background: rgba(255, 255, 255, 0.1); padding: 8px 12px; border-radius: 20px; margin-top: 10px; border: 1px solid rgba(255, 255, 255, 0.15); backdrop-filter: blur(10px); } @keyframes cameraGlow { 0% { filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3)); transform: scale(1); } 100% { filter: drop-shadow(0 2px 8px rgba(255, 255, 255, 0.4)); transform: scale(1.05); } } /* Scenario action button styles */ .scenario-action { background: linear-gradient(135deg, #e91e63 0%, #ad1457 100%); color: white; border: none; border-radius: 16px; padding: 20px 25px; margin: 15px 0; width: 100%; cursor: pointer; transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); font-size: 16px; position: relative; overflow: hidden; box-shadow: 0 8px 20px rgba(233, 30, 99, 0.3); border: 2px solid rgba(255, 255, 255, 0.1); } .scenario-action:hover { background: linear-gradient(135deg, #c2185b 0%, #880e4f 100%); transform: translateY(-3px) scale(1.02); box-shadow: 0 12px 28px rgba(233, 30, 99, 0.4); border-color: rgba(255, 255, 255, 0.2); } .scenario-action:active { transform: translateY(-1px) scale(1.01); box-shadow: 0 6px 16px rgba(233, 30, 99, 0.3); } .scenario-action.active { background: linear-gradient(135deg, #ff5722 0%, #d84315 100%); cursor: not-allowed; animation: actionPulse 1s ease-in-out infinite; } .scenario-action.completed { background: linear-gradient(135deg, #4caf50 0%, #2e7d32 100%); cursor: default; } .action-text { font-weight: bold; margin-bottom: 8px; font-size: 18px; text-align: center; text-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); } .action-timer { font-size: 24px; font-weight: bold; color: #fff; background: rgba(255, 255, 255, 0.2); padding: 8px 16px; border-radius: 20px; text-align: center; border: 1px solid rgba(255, 255, 255, 0.15); backdrop-filter: blur(10px); } @keyframes actionPulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.8; transform: scale(1.02); } } /* TTS Control Styling */ .tts-control { background: linear-gradient(135deg, #37474f 0%, #263238 100%); color: #b0bec5; border: none; border-radius: 12px; padding: 8px 16px; margin: 0 5px; cursor: pointer; transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1); font-size: 13px; font-weight: 500; position: relative; overflow: hidden; box-shadow: 0 3px 8px rgba(0, 0, 0, 0.2); border: 1px solid rgba(255, 255, 255, 0.05); text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); } .tts-control::before { content: ''; position: absolute; top: 0; left: -100%; width: 100%; height: 100%; background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1), transparent); transition: left 0.5s; } .tts-control:hover { background: linear-gradient(135deg, #455a64 0%, #37474f 100%); transform: translateY(-2px); box-shadow: 0 6px 16px rgba(0, 0, 0, 0.3); border-color: rgba(255, 255, 255, 0.1); color: #cfd8dc; } .tts-control:hover::before { left: 100%; } .tts-control:active { transform: translateY(0); box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2); } .tts-control.enabled { background: linear-gradient(135deg, #43a047 0%, #2e7d32 100%); color: white; box-shadow: 0 3px 8px rgba(67, 160, 71, 0.3); border-color: rgba(255, 255, 255, 0.1); } .tts-control.enabled:hover { background: linear-gradient(135deg, #4caf50 0%, #388e3c 100%); box-shadow: 0 6px 16px rgba(67, 160, 71, 0.4); transform: translateY(-2px); } .tts-control.disabled { background: linear-gradient(135deg, #757575 0%, #424242 100%); color: #bdbdbd; box-shadow: 0 3px 8px rgba(117, 117, 117, 0.2); } .tts-control.disabled:hover { background: linear-gradient(135deg, #616161 0%, #424242 100%); box-shadow: 0 6px 16px rgba(117, 117, 117, 0.3); color: #e0e0e0; } /* Scenario controls container */ .scenario-controls { display: flex; align-items: center; gap: 8px; } /* Responsive adjustments */ @media (max-width: 768px) { .scenario-camera-btn { padding: 18px 20px; border-radius: 14px; } .camera-text { font-size: 16px; } .camera-text::before { font-size: 22px; } } `; document.head.appendChild(styles); } /** * Add styles for give up warning modal */ addGiveUpWarningStyles() { if (document.getElementById('give-up-warning-styles')) return; const styles = document.createElement('style'); styles.id = 'give-up-warning-styles'; styles.textContent = ` #give-up-warning-modal.modal-overlay { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0, 0, 0, 0.85); display: flex; align-items: center; justify-content: center; z-index: 10000; backdrop-filter: blur(5px); } .give-up-warning { background: linear-gradient(135deg, #1a1a1a 0%, #2d1b1b 100%); border: 2px solid #d32f2f; border-radius: 20px; padding: 30px; max-width: 500px; margin: 20px; box-shadow: 0 20px 40px rgba(211, 47, 47, 0.3); animation: warningPulse 0.5s ease-out; } @keyframes warningPulse { 0% { transform: scale(0.8); opacity: 0; } 50% { transform: scale(1.05); } 100% { transform: scale(1); opacity: 1; } } .warning-header { text-align: center; margin-bottom: 20px; } .warning-header h2 { color: #f44336; margin: 0; font-size: 24px; text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5); font-weight: bold; } .warning-message { margin-bottom: 25px; text-align: center; } .warning-message p { color: #ffcdd2; font-size: 16px; line-height: 1.5; margin: 15px 0; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5); } .warning-consequence { color: #ff8a80 !important; font-weight: bold; font-style: italic; border-top: 1px solid rgba(211, 47, 47, 0.3); padding-top: 15px; margin-top: 20px !important; } .warning-buttons { display: flex; gap: 15px; justify-content: center; flex-wrap: wrap; } .warning-btn { padding: 12px 20px; border: none; border-radius: 12px; font-weight: bold; cursor: pointer; transition: all 0.3s ease; font-size: 14px; min-width: 180px; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); } .warning-btn.btn-danger { background: linear-gradient(135deg, #d32f2f 0%, #b71c1c 100%); color: white; box-shadow: 0 4px 12px rgba(211, 47, 47, 0.3); } .warning-btn.btn-danger:hover { background: linear-gradient(135deg, #f44336 0%, #d32f2f 100%); transform: translateY(-2px); box-shadow: 0 6px 16px rgba(211, 47, 47, 0.4); } .warning-btn.btn-success { background: linear-gradient(135deg, #388e3c 0%, #2e7d32 100%); color: white; box-shadow: 0 4px 12px rgba(56, 142, 60, 0.3); } .warning-btn.btn-success:hover { background: linear-gradient(135deg, #4caf50 0%, #388e3c 100%); transform: translateY(-2px); box-shadow: 0 6px 16px rgba(56, 142, 60, 0.4); } .warning-btn:active { transform: translateY(0); } @media (max-width: 600px) { .give-up-warning { margin: 10px; padding: 20px; } .warning-buttons { flex-direction: column; } .warning-btn { min-width: auto; width: 100%; } } `; document.head.appendChild(styles); } } // Export for use in other modules if (typeof module !== 'undefined' && module.exports) { module.exports = InteractiveTaskManager; } // Make available globally for dependency validation window.InteractiveTaskManager = InteractiveTaskManager;