commiting in case claude breaks it again
This commit is contained in:
parent
0da0c9e139
commit
0679df3fd9
435
campaign.html
435
campaign.html
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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!`);
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
Loading…
Reference in New Issue