commiting in case claude breaks it again

This commit is contained in:
dilgenfritz 2025-12-04 22:21:41 -06:00
parent 0da0c9e139
commit 0679df3fd9
9 changed files with 1462 additions and 293 deletions

View File

@ -1311,8 +1311,8 @@
<!-- Main Content -->
<div class="main-content">
<!-- Training Academy Setup Screen -->
<div class="academy-setup" id="academy-setup">
<!-- Training Academy Setup Screen (Hidden - no longer needed) -->
<div class="academy-setup" id="academy-setup" style="display: none;">
<div class="setup-container">
<div class="setup-title">
<h2 style="text-align: center; color: var(--color-primary); font-family: 'Audiowide', sans-serif; font-size: 28px; margin: 20px 0; text-shadow: var(--shadow-glow-primary);">🎓 Training Academy Setup</h2>
@ -1365,8 +1365,8 @@
</div>
</div>
<!-- Level Select Screen (Hidden initially) -->
<div id="levelSelectScreen" class="level-select-screen" style="display: none;">
<!-- Level Select Screen (Now the opening screen) -->
<div id="levelSelectScreen" class="level-select-screen" style="display: block;">
<div class="level-select-header">
<h2>🎯 Campaign Mode - Select Level</h2>
<p class="progress-subtitle">Progress through 30 levels across 6 story arcs</p>
@ -1392,14 +1392,34 @@
<!-- Game Interface (Hidden initially) -->
<div id="gameInterface" style="display: none;">
<!-- Feature Controls Panel -->
<div id="featuresPanel" class="features-panel" style="display: none;">
<div id="featuresPanel" class="features-panel">
<button id="toggleFeaturesBtn" class="toggle-features-btn" onclick="toggleFeaturesPanel()">
⚙️ Features
⚙️ Settings
</button>
<div id="featuresList" class="features-list" style="display: none;">
<h4>Unlocked Features</h4>
<div id="featuresContent">
<!-- Features will be injected here -->
<h4>Audio & Display Settings</h4>
<div id="featuresContent" style="padding: 15px;">
<div style="margin-bottom: 20px;">
<label style="display: block; color: var(--color-primary); font-weight: bold; margin-bottom: 5px;">
🔊 Ambient Audio Volume
</label>
<input type="range" id="ambient-volume-slider" min="0" max="100" value="50"
style="width: 100%; margin-bottom: 5px;">
<div style="text-align: center; color: var(--text-secondary); font-size: 0.9em;">
<span id="ambient-volume-value">50</span>%
</div>
</div>
<div style="margin-bottom: 20px;">
<label style="display: block; color: var(--color-primary); font-weight: bold; margin-bottom: 5px;">
💬 Caption Size
</label>
<input type="range" id="caption-size-slider" min="50" max="200" value="100"
style="width: 100%; margin-bottom: 5px;">
<div style="text-align: center; color: var(--text-secondary); font-size: 0.9em;">
<span id="caption-size-value">100</span>%
</div>
</div>
</div>
</div>
</div>
@ -1488,6 +1508,17 @@
<!-- Note: main.js is Electron-specific and not needed in browser -->
<script>
// 🔍 DEBUG: Monitor ALL localStorage writes to webGame-data
const originalSetItem = localStorage.setItem;
localStorage.setItem = function(key, value) {
if (key === 'webGame-data') {
const parsed = JSON.parse(value);
console.log('🔍 LOCALSTORAGE WRITE to webGame-data - completedLevels:', parsed.academyProgress?.completedLevels);
console.trace('Write stack trace:');
}
return originalSetItem.apply(this, arguments);
};
// Training Academy Global Variables
let trainingPhotoLibrary = [];
let selectedTrainingMode = null;
@ -1499,7 +1530,7 @@
// TTS Variables
let voiceManager = null;
let ttsEnabled = true;
let ttsEnabled = false;
let currentUtterance = null;
let ttsQueue = [];
@ -1830,16 +1861,21 @@
console.error('⚠️ Failed to load captured photos from file system:', error);
}
document.getElementById('photoLibraryStatus').innerHTML =
`<span style="color: var(--color-success);">✅ ${trainingPhotoLibrary.length} photos available</span>`;
// Update status if element exists (setup screen may be hidden)
const statusEl = document.getElementById('photoLibraryStatus');
if (statusEl) {
statusEl.innerHTML =
`<span style="color: var(--color-success);">✅ ${trainingPhotoLibrary.length} photos available</span>`;
}
// Show gallery button if there are photos
const totalPhotoCount = trainingPhotoLibrary.length;
if (totalPhotoCount > 0) {
document.getElementById('view-gallery-btn').style.display = 'inline-block';
const galleryBtn = document.getElementById('view-gallery-btn');
if (totalPhotoCount > 0 && galleryBtn) {
galleryBtn.style.display = 'inline-block';
// Update button text to indicate total photos
document.getElementById('view-gallery-btn').innerHTML =
galleryBtn.innerHTML =
`📸 View Gallery (${totalPhotoCount} photos)`;
console.log('📸 Gallery button updated. Total photos:', totalPhotoCount);
@ -1848,8 +1884,11 @@
} catch (error) {
console.error('❌ Error initializing photo library:', error);
document.getElementById('photoLibraryStatus').innerHTML =
'<span style="color: var(--color-danger);">❌ Error loading photo library</span>';
const statusEl = document.getElementById('photoLibraryStatus');
if (statusEl) {
statusEl.innerHTML =
'<span style="color: var(--color-danger);">❌ Error loading photo library</span>';
}
}
}
@ -1971,7 +2010,7 @@
// Load Academy Settings from localStorage
function loadAcademySettings() {
const defaultSettings = {
enableTTS: true,
enableTTS: false,
ttsVolume: 80,
selectedMode: null
};
@ -2029,11 +2068,11 @@
function showLevelSelectScreen() {
console.log('🎯 Showing level select screen');
// Hide setup screen
const setupScreen = document.getElementById('academy-setup');
if (setupScreen) {
setupScreen.style.display = 'none';
}
// Don't reload from localStorage - just use the in-memory window.gameData which is already up-to-date
console.log('📊 Using in-memory academyProgress:', {
currentLevel: window.gameData.academyProgress.currentLevel,
completedLevels: window.gameData.academyProgress.completedLevels
});
// Show level select screen
const levelSelectScreen = document.getElementById('levelSelectScreen');
@ -2043,15 +2082,6 @@
// Populate level grid
populateLevelGrid();
// Setup back button
const backBtn = document.getElementById('back-to-setup-btn');
if (backBtn) {
backBtn.addEventListener('click', () => {
levelSelectScreen.style.display = 'none';
setupScreen.style.display = 'block';
});
}
}
// Populate Level Grid
@ -2080,6 +2110,17 @@
completed: progress.completedLevels || Object.keys(progress.completed || {})
});
// 🔍 Debug: Check what's in localStorage
const storedData = localStorage.getItem('webGame-data');
if (storedData) {
try {
const parsed = JSON.parse(storedData);
console.log('🔍 localStorage completedLevels:', parsed.academyProgress?.completedLevels);
} catch (e) {
console.error('Failed to parse localStorage:', e);
}
}
// Update progress bar
const completedCount = progress.completedLevels
? progress.completedLevels.length
@ -2173,10 +2214,14 @@
function startLevel(scenario, levelNum) {
console.log(`🎓 Starting Level ${levelNum}: ${scenario.name}`);
// Set training mode for campaign
selectedTrainingMode = 'training-academy';
// Store selected level and track start time
window.selectedAcademyLevel = scenario;
window.selectedLevelNumber = levelNum;
window.levelStartTime = Date.now();
console.log(`📌 SET window.selectedLevelNumber = ${levelNum} at startLevel()`);
// Hide level select screen
const levelSelectScreen = document.getElementById('levelSelectScreen');
@ -2211,6 +2256,57 @@
currentScenarioTask.cleanup();
}
// Clean up all PiP overlays (dual-video, webcam)
console.log('🧹 Cleaning up PiP overlays');
const pipOverlays = document.querySelectorAll('.dual-video-pip-overlay, .pip-webcam-container');
pipOverlays.forEach(overlay => {
const video = overlay.querySelector('video');
if (video) {
if (video.srcObject) {
const tracks = video.srcObject.getTracks();
tracks.forEach(track => track.stop());
}
video.pause();
video.src = '';
}
overlay.remove();
});
// Clean up webcam streams
console.log('🧹 Cleaning up webcam streams');
const allVideos = document.querySelectorAll('video');
allVideos.forEach(video => {
if (video.srcObject) {
const tracks = video.srcObject.getTracks();
tracks.forEach(track => track.stop());
video.srcObject = null;
}
});
// Clean up videoPlayerManager overlay reference
if (window.videoPlayerManager && window.videoPlayerManager.overlayPlayer) {
console.log('🧹 Clearing videoPlayerManager overlay reference');
window.videoPlayerManager.overlayPlayer = null;
}
// Hide and reset sidebar countdown timer
const countdownWrapper = document.getElementById('sidebar-countdown-timer');
if (countdownWrapper) {
countdownWrapper.style.display = 'none';
}
const countdownDisplay = document.getElementById('sidebar-countdown-display');
if (countdownDisplay) {
countdownDisplay.textContent = '00:00';
}
// Clean up ambient audio elements
console.log('🧹 Cleaning up ambient audio');
const ambientAudios = document.querySelectorAll('audio[data-ambient="true"]');
ambientAudios.forEach(audio => {
audio.pause();
audio.remove();
});
// Clear any active training state
currentScenarioTask = null;
currentInteractiveTask = null;
@ -2313,14 +2409,83 @@
const featuresList = document.getElementById('featuresList');
if (featuresList.style.display === 'none') {
featuresList.style.display = 'block';
updateFeaturesPanel();
} else {
featuresList.style.display = 'none';
}
}
// Initialize settings sliders
function initializeSettingsSliders() {
// Ambient volume slider
const ambientVolumeSlider = document.getElementById('ambient-volume-slider');
const ambientVolumeValue = document.getElementById('ambient-volume-value');
if (ambientVolumeSlider) {
// Load saved value
const savedVolume = localStorage.getItem('ambient-audio-volume');
if (savedVolume !== null) {
ambientVolumeSlider.value = savedVolume;
ambientVolumeValue.textContent = savedVolume;
}
// Set global variable
window.ambientAudioVolume = parseInt(ambientVolumeSlider.value) / 100;
ambientVolumeSlider.addEventListener('input', (e) => {
const value = e.target.value;
ambientVolumeValue.textContent = value;
window.ambientAudioVolume = parseInt(value) / 100;
localStorage.setItem('ambient-audio-volume', value);
// Update any currently playing ambient audio
const ambientAudio = document.querySelector('audio[data-ambient="true"]');
if (ambientAudio) {
ambientAudio.volume = window.ambientAudioVolume;
}
});
}
// Caption size slider
const captionSizeSlider = document.getElementById('caption-size-slider');
const captionSizeValue = document.getElementById('caption-size-value');
if (captionSizeSlider) {
// Load saved value
const savedSize = localStorage.getItem('caption-font-size');
if (savedSize !== null) {
captionSizeSlider.value = savedSize;
captionSizeValue.textContent = savedSize;
}
// Set global variable
window.captionFontSizeMultiplier = parseInt(captionSizeSlider.value) / 100;
captionSizeSlider.addEventListener('input', (e) => {
const value = e.target.value;
captionSizeValue.textContent = value;
window.captionFontSizeMultiplier = parseInt(value) / 100;
localStorage.setItem('caption-font-size', value);
// Update any visible captions
const captions = document.querySelectorAll('[id*="caption"]');
captions.forEach(caption => {
if (caption.style.fontSize) {
const baseSize = parseFloat(caption.dataset.baseSize || '3');
caption.style.fontSize = (baseSize * window.captionFontSizeMultiplier) + 'em';
}
});
});
}
}
// Initialize sliders when page loads
document.addEventListener('DOMContentLoaded', initializeSettingsSliders);
// Update the features panel with unlocked features
function updateFeaturesPanel() {
// Disabled - features panel now used for settings sliders
return;
const featuresContent = document.getElementById('featuresContent');
if (!featuresContent || !window.campaignManager) return;
@ -2513,12 +2678,60 @@
// Start Training Session (Updated to use setup screen)
function startTrainingSession() {
if (!selectedTrainingMode) {
alert('Please select a training mode first!');
return;
console.log(`🚀 Starting campaign session: ${selectedTrainingMode}`);
// COMPREHENSIVE CLEANUP BEFORE STARTING NEW LEVEL
console.log('🧹 Pre-level cleanup - ensuring clean slate');
// Clean up any lingering PiP overlays
const pipOverlays = document.querySelectorAll('.dual-video-pip-overlay, .pip-webcam-container');
pipOverlays.forEach(overlay => {
const video = overlay.querySelector('video');
if (video) {
if (video.srcObject) {
const tracks = video.srcObject.getTracks();
tracks.forEach(track => track.stop());
}
video.pause();
video.src = '';
}
overlay.remove();
});
// Clean up any webcam streams
const allVideos = document.querySelectorAll('video');
allVideos.forEach(video => {
if (video.srcObject) {
const tracks = video.srcObject.getTracks();
tracks.forEach(track => track.stop());
video.srcObject = null;
}
});
// Clear videoPlayerManager overlay reference
if (window.videoPlayerManager && window.videoPlayerManager.overlayPlayer) {
window.videoPlayerManager.overlayPlayer = null;
}
console.log(`🚀 Starting campaign session: ${selectedTrainingMode}`);
// Clean up ambient audio
const ambientAudios = document.querySelectorAll('audio[data-ambient="true"]');
ambientAudios.forEach(audio => {
audio.pause();
audio.remove();
});
// Reset sidebar countdown timer visibility
const countdownWrapper = document.getElementById('sidebar-countdown-timer');
if (countdownWrapper) {
countdownWrapper.style.display = 'none';
}
// Show sidebar (game stats)
const sidebar = document.getElementById('campaign-sidebar');
if (sidebar) {
sidebar.style.display = 'block';
console.log('📊 Sidebar stats shown for new level');
}
// Apply settings before starting
applyAcademySettings();
@ -2770,6 +2983,13 @@
</div>
`;
// Ensure sidebar is visible after container setup
const setupSidebar = document.getElementById('campaign-sidebar');
if (setupSidebar) {
setupSidebar.style.display = 'block';
console.log('📊 Sidebar shown after container setup');
}
// Initialize the game properly using the existing game system
setTimeout(() => {
if (window.game) {
@ -3023,6 +3243,13 @@
</div>
`;
// Ensure sidebar stays visible after container update
const displaySidebar = document.getElementById('campaign-sidebar');
if (displaySidebar) {
displaySidebar.style.display = 'block';
console.log('📊 Sidebar enforced after task display');
}
// TTS narration for the task
setTimeout(() => {
let narrationText = `New training task. ${task.text}`;
@ -3096,6 +3323,14 @@
fallbackContainer.appendChild(tempDiv);
console.log('✅ Fallback task display created and added to DOM');
console.log('🎯 Display element:', tempDiv);
// Ensure sidebar stays visible after fallback display
const fallbackDisplaySidebar = document.getElementById('campaign-sidebar');
if (fallbackDisplaySidebar) {
fallbackDisplaySidebar.style.display = 'block';
console.log('📊 Sidebar enforced after fallback display');
}
return;
}
@ -3143,6 +3378,13 @@
window.game.gameState.currentTask = trainingTasks[0]; // CRITICAL: Set gameState.currentTask for scenario progression
window.game.displayCurrentTask();
console.log('✅ Training task loaded successfully');
// Ensure sidebar is visible after task loads
const taskSidebar = document.getElementById('campaign-sidebar');
if (taskSidebar) {
taskSidebar.style.display = 'block';
console.log('📊 Sidebar shown after task load');
}
} else {
throw new Error('No training tasks available to load');
}
@ -3155,6 +3397,13 @@
try {
window.game.startGame();
console.log('✅ Standard game start succeeded');
// Ensure sidebar is visible
const fallbackSidebar = document.getElementById('campaign-sidebar');
if (fallbackSidebar) {
fallbackSidebar.style.display = 'block';
console.log('📊 Sidebar shown after fallback start');
}
} catch (e2) {
console.error('❌ Standard game start also failed:', e2);
alert('Error: Unable to start training session. Please refresh and try again.');
@ -3236,6 +3485,14 @@
const nextTask = trainingTasks[nextIndex];
console.log('📋 Loading next task:', nextTask.id);
// Update selected level number if in training academy mode
if (selectedTrainingMode === 'training-academy' && nextTask.level) {
window.selectedLevelNumber = nextTask.level;
window.levelStartTime = Date.now();
console.log(`🎓 Updated selected level to: ${nextTask.level}`);
console.log(`📌 SET window.selectedLevelNumber = ${nextTask.level} at loadNextTrainingTask()`);
}
// Set as current task in both locations
window.game.gameState.currentTask = nextTask;
window.game.currentTask = nextTask;
@ -3776,6 +4033,12 @@
return;
}
// Ensure sidebar (game stats) is visible for all steps
const stepSidebar = document.getElementById('campaign-sidebar');
if (stepSidebar) {
stepSidebar.style.display = 'block';
}
const scenario = currentScenarioTask.interactiveData;
const step = scenario.steps[stepId];
@ -4079,13 +4342,25 @@
}, 100); // Small delay to ensure DOM is ready
} else if (step.type === 'completion') {
// Save Academy progress if this was a level using the campaignManager
console.log('🏁 COMPLETION STEP TRIGGERED');
console.log('🔍 selectedTrainingMode:', selectedTrainingMode);
console.log('🔍 window.selectedLevelNumber:', window.selectedLevelNumber);
console.log('🔍 window.campaignManager exists:', !!window.campaignManager);
let completionResult = null;
if (selectedTrainingMode === 'training-academy' && window.selectedLevelNumber && window.campaignManager) {
const levelNum = window.selectedLevelNumber;
console.log(`🎓 Calling campaignManager.completeLevel(${levelNum})`);
completionResult = window.campaignManager.completeLevel(levelNum, {
duration: Date.now() - (window.levelStartTime || Date.now())
});
console.log(`✅ Level ${levelNum} completed:`, completionResult);
} else {
console.warn('⚠️ Skipping campaign completion - conditions not met:', {
isTrainingAcademy: selectedTrainingMode === 'training-academy',
hasLevelNumber: !!window.selectedLevelNumber,
hasCampaignManager: !!window.campaignManager
});
}
// Build features unlocked message
@ -4308,6 +4583,13 @@
container.innerHTML = stepHtml;
// Ensure sidebar remains visible after container update
const containerSidebar = document.getElementById('campaign-sidebar');
if (containerSidebar) {
containerSidebar.style.display = 'block';
console.log('📊 Sidebar visibility enforced after container update');
}
// TTS narration for scenario steps
setTimeout(() => {
if (step.type === 'choice') {
@ -5093,12 +5375,20 @@
// Set up training mode selection
setupTrainingModeSelection();
// Initialize setup screen
// Initialize setup screen (still needed for settings, but hidden)
initializeAcademySetupScreen();
// Load TTS setting from localStorage
const savedSettings = loadAcademySettings();
ttsEnabled = savedSettings.enableTTS;
console.log(`🔊 TTS initialized: ${ttsEnabled ? 'enabled' : 'disabled'}`);
// Initialize photo library
await initializePhotoLibrary();
// Show level select screen immediately (skip setup screen)
showLevelSelectScreen();
console.log('✅ Campaign Mode ready');
} catch (error) {
@ -6038,47 +6328,44 @@
devModeEnabled = !devModeEnabled;
if (devModeEnabled) {
// Unlock all levels
if (window.campaignManager && window.gameData.academyProgress) {
window.gameData.academyProgress.highestUnlockedLevel = 30;
window.campaignManager.saveProgress();
console.log('🔓 DEV MODE ENABLED - All 30 levels unlocked!');
// Refresh level grid if it's visible
const levelGrid = document.getElementById('level-grid');
if (levelGrid && levelGrid.offsetParent !== null) {
populateLevelGrid();
}
// Show flash message
if (window.flashMessageManager) {
window.flashMessageManager.showMessage('🔧 DEV MODE: All levels unlocked!', 'success');
}
} else {
console.warn('⚠️ Campaign manager not initialized yet');
console.log('🔓 DEV MODE ENABLED - All 30 levels unlocked!');
// Refresh level grid if it's visible
const levelGrid = document.getElementById('level-grid');
if (levelGrid && levelGrid.offsetParent !== null) {
populateLevelGrid();
}
// Show flash message
if (window.flashMessageManager) {
window.flashMessageManager.showMessage('🔧 DEV MODE: All levels unlocked!', 'success');
}
} else {
// Disable dev mode - reset to actual progress
if (window.campaignManager && window.gameData.academyProgress) {
const completedLevels = window.gameData.academyProgress.completedLevels || [];
const highestCompleted = completedLevels.length > 0 ? Math.max(...completedLevels) : 0;
window.gameData.academyProgress.highestUnlockedLevel = Math.min(highestCompleted + 1, 30);
window.campaignManager.saveProgress();
console.log('🔒 DEV MODE DISABLED - Progress restored to actual completion');
// Refresh level grid if it's visible
const levelGrid = document.getElementById('level-grid');
if (levelGrid && levelGrid.offsetParent !== null) {
populateLevelGrid();
}
// Show flash message
if (window.flashMessageManager) {
window.flashMessageManager.showMessage('🔒 Dev mode disabled', 'info');
}
console.log('🔒 DEV MODE DISABLED - Normal progression restored');
// Refresh level grid if it's visible
const levelGrid = document.getElementById('level-grid');
if (levelGrid && levelGrid.offsetParent !== null) {
populateLevelGrid();
}
// Show flash message
if (window.flashMessageManager) {
window.flashMessageManager.showMessage('🔒 Dev mode disabled', 'info');
}
}
// Toggle visibility of all skip-task-btn buttons
const skipButtons = document.querySelectorAll('.skip-task-btn');
skipButtons.forEach(btn => {
btn.style.display = devModeEnabled ? 'inline-block' : 'none';
});
return devModeEnabled;
};
// Getter function for dev mode state
window.isDevMode = function() {
return devModeEnabled;
};

View File

@ -0,0 +1,268 @@
# Rhythm Timeline Implementation Reference
## Overview
A Canvas-based scrolling beat timeline for the rhythm-training interactive task in Level 16. Displays dots scrolling horizontally across a line that pulse when crossing a center "HIT" marker.
## Visual Design
### Canvas Setup
- **Element**: `<canvas id="beat-timeline" width="600" height="120">`
- **Styling**: 2px solid border, 10px border-radius, purple gradient background
- **Context**: 2D rendering context
### Visual Elements
1. **Horizontal Line**: Runs across middle (y = height/2), purple semi-transparent
2. **Center HIT Marker**: Vertical white line at center (x = width/2), ±40px tall, with "HIT" text above
3. **Beat Dots**: Purple circles (6px radius) scrolling from right to left
4. **Pulse Effect**: When dot crosses center (within 5px), it grows 1.8x and glows bright purple
## Tempo Sequence
### 5-Minute Cycle (300 seconds)
The rhythm follows a hardcoded 7-segment tempo progression:
```javascript
const tempoSegments = [
{ tempo: 60, duration: 42.86 }, // seconds per segment
{ tempo: 120, duration: 42.86 },
{ tempo: 150, duration: 42.86 },
{ tempo: 60, duration: 42.86 },
{ tempo: 15, duration: 42.86 },
{ tempo: 200, duration: 42.86 },
{ tempo: 240, duration: 42.86 }
];
```
Each segment is ~42.86 seconds (300s ÷ 7 segments).
## Pre-Generation System
### Why Pre-Generate?
To avoid dynamic calculation glitches and maintain smooth constant-speed scrolling regardless of tempo changes.
### Beat Position Calculation
```javascript
const beatPositions = [];
const scrollSpeed = 50; // pixels per second (constant)
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; // Total pixels for entire 5-min cycle
```
### Key Principle
- **Scroll speed is constant** (50 px/s)
- **Beat spacing varies** based on tempo
- Higher tempo = dots closer together (more beats in same space)
- Lower tempo = dots farther apart (fewer beats in same space)
Example:
- 60 BPM: `pixelsPerBeat = 50 * (60/60) = 50px`
- 120 BPM: `pixelsPerBeat = 50 * (60/120) = 25px` (twice as many dots)
- 240 BPM: `pixelsPerBeat = 50 * (60/240) = 12.5px` (four times as many)
## Animation System
### Core Variables
```javascript
let scrollOffset = 0; // Accumulated scroll distance
let animationStartTime = null; // When animation started
let nextBeatTime = null; // Timestamp for next audio click
let lastFrameTime = null; // Previous frame timestamp for delta
```
### Animation Loop (60fps via requestAnimationFrame)
```javascript
const animateTimeline = () => {
const now = Date.now();
// Delta-time based scrolling (smooth regardless of framerate)
if (lastFrameTime) {
const deltaTime = (now - lastFrameTime) / 1000; // seconds
scrollOffset += scrollSpeed * deltaTime;
}
lastFrameTime = now;
// Trigger beat audio when time reached
if (now >= nextBeatTime) {
playBeat();
nextBeatTime = now + beatDuration;
}
drawTimeline();
animationFrameId = requestAnimationFrame(animateTimeline);
};
```
### Drawing Loop
```javascript
const drawTimeline = () => {
// Use modulo for seamless looping
const currentOffset = scrollOffset % totalDistance;
for (let i = 0; i < beatPositions.length; i++) {
const beat = beatPositions[i];
let x = centerX + beat.position - currentOffset;
// Handle wrapping when dot scrolls off left edge
if (x < -100) x += totalDistance;
if (x > width + 100) continue;
// Calculate pulse effect
const distanceFromCenter = Math.abs(x - centerX);
const isPulsing = distanceFromCenter < 5;
const dotRadius = 6;
const pulseRadius = isPulsing ? dotRadius * 1.8 : dotRadius;
// Draw dot with shadow when pulsing
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;
}
};
```
## Audio System
### Single Shared AudioContext
**Critical**: Only create ONE AudioContext instance to avoid browser errors.
```javascript
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
const playBeat = () => {
beatCount++;
// Reuse shared context
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
oscillator.frequency.value = 800; // Hz
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);
};
```
## Tempo Management
### Dynamic Tempo Updates
Tempo changes happen automatically based on elapsed time:
```javascript
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)) {
// Cycle complete, restart
segmentStartTime = now;
currentSegmentIndex = 0;
updateTempo(tempoSequence[0]);
}
```
### Update Tempo Function
```javascript
const updateTempo = (newTempo) => {
currentTempo = Math.max(15, Math.min(240, newTempo));
beatDuration = (60 / currentTempo) * 1000; // ms per beat
if (tempoDisplay) tempoDisplay.textContent = currentTempo;
};
```
**Note**: Changing tempo does NOT affect scroll speed or existing beat positions. The pre-generated pattern already contains all tempo changes baked in.
## Start/Stop Control
### Start Metronome
```javascript
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();
};
```
### Pause/Resume
```javascript
stopBtn.addEventListener('click', () => {
if (animationFrameId) {
// Pause
cancelAnimationFrame(animationFrameId);
animationFrameId = null;
animationStartTime = null;
lastFrameTime = null;
} else {
// Resume
startMetronome();
}
});
```
## Common Issues & Solutions
### Issue: Timeline Glitching/Jumping Backwards
**Cause**: Resetting scrollOffset during animation or using tempo-based scroll speed
**Solution**: Use constant scroll speed with delta time, never reset scrollOffset mid-animation
### Issue: Multiple AudioContext Errors
**Cause**: Creating new AudioContext on every beat
**Solution**: Create ONE shared AudioContext at initialization, reuse for all beats
### Issue: Dots Speed Up When Tempo Changes
**Cause**: Calculating spacing dynamically instead of using pre-generated positions
**Solution**: Pre-calculate all beat positions at init time with correct spacing per tempo segment
### Issue: Visual Stutter on Tempo Transitions
**Cause**: Not pre-generating, recalculating positions when tempo changes
**Solution**: Entire 5-minute pattern is pre-calculated, tempo changes are already in the data
## File Location
Implementation: `src/features/tasks/interactiveTaskManager.js`
- Lines ~7265-7295: Variable declarations and beat pre-generation
- Lines ~7300-7375: drawTimeline() function
- Lines ~7377-7410: animateTimeline() function
- Lines ~7412-7440: playBeat() and startMetronome() functions
## Integration with Level 16
Used in `rhythm-training` interactive task type:
- Duration: 5 minutes (300s)
- Repeats cycle continuously throughout task
- Task completes after specified duration regardless of tempo cycle position

View File

@ -6591,6 +6591,10 @@ class DataManager {
// Auto-save functionality
startAutoSave() {
this.autoSaveInterval = setInterval(() => {
// Sync with window.gameData before saving to prevent overwriting campaign progress
if (window.gameData) {
this.data = window.gameData;
}
this.saveData();
}, 30000); // Save every 30 seconds
}

View File

@ -1273,6 +1273,8 @@ const trainingGameData = {
useTags: true,
useCaptions: true,
prioritizePreferences: true,
ambientAudio: 'audio/ambient/moaning-1.mp3',
ambientVolume: 0.6,
showTimer: true
},
nextStep: 'phase4_final_video'
@ -1459,7 +1461,9 @@ const trainingGameData = {
tempo: 60,
duration: 900,
allowTempoControl: true,
showMetronome: true
showMetronome: true,
backgroundAudio: 'audio/ambient/moaning-1.mp3',
audioVolume: 0.5
},
nextStep: 'webcam_setup'
},
@ -1498,8 +1502,9 @@ const trainingGameData = {
duration: 1500,
interval: 8,
captions: true,
backgroundAudio: 'sensual-1.mp3',
audioVolume: 0.3
showTimer: true,
ambientAudio: 'audio/ambient/moaning-1.mp3',
ambientVolume: 0.5
},
nextStep: 'final_quad'
},
@ -1512,7 +1517,8 @@ const trainingGameData = {
duration: 1800,
edgeCount: 30,
webcamActive: true,
captions: true
captions: true,
showTimer: true
},
nextStep: 'completion'
},
@ -1552,7 +1558,8 @@ const trainingGameData = {
interval: 6,
captions: true,
ambientAudio: 'moaning-1.mp3',
ambientVolume: 0.5
ambientVolume: 0.5,
showTimer: true
},
nextStep: 'tts_commands'
},
@ -1567,7 +1574,8 @@ const trainingGameData = {
ttsEnabled: true,
ttsFrequency: 'medium',
captions: true,
webcamActive: true
webcamActive: true,
showTimer: true
},
nextStep: 'rhythm_audio'
},
@ -1581,7 +1589,7 @@ const trainingGameData = {
duration: 1500,
allowTempoControl: true,
showMetronome: true,
backgroundAudio: 'ambient-1.mp3',
backgroundAudio: 'audio/ambient/moaning-1.mp3',
audioVolume: 0.4
},
nextStep: 'dual_slideshow_audio'
@ -1596,8 +1604,9 @@ const trainingGameData = {
leftInterval: 5,
rightInterval: 7,
captions: true,
ambientAudio: 'moaning-2.mp3',
ambientVolume: 0.6
ambientAudio: 'audio/ambient/moaning-1.mp3',
ambientVolume: 0.6,
showTimer: true
},
nextStep: 'final_quad_tts'
},
@ -1611,7 +1620,8 @@ const trainingGameData = {
edgeCount: 30,
ttsEnabled: true,
captions: true,
webcamActive: true
webcamActive: true,
showTimer: true
},
nextStep: 'completion'
},
@ -1746,7 +1756,7 @@ const trainingGameData = {
allowTempoControl: true,
showMetronome: true,
webcamActive: true,
backgroundAudio: 'ambient-1.mp3',
backgroundAudio: 'audio/ambient/moaning-1.mp3',
audioVolume: 0.3
},
nextStep: 'phase2_dual_slideshow'
@ -1761,7 +1771,7 @@ const trainingGameData = {
leftInterval: 6,
rightInterval: 8,
captions: true,
ambientAudio: 'moaning-2.mp3',
ambientAudio: 'audio/ambient/moaning-1.mp3',
ambientVolume: 0.5
},
nextStep: 'phase3_dual_video_full'

View File

@ -67,6 +67,9 @@ class CampaignManager {
// 🔧 Migration: Fix currentLevel if it's behind the completion progress
this.fixCurrentLevelMismatch();
// 🔧 Migration: Fix highestUnlockedLevel if it was set too high by old dev mode
this.fixHighestUnlockedLevel();
// Log the actual localStorage contents for debugging
const stored = localStorage.getItem('webGame-data');
if (stored) {
@ -82,6 +85,33 @@ class CampaignManager {
}
}
/**
* Fix highestUnlockedLevel if it was set incorrectly by old dev mode
*/
fixHighestUnlockedLevel() {
const progress = window.gameData.academyProgress;
const completedLevels = progress.completedLevels || [];
// Calculate what the highest unlocked level SHOULD be based on completions
let expectedHighest = 1; // Always at least level 1
if (completedLevels.length > 0) {
const highestCompleted = Math.max(...completedLevels);
expectedHighest = Math.min(highestCompleted + 1, 30); // Next level after highest completed
}
// If highestUnlockedLevel is suspiciously high (like 30 when you've only completed 7 levels),
// it was probably set by old dev mode - reset it
if (progress.highestUnlockedLevel > expectedHighest) {
console.warn(`🔧 Fixing highestUnlockedLevel: was ${progress.highestUnlockedLevel}, should be ${expectedHighest}`);
console.log(`📊 Completed levels: [${completedLevels.join(', ')}]`);
console.log(`🏆 Highest completed: ${completedLevels.length > 0 ? Math.max(...completedLevels) : 'none'}`);
progress.highestUnlockedLevel = expectedHighest;
this.saveProgress();
console.log(`✅ Fixed: highestUnlockedLevel is now ${expectedHighest}`);
}
}
/**
* Fix currentLevel if it's behind completed levels (migration helper)
*/
@ -121,6 +151,9 @@ class CampaignManager {
*/
saveProgress() {
try {
console.log(`💾 saveProgress() CALLED - completedLevels:`, window.gameData.academyProgress?.completedLevels);
console.trace('Stack trace:');
// Ensure academyProgress exists
if (!window.gameData.academyProgress) {
console.error('❌ academyProgress is missing from gameData!');
@ -175,6 +208,12 @@ class CampaignManager {
* @returns {boolean}
*/
isLevelUnlocked(levelNum) {
// If dev mode is active, all levels are unlocked
if (window.isDevMode && window.isDevMode()) {
return true;
}
// Otherwise, check against the highest unlocked level
return levelNum <= window.gameData.academyProgress.highestUnlockedLevel;
}
@ -236,6 +275,7 @@ class CampaignManager {
if (!progress.completedLevels.includes(levelNum)) {
progress.completedLevels.push(levelNum);
console.log(`✅ Added ${levelNum} to completed levels`);
console.log(`📋 completedLevels array is now:`, progress.completedLevels);
}
// Check for feature unlocks
@ -283,7 +323,15 @@ class CampaignManager {
progress.consecutiveLevelsWithoutFailure =
(progress.consecutiveLevelsWithoutFailure || 0) + 1;
console.log(`💾 BEFORE saveProgress() - completedLevels:`, progress.completedLevels);
this.saveProgress();
// Verify save immediately after
const verifyData = localStorage.getItem('webGame-data');
if (verifyData) {
const parsed = JSON.parse(verifyData);
console.log(`✅ VERIFIED localStorage after save - completedLevels:`, parsed.academyProgress.completedLevels);
}
console.log(`✅ Level ${levelNum} completed!`);

View File

@ -769,6 +769,8 @@ class LibraryManager {
*/
saveLibrary() {
try {
console.log('💾 libraryManager.saveLibrary() - completedLevels:', window.gameData.academyProgress?.completedLevels);
console.trace('Stack trace:');
localStorage.setItem('webGame-data', JSON.stringify(window.gameData));
} catch (error) {
console.error('Failed to save library:', error);

View File

@ -466,6 +466,8 @@ class PreferenceManager {
*/
savePreferences() {
try {
console.log('💾 preferenceManager.savePreferences() - completedLevels:', window.gameData.academyProgress?.completedLevels);
console.trace('Stack trace:');
localStorage.setItem('webGame-data', JSON.stringify(window.gameData));
return true;
} catch (error) {

File diff suppressed because it is too large Load Diff

View File

@ -378,6 +378,23 @@ class VideoPlayerManager {
return enabledVideos[Math.floor(Math.random() * enabledVideos.length)];
}
/**
* Get all enabled videos from a category (filters out disabled videos)
*/
getEnabledVideos(category) {
const videos = this.videoLibrary[category] || [];
if (videos.length === 0) return [];
// Filter out disabled videos
const disabledVideos = JSON.parse(localStorage.getItem('disabledVideos') || '[]');
const enabledVideos = videos.filter(video => {
const videoPath = typeof video === 'string' ? video : video.path;
return !disabledVideos.includes(videoPath);
});
return enabledVideos;
}
/**
* Get full path for video
*/