/**
* 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 = `
Complete Challenge
Give Up
๐ฌ Multi-Screen Mode
`;
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 = `
${randomMessage}
Are you absolutely certain you want to prove what a quitter you are?
๐ข Yes, I'm a Pathetic Quitter
๐ช No, I'll Actually Try
`;
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.prompt || 'Enter your response:'}
Minimum ${task.minLength || 10} characters required
Submit Response
`;
// 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 = `
Target: ${task.targetValue || 50}${task.valueUnit || '%'}
Time Limit: ${task.duration || 60} seconds
Move the slider to reach the exact target value!
๐๏ธ Start Challenge
โ
Submit Answer
`;
// 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}
Continue
`;
}
// 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 = `
Duration: ${task.duration || 60} seconds
Target BPM: ${task.targetBPM || 120}
Tap the beat button to match the rhythm!
๐ฅ Start Rhythm Challenge
`;
// 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}
Continue
`;
}
// 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.instructions || 'Hold the position and focus for the required time'}
${task.duration || 60} seconds
๐ง Start Focus Session
`;
// 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 = `
`;
// 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
โฉ Skip Timer (Testing)
`;
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 = `
${step.mirrorInstructions || 'Look at yourself while completing the task'}
๐ช Open Webcam Mirror
${step.mirrorTaskText || ''}
Time remaining: ${step.duration || 60} s
โฉ Skip Timer (Testing)
`;
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 = `
`;
// 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 = `
${task.mirrorInstructions || task.story || 'Complete the task while looking at yourself'}
๐ช Open Webcam Mirror
${task.mirrorTaskText || ''}
${task.duration || 60}s
Mirror session in progress...
โฉ Skip Timer (Testing)
`;
// 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 = `
Required Pose:
${poseInstructions}
๐ Use your webcam to verify you're maintaining the correct pose
โฑ๏ธ Hold the position for ${verificationDuration} seconds
๐ Start Pose Verification
${verificationDuration}s
Verification in progress...
โฉ Skip Timer (Testing)
`;
// 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 = `
๐ธ Photography Task Detected
This task involves photography. Would you like to use your webcam for a more immersive experience?
Task: ${task.text}
๐ท Use Webcam
๐ Text Only
๐ Photos are processed locally and never uploaded
`;
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}
๐ธ Complete Photography Task
`;
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 += `
${item.label}
`;
} else {
// Dropdown select
questionnaireHTML += `
${item.label}:
${item.options.map(opt =>
`${opt} `
).join('')}
`;
}
});
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 = `
${allItems.map(item => `${item} `).join('')}
`;
} 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
๐ฏ Start Edging Session
โฉ Skip Task (Dev)
`;
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 ? `
` : `
`}
${bpmSequence[0] === 0 ? 'PAUSE' : bpmSequence[0]} BPM
Phase ${currentPhase + 1} of ${bpmSequence.length}
โฉ Skip Task (Dev)
`;
// 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(' ')}
` : ''}
๐ Select Directory
`;
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(' ')}
` : ''}
๐ Select Directory
`;
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)
๐ท๏ธ Open Library Tagging Interface
๐ Refresh Progress
`;
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}
๐น Start Webcam
`;
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 ? '
' : ''}
โถ๏ธ Start Dual Video
โญ๏ธ Skip to New Videos
โฉ Skip Task (Dev)
`;
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}"
๐ Play Voice Command
`;
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
${hypnoOverlay ? '
' : ''}
โถ๏ธ Start Quad Video
โญ๏ธ Load All New Videos
๐ฅ๏ธ Fullscreen
โฉ Skip Task (Dev)
`;
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
โถ๏ธ BEGIN CHAOS
๐ฅ๏ธ Fullscreen
`;
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 ? '
' : ''}
โถ๏ธ Start Chaos
${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 ? '
' : ''}
${tempo} BPM
Tempo Progression: ${tempoProgressionText}
โถ๏ธ Start Rhythm
โธ๏ธ Pause
โฉ Skip Task (Dev)
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.
๐น Enable Webcam
โ
Position Confirmed
${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.
โถ๏ธ Start Slideshow
โฉ Skip Task (Dev)
`;
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
` : ''}
โญ๏ธ Skip Slideshow
โฉ Skip Task (Dev)
`;
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.
Continue Anyway
`;
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
โญ๏ธ Skip Spiral
`;
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 => `
${tag.name}
`).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.
Preferred Content Tags (select all that apply):
${tagCheckboxesHTML}
Session Intensity:
Light - Gentle pacing
Moderate - Balanced
Intense - Maximum arousal
๐พ Save Preferences
โญ๏ธ Skip (Use Defaults)
`;
// 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
` : ''}
โ I Edged
โถ๏ธ Start Session
`;
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 = `
`;
// 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:
I EDGED
โ ๏ธ 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:
${types.map(type => `${type} `).join('')}
Frequency: ${frequency}
You must obey immediately when interrupted.
I Understand - Activate
`;
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 = `
`;
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;