levels 1-5 refined

This commit is contained in:
dilgenfritz 2025-12-02 23:44:23 -06:00
parent c3669f309c
commit 5bd002e72d
4 changed files with 6463 additions and 105 deletions

6007
campaign.html Normal file

File diff suppressed because it is too large Load Diff

View File

@ -226,8 +226,12 @@
<!-- Feature Highlights - Now Main Action Buttons -->
<div class="hero-features">
<button class="hero-feature btn-feature" id="training-academy-btn">
<button class="hero-feature btn-feature" id="campaign-btn">
<span class="feature-icon">🎯</span>
<span class="feature-text">Campaign Mode</span>
</button>
<button class="hero-feature btn-feature" id="training-academy-btn">
<span class="feature-icon">🎓</span>
<span class="feature-text">Training Academy</span>
</button>
<button class="hero-feature btn-feature" id="quick-play-btn">
@ -3747,6 +3751,16 @@
});
}
// Set up campaign mode button (only once)
const campaignBtn = document.getElementById('campaign-btn');
if (campaignBtn && !campaignBtn.hasAttribute('data-handler-attached')) {
campaignBtn.setAttribute('data-handler-attached', 'true');
campaignBtn.addEventListener('click', () => {
console.log('🎯 Opening Campaign Mode...');
window.location.href = 'campaign.html';
});
}
// Set up training academy button (only once)
const trainingAcademyBtn = document.getElementById('training-academy-btn');
if (trainingAcademyBtn && !trainingAcademyBtn.hasAttribute('data-handler-attached')) {

View File

@ -514,7 +514,7 @@ const trainingGameData = {
mood: 'focused',
story: 'Good. Now follow the rhythm pattern: slow... fast... slow. Let the metronome guide your hand. Feel how changing pace builds different sensations. Slow is teasing. Fast is intense. Together, they create perfect edging rhythm.',
interactiveType: 'rhythm',
params: { pattern: 'slow-fast-slow', duration: 180 },
params: { pattern: 'slow-fast-slow', duration: 180, enableVideo: false },
nextStep: 'final_edges'
},
final_edges: {
@ -556,7 +556,7 @@ const trainingGameData = {
mood: 'intense',
story: 'I am starting a video from your library. Watch it. Let it fill your vision. Let it fill your mind. This is what you stroke to now. Not fantasy. Not imagination. Reality captured on screen.',
interactiveType: 'video-start',
params: { player: 'focus', tags: ['amateur'] },
params: { player: 'focus', tags: ['amateur'], duration: 120 },
nextStep: 'tagging_task'
},
tagging_task: {
@ -568,18 +568,14 @@ const trainingGameData = {
minFiles: 10,
suggestedTags: ['pov', 'blowjob', 'riding', 'amateur']
},
nextStep: 'visual_edges'
nextStep: 'extended_video_watch'
},
visual_edges: {
extended_video_watch: {
type: 'action',
mood: 'commanding',
story: 'Good. Now edge while watching. Feel the difference. The video makes it so much more intense, doesn\'t it? This is why gooners worship porn. It amplifies everything.',
interactiveType: 'edge',
params: {
duration: 300, // 5 minutes of edging
instruction: 'Edge and stroke while watching the video',
keepVideoPlaying: true // Signal to keep video active
},
mood: 'intense',
story: 'Good. Your library is growing more organized. Now watch for 10 minutes while you continue stroking. Let the videos consume you. Feel how the visual stimulation makes everything more intense. This is training.',
interactiveType: 'video-start',
params: { player: 'focus', tags: ['amateur'], duration: 600 },
nextStep: 'completion'
},
completion: {
@ -597,7 +593,7 @@ const trainingGameData = {
name: 'Multi-Tasking Challenge',
arc: 'Foundation',
level: 4,
duration: 1200, // 20 minutes
duration: 1800, // 30 minutes
interactiveType: 'scenario-adventure',
interactiveData: {
title: 'Level 4: Multi-Tasking Challenge',
@ -605,35 +601,30 @@ const trainingGameData = {
start: {
type: 'story',
mood: 'challenging',
story: 'You are progressing well. Now we test if you can combine what you\'ve learned. Rhythm + Video + Edges. All at once. This is where casual masturbators fail, but gooners thrive. Show me you can handle it.',
nextStep: 'everything_at_once'
story: 'You are progressing well. Now we test if you can combine what you\'ve learned. Rhythm + Video. All at once. This is where casual masturbators fail, but gooners thrive. Show me you can handle extended multi-tasking. The rhythm will change throughout - stay focused.',
nextStep: 'continuous_rhythm_session'
},
everything_at_once: {
continuous_rhythm_session: {
type: 'action',
mood: 'intense',
story: 'Video starting. Now follow this rhythm: FAST... slow... FAST. While watching. While edging. Your brain is being rewired to handle multiple streams of pleasure at once. This is gooner multitasking.',
story: 'Video starting. Follow the rhythm. It will change constantly - slow, fast, faster, INTENSE. Stay focused. Stay stroking. This is gooner multitasking at its finest.',
interactiveType: 'rhythm',
params: { pattern: 'fast-slow-fast', duration: 240 },
nextStep: 'sustained_edges'
},
sustained_edges: {
type: 'action',
mood: 'demanding',
story: 'Don\'t stop the video. Don\'t lose the rhythm. Now edge 20 times. I know it\'s hard. I know your hand is tired. But gooners don\'t quit. Gooners push through. Edge. Again. AGAIN.',
interactiveType: 'edge',
params: { count: 20, instruction: 'Edge 20 times - maintain rhythm and focus' },
nextStep: 'almost_there'
},
almost_there: {
type: 'story',
mood: 'encouraging',
story: 'Keep going... faster... you\'re doing it. You\'re combining all the skills. This is what a gooner looks like - pumping, watching, edging, completely immersed. Almost there...',
params: {
enableVideo: true,
duration: 1260,
multiPattern: [
{ pattern: 'fast-slow-fast', duration: 300 },
{ pattern: 'varied-slow', duration: 300 },
{ pattern: 'varied-medium', duration: 360 },
{ pattern: 'varied-intense', duration: 300 }
]
},
nextStep: 'completion'
},
completion: {
type: 'completion',
mood: 'triumphant',
story: '✅ Level 4 Complete - You can multi-task like a true gooner. Foundation Arc nearly complete.',
story: '✅ Level 4 Complete - You can multi-task like a true gooner. You\'ve mastered extended rhythm sessions. Foundation Arc nearly complete.',
outcome: 'level4_complete'
}
}
@ -646,7 +637,7 @@ const trainingGameData = {
arc: 'Foundation',
level: 5,
isCheckpoint: true,
duration: 1500, // 25 minutes
duration: 2100, // 35 minutes
interactiveType: 'scenario-adventure',
interactiveData: {
title: 'Level 5: Foundation Checkpoint',
@ -654,35 +645,70 @@ const trainingGameData = {
start: {
type: 'story',
mood: 'ceremonial',
story: 'You have reached your first checkpoint. Levels 1-4 have taught you edges, rhythm, videos, and multi-tasking. Now we test everything together and update your preferences. This is a milestone. Prove you are ready for what comes next.',
story: 'You have reached your first checkpoint. Levels 1-4 have taught you edges, rhythm, videos, and multi-tasking. Now we test everything together. This is your Foundation Arc final exam - prove you are ready for what comes next.',
nextStep: 'preference_update'
},
preference_update: {
type: 'action',
mood: 'reflective',
story: 'First, tell The Academy what you crave. What themes excite you? What content makes you hardest? Update your preferences so your training can be personalized. This is your checkpoint configuration.',
story: 'First, tell The Academy what you crave. What themes excite you? What content makes you hardest? Update your preferences so your training can be personalized for Arc 2.',
interactiveType: 'update-preferences',
params: { checkpoint: 5 },
nextStep: 'recap_training'
nextStep: 'warmup_video'
},
recap_training: {
warmup_video: {
type: 'action',
mood: 'building',
story: 'Good. Now let\'s begin your comprehensive test. Start with 3 minutes of video watching. Get aroused. Get focused. The real challenge comes next.',
interactiveType: 'video-start',
params: { player: 'focus', duration: 180 },
nextStep: 'test_rhythm'
},
test_rhythm: {
type: 'action',
mood: 'challenging',
story: 'Rhythm test: 8 minutes of varied patterns while watching video. Prove you can maintain rhythm control while visually stimulated. This combines Levels 2 and 3.',
interactiveType: 'rhythm',
params: {
enableVideo: true,
duration: 480,
multiPattern: [
{ pattern: 'slow-fast-slow', duration: 120 },
{ pattern: 'escalating', duration: 180 },
{ pattern: 'varied-medium', duration: 180 }
]
},
nextStep: 'library_expansion'
},
library_expansion: {
type: 'action',
mood: 'practical',
story: 'While you catch your breath, expand your library. Add more content. Tag 15 files. A gooner maintains their collection. This is part of the lifestyle.',
interactiveType: 'tag-files',
params: {
minFiles: 15,
suggestedTags: ['pov', 'blowjob', 'riding', 'amateur', 'solo']
},
nextStep: 'endurance_challenge'
},
endurance_challenge: {
type: 'action',
mood: 'intense',
story: 'Now we recap all your training. Video on. Rhythm: steady. Edge as many times as you can in the time remaining. This is your Foundation Arc final exam. Show me you\'ve learned.',
interactiveType: 'free-edge-session',
params: { duration: 900, allowedFeatures: ['video', 'rhythm'] },
story: 'Final test: 15 minutes of continuous video immersion. No rhythm guidance. No structure. Just you, the porn, and your hand. Prove you can maintain focus and edge without assistance. This is what all your training has built toward.',
interactiveType: 'video-start',
params: { player: 'focus', duration: 900 },
nextStep: 'arc_complete'
},
arc_complete: {
type: 'story',
mood: 'triumphant',
story: 'Foundation Arc Complete. You are no longer a beginner. You understand edges. You have rhythm. You use porn as fuel. Arc 2 awaits: Feature Discovery. Get ready - things are about to get much more intense.',
story: '🎯 Foundation Arc Complete. You are no longer a beginner. You understand edges. You have rhythm. You use porn as fuel. You can multi-task like a true gooner. Arc 2 awaits: Feature Discovery. Get ready - things are about to get much more intense.',
nextStep: 'completion'
},
completion: {
type: 'completion',
mood: 'accomplished',
story: '✅🎯 Level 5 Complete - FOUNDATION ARC FINISHED. Feature Discovery begins next.',
story: '✅🏆 Level 5 Complete - FOUNDATION ARC FINISHED. Feature Discovery begins next.',
outcome: 'level5_complete'
}
}
@ -709,17 +735,21 @@ const trainingGameData = {
webcam_intro: {
type: 'action',
mood: 'revealing',
story: 'Turn on your webcam. Position it so you can see yourself stroking. Look at yourself. Really look. That is who you are now. A gooner. Pumping. Edging. Addicted to porn. Embrace it.',
interactiveType: 'enable-webcam',
params: { instruction: 'Enable your webcam and watch yourself goon' },
story: 'Turn on your webcam. Position it so you can see yourself stroking. And I will add a video feed to accompany you. Look at yourself while porn plays. See yourself as a gooner - pumping, watching, edging. Embrace it.',
interactiveType: 'dual-video',
params: {
mainVideo: 'webcam',
pipVideo: 'overlay',
pipPosition: 'bottom-right'
},
nextStep: 'mirror_edges'
},
mirror_edges: {
type: 'action',
mood: 'intense',
story: 'Now edge 25 times while watching yourself. See your face flush. See your hand pumping. This is your reality. This is who you\'re becoming. And it feels good, doesn\'t it?',
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' },
params: { count: 25, instruction: 'Edge 25 times while watching yourself', keepVideoPlaying: true },
nextStep: 'completion'
},
completion: {

View File

@ -4714,62 +4714,249 @@ class InteractiveTaskManager {
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 patterns = {
'slow-fast-slow': [60, 120, 60],
'fast-slow-fast': [120, 60, 120],
'steady': [90, 90, 90],
'escalating': [60, 90, 120, 150]
'escalating': [60, 90, 120, 150],
'varied-slow': [70, 100, 80, 0, 120, 90, 140],
'varied-medium': [100, 150, 120, 180, 0, 140, 200],
'varied-intense': [140, 200, 0, 160, 220, 180, 240]
};
const bpmSequence = patterns[pattern] || patterns['steady'];
// 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 = `
<div class="academy-rhythm-task">
<h3>🎵 Rhythm Pattern: ${pattern}</h3>
<div class="rhythm-display">
<div class="metronome-visual" id="metronome-visual"></div>
<h3 id="rhythm-pattern-title">🎵 Rhythm Pattern: ${currentPatternData.pattern}</h3>
${enableVideo ? `
<div id="rhythm-video-container" class="video-player-container" style="margin: 20px auto; min-height: 500px; max-width: 1200px; text-align: center; position: relative;"></div>
<!-- Video Controls -->
<div class="rhythm-controls" style="display: flex; gap: 15px; justify-content: center; align-items: center; margin: 15px 0; flex-wrap: wrap;">
<button id="rhythm-skip-video" class="btn btn-secondary" style="padding: 8px 15px;"> Skip Video</button>
<div style="display: flex; align-items: center; gap: 8px;">
<label style="font-size: 0.9em;">🔊 Video:</label>
<input type="range" id="rhythm-video-volume" min="0" max="100" value="50" style="width: 120px;">
<span id="rhythm-video-volume-display" style="font-size: 0.9em; min-width: 40px;">50%</span>
</div>
<div style="display: flex; align-items: center; gap: 8px;">
<label style="font-size: 0.9em;">🎵 Metronome:</label>
<input type="range" id="rhythm-metronome-volume" min="0" max="100" value="30" style="width: 120px;">
<span id="rhythm-metronome-volume-display" style="font-size: 0.9em; min-width: 40px;">30%</span>
</div>
</div>
` : `
<!-- Metronome Control Only -->
<div class="rhythm-controls" style="display: flex; gap: 15px; justify-content: center; align-items: center; margin: 15px 0;">
<div style="display: flex; align-items: center; gap: 8px;">
<label style="font-size: 0.9em;">🎵 Metronome:</label>
<input type="range" id="rhythm-metronome-volume" min="0" max="100" value="30" style="width: 120px;">
<span id="rhythm-metronome-volume-display" style="font-size: 0.9em; min-width: 40px;">30%</span>
</div>
</div>
`}
<div class="rhythm-display" style="position: relative; z-index: 10;">
<div class="metronome-visual" id="metronome-visual" style="position: relative; z-index: 15;"></div>
<div class="bpm-display">
<span class="bpm-value">${bpmSequence[0]}</span> BPM
<span class="bpm-value">${bpmSequence[0] === 0 ? 'PAUSE' : bpmSequence[0]}</span> BPM
</div>
<div class="phase-indicator">
Phase ${currentPhase + 1} of ${bpmSequence.length}
</div>
</div>
<div class="timer-display">
<span id="rhythm-timer">${timeRemaining}s</span> remaining
</div>
</div>
`;
// Add timer to sidebar
const sidebar = document.getElementById('campaign-sidebar');
if (sidebar) {
sidebar.style.display = 'block';
const mins = Math.floor(timeRemaining / 60);
const secs = timeRemaining % 60;
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="rhythm-timer" style="font-size: 2em; font-weight: bold; color: var(--color-primary);">${mins}:${String(secs).padStart(2, '0')}</div>
</div>
`;
}
// 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.videoLibrary?.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();
}
// 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');
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 timerDisplay = container.querySelector('#rhythm-timer');
const timerDisplay = document.getElementById('rhythm-timer'); // Now in sidebar
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
const phaseDuration = duration / bpmSequence.length;
let phaseDuration = currentPatternData.duration / bpmSequence.length;
let phaseTime = phaseDuration;
const countdown = setInterval(() => {
timeRemaining--;
patternTimeRemaining--;
phaseTime--;
timerDisplay.textContent = `${timeRemaining}s`;
if (phaseTime <= 0 && currentPhase < bpmSequence.length - 1) {
const mins = Math.floor(timeRemaining / 60);
const secs = timeRemaining % 60;
timerDisplay.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];
@ -4778,7 +4965,7 @@ class InteractiveTaskManager {
clearInterval(beatTimer);
beatTimer = setInterval(beat, beatInterval);
bpmDisplay.textContent = bpm;
bpmDisplay.textContent = bpm === 0 ? 'PAUSE' : bpm;
phaseIndicator.textContent = `Phase ${currentPhase + 1} of ${bpmSequence.length}`;
}
@ -4798,8 +4985,23 @@ class InteractiveTaskManager {
// 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));
}
// Hide sidebar
const sidebar = document.getElementById('campaign-sidebar');
if (sidebar) {
sidebar.style.display = 'none';
sidebar.innerHTML = '';
}
};
}
@ -4872,30 +5074,70 @@ class InteractiveTaskManager {
const suggestedTags = task.params?.suggestedTags || [];
const preserveContent = task.params?.preserveContent !== false; // Default true
// Track tagged files count
if (!task.taggedFilesCount) {
task.taggedFilesCount = 0;
// 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 = `
<div class="academy-tag-task" style="${preserveContent ? 'margin-top: 20px; padding-top: 20px; border-top: 2px solid var(--color-primary);' : ''}">
<h3>🏷 Tag Your Files</h3>
<p>Tag at least ${minFiles} files in your library to improve content filtering.</p>
<p>Add ${minFiles} tags to your library (you can add multiple tags to the same file).</p>
${directory ? `<p>Focus on: <code>${directory}</code></p>` : ''}
${suggestedTags.length > 0 ? `
<p class="suggestion">💡 Suggested tags: ${suggestedTags.map(t => `<span class="tag-chip" style="display: inline-block; background: var(--color-primary); color: white; padding: 4px 12px; border-radius: 12px; margin: 2px; font-size: 0.9em;">${t}</span>`).join(' ')}</p>
` : ''}
<div class="tag-progress" style="margin: 15px 0; padding: 10px; background: rgba(0,0,0,0.2); border-radius: 8px;">
<div style="font-size: 1.2em; font-weight: bold;">
<span id="files-tagged">${task.taggedFilesCount || 0}</span> / ${minFiles} files tagged
<span id="tags-added-count">${task.totalTagsCount || 0}</span> / ${targetTags} tags
<span style="font-size: 0.8em; color: var(--text-muted); display: block; margin-top: 4px;">
(+<span id="tags-added-session">${Math.max(0, task.totalTagsCount - task.initialTagsCount)}</span> added this session)
</span>
</div>
<div style="margin-top: 8px; height: 8px; background: rgba(255,255,255,0.1); border-radius: 4px; overflow: hidden;">
<div id="tag-progress-bar" style="height: 100%; background: var(--color-success); width: ${Math.min(100, (task.taggedFilesCount || 0) / minFiles * 100)}%; transition: width 0.3s;"></div>
<div id="tag-progress-bar" style="height: 100%; background: var(--color-success); width: ${Math.min(100, (task.totalTagsCount || 0) / targetTags * 100)}%; transition: width 0.3s;"></div>
</div>
</div>
<button class="btn btn-primary" id="open-tagging-btn" style="width: 100%; padding: 12px; font-size: 1.1em;">
🏷 Open Library Tagging Interface
</button>
<button class="btn btn-secondary" id="refresh-progress-btn" style="width: 100%; padding: 10px; margin-top: 8px; font-size: 0.95em;">
🔄 Refresh Progress
</button>
<div id="tag-task-status" class="status-area" style="margin-top: 10px;"></div>
</div>
`;
@ -4909,58 +5151,77 @@ class InteractiveTaskManager {
}
const btn = container.querySelector('#open-tagging-btn');
const progressEl = container.querySelector('#files-tagged');
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 how many media items have tags by checking localStorage directly
// 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 media items that have at least one tag
const taggedMedia = Object.keys(mediaTags).filter(mediaId => {
const tags = mediaTags[mediaId];
return tags && tags.length > 0;
// 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.taggedFilesCount = taggedMedia.length;
console.log(`🏷️ Progress update: ${task.taggedFilesCount} files tagged`);
task.totalTagsCount = totalTags;
console.log(`🏷️ Progress update: ${task.totalTagsCount} total tags (started with ${initialCount})`);
} else {
task.taggedFilesCount = 0;
console.log('🏷️ No tag data found in localStorage');
task.totalTagsCount = initialCount;
console.log('🏷️ No tag data found in localStorage, keeping initial count');
}
} catch (error) {
console.error('❌ Error reading tag data:', error);
task.taggedFilesCount = 0;
task.totalTagsCount = initialCount;
}
progressEl.textContent = task.taggedFilesCount;
const percent = Math.min(100, (task.taggedFilesCount / minFiles) * 100);
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.taggedFilesCount >= minFiles) {
if (task.totalTagsCount >= targetTags) {
task.completed = true;
btn.disabled = true;
btn.textContent = '✅ Tagging Complete';
btn.style.background = 'var(--color-success)';
statusArea.innerHTML = `<div class="success">✅ You have tagged ${task.taggedFilesCount} files! Task complete.</div>`;
statusArea.innerHTML = `<div class="success">✅ You have added ${tagsAdded} new tags! Task complete.</div>`;
const completeBtn = document.getElementById('interactive-complete-btn');
if (completeBtn) {
completeBtn.disabled = false;
}
} else {
statusArea.innerHTML = `<div class="info"> Tag ${minFiles - task.taggedFilesCount} more file(s) to complete this task</div>`;
const remaining = targetTags - task.totalTagsCount;
statusArea.innerHTML = `<div class="info"> Add ${remaining} more tag(s) to complete this task</div>`;
}
};
// Check initial progress
updateProgress();
// Manual refresh button
refreshBtn.addEventListener('click', () => {
console.log('🔄 Manual progress refresh triggered');
updateProgress();
statusArea.innerHTML = '<div class="info">🔄 Progress refreshed!</div>';
});
btn.addEventListener('click', async () => {
console.log('🏷️ Tag Files button clicked');
console.log('🏷️ electronAPI available:', !!window.electronAPI);
@ -5773,13 +6034,12 @@ class InteractiveTaskManager {
async createVideoStartTask(task, container) {
const player = task.params?.player || 'task';
const tags = task.params?.tags || [];
const minDuration = task.params?.minDuration || 60; // Default 60 seconds minimum viewing
const minDuration = task.params?.minDuration || task.params?.duration || 60; // Support both minDuration and duration params
container.innerHTML = `
<div class="academy-video-start-task">
<h3>🎬 Video Immersion</h3>
<p>Prepare to focus on the video content</p>
<p class="info-text">Minimum viewing time: <strong>${minDuration} seconds</strong></p>
${tags.length > 0 ? '<p>Filtering by tags: ' + tags.map(t => '<span class="tag">' + t + '</span>').join(' ') + '</p>' : ''}
<div id="video-player-container" class="video-player-container" style="margin: 20px 0; min-height: 400px; text-align: center;"></div>
<div style="display: flex; gap: 10px; justify-content: center; margin: 10px 0;">
@ -5790,7 +6050,6 @@ class InteractiveTaskManager {
Skip to Next Video
</button>
</div>
<div id="video-timer-display" class="timer-display" style="display: none; margin: 15px 0; font-size: 1.2em; font-weight: bold;"></div>
<div id="video-start-status" class="status-area"></div>
</div>
`;
@ -5799,9 +6058,9 @@ class InteractiveTaskManager {
const skipBtn = container.querySelector('#skip-video-btn');
const statusArea = container.querySelector('#video-start-status');
const videoContainer = container.querySelector('#video-player-container');
const timerDisplay = container.querySelector('#video-timer-display');
let availableVideos = [];
let timerInterval = null;
const playRandomVideo = async () => {
if (availableVideos.length === 0) return;
@ -5848,21 +6107,40 @@ class InteractiveTaskManager {
btn.style.display = 'none';
skipBtn.style.display = 'inline-block';
timerDisplay.style.display = 'block';
// Add timer to sidebar
const sidebar = document.getElementById('campaign-sidebar');
if (sidebar) {
sidebar.style.display = 'block';
const mins = Math.floor(minDuration / 60);
const secs = minDuration % 60;
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="video-timer" style="font-size: 2em; font-weight: bold; color: var(--color-primary);">${mins}:${String(secs).padStart(2, '0')}</div>
</div>
`;
}
// Start countdown timer
let remainingTime = minDuration;
timerDisplay.innerHTML = `⏱️ Keep watching... ${remainingTime}s remaining`;
const timerDisplay = document.getElementById('video-timer');
const timerInterval = setInterval(() => {
timerInterval = setInterval(() => {
remainingTime--;
if (remainingTime > 0) {
timerDisplay.innerHTML = `⏱️ Keep watching... ${remainingTime}s remaining`;
const mins = Math.floor(remainingTime / 60);
const secs = remainingTime % 60;
if (timerDisplay) {
timerDisplay.textContent = `${mins}:${String(secs).padStart(2, '0')}`;
}
} else {
clearInterval(timerInterval);
timerDisplay.innerHTML = '✅ Minimum viewing time complete!';
timerDisplay.style.color = 'var(--color-success, #4caf50)';
if (timerDisplay) {
timerDisplay.textContent = 'COMPLETE!';
timerDisplay.style.color = 'var(--color-success, #4caf50)';
}
statusArea.innerHTML = '<div class="success">✅ You may now continue when ready</div>';
task.completed = true;
@ -5885,6 +6163,21 @@ class InteractiveTaskManager {
}
}
});
// Store cleanup function
task.cleanup = () => {
console.log('🧹 Video start task cleanup called');
if (timerInterval) {
clearInterval(timerInterval);
}
// Hide sidebar
const sidebar = document.getElementById('campaign-sidebar');
if (sidebar) {
sidebar.style.display = 'none';
sidebar.innerHTML = '';
}
};
}
validateVideoStartTask(task) {
@ -6047,7 +6340,7 @@ class InteractiveTaskManager {
/**
* Hypno Caption Combo - Hypno spiral with timed captions
*/
createHypnoCaptionComboTask(container, task) {
createHypnoCaptionComboTask(task, container) {
const params = task.params || {};
const duration = params.duration || 300;
const captionTexts = params.captions || [
@ -6136,7 +6429,7 @@ class InteractiveTaskManager {
/**
* Dynamic Captions - Preference-based captions
*/
createDynamicCaptionsTask(container, task) {
createDynamicCaptionsTask(task, container) {
const params = task.params || {};
const duration = params.duration || 300;
@ -6212,7 +6505,7 @@ class InteractiveTaskManager {
/**
* TTS Hypno Sync - Voice + hypno synchronized
*/
createTTSHypnoSyncTask(container, task) {
createTTSHypnoSyncTask(task, container) {
const params = task.params || {};
const duration = params.duration || 300;
const commands = params.commands || [
@ -6223,12 +6516,17 @@ class InteractiveTaskManager {
'Obey and edge'
];
if (!container) {
console.error('❌ TTS Hypno Sync: No container provided');
return;
}
container.innerHTML = `
<div class="tts-hypno-container">
<div id="tts-hypno-spiral" class="hypno-spiral-background"></div>
<div id="tts-command-display" class="tts-command"></div>
<div class="timer-display">
Time: <span id="tts-time-remaining"></span>
Time: <span id="tts-time-remaining">0:00</span>
</div>
</div>
`;
@ -6240,6 +6538,15 @@ class InteractiveTaskManager {
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;
@ -6290,7 +6597,7 @@ class InteractiveTaskManager {
/**
* Sensory Overload - All features combined
*/
createSensoryOverloadTask(container, task) {
createSensoryOverloadTask(task, container) {
const params = task.params || {};
const duration = params.duration || 600;
const edgeTarget = params.edgeCount || 50;
@ -6382,7 +6689,7 @@ class InteractiveTaskManager {
/**
* Enable Interruptions - Activate interruption system
*/
createEnableInterruptionsTask(container, task) {
createEnableInterruptionsTask(task, container) {
const params = task.params || {};
const types = params.types || ['edge', 'pose', 'mantra', 'stop-stroking'];
const frequency = params.frequency || 'medium';
@ -6421,7 +6728,7 @@ class InteractiveTaskManager {
/**
* Denial Training - Timed stroking/stopping periods
*/
createDenialTrainingTask(container, task) {
createDenialTrainingTask(task, container) {
const params = task.params || {};
const periods = params.denialPeriods || [
{ allowStroking: 300, instruction: 'Stroke for 5 minutes' },
@ -6488,7 +6795,7 @@ class InteractiveTaskManager {
/**
* Stop Stroking - Enforced hands-off period
*/
createStopStrokingTask(container, task) {
createStopStrokingTask(task, container) {
const params = task.params || {};
const duration = params.duration || 120;
@ -6532,7 +6839,7 @@ class InteractiveTaskManager {
/**
* Enable Popups - Activate popup system
*/
createEnablePopupsTask(container, task) {
createEnablePopupsTask(task, container) {
const params = task.params || {};
const frequency = params.frequency || 'medium';
const sources = params.sources || ['tasks', 'consequences'];
@ -6571,7 +6878,7 @@ class InteractiveTaskManager {
/**
* Popup Image - Display specific popup
*/
createPopupImageTask(container, task) {
createPopupImageTask(task, container) {
const params = task.params || {};
const imagePath = params.image || '';
const displayTime = params.duration || 10;