485 lines
17 KiB
JavaScript
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();
|