// Validate required dependencies before initializing game function validateDependencies() { const required = ['DataManager', 'InteractiveTaskManager']; const optional = ['WebcamManager']; for (const dep of required) { if (typeof window[dep] === 'undefined') { console.error(`❌ Required dependency ${dep} not found`); return false; } } for (const dep of optional) { if (typeof window[dep] === 'undefined') { console.warn(`⚠️ Optional dependency ${dep} not found, related features will be disabled`); } } return true; } // Game state management class TaskChallengeGame { constructor() { // Show loading overlay immediately this.showLoadingOverlay(); this.loadingProgress = 0; this.isInitialized = false; // Initialize data management system first this.dataManager = new DataManager(); this.updateLoadingProgress(10, 'Data manager initialized...'); // Initialize interactive task manager this.interactiveTaskManager = new InteractiveTaskManager(this); this.updateLoadingProgress(20, 'Interactive task manager loaded...'); // Initialize webcam manager for photography tasks (with safety check) if (typeof WebcamManager !== 'undefined') { this.webcamManager = new WebcamManager(this); this.updateLoadingProgress(30, 'Webcam manager initialized...'); } else { console.error('⚠️ WebcamManager not available, webcam features will be disabled'); this.webcamManager = null; this.updateLoadingProgress(30, 'Webcam manager skipped...'); } // Initialize desktop features early this.initDesktopFeatures(); this.updateLoadingProgress(40, 'Desktop features initialized...'); this.gameState = { isRunning: false, isPaused: false, currentTask: null, isConsequenceTask: false, startTime: null, pausedTime: 0, totalPausedTime: 0, completedCount: 0, skippedCount: 0, consequenceCount: 0, score: 0, lastSkippedTask: null, // Track the last skipped task for mercy cost calculation usedMainTasks: [], usedConsequenceTasks: [], usedTaskImages: [], // Track which task images have been shown usedConsequenceImages: [], // Track which consequence images have been shown gameMode: 'complete-all', // Game mode: 'complete-all', 'timed', 'xp-target' timeLimit: 300, // Time limit in seconds (5 minutes default) xpTarget: 300, // XP target (default 300 XP) xp: 0, // Current session XP earned taskCompletionXp: 0, // XP earned from completing tasks (tracked separately) sessionStartTime: null, // When the current session started currentStreak: 0, // Track consecutive completed regular tasks lastStreakMilestone: 0, // Track the last streak milestone reached focusInterruptionChance: 0, // Percentage chance (0-50) for focus-hold interruptions in scenarios // Scenario-specific XP tracking (separate from main game) scenarioXp: { timeBased: 0, // 1 XP per 2 minutes focusBonuses: 0, // 3 XP per 30 seconds of focus webcamBonuses: 0, // 3 XP per 30 seconds of webcam mirror photoRewards: 0, // 1 XP per photo taken stepCompletion: 0, // 5 XP per scenario step (existing) total: 0 // Sum of all scenario XP }, scenarioTracking: { startTime: null, // Scenario session start time lastTimeXpAwarded: null, // Last time-based XP award lastFocusXpAwarded: null,// Last focus bonus award lastWebcamXpAwarded: null,// Last webcam bonus award isInFocusActivity: false, // Currently in a focus-hold task isInWebcamActivity: false,// Currently in webcam mirror task totalPhotosThisSession: 0 // Photos taken in this scenario session } }; this.timerInterval = null; this.imageDiscoveryComplete = false; this.imageManagementListenersAttached = false; this.audioDiscoveryComplete = false; this.audioManagementListenersAttached = false; this.musicManager = new MusicManager(this.dataManager); this.updateLoadingProgress(50, 'Music manager loaded...'); // Initialize Flash Message System this.flashMessageManager = new FlashMessageManager(this.dataManager); this.updateLoadingProgress(55, 'Flash message system loaded...'); // Initialize Popup Image System (Punishment for skips) this.popupImageManager = new PopupImageManager(this.dataManager); window.popupImageManager = this.popupImageManager; // Make available globally for HTML onclick handlers this.updateLoadingProgress(60, 'Popup system loaded...'); // Initialize AI Task Generation System this.aiTaskManager = new AITaskManager(this.dataManager); this.updateLoadingProgress(65, 'AI task manager loaded...'); // Initialize Audio Management System this.audioManager = new AudioManager(this.dataManager, this); this.updateLoadingProgress(70, 'Audio manager loaded...'); // Initialize Game Mode Manager this.gameModeManager = new GameModeManager(); window.gameModeManager = this.gameModeManager; this.updateLoadingProgress(75, 'Game mode manager loaded...'); // Initialize Video Player Manager this.initializeVideoManager(); this.updateLoadingProgress(78, 'Video manager initialized...'); // Initialize webcam for photography tasks this.initializeWebcam(); this.updateLoadingProgress(80, 'Webcam initialized...'); this.initializeEventListeners(); this.setupKeyboardShortcuts(); this.setupWindowResizeHandling(); this.initializeCustomTasks(); // Simple override for desktop mode - skip complex discovery if (typeof DesktopFileManager !== 'undefined' && window.electronAPI) { console.log('🖥️ Desktop mode detected - using simplified image discovery'); // Give desktop file manager time to scan, then force completion setTimeout(() => { const customImages = this.dataManager.get('customImages') || { task: [], consequence: [] }; let taskImages = []; let consequenceImages = []; if (Array.isArray(customImages)) { taskImages = customImages; } else { taskImages = customImages.task || []; consequenceImages = customImages.consequence || []; } gameData.discoveredTaskImages = taskImages.map(img => typeof img === 'string' ? img : img.name); gameData.discoveredConsequenceImages = consequenceImages.map(img => typeof img === 'string' ? img : img.name); console.log(`📸 Desktop mode - Task images: ${gameData.discoveredTaskImages.length}, Consequence images: ${gameData.discoveredConsequenceImages.length}`); this.imageDiscoveryComplete = true; this.showScreen('start-screen'); }, 3000); } else { this.discoverImages().then(() => { this.showScreen('start-screen'); }).catch(error => { console.error('🚨 Error in discoverImages():', error); // Fallback: just mark as complete and show start screen this.imageDiscoveryComplete = true; this.showScreen('start-screen'); }); } this.discoverAudio(); this.updateLoadingProgress(90, 'Audio discovery started...'); // Load video library setTimeout(() => { if (typeof loadUnifiedVideoGallery === 'function') { loadUnifiedVideoGallery(); } }, 100); // Check for auto-resume after initialization this.checkAutoResume(); // Complete initialization setTimeout(() => { this.updateLoadingProgress(100, 'Initialization complete!'); setTimeout(() => { this.hideLoadingOverlay(); this.isInitialized = true; // Initialize stats display including overall XP this.updateStats(); // Initialize overall XP display on home screen this.updateOverallXpDisplay(); }, 500); }, 1500); // Increased delay to allow video loading } showLoadingOverlay() { const overlay = document.getElementById('loading-overlay'); if (overlay) { overlay.style.display = 'flex'; overlay.classList.add('visible'); } } updateLoadingProgress(percentage, message) { this.loadingProgress = percentage; const progressBar = document.querySelector('.loading-progress-fill'); const statusText = document.querySelector('.loading-status'); const percentageElement = document.querySelector('.loading-percentage'); if (progressBar) { progressBar.style.width = `${percentage}%`; } if (statusText) { statusText.textContent = message; } if (percentageElement) { percentageElement.textContent = `${percentage}%`; } } hideLoadingOverlay() { const overlay = document.getElementById('loading-overlay'); if (overlay) { overlay.classList.remove('visible'); setTimeout(() => { overlay.style.display = 'none'; }, 300); // Wait for fade out animation } } initializeVideoManager() { // Initialize video player manager if available if (typeof VideoPlayerManager !== 'undefined') { if (!window.videoPlayerManager) { window.videoPlayerManager = new VideoPlayerManager(); console.log('🎬 Video Player Manager created'); } // Initialize the video system if (window.videoPlayerManager && typeof window.videoPlayerManager.init === 'function') { window.videoPlayerManager.init(); console.log('🎬 Video Player Manager initialized'); } } else { console.warn('⚠️ VideoPlayerManager not available, video features will be disabled'); } } async initDesktopFeatures() { // Initialize desktop file manager if (typeof DesktopFileManager !== 'undefined') { this.fileManager = new DesktopFileManager(this.dataManager); window.desktopFileManager = this.fileManager; // Auto-scan directories on startup setTimeout(async () => { if (this.fileManager && this.fileManager.isElectron) { console.log('🔍 Auto-scanning directories on startup...'); await this.fileManager.scanAllDirectories(); // Reload video player manager after scanning if (window.videoPlayerManager && window.videoPlayerManager.loadVideoFiles) { console.log('🔄 Reloading video library after directory scan...'); await window.videoPlayerManager.loadVideoFiles(); } } }, 1000); // Wait 1 second for initialization to complete } // Check if we're in Electron and update UI accordingly setTimeout(() => { const isElectron = window.electronAPI !== undefined; if (isElectron) { document.body.classList.add('desktop-mode'); // Show desktop-specific features document.querySelectorAll('.desktop-only').forEach(el => el.style.display = ''); document.querySelectorAll('.desktop-feature').forEach(el => el.style.display = ''); document.querySelectorAll('.web-feature').forEach(el => el.style.display = 'none'); console.log('🖥️ Desktop mode activated'); } else { document.body.classList.add('web-mode'); // Hide desktop-only features document.querySelectorAll('.desktop-only').forEach(el => el.style.display = 'none'); document.querySelectorAll('.desktop-feature').forEach(el => el.style.display = 'none'); document.querySelectorAll('.web-feature').forEach(el => el.style.display = ''); console.log('🌐 Web mode activated'); } }, 100); } initializeCustomTasks() { // Load custom tasks from localStorage or use defaults const savedMainTasks = localStorage.getItem('customMainTasks'); const savedConsequenceTasks = localStorage.getItem('customConsequenceTasks'); if (savedMainTasks) { gameData.mainTasks = JSON.parse(savedMainTasks); } if (savedConsequenceTasks) { gameData.consequenceTasks = JSON.parse(savedConsequenceTasks); } // Always add interactive tasks - mode filtering happens later this.addInteractiveTasksToGameData(); console.log(`Loaded ${gameData.mainTasks.length} main tasks and ${gameData.consequenceTasks.length} consequence tasks`); // Debug: log interactive tasks const interactiveTasks = gameData.mainTasks.filter(t => t.interactiveType); console.log(`Interactive tasks available: ${interactiveTasks.length}`, interactiveTasks.map(t => `${t.id}:${t.interactiveType}`)); } addInteractiveTasksToGameData() { // Define our interactive tasks - Scenario Adventures only const interactiveTasks = [ { id: 'scenario-training-session', text: "Enter a guided training session", difficulty: "Medium", interactiveType: "scenario-adventure", interactiveData: { title: "Personal Training Session", steps: { start: { type: 'choice', mood: 'playful', story: "You've decided to have a private training session. Your trainer enters the room with a confident smile. 'Ready to push your limits today?' they ask, eyeing you with interest. The session could go in many directions...", choices: [ { text: "Yes, I want to be challenged", type: "dominant", preview: "Show eagerness for intense training", nextStep: "eager_path" }, { text: "I'm nervous, please be gentle", type: "submissive", preview: "Ask for a softer approach", nextStep: "gentle_path" }, { text: "I want to set the pace myself", type: "normal", preview: "Take control of the session", nextStep: "controlled_path" } ] }, eager_path: { type: 'action', mood: 'intense', story: "Your trainer grins. 'Good, I like enthusiasm. Let's start with some warm-up exercises. I want you to edge for exactly 30 seconds - no more, no less. Show me your control.'", actionText: "Edge for exactly 30 seconds", duration: 30, nextStep: "post_warmup" }, gentle_path: { type: 'action', mood: 'seductive', story: "Your trainer's expression softens. 'Of course, we'll take this slow. Begin with gentle touches, just enough to warm up. Take your time - 45 seconds of light stimulation.'", actionText: "Gentle warm-up touches", duration: 45, nextStep: "post_warmup" }, controlled_path: { type: 'choice', mood: 'neutral', story: "Your trainer nods approvingly. 'I respect that. What kind of challenge would you like to set for yourself?'", choices: [ { text: "Test my endurance", preview: "Long, controlled session", nextStep: "endurance_test" }, { text: "Practice precise control", preview: "Focus on technique", nextStep: "precision_test" } ] }, post_warmup: { type: 'choice', mood: 'seductive', story: "Your trainer watches your performance with interest. 'Not bad... but now comes the real test. Your level is showing, and you need to focus. What's next?'", choices: [ { text: "Push me harder", type: "risky", preview: "Increase the intensity", nextStep: "intense_challenge" }, { text: "I need to slow down", preview: "Take a breather", nextStep: "recovery_break" }, { text: "Let's try something different", preview: "Mix up the routine", nextStep: "creative_challenge" } ] }, intense_challenge: { type: 'action', mood: 'dominant', story: "Your trainer's eyes light up. 'Now we're talking! I want you to edge three times in a row - get close, then stop completely. Each edge should be more intense than the last. Can you handle it?'", actionText: "Triple edge challenge - 60 seconds", duration: 60, nextStep: "final_outcome" }, recovery_break: { type: 'action', mood: 'gentle', story: "Your trainer nods understandingly. 'Smart choice. Sometimes knowing your limits is the most important skill. Take 30 seconds to breathe and center yourself.'", actionText: "Recovery breathing - hands off", duration: 30, nextStep: "final_outcome" }, creative_challenge: { type: 'choice', mood: 'playful', story: "Your trainer smirks. 'Creative, I like that. Let's see... how about we incorporate some variety?'", choices: [ { text: "Change positions every 10 seconds", preview: "Dynamic movement challenge", nextStep: "position_challenge" }, { text: "Use different rhythms", preview: "Rhythm variation exercise", nextStep: "rhythm_challenge" } ] }, position_challenge: { type: 'action', mood: 'playful', story: "Your trainer starts counting. 'Every 10 seconds, I want you to change your position. Standing, sitting, lying down - keep moving, keep stimulating. Ready?'", actionText: "Position changes every 10 seconds", duration: 40, nextStep: "final_outcome" }, rhythm_challenge: { type: 'action', mood: 'seductive', story: "Your trainer begins snapping their fingers in different rhythms. 'Match my beat. Slow... fast... slow again... Can you keep up while staying focused?'", actionText: "Follow the changing rhythm", duration: 45, nextStep: "final_outcome" }, final_outcome: { type: 'ending', mood: 'satisfied', story: "Your trainer evaluates your performance with a satisfied expression. Based on your choices and control, they have some final words for you...", endingText: "Training session complete. You have completed the session. You've learned something valuable about yourself today.", outcome: "success" } } }, hint: "Make choices that align with your comfort level and goals" }, { id: 'scenario-mysterious-game', text: "Accept an invitation to a mysterious game", difficulty: "Hard", interactiveType: "scenario-adventure", interactiveData: { title: "The Mysterious Game", steps: { start: { type: 'choice', mood: 'mysterious', story: "A mysterious note appears: 'You've been selected for a special game. The rules are simple - complete the challenges, earn rewards. Fail, and face consequences. The choice to play is yours...'", choices: [ { text: "Accept the challenge", type: "risky", preview: "Enter the unknown game", nextStep: "game_begins" }, { text: "Decline politely", preview: "Play it safe", nextStep: "safe_exit" } ] }, game_begins: { type: 'choice', mood: 'intense', story: "The game begins. A voice whispers: 'First challenge - prove your dedication. Choose your trial...'", choices: [ { text: "Trial of Endurance", type: "risky", preview: "Test your limits", nextStep: "endurance_trial" }, { text: "Trial of Precision", preview: "Test your control", nextStep: "precision_trial" } ] }, endurance_trial: { type: 'action', mood: 'intense', story: "The voice commands: 'Edge continuously for 90 seconds. Do not stop, do not climax. Prove your endurance...'", actionText: "Endurance test - 90 seconds", duration: 90, nextStep: "game_result" }, precision_trial: { type: 'action', mood: 'focused', story: "The voice instructs: 'Edge exactly to the point of no return, then stop. Hold for 10 seconds. Repeat 3 times. Precision is key...'", actionText: "Precision control test", duration: 60, nextStep: "game_result" }, game_result: { type: 'ending', mood: 'mysterious', story: "The mysterious voice evaluates your performance. Your performance determine your fate...", endingText: "The game concludes. You have proven yourself worthy of... what comes next remains a mystery for another time.", outcome: "partial" }, safe_exit: { type: 'ending', mood: 'neutral', story: "You wisely decline the mysterious invitation. Sometimes discretion is the better part of valor.", endingText: "You've chosen safety over risk. Perhaps another time, when you're feeling more adventurous...", outcome: "partial" } } }, hint: "This scenario has higher stakes - choose carefully" }, { id: 'scenario-punishment-session', text: "Report for your punishment session", difficulty: "Hard", interactiveType: "scenario-adventure", interactiveData: { title: "Punishment Session", steps: { start: { type: 'choice', mood: 'dominant', story: "You've been summoned for punishment. Your infractions have been noted, and now it's time to face the consequences. The room is prepared, and you stand waiting for instructions. How do you approach this punishment?", choices: [ { text: "Accept punishment willingly", type: "submissive", preview: "Show complete submission", nextStep: "willing_punishment" }, { text: "Try to negotiate for mercy", type: "normal", preview: "Attempt to reduce the severity", nextStep: "bargaining" }, { text: "Remain defiant and silent", type: "risky", preview: "Show no submission", nextStep: "defiant_path" } ] }, willing_punishment: { type: 'choice', mood: 'intense', story: "Your submission is noted. 'Good. Since you're being cooperative, choose your first punishment. Remember, this is just the beginning.'", choices: [ { text: "Painful nipple pinching for 30 seconds", type: "submissive", preview: "Sharp, focused pain", nextStep: "nipple_punishment" }, { text: "Humiliating position holding", type: "submissive", preview: "Degrading poses", nextStep: "position_punishment" }, { text: "Denial edging with no release", type: "risky", preview: "Cruel frustration", nextStep: "denial_punishment" } ] }, bargaining: { type: 'choice', mood: 'stern', story: "Your attempt at negotiation is met with a cold stare. 'You think you can bargain your way out of this? That just earned you additional punishment. Now choose - original punishment plus extra, or double punishment.'", choices: [ { text: "Accept the extra punishment", preview: "Original plus additional tasks", nextStep: "extra_punishment" }, { text: "Take the double punishment", type: "risky", preview: "Twice the consequences", nextStep: "double_punishment" } ] }, defiant_path: { type: 'action', mood: 'dominant', story: "Your defiance is noted with displeasure. 'So be it. We'll break that attitude. Strip completely and assume a humiliating position on all fours. Hold it for 2 full minutes while contemplating your disobedience.'", actionText: "Humiliating position - naked on all fours", duration: 120, nextStep: "broken_defiance" }, nipple_punishment: { type: 'action', mood: 'intense', story: "Take your nipples between your fingers. Pinch them hard - harder than comfortable. Hold that pressure for the full duration. No releasing early.", actionText: "Painful nipple pinching", duration: 30, nextStep: "punishment_continues" }, position_punishment: { type: 'action', mood: 'degrading', story: "Get on your knees, spread them wide, and put your hands behind your head. Arch your back and present yourself shamefully. Hold this degrading position and think about what a pathetic display you're making.", actionText: "Humiliating presentation position", duration: 45, nextStep: "punishment_continues" }, denial_punishment: { type: 'action', mood: 'cruel', story: "Begin edging yourself slowly. Build up the pleasure, get close to the edge, then stop completely. Repeat this cycle, but you are absolutely forbidden from cumming. Feel the cruel frustration build.", actionText: "Denial edging - no release allowed", duration: 90, nextStep: "punishment_continues" }, extra_punishment: { type: 'action', mood: 'stern', story: "Your bargaining has consequences. Slap your inner thighs 10 times each, then edge for 60 seconds without completion. This is what happens when you try to negotiate.", actionText: "Thigh slapping plus denial edging", duration: 75, nextStep: "punishment_continues" }, double_punishment: { type: 'action', mood: 'harsh', story: "You chose the harder path. First, painful nipple clamps or pinching for 45 seconds, then immediately into frustrating edge denial for another 45 seconds. No breaks between.", actionText: "Double punishment - pain then denial", duration: 90, nextStep: "punishment_continues" }, broken_defiance: { type: 'choice', mood: 'dominant', story: "Your defiant attitude is cracking. The humiliating position has had its effect. Now, will you finally submit properly, or do you need more correction?", choices: [ { text: "I submit completely now", type: "submissive", preview: "Full surrender", nextStep: "final_submission" }, { text: "I still resist", type: "risky", preview: "Continue defiance", nextStep: "ultimate_punishment" } ] }, punishment_continues: { type: 'choice', mood: 'evaluating', story: "Your punishment is having its effect. The punishment is having its effect. There's still more to your sentence. What comes next?", choices: [ { text: "Please, I've learned my lesson", type: "submissive", preview: "Beg for mercy", nextStep: "mercy_consideration" }, { text: "I can take whatever you give me", type: "risky", preview: "Challenge for more", nextStep: "escalated_punishment" }, { text: "I accept my punishment silently", preview: "Endure stoically", nextStep: "final_punishment" } ] }, mercy_consideration: { type: 'ending', mood: 'satisfied', story: "Your begging is noted. Perhaps you have learned something after all. Your punishment ends here, but remember this lesson.", endingText: "Punishment session concluded. Your state show the effectiveness of proper discipline.", outcome: "partial" }, escalated_punishment: { type: 'action', mood: 'harsh', story: "Your boldness will be your downfall. Final punishment: intense edging while in a painful position. Build to the edge three times but never release. Learn your place.", actionText: "Intense edge torture - no release", duration: 120, nextStep: "punishment_complete" }, final_punishment: { type: 'action', mood: 'concluding', story: "Your acceptance is noted. One final test of your submission: edge slowly and stop exactly when told. Show you've learned control through punishment.", actionText: "Controlled edging demonstration", duration: 60, nextStep: "punishment_complete" }, final_submission: { type: 'ending', mood: 'satisfied', story: "Finally, proper submission. Your defiance has been broken and replaced with appropriate obedience.", endingText: "Your punishment session ends with your complete submission. Your session state. Remember this feeling.", outcome: "success" }, ultimate_punishment: { type: 'action', mood: 'severe', story: "Your continued resistance demands the ultimate punishment. Painful edging in the most humiliating position possible, building arousal while maintaining your shameful display.", actionText: "Ultimate humiliation punishment", duration: 150, nextStep: "punishment_complete" }, punishment_complete: { type: 'ending', mood: 'concluded', story: "Your punishment session is complete. The lesson has been delivered and received.", endingText: "Punishment concluded. Final state - Your session state. You have experienced the consequences of your actions.", outcome: "punishment" } } }, hint: "Different approaches lead to different types of punishment" }, { id: 'scenario-humiliation-task', text: "Complete a series of humiliating tasks", difficulty: "Medium", interactiveType: "scenario-adventure", interactiveData: { title: "Humiliation Challenge", steps: { start: { type: 'choice', mood: 'playful', story: "Today's session focuses on breaking down your pride through carefully crafted humiliation. Each task is designed to make you feel more submissive and exposed. Are you ready to embrace the shame?", choices: [ { text: "Yes, I want to feel humiliated", type: "submissive", preview: "Embrace the degradation", nextStep: "eager_humiliation" }, { text: "Start with something mild", type: "normal", preview: "Ease into it", nextStep: "mild_start" }, { text: "I'm not sure about this", preview: "Hesitant participation", nextStep: "reluctant_start" } ] }, eager_humiliation: { type: 'choice', mood: 'degrading', story: "Perfect. Your eagerness for humiliation is noted. Let's start with something that will make you feel properly shameful. Choose your first degrading task.", choices: [ { text: "Crawl around naked on all fours", type: "submissive", preview: "Animal-like degradation", nextStep: "crawling_task" }, { text: "Repeat degrading phrases about yourself", type: "submissive", preview: "Verbal self-humiliation", nextStep: "verbal_humiliation" }, { text: "Assume shameful positions", type: "submissive", preview: "Degrading physical poses", nextStep: "position_humiliation" } ] }, mild_start: { type: 'action', mood: 'gentle', story: "We'll ease you into this. Start by removing your clothes slowly while thinking about how exposed and vulnerable you're becoming. Take your time and really feel the vulnerability.", actionText: "Slow, deliberate undressing", duration: 45, nextStep: "building_shame" }, reluctant_start: { type: 'choice', mood: 'encouraging', story: "Hesitation is natural. Sometimes the best experiences come from pushing past our comfort zones. Would you like to try something very gentle, or would you prefer to stop here?", choices: [ { text: "Try something very gentle", preview: "Minimal humiliation", nextStep: "gentle_humiliation" }, { text: "I think I should stop", preview: "Exit the scenario", nextStep: "respectful_exit" } ] }, crawling_task: { type: 'action', mood: 'degrading', story: "Get down on your hands and knees. Crawl in a circle around the room like the animal you are. Feel how degrading this position is. You're not a person right now - you're just a pathetic creature on display.", actionText: "Crawling like an animal", duration: 60, nextStep: "humiliation_escalates" }, verbal_humiliation: { type: 'action', mood: 'degrading', story: "Complete the webcam mirror task to look at yourself and repeat degrading phrases: 'I am a pathetic toy', 'I exist for others' pleasure', 'I have no dignity'. Say each phrase 5 times while looking at yourself.", actionText: "Complete webcam mirror self-degradation task", duration: 45, nextStep: "humiliation_escalates", interactiveType: "mirror-task", mirrorInstructions: "Look directly at yourself through the webcam while speaking the degrading phrases", mirrorTaskText: "Repeat each phrase 5 times while maintaining eye contact with yourself: 'I am a pathetic toy', 'I exist for others' pleasure', 'I have no dignity'" }, position_humiliation: { type: 'action', mood: 'shameful', story: "Assume the most shameful position you can think of. Spread yourself wide, arch your back, present yourself like you're begging for attention. Hold this degrading pose and feel the shame wash over you.", actionText: "Shameful presentation position", duration: 50, nextStep: "humiliation_escalates" }, building_shame: { type: 'choice', mood: 'building', story: "Good. Now that you're exposed and vulnerable, you can feel the humiliation starting to build. Your state is evident. Ready for the next level of degradation?", choices: [ { text: "Yes, humiliate me more", type: "submissive", preview: "Increase the shame", nextStep: "deeper_humiliation" }, { text: "This is enough for now", preview: "Stop at current level", nextStep: "gentle_conclusion" } ] }, gentle_humiliation: { type: 'action', mood: 'gentle', story: "Just stand naked and look at yourself. Notice how exposed you feel. There's something humbling about vulnerability, isn't there? Take a moment to appreciate this feeling.", actionText: "Gentle self-awareness", duration: 30, nextStep: "gentle_conclusion" }, humiliation_escalates: { type: 'choice', mood: 'intense', story: "The humiliation is taking effect. Your state and responses show how the degradation is affecting you. Time for the final humiliating task.", choices: [ { text: "Beg to be used and humiliated", type: "submissive", preview: "Complete verbal submission", nextStep: "complete_degradation" }, { text: "Perform the most shameful act I can think of", type: "risky", preview: "Ultimate personal humiliation", nextStep: "ultimate_shame" }, { text: "I can't take any more", preview: "Reach personal limit", nextStep: "mercy_ending" } ] }, deeper_humiliation: { type: 'action', mood: 'degrading', story: "Now for deeper shame. Assume a degrading position and edge yourself while repeating how pathetic and shameful you are. Build the arousal while embracing the humiliation.", actionText: "Degrading edge while self-shaming", duration: 75, nextStep: "humiliation_complete" }, complete_degradation: { type: 'action', mood: 'degrading', story: "Beg out loud. Say exactly what you want done to you. Describe how pathetic you are and how much you need to be used. Let the words shame you as much as the actions.", actionText: "Verbal begging and self-degradation", duration: 60, nextStep: "humiliation_complete" }, ultimate_shame: { type: 'action', mood: 'extreme', story: "Do whatever shameful act comes to mind - the thing that would embarrass you most. This is your ultimate degradation, chosen by your own twisted desires.", actionText: "Personal ultimate shame act", duration: 90, nextStep: "humiliation_complete" }, gentle_conclusion: { type: 'ending', mood: 'gentle', story: "You've experienced a taste of humiliation without going too far. Sometimes just the hint of shame is enough.", endingText: "Gentle humiliation session complete. You maintained some dignity while exploring new feelings. Your session state.", outcome: "partial" }, mercy_ending: { type: 'ending', mood: 'understanding', story: "You've reached your limit, and that's perfectly acceptable. Knowing your boundaries is important.", endingText: "Humiliation session ended at your request. You explored your limits safely. Final state recorded.", outcome: "partial" }, humiliation_complete: { type: 'ending', mood: 'satisfied', story: "Your humiliation session is complete. You've been thoroughly degraded and shamefully aroused.", endingText: "Complete humiliation achieved. Your final state shows the effect: Arousal your state, Control your focus. You've been properly put in your place.", outcome: "success" }, respectful_exit: { type: 'ending', mood: 'respectful', story: "You've chosen to respect your own boundaries. This shows wisdom and self-awareness.", endingText: "Session ended by your choice. Respecting limits is always the right decision.", outcome: "partial" } } }, hint: "Explore humiliation at your own comfort level" }, { id: 'scenario-edging-marathon', text: "Endure an intensive edging marathon", difficulty: "Hard", interactiveType: "scenario-adventure", interactiveData: { title: "Edging Marathon Challenge", steps: { start: { type: 'choice', mood: 'intense', story: "Welcome to the edging marathon - a test of your endurance, self-control, and ability to withstand prolonged arousal without release. This will push your limits. How do you want to approach this challenge?", choices: [ { text: "I want the full intense experience", type: "risky", preview: "Maximum difficulty marathon", nextStep: "intense_marathon" }, { text: "Give me a challenging but manageable pace", type: "normal", preview: "Moderate intensity marathon", nextStep: "moderate_marathon" }, { text: "Start me off gently", preview: "Easier introduction to edging", nextStep: "gentle_marathon" } ] }, intense_marathon: { type: 'action', mood: 'cruel', story: "The intense marathon begins now. Edge for 90 seconds, getting as close as possible to climax, then stop completely for 30 seconds. This cycle will repeat. No mercy, no early finish.", actionText: "Intense edging cycle - 90 seconds", duration: 90, nextStep: "marathon_continues" }, moderate_marathon: { type: 'action', mood: 'challenging', story: "The moderate marathon begins. Edge for 60 seconds, building arousal steadily, then take a 20-second break. Find your rhythm and try to maintain control.", actionText: "Moderate edging cycle - 60 seconds", duration: 60, nextStep: "marathon_continues" }, gentle_marathon: { type: 'action', mood: 'building', story: "The gentle marathon starts slowly. Edge for 45 seconds, focusing on building arousal gradually. You'll have time to recover between cycles.", actionText: "Gentle edging introduction - 45 seconds", duration: 45, nextStep: "marathon_continues" }, marathon_continues: { type: 'choice', mood: 'testing', story: "Round one complete. Your arousal is at your state and your control is at your focus. The marathon continues. How are you feeling for the next round?", choices: [ { text: "I can handle more intensity", type: "risky", preview: "Increase the challenge", nextStep: "escalated_round" }, { text: "Keep the same pace", preview: "Maintain current intensity", nextStep: "consistent_round" }, { text: "I need to slow down", preview: "Reduce intensity", nextStep: "recovery_round" }, { text: "I can't continue", preview: "End the marathon early", nextStep: "early_finish" } ] }, escalated_round: { type: 'action', mood: 'intense', story: "You asked for more intensity. Edge for 2 full minutes this time, getting closer to the edge than before. No stopping until the timer ends. Push your limits.", actionText: "Extended intense edging - 120 seconds", duration: 120, nextStep: "final_challenge" }, consistent_round: { type: 'action', mood: 'steady', story: "Maintaining the same intensity. Continue your edging pattern, staying consistent with your rhythm. Build that arousal steadily.", actionText: "Consistent edging round", duration: 75, nextStep: "final_challenge" }, recovery_round: { type: 'action', mood: 'gentle', story: "Taking it easier this round. Light touches and gentle stimulation. Focus on maintaining arousal without overwhelming yourself.", actionText: "Recovery round - gentle touches", duration: 45, nextStep: "final_challenge" }, final_challenge: { type: 'choice', mood: 'climactic', story: "This is the final challenge of your edging marathon. The punishment is having its effect. Choose how you want to finish this marathon.", choices: [ { text: "Ultimate edge - get as close as possible", type: "risky", preview: "Maximum arousal challenge", nextStep: "ultimate_edge" }, { text: "Controlled finish - demonstrate precision", preview: "Skill-based conclusion", nextStep: "precision_finish" }, { text: "Denied finish - stop before completion", type: "submissive", preview: "Frustrating denial ending", nextStep: "denial_finish" } ] }, ultimate_edge: { type: 'action', mood: 'extreme', story: "The ultimate edge challenge. Get as close to climax as humanly possible without going over. This is the test of your marathon training. Hold that edge for as long as you can.", actionText: "Ultimate edge challenge", duration: 60, nextStep: "marathon_complete" }, precision_finish: { type: 'action', mood: 'controlled', story: "Show the precision you've developed. Edge exactly to your predetermined point, hold for 10 seconds, then stop cleanly. Demonstrate your newfound control.", actionText: "Precision control demonstration", duration: 45, nextStep: "marathon_complete" }, denial_finish: { type: 'action', mood: 'cruel', story: "Build up your arousal one final time, then stop completely when you're desperately close. Feel the cruel frustration of denial after all that work.", actionText: "Final denial - no release", duration: 30, nextStep: "marathon_complete" }, early_finish: { type: 'ending', mood: 'understanding', story: "You've pushed yourself as far as you could go. Sometimes knowing when to stop is the greatest skill.", endingText: "Marathon ended early by choice. You challenged yourself and learned your limits. Your session state.", outcome: "partial" }, marathon_complete: { type: 'ending', mood: 'accomplished', story: "The edging marathon is complete. You've endured the full challenge and tested your limits.", endingText: "Edging marathon finished! Your final state shows your endurance: Arousal your state, Control your focus. You've proven your dedication.", outcome: "success" } } }, hint: "Test your endurance and self-control" }, { id: 'scenario-obedience-training', text: "Begin strict obedience training", difficulty: "Medium", interactiveType: "scenario-adventure", interactiveData: { title: "Obedience Training Program", steps: { start: { type: 'choice', mood: 'authoritative', story: "You've enrolled in an obedience training program. This is about learning to follow instructions precisely, without question or hesitation. Your trainer evaluates you with a stern gaze. How do you present yourself?", choices: [ { text: "Stand at attention, ready to obey", type: "submissive", preview: "Show immediate submission", nextStep: "good_start" }, { text: "Ask what the training involves", type: "normal", preview: "Seek clarification", nextStep: "questioning_attitude" }, { text: "Cross arms and look skeptical", type: "risky", preview: "Show resistance", nextStep: "resistance_noted" } ] }, good_start: { type: 'choice', mood: 'approving', story: "Excellent posture. You're showing the right attitude already. Let's begin with basic commands. Your first lesson in obedience:", choices: [ { text: "Position training - assume commanded poses", type: "submissive", preview: "Physical obedience", nextStep: "position_training" }, { text: "Response training - verbal acknowledgments", preview: "Verbal obedience", nextStep: "response_training" }, { text: "Timing training - precise execution", preview: "Precision obedience", nextStep: "timing_training" } ] }, questioning_attitude: { type: 'choice', mood: 'stern', story: "Questions show you're not ready to simply obey. That needs to be corrected. Choose your response to this mild correction:", choices: [ { text: "Apologize and prepare to obey", type: "submissive", preview: "Accept correction", nextStep: "corrected_attitude" }, { text: "Explain that questions help understanding", preview: "Justify the questions", nextStep: "discipline_needed" } ] }, resistance_noted: { type: 'action', mood: 'stern', story: "Your resistance is noted and will be addressed. First, you'll learn what happens to those who resist. Assume a stress position - hands behind head, standing on toes for 60 seconds.", actionText: "Stress position for resistance", duration: 60, nextStep: "resistance_broken" }, position_training: { type: 'action', mood: 'instructional', story: "When I say 'present', you will immediately assume a kneeling position with hands on thighs. When I say 'attention', stand straight with hands at sides. Practice these transitions for 45 seconds.", actionText: "Position command training", duration: 45, nextStep: "training_progresses" }, response_training: { type: 'action', mood: 'instructional', story: "You will respond to every command with 'Yes Sir' or 'Yes Ma'am'. Practice this now while performing simple actions. Say it out loud with each instruction you follow.", actionText: "Verbal response training", duration: 30, nextStep: "training_progresses" }, timing_training: { type: 'action', mood: 'precise', story: "Obedience must be immediate. When given a command, you have exactly 3 seconds to begin compliance. Practice instant response to touch commands for 40 seconds.", actionText: "Instant response training", duration: 40, nextStep: "training_progresses" }, corrected_attitude: { type: 'action', mood: 'approving', story: "Better. Now that your attitude is corrected, we can proceed with proper training. Begin with basic submission positions - kneel and wait for commands.", actionText: "Basic submission training", duration: 35, nextStep: "training_progresses" }, discipline_needed: { type: 'action', mood: 'disciplinary', story: "Your continued questioning shows you need discipline before training can begin. Hold a stress position while contemplating the value of unquestioning obedience.", actionText: "Disciplinary stress position", duration: 75, nextStep: "discipline_learned" }, resistance_broken: { type: 'choice', mood: 'evaluating', story: "Your resistance is starting to crack. The stress position has had its effect. Are you ready to proceed with proper obedience training now?", choices: [ { text: "Yes, I'm ready to obey", type: "submissive", preview: "Submit to training", nextStep: "obedience_accepted" }, { text: "I still have questions", preview: "Continue resistance", nextStep: "additional_discipline" } ] }, training_progresses: { type: 'choice', mood: 'progressive', story: "Good progress. Your arousal at your state and control at your focus show the training is having an effect. Time for more advanced obedience lessons.", choices: [ { text: "Advanced position training", type: "submissive", preview: "More complex positions", nextStep: "advanced_positions" }, { text: "Endurance obedience test", type: "risky", preview: "Sustained obedience", nextStep: "endurance_test" }, { text: "Precision command following", preview: "Exact obedience", nextStep: "precision_commands" } ] }, discipline_learned: { type: 'choice', mood: 'testing', story: "The discipline seems to have taught you something. Your questioning attitude should be properly adjusted now. Shall we proceed with obedience training?", choices: [ { text: "Yes, no more questions", type: "submissive", preview: "Accept training authority", nextStep: "training_progresses" }, { text: "I understand but need clarification", preview: "Respectful inquiry", nextStep: "final_lesson" } ] }, obedience_accepted: { type: 'action', mood: 'satisfied', story: "Excellent. Now we can begin real training. Follow a series of position commands exactly as given. Show me your newfound obedience.", actionText: "Obedience demonstration", duration: 60, nextStep: "training_complete" }, additional_discipline: { type: 'action', mood: 'harsh', story: "More discipline is clearly needed. Extended stress position combined with arousal denial. You'll learn that questions are not your place.", actionText: "Extended disciplinary training", duration: 90, nextStep: "complete_submission" }, advanced_positions: { type: 'action', mood: 'demanding', story: "Advanced training requires more challenging positions. Transition between multiple poses rapidly while maintaining arousal. Show your growing obedience.", actionText: "Advanced position sequences", duration: 75, nextStep: "training_complete" }, endurance_test: { type: 'action', mood: 'testing', story: "The ultimate test - maintain a challenging position while building arousal, but you may not climax without permission. Endure for the full duration.", actionText: "Endurance obedience test", duration: 120, nextStep: "training_complete" }, precision_commands: { type: 'action', mood: 'exacting', story: "Execute precise movements exactly as commanded. Every hesitation, every imperfection will be noted. Show perfect obedience.", actionText: "Precision command execution", duration: 60, nextStep: "training_complete" }, final_lesson: { type: 'action', mood: 'conclusive', story: "Final lesson: sometimes obedience means accepting what you don't understand. Perform actions without explanation for the remainder of this training.", actionText: "Unquestioned obedience", duration: 45, nextStep: "training_complete" }, complete_submission: { type: 'ending', mood: 'dominant', story: "Your resistance has been completely broken. You now understand the meaning of true obedience.", endingText: "Obedience training completed through discipline. Final state: Arousal your state, Control your focus. You have learned to obey without question.", outcome: "success" }, training_complete: { type: 'ending', mood: 'accomplished', story: "Your obedience training is complete. You've learned to follow commands properly and promptly.", endingText: "Training program completed successfully. Final assessment: Arousal your state, Control your focus. You've developed proper obedience skills.", outcome: "success" } } }, hint: "Learn the value of unquestioning obedience" } ]; // Add interactive tasks to gameData.mainTasks if they don't already exist for (const interactiveTask of interactiveTasks) { const exists = gameData.mainTasks.find(t => t.id === interactiveTask.id); if (!exists) { gameData.mainTasks.push(interactiveTask); console.log(`Added interactive task: ${interactiveTask.text}`); } } } async discoverImages() { console.log('🚀 Original discoverImages method - will be overridden by fix'); // This method will be replaced by the image-discovery-fix.js gameData.discoveredTaskImages = []; gameData.discoveredConsequenceImages = []; this.imageDiscoveryComplete = true; } getEmbeddedManifest() { // Empty manifest - users must upload or scan for their own images return { "tasks": [], "consequences": [] }; } async updateManifestWithNewImages(manifest) { console.log('Scanning for new images...'); // Get cached manifest or current one let cachedManifest = this.dataManager.get('cachedManifest'); if (!cachedManifest) { cachedManifest = {...manifest}; // Copy original } // Scan for new images using pattern detection const newTaskImages = await this.scanDirectoryForNewImages('images/tasks/', cachedManifest.tasks); const newConsequenceImages = await this.scanDirectoryForNewImages('images/consequences/', cachedManifest.consequences); // Add any new images found if (newTaskImages.length > 0) { cachedManifest.tasks = [...new Set([...cachedManifest.tasks, ...newTaskImages])]; // Remove duplicates console.log(`Found ${newTaskImages.length} new task images:`, newTaskImages); } if (newConsequenceImages.length > 0) { cachedManifest.consequences = [...new Set([...cachedManifest.consequences, ...newConsequenceImages])]; // Remove duplicates console.log(`Found ${newConsequenceImages.length} new consequence images:`, newConsequenceImages); } // Save updated manifest to localStorage this.dataManager.set('cachedManifest', cachedManifest); return cachedManifest; } async scanDirectoryForNewImages(directory, knownImages) { const newImages = []; // Comprehensive pattern scan for new images const patterns = [ // Numbers 1-100 ...Array.from({length: 100}, (_, i) => (i + 1).toString()), // Image patterns ...Array.from({length: 50}, (_, i) => `image${i + 1}`), ...Array.from({length: 50}, (_, i) => `img${i + 1}`), ...Array.from({length: 50}, (_, i) => `photo${i + 1}`), ...Array.from({length: 50}, (_, i) => `pic${i + 1}`), // Date-based patterns (common camera formats) ...Array.from({length: 50}, (_, i) => `IMG_${20210101 + i}`), ...Array.from({length: 50}, (_, i) => `DSC${String(i + 1).padStart(4, '0')}`), // Letters a-z ...Array.from({length: 26}, (_, i) => String.fromCharCode(97 + i)), // Random common names 'new', 'test', 'sample', 'demo', 'example', 'temp', 'screenshot', 'capture', // UUID-like patterns ...Array.from({length: 20}, (_, i) => `img_${Date.now() + i}`) ]; console.log(`Scanning ${directory} for new images (${patterns.length} patterns)...`); // Test each pattern with each supported format let checkedCount = 0; const maxChecks = 500; // Reasonable limit to prevent hanging for (const pattern of patterns) { if (checkedCount >= maxChecks) { console.log(`Reached maximum scan limit (${maxChecks} checks)`); break; } for (const format of gameData.supportedImageFormats) { checkedCount++; const filename = `${pattern}${format}`; // Skip if we already know about this image if (knownImages.includes(filename)) { continue; } const imagePath = `${directory}${filename}`; const exists = await this.checkImageExists(imagePath); if (exists) { newImages.push(filename); console.log('✓ Found NEW image:', filename); } } } console.log(`Scanned ${checkedCount} possibilities, found ${newImages.length} new images`); return newImages; } async verifyImagesFromManifest(imageList, directory) { const validImages = []; for (const imageName of imageList) { const imagePath = `${directory}${imageName}`; const exists = await this.checkImageExists(imagePath); if (exists) { validImages.push(imagePath); console.log('✓ Verified image:', imagePath); } else { console.log('✗ Missing image:', imagePath); } } return validImages; } async fallbackImageDiscovery() { // Fallback to simplified pattern detection if manifest fails console.log('Using fallback pattern detection...'); const commonPatterns = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10']; gameData.discoveredTaskImages = await this.findImagesWithPatterns('images/tasks/', commonPatterns); gameData.discoveredConsequenceImages = await this.findImagesWithPatterns('images/consequences/', commonPatterns); // If still no images found, show helpful message but don't use placeholders if (gameData.discoveredTaskImages.length === 0 && gameData.discoveredConsequenceImages.length === 0) { console.log('No images found. Users will need to upload or scan for images.'); gameData.discoveredTaskImages = []; gameData.discoveredConsequenceImages = []; } } async findImagesWithPatterns(directory, patterns) { const foundImages = []; for (const pattern of patterns) { for (const format of gameData.supportedImageFormats) { const imagePath = `${directory}${pattern}${format}`; const exists = await this.checkImageExists(imagePath); if (exists) { foundImages.push(imagePath); console.log('✓ Found image:', imagePath); } } } return foundImages; } setupPlaceholderImages() { gameData.discoveredTaskImages = [this.createPlaceholderImage('Task Image')]; gameData.discoveredConsequenceImages = [this.createPlaceholderImage('Consequence Image')]; } checkImageExists(imagePath) { return new Promise((resolve) => { const img = new Image(); img.onload = () => { console.log(`✓ Found image: ${imagePath}`); resolve(true); }; img.onerror = () => { console.log(`✗ Missing image: ${imagePath}`); resolve(false); }; img.src = imagePath; // Timeout after 1 second (faster discovery) setTimeout(() => { console.log(`⏰ Timeout for image: ${imagePath}`); resolve(false); }, 1000); }); } createPlaceholderImage(label = 'Task Image') { const encodedLabel = encodeURIComponent(label); return `data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjZjBmMGYwIiBzdHJva2U9IiNjY2MiIHN0cm9rZS13aWR0aD0iMiIvPjx0ZXh0IHg9IjUwJSIgeT0iNDAlIiBmb250LXNpemU9IjE2IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBkeT0iLjNlbSIgZmlsbD0iIzY2NiI+JHtsYWJlbH08L3RleHQ+PHRleHQgeD0iNTAlIiB5PSI2MCUiIGZvbnQtc2l6ZT0iMTIiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGR5PSIuM2VtIiBmaWxsPSIjOTk5Ij5BZGQgaW1hZ2VzIHRvIGZvbGRlcjwvdGV4dD48L3N2Zz4=`; } // Audio Discovery Functions async discoverAudio() { try { console.log('Discovering audio files...'); // Initialize audio discovery - scan directories if desktop mode if (this.fileManager) { await this.fileManager.scanDirectoryForAudio('background'); await this.fileManager.scanDirectoryForAudio('ambient'); console.log('Desktop audio discovery completed'); } else { console.log('Web mode - audio discovery skipped'); } } catch (error) { console.log('Audio discovery failed:', error); } this.audioDiscoveryComplete = true; console.log('Audio discovery completed'); } initializeEventListeners() { // Screen navigation document.getElementById('start-btn').addEventListener('click', () => this.startGame()); document.getElementById('resume-btn').addEventListener('click', () => this.resumeGame()); document.getElementById('quit-btn').addEventListener('click', () => this.quitGame()); document.getElementById('play-again-btn').addEventListener('click', () => this.resetGame()); // Game mode selection this.initializeGameModeListeners(); // Game actions document.getElementById('complete-btn').addEventListener('click', () => this.completeTask()); document.getElementById('skip-btn').addEventListener('click', () => this.skipTask()); document.getElementById('mercy-skip-btn').addEventListener('click', () => this.mercySkip()); document.getElementById('pause-btn').addEventListener('click', () => this.pauseGame()); // Theme selector document.getElementById('theme-dropdown').addEventListener('change', (e) => this.changeTheme(e.target.value)); // Options menu toggle document.getElementById('options-menu-btn').addEventListener('click', () => this.toggleOptionsMenu()); // Window cleanup - stop audio when app is closed window.addEventListener('beforeunload', () => { console.log('Window closing - stopping all audio'); this.audioManager.stopAllImmediate(); }); // Music controls document.getElementById('music-toggle').addEventListener('click', () => this.toggleMusic()); document.getElementById('music-toggle-compact').addEventListener('click', (e) => { e.stopPropagation(); // Prevent event bubbling // The hover panel will show automatically, just indicate it's interactive }); document.getElementById('loop-btn').addEventListener('click', () => this.toggleLoop()); document.getElementById('shuffle-btn').addEventListener('click', () => this.toggleShuffle()); document.getElementById('track-selector').addEventListener('change', (e) => this.changeTrack(parseInt(e.target.value))); document.getElementById('volume-slider').addEventListener('input', (e) => this.changeVolume(parseInt(e.target.value))); // Task management document.getElementById('manage-tasks-btn').addEventListener('click', () => this.showTaskManagement()); document.getElementById('back-to-start-btn').addEventListener('click', () => this.showScreen('start-screen')); document.getElementById('add-task-btn').addEventListener('click', () => this.addNewTask()); document.getElementById('reset-tasks-btn').addEventListener('click', () => this.resetToDefaultTasks()); document.getElementById('main-tasks-tab').addEventListener('click', () => this.showTaskTab('main')); document.getElementById('consequence-tasks-tab').addEventListener('click', () => this.showTaskTab('consequence')); document.getElementById('new-task-type').addEventListener('change', () => this.toggleDifficultyDropdown()); // Data management document.getElementById('export-btn').addEventListener('click', () => this.exportData()); document.getElementById('import-btn').addEventListener('click', () => this.importData()); document.getElementById('import-file').addEventListener('change', (e) => this.handleFileImport(e)); document.getElementById('stats-btn').addEventListener('click', () => this.showStats()); document.getElementById('help-btn').addEventListener('click', () => this.showHelp()); document.getElementById('close-stats').addEventListener('click', () => this.hideStats()); document.getElementById('close-help').addEventListener('click', () => this.hideHelp()); document.getElementById('reset-stats-btn').addEventListener('click', () => this.resetStats()); document.getElementById('export-stats-btn').addEventListener('click', () => this.exportStatsOnly()); // Audio controls this.initializeAudioControls(); // Image management - only the main button, others will be attached when screen is shown document.getElementById('manage-images-btn').addEventListener('click', () => this.showImageManagement()); // Audio management - only the main button, others will be attached when screen is shown document.getElementById('manage-audio-btn').addEventListener('click', () => this.showAudioManagement()); // Photo gallery management document.getElementById('photo-gallery-btn').addEventListener('click', () => this.showPhotoGallery()); // Annoyance management - main button and basic controls document.getElementById('manage-annoyance-btn').addEventListener('click', () => this.showAnnoyanceManagement()); // Load saved theme this.loadSavedTheme(); } /** * Initialize webcam system for photography tasks */ async initializeWebcam() { console.log('🎥 Initializing webcam system...'); if (!this.webcamManager) { console.log('⚠️ WebcamManager not available, skipping webcam initialization'); return false; } try { const initialized = await this.webcamManager.init(); if (initialized) { console.log('✅ Webcam system ready for photography tasks'); // Listen for photo task events document.addEventListener('photoTaken', (event) => { this.handlePhotoTaken(event.detail); }); // Listen for photo session completion document.addEventListener('photoSessionComplete', (event) => { this.handlePhotoSessionComplete(event.detail); }); } else { console.log('📷 Webcam not available - photography tasks will use standard mode'); } } catch (error) { console.error('❌ Webcam initialization failed:', error); } } /** * Handle photo taken event from webcam manager */ handlePhotoTaken(detail) { console.log('📸 Photo taken for task:', detail.sessionType); // Show confirmation message this.showNotification('Photo captured successfully! 📸', 'success', 3000); // Progress the interactive task if applicable if (this.interactiveTaskManager && this.interactiveTaskManager.currentTask) { this.interactiveTaskManager.handlePhotoCompletion(detail); } } /** * Handle photo session completion */ handlePhotoSessionComplete(detail) { console.log('🎉 Photo session completed:', detail.sessionType, `(${detail.photos.length} photos)`); // Show completion message this.showNotification(`Photography session completed! ${detail.photos.length} photos taken 📸`, 'success', 4000); // Progress the scenario task if (this.interactiveTaskManager && this.interactiveTaskManager.currentInteractiveTask) { this.interactiveTaskManager.handlePhotoSessionCompletion(detail); } } initializeAudioControls() { // Volume sliders const masterVolumeSlider = document.getElementById('master-volume'); const taskAudioSlider = document.getElementById('task-audio-volume'); const punishmentAudioSlider = document.getElementById('punishment-audio-volume'); const rewardAudioSlider = document.getElementById('reward-audio-volume'); // Volume displays const masterVolumeDisplay = document.getElementById('master-volume-display'); const taskAudioDisplay = document.getElementById('task-audio-volume-display'); const punishmentAudioDisplay = document.getElementById('punishment-audio-volume-display'); const rewardAudioDisplay = document.getElementById('reward-audio-volume-display'); // Audio toggles const enableTaskAudio = document.getElementById('enable-task-audio'); const enablePunishmentAudio = document.getElementById('enable-punishment-audio'); const enableRewardAudio = document.getElementById('enable-reward-audio'); // Preview buttons const previewTaskAudio = document.getElementById('preview-task-audio'); const previewPunishmentAudio = document.getElementById('preview-punishment-audio'); const previewRewardAudio = document.getElementById('preview-reward-audio'); // Load current settings this.loadAudioSettings(); // Master volume control masterVolumeSlider.addEventListener('input', (e) => { const volume = parseInt(e.target.value) / 100; this.audioManager.setMasterVolume(volume); masterVolumeDisplay.textContent = e.target.value + '%'; }); // Category volume controls - all now control the same background audio taskAudioSlider.addEventListener('input', (e) => { const volume = parseInt(e.target.value) / 100; this.audioManager.setCategoryVolume('background', volume); // Update all displays since they all represent the same setting now taskAudioDisplay.textContent = e.target.value + '%'; punishmentAudioDisplay.textContent = e.target.value + '%'; rewardAudioDisplay.textContent = e.target.value + '%'; // Sync all sliders punishmentAudioSlider.value = e.target.value; rewardAudioSlider.value = e.target.value; }); punishmentAudioSlider.addEventListener('input', (e) => { const volume = parseInt(e.target.value) / 100; this.audioManager.setCategoryVolume('background', volume); // Update all displays taskAudioDisplay.textContent = e.target.value + '%'; punishmentAudioDisplay.textContent = e.target.value + '%'; rewardAudioDisplay.textContent = e.target.value + '%'; // Sync all sliders taskAudioSlider.value = e.target.value; rewardAudioSlider.value = e.target.value; }); rewardAudioSlider.addEventListener('input', (e) => { const volume = parseInt(e.target.value) / 100; this.audioManager.setCategoryVolume('background', volume); // Update all displays taskAudioDisplay.textContent = e.target.value + '%'; punishmentAudioDisplay.textContent = e.target.value + '%'; rewardAudioDisplay.textContent = e.target.value + '%'; // Sync all sliders taskAudioSlider.value = e.target.value; punishmentAudioSlider.value = e.target.value; }); // Enable/disable toggles - all now control the same background audio enableTaskAudio.addEventListener('change', (e) => { this.audioManager.setCategoryEnabled('background', e.target.checked); // Sync all toggles since they represent the same setting enablePunishmentAudio.checked = e.target.checked; enableRewardAudio.checked = e.target.checked; }); enablePunishmentAudio.addEventListener('change', (e) => { this.audioManager.setCategoryEnabled('background', e.target.checked); // Sync all toggles enableTaskAudio.checked = e.target.checked; enableRewardAudio.checked = e.target.checked; }); enableRewardAudio.addEventListener('change', (e) => { this.audioManager.setCategoryEnabled('background', e.target.checked); // Sync all toggles enableTaskAudio.checked = e.target.checked; enablePunishmentAudio.checked = e.target.checked; }); // Periodic popup controls this.setupPeriodicPopupControls(); // Preview buttons - all now preview background audio previewTaskAudio.addEventListener('click', () => { this.audioManager.playBackgroundAudio({ fadeIn: 300 }); }); previewPunishmentAudio.addEventListener('click', () => { this.audioManager.playBackgroundAudio({ fadeIn: 300 }); }); previewRewardAudio.addEventListener('click', () => { this.audioManager.playBackgroundAudio({ fadeIn: 300 }); }); // Debug button const debugAudio = document.getElementById('debug-audio'); debugAudio.addEventListener('click', () => { console.log('=== AUDIO DEBUG INFO ==='); console.log('AudioManager initialized:', this.audioManager.isInitialized); console.log('AudioManager library:', this.audioManager.audioLibrary); console.log('AudioManager categories:', this.audioManager.categories); console.log('Master volume:', this.audioManager.masterVolume); this.audioManager.testAudio(); }); } loadAudioSettings() { const settings = this.audioManager.getSettings(); // Update sliders and displays const masterVolume = Math.round(settings.masterVolume * 100); document.getElementById('master-volume').value = masterVolume; document.getElementById('master-volume-display').textContent = masterVolume + '%'; // Use background audio settings for all audio controls since we simplified to one category const backgroundVolume = Math.round((settings.categories.background?.volume || 0.7) * 100); // Update all audio volume controls to use the same background audio setting const audioVolumeElements = [ 'task-audio-volume', 'punishment-audio-volume', 'reward-audio-volume' ]; const audioDisplayElements = [ 'task-audio-volume-display', 'punishment-audio-volume-display', 'reward-audio-volume-display' ]; audioVolumeElements.forEach(id => { const element = document.getElementById(id); if (element) element.value = backgroundVolume; }); audioDisplayElements.forEach(id => { const element = document.getElementById(id); if (element) element.textContent = backgroundVolume + '%'; }); // Update toggles - all use the same background audio enabled setting const backgroundEnabled = settings.categories.background?.enabled || false; const audioToggleElements = [ 'enable-task-audio', 'enable-punishment-audio', 'enable-reward-audio' ]; audioToggleElements.forEach(id => { const element = document.getElementById(id); if (element) element.checked = backgroundEnabled; }); } setupPeriodicPopupControls() { // Get periodic popup control elements const enablePeriodicPopups = document.getElementById('enable-periodic-popups'); const popupMinInterval = document.getElementById('popup-min-interval'); const popupMaxInterval = document.getElementById('popup-max-interval'); const popupDisplayDuration = document.getElementById('popup-display-duration'); const testPeriodicPopup = document.getElementById('test-periodic-popup'); if (!enablePeriodicPopups || !popupMinInterval || !popupMaxInterval || !popupDisplayDuration) { console.log('⚠️ Periodic popup controls not found'); return; } // Load saved settings const savedSettings = this.dataManager.get('periodicPopupSettings') || { enabled: true, minInterval: 30, maxInterval: 120, displayDuration: 5 }; enablePeriodicPopups.checked = savedSettings.enabled; popupMinInterval.value = savedSettings.minInterval; popupMaxInterval.value = savedSettings.maxInterval; popupDisplayDuration.value = savedSettings.displayDuration; // Setup event listeners enablePeriodicPopups.addEventListener('change', (e) => { const isEnabled = e.target.checked; this.savePeriodicPopupSettings(); if (isEnabled && this.gameState.isRunning) { this.popupImageManager.startPeriodicPopups(); } else { this.popupImageManager.stopPeriodicPopups(); } }); popupMinInterval.addEventListener('input', () => { this.validateIntervals(); this.savePeriodicPopupSettings(); this.updatePeriodicPopupSettings(); }); popupMaxInterval.addEventListener('input', () => { this.validateIntervals(); this.savePeriodicPopupSettings(); this.updatePeriodicPopupSettings(); }); popupDisplayDuration.addEventListener('input', () => { this.savePeriodicPopupSettings(); this.updatePeriodicPopupSettings(); }); if (testPeriodicPopup) { testPeriodicPopup.addEventListener('click', () => { this.testPeriodicPopup(); }); } console.log('🔧 Periodic popup controls initialized'); } validateIntervals() { const minInterval = document.getElementById('popup-min-interval'); const maxInterval = document.getElementById('popup-max-interval'); if (parseInt(minInterval.value) >= parseInt(maxInterval.value)) { maxInterval.value = parseInt(minInterval.value) + 10; } } savePeriodicPopupSettings() { const settings = { enabled: document.getElementById('enable-periodic-popups').checked, minInterval: parseInt(document.getElementById('popup-min-interval').value), maxInterval: parseInt(document.getElementById('popup-max-interval').value), displayDuration: parseInt(document.getElementById('popup-display-duration').value) }; this.dataManager.set('periodicPopupSettings', settings); } updatePeriodicPopupSettings() { const settings = { minInterval: parseInt(document.getElementById('popup-min-interval').value), maxInterval: parseInt(document.getElementById('popup-max-interval').value), displayDuration: parseInt(document.getElementById('popup-display-duration').value) }; if (this.popupImageManager && this.popupImageManager.updatePeriodicSettings) { this.popupImageManager.updatePeriodicSettings(settings); } } testPeriodicPopup() { if (this.popupImageManager && this.popupImageManager.showPeriodicPopup) { console.log('🧪 Testing periodic popup'); this.popupImageManager.showPeriodicPopup(); } else { console.log('⚠️ Popup manager not available for testing'); } } getAudioIntensityForDifficulty(difficulty) { switch (difficulty.toLowerCase()) { case 'easy': case 'beginner': return 'teasing'; case 'medium': case 'intermediate': return 'teasing'; case 'hard': case 'advanced': return 'intense'; case 'extreme': case 'expert': return 'intense'; default: return 'teasing'; } } initializeGameModeListeners() { console.log('🎮 Initializing game mode listeners...'); const gameModeRadios = document.querySelectorAll('input[name="gameMode"]'); console.log(`🎮 Found ${gameModeRadios.length} game mode radio buttons`); gameModeRadios.forEach((radio, index) => { console.log(`🎮 Radio ${index}: ${radio.id} (${radio.value})`); radio.addEventListener('change', () => { console.log(`🎮 Radio changed: ${radio.value}`); this.handleGameModeChange(); }); }); // Add listeners for dropdown changes (if elements exist) const timeLimitSelect = document.getElementById('time-limit-select'); if (timeLimitSelect) { console.log('⏱️ Found time limit select, adding listener'); timeLimitSelect.addEventListener('change', () => { console.log('⏱️ Time limit select changed'); this.handleTimeLimitChange(); }); } else { console.log('⚠️ Time limit select not found'); } const xpTargetSelect = document.getElementById('xp-target-select'); if (xpTargetSelect) { console.log('⭐ Found XP target select, adding listener'); xpTargetSelect.addEventListener('change', () => { console.log('⭐ XP target select changed'); this.handleXpTargetChange(); }); } else { console.log('⚠️ XP target select not found'); } // Add listeners for custom input changes (if elements exist) const customTimeValue = document.getElementById('custom-time-value'); if (customTimeValue) { customTimeValue.addEventListener('input', () => { this.handleCustomTimeChange(); }); } const customXpValue = document.getElementById('custom-xp-value'); if (customXpValue) { customXpValue.addEventListener('input', () => { this.handleCustomXpChange(); }); } // Initialize with default mode this.handleGameModeChange(); } handleGameModeChange() { const selectedMode = document.querySelector('input[name="gameMode"]:checked')?.value; if (selectedMode) { this.gameState.gameMode = selectedMode; } console.log(`🎮 Game mode changed to: ${selectedMode}`); // Show/hide configuration options based on selected mode (if elements exist) const timedConfig = document.getElementById('timed-config'); const xpTargetConfig = document.getElementById('xp-target-config'); // Hide all configs first if (timedConfig) { timedConfig.style.display = 'none'; } if (xpTargetConfig) { xpTargetConfig.style.display = 'none'; } // Show appropriate config if (selectedMode === 'timed' && timedConfig) { timedConfig.style.display = 'block'; console.log('⏱️ Showing timed configuration options'); this.handleTimeLimitChange(); } else if (selectedMode === 'xp-target' && xpTargetConfig) { xpTargetConfig.style.display = 'block'; console.log('⭐ Showing XP target configuration options'); this.handleXpTargetChange(); } console.log(`Game state updated:`, { gameMode: this.gameState.gameMode, timeLimit: this.gameState.timeLimit, xpTarget: this.gameState.xpTarget }); } handleTimeLimitChange() { const timeLimitSelect = document.getElementById('time-limit-select'); const customTimeInput = document.getElementById('custom-time-input'); if (!timeLimitSelect) { console.log('⚠️ Time limit select element not found'); return; } const selectedValue = timeLimitSelect.value; console.log(`⏱️ Time limit selection: ${selectedValue}`); if (customTimeInput) { if (selectedValue === 'custom') { customTimeInput.style.display = 'block'; console.log('⏱️ Showing custom time input'); this.handleCustomTimeChange(); } else { customTimeInput.style.display = 'none'; this.gameState.timeLimit = parseInt(selectedValue); console.log(`⏱️ Time limit set to: ${this.gameState.timeLimit} seconds`); } } else if (selectedValue !== 'custom') { this.gameState.timeLimit = parseInt(selectedValue); console.log(`⏱️ Time limit set to: ${this.gameState.timeLimit} seconds`); } } handleXpTargetChange() { const xpTargetSelect = document.getElementById('xp-target-select'); const customXpInput = document.getElementById('custom-xp-input'); if (!xpTargetSelect) { console.log('⚠️ XP target select element not found'); return; } const selectedValue = xpTargetSelect.value; console.log(`⭐ XP target selection: ${selectedValue}`); if (customXpInput) { if (selectedValue === 'custom') { customXpInput.style.display = 'block'; console.log('⭐ Showing custom XP input'); this.handleCustomXpChange(); } else { customXpInput.style.display = 'none'; this.gameState.xpTarget = parseInt(selectedValue); console.log(`⭐ XP target set to: ${this.gameState.xpTarget} XP`); } } else if (selectedValue !== 'custom') { this.gameState.xpTarget = parseInt(selectedValue); console.log(`⭐ XP target set to: ${this.gameState.xpTarget} XP`); } } handleCustomTimeChange() { const customTimeValue = document.getElementById('custom-time-value'); if (customTimeValue) { const minutes = parseInt(customTimeValue.value) || 15; this.gameState.timeLimit = minutes * 60; // Convert minutes to seconds console.log(`Custom time limit set to ${minutes} minutes (${this.gameState.timeLimit} seconds)`); } } handleCustomXpChange() { const customXpValue = document.getElementById('custom-xp-value'); if (customXpValue) { const xp = parseInt(customXpValue.value) || 100; this.gameState.xpTarget = xp; console.log(`Custom XP target set to ${xp} XP`); } } // XP Calculation Methods calculateTimeBasedXp() { if (!this.gameState.sessionStartTime) return 0; const currentTime = Date.now(); const sessionDuration = (currentTime - this.gameState.sessionStartTime) / 60000; // Convert to minutes return Math.floor(sessionDuration / 2); // 1 XP per 2 minutes } calculateActivityBonusXp() { let bonusXp = 0; // Focus session bonus: 5 XP per minute const focusMinutes = (this.gameState.focusSessionTime || 0) / 60000; bonusXp += Math.floor(focusMinutes * 5); // Webcam mirror bonus: 5 XP per minute const webcamMinutes = (this.gameState.webcamMirrorTime || 0) / 60000; bonusXp += Math.floor(webcamMinutes * 5); // Photo bonus: 1 XP per photo bonusXp += this.gameState.photosTaken || 0; return bonusXp; } updateXp() { // XP comes only from task completion - no time or activity restrictions const taskCompletionXp = this.gameState.taskCompletionXp || 0; // Set current session XP to task completion XP only this.gameState.xp = taskCompletionXp; // Debug logging to track XP sources if (taskCompletionXp > 0) { console.log(`💰 XP Update - Task XP: ${taskCompletionXp}`); } this.updateStats(); // Check if XP target is reached for xp-target mode if (this.gameState.gameMode === 'xp-target' && taskCompletionXp >= this.gameState.xpTarget) { console.log(`⭐ XP target of ${this.gameState.xpTarget} reached! Current XP: ${taskCompletionXp}`); this.endGame('target-reached'); } return taskCompletionXp; } trackFocusSession(isActive) { if (isActive && !this.gameState._focusSessionStart) { this.gameState._focusSessionStart = Date.now(); // Start scenario focus tracking if in scenario mode if (window.gameModeManager && window.gameModeManager.isScenarioMode()) { this.startScenarioFocusActivity(); } } else if (!isActive && this.gameState._focusSessionStart) { const duration = Date.now() - this.gameState._focusSessionStart; this.gameState.focusSessionTime = (this.gameState.focusSessionTime || 0) + duration; delete this.gameState._focusSessionStart; // Stop scenario focus tracking if in scenario mode if (window.gameModeManager && window.gameModeManager.isScenarioMode()) { this.stopScenarioFocusActivity(); } else { this.updateXp(); } } } trackWebcamMirror(isActive) { if (isActive && !this.gameState._webcamMirrorStart) { this.gameState._webcamMirrorStart = Date.now(); // Start scenario webcam tracking if in scenario mode if (window.gameModeManager && window.gameModeManager.isScenarioMode()) { this.startScenarioWebcamActivity(); } } else if (!isActive && this.gameState._webcamMirrorStart) { const duration = Date.now() - this.gameState._webcamMirrorStart; this.gameState.webcamMirrorTime = (this.gameState.webcamMirrorTime || 0) + duration; delete this.gameState._webcamMirrorStart; // Stop scenario webcam tracking if in scenario mode if (window.gameModeManager && window.gameModeManager.isScenarioMode()) { this.stopScenarioWebcamActivity(); } else { this.updateXp(); } } } incrementPhotosTaken() { this.gameState.photosTaken = (this.gameState.photosTaken || 0) + 1; // Award scenario photo XP if in scenario mode if (window.gameModeManager && window.gameModeManager.isScenarioMode()) { this.awardScenarioPhotoXp(); } else { this.updateXp(); console.log(`📷 Photo taken! Total photos: ${this.gameState.photosTaken}, XP gained: +1`); } } // ===== SCENARIO XP SYSTEM ===== // Separate from main game XP - implements ROADMAP requirements initializeScenarioXp() { // Initialize scenario XP tracking when entering a scenario const now = Date.now(); this.gameState.scenarioXp = { timeBased: 0, focusBonuses: 0, webcamBonuses: 0, photoRewards: 0, stepCompletion: 0, total: 0 }; this.gameState.scenarioTracking = { startTime: now, lastTimeXpAwarded: now, lastFocusXpAwarded: now, lastWebcamXpAwarded: now, isInFocusActivity: false, isInWebcamActivity: false, totalPhotosThisSession: 0 }; console.log('🎭 Scenario XP tracking initialized'); } updateScenarioTimeBasedXp() { // Award 1 XP per 2 minutes of scenario gameplay (only when not paused) if (!this.gameState.scenarioTracking.startTime || this.gameState.isPaused) return; const now = Date.now(); const timeSinceLastAward = now - this.gameState.scenarioTracking.lastTimeXpAwarded; const twoMinutesMs = 2 * 60 * 1000; // 2 minutes in milliseconds if (timeSinceLastAward >= twoMinutesMs) { const xpToAward = Math.floor(timeSinceLastAward / twoMinutesMs); this.gameState.scenarioXp.timeBased += xpToAward; this.gameState.scenarioTracking.lastTimeXpAwarded = now; this.updateScenarioTotalXp(); console.log(`⏰ Time-based XP: +${xpToAward} (${(timeSinceLastAward/60000).toFixed(1)} minutes)`); } } updateScenarioFocusXp() { // Award 3 XP per 30 seconds during focus activities if (!this.gameState.scenarioTracking.isInFocusActivity || this.gameState.isPaused) return; const now = Date.now(); const timeSinceLastAward = now - this.gameState.scenarioTracking.lastFocusXpAwarded; const thirtySecondsMs = 30 * 1000; // 30 seconds in milliseconds if (timeSinceLastAward >= thirtySecondsMs) { const periods = Math.floor(timeSinceLastAward / thirtySecondsMs); const xpToAward = periods * 3; this.gameState.scenarioXp.focusBonuses += xpToAward; this.gameState.scenarioTracking.lastFocusXpAwarded = now; this.updateScenarioTotalXp(); console.log(`🎯 Focus bonus XP: +${xpToAward} (${periods} x 30s periods)`); } } updateScenarioWebcamXp() { // Award 3 XP per 30 seconds during webcam mirror activities if (!this.gameState.scenarioTracking.isInWebcamActivity || this.gameState.isPaused) return; const now = Date.now(); const timeSinceLastAward = now - this.gameState.scenarioTracking.lastWebcamXpAwarded; const thirtySecondsMs = 30 * 1000; // 30 seconds in milliseconds if (timeSinceLastAward >= thirtySecondsMs) { const periods = Math.floor(timeSinceLastAward / thirtySecondsMs); const xpToAward = periods * 3; this.gameState.scenarioXp.webcamBonuses += xpToAward; this.gameState.scenarioTracking.lastWebcamXpAwarded = now; this.updateScenarioTotalXp(); console.log(`📹 Webcam bonus XP: +${xpToAward} (${periods} x 30s periods)`); } } awardScenarioPhotoXp() { // Award 1 XP per photo taken during scenarios this.gameState.scenarioXp.photoRewards += 1; this.gameState.scenarioTracking.totalPhotosThisSession += 1; this.updateScenarioTotalXp(); console.log(`📸 Photo XP: +1 (Total photos this session: ${this.gameState.scenarioTracking.totalPhotosThisSession})`); } awardScenarioStepXp() { // Award 5 XP per scenario step completion (existing system) this.gameState.scenarioXp.stepCompletion += 5; this.updateScenarioTotalXp(); console.log(`🎭 Scenario step XP: +5`); } updateScenarioTotalXp() { // Calculate total scenario XP and update session XP const total = this.gameState.scenarioXp.timeBased + this.gameState.scenarioXp.focusBonuses + this.gameState.scenarioXp.webcamBonuses + this.gameState.scenarioXp.photoRewards + this.gameState.scenarioXp.stepCompletion; this.gameState.scenarioXp.total = total; // For scenarios, use scenario XP instead of task completion XP if (window.gameModeManager && window.gameModeManager.isScenarioMode()) { this.gameState.xp = total; this.updateStats(); } } startScenarioFocusActivity() { // Called when entering focus-hold activities this.gameState.scenarioTracking.isInFocusActivity = true; this.gameState.scenarioTracking.lastFocusXpAwarded = Date.now(); console.log('🎯 Focus activity started - tracking XP bonuses'); } stopScenarioFocusActivity() { // Called when exiting focus-hold activities this.gameState.scenarioTracking.isInFocusActivity = false; console.log('🎯 Focus activity stopped'); } startScenarioWebcamActivity() { // Called when entering webcam mirror activities this.gameState.scenarioTracking.isInWebcamActivity = true; this.gameState.scenarioTracking.lastWebcamXpAwarded = Date.now(); console.log('📹 Webcam activity started - tracking XP bonuses'); } stopScenarioWebcamActivity() { // Called when exiting webcam mirror activities this.gameState.scenarioTracking.isInWebcamActivity = false; console.log('📹 Webcam activity stopped'); } setupKeyboardShortcuts() { document.addEventListener('keydown', (e) => { // Don't trigger shortcuts when typing in inputs or textareas if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') { return; } // Don't trigger shortcuts during scenario modes (except basic navigation) const isScenarioMode = window.gameModeManager && window.gameModeManager.isScenarioMode(); if (isScenarioMode && this.gameState.isRunning) { // Only allow basic navigation shortcuts in scenario mode switch(e.key.toLowerCase()) { case 'escape': // Escape key - still allow modal closing e.preventDefault(); this.handleEscapeKey(); break; case 'h': // H key - still allow help e.preventDefault(); this.toggleHelp(); break; case 'm': // M key - still allow music toggle e.preventDefault(); this.toggleMusic(); break; } return; // Block all other shortcuts in scenario mode } switch(e.key.toLowerCase()) { case ' ': // Spacebar - pause/unpause game e.preventDefault(); if (this.gameState.isRunning) { this.pauseGame(); } else if (this.gameState.isPaused) { this.resumeGame(); } break; case 'p': // P key - pause/unpause game e.preventDefault(); if (this.gameState.isRunning) { this.pauseGame(); } else if (this.gameState.isPaused) { this.resumeGame(); } break; case 'm': // M key - toggle music e.preventDefault(); this.toggleMusic(); break; case 'escape': // Escape key - close modals or return to start e.preventDefault(); this.handleEscapeKey(); break; case 'h': // H key - toggle help menu e.preventDefault(); this.toggleHelp(); break; case 'enter': // Enter key - complete task e.preventDefault(); if (this.gameState.isRunning && !this.gameState.isPaused) { this.completeTask(); } break; case 'control': // Ctrl key - regular skip only e.preventDefault(); if (this.gameState.isRunning && !this.gameState.isPaused) { // Check for Shift+Ctrl for mercy skip if (e.shiftKey) { const mercyBtn = document.getElementById('mercy-skip-btn'); if (mercyBtn && mercyBtn.style.display !== 'none' && !mercyBtn.disabled) { this.mercySkip(); } } else { this.skipTask(); } } break; } }); // Show keyboard shortcuts hint console.log('Keyboard shortcuts enabled: Enter (complete), Ctrl (skip), Shift+Ctrl (mercy skip), Space/P (pause), M (music), H (help), Escape (close/back)'); console.log('Note: Most shortcuts are disabled during scenario modes to prevent interference'); } /** * Handle escape key functionality */ handleEscapeKey() { // Close help modal if open const helpModal = document.getElementById('help-modal'); if (helpModal && helpModal.style.display !== 'none') { this.hideHelp(); return; } // Close stats modal if open const statsModal = document.getElementById('stats-modal'); if (statsModal && !statsModal.hidden) { this.hideStats(); return; } // If in task management, go back to start screen if (document.getElementById('task-management-screen').style.display !== 'none') { this.showScreen('start-screen'); return; } // If in game, pause it if (this.gameState.isRunning) { this.pauseGame(); } } setupWindowResizeHandling() { let resizeTimeout; // Handle window resize events for dynamic image scaling window.addEventListener('resize', () => { // Debounce resize events to avoid excessive processing clearTimeout(resizeTimeout); resizeTimeout = setTimeout(() => { this.handleWindowResize(); }, 150); // Wait 150ms after resize stops }); // Initial setup this.handleWindowResize(); } handleWindowResize() { const taskImage = document.getElementById('task-image'); if (!taskImage) return; // Get current window dimensions const windowWidth = window.innerWidth; const windowHeight = window.innerHeight; // Add dynamic class based on window size for additional styling hooks const gameContainer = document.querySelector('.game-container'); if (gameContainer) { gameContainer.classList.remove('window-small', 'window-medium', 'window-large', 'window-xl'); if (windowWidth >= 1600) { gameContainer.classList.add('window-xl'); } else if (windowWidth >= 1200) { gameContainer.classList.add('window-large'); } else if (windowWidth >= 900) { gameContainer.classList.add('window-medium'); } else { gameContainer.classList.add('window-small'); } } // Force layout recalculation for better image sizing if (taskImage.src && this.gameState.isRunning) { // Trigger a reflow to ensure proper image sizing taskImage.style.opacity = '0.99'; requestAnimationFrame(() => { taskImage.style.opacity = ''; }); } console.log(`Window resized to ${windowWidth}x${windowHeight}, image scaling updated`); } checkAutoResume() { try { // Check if there's a saved game state to resume const savedGameState = this.dataManager.get('autoSaveGameState'); if (savedGameState && savedGameState.isRunning === false && savedGameState.isPaused === true) { // Show auto-resume prompt setTimeout(() => { if (confirm('You have a paused game session. Would you like to resume where you left off?')) { this.loadAutoSavedGame(savedGameState); } else { // Clear the auto-save if user declines localStorage.removeItem('autoSaveGameState'); } }, 500); // Small delay to ensure UI is ready } } catch (error) { console.warn('Auto-resume check failed:', this.formatErrorMessage('auto-resume', error)); // Clear potentially corrupted auto-save data localStorage.removeItem('autoSaveGameState'); } } autoSaveGameState() { // Save current game state for auto-resume if (this.gameState.isRunning || this.gameState.isPaused) { const saveState = { ...this.gameState, currentTask: this.gameState.currentTask, savedAt: new Date().toISOString() }; this.dataManager.set('autoSaveGameState', saveState); } } loadAutoSavedGame(savedState) { // Restore the saved game state this.gameState = { ...this.gameState, ...savedState, isRunning: false, // Will be set to true when resumed isPaused: true }; // Update UI to show the paused game this.showScreen('game-screen'); this.updateDisplay(); // Show the resume notification this.showNotification('Game session restored! Click Resume to continue.', 'success'); } // Loading indicator methods showButtonLoading(buttonId) { const button = document.getElementById(buttonId); if (button) { button.disabled = true; const textSpan = button.querySelector('.btn-text'); const loadingSpan = button.querySelector('.btn-loading'); if (textSpan && loadingSpan) { textSpan.style.display = 'none'; loadingSpan.style.display = 'inline'; } } } hideButtonLoading(buttonId) { const button = document.getElementById(buttonId); if (button) { button.disabled = false; const textSpan = button.querySelector('.btn-text'); const loadingSpan = button.querySelector('.btn-loading'); if (textSpan && loadingSpan) { textSpan.style.display = 'inline'; loadingSpan.style.display = 'none'; } } } toggleDifficultyDropdown() { const taskType = document.getElementById('new-task-type').value; const difficultyGroup = document.getElementById('difficulty-input-group'); if (taskType === 'main') { difficultyGroup.style.display = 'block'; } else { difficultyGroup.style.display = 'none'; } } // Task Management Methods showTaskManagement() { this.showScreen('task-management-screen'); this.populateTaskLists(); this.toggleDifficultyDropdown(); // Set initial visibility } showTaskTab(tabType) { // Update tab buttons document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active')); document.getElementById(`${tabType}-tasks-tab`).classList.add('active'); // Update task lists document.querySelectorAll('.task-list').forEach(list => list.classList.remove('active')); document.getElementById(`${tabType}-tasks-list`).classList.add('active'); } populateTaskLists() { this.populateTaskList('main', gameData.mainTasks); this.populateTaskList('consequence', gameData.consequenceTasks); } populateTaskList(type, tasks) { const listElement = document.getElementById(`${type}-tasks-list`); if (tasks.length === 0) { listElement.innerHTML = '
No tasks added yet. Add some tasks to get started!
'; return; } listElement.innerHTML = tasks.map((task, index) => { let difficultyDisplay = ''; if (type === 'main' && task.difficulty) { const emoji = this.getDifficultyEmoji(task.difficulty); const points = this.getPointsForDifficulty(task.difficulty); difficultyDisplay = `
${emoji} ${task.difficulty} (${points} ${points === 1 ? 'point' : 'points'})
`; } return `
${task.text}
${difficultyDisplay}
`; }).join(''); } addNewTask() { const taskText = document.getElementById('new-task-text').value.trim(); const taskType = document.getElementById('new-task-type').value; const taskDifficulty = document.getElementById('new-task-difficulty').value; if (!taskText) { alert('Please enter a task description!'); return; } // Create new task object const newTask = { id: Date.now(), // Use timestamp as unique ID text: taskText }; // Add difficulty for main tasks only if (taskType === 'main') { newTask.difficulty = taskDifficulty; } // Add to appropriate array if (taskType === 'main') { gameData.mainTasks.push(newTask); localStorage.setItem('customMainTasks', JSON.stringify(gameData.mainTasks)); } else { gameData.consequenceTasks.push(newTask); localStorage.setItem('customConsequenceTasks', JSON.stringify(gameData.consequenceTasks)); } // Clear input and refresh list document.getElementById('new-task-text').value = ''; document.getElementById('new-task-difficulty').value = 'Medium'; this.populateTaskLists(); // Switch to the appropriate tab this.showTaskTab(taskType === 'main' ? 'main' : 'consequence'); console.log(`Added new ${taskType} task: ${taskText}`); } editTask(type, index) { const tasks = type === 'main' ? gameData.mainTasks : gameData.consequenceTasks; const task = tasks[index]; const newText = prompt(`Edit ${type} task:`, task.text); if (newText !== null && newText.trim() !== '') { tasks[index].text = newText.trim(); // Save to localStorage const storageKey = type === 'main' ? 'customMainTasks' : 'customConsequenceTasks'; localStorage.setItem(storageKey, JSON.stringify(tasks)); // Refresh display this.populateTaskLists(); console.log(`Edited ${type} task: ${newText}`); } } deleteTask(type, index) { const tasks = type === 'main' ? gameData.mainTasks : gameData.consequenceTasks; const task = tasks[index]; if (confirm(`Are you sure you want to delete this ${type} task?\n\n"${task.text}"`)) { tasks.splice(index, 1); // Save to localStorage const storageKey = type === 'main' ? 'customMainTasks' : 'customConsequenceTasks'; localStorage.setItem(storageKey, JSON.stringify(tasks)); // Refresh display this.populateTaskLists(); console.log(`Deleted ${type} task: ${task.text}`); } } resetToDefaultTasks() { if (confirm('This will reset all tasks to the original defaults. Are you sure?')) { // Clear localStorage localStorage.removeItem('customMainTasks'); localStorage.removeItem('customConsequenceTasks'); // Reload the page to get fresh default tasks location.reload(); } } changeTheme(themeName) { // Remove any existing theme link const existingThemeLink = document.getElementById('dynamic-theme'); if (existingThemeLink) { existingThemeLink.remove(); } // Remove old theme classes if they exist const oldThemeClasses = ['theme-sunset', 'theme-forest', 'theme-midnight', 'theme-pastel', 'theme-neon', 'theme-autumn', 'theme-monochrome']; document.body.classList.remove(...oldThemeClasses); // Load the appropriate theme CSS file if (themeName && themeName !== 'balanced-purple') { const themeLink = document.createElement('link'); themeLink.id = 'dynamic-theme'; themeLink.rel = 'stylesheet'; themeLink.href = `${themeName}-theme.css`; document.head.appendChild(themeLink); } // balanced-purple is our default theme loaded in the HTML, so no need to load additional CSS // Save theme preference localStorage.setItem('gameTheme', themeName); console.log(`Theme changed to: ${themeName}`); } loadSavedTheme() { // Load saved theme or default to balanced-purple const savedTheme = localStorage.getItem('gameTheme') || 'balanced-purple'; // Make sure the dropdown reflects the saved theme const themeDropdown = document.getElementById('theme-dropdown'); if (themeDropdown) { themeDropdown.value = savedTheme; } // Apply the theme this.changeTheme(savedTheme); } showScreen(screenId) { // Hide all screens document.querySelectorAll('.screen').forEach(screen => { screen.classList.remove('active'); }); // Show target screen const targetScreen = document.getElementById(screenId); if (targetScreen) { targetScreen.classList.add('active'); } else { console.error(`Screen element not found: ${screenId}`); } // Update start screen status when showing start screen if (screenId === 'start-screen') { this.updateStartScreenStatus(); } } updateStartScreenStatus() { // Status message removed for cleaner home screen interface const statusDiv = document.getElementById('start-screen-status'); if (statusDiv) { statusDiv.style.display = 'none'; } } toggleOptionsMenu() { const optionsMenu = document.getElementById('options-menu'); const button = document.getElementById('options-menu-btn'); if (optionsMenu.style.display === 'none' || optionsMenu.style.display === '') { optionsMenu.style.display = 'block'; button.textContent = '⚙️ Close Options'; } else { optionsMenu.style.display = 'none'; button.textContent = '⚙️ Options'; } } toggleMusic() { this.musicManager.toggle(); } toggleLoop() { this.musicManager.toggleLoopMode(); } toggleShuffle() { this.musicManager.toggleShuffleMode(); } changeTrack(trackIndex) { this.musicManager.changeTrack(trackIndex); } changeVolume(volume) { this.musicManager.setVolume(volume); } // Data Management Methods exportData() { try { this.showButtonLoading('export-btn'); // Small delay to show loading indicator setTimeout(() => { try { this.dataManager.exportData(true); this.showNotification('💾 Save file exported successfully!', 'success'); } catch (error) { this.showNotification('❌ ' + this.formatErrorMessage('export', error), 'error'); } finally { this.hideButtonLoading('export-btn'); } }, 300); } catch (error) { this.showNotification('❌ ' + this.formatErrorMessage('export', error), 'error'); this.hideButtonLoading('export-btn'); } } importData() { // Trigger the file input document.getElementById('import-file').click(); } async handleFileImport(event) { const file = event.target.files[0]; if (!file) return; this.showButtonLoading('import-btn'); try { await this.dataManager.importData(file); this.showNotification('📁 Save file imported successfully!', 'success'); // Refresh UI to show new data this.musicManager.volume = this.dataManager.getSetting('music.volume') || 30; this.musicManager.currentTrackIndex = this.dataManager.getSetting('music.currentTrack') || 0; this.musicManager.loopMode = this.dataManager.getSetting('music.loopMode') || 0; this.musicManager.shuffleMode = this.dataManager.getSetting('music.shuffle') || false; this.musicManager.initializeVolumeUI(); // Update theme const theme = this.dataManager.getSetting('theme') || 'balanced-purple'; this.changeTheme(theme); } catch (error) { this.showNotification('❌ ' + this.formatErrorMessage('import', error), 'error'); } finally { this.hideButtonLoading('import-btn'); } // Clear the file input event.target.value = ''; } showStats() { const stats = this.dataManager.getStats(); // Update stat displays document.getElementById('stat-games').textContent = stats.totalGamesPlayed; document.getElementById('stat-completed').textContent = stats.totalTasksCompleted; document.getElementById('stat-score').textContent = stats.bestScore; document.getElementById('stat-streak').textContent = stats.currentStreak; document.getElementById('stat-rate').textContent = stats.completionPercentage + '%'; document.getElementById('stat-hours').textContent = stats.hoursPlayed; document.getElementById('stats-modal').style.display = 'block'; } hideStats() { document.getElementById('stats-modal').style.display = 'none'; } showHelp() { document.getElementById('help-modal').style.display = 'block'; } hideHelp() { document.getElementById('help-modal').style.display = 'none'; } toggleHelp() { const helpModal = document.getElementById('help-modal'); if (helpModal.style.display === 'none' || helpModal.style.display === '') { this.showHelp(); } else { this.hideHelp(); } } // Image Management Methods showImageManagement() { // Reset listener flag to allow fresh attachment this.imageManagementListenersAttached = false; this.showScreen('image-management-screen'); this.setupImageManagementEventListeners(); // Wait for image discovery to complete before loading gallery if (!this.imageDiscoveryComplete) { const gallery = document.getElementById('image-gallery'); gallery.innerHTML = '
Discovering images...
'; // Wait and try again setTimeout(() => { if (this.imageDiscoveryComplete) { this.loadImageGallery(); } else { gallery.innerHTML = '
Still discovering images... Please wait
'; setTimeout(() => this.loadImageGallery(), 1000); } }, 500); } else { this.loadImageGallery(); } } switchImageTab(tabType) { // Update tab buttons const taskTab = document.getElementById('task-images-tab'); const consequenceTab = document.getElementById('consequence-images-tab'); const taskGallery = document.getElementById('task-images-gallery'); const consequenceGallery = document.getElementById('consequence-images-gallery'); if (tabType === 'task') { taskTab.classList.add('active'); consequenceTab.classList.remove('active'); taskGallery.classList.add('active'); consequenceGallery.classList.remove('active'); } else { taskTab.classList.remove('active'); consequenceTab.classList.add('active'); taskGallery.classList.remove('active'); consequenceGallery.classList.add('active'); } // Update gallery controls to work with current tab this.updateImageGalleryControls(tabType); } updateImageGalleryControls(activeTab) { // Update the select/deselect/delete buttons to work with the active tab const selectAllBtn = document.getElementById('select-all-images-btn'); const deselectAllBtn = document.getElementById('deselect-all-images-btn'); const deleteBtn = document.getElementById('delete-selected-btn'); if (selectAllBtn) { selectAllBtn.onclick = () => this.selectAllImages(activeTab); } if (deselectAllBtn) { deselectAllBtn.onclick = () => this.deselectAllImages(activeTab); } if (deleteBtn) { deleteBtn.onclick = () => this.deleteSelectedImages(activeTab); } } setupImageManagementEventListeners() { // Check if we already have listeners attached to prevent duplicates if (this.imageManagementListenersAttached) { return; } // Back button const backBtn = document.getElementById('back-to-start-from-images-btn'); if (backBtn) { backBtn.onclick = () => this.showScreen('start-screen'); } // Desktop import buttons const importTaskBtn = document.getElementById('import-task-images-btn'); if (importTaskBtn) { importTaskBtn.onclick = async () => { if (this.fileManager) { await this.fileManager.selectAndImportImages('task'); this.loadImageGallery(); // Refresh the gallery to show new images } else { this.showNotification('Desktop file manager not available', 'warning'); } }; } const importConsequenceBtn = document.getElementById('import-consequence-images-btn'); if (importConsequenceBtn) { importConsequenceBtn.onclick = async () => { if (this.fileManager) { await this.fileManager.selectAndImportImages('consequence'); this.loadImageGallery(); // Refresh the gallery to show new images } else { this.showNotification('Desktop file manager not available', 'warning'); } }; } // Storage info button const storageInfoBtn = document.getElementById('storage-info-btn'); if (storageInfoBtn) { storageInfoBtn.onclick = () => this.showStorageInfo(); } // Tab buttons const taskImagesTab = document.getElementById('task-images-tab'); if (taskImagesTab) { taskImagesTab.onclick = () => this.switchImageTab('task'); } const consequenceImagesTab = document.getElementById('consequence-images-tab'); if (consequenceImagesTab) { consequenceImagesTab.onclick = () => this.switchImageTab('consequence'); } // Upload input (fallback for web mode) const uploadInput = document.getElementById('image-upload-input'); if (uploadInput) { uploadInput.onchange = (e) => this.handleImageUpload(e); } // Web upload button (fallback) const uploadBtn = document.getElementById('upload-images-btn'); if (uploadBtn) { uploadBtn.onclick = () => this.uploadImages(); } // Mark listeners as attached this.imageManagementListenersAttached = true; } loadImageGallery() { // Load both task and consequence image galleries this.cleanupInvalidImages(); // Clean up invalid images first this.loadTaskImages(); this.loadConsequenceImages(); this.updateImageCounts(); // Initialize with task tab active and update controls this.updateImageGalleryControls('task'); } loadTaskImages() { const gallery = document.getElementById('task-images-gallery'); gallery.innerHTML = '
Loading task images...
'; // Get task images const taskImages = gameData.discoveredTaskImages || []; const customImages = this.dataManager.get('customImages') || { task: [], consequence: [] }; const disabledImages = this.dataManager.get('disabledImages') || []; // Get custom task images (handle both old array format and new object format) let customTaskImages = []; if (Array.isArray(customImages)) { // Old format - treat all as task images for backward compatibility customTaskImages = customImages; } else { // New format - get task images specifically customTaskImages = customImages.task || []; } const allTaskImages = [...taskImages, ...customTaskImages]; if (allTaskImages.length === 0) { gallery.innerHTML = '
No task images found. Upload or scan for task images!
'; return; } gallery.innerHTML = ''; this.renderImageItems(gallery, allTaskImages, disabledImages, customTaskImages); } loadConsequenceImages() { const gallery = document.getElementById('consequence-images-gallery'); gallery.innerHTML = '
Loading consequence images...
'; // Get consequence images const consequenceImages = gameData.discoveredConsequenceImages || []; const customImages = this.dataManager.get('customImages') || { task: [], consequence: [] }; const disabledImages = this.dataManager.get('disabledImages') || []; // Get custom consequence images (new format only) let customConsequenceImages = []; if (!Array.isArray(customImages)) { customConsequenceImages = customImages.consequence || []; } const allConsequenceImages = [...consequenceImages, ...customConsequenceImages]; if (allConsequenceImages.length === 0) { gallery.innerHTML = '
No consequence images found. Upload or scan for consequence images!
'; return; } gallery.innerHTML = ''; this.renderImageItems(gallery, allConsequenceImages, disabledImages, customConsequenceImages); } renderImageItems(gallery, images, disabledImages, customImages) { images.forEach((imageData, index) => { const imageItem = document.createElement('div'); imageItem.className = 'image-item'; // Handle both old base64 format and new cached metadata format let imagePath, displayName, imageSrc; if (typeof imageData === 'string') { // Old base64 format or regular file path imagePath = imageData; imageSrc = imageData; displayName = this.getImageDisplayName(imageData); } else { // New cached metadata format imagePath = imageData.cachedPath || imageData.originalName; // Use dataUrl if available, otherwise show placeholder if (imageData.dataUrl) { imageSrc = imageData.dataUrl; } else { // Legacy cached image without dataUrl - show placeholder imageSrc = this.createPlaceholderImage('Missing Data'); console.warn('Image missing dataUrl:', imageData.originalName); } displayName = imageData.originalName || 'Cached Image'; } imageItem.dataset.imagePath = imagePath; const isDisabled = disabledImages.includes(imagePath); imageItem.innerHTML = ` Image
${isDisabled ? 'Disabled' : 'Enabled'}
`; // Add event listener for enable/disable const checkbox = imageItem.querySelector('.image-checkbox'); checkbox.addEventListener('change', (e) => this.toggleImageEnabled(imagePath, e.target.checked)); // Add event listener for individual image selection (click to select/deselect) imageItem.addEventListener('click', (e) => { // Don't toggle selection if clicking on the checkbox if (e.target.type !== 'checkbox') { this.toggleImageSelection(imageItem); } }); gallery.appendChild(imageItem); }); } updateImageCounts() { const imageCount = document.querySelector('.image-count'); const taskImages = gameData.discoveredTaskImages || []; const consequenceImages = gameData.discoveredConsequenceImages || []; const customImages = this.dataManager.get('customImages') || { task: [], consequence: [] }; const disabledImages = this.dataManager.get('disabledImages') || []; // Handle both old array format and new object format let customTaskCount = 0; let customConsequenceCount = 0; let totalCustomImages = 0; if (Array.isArray(customImages)) { // Old format - treat all as task images customTaskCount = customImages.length; totalCustomImages = customImages.length; } else { // New format customTaskCount = (customImages.task || []).length; customConsequenceCount = (customImages.consequence || []).length; totalCustomImages = customTaskCount + customConsequenceCount; } const totalTaskImages = taskImages.length + customTaskCount; const totalConsequenceImages = consequenceImages.length + customConsequenceCount; const totalImages = totalTaskImages + totalConsequenceImages; const enabledImages = totalImages - disabledImages.length; if (imageCount) { imageCount.textContent = `${totalImages} total images (${totalTaskImages} task, ${totalConsequenceImages} consequence, ${enabledImages} enabled)`; } } getAllImages() { // Return the current images from the game's discovery system const taskImages = gameData.discoveredTaskImages || []; const consequenceImages = gameData.discoveredConsequenceImages || []; console.log('getAllImages - taskImages:', taskImages); console.log('getAllImages - consequenceImages:', consequenceImages); console.log('getAllImages - imageDiscoveryComplete:', this.imageDiscoveryComplete); return [...taskImages, ...consequenceImages]; } getImageDisplayName(imagePath) { // Extract filename from path const parts = imagePath.split('/'); return parts[parts.length - 1]; } toggleImageEnabled(imagePath, enabled) { let disabledImages = this.dataManager.get('disabledImages') || []; if (enabled) { disabledImages = disabledImages.filter(img => img !== imagePath); } else { if (!disabledImages.includes(imagePath)) { disabledImages.push(imagePath); } } this.dataManager.set('disabledImages', disabledImages); // Update the status display const imageItem = document.querySelector(`[data-image-path="${imagePath}"]`); if (imageItem) { const statusElement = imageItem.querySelector('.image-status'); statusElement.className = `image-status ${enabled ? 'image-enabled' : 'image-disabled'}`; statusElement.textContent = enabled ? 'Enabled' : 'Disabled'; } // Update count this.updateImageCount(); this.showNotification(`Image ${enabled ? 'enabled' : 'disabled'} successfully!`, 'success'); } toggleImageSelection(imageItem) { imageItem.classList.toggle('selected'); } uploadImages() { document.getElementById('image-upload-input').click(); } async handleImageUpload(event) { const files = Array.from(event.target.files); if (files.length === 0) return; // Determine which tab is active to know where to save images const activeTab = this.getActiveImageTab(); this.showNotification(`Processing ${files.length} images for ${activeTab} category...`, 'info'); let successCount = 0; let failedCount = 0; const maxImages = 50; // Reasonable limit to prevent storage overflow // Check current image count const customImages = this.dataManager.get('customImages') || { task: [], consequence: [] }; const currentTaskCount = Array.isArray(customImages) ? customImages.length : (customImages.task || []).length; const currentConsequenceCount = Array.isArray(customImages) ? 0 : (customImages.consequence || []).length; const totalCurrentImages = currentTaskCount + currentConsequenceCount; if (totalCurrentImages >= maxImages) { this.showNotification(`Image limit reached (${maxImages} max). Please delete some images first.`, 'error'); return; } for (const file of files) { if (successCount + totalCurrentImages >= maxImages) { this.showNotification(`Stopped at image limit (${maxImages} max). ${successCount} uploaded, ${files.length - successCount} skipped.`, 'warning'); break; } try { const imageData = await this.processUploadedImage(file, activeTab); if (imageData) { // Try to save to localStorage with error handling if (await this.saveImageWithQuotaCheck(imageData, activeTab)) { successCount++; } else { failedCount++; console.warn('Storage quota exceeded for image:', file.name); break; // Stop processing if quota exceeded } } } catch (error) { console.warn('Failed to process image:', file.name, error); failedCount++; } } if (successCount > 0) { this.showNotification(`${successCount} ${activeTab} image(s) uploaded successfully!${failedCount > 0 ? ` (${failedCount} failed due to storage limits)` : ''}`, successCount > failedCount ? 'success' : 'warning'); this.loadImageGallery(); // Refresh the gallery this.cleanupInvalidImages(); // Clean up any images without dataUrl } else { this.showNotification(`Upload failed. ${failedCount > 0 ? 'Storage quota exceeded. Try deleting some images first.' : 'Please try again.'}`, 'error'); } // Clear the input event.target.value = ''; } async saveImageWithQuotaCheck(imageData, activeTab) { try { let customImages = this.dataManager.get('customImages') || { task: [], consequence: [] }; // Ensure the structure exists if (!customImages.task) customImages.task = []; if (!customImages.consequence) customImages.consequence = []; // Add to the appropriate category customImages[activeTab].push(imageData); // Try to save with quota checking this.dataManager.set('customImages', customImages); return true; } catch (error) { if (error.name === 'QuotaExceededError') { console.error('Storage quota exceeded:', error); return false; } else { console.error('Error saving image:', error); return false; } } } getActiveImageTab() { const taskTab = document.getElementById('task-images-tab'); return taskTab && taskTab.classList.contains('active') ? 'task' : 'consequence'; } cleanupInvalidImages() { const customImages = this.dataManager.get('customImages') || { task: [], consequence: [] }; let hasChanges = false; if (Array.isArray(customImages)) { // Old format - clean up and convert to new format const validImages = customImages.filter(img => { if (typeof img === 'string') { return true; // Keep old base64 strings } else { return !!img.dataUrl; // Only keep metadata objects with dataUrl } }); if (validImages.length !== customImages.length) { console.log(`Cleaned up ${customImages.length - validImages.length} invalid cached images`); // Convert to new format - put all in task category for backward compatibility this.dataManager.set('customImages', { task: validImages, consequence: [] }); hasChanges = true; } else { // Convert to new format without cleaning this.dataManager.set('customImages', { task: customImages, consequence: [] }); hasChanges = true; } } else { // New format - clean up both categories const cleanCategory = (category) => { const images = customImages[category] || []; return images.filter(img => { if (typeof img === 'string') { return true; // Keep old base64 strings } else { return !!img.dataUrl; // Only keep metadata objects with dataUrl } }); }; const validTaskImages = cleanCategory('task'); const validConsequenceImages = cleanCategory('consequence'); const originalTaskCount = (customImages.task || []).length; const originalConsequenceCount = (customImages.consequence || []).length; if (validTaskImages.length !== originalTaskCount || validConsequenceImages.length !== originalConsequenceCount) { const cleanedCount = (originalTaskCount + originalConsequenceCount) - (validTaskImages.length + validConsequenceImages.length); console.log(`Cleaned up ${cleanedCount} invalid cached images`); this.dataManager.set('customImages', { task: validTaskImages, consequence: validConsequenceImages }); hasChanges = true; } } return hasChanges; } async scanForNewImages() { this.showNotification('Scanning directories for new images...', 'info'); try { // Get the current embedded manifest let manifest = this.getEmbeddedManifest(); // Force a fresh scan by clearing any cached manifest this.dataManager.set('cachedManifest', null); // Perform comprehensive scan for new images console.log('Performing user-requested scan for new images...'); manifest = await this.updateManifestWithNewImages(manifest); // Update discovered images with new manifest gameData.discoveredTaskImages = await this.verifyImagesFromManifest(manifest.tasks, 'images/tasks/'); gameData.discoveredConsequenceImages = await this.verifyImagesFromManifest(manifest.consequences, 'images/consequences/'); // Check how many new images were found const totalImages = gameData.discoveredTaskImages.length + gameData.discoveredConsequenceImages.length; this.showNotification(`Scan complete! Found ${totalImages} total images.`, 'success'); this.loadImageGallery(); // Refresh the gallery to show any new images } catch (error) { console.error('Directory scan failed:', error); this.showNotification('Directory scan failed. Please try again.', 'error'); } } clearImageCache() { // Clear the cached manifest to force a fresh scan next time this.dataManager.set('cachedManifest', null); this.showNotification('Image cache cleared. Next scan will be fresh.', 'success'); } showStorageInfo() { try { const isDesktop = window.electronAPI !== undefined; const customImages = this.dataManager.get('customImages') || { task: [], consequence: [] }; let totalCustomImages = 0; let taskCount = 0; let consequenceCount = 0; if (Array.isArray(customImages)) { // Old format totalCustomImages = customImages.length; taskCount = customImages.length; consequenceCount = 0; } else { // New format - count both categories taskCount = (customImages.task || []).length; consequenceCount = (customImages.consequence || []).length; totalCustomImages = taskCount + consequenceCount; } let message; if (isDesktop) { message = ` 📊 Desktop Application Storage: 🖼️ Image Library: ├─ Task Images: ${taskCount} └─ Consequence Images: ${consequenceCount} Total Images: ${totalCustomImages} 💾 Storage Type: Native File System 📁 Storage Location: images/ folder in app directory 💡 Storage Capacity: Unlimited (depends on available disk space) ✅ Desktop Benefits: • No browser storage limitations • Full-resolution image support • Native file system performance • Automatic directory organization • Cross-platform file compatibility 🎯 Image Management: • Import images using native file dialogs • Images automatically organized by category • No compression or size restrictions • Direct file system access for best performance ${totalCustomImages === 0 ? '📷 No custom images imported yet. Use the Import buttons to add your images!' : ''} `.trim(); } else { // Original web version info const storageData = localStorage.getItem(this.dataManager.storageKey); const storageSize = storageData ? storageData.length : 0; const storageMB = (storageSize / (1024 * 1024)).toFixed(2); const maxMB = 6; const usagePercent = ((storageSize / (maxMB * 1024 * 1024)) * 100).toFixed(1); message = ` 📊 Browser Storage Information: LocalStorage Used: ${storageMB} MB / ~${maxMB} MB (${usagePercent}%) Total Custom Images: ${totalCustomImages} ${usagePercent > 85 ? '⚠️ Storage getting full - consider deleting some images' : usagePercent > 70 ? '⚡ Storage usage is moderate' : '✅ Storage usage is healthy'} 💡 Browser Limitations: - Limited to ~6MB total storage - Image compression required - 50 image limit to prevent storage issues 💡 Consider upgrading to the desktop version for unlimited storage! `.trim(); } alert(message); } catch (error) { this.showNotification('Failed to get storage info', 'error'); } } async processUploadedImage(file, category = 'task') { return new Promise(async (resolve, reject) => { try { // Validate file type if (!file.type.startsWith('image/')) { reject(new Error('Not an image file')); return; } // Check file size before processing - 20MB limit if (file.size > 20 * 1024 * 1024) { reject(new Error('Image too large. Please use images smaller than 20MB.')); return; } console.log(`Processing image: ${file.name}, size: ${(file.size / (1024 * 1024)).toFixed(2)}MB`); // Generate unique filename for cached image const timestamp = Date.now(); const fileExtension = file.name.split('.').pop().toLowerCase(); const cachedFileName = `cached_${timestamp}.${fileExtension}`; const cachedImagePath = `images/cached/${cachedFileName}`; // For web compatibility, we'll still compress but save as blob URL reference const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); const img = new Image(); img.onload = async () => { try { // Calculate new dimensions (higher quality - 1600x1200) let { width, height } = img; const maxWidth = 1600; const maxHeight = 1200; console.log(`Original dimensions: ${width}x${height}`); if (width > maxWidth || height > maxHeight) { const ratio = Math.min(maxWidth / width, maxHeight / height); width = Math.floor(width * ratio); height = Math.floor(height * ratio); console.log(`Resized dimensions: ${width}x${height}`); } // Set canvas size and draw high-quality image canvas.width = width; canvas.height = height; ctx.drawImage(img, 0, 0, width, height); // Convert to blob with high quality (0.95 for minimal compression) canvas.toBlob(async (blob) => { try { // Save the blob as file reference const imageData = await this.saveBlobToCache(blob, cachedFileName, file.name); const compressedSize = (blob.size / (1024 * 1024)).toFixed(2); console.log(`Cached image size: ${compressedSize}MB at ${cachedImagePath}`); resolve(imageData); } catch (error) { console.error('Failed to save to cache:', error); reject(error); } }, 'image/jpeg', 0.95); } catch (error) { console.error('Image processing error:', error); reject(error); } }; img.onerror = () => reject(new Error('Failed to load image')); // Create object URL for the image const reader = new FileReader(); reader.onload = (e) => { img.src = e.target.result; }; reader.onerror = reject; reader.readAsDataURL(file); } catch (error) { reject(error); } }); } async saveBlobToCache(blob, cachedFileName, originalName) { // Convert blob to base64 for reliable storage and display return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => { const imageMetadata = { cachedFileName: cachedFileName, originalName: originalName, cachedPath: `images/cached/${cachedFileName}`, dataUrl: reader.result, // Store the base64 data URL size: blob.size, type: blob.type, timestamp: Date.now() }; resolve(imageMetadata); }; reader.onerror = reject; reader.readAsDataURL(blob); }); } selectAllImages(tabType = 'task') { const galleryId = tabType === 'task' ? 'task-images-gallery' : 'consequence-images-gallery'; const gallery = document.getElementById(galleryId); const imageItems = gallery.querySelectorAll('.image-item'); imageItems.forEach(imageItem => { imageItem.classList.add('selected'); }); } deselectAllImages(tabType = 'task') { const galleryId = tabType === 'task' ? 'task-images-gallery' : 'consequence-images-gallery'; const gallery = document.getElementById(galleryId); const imageItems = gallery.querySelectorAll('.image-item'); imageItems.forEach(imageItem => { imageItem.classList.remove('selected'); }); } async deleteSelectedImages(tabType = 'task') { const galleryId = tabType === 'task' ? 'task-images-gallery' : 'consequence-images-gallery'; const gallery = document.getElementById(galleryId); const selectedItems = gallery.querySelectorAll('.image-item.selected'); if (selectedItems.length === 0) { this.showNotification('No images selected for deletion.', 'warning'); return; } if (!confirm(`Are you sure you want to delete ${selectedItems.length} selected image(s)?`)) { return; } let customImages = this.dataManager.get('customImages') || { task: [], consequence: [] }; let deletedCount = 0; let fileDeletedCount = 0; // Handle both old array format and new object format if (Array.isArray(customImages)) { // Old format - convert to new format first customImages = { task: customImages, consequence: [] }; } // If we're in desktop mode, also delete the actual files const isDesktop = this.fileManager && this.fileManager.isElectron; for (const item of selectedItems) { const imagePath = item.dataset.imagePath; // Determine which category this image belongs to let imageCategory = 'task'; // default if (imagePath.includes('consequences') || imagePath.includes('consequence')) { imageCategory = 'consequence'; } else if (imagePath.includes('tasks') || imagePath.includes('task')) { imageCategory = 'task'; } // Delete the actual file if in desktop mode if (isDesktop) { try { const fileDeleted = await this.fileManager.deleteImage(imagePath, imageCategory); if (fileDeleted) { fileDeletedCount++; deletedCount++; // File manager handles storage cleanup } } catch (error) { console.error('Error deleting file:', error); } } else { // Web mode - only remove from storage ['task', 'consequence'].forEach(category => { const initialLength = customImages[category].length; customImages[category] = customImages[category].filter(img => { if (typeof img === 'string') { return img !== imagePath; // Old format: direct string comparison } else { // New format: compare against cachedPath or originalName const imgPath = img.cachedPath || img.originalName; return imgPath !== imagePath; } }); if (customImages[category].length < initialLength) { deletedCount++; } }); } } // Update storage if not in desktop mode (desktop mode handles this in deleteImage) if (!isDesktop && deletedCount > 0) { this.dataManager.set('customImages', customImages); } // Clean up disabled image entries for deleted images if (deletedCount > 0) { let disabledImages = this.dataManager.get('disabledImages') || []; const originalDisabledLength = disabledImages.length; // Remove disabled entries for deleted images selectedItems.forEach(item => { const imagePath = item.dataset.imagePath; disabledImages = disabledImages.filter(img => img !== imagePath); }); if (disabledImages.length < originalDisabledLength) { this.dataManager.set('disabledImages', disabledImages); } const message = isDesktop ? `${fileDeletedCount} image file(s) and references deleted successfully!` : `${deletedCount} custom image(s) deleted successfully!`; this.showNotification(message, 'success'); this.loadImageGallery(); // Refresh the gallery } else { this.showNotification('No custom images were selected. Default images cannot be deleted.', 'warning'); } } updateImageCount() { const images = this.getAllImages(); const customImages = this.dataManager.get('customImages') || { task: [], consequence: [] }; const disabledImages = this.dataManager.get('disabledImages') || []; // Handle both old and new customImages format let customImagesArray = []; if (Array.isArray(customImages)) { // Old format - flat array customImagesArray = customImages; } else { // New format - object with task and consequence arrays customImagesArray = [...(customImages.task || []), ...(customImages.consequence || [])]; } const allImages = [...images, ...customImagesArray]; const imageCount = document.querySelector('.image-count'); if (imageCount) { imageCount.textContent = `${allImages.length} images (${allImages.length - disabledImages.length} enabled)`; } } resetStats() { if (this.dataManager.resetStats()) { this.showNotification('📊 Statistics reset successfully!', 'success'); this.hideStats(); } } exportStatsOnly() { try { this.showButtonLoading('export-stats-btn'); // Small delay to show loading indicator setTimeout(() => { try { this.dataManager.exportData(false); this.showNotification('📊 Statistics exported successfully!', 'success'); } catch (error) { this.showNotification('❌ ' + this.formatErrorMessage('stats-export', error), 'error'); } finally { this.hideButtonLoading('export-stats-btn'); } }, 300); } catch (error) { this.showNotification('❌ ' + this.formatErrorMessage('stats-export', error), 'error'); this.hideButtonLoading('export-stats-btn'); } } // Better error message formatting formatErrorMessage(operation, error) { const baseMessages = { 'export': 'Failed to export save data', 'import': 'Failed to import save data', 'stats-export': 'Failed to export statistics', 'auto-resume': 'Failed to restore previous session' }; const specificErrors = { 'QuotaExceededError': 'Not enough storage space available', 'SecurityError': 'File access blocked by browser security', 'TypeError': 'Invalid file format detected', 'SyntaxError': 'Corrupted save file data' }; const baseMessage = baseMessages[operation] || 'Operation failed'; const specificMessage = specificErrors[error.name] || error.message; return `${baseMessage}: ${specificMessage}`; } // Audio Management Functions showAudioManagement() { // Reset listener flag to allow fresh attachment this.audioManagementListenersAttached = false; this.showScreen('audio-management-screen'); this.setupAudioManagementEventListeners(); // Wait for audio discovery to complete before loading gallery if (!this.audioDiscoveryComplete) { const galleries = document.querySelectorAll('.audio-gallery'); galleries.forEach(gallery => { gallery.innerHTML = '
Discovering audio files...
'; }); // Wait and try again setTimeout(() => { if (this.audioDiscoveryComplete) { this.loadAudioGallery(); } else { galleries.forEach(gallery => { gallery.innerHTML = '
Still discovering audio... Please wait
'; }); setTimeout(() => this.loadAudioGallery(), 1000); } }, 500); } else { this.loadAudioGallery(); } } switchAudioTab(tabType) { // Update tab buttons const backgroundTab = document.getElementById('background-audio-tab'); const ambientTab = document.getElementById('ambient-audio-tab'); const backgroundGallery = document.getElementById('background-audio-gallery'); const ambientGallery = document.getElementById('ambient-audio-gallery'); // Remove active class from all tabs and galleries [backgroundTab, ambientTab].forEach(tab => tab && tab.classList.remove('active')); [backgroundGallery, ambientGallery].forEach(gallery => gallery && gallery.classList.remove('active')); // Add active class to selected tab and gallery if (tabType === 'background') { backgroundTab && backgroundTab.classList.add('active'); backgroundGallery && backgroundGallery.classList.add('active'); } else if (tabType === 'ambient') { ambientTab && ambientTab.classList.add('active'); ambientGallery && ambientGallery.classList.add('active'); } // Update gallery controls to work with current tab this.updateAudioGalleryControls(tabType); } setupAudioManagementEventListeners() { // Check if we already have listeners attached to prevent duplicates if (this.audioManagementListenersAttached) { return; } // Back button const backBtn = document.getElementById('back-to-start-from-audio-btn'); if (backBtn) { backBtn.onclick = () => this.showScreen('start-screen'); } // Desktop import buttons const importBackgroundBtn = document.getElementById('import-background-audio-btn'); if (importBackgroundBtn) { importBackgroundBtn.onclick = async () => { if (this.fileManager) { await this.fileManager.selectAndImportAudio('background'); this.loadAudioGallery(); // Refresh the gallery to show new audio } else { this.showNotification('Desktop file manager not available', 'warning'); } }; } const importAmbientBtn = document.getElementById('import-ambient-audio-btn'); if (importAmbientBtn) { importAmbientBtn.onclick = async () => { if (this.fileManager) { await this.fileManager.selectAndImportAudio('ambient'); this.loadAudioGallery(); // Refresh the gallery to show new audio } else { this.showNotification('Desktop file manager not available', 'warning'); } }; } // Audio storage info button const audioStorageInfoBtn = document.getElementById('audio-storage-info-btn'); if (audioStorageInfoBtn) { audioStorageInfoBtn.onclick = () => this.showAudioStorageInfo(); } // Scan directories button const scanAudioDirectoriesBtn = document.getElementById('scan-audio-directories-btn'); if (scanAudioDirectoriesBtn) { scanAudioDirectoriesBtn.onclick = () => this.scanAudioDirectories(); } // Tab buttons const backgroundAudioTab = document.getElementById('background-audio-tab'); if (backgroundAudioTab) { backgroundAudioTab.onclick = () => this.switchAudioTab('background'); } const ambientAudioTab = document.getElementById('ambient-audio-tab'); if (ambientAudioTab) { ambientAudioTab.onclick = () => this.switchAudioTab('ambient'); } // Gallery control buttons - assign onclick directly to avoid expensive DOM operations const selectAllAudioBtn = document.getElementById('select-all-audio-btn'); if (selectAllAudioBtn) { selectAllAudioBtn.onclick = () => this.selectAllAudio(); } const deselectAllAudioBtn = document.getElementById('deselect-all-audio-btn'); if (deselectAllAudioBtn) { deselectAllAudioBtn.onclick = () => this.deselectAllAudio(); } const deleteSelectedAudioBtn = document.getElementById('delete-selected-audio-btn'); if (deleteSelectedAudioBtn) { deleteSelectedAudioBtn.onclick = () => this.deleteSelectedAudio(); } const previewSelectedAudioBtn = document.getElementById('preview-selected-audio-btn'); if (previewSelectedAudioBtn) { previewSelectedAudioBtn.onclick = () => this.previewSelectedAudio(); } // Close preview button const closePreviewBtn = document.getElementById('close-preview-btn'); if (closePreviewBtn) { closePreviewBtn.onclick = () => this.closeAudioPreview(); } // Mark listeners as attached this.audioManagementListenersAttached = true; } loadAudioGallery() { const backgroundGallery = document.getElementById('background-audio-gallery'); const ambientGallery = document.getElementById('ambient-audio-gallery'); if (!backgroundGallery || !ambientGallery) { console.error('Audio gallery elements not found'); return; } // Get custom audio from storage const customAudio = this.dataManager.get('customAudio') || { background: [], ambient: [] }; // Load each category this.loadAudioCategory('background', backgroundGallery, customAudio.background); this.loadAudioCategory('ambient', ambientGallery, customAudio.ambient); // Update audio count this.updateAudioCount(); // Setup initial gallery controls for the active tab this.updateAudioGalleryControls('background'); // Refresh the music manager to include new custom tracks if (this.musicManager) { this.musicManager.refreshCustomTracks(); } // Refresh the audio manager to include new audio files if (this.audioManager) { this.audioManager.refreshAudioLibrary(); } } loadAudioCategory(category, gallery, audioFiles) { if (!audioFiles || audioFiles.length === 0) { gallery.innerHTML = `
No ${category} audio files found. Use the import button to add some!
`; return; } const audioItems = audioFiles.map(audio => { return `
${audio.title}
${audio.name}
`; }).join(''); gallery.innerHTML = audioItems; // Remove any existing event listeners to prevent duplicates gallery.removeEventListener('click', gallery._previewHandler); // Add event delegation for preview buttons to avoid path corruption in HTML attributes gallery._previewHandler = (e) => { if (e.target.classList.contains('audio-preview-btn')) { const audioPath = e.target.getAttribute('data-audio-path'); const audioTitle = e.target.getAttribute('data-audio-title'); if (audioPath && audioTitle) { console.log('Preview button clicked:', audioPath); this.previewAudio(audioPath, audioTitle); } } }; gallery.addEventListener('click', gallery._previewHandler); } updateAudioGalleryControls(activeCategory = 'background') { // This will be called when the active tab changes to update controls // for the current category console.log(`Audio gallery controls updated for ${activeCategory} category`); } updateAudioCount() { const customAudio = this.dataManager.get('customAudio') || { background: [], ambient: [] }; const backgroundCount = customAudio.background ? customAudio.background.length : 0; const ambientCount = customAudio.ambient ? customAudio.ambient.length : 0; const total = backgroundCount + ambientCount; const audioCountElement = document.querySelector('.audio-count'); if (audioCountElement) { audioCountElement.textContent = `${total} total audio files (${backgroundCount} background, ${ambientCount} ambient)`; } } selectAllAudio() { const activeGallery = document.querySelector('.audio-gallery.active'); if (activeGallery) { const audioItems = activeGallery.querySelectorAll('.audio-item'); audioItems.forEach(item => item.classList.add('selected')); } } deselectAllAudio() { const activeGallery = document.querySelector('.audio-gallery.active'); if (activeGallery) { const audioItems = activeGallery.querySelectorAll('.audio-item'); audioItems.forEach(item => item.classList.remove('selected')); } } async deleteSelectedAudio() { const activeGallery = document.querySelector('.audio-gallery.active'); if (!activeGallery) return; const selectedItems = activeGallery.querySelectorAll('.audio-item.selected'); if (selectedItems.length === 0) { this.showNotification('No audio files selected', 'warning'); return; } if (!confirm(`Are you sure you want to delete ${selectedItems.length} selected audio file(s)?`)) { return; } let deletedCount = 0; const isDesktop = window.electronAPI !== undefined; // Use Promise.all to handle async operations properly const deletionPromises = Array.from(selectedItems).map(async (item) => { const category = item.dataset.category; const filename = item.dataset.filename; if (isDesktop && this.fileManager) { // Desktop mode - delete actual file // Need to find the full audio file path from storage const customAudio = this.dataManager.get('customAudio') || { background: [], ambient: [], effects: [] }; console.log(`Looking for audio file in category ${category}:`, customAudio[category]); console.log(`Searching for filename: ${filename}`); const audioFile = customAudio[category].find(audio => (typeof audio === 'string' && audio.includes(filename)) || (typeof audio === 'object' && (audio.name === filename)) ); console.log(`Found audio file:`, audioFile); if (audioFile) { const audioPath = typeof audioFile === 'string' ? audioFile : audioFile.path; console.log(`Attempting to delete audio file: ${audioPath}`); const success = await this.fileManager.deleteAudio(audioPath, category); if (success) { console.log(`Successfully deleted: ${filename}`); return true; } else { console.error(`Failed to delete audio file: ${filename}`); return false; } } else { console.error(`Audio file not found in storage: ${filename}`); return false; } } else { // Web mode - remove from storage only this.removeAudioFromStorage(category, filename); return true; } }); // Wait for all deletions to complete try { const results = await Promise.all(deletionPromises); deletedCount = results.filter(result => result === true).length; if (deletedCount > 0) { const modeText = isDesktop ? 'file(s) deleted from disk' : 'reference(s) removed from storage'; this.showNotification(`${deletedCount} audio ${modeText}`, 'success'); this.loadAudioGallery(); // Refresh the gallery } else { this.showNotification('No audio files were deleted', 'warning'); } } catch (error) { console.error('Error during audio deletion:', error); this.showNotification('Error occurred during deletion', 'error'); } } removeAudioFromStorage(category, filename) { const customAudio = this.dataManager.get('customAudio') || { background: [], ambient: [], effects: [] }; if (customAudio[category]) { customAudio[category] = customAudio[category].filter(audio => { if (typeof audio === 'string') { return !audio.includes(filename); } else if (typeof audio === 'object') { return audio.name !== filename; } return true; }); this.dataManager.set('customAudio', customAudio); } } previewSelectedAudio() { const activeGallery = document.querySelector('.audio-gallery.active'); if (!activeGallery) return; const selectedItems = activeGallery.querySelectorAll('.audio-item.selected'); if (selectedItems.length === 0) { this.showNotification('No audio file selected for preview', 'warning'); return; } if (selectedItems.length > 1) { this.showNotification('Please select only one audio file for preview', 'warning'); return; } const selectedItem = selectedItems[0]; const category = selectedItem.dataset.category; const filename = selectedItem.dataset.filename; // Find the audio file data const customAudio = this.dataManager.get('customAudio') || { background: [], ambient: [], effects: [] }; const audioFile = customAudio[category].find(audio => audio.filename === filename); if (audioFile) { this.previewAudio(audioFile.path, audioFile.title); } } previewAudio(audioPath, title) { const previewSection = document.getElementById('audio-preview-section'); const audioPlayer = document.getElementById('audio-preview-player'); const previewFileName = document.getElementById('preview-file-name'); if (previewSection && audioPlayer && previewFileName) { previewSection.style.display = 'block'; // Convert path to proper file URL for Electron let audioSrc = audioPath; if (window.electronAPI && audioPath.match(/^[A-Za-z]:\//)) { // Absolute Windows path - convert to file:// URL audioSrc = `file:///${audioPath}`; console.log(`Preview: Converted to file URL: ${audioSrc}`); } else if (window.electronAPI && !audioPath.startsWith('file://')) { // Relative path in Electron - use as-is audioSrc = audioPath; console.log(`Preview: Using relative path: ${audioSrc}`); } audioPlayer.src = audioSrc; previewFileName.textContent = title || 'Unknown'; // Scroll to preview section previewSection.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } } closeAudioPreview() { const previewSection = document.getElementById('audio-preview-section'); const audioPlayer = document.getElementById('audio-preview-player'); if (previewSection && audioPlayer) { previewSection.style.display = 'none'; audioPlayer.pause(); audioPlayer.src = ''; } } toggleAudioEnabled(category, filename, enabled) { const customAudio = this.dataManager.get('customAudio') || { background: [], ambient: [], effects: [] }; if (customAudio[category]) { const audioFile = customAudio[category].find(audio => audio.name === filename); if (audioFile) { audioFile.enabled = enabled; this.dataManager.set('customAudio', customAudio); // Update the visual status this.loadAudioGallery(); this.showNotification(`Audio ${enabled ? 'enabled' : 'disabled'}`, 'success'); } } } toggleAudioSelection(audioItem) { audioItem.classList.toggle('selected'); } showAudioStorageInfo() { try { const isDesktop = window.electronAPI !== undefined; const customAudio = this.dataManager.get('customAudio') || { background: [], ambient: [], effects: [] }; const backgroundCount = customAudio.background ? customAudio.background.length : 0; const ambientCount = customAudio.ambient ? customAudio.ambient.length : 0; const effectsCount = customAudio.effects ? customAudio.effects.length : 0; const totalCustomAudio = backgroundCount + ambientCount + effectsCount; let message = `📊 Audio Storage Information\n\n`; if (isDesktop) { message += `🖥️ Desktop Mode - Unlimited Storage\n`; message += `Files stored in native file system\n\n`; message += `📁 Audio Categories:\n`; message += `🎵 Background Music: ${backgroundCount} files\n`; message += `🌿 Ambient Sounds: ${ambientCount} files\n`; message += `🔊 Sound Effects: ${effectsCount} files\n`; message += `📊 Total Custom Audio: ${totalCustomAudio} files\n\n`; message += `💾 Storage: Uses native file system\n`; message += `📂 Location: audio/ folder in app directory\n`; message += `🔄 Auto-scanned on startup`; } else { message += `🌐 Web Mode - Browser Storage\n`; message += `Limited by browser storage quotas\n\n`; message += `📁 Audio References:\n`; message += `🎵 Background Music: ${backgroundCount} files\n`; message += `🌿 Ambient Sounds: ${ambientCount} files\n`; message += `🔊 Sound Effects: ${effectsCount} files\n`; message += `📊 Total Custom Audio: ${totalCustomAudio} files\n\n`; message += `💾 Storage: Browser localStorage\n`; message += `⚠️ Subject to browser storage limits`; } alert(message); } catch (error) { console.error('Error showing audio storage info:', error); alert('Error retrieving audio storage information.'); } } async scanAudioDirectories() { try { const isDesktop = window.electronAPI !== undefined; if (!isDesktop) { alert('🔍 Directory Scanning\n\nDirectory scanning is only available in desktop mode.\nTo use this feature, run the app with: npm start\n\nFor web mode, please use the Import buttons to add audio files manually.'); return; } // Show loading message if (this.flashMessageManager) { this.flashMessageManager.show('🔍 Scanning audio directories...', 'info'); } console.log('🔍 Starting audio directory scan...'); // Call the AudioManager's scan method if (this.audioManager) { const scannedFiles = await this.audioManager.scanAudioDirectories(); if (scannedFiles.length > 0) { // Refresh the audio gallery to show new files this.loadAudioGallery(); console.log(`✅ Directory scan completed - found ${scannedFiles.length} files`); alert(`🔍 Directory Scan Complete\n\nFound ${scannedFiles.length} audio files in the following directories:\n• audio/background\n• audio/ambient\n\nFiles have been added to your audio library and are ready to use!`); } else { console.log('ℹ️ Directory scan completed - no new files found'); alert('🔍 Directory Scan Complete\n\nNo new audio files were found in the audio directories.\n\nTo add audio files:\n1. Place them in the audio/ folder subdirectories\n2. Use the Import buttons to add files manually\n3. Restart the app to auto-scan'); } } else { console.error('AudioManager not available for directory scanning'); alert('❌ Error: Audio system not initialized.\nPlease try again or restart the app.'); } } catch (error) { console.error('Error during audio directory scan:', error); if (this.flashMessageManager) { this.flashMessageManager.show('❌ Directory scan failed', 'error'); } alert(`❌ Directory Scan Failed\n\nError: ${error.message}\n\nPlease check the console for more details or try restarting the app.`); } } showNotification(message, type = 'info') { // Create notification element if it doesn't exist let notification = document.getElementById('notification'); if (!notification) { notification = document.createElement('div'); notification.id = 'notification'; notification.className = 'notification'; document.body.appendChild(notification); } notification.textContent = message; notification.className = `notification ${type} show`; // Hide after 3 seconds setTimeout(() => { notification.classList.remove('show'); }, 3000); } startGame() { if (!this.imageDiscoveryComplete) { console.log('Image discovery not complete, retrying in 500ms...'); // Check if we've been waiting too long and force completion if (!this.startGameAttempts) this.startGameAttempts = 0; this.startGameAttempts++; if (this.startGameAttempts > 20) { // After 10 seconds, force start console.log('🚨 Forcing image discovery completion after timeout'); // Try to get images from desktop file manager one more time if (this.fileManager && this.fileManager.isElectron) { const customImages = this.dataManager.get('customImages') || { task: [], consequence: [] }; let taskImages = []; let consequenceImages = []; if (Array.isArray(customImages)) { taskImages = customImages; } else { taskImages = customImages.task || []; consequenceImages = customImages.consequence || []; } if (taskImages.length > 0 || consequenceImages.length > 0) { gameData.discoveredTaskImages = taskImages.map(img => typeof img === 'string' ? img : img.name); gameData.discoveredConsequenceImages = consequenceImages.map(img => typeof img === 'string' ? img : img.name); console.log(`🎯 Forced completion - Task images: ${gameData.discoveredTaskImages.length}, Consequence images: ${gameData.discoveredConsequenceImages.length}`); } else { gameData.discoveredTaskImages = []; gameData.discoveredConsequenceImages = []; console.log('🎯 Forced completion - No images found, using empty arrays'); } } else { gameData.discoveredTaskImages = []; gameData.discoveredConsequenceImages = []; console.log('🎯 Forced completion - Web mode, using empty arrays'); } this.imageDiscoveryComplete = true; this.startGameAttempts = 0; // Reset counter // Don't return, continue with game start } else { setTimeout(() => this.startGame(), 500); return; } } // Reset attempt counter on successful start this.startGameAttempts = 0; // Check if we have any images available const totalImages = gameData.discoveredTaskImages.length + gameData.discoveredConsequenceImages.length; const customImages = this.dataManager.get('customImages') || { task: [], consequence: [] }; // Handle both old array format and new object format for backward compatibility let customImageCount = 0; if (Array.isArray(customImages)) { customImageCount = customImages.length; } else { customImageCount = (customImages.task || []).length + (customImages.consequence || []).length; } if (totalImages === 0 && customImageCount === 0) { // No images available - guide user to add images this.showNotification('No images found! Please upload images or scan directories first.', 'error', 5000); this.showImageManagement(); // Use the proper method that sets up event listeners return; } // Get selected game mode const selectedMode = document.querySelector('input[name="gameMode"]:checked')?.value || 'standard'; if (window.gameModeManager) { window.gameModeManager.currentMode = selectedMode; // Map the new mode names to the original game engine modes this.gameState.gameMode = window.gameModeManager.getGameModeForEngine(); } else { // Fallback if gameModeManager is not available this.gameState.gameMode = selectedMode === 'timed' ? 'timed' : selectedMode === 'scored' ? 'xp-target' : 'complete-all'; } this.gameState.isRunning = true; this.gameState.isPaused = false; this.gameState.startTime = Date.now(); this.gameState.sessionStartTime = Date.now(); // Track session start for logging this.gameState.totalPausedTime = 0; this.gameState.xp = 0; // Initialize session XP this.gameState.taskCompletionXp = 0; // Initialize task completion XP tracking this.gameState.lastSkippedTask = null; // Initialize scenario XP system if in scenario mode if (window.gameModeManager && window.gameModeManager.isScenarioMode()) { this.initializeScenarioXp(); } // Re-enable audio for new game if (this.audioManager && this.audioManager.enableAudio) { this.audioManager.enableAudio(); // Start continuous background audio playlist this.audioManager.startBackgroundPlaylist(); } // Load focus interruption setting const savedInterruptionChance = this.dataManager.get('focusInterruptionChance') || 0; this.gameState.focusInterruptionChance = savedInterruptionChance; // Record game start in data manager this.dataManager.recordGameStart(); this.startTimer(); this.loadNextTask(); this.showScreen('game-screen'); this.flashMessageManager.start(); this.flashMessageManager.triggerEventMessage('gameStart'); const periodicSettings = this.dataManager.get('periodicPopupSettings') || { enabled: true }; if (this.popupImageManager && this.popupImageManager.startPeriodicPopups && periodicSettings.enabled) { this.popupImageManager.startPeriodicPopups(); } this.updateStats(); } loadNextTask() { if (this.gameState.isConsequenceTask) { // Load a consequence task this.loadConsequenceTask(); } else { // Load a main task this.loadMainTask(); } // Only display task if one was successfully loaded if (this.gameState.currentTask) { this.displayCurrentTask(); } } loadMainTask() { // Get tasks from the game mode manager let tasksToUse = gameData.mainTasks; if (window.gameModeManager && window.gameModeManager.isScenarioMode()) { tasksToUse = window.gameModeManager.getTasksForMode(); } else { // For standard, timed, and scored modes, exclude interactive tasks const currentMode = window.gameModeManager ? window.gameModeManager.currentMode : 'standard'; if (currentMode === 'standard' || currentMode === 'timed' || currentMode === 'scored') { tasksToUse = gameData.mainTasks.filter(task => !task.interactiveType); } } const availableTasks = tasksToUse.filter( task => !this.gameState.usedMainTasks.includes(task.id) ); // Filter interactive tasks to only show scenario-adventure types const interactiveTasksForSelection = availableTasks.filter(t => t.interactiveType === 'scenario-adventure' || t.interactiveType === 'mirror-task' ); if (interactiveTasksForSelection.length === 0 && window.gameModeManager?.isScenarioMode()) { // All main tasks completed if (this.gameState.gameMode === 'complete-all') { // Only end game in complete-all mode this.endGame('complete-all'); return; } else if (window.gameModeManager && window.gameModeManager.isScenarioMode()) { // In scenario modes, if no scenarios are available, show error and return to menu console.error('❌ No scenario tasks available - returning to start screen'); alert('No scenarios could be loaded for this mode. Please check the console for details.'); this.showScreen('start-screen'); return; // Don't continue to displayCurrentTask } else { // In timed and xp-target modes, reset used tasks and continue console.log(`All tasks completed in ${this.gameState.gameMode} mode, cycling through tasks again`); this.gameState.usedMainTasks = []; // Recursively call loadMainTask to select from reset pool this.loadMainTask(); return; } } // For scenario modes, use the filtered interactive tasks, otherwise use all available tasks const tasksForSelection = (window.gameModeManager?.isScenarioMode()) ? interactiveTasksForSelection : availableTasks; if (tasksForSelection.length === 0) { // Handle empty task list if (this.gameState.gameMode === 'complete-all') { this.endGame('complete-all'); return; } else { console.log(`All tasks completed in ${this.gameState.gameMode} mode, cycling through tasks again`); this.gameState.usedMainTasks = []; this.loadMainTask(); return; } } // Select random task and random image from task pool const randomIndex = Math.floor(Math.random() * tasksForSelection.length); const selectedTask = tasksForSelection[randomIndex]; this.gameState.currentTask = { ...selectedTask, image: this.getRandomImage(false) // false = main task image }; this.gameState.isConsequenceTask = false; } loadConsequenceTask() { const availableTasks = gameData.consequenceTasks.filter( task => !this.gameState.usedConsequenceTasks.includes(task.id) ); if (availableTasks.length === 0) { // Reset consequence tasks if all used this.gameState.usedConsequenceTasks = []; } const tasksToChooseFrom = availableTasks.length > 0 ? availableTasks : gameData.consequenceTasks; const randomIndex = Math.floor(Math.random() * tasksToChooseFrom.length); this.gameState.currentTask = { ...tasksToChooseFrom[randomIndex], image: this.getRandomImage(true) // true = consequence task image }; this.gameState.isConsequenceTask = true; } getRandomImage(isConsequence = false) { let imagePool; let imageType; let usedImagesArray; if (isConsequence) { imagePool = gameData.discoveredConsequenceImages; imageType = 'consequence'; usedImagesArray = this.gameState.usedConsequenceImages; } else { imagePool = gameData.discoveredTaskImages; imageType = 'task'; usedImagesArray = this.gameState.usedTaskImages; } // Add custom images to the pool const customImages = this.dataManager.get('customImages') || { task: [], consequence: [] }; // Handle both old array format and new object format let customImagePool = []; if (Array.isArray(customImages)) { // Old format - treat all as task images customImagePool = isConsequence ? [] : customImages; } else { // New format - get appropriate category customImagePool = customImages[imageType] || []; } imagePool = [...imagePool, ...customImagePool]; // Filter out disabled images - need to handle both formats const disabledImages = this.dataManager.get('disabledImages') || []; imagePool = imagePool.filter(img => { // Handle both string paths and metadata objects const imagePath = typeof img === 'string' ? img : (img.cachedPath || img.originalName); return !disabledImages.includes(imagePath); }); if (imagePool.length === 0) { console.log(`No enabled ${imageType} images found, using placeholder`); return this.createPlaceholderImage(isConsequence ? 'Consequence Image' : 'Task Image'); } // Get image identifiers for tracking const getImageId = (img) => { return typeof img === 'string' ? img : (img.cachedPath || img.originalName); }; // Filter out images that have already been used let availableImages = imagePool.filter(img => { const imageId = getImageId(img); return !usedImagesArray.includes(imageId); }); // If all images have been used, reset the used array and use all images again if (availableImages.length === 0) { console.log(`All ${imageType} images have been shown, resetting for repeat cycle`); usedImagesArray.length = 0; // Clear the used images array availableImages = imagePool; // Use all available images again } // Select a random image from available images const randomIndex = Math.floor(Math.random() * availableImages.length); const selectedImage = availableImages[randomIndex]; const selectedImageId = getImageId(selectedImage); // Mark this image as used usedImagesArray.push(selectedImageId); // Convert to displayable format const displayImage = this.getImageSrc(selectedImage); console.log(`Selected ${imageType} image: ${typeof selectedImage === 'string' ? selectedImage : selectedImage.originalName} (${usedImagesArray.length}/${imagePool.length} used)`); return displayImage; } getImageSrc(imageData) { // Handle both old base64/path format and new cached metadata format if (typeof imageData === 'string') { return imageData; // Old format: base64 or file path } else { return imageData.dataUrl || imageData.cachedPath; // New format: use data URL or cached path } } displayCurrentTask() { const taskText = document.getElementById('task-text'); const taskImage = document.getElementById('task-image'); const taskImageContainer = document.querySelector('.task-image-container'); const taskContainer = document.querySelector('.task-container'); const taskTypeIndicator = document.getElementById('task-type-indicator'); const mercySkipBtn = document.getElementById('mercy-skip-btn'); const skipBtn = document.getElementById('skip-btn'); // Note: task-difficulty and task-points elements were removed during XP conversion // Note: task-type-indicator element may not exist in current HTML // Check if this is a scenario mode const isScenarioMode = window.gameModeManager && window.gameModeManager.isScenarioMode(); // Apply or remove scenario mode styling if (taskContainer) { if (isScenarioMode) { taskContainer.classList.add('scenario-mode'); } else { taskContainer.classList.remove('scenario-mode'); } } // Update skip button text for scenario modes if (isScenarioMode) { skipBtn.textContent = 'Give Up'; skipBtn.className = 'btn btn-danger'; // Change to red for more serious action } else { skipBtn.textContent = 'Skip'; skipBtn.className = 'btn btn-warning'; // Default yellow } taskText.textContent = this.gameState.currentTask.text; // Hide task image for scenario games to prevent scrolling issues if (isScenarioMode) { if (taskImageContainer) { taskImageContainer.style.display = 'none'; } else if (taskImage) { taskImage.style.display = 'none'; } } else { // Show image for regular games if (taskImageContainer) { taskImageContainer.style.display = 'block'; } else if (taskImage) { taskImage.style.display = 'block'; } } // Check if this is an interactive task if (this.interactiveTaskManager.isInteractiveTask(this.gameState.currentTask)) { this.interactiveTaskManager.displayInteractiveTask(this.gameState.currentTask); return; // Interactive task manager handles the rest } // Set image with error handling (only for non-scenario modes) if (!isScenarioMode) { taskImage.src = this.gameState.currentTask.image; taskImage.onerror = () => { console.log('Image failed to load:', this.gameState.currentTask.image); taskImage.src = this.createPlaceholderImage(); }; } // Play task audio based on type if (this.gameState.isConsequenceTask) { // Playlist mode - no need to stop/start audio during task transitions // The continuous playlist will keep playing throughout the game // Clear the skip audio flag for next time if (this.gameState.skipAudioPlayed) { this.gameState.skipAudioPlayed = false; console.log('Cleared skip audio flag - playlist continues playing'); } } else { // Playlist mode - no need to stop/start audio during task transitions // The continuous playlist will keep playing throughout the game // Playlist mode - continuous audio, no need for task-specific audio calls } // Update task type indicator and button visibility if (this.gameState.isConsequenceTask) { if (taskTypeIndicator) { taskTypeIndicator.textContent = 'CONSEQUENCE TASK'; taskTypeIndicator.classList.add('consequence'); } // Hide regular skip button for consequence tasks skipBtn.style.display = 'none'; // Show mercy skip button for consequence tasks const originalTask = this.findOriginalSkippedTask(); if (originalTask) { // In XP system, mercy skip costs 5 XP (flat rate as per ROADMAP) const mercyCost = 5; const canAfford = this.gameState.xp >= mercyCost; if (canAfford) { mercySkipBtn.style.display = 'block'; document.getElementById('mercy-skip-cost').textContent = `-${mercyCost} XP`; mercySkipBtn.disabled = false; } else { mercySkipBtn.style.display = 'block'; document.getElementById('mercy-skip-cost').textContent = `-${mercyCost} XP (Not enough!)`; mercySkipBtn.disabled = true; } } else { mercySkipBtn.style.display = 'none'; } } else { if (taskTypeIndicator) { taskTypeIndicator.textContent = 'MAIN TASK'; taskTypeIndicator.classList.remove('consequence'); } // Show regular skip button for main tasks skipBtn.style.display = 'block'; // Hide mercy skip button for main tasks mercySkipBtn.style.display = 'none'; // Note: In XP system, no need to display difficulty/points separately // The XP is awarded based on time and activities, not task difficulty } } findOriginalSkippedTask() { return this.gameState.lastSkippedTask; } mercySkip() { if (!this.gameState.isRunning || this.gameState.isPaused) return; if (!this.gameState.isConsequenceTask) return; const originalTask = this.findOriginalSkippedTask(); if (!originalTask) return; // In XP system, mercy skip costs 5 XP (flat rate as per ROADMAP) const mercyCost = 5; if (this.gameState.xp < mercyCost) { alert(`Not enough XP! You need ${mercyCost} XP but only have ${this.gameState.xp}.`); return; } // Confirm the mercy skip const confirmed = confirm( `Use Mercy Skip?\n\n` + `This will cost you ${mercyCost} XP from your total.\n` + `Your XP will go from ${this.gameState.xp} to ${this.gameState.xp - mercyCost}.\n\n` + `Are you sure you want to skip this consequence task?` ); if (!confirmed) return; // Deduct XP and skip the consequence task this.gameState.xp -= mercyCost; // Clear the last skipped task and load next main task this.gameState.lastSkippedTask = null; this.gameState.isConsequenceTask = false; this.loadNextTask(); this.updateStats(); // Show feedback alert(`Mercy Skip used! ${mercyCost} points deducted from your score.`); } getPointsForDifficulty(difficulty) { switch(difficulty) { case 'Easy': return 1; case 'Medium': return 3; case 'Hard': return 5; default: return 3; } } getDifficultyEmoji(difficulty) { switch(difficulty) { case 'Easy': return '🟢'; case 'Medium': return '🟡'; case 'Hard': return '🔴'; default: return '🟡'; } } checkStreakBonus() { // Award streak bonus every 10 completed tasks const streakMilestone = Math.floor(this.gameState.currentStreak / 10); if (streakMilestone > this.gameState.lastStreakMilestone) { this.gameState.lastStreakMilestone = streakMilestone; return 1; // Return 1 to indicate streak bonus earned (5 XP will be awarded) } return 0; // No streak bonus } showStreakBonusNotification(streak, bonusPoints) { // Create streak bonus notification const notification = document.createElement('div'); notification.className = 'streak-bonus-notification'; notification.innerHTML = `
🔥
${streak} Task Streak!
+${bonusPoints} Bonus Points
`; document.body.appendChild(notification); // Animate in setTimeout(() => notification.classList.add('show'), 10); // Remove after 3 seconds setTimeout(() => { notification.classList.remove('show'); setTimeout(() => document.body.removeChild(notification), 300); }, 3000); } completeTask() { if (!this.gameState.isRunning || this.gameState.isPaused) return; // Stop current task audio immediately (no fade to prevent overlap) console.log('Task completed - stopping all audio immediately'); this.audioManager.stopCategory('background', 0); // Playlist mode - continuous background audio, no need for task completion audio calls // Mark task as used and award points if (this.gameState.isConsequenceTask) { this.gameState.usedConsequenceTasks.push(this.gameState.currentTask.id); this.gameState.consequenceCount++; // Clear the last skipped task when consequence is completed this.gameState.lastSkippedTask = null; // Consequence tasks don't award points or affect streak } else { this.gameState.usedMainTasks.push(this.gameState.currentTask.id); this.gameState.completedCount++; // Check if this is a scenario mode const isScenarioMode = window.gameModeManager && window.gameModeManager.isScenarioMode(); if (!isScenarioMode) { // Regular game mode - use standard scoring // Increment streak for regular tasks this.gameState.currentStreak++; // Award XP for completing main tasks (tracked separately from time/activity XP) const taskCompletionXp = 1; // 1 XP per completed task (as per ROADMAP) this.gameState.taskCompletionXp = (this.gameState.taskCompletionXp || 0) + taskCompletionXp; console.log(`⭐ Task completed! +${taskCompletionXp} XP (Total task XP: ${this.gameState.taskCompletionXp})`); // Check for streak bonus (every 10 consecutive completed tasks) const streakBonus = this.checkStreakBonus(); if (streakBonus > 0) { // 5 XP for streak of 10 completed tasks (as per ROADMAP) const streakXp = 5; // Fixed 5 XP per 10-task streak this.gameState.taskCompletionXp += streakXp; this.gameState.totalStreakBonuses += streakXp; this.showStreakBonusNotification(this.gameState.currentStreak, streakXp); console.log(`🔥 Streak bonus! +${streakXp} XP for ${this.gameState.currentStreak} streak`); // Trigger special streak message this.flashMessageManager.triggerEventMessage('streak'); } else { // Trigger regular completion message this.flashMessageManager.triggerEventMessage('taskComplete'); } } else { // Scenario mode - use new scenario XP system this.awardScenarioStepXp(); // Awards 5 XP per step to scenario system // Scenarios don't use streaks - each step is a meaningful progression // Trigger scenario progress message this.flashMessageManager.triggerEventMessage('taskComplete'); } // Check if XP target is reached for xp-target mode if (this.gameState.gameMode === 'xp-target' && this.gameState.xp >= this.gameState.xpTarget) { console.log(`⭐ XP target of ${this.gameState.xpTarget} reached! Current XP: ${this.gameState.xp}`); this.updateStats(); // Update stats before ending game this.endGame('target-reached'); return; } // In scenario modes, only end game when reaching an ending step if (window.gameModeManager && window.gameModeManager.isScenarioMode() && this.gameState.currentTask && this.gameState.currentTask.isScenario) { // Check if current scenario step is an ending const task = this.gameState.currentTask; if (task.scenarioState && task.scenarioState.completed) { console.log('🎭 Scenario ending reached - completing game'); this.updateStats(); // Update stats before ending game this.endGame('scenario-complete'); return; } else { console.log('🎭 Scenario step completed - continuing to next step'); // Don't end game, let interactive task manager handle step progression } } } // Load next main task (consequence tasks don't chain) this.gameState.isConsequenceTask = false; this.loadNextTask(); this.updateStats(); } skipTask() { if (!this.gameState.isRunning || this.gameState.isPaused) return; // In scenario modes, "Give Up" quits the entire game if (window.gameModeManager && window.gameModeManager.isScenarioMode()) { console.log('🏃 Give up in scenario mode - quitting game'); this.endGame('scenario-quit'); return; } if (this.gameState.isConsequenceTask) { // Can't skip consequence tasks - they must be completed alert("Consequence tasks cannot be skipped!"); return; } // Stop current task audio immediately (no fade to prevent overlap) console.log('Task skipped - stopping task audio immediately'); this.audioManager.stopCategory('background', 0); // Play immediate punishment audio // Playlist mode - continuous background audio, no need for skip audio calls // Store the skipped task for mercy cost calculation this.gameState.lastSkippedTask = this.gameState.currentTask; // Mark main task as used and increment skip count this.gameState.usedMainTasks.push(this.gameState.currentTask.id); this.gameState.skippedCount++; // Reset streak when task is skipped this.gameState.currentStreak = 0; this.gameState.lastStreakMilestone = 0; // Trigger skip message this.flashMessageManager.triggerEventMessage('taskSkip'); // Trigger punishment popups for skipping this.popupImageManager.triggerPunishmentPopups(); // Load a consequence task this.gameState.isConsequenceTask = true; this.gameState.skipAudioPlayed = true; // Flag to prevent duplicate audio // Playlist mode - no need to re-enable audio, playlist continues playing this.loadNextTask(); this.updateStats(); } pauseGame() { if (!this.gameState.isRunning) return; this.gameState.isPaused = true; this.gameState.pausedTime = Date.now(); this.stopTimer(); this.musicManager.pause(); this.flashMessageManager.pause(); // Pause playlist instead of stopping all audio this.audioManager.pausePlaylist(); // Stop any ongoing TTS narration when pausing if (this.interactiveTaskManager && this.interactiveTaskManager.stopTTS) { this.interactiveTaskManager.stopTTS(); } this.showScreen('paused-screen'); // Auto-save the paused game state this.autoSaveGameState(); } resumeGame() { if (!this.gameState.isRunning || !this.gameState.isPaused) return; this.gameState.totalPausedTime += Date.now() - this.gameState.pausedTime; this.gameState.isPaused = false; this.startTimer(); this.musicManager.resume(); this.flashMessageManager.resume(); // Resume playlist this.audioManager.resumePlaylist(); this.showScreen('game-screen'); } quitGame() { this.endGame('quit'); } endGame(reason = 'complete-all') { this.gameState.isRunning = false; this.stopTimer(); this.flashMessageManager.stop(); // Handle XP calculation - use scenario XP for scenarios, task XP for main game let sessionXp; if (window.gameModeManager && window.gameModeManager.isScenarioMode()) { // For scenarios, use the total scenario XP sessionXp = this.gameState.scenarioXp ? this.gameState.scenarioXp.total : 0; } else { // For main game, use task completion XP sessionXp = this.gameState.taskCompletionXp || 0; } const currentTime = Date.now(); const sessionDuration = this.gameState.sessionStartTime ? (currentTime - this.gameState.sessionStartTime) / 60000 : 0; // minutes // Check if session was quit early const wasQuit = reason === 'quit' || reason === 'scenario-quit'; if (wasQuit) { const sessionType = window.gameModeManager && window.gameModeManager.isScenarioMode() ? 'Scenario' : 'Main game'; console.log(`❌ ${sessionType} quit early (${sessionDuration.toFixed(1)} minutes) - Session XP: ${sessionXp}, but not added to overall total`); this.gameState.xp = sessionXp; // Show session XP but don't add to overall } else { // Award XP for completed session - add to overall XP counter for rankings/leveling this.gameState.xp = sessionXp; const overallXp = this.dataManager.get('overallXp') || 0; const newOverallXp = overallXp + sessionXp; this.dataManager.set('overallXp', newOverallXp); // Update overall XP display on home screen this.updateOverallXpDisplay(); const sessionType = window.gameModeManager && window.gameModeManager.isScenarioMode() ? 'Scenario' : 'Main game'; console.log(`⭐ ${sessionType} completed! Session XP: ${sessionXp}, Overall XP: ${newOverallXp} (${sessionDuration.toFixed(1)} minutes)`); } // Stop periodic popup system if (this.popupImageManager && this.popupImageManager.stopPeriodicPopups) { this.popupImageManager.stopPeriodicPopups(); } // Stop all audio immediately when game ends - multiple attempts to ensure it stops this.audioManager.stopAllImmediate(); // Stop any ongoing TTS narration if (this.interactiveTaskManager && this.interactiveTaskManager.stopTTS) { this.interactiveTaskManager.stopTTS(); } // Additional safeguard - stop all audio again after a brief delay setTimeout(() => { this.audioManager.stopAllImmediate(); // Double-check TTS is stopped if (this.interactiveTaskManager && this.interactiveTaskManager.stopTTS) { this.interactiveTaskManager.stopTTS(); } }, 100); // Show photo gallery if any photos were taken during the game this.showGamePhotoGallery(() => { this.showFinalStats(reason); this.showScreen('game-over-screen'); }); } /** * Show photo gallery with all photos taken during the game session */ showGamePhotoGallery(onComplete) { // Get all photos from webcam manager const allPhotos = this.webcamManager.capturedPhotos || []; if (allPhotos.length === 0) { // No photos taken, proceed directly to final stats onComplete(); return; } console.log(`📸 Showing game photo gallery with ${allPhotos.length} photos`); const gallery = document.createElement('div'); gallery.id = 'game-photo-gallery'; gallery.innerHTML = ` `; document.body.appendChild(gallery); this.addGameGalleryStyles(); // Bind continue button document.getElementById('continue-to-stats').addEventListener('click', () => { gallery.remove(); onComplete(); }); // Make photo viewer available globally window.showGamePhoto = (index) => { this.showGameFullPhoto(index, allPhotos); }; } /** * Show full-size photo viewer for game photos */ showGameFullPhoto(index, photos) { if (!photos || !photos[index]) return; const photo = photos[index]; const viewer = document.createElement('div'); viewer.id = 'game-photo-viewer'; viewer.innerHTML = `

Photo ${index + 1} of ${photos.length}

${photo.sessionType || 'Photography Session'}
Game Photo ${index + 1}
${index > 0 ? `` : '
'} ${index < photos.length - 1 ? `` : '
'}
`; document.body.appendChild(viewer); } /** * Add styles for game photo gallery */ addGameGalleryStyles() { if (document.getElementById('game-gallery-styles')) return; const styles = document.createElement('style'); styles.id = 'game-gallery-styles'; styles.textContent = ` #game-photo-gallery .game-gallery-overlay { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0, 0, 0, 0.95); display: flex; align-items: center; justify-content: center; z-index: 11000; } #game-photo-gallery .game-gallery-container { background: linear-gradient(135deg, #2a2a2a, #3a3a3a); border-radius: 15px; padding: 40px; max-width: 95vw; max-height: 95vh; overflow-y: auto; color: white; box-shadow: 0 10px 30px rgba(0,0,0,0.5); } #game-photo-gallery .game-gallery-header { text-align: center; margin-bottom: 30px; } #game-photo-gallery .game-gallery-header h3 { color: #ff6b6b; font-size: 28px; margin-bottom: 10px; } #game-photo-gallery .game-gallery-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 20px; margin-bottom: 40px; } #game-photo-gallery .game-gallery-item { position: relative; cursor: pointer; border-radius: 12px; overflow: hidden; transition: all 0.3s ease; border: 3px solid #444; background: #333; } #game-photo-gallery .game-gallery-item:hover { transform: scale(1.08); box-shadow: 0 8px 25px rgba(255, 107, 107, 0.4); border-color: #ff6b6b; } #game-photo-gallery .game-gallery-item img { width: 100%; height: 180px; object-fit: cover; } #game-photo-gallery .game-photo-number { position: absolute; top: 8px; right: 8px; background: linear-gradient(45deg, #ff6b6b, #ff8e53); color: white; border-radius: 50%; width: 30px; height: 30px; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; box-shadow: 0 2px 5px rgba(0,0,0,0.3); } #game-photo-gallery .game-photo-session { position: absolute; bottom: 0; left: 0; right: 0; background: linear-gradient(transparent, rgba(0,0,0,0.8)); color: white; padding: 8px; font-size: 12px; text-align: center; } #game-photo-gallery .game-gallery-controls { text-align: center; margin: 30px 0; } #game-photo-gallery .game-gallery-continue { background: linear-gradient(45deg, #2ed573, #17a2b8); color: white; border: none; padding: 18px 36px; border-radius: 8px; font-size: 18px; font-weight: bold; cursor: pointer; transition: all 0.3s; box-shadow: 0 4px 15px rgba(46, 213, 115, 0.3); } #game-photo-gallery .game-gallery-continue:hover { transform: translateY(-3px); box-shadow: 0 6px 20px rgba(46, 213, 115, 0.4); } #game-photo-gallery .game-gallery-note { text-align: center; color: #aaa; font-size: 14px; border-top: 1px solid #444; padding-top: 20px; } /* Game Photo Viewer Styles */ #game-photo-viewer .game-photo-viewer-overlay { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0, 0, 0, 0.98); display: flex; align-items: center; justify-content: center; z-index: 11001; } #game-photo-viewer .game-photo-viewer-container { background: linear-gradient(135deg, #2a2a2a, #3a3a3a); border-radius: 15px; max-width: 90vw; max-height: 90vh; color: white; overflow: hidden; box-shadow: 0 10px 30px rgba(0,0,0,0.5); } #game-photo-viewer .game-photo-viewer-header { padding: 20px 25px; background: linear-gradient(45deg, #ff6b6b, #ff8e53); display: flex; justify-content: space-between; align-items: center; } #game-photo-viewer .photo-session-info { font-size: 14px; opacity: 0.9; } #game-photo-viewer .game-photo-viewer-close { background: none; border: none; color: white; font-size: 28px; cursor: pointer; padding: 0; width: 35px; height: 35px; display: flex; align-items: center; justify-content: center; border-radius: 50%; transition: background 0.3s; } #game-photo-viewer .game-photo-viewer-close:hover { background: rgba(255,255,255,0.2); } #game-photo-viewer .game-photo-viewer-content { padding: 30px; text-align: center; background: #2a2a2a; } #game-photo-viewer .game-photo-viewer-content img { max-width: 100%; max-height: 70vh; border-radius: 8px; box-shadow: 0 4px 15px rgba(0,0,0,0.3); } #game-photo-viewer .game-photo-viewer-nav { padding: 20px 25px; background: #3a3a3a; display: flex; justify-content: space-between; align-items: center; } #game-photo-viewer .game-photo-viewer-nav button { background: linear-gradient(45deg, #ff6b6b, #ff8e53); color: white; border: none; padding: 12px 24px; border-radius: 6px; cursor: pointer; transition: all 0.3s; font-weight: bold; } #game-photo-viewer .game-photo-viewer-nav button:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(255, 107, 107, 0.4); } `; document.head.appendChild(styles); } resetGame() { this.gameState = { isRunning: false, isPaused: false, currentTask: null, isConsequenceTask: false, startTime: null, pausedTime: 0, totalPausedTime: 0, completedCount: 0, skippedCount: 0, consequenceCount: 0, score: 0, lastSkippedTask: null, usedMainTasks: [], usedConsequenceTasks: [], usedTaskImages: [], usedConsequenceImages: [], gameMode: 'complete-all', timeLimit: 300, xpTarget: 100, currentStreak: 0, totalStreakBonuses: 0, lastStreakMilestone: 0 }; this.stopTimer(); this.showScreen('start-screen'); } startTimer() { this.timerInterval = setInterval(() => { this.updateTimer(); }, 1000); } stopTimer() { if (this.timerInterval) { clearInterval(this.timerInterval); this.timerInterval = null; } } updateTimer() { if (!this.gameState.isRunning || this.gameState.isPaused) return; const currentTime = Date.now(); const elapsed = currentTime - this.gameState.startTime - this.gameState.totalPausedTime; // Update XP every second this.updateXp(); // Update scenario XP if in scenario mode if (window.gameModeManager && window.gameModeManager.isScenarioMode()) { this.updateScenarioTimeBasedXp(); this.updateScenarioFocusXp(); this.updateScenarioWebcamXp(); } let formattedTime; if (this.gameState.gameMode === 'timed') { // Countdown timer for timed mode const remainingMs = (this.gameState.timeLimit * 1000) - elapsed; if (remainingMs <= 0) { // Time's up! formattedTime = '00:00'; document.getElementById('timer').textContent = formattedTime; this.endGame('time'); return; } formattedTime = this.formatTime(remainingMs); // Change color when time is running low (less than 30 seconds) const timerElement = document.getElementById('timer'); if (remainingMs <= 30000) { timerElement.style.color = '#ff4757'; timerElement.style.fontWeight = 'bold'; } else if (remainingMs <= 60000) { timerElement.style.color = '#ffa502'; timerElement.style.fontWeight = 'bold'; } else { timerElement.style.color = ''; timerElement.style.fontWeight = ''; } } else { // Normal elapsed timer for other modes formattedTime = this.formatTime(elapsed); } document.getElementById('timer').textContent = formattedTime; // Update timer status const timerStatus = document.getElementById('timer-status'); if (this.gameState.isPaused) { timerStatus.textContent = '(PAUSED)'; } else if (this.gameState.gameMode === 'timed') { timerStatus.textContent = '(TIME LEFT)'; } else { timerStatus.textContent = ''; } } formatTime(milliseconds) { const totalSeconds = Math.floor(milliseconds / 1000); const minutes = Math.floor(totalSeconds / 60); const seconds = totalSeconds % 60; return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; } updateStats() { document.getElementById('completed-count').textContent = this.gameState.completedCount; document.getElementById('skipped-count').textContent = this.gameState.skippedCount; document.getElementById('consequence-count').textContent = this.gameState.consequenceCount; document.getElementById('xp').textContent = this.gameState.xp || 0; // Update streak display const streakElement = document.getElementById('current-streak'); if (streakElement) { streakElement.textContent = this.gameState.currentStreak; // Add visual indicator for streak milestones const streakContainer = streakElement.parentElement; if (this.gameState.currentStreak >= 10) { streakContainer.classList.add('streak-milestone'); } else { streakContainer.classList.remove('streak-milestone'); } } } updateOverallXpDisplay() { // Update overall XP display on home screen header const overallXpElement = document.getElementById('overall-xp-header'); if (overallXpElement) { const overallXp = this.dataManager.get('overallXp') || 0; overallXpElement.textContent = overallXp; } } showFinalStats(reason = 'complete-all') { const currentTime = Date.now(); const totalTime = currentTime - this.gameState.startTime - this.gameState.totalPausedTime; const formattedTime = this.formatTime(totalTime); // Update the game over title based on end reason const gameOverTitle = document.querySelector('#game-over-screen h2'); const gameOverMessage = document.querySelector('#game-over-screen p'); // Use game mode manager for completion message if available if (window.gameModeManager && window.gameModeManager.isScenarioMode()) { if (reason === 'scenario-complete') { gameOverTitle.textContent = '🎭 Scenario Complete!'; gameOverMessage.textContent = window.gameModeManager.getCompletionMessage(); } else if (reason === 'scenario-quit') { gameOverTitle.textContent = '😞 Scenario Abandoned'; gameOverMessage.textContent = 'You gave up on the scenario. Your progress was not saved.'; } else { gameOverTitle.textContent = '🎉 Mode Complete!'; gameOverMessage.textContent = window.gameModeManager.getCompletionMessage(); } } else { switch (reason) { case 'time': gameOverTitle.textContent = '⏰ Time\'s Up!'; gameOverMessage.textContent = 'You ran out of time! See how many tasks you completed.'; break; case 'target-reached': gameOverTitle.textContent = '⭐ XP Target Reached!'; gameOverMessage.textContent = `Congratulations! You reached the target of ${this.gameState.xpTarget} XP!`; break; case 'complete-all': default: gameOverTitle.textContent = '🎉 All Tasks Complete!'; gameOverMessage.textContent = 'Congratulations! You\'ve completed all available tasks!'; break; } } // Handle scenario vs regular game stats display if (window.gameModeManager && window.gameModeManager.isScenarioMode()) { this.displayScenarioStats(reason); } else { this.displayRegularGameStats(); } } /** * Display scenario-specific stats */ displayScenarioStats(reason) { const scenarioXp = this.gameState.scenarioXp || {}; const scenarioName = window.gameModeManager.getCurrentMode()?.name || 'Unknown Scenario'; // Calculate total scenario XP const totalScenarioXp = (scenarioXp.timeBasedXp || 0) + (scenarioXp.focusXp || 0) + (scenarioXp.webcamXp || 0) + (scenarioXp.photoXp || 0) + (scenarioXp.stepCompletion || 0); // Display scenario mode document.getElementById('final-game-mode').textContent = scenarioName; // Display scenario XP (separate from main game XP) document.getElementById('final-xp').textContent = reason === 'scenario-quit' ? 0 : totalScenarioXp; // Format time properly const totalSeconds = Math.floor(this.gameState.timer); const minutes = Math.floor(totalSeconds / 60); const seconds = totalSeconds % 60; const formattedTime = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; document.getElementById('final-time').textContent = formattedTime; // Hide irrelevant stats for scenarios const statsToHide = ['final-completed', 'final-skipped', 'final-consequences', 'final-best-streak', 'final-streak-bonuses']; statsToHide.forEach(statId => { const element = document.getElementById(statId); if (element && element.parentElement) { element.parentElement.style.display = 'none'; } }); } /** * Display regular game stats */ displayRegularGameStats() { // Show all stats elements for regular games const statsToShow = ['final-completed', 'final-skipped', 'final-consequences', 'final-best-streak', 'final-streak-bonuses']; statsToShow.forEach(statId => { const element = document.getElementById(statId); if (element && element.parentElement) { element.parentElement.style.display = 'block'; } }); document.getElementById('final-xp').textContent = this.gameState.xp || 0; const formattedTime = this.formatTime(this.gameState.timer); document.getElementById('final-time').textContent = formattedTime; document.getElementById('final-completed').textContent = this.gameState.completedCount; document.getElementById('final-skipped').textContent = this.gameState.skippedCount; document.getElementById('final-consequences').textContent = this.gameState.consequenceCount; // Update streak bonus stats const bestStreakElement = document.getElementById('final-best-streak'); const streakBonusesElement = document.getElementById('final-streak-bonuses'); if (bestStreakElement) { bestStreakElement.textContent = this.gameState.currentStreak; } if (streakBonusesElement) { streakBonusesElement.textContent = this.gameState.totalStreakBonuses; } // Add game mode info to stats let gameModeText = ''; switch (this.gameState.gameMode) { case 'timed': gameModeText = `Timed Challenge (${this.gameState.timeLimit / 60} minutes)`; break; case 'xp-target': gameModeText = `XP Target (${this.gameState.xpTarget} XP)`; break; case 'complete-all': default: gameModeText = 'Complete All Tasks'; break; } // Show game mode info if element exists const gameModeElement = document.getElementById('final-game-mode'); if (gameModeElement) { gameModeElement.textContent = gameModeText; } } /** * Get total steps in current scenario */ getScenarioTotalSteps() { const task = this.gameState.currentTask; if (task && task.interactiveData && task.interactiveData.steps) { return Object.keys(task.interactiveData.steps).length; } return 0; } } // Unified Data Management System class DataManager { constructor() { this.version = "1.0"; this.storageKey = "webGame-data"; this.data = this.loadData(); this.autoSaveInterval = null; this.startAutoSave(); } getDefaultData() { return { version: this.version, timestamp: Date.now(), settings: { theme: "default", difficulty: "medium", music: { volume: 30, loopMode: 0, shuffle: false, currentTrack: 0 } }, gameplay: { totalGamesPlayed: 0, totalTasksCompleted: 0, totalTasksSkipped: 0, bestScore: 0, currentStreak: 0, longestStreak: 0, totalPlayTime: 0, mercyPointsEarned: 0, mercyPointsSpent: 0 }, customContent: { tasks: { main: [], consequence: [] }, customImages: [], disabledImages: [] }, statistics: { taskCompletionRate: 0, averageGameDuration: 0, difficultyStats: { easy: { played: 0, completed: 0 }, medium: { played: 0, completed: 0 }, hard: { played: 0, completed: 0 } }, themeUsage: {}, lastPlayed: null } }; } loadData() { try { // Try new unified format first const unified = localStorage.getItem(this.storageKey); if (unified) { const data = JSON.parse(unified); return this.migrateData(data); } // Migrate from old scattered localStorage return this.migrateFromOldFormat(); } catch (error) { console.warn('Data loading failed, using defaults:', error); return this.getDefaultData(); } } migrateFromOldFormat() { const data = this.getDefaultData(); // Migrate existing scattered data try { // Theme const theme = localStorage.getItem('selectedTheme'); if (theme) data.settings.theme = theme; // Music settings const volume = localStorage.getItem('gameMusic-volume'); if (volume) data.settings.music.volume = parseInt(volume); const track = localStorage.getItem('gameMusic-track'); if (track) data.settings.music.currentTrack = parseInt(track); const loopMode = localStorage.getItem('gameMusic-loopMode'); if (loopMode) data.settings.music.loopMode = parseInt(loopMode); const shuffle = localStorage.getItem('gameMusic-shuffleMode'); if (shuffle) data.settings.music.shuffle = shuffle === 'true'; // Custom tasks const mainTasks = localStorage.getItem('customMainTasks'); if (mainTasks) data.customContent.tasks.main = JSON.parse(mainTasks); const consequenceTasks = localStorage.getItem('customConsequenceTasks'); if (consequenceTasks) data.customContent.tasks.consequence = JSON.parse(consequenceTasks); // Difficulty const difficulty = localStorage.getItem('selectedDifficulty'); if (difficulty) data.settings.difficulty = difficulty; console.log('Migrated data from old format'); this.saveData(); // Save in new format this.cleanupOldStorage(); // Remove old keys } catch (error) { console.warn('Migration failed, using defaults:', error); } return data; } migrateData(data) { // Handle version upgrades here if (!data.version || data.version < this.version) { console.log(`Upgrading data from ${data.version || 'unknown'} to ${this.version}`); // Add any missing properties from default const defaultData = this.getDefaultData(); data = this.deepMerge(defaultData, data); data.version = this.version; } return data; } deepMerge(target, source) { const result = { ...target }; for (const key in source) { if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) { result[key] = this.deepMerge(target[key] || {}, source[key]); } else { result[key] = source[key]; } } return result; } cleanupOldStorage() { // Remove old scattered localStorage keys const oldKeys = [ 'selectedTheme', 'gameMusic-volume', 'gameMusic-track', 'gameMusic-loopMode', 'gameMusic-shuffleMode', 'customMainTasks', 'customConsequenceTasks', 'selectedDifficulty' ]; oldKeys.forEach(key => localStorage.removeItem(key)); console.log('Cleaned up old storage keys'); } saveData() { try { if (!this.data) { this.data = this.getDefaultData(); } this.data.timestamp = Date.now(); localStorage.setItem(this.storageKey, JSON.stringify(this.data)); } catch (error) { console.error('Failed to save data:', error); throw error; // Re-throw so calling code can handle it } } // Settings Management getSetting(path) { return this.getNestedValue(this.data.settings, path); } setSetting(path, value) { this.setNestedValue(this.data.settings, path, value); this.saveData(); } // Generic data access get(key) { // Handle special cases first if (key === 'autoSaveGameState') { return JSON.parse(localStorage.getItem('autoSaveGameState') || 'null'); } // For custom images and disabled images, store them in customContent if (key === 'customImages') { return this.data.customContent.customImages || []; } if (key === 'disabledImages') { return this.data.customContent.disabledImages || []; } // Generic access to data properties return this.data[key]; } set(key, value) { // Handle special cases first if (key === 'autoSaveGameState') { localStorage.setItem('autoSaveGameState', JSON.stringify(value)); return; } // For custom images and disabled images, store them in customContent if (key === 'customImages') { if (!this.data.customContent.customImages) { this.data.customContent.customImages = []; } this.data.customContent.customImages = value; this.saveData(); return; } if (key === 'disabledImages') { if (!this.data.customContent.disabledImages) { this.data.customContent.disabledImages = []; } this.data.customContent.disabledImages = value; this.saveData(); return; } // Generic setter this.data[key] = value; this.saveData(); } // Gameplay Statistics recordGameStart() { this.data.gameplay.totalGamesPlayed++; this.data.statistics.lastPlayed = Date.now(); this.saveData(); } recordTaskComplete(difficulty, points, isConsequence = false) { this.data.gameplay.totalTasksCompleted++; if (!isConsequence) { this.data.gameplay.currentStreak++; this.data.gameplay.longestStreak = Math.max( this.data.gameplay.longestStreak, this.data.gameplay.currentStreak ); // Track difficulty stats if (this.data.statistics.difficultyStats[difficulty]) { this.data.statistics.difficultyStats[difficulty].completed++; } } this.updateStatistics(); this.saveData(); } recordTaskSkip(mercyPoints = 0) { this.data.gameplay.totalTasksSkipped++; this.data.gameplay.currentStreak = 0; if (mercyPoints > 0) { this.data.gameplay.mercyPointsSpent += mercyPoints; } this.updateStatistics(); this.saveData(); } recordScore(score) { this.data.gameplay.bestScore = Math.max(this.data.gameplay.bestScore, score); this.saveData(); } recordPlayTime(duration) { this.data.gameplay.totalPlayTime += duration; this.updateStatistics(); this.saveData(); } updateStatistics() { const { totalTasksCompleted, totalTasksSkipped } = this.data.gameplay; const total = totalTasksCompleted + totalTasksSkipped; this.data.statistics.taskCompletionRate = total > 0 ? totalTasksCompleted / total : 0; if (this.data.gameplay.totalGamesPlayed > 0) { this.data.statistics.averageGameDuration = this.data.gameplay.totalPlayTime / this.data.gameplay.totalGamesPlayed; } } // Custom Content Management getCustomTasks(type = 'main') { return this.data.customContent.tasks[type] || []; } addCustomTask(task, type = 'main') { if (!this.data.customContent.tasks[type]) { this.data.customContent.tasks[type] = []; } this.data.customContent.tasks[type].push(task); this.saveData(); } removeCustomTask(index, type = 'main') { if (this.data.customContent.tasks[type]) { this.data.customContent.tasks[type].splice(index, 1); this.saveData(); } } resetCustomTasks(type = 'main') { this.data.customContent.tasks[type] = []; this.saveData(); } // Export/Import System exportData(includeStats = true) { const exportData = { version: this.version, exportDate: new Date().toISOString(), settings: this.data.settings, customContent: this.data.customContent }; if (includeStats) { exportData.gameplay = this.data.gameplay; exportData.statistics = this.data.statistics; } const dataStr = JSON.stringify(exportData, null, 2); const blob = new Blob([dataStr], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `webgame-save-${new Date().toISOString().split('T')[0]}.json`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); return exportData; } importData(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = (e) => { try { const importedData = JSON.parse(e.target.result); // Validate import if (!this.validateImportData(importedData)) { throw new Error('Invalid save file format'); } // Merge imported data this.data = this.deepMerge(this.getDefaultData(), importedData); this.data.version = this.version; this.data.timestamp = Date.now(); this.saveData(); resolve(importedData); } catch (error) { reject(error); } }; reader.onerror = () => reject(new Error('Failed to read file')); reader.readAsText(file); }); } validateImportData(data) { // Basic validation return data && typeof data === 'object' && (data.settings || data.customContent || data.gameplay); } // Utility functions getNestedValue(obj, path) { return path.split('.').reduce((current, key) => current && current[key], obj); } setNestedValue(obj, path, value) { const keys = path.split('.'); const lastKey = keys.pop(); const target = keys.reduce((current, key) => { if (!current[key]) current[key] = {}; return current[key]; }, obj); target[lastKey] = value; } // Auto-save functionality startAutoSave() { this.autoSaveInterval = setInterval(() => { this.saveData(); }, 30000); // Save every 30 seconds } stopAutoSave() { if (this.autoSaveInterval) { clearInterval(this.autoSaveInterval); this.autoSaveInterval = null; } } // Statistics getters for UI getStats() { return { ...this.data.gameplay, ...this.data.statistics, hoursPlayed: Math.round(this.data.gameplay.totalPlayTime / 3600000 * 10) / 10, completionPercentage: Math.round(this.data.statistics.taskCompletionRate * 100) }; } // Reset functionality resetAllData() { if (confirm('Are you sure you want to reset all data? This cannot be undone!')) { this.data = this.getDefaultData(); this.saveData(); return true; } return false; } resetStats() { if (confirm('Reset all statistics? Your settings and custom tasks will be kept.')) { this.data.gameplay = this.getDefaultData().gameplay; this.data.statistics = this.getDefaultData().statistics; this.saveData(); return true; } return false; } } // Make DataManager available globally window.DataManager = DataManager; // Simple Music Management System with Playlist Controls class MusicManager { constructor(dataManager) { this.dataManager = dataManager; this.currentAudio = null; this.isPlaying = false; this.volume = this.dataManager.getSetting('music.volume') || 30; this.loopMode = this.dataManager.getSetting('music.loopMode') || 0; this.shuffleMode = this.dataManager.getSetting('music.shuffle') || false; this.currentTrackIndex = this.dataManager.getSetting('music.currentTrack') || 0; this.playHistory = []; // Initialize empty tracks array - custom tracks will be loaded next this.tracks = []; // Load and add custom background music this.loadCustomTracks(); this.updateUI(); this.initializeVolumeUI(); } saveSettings() { this.dataManager.setSetting('music.volume', this.volume); this.dataManager.setSetting('music.currentTrack', this.currentTrackIndex); this.dataManager.setSetting('music.loopMode', this.loopMode); this.dataManager.setSetting('music.shuffle', this.shuffleMode); } loadCustomTracks() { // Get custom background music from storage const customAudio = this.dataManager.get('customAudio') || { background: [], ambient: [], effects: [] }; const backgroundMusic = customAudio.background || []; // Add enabled custom background music to tracks backgroundMusic.forEach(audio => { if (audio.enabled !== false) { // Include if not explicitly disabled this.tracks.push({ name: audio.title || audio.name, file: audio.path, isBuiltIn: false, isCustom: true }); } }); console.log(`MusicManager: Loaded ${backgroundMusic.length} custom background tracks`); } refreshCustomTracks() { // Clear all tracks since we only have custom tracks now this.tracks = []; // Reload custom tracks this.loadCustomTracks(); // Update the UI to reflect new tracks this.updateTrackSelector(); // If current track was custom and no longer exists, reset to first track if (this.currentTrackIndex >= this.tracks.length) { this.currentTrackIndex = 0; this.saveSettings(); } console.log(`MusicManager: Refreshed tracks, now have ${this.tracks.length} total tracks`); } initializeVolumeUI() { const volumeSlider = document.getElementById('volume-slider'); const volumePercent = document.getElementById('volume-percent'); const trackSelector = document.getElementById('track-selector'); volumeSlider.value = this.volume; volumePercent.textContent = `${this.volume}%`; // Build track selector with all tracks this.updateTrackSelector(); this.updateLoopButton(); this.updateShuffleButton(); } // Loop mode cycling: 0 -> 1 -> 2 -> 0 toggleLoopMode() { this.loopMode = (this.loopMode + 1) % 3; this.saveSettings(); this.updateLoopButton(); this.updateAudioLoop(); } toggleShuffleMode() { this.shuffleMode = !this.shuffleMode; this.saveSettings(); this.updateShuffleButton(); if (this.shuffleMode) { this.playHistory = [this.currentTrackIndex]; // Start tracking } } updateLoopButton() { const loopBtn = document.getElementById('loop-btn'); switch(this.loopMode) { case 0: loopBtn.classList.remove('active'); loopBtn.innerHTML = '🔁'; loopBtn.title = 'Loop: Off'; break; case 1: loopBtn.classList.add('active'); loopBtn.innerHTML = '🔁'; loopBtn.title = 'Loop: Playlist'; break; case 2: loopBtn.classList.add('active'); loopBtn.innerHTML = '🔂'; loopBtn.title = 'Loop: Current Track'; break; } } updateShuffleButton() { const shuffleBtn = document.getElementById('shuffle-btn'); if (this.shuffleMode) { shuffleBtn.classList.add('active'); shuffleBtn.title = 'Shuffle: On'; } else { shuffleBtn.classList.remove('active'); shuffleBtn.title = 'Shuffle: Off'; } } updateAudioLoop() { if (this.currentAudio) { this.currentAudio.loop = (this.loopMode === 2); } } getNextTrack() { if (this.shuffleMode) { return this.getShuffledNextTrack(); } else { return (this.currentTrackIndex + 1) % this.tracks.length; } } getShuffledNextTrack() { if (this.tracks.length <= 1) return 0; let availableTracks = []; for (let i = 0; i < this.tracks.length; i++) { // Avoid last 2 played tracks if possible if (this.playHistory.length < 2 || !this.playHistory.slice(-2).includes(i)) { availableTracks.push(i); } } // If no tracks available (shouldn't happen with 4+ tracks), use all except current if (availableTracks.length === 0) { availableTracks = this.tracks.map((_, i) => i).filter(i => i !== this.currentTrackIndex); } return availableTracks[Math.floor(Math.random() * availableTracks.length)]; } // Called when track ends naturally onTrackEnded() { if (this.loopMode === 2) { // Loop current track - handled by audio.loop = true return; } else if (this.loopMode === 1) { // Loop playlist - go to next track this.playNextTrack(); } else { // No loop - stop playing this.isPlaying = false; this.updateUI(); } } playNextTrack() { const nextIndex = this.getNextTrack(); this.changeTrack(nextIndex); // Update play history for shuffle if (this.shuffleMode) { this.playHistory.push(nextIndex); // Keep only last 3 tracks in history if (this.playHistory.length > 3) { this.playHistory.shift(); } } } toggle() { if (this.isPlaying) { this.stop(); } else { this.play(); } } play() { try { if (!this.currentAudio) { this.loadCurrentTrack(); } this.currentAudio.play(); this.isPlaying = true; this.updateUI(); } catch (error) { console.log('Audio playback failed:', error); // Fallback to silent mode this.isPlaying = true; this.updateUI(); } } stop() { if (this.currentAudio) { this.currentAudio.pause(); this.currentAudio.currentTime = 0; } this.isPlaying = false; this.updateUI(); } pause() { if (this.currentAudio && this.isPlaying) { this.currentAudio.pause(); } } resume() { if (this.currentAudio && this.isPlaying) { try { this.currentAudio.play(); } catch (error) { console.log('Audio resume failed:', error); } } } changeTrack(trackIndex) { const wasPlaying = this.isPlaying; if (this.currentAudio) { this.currentAudio.pause(); this.currentAudio = null; } this.currentTrackIndex = trackIndex; this.saveSettings(); if (wasPlaying) { this.play(); } else { this.updateUI(); } } setVolume(volume) { this.volume = volume; this.saveSettings(); if (this.currentAudio) { this.currentAudio.volume = volume / 100; } const volumePercent = document.getElementById('volume-percent'); volumePercent.textContent = `${volume}%`; // Update volume icon based on level const volumeIcon = document.querySelector('.volume-icon'); if (volume === 0) { volumeIcon.textContent = '🔇'; } else if (volume < 30) { volumeIcon.textContent = '🔉'; } else { volumeIcon.textContent = '🔊'; } } loadCurrentTrack() { const track = this.tracks[this.currentTrackIndex]; // Create audio element with fallback this.currentAudio = new Audio(); this.currentAudio.volume = this.volume / 100; this.updateAudioLoop(); // Set source with error handling this.currentAudio.src = track.file; // Handle loading errors gracefully this.currentAudio.onerror = () => { console.log(`Could not load audio: ${track.file}`); // Continue without audio }; // Handle successful loading this.currentAudio.onloadeddata = () => { console.log(`Loaded audio: ${track.name}`); }; // Handle track ending this.currentAudio.onended = () => { if (this.isPlaying) { this.onTrackEnded(); } }; } updateUI() { const toggleBtn = document.getElementById('music-toggle'); const statusSpan = document.getElementById('music-status'); const trackSelector = document.getElementById('track-selector'); const currentTrack = this.tracks[this.currentTrackIndex]; // Update track selector trackSelector.value = this.currentTrackIndex; if (this.isPlaying) { toggleBtn.classList.add('playing'); toggleBtn.innerHTML = '⏸️'; // Show current mode in status let modeText = ''; if (this.shuffleMode) modeText += ' 🔀'; if (this.loopMode === 1) modeText += ' 🔁'; else if (this.loopMode === 2) modeText += ' 🔂'; statusSpan.textContent = `♪ ${currentTrack.name}${modeText}`; } else { toggleBtn.classList.remove('playing'); toggleBtn.innerHTML = '🎵'; statusSpan.textContent = 'Music: Off'; } } updateTrackSelector() { const trackSelector = document.getElementById('track-selector'); if (!trackSelector) return; // Clear existing options trackSelector.innerHTML = ''; // Add all tracks (built-in and custom) this.tracks.forEach((track, index) => { const option = document.createElement('option'); option.value = index; option.textContent = track.isCustom ? `${track.name} (Custom)` : track.name; trackSelector.appendChild(option); }); // Set current selection trackSelector.value = this.currentTrackIndex; } } // Photo Gallery Management Methods TaskChallengeGame.prototype.showPhotoGallery = function() { this.showScreen('photo-gallery-screen'); this.setupPhotoGalleryEventListeners(); this.loadPhotoGallery(); }; TaskChallengeGame.prototype.setupPhotoGalleryEventListeners = function() { // Back button const backBtn = document.getElementById('back-to-start-from-photos-btn'); if (backBtn) { backBtn.onclick = () => this.showScreen('start-screen'); } // Action buttons const downloadAllBtn = document.getElementById('download-all-photos-btn'); if (downloadAllBtn) { downloadAllBtn.onclick = () => this.downloadAllPhotos(); } const downloadSelectedBtn = document.getElementById('download-selected-photos-btn'); if (downloadSelectedBtn) { downloadSelectedBtn.onclick = () => this.downloadSelectedPhotos(); } const selectAllBtn = document.getElementById('select-all-photos-btn'); if (selectAllBtn) { selectAllBtn.onclick = () => this.selectAllPhotos(); } const deselectAllBtn = document.getElementById('deselect-all-photos-btn'); if (deselectAllBtn) { deselectAllBtn.onclick = () => this.deselectAllPhotos(); } const clearAllBtn = document.getElementById('clear-all-photos-btn'); if (clearAllBtn) { clearAllBtn.onclick = () => this.clearAllPhotos(); } const settingsBtn = document.getElementById('photo-storage-settings-btn'); if (settingsBtn) { settingsBtn.onclick = () => this.showPhotoSettings(); } // Filter controls const sessionFilter = document.getElementById('photo-session-filter'); if (sessionFilter) { sessionFilter.onchange = () => this.filterPhotos(); } const sortOrder = document.getElementById('photo-sort-order'); if (sortOrder) { sortOrder.onchange = () => this.filterPhotos(); } // Modal close buttons const closeDetailBtn = document.getElementById('close-photo-detail'); if (closeDetailBtn) { closeDetailBtn.onclick = () => this.closePhotoDetail(); } const closeSettingsBtn = document.getElementById('close-photo-settings'); if (closeSettingsBtn) { closeSettingsBtn.onclick = () => this.closePhotoSettings(); } // Photo detail modal actions const downloadBtn = document.getElementById('download-photo-btn'); if (downloadBtn) { downloadBtn.onclick = () => this.downloadCurrentPhoto(); } const deleteBtn = document.getElementById('delete-photo-btn'); if (deleteBtn) { deleteBtn.onclick = () => this.deleteCurrentPhoto(); } // Settings modal actions const saveSettingsBtn = document.getElementById('save-photo-settings-btn'); if (saveSettingsBtn) { saveSettingsBtn.onclick = () => this.savePhotoSettings(); } }; TaskChallengeGame.prototype.loadPhotoGallery = function() { const stats = this.webcamManager.getPhotoStats(); this.updatePhotoStats(stats); const photos = this.webcamManager.getSavedPhotos(); const grid = document.getElementById('photo-grid'); const noPhotosMsg = document.getElementById('no-photos-message'); if (photos.length === 0) { grid.style.display = 'none'; noPhotosMsg.style.display = 'block'; return; } grid.style.display = 'grid'; noPhotosMsg.style.display = 'none'; this.renderPhotoGrid(photos); }; TaskChallengeGame.prototype.updatePhotoStats = function(stats) { const countDisplay = document.getElementById('photo-count-display'); const sizeDisplay = document.getElementById('storage-size-display'); if (countDisplay) { countDisplay.textContent = `${stats.count} photos`; } if (sizeDisplay) { const sizeInKB = (stats.totalSize / 1024).toFixed(1); sizeDisplay.textContent = `${sizeInKB} KB used`; } }; TaskChallengeGame.prototype.renderPhotoGrid = function(photos) { const grid = document.getElementById('photo-grid'); const sessionFilter = document.getElementById('photo-session-filter').value; const sortOrder = document.getElementById('photo-sort-order').value; // Initialize selected photos array if not exists if (!this.selectedPhotos) { this.selectedPhotos = new Set(); } // Filter photos let filteredPhotos = photos; if (sessionFilter !== 'all') { filteredPhotos = photos.filter(photo => photo.sessionType === sessionFilter); } // Sort photos switch (sortOrder) { case 'newest': filteredPhotos.sort((a, b) => b.timestamp - a.timestamp); break; case 'oldest': filteredPhotos.sort((a, b) => a.timestamp - b.timestamp); break; case 'session': filteredPhotos.sort((a, b) => a.sessionType.localeCompare(b.sessionType)); break; } // Render photo items grid.innerHTML = filteredPhotos.map(photo => this.createPhotoItem(photo)).join(''); // Add click listeners grid.querySelectorAll('.photo-item').forEach(item => { const photoId = item.dataset.photoId; const checkbox = item.querySelector('.selection-checkbox'); // Checkbox click handler checkbox.onchange = (e) => { e.stopPropagation(); this.togglePhotoSelection(photoId, checkbox.checked); }; // Click anywhere on item to select/deselect (like image gallery) item.addEventListener('click', (e) => { // Don't toggle selection if clicking on checkbox if (e.target.type !== 'checkbox') { const isCurrentlySelected = this.selectedPhotos.has(photoId); this.togglePhotoSelection(photoId, !isCurrentlySelected); checkbox.checked = !isCurrentlySelected; } }); // Update checkbox state if photo was previously selected if (this.selectedPhotos.has(photoId)) { checkbox.checked = true; item.classList.add('selected'); } }); // Update selection count this.updateSelectionCount(); }; TaskChallengeGame.prototype.createPhotoItem = function(photo) { const date = new Date(photo.timestamp).toLocaleDateString(); const sessionName = this.formatSessionName(photo.sessionType); return `
Captured photo
${date}
${sessionName}
`; }; TaskChallengeGame.prototype.formatSessionName = function(sessionType) { const sessionNames = { 'dress-up-photo': 'Dress-up', 'photography-studio': 'Studio', 'custom': 'Custom' }; return sessionNames[sessionType] || sessionType; }; TaskChallengeGame.prototype.filterPhotos = function() { const photos = this.webcamManager.getSavedPhotos(); this.renderPhotoGrid(photos); }; // Photo selection management TaskChallengeGame.prototype.togglePhotoSelection = function(photoId, isSelected) { if (!this.selectedPhotos) { this.selectedPhotos = new Set(); } const photoItem = document.querySelector(`.photo-item[data-photo-id="${photoId}"]`); if (isSelected) { this.selectedPhotos.add(photoId); photoItem.classList.add('selected'); } else { this.selectedPhotos.delete(photoId); photoItem.classList.remove('selected'); } this.updateSelectionCount(); }; TaskChallengeGame.prototype.selectAllPhotos = function() { if (!this.selectedPhotos) { this.selectedPhotos = new Set(); } document.querySelectorAll('.photo-checkbox').forEach(checkbox => { checkbox.checked = true; this.selectedPhotos.add(checkbox.dataset.photoId); checkbox.closest('.photo-item').classList.add('selected'); }); this.updateSelectionCount(); }; TaskChallengeGame.prototype.deselectAllPhotos = function() { if (!this.selectedPhotos) { this.selectedPhotos = new Set(); } document.querySelectorAll('.photo-checkbox').forEach(checkbox => { checkbox.checked = false; checkbox.closest('.photo-item').classList.remove('selected'); }); this.selectedPhotos.clear(); this.updateSelectionCount(); }; TaskChallengeGame.prototype.updateSelectionCount = function() { const count = this.selectedPhotos ? this.selectedPhotos.size : 0; const downloadBtn = document.getElementById('download-selected-photos-btn'); if (downloadBtn) { downloadBtn.textContent = count > 0 ? `📥 Download Selected (${count})` : '📥 Download Selected'; downloadBtn.disabled = count === 0; if (count === 0) { downloadBtn.classList.add('btn-disabled'); } else { downloadBtn.classList.remove('btn-disabled'); } } }; TaskChallengeGame.prototype.downloadSelectedPhotos = function() { if (!this.selectedPhotos || this.selectedPhotos.size === 0) { this.showNotification('Please select photos to download', 'warning'); return; } const allPhotos = this.webcamManager.getSavedPhotos(); const selectedPhotos = allPhotos.filter(photo => this.selectedPhotos.has(photo.id)); this.webcamManager.downloadSelectedPhotos(selectedPhotos); }; TaskChallengeGame.prototype.showPhotoDetail = function(photoId) { const photos = this.webcamManager.getSavedPhotos(); const photo = photos.find(p => p.id === photoId); if (!photo) return; this.currentDetailPhoto = photo; // Populate modal document.getElementById('photo-detail-image').src = photo.dataURL; document.getElementById('photo-detail-date').textContent = new Date(photo.timestamp).toLocaleString(); document.getElementById('photo-detail-session').textContent = this.formatSessionName(photo.sessionType); document.getElementById('photo-detail-task').textContent = photo.taskId || 'Unknown'; document.getElementById('photo-detail-size').textContent = `${Math.round(photo.size / 1024)} KB`; // Show modal document.getElementById('photo-detail-modal').style.display = 'flex'; }; TaskChallengeGame.prototype.closePhotoDetail = function() { document.getElementById('photo-detail-modal').style.display = 'none'; this.currentDetailPhoto = null; }; TaskChallengeGame.prototype.downloadCurrentPhoto = function() { if (this.currentDetailPhoto) { this.webcamManager.downloadPhoto(this.currentDetailPhoto); } }; TaskChallengeGame.prototype.deleteCurrentPhoto = function() { if (!this.currentDetailPhoto) return; if (confirm('Are you sure you want to delete this photo? This cannot be undone.')) { const success = this.webcamManager.deletePhoto(this.currentDetailPhoto.id); if (success) { this.closePhotoDetail(); this.loadPhotoGallery(); // Refresh the gallery this.webcamManager.showNotification('Photo deleted', 'success'); } else { this.webcamManager.showNotification('Failed to delete photo', 'error'); } } }; TaskChallengeGame.prototype.downloadAllPhotos = function() { this.webcamManager.downloadAllPhotos(); }; TaskChallengeGame.prototype.clearAllPhotos = function() { if (confirm('Are you sure you want to delete ALL photos? This cannot be undone.')) { const success = this.webcamManager.clearAllPhotos(); if (success) { this.loadPhotoGallery(); // Refresh the gallery this.webcamManager.showNotification('All photos cleared', 'success'); } else { this.webcamManager.showNotification('Failed to clear photos', 'error'); } } }; TaskChallengeGame.prototype.showPhotoSettings = function() { const stats = this.webcamManager.getPhotoStats(); // Update consent radio buttons const consentValue = stats.storageConsent; document.getElementById('consent-enable').checked = consentValue === 'true'; document.getElementById('consent-disable').checked = consentValue === 'false'; // Update stats document.getElementById('settings-photo-count').textContent = stats.count; document.getElementById('settings-storage-size').textContent = `${(stats.totalSize / 1024).toFixed(1)} KB`; document.getElementById('settings-oldest-photo').textContent = stats.oldestPhoto ? new Date(stats.oldestPhoto).toLocaleDateString() : 'None'; // Show modal document.getElementById('photo-settings-modal').style.display = 'flex'; }; TaskChallengeGame.prototype.closePhotoSettings = function() { document.getElementById('photo-settings-modal').style.display = 'none'; }; TaskChallengeGame.prototype.savePhotoSettings = function() { const consentValue = document.querySelector('input[name="photo-consent"]:checked').value; localStorage.setItem('photoStorageConsent', consentValue); this.closePhotoSettings(); this.webcamManager.showNotification('Photo settings saved', 'success'); }; // Annoyance Management Methods - Phase 2: Advanced Message Management TaskChallengeGame.prototype.showAnnoyanceManagement = function() { this.showScreen('annoyance-management-screen'); this.setupAnnoyanceManagementEventListeners(); this.loadAnnoyanceSettings(); this.showAnnoyanceTab('messages'); // Default to messages tab }; TaskChallengeGame.prototype.setupAnnoyanceManagementEventListeners = function() { // Back button const backBtn = document.getElementById('back-to-start-from-annoyance-btn'); if (backBtn) { backBtn.onclick = () => this.showScreen('start-screen'); } // Save settings button const saveBtn = document.getElementById('save-annoyance-settings'); if (saveBtn) { saveBtn.onclick = () => this.saveAllAnnoyanceSettings(); } // Tab navigation document.getElementById('messages-tab').onclick = () => this.showAnnoyanceTab('messages'); document.getElementById('appearance-tab').onclick = () => this.showAnnoyanceTab('appearance'); document.getElementById('behavior-tab').onclick = () => this.showAnnoyanceTab('behavior'); document.getElementById('popup-images-tab').onclick = () => this.showAnnoyanceTab('popup-images'); document.getElementById('import-export-tab').onclick = () => this.showAnnoyanceTab('import-export'); document.getElementById('ai-tasks-tab').onclick = () => this.showAnnoyanceTab('ai-tasks'); this.setupMessagesTabListeners(); this.setupAppearanceTabListeners(); this.setupBehaviorTabListeners(); this.setupPopupImagesTabListeners(); this.setupImportExportTabListeners(); this.setupAITasksTabListeners(); }; TaskChallengeGame.prototype.showAnnoyanceTab = function(tabName) { // Update tab buttons document.querySelectorAll('.annoyance-tab').forEach(tab => tab.classList.remove('active')); document.getElementById(`${tabName}-tab`).classList.add('active'); // Update tab content document.querySelectorAll('.annoyance-tab-content').forEach(content => content.classList.remove('active')); document.getElementById(`${tabName}-tab-content`).classList.add('active'); // Load tab-specific content switch(tabName) { case 'messages': this.loadMessagesTab(); break; case 'appearance': this.loadAppearanceTab(); break; case 'behavior': this.loadBehaviorTab(); break; case 'popup-images': this.loadPopupImagesSettings(); break; case 'import-export': this.loadImportExportTab(); break; case 'ai-tasks': this.loadAITasksTab(); break; } }; // Messages Tab Management TaskChallengeGame.prototype.setupMessagesTabListeners = function() { const enabledCheckbox = document.getElementById('flash-messages-enabled'); const addBtn = document.getElementById('add-new-message-btn'); const closeEditorBtn = document.getElementById('close-editor-btn'); const saveMessageBtn = document.getElementById('save-message-btn'); const previewCurrentBtn = document.getElementById('preview-current-message-btn'); const cancelEditBtn = document.getElementById('cancel-edit-btn'); const categoryFilter = document.getElementById('category-filter'); const showDisabledCheckbox = document.getElementById('show-disabled-messages'); const messageTextarea = document.getElementById('message-text'); if (enabledCheckbox) { enabledCheckbox.onchange = (e) => this.updateFlashMessageSetting('enabled', e.target.checked); } if (addBtn) { addBtn.onclick = () => this.showMessageEditor(); } if (closeEditorBtn) { closeEditorBtn.onclick = () => this.hideMessageEditor(); } if (saveMessageBtn) { saveMessageBtn.onclick = () => this.saveCurrentMessage(); } if (previewCurrentBtn) { previewCurrentBtn.onclick = () => this.previewCurrentMessage(); } if (cancelEditBtn) { cancelEditBtn.onclick = () => this.hideMessageEditor(); } if (categoryFilter) { categoryFilter.onchange = () => this.refreshMessageList(); } if (showDisabledCheckbox) { showDisabledCheckbox.onchange = () => this.refreshMessageList(); } if (messageTextarea) { messageTextarea.oninput = () => this.updateCharacterCount(); } }; TaskChallengeGame.prototype.loadMessagesTab = function() { const config = this.flashMessageManager.getConfig(); const enabledCheckbox = document.getElementById('flash-messages-enabled'); if (enabledCheckbox) enabledCheckbox.checked = config.enabled; this.refreshMessageList(); this.hideMessageEditor(); // Ensure editor is closed by default }; TaskChallengeGame.prototype.refreshMessageList = function() { const categoryFilter = document.getElementById('category-filter'); const showDisabled = document.getElementById('show-disabled-messages').checked; const selectedCategory = categoryFilter.value; let messages = this.flashMessageManager.getMessagesByCategory(selectedCategory); if (!showDisabled) { messages = messages.filter(msg => msg.enabled !== false); } this.renderMessageList(messages); this.updateMessageStats(); }; TaskChallengeGame.prototype.renderMessageList = function(messages) { const listElement = document.getElementById('message-list'); if (messages.length === 0) { listElement.innerHTML = '
No messages match your criteria.
'; return; } listElement.innerHTML = messages.map(msg => `
${msg.text}
${this.getCategoryEmoji(msg.category)} ${msg.category || 'Custom'} Priority: ${msg.priority || 'Normal'} ${msg.isCustom ? 'Custom' : 'Default'}
${msg.isCustom ? `` : ''}
`).join(''); }; TaskChallengeGame.prototype.updateMessageStats = function() { const stats = this.flashMessageManager.getMessageStats(); const statsElement = document.getElementById('message-stats'); if (statsElement) { statsElement.textContent = `${stats.total} messages (${stats.enabled} enabled, ${stats.disabled} disabled)`; } }; TaskChallengeGame.prototype.showMessageEditor = function(messageData = null) { const editor = document.getElementById('message-editor'); const title = document.getElementById('editor-title'); const textarea = document.getElementById('message-text'); const category = document.getElementById('message-category'); const priority = document.getElementById('message-priority'); if (messageData) { title.textContent = 'Edit Message'; textarea.value = messageData.text || ''; category.value = messageData.category || 'custom'; priority.value = messageData.priority || 'normal'; editor.dataset.editingId = messageData.id; } else { title.textContent = 'Add New Message'; textarea.value = ''; category.value = 'custom'; priority.value = 'normal'; delete editor.dataset.editingId; } editor.style.display = 'block'; textarea.focus(); this.updateCharacterCount(); }; TaskChallengeGame.prototype.hideMessageEditor = function() { const editor = document.getElementById('message-editor'); editor.style.display = 'none'; }; TaskChallengeGame.prototype.saveCurrentMessage = function() { const textarea = document.getElementById('message-text'); const category = document.getElementById('message-category'); const priority = document.getElementById('message-priority'); const editor = document.getElementById('message-editor'); const messageText = textarea.value.trim(); if (!messageText) { this.showNotification('Please enter a message text!', 'error'); return; } const messageData = { text: messageText, category: category.value, priority: priority.value }; if (editor.dataset.editingId) { // Edit existing message const updated = this.flashMessageManager.editMessage(parseInt(editor.dataset.editingId), messageData); if (updated) { this.showNotification('✅ Message updated successfully!', 'success'); } else { this.showNotification('❌ Failed to update message', 'error'); } } else { // Add new message const newMessage = this.flashMessageManager.addMessage(messageData); this.showNotification('✅ Message added successfully!', 'success'); } this.hideMessageEditor(); this.refreshMessageList(); }; TaskChallengeGame.prototype.previewCurrentMessage = function() { const textarea = document.getElementById('message-text'); const messageText = textarea.value.trim(); if (!messageText) { this.showNotification('Please enter a message to preview!', 'warning'); return; } this.flashMessageManager.previewMessage({ text: messageText }); }; TaskChallengeGame.prototype.updateCharacterCount = function() { const textarea = document.getElementById('message-text'); const counter = document.getElementById('char-count'); const currentLength = textarea.value.length; const maxLength = 200; counter.textContent = currentLength; const counterElement = counter.parentElement; counterElement.className = 'char-counter'; if (currentLength > maxLength * 0.9) { counterElement.className += ' warning'; } if (currentLength > maxLength) { counterElement.className += ' error'; } }; // Message action methods TaskChallengeGame.prototype.toggleMessage = function(messageId) { const enabled = this.flashMessageManager.toggleMessageEnabled(messageId); this.refreshMessageList(); this.showNotification(`Message ${enabled ? 'enabled' : 'disabled'}`, 'success'); }; TaskChallengeGame.prototype.editMessage = function(messageId) { const messages = this.flashMessageManager.getAllMessages(); const message = messages.find(msg => msg.id === messageId); if (message) { this.showMessageEditor(message); } }; TaskChallengeGame.prototype.previewMessage = function(messageId) { const messages = this.flashMessageManager.getAllMessages(); const message = messages.find(msg => msg.id === messageId); if (message) { this.flashMessageManager.previewMessage(message); } }; TaskChallengeGame.prototype.deleteMessage = function(messageId) { const messages = this.flashMessageManager.getAllMessages(); const message = messages.find(msg => msg.id === messageId); if (message && confirm(`Delete this message?\n\n"${message.text}"`)) { const deleted = this.flashMessageManager.deleteMessage(messageId); if (deleted) { this.showNotification('✅ Message deleted', 'success'); this.refreshMessageList(); } } }; TaskChallengeGame.prototype.getCategoryEmoji = function(category) { const emojis = { motivational: '💪', encouraging: '🌟', achievement: '🏆', persistence: '🔥', custom: '✨' }; return emojis[category] || '✨'; }; // Appearance Tab Management TaskChallengeGame.prototype.setupAppearanceTabListeners = function() { const controls = { position: document.getElementById('message-position'), animation: document.getElementById('animation-style'), fontSize: document.getElementById('font-size'), opacity: document.getElementById('message-opacity'), textColor: document.getElementById('text-color'), backgroundColor: document.getElementById('background-color'), resetBtn: document.getElementById('reset-appearance-btn'), previewBtn: document.getElementById('preview-appearance-btn') }; if (controls.position) { controls.position.onchange = (e) => this.updateFlashMessageSetting('position', e.target.value); } if (controls.animation) { controls.animation.onchange = (e) => this.updateFlashMessageSetting('animation', e.target.value); } if (controls.fontSize) { controls.fontSize.oninput = (e) => { const value = parseInt(e.target.value); document.getElementById('font-size-display').textContent = `${value}px`; this.updateFlashMessageSetting('fontSize', `${value}px`); }; } if (controls.opacity) { controls.opacity.oninput = (e) => { const value = parseInt(e.target.value); document.getElementById('opacity-display').textContent = `${value}%`; const bgColor = this.hexToRgba(controls.backgroundColor.value, value / 100); this.updateFlashMessageSetting('backgroundColor', bgColor); }; } if (controls.textColor) { controls.textColor.onchange = (e) => this.updateFlashMessageSetting('color', e.target.value); } if (controls.backgroundColor) { controls.backgroundColor.onchange = (e) => { const opacity = parseInt(controls.opacity.value) / 100; const bgColor = this.hexToRgba(e.target.value, opacity); this.updateFlashMessageSetting('backgroundColor', bgColor); }; } if (controls.resetBtn) { controls.resetBtn.onclick = () => this.resetAppearanceToDefaults(); } if (controls.previewBtn) { controls.previewBtn.onclick = () => this.previewAppearanceStyle(); } }; TaskChallengeGame.prototype.loadAppearanceTab = function() { const config = this.flashMessageManager.getConfig(); const controls = { position: document.getElementById('message-position'), animation: document.getElementById('animation-style'), fontSize: document.getElementById('font-size'), opacity: document.getElementById('message-opacity'), textColor: document.getElementById('text-color'), backgroundColor: document.getElementById('background-color') }; if (controls.position) controls.position.value = config.position; if (controls.animation) controls.animation.value = config.animation; if (controls.fontSize) { const fontSize = parseInt(config.fontSize) || 24; controls.fontSize.value = fontSize; document.getElementById('font-size-display').textContent = `${fontSize}px`; } // Extract opacity from backgroundColor if it's rgba let opacity = 90; let bgHex = '#007bff'; if (config.backgroundColor.includes('rgba')) { const rgbaMatch = config.backgroundColor.match(/rgba\((\d+),\s*(\d+),\s*(\d+),\s*([01]?\.?\d*)\)/); if (rgbaMatch) { opacity = Math.round(parseFloat(rgbaMatch[4]) * 100); bgHex = this.rgbToHex(parseInt(rgbaMatch[1]), parseInt(rgbaMatch[2]), parseInt(rgbaMatch[3])); } } if (controls.opacity) { controls.opacity.value = opacity; document.getElementById('opacity-display').textContent = `${opacity}%`; } if (controls.textColor) controls.textColor.value = config.color || '#ffffff'; if (controls.backgroundColor) controls.backgroundColor.value = bgHex; }; TaskChallengeGame.prototype.resetAppearanceToDefaults = function() { const defaults = gameData.defaultFlashConfig; this.flashMessageManager.updateConfig({ position: defaults.position, animation: defaults.animation, fontSize: defaults.fontSize, color: defaults.color, backgroundColor: defaults.backgroundColor }); this.loadAppearanceTab(); this.showNotification('✅ Appearance reset to defaults', 'success'); }; TaskChallengeGame.prototype.previewAppearanceStyle = function() { const messages = this.flashMessageManager.getMessages(); if (messages.length > 0) { const sampleMessage = messages[0]; this.flashMessageManager.previewMessage(sampleMessage); } else { this.flashMessageManager.previewMessage({ text: "This is a preview of your style settings!" }); } }; // Behavior Tab Management TaskChallengeGame.prototype.setupBehaviorTabListeners = function() { const controls = { focusInterruption: document.getElementById('focus-interruption-chance'), duration: document.getElementById('display-duration'), interval: document.getElementById('interval-delay'), variation: document.getElementById('time-variation'), eventBased: document.getElementById('event-based-messages'), pauseOnHover: document.getElementById('pause-on-hover'), testBtn: document.getElementById('test-behavior-btn') }; if (controls.focusInterruption) { controls.focusInterruption.oninput = (e) => { const value = parseInt(e.target.value); document.getElementById('focus-interruption-display').textContent = `${value}%`; this.gameState.focusInterruptionChance = value; // Save to localStorage for persistence this.dataManager.set('focusInterruptionChance', value); console.log(`🧘 Focus interruption chance set to: ${value}%`); }; } if (controls.duration) { controls.duration.oninput = (e) => { const value = parseInt(e.target.value); document.getElementById('duration-display').textContent = `${(value / 1000).toFixed(1)}s`; this.updateFlashMessageSetting('displayDuration', value); }; } if (controls.interval) { controls.interval.oninput = (e) => { const value = parseInt(e.target.value); document.getElementById('interval-display').textContent = `${Math.round(value / 1000)}s`; this.updateFlashMessageSetting('intervalDelay', value); }; } if (controls.variation) { controls.variation.oninput = (e) => { const value = parseInt(e.target.value); document.getElementById('variation-display').textContent = `±${Math.round(value / 1000)}s`; this.updateFlashMessageSetting('timeVariation', value); }; } if (controls.eventBased) { controls.eventBased.onchange = (e) => this.updateFlashMessageSetting('eventBasedMessages', e.target.checked); } if (controls.pauseOnHover) { controls.pauseOnHover.onchange = (e) => this.updateFlashMessageSetting('pauseOnHover', e.target.checked); } if (controls.testBtn) { controls.testBtn.onclick = () => this.testCurrentBehaviorSettings(); } }; TaskChallengeGame.prototype.loadBehaviorTab = function() { const config = this.flashMessageManager.getConfig(); const focusInterruptionSlider = document.getElementById('focus-interruption-chance'); const durationSlider = document.getElementById('display-duration'); const intervalSlider = document.getElementById('interval-delay'); const variationSlider = document.getElementById('time-variation'); const eventBasedCheckbox = document.getElementById('event-based-messages'); const pauseOnHoverCheckbox = document.getElementById('pause-on-hover'); // Load focus interruption setting if (focusInterruptionSlider) { const savedChance = this.dataManager.get('focusInterruptionChance') || 0; this.gameState.focusInterruptionChance = savedChance; focusInterruptionSlider.value = savedChance; document.getElementById('focus-interruption-display').textContent = `${savedChance}%`; console.log(`🧘 Focus interruption setting loaded: ${savedChance}%`); } if (durationSlider) { durationSlider.value = config.displayDuration; document.getElementById('duration-display').textContent = `${(config.displayDuration / 1000).toFixed(1)}s`; } if (intervalSlider) { intervalSlider.value = config.intervalDelay; document.getElementById('interval-display').textContent = `${Math.round(config.intervalDelay / 1000)}s`; } if (variationSlider) { const variation = config.timeVariation || 5000; variationSlider.value = variation; document.getElementById('variation-display').textContent = `±${Math.round(variation / 1000)}s`; } if (eventBasedCheckbox) { eventBasedCheckbox.checked = config.eventBasedMessages !== false; } if (pauseOnHoverCheckbox) { pauseOnHoverCheckbox.checked = config.pauseOnHover || false; } }; TaskChallengeGame.prototype.testCurrentBehaviorSettings = function() { this.showNotification('🧪 Testing behavior settings with 3 quick messages...', 'info'); let count = 0; const showTestMessage = () => { if (count >= 3) return; const messages = this.flashMessageManager.getMessages(); if (messages.length > 0) { const message = messages[Math.floor(Math.random() * messages.length)]; this.flashMessageManager.previewMessage(message); } count++; if (count < 3) { setTimeout(showTestMessage, 2000); } else { setTimeout(() => { this.showNotification('✅ Behavior test complete!', 'success'); }, this.flashMessageManager.getConfig().displayDuration + 500); } }; setTimeout(showTestMessage, 500); }; // Popup Images Tab Management TaskChallengeGame.prototype.setupPopupImagesTabListeners = function() { // Enable/disable toggle const enabledCheckbox = document.getElementById('popup-images-enabled'); if (enabledCheckbox) { enabledCheckbox.onchange = () => { const config = this.popupImageManager.getConfig(); config.enabled = enabledCheckbox.checked; this.popupImageManager.updateConfig(config); this.updatePopupImagesInfo(); }; } // Image count mode const countModeSelect = document.getElementById('popup-count-mode'); if (countModeSelect) { countModeSelect.onchange = () => { this.updatePopupCountControls(countModeSelect.value); const config = this.popupImageManager.getConfig(); config.imageCountMode = countModeSelect.value; this.popupImageManager.updateConfig(config); }; } // Fixed count slider const countSlider = document.getElementById('popup-image-count'); const countValue = document.getElementById('popup-image-count-value'); if (countSlider && countValue) { countSlider.oninput = () => { const count = parseInt(countSlider.value); countValue.textContent = count; const config = this.popupImageManager.getConfig(); config.imageCount = count; this.popupImageManager.updateConfig(config); this.updatePopupCountWarning(count); }; } // Range count inputs const minCountInput = document.getElementById('popup-min-count'); const maxCountInput = document.getElementById('popup-max-count'); if (minCountInput) { minCountInput.onchange = () => { const config = this.popupImageManager.getConfig(); config.minCount = parseInt(minCountInput.value); this.popupImageManager.updateConfig(config); }; } if (maxCountInput) { maxCountInput.onchange = () => { const config = this.popupImageManager.getConfig(); const maxCount = parseInt(maxCountInput.value); config.maxCount = maxCount; this.popupImageManager.updateConfig(config); this.updatePopupCountWarning(maxCount); }; } // Duration mode const durationModeSelect = document.getElementById('popup-duration-mode'); if (durationModeSelect) { durationModeSelect.onchange = () => { this.updatePopupDurationControls(durationModeSelect.value); const config = this.popupImageManager.getConfig(); config.durationMode = durationModeSelect.value; this.popupImageManager.updateConfig(config); }; } // Fixed duration slider const durationSlider = document.getElementById('popup-display-duration'); const durationValue = document.getElementById('popup-display-duration-value'); if (durationSlider && durationValue) { durationSlider.oninput = () => { durationValue.textContent = durationSlider.value + 's'; const config = this.popupImageManager.getConfig(); config.displayDuration = parseInt(durationSlider.value) * 1000; this.popupImageManager.updateConfig(config); }; } // Range duration inputs const minDurationInput = document.getElementById('popup-min-duration'); const maxDurationInput = document.getElementById('popup-max-duration'); if (minDurationInput) { minDurationInput.onchange = () => { const config = this.popupImageManager.getConfig(); config.minDuration = parseInt(minDurationInput.value) * 1000; this.popupImageManager.updateConfig(config); }; } if (maxDurationInput) { maxDurationInput.onchange = () => { const config = this.popupImageManager.getConfig(); config.maxDuration = parseInt(maxDurationInput.value) * 1000; this.popupImageManager.updateConfig(config); }; } // Positioning const positioningSelect = document.getElementById('popup-positioning'); if (positioningSelect) { positioningSelect.onchange = () => { const config = this.popupImageManager.getConfig(); config.positioning = positioningSelect.value; this.popupImageManager.updateConfig(config); }; } // Visual effect checkboxes const setupCheckbox = (id, configKey) => { const checkbox = document.getElementById(id); if (checkbox) { checkbox.onchange = () => { const config = this.popupImageManager.getConfig(); config[configKey] = checkbox.checked; this.popupImageManager.updateConfig(config); }; } }; setupCheckbox('popup-allow-overlap', 'allowOverlap'); setupCheckbox('popup-fade-animation', 'fadeAnimation'); setupCheckbox('popup-blur-background', 'blurBackground'); setupCheckbox('popup-show-timer', 'showTimer'); setupCheckbox('popup-prevent-close', 'preventClose'); // Test buttons const testSingleBtn = document.getElementById('test-popup-single'); if (testSingleBtn) { testSingleBtn.onclick = () => { this.popupImageManager.previewPunishmentPopups(1); setTimeout(() => this.updatePopupImagesInfo(), 100); }; } const testMultipleBtn = document.getElementById('test-popup-multiple'); if (testMultipleBtn) { testMultipleBtn.onclick = () => { this.popupImageManager.triggerPunishmentPopups(); setTimeout(() => this.updatePopupImagesInfo(), 100); }; } const clearAllBtn = document.getElementById('clear-all-popups'); if (clearAllBtn) { clearAllBtn.onclick = () => { this.popupImageManager.clearAllPopups(); setTimeout(() => this.updatePopupImagesInfo(), 100); }; } // Size control listeners const setupSizeSlider = (elementId, configKey, suffix = '') => { const slider = document.getElementById(elementId); const valueDisplay = document.getElementById(`${elementId}-value`); if (slider && valueDisplay) { slider.oninput = () => { const value = parseInt(slider.value); valueDisplay.textContent = value + suffix; const config = this.popupImageManager.getConfig(); config[configKey] = configKey.includes('viewport') ? value / 100 : value; this.popupImageManager.updateConfig(config); }; } }; const setupSizeInput = (elementId, configKey) => { const input = document.getElementById(elementId); if (input) { input.onchange = () => { const value = parseInt(input.value); if (!isNaN(value)) { const config = this.popupImageManager.getConfig(); config[configKey] = value; this.popupImageManager.updateConfig(config); } }; } }; setupSizeSlider('popup-viewport-width', 'viewportWidthRatio', '%'); setupSizeSlider('popup-viewport-height', 'viewportHeightRatio', '%'); setupSizeInput('popup-min-width', 'minWidth'); setupSizeInput('popup-max-width', 'maxWidth'); setupSizeInput('popup-min-height', 'minHeight'); setupSizeInput('popup-max-height', 'maxHeight'); }; TaskChallengeGame.prototype.updatePopupCountControls = function(mode) { const fixedDiv = document.getElementById('popup-fixed-count'); const rangeDiv = document.getElementById('popup-range-count'); if (fixedDiv) fixedDiv.style.display = mode === 'fixed' ? 'block' : 'none'; if (rangeDiv) rangeDiv.style.display = mode === 'range' ? 'block' : 'none'; }; TaskChallengeGame.prototype.updatePopupDurationControls = function(mode) { const fixedDiv = document.getElementById('popup-fixed-duration'); const rangeDiv = document.getElementById('popup-range-duration'); if (fixedDiv) fixedDiv.style.display = mode === 'fixed' ? 'block' : 'none'; if (rangeDiv) rangeDiv.style.display = mode === 'range' ? 'block' : 'none'; }; TaskChallengeGame.prototype.loadPopupImagesSettings = function() { const config = this.popupImageManager.getConfig(); // Enable/disable const enabledCheckbox = document.getElementById('popup-images-enabled'); if (enabledCheckbox) enabledCheckbox.checked = config.enabled; // Count settings const countModeSelect = document.getElementById('popup-count-mode'); if (countModeSelect) countModeSelect.value = config.imageCountMode; const countSlider = document.getElementById('popup-image-count'); const countValue = document.getElementById('popup-image-count-value'); if (countSlider) countSlider.value = config.imageCount; if (countValue) countValue.textContent = config.imageCount; const minCountInput = document.getElementById('popup-min-count'); const maxCountInput = document.getElementById('popup-max-count'); if (minCountInput) minCountInput.value = config.minCount; if (maxCountInput) maxCountInput.value = config.maxCount; // Duration settings const durationModeSelect = document.getElementById('popup-duration-mode'); if (durationModeSelect) durationModeSelect.value = config.durationMode; const durationSlider = document.getElementById('popup-display-duration'); const durationValue = document.getElementById('popup-display-duration-value'); if (durationSlider) durationSlider.value = config.displayDuration / 1000; if (durationValue) durationValue.textContent = (config.displayDuration / 1000) + 's'; const minDurationInput = document.getElementById('popup-min-duration'); const maxDurationInput = document.getElementById('popup-max-duration'); if (minDurationInput) minDurationInput.value = config.minDuration / 1000; if (maxDurationInput) maxDurationInput.value = config.maxDuration / 1000; // Positioning const positioningSelect = document.getElementById('popup-positioning'); if (positioningSelect) positioningSelect.value = config.positioning; // Visual effects const checkboxes = { 'popup-allow-overlap': config.allowOverlap, 'popup-fade-animation': config.fadeAnimation, 'popup-blur-background': config.blurBackground, 'popup-show-timer': config.showTimer, 'popup-prevent-close': config.preventClose }; Object.entries(checkboxes).forEach(([id, value]) => { const checkbox = document.getElementById(id); if (checkbox) checkbox.checked = value; }); // Size settings const viewportWidthSlider = document.getElementById('popup-viewport-width'); const viewportWidthValue = document.getElementById('popup-viewport-width-value'); if (viewportWidthSlider) viewportWidthSlider.value = (config.viewportWidthRatio || 0.35) * 100; if (viewportWidthValue) viewportWidthValue.textContent = Math.round((config.viewportWidthRatio || 0.35) * 100) + '%'; const viewportHeightSlider = document.getElementById('popup-viewport-height'); const viewportHeightValue = document.getElementById('popup-viewport-height-value'); if (viewportHeightSlider) viewportHeightSlider.value = (config.viewportHeightRatio || 0.4) * 100; if (viewportHeightValue) viewportHeightValue.textContent = Math.round((config.viewportHeightRatio || 0.4) * 100) + '%'; const sizeInputs = { 'popup-min-width': config.minWidth || 200, 'popup-max-width': config.maxWidth || 500, 'popup-min-height': config.minHeight || 150, 'popup-max-height': config.maxHeight || 400 }; Object.entries(sizeInputs).forEach(([id, value]) => { const input = document.getElementById(id); if (input) input.value = value; }); // Update control visibility this.updatePopupCountControls(config.imageCountMode); this.updatePopupDurationControls(config.durationMode); // Update info display and warnings this.updatePopupImagesInfo(); // Check for high count warnings const currentCount = config.imageCountMode === 'fixed' ? config.imageCount : config.maxCount; this.updatePopupCountWarning(currentCount); }; TaskChallengeGame.prototype.updatePopupImagesInfo = function() { const availableCountEl = document.getElementById('available-images-count'); const activeCountEl = document.getElementById('active-popups-count'); if (availableCountEl) { const availableImages = this.popupImageManager.getAvailableImages(); availableCountEl.textContent = availableImages.length; } if (activeCountEl) { const activeCount = this.popupImageManager.getActiveCount(); activeCountEl.textContent = activeCount; } }; TaskChallengeGame.prototype.updatePopupCountWarning = function(count) { const warningEl = document.getElementById('popup-warning'); if (warningEl) { if (count > 20) { warningEl.style.display = 'block'; } else { warningEl.style.display = 'none'; } } }; // Import/Export Tab Management TaskChallengeGame.prototype.setupImportExportTabListeners = function() { const exportAllBtn = document.getElementById('export-all-messages-btn'); const exportEnabledBtn = document.getElementById('export-enabled-messages-btn'); const exportCustomBtn = document.getElementById('export-custom-messages-btn'); const importBtn = document.getElementById('import-messages-btn'); const importFile = document.getElementById('import-messages-file'); const resetDefaultsBtn = document.getElementById('reset-to-defaults-btn'); const clearAllBtn = document.getElementById('clear-all-messages-btn'); if (exportAllBtn) { exportAllBtn.onclick = () => this.exportMessages('all'); } if (exportEnabledBtn) { exportEnabledBtn.onclick = () => this.exportMessages('enabled'); } if (exportCustomBtn) { exportCustomBtn.onclick = () => this.exportMessages('custom'); } if (importBtn) { importBtn.onclick = () => importFile.click(); } if (importFile) { importFile.onchange = (e) => this.handleMessageImport(e); } if (resetDefaultsBtn) { resetDefaultsBtn.onclick = () => this.resetMessagesToDefaults(); } if (clearAllBtn) { clearAllBtn.onclick = () => this.clearAllMessages(); } }; TaskChallengeGame.prototype.loadImportExportTab = function() { // No specific loading needed for this tab }; TaskChallengeGame.prototype.exportMessages = function(type) { let includeDisabled = true; let customOnly = false; let filename = 'flash_messages_all.json'; switch(type) { case 'enabled': includeDisabled = false; filename = 'flash_messages_enabled.json'; break; case 'custom': customOnly = true; filename = 'flash_messages_custom.json'; break; } const exportData = this.flashMessageManager.exportMessages(includeDisabled, customOnly); this.downloadFile(exportData, filename, 'application/json'); this.showNotification(`✅ Messages exported to ${filename}`, 'success'); }; TaskChallengeGame.prototype.handleMessageImport = function(event) { const file = event.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (e) => { try { const importMode = document.querySelector('input[name="importMode"]:checked').value; const result = this.flashMessageManager.importMessages(e.target.result, importMode); if (result.success) { this.showNotification(`✅ Successfully imported ${result.imported} messages (${result.total} total)`, 'success'); if (this.getCurrentAnnoyanceTab() === 'messages') { this.refreshMessageList(); } } else { this.showNotification(`❌ Import failed: ${result.error}`, 'error'); } } catch (error) { this.showNotification('❌ Invalid file format', 'error'); } }; reader.readAsText(file); // Clear the file input event.target.value = ''; }; TaskChallengeGame.prototype.resetMessagesToDefaults = function() { if (confirm('Reset to default messages? This will remove all custom messages and cannot be undone.')) { const result = this.flashMessageManager.resetToDefaults(); this.showNotification(`✅ Reset to ${result.messages} default messages`, 'success'); if (this.getCurrentAnnoyanceTab() === 'messages') { this.refreshMessageList(); } } }; TaskChallengeGame.prototype.clearAllMessages = function() { if (confirm('Clear ALL messages? This will remove every message and cannot be undone. You will need to import messages to use the flash message system.')) { this.flashMessageManager.updateMessages([]); this.showNotification('⚠️ All messages cleared', 'warning'); if (this.getCurrentAnnoyanceTab() === 'messages') { this.refreshMessageList(); } } }; // Utility Methods TaskChallengeGame.prototype.updateFlashMessageSetting = function(setting, value) { const currentConfig = this.flashMessageManager.getConfig(); const newConfig = { ...currentConfig, [setting]: value }; this.flashMessageManager.updateConfig(newConfig); console.log(`Updated flash message setting: ${setting} = ${value}`); }; TaskChallengeGame.prototype.saveAllAnnoyanceSettings = function() { this.showNotification('✅ All annoyance settings saved!', 'success'); }; TaskChallengeGame.prototype.loadAnnoyanceSettings = function() { // This method is called when the annoyance screen first loads // Individual tabs will load their specific settings when shown }; TaskChallengeGame.prototype.getCurrentAnnoyanceTab = function() { const activeTab = document.querySelector('.annoyance-tab.active'); return activeTab ? activeTab.id.replace('-tab', '') : 'messages'; }; TaskChallengeGame.prototype.downloadFile = function(content, filename, mimeType) { const blob = new Blob([content], { type: mimeType }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = filename; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); }; TaskChallengeGame.prototype.hexToRgba = function(hex, alpha) { const r = parseInt(hex.slice(1, 3), 16); const g = parseInt(hex.slice(3, 5), 16); const b = parseInt(hex.slice(5, 7), 16); return `rgba(${r}, ${g}, ${b}, ${alpha})`; }; TaskChallengeGame.prototype.rgbToHex = function(r, g, b) { return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1); }; // ======================================== // AI Tasks Tab Management // ======================================== TaskChallengeGame.prototype.setupAITasksTabListeners = function() { // Test Connection Button const testConnectionBtn = document.getElementById('test-connection'); if (testConnectionBtn) { testConnectionBtn.onclick = async () => { const btn = testConnectionBtn; const originalText = btn.textContent; btn.textContent = 'Testing...'; btn.disabled = true; try { if (this.aiTaskManager) { const isConnected = await this.aiTaskManager.testConnection(); this.updateConnectionStatus(); if (isConnected) { this.flashMessageManager.showMessage('✅ Connected to Ollama successfully!'); } else { this.flashMessageManager.showMessage('❌ Cannot connect to Ollama. Check if it\'s running.'); } } else { this.flashMessageManager.showMessage('⚠️ AI Manager not initialized'); } } catch (error) { this.flashMessageManager.showMessage('❌ Connection test failed: ' + error.message); } finally { btn.textContent = originalText; btn.disabled = false; } }; } // Generate Test Task Button const generateTestBtn = document.getElementById('generate-test-task'); if (generateTestBtn) { generateTestBtn.onclick = async () => { const btn = generateTestBtn; const preview = document.getElementById('test-task-output'); const originalText = btn.textContent; if (!preview) { console.error('Test task output element not found'); this.flashMessageManager.showMessage('❌ UI element not found'); return; } btn.textContent = 'Generating...'; btn.disabled = true; preview.className = 'task-preview generating'; preview.textContent = 'AI is creating a personalized edging task...'; try { if (this.aiTaskManager) { const task = await this.aiTaskManager.generateEdgingTask(); preview.className = 'task-preview'; preview.textContent = task.instruction || task.description || task; this.flashMessageManager.showMessage('🎯 Test task generated successfully!'); } else { throw new Error('AI Manager not initialized'); } } catch (error) { preview.className = 'task-preview error'; preview.textContent = `Error generating task: ${error.message}`; this.flashMessageManager.showMessage('❌ Failed to generate test task'); } finally { btn.textContent = originalText; btn.disabled = false; } }; } // Model Selection Change const modelSelect = document.getElementById('ai-model'); if (modelSelect) { modelSelect.onchange = (e) => { const model = e.target.value; if (this.aiTaskManager) { this.aiTaskManager.updateSettings({ model: model }); this.flashMessageManager.showMessage(`🔄 Switched to model: ${model}`); } }; } // Temperature Slider const tempSlider = document.getElementById('ai-temperature'); const tempValue = document.getElementById('temp-value'); if (tempSlider && tempValue) { tempSlider.oninput = (e) => { const temperature = parseFloat(e.target.value); tempValue.textContent = temperature.toFixed(1); if (this.aiTaskManager) { this.aiTaskManager.updateSettings({ temperature: temperature }); } }; } // Max Tokens Input const maxTokensInput = document.getElementById('ai-max-tokens'); if (maxTokensInput) { maxTokensInput.onchange = (e) => { const maxTokens = parseInt(e.target.value); if (this.aiTaskManager) { this.aiTaskManager.updateSettings({ maxTokens: maxTokens }); } }; } // Difficulty Level Change const difficultySelect = document.getElementById('task-difficulty'); if (difficultySelect) { difficultySelect.onchange = (e) => { const difficulty = e.target.value; if (this.aiTaskManager) { this.aiTaskManager.updateSettings({ difficulty: difficulty }); this.flashMessageManager.showMessage(`🎯 Difficulty set to: ${difficulty}`); } }; } // Personal Preferences Textarea const prefsTextarea = document.getElementById('personal-preferences'); if (prefsTextarea) { prefsTextarea.oninput = this.debounce((e) => { const preferences = e.target.value; if (this.aiTaskManager) { this.aiTaskManager.updateSettings({ personalPreferences: preferences }); } }, 500); } // Enable AI Toggle const aiToggle = document.getElementById('enable-ai'); if (aiToggle) { aiToggle.onchange = (e) => { const enabled = e.target.checked; if (this.aiTaskManager) { this.aiTaskManager.updateSettings({ enabled: enabled }); this.flashMessageManager.showMessage(`🤖 AI Tasks ${enabled ? 'enabled' : 'disabled'}`); // Update UI based on toggle const configSection = document.querySelector('.ai-config'); if (configSection) { configSection.style.opacity = enabled ? '1' : '0.6'; } } }; } // Auto-generate Toggle const autoGenToggle = document.getElementById('auto-generate'); if (autoGenToggle) { autoGenToggle.onchange = (e) => { const autoGenerate = e.target.checked; if (this.aiTaskManager) { this.aiTaskManager.updateSettings({ autoGenerate: autoGenerate }); this.flashMessageManager.showMessage(`🔄 Auto-generate ${autoGenerate ? 'enabled' : 'disabled'}`); } }; } }; TaskChallengeGame.prototype.loadAITasksTab = function() { if (!this.aiTaskManager) return; const settings = this.aiTaskManager.getSettings(); // Load model selection const modelSelect = document.getElementById('ai-model'); if (modelSelect) { modelSelect.value = settings.model || 'llama3.2'; } // Load temperature const tempSlider = document.getElementById('ai-temperature'); const tempValue = document.getElementById('temp-value'); if (tempSlider && tempValue) { tempSlider.value = settings.temperature || 0.7; tempValue.textContent = (settings.temperature || 0.7).toFixed(1); } // Load max tokens const maxTokensInput = document.getElementById('ai-max-tokens'); if (maxTokensInput) { maxTokensInput.value = settings.maxTokens || 200; } // Load difficulty const difficultySelect = document.getElementById('task-difficulty'); if (difficultySelect) { difficultySelect.value = settings.difficulty || 'medium'; } // Load personal preferences const prefsTextarea = document.getElementById('personal-preferences'); if (prefsTextarea) { prefsTextarea.value = settings.personalPreferences || ''; } // Load toggles const aiToggle = document.getElementById('enable-ai'); if (aiToggle) { aiToggle.checked = settings.enabled !== false; } const autoGenToggle = document.getElementById('auto-generate'); if (autoGenToggle) { autoGenToggle.checked = settings.autoGenerate === true; } // Update UI state const configSection = document.querySelector('.ai-config'); if (configSection) { configSection.style.opacity = settings.enabled !== false ? '1' : '0.6'; } // Update connection status (async) this.updateConnectionStatus().catch(error => { console.error('Failed to update connection status:', error); }); }; TaskChallengeGame.prototype.updateConnectionStatus = async function() { const statusValue = document.getElementById('connection-status'); const modelStatus = document.getElementById('model-status'); if (!statusValue || !this.aiTaskManager) { if (statusValue) { statusValue.textContent = 'Not Available'; statusValue.className = 'status-value disconnected'; } if (modelStatus) { modelStatus.textContent = 'N/A'; modelStatus.className = 'status-value'; } return; } try { // Set loading state statusValue.textContent = 'Checking...'; statusValue.className = 'status-value'; // Force a fresh connection test const isConnected = await this.aiTaskManager.testConnection(); const settings = this.aiTaskManager.getSettings(); if (isConnected) { statusValue.textContent = 'Connected'; statusValue.className = 'status-value connected'; if (modelStatus) { modelStatus.textContent = settings.model || this.aiTaskManager.currentModel || 'wizardlm-uncensored:13b'; modelStatus.className = 'status-value'; } } else { statusValue.textContent = 'Disconnected'; statusValue.className = 'status-value disconnected'; if (modelStatus) { modelStatus.textContent = 'N/A'; modelStatus.className = 'status-value'; } } } catch (error) { console.error('Error updating connection status:', error); statusValue.textContent = 'Error'; statusValue.className = 'status-value disconnected'; if (modelStatus) { modelStatus.textContent = 'N/A'; modelStatus.className = 'status-value'; } } }; // Utility function for debouncing input TaskChallengeGame.prototype.debounce = function(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func.apply(this, args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; }; // Initialize game when page loads document.addEventListener('DOMContentLoaded', () => { console.log('🎮 Starting game initialization...'); // Show loading overlay immediately const overlay = document.getElementById('loading-overlay'); if (overlay) { overlay.style.display = 'flex'; overlay.classList.add('visible'); } // Disable all interactive elements during initialization const disableElements = () => { const buttons = document.querySelectorAll('button'); const inputs = document.querySelectorAll('input, select, textarea'); const clickables = document.querySelectorAll('[onclick], .clickable, .tab-btn, .choice-btn'); [...buttons, ...inputs, ...clickables].forEach(element => { element.disabled = true; element.style.pointerEvents = 'none'; element.classList.add('loading-disabled'); }); }; // Re-enable all interactive elements after initialization const enableElements = () => { const buttons = document.querySelectorAll('button'); const inputs = document.querySelectorAll('input, select, textarea'); const clickables = document.querySelectorAll('[onclick], .clickable, .tab-btn, .choice-btn'); [...buttons, ...inputs, ...clickables].forEach(element => { element.disabled = false; element.style.pointerEvents = ''; element.classList.remove('loading-disabled'); }); }; disableElements(); // Validate dependencies before creating game if (!validateDependencies()) { console.error('❌ Critical dependencies missing, cannot start game'); return; } try { window.game = new TaskChallengeGame(); console.log('✅ Game instance created successfully'); // Wait for initialization to complete const checkInitialization = () => { if (window.game && window.game.isInitialized) { enableElements(); console.log('✅ Game fully initialized and ready for interaction'); } else { setTimeout(checkInitialization, 100); } }; checkInitialization(); } catch (error) { console.error('❌ Failed to create game instance:', error); enableElements(); // Re-enable on error return; } // Initialize game mode manager after DOM is fully ready setTimeout(() => { if (window.gameModeManager) { console.log('🎮 Initializing GameModeManager...'); window.gameModeManager.init(); console.log('🎮 Game Mode Manager initialization complete'); } else { console.error('❌ GameModeManager not found on window'); } }, 1000); // Give more time for DOM to be ready });