/** * Preference Management System for The Academy * Handles user preferences across 8 categories, checkpoint modals, and preference-based filtering */ class PreferenceManager { constructor() { this.initializePreferences(); } /** * Initialize preferences structure in gameData if it doesn't exist */ initializePreferences() { if (!window.gameData.academyPreferences) { window.gameData.academyPreferences = { version: 1, lastUpdated: new Date().toISOString(), checkpointHistory: [], // Track when preferences were updated // Category 1: Content Themes (multi-select) contentThemes: { dominance: false, submission: false, humiliation: false, worship: false, edging: false, denial: false, cei: false, sissy: false, bbc: false, feet: false, femdom: false, maledom: false, lesbian: false, gay: false, trans: false, hentai: false, pov: false, joi: false, gooning: false, mindbreak: false }, // Category 2: Visual Preferences (multi-select) visualPreferences: { solo: false, couples: false, group: false, amateur: false, professional: false, animated: false, realPorn: false, closeups: false, fullBody: false, pov: false, compilation: false, pmv: false, slowmo: false, artistic: false, raw: false }, // Category 3: Intensity Levels (slider 1-5) intensity: { visualIntensity: 3, // How explicit/hardcore paceIntensity: 3, // How fast/aggressive mentalIntensity: 3, // How mind-breaking audioIntensity: 3, // How loud/overwhelming taskDifficulty: 3 // How challenging }, // Category 4: Caption/Text Tone (multi-select) captionTone: { encouraging: false, mocking: false, commanding: false, seductive: false, degrading: false, playful: false, serious: false, casual: false, formal: false, extreme: false }, // Category 5: Audio Preferences (multi-select) audioPreferences: { femaleVoice: false, maleVoice: false, moaning: false, talking: false, music: false, ambience: false, asmr: false, binaural: false, silence: false, soundEffects: false }, // Category 6: Session Duration (single-select) sessionDuration: { preferred: 'medium', // 'short' (5-15m), 'medium' (15-30m), 'long' (30-60m), 'marathon' (60m+) allowFlexibility: true // Can go shorter/longer based on performance }, // Category 7: Feature Preferences (multi-select - unlocks as features discovered) featurePreferences: { webcam: null, // null = not yet discovered, true/false after discovery tts: null, interactiveTasks: null, edgingTimer: null, denialLocks: null, punishments: null, rewards: null, progressTracking: null, mediaLibrary: null, customPlaylists: null }, // Category 8: Boundaries (multi-select for hard limits) boundaries: { hardLimits: [], // User-specified content to NEVER show softLimits: [], // User-specified content to show sparingly triggerWarnings: [], // User wants warnings before certain content requireConfirmation: [] // User wants to confirm before certain tasks } }; this.savePreferences(); } } /** * Get all current preferences * @returns {Object} Full preferences object */ getPreferences() { return window.gameData.academyPreferences; } /** * Get preferences for a specific category * @param {string} category - Category name (contentThemes, visualPreferences, etc.) * @returns {Object} Category preferences */ getCategoryPreferences(category) { const prefs = this.getPreferences(); return prefs[category] || null; } /** * Update preferences for a specific category * @param {string} category - Category name * @param {Object} updates - Object with preference updates * @param {number} checkpointLevel - Level number where update occurred (optional) * @returns {boolean} Success status */ updateCategoryPreferences(category, updates, checkpointLevel = null) { const prefs = this.getPreferences(); if (!prefs[category]) { console.error(`Invalid category: ${category}`); return false; } // Update the category Object.assign(prefs[category], updates); // Update metadata prefs.lastUpdated = new Date().toISOString(); // Track checkpoint history if (checkpointLevel !== null) { prefs.checkpointHistory.push({ level: checkpointLevel, category: category, timestamp: new Date().toISOString(), changes: updates }); } this.savePreferences(); return true; } /** * Bulk update multiple categories at once (used at checkpoints) * @param {Object} categoryUpdates - Object with category names as keys, updates as values * @param {number} checkpointLevel - Level number where update occurred * @returns {boolean} Success status */ updateMultipleCategories(categoryUpdates, checkpointLevel) { const prefs = this.getPreferences(); for (const [category, updates] of Object.entries(categoryUpdates)) { if (!prefs[category]) { console.error(`Invalid category: ${category}`); continue; } Object.assign(prefs[category], updates); } // Update metadata prefs.lastUpdated = new Date().toISOString(); // Track checkpoint history prefs.checkpointHistory.push({ level: checkpointLevel, categories: Object.keys(categoryUpdates), timestamp: new Date().toISOString(), changes: categoryUpdates }); return this.savePreferences(); } /** * Get content filter based on current preferences * Used to filter media library for level content selection * @returns {Object} Filter configuration */ getContentFilter() { const prefs = this.getPreferences(); return { // Theme filters themes: this.getActivePreferences(prefs.contentThemes), // Visual filters visuals: this.getActivePreferences(prefs.visualPreferences), // Audio filters audio: this.getActivePreferences(prefs.audioPreferences), // Tone filters tones: this.getActivePreferences(prefs.captionTone), // Intensity filters intensity: { visual: prefs.intensity.visualIntensity, pace: prefs.intensity.paceIntensity, mental: prefs.intensity.mentalIntensity, audio: prefs.intensity.audioIntensity, difficulty: prefs.intensity.taskDifficulty }, // Duration preference duration: prefs.sessionDuration.preferred, allowFlexibility: prefs.sessionDuration.allowFlexibility, // Boundaries (exclude these) exclude: { hardLimits: prefs.boundaries.hardLimits, softLimits: prefs.boundaries.softLimits }, // Warnings needed warnings: prefs.boundaries.triggerWarnings, // Confirmation required confirmations: prefs.boundaries.requireConfirmation }; } /** * Get list of active (true) preferences from a category object * @param {Object} categoryPrefs - Category preferences object * @returns {Array} Array of active preference names */ getActivePreferences(categoryPrefs) { return Object.entries(categoryPrefs) .filter(([key, value]) => value === true) .map(([key]) => key); } /** * Check if a checkpoint level should show preference modal * @param {number} levelNum - Level number * @returns {boolean} True if checkpoint level */ isCheckpointLevel(levelNum) { return [1, 5, 10, 15, 20, 25].includes(levelNum); } /** * Get checkpoint modal configuration for a specific level * @param {number} levelNum - Level number * @returns {Object} Modal configuration with categories to show */ getCheckpointModalConfig(levelNum) { const configs = { 1: { title: "Welcome to The Academy", description: "Let's set up your initial preferences. You can always change these later at checkpoints (L5, 10, 15, 20, 25).", categories: ['contentThemes', 'visualPreferences', 'sessionDuration'], isInitial: true }, 5: { title: "Checkpoint: Refine Your Experience", description: "You've completed the Foundation arc. Let's refine your preferences based on what you've experienced.", categories: ['contentThemes', 'visualPreferences', 'intensity', 'captionTone'], isInitial: false }, 10: { title: "Checkpoint: Feature Discovery Complete", description: "Now that you've explored all features, let's optimize your sessions.", categories: ['featurePreferences', 'audioPreferences', 'intensity', 'sessionDuration'], isInitial: false }, 15: { title: "Checkpoint: Deepening Your Training", description: "You're halfway through. Time to deepen your preferences.", categories: ['contentThemes', 'captionTone', 'intensity', 'boundaries'], isInitial: false }, 20: { title: "Checkpoint: Advanced Personalization", description: "You're in the Mastery arc. Let's perfect your experience.", categories: ['visualPreferences', 'audioPreferences', 'featurePreferences', 'intensity'], isInitial: false }, 25: { title: "Checkpoint: Final Refinement", description: "Almost at graduation. Final chance to perfect your preferences.", categories: ['contentThemes', 'visualPreferences', 'intensity', 'captionTone', 'boundaries'], isInitial: false } }; return configs[levelNum] || null; } /** * Get checkpoint history * @returns {Array} Array of checkpoint update records */ getCheckpointHistory() { return this.getPreferences().checkpointHistory || []; } /** * Mark a feature as discovered (unlocks it in featurePreferences) * @param {string} featureName - Feature name (webcam, tts, etc.) * @returns {boolean} Success status */ discoverFeature(featureName) { const prefs = this.getPreferences(); if (!prefs.featurePreferences.hasOwnProperty(featureName)) { console.error(`Invalid feature: ${featureName}`); return false; } // Set to false initially (discovered but not enabled) // User will choose at next checkpoint if (prefs.featurePreferences[featureName] === null) { prefs.featurePreferences[featureName] = false; prefs.lastUpdated = new Date().toISOString(); this.savePreferences(); } return true; } /** * Get list of discovered features * @returns {Array} Array of discovered feature names */ getDiscoveredFeatures() { const prefs = this.getPreferences(); return Object.entries(prefs.featurePreferences) .filter(([key, value]) => value !== null) .map(([key]) => key); } /** * Get list of enabled features * @returns {Array} Array of enabled feature names */ getEnabledFeatures() { const prefs = this.getPreferences(); return Object.entries(prefs.featurePreferences) .filter(([key, value]) => value === true) .map(([key]) => key); } /** * Add a hard limit (content to never show) * @param {string} limitTag - Tag to add to hard limits * @returns {boolean} Success status */ addHardLimit(limitTag) { const prefs = this.getPreferences(); if (!prefs.boundaries.hardLimits.includes(limitTag)) { prefs.boundaries.hardLimits.push(limitTag); prefs.lastUpdated = new Date().toISOString(); this.savePreferences(); } return true; } /** * Remove a hard limit * @param {string} limitTag - Tag to remove from hard limits * @returns {boolean} Success status */ removeHardLimit(limitTag) { const prefs = this.getPreferences(); const index = prefs.boundaries.hardLimits.indexOf(limitTag); if (index > -1) { prefs.boundaries.hardLimits.splice(index, 1); prefs.lastUpdated = new Date().toISOString(); this.savePreferences(); } return true; } /** * Get statistics about preferences * @returns {Object} Stats object */ getPreferenceStats() { const prefs = this.getPreferences(); return { totalCheckpoints: prefs.checkpointHistory.length, lastUpdated: prefs.lastUpdated, activeThemes: this.getActivePreferences(prefs.contentThemes).length, activeVisuals: this.getActivePreferences(prefs.visualPreferences).length, activeAudio: this.getActivePreferences(prefs.audioPreferences).length, activeTones: this.getActivePreferences(prefs.captionTone).length, discoveredFeatures: this.getDiscoveredFeatures().length, enabledFeatures: this.getEnabledFeatures().length, averageIntensity: this.getAverageIntensity(), hardLimitsSet: prefs.boundaries.hardLimits.length, softLimitsSet: prefs.boundaries.softLimits.length, preferredDuration: prefs.sessionDuration.preferred }; } /** * Calculate average intensity across all intensity categories * @returns {number} Average intensity (1-5) */ getAverageIntensity() { const prefs = this.getPreferences(); const intensities = Object.values(prefs.intensity); const sum = intensities.reduce((acc, val) => acc + val, 0); return (sum / intensities.length).toFixed(1); } /** * Reset all preferences to defaults * @returns {boolean} Success status */ resetPreferences() { delete window.gameData.academyPreferences; this.initializePreferences(); return true; } /** * Save preferences to localStorage */ 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) { console.error('Failed to save preferences:', error); if (error.name === 'QuotaExceededError') { alert('⚠️ Storage full! Preferences could not be saved.\n\nThe app has cleared old data. Please try again.'); } return false; } } } // Create global instance window.preferenceManager = new PreferenceManager();