// 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(options = {}) {
// Show loading overlay immediately
this.showLoadingOverlay();
this.loadingProgress = 0;
this.isInitialized = false;
this.options = options; // Store options for later use
// 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 (unchanged)
webcamBonuses: 0, // x2 multiplier per minute of webcam
photoRewards: 0, // 2 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 Audio Management System
this.audioManager = new AudioManager(this.dataManager, this);
this.updateLoadingProgress(65, '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(this.options);
// Simple override for desktop mode - use linked images instead of old discovery
if (typeof DesktopFileManager !== 'undefined' && window.electronAPI) {
console.log('🖥️ Desktop mode detected - using linked images');
// Give desktop file manager time to scan, then load linked images
setTimeout(async () => {
try {
const linkedImages = await this.getLinkedImages();
// Store linked images globally for access by other components
window.linkedImages = linkedImages;
// Separate images into task and consequence categories if needed
// For now, use all linked images as task images
const taskImages = linkedImages.map(img => img.path);
const consequenceImages = []; // Could implement categorization later
gameData.discoveredTaskImages = taskImages;
gameData.discoveredConsequenceImages = consequenceImages;
console.log(`📸 Desktop mode - Linked images loaded: ${gameData.discoveredTaskImages.length} task images, ${gameData.discoveredConsequenceImages.length} consequence images`);
} catch (error) {
console.error('📸 Error loading linked images:', error);
// Fallback to empty arrays
gameData.discoveredTaskImages = [];
gameData.discoveredConsequenceImages = [];
}
this.imageDiscoveryComplete = true;
// Only show start screen if it exists (not in dedicated mode like Quick Play)
if (document.getElementById('start-screen')) {
this.showScreen('start-screen');
} else {
console.log('📱 Running in dedicated screen mode - skipping start screen display');
}
}, 3000);
} else {
this.discoverImages().then(() => {
// Only show start screen if it exists (not in dedicated mode like Quick Play)
if (document.getElementById('start-screen')) {
this.showScreen('start-screen');
} else {
console.log('📱 Running in dedicated screen mode - skipping start screen display');
}
}).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);
await this.fileManager.init();
window.desktopFileManager = this.fileManager;
// Auto-scan directories on startup
setTimeout(async () => {
if (this.fileManager && this.fileManager.isElectron) {
console.log('🔍 Auto-scanning directories on startup...');
try {
// Refresh all linked directories instead of scanning all
await this.fileManager.refreshAllDirectories();
// Reload video player manager after scanning
if (window.videoPlayerManager && window.videoPlayerManager.loadVideoFiles) {
console.log('🔄 Reloading video library after directory scan...');
await window.videoPlayerManager.loadVideoFiles();
}
} catch (error) {
console.warn('⚠️ Auto-scan failed:', error);
}
}
}, 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);
}
/**
* Get all images from linked directories and individual files (same as main library)
*/
async getLinkedImages() {
const allImages = [];
try {
// Get linked directories
let linkedDirs;
try {
linkedDirs = JSON.parse(localStorage.getItem('linkedImageDirectories') || '[]');
if (!Array.isArray(linkedDirs)) {
linkedDirs = [];
}
} catch (e) {
console.log('Error parsing linkedImageDirectories:', e);
linkedDirs = [];
}
// Get individual linked images
let linkedIndividualImages;
try {
linkedIndividualImages = JSON.parse(localStorage.getItem('linkedIndividualImages') || '[]');
if (!Array.isArray(linkedIndividualImages)) {
linkedIndividualImages = [];
}
} catch (e) {
console.log('Error parsing linkedIndividualImages:', e);
linkedIndividualImages = [];
}
console.log(`📸 Game found ${linkedDirs.length} linked directories and ${linkedIndividualImages.length} individual images`);
// Load images from linked directories using Electron API
if (window.electronAPI && linkedDirs.length > 0) {
const imageExtensions = /\.(jpg|jpeg|png|gif|webp|bmp)$/i;
for (const dir of linkedDirs) {
try {
console.log(`📸 Scanning directory: ${dir.path}`);
let files = [];
if (window.electronAPI.readDirectory) {
const result = window.electronAPI.readDirectory(dir.path);
// Handle both sync and async results
if (result && typeof result.then === 'function') {
try {
files = await result;
console.log(`📸 Async result - found ${files ? files.length : 0} files`);
} catch (asyncError) {
console.error(`📸 Async error for ${dir.path}:`, asyncError);
continue;
}
} else if (Array.isArray(result)) {
files = result;
console.log(`📸 Sync result - found ${files.length} files`);
} else {
console.error(`📸 Unexpected result type for ${dir.path}:`, typeof result);
continue;
}
if (files && Array.isArray(files)) {
const imageFiles = files.filter(file => imageExtensions.test(file.name));
console.log(`📸 Found ${imageFiles.length} image files in ${dir.name}`);
imageFiles.forEach(file => {
allImages.push({
name: file.name,
path: file.path,
category: 'directory',
directory: dir.name
});
});
} else {
console.log(`📸 No valid files array for ${dir.path}`);
}
}
} catch (error) {
console.error(`📸 Error loading images from directory ${dir.path}:`, error);
}
}
}
// Add individual linked images
linkedIndividualImages.forEach(image => {
allImages.push({
name: image.name || 'Unknown Image',
path: image.path,
category: 'individual',
directory: 'Individual Images'
});
});
console.log(`📸 Game loaded ${allImages.length} total linked images`);
} catch (error) {
console.error('📸 Error loading linked images in Game:', error);
}
return allImages;
}
initializeCustomTasks(options = {}) {
// 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(options);
// Filter out scenario tasks if disabled in options
if (options && options.includeScenarioTasks === false) {
console.log('🎭 Filtering out scenario tasks from game data');
const originalCount = gameData.mainTasks.length;
gameData.mainTasks = gameData.mainTasks.filter(task => {
// Remove tasks with scenario-adventure interactive type
if (task.interactiveType === 'scenario-adventure') {
console.log('🗑️ Removing scenario task:', task.id || task.text);
return false;
}
return true;
});
console.log(`📊 Filtered tasks: ${originalCount} → ${gameData.mainTasks.length}`);
}
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}`));
// Debug: check for scenario tasks specifically
const scenarioTasks = gameData.mainTasks.filter(t => t.interactiveType === 'scenario-adventure');
console.log(`🎭 Scenario tasks in final data: ${scenarioTasks.length}`, scenarioTasks.map(t => t.id || t.text));
}
addInteractiveTasksToGameData(options = {}) {
// Check configuration options (default: include all for backward compatibility)
const includeScenarioTasks = options.includeScenarioTasks !== false;
const includeStandardTasks = options.includeStandardTasks !== false;
console.log('🎯 Task filtering options:', options);
console.log('🎭 Include scenario tasks:', includeScenarioTasks);
console.log('📝 Include standard tasks:', includeStandardTasks);
// Define our interactive tasks
const interactiveTasks = [];
// Add mirror task (considered a standard interactive task)
if (includeStandardTasks) {
// Mirror task is already in gameData.js, so we don't need to add it here
console.log('🪞 Mirror tasks enabled (from gameData.js)');
}
// Only add scenario tasks if enabled
if (includeScenarioTasks) {
console.log('🎭 Scenario tasks enabled - adding scenario adventures');
interactiveTasks.push(
{
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: 'verification-required',
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.'",
verificationInstructions: "Strip completely and get on all fours in a humiliating position",
verificationDuration: 90,
verificationText: "Hold this humiliating all-fours position while contemplating your disobedience",
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: 'verification-required',
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.",
verificationInstructions: "Kneel with knees spread wide, hands behind head, arch your back shamefully",
verificationDuration: 40,
verificationText: "Maintain this humiliating presentation position while thinking about what a pathetic display you're making",
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: 'verification-required',
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.",
verificationInstructions: "Get on hands and knees in a crawling position like an animal",
verificationDuration: 50,
verificationText: "Maintain this animal-like crawling position while feeling how degrading it is",
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: 'verification-required',
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.",
verificationInstructions: "Get in a shameful presentation position - spread wide, arch your back, present yourself",
verificationDuration: 45,
verificationText: "Maintain this degrading position while feeling the shame wash over you",
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: 'verification-required',
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.",
verificationInstructions: "Stand on your toes with hands behind head in a stress position",
verificationDuration: 55,
verificationText: "Maintain this challenging stress position to learn what happens to those who resist",
nextStep: "resistance_broken"
},
position_training: {
type: 'verification-required',
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.",
verificationInstructions: "Start in kneeling position with hands on thighs - demonstrate proper 'present' position",
verificationDuration: 40,
verificationText: "Hold the proper 'present' position to show you understand position command training",
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"
}
); // Close the interactiveTasks.push
} else {
console.log('🎭 Scenario tasks disabled');
}
// 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('� Discovering images from directories...');
try {
gameData.discoveredTaskImages = [];
gameData.discoveredConsequenceImages = [];
// Use fileManager to discover images
if (this.fileManager && this.fileManager.isElectron) {
// Discovery is handled by the main initialization process
console.log('📸 Image discovery delegated to file manager');
} else {
console.log('⚠️ File manager not available for image discovery');
}
this.imageDiscoveryComplete = true;
console.log('✅ Image discovery completed');
} catch (error) {
console.error('❌ Error during image discovery:', error);
this.imageDiscoveryComplete = true; // Mark as complete even on error
}
}
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;
}
// 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() {
// Helper function to safely add event listeners
const safeAddListener = (id, event, handler) => {
const element = document.getElementById(id);
if (element) {
element.addEventListener(event, handler);
} else {
console.log(`⚠️ Element '${id}' not found, skipping event listener`);
}
};
// Screen navigation
safeAddListener('start-btn', 'click', () => this.startGame());
safeAddListener('resume-btn', 'click', () => this.resumeGame());
safeAddListener('quit-btn', 'click', () => this.quitGame());
safeAddListener('play-again-btn', 'click', () => this.resetGame());
// Game mode selection
this.initializeGameModeListeners();
// Game actions - support both main game and Quick Play button IDs
// Skip setting up handlers if in Quick Play mode (Quick Play handles its own)
if (!this.gameState.isQuickPlay) {
safeAddListener('complete-btn', 'click', () => this.completeTask());
safeAddListener('complete-task', 'click', () => this.completeTask());
safeAddListener('skip-btn', 'click', () => this.skipTask());
safeAddListener('skip-task', 'click', () => this.skipTask());
safeAddListener('mercy-skip-btn', 'click', () => this.mercySkip());
} else {
console.log('🎮 Quick Play mode detected - skipping main game button handlers');
}
safeAddListener('pause-btn', 'click', () => this.pauseGame());
// Theme selector
safeAddListener('theme-dropdown', 'change', (e) => this.changeTheme(e.target.value));
// Options menu toggle
safeAddListener('options-menu-btn', 'click', () => this.toggleOptionsMenu());
// Window cleanup - stop audio when app is closed
let audioCleanupDone = false;
window.addEventListener('beforeunload', () => {
if (!audioCleanupDone && !window.isForceExiting) {
console.log('Window closing - stopping all audio');
audioCleanupDone = true;
try {
this.audioManager.stopAllImmediate();
} catch (error) {
console.warn('Error during audio cleanup:', error);
}
}
});
// Music controls
safeAddListener('music-toggle', 'click', () => this.toggleMusic());
safeAddListener('music-toggle-compact', 'click', (e) => {
e.stopPropagation(); // Prevent event bubbling
// The hover panel will show automatically, just indicate it's interactive
});
safeAddListener('loop-btn', 'click', () => this.toggleLoop());
safeAddListener('shuffle-btn', 'click', () => this.toggleShuffle());
safeAddListener('track-selector', 'change', (e) => this.changeTrack(parseInt(e.target.value)));
safeAddListener('volume-slider', 'input', (e) => this.changeVolume(parseInt(e.target.value)));
// Task management - Now handled in Quick Play
// safeAddListener('manage-tasks-btn', 'click', () => this.showTaskManagement());
safeAddListener('back-to-start-btn', 'click', () => this.showScreen('start-screen'));
// Legacy task management functions - disabled as task management moved to Quick Play
// safeAddListener('add-task-btn', 'click', () => this.addNewTask());
// safeAddListener('reset-tasks-btn', 'click', () => this.resetToDefaultTasks());
// safeAddListener('main-tasks-tab', 'click', () => this.showTaskTab('main'));
// safeAddListener('consequence-tasks-tab', 'click', () => this.showTaskTab('consequence'));
// safeAddListener('new-task-type', 'change', () => this.toggleDifficultyDropdown());
// Data management
safeAddListener('export-btn', 'click', () => this.exportData());
safeAddListener('import-btn', 'click', () => this.importData());
safeAddListener('import-file', 'change', (e) => this.handleFileImport(e));
safeAddListener('stats-btn', 'click', () => this.showStats());
safeAddListener('help-btn', 'click', () => this.showHelp());
safeAddListener('close-stats', 'click', () => this.hideStats());
safeAddListener('close-help', 'click', () => this.hideHelp());
safeAddListener('reset-stats-btn', 'click', () => this.resetStats());
safeAddListener('export-stats-btn', 'click', () => this.exportStatsOnly());
// Audio controls
this.initializeAudioControls();
// Image management - only the main button, others will be attached when screen is shown
safeAddListener('manage-images-btn', 'click', () => this.showImageManagement());
// Audio management - only the main button, others will be attached when screen is shown
safeAddListener('manage-audio-btn', 'click', () => this.showAudioManagement());
// Photo gallery management
safeAddListener('photo-gallery-btn', 'click', () => this.showPhotoGallery());
// 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() {
// Check if audio controls exist before trying to initialize them
const masterVolumeSlider = document.getElementById('master-volume');
if (!masterVolumeSlider) {
console.log('⚠️ Audio controls not found, skipping audio control initialization');
return;
}
// Volume sliders
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();
});
});
// 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 allConfigs = document.querySelectorAll('.mode-config');
allConfigs.forEach(config => config.style.display = 'none');
console.log(`Game state updated:`, {
gameMode: this.gameState.gameMode,
timeLimit: this.gameState.timeLimit
});
}
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)
// Initialize scenarioXp if not already done (for Quick Play compatibility)
if (!this.gameState.scenarioXp) {
this.initializeScenarioXp();
}
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
// Initialize scenarioXp if not already done (for Quick Play compatibility)
if (!this.gameState.scenarioXp) {
this.initializeScenarioXp();
}
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 XP for webcam mirror activity (x2 multiplier = 1 XP per minute)
// Initialize scenarioXp if not already done (for Quick Play compatibility)
if (!this.gameState.scenarioXp) {
this.initializeScenarioXp();
}
if (!this.gameState.scenarioTracking.isInWebcamActivity || this.gameState.isPaused) return;
const now = Date.now();
const timeSinceLastAward = now - this.gameState.scenarioTracking.lastWebcamXpAwarded;
const oneMinuteMs = 60 * 1000; // 1 minute in milliseconds
if (timeSinceLastAward >= oneMinuteMs) {
const periods = Math.floor(timeSinceLastAward / oneMinuteMs);
const xpToAward = periods * 1; // 1 XP per minute (x2 multiplier of base rate)
this.gameState.scenarioXp.webcamBonuses += xpToAward;
this.gameState.scenarioTracking.lastWebcamXpAwarded = now;
this.updateScenarioTotalXp();
console.log(`📹 Webcam bonus XP: +${xpToAward} (${periods} x 1min periods, x2 multiplier)`);
}
}
awardScenarioPhotoXp() {
// Award 2 XP per photo taken during scenarios (ROADMAP requirement)
// Initialize scenarioXp if not already done (for Quick Play compatibility)
if (!this.gameState.scenarioXp) {
this.initializeScenarioXp();
}
this.gameState.scenarioXp.photoRewards += 2;
this.gameState.scenarioTracking.totalPhotosThisSession += 1;
this.updateScenarioTotalXp();
console.log(`📸 Photo XP: +2 (Total photos this session: ${this.gameState.scenarioTracking.totalPhotosThisSession})`);
}
awardScenarioStepXp() {
// Award 5 XP per scenario step completion (existing system)
// Initialize scenarioXp if not already done (for Quick Play compatibility)
if (!this.gameState.scenarioXp) {
this.initializeScenarioXp();
}
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!
`;
}).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);
}
createPlaceholderImage(text = 'Image Not Found') {
// Create a data URL for a simple placeholder image
const canvas = document.createElement('canvas');
canvas.width = 400;
canvas.height = 300;
const ctx = canvas.getContext('2d');
// Fill background
ctx.fillStyle = '#1a1a1a';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Add border
ctx.strokeStyle = '#00d4ff';
ctx.lineWidth = 2;
ctx.strokeRect(1, 1, canvas.width - 2, canvas.height - 2);
// Add text
ctx.fillStyle = '#00d4ff';
ctx.font = '16px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(text, canvas.width / 2, canvas.height / 2 - 10);
ctx.fillText('📷', canvas.width / 2, canvas.height / 2 + 20);
return canvas.toDataURL();
}
startGame() {
// Clean up any existing interactive resources before starting new game
if (this.interactiveTaskManager) {
this.interactiveTaskManager.cleanup();
}
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 or data manager
let tasksToUse = [];
if (window.gameModeManager && window.gameModeManager.isScenarioMode()) {
tasksToUse = window.gameModeManager.getTasksForMode();
} else {
// For standard, timed, and scored modes, use GameDataManager if available
const currentMode = window.gameModeManager ? window.gameModeManager.currentMode : 'standard';
if (window.gameDataManager) {
// Use GameDataManager to get tasks for the current mode
tasksToUse = window.gameDataManager.getTasksForMode(currentMode);
console.log(`📋 Using GameDataManager for ${currentMode} mode: ${tasksToUse.length} tasks`);
} else {
// Fallback to legacy gameData
tasksToUse = gameData.mainTasks || [];
console.log(`⚠️ GameDataManager not available, using legacy gameData: ${tasksToUse.length} tasks`);
}
// For standard modes, exclude interactive tasks
if (currentMode === 'standard' || currentMode === 'timed' || currentMode === 'scored') {
tasksToUse = tasksToUse.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 = [];
// Use setTimeout to break the recursion chain and prevent stack overflow
setTimeout(() => this.loadMainTask(), 0);
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 = [];
// Use setTimeout to break the recursion chain and prevent stack overflow
setTimeout(() => this.loadMainTask(), 0);
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() {
// Get consequence tasks from GameDataManager or fallback to legacy
let consequenceTasks = [];
if (this.dataManager && this.dataManager.gameDataManager) {
consequenceTasks = this.dataManager.gameDataManager.getConsequenceTasksForMode('main') || [];
} else {
// Fallback to legacy gameData
consequenceTasks = gameData.consequenceTasks || [];
}
const availableTasks = 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 : consequenceTasks;
if (tasksToChooseFrom.length === 0) {
console.warn('No consequence tasks available, using fallback');
this.gameState.currentTask = {
id: 'fallback-consequence',
text: 'Take a 30 second break and then continue your training',
image: this.getRandomImage(true)
};
} else {
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 linked images from directories to the pool
if (window.linkedImages && Array.isArray(window.linkedImages)) {
const linkedImagePaths = window.linkedImages.map(img => img.path);
imagePool = [...imagePool, ...linkedImagePaths];
}
// 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') || document.getElementById('skip-task');
// 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) {
skipBtn.textContent = 'Give Up';
skipBtn.className = 'btn btn-danger'; // Change to red for more serious action
} else if (skipBtn) {
skipBtn.textContent = 'Skip';
skipBtn.className = 'btn btn-warning'; // Default yellow
}
if (taskText) {
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) {
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
if (skipBtn) {
skipBtn.style.display = 'none';
}
// Show mercy skip button for consequence tasks
const originalTask = this.findOriginalSkippedTask();
if (originalTask && mercySkipBtn) {
// 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';
const mercyCostElement = document.getElementById('mercy-skip-cost');
if (mercyCostElement) {
mercyCostElement.textContent = `-${mercyCost} XP`;
}
mercySkipBtn.disabled = false;
} else {
mercySkipBtn.style.display = 'block';
const mercyCostElement = document.getElementById('mercy-skip-cost');
if (mercyCostElement) {
mercyCostElement.textContent = `-${mercyCost} XP (Not enough!)`;
}
mercySkipBtn.disabled = true;
}
} else if (mercySkipBtn) {
mercySkipBtn.style.display = 'none';
}
} else {
if (taskTypeIndicator) {
taskTypeIndicator.textContent = 'MAIN TASK';
taskTypeIndicator.classList.remove('consequence');
}
// Show regular skip button for main tasks
if (skipBtn) {
skipBtn.style.display = 'block';
}
// Hide mercy skip button for main tasks
if (mercySkipBtn) {
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
}
// Show complete button for all tasks (support both ID patterns)
const completeBtn = document.getElementById('complete-btn') || document.getElementById('complete-task');
if (completeBtn) {
completeBtn.style.display = 'block';
}
}
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();
// Clean up interactive task resources (including quad video player)
if (this.interactiveTaskManager) {
this.interactiveTaskManager.cleanup();
}
// 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 = `
🎉 Game Complete - Your Photo Session
You captured ${allPhotos.length} photos during this game!
${allPhotos.map((photo, index) => `
${index + 1}
${photo.sessionType || 'Photo'}
`).join('')}
🔒 All photos are stored locally and never uploaded
💾 Photos will be cleared when you start a new game