refined level 6-8
This commit is contained in:
parent
5bd002e72d
commit
c9a01f8ed6
|
|
@ -4814,6 +4814,12 @@
|
|||
function completeScenarioAction(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
|
||||
const webcamVideo = document.getElementById('webcam-video');
|
||||
if (webcamVideo && webcamVideo.srcObject) {
|
||||
|
|
|
|||
|
|
@ -747,9 +747,14 @@ const trainingGameData = {
|
|||
mirror_edges: {
|
||||
type: 'action',
|
||||
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?',
|
||||
interactiveType: 'edge',
|
||||
params: { count: 25, instruction: 'Edge 25 times while watching yourself', keepVideoPlaying: true },
|
||||
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: 'dual-video',
|
||||
params: {
|
||||
mainVideo: 'focus',
|
||||
pipVideo: 'webcam',
|
||||
pipPosition: 'bottom-right',
|
||||
minDuration: 900
|
||||
},
|
||||
nextStep: 'completion'
|
||||
},
|
||||
completion: {
|
||||
|
|
@ -767,7 +772,7 @@ const trainingGameData = {
|
|||
name: 'Dual Focus',
|
||||
arc: 'Feature Discovery',
|
||||
level: 7,
|
||||
duration: 2400, // 40 minutes
|
||||
duration: 3600, // 60 minutes
|
||||
interactiveType: 'scenario-adventure',
|
||||
interactiveData: {
|
||||
title: 'Level 7: Dual Focus',
|
||||
|
|
@ -783,21 +788,33 @@ const trainingGameData = {
|
|||
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.',
|
||||
interactiveType: 'dual-video',
|
||||
params: { mainVideo: 'focus', pipVideo: 'overlay', pipPosition: 'bottom-right' },
|
||||
nextStep: 'dual_edges'
|
||||
params: { mainVideo: 'focus', pipVideo: 'overlay', pipPosition: 'bottom-right', minDuration: 120 },
|
||||
nextStep: 'media_linking'
|
||||
},
|
||||
dual_edges: {
|
||||
media_linking: {
|
||||
type: 'action',
|
||||
mood: 'demanding',
|
||||
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.',
|
||||
interactiveType: 'edge',
|
||||
params: { count: 30, instruction: 'Edge 30 times to dual video streams' },
|
||||
mood: 'instructional',
|
||||
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: 'link-media',
|
||||
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'
|
||||
},
|
||||
completion: {
|
||||
type: 'completion',
|
||||
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'
|
||||
}
|
||||
}
|
||||
|
|
@ -806,43 +823,66 @@ const trainingGameData = {
|
|||
|
||||
level8: {
|
||||
id: 'academy-level-8',
|
||||
name: 'Vocal Commands',
|
||||
name: 'Audio Immersion',
|
||||
arc: 'Feature Discovery',
|
||||
level: 8,
|
||||
duration: 3000, // 50 minutes
|
||||
duration: 2700, // 45 minutes
|
||||
interactiveType: 'scenario-adventure',
|
||||
interactiveData: {
|
||||
title: 'Level 8: Vocal Commands',
|
||||
title: 'Level 8: Audio Immersion',
|
||||
steps: {
|
||||
start: {
|
||||
type: 'story',
|
||||
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.',
|
||||
nextStep: 'tts_intro'
|
||||
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: 'audio_warmup'
|
||||
},
|
||||
tts_intro: {
|
||||
audio_warmup: {
|
||||
type: 'action',
|
||||
mood: 'authoritative',
|
||||
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.',
|
||||
interactiveType: 'tts-command',
|
||||
mood: 'hypnotic',
|
||||
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: 'video-start',
|
||||
params: {
|
||||
text: 'Good gooner. You are learning to obey. Edge for me. Now.',
|
||||
voice: 'feminine'
|
||||
player: 'focus',
|
||||
duration: 300,
|
||||
ambientAudio: 'audio/ambient/moaning-1.mp3',
|
||||
ambientVolume: 0.5
|
||||
},
|
||||
nextStep: 'tts_edges'
|
||||
nextStep: 'library_expansion'
|
||||
},
|
||||
tts_edges: {
|
||||
library_expansion: {
|
||||
type: 'action',
|
||||
mood: 'controlled',
|
||||
story: 'The voice will count. You will edge each time it commands. 35 edges total. Listen. Obey. Edge.',
|
||||
interactiveType: 'edge',
|
||||
params: { count: 35, instruction: 'Edge 35 times on command' },
|
||||
mood: 'instructional',
|
||||
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: 'tag-files',
|
||||
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'
|
||||
},
|
||||
completion: {
|
||||
type: 'completion',
|
||||
mood: 'authoritative',
|
||||
story: '✅ Level 8 Complete - TTS UNLOCKED. You can hear your training now.',
|
||||
mood: 'satisfied',
|
||||
story: '✅ Level 8 Complete - AUDIO MASTERED. Sound is now part of your gooning arsenal.',
|
||||
outcome: 'level8_complete'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4717,6 +4717,10 @@ class InteractiveTaskManager {
|
|||
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, 120, 60],
|
||||
|
|
@ -4865,6 +4869,16 @@ class InteractiveTaskManager {
|
|||
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)
|
||||
const skipVideoBtn = enableVideo ? container.querySelector('#rhythm-skip-video') : 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));
|
||||
}
|
||||
|
||||
// Stop ambient audio
|
||||
if (ambientAudioElement) {
|
||||
ambientAudioElement.pause();
|
||||
ambientAudioElement = null;
|
||||
}
|
||||
|
||||
// Hide sidebar
|
||||
const sidebar = document.getElementById('campaign-sidebar');
|
||||
if (sidebar) {
|
||||
|
|
@ -5402,7 +5422,6 @@ class InteractiveTaskManager {
|
|||
⏭️ Skip to New Videos
|
||||
</button>
|
||||
</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>
|
||||
`;
|
||||
|
|
@ -5411,28 +5430,80 @@ class InteractiveTaskManager {
|
|||
const skipBtn = container.querySelector('#skip-dual-video-btn');
|
||||
const statusArea = container.querySelector('#dual-video-status');
|
||||
const mainContainer = container.querySelector('#dual-video-main-container');
|
||||
const timerDisplay = container.querySelector('#dual-video-timer');
|
||||
|
||||
let availableVideos = [];
|
||||
|
||||
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
|
||||
mainContainer.innerHTML = '';
|
||||
|
||||
// Play main video
|
||||
await window.videoPlayerManager.playTaskVideo(video1, mainContainer);
|
||||
// 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 = '<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)
|
||||
await window.videoPlayerManager.playOverlayVideo(video2);
|
||||
// Play overlay video (PiP style) - can be webcam or video
|
||||
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
|
||||
if (window.videoPlayerManager.overlayPlayer) {
|
||||
|
|
@ -5466,29 +5537,33 @@ class InteractiveTaskManager {
|
|||
${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 = 'auto';
|
||||
pipPlayer.style.bottom = 'auto';
|
||||
pipPlayer.style.left = 'auto';
|
||||
pipPlayer.style.right = 'auto';
|
||||
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: 2px;
|
||||
right: 2px;
|
||||
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: 101;
|
||||
z-index: 102;
|
||||
pointer-events: auto;
|
||||
`;
|
||||
resizeHandle.innerHTML = '⋰';
|
||||
resizeHandle.style.textAlign = 'center';
|
||||
|
|
@ -5557,9 +5632,17 @@ class InteractiveTaskManager {
|
|||
};
|
||||
|
||||
skipBtn.addEventListener('click', async () => {
|
||||
if (availableVideos.length >= 2) {
|
||||
await playDualVideos();
|
||||
statusArea.innerHTML = '<div class="info">⏭️ Loaded new videos</div>';
|
||||
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 = '<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 {
|
||||
const videos = window.videoPlayerManager.videoLibrary?.background || [];
|
||||
|
||||
if (videos.length < 2) {
|
||||
statusArea.innerHTML = '<div class="info">ℹ️ Need at least 2 videos - continuing anyway</div>';
|
||||
// For webcam mode, we only need 1 video (for PiP)
|
||||
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;
|
||||
const completeBtn = document.getElementById('interactive-complete-btn');
|
||||
if (completeBtn) completeBtn.disabled = false;
|
||||
|
|
@ -5592,153 +5678,81 @@ class InteractiveTaskManager {
|
|||
btn.style.display = 'none';
|
||||
skipBtn.style.display = 'inline-block';
|
||||
|
||||
// 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' }
|
||||
// Show sidebar and start countdown timer
|
||||
const sidebar = document.getElementById('campaign-sidebar');
|
||||
|
||||
if (sidebar) {
|
||||
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')}`;
|
||||
};
|
||||
|
||||
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;
|
||||
sidebar.style.display = 'block';
|
||||
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;">
|
||||
<div style="font-size: 0.9em; color: var(--text-muted); margin-bottom: 8px;">Time Remaining</div>
|
||||
<div id="dual-video-timer" style="font-size: 2em; font-weight: bold; color: var(--color-primary);">${formatTime(remainingTime)}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Move player into container
|
||||
pipPlayer.parentElement.insertBefore(pipContainer, pipPlayer);
|
||||
pipContainer.appendChild(pipPlayer);
|
||||
pipPlayer.style.position = 'relative';
|
||||
pipPlayer.style.top = 'auto';
|
||||
pipPlayer.style.bottom = 'auto';
|
||||
pipPlayer.style.left = 'auto';
|
||||
pipPlayer.style.right = 'auto';
|
||||
|
||||
// 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;
|
||||
const timerInterval = setInterval(() => {
|
||||
remainingTime--;
|
||||
const timerEl = document.getElementById('dual-video-timer');
|
||||
|
||||
if (remainingTime > 0 && timerEl) {
|
||||
timerEl.textContent = formatTime(remainingTime);
|
||||
} else {
|
||||
clearInterval(timerInterval);
|
||||
if (sidebar) sidebar.style.display = 'none';
|
||||
|
||||
// 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));
|
||||
statusArea.innerHTML = '<div class="success">✅ Dual video session complete</div>';
|
||||
task.completed = true;
|
||||
|
||||
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';
|
||||
const completeBtn = document.getElementById('interactive-complete-btn');
|
||||
if (completeBtn) {
|
||||
completeBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
document.addEventListener('mouseup', () => {
|
||||
if (isDragging) {
|
||||
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 {
|
||||
// Store cleanup function
|
||||
task.cleanup = () => {
|
||||
clearInterval(timerInterval);
|
||||
timerDisplay.innerHTML = '✅ Minimum viewing time complete!';
|
||||
timerDisplay.style.color = 'var(--color-success, #4caf50)';
|
||||
if (sidebar) sidebar.style.display = 'none';
|
||||
|
||||
statusArea.innerHTML = '<div class="success">✅ Dual video session complete</div>';
|
||||
task.completed = true;
|
||||
|
||||
const completeBtn = document.getElementById('interactive-complete-btn');
|
||||
if (completeBtn) {
|
||||
completeBtn.disabled = false;
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}, 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) {
|
||||
console.error('Dual video error:', error);
|
||||
|
|
@ -6035,6 +6049,10 @@ class InteractiveTaskManager {
|
|||
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;
|
||||
|
||||
let ambientAudioElement = null;
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="academy-video-start-task">
|
||||
|
|
@ -6108,6 +6126,16 @@ class InteractiveTaskManager {
|
|||
btn.style.display = 'none';
|
||||
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
|
||||
const sidebar = document.getElementById('campaign-sidebar');
|
||||
if (sidebar) {
|
||||
|
|
@ -6170,6 +6198,10 @@ class InteractiveTaskManager {
|
|||
if (timerInterval) {
|
||||
clearInterval(timerInterval);
|
||||
}
|
||||
if (ambientAudioElement) {
|
||||
ambientAudioElement.pause();
|
||||
ambientAudioElement = null;
|
||||
}
|
||||
|
||||
// Hide sidebar
|
||||
const sidebar = document.getElementById('campaign-sidebar');
|
||||
|
|
|
|||
Loading…
Reference in New Issue