514 lines
17 KiB
JavaScript
514 lines
17 KiB
JavaScript
/**
|
||
* 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');
|