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

485 lines
17 KiB
JavaScript

/**
* 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<string>} 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<string>} 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<string>} 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();