refined level 6-8

This commit is contained in:
dilgenfritz 2025-12-03 00:57:41 -06:00
parent 5bd002e72d
commit c9a01f8ed6
3 changed files with 272 additions and 194 deletions

View File

@ -4814,6 +4814,12 @@
function completeScenarioAction(nextStep) { function completeScenarioAction(nextStep) {
console.log('✅ Scenario action completed, moving to:', nextStep); console.log('✅ Scenario action completed, moving to:', nextStep);
// Call cleanup on current interactive task if it exists
if (currentInteractiveTask && typeof currentInteractiveTask.cleanup === 'function') {
console.log('🧹 Calling interactive task cleanup');
currentInteractiveTask.cleanup();
}
// Clean up any active webcam streams // Clean up any active webcam streams
const webcamVideo = document.getElementById('webcam-video'); const webcamVideo = document.getElementById('webcam-video');
if (webcamVideo && webcamVideo.srcObject) { if (webcamVideo && webcamVideo.srcObject) {

View File

@ -747,9 +747,14 @@ const trainingGameData = {
mirror_edges: { mirror_edges: {
type: 'action', type: 'action',
mood: 'intense', mood: 'intense',
story: 'Now edge 25 times while watching yourself stroke to porn. See your face flush. See your hand pumping. Watch yourself being consumed by the video. This is your reality. This is who you\'re becoming. And it feels good, doesn\'t it?', story: 'Now we flip it. The porn becomes your focus, and you watch yourself in the corner. 15 minutes of stroking to porn while glimpsing yourself in the mirror of the PiP. See how natural it looks now - you, stroking, watching. This is what a gooner does.',
interactiveType: 'edge', interactiveType: 'dual-video',
params: { count: 25, instruction: 'Edge 25 times while watching yourself', keepVideoPlaying: true }, params: {
mainVideo: 'focus',
pipVideo: 'webcam',
pipPosition: 'bottom-right',
minDuration: 900
},
nextStep: 'completion' nextStep: 'completion'
}, },
completion: { completion: {
@ -767,7 +772,7 @@ const trainingGameData = {
name: 'Dual Focus', name: 'Dual Focus',
arc: 'Feature Discovery', arc: 'Feature Discovery',
level: 7, level: 7,
duration: 2400, // 40 minutes duration: 3600, // 60 minutes
interactiveType: 'scenario-adventure', interactiveType: 'scenario-adventure',
interactiveData: { interactiveData: {
title: 'Level 7: Dual Focus', title: 'Level 7: Dual Focus',
@ -783,21 +788,33 @@ const trainingGameData = {
mood: 'overwhelming', mood: 'overwhelming',
story: 'Starting dual video mode. Main screen + picture-in-picture. TWO feeds of porn. Your eyes won\'t know where to look. Your brain won\'t know what to process. That\'s the point. Sensory overload. Pure gooning.', story: 'Starting dual video mode. Main screen + picture-in-picture. TWO feeds of porn. Your eyes won\'t know where to look. Your brain won\'t know what to process. That\'s the point. Sensory overload. Pure gooning.',
interactiveType: 'dual-video', interactiveType: 'dual-video',
params: { mainVideo: 'focus', pipVideo: 'overlay', pipPosition: 'bottom-right' }, params: { mainVideo: 'focus', pipVideo: 'overlay', pipPosition: 'bottom-right', minDuration: 120 },
nextStep: 'dual_edges' nextStep: 'media_linking'
}, },
dual_edges: { media_linking: {
type: 'action', type: 'action',
mood: 'demanding', mood: 'instructional',
story: 'Edge 30 times while both videos play. Try to watch both at once. Let your mind split between the two streams. This is advanced gooner technique - fractured attention, unified pleasure.', story: 'Before we continue, we need to expand your library. Link 10 more videos to your collection. The more content you have, the deeper you can goon. Feed the machine.',
interactiveType: 'edge', interactiveType: 'link-media',
params: { count: 30, instruction: 'Edge 30 times to dual video streams' }, params: {
minFiles: 10,
mediaType: 'video',
suggestedTags: ['pov', 'blowjob', 'riding', 'amateur', 'solo']
},
nextStep: 'dual_video_extended'
},
dual_video_extended: {
type: 'action',
mood: 'overwhelming',
story: 'Now the real test. 20 minutes of dual video mode. Two streams. Constant stimulation. Your eyes will dart between them. Your mind will fracture and reform around the porn. This is what it means to truly goon.',
interactiveType: 'dual-video',
params: { mainVideo: 'focus', pipVideo: 'overlay', pipPosition: 'bottom-right', minDuration: 1200 },
nextStep: 'completion' nextStep: 'completion'
}, },
completion: { completion: {
type: 'completion', type: 'completion',
mood: 'intense', mood: 'intense',
story: '✅ Level 7 Complete - DUAL VIDEO UNLOCKED. More screens = more stimulation.', story: '✅ Level 7 Complete - DUAL VIDEO MASTERED. More screens = more stimulation.',
outcome: 'level7_complete' outcome: 'level7_complete'
} }
} }
@ -806,43 +823,66 @@ const trainingGameData = {
level8: { level8: {
id: 'academy-level-8', id: 'academy-level-8',
name: 'Vocal Commands', name: 'Audio Immersion',
arc: 'Feature Discovery', arc: 'Feature Discovery',
level: 8, level: 8,
duration: 3000, // 50 minutes duration: 2700, // 45 minutes
interactiveType: 'scenario-adventure', interactiveType: 'scenario-adventure',
interactiveData: { interactiveData: {
title: 'Level 8: Vocal Commands', title: 'Level 8: Audio Immersion',
steps: { steps: {
start: { start: {
type: 'story', type: 'story',
mood: 'commanding', mood: 'commanding',
story: 'You have eyes. You have hands. Now we add ears. Voice commands via text-to-speech will guide your edging. You will learn to obey the voice. To respond instantly. Gooners don\'t think - they obey.', story: 'You have mastered visuals. Webcam. Dual video. Now we unlock a new dimension: audio. Hypnotic voices. Moaning. Instructions whispered directly into your brain. This changes everything.',
nextStep: 'tts_intro' nextStep: 'audio_warmup'
}, },
tts_intro: { audio_warmup: {
type: 'action', type: 'action',
mood: 'authoritative', mood: 'hypnotic',
story: 'Listen to the voice. It will tell you what to do. And you will do it. Without hesitation. This is training in obedience and responsiveness.', story: 'First, experience the power of audio. 5 minutes of video with sound cranked up. Let the moans fill your ears. Let them guide your hand. Audio is not just background - it\'s fuel.',
interactiveType: 'tts-command', interactiveType: 'video-start',
params: { params: {
text: 'Good gooner. You are learning to obey. Edge for me. Now.', player: 'focus',
voice: 'feminine' duration: 300,
ambientAudio: 'audio/ambient/moaning-1.mp3',
ambientVolume: 0.5
}, },
nextStep: 'tts_edges' nextStep: 'library_expansion'
}, },
tts_edges: { library_expansion: {
type: 'action', type: 'action',
mood: 'controlled', mood: 'instructional',
story: 'The voice will count. You will edge each time it commands. 35 edges total. Listen. Obey. Edge.', story: 'Your library needs better organization. Tag 15 items with audio-focused labels: moaning, dirty-talk, wet-sounds. Proper tagging means better gooning sessions later.',
interactiveType: 'edge', interactiveType: 'tag-files',
params: { count: 35, instruction: 'Edge 35 times on command' }, params: {
minFiles: 15,
suggestedTags: ['audio', 'moaning', 'dirty-talk', 'pov', 'wet-sounds']
},
nextStep: 'audio_rhythm_combo'
},
audio_rhythm_combo: {
type: 'action',
mood: 'intense',
story: 'Now combine everything you\'ve learned. 25 minutes of rhythm training with video AND audio. Stroke to the beat. Pump to the moans. Let sound and sight merge into pure gooning bliss.',
interactiveType: 'rhythm',
params: {
enableVideo: true,
duration: 1500,
ambientAudio: 'audio/ambient/moaning-1.mp3',
ambientVolume: 0.4,
multiPattern: [
{ pattern: 'slow-fast-slow', duration: 450 },
{ pattern: 'varied-medium', duration: 600 },
{ pattern: 'varied-intense', duration: 450 }
]
},
nextStep: 'completion' nextStep: 'completion'
}, },
completion: { completion: {
type: 'completion', type: 'completion',
mood: 'authoritative', mood: 'satisfied',
story: '✅ Level 8 Complete - TTS UNLOCKED. You can hear your training now.', story: '✅ Level 8 Complete - AUDIO MASTERED. Sound is now part of your gooning arsenal.',
outcome: 'level8_complete' outcome: 'level8_complete'
} }
} }

View File

@ -4717,6 +4717,10 @@ class InteractiveTaskManager {
const enableMetronomeSound = task.params?.enableMetronomeSound !== false; // Default to true const enableMetronomeSound = task.params?.enableMetronomeSound !== false; // Default to true
const enableVideo = task.params?.enableVideo !== 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 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 = { const patterns = {
'slow-fast-slow': [60, 120, 60], 'slow-fast-slow': [60, 120, 60],
@ -4865,6 +4869,16 @@ class InteractiveTaskManager {
await playRandomVideo(); await playRandomVideo();
} }
// Start ambient audio if provided
if (ambientAudio) {
ambientAudioElement = new Audio(ambientAudio);
ambientAudioElement.loop = true;
ambientAudioElement.volume = ambientVolume;
ambientAudioElement.play().catch(err => {
console.warn('Could not play ambient audio:', err);
});
}
// Setup video controls (only if video enabled) // Setup video controls (only if video enabled)
const skipVideoBtn = enableVideo ? container.querySelector('#rhythm-skip-video') : null; const skipVideoBtn = enableVideo ? container.querySelector('#rhythm-skip-video') : null;
const videoVolumeSlider = enableVideo ? container.querySelector('#rhythm-video-volume') : null; const videoVolumeSlider = enableVideo ? container.querySelector('#rhythm-video-volume') : null;
@ -4996,6 +5010,12 @@ class InteractiveTaskManager {
audioContext.close().catch(err => console.warn('⚠️ Error closing audio context:', err)); audioContext.close().catch(err => console.warn('⚠️ Error closing audio context:', err));
} }
// Stop ambient audio
if (ambientAudioElement) {
ambientAudioElement.pause();
ambientAudioElement = null;
}
// Hide sidebar // Hide sidebar
const sidebar = document.getElementById('campaign-sidebar'); const sidebar = document.getElementById('campaign-sidebar');
if (sidebar) { if (sidebar) {
@ -5402,7 +5422,6 @@ class InteractiveTaskManager {
Skip to New Videos Skip to New Videos
</button> </button>
</div> </div>
<div id="dual-video-timer" class="timer-display" style="display: none; margin: 15px 0; font-size: 1.2em; font-weight: bold;"></div>
<div id="dual-video-status" class="status-area"></div> <div id="dual-video-status" class="status-area"></div>
</div> </div>
`; `;
@ -5411,28 +5430,80 @@ class InteractiveTaskManager {
const skipBtn = container.querySelector('#skip-dual-video-btn'); const skipBtn = container.querySelector('#skip-dual-video-btn');
const statusArea = container.querySelector('#dual-video-status'); const statusArea = container.querySelector('#dual-video-status');
const mainContainer = container.querySelector('#dual-video-main-container'); const mainContainer = container.querySelector('#dual-video-main-container');
const timerDisplay = container.querySelector('#dual-video-timer');
let availableVideos = []; let availableVideos = [];
const playDualVideos = async () => { const playDualVideos = async () => {
if (availableVideos.length < 2) return;
// Select two random videos
const video1 = availableVideos[Math.floor(Math.random() * availableVideos.length)];
let video2 = availableVideos[Math.floor(Math.random() * availableVideos.length)];
while (video2 === video1 && availableVideos.length > 1) {
video2 = availableVideos[Math.floor(Math.random() * availableVideos.length)];
}
// Clear existing videos // Clear existing videos
mainContainer.innerHTML = ''; mainContainer.innerHTML = '';
// Play main video // Handle main display based on mainVideo parameter
await window.videoPlayerManager.playTaskVideo(video1, mainContainer); 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 = '<div class="info">📸 Webcam active - watch yourself goon</div>';
} catch (error) {
console.error('Webcam error:', error);
statusArea.innerHTML = '<div class="error">⚠️ Could not access webcam</div>';
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);
}
// Play overlay video (PiP style) // Play overlay video (PiP style) - can be webcam or video
await window.videoPlayerManager.playOverlayVideo(video2); 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 = '<div class="error">⚠️ Could not access webcam for PiP</div>';
}
} else if (availableVideos.length >= 1) {
// pipVideo === 'overlay' - play a video in PiP
const video2 = availableVideos[Math.floor(Math.random() * availableVideos.length)];
await window.videoPlayerManager.playOverlayVideo(video2);
}
// Position the overlay player as PiP with draggable and resizable // Position the overlay player as PiP with draggable and resizable
if (window.videoPlayerManager.overlayPlayer) { if (window.videoPlayerManager.overlayPlayer) {
@ -5466,29 +5537,33 @@ class InteractiveTaskManager {
${pos.left ? 'left: ' + pos.left : ''}; ${pos.left ? 'left: ' + pos.left : ''};
${pos.right ? 'right: ' + pos.right : ''}; ${pos.right ? 'right: ' + pos.right : ''};
z-index: 100; z-index: 100;
width: 300px;
display: inline-block;
`; `;
// Move player into container // Move player into container
pipPlayer.parentElement.insertBefore(pipContainer, pipPlayer); pipPlayer.parentElement.insertBefore(pipContainer, pipPlayer);
pipContainer.appendChild(pipPlayer); pipContainer.appendChild(pipPlayer);
pipPlayer.style.position = 'relative'; pipPlayer.style.position = 'relative';
pipPlayer.style.top = 'auto'; pipPlayer.style.top = '0';
pipPlayer.style.bottom = 'auto'; pipPlayer.style.bottom = '0';
pipPlayer.style.left = 'auto'; pipPlayer.style.left = '0';
pipPlayer.style.right = 'auto'; pipPlayer.style.right = '0';
pipPlayer.style.display = 'block';
// Create resize handle // Create resize handle
const resizeHandle = document.createElement('div'); const resizeHandle = document.createElement('div');
resizeHandle.style.cssText = ` resizeHandle.style.cssText = `
position: absolute; position: absolute;
bottom: 2px; bottom: 0;
right: 2px; right: 0;
width: 20px; width: 20px;
height: 20px; height: 20px;
background: rgba(255,255,255,0.7); background: rgba(255,255,255,0.7);
cursor: nwse-resize; cursor: nwse-resize;
border-radius: 0 0 6px 0; border-radius: 0 0 6px 0;
z-index: 101; z-index: 102;
pointer-events: auto;
`; `;
resizeHandle.innerHTML = '⋰'; resizeHandle.innerHTML = '⋰';
resizeHandle.style.textAlign = 'center'; resizeHandle.style.textAlign = 'center';
@ -5557,9 +5632,17 @@ class InteractiveTaskManager {
}; };
skipBtn.addEventListener('click', async () => { skipBtn.addEventListener('click', async () => {
if (availableVideos.length >= 2) { const minVideosRequired = (mainVideo === 'webcam') ? 1 : 2;
await playDualVideos(); if (availableVideos.length >= minVideosRequired) {
statusArea.innerHTML = '<div class="info">⏭️ Loaded new videos</div>'; // 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 = '<div class="info">⏭️ Loaded new PiP video</div>';
} else {
await playDualVideos();
statusArea.innerHTML = '<div class="info">⏭️ Loaded new videos</div>';
}
} }
}); });
@ -5578,8 +5661,11 @@ class InteractiveTaskManager {
try { try {
const videos = window.videoPlayerManager.videoLibrary?.background || []; const videos = window.videoPlayerManager.videoLibrary?.background || [];
if (videos.length < 2) { // For webcam mode, we only need 1 video (for PiP)
statusArea.innerHTML = '<div class="info"> Need at least 2 videos - continuing anyway</div>'; const minVideosRequired = (mainVideo === 'webcam') ? 1 : 2;
if (videos.length < minVideosRequired) {
statusArea.innerHTML = `<div class="info"> Need at least ${minVideosRequired} video(s) - continuing anyway</div>`;
task.completed = true; task.completed = true;
const completeBtn = document.getElementById('interactive-complete-btn'); const completeBtn = document.getElementById('interactive-complete-btn');
if (completeBtn) completeBtn.disabled = false; if (completeBtn) completeBtn.disabled = false;
@ -5592,153 +5678,81 @@ class InteractiveTaskManager {
btn.style.display = 'none'; btn.style.display = 'none';
skipBtn.style.display = 'inline-block'; skipBtn.style.display = 'inline-block';
// Position the overlay player as PiP with draggable and resizable // Show sidebar and start countdown timer
if (window.videoPlayerManager.overlayPlayer) { const sidebar = document.getElementById('campaign-sidebar');
const pipPlayer = window.videoPlayerManager.overlayPlayer;
pipPlayer.style.position = 'fixed'; if (sidebar) {
pipPlayer.style.width = '300px'; let remainingTime = minDuration;
pipPlayer.style.height = 'auto'; const formatTime = (seconds) => {
pipPlayer.style.zIndex = '100'; const mins = Math.floor(seconds / 60);
pipPlayer.style.boxShadow = '0 0 20px rgba(0,0,0,0.5)'; const secs = seconds % 60;
pipPlayer.style.cursor = 'move'; return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
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']; sidebar.style.display = 'block';
Object.assign(pipPlayer.style, pos); sidebar.innerHTML = `
<div style="background: rgba(0, 0, 0, 0.9); padding: 20px; border-radius: 10px; border: 2px solid var(--color-primary); text-align: center;">
// Wrap in container for resize handle <div style="font-size: 0.9em; color: var(--text-muted); margin-bottom: 8px;">Time Remaining</div>
const pipContainer = document.createElement('div'); <div id="dual-video-timer" style="font-size: 2em; font-weight: bold; color: var(--color-primary);">${formatTime(remainingTime)}</div>
pipContainer.style.cssText = ` </div>
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;
`; `;
// Move player into container const timerInterval = setInterval(() => {
pipPlayer.parentElement.insertBefore(pipContainer, pipPlayer); remainingTime--;
pipContainer.appendChild(pipPlayer); const timerEl = document.getElementById('dual-video-timer');
pipPlayer.style.position = 'relative';
pipPlayer.style.top = 'auto'; if (remainingTime > 0 && timerEl) {
pipPlayer.style.bottom = 'auto'; timerEl.textContent = formatTime(remainingTime);
pipPlayer.style.left = 'auto'; } else {
pipPlayer.style.right = 'auto'; clearInterval(timerInterval);
if (sidebar) sidebar.style.display = 'none';
// Create resize handle
const resizeHandle = document.createElement('div');
resizeHandle.style.cssText = `
position: absolute;
bottom: 2px;
right: 2px;
width: 20px;
height: 20px;
background: rgba(255,255,255,0.7);
cursor: nwse-resize;
border-radius: 0 0 6px 0;
z-index: 101;
`;
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 statusArea.innerHTML = '<div class="success">✅ Dual video session complete</div>';
currentX = Math.max(0, Math.min(currentX, window.innerWidth - pipContainer.offsetWidth)); task.completed = true;
currentY = Math.max(0, Math.min(currentY, window.innerHeight - pipContainer.offsetHeight));
pipContainer.style.left = currentX + 'px'; const completeBtn = document.getElementById('interactive-complete-btn');
pipContainer.style.top = currentY + 'px'; if (completeBtn) {
pipContainer.style.right = 'auto'; completeBtn.disabled = false;
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';
} }
}); }, 1000);
document.addEventListener('mouseup', () => { // Store cleanup function
if (isDragging) { task.cleanup = () => {
isDragging = false;
pipPlayer.style.opacity = '1';
}
if (isResizing) {
isResizing = false;
}
});
}
btn.style.display = 'none';
timerDisplay.style.display = 'block';
// Start countdown timer
let remainingTime = minDuration;
timerDisplay.innerHTML = `⏱️ Keep watching both videos... ${remainingTime}s remaining`;
const timerInterval = setInterval(() => {
remainingTime--;
if (remainingTime > 0) {
timerDisplay.innerHTML = `⏱️ Keep watching both videos... ${remainingTime}s remaining`;
} else {
clearInterval(timerInterval); clearInterval(timerInterval);
timerDisplay.innerHTML = '✅ Minimum viewing time complete!'; if (sidebar) sidebar.style.display = 'none';
timerDisplay.style.color = 'var(--color-success, #4caf50)';
statusArea.innerHTML = '<div class="success">✅ Dual video session complete</div>'; // Clean up overlay player and its wrapper container
task.completed = true; if (window.videoPlayerManager && window.videoPlayerManager.overlayPlayer) {
const overlay = window.videoPlayerManager.overlayPlayer;
const completeBtn = document.getElementById('interactive-complete-btn');
if (completeBtn) { // Stop webcam stream if it's a webcam PiP
completeBtn.disabled = false; 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;
} }
}
}, 1000); // 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) { } catch (error) {
console.error('Dual video error:', error); console.error('Dual video error:', error);
@ -6035,6 +6049,10 @@ class InteractiveTaskManager {
const player = task.params?.player || 'task'; const player = task.params?.player || 'task';
const tags = task.params?.tags || []; const tags = task.params?.tags || [];
const minDuration = task.params?.minDuration || task.params?.duration || 60; // Support both minDuration and duration params 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;
let ambientAudioElement = null;
container.innerHTML = ` container.innerHTML = `
<div class="academy-video-start-task"> <div class="academy-video-start-task">
@ -6108,6 +6126,16 @@ class InteractiveTaskManager {
btn.style.display = 'none'; btn.style.display = 'none';
skipBtn.style.display = 'inline-block'; skipBtn.style.display = 'inline-block';
// Start ambient audio if provided
if (ambientAudio) {
ambientAudioElement = new Audio(ambientAudio);
ambientAudioElement.loop = true;
ambientAudioElement.volume = ambientVolume;
ambientAudioElement.play().catch(err => {
console.warn('Could not play ambient audio:', err);
});
}
// Add timer to sidebar // Add timer to sidebar
const sidebar = document.getElementById('campaign-sidebar'); const sidebar = document.getElementById('campaign-sidebar');
if (sidebar) { if (sidebar) {
@ -6170,6 +6198,10 @@ class InteractiveTaskManager {
if (timerInterval) { if (timerInterval) {
clearInterval(timerInterval); clearInterval(timerInterval);
} }
if (ambientAudioElement) {
ambientAudioElement.pause();
ambientAudioElement = null;
}
// Hide sidebar // Hide sidebar
const sidebar = document.getElementById('campaign-sidebar'); const sidebar = document.getElementById('campaign-sidebar');