training-academy/src/features/academy/campaignManager.js

514 lines
17 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Campaign Manager
* Handles The Academy's 30-level campaign progression system
*/
class CampaignManager {
constructor() {
this.initializeProgress();
// Map levels to features they unlock
this.featureUnlocks = {
3: ['video'], // Level 3: Video playback
6: ['webcam'], // Level 6: Webcam mirror
7: ['dual-video'], // Level 7: Dual video
8: ['tts'], // Level 8: Text-to-speech
9: ['quad-video'], // Level 9: Quad video
10: ['hypno-spiral'], // Level 10: Hypno spiral
11: ['hypno-captions'], // Level 11: Hypno with captions
12: ['dynamic-captions'], // Level 12: Dynamic captions
13: ['tts-hypno-sync'], // Level 13: TTS + Hypno sync
15: ['interruptions'], // Level 15: Random interruptions
16: ['denial-training'], // Level 16: Denial mechanics
17: ['popup-images'], // Level 17: Popup images
18: ['edge-counter'], // Level 18: Edge counter
19: ['sensory-overload'], // Level 19: All features combined
25: ['advanced-mode'], // Level 25: Advanced training mode
30: ['graduation'] // Level 30: Graduate status
};
}
/**
* Initialize academy progress if it doesn't exist
*/
initializeProgress() {
console.log('🎯 CampaignManager: Initializing progress...');
console.log('🎯 window.gameData exists:', !!window.gameData);
console.log('🎯 window.gameData.academyProgress exists:', !!window.gameData?.academyProgress);
// gameData is available globally via window.gameData
if (!window.gameData.academyProgress) {
console.log('🆕 Creating fresh academy progress');
window.gameData.academyProgress = {
version: 1,
currentLevel: 1,
highestUnlockedLevel: 1,
completedLevels: [],
currentArc: 'Foundation',
failedAttempts: {},
totalSessionTime: 0,
lastPlayedLevel: null,
lastPlayedDate: null,
graduationCompleted: false,
freeplayUnlocked: false,
ascendedModeUnlocked: false,
selectedPath: null,
featuresUnlocked: []
};
this.saveProgress();
} else {
console.log('✅ Existing academy progress loaded:', {
currentLevel: window.gameData.academyProgress.currentLevel,
highestUnlocked: window.gameData.academyProgress.highestUnlockedLevel,
completedLevels: window.gameData.academyProgress.completedLevels,
features: window.gameData.academyProgress.featuresUnlocked
});
// 🔧 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) {
try {
const parsed = JSON.parse(stored);
console.log('🔍 localStorage academyProgress:', parsed.academyProgress);
} catch (e) {
console.error('❌ Failed to parse localStorage data:', e);
}
} else {
console.warn('⚠️ No data in localStorage yet');
}
}
}
/**
* 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)
*/
fixCurrentLevelMismatch() {
const progress = window.gameData.academyProgress;
const completedLevels = progress.completedLevels || [];
if (completedLevels.length === 0) return; // No completed levels, currentLevel should be 1
// Find the highest completed level
const highestCompleted = Math.max(...completedLevels);
// currentLevel should be at least highestCompleted + 1 (next level after highest completed)
const expectedCurrentLevel = Math.min(highestCompleted + 1, 30);
if (progress.currentLevel < expectedCurrentLevel) {
console.warn(`🔧 Fixing currentLevel mismatch: was ${progress.currentLevel}, should be ${expectedCurrentLevel}`);
console.log(`📊 Completed levels: [${completedLevels.join(', ')}]`);
console.log(`🏆 Highest completed: ${highestCompleted}`);
progress.currentLevel = expectedCurrentLevel;
progress.currentArc = this.getArcForLevel(expectedCurrentLevel);
// Also ensure highestUnlockedLevel is correct
if (progress.highestUnlockedLevel < expectedCurrentLevel) {
progress.highestUnlockedLevel = expectedCurrentLevel;
console.log(`🔓 Also updated highestUnlockedLevel to ${expectedCurrentLevel}`);
}
this.saveProgress();
console.log(`✅ Fixed: currentLevel is now ${progress.currentLevel} (${progress.currentArc} Arc)`);
}
}
/**
* Save progress to localStorage
*/
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!');
return;
}
// Save to localStorage directly
const dataToSave = JSON.stringify(window.gameData);
localStorage.setItem('webGame-data', dataToSave);
// Verify the save worked
const verified = localStorage.getItem('webGame-data');
if (!verified) {
console.error('❌ Failed to save to localStorage - data not persisted');
return;
}
// Also update simpleDataManager if available
if (window.simpleDataManager) {
window.simpleDataManager.data = window.gameData;
window.simpleDataManager.saveData();
}
console.log('💾 Campaign progress saved successfully:', {
currentLevel: window.gameData.academyProgress.currentLevel,
highestUnlocked: window.gameData.academyProgress.highestUnlockedLevel,
completed: window.gameData.academyProgress.completedLevels,
features: window.gameData.academyProgress.featuresUnlocked,
savedSize: dataToSave.length + ' bytes'
});
} catch (error) {
console.error('❌ Error saving campaign progress:', error);
}
}
/**
* Get list of unlocked levels
* @returns {Array<number>} Array of unlocked level numbers
*/
getUnlockedLevels() {
const unlocked = [];
const highest = window.gameData.academyProgress.highestUnlockedLevel;
for (let i = 1; i <= highest; i++) {
unlocked.push(i);
}
return unlocked;
}
/**
* Check if a level is unlocked
* @param {number} levelNum - Level number to check
* @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;
}
/**
* Check if a level is completed
* @param {number} levelNum - Level number to check
* @returns {boolean}
*/
isLevelCompleted(levelNum) {
return window.gameData.academyProgress.completedLevels.includes(levelNum);
}
/**
* Start a level
* @param {number} levelNum - Level number to start
* @returns {Object|null} Level config or null if invalid
*/
startLevel(levelNum) {
// Validate level is unlocked
if (!this.isLevelUnlocked(levelNum)) {
console.error(`Level ${levelNum} is locked. Complete previous levels first.`);
return null;
}
// Update current level
window.gameData.academyProgress.currentLevel = levelNum;
window.gameData.academyProgress.lastPlayedLevel = levelNum;
window.gameData.academyProgress.lastPlayedDate = new Date().toISOString();
// Update current arc
window.gameData.academyProgress.currentArc = this.getArcForLevel(levelNum);
this.saveProgress();
console.log(`Started Level ${levelNum} (${gameData.academyProgress.currentArc} Arc)`);
return {
levelNum,
arc: window.gameData.academyProgress.currentArc,
isCheckpoint: this.isCheckpointLevel(levelNum)
};
}
/**
* Complete a level
* @param {number} levelNum - Level number completed
* @param {Object} sessionData - Data from the session
* @returns {Object} Completion results
*/
completeLevel(levelNum, sessionData = {}) {
const progress = window.gameData.academyProgress;
console.log(`🎓 Completing Level ${levelNum}`, {
currentCompleted: progress.completedLevels,
currentHighest: progress.highestUnlockedLevel
});
// Add to completed levels if not already there
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
const unlockedFeatures = [];
if (this.featureUnlocks[levelNum]) {
const newFeatures = this.featureUnlocks[levelNum];
newFeatures.forEach(feature => {
if (!progress.featuresUnlocked.includes(feature)) {
progress.featuresUnlocked.push(feature);
unlockedFeatures.push(feature);
console.log(`🎁 Feature unlocked: ${feature}`);
}
});
}
// Unlock next level and advance currentLevel
const nextLevel = levelNum + 1;
let nextLevelUnlocked = false;
if (nextLevel <= 30 && nextLevel > progress.highestUnlockedLevel) {
progress.highestUnlockedLevel = nextLevel;
nextLevelUnlocked = true;
console.log(`🔓 Level ${nextLevel} unlocked! (highestUnlockedLevel updated to ${nextLevel})`);
} else if (nextLevel <= 30) {
console.log(` Level ${nextLevel} already unlocked (highest: ${progress.highestUnlockedLevel})`);
}
// Advance currentLevel to the next level if available
if (nextLevel <= 30) {
progress.currentLevel = nextLevel;
progress.currentArc = this.getArcForLevel(nextLevel);
console.log(`➡️ Advanced currentLevel to ${nextLevel} (${progress.currentArc} Arc)`);
} else {
console.log(`🎓 All levels completed! (Level ${levelNum} was final)`);
}
// Update session time
if (sessionData.duration) {
progress.totalSessionTime += sessionData.duration;
}
// Check for arc completion
const arcComplete = this.checkArcCompletion(levelNum);
// Reset consecutive failures on success
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!`);
return {
levelCompleted: levelNum,
nextLevelUnlocked,
nextLevel: nextLevelUnlocked ? nextLevel : null,
arcComplete,
completedArc: arcComplete ? this.getArcForLevel(levelNum) : null,
unlockedFeatures: unlockedFeatures
};
}
/**
* Fail a level
* @param {number} levelNum - Level that was failed
* @param {string} reason - Failure reason ('cumming', 'abandoned', 'feature-closed')
*/
failLevel(levelNum, reason) {
const progress = window.gameData.academyProgress;
// Increment failed attempts for this level
if (!progress.failedAttempts[levelNum]) {
progress.failedAttempts[levelNum] = 0;
}
progress.failedAttempts[levelNum]++;
// Track total failures
if (!progress.totalFailedAttempts) {
progress.totalFailedAttempts = 0;
}
progress.totalFailedAttempts++;
// Track failure by reason
if (!progress.failuresByReason) {
progress.failuresByReason = {
cumming: 0,
abandoned: 0,
featureClosed: 0
};
}
if (progress.failuresByReason[reason] !== undefined) {
progress.failuresByReason[reason]++;
}
// Reset consecutive success streak
progress.consecutiveLevelsWithoutFailure = 0;
this.saveProgress();
console.log(`❌ Level ${levelNum} failed (${reason}). Attempts: ${progress.failedAttempts[levelNum]}`);
}
/**
* Get the arc name for a level
* @param {number} levelNum - Level number
* @returns {string} Arc name
*/
getArcForLevel(levelNum) {
if (levelNum >= 1 && levelNum <= 5) return 'Foundation';
if (levelNum >= 6 && levelNum <= 10) return 'Feature Discovery';
if (levelNum >= 11 && levelNum <= 15) return 'Mind & Body';
if (levelNum >= 16 && levelNum <= 20) return 'Advanced Training';
if (levelNum >= 21 && levelNum <= 25) return 'Path Specialization';
if (levelNum >= 26 && levelNum <= 30) return 'Ultimate Mastery';
return 'Unknown';
}
/**
* Get current arc
* @returns {string} Current arc name
*/
getCurrentArc() {
return window.gameData.academyProgress.currentArc;
}
/**
* Check if level is a checkpoint (1, 5, 10, 15, 20, 25)
* @param {number} levelNum - Level number
* @returns {boolean}
*/
isCheckpointLevel(levelNum) {
return [1, 5, 10, 15, 20, 25].includes(levelNum);
}
/**
* Check if an arc is complete
* @param {number} levelNum - Level just completed
* @returns {boolean}
*/
checkArcCompletion(levelNum) {
const arcEndLevels = [5, 10, 15, 20, 25, 30];
return arcEndLevels.includes(levelNum);
}
/**
* Get progress statistics
* @returns {Object} Progress stats
*/
getProgressStats() {
const progress = window.gameData.academyProgress;
return {
currentLevel: progress.currentLevel,
highestUnlockedLevel: progress.highestUnlockedLevel,
completedLevels: progress.completedLevels.length,
totalLevels: 30,
percentComplete: (progress.completedLevels.length / 30) * 100,
currentArc: progress.currentArc,
totalSessionTime: progress.totalSessionTime,
failedAttempts: Object.values(progress.failedAttempts).reduce((a, b) => a + b, 0),
consecutiveSuccesses: progress.consecutiveLevelsWithoutFailure || 0,
featuresUnlocked: progress.featuresUnlocked || []
};
}
/**
* Check if a feature is unlocked
* @param {string} featureName - Feature to check
* @returns {boolean} True if unlocked
*/
isFeatureUnlocked(featureName) {
const progress = window.gameData.academyProgress;
return progress.featuresUnlocked && progress.featuresUnlocked.includes(featureName);
}
/**
* Get all unlocked features
* @returns {Array<string>} List of unlocked features
*/
getUnlockedFeatures() {
const progress = window.gameData.academyProgress;
return progress.featuresUnlocked || [];
}
/**
* Get features that will be unlocked at a specific level
* @param {number} levelNum - Level number
* @returns {Array<string>} Features unlocked at this level
*/
getFeaturesForLevel(levelNum) {
return this.featureUnlocks[levelNum] || [];
}
/**
* Reset progress (for testing or fresh start)
*/
resetProgress() {
window.gameData.academyProgress = {
version: 1,
currentLevel: 1,
highestUnlockedLevel: 1,
completedLevels: [],
currentArc: 'Foundation',
failedAttempts: {},
totalSessionTime: 0,
lastPlayedLevel: null,
lastPlayedDate: null,
graduationCompleted: false,
freeplayUnlocked: false,
ascendedModeUnlocked: false,
selectedPath: null,
featuresUnlocked: [],
totalFailedAttempts: 0,
failuresByReason: {
cumming: 0,
abandoned: 0,
featureClosed: 0
},
consecutiveLevelsWithoutFailure: 0
};
this.saveProgress();
console.log('🔄 Academy progress reset');
}
}
// Create singleton instance and expose globally
window.campaignManager = new CampaignManager();
console.log('🎓 Campaign Manager initialized');