/** * Interactive Task Manager - Handles advanced task types with user interaction * Supports mini-games, interactive elements, progress tracking, and dynamic content */ console.log('๐Ÿ“ฆ Loading InteractiveTaskManager...'); 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); } } /** * Get randomized ambient audio playlist * Returns array of ambient audio file paths, shuffled */ getAmbientAudioPlaylist() { const ambientFiles = []; // Check for audio files in multiple sources if (this.game?.audioManager?.audioLibrary?.ambient) { const ambientLib = this.game.audioManager.audioLibrary.ambient; if (Array.isArray(ambientLib) && ambientLib.length > 0) { ambientFiles.push(...ambientLib.map(file => file.path || file)); } } // Fallback: scan audio/ambient directory directly if (ambientFiles.length === 0) { // Default ambient files from audio/ambient folder const defaultFiles = [ 'audio/ambient/moaning-1.mp3', 'audio/ambient/moaning-2.mp3', 'audio/ambient/moaning-3.mp3', 'audio/ambient/moaning-4.mp3', 'audio/ambient/moaning-5.mp3', 'audio/ambient/moaning-6.mp3', 'audio/ambient/moaning-7.mp3', 'audio/ambient/moaning-8.mp3', 'audio/ambient/moaining-9.mp3' ]; ambientFiles.push(...defaultFiles); } // Shuffle using Fisher-Yates algorithm const shuffled = [...ambientFiles]; for (let i = shuffled.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; } console.log(`๐ŸŽต Created ambient audio playlist with ${shuffled.length} tracks`); return shuffled; } /** * Create ambient audio player that loops through randomized playlist * Returns { element, playlist, currentIndex, playNext } */ createAmbientAudioPlayer(volume = 0.5) { const playlist = this.getAmbientAudioPlaylist(); if (playlist.length === 0) { console.warn('No ambient audio files found'); return null; } console.log(`๐ŸŽต Ambient playlist created with ${playlist.length} tracks:`, playlist); let currentIndex = 0; const audioElement = new Audio(playlist[currentIndex]); audioElement.volume = volume; audioElement.loop = false; // Explicitly set to false const playNext = () => { currentIndex = (currentIndex + 1) % playlist.length; console.log(`๐ŸŽต Playing track ${currentIndex + 1}/${playlist.length}: ${playlist[currentIndex]}`); audioElement.src = playlist[currentIndex]; audioElement.load(); // Force reload audioElement.play().catch(err => { console.warn('Could not play next ambient audio:', err); }); }; // Auto-advance to next track when current ends audioElement.addEventListener('ended', () => { console.log(`๐ŸŽต Track ended, advancing to next...`); playNext(); }); console.log(`๐ŸŽต Starting with track 1/${playlist.length}: ${playlist[0]}`); return { element: audioElement, playlist: playlist, currentIndex: currentIndex, playNext: playNext }; } /** * 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) }); // Academy Training Tasks this.registerTaskType('edge', { name: 'Edge Training', description: 'Complete specified number of edges', handler: this.createEdgeTask.bind(this), validator: this.validateEdgeTask.bind(this) }); this.registerTaskType('rhythm', { name: 'Rhythm Pattern', description: 'Follow a specific stroking rhythm', handler: this.createRhythmTask.bind(this), validator: this.validateRhythmTask.bind(this) }); this.registerTaskType('add-library-directory', { name: 'Add Library Directory', description: 'Add a directory to your media library', handler: this.createAddLibraryTask.bind(this), validator: this.validateAddLibraryTask.bind(this) }); this.registerTaskType('link-media', { name: 'Link Media Files', description: 'Link individual media files to your library', handler: this.createLinkMediaTask.bind(this), validator: this.validateLinkMediaTask.bind(this) }); this.registerTaskType('tag-files', { name: 'Tag Files', description: 'Tag files in your library', handler: this.createTagFilesTask.bind(this), validator: this.validateTagFilesTask.bind(this) }); this.registerTaskType('preference-slideshow', { name: 'Preference-Based Slideshow', description: 'Photo slideshow with preference-matched captions', handler: this.createPreferenceSlideshowTask.bind(this), validator: this.validatePreferenceSlideshowTask.bind(this) }); this.registerTaskType('dual-slideshow', { name: 'Dual Slideshow', description: 'Two simultaneous slideshows with preference-matched content', handler: this.createDualSlideshowTask.bind(this), validator: this.validateDualSlideshowTask.bind(this) }); this.registerTaskType('enable-webcam', { name: 'Enable Webcam', description: 'Activate webcam for mirror viewing', handler: this.createEnableWebcamTask.bind(this), validator: this.validateEnableWebcamTask.bind(this) }); this.registerTaskType('dual-video', { name: 'Dual Video', description: 'Watch two videos simultaneously', handler: this.createDualVideoTask.bind(this), validator: this.validateDualVideoTask.bind(this) }); this.registerTaskType('tts-command', { name: 'TTS Command', description: 'Receive voice commands via text-to-speech', handler: this.createTTSCommandTask.bind(this), validator: this.validateTTSCommandTask.bind(this) }); this.registerTaskType('quad-video', { name: 'Quad Video', description: 'Watch four videos simultaneously', handler: this.createQuadVideoTask.bind(this), validator: this.validateQuadVideoTask.bind(this) }); this.registerTaskType('chaos-quad-video', { name: 'Chaos Mode Quad Video', description: 'Quad video with random swaps, volume changes, and interruptions', handler: this.createChaosQuadVideoTask.bind(this), validator: this.validateChaosQuadVideoTask.bind(this) }); this.registerTaskType('chaos-triple-video', { name: 'Chaos Mode Triple Video', description: 'Triple video with random swaps and volume changes', handler: this.createChaosTripleVideoTask.bind(this), validator: this.validateChaosTripleVideoTask.bind(this) }); this.registerTaskType('rhythm-training', { name: 'Rhythm Training', description: 'Metronome-guided rhythm training with optional webcam and audio', handler: this.createRhythmTrainingTask.bind(this), validator: this.validateRhythmTrainingTask.bind(this) }); this.registerTaskType('webcam-setup', { name: 'Webcam Setup', description: 'Position verification and webcam activation', handler: this.createWebcamSetupTask.bind(this), validator: this.validateWebcamSetupTask.bind(this) }); this.registerTaskType('hypno-spiral', { name: 'Hypno Spiral', description: 'View hypnotic spiral overlay', handler: this.createHypnoSpiralTask.bind(this), validator: this.validateHypnoSpiralTask.bind(this) }); this.registerTaskType('video-start', { name: 'Start Video', description: 'Start a video player', handler: this.createVideoStartTask.bind(this), validator: this.validateVideoStartTask.bind(this) }); this.registerTaskType('update-preferences', { name: 'Update Preferences', description: 'Update Academy preferences at checkpoint', handler: this.createUpdatePreferencesTask.bind(this), validator: this.validateUpdatePreferencesTask.bind(this) }); this.registerTaskType('free-edge-session', { name: 'Free Edge Session', description: 'Free-form edging session with features enabled', handler: this.createFreeEdgeSessionTask.bind(this), validator: this.validateFreeEdgeSessionTask.bind(this) }); // Advanced Training Arc Actions (Levels 11-20) this.registerTaskType('hypno-caption-combo', { name: 'Hypno Caption Combo', description: 'Hypno spiral with timed caption overlays', handler: this.createHypnoCaptionComboTask.bind(this), validator: this.validateTimedTask.bind(this) }); this.registerTaskType('dynamic-captions', { name: 'Dynamic Captions', description: 'Preference-based caption display', handler: this.createDynamicCaptionsTask.bind(this), validator: this.validateTimedTask.bind(this) }); this.registerTaskType('tts-hypno-sync', { name: 'TTS Hypno Sync', description: 'Voice commands synchronized with hypno spiral', handler: this.createTTSHypnoSyncTask.bind(this), validator: this.validateTimedTask.bind(this) }); this.registerTaskType('sensory-overload', { name: 'Sensory Overload', description: 'All features combined in one session', handler: this.createSensoryOverloadTask.bind(this), validator: this.validateTimedTask.bind(this) }); this.registerTaskType('enable-interruptions', { name: 'Enable Interruptions', description: 'Activate random interruption system', handler: this.createEnableInterruptionsTask.bind(this), validator: this.validateInstantTask.bind(this) }); this.registerTaskType('denial-training', { name: 'Denial Training', description: 'Timed stroking/stopping periods', handler: this.createDenialTrainingTask.bind(this), validator: this.validateTimedTask.bind(this) }); this.registerTaskType('stop-stroking', { name: 'Stop Stroking', description: 'Enforced hands-off period', handler: this.createStopStrokingTask.bind(this), validator: this.validateTimedTask.bind(this) }); this.registerTaskType('enable-popups', { name: 'Enable Popups', description: 'Activate random popup image system', handler: this.createEnablePopupsTask.bind(this), validator: this.validateInstantTask.bind(this) }); this.registerTaskType('popup-image', { name: 'Popup Image', description: 'Display specific popup image', handler: this.createPopupImageTask.bind(this), validator: this.validateTimedTask.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'); // Remove any existing interactive controls first to prevent duplicates const existingControls = actionButtons.querySelector('.interactive-controls'); if (existingControls) { existingControls.remove(); } // Add interactive control buttons const interactiveControls = document.createElement('div'); interactiveControls.className = 'interactive-controls'; interactiveControls.innerHTML = ` `; actionButtons.appendChild(interactiveControls); // Add event listeners const completeBtn = document.getElementById('interactive-complete-btn'); completeBtn.addEventListener('click', () => { if (completeBtn.disabled) { // Show feedback if button is disabled const actionButtons = document.querySelector('.action-buttons'); let feedback = actionButtons.querySelector('.disabled-feedback'); if (!feedback) { feedback = document.createElement('div'); feedback.className = 'disabled-feedback'; feedback.style.cssText = 'color: var(--color-warning); margin-top: 10px; font-weight: bold; text-align: center;'; actionButtons.appendChild(feedback); } feedback.textContent = 'โš ๏ธ Complete the required steps first!'; feedback.style.animation = 'none'; setTimeout(() => { feedback.style.animation = 'pulse 0.5s ease-in-out 3'; }, 10); setTimeout(() => { feedback.textContent = ''; }, 3000); return; } 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()); document.getElementById('multi-screen-btn').addEventListener('click', () => this.openMultiScreenMode()); } /** * Validate and complete interactive task */ async completeInteractiveTask() { console.log('๐Ÿ” CompleteInteractiveTask called - currentInteractiveTask:', this.currentInteractiveTask); // 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; } // Handle scenario completion even if currentInteractiveTask is null if (!this.currentInteractiveTask) { console.log('โš ๏ธ No current interactive task found - checking for scenario completion'); // Check if this is a scenario completion const currentTask = this.game.gameState.currentTask; if (currentTask && currentTask.interactiveType === 'scenario-adventure' && currentTask.scenarioState && currentTask.scenarioState.completed) { console.log('โœ… Found completed scenario - proceeding with completion'); this.showFeedback('success', 'Scenario completed successfully! ๐ŸŽ‰'); setTimeout(() => { if (this.game.gameState.isRunning) { this.cleanupInteractiveTask(); this.game.completeTask(); } }, 1500); return; } console.log('โŒ No current interactive task or completed scenario found!'); 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 || {}; // Set this task as the current interactive task this.currentInteractiveTask = task; console.log('๐ŸŽญ Set scenario task as current interactive task:', task.id); 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 === 'inventory-check') { // Handle inventory questionnaire this.displayInventoryQuestionnaire(task, scenario, step, choicesEl); } else if (step.type === 'path-generation') { // Handle dynamic path generation based on inventory this.generateInventoryPath(task, scenario, step, 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); // For inventory-based endings, populate the items list if (task.scenarioState.tier && task.scenarioState.inventoryManager) { setTimeout(() => { this.populateInventoryItemsList(task.scenarioState.tier, task.scenarioState.inventoryManager); }, 100); } // 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'); // Add skip button handler const skipBtn = document.getElementById('skip-action-timer-btn'); if (skipBtn) { skipBtn.addEventListener('click', () => { timeLeft = 0; // This will trigger completion on next interval }); } 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; } // Add skip button handler const skipBtn = document.getElementById('skip-scenario-mirror-timer-btn'); if (skipBtn) { skipBtn.addEventListener('click', () => { timeLeft = 0; // This will trigger completion on next interval }); } 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; } // Add skip button handler const skipBtn = container.querySelector('#skip-mirror-timer-btn'); if (skipBtn) { skipBtn.addEventListener('click', () => { timeLeft = 0; // This will trigger completion on next interval }); } 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 } /** * Create pose verification task */ async createPoseVerificationTask(task, container) { console.log('๐Ÿ” Creating pose verification task'); // Extract pose information from task const poseInstructions = this.extractPoseInstructions(task); const verificationDuration = task.verificationDuration || task.duration || 30; // Create pose verification interface container.innerHTML = `

๐Ÿ” Pose Verification Required

${task.story || 'Assume the required pose and verify with webcam'}

Required Pose:

${poseInstructions}

๐Ÿ” Use your webcam to verify you're maintaining the correct pose

โฑ๏ธ Hold the position for ${verificationDuration} seconds

`; // Add event listener for the start button const startBtn = container.querySelector('#start-pose-verification-btn'); // Add hover effects startBtn.addEventListener('mouseenter', () => { startBtn.style.transform = 'translateY(-2px) scale(1.05)'; startBtn.style.boxShadow = '0 5px 15px rgba(231, 76, 60, 0.5)'; }); startBtn.addEventListener('mouseleave', () => { startBtn.style.transform = 'translateY(0) scale(1)'; startBtn.style.boxShadow = '0 3px 10px rgba(231, 76, 60, 0.3)'; }); // Add click handler to start pose verification startBtn.addEventListener('click', () => { if (this.game.webcamManager) { const verificationData = { instructions: `Assume the required pose: ${poseInstructions}`, verificationInstructions: poseInstructions, verificationDuration: verificationDuration, verificationText: task.actionText || task.story, task: task }; // Start verification mode this.game.webcamManager.startVerificationMode(verificationData); // Start the timer and progress bar this.startPoseVerificationTimer(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.completePoseVerificationTask(task); }, 2000); } }); return true; } /** * Extract pose instructions from task data */ extractPoseInstructions(task) { // Check for specific pose indicators in task text const text = `${task.story || ''} ${task.actionText || ''}`.toLowerCase(); if (text.includes('all fours') || text.includes('hands and knees')) { return 'Get on all fours with hands and knees on the ground'; } else if (text.includes('kneel') || text.includes('kneeling')) { return 'Kneel with proper posture, hands on thighs'; } else if (text.includes('present') || text.includes('spread')) { return 'Assume a presenting position as described'; } else if (text.includes('position') && text.includes('stress')) { return 'Maintain stress position - hands behind head, standing on toes'; } else if (text.includes('crawl')) { return 'Get into crawling position on hands and knees'; } else if (text.includes('shameful') && text.includes('position')) { return 'Assume the shameful position as described in the task'; } else { return task.actionText || 'Assume the position described in the task'; } } /** * Start pose verification timer */ startPoseVerificationTimer(task, container) { const duration = task.verificationDuration || task.duration || 30; const timerSection = container.querySelector('#pose-verification-timer'); const progressBar = container.querySelector('#pose-verification-progress-bar'); const timeDisplay = container.querySelector('#pose-verification-time'); const startBtn = container.querySelector('#start-pose-verification-btn'); // Show timer and hide start button timerSection.style.display = 'block'; startBtn.style.display = 'none'; let timeLeft = duration; // Add skip button handler const skipBtn = container.querySelector('#skip-pose-timer-btn'); if (skipBtn) { skipBtn.addEventListener('click', () => { timeLeft = 0; // This will trigger completion on next interval }); } const interval = setInterval(() => { timeLeft--; timeDisplay.textContent = `${timeLeft}s`; const progress = ((duration - timeLeft) / duration) * 100; progressBar.style.width = `${progress}%`; if (timeLeft <= 0) { clearInterval(interval); this.completePoseVerificationTask(task); } }, 1000); // Store interval for cleanup this.poseVerificationInterval = interval; } /** * Complete pose verification task */ completePoseVerificationTask(task) { console.log('โœ… Pose verification task completed'); // Clean up interval if (this.poseVerificationInterval) { clearInterval(this.poseVerificationInterval); this.poseVerificationInterval = null; } // Close webcam verification if active if (this.game.webcamManager) { this.game.webcamManager.stopCamera(); } // Complete the task this.completeInteractiveTask(); } /** * Validate pose verification task */ async validatePoseVerificationTask(task) { return true; // Pose verification tasks are completed when timer expires } /** * 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 photoRequirements object (inventory-based progression) if (step.photoRequirements) { return { count: step.photoRequirements.count || 3, description: `Take ${step.photoRequirements.count || 3} photos to complete this task`, items: step.photoRequirements.items || [], pose: step.photoRequirements.pose || null, edging: step.photoRequirements.edging || false }; } // 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 this is a scenario step (not a standalone task) const currentTask = this.game?.gameState?.currentTask; console.log('๐Ÿ” Current task check:', currentTask); console.log('๐Ÿ” Task details:', { hasTask: !!currentTask, taskId: currentTask?.id, interactiveType: currentTask?.interactiveType, hasScenarioState: !!currentTask?.scenarioState, scenarioState: currentTask?.scenarioState, currentStep: currentTask?.scenarioState?.currentStep }); if (currentTask && currentTask.interactiveType === 'scenario-adventure' && currentTask.scenarioState) { console.log('โœ… IS SCENARIO - Completing scenario mirror step'); // Get the current step const scenario = currentTask.interactiveData; const currentStepId = currentTask.scenarioState.currentStep; const step = scenario.steps[currentStepId]; console.log('๐Ÿ” Step info:', { currentStepId, stepType: step?.type, hasNextStep: !!(step?.nextStep || step?.next), nextStep: step?.nextStep || step?.next, step: step }); if (step && (step.nextStep || step.next)) { // Apply step effects this.applyActionEffects(step, currentTask.scenarioState); // Move to next step const nextStep = step.next || step.nextStep || this.getDefaultNextStep(currentTask.scenarioState); console.log('โญ๏ธ Advancing to next step:', nextStep); setTimeout(() => { currentTask.scenarioState.currentStep = nextStep; currentTask.scenarioState.stepNumber++; // Check if we're in training academy mode if (window.proceedToNextStep && typeof window.proceedToNextStep === 'function') { console.log('๐ŸŽ“ Using training academy progression'); window.proceedToNextStep(nextStep); } else { // Standard scenario progression this.displayScenarioStep(currentTask, scenario, nextStep); } }, 1000); return; // Don't complete the whole task, just advance the scenario } else { console.log('โŒ Step has no nextStep property'); } } else { console.log('โŒ NOT A SCENARIO - will complete task normally'); console.log(' - currentTask exists:', !!currentTask); console.log(' - is scenario-adventure:', currentTask?.interactiveType === 'scenario-adventure'); console.log(' - has scenarioState:', !!currentTask?.scenarioState); } // Only try to complete the task if game is running if (!this.game?.gameState?.isRunning) { console.log('Mirror task completion cancelled - game no longer running'); return; } // Show completion message for standalone tasks 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 console.log('๐Ÿ“ž Calling game.completeTask()'); 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); } /* Skip Timer Button Styling */ .skip-timer-btn { background: linear-gradient(135deg, #ff9800 0%, #f57c00 100%); color: white; border: 2px solid rgba(255, 255, 255, 0.2); border-radius: 12px; padding: 10px 20px; font-size: 14px; font-weight: bold; cursor: pointer; transition: all 0.3s ease; box-shadow: 0 4px 12px rgba(255, 152, 0, 0.4); } .skip-timer-btn:hover { background: linear-gradient(135deg, #fb8c00 0%, #e65100 100%); transform: translateY(-2px); box-shadow: 0 6px 16px rgba(255, 152, 0, 0.5); } .skip-timer-btn:active { transform: translateY(0); box-shadow: 0 2px 8px rgba(255, 152, 0, 0.3); } @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); } /** * Open multi-screen mode overlay for intensive training */ async openMultiScreenMode() { console.log('๐ŸŽฌ Opening Multi-Screen Mode...'); try { // Check if quad player exists and is minimized if (this.quadPlayer && this.quadPlayer.isMinimized) { console.log('๐ŸŽฌ Restoring minimized QuadVideoPlayer'); this.quadPlayer.restore(); return; } // If quad player exists but might be corrupted, destroy and recreate if (this.quadPlayer) { console.log('๐ŸŽฌ Recreating QuadVideoPlayer to prevent corruption'); this.quadPlayer.destroy(); this.quadPlayer = null; } // Create new quad player this.quadPlayer = new QuadVideoPlayer(); const initialized = await this.quadPlayer.initialize(); if (!initialized) { console.error('โŒ Failed to initialize QuadVideoPlayer'); // Show user-friendly error if (this.game && this.game.showNotification) { this.game.showNotification('โš ๏ธ No videos available for multi-screen mode', 'warning'); } return; } // Show the quad overlay this.quadPlayer.show(); // Log for user feedback if (this.game && this.game.showNotification) { this.game.showNotification('๐ŸŽฌ Multi-Screen Mode activated! Press ESC to close, โ€” to minimize', 'info'); } } catch (error) { console.error('โŒ Error opening multi-screen mode:', error); if (this.game && this.game.showNotification) { this.game.showNotification('โŒ Failed to open multi-screen mode', 'error'); } } } /** * Display inventory questionnaire */ displayInventoryQuestionnaire(task, scenario, step, choicesEl) { console.log('๐Ÿ“‹ Displaying inventory questionnaire'); // Initialize inventory in scenario state if not exists if (!task.scenarioState.inventory) { task.scenarioState.inventory = {}; } const categories = step.inventoryCategories || {}; const categoryKeys = Object.keys(categories); // Build questionnaire HTML let questionnaireHTML = '
'; categoryKeys.forEach(categoryKey => { const category = categories[categoryKey]; questionnaireHTML += `

${category.title}

`; Object.keys(category.items).forEach(itemKey => { const item = category.items[itemKey]; const currentValue = task.scenarioState.inventory[itemKey] || (item.type === 'boolean' ? false : 'none'); if (item.type === 'boolean') { // Boolean checkbox questionnaireHTML += `
`; } else { // Dropdown select questionnaireHTML += `
`; } }); questionnaireHTML += `
`; }); questionnaireHTML += '
'; // Display the questionnaire choicesEl.innerHTML = questionnaireHTML; // Add submit button const submitBtn = document.createElement('button'); submitBtn.className = 'scenario-choice primary'; submitBtn.innerHTML = '
Submit Inventory
'; submitBtn.addEventListener('click', () => { // Collect all inventory values document.querySelectorAll('.inventory-select').forEach(select => { const itemKey = select.dataset.item; task.scenarioState.inventory[itemKey] = select.value; }); document.querySelectorAll('.inventory-checkbox').forEach(checkbox => { const itemKey = checkbox.dataset.item; task.scenarioState.inventory[itemKey] = checkbox.checked; }); console.log('๐Ÿ“ฆ Collected inventory:', task.scenarioState.inventory); // Move to next step const nextStep = step.nextStep; task.scenarioState.currentStep = nextStep; task.scenarioState.stepNumber++; this.displayScenarioStep(task, scenario, nextStep); }); choicesEl.appendChild(submitBtn); } /** * Populate inventory items list in certificate ending */ populateInventoryItemsList(tier, inventoryManager) { const listEl = document.getElementById(`tier-${tier}-items-list`); if (!listEl) { console.warn(`Could not find tier-${tier}-items-list element`); return; } const summary = inventoryManager.getInventorySummary(); let itemsHTML = ''; // Combine all items into one list const allItems = [ ...summary.clothing.map(item => `๐Ÿ‘— ${item}`), ...summary.accessories.map(item => `๐Ÿ’„ ${item}`), ...summary.toys.map(item => `๐Ÿ”ž ${item}`), ...summary.environment.map(item => `๐Ÿ“ธ ${item}`) ]; if (allItems.length > 0) { itemsHTML = ``; } else { itemsHTML = '

No items used

'; } listEl.innerHTML = itemsHTML; } /** * Generate dynamic photo progression based on inventory */ async generateInventoryPath(task, scenario, step, choicesEl) { console.log('๐ŸŽฏ Generating inventory-based path'); // Load InventoryManager if not already loaded if (!window.InventoryManager) { console.error('โŒ InventoryManager not loaded!'); return; } // Create inventory manager instance const inventoryManager = new window.InventoryManager(); inventoryManager.setInventory(task.scenarioState.inventory); // Get tier and summary const tier = inventoryManager.tier; const summary = inventoryManager.getInventorySummary(); const photoCount = inventoryManager.getTotalPhotos(); console.log(`๐Ÿ“Š Tier ${tier}: ${summary.totalItems} items, ${photoCount} photos`); // Store in scenario state task.scenarioState.tier = tier; task.scenarioState.photoCount = photoCount; task.scenarioState.inventoryManager = inventoryManager; // Generate photo progression const progression = inventoryManager.generatePhotoProgression(); console.log(`๐Ÿ“ธ Generated ${progression.length} photo challenges`); // Load pose bank if (!window.InventoryPoseBank) { console.error('โŒ InventoryPoseBank not loaded!'); return; } // Dynamically add challenge steps to scenario progression.forEach((challenge, index) => { const stepId = `challenge_${index + 1}`; const isLastChallenge = index === progression.length - 1; const nextStepId = isLastChallenge ? `tier_${tier}_ending` : `challenge_${index + 2}`; // Create photo-verification step scenario.steps[stepId] = { type: 'photo-verification', mood: 'progressive', story: challenge.instruction, photoRequirements: { items: challenge.requiredItems, pose: challenge.pose, edging: challenge.edging, count: 3 }, nextStep: nextStepId }; }); console.log(`โœ… Added ${progression.length} challenge steps to scenario`); // Display summary const summaryHTML = `

๐Ÿ“Š Your Photo Journey

Tier ${tier}
Items Available: ${summary.totalItems}
Photos Required: ${photoCount}
${summary.clothing.length > 0 ? `

๐Ÿ‘— Clothing

    ${summary.clothing.map(item => `
  • ${item}
  • `).join('')}
` : ''} ${summary.accessories.length > 0 ? `

๐Ÿ’„ Accessories

    ${summary.accessories.map(item => `
  • ${item}
  • `).join('')}
` : ''} ${summary.toys.length > 0 ? `

๐Ÿ”ž Toys

    ${summary.toys.map(item => `
  • ${item}
  • `).join('')}
` : ''} ${summary.environment.length > 0 ? `

๐Ÿ“ธ Environment

    ${summary.environment.map(item => `
  • ${item}
  • `).join('')}
` : ''}
`; choicesEl.innerHTML = summaryHTML; // Add choice buttons const choicesHTML = step.choices.map((choice, index) => { const btnClass = index === 0 ? 'primary' : 'secondary'; const btn = document.createElement('button'); btn.className = `scenario-choice ${btnClass}`; btn.innerHTML = `
${choice.text}
`; btn.addEventListener('click', () => { const nextStep = choice.nextStep; task.scenarioState.currentStep = nextStep; task.scenarioState.stepNumber++; this.displayScenarioStep(task, scenario, nextStep); }); return btn; }).forEach(btn => choicesEl.appendChild(btn)); } // ======================================== // ACADEMY TRAINING ACTION HANDLERS // ======================================== /** * Edge Training Task - Timed edging/gooning session */ async createEdgeTask(task, container) { const duration = task.params?.duration || 300; // Default 5 minutes const instruction = task.params?.instruction || 'Edge and goon for the full duration'; const withVideo = task.params?.keepVideoPlaying || false; const preserveContent = task.params?.preserveContent !== false; // Default true const showTimer = task.params?.showTimer !== false; // Default true // If preserving content, add edge UI below existing content instead of replacing const edgeUI = `

๐ŸŽฏ Edge Training

${instruction}

Session Duration: ${Math.floor(duration / 60)} minutes ${duration % 60} seconds
`; if (preserveContent) { // Append to existing content container.insertAdjacentHTML('beforeend', edgeUI); } else { // Replace content (old behavior) container.innerHTML = edgeUI; } const btn = container.querySelector('#start-edge-session-btn'); const skipTaskBtn = container.querySelector('#skip-edge-task-btn'); const statusArea = container.querySelector('#edge-status'); const countdownEl = showTimer ? document.getElementById('sidebar-countdown-display') : null; const countdownWrapper = showTimer ? document.getElementById('sidebar-countdown-timer') : null; // Show skip button if dev mode is enabled if (window.isDevMode && window.isDevMode()) { skipTaskBtn.style.display = 'inline-block'; } let timerInterval = null; btn.addEventListener('click', async () => { btn.disabled = true; btn.style.display = 'none'; console.log('๐ŸŽฏ Starting edge session - features remain active from previous step'); // Show sidebar countdown timer const gameStatsPanel = document.getElementById('game-stats-panel'); console.log('๐ŸŽฏ EDGE TASK START - Stats panel element:', gameStatsPanel); console.log('๐ŸŽฏ EDGE TASK START - Stats panel display BEFORE:', gameStatsPanel?.style.display); console.log('๐ŸŽฏ EDGE TASK START - Countdown wrapper display BEFORE:', countdownWrapper?.style.display); if (showTimer && countdownWrapper) { countdownWrapper.style.display = 'block'; console.log('๐ŸŽฏ EDGE TASK START - Countdown wrapper set to: block'); } console.log('๐ŸŽฏ EDGE TASK START - Stats panel display AFTER:', gameStatsPanel?.style.display); console.log('๐ŸŽฏ EDGE TASK START - Countdown wrapper display AFTER:', countdownWrapper?.style.display); let timeRemaining = duration; // Initialize sidebar timer display if (showTimer && countdownEl) { const minutes = Math.floor(timeRemaining / 60); const seconds = timeRemaining % 60; countdownEl.textContent = `${minutes}:${seconds.toString().padStart(2, '0')}`; } statusArea.innerHTML = '
๐ŸŽฏ Edge and stroke... don\'t stop until time is up!
'; timerInterval = setInterval(() => { timeRemaining--; // Update sidebar timer if (showTimer && countdownEl) { const minutes = Math.floor(timeRemaining / 60); const seconds = timeRemaining % 60; countdownEl.textContent = `${minutes}:${seconds.toString().padStart(2, '0')}`; } if (timeRemaining <= 0) { clearInterval(timerInterval); // Hide sidebar timer if (showTimer && countdownWrapper) { countdownWrapper.style.display = 'none'; } statusArea.innerHTML = '
โœ… Edge training session complete!
'; task.completed = true; const completeBtn = document.getElementById('interactive-complete-btn'); if (completeBtn) { completeBtn.disabled = false; completeBtn.textContent = 'Continue to Next Step โœ“'; completeBtn.style.background = 'linear-gradient(135deg, var(--color-success), var(--color-primary))'; } } }, 1000); }); // Dev skip button handler if (skipTaskBtn) { skipTaskBtn.addEventListener('click', () => { console.log('โฉ Dev skip - completing edge task'); if (timerInterval) { clearInterval(timerInterval); } // Hide sidebar timer if (showTimer && countdownWrapper) { countdownWrapper.style.display = 'none'; } statusArea.innerHTML = '
โœ… Edge training session complete! (Dev skip)
'; task.completed = true; const completeBtn = document.getElementById('interactive-complete-btn'); if (completeBtn) { completeBtn.disabled = false; completeBtn.textContent = 'Continue to Next Step โœ“'; completeBtn.style.background = 'linear-gradient(135deg, var(--color-success), var(--color-primary))'; } }); } // Cleanup function for when task is skipped/completed task.cleanup = () => { if (timerInterval) { clearInterval(timerInterval); } if (showTimer && countdownWrapper) { countdownWrapper.style.display = 'none'; } }; } validateEdgeTask(task) { return task.completed === true; } /** * Rhythm Pattern Task - Follow stroking rhythm */ async createRhythmTask(task, container) { const pattern = task.params?.pattern || 'slow-fast-slow'; const duration = task.params?.duration || 120; const enableMetronomeSound = task.params?.enableMetronomeSound !== false; // Default to true const enableVideo = task.params?.enableVideo !== false; // Default to true const multiPattern = task.params?.multiPattern; // Array of pattern objects: [{pattern: 'fast-slow-fast', duration: 300}, ...] const ambientAudio = task.params?.ambientAudio || null; const ambientVolume = task.params?.ambientVolume || 0.5; let ambientAudioElement = null; const patterns = { 'slow-fast-slow': [60, 80, 100, 120, 140, 120, 100, 80, 60], 'fast-slow-fast': [140, 120, 100, 80, 60, 80, 100, 120, 140], 'steady': [90, 90, 90, 90, 90, 90], 'escalating': [60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160, 170, 180], 'varied-slow': [70, 80, 90, 100, 0, 80, 90, 110, 100, 0, 70, 85, 95, 80, 100], 'varied-medium': [100, 120, 140, 160, 150, 0, 130, 150, 170, 140, 180, 0, 120, 160, 140, 180, 200], 'varied-intense': [140, 160, 180, 200, 0, 160, 180, 200, 220, 0, 180, 200, 220, 240, 0, 200, 220, 180, 240] }; // If multiPattern provided, use it; otherwise use single pattern let patternSchedule = multiPattern || [{ pattern: pattern, duration: duration }]; let currentPatternIndex = 0; let currentPatternData = patternSchedule[currentPatternIndex]; let bpmSequence = patterns[currentPatternData.pattern] || patterns['steady']; let currentPhase = 0; let timeRemaining = duration; let patternTimeRemaining = currentPatternData.duration; let metronomeVolume = 0.3; // Default volume container.innerHTML = `

๐ŸŽต Rhythm Pattern: ${currentPatternData.pattern}

${enableVideo ? `
50%
30%
` : `
30%
`}
${bpmSequence[0] === 0 ? 'PAUSE' : bpmSequence[0]} BPM
Phase ${currentPhase + 1} of ${bpmSequence.length}
`; // Add timer to sidebar countdown element (don't replace entire sidebar!) const showTimer = task.params?.showTimer !== false; const countdownEl = showTimer ? document.getElementById('sidebar-countdown-display') : null; const countdownWrapper = showTimer ? document.getElementById('sidebar-countdown-timer') : null; const gameStatsPanel = document.getElementById('game-stats-panel'); console.log('๐ŸŽต RHYTHM TASK - Stats panel element:', gameStatsPanel); console.log('๐ŸŽต RHYTHM TASK - Stats panel display BEFORE:', gameStatsPanel?.style.display); console.log('๐ŸŽต RHYTHM TASK - Countdown wrapper display BEFORE:', countdownWrapper?.style.display); if (showTimer && countdownWrapper) { countdownWrapper.style.display = 'block'; console.log('๐ŸŽต RHYTHM TASK - Countdown wrapper set to: block'); } if (showTimer && countdownEl) { const mins = Math.floor(timeRemaining / 60); const secs = timeRemaining % 60; countdownEl.textContent = `${mins}:${String(secs).padStart(2, '0')}`; } console.log('๐ŸŽต RHYTHM TASK - Stats panel display AFTER:', gameStatsPanel?.style.display); console.log('๐ŸŽต RHYTHM TASK - Countdown wrapper display AFTER:', countdownWrapper?.style.display); // Create metronome sound (simple beep using Web Audio API) let audioContext; if (enableMetronomeSound && (window.AudioContext || window.webkitAudioContext)) { try { audioContext = new (window.AudioContext || window.webkitAudioContext)(); } catch (err) { console.warn('โš ๏ธ Could not create audio context for metronome:', err); } } const playMetronomeTick = () => { if (!audioContext || metronomeVolume === 0) return; try { const oscillator = audioContext.createOscillator(); const gainNode = audioContext.createGain(); oscillator.connect(gainNode); gainNode.connect(audioContext.destination); oscillator.frequency.value = 800; // 800 Hz tick sound oscillator.type = 'sine'; gainNode.gain.setValueAtTime(metronomeVolume, audioContext.currentTime); gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.1); oscillator.start(audioContext.currentTime); oscillator.stop(audioContext.currentTime + 0.1); } catch (err) { console.warn('โš ๏ธ Error playing metronome tick:', err); } }; // Start video (only if enabled) const videoContainer = enableVideo ? container.querySelector('#rhythm-video-container') : null; let currentVideoElement = null; const playRandomVideo = async () => { if (videoContainer && window.videoPlayerManager) { try { const availableVideos = window.videoPlayerManager.getEnabledVideos('background') || []; if (availableVideos.length > 0) { const randomVideo = availableVideos[Math.floor(Math.random() * availableVideos.length)]; const videoPath = typeof randomVideo === 'object' ? randomVideo.path : randomVideo; await window.videoPlayerManager.playTaskVideo(videoPath, videoContainer); // Get the video element reference currentVideoElement = videoContainer.querySelector('video'); // Apply current volume if (currentVideoElement) { const volumeSlider = container.querySelector('#rhythm-video-volume'); currentVideoElement.volume = (volumeSlider?.value || 50) / 100; // Auto-play next video when current ends currentVideoElement.addEventListener('ended', async () => { await playRandomVideo(); }); } } } catch (err) { console.warn('โš ๏ธ Could not start video for rhythm task:', err); } } }; if (enableVideo) { await playRandomVideo(); } // Start ambient audio playlist if provided or use default if (ambientAudio || true) { // Always use ambient if available const player = this.createAmbientAudioPlayer(ambientVolume); if (player) { ambientAudioElement = player.element; ambientAudioElement.play().catch(err => { console.warn('Could not play ambient audio:', err); }); } } // Setup video controls (only if video enabled) const skipVideoBtn = enableVideo ? container.querySelector('#rhythm-skip-video') : null; const videoVolumeSlider = enableVideo ? container.querySelector('#rhythm-video-volume') : null; const videoVolumeDisplay = enableVideo ? container.querySelector('#rhythm-video-volume-display') : null; const metronomeVolumeSlider = container.querySelector('#rhythm-metronome-volume'); const metronomeVolumeDisplay = container.querySelector('#rhythm-metronome-volume-display'); const skipTaskBtn = container.querySelector('#skip-rhythm-task-btn'); // Show skip button if dev mode is enabled if (window.isDevMode && window.isDevMode()) { skipTaskBtn.style.display = 'inline-block'; } if (skipVideoBtn) { skipVideoBtn.addEventListener('click', async () => { await playRandomVideo(); }); } if (videoVolumeSlider && videoVolumeDisplay) { videoVolumeSlider.addEventListener('input', (e) => { const volume = e.target.value; videoVolumeDisplay.textContent = `${volume}%`; if (currentVideoElement) { currentVideoElement.volume = volume / 100; } }); } if (metronomeVolumeSlider && metronomeVolumeDisplay) { metronomeVolumeSlider.addEventListener('input', (e) => { const volume = e.target.value; metronomeVolumeDisplay.textContent = `${volume}%`; metronomeVolume = volume / 100; }); } const metronomeVisual = container.querySelector('#metronome-visual'); const bpmDisplay = container.querySelector('.bpm-value'); const phaseIndicator = container.querySelector('.phase-indicator'); const patternTitle = container.querySelector('#rhythm-pattern-title'); // Start metronome animation let bpm = bpmSequence[currentPhase]; let beatInterval = (60 / bpm) * 1000; const beat = () => { // Skip beat if in pause phase (BPM = 0) if (bpm === 0) return; metronomeVisual.classList.add('beat'); setTimeout(() => metronomeVisual.classList.remove('beat'), 100); if (enableMetronomeSound) { playMetronomeTick(); } }; let beatTimer = setInterval(beat, beatInterval); // Countdown timer and phase progression let phaseDuration = currentPatternData.duration / bpmSequence.length; let phaseTime = phaseDuration; const countdown = setInterval(() => { timeRemaining--; patternTimeRemaining--; phaseTime--; // Update sidebar countdown timer if (showTimer && countdownEl) { const mins = Math.floor(timeRemaining / 60); const secs = timeRemaining % 60; countdownEl.textContent = `${mins}:${String(secs).padStart(2, '0')}`; } // Check if we need to switch to next pattern in schedule if (multiPattern && patternTimeRemaining <= 0 && currentPatternIndex < patternSchedule.length - 1) { currentPatternIndex++; currentPatternData = patternSchedule[currentPatternIndex]; bpmSequence = patterns[currentPatternData.pattern] || patterns['steady']; currentPhase = 0; patternTimeRemaining = currentPatternData.duration; phaseDuration = currentPatternData.duration / bpmSequence.length; phaseTime = phaseDuration; bpm = bpmSequence[currentPhase]; beatInterval = (60 / bpm) * 1000; clearInterval(beatTimer); beatTimer = setInterval(beat, beatInterval); bpmDisplay.textContent = bpm === 0 ? 'PAUSE' : bpm; phaseIndicator.textContent = `Phase ${currentPhase + 1} of ${bpmSequence.length}`; patternTitle.textContent = `๐ŸŽต Rhythm Pattern: ${currentPatternData.pattern}`; console.log(`๐ŸŽต Pattern switched to: ${currentPatternData.pattern}`); } // Check phase progression within current pattern else if (phaseTime <= 0 && currentPhase < bpmSequence.length - 1) { currentPhase++; phaseTime = phaseDuration; bpm = bpmSequence[currentPhase]; beatInterval = (60 / bpm) * 1000; clearInterval(beatTimer); beatTimer = setInterval(beat, beatInterval); bpmDisplay.textContent = bpm === 0 ? 'PAUSE' : bpm; } if (timeRemaining <= 0) { clearInterval(countdown); clearInterval(beatTimer); task.completed = true; // Hide sidebar countdown timer if (showTimer && countdownWrapper) { countdownWrapper.style.display = 'none'; } container.innerHTML += '
โœ… Rhythm pattern complete!
'; const completeBtn = document.getElementById('interactive-complete-btn'); if (completeBtn) { completeBtn.disabled = false; } } }, 1000); // Dev skip button handler if (skipTaskBtn) { skipTaskBtn.addEventListener('click', () => { console.log('โฉ Dev skip - completing rhythm task'); clearInterval(countdown); clearInterval(beatTimer); // Stop audio if (audioContext) { audioContext.close().catch(err => console.warn('โš ๏ธ Error closing audio context:', err)); } if (ambientAudioElement) { ambientAudioElement.pause(); ambientAudioElement = null; } // Hide sidebar countdown timer if (showTimer && countdownWrapper) { countdownWrapper.style.display = 'none'; } container.innerHTML += '
โœ… Rhythm pattern complete! (Dev skip)
'; task.completed = true; const completeBtn = document.getElementById('interactive-complete-btn'); if (completeBtn) { completeBtn.disabled = false; } }); } // Store cleanup task.cleanup = () => { console.log('๐Ÿงน Rhythm task cleanup called'); clearInterval(countdown); clearInterval(beatTimer); console.log('โœ… Cleared intervals'); // Stop audio context if (audioContext) { console.log('๐Ÿ”‡ Closing audio context'); audioContext.close().catch(err => console.warn('โš ๏ธ Error closing audio context:', err)); } // Stop ambient audio if (ambientAudioElement) { ambientAudioElement.pause(); ambientAudioElement = null; } // Hide countdown timer only (not entire sidebar) const countdownTimer = document.getElementById('sidebar-countdown-timer'); if (countdownTimer) { countdownTimer.style.display = 'none'; } }; } validateRhythmTask(task) { return task.completed === true; } /** * Add Library Directory Task */ async createAddLibraryTask(task, container) { const suggestedDir = task.params?.directory || ''; const suggestedTags = task.params?.suggestedTags || []; container.innerHTML = `

๐Ÿ“ Add Library Directory

Add a directory containing your media files to The Academy library.

${suggestedDir ? `

Suggested: ${suggestedDir}

` : ''} ${suggestedTags.length > 0 ? `

Suggested tags: ${suggestedTags.map(t => `${t}`).join(' ')}

` : ''}
`; const btn = container.querySelector('#select-directory-btn'); const resultArea = container.querySelector('#directory-result'); btn.addEventListener('click', async () => { if (window.libraryManager) { try { const result = await window.libraryManager.addDirectory(suggestedTags); if (result) { resultArea.innerHTML = `
โœ… Directory added successfully!

Files found: ${result.fileCount || 'Scanning...'}

`; task.completed = true; const completeBtn = document.getElementById('interactive-complete-btn'); if (completeBtn) { completeBtn.disabled = false; } } } catch (error) { resultArea.innerHTML = `
Error: ${error.message}
`; } } else { resultArea.innerHTML = '
Library manager not available
'; } }); } validateAddLibraryTask(task) { return task.completed === true; } /** * Link Media Files Task */ async createLinkMediaTask(task, container) { const minFiles = task.params?.minFiles || 10; const mediaType = task.params?.mediaType || 'video'; // 'video' or 'image' const suggestedTags = task.params?.suggestedTags || []; container.innerHTML = `

๐Ÿ“Ž Link Media Files

Add a directory containing ${mediaType === 'video' ? 'video' : 'image'} files to expand your library.

Minimum ${minFiles} files recommended

${suggestedTags.length > 0 ? `

Suggested tags: ${suggestedTags.map(t => `${t}`).join(' ')}

` : ''}
`; const btn = container.querySelector('#link-directory-btn'); const resultArea = container.querySelector('#link-result'); btn.addEventListener('click', async () => { if (window.libraryManager) { try { const result = await window.libraryManager.addDirectory(suggestedTags); if (result) { const fileCount = result.fileCount || 0; resultArea.innerHTML = `
โœ… Directory linked successfully!

Files found: ${fileCount}

${fileCount >= minFiles ? '

Target reached!

' : `

You can add more directories to reach ${minFiles} files.

`}
`; // Complete if we have enough files if (fileCount >= minFiles) { task.completed = true; const completeBtn = document.getElementById('interactive-complete-btn'); if (completeBtn) { completeBtn.disabled = false; } } else { // Allow completing even if under target setTimeout(() => { task.completed = true; const completeBtn = document.getElementById('interactive-complete-btn'); if (completeBtn) { completeBtn.disabled = false; } }, 3000); } } } catch (error) { resultArea.innerHTML = `
Error: ${error.message}
`; // Allow completing even on error setTimeout(() => { task.completed = true; const completeBtn = document.getElementById('interactive-complete-btn'); if (completeBtn) { completeBtn.disabled = false; } }, 2000); } } else { // Fallback - just complete the task resultArea.innerHTML = '
โ„น๏ธ Library manager not available - task auto-completed
'; task.completed = true; const completeBtn = document.getElementById('interactive-complete-btn'); if (completeBtn) { completeBtn.disabled = false; } } }); } validateLinkMediaTask(task) { return task.completed === true; } validateAddLibraryTask(task) { return task.completed === true; } /** * Tag Files Task */ async createTagFilesTask(task, container) { const directory = task.params?.directory || ''; const minFiles = task.params?.minFiles || 10; const suggestedTags = task.params?.suggestedTags || []; const preserveContent = task.params?.preserveContent !== false; // Default true // Track tagged files count and initial count if (!task.totalTagsCount) { task.totalTagsCount = 0; } // Track initial total tags count (first time task is created) if (task.initialTagsCount === undefined) { // Count total number of tags across all media try { const tagsData = localStorage.getItem('library-mediaTags'); if (tagsData) { const parsed = JSON.parse(tagsData); const mediaTags = parsed.mediaTags || {}; // Count total tags (sum of all tag arrays) let totalTags = 0; Object.values(mediaTags).forEach(tags => { if (tags && Array.isArray(tags)) { totalTags += tags.length; } }); task.initialTagsCount = totalTags; task.totalTagsCount = totalTags; console.log(`๐Ÿท๏ธ Initial total tags: ${task.initialTagsCount}`); } else { task.initialTagsCount = 0; task.totalTagsCount = 0; } } catch (error) { console.error('โŒ Error reading initial tag data:', error); task.initialTagsCount = 0; task.totalTagsCount = 0; } } const initialCount = task.initialTagsCount; // Calculate target based on initial count const targetTags = initialCount + minFiles; const tagUI = `

๐Ÿท๏ธ Tag Your Files

Add ${minFiles} tags to your library (you can add multiple tags to the same file).

${directory ? `

Focus on: ${directory}

` : ''} ${suggestedTags.length > 0 ? `

๐Ÿ’ก Suggested tags: ${suggestedTags.map(t => `${t}`).join(' ')}

` : ''}
${task.totalTagsCount || 0} / ${targetTags} tags (+${Math.max(0, task.totalTagsCount - task.initialTagsCount)} added this session)
`; if (preserveContent) { // Append to existing content (e.g., keep video player active) container.insertAdjacentHTML('beforeend', tagUI); } else { // Replace content (old behavior) container.innerHTML = tagUI; } const btn = container.querySelector('#open-tagging-btn'); const refreshBtn = container.querySelector('#refresh-progress-btn'); const progressEl = container.querySelector('#tags-added-count'); const progressBar = container.querySelector('#tag-progress-bar'); const statusArea = container.querySelector('#tag-task-status'); const tagsAddedSessionEl = container.querySelector('#tags-added-session'); // Function to update progress const updateProgress = () => { // Count total tags across all media try { // Tags are stored in localStorage under 'library-mediaTags' key const tagsData = localStorage.getItem('library-mediaTags'); if (tagsData) { const parsed = JSON.parse(tagsData); const mediaTags = parsed.mediaTags || {}; // Count total tags (sum of all tag arrays) let totalTags = 0; Object.values(mediaTags).forEach(tags => { if (tags && Array.isArray(tags)) { totalTags += tags.length; } }); task.totalTagsCount = totalTags; console.log(`๐Ÿท๏ธ Progress update: ${task.totalTagsCount} total tags (started with ${initialCount})`); } else { task.totalTagsCount = initialCount; console.log('๐Ÿท๏ธ No tag data found in localStorage, keeping initial count'); } } catch (error) { console.error('โŒ Error reading tag data:', error); task.totalTagsCount = initialCount; } const tagsAdded = Math.max(0, task.totalTagsCount - initialCount); const targetTags = initialCount + minFiles; progressEl.textContent = task.totalTagsCount; if (tagsAddedSessionEl) { tagsAddedSessionEl.textContent = tagsAdded; } const percent = Math.min(100, (task.totalTagsCount / targetTags) * 100); progressBar.style.width = percent + '%'; if (task.totalTagsCount >= targetTags) { task.completed = true; btn.disabled = true; btn.textContent = 'โœ… Tagging Complete'; btn.style.background = 'var(--color-success)'; statusArea.innerHTML = `
โœ… You have added ${tagsAdded} new tags! Task complete.
`; const completeBtn = document.getElementById('interactive-complete-btn'); if (completeBtn) { completeBtn.disabled = false; } } else { const remaining = targetTags - task.totalTagsCount; statusArea.innerHTML = `
โ„น๏ธ Add ${remaining} more tag(s) to complete this task
`; } }; // Check initial progress updateProgress(); // Manual refresh button refreshBtn.addEventListener('click', () => { console.log('๐Ÿ”„ Manual progress refresh triggered'); updateProgress(); statusArea.innerHTML = '
๐Ÿ”„ Progress refreshed!
'; }); btn.addEventListener('click', async () => { console.log('๐Ÿท๏ธ Tag Files button clicked'); console.log('๐Ÿท๏ธ electronAPI available:', !!window.electronAPI); console.log('๐Ÿท๏ธ openChildWindow available:', !!window.electronAPI?.openChildWindow); try { // Try Electron child window first (desktop app) if (window.electronAPI && window.electronAPI.openChildWindow) { console.log('๐Ÿ–ฅ๏ธ Using Electron child window for Library Tagging'); const result = await window.electronAPI.openChildWindow({ url: 'library.html?tab=images&focus=tagging', windowName: 'Library Tagging', width: 1400, height: 900 }); console.log('๐Ÿ–ฅ๏ธ Child window result:', result); if (result.success) { statusArea.innerHTML = `
๐Ÿ“– Library ${result.action === 'focused' ? 'focused' : 'opened'} - Tag your files and close when done
`; // Poll for progress updates periodically const checkInterval = setInterval(() => { updateProgress(); }, 2000); // Check every 2 seconds // Store interval ID for cleanup task.progressCheckInterval = checkInterval; } else { console.error('โŒ Failed to open child window:', result.error); statusArea.innerHTML = `
โš ๏ธ Could not open library window: ${result.error || 'Unknown error'}
`; } } else { // Fallback to browser window.open console.log('๐ŸŒ Using browser window.open for Library Tagging'); const libraryUrl = 'library.html?tab=images&focus=tagging'; const libraryWindow = window.open(libraryUrl, 'LibraryTagging', 'width=1400,height=900'); if (libraryWindow) { statusArea.innerHTML = '
๐Ÿ“– Library opened - Tag your files and close the window when done
'; // Poll for progress updates when window closes or periodically const checkInterval = setInterval(() => { // Check if window is closed if (libraryWindow.closed) { clearInterval(checkInterval); updateProgress(); statusArea.innerHTML = '
๐Ÿ“– Library closed - Progress updated
'; } else { // Update progress while window is open updateProgress(); } }, 2000); // Check every 2 seconds // Store interval ID for cleanup task.progressCheckInterval = checkInterval; } else { statusArea.innerHTML = '
โš ๏ธ Could not open library - please disable popup blocker
'; } } } catch (error) { console.error('โŒ Error opening library:', error); statusArea.innerHTML = `
โŒ Error opening library: ${error.message}
`; } }); // Cleanup interval when task changes if (task.progressCheckInterval) { clearInterval(task.progressCheckInterval); } } validateTagFilesTask(task) { return task.completed === true; } /** * Enable Webcam Task */ async createEnableWebcamTask(task, container) { const instruction = task.params?.instruction || 'Turn on your webcam to watch yourself'; container.innerHTML = `

๐Ÿ“น Enable Webcam

${instruction}

`; const btn = container.querySelector('#enable-webcam-btn'); const statusArea = container.querySelector('#webcam-status'); const videoEl = container.querySelector('#webcam-video'); btn.addEventListener('click', async () => { btn.disabled = true; btn.textContent = 'Requesting permission...'; try { // Request webcam access directly using browser API const stream = await navigator.mediaDevices.getUserMedia({ video: { width: { ideal: 1280 }, height: { ideal: 720 } }, audio: false }); // Display webcam feed videoEl.srcObject = stream; videoEl.style.display = 'block'; statusArea.innerHTML = '
โœ… Webcam active - Watch yourself as you train
'; btn.textContent = 'โœ… Webcam Started'; task.completed = true; const completeBtn = document.getElementById('interactive-complete-btn'); if (completeBtn) { completeBtn.disabled = false; } // Store stream reference for cleanup task.webcamStream = stream; } catch (error) { console.error('Webcam error:', error); let errorMsg = 'Failed to access webcam'; if (error.name === 'NotAllowedError') { errorMsg = 'Webcam permission denied - you can continue without it'; } else if (error.name === 'NotFoundError') { errorMsg = 'No webcam found - you can continue without it'; } statusArea.innerHTML = `
โ„น๏ธ ${errorMsg}
`; btn.textContent = 'โš ๏ธ Webcam Unavailable'; // Auto-complete even on error task.completed = true; const completeBtn = document.getElementById('interactive-complete-btn'); if (completeBtn) { completeBtn.disabled = false; } } }); } validateEnableWebcamTask(task) { return task.completed === true; } /** * Dual Video Task */ async createDualVideoTask(task, container) { const mainVideo = task.params?.mainVideo || 'focus'; // Handle webcamActive parameter - if true, show webcam as overlay const webcamActive = task.params?.webcamActive || false; let pipVideo = task.params?.pipVideo || 'overlay'; // If webcamActive is true, override to show webcam as PiP overlay if (webcamActive) { pipVideo = 'webcam'; } const pipPosition = task.params?.pipPosition || 'bottom-right'; const minDuration = task.params?.duration || task.params?.minDuration || 120; // Support both duration and minDuration // Support both old parameter names (showCaptions) and new ones (captions) const showCaptions = task.params?.showCaptions || task.params?.captions || false; const captionInterval = task.params?.captionInterval || 10; // Default 10 seconds // Show timer if duration is specified const showTimer = task.params?.showTimer || (task.params?.duration !== undefined); container.innerHTML = `

๐ŸŽฌ Dual Video Mode

Watch two videos simultaneously - main video with picture-in-picture overlay.

Minimum viewing time: ${minDuration} seconds

${showCaptions ? '
' : ''}
80%
50%
`; const btn = container.querySelector('#start-dual-video-btn'); const skipBtn = container.querySelector('#skip-dual-video-btn'); const skipTaskBtn = container.querySelector('#skip-dual-video-task-btn'); const statusArea = container.querySelector('#dual-video-status'); const mainContainer = container.querySelector('#dual-video-main-container'); const captionEl = showCaptions ? container.querySelector('#dual-video-caption') : null; // Volume controls const mainVolumeSlider = container.querySelector('#dual-video-main-volume'); const mainVolumeLabel = container.querySelector('#dual-video-main-volume-label'); const pipVolumeSlider = container.querySelector('#dual-video-pip-volume'); const pipVolumeLabel = container.querySelector('#dual-video-pip-volume-label'); console.log('๐ŸŽฌ Dual-video skip task button found:', !!skipTaskBtn); // Show skip button if dev mode is enabled if (window.isDevMode && window.isDevMode()) { console.log('๐ŸŽฌ Dev mode active - showing skip task button'); skipTaskBtn.style.display = 'inline-block'; } let availableVideos = []; let captionInterval_id = null; let mainVideoElement = null; let pipVideoElement = null; // Volume control handlers const updateMainVolume = () => { const volume = mainVolumeSlider.value / 100; mainVolumeLabel.textContent = `${mainVolumeSlider.value}%`; if (mainVideoElement) { mainVideoElement.volume = volume; } }; const updatePipVolume = () => { const volume = pipVolumeSlider.value / 100; pipVolumeLabel.textContent = `${pipVolumeSlider.value}%`; if (pipVideoElement) { pipVideoElement.volume = volume; } }; mainVolumeSlider.addEventListener('input', updateMainVolume); pipVolumeSlider.addEventListener('input', updatePipVolume); const playNextMainVideo = async () => { if (mainVideo === 'webcam' || availableVideos.length < 1) return; const video = availableVideos[Math.floor(Math.random() * availableVideos.length)]; console.log('๐ŸŽฌ Auto-playing next main video:', video); await window.videoPlayerManager.playTaskVideo(video, mainContainer); // Re-attach ended listener and set volume if (window.videoPlayerManager.currentPlayer) { mainVideoElement = window.videoPlayerManager.currentPlayer; mainVideoElement.volume = mainVolumeSlider.value / 100; mainVideoElement.addEventListener('ended', playNextMainVideo); } }; const playNextPipVideo = async () => { if (pipVideo === 'webcam' || availableVideos.length < 1) return; const video = availableVideos[Math.floor(Math.random() * availableVideos.length)]; console.log('๐ŸŽฌ Auto-playing next PiP video:', video); await window.videoPlayerManager.playOverlayVideo(video); // Re-attach ended listener and set volume if (window.videoPlayerManager.overlayPlayer) { pipVideoElement = window.videoPlayerManager.overlayPlayer.querySelector('video'); if (pipVideoElement) { pipVideoElement.volume = pipVolumeSlider.value / 100; pipVideoElement.addEventListener('ended', playNextPipVideo); } } }; const playDualVideos = async () => { console.log('๐ŸŽฌ playDualVideos called - mainVideo:', mainVideo, 'pipVideo:', pipVideo); // Clear existing videos mainContainer.innerHTML = ''; // Handle main display based on mainVideo parameter if (mainVideo === 'webcam') { // Create webcam video element in main container try { const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: false }); const webcamVideo = document.createElement('video'); webcamVideo.srcObject = stream; webcamVideo.autoplay = true; webcamVideo.muted = true; webcamVideo.style.cssText = 'width: 100%; max-width: 800px; border-radius: 8px; box-shadow: 0 4px 6px rgba(0,0,0,0.3);'; mainContainer.appendChild(webcamVideo); statusArea.innerHTML = '
๐Ÿ“ธ Webcam active - watch yourself goon
'; } catch (error) { console.error('Webcam error:', error); statusArea.innerHTML = '
โš ๏ธ Could not access webcam
'; return; } } else { // mainVideo === 'focus' - play a video if (availableVideos.length < 1) return; // Select random video for main display const video1 = availableVideos[Math.floor(Math.random() * availableVideos.length)]; await window.videoPlayerManager.playTaskVideo(video1, mainContainer); // Add ended listener for autoplay and set volume if (window.videoPlayerManager.currentPlayer) { mainVideoElement = window.videoPlayerManager.currentPlayer; mainVideoElement.volume = mainVolumeSlider.value / 100; mainVideoElement.addEventListener('ended', playNextMainVideo); } } // Play overlay video (PiP style) - can be webcam or video console.log('๐ŸŽฌ Setting up PiP overlay - type:', pipVideo, 'available videos:', availableVideos.length); if (pipVideo === 'webcam') { // Clean up any existing overlay player first if (window.videoPlayerManager && window.videoPlayerManager.overlayPlayer) { const oldOverlay = window.videoPlayerManager.overlayPlayer; const oldVideo = oldOverlay.querySelector('video'); if (oldVideo && oldVideo.srcObject) { const tracks = oldVideo.srcObject.getTracks(); tracks.forEach(track => track.stop()); } oldOverlay.remove(); window.videoPlayerManager.overlayPlayer = null; } // Create webcam in PiP overlay position try { const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: false }); const webcamPip = document.createElement('video'); webcamPip.srcObject = stream; webcamPip.autoplay = true; webcamPip.muted = true; webcamPip.style.cssText = 'width: 100%; border-radius: 8px;'; // Create PiP container for webcam const webcamContainer = document.createElement('div'); webcamContainer.className = 'pip-webcam-container'; webcamContainer.appendChild(webcamPip); document.body.appendChild(webcamContainer); // Store reference for positioning code below window.videoPlayerManager = window.videoPlayerManager || {}; window.videoPlayerManager.overlayPlayer = webcamContainer; } catch (error) { console.error('Webcam PiP error:', error); statusArea.innerHTML = '
โš ๏ธ Could not access webcam for PiP
'; } } else if (availableVideos.length >= 1) { console.log('๐ŸŽฌ Creating video PiP overlay...'); // pipVideo === 'overlay' - play a video in PiP // Clean up any existing dual-video overlay player first const existingPipOverlay = document.querySelector('.dual-video-pip-overlay'); if (existingPipOverlay) { console.log('๐Ÿงน Cleaning up existing PiP overlay'); const video = existingPipOverlay.querySelector('video'); if (video) { video.pause(); video.src = ''; } existingPipOverlay.remove(); } // Create PiP overlay container const pipOverlay = document.createElement('div'); pipOverlay.className = 'dual-video-pip-overlay'; pipOverlay.style.cssText = ` position: fixed; width: 300px; height: auto; z-index: 100; box-shadow: 0 0 20px rgba(0,0,0,0.5); cursor: move; border: 2px solid rgba(255,255,255,0.3); border-radius: 8px; overflow: hidden; `; // Create video element for PiP const video2Path = availableVideos[Math.floor(Math.random() * availableVideos.length)]; console.log('๐ŸŽฌ Creating new PiP video:', video2Path); const pipVideo = document.createElement('video'); pipVideo.src = window.videoPlayerManager.getVideoPath(video2Path); pipVideo.autoplay = true; pipVideo.loop = false; pipVideo.volume = pipVolumeSlider.value / 100; pipVideo.style.cssText = 'width: 100%; display: block; border-radius: 6px;'; pipOverlay.appendChild(pipVideo); document.body.appendChild(pipOverlay); // Store reference pipVideoElement = pipVideo; window.videoPlayerManager.overlayPlayer = pipOverlay; // Add ended listener for autoplay pipVideo.addEventListener('ended', playNextPipVideo); console.log('๐ŸŽฌ PiP video element created and configured'); } else { console.warn('โš ๏ธ Not enough videos for PiP overlay'); } // Position the overlay player as PiP with draggable and resizable if (window.videoPlayerManager.overlayPlayer) { const pipPlayer = window.videoPlayerManager.overlayPlayer; pipPlayer.style.position = 'fixed'; pipPlayer.style.width = '300px'; pipPlayer.style.height = 'auto'; pipPlayer.style.zIndex = '100'; pipPlayer.style.boxShadow = '0 0 20px rgba(0,0,0,0.5)'; pipPlayer.style.cursor = 'move'; pipPlayer.style.border = '2px solid rgba(255,255,255,0.3)'; pipPlayer.style.borderRadius = '8px'; // Position based on parameter const positions = { 'bottom-right': { bottom: '20px', right: '20px', top: 'auto', left: 'auto' }, 'bottom-left': { bottom: '20px', left: '20px', top: 'auto', right: 'auto' }, 'top-right': { top: '20px', right: '20px', bottom: 'auto', left: 'auto' }, 'top-left': { top: '20px', left: '20px', bottom: 'auto', right: 'auto' } }; const pos = positions[pipPosition] || positions['bottom-right']; Object.assign(pipPlayer.style, pos); // Wrap in container for resize handle const pipContainer = document.createElement('div'); pipContainer.style.cssText = ` position: fixed; ${pos.top ? 'top: ' + pos.top : ''}; ${pos.bottom ? 'bottom: ' + pos.bottom : ''}; ${pos.left ? 'left: ' + pos.left : ''}; ${pos.right ? 'right: ' + pos.right : ''}; z-index: 100; width: 300px; display: inline-block; `; // Move player into container pipPlayer.parentElement.insertBefore(pipContainer, pipPlayer); pipContainer.appendChild(pipPlayer); pipPlayer.style.position = 'relative'; pipPlayer.style.top = '0'; pipPlayer.style.bottom = '0'; pipPlayer.style.left = '0'; pipPlayer.style.right = '0'; pipPlayer.style.display = 'block'; // Create resize handle const resizeHandle = document.createElement('div'); resizeHandle.style.cssText = ` position: absolute; bottom: 0; right: 0; width: 20px; height: 20px; background: rgba(255,255,255,0.7); cursor: nwse-resize; border-radius: 0 0 6px 0; z-index: 102; pointer-events: auto; `; resizeHandle.innerHTML = 'โ‹ฐ'; resizeHandle.style.textAlign = 'center'; resizeHandle.style.lineHeight = '20px'; resizeHandle.style.fontSize = '12px'; pipContainer.appendChild(resizeHandle); // Make draggable let isDragging = false; let isResizing = false; let startX, startY, startWidth, startHeight; let currentX, currentY; // Drag functionality pipPlayer.addEventListener('mousedown', (e) => { if (e.target === resizeHandle) return; isDragging = true; const rect = pipContainer.getBoundingClientRect(); startX = e.clientX - rect.left; startY = e.clientY - rect.top; pipPlayer.style.opacity = '0.8'; e.preventDefault(); }); // Resize functionality resizeHandle.addEventListener('mousedown', (e) => { e.stopPropagation(); e.preventDefault(); isResizing = true; startX = e.clientX; startY = e.clientY; startWidth = pipPlayer.offsetWidth; startHeight = pipPlayer.offsetHeight; }); document.addEventListener('mousemove', (e) => { if (isDragging) { currentX = e.clientX - startX; currentY = e.clientY - startY; // Keep within viewport bounds currentX = Math.max(0, Math.min(currentX, window.innerWidth - pipContainer.offsetWidth)); currentY = Math.max(0, Math.min(currentY, window.innerHeight - pipContainer.offsetHeight)); pipContainer.style.left = currentX + 'px'; pipContainer.style.top = currentY + 'px'; pipContainer.style.right = 'auto'; pipContainer.style.bottom = 'auto'; } else if (isResizing) { const deltaX = e.clientX - startX; const newWidth = Math.max(200, Math.min(800, startWidth + deltaX)); pipPlayer.style.width = newWidth + 'px'; } }); document.addEventListener('mouseup', () => { if (isDragging) { isDragging = false; pipPlayer.style.opacity = '1'; } if (isResizing) { isResizing = false; } }); } }; skipTaskBtn.addEventListener('click', () => { console.log('โฉ Dev skip - completing dual-video task'); statusArea.innerHTML = '
โœ… Task skipped (Dev Mode)
'; task.completed = true; const completeBtn = document.getElementById('interactive-complete-btn'); if (completeBtn) completeBtn.disabled = false; }); skipBtn.addEventListener('click', async () => { const minVideosRequired = (mainVideo === 'webcam') ? 1 : 2; if (availableVideos.length >= minVideosRequired) { // For webcam mode, only refresh the PiP video if (mainVideo === 'webcam') { const video = availableVideos[Math.floor(Math.random() * availableVideos.length)]; await window.videoPlayerManager.playOverlayVideo(video); statusArea.innerHTML = '
โญ๏ธ Loaded new PiP video
'; } else { await playDualVideos(); statusArea.innerHTML = '
โญ๏ธ Loaded new videos
'; } } }); btn.addEventListener('click', async () => { btn.disabled = true; btn.textContent = 'Loading...'; if (!window.videoPlayerManager) { statusArea.innerHTML = '
โš ๏ธ Video manager not initialized
'; task.completed = true; const completeBtn = document.getElementById('interactive-complete-btn'); if (completeBtn) completeBtn.disabled = false; return; } try { const videos = window.videoPlayerManager.getEnabledVideos('background') || []; // For webcam mode, we only need 1 video (for PiP) const minVideosRequired = (mainVideo === 'webcam') ? 1 : 2; if (videos.length < minVideosRequired) { statusArea.innerHTML = `
โ„น๏ธ Need at least ${minVideosRequired} video(s) - continuing anyway
`; task.completed = true; const completeBtn = document.getElementById('interactive-complete-btn'); if (completeBtn) completeBtn.disabled = false; return; } availableVideos = videos; await playDualVideos(); btn.style.display = 'none'; skipBtn.style.display = 'inline-block'; // Show dev skip button if dev mode is enabled if (skipTaskBtn && window.isDevMode && window.isDevMode()) { skipTaskBtn.style.display = 'inline-block'; } // Start caption cycling if enabled if (showCaptions && captionEl) { console.log('๐ŸŽฌ Caption overlay enabled'); console.log('๐ŸŽฌ Caption library available:', !!window.captionLibrary); const getRandomCaption = () => { // Fallback captions if library not loaded const fallbackCaptions = [ 'You love this...', 'Keep watching...', 'Good gooner...', 'Don\'t stop...', 'Edge for me...', 'You need this...' ]; if (!window.captionLibrary) { console.log('โš ๏ธ Caption library not loaded, using fallback'); return fallbackCaptions[Math.floor(Math.random() * fallbackCaptions.length)]; } // Check for user preferences in localStorage const savedPrefs = JSON.parse(localStorage.getItem('gooner-preferences') || '{}'); const preferredTones = savedPrefs.captionTones || []; let categories; if (preferredTones.length > 0) { // Use only preferred caption categories categories = preferredTones; console.log('๐ŸŽฌ Using preferred caption tones:', categories); } else { // Use all available categories categories = Object.keys(window.captionLibrary); } const randomCategory = categories[Math.floor(Math.random() * categories.length)]; const captions = window.captionLibrary[randomCategory]; return captions[Math.floor(Math.random() * captions.length)]; }; // Show first caption immediately const firstCaption = getRandomCaption(); console.log('๐ŸŽฌ Showing caption:', firstCaption); captionEl.textContent = firstCaption; captionEl.style.opacity = '1'; // Apply font size multiplier if set if (window.captionFontSizeMultiplier !== undefined) { const baseSize = parseFloat(captionEl.dataset.baseSize || '3'); captionEl.style.fontSize = (baseSize * window.captionFontSizeMultiplier) + 'em'; } // Cycle captions captionInterval_id = setInterval(() => { // Fade out captionEl.style.transition = 'opacity 0.5s'; captionEl.style.opacity = '0'; setTimeout(() => { // Change caption and fade in const newCaption = getRandomCaption(); console.log('๐ŸŽฌ Cycling to caption:', newCaption); captionEl.textContent = newCaption; captionEl.style.opacity = '1'; }, 500); }, captionInterval * 1000); } // Handle timer display (use the showTimer variable we set at the top) console.log('โฑ๏ธ Dual-video timer setup:', { showTimer, minDuration, params: task.params }); const countdownEl = showTimer ? document.getElementById('sidebar-countdown-display') : null; const countdownWrapper = showTimer ? document.getElementById('sidebar-countdown-timer') : null; console.log('โฑ๏ธ Found timer elements:', { wrapper: !!countdownWrapper, display: !!countdownEl }); let remainingTime = minDuration; const formatTime = (seconds) => { const mins = Math.floor(seconds / 60); const secs = seconds % 60; return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; }; // Show countdown in sidebar if showTimer is enabled if (showTimer && countdownWrapper && countdownEl) { console.log('โฑ๏ธ Showing countdown timer in sidebar - duration:', formatTime(remainingTime)); countdownWrapper.style.display = 'block'; countdownEl.textContent = formatTime(remainingTime); // Ensure game stats panel stays visible const gameStatsPanel = document.getElementById('game-stats-panel'); if (gameStatsPanel && gameStatsPanel.style.display !== 'none') { console.log('๐Ÿ“Š Keeping game stats panel visible alongside timer'); } } else { console.log('โฑ๏ธ Timer NOT shown - reasons:', { showTimer, hasWrapper: !!countdownWrapper, hasDisplay: !!countdownEl }); } const timerInterval = setInterval(() => { remainingTime--; if (showTimer && countdownEl) { countdownEl.textContent = formatTime(remainingTime); } if (remainingTime <= 0) { clearInterval(timerInterval); // Hide countdown timer if (showTimer && countdownWrapper) { countdownWrapper.style.display = 'none'; } statusArea.innerHTML = '
โœ… Dual video session complete
'; task.completed = true; const completeBtn = document.getElementById('interactive-complete-btn'); if (completeBtn) { completeBtn.disabled = false; } } }, 1000); // Store cleanup function task.cleanup = () => { clearInterval(timerInterval); if (captionInterval_id) clearInterval(captionInterval_id); // Remove event listeners if (mainVideoElement) { mainVideoElement.removeEventListener('ended', playNextMainVideo); } if (pipVideoElement) { pipVideoElement.removeEventListener('ended', playNextPipVideo); } // Hide countdown timer if (showTimer && countdownWrapper) { countdownWrapper.style.display = 'none'; } // Clean up overlay player and its wrapper container if (window.videoPlayerManager && window.videoPlayerManager.overlayPlayer) { const overlay = window.videoPlayerManager.overlayPlayer; // Stop webcam stream if it's a webcam PiP const video = overlay.querySelector('video'); if (video && video.srcObject) { const tracks = video.srcObject.getTracks(); tracks.forEach(track => track.stop()); } // Remove the wrapper container (which contains overlay + resize handle) const wrapper = overlay.parentElement; if (wrapper && wrapper !== document.body) { wrapper.remove(); } else { // If no wrapper, just remove the overlay directly overlay.remove(); } window.videoPlayerManager.overlayPlayer = null; } // Clean up main webcam if it exists const mainVideo = mainContainer.querySelector('video'); if (mainVideo && mainVideo.srcObject) { const tracks = mainVideo.srcObject.getTracks(); tracks.forEach(track => track.stop()); } }; } catch (error) { console.error('Dual video error:', error); statusArea.innerHTML = `
โš ๏ธ ${error.message} - Continuing anyway
`; task.completed = true; const completeBtn = document.getElementById('interactive-complete-btn'); if (completeBtn) { completeBtn.disabled = false; } } }); } validateDualVideoTask(task) { return task.completed === true; } /** * TTS Command Task */ async createTTSCommandTask(task, container) { const text = task.params?.text || 'Good gooner. Edge again.'; const voice = task.params?.voice || 'feminine'; container.innerHTML = `

๐ŸŽค Voice Command

Listen to the voice command and follow instructions.

"${text}"
`; const btn = container.querySelector('#play-tts-btn'); btn.addEventListener('click', async () => { // Feature TTS always works regardless of global TTS toggle if (this.voiceManager) { try { // Create flashing text overlay const flashOverlay = document.createElement('div'); flashOverlay.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(0, 0, 0, 0.9); color: #ff00ff; padding: 40px 60px; font-size: 48px; font-weight: bold; text-align: center; z-index: 10000; border: 3px solid #ff00ff; border-radius: 10px; box-shadow: 0 0 30px rgba(255, 0, 255, 0.8); animation: flashText 0.5s ease-in-out infinite; max-width: 80%; word-wrap: break-word; `; flashOverlay.textContent = text; // Add CSS animation if not already present if (!document.getElementById('tts-flash-style')) { const style = document.createElement('style'); style.id = 'tts-flash-style'; style.textContent = ` @keyframes flashText { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } } `; document.head.appendChild(style); } document.body.appendChild(flashOverlay); await this.voiceManager.speak(text); // Remove overlay after speaking flashOverlay.remove(); task.completed = true; btn.disabled = true; btn.textContent = 'โœ… Command Received'; const completeBtn = document.getElementById('interactive-complete-btn'); if (completeBtn) { completeBtn.disabled = false; } } catch (error) { console.error('TTS error:', error); // Remove overlay if error occurs const overlay = document.querySelector('[style*="flashText"]'); if (overlay) overlay.remove(); } } else { // TTS not available, auto-complete task.completed = true; btn.disabled = true; btn.textContent = 'โœ… Command Received (TTS unavailable)'; const completeBtn = document.getElementById('interactive-complete-btn'); if (completeBtn) { completeBtn.disabled = false; } } }); } validateTTSCommandTask(task) { return task.completed === true; } /** * Quad Video Task */ async createQuadVideoTask(task, container) { const layout = task.params?.layout || 'grid'; const minDuration = task.params?.minDuration || 180; // Default 3 minutes const duration = task.params?.duration || minDuration; const showTimer = task.params?.showTimer || false; const ambientAudio = task.params?.ambientAudio; const ambientVolume = task.params?.ambientVolume || 0.5; const hypnoOverlay = task.params?.hypnoOverlay || false; container.innerHTML = `

๐ŸŽฌ Quad Video Mode${hypnoOverlay ? ' ๐ŸŒ€' : ''}

Watch four videos simultaneously in a ${layout} layout.

Minimum viewing time: ${Math.floor(duration / 60)} minutes

`; const btn = container.querySelector('#start-quad-video-btn'); const skipBtn = container.querySelector('#skip-quad-video-btn'); const skipTaskBtn = container.querySelector('#skip-quad-video-task-btn'); const fullscreenBtn = container.querySelector('#fullscreen-quad-video-btn'); const statusArea = container.querySelector('#quad-video-status'); const gridContainer = container.querySelector('#quad-video-grid'); let availableVideos = []; const playedVideos = new Set(); const videoElements = [null, null, null, null]; // Handle wrapper hover for controls for (let i = 1; i <= 4; i++) { const wrapper = container.querySelector(`#quad-video-${i}-wrapper`); const controls = wrapper.querySelector('.quad-video-controls'); wrapper.addEventListener('mouseenter', () => { controls.style.opacity = '1'; }); wrapper.addEventListener('mouseleave', () => { controls.style.opacity = '0'; }); } // Fullscreen toggle fullscreenBtn.addEventListener('click', () => { if (!document.fullscreenElement) { gridContainer.requestFullscreen(); fullscreenBtn.innerHTML = '๐Ÿ—™ Exit Fullscreen'; } else { document.exitFullscreen(); fullscreenBtn.innerHTML = '๐Ÿ–ฅ๏ธ Fullscreen'; } }); const playQuadVideos = async () => { if (availableVideos.length < 4) return; // Select 4 random unique videos const selectedVideos = []; const availableIndices = [...Array(availableVideos.length).keys()]; for (let i = 0; i < 4; i++) { const randomIndex = Math.floor(Math.random() * availableIndices.length); selectedVideos.push(availableVideos[availableIndices[randomIndex]]); availableIndices.splice(randomIndex, 1); } // Create 4 video elements in the grid gridContainer.style.display = 'block'; for (let i = 0; i < 4; i++) { const videoContainer = container.querySelector(`#quad-video-${i + 1}`); videoContainer.innerHTML = ''; // Clear existing video const videoEl = document.createElement('video'); videoEl.style.cssText = 'width: 100%; height: auto; display: block;'; videoEl.autoplay = true; videoEl.loop = false; videoEl.volume = 0.5; // Default 50% volume const videoPath = selectedVideos[i]; videoEl.src = window.videoPlayerManager.getVideoPath(videoPath); // Auto-advance to next video when ended videoEl.addEventListener('ended', () => { const unplayedVideos = availableVideos.filter(v => !playedVideos.has(v)); if (unplayedVideos.length === 0 && availableVideos.length > 0) { playedVideos.clear(); // Reset if all videos played } const videosToChooseFrom = unplayedVideos.length > 0 ? unplayedVideos : availableVideos; if (videosToChooseFrom.length > 0) { const randomVideo = videosToChooseFrom[Math.floor(Math.random() * videosToChooseFrom.length)]; playedVideos.add(randomVideo); const randomPath = typeof randomVideo === 'object' ? randomVideo.path : randomVideo; videoEl.src = window.videoPlayerManager.getVideoPath(randomPath); videoEl.play().catch(e => console.warn('Failed to play next video:', e)); } }); videoContainer.appendChild(videoEl); videoElements[i] = videoEl; // Track for volume control playedVideos.add(videoPath); try { await videoEl.play(); } catch (e) { console.warn(`Failed to play video ${i + 1}:`, e); } } }; const loadSingleVideo = async (index) => { if (availableVideos.length === 0) return; // Select random video const randomVideo = availableVideos[Math.floor(Math.random() * availableVideos.length)]; const videoContainer = container.querySelector(`#quad-video-${index + 1}`); videoContainer.innerHTML = ''; // Clear existing video const videoEl = document.createElement('video'); videoEl.style.cssText = 'width: 100%; height: auto; display: block;'; videoEl.autoplay = true; videoEl.loop = false; // Preserve volume from slider const volumeSlider = container.querySelector(`#volume-${index + 1}`); videoEl.volume = (volumeSlider ? volumeSlider.value : 50) / 100; // Auto-advance to next video when ended videoEl.addEventListener('ended', () => { const unplayedVideos = availableVideos.filter(v => !playedVideos.has(v)); if (unplayedVideos.length === 0 && availableVideos.length > 0) { playedVideos.clear(); // Reset if all videos played } const videosToChooseFrom = unplayedVideos.length > 0 ? unplayedVideos : availableVideos; if (videosToChooseFrom.length > 0) { const randomVideo = videosToChooseFrom[Math.floor(Math.random() * videosToChooseFrom.length)]; playedVideos.add(randomVideo); const randomPath = typeof randomVideo === 'object' ? randomVideo.path : randomVideo; videoEl.src = window.videoPlayerManager.getVideoPath(randomPath); videoEl.play().catch(e => console.warn('Failed to play next video:', e)); } }); const videoPath = randomVideo; videoEl.src = window.videoPlayerManager.getVideoPath(videoPath); videoContainer.appendChild(videoEl); videoElements[index] = videoEl; // Update tracked element try { await videoEl.play(); statusArea.innerHTML = `
โญ๏ธ Loaded new video ${index + 1}
`; } catch (e) { console.warn(`Failed to play video ${index + 1}:`, e); } }; // Setup individual skip buttons for (let i = 0; i < 4; i++) { const skipVideoBtn = container.querySelector(`#skip-video-${i + 1}`); skipVideoBtn.addEventListener('click', () => loadSingleVideo(i)); // Setup volume controls const volumeSlider = container.querySelector(`#volume-${i + 1}`); const volumeDisplay = container.querySelector(`#volume-display-${i + 1}`); volumeSlider.addEventListener('input', (e) => { const volume = e.target.value; volumeDisplay.textContent = `${volume}%`; if (videoElements[i]) { videoElements[i].volume = volume / 100; } }); } skipTaskBtn.addEventListener('click', () => { console.log('โฉ Dev skip - completing quad-video task'); statusArea.innerHTML = '
โœ… Task skipped (Dev Mode)
'; task.completed = true; const completeBtn = document.getElementById('interactive-complete-btn'); if (completeBtn) completeBtn.disabled = false; }); skipBtn.addEventListener('click', async () => { if (availableVideos.length >= 4) { await playQuadVideos(); statusArea.innerHTML = '
โญ๏ธ Loaded 4 new videos
'; } }); btn.addEventListener('click', async () => { btn.disabled = true; btn.textContent = 'Loading...'; if (!window.videoPlayerManager) { statusArea.innerHTML = '
โš ๏ธ Video manager not initialized
'; task.completed = true; const completeBtn = document.getElementById('interactive-complete-btn'); if (completeBtn) completeBtn.disabled = false; return; } try { const videos = window.videoPlayerManager.getEnabledVideos('background') || []; if (videos.length < 4) { statusArea.innerHTML = '
โ„น๏ธ Need at least 4 videos - continuing anyway
'; task.completed = true; const completeBtn = document.getElementById('interactive-complete-btn'); if (completeBtn) completeBtn.disabled = false; return; } availableVideos = videos; await playQuadVideos(); // Start ambient audio playlist const player = this.createAmbientAudioPlayer(ambientVolume); if (player) { ambientAudioElement = player.element; try { await ambientAudioElement.play(); } catch (e) { console.warn('Failed to play ambient audio:', e); } } btn.style.display = 'none'; skipBtn.style.display = 'inline-block'; fullscreenBtn.style.display = 'inline-block'; // Show sidebar countdown timer if enabled const countdownEl = showTimer ? document.getElementById('sidebar-countdown-display') : null; const countdownWrapper = showTimer ? document.getElementById('sidebar-countdown-timer') : null; if (showTimer && countdownWrapper) { countdownWrapper.style.display = 'block'; // Ensure game stats panel stays visible const gameStatsPanel = document.getElementById('game-stats-panel'); if (gameStatsPanel && gameStatsPanel.style.display !== 'none') { console.log('๐Ÿ“Š Keeping game stats panel visible alongside timer'); } } // Start countdown timer let remainingTime = duration; if (showTimer && countdownEl) { const minutes = Math.floor(remainingTime / 60); const seconds = remainingTime % 60; countdownEl.textContent = `${minutes}:${seconds.toString().padStart(2, '0')}`; } const timerInterval = setInterval(() => { remainingTime--; if (remainingTime > 0) { if (showTimer && countdownEl) { const mins = Math.floor(remainingTime / 60); const secs = remainingTime % 60; countdownEl.textContent = `${mins}:${secs.toString().padStart(2, '0')}`; } } else { clearInterval(timerInterval); if (showTimer && countdownEl) { countdownEl.textContent = 'โœ…'; countdownEl.style.color = 'var(--color-success, #4caf50)'; } if (showTimer && countdownWrapper) { countdownWrapper.style.display = 'none'; } statusArea.innerHTML = '
โœ… Quad video session complete
'; task.completed = true; const completeBtn = document.getElementById('interactive-complete-btn'); if (completeBtn) { completeBtn.disabled = false; } } }, 1000); } catch (error) { console.error('Quad video error:', error); statusArea.innerHTML = `
โš ๏ธ ${error.message} - Continuing anyway
`; task.completed = true; const completeBtn = document.getElementById('interactive-complete-btn'); if (completeBtn) { completeBtn.disabled = false; } } }); // Cleanup function task.cleanup = () => { if (ambientAudioElement) { ambientAudioElement.pause(); ambientAudioElement = null; } }; } validateQuadVideoTask(task) { return task.completed === true; } /** * Chaos Mode Quad Video Task - Videos randomly swap, volumes change, TTS interrupts */ async createChaosQuadVideoTask(task, container) { const layout = task.params?.layout || 'grid'; const minDuration = task.params?.minDuration || 300; // Default 5 minutes const ambientAudio = task.params?.ambientAudio; const ambientVolume = task.params?.ambientVolume || 0.5; const swapInterval = task.params?.swapInterval || [60, 90]; // Random 60-90 seconds const ttsEnabled = task.params?.tts || false; const webcamCheck = task.params?.webcam || false; container.innerHTML = `

๐ŸŒ€ CHAOS MODE ๐ŸŒ€

โš ๏ธ UNPREDICTABLE: Videos will randomly swap positions, volumes will fluctuate, chaos reigns!

Minimum chaos time: ${minDuration} seconds

`; const btn = container.querySelector('#start-chaos-quad-video-btn'); const fullscreenBtn = container.querySelector('#fullscreen-chaos-quad-video-btn'); const statusArea = container.querySelector('#chaos-status'); const gridContainer = container.querySelector('#chaos-quad-video-grid'); let availableVideos = []; let playedVideos = new Set(); // Track played videos to avoid repeats let videoElements = [null, null, null, null]; let ambientAudioElement = null; let chaosIntervals = []; // Fullscreen functionality fullscreenBtn.addEventListener('click', () => { if (gridContainer.requestFullscreen) { gridContainer.requestFullscreen(); } else if (gridContainer.webkitRequestFullscreen) { gridContainer.webkitRequestFullscreen(); } else if (gridContainer.msRequestFullscreen) { gridContainer.msRequestFullscreen(); } }); const playQuadVideos = async () => { if (availableVideos.length < 4) return; // Select 4 random unique videos const selectedVideos = []; const availableIndices = [...Array(availableVideos.length).keys()]; for (let i = 0; i < 4; i++) { const randomIndex = Math.floor(Math.random() * availableIndices.length); selectedVideos.push(availableVideos[availableIndices[randomIndex]]); availableIndices.splice(randomIndex, 1); } gridContainer.style.display = 'block'; for (let i = 0; i < 4; i++) { const videoContainer = container.querySelector(`#chaos-video-${i + 1}`); videoContainer.innerHTML = ''; const videoEl = document.createElement('video'); videoEl.style.cssText = 'width: 100%; height: auto; display: block;'; videoEl.autoplay = true; videoEl.loop = false; videoEl.volume = 0.5; const videoPath = selectedVideos[i]; videoEl.src = window.videoPlayerManager.getVideoPath(videoPath); // Auto-advance to next video when ended videoEl.addEventListener('ended', () => { const unplayedVideos = availableVideos.filter(v => !playedVideos.has(v)); if (unplayedVideos.length === 0 && availableVideos.length > 0) { playedVideos.clear(); // Reset if all videos played } const videosToChooseFrom = unplayedVideos.length > 0 ? unplayedVideos : availableVideos; if (videosToChooseFrom.length > 0) { const nextVideo = videosToChooseFrom[Math.floor(Math.random() * videosToChooseFrom.length)]; playedVideos.add(nextVideo); videoEl.src = window.videoPlayerManager.getVideoPath(nextVideo); videoEl.play().catch(e => console.warn('Autoplay failed:', e)); } }); videoContainer.appendChild(videoEl); videoElements[i] = videoEl; try { await videoEl.play(); } catch (e) { console.warn(`Failed to play chaos video ${i + 1}:`, e); } } }; const swapTwoVideos = () => { // Pick two random positions const pos1 = Math.floor(Math.random() * 4); let pos2 = Math.floor(Math.random() * 4); while (pos2 === pos1) { pos2 = Math.floor(Math.random() * 4); } // Swap the video elements const container1 = container.querySelector(`#chaos-video-${pos1 + 1}`); const container2 = container.querySelector(`#chaos-video-${pos2 + 1}`); const temp = videoElements[pos1]; videoElements[pos1] = videoElements[pos2]; videoElements[pos2] = temp; // Visual swap with animation container1.innerHTML = ''; container2.innerHTML = ''; if (videoElements[pos1]) container1.appendChild(videoElements[pos1]); if (videoElements[pos2]) container2.appendChild(videoElements[pos2]); statusArea.innerHTML = `
๐Ÿ”€ Videos ${pos1 + 1} and ${pos2 + 1} SWAPPED!
`; }; const randomizeVolume = () => { const randomVideo = Math.floor(Math.random() * 4); const randomVolume = Math.random(); // 0 to 1 if (videoElements[randomVideo]) { videoElements[randomVideo].volume = randomVolume; const percentage = Math.round(randomVolume * 100); statusArea.innerHTML = `
๐Ÿ”Š Video ${randomVideo + 1} volume changed to ${percentage}%
`; } }; const ttsInterrupt = async () => { if (!this.ttsEnabled || !this.voiceManager) return; const commands = [ 'Keep watching.', 'Don\'t look away.', 'Focus harder.', 'Let the chaos consume you.', 'You can\'t escape.', 'Surrender to the videos.', 'Edge for me.', 'Faster.', 'Don\'t you dare cum.', 'Good gooner.' ]; const randomCommand = commands[Math.floor(Math.random() * commands.length)]; statusArea.innerHTML = `
๐ŸŽค "${randomCommand}"
`; try { await this.voiceManager.speak(randomCommand); } catch (e) { console.warn('TTS failed:', e); } }; const getCheckInText = () => { // Get user preferences const prefs = window.gameData?.academyPreferences; if (!prefs || !window.webcamCheckInTexts) { return window.webcamCheckInTexts?.generic?.[0] || "Keep watching. Keep stroking."; } const textLib = window.webcamCheckInTexts; const enabledCategories = []; // Check caption tone preferences (highest priority) const tones = prefs.captionTone || {}; if (tones.encouraging) enabledCategories.push('encouraging'); if (tones.mocking) enabledCategories.push('mocking'); if (tones.commanding) enabledCategories.push('commanding'); if (tones.seductive) enabledCategories.push('seductive'); if (tones.degrading) enabledCategories.push('degrading'); if (tones.playful) enabledCategories.push('playful'); if (tones.serious) enabledCategories.push('serious'); if (tones.casual) enabledCategories.push('casual'); if (tones.extreme) enabledCategories.push('extreme'); // Check content theme preferences const themes = prefs.contentThemes || {}; if (themes.dominance) enabledCategories.push('dominance'); if (themes.submission) enabledCategories.push('submission'); if (themes.humiliation) enabledCategories.push('humiliation'); if (themes.worship) enabledCategories.push('worship'); if (themes.edging) enabledCategories.push('edging'); if (themes.denial) enabledCategories.push('denial'); if (themes.mindbreak) enabledCategories.push('mindbreak'); if (themes.gooning) enabledCategories.push('gooning'); if (themes.sissy) enabledCategories.push('sissy'); if (themes.bbc) enabledCategories.push('bbc'); if (themes.feet) enabledCategories.push('feet'); if (themes.cei) enabledCategories.push('cei'); if (themes.pov) enabledCategories.push('pov'); if (themes.joi) enabledCategories.push('joi'); if (themes.femdom) enabledCategories.push('femdom'); if (themes.maledom) enabledCategories.push('maledom'); // Select random category from enabled ones if (enabledCategories.length === 0) { enabledCategories.push('generic'); } const randomCategory = enabledCategories[Math.floor(Math.random() * enabledCategories.length)]; const textsInCategory = textLib[randomCategory]; if (!textsInCategory || textsInCategory.length === 0) { return textLib.generic[0]; } return textsInCategory[Math.floor(Math.random() * textsInCategory.length)]; }; const webcamCheckIn = async () => { if (!this.game?.webcamManager) { statusArea.innerHTML = `
๐Ÿ“ธ Webcam checking... (Not available)
`; return; } const checkInText = getCheckInText(); statusArea.innerHTML = `
๐Ÿ“ธ WEBCAM CHECK-IN - 15 seconds
`; try { // Start webcam const accessGranted = await this.game.webcamManager.requestCameraAccess(); if (!accessGranted) { statusArea.innerHTML = `
โŒ Webcam access denied
`; return; } // Determine if we're in fullscreen and get the container const fullscreenElement = document.fullscreenElement || document.webkitFullscreenElement || document.msFullscreenElement; const targetContainer = fullscreenElement || document.body; // Create webcam overlay const overlay = document.createElement('div'); overlay.id = 'chaos-webcam-overlay'; overlay.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 10000; background: rgba(0,0,0,0.95); padding: 20px; border-radius: 15px; border: 3px solid #00ffff; box-shadow: 0 0 30px rgba(0,255,255,0.5); max-width: 90vw; `; const videoEl = this.game.webcamManager.video; videoEl.style.cssText = 'width: 640px; max-width: 100%; height: 480px; border-radius: 10px; display: block;'; // Add text overlay const textDiv = document.createElement('div'); textDiv.style.cssText = ` color: #ffffff; text-align: center; font-size: 1.8em; margin-bottom: 15px; font-weight: bold; text-shadow: 2px 2px 4px rgba(0,0,0,0.8); padding: 10px; background: rgba(0,255,255,0.1); border-radius: 8px; border: 2px solid rgba(0,255,255,0.3); `; textDiv.textContent = checkInText; const timerDiv = document.createElement('div'); timerDiv.style.cssText = 'color: #00ffff; text-align: center; font-size: 1.5em; margin-top: 10px; font-weight: bold;'; timerDiv.textContent = '15s'; overlay.appendChild(textDiv); overlay.appendChild(videoEl); overlay.appendChild(timerDiv); // Append to fullscreen container if in fullscreen, otherwise body targetContainer.appendChild(overlay); // Countdown timer let countdown = 15; const countdownInterval = setInterval(() => { countdown--; timerDiv.textContent = `${countdown}s`; if (countdown <= 0) { clearInterval(countdownInterval); // Close webcam this.game.webcamManager.stopCamera(); videoEl.style.display = 'none'; overlay.remove(); statusArea.innerHTML = `
โœ… Check-in complete
`; } }, 1000); } catch (e) { console.error('Webcam check-in failed:', e); statusArea.innerHTML = `
โŒ Webcam error
`; } }; btn.addEventListener('click', async () => { btn.disabled = true; btn.textContent = 'Loading...'; if (!window.videoPlayerManager) { statusArea.innerHTML = '
โš ๏ธ Video manager not initialized
'; task.completed = true; const completeBtn = document.getElementById('interactive-complete-btn'); if (completeBtn) completeBtn.disabled = false; return; } try { const videos = window.videoPlayerManager.getEnabledVideos('background') || []; if (videos.length < 4) { statusArea.innerHTML = '
โ„น๏ธ Need at least 4 videos - continuing anyway
'; task.completed = true; const completeBtn = document.getElementById('interactive-complete-btn'); if (completeBtn) completeBtn.disabled = false; return; } availableVideos = videos; await playQuadVideos(); // Start ambient audio playlist const player = this.createAmbientAudioPlayer(ambientVolume); if (player) { ambientAudioElement = player.element; try { await ambientAudioElement.play(); } catch (e) { console.warn('Failed to play ambient audio:', e); } } // Setup chaos intervals // Video swaps every 60-90 seconds const scheduleNextSwap = () => { const delay = (swapInterval[0] + Math.random() * (swapInterval[1] - swapInterval[0])) * 1000; const timeout = setTimeout(() => { swapTwoVideos(); scheduleNextSwap(); }, delay); chaosIntervals.push(timeout); }; scheduleNextSwap(); // Random volume changes every 30-60 seconds const scheduleNextVolume = () => { const delay = (30 + Math.random() * 30) * 1000; const timeout = setTimeout(() => { randomizeVolume(); scheduleNextVolume(); }, delay); chaosIntervals.push(timeout); }; scheduleNextVolume(); // Ambient audio volume randomization every 45-75 seconds if (ambientAudioElement) { const scheduleAmbientChange = () => { const delay = (45 + Math.random() * 30) * 1000; const timeout = setTimeout(() => { if (ambientAudioElement) { const newVolume = 0.3 + Math.random() * 0.5; // 30-80% ambientAudioElement.volume = newVolume; statusArea.innerHTML = `
๐ŸŽต Ambient audio: ${Math.round(newVolume * 100)}%
`; } scheduleAmbientChange(); }, delay); chaosIntervals.push(timeout); }; scheduleAmbientChange(); } // TTS interruptions if enabled if (ttsEnabled) { const scheduleTTS = () => { const delay = (40 + Math.random() * 50) * 1000; // 40-90 seconds const timeout = setTimeout(async () => { await ttsInterrupt(); scheduleTTS(); }, delay); chaosIntervals.push(timeout); }; scheduleTTS(); } // Webcam checks if enabled if (webcamCheck) { const scheduleWebcam = () => { const delay = (120 + Math.random() * 120) * 1000; // 2-4 minutes const timeout = setTimeout(() => { webcamCheckIn(); scheduleWebcam(); }, delay); chaosIntervals.push(timeout); }; scheduleWebcam(); } btn.style.display = 'none'; fullscreenBtn.style.display = 'inline-block'; // Show sidebar and timer const sidebar = document.getElementById('campaign-sidebar'); const timerWrapper = document.getElementById('focus-timer-display-wrapper'); const timerDisplay = document.getElementById('focus-timer-display'); if (sidebar) sidebar.style.display = 'block'; if (timerWrapper) timerWrapper.style.display = 'block'; // Start countdown timer in sidebar let remainingTime = minDuration; if (timerDisplay) { const minutes = Math.floor(remainingTime / 60); const seconds = remainingTime % 60; timerDisplay.innerHTML = `${minutes}:${seconds.toString().padStart(2, '0')}`; } const timerInterval = setInterval(() => { remainingTime--; if (remainingTime > 0) { if (timerDisplay) { const mins = Math.floor(remainingTime / 60); const secs = remainingTime % 60; timerDisplay.innerHTML = `${mins}:${secs.toString().padStart(2, '0')}`; } } else { clearInterval(timerInterval); // Clear all chaos intervals chaosIntervals.forEach(interval => clearTimeout(interval)); chaosIntervals = []; if (timerDisplay) { timerDisplay.innerHTML = 'โœ…'; timerDisplay.style.color = 'var(--color-success, #4caf50)'; } statusArea.innerHTML = '
โœ… CHAOS SURVIVED
'; task.completed = true; const completeBtn = document.getElementById('interactive-complete-btn'); if (completeBtn) completeBtn.disabled = false; } }, 1000); statusArea.innerHTML = '
โš ๏ธ CHAOS MODE ACTIVE - Expect the unexpected!
'; } catch (error) { console.error('Chaos quad video error:', error); statusArea.innerHTML = `
โš ๏ธ ${error.message} - Continuing anyway
`; task.completed = true; const completeBtn = document.getElementById('interactive-complete-btn'); if (completeBtn) completeBtn.disabled = false; } }); // Cleanup function task.cleanup = () => { chaosIntervals.forEach(interval => clearTimeout(interval)); chaosIntervals = []; if (ambientAudioElement) { ambientAudioElement.pause(); ambientAudioElement = null; } }; } validateChaosQuadVideoTask(task) { return task.completed === true; } /** * Chaos Triple Video Task - 3 videos with random swaps and volume changes */ async createChaosTripleVideoTask(task, container) { const duration = task.params?.duration || 300; const ambientAudio = task.params?.ambientAudio; const ambientVolume = task.params?.ambientVolume || 0.5; const showCaptions = task.params?.showCaptions || false; const captionInterval = task.params?.captionInterval || 10; const useTags = task.params?.useTags || false; const prioritizePreferences = task.params?.prioritizePreferences || false; container.innerHTML = `

๐ŸŒ€ Triple Chaos

Three videos. Random swaps. Volume chaos. Let it overwhelm you.

${showCaptions ? '
' : ''}
${duration}s remaining
`; const btn = document.getElementById('start-chaos-triple-btn'); const statusArea = document.getElementById('chaos-triple-status'); const gridContainer = document.getElementById('chaos-triple-video-grid'); const timerEl = document.getElementById('chaos-triple-timer'); const captionEl = showCaptions ? document.getElementById('chaos-triple-caption') : null; let availableVideos = []; let playedVideos = new Set(); // Track played videos to avoid repeats let videoElements = [null, null, null]; let ambientAudioElement = null; let chaosIntervals = []; let captionInterval_handle = null; // Get filtered videos by preferences const getPreferenceFilteredVideos = (allVideos) => { if (!prioritizePreferences || !useTags) { return allVideos; } const prefs = JSON.parse(localStorage.getItem('gooner-preferences') || '{}'); const preferredTags = prefs.preferredTags || []; if (preferredTags.length === 0) { return allVideos; } const mediaTagsData = JSON.parse(localStorage.getItem('library-mediaTags') || '{}'); const mediaTags = mediaTagsData.mediaTags || {}; const matchedVideos = allVideos.filter(videoPath => { const pathStr = typeof videoPath === 'string' ? videoPath : (videoPath.path || videoPath.src || ''); const normalizedPath = pathStr.startsWith('./') ? pathStr : `./${pathStr}`; const videoTags = mediaTags[normalizedPath] || []; return videoTags.some(tagId => preferredTags.includes(tagId)); }); return matchedVideos.length > 0 ? matchedVideos : allVideos; }; // Get random caption const getRandomCaption = () => { const prefs = JSON.parse(localStorage.getItem('gooner-preferences') || '{}'); const preferredTones = prefs.captionTones || []; if (preferredTones.length === 0 || !window.captionLibrary) { return 'Keep watching...'; } const randomTone = preferredTones[Math.floor(Math.random() * preferredTones.length)]; const toneLibrary = window.captionLibrary[randomTone]; if (!toneLibrary || toneLibrary.length === 0) { return 'Keep watching...'; } return toneLibrary[Math.floor(Math.random() * toneLibrary.length)]; }; // Update caption with fade const updateCaption = () => { if (!captionEl) return; captionEl.style.opacity = '0'; setTimeout(() => { captionEl.textContent = getRandomCaption(); captionEl.style.opacity = '1'; }, 500); }; const playTripleVideos = async () => { if (availableVideos.length < 3) return; const selectedVideos = []; const availableIndices = [...Array(availableVideos.length).keys()]; for (let i = 0; i < 3; i++) { const randomIndex = Math.floor(Math.random() * availableIndices.length); selectedVideos.push(availableVideos[availableIndices[randomIndex]]); availableIndices.splice(randomIndex, 1); } gridContainer.style.display = 'block'; for (let i = 0; i < 3; i++) { const videoContainer = document.getElementById(`chaos-triple-${i + 1}`); videoContainer.innerHTML = ''; const videoEl = document.createElement('video'); videoEl.style.cssText = 'width: 100%; height: auto; display: block;'; videoEl.autoplay = true; videoEl.loop = false; videoEl.volume = 0.5; const videoPath = selectedVideos[i]; videoEl.src = window.videoPlayerManager.getVideoPath(videoPath); // Auto-advance to next video when ended videoEl.addEventListener('ended', () => { const unplayedVideos = availableVideos.filter(v => !playedVideos.has(v)); if (unplayedVideos.length === 0 && availableVideos.length > 0) { playedVideos.clear(); // Reset if all videos played } const videosToChooseFrom = unplayedVideos.length > 0 ? unplayedVideos : availableVideos; if (videosToChooseFrom.length > 0) { const nextVideo = videosToChooseFrom[Math.floor(Math.random() * videosToChooseFrom.length)]; playedVideos.add(nextVideo); videoEl.src = window.videoPlayerManager.getVideoPath(nextVideo); videoEl.play().catch(e => console.warn('Autoplay failed:', e)); } }); videoContainer.appendChild(videoEl); videoElements[i] = videoEl; try { await videoEl.play(); } catch (e) { console.warn(`Failed to play triple video ${i + 1}:`, e); } } }; const swapTwoVideos = () => { const pos1 = Math.floor(Math.random() * 3); let pos2 = Math.floor(Math.random() * 3); while (pos2 === pos1) { pos2 = Math.floor(Math.random() * 3); } const container1 = document.getElementById(`chaos-triple-${pos1 + 1}`); const container2 = document.getElementById(`chaos-triple-${pos2 + 1}`); const temp = videoElements[pos1]; videoElements[pos1] = videoElements[pos2]; videoElements[pos2] = temp; container1.innerHTML = ''; container2.innerHTML = ''; if (videoElements[pos1]) container1.appendChild(videoElements[pos1]); if (videoElements[pos2]) container2.appendChild(videoElements[pos2]); statusArea.innerHTML = `
๐Ÿ”€ Swapped!
`; }; const randomizeVolume = () => { const randomVideo = Math.floor(Math.random() * 3); const randomVolume = Math.random(); if (videoElements[randomVideo]) { videoElements[randomVideo].volume = randomVolume; statusArea.innerHTML = `
๐Ÿ”Š Volume ${Math.round(randomVolume * 100)}%
`; } }; btn.addEventListener('click', async () => { btn.disabled = true; btn.textContent = 'Loading...'; if (!window.videoPlayerManager) { statusArea.innerHTML = '
โš ๏ธ Video manager not initialized
'; task.completed = true; return; } try { const allVideos = window.videoPlayerManager.getEnabledVideos('background') || []; if (allVideos.length < 3) { statusArea.innerHTML = '
โ„น๏ธ Need at least 3 videos
'; task.completed = true; return; } availableVideos = getPreferenceFilteredVideos(allVideos); await playTripleVideos(); // Start ambient audio playlist const globalVolume = window.ambientAudioVolume; const effectiveVolume = (globalVolume !== undefined && globalVolume > 0) ? globalVolume : ambientVolume; const player = this.createAmbientAudioPlayer(effectiveVolume); if (player) { ambientAudioElement = player.element; ambientAudioElement.setAttribute('data-ambient', 'true'); ambientAudioElement.style.display = 'none'; document.body.appendChild(ambientAudioElement); ambientAudioElement.play().catch(e => console.warn('Audio error:', e)); } // Start captions if (showCaptions && captionEl) { updateCaption(); captionInterval_handle = setInterval(updateCaption, captionInterval * 1000); } // Chaos intervals const scheduleNextSwap = () => { const delay = (60 + Math.random() * 30) * 1000; const timeout = setTimeout(() => { swapTwoVideos(); scheduleNextSwap(); }, delay); chaosIntervals.push(timeout); }; scheduleNextSwap(); const scheduleNextVolume = () => { const delay = (30 + Math.random() * 30) * 1000; const timeout = setTimeout(() => { randomizeVolume(); scheduleNextVolume(); }, delay); chaosIntervals.push(timeout); }; scheduleNextVolume(); btn.style.display = 'none'; statusArea.innerHTML = '
โš ๏ธ CHAOS ACTIVE
'; // Timer countdown let timeRemaining = duration; const timerInterval = setInterval(() => { timeRemaining--; timerEl.textContent = `${timeRemaining}s`; if (timeRemaining <= 0) { clearInterval(timerInterval); chaosIntervals.forEach(interval => clearTimeout(interval)); if (captionInterval_handle) clearInterval(captionInterval_handle); if (ambientAudioElement) ambientAudioElement.pause(); statusArea.innerHTML = '
โœ… Chaos Complete
'; task.completed = true; } }, 1000); } catch (error) { console.error('Triple chaos error:', error); statusArea.innerHTML = `
โš ๏ธ ${error.message}
`; task.completed = true; } }); // Cleanup task.cleanup = () => { chaosIntervals.forEach(interval => clearTimeout(interval)); if (captionInterval_handle) clearInterval(captionInterval_handle); if (ambientAudioElement) { ambientAudioElement.pause(); ambientAudioElement = null; } }; } validateChaosTripleVideoTask(task) { return task.completed === true; } /** * Rhythm Training Task - Metronome-guided rhythm training */ async createRhythmTrainingTask(task, container) { const duration = task.params?.duration || 900; const tempo = task.params?.tempo || 60; const showMetronome = task.params?.showMetronome !== false; const webcamActive = task.params?.webcamActive || false; const backgroundAudio = task.params?.backgroundAudio || null; const audioVolume = task.params?.audioVolume || 0.3; const customTempoSequence = task.params?.tempoSequence || null; // Accept custom tempo sequence let timerInterval = null; let audioElement = null; let currentTempo = tempo; let beatCount = 0; let timeRemaining = duration; // Use custom tempo sequence if provided, otherwise use default 5-minute progression const tempoSequence = customTempoSequence ? customTempoSequence.map(seg => seg.tempo) : [60, 120, 150, 60, 150, 200, 240]; const segmentDuration = customTempoSequence ? (customTempoSequence[0].duration * 1000) : // Use duration from first segment (all should be same) ((5 * 60 * 1000) / tempoSequence.length); let currentSegmentIndex = 0; let segmentStartTime = null; // Build tempo progression display string const tempoProgressionText = tempoSequence.join(' โ†’ '); container.innerHTML = `

๐ŸŽต Rhythm Training

Follow the beat. Sync your strokes. Master the rhythm.

${webcamActive ? '
' : ''}
-
Next beat in:
-
${tempo} BPM
Tempo Progression: ${tempoProgressionText}
Time Remaining: ${duration}s
๐Ÿ’ก Tip: Stroke once per beat. Find your perfect tempo.
`; const startBtn = document.getElementById('start-rhythm-btn'); const stopBtn = document.getElementById('stop-rhythm-btn'); const skipTaskBtn = document.getElementById('skip-rhythm-task-btn'); const statusArea = document.getElementById('rhythm-status'); const timerEl = document.getElementById('rhythm-timer'); const tempoDisplay = document.getElementById('current-tempo'); const beatDisplay = document.getElementById('beat-number'); const beatCircle = document.getElementById('metronome-beat'); const canvas = document.getElementById('beat-timeline'); // Show skip task button if dev mode is enabled if (window.isDevMode && window.isDevMode()) { skipTaskBtn.style.display = 'inline-block'; } const ctx = canvas ? canvas.getContext('2d') : null; const beatCountdown = document.getElementById('beat-countdown'); let webcamStream = null; if (webcamActive) { const video = document.getElementById('rhythm-webcam-video'); if (video && navigator.mediaDevices) { try { webcamStream = await navigator.mediaDevices.getUserMedia({ video: true }); video.srcObject = webcamStream; } catch (e) { console.warn('Webcam access failed:', e); } } } // Setup background audio if provided if (backgroundAudio) { audioElement = new Audio(backgroundAudio); audioElement.loop = true; audioElement.dataset.ambient = 'true'; // Mark as ambient audio // Use global volume setting if available, otherwise use task param const volumeSetting = window.ambientAudioVolume !== undefined ? window.ambientAudioVolume : audioVolume; audioElement.volume = volumeSetting; } let animationFrameId = null; let animationStartTime = null; let beatDuration = (60 / currentTempo) * 1000; let scrollOffset = 0; let nextBeatTime = null; const scrollSpeed = 50; // Fixed pixels per second let lastFrameTime = null; // Pre-generate beat positions for entire cycle const beatPositions = []; const tempoSegments = customTempoSequence || [ { tempo: 60, duration: 42.86 }, // 42.86 seconds { tempo: 120, duration: 42.86 }, { tempo: 150, duration: 42.86 }, { tempo: 60, duration: 42.86 }, { tempo: 150, duration: 42.86 }, { tempo: 200, duration: 42.86 }, { tempo: 240, duration: 42.86 } ]; let position = 0; for (const segment of tempoSegments) { const beatsInSegment = Math.floor((segment.duration / 60) * segment.tempo); const pixelsPerBeat = scrollSpeed * (60 / segment.tempo); for (let i = 0; i < beatsInSegment; i++) { beatPositions.push({ position: position, tempo: segment.tempo, isDownbeat: (beatPositions.length % 4) === 0 }); position += pixelsPerBeat; } } const totalDistance = position; // Create single AudioContext for metronome clicks const audioContext = new (window.AudioContext || window.webkitAudioContext)(); const drawTimeline = () => { if (!ctx) return; const width = canvas.width; const height = canvas.height; const centerX = width / 2; // Clear canvas ctx.clearRect(0, 0, width, height); // Draw scrolling line across middle ctx.strokeStyle = 'rgba(138, 43, 226, 0.4)'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(0, height / 2); ctx.lineTo(width, height / 2); ctx.stroke(); // Draw hit zone marker (vertical line) ctx.strokeStyle = 'rgba(255, 255, 255, 0.9)'; ctx.lineWidth = 3; ctx.beginPath(); ctx.moveTo(centerX, height / 2 - 40); ctx.lineTo(centerX, height / 2 + 40); ctx.stroke(); // Draw "HIT" text above marker ctx.fillStyle = 'rgba(255, 255, 255, 0.9)'; ctx.font = 'bold 14px Arial'; ctx.textAlign = 'center'; ctx.fillText('HIT', centerX, height / 2 - 50); // Draw pre-generated beat markers (dots) const currentOffset = scrollOffset % totalDistance; for (let i = 0; i < beatPositions.length; i++) { const beat = beatPositions[i]; let x = centerX + beat.position - currentOffset; // Handle wrapping if (x < -100) x += totalDistance; if (x > width + 100) continue; // Calculate distance from center for pulse effect const distanceFromCenter = Math.abs(x - centerX); const isPulsing = distanceFromCenter < 5; // Within 5px of center // Draw dot (all same size) const dotRadius = 6; const pulseRadius = isPulsing ? dotRadius * 1.8 : dotRadius; ctx.beginPath(); ctx.arc(x, height / 2, pulseRadius, 0, Math.PI * 2); if (isPulsing) { ctx.fillStyle = 'rgba(138, 43, 226, 1)'; ctx.shadowBlur = 15; ctx.shadowColor = '#8a2be2'; } else { ctx.fillStyle = 'rgba(138, 43, 226, 0.6)'; ctx.shadowBlur = 0; } ctx.fill(); ctx.shadowBlur = 0; } }; const animateTimeline = () => { if (!animationStartTime) return; // Stop if task is completed if (task.completed) { if (animationFrameId) cancelAnimationFrame(animationFrameId); animationFrameId = null; return; } const now = Date.now(); const cycleElapsed = now - segmentStartTime; const targetSegmentIndex = Math.floor(cycleElapsed / segmentDuration); if (targetSegmentIndex !== currentSegmentIndex && targetSegmentIndex < tempoSequence.length) { currentSegmentIndex = targetSegmentIndex; updateTempo(tempoSequence[currentSegmentIndex]); } else if (cycleElapsed >= (5 * 60 * 1000)) { segmentStartTime = now; currentSegmentIndex = 0; updateTempo(tempoSequence[0]); } // Update scroll based on delta time if (lastFrameTime) { const deltaTime = (now - lastFrameTime) / 1000; // seconds scrollOffset += scrollSpeed * deltaTime; } lastFrameTime = now; // Check if beat should trigger if (now >= nextBeatTime) { playBeat(); nextBeatTime = now + beatDuration; } const timeToNextBeat = nextBeatTime - now; if (beatCountdown) { beatCountdown.textContent = Math.max(0, timeToNextBeat / 1000).toFixed(2) + 's'; } drawTimeline(); animationFrameId = requestAnimationFrame(animateTimeline); }; const playBeat = () => { beatCount++; if (beatDisplay) { beatDisplay.textContent = (beatCount % 4) + 1; } if (beatCircle) { beatCircle.style.transform = 'scale(1.3)'; beatCircle.style.background = 'rgba(138, 43, 226, 0.9)'; beatCircle.style.boxShadow = '0 0 30px rgba(138, 43, 226, 0.8)'; setTimeout(() => { beatCircle.style.transform = 'scale(1)'; beatCircle.style.background = 'rgba(138, 43, 226, 0.2)'; beatCircle.style.boxShadow = 'none'; }, 100); } // Reuse shared AudioContext const oscillator = audioContext.createOscillator(); const gainNode = audioContext.createGain(); oscillator.connect(gainNode); gainNode.connect(audioContext.destination); oscillator.frequency.value = 800; oscillator.type = 'sine'; gainNode.gain.setValueAtTime(0.3, audioContext.currentTime); gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.1); oscillator.start(audioContext.currentTime); oscillator.stop(audioContext.currentTime + 0.1); }; const startMetronome = () => { if (animationFrameId) cancelAnimationFrame(animationFrameId); beatDuration = (60 / currentTempo) * 1000; animationStartTime = Date.now(); nextBeatTime = Date.now() + beatDuration; beatCount = 0; scrollOffset = 0; lastFrameTime = null; playBeat(); animateTimeline(); }; const updateTempo = (newTempo) => { currentTempo = Math.max(15, Math.min(240, newTempo)); beatDuration = (60 / currentTempo) * 1000; if (tempoDisplay) tempoDisplay.textContent = currentTempo; // Recalculate next beat time based on new tempo if (nextBeatTime) { const now = Date.now(); nextBeatTime = now + beatDuration; } }; startBtn.addEventListener('click', async () => { startMetronome(); startBtn.disabled = true; stopBtn.disabled = false; stopBtn.style.display = 'inline-block'; startBtn.style.display = 'none'; statusArea.innerHTML = '
๐ŸŽต Metronome Started
'; // Start background audio if available if (audioElement) { audioElement.play().catch(e => console.warn('Audio play failed:', e)); } // Show sidebar countdown timer const sidebarTimer = document.getElementById('sidebar-countdown-timer'); const sidebarDisplay = document.getElementById('sidebar-countdown-display'); const gameStatsPanel = document.getElementById('game-stats-panel'); console.log('๐ŸŽต RHYTHM TASK START - Stats panel element:', gameStatsPanel); console.log('๐ŸŽต RHYTHM TASK START - Stats panel display BEFORE:', gameStatsPanel?.style.display); console.log('๐ŸŽต RHYTHM TASK START - Sidebar timer display BEFORE:', sidebarTimer?.style.display); if (sidebarTimer) { sidebarTimer.style.display = 'block'; console.log('๐ŸŽต RHYTHM TASK START - Sidebar timer set to: block'); } console.log('๐ŸŽต RHYTHM TASK START - Stats panel display AFTER:', gameStatsPanel?.style.display); console.log('๐ŸŽต RHYTHM TASK START - Sidebar timer display AFTER:', sidebarTimer?.style.display); timerInterval = setInterval(() => { timeRemaining--; const minutes = Math.floor(timeRemaining / 60); const seconds = timeRemaining % 60; const timeString = `${minutes}:${seconds.toString().padStart(2, '0')}`; timerEl.textContent = timeString; if (sidebarDisplay) sidebarDisplay.textContent = timeString; // Update tempo based on progression if (timeRemaining % 43 === 0 && timeRemaining < task.duration) { const segmentIndex = Math.floor((task.duration - timeRemaining) / 43); if (segmentIndex < tempoSequence.length) { updateTempo(tempoSequence[segmentIndex]); } } if (timeRemaining <= 0) { clearInterval(timerInterval); if (animationFrameId) cancelAnimationFrame(animationFrameId); if (audioElement) audioElement.pause(); // Hide sidebar timer if (sidebarTimer) sidebarTimer.style.display = 'none'; statusArea.innerHTML = '
โœ… Rhythm Training Complete
'; task.completed = true; const completeBtn = document.getElementById('interactive-complete-btn'); if (completeBtn) completeBtn.disabled = false; } }, 1000); }); skipTaskBtn.addEventListener('click', () => { console.log('โฉ Dev skip - completing rhythm-training task'); statusArea.innerHTML = '
โœ… Task skipped (Dev Mode)
'; task.completed = true; const completeBtn = document.getElementById('interactive-complete-btn'); if (completeBtn) completeBtn.disabled = false; }); startBtn.addEventListener('click', () => { if (skipTaskBtn && window.isDevMode && window.isDevMode()) { skipTaskBtn.style.display = 'inline-block'; } }); stopBtn.addEventListener('click', () => { if (animationFrameId) { cancelAnimationFrame(animationFrameId); animationFrameId = null; animationStartTime = null; lastFrameTime = null; stopBtn.textContent = 'โ–ถ๏ธ Resume'; statusArea.innerHTML = '
โธ๏ธ Paused
'; } else { startMetronome(); stopBtn.textContent = 'โธ๏ธ Pause'; statusArea.innerHTML = '
๐ŸŽต Resumed
'; } }); task.cleanup = () => { if (animationFrameId) cancelAnimationFrame(animationFrameId); if (timerInterval) clearInterval(timerInterval); if (audioElement) { audioElement.pause(); audioElement = null; } if (webcamStream) { webcamStream.getTracks().forEach(track => track.stop()); } }; } validateRhythmTrainingTask(task) { return task.completed === true; } /** * Webcam Setup Task - Position verification and webcam activation */ async createWebcamSetupTask(task, container) { const requirePosition = task.params?.requirePosition !== false; const captureInterval = task.params?.captureInterval || 120; // seconds between captures container.innerHTML = `

๐Ÿ“น Webcam Setup

I want to SEE you. Position yourself properly.

Position yourself here
${requirePosition ? '๐Ÿ’ก Tip: Center yourself in the guide box for best results' : '๐Ÿ’ก Tip: Webcam will monitor your session'}
`; const enableBtn = document.getElementById('enable-webcam-btn'); const confirmBtn = document.getElementById('confirm-position-btn'); const statusArea = document.getElementById('webcam-status'); const video = document.getElementById('webcam-video'); let webcamStream = null; enableBtn.addEventListener('click', async () => { enableBtn.disabled = true; enableBtn.textContent = 'Accessing camera...'; try { if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { throw new Error('Webcam not supported in this browser'); } webcamStream = await navigator.mediaDevices.getUserMedia({ video: { width: { ideal: 1280 }, height: { ideal: 720 } } }); video.srcObject = webcamStream; statusArea.innerHTML = '
โœ… Webcam enabled!
'; enableBtn.style.display = 'none'; confirmBtn.style.display = 'inline-block'; if (!requirePosition) { // Auto-complete if position not required setTimeout(() => { console.log('๐Ÿ“น Webcam auto-completing (no position required)'); statusArea.innerHTML = '
โœ… Webcam Active
'; task.completed = true; const completeBtn = document.getElementById('interactive-complete-btn'); console.log('๐Ÿ“น Auto-complete: Complete button found:', !!completeBtn); if (completeBtn) { completeBtn.disabled = false; console.log('๐Ÿ“น Auto-complete: Complete button enabled'); } }, 1000); } } catch (error) { console.error('Webcam error:', error); statusArea.innerHTML = `
โš ๏ธ Camera access denied or unavailable. ${error.message}
`; enableBtn.disabled = false; enableBtn.textContent = '๐Ÿ“น Try Again'; } }); confirmBtn.addEventListener('click', () => { console.log('๐Ÿ“น Webcam position confirmed - marking task as complete'); statusArea.innerHTML = '
โœ… Position Confirmed - Webcam Active
'; task.completed = true; confirmBtn.style.display = 'none'; const completeBtn = document.getElementById('interactive-complete-btn'); console.log('๐Ÿ“น Complete button found:', !!completeBtn); if (completeBtn) { completeBtn.disabled = false; console.log('๐Ÿ“น Complete button enabled'); } }); // Cleanup function task.cleanup = () => { if (webcamStream) { webcamStream.getTracks().forEach(track => track.stop()); } }; } validateWebcamSetupTask(task) { return task.completed === true; } /** * Preference-Based Slideshow Task * Shows images from user's library with captions matching their preferences */ async createPreferenceSlideshowTask(task, container) { const duration = task.params?.duration || 600; const imageInterval = task.params?.imageInterval || 8; // seconds per image const useTags = task.params?.useTags !== false; const useCaptions = task.params?.useCaptions !== false; const prioritizePreferences = task.params?.prioritizePreferences !== false; const showTimer = task.params?.showTimer || false; const ambientAudio = task.params?.ambientAudio || null; const ambientVolume = task.params?.ambientVolume || 0.5; const overridePreferredTags = task.params?.preferredTags || null; // Allow task to override tags let ambientAudioElement = null; container.innerHTML = `

๐Ÿ–ผ๏ธ Personalized Slideshow

Images from YOUR library. Captions matching YOUR preferences.

`; const btn = container.querySelector('#start-slideshow-btn'); const skipTaskBtn = container.querySelector('#skip-slideshow-task-btn'); const statusArea = container.querySelector('#slideshow-status'); const slideshowContainer = container.querySelector('#slideshow-container'); const imageEl = container.querySelector('#slideshow-image'); const captionEl = container.querySelector('#slideshow-caption'); const countdownEl = showTimer ? document.getElementById('sidebar-countdown-display') : null; const countdownWrapper = showTimer ? document.getElementById('sidebar-countdown-timer') : null; // Show skip button if dev mode is enabled if (window.isDevMode && window.isDevMode()) { skipTaskBtn.style.display = 'inline-block'; } // Show countdown timer in sidebar if enabled if (showTimer && countdownWrapper) { countdownWrapper.style.display = 'block'; // Ensure game stats panel stays visible const gameStatsPanel = document.getElementById('game-stats-panel'); if (gameStatsPanel && gameStatsPanel.style.display !== 'none') { console.log('๐Ÿ“Š Keeping game stats panel visible alongside timer'); } } let slideshowInterval = null; let countdownInterval = null; let availableImages = []; const getPreferenceMatchedImages = () => { // Use linked images from desktop file manager let allImages = []; // 1. Try allLinkedImages (from desktop file manager linked directories) if (window.allLinkedImages && window.allLinkedImages.length > 0) { allImages = window.allLinkedImages; console.log(`๐Ÿ“ธ Using ${allImages.length} linked images from library`); } // 2. Fallback: Try discoveredTaskImages else if (window.gameData?.discoveredTaskImages?.length > 0) { allImages = window.gameData.discoveredTaskImages; console.log(`๐Ÿ“ธ Using ${allImages.length} task images`); } // 3. Fallback: Try discoveredConsequenceImages else if (window.gameData?.discoveredConsequenceImages?.length > 0) { allImages = window.gameData.discoveredConsequenceImages; console.log(`๐Ÿ“ธ Using ${allImages.length} consequence images`); } // 4. Fallback: Try popup image manager else if (this.game?.popupImageManager) { const popupImages = this.game.popupImageManager.getAllAvailableImages?.() || []; if (popupImages.length > 0) { allImages = popupImages.map(img => img.path || img); console.log(`๐Ÿ“ธ Using ${allImages.length} popup images`); } } if (allImages.length === 0) { console.warn('โš ๏ธ No images found in any source'); return []; } // allLinkedImages already contains full file:// paths, no need to convert // For other sources, convert relative paths if needed if (!window.allLinkedImages || window.allLinkedImages.length === 0) { allImages = allImages.map(img => { if (typeof img === 'string') { return img.startsWith('images/') ? img : `images/tasks/${img}`; } return img; }); } if (!prioritizePreferences || !useTags) { return allImages; } // Get user preferences from localStorage const savedPrefs = JSON.parse(localStorage.getItem('gooner-preferences') || '{}'); // Use override tags from task params if provided, otherwise use saved preferences const preferredTags = overridePreferredTags || savedPrefs.preferredTags || []; if (preferredTags.length === 0) { console.log('๐Ÿ“ธ No preferred tags set, showing all images'); return allImages; } console.log('๐Ÿ“ธ Filtering images by preferred tags:', preferredTags); // Load media tags from library storage let mediaTags = {}; try { const libraryTagData = localStorage.getItem('library-mediaTags'); if (libraryTagData) { const parsed = JSON.parse(libraryTagData); if (parsed && parsed.mediaTags) { mediaTags = parsed.mediaTags; console.log('๐Ÿ“ธ Loaded media-tag associations:', Object.keys(mediaTags).length, 'items'); } } } catch (error) { console.error('Error loading media tags:', error); } // Also get tag lookup (tag ID -> tag name) let tagLookup = {}; try { const libraryTagData = localStorage.getItem('library-mediaTags'); if (libraryTagData) { const parsed = JSON.parse(libraryTagData); if (parsed && parsed.tags) { parsed.tags.forEach(tag => { tagLookup[tag.id] = tag.name; }); } } } catch (error) { console.error('Error loading tag lookup:', error); } // Filter images that have at least one of the preferred tags if (Object.keys(mediaTags).length > 0) { const filteredImages = []; for (const imagePath of allImages) { // Get tag IDs for this image const imageTagIds = mediaTags[imagePath]; if (!imageTagIds || imageTagIds.length === 0) { continue; // Skip untagged images when preferences are set } // Convert tag IDs to tag names const imageTagNames = imageTagIds.map(id => tagLookup[id]).filter(Boolean); // Check if image has any of the preferred tags const hasPreferredTag = preferredTags.some(prefTag => imageTagNames.some(imgTag => imgTag.toLowerCase() === prefTag.toLowerCase()) ); if (hasPreferredTag) { filteredImages.push(imagePath); } } if (filteredImages.length > 0) { console.log(`๐Ÿ“ธ Found ${filteredImages.length} images matching preferences (out of ${allImages.length})`); return filteredImages; } else { console.log('๐Ÿ“ธ No images match preferred tags, falling back to all images'); return allImages; } } // Fallback: If tag manager not available, return all images return allImages; }; const getRandomCaption = () => { if (!useCaptions || !window.captionLibrary) { return ''; } // Load preferences from localStorage const savedPrefs = JSON.parse(localStorage.getItem('gooner-preferences') || '{}'); const preferredTones = savedPrefs.captionTones || []; const captionLib = window.captionLibrary; let enabledCategories = []; // Use preferred caption tones if any are selected if (preferredTones.length > 0) { enabledCategories = [...preferredTones]; console.log('๐Ÿ“ Using preferred caption categories:', enabledCategories); } else { // Default to generic if no preferences set enabledCategories.push('generic'); } // Select random category from enabled ones const randomCategory = enabledCategories[Math.floor(Math.random() * enabledCategories.length)]; const textsInCategory = captionLib[randomCategory]; if (!textsInCategory || textsInCategory.length === 0) { return captionLib.generic[Math.floor(Math.random() * captionLib.generic.length)]; } return textsInCategory[Math.floor(Math.random() * textsInCategory.length)]; }; const showNextImage = () => { if (availableImages.length === 0) return; const randomImage = availableImages[Math.floor(Math.random() * availableImages.length)]; imageEl.src = randomImage; if (useCaptions) { // Fade out captionEl.style.opacity = '0'; setTimeout(() => { // Change caption and fade in captionEl.textContent = getRandomCaption(); captionEl.style.opacity = '1'; }, 500); } }; skipTaskBtn.addEventListener('click', () => { console.log('โฉ Dev skip - completing preference-slideshow task'); statusArea.innerHTML = '
โœ… Task skipped (Dev Mode)
'; task.completed = true; const completeBtn = document.getElementById('interactive-complete-btn'); if (completeBtn) completeBtn.disabled = false; }); btn.addEventListener('click', async () => { btn.disabled = true; btn.textContent = 'Loading...'; // Load linked images if function is available if (typeof window.loadLinkedImages === 'function') { console.log('๐Ÿ“ธ Loading linked images from library...'); try { await window.loadLinkedImages(); console.log(`๐Ÿ“ธ Loaded ${window.allLinkedImages?.length || 0} linked images`); } catch (error) { console.warn('โš ๏ธ Error loading linked images:', error); } } else { console.warn('โš ๏ธ loadLinkedImages function not available'); } // Get matched images availableImages = getPreferenceMatchedImages(); if (availableImages.length === 0) { statusArea.innerHTML = '
โ„น๏ธ No images found in library. Add images to linked directories in Library tab.
'; task.completed = true; const completeBtn = document.getElementById('interactive-complete-btn'); if (completeBtn) completeBtn.disabled = false; return; } statusArea.innerHTML = `
โœ… Slideshow started! ${availableImages.length} images available.
`; btn.style.display = 'none'; slideshowContainer.style.display = 'block'; // Start ambient audio if provided if (ambientAudio) { const audioPath = ambientAudio.startsWith('audio/') ? ambientAudio : `audio/ambient/${ambientAudio}`; ambientAudioElement = new Audio(audioPath); ambientAudioElement.loop = true; const globalVolume = window.ambientAudioVolume; ambientAudioElement.volume = (globalVolume !== undefined && globalVolume > 0) ? globalVolume : ambientVolume; ambientAudioElement.setAttribute('data-ambient', 'true'); ambientAudioElement.style.display = 'none'; document.body.appendChild(ambientAudioElement); ambientAudioElement.play().catch(err => { console.warn('โš ๏ธ Could not play ambient audio:', err); }); } // Show first image immediately showNextImage(); // Start slideshow interval slideshowInterval = setInterval(() => { showNextImage(); }, imageInterval * 1000); // Start countdown timer if enabled if (showTimer && countdownEl) { let timeRemaining = duration; const updateCountdown = () => { const minutes = Math.floor(timeRemaining / 60); const seconds = timeRemaining % 60; countdownEl.textContent = `${minutes}:${seconds.toString().padStart(2, '0')}`; timeRemaining--; }; updateCountdown(); // Show initial time countdownInterval = setInterval(updateCountdown, 1000); } // Start duration timer setTimeout(() => { if (slideshowInterval) { clearInterval(slideshowInterval); slideshowInterval = null; } if (countdownInterval) { clearInterval(countdownInterval); countdownInterval = null; } statusArea.innerHTML = '
โœ… Slideshow complete - Your mind has been conditioned
'; task.completed = true; const completeBtn = document.getElementById('interactive-complete-btn'); if (completeBtn) { completeBtn.disabled = false; } }, duration * 1000); }); // Cleanup function task.cleanup = () => { if (slideshowInterval) { clearInterval(slideshowInterval); slideshowInterval = null; } if (countdownInterval) { clearInterval(countdownInterval); countdownInterval = null; } if (ambientAudioElement) { ambientAudioElement.pause(); ambientAudioElement = null; } // Hide countdown timer in sidebar const countdownWrapper = document.getElementById('sidebar-countdown-timer'); if (countdownWrapper) { countdownWrapper.style.display = 'none'; } }; } validatePreferenceSlideshowTask(task) { return task.completed === true; } /** * Dual Slideshow Task - Two simultaneous preference-based slideshows */ async createDualSlideshowTask(task, container) { const duration = task.params?.duration || 120; const leftInterval = task.params?.leftInterval || task.params?.imageInterval || 8; const rightInterval = task.params?.rightInterval || task.params?.imageInterval || 8; const captionInterval = task.params?.captionInterval || 10; const useTags = task.params?.useTags || true; const captions = task.params?.captions !== false; const useCaptions = captions; const prioritizePreferences = task.params?.prioritizePreferences || true; const showTimer = task.params?.showTimer !== false; const ambientAudio = task.params?.ambientAudio; const ambientVolume = task.params?.ambientVolume || 0.5; let timeRemaining = duration; let ambientAudioElement = null; let leftImageIndex = 0; let rightImageIndex = 0; let leftImages = []; let rightImages = []; let leftImageInterval = null; let rightImageInterval = null; let leftCaptionInterval = null; let rightCaptionInterval = null; let timerInterval = null; // Create dual slideshow HTML container.innerHTML = `

๐Ÿ–ผ๏ธ Dual Slideshow

Two streams of imagery flowing into your mind.

${useCaptions ? '
' : ''}
${useCaptions ? '
' : ''}
${showTimer ? `
${timeRemaining}s remaining
` : ''}
`; const leftImageEl = document.getElementById('left-slideshow-image'); const rightImageEl = document.getElementById('right-slideshow-image'); const leftCaptionEl = useCaptions ? document.getElementById('left-slideshow-caption') : null; const rightCaptionEl = useCaptions ? document.getElementById('right-slideshow-caption') : null; const timerEl = showTimer ? document.getElementById('dual-slideshow-timer') : null; const skipBtn = document.getElementById('skip-dual-slideshow-btn'); const skipTaskBtn = document.getElementById('skip-dual-slideshow-task-btn'); // Load preference-matched images const getPreferenceMatchedImages = (allImages) => { if (!prioritizePreferences || !useTags) { return allImages; } const prefs = JSON.parse(localStorage.getItem('gooner-preferences') || '{}'); const preferredTags = prefs.preferredTags || []; if (preferredTags.length === 0) { return allImages; } // Load mediaTags from localStorage const mediaTagsData = JSON.parse(localStorage.getItem('library-mediaTags') || '{}'); const mediaTags = mediaTagsData.mediaTags || {}; // Filter images by preferred tags const matchedImages = allImages.filter(imgPath => { const normalizedPath = imgPath.startsWith('./') ? imgPath : `./${imgPath}`; const imageTags = mediaTags[normalizedPath] || []; return imageTags.some(tagId => preferredTags.includes(tagId)); }); return matchedImages.length > 0 ? matchedImages : allImages; }; // Get all available images let allImages = []; console.log('๐Ÿ–ผ๏ธ Dual-slideshow image source diagnostics:', { hasGame: !!window.game, gameKeys: window.game ? Object.keys(window.game) : [], hasWindowLinkedImages: !!(window.linkedImages), windowLinkedCount: window.linkedImages?.length || 0, hasDesktopFM: !!window.desktopFileManager, desktopFMKeys: window.desktopFileManager ? Object.keys(window.desktopFileManager) : [], hasPhotoLibrary: !!(window.desktopFileManager && window.desktopFileManager.photoLibrary), photoLibraryCount: window.desktopFileManager?.photoLibrary?.length || 0, hasGetAllPhotos: !!(window.desktopFileManager && typeof window.desktopFileManager.getAllPhotos === 'function'), linkedDirectories: window.desktopFileManager?.linkedDirectories?.length || 0 }); // Try to get images from linked directories (desktop mode) // Note: window.linkedImages is an array of {path, tags} objects if (window.linkedImages && Array.isArray(window.linkedImages) && window.linkedImages.length > 0) { console.log(`๐Ÿ–ผ๏ธ Using ${window.linkedImages.length} images from window.linkedImages`); // Extract just the paths allImages = window.linkedImages.map(img => typeof img === 'string' ? img : img.path); } else if (window.desktopFileManager && window.desktopFileManager.photoLibrary && window.desktopFileManager.photoLibrary.length > 0) { console.log(`๐Ÿ–ผ๏ธ Using ${window.desktopFileManager.photoLibrary.length} images from desktopFileManager.photoLibrary`); allImages = window.desktopFileManager.photoLibrary; } else if (window.desktopFileManager && typeof window.desktopFileManager.getAllPhotos === 'function') { // Try desktop file manager's getAllPhotos method const photos = window.desktopFileManager.getAllPhotos(); console.log(`๐Ÿ–ผ๏ธ Using ${photos.length} images from getAllPhotos()`); allImages = photos; } else { // Fallback to old assets structure (deprecated) console.warn('โš ๏ธ No linked images found, using fallback assets (may not exist)'); allImages = [ ...Array.from({ length: 100 }, (_, i) => `assets/pornstars/pornstar${i + 1}.jpg`), ...Array.from({ length: 100 }, (_, i) => `assets/hentai/hentai${i + 1}.jpg`), ...Array.from({ length: 20 }, (_, i) => `assets/feet/feet${i + 1}.jpg`), ...Array.from({ length: 50 }, (_, i) => `assets/BBC/bbc${i + 1}.jpg`) ]; } if (allImages.length === 0) { container.innerHTML = `

๐Ÿ–ผ๏ธ Dual Slideshow

โŒ No images available. Please add directories to your library first.

`; task.completed = true; return; } console.log(`๐Ÿ–ผ๏ธ Loaded ${allImages.length} total images for dual slideshow`); // Get preference-filtered images and shuffle separately for each slideshow const filteredImages = getPreferenceMatchedImages(allImages); leftImages = [...filteredImages].sort(() => Math.random() - 0.5); rightImages = [...filteredImages].sort(() => Math.random() - 0.5); if (leftImages.length === 0 || rightImages.length === 0) { container.innerHTML = '

No images available for slideshow. Please tag some images in the library.

'; task.completed = true; return; } // Get random caption from preferences const getRandomCaption = () => { const prefs = JSON.parse(localStorage.getItem('gooner-preferences') || '{}'); const preferredTones = prefs.captionTones || []; if (preferredTones.length === 0 || !window.captionLibrary) { return 'Keep watching...'; } // Get random tone from preferences const randomTone = preferredTones[Math.floor(Math.random() * preferredTones.length)]; const toneLibrary = window.captionLibrary[randomTone]; if (!toneLibrary || toneLibrary.length === 0) { return 'Keep watching...'; } return toneLibrary[Math.floor(Math.random() * toneLibrary.length)]; }; // Update left slideshow image const updateLeftImage = () => { if (leftImages.length === 0) return; leftImageEl.src = leftImages[leftImageIndex]; leftImageIndex = (leftImageIndex + 1) % leftImages.length; }; // Update right slideshow image const updateRightImage = () => { if (rightImages.length === 0) return; rightImageEl.src = rightImages[rightImageIndex]; rightImageIndex = (rightImageIndex + 1) % rightImages.length; }; // Update left caption with fade const updateLeftCaption = () => { if (!leftCaptionEl) return; leftCaptionEl.style.opacity = '0'; setTimeout(() => { leftCaptionEl.textContent = getRandomCaption(); leftCaptionEl.style.opacity = '1'; }, 500); }; // Update right caption with fade const updateRightCaption = () => { if (!rightCaptionEl) return; rightCaptionEl.style.opacity = '0'; setTimeout(() => { rightCaptionEl.textContent = getRandomCaption(); rightCaptionEl.style.opacity = '1'; }, 500); }; // Initialize first images and captions updateLeftImage(); updateRightImage(); if (useCaptions) { updateLeftCaption(); updateRightCaption(); } // Start image cycling intervals leftImageInterval = setInterval(updateLeftImage, leftInterval * 1000); rightImageInterval = setInterval(updateRightImage, rightInterval * 1000); // Start caption cycling intervals (offset from each other) if (useCaptions) { leftCaptionInterval = setInterval(updateLeftCaption, captionInterval * 1000); rightCaptionInterval = setInterval(updateRightCaption, (captionInterval * 1000) + 2000); // 2s offset } // Start ambient audio playlist console.log('๐Ÿ”Š Loading ambient audio playlist'); const globalVolume = window.ambientAudioVolume; const effectiveVolume = (globalVolume !== undefined && globalVolume > 0) ? globalVolume : ambientVolume; const player = this.createAmbientAudioPlayer(effectiveVolume); if (player) { ambientAudioElement = player.element; ambientAudioElement.setAttribute('data-ambient', 'true'); ambientAudioElement.style.display = 'none'; document.body.appendChild(ambientAudioElement); ambientAudioElement.play().catch(err => { console.warn('โš ๏ธ Could not play ambient audio:', err); }); } // Show sidebar countdown timer if enabled const countdownEl = showTimer ? document.getElementById('sidebar-countdown-display') : null; const countdownWrapper = showTimer ? document.getElementById('sidebar-countdown-timer') : null; if (showTimer && countdownWrapper) { countdownWrapper.style.display = 'block'; // Ensure game stats panel stays visible const gameStatsPanel = document.getElementById('game-stats-panel'); if (gameStatsPanel && gameStatsPanel.style.display !== 'none') { console.log('๐Ÿ“Š Keeping game stats panel visible alongside timer'); } } if (showTimer && countdownEl) { const minutes = Math.floor(timeRemaining / 60); const seconds = timeRemaining % 60; countdownEl.textContent = `${minutes}:${seconds.toString().padStart(2, '0')}`; } // Timer countdown if (showTimer) { timerInterval = setInterval(() => { timeRemaining--; if (countdownEl) { const mins = Math.floor(timeRemaining / 60); const secs = timeRemaining % 60; countdownEl.textContent = `${mins}:${secs.toString().padStart(2, '0')}`; } if (timerEl) { timerEl.textContent = `${timeRemaining}s`; } if (timeRemaining <= 0) { clearInterval(timerInterval); clearInterval(leftImageInterval); clearInterval(rightImageInterval); if (leftCaptionInterval) clearInterval(leftCaptionInterval); if (rightCaptionInterval) clearInterval(rightCaptionInterval); if (ambientAudioElement) { ambientAudioElement.pause(); ambientAudioElement.remove(); } if (countdownWrapper) countdownWrapper.style.display = 'none'; task.completed = true; } }, 1000); } else { // Auto-complete after duration setTimeout(() => { clearInterval(leftImageInterval); clearInterval(rightImageInterval); if (leftCaptionInterval) clearInterval(leftCaptionInterval); if (rightCaptionInterval) clearInterval(rightCaptionInterval); if (ambientAudioElement) { ambientAudioElement.pause(); ambientAudioElement.remove(); } task.completed = true; }, duration * 1000); } // Skip button skipBtn.addEventListener('click', () => { if (timerInterval) clearInterval(timerInterval); clearInterval(leftImageInterval); clearInterval(rightImageInterval); if (leftCaptionInterval) clearInterval(leftCaptionInterval); if (rightCaptionInterval) clearInterval(rightCaptionInterval); if (ambientAudioElement) { ambientAudioElement.pause(); ambientAudioElement.remove(); } if (countdownWrapper) countdownWrapper.style.display = 'none'; task.completed = true; }); // Dev skip button skipTaskBtn.addEventListener('click', () => { console.log('โฉ Dev skip - completing dual-slideshow task'); if (timerInterval) clearInterval(timerInterval); clearInterval(leftImageInterval); clearInterval(rightImageInterval); if (leftCaptionInterval) clearInterval(leftCaptionInterval); if (rightCaptionInterval) clearInterval(rightCaptionInterval); if (ambientAudioElement) { ambientAudioElement.pause(); ambientAudioElement.remove(); } if (countdownWrapper) countdownWrapper.style.display = 'none'; task.completed = true; const completeBtn = document.getElementById('interactive-complete-btn'); if (completeBtn) completeBtn.disabled = false; }); // Cleanup function for when task is skipped/completed task.cleanup = () => { if (timerInterval) clearInterval(timerInterval); if (leftImageInterval) clearInterval(leftImageInterval); if (rightImageInterval) clearInterval(rightImageInterval); if (leftCaptionInterval) clearInterval(leftCaptionInterval); if (rightCaptionInterval) clearInterval(rightCaptionInterval); if (ambientAudioElement) { ambientAudioElement.pause(); ambientAudioElement.remove(); } }; } validateDualSlideshowTask(task) { return task.completed === true; } /** * Hypno Spiral Task */ async createHypnoSpiralTask(task, container) { const duration = task.params?.duration || 120; const overlay = task.params?.overlay || true; const opacity = task.params?.opacity || 0.5; let timeRemaining = duration; container.innerHTML = `

๐ŸŒ€ Hypnotic Spiral

Stare at the spiral. Let it pull you deeper.

${timeRemaining}s remaining
`; const skipBtn = container.querySelector('#skip-hypno-btn'); const spiral = container.querySelector('#hypno-spiral'); const timerDisplay = container.querySelector('#hypno-timer'); if (overlay) { spiral.style.opacity = opacity; } // Auto-start the countdown const countdown = setInterval(() => { timeRemaining--; timerDisplay.textContent = `${timeRemaining}s`; if (timeRemaining <= 0) { clearInterval(countdown); task.completed = true; spiral.classList.remove('spinning'); container.innerHTML += '
โœ… Spiral session complete
'; const completeBtn = document.getElementById('interactive-complete-btn'); if (completeBtn) { completeBtn.disabled = false; } } }, 1000); // Skip button ends immediately skipBtn.addEventListener('click', () => { clearInterval(countdown); task.completed = true; spiral.classList.remove('spinning'); const completeBtn = document.getElementById('interactive-complete-btn'); if (completeBtn) { completeBtn.disabled = false; } skipBtn.disabled = true; skipBtn.textContent = 'โœ… Skipped'; }); task.cleanup = () => clearInterval(countdown); } validateHypnoSpiralTask(task) { return task.completed === true; } /** * Video Start Task - Start a video player */ async createVideoStartTask(task, container) { const player = task.params?.player || 'task'; const tags = task.params?.tags || []; const minDuration = task.params?.minDuration || task.params?.duration || 60; // Support both minDuration and duration params const ambientAudio = task.params?.ambientAudio || null; const ambientVolume = task.params?.ambientVolume || 0.5; const showCaptions = task.params?.showCaptions || false; const captionInterval = task.params?.captionInterval || 10; // Default 10 seconds let ambientAudioElement = null; container.innerHTML = `

๐ŸŽฌ Video Immersion

Prepare to focus on the video content

${tags.length > 0 ? '

Filtering by tags: ' + tags.map(t => '' + t + '').join(' ') + '

' : ''}
${showCaptions ? '
' : ''}
`; const btn = container.querySelector('#start-video-player-btn'); const skipBtn = container.querySelector('#skip-video-btn'); const skipTaskBtn = container.querySelector('#skip-video-task-btn'); const statusArea = container.querySelector('#video-start-status'); const videoContainer = container.querySelector('#video-player-container'); const videoControls = container.querySelector('#video-controls'); const videoVolumeSlider = container.querySelector('#video-volume-slider'); const videoVolumeDisplay = container.querySelector('#video-volume-display'); const captionEl = showCaptions ? container.querySelector('#video-caption') : null; // Show skip task button if dev mode is enabled if (window.isDevMode && window.isDevMode()) { skipTaskBtn.style.display = 'inline-block'; } let availableVideos = []; let playedVideos = new Set(); // Track played videos to avoid repeats let timerInterval = null; let currentVideoElement = null; let captionInterval_id = null; const getRandomCaption = () => { const fallbackCaptions = [ 'You love this...', 'Keep watching...', 'Good gooner...', 'Don\'t stop...', 'Edge for me...', 'You need this...' ]; if (!window.captionLibrary) { return fallbackCaptions[Math.floor(Math.random() * fallbackCaptions.length)]; } // Check for user preferences in localStorage const savedPrefs = JSON.parse(localStorage.getItem('gooner-preferences') || '{}'); const preferredTones = savedPrefs.captionTones || []; let categories; if (preferredTones.length > 0) { // Use only preferred caption categories categories = preferredTones; } else { // Use all available categories categories = Object.keys(window.captionLibrary); } const randomCategory = categories[Math.floor(Math.random() * categories.length)]; const captions = window.captionLibrary[randomCategory]; return captions[Math.floor(Math.random() * captions.length)]; }; const playRandomVideo = async () => { if (availableVideos.length === 0) return; // Get unplayed videos const unplayedVideos = availableVideos.filter(v => !playedVideos.has(v)); if (unplayedVideos.length === 0 && availableVideos.length > 0) { playedVideos.clear(); // Reset if all videos played } const videosToChooseFrom = unplayedVideos.length > 0 ? unplayedVideos : availableVideos; const randomVideo = videosToChooseFrom[Math.floor(Math.random() * videosToChooseFrom.length)]; playedVideos.add(randomVideo); // Extract path from video object if it's an object, otherwise use as-is const videoPath = typeof randomVideo === 'object' ? randomVideo.path : randomVideo; await window.videoPlayerManager.playTaskVideo(videoPath, videoContainer); // Get reference to video element and apply volume currentVideoElement = videoContainer.querySelector('video'); if (currentVideoElement && videoVolumeSlider) { currentVideoElement.volume = videoVolumeSlider.value / 100; } // Add autoplay to next video when current ends if (currentVideoElement) { currentVideoElement.addEventListener('ended', async () => { if (availableVideos.length > 0) { await playRandomVideo(); } }); } }; // Video volume control if (videoVolumeSlider && videoVolumeDisplay) { videoVolumeSlider.addEventListener('input', (e) => { const volume = e.target.value; videoVolumeDisplay.textContent = `${volume}%`; if (currentVideoElement) { currentVideoElement.volume = volume / 100; } }); } skipBtn.addEventListener('click', async () => { if (availableVideos.length > 0) { await playRandomVideo(); statusArea.innerHTML = '
โญ๏ธ Skipped to new video
'; } }); skipTaskBtn.addEventListener('click', () => { console.log('โฉ Dev skip - completing video-start task'); // Clear intervals if (timerInterval) clearInterval(timerInterval); if (captionInterval_id) clearInterval(captionInterval_id); // Stop audio if (ambientAudioElement) { ambientAudioElement.pause(); ambientAudioElement = null; } // Hide sidebar timer const sidebarTimer = document.getElementById('sidebar-countdown-timer'); if (sidebarTimer) { sidebarTimer.style.display = 'none'; } statusArea.innerHTML = '
โœ… Task skipped (Dev Mode)
'; task.completed = true; const completeBtn = document.getElementById('interactive-complete-btn'); if (completeBtn) { completeBtn.disabled = false; } }); btn.addEventListener('click', async () => { btn.disabled = true; btn.textContent = 'Loading...'; if (!window.videoPlayerManager) { statusArea.innerHTML = '
โš ๏ธ Video manager not initialized
'; task.completed = true; const completeBtn = document.getElementById('interactive-complete-btn'); if (completeBtn) completeBtn.disabled = false; return; } try { // Check if videos are available let allVideos = window.videoPlayerManager.getEnabledVideos('background') || []; console.log(`๐Ÿ“น Videos available: ${allVideos.length}`); // Filter videos by preferred tags const savedPrefs = JSON.parse(localStorage.getItem('gooner-preferences') || '{}'); const preferredTags = savedPrefs.preferredTags || []; if (preferredTags.length > 0) { console.log('๐Ÿ“น Filtering videos by preferred tags:', preferredTags); // Load media tags from library storage let mediaTags = {}; let tagLookup = {}; try { const libraryTagData = localStorage.getItem('library-mediaTags'); if (libraryTagData) { const parsed = JSON.parse(libraryTagData); if (parsed && parsed.mediaTags) { mediaTags = parsed.mediaTags; } if (parsed && parsed.tags) { parsed.tags.forEach(tag => { tagLookup[tag.id] = tag.name; }); } } } catch (error) { console.error('Error loading media tags:', error); } // Filter videos that have at least one preferred tag if (Object.keys(mediaTags).length > 0) { const filteredVideos = []; for (const video of allVideos) { const videoPath = typeof video === 'object' ? video.path : video; const videoTagIds = mediaTags[videoPath]; if (!videoTagIds || videoTagIds.length === 0) { continue; // Skip untagged videos } // Convert tag IDs to tag names const videoTagNames = videoTagIds.map(id => tagLookup[id]).filter(Boolean); // Check if video has any preferred tags const hasPreferredTag = preferredTags.some(prefTag => videoTagNames.some(vidTag => vidTag.toLowerCase() === prefTag.toLowerCase()) ); if (hasPreferredTag) { filteredVideos.push(video); } } if (filteredVideos.length > 0) { console.log(`๐Ÿ“น Found ${filteredVideos.length} videos matching preferences (out of ${allVideos.length})`); availableVideos = filteredVideos; } else { console.log('๐Ÿ“น No videos match preferred tags, using all videos'); availableVideos = allVideos; } } else { console.log('๐Ÿ“น No media tags found, using all videos'); availableVideos = allVideos; } } else { console.log('๐Ÿ“น No preferred tags set, using all videos'); availableVideos = allVideos; } if (availableVideos.length === 0) { statusArea.innerHTML = '
โ„น๏ธ No videos match your preferences - continuing anyway
'; task.completed = true; const completeBtn = document.getElementById('interactive-complete-btn'); if (completeBtn) completeBtn.disabled = false; return; } await playRandomVideo(); btn.style.display = 'none'; skipBtn.style.display = 'inline-block'; if (videoControls) videoControls.style.display = 'flex'; // Start caption cycling if enabled if (showCaptions && captionEl) { console.log('๐ŸŽฌ Caption overlay enabled for video-start'); // Show first caption immediately const firstCaption = getRandomCaption(); captionEl.textContent = firstCaption; captionEl.style.opacity = '1'; // Cycle captions captionInterval_id = setInterval(() => { // Fade out captionEl.style.transition = 'opacity 0.5s'; captionEl.style.opacity = '0'; setTimeout(() => { // Change caption and fade in const newCaption = getRandomCaption(); captionEl.textContent = newCaption; captionEl.style.opacity = '1'; }, 500); }, captionInterval * 1000); } // Start ambient audio playlist const player = this.createAmbientAudioPlayer(ambientVolume); if (player) { ambientAudioElement = player.element; ambientAudioElement.play().catch(err => { console.warn('Could not play ambient audio:', err); }); } // Show sidebar countdown timer (don't replace innerHTML!) const sidebarTimer = document.getElementById('sidebar-countdown-timer'); const sidebarDisplay = document.getElementById('sidebar-countdown-display'); const gameStatsPanel = document.getElementById('game-stats-panel'); console.log('๐ŸŽฌ VIDEO TASK START - Stats panel element:', gameStatsPanel); console.log('๐ŸŽฌ VIDEO TASK START - Stats panel display BEFORE:', gameStatsPanel?.style.display); if (sidebarTimer) { sidebarTimer.style.display = 'block'; } if (sidebarDisplay) { const mins = Math.floor(minDuration / 60); const secs = minDuration % 60; sidebarDisplay.textContent = `${mins}:${String(secs).padStart(2, '0')}`; } console.log('๐ŸŽฌ VIDEO TASK START - Stats panel display AFTER:', gameStatsPanel?.style.display); // Start countdown timer let remainingTime = minDuration; timerInterval = setInterval(() => { remainingTime--; if (remainingTime > 0) { const mins = Math.floor(remainingTime / 60); const secs = remainingTime % 60; if (sidebarDisplay) { sidebarDisplay.textContent = `${mins}:${String(secs).padStart(2, '0')}`; } } else { clearInterval(timerInterval); // Hide sidebar timer if (sidebarTimer) { sidebarTimer.style.display = 'none'; } statusArea.innerHTML = '
โœ… You may now continue when ready
'; task.completed = true; const completeBtn = document.getElementById('interactive-complete-btn'); if (completeBtn) { completeBtn.disabled = false; } } }, 1000); } catch (error) { console.error('Video start error:', error); statusArea.innerHTML = `
โš ๏ธ ${error.message} - Continuing anyway
`; // Hide sidebar timer on error const sidebarTimer = document.getElementById('sidebar-countdown-timer'); if (sidebarTimer) { sidebarTimer.style.display = 'none'; } // Auto-complete even on error task.completed = true; const completeBtn = document.getElementById('interactive-complete-btn'); if (completeBtn) { completeBtn.disabled = false; } } }); // Store cleanup function task.cleanup = () => { console.log('๐Ÿงน Video start task cleanup called'); if (timerInterval) { clearInterval(timerInterval); } if (captionInterval_id) { clearInterval(captionInterval_id); } if (ambientAudioElement) { ambientAudioElement.pause(); ambientAudioElement = null; } // Hide countdown timer only (not entire sidebar) const countdownTimer = document.getElementById('sidebar-countdown-timer'); if (countdownTimer) { countdownTimer.style.display = 'none'; } }; } validateVideoStartTask(task) { return task.completed === true; } /** * Update Preferences Task - Show preference update modal at checkpoint */ async createUpdatePreferencesTask(task, container) { const checkpoint = task.params?.checkpoint || 1; // Load current preferences const savedPrefs = JSON.parse(localStorage.getItem('gooner-preferences') || '{}'); // Get all available tags from tag manager let availableTags = []; // Try to load tags from library storage (where they're actually stored) try { const libraryTagData = localStorage.getItem('library-mediaTags'); if (libraryTagData) { const parsed = JSON.parse(libraryTagData); if (parsed && parsed.tags) { availableTags = parsed.tags; console.log('๐Ÿ“‹ Loaded tags from library storage:', availableTags); } } } catch (error) { console.error('Error loading library tags:', error); } // Fallback: Try tag manager if available if (availableTags.length === 0) { console.log('๐Ÿ” Checking for tag manager...'); if (window.globalMediaTagManager) { availableTags = window.globalMediaTagManager.getAllTags(); console.log('๐Ÿ“‹ Loaded tags from tag manager:', availableTags); } else if (window.game?.dataManager && window.MediaTagManager) { console.log('๐Ÿ”ง Initializing MediaTagManager...'); window.globalMediaTagManager = new window.MediaTagManager(window.game.dataManager); availableTags = window.globalMediaTagManager.getAllTags(); console.log('๐Ÿ“‹ Loaded tags after initialization:', availableTags); } } // Generate tag checkboxes dynamically let tagCheckboxesHTML = ''; if (availableTags.length > 0) { tagCheckboxesHTML = availableTags .sort((a, b) => a.name.localeCompare(b.name)) .map(tag => ` `).join(''); } else { tagCheckboxesHTML = '

No tags created yet. Visit the Media Library to create tags.

'; } container.innerHTML = `

โš™๏ธ Checkpoint ${checkpoint} - Update Preferences

Select what you crave. These preferences personalize your training.

Caption Tone (select all that apply):

Preferred Content Tags (select all that apply):

${tagCheckboxesHTML}

Session Intensity:

`; // Load and set saved preferences const toneCheckboxes = container.querySelectorAll('.pref-tone'); const tagCheckboxes = container.querySelectorAll('.pref-tag'); const intensitySelect = container.querySelector('#pref-intensity'); if (savedPrefs.captionTones && Array.isArray(savedPrefs.captionTones)) { savedPrefs.captionTones.forEach(tone => { const checkbox = container.querySelector(`.pref-tone[value="${tone}"]`); if (checkbox) checkbox.checked = true; }); } if (savedPrefs.preferredTags && Array.isArray(savedPrefs.preferredTags)) { savedPrefs.preferredTags.forEach(tag => { const checkbox = container.querySelector(`.pref-tag[value="${tag}"]`); if (checkbox) checkbox.checked = true; }); } if (savedPrefs.intensity) { intensitySelect.value = savedPrefs.intensity; } const saveBtn = container.querySelector('#save-preferences-btn'); const skipBtn = container.querySelector('#skip-preferences-btn'); const statusArea = container.querySelector('#preference-status'); const completeTask = (message) => { statusArea.innerHTML = `
${message}
`; task.completed = true; if (saveBtn) saveBtn.disabled = true; if (skipBtn) skipBtn.disabled = true; const completeBtn = document.getElementById('interactive-complete-btn'); if (completeBtn) { completeBtn.disabled = false; } }; skipBtn.addEventListener('click', () => { completeTask('โœ… Using current preferences'); }); saveBtn.addEventListener('click', () => { // Gather selected preferences const selectedTones = Array.from(toneCheckboxes) .filter(cb => cb.checked) .map(cb => cb.value); const selectedTags = Array.from(tagCheckboxes) .filter(cb => cb.checked) .map(cb => cb.value); const preferences = { captionTones: selectedTones, preferredTags: selectedTags, intensity: intensitySelect.value, checkpoint: checkpoint, lastUpdated: Date.now() }; localStorage.setItem('gooner-preferences', JSON.stringify(preferences)); console.log('๐Ÿ’พ Saved preferences:', preferences); completeTask(`โœ… Preferences saved! (${selectedTones.length} tones, ${selectedTags.length} tags)`); }); } validateUpdatePreferencesTask(task) { return task.completed === true; } /** * Free Edge Session Task - Free-form edging with features enabled */ async createFreeEdgeSessionTask(task, container) { const duration = task.params?.duration || 600; const targetEdges = task.params?.targetEdges || null; const features = task.params?.features || task.params?.allowedFeatures || ['video']; const showTimer = task.params?.showTimer || false; let timeRemaining = duration; let edgeCount = 0; container.innerHTML = `

๐ŸŽฏ Free Edge Session

${targetEdges ? `

Complete ${targetEdges} edges while these features are enabled:

` : `

Edge as much as you can with these features enabled:

`}
${features.map(f => `${f}`).join(' ')}
0 Edges${targetEdges ? ` / ${targetEdges}` : ''}
${!showTimer ? `
${Math.floor(timeRemaining / 60)}:${String(timeRemaining % 60).padStart(2, '0')} Time Remaining
` : ''}
`; const startBtn = container.querySelector('#start-session-btn'); const edgeBtn = container.querySelector('#log-edge-btn'); const edgeCountEl = container.querySelector('#edge-count'); const timeRemainingEl = showTimer ? document.getElementById('sidebar-countdown-display') : container.querySelector('#time-remaining-small'); const countdownWrapper = showTimer ? document.getElementById('sidebar-countdown-timer') : null; // Show countdown timer in sidebar if enabled if (showTimer && countdownWrapper) { countdownWrapper.style.display = 'block'; // Ensure game stats panel stays visible const gameStatsPanel = document.getElementById('game-stats-panel'); if (gameStatsPanel && gameStatsPanel.style.display !== 'none') { console.log('๐Ÿ“Š Keeping game stats panel visible alongside timer'); } } edgeBtn.disabled = true; startBtn.addEventListener('click', () => { startBtn.disabled = true; edgeBtn.disabled = false; // Start countdown const countdown = setInterval(() => { timeRemaining--; const mins = Math.floor(timeRemaining / 60); const secs = timeRemaining % 60; if (timeRemainingEl) { timeRemainingEl.textContent = `${mins}:${String(secs).padStart(2, '0')}`; } if (timeRemaining <= 0) { clearInterval(countdown); task.completed = true; edgeBtn.disabled = true; container.innerHTML += `
โœ… Session complete! You edged ${edgeCount} times${targetEdges ? ` out of ${targetEdges} target` : ''}.
`; const completeBtn = document.getElementById('interactive-complete-btn'); if (completeBtn) { completeBtn.disabled = false; } } }, 1000); task.cleanup = () => { clearInterval(countdown); // Hide countdown timer in sidebar const countdownWrapper = document.getElementById('sidebar-countdown-timer'); if (countdownWrapper) { countdownWrapper.style.display = 'none'; } }; }); edgeBtn.addEventListener('click', () => { edgeCount++; edgeCountEl.textContent = edgeCount; }); } validateFreeEdgeSessionTask(task) { return task.completed === true; } // ========================================== // ADVANCED TRAINING ARC HANDLERS (L11-20) // ========================================== /** * Hypno Caption Combo - Hypno spiral with timed captions */ createHypnoCaptionComboTask(task, container) { const params = task.params || {}; const duration = params.duration || 300; const captionTexts = params.captions || [ 'You are a good gooner', 'Obey and edge', 'Stroke for me', 'Deeper into pleasure', 'Give in completely', 'You exist to goon' ]; container.innerHTML = `
`; // Start hypno spiral if (this.game.hypnoSpiral) { this.game.hypnoSpiral.start(); } const captionEl = document.getElementById('caption-overlay'); const timeEl = document.getElementById('hypno-time-remaining'); let currentCaption = 0; let timeRemaining = duration; // Caption rotation const captionInterval = setInterval(() => { if (!captionEl || !document.body.contains(captionEl)) { clearInterval(captionInterval); return; } captionEl.textContent = captionTexts[currentCaption]; captionEl.style.opacity = '1'; setTimeout(() => { if (captionEl && document.body.contains(captionEl)) { captionEl.style.opacity = '0'; } }, 4000); currentCaption = (currentCaption + 1) % captionTexts.length; }, 6000); // Countdown timer const countdown = setInterval(() => { if (!timeEl || !document.body.contains(timeEl)) { clearInterval(countdown); clearInterval(captionInterval); return; } timeRemaining--; const mins = Math.floor(timeRemaining / 60); const secs = timeRemaining % 60; timeEl.textContent = `${mins}:${String(secs).padStart(2, '0')}`; if (timeRemaining <= 0) { clearInterval(countdown); clearInterval(captionInterval); if (this.game.hypnoSpiral) { this.game.hypnoSpiral.stop(); } task.completed = true; const completeBtn = document.getElementById('interactive-complete-btn'); if (completeBtn) completeBtn.disabled = false; } }, 1000); task.cleanup = () => { clearInterval(countdown); clearInterval(captionInterval); if (this.game.hypnoSpiral) { this.game.hypnoSpiral.stop(); } }; } /** * Dynamic Captions - Preference-based captions */ createDynamicCaptionsTask(task, container) { const params = task.params || {}; const duration = params.duration || 300; // Get user preferences (simplified for now) const preferences = this.game.playerStats?.preferences || {}; const captionPool = this.generatePreferenceBasedCaptions(preferences); container.innerHTML = `
Time:
Captions personalized to your preferences
`; const captionEl = document.getElementById('dynamic-caption-display'); const timeEl = document.getElementById('dynamic-time-remaining'); let timeRemaining = duration; // Show random caption every 8 seconds const captionInterval = setInterval(() => { if (!captionEl) return; const caption = captionPool[Math.floor(Math.random() * captionPool.length)]; captionEl.textContent = caption; captionEl.style.opacity = '1'; setTimeout(() => { if (captionEl) captionEl.style.opacity = '0'; }, 6000); }, 8000); // Countdown const countdown = setInterval(() => { timeRemaining--; const mins = Math.floor(timeRemaining / 60); const secs = timeRemaining % 60; if (timeEl) timeEl.textContent = `${mins}:${String(secs).padStart(2, '0')}`; if (timeRemaining <= 0) { clearInterval(countdown); clearInterval(captionInterval); task.completed = true; const completeBtn = document.getElementById('interactive-complete-btn'); if (completeBtn) completeBtn.disabled = false; } }, 1000); task.cleanup = () => { clearInterval(countdown); clearInterval(captionInterval); }; } generatePreferenceBasedCaptions(preferences) { // Default captions if no preferences return [ 'You love to edge', 'Gooning is your purpose', 'Stroke and obey', 'Deeper into pleasure', 'You are addicted', 'Edge for hours', 'No thinking, only gooning', 'You need this', 'Pump and goon', 'Surrender completely' ]; } /** * TTS Hypno Sync - Voice + hypno synchronized */ createTTSHypnoSyncTask(task, container) { const params = task.params || {}; const duration = params.duration || 300; const commands = params.commands || [ 'Focus on the spiral', 'Let your mind go blank', 'Stroke slowly and deeply', 'You are under my control', 'Obey and edge' ]; if (!container) { console.error('โŒ TTS Hypno Sync: No container provided'); return; } container.innerHTML = `
Time: 0:00
`; // Start hypno if (this.game.hypnoSpiral) { this.game.hypnoSpiral.start(); } const commandEl = document.getElementById('tts-command-display'); const timeEl = document.getElementById('tts-time-remaining'); // Safety check for elements if (!commandEl || !timeEl) { console.error('โŒ TTS Hypno Sync elements not found in DOM'); console.log('Container:', container); console.log('Container innerHTML:', container?.innerHTML); return; } let timeRemaining = duration; let commandIndex = 0; // Speak commands every 30 seconds const speakCommand = () => { const command = commands[commandIndex]; commandEl.textContent = command; if (this.voiceManager && this.ttsEnabled) { this.voiceManager.speak(command); } commandIndex = (commandIndex + 1) % commands.length; }; speakCommand(); // First command immediately const commandInterval = setInterval(speakCommand, 30000); // Countdown const countdown = setInterval(() => { timeRemaining--; const mins = Math.floor(timeRemaining / 60); const secs = timeRemaining % 60; timeEl.textContent = `${mins}:${String(secs).padStart(2, '0')}`; if (timeRemaining <= 0) { clearInterval(countdown); clearInterval(commandInterval); if (this.game.hypnoSpiral) { this.game.hypnoSpiral.stop(); } task.completed = true; const completeBtn = document.getElementById('interactive-complete-btn'); if (completeBtn) completeBtn.disabled = false; } }, 1000); task.cleanup = () => { clearInterval(countdown); clearInterval(commandInterval); if (this.game.hypnoSpiral) { this.game.hypnoSpiral.stop(); } }; } /** * Sensory Overload - All features combined */ createSensoryOverloadTask(task, container) { const params = task.params || {}; const duration = params.duration || 600; const edgeTarget = params.edgeCount || 50; container.innerHTML = `
Edges: 0 / ${edgeTarget}
Time:
โš ๏ธ ALL FEATURES ACTIVE โš ๏ธ
`; // Start ALL features if (this.game.hypnoSpiral) { this.game.hypnoSpiral.start(); } const captionEl = document.getElementById('overload-caption'); const edgeCountEl = document.getElementById('overload-edge-count'); const timeEl = document.getElementById('overload-time-remaining'); const edgeBtn = document.getElementById('overload-edge-btn'); let edgeCount = 0; let timeRemaining = duration; const captions = [ 'EDGE NOW', 'OBEY', 'GOON HARDER', 'SUBMIT', 'EDGE AGAIN', 'YOU ARE MINE', 'STROKE', 'PUMP', 'DEEPER', 'MORE' ]; // Rapid caption rotation const captionInterval = setInterval(() => { if (captionEl) captionEl.textContent = captions[Math.floor(Math.random() * captions.length)]; }, 3000); // TTS commands const ttsCommands = ['Edge now', 'Faster', 'Slower', 'Stop', 'Continue']; const ttsInterval = setInterval(() => { if (this.voiceManager && this.ttsEnabled) { const cmd = ttsCommands[Math.floor(Math.random() * ttsCommands.length)]; this.voiceManager.speak(cmd); } }, 25000); // Countdown const countdown = setInterval(() => { timeRemaining--; const mins = Math.floor(timeRemaining / 60); const secs = timeRemaining % 60; if (timeEl) timeEl.textContent = `${mins}:${String(secs).padStart(2, '0')}`; if (timeRemaining <= 0) { clearInterval(countdown); clearInterval(captionInterval); clearInterval(ttsInterval); if (this.game.hypnoSpiral) { this.game.hypnoSpiral.stop(); } task.completed = true; if (edgeBtn) edgeBtn.disabled = true; const completeBtn = document.getElementById('interactive-complete-btn'); if (completeBtn) completeBtn.disabled = false; } }, 1000); if (edgeBtn) { edgeBtn.addEventListener('click', () => { edgeCount++; if (edgeCountEl) edgeCountEl.textContent = edgeCount; }); } task.cleanup = () => { clearInterval(countdown); clearInterval(captionInterval); clearInterval(ttsInterval); if (this.game.hypnoSpiral) { this.game.hypnoSpiral.stop(); } }; } /** * Enable Interruptions - Activate interruption system */ createEnableInterruptionsTask(task, container) { const params = task.params || {}; const types = params.types || ['edge', 'pose', 'mantra', 'stop-stroking']; const frequency = params.frequency || 'medium'; container.innerHTML = `

๐Ÿšจ INTERRUPTIONS ACTIVATED ๐Ÿšจ

Random commands will interrupt your sessions:

Frequency: ${frequency}

You must obey immediately when interrupted.

`; const acceptBtn = document.getElementById('accept-interruptions-btn'); acceptBtn.addEventListener('click', () => { // Store interruption settings in game state if (!this.game.interruptionSettings) { this.game.interruptionSettings = {}; } this.game.interruptionSettings.enabled = true; this.game.interruptionSettings.types = types; this.game.interruptionSettings.frequency = frequency; task.completed = true; const completeBtn = document.getElementById('interactive-complete-btn'); if (completeBtn) completeBtn.disabled = false; }); } /** * Denial Training - Timed stroking/stopping periods */ createDenialTrainingTask(task, container) { const params = task.params || {}; const periods = params.denialPeriods || [ { allowStroking: 300, instruction: 'Stroke for 5 minutes' }, { stopDuration: 120, instruction: 'HANDS OFF for 2 minutes' } ]; let currentPeriod = 0; let timeInPeriod = 0; const updateDisplay = () => { const period = periods[currentPeriod]; const isStroking = period.hasOwnProperty('allowStroking'); const duration = isStroking ? period.allowStroking : period.stopDuration; const remaining = duration - timeInPeriod; container.innerHTML = `

${isStroking ? 'โœ‹ STROKE' : '๐Ÿšซ STOP - HANDS OFF'}

${period.instruction}
${Math.floor(remaining / 60)}:${String(remaining % 60).padStart(2, '0')}
Period ${currentPeriod + 1} of ${periods.length}
`; }; updateDisplay(); const interval = setInterval(() => { timeInPeriod++; const period = periods[currentPeriod]; const duration = period.hasOwnProperty('allowStroking') ? period.allowStroking : period.stopDuration; if (timeInPeriod >= duration) { currentPeriod++; timeInPeriod = 0; if (currentPeriod >= periods.length) { clearInterval(interval); task.completed = true; container.innerHTML = `

โœ… Denial Training Complete

You have learned discipline through denial.

`; const completeBtn = document.getElementById('interactive-complete-btn'); if (completeBtn) completeBtn.disabled = false; return; } } updateDisplay(); }, 1000); task.cleanup = () => clearInterval(interval); } /** * Stop Stroking - Enforced hands-off period */ createStopStrokingTask(task, container) { const params = task.params || {}; const duration = params.duration || 120; container.innerHTML = `

๐Ÿšซ STOP STROKING ๐Ÿšซ

HANDS OFF. NO TOUCHING.
You must wait the full duration.
`; const timerEl = document.getElementById('stop-timer'); let timeRemaining = duration; const countdown = setInterval(() => { timeRemaining--; const mins = Math.floor(timeRemaining / 60); const secs = timeRemaining % 60; timerEl.textContent = `${mins}:${String(secs).padStart(2, '0')}`; if (timeRemaining <= 0) { clearInterval(countdown); task.completed = true; container.innerHTML = `

โœ… Denial Period Complete

You may resume stroking.

`; const completeBtn = document.getElementById('interactive-complete-btn'); if (completeBtn) completeBtn.disabled = false; } }, 1000); task.cleanup = () => clearInterval(countdown); } /** * Enable Popups - Activate popup system */ createEnablePopupsTask(task, container) { const params = task.params || {}; const frequency = params.frequency || 'medium'; const sources = params.sources || ['tasks', 'consequences']; container.innerHTML = `

๐Ÿ“ธ POPUP SYSTEM ACTIVATED ๐Ÿ“ธ

Random images will appear during your sessions:

Frequency: ${frequency}

You must read and acknowledge each popup.

`; const acceptBtn = document.getElementById('accept-popups-btn'); acceptBtn.addEventListener('click', () => { // Store popup settings in game state if (!this.game.popupSettings) { this.game.popupSettings = {}; } this.game.popupSettings.enabled = true; this.game.popupSettings.sources = sources; this.game.popupSettings.frequency = frequency; task.completed = true; const completeBtn = document.getElementById('interactive-complete-btn'); if (completeBtn) completeBtn.disabled = false; }); } /** * Popup Image - Display specific popup */ createPopupImageTask(task, container) { const params = task.params || {}; const imagePath = params.image || ''; const displayTime = params.duration || 10; const message = params.message || 'Acknowledge this popup'; container.innerHTML = ` `; const timerEl = document.getElementById('popup-timer'); const ackBtn = document.getElementById('popup-acknowledge-btn'); let timeRemaining = displayTime; const countdown = setInterval(() => { timeRemaining--; timerEl.textContent = timeRemaining; if (timeRemaining <= 0) { clearInterval(countdown); task.completed = true; const completeBtn = document.getElementById('interactive-complete-btn'); if (completeBtn) completeBtn.disabled = false; } }, 1000); ackBtn.addEventListener('click', () => { clearInterval(countdown); task.completed = true; const completeBtn = document.getElementById('interactive-complete-btn'); if (completeBtn) completeBtn.disabled = false; }); task.cleanup = () => clearInterval(countdown); } /** * Generic validators for timed tasks */ validateTimedTask(task) { return task.completed === true; } validateInstantTask(task) { return task.completed === true; } /** * Clean up quad video player and other resources */ cleanup() { if (this.quadPlayer) { console.log('๐Ÿงน Cleaning up QuadVideoPlayer...'); // Always stop all videos during cleanup this.quadPlayer.stopAllVideos(); // Fully destroy the quad player to prevent DOM corruption this.quadPlayer.destroy(); this.quadPlayer = null; console.log('โœ… QuadVideoPlayer fully cleaned up'); } } } // Export for use in other modules if (typeof module !== 'undefined' && module.exports) { module.exports = InteractiveTaskManager; } // Make available globally for dependency validation window.InteractiveTaskManager = InteractiveTaskManager;