8712 lines
366 KiB
JavaScript
8712 lines
366 KiB
JavaScript
// 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
|
||
webcamBonuses: 0, // 3 XP per 30 seconds of webcam mirror
|
||
photoRewards: 0, // 1 XP per photo taken
|
||
stepCompletion: 0, // 5 XP per scenario step (existing)
|
||
total: 0 // Sum of all scenario XP
|
||
},
|
||
scenarioTracking: {
|
||
startTime: null, // Scenario session start time
|
||
lastTimeXpAwarded: null, // Last time-based XP award
|
||
lastFocusXpAwarded: null,// Last focus bonus award
|
||
lastWebcamXpAwarded: null,// Last webcam bonus award
|
||
isInFocusActivity: false, // Currently in a focus-hold task
|
||
isInWebcamActivity: false,// Currently in webcam mirror task
|
||
totalPhotosThisSession: 0 // Photos taken in this scenario session
|
||
}
|
||
};
|
||
|
||
this.timerInterval = null;
|
||
this.imageDiscoveryComplete = false;
|
||
this.imageManagementListenersAttached = false;
|
||
this.audioDiscoveryComplete = false;
|
||
this.audioManagementListenersAttached = false;
|
||
this.musicManager = new MusicManager(this.dataManager);
|
||
this.updateLoadingProgress(50, 'Music manager loaded...');
|
||
|
||
// Initialize Flash Message System
|
||
this.flashMessageManager = new FlashMessageManager(this.dataManager);
|
||
this.updateLoadingProgress(55, 'Flash message system loaded...');
|
||
|
||
// Initialize Popup Image System (Punishment for skips)
|
||
this.popupImageManager = new PopupImageManager(this.dataManager);
|
||
window.popupImageManager = this.popupImageManager; // Make available globally for HTML onclick handlers
|
||
this.updateLoadingProgress(60, 'Popup system loaded...');
|
||
|
||
// Initialize AI Task Generation System
|
||
this.aiTaskManager = new AITaskManager(this.dataManager);
|
||
this.updateLoadingProgress(65, 'AI task manager loaded...');
|
||
|
||
// Initialize Audio Management System
|
||
this.audioManager = new AudioManager(this.dataManager, this);
|
||
this.updateLoadingProgress(70, 'Audio manager loaded...');
|
||
|
||
// Initialize Game Mode Manager
|
||
this.gameModeManager = new GameModeManager();
|
||
window.gameModeManager = this.gameModeManager;
|
||
this.updateLoadingProgress(75, 'Game mode manager loaded...');
|
||
|
||
// Initialize Video Player Manager
|
||
this.initializeVideoManager();
|
||
this.updateLoadingProgress(78, 'Video manager initialized...');
|
||
|
||
// Initialize webcam for photography tasks
|
||
this.initializeWebcam();
|
||
this.updateLoadingProgress(80, 'Webcam initialized...');
|
||
|
||
this.initializeEventListeners();
|
||
this.setupKeyboardShortcuts();
|
||
this.setupWindowResizeHandling();
|
||
this.initializeCustomTasks(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();
|
||
|
||
// 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);
|
||
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: 'action',
|
||
mood: 'dominant',
|
||
story: "Your defiance is noted with displeasure. 'So be it. We'll break that attitude. Strip completely and assume a humiliating position on all fours. Hold it for 2 full minutes while contemplating your disobedience.'",
|
||
actionText: "Humiliating position - naked on all fours",
|
||
duration: 120,
|
||
nextStep: "broken_defiance"
|
||
},
|
||
nipple_punishment: {
|
||
type: 'action',
|
||
mood: 'intense',
|
||
story: "Take your nipples between your fingers. Pinch them hard - harder than comfortable. Hold that pressure for the full duration. No releasing early.",
|
||
actionText: "Painful nipple pinching",
|
||
duration: 30,
|
||
nextStep: "punishment_continues"
|
||
},
|
||
position_punishment: {
|
||
type: 'action',
|
||
mood: 'degrading',
|
||
story: "Get on your knees, spread them wide, and put your hands behind your head. Arch your back and present yourself shamefully. Hold this degrading position and think about what a pathetic display you're making.",
|
||
actionText: "Humiliating presentation position",
|
||
duration: 45,
|
||
nextStep: "punishment_continues"
|
||
},
|
||
denial_punishment: {
|
||
type: 'action',
|
||
mood: 'cruel',
|
||
story: "Begin edging yourself slowly. Build up the pleasure, get close to the edge, then stop completely. Repeat this cycle, but you are absolutely forbidden from cumming. Feel the cruel frustration build.",
|
||
actionText: "Denial edging - no release allowed",
|
||
duration: 90,
|
||
nextStep: "punishment_continues"
|
||
},
|
||
extra_punishment: {
|
||
type: 'action',
|
||
mood: 'stern',
|
||
story: "Your bargaining has consequences. Slap your inner thighs 10 times each, then edge for 60 seconds without completion. This is what happens when you try to negotiate.",
|
||
actionText: "Thigh slapping plus denial edging",
|
||
duration: 75,
|
||
nextStep: "punishment_continues"
|
||
},
|
||
double_punishment: {
|
||
type: 'action',
|
||
mood: 'harsh',
|
||
story: "You chose the harder path. First, painful nipple clamps or pinching for 45 seconds, then immediately into frustrating edge denial for another 45 seconds. No breaks between.",
|
||
actionText: "Double punishment - pain then denial",
|
||
duration: 90,
|
||
nextStep: "punishment_continues"
|
||
},
|
||
broken_defiance: {
|
||
type: 'choice',
|
||
mood: 'dominant',
|
||
story: "Your defiant attitude is cracking. The humiliating position has had its effect. Now, will you finally submit properly, or do you need more correction?",
|
||
choices: [
|
||
{
|
||
text: "I submit completely now",
|
||
type: "submissive",
|
||
preview: "Full surrender",
|
||
nextStep: "final_submission"
|
||
},
|
||
{
|
||
text: "I still resist",
|
||
type: "risky",
|
||
preview: "Continue defiance",
|
||
nextStep: "ultimate_punishment"
|
||
}
|
||
]
|
||
},
|
||
punishment_continues: {
|
||
type: 'choice',
|
||
mood: 'evaluating',
|
||
story: "Your punishment is having its effect. The punishment is having its effect. There's still more to your sentence. What comes next?",
|
||
choices: [
|
||
{
|
||
text: "Please, I've learned my lesson",
|
||
type: "submissive",
|
||
preview: "Beg for mercy",
|
||
nextStep: "mercy_consideration"
|
||
},
|
||
{
|
||
text: "I can take whatever you give me",
|
||
type: "risky",
|
||
preview: "Challenge for more",
|
||
nextStep: "escalated_punishment"
|
||
},
|
||
{
|
||
text: "I accept my punishment silently",
|
||
preview: "Endure stoically",
|
||
nextStep: "final_punishment"
|
||
}
|
||
]
|
||
},
|
||
mercy_consideration: {
|
||
type: 'ending',
|
||
mood: 'satisfied',
|
||
story: "Your begging is noted. Perhaps you have learned something after all. Your punishment ends here, but remember this lesson.",
|
||
endingText: "Punishment session concluded. Your state show the effectiveness of proper discipline.",
|
||
outcome: "partial"
|
||
},
|
||
escalated_punishment: {
|
||
type: 'action',
|
||
mood: 'harsh',
|
||
story: "Your boldness will be your downfall. Final punishment: intense edging while in a painful position. Build to the edge three times but never release. Learn your place.",
|
||
actionText: "Intense edge torture - no release",
|
||
duration: 120,
|
||
nextStep: "punishment_complete"
|
||
},
|
||
final_punishment: {
|
||
type: 'action',
|
||
mood: 'concluding',
|
||
story: "Your acceptance is noted. One final test of your submission: edge slowly and stop exactly when told. Show you've learned control through punishment.",
|
||
actionText: "Controlled edging demonstration",
|
||
duration: 60,
|
||
nextStep: "punishment_complete"
|
||
},
|
||
final_submission: {
|
||
type: 'ending',
|
||
mood: 'satisfied',
|
||
story: "Finally, proper submission. Your defiance has been broken and replaced with appropriate obedience.",
|
||
endingText: "Your punishment session ends with your complete submission. Your session state. Remember this feeling.",
|
||
outcome: "success"
|
||
},
|
||
ultimate_punishment: {
|
||
type: 'action',
|
||
mood: 'severe',
|
||
story: "Your continued resistance demands the ultimate punishment. Painful edging in the most humiliating position possible, building arousal while maintaining your shameful display.",
|
||
actionText: "Ultimate humiliation punishment",
|
||
duration: 150,
|
||
nextStep: "punishment_complete"
|
||
},
|
||
punishment_complete: {
|
||
type: 'ending',
|
||
mood: 'concluded',
|
||
story: "Your punishment session is complete. The lesson has been delivered and received.",
|
||
endingText: "Punishment concluded. Final state - Your session state. You have experienced the consequences of your actions.",
|
||
outcome: "punishment"
|
||
}
|
||
}
|
||
},
|
||
hint: "Different approaches lead to different types of punishment"
|
||
},
|
||
{
|
||
id: 'scenario-humiliation-task',
|
||
text: "Complete a series of humiliating tasks",
|
||
difficulty: "Medium",
|
||
interactiveType: "scenario-adventure",
|
||
interactiveData: {
|
||
title: "Humiliation Challenge",
|
||
steps: {
|
||
start: {
|
||
type: 'choice',
|
||
mood: 'playful',
|
||
story: "Today's session focuses on breaking down your pride through carefully crafted humiliation. Each task is designed to make you feel more submissive and exposed. Are you ready to embrace the shame?",
|
||
choices: [
|
||
{
|
||
text: "Yes, I want to feel humiliated",
|
||
type: "submissive",
|
||
preview: "Embrace the degradation",
|
||
nextStep: "eager_humiliation"
|
||
},
|
||
{
|
||
text: "Start with something mild",
|
||
type: "normal",
|
||
preview: "Ease into it",
|
||
nextStep: "mild_start"
|
||
},
|
||
{
|
||
text: "I'm not sure about this",
|
||
preview: "Hesitant participation",
|
||
nextStep: "reluctant_start"
|
||
}
|
||
]
|
||
},
|
||
eager_humiliation: {
|
||
type: 'choice',
|
||
mood: 'degrading',
|
||
story: "Perfect. Your eagerness for humiliation is noted. Let's start with something that will make you feel properly shameful. Choose your first degrading task.",
|
||
choices: [
|
||
{
|
||
text: "Crawl around naked on all fours",
|
||
type: "submissive",
|
||
preview: "Animal-like degradation",
|
||
nextStep: "crawling_task"
|
||
},
|
||
{
|
||
text: "Repeat degrading phrases about yourself",
|
||
type: "submissive",
|
||
preview: "Verbal self-humiliation",
|
||
nextStep: "verbal_humiliation"
|
||
},
|
||
{
|
||
text: "Assume shameful positions",
|
||
type: "submissive",
|
||
preview: "Degrading physical poses",
|
||
nextStep: "position_humiliation"
|
||
}
|
||
]
|
||
},
|
||
mild_start: {
|
||
type: 'action',
|
||
mood: 'gentle',
|
||
story: "We'll ease you into this. Start by removing your clothes slowly while thinking about how exposed and vulnerable you're becoming. Take your time and really feel the vulnerability.",
|
||
actionText: "Slow, deliberate undressing",
|
||
duration: 45,
|
||
nextStep: "building_shame"
|
||
},
|
||
reluctant_start: {
|
||
type: 'choice',
|
||
mood: 'encouraging',
|
||
story: "Hesitation is natural. Sometimes the best experiences come from pushing past our comfort zones. Would you like to try something very gentle, or would you prefer to stop here?",
|
||
choices: [
|
||
{
|
||
text: "Try something very gentle",
|
||
preview: "Minimal humiliation",
|
||
nextStep: "gentle_humiliation"
|
||
},
|
||
{
|
||
text: "I think I should stop",
|
||
preview: "Exit the scenario",
|
||
nextStep: "respectful_exit"
|
||
}
|
||
]
|
||
},
|
||
crawling_task: {
|
||
type: 'action',
|
||
mood: 'degrading',
|
||
story: "Get down on your hands and knees. Crawl in a circle around the room like the animal you are. Feel how degrading this position is. You're not a person right now - you're just a pathetic creature on display.",
|
||
actionText: "Crawling like an animal",
|
||
duration: 60,
|
||
nextStep: "humiliation_escalates"
|
||
},
|
||
verbal_humiliation: {
|
||
type: 'action',
|
||
mood: 'degrading',
|
||
story: "Complete the webcam mirror task to look at yourself and repeat degrading phrases: 'I am a pathetic toy', 'I exist for others' pleasure', 'I have no dignity'. Say each phrase 5 times while looking at yourself.",
|
||
actionText: "Complete webcam mirror self-degradation task",
|
||
duration: 45,
|
||
nextStep: "humiliation_escalates",
|
||
interactiveType: "mirror-task",
|
||
mirrorInstructions: "Look directly at yourself through the webcam while speaking the degrading phrases",
|
||
mirrorTaskText: "Repeat each phrase 5 times while maintaining eye contact with yourself: 'I am a pathetic toy', 'I exist for others' pleasure', 'I have no dignity'"
|
||
},
|
||
position_humiliation: {
|
||
type: 'action',
|
||
mood: 'shameful',
|
||
story: "Assume the most shameful position you can think of. Spread yourself wide, arch your back, present yourself like you're begging for attention. Hold this degrading pose and feel the shame wash over you.",
|
||
actionText: "Shameful presentation position",
|
||
duration: 50,
|
||
nextStep: "humiliation_escalates"
|
||
},
|
||
building_shame: {
|
||
type: 'choice',
|
||
mood: 'building',
|
||
story: "Good. Now that you're exposed and vulnerable, you can feel the humiliation starting to build. Your state is evident. Ready for the next level of degradation?",
|
||
choices: [
|
||
{
|
||
text: "Yes, humiliate me more",
|
||
type: "submissive",
|
||
preview: "Increase the shame",
|
||
nextStep: "deeper_humiliation"
|
||
},
|
||
{
|
||
text: "This is enough for now",
|
||
preview: "Stop at current level",
|
||
nextStep: "gentle_conclusion"
|
||
}
|
||
]
|
||
},
|
||
gentle_humiliation: {
|
||
type: 'action',
|
||
mood: 'gentle',
|
||
story: "Just stand naked and look at yourself. Notice how exposed you feel. There's something humbling about vulnerability, isn't there? Take a moment to appreciate this feeling.",
|
||
actionText: "Gentle self-awareness",
|
||
duration: 30,
|
||
nextStep: "gentle_conclusion"
|
||
},
|
||
humiliation_escalates: {
|
||
type: 'choice',
|
||
mood: 'intense',
|
||
story: "The humiliation is taking effect. Your state and responses show how the degradation is affecting you. Time for the final humiliating task.",
|
||
choices: [
|
||
{
|
||
text: "Beg to be used and humiliated",
|
||
type: "submissive",
|
||
preview: "Complete verbal submission",
|
||
nextStep: "complete_degradation"
|
||
},
|
||
{
|
||
text: "Perform the most shameful act I can think of",
|
||
type: "risky",
|
||
preview: "Ultimate personal humiliation",
|
||
nextStep: "ultimate_shame"
|
||
},
|
||
{
|
||
text: "I can't take any more",
|
||
preview: "Reach personal limit",
|
||
nextStep: "mercy_ending"
|
||
}
|
||
]
|
||
},
|
||
deeper_humiliation: {
|
||
type: 'action',
|
||
mood: 'degrading',
|
||
story: "Now for deeper shame. Assume a degrading position and edge yourself while repeating how pathetic and shameful you are. Build the arousal while embracing the humiliation.",
|
||
actionText: "Degrading edge while self-shaming",
|
||
duration: 75,
|
||
nextStep: "humiliation_complete"
|
||
},
|
||
complete_degradation: {
|
||
type: 'action',
|
||
mood: 'degrading',
|
||
story: "Beg out loud. Say exactly what you want done to you. Describe how pathetic you are and how much you need to be used. Let the words shame you as much as the actions.",
|
||
actionText: "Verbal begging and self-degradation",
|
||
duration: 60,
|
||
nextStep: "humiliation_complete"
|
||
},
|
||
ultimate_shame: {
|
||
type: 'action',
|
||
mood: 'extreme',
|
||
story: "Do whatever shameful act comes to mind - the thing that would embarrass you most. This is your ultimate degradation, chosen by your own twisted desires.",
|
||
actionText: "Personal ultimate shame act",
|
||
duration: 90,
|
||
nextStep: "humiliation_complete"
|
||
},
|
||
gentle_conclusion: {
|
||
type: 'ending',
|
||
mood: 'gentle',
|
||
story: "You've experienced a taste of humiliation without going too far. Sometimes just the hint of shame is enough.",
|
||
endingText: "Gentle humiliation session complete. You maintained some dignity while exploring new feelings. Your session state.",
|
||
outcome: "partial"
|
||
},
|
||
mercy_ending: {
|
||
type: 'ending',
|
||
mood: 'understanding',
|
||
story: "You've reached your limit, and that's perfectly acceptable. Knowing your boundaries is important.",
|
||
endingText: "Humiliation session ended at your request. You explored your limits safely. Final state recorded.",
|
||
outcome: "partial"
|
||
},
|
||
humiliation_complete: {
|
||
type: 'ending',
|
||
mood: 'satisfied',
|
||
story: "Your humiliation session is complete. You've been thoroughly degraded and shamefully aroused.",
|
||
endingText: "Complete humiliation achieved. Your final state shows the effect: Arousal your state, Control your focus. You've been properly put in your place.",
|
||
outcome: "success"
|
||
},
|
||
respectful_exit: {
|
||
type: 'ending',
|
||
mood: 'respectful',
|
||
story: "You've chosen to respect your own boundaries. This shows wisdom and self-awareness.",
|
||
endingText: "Session ended by your choice. Respecting limits is always the right decision.",
|
||
outcome: "partial"
|
||
}
|
||
}
|
||
},
|
||
hint: "Explore humiliation at your own comfort level"
|
||
},
|
||
{
|
||
id: 'scenario-edging-marathon',
|
||
text: "Endure an intensive edging marathon",
|
||
difficulty: "Hard",
|
||
interactiveType: "scenario-adventure",
|
||
interactiveData: {
|
||
title: "Edging Marathon Challenge",
|
||
steps: {
|
||
start: {
|
||
type: 'choice',
|
||
mood: 'intense',
|
||
story: "Welcome to the edging marathon - a test of your endurance, self-control, and ability to withstand prolonged arousal without release. This will push your limits. How do you want to approach this challenge?",
|
||
choices: [
|
||
{
|
||
text: "I want the full intense experience",
|
||
type: "risky",
|
||
preview: "Maximum difficulty marathon",
|
||
nextStep: "intense_marathon"
|
||
},
|
||
{
|
||
text: "Give me a challenging but manageable pace",
|
||
type: "normal",
|
||
preview: "Moderate intensity marathon",
|
||
nextStep: "moderate_marathon"
|
||
},
|
||
{
|
||
text: "Start me off gently",
|
||
preview: "Easier introduction to edging",
|
||
nextStep: "gentle_marathon"
|
||
}
|
||
]
|
||
},
|
||
intense_marathon: {
|
||
type: 'action',
|
||
mood: 'cruel',
|
||
story: "The intense marathon begins now. Edge for 90 seconds, getting as close as possible to climax, then stop completely for 30 seconds. This cycle will repeat. No mercy, no early finish.",
|
||
actionText: "Intense edging cycle - 90 seconds",
|
||
duration: 90,
|
||
nextStep: "marathon_continues"
|
||
},
|
||
moderate_marathon: {
|
||
type: 'action',
|
||
mood: 'challenging',
|
||
story: "The moderate marathon begins. Edge for 60 seconds, building arousal steadily, then take a 20-second break. Find your rhythm and try to maintain control.",
|
||
actionText: "Moderate edging cycle - 60 seconds",
|
||
duration: 60,
|
||
nextStep: "marathon_continues"
|
||
},
|
||
gentle_marathon: {
|
||
type: 'action',
|
||
mood: 'building',
|
||
story: "The gentle marathon starts slowly. Edge for 45 seconds, focusing on building arousal gradually. You'll have time to recover between cycles.",
|
||
actionText: "Gentle edging introduction - 45 seconds",
|
||
duration: 45,
|
||
nextStep: "marathon_continues"
|
||
},
|
||
marathon_continues: {
|
||
type: 'choice',
|
||
mood: 'testing',
|
||
story: "Round one complete. Your arousal is at your state and your control is at your focus. The marathon continues. How are you feeling for the next round?",
|
||
choices: [
|
||
{
|
||
text: "I can handle more intensity",
|
||
type: "risky",
|
||
preview: "Increase the challenge",
|
||
nextStep: "escalated_round"
|
||
},
|
||
{
|
||
text: "Keep the same pace",
|
||
preview: "Maintain current intensity",
|
||
nextStep: "consistent_round"
|
||
},
|
||
{
|
||
text: "I need to slow down",
|
||
preview: "Reduce intensity",
|
||
nextStep: "recovery_round"
|
||
},
|
||
{
|
||
text: "I can't continue",
|
||
preview: "End the marathon early",
|
||
nextStep: "early_finish"
|
||
}
|
||
]
|
||
},
|
||
escalated_round: {
|
||
type: 'action',
|
||
mood: 'intense',
|
||
story: "You asked for more intensity. Edge for 2 full minutes this time, getting closer to the edge than before. No stopping until the timer ends. Push your limits.",
|
||
actionText: "Extended intense edging - 120 seconds",
|
||
duration: 120,
|
||
nextStep: "final_challenge"
|
||
},
|
||
consistent_round: {
|
||
type: 'action',
|
||
mood: 'steady',
|
||
story: "Maintaining the same intensity. Continue your edging pattern, staying consistent with your rhythm. Build that arousal steadily.",
|
||
actionText: "Consistent edging round",
|
||
duration: 75,
|
||
nextStep: "final_challenge"
|
||
},
|
||
recovery_round: {
|
||
type: 'action',
|
||
mood: 'gentle',
|
||
story: "Taking it easier this round. Light touches and gentle stimulation. Focus on maintaining arousal without overwhelming yourself.",
|
||
actionText: "Recovery round - gentle touches",
|
||
duration: 45,
|
||
nextStep: "final_challenge"
|
||
},
|
||
final_challenge: {
|
||
type: 'choice',
|
||
mood: 'climactic',
|
||
story: "This is the final challenge of your edging marathon. The punishment is having its effect. Choose how you want to finish this marathon.",
|
||
choices: [
|
||
{
|
||
text: "Ultimate edge - get as close as possible",
|
||
type: "risky",
|
||
preview: "Maximum arousal challenge",
|
||
nextStep: "ultimate_edge"
|
||
},
|
||
{
|
||
text: "Controlled finish - demonstrate precision",
|
||
preview: "Skill-based conclusion",
|
||
nextStep: "precision_finish"
|
||
},
|
||
{
|
||
text: "Denied finish - stop before completion",
|
||
type: "submissive",
|
||
preview: "Frustrating denial ending",
|
||
nextStep: "denial_finish"
|
||
}
|
||
]
|
||
},
|
||
ultimate_edge: {
|
||
type: 'action',
|
||
mood: 'extreme',
|
||
story: "The ultimate edge challenge. Get as close to climax as humanly possible without going over. This is the test of your marathon training. Hold that edge for as long as you can.",
|
||
actionText: "Ultimate edge challenge",
|
||
duration: 60,
|
||
nextStep: "marathon_complete"
|
||
},
|
||
precision_finish: {
|
||
type: 'action',
|
||
mood: 'controlled',
|
||
story: "Show the precision you've developed. Edge exactly to your predetermined point, hold for 10 seconds, then stop cleanly. Demonstrate your newfound control.",
|
||
actionText: "Precision control demonstration",
|
||
duration: 45,
|
||
nextStep: "marathon_complete"
|
||
},
|
||
denial_finish: {
|
||
type: 'action',
|
||
mood: 'cruel',
|
||
story: "Build up your arousal one final time, then stop completely when you're desperately close. Feel the cruel frustration of denial after all that work.",
|
||
actionText: "Final denial - no release",
|
||
duration: 30,
|
||
nextStep: "marathon_complete"
|
||
},
|
||
early_finish: {
|
||
type: 'ending',
|
||
mood: 'understanding',
|
||
story: "You've pushed yourself as far as you could go. Sometimes knowing when to stop is the greatest skill.",
|
||
endingText: "Marathon ended early by choice. You challenged yourself and learned your limits. Your session state.",
|
||
outcome: "partial"
|
||
},
|
||
marathon_complete: {
|
||
type: 'ending',
|
||
mood: 'accomplished',
|
||
story: "The edging marathon is complete. You've endured the full challenge and tested your limits.",
|
||
endingText: "Edging marathon finished! Your final state shows your endurance: Arousal your state, Control your focus. You've proven your dedication.",
|
||
outcome: "success"
|
||
}
|
||
}
|
||
},
|
||
hint: "Test your endurance and self-control"
|
||
},
|
||
{
|
||
id: 'scenario-obedience-training',
|
||
text: "Begin strict obedience training",
|
||
difficulty: "Medium",
|
||
interactiveType: "scenario-adventure",
|
||
interactiveData: {
|
||
title: "Obedience Training Program",
|
||
steps: {
|
||
start: {
|
||
type: 'choice',
|
||
mood: 'authoritative',
|
||
story: "You've enrolled in an obedience training program. This is about learning to follow instructions precisely, without question or hesitation. Your trainer evaluates you with a stern gaze. How do you present yourself?",
|
||
choices: [
|
||
{
|
||
text: "Stand at attention, ready to obey",
|
||
type: "submissive",
|
||
preview: "Show immediate submission",
|
||
nextStep: "good_start"
|
||
},
|
||
{
|
||
text: "Ask what the training involves",
|
||
type: "normal",
|
||
preview: "Seek clarification",
|
||
nextStep: "questioning_attitude"
|
||
},
|
||
{
|
||
text: "Cross arms and look skeptical",
|
||
type: "risky",
|
||
preview: "Show resistance",
|
||
nextStep: "resistance_noted"
|
||
}
|
||
]
|
||
},
|
||
good_start: {
|
||
type: 'choice',
|
||
mood: 'approving',
|
||
story: "Excellent posture. You're showing the right attitude already. Let's begin with basic commands. Your first lesson in obedience:",
|
||
choices: [
|
||
{
|
||
text: "Position training - assume commanded poses",
|
||
type: "submissive",
|
||
preview: "Physical obedience",
|
||
nextStep: "position_training"
|
||
},
|
||
{
|
||
text: "Response training - verbal acknowledgments",
|
||
preview: "Verbal obedience",
|
||
nextStep: "response_training"
|
||
},
|
||
{
|
||
text: "Timing training - precise execution",
|
||
preview: "Precision obedience",
|
||
nextStep: "timing_training"
|
||
}
|
||
]
|
||
},
|
||
questioning_attitude: {
|
||
type: 'choice',
|
||
mood: 'stern',
|
||
story: "Questions show you're not ready to simply obey. That needs to be corrected. Choose your response to this mild correction:",
|
||
choices: [
|
||
{
|
||
text: "Apologize and prepare to obey",
|
||
type: "submissive",
|
||
preview: "Accept correction",
|
||
nextStep: "corrected_attitude"
|
||
},
|
||
{
|
||
text: "Explain that questions help understanding",
|
||
preview: "Justify the questions",
|
||
nextStep: "discipline_needed"
|
||
}
|
||
]
|
||
},
|
||
resistance_noted: {
|
||
type: 'action',
|
||
mood: 'stern',
|
||
story: "Your resistance is noted and will be addressed. First, you'll learn what happens to those who resist. Assume a stress position - hands behind head, standing on toes for 60 seconds.",
|
||
actionText: "Stress position for resistance",
|
||
duration: 60,
|
||
nextStep: "resistance_broken"
|
||
},
|
||
position_training: {
|
||
type: 'action',
|
||
mood: 'instructional',
|
||
story: "When I say 'present', you will immediately assume a kneeling position with hands on thighs. When I say 'attention', stand straight with hands at sides. Practice these transitions for 45 seconds.",
|
||
actionText: "Position command training",
|
||
duration: 45,
|
||
nextStep: "training_progresses"
|
||
},
|
||
response_training: {
|
||
type: 'action',
|
||
mood: 'instructional',
|
||
story: "You will respond to every command with 'Yes Sir' or 'Yes Ma'am'. Practice this now while performing simple actions. Say it out loud with each instruction you follow.",
|
||
actionText: "Verbal response training",
|
||
duration: 30,
|
||
nextStep: "training_progresses"
|
||
},
|
||
timing_training: {
|
||
type: 'action',
|
||
mood: 'precise',
|
||
story: "Obedience must be immediate. When given a command, you have exactly 3 seconds to begin compliance. Practice instant response to touch commands for 40 seconds.",
|
||
actionText: "Instant response training",
|
||
duration: 40,
|
||
nextStep: "training_progresses"
|
||
},
|
||
corrected_attitude: {
|
||
type: 'action',
|
||
mood: 'approving',
|
||
story: "Better. Now that your attitude is corrected, we can proceed with proper training. Begin with basic submission positions - kneel and wait for commands.",
|
||
actionText: "Basic submission training",
|
||
duration: 35,
|
||
nextStep: "training_progresses"
|
||
},
|
||
discipline_needed: {
|
||
type: 'action',
|
||
mood: 'disciplinary',
|
||
story: "Your continued questioning shows you need discipline before training can begin. Hold a stress position while contemplating the value of unquestioning obedience.",
|
||
actionText: "Disciplinary stress position",
|
||
duration: 75,
|
||
nextStep: "discipline_learned"
|
||
},
|
||
resistance_broken: {
|
||
type: 'choice',
|
||
mood: 'evaluating',
|
||
story: "Your resistance is starting to crack. The stress position has had its effect. Are you ready to proceed with proper obedience training now?",
|
||
choices: [
|
||
{
|
||
text: "Yes, I'm ready to obey",
|
||
type: "submissive",
|
||
preview: "Submit to training",
|
||
nextStep: "obedience_accepted"
|
||
},
|
||
{
|
||
text: "I still have questions",
|
||
preview: "Continue resistance",
|
||
nextStep: "additional_discipline"
|
||
}
|
||
]
|
||
},
|
||
training_progresses: {
|
||
type: 'choice',
|
||
mood: 'progressive',
|
||
story: "Good progress. Your arousal at your state and control at your focus show the training is having an effect. Time for more advanced obedience lessons.",
|
||
choices: [
|
||
{
|
||
text: "Advanced position training",
|
||
type: "submissive",
|
||
preview: "More complex positions",
|
||
nextStep: "advanced_positions"
|
||
},
|
||
{
|
||
text: "Endurance obedience test",
|
||
type: "risky",
|
||
preview: "Sustained obedience",
|
||
nextStep: "endurance_test"
|
||
},
|
||
{
|
||
text: "Precision command following",
|
||
preview: "Exact obedience",
|
||
nextStep: "precision_commands"
|
||
}
|
||
]
|
||
},
|
||
discipline_learned: {
|
||
type: 'choice',
|
||
mood: 'testing',
|
||
story: "The discipline seems to have taught you something. Your questioning attitude should be properly adjusted now. Shall we proceed with obedience training?",
|
||
choices: [
|
||
{
|
||
text: "Yes, no more questions",
|
||
type: "submissive",
|
||
preview: "Accept training authority",
|
||
nextStep: "training_progresses"
|
||
},
|
||
{
|
||
text: "I understand but need clarification",
|
||
preview: "Respectful inquiry",
|
||
nextStep: "final_lesson"
|
||
}
|
||
]
|
||
},
|
||
obedience_accepted: {
|
||
type: 'action',
|
||
mood: 'satisfied',
|
||
story: "Excellent. Now we can begin real training. Follow a series of position commands exactly as given. Show me your newfound obedience.",
|
||
actionText: "Obedience demonstration",
|
||
duration: 60,
|
||
nextStep: "training_complete"
|
||
},
|
||
additional_discipline: {
|
||
type: 'action',
|
||
mood: 'harsh',
|
||
story: "More discipline is clearly needed. Extended stress position combined with arousal denial. You'll learn that questions are not your place.",
|
||
actionText: "Extended disciplinary training",
|
||
duration: 90,
|
||
nextStep: "complete_submission"
|
||
},
|
||
advanced_positions: {
|
||
type: 'action',
|
||
mood: 'demanding',
|
||
story: "Advanced training requires more challenging positions. Transition between multiple poses rapidly while maintaining arousal. Show your growing obedience.",
|
||
actionText: "Advanced position sequences",
|
||
duration: 75,
|
||
nextStep: "training_complete"
|
||
},
|
||
endurance_test: {
|
||
type: 'action',
|
||
mood: 'testing',
|
||
story: "The ultimate test - maintain a challenging position while building arousal, but you may not climax without permission. Endure for the full duration.",
|
||
actionText: "Endurance obedience test",
|
||
duration: 120,
|
||
nextStep: "training_complete"
|
||
},
|
||
precision_commands: {
|
||
type: 'action',
|
||
mood: 'exacting',
|
||
story: "Execute precise movements exactly as commanded. Every hesitation, every imperfection will be noted. Show perfect obedience.",
|
||
actionText: "Precision command execution",
|
||
duration: 60,
|
||
nextStep: "training_complete"
|
||
},
|
||
final_lesson: {
|
||
type: 'action',
|
||
mood: 'conclusive',
|
||
story: "Final lesson: sometimes obedience means accepting what you don't understand. Perform actions without explanation for the remainder of this training.",
|
||
actionText: "Unquestioned obedience",
|
||
duration: 45,
|
||
nextStep: "training_complete"
|
||
},
|
||
complete_submission: {
|
||
type: 'ending',
|
||
mood: 'dominant',
|
||
story: "Your resistance has been completely broken. You now understand the meaning of true obedience.",
|
||
endingText: "Obedience training completed through discipline. Final state: Arousal your state, Control your focus. You have learned to obey without question.",
|
||
outcome: "success"
|
||
},
|
||
training_complete: {
|
||
type: 'ending',
|
||
mood: 'accomplished',
|
||
story: "Your obedience training is complete. You've learned to follow commands properly and promptly.",
|
||
endingText: "Training program completed successfully. Final assessment: Arousal your state, Control your focus. You've developed proper obedience skills.",
|
||
outcome: "success"
|
||
}
|
||
}
|
||
},
|
||
hint: "Learn the value of unquestioning obedience"
|
||
}
|
||
); // 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('<27> 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
|
||
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());
|
||
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
|
||
safeAddListener('manage-tasks-btn', 'click', () => this.showTaskManagement());
|
||
safeAddListener('back-to-start-btn', 'click', () => this.showScreen('start-screen'));
|
||
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());
|
||
|
||
// Annoyance management - main button and basic controls
|
||
safeAddListener('manage-annoyance-btn', 'click', () => this.showAnnoyanceManagement());
|
||
|
||
// Load saved theme
|
||
this.loadSavedTheme();
|
||
}
|
||
|
||
/**
|
||
* Initialize webcam system for photography tasks
|
||
*/
|
||
async initializeWebcam() {
|
||
console.log('🎥 Initializing webcam system...');
|
||
|
||
if (!this.webcamManager) {
|
||
console.log('⚠️ WebcamManager not available, skipping webcam initialization');
|
||
return false;
|
||
}
|
||
|
||
try {
|
||
const initialized = await this.webcamManager.init();
|
||
if (initialized) {
|
||
console.log('✅ Webcam system ready for photography tasks');
|
||
|
||
// Listen for photo task events
|
||
document.addEventListener('photoTaken', (event) => {
|
||
this.handlePhotoTaken(event.detail);
|
||
});
|
||
|
||
// Listen for photo session completion
|
||
document.addEventListener('photoSessionComplete', (event) => {
|
||
this.handlePhotoSessionComplete(event.detail);
|
||
});
|
||
|
||
} else {
|
||
console.log('📷 Webcam not available - photography tasks will use standard mode');
|
||
}
|
||
} catch (error) {
|
||
console.error('❌ Webcam initialization failed:', error);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Handle photo taken event from webcam manager
|
||
*/
|
||
handlePhotoTaken(detail) {
|
||
console.log('📸 Photo taken for task:', detail.sessionType);
|
||
|
||
// Show confirmation message
|
||
this.showNotification('Photo captured successfully! 📸', 'success', 3000);
|
||
|
||
// Progress the interactive task if applicable
|
||
if (this.interactiveTaskManager && this.interactiveTaskManager.currentTask) {
|
||
this.interactiveTaskManager.handlePhotoCompletion(detail);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Handle photo session completion
|
||
*/
|
||
handlePhotoSessionComplete(detail) {
|
||
console.log('🎉 Photo session completed:', detail.sessionType, `(${detail.photos.length} photos)`);
|
||
|
||
// Show completion message
|
||
this.showNotification(`Photography session completed! ${detail.photos.length} photos taken 📸`, 'success', 4000);
|
||
|
||
// Progress the scenario task
|
||
if (this.interactiveTaskManager && this.interactiveTaskManager.currentInteractiveTask) {
|
||
this.interactiveTaskManager.handlePhotoSessionCompletion(detail);
|
||
}
|
||
}
|
||
|
||
initializeAudioControls() {
|
||
// 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)
|
||
if (!this.gameState.scenarioTracking.startTime || this.gameState.isPaused) return;
|
||
|
||
const now = Date.now();
|
||
const timeSinceLastAward = now - this.gameState.scenarioTracking.lastTimeXpAwarded;
|
||
const twoMinutesMs = 2 * 60 * 1000; // 2 minutes in milliseconds
|
||
|
||
if (timeSinceLastAward >= twoMinutesMs) {
|
||
const xpToAward = Math.floor(timeSinceLastAward / twoMinutesMs);
|
||
this.gameState.scenarioXp.timeBased += xpToAward;
|
||
this.gameState.scenarioTracking.lastTimeXpAwarded = now;
|
||
this.updateScenarioTotalXp();
|
||
console.log(`⏰ Time-based XP: +${xpToAward} (${(timeSinceLastAward/60000).toFixed(1)} minutes)`);
|
||
}
|
||
}
|
||
|
||
updateScenarioFocusXp() {
|
||
// Award 3 XP per 30 seconds during focus activities
|
||
if (!this.gameState.scenarioTracking.isInFocusActivity || this.gameState.isPaused) return;
|
||
|
||
const now = Date.now();
|
||
const timeSinceLastAward = now - this.gameState.scenarioTracking.lastFocusXpAwarded;
|
||
const thirtySecondsMs = 30 * 1000; // 30 seconds in milliseconds
|
||
|
||
if (timeSinceLastAward >= thirtySecondsMs) {
|
||
const periods = Math.floor(timeSinceLastAward / thirtySecondsMs);
|
||
const xpToAward = periods * 3;
|
||
this.gameState.scenarioXp.focusBonuses += xpToAward;
|
||
this.gameState.scenarioTracking.lastFocusXpAwarded = now;
|
||
this.updateScenarioTotalXp();
|
||
console.log(`🎯 Focus bonus XP: +${xpToAward} (${periods} x 30s periods)`);
|
||
}
|
||
}
|
||
|
||
updateScenarioWebcamXp() {
|
||
// Award 3 XP per 30 seconds during webcam mirror activities
|
||
if (!this.gameState.scenarioTracking.isInWebcamActivity || this.gameState.isPaused) return;
|
||
|
||
const now = Date.now();
|
||
const timeSinceLastAward = now - this.gameState.scenarioTracking.lastWebcamXpAwarded;
|
||
const thirtySecondsMs = 30 * 1000; // 30 seconds in milliseconds
|
||
|
||
if (timeSinceLastAward >= thirtySecondsMs) {
|
||
const periods = Math.floor(timeSinceLastAward / thirtySecondsMs);
|
||
const xpToAward = periods * 3;
|
||
this.gameState.scenarioXp.webcamBonuses += xpToAward;
|
||
this.gameState.scenarioTracking.lastWebcamXpAwarded = now;
|
||
this.updateScenarioTotalXp();
|
||
console.log(`📹 Webcam bonus XP: +${xpToAward} (${periods} x 30s periods)`);
|
||
}
|
||
}
|
||
|
||
awardScenarioPhotoXp() {
|
||
// Award 1 XP per photo taken during scenarios
|
||
this.gameState.scenarioXp.photoRewards += 1;
|
||
this.gameState.scenarioTracking.totalPhotosThisSession += 1;
|
||
this.updateScenarioTotalXp();
|
||
console.log(`📸 Photo XP: +1 (Total photos this session: ${this.gameState.scenarioTracking.totalPhotosThisSession})`);
|
||
}
|
||
|
||
awardScenarioStepXp() {
|
||
// Award 5 XP per scenario step completion (existing system)
|
||
this.gameState.scenarioXp.stepCompletion += 5;
|
||
this.updateScenarioTotalXp();
|
||
console.log(`🎭 Scenario step XP: +5`);
|
||
}
|
||
|
||
updateScenarioTotalXp() {
|
||
// Calculate total scenario XP and update session XP
|
||
const total = this.gameState.scenarioXp.timeBased +
|
||
this.gameState.scenarioXp.focusBonuses +
|
||
this.gameState.scenarioXp.webcamBonuses +
|
||
this.gameState.scenarioXp.photoRewards +
|
||
this.gameState.scenarioXp.stepCompletion;
|
||
|
||
this.gameState.scenarioXp.total = total;
|
||
|
||
// For scenarios, use scenario XP instead of task completion XP
|
||
if (window.gameModeManager && window.gameModeManager.isScenarioMode()) {
|
||
this.gameState.xp = total;
|
||
this.updateStats();
|
||
}
|
||
}
|
||
|
||
startScenarioFocusActivity() {
|
||
// Called when entering focus-hold activities
|
||
this.gameState.scenarioTracking.isInFocusActivity = true;
|
||
this.gameState.scenarioTracking.lastFocusXpAwarded = Date.now();
|
||
console.log('🎯 Focus activity started - tracking XP bonuses');
|
||
}
|
||
|
||
stopScenarioFocusActivity() {
|
||
// Called when exiting focus-hold activities
|
||
this.gameState.scenarioTracking.isInFocusActivity = false;
|
||
console.log('🎯 Focus activity stopped');
|
||
}
|
||
|
||
startScenarioWebcamActivity() {
|
||
// Called when entering webcam mirror activities
|
||
this.gameState.scenarioTracking.isInWebcamActivity = true;
|
||
this.gameState.scenarioTracking.lastWebcamXpAwarded = Date.now();
|
||
console.log('📹 Webcam activity started - tracking XP bonuses');
|
||
}
|
||
|
||
stopScenarioWebcamActivity() {
|
||
// Called when exiting webcam mirror activities
|
||
this.gameState.scenarioTracking.isInWebcamActivity = false;
|
||
console.log('📹 Webcam activity stopped');
|
||
}
|
||
|
||
setupKeyboardShortcuts() {
|
||
document.addEventListener('keydown', (e) => {
|
||
// Don't trigger shortcuts when typing in inputs or textareas
|
||
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') {
|
||
return;
|
||
}
|
||
|
||
// Don't trigger shortcuts during scenario modes (except basic navigation)
|
||
const isScenarioMode = window.gameModeManager && window.gameModeManager.isScenarioMode();
|
||
if (isScenarioMode && this.gameState.isRunning) {
|
||
// Only allow basic navigation shortcuts in scenario mode
|
||
switch(e.key.toLowerCase()) {
|
||
case 'escape': // Escape key - still allow modal closing
|
||
e.preventDefault();
|
||
this.handleEscapeKey();
|
||
break;
|
||
case 'h': // H key - still allow help
|
||
e.preventDefault();
|
||
this.toggleHelp();
|
||
break;
|
||
case 'm': // M key - still allow music toggle
|
||
e.preventDefault();
|
||
this.toggleMusic();
|
||
break;
|
||
}
|
||
return; // Block all other shortcuts in scenario mode
|
||
}
|
||
|
||
switch(e.key.toLowerCase()) {
|
||
case ' ': // Spacebar - pause/unpause game
|
||
e.preventDefault();
|
||
if (this.gameState.isRunning) {
|
||
this.pauseGame();
|
||
} else if (this.gameState.isPaused) {
|
||
this.resumeGame();
|
||
}
|
||
break;
|
||
|
||
case 'p': // P key - pause/unpause game
|
||
e.preventDefault();
|
||
if (this.gameState.isRunning) {
|
||
this.pauseGame();
|
||
} else if (this.gameState.isPaused) {
|
||
this.resumeGame();
|
||
}
|
||
break;
|
||
|
||
case 'm': // M key - toggle music
|
||
e.preventDefault();
|
||
this.toggleMusic();
|
||
break;
|
||
|
||
case 'escape': // Escape key - close modals or return to start
|
||
e.preventDefault();
|
||
this.handleEscapeKey();
|
||
break;
|
||
|
||
case 'h': // H key - toggle help menu
|
||
e.preventDefault();
|
||
this.toggleHelp();
|
||
break;
|
||
|
||
case 'enter': // Enter key - complete task
|
||
e.preventDefault();
|
||
if (this.gameState.isRunning && !this.gameState.isPaused) {
|
||
this.completeTask();
|
||
}
|
||
break;
|
||
|
||
case 'control': // Ctrl key - regular skip only
|
||
e.preventDefault();
|
||
if (this.gameState.isRunning && !this.gameState.isPaused) {
|
||
// Check for Shift+Ctrl for mercy skip
|
||
if (e.shiftKey) {
|
||
const mercyBtn = document.getElementById('mercy-skip-btn');
|
||
if (mercyBtn && mercyBtn.style.display !== 'none' && !mercyBtn.disabled) {
|
||
this.mercySkip();
|
||
}
|
||
} else {
|
||
this.skipTask();
|
||
}
|
||
}
|
||
break;
|
||
}
|
||
});
|
||
|
||
// Show keyboard shortcuts hint
|
||
console.log('Keyboard shortcuts enabled: Enter (complete), Ctrl (skip), Shift+Ctrl (mercy skip), Space/P (pause), M (music), H (help), Escape (close/back)');
|
||
console.log('Note: Most shortcuts are disabled during scenario modes to prevent interference');
|
||
}
|
||
|
||
/**
|
||
* Handle escape key functionality
|
||
*/
|
||
handleEscapeKey() {
|
||
// Close help modal if open
|
||
const helpModal = document.getElementById('help-modal');
|
||
if (helpModal && helpModal.style.display !== 'none') {
|
||
this.hideHelp();
|
||
return;
|
||
}
|
||
|
||
// Close stats modal if open
|
||
const statsModal = document.getElementById('stats-modal');
|
||
if (statsModal && !statsModal.hidden) {
|
||
this.hideStats();
|
||
return;
|
||
}
|
||
|
||
// If in task management, go back to start screen
|
||
if (document.getElementById('task-management-screen').style.display !== 'none') {
|
||
this.showScreen('start-screen');
|
||
return;
|
||
}
|
||
|
||
// If in game, pause it
|
||
if (this.gameState.isRunning) {
|
||
this.pauseGame();
|
||
}
|
||
}
|
||
|
||
setupWindowResizeHandling() {
|
||
let resizeTimeout;
|
||
|
||
// Handle window resize events for dynamic image scaling
|
||
window.addEventListener('resize', () => {
|
||
// Debounce resize events to avoid excessive processing
|
||
clearTimeout(resizeTimeout);
|
||
resizeTimeout = setTimeout(() => {
|
||
this.handleWindowResize();
|
||
}, 150); // Wait 150ms after resize stops
|
||
});
|
||
|
||
// Initial setup
|
||
this.handleWindowResize();
|
||
}
|
||
|
||
handleWindowResize() {
|
||
const taskImage = document.getElementById('task-image');
|
||
if (!taskImage) return;
|
||
|
||
// Get current window dimensions
|
||
const windowWidth = window.innerWidth;
|
||
const windowHeight = window.innerHeight;
|
||
|
||
// Add dynamic class based on window size for additional styling hooks
|
||
const gameContainer = document.querySelector('.game-container');
|
||
if (gameContainer) {
|
||
gameContainer.classList.remove('window-small', 'window-medium', 'window-large', 'window-xl');
|
||
|
||
if (windowWidth >= 1600) {
|
||
gameContainer.classList.add('window-xl');
|
||
} else if (windowWidth >= 1200) {
|
||
gameContainer.classList.add('window-large');
|
||
} else if (windowWidth >= 900) {
|
||
gameContainer.classList.add('window-medium');
|
||
} else {
|
||
gameContainer.classList.add('window-small');
|
||
}
|
||
}
|
||
|
||
// Force layout recalculation for better image sizing
|
||
if (taskImage.src && this.gameState.isRunning) {
|
||
// Trigger a reflow to ensure proper image sizing
|
||
taskImage.style.opacity = '0.99';
|
||
requestAnimationFrame(() => {
|
||
taskImage.style.opacity = '';
|
||
});
|
||
}
|
||
|
||
console.log(`Window resized to ${windowWidth}x${windowHeight}, image scaling updated`);
|
||
}
|
||
|
||
checkAutoResume() {
|
||
try {
|
||
// Check if there's a saved game state to resume
|
||
const savedGameState = this.dataManager.get('autoSaveGameState');
|
||
|
||
if (savedGameState && savedGameState.isRunning === false && savedGameState.isPaused === true) {
|
||
// Show auto-resume prompt
|
||
setTimeout(() => {
|
||
if (confirm('You have a paused game session. Would you like to resume where you left off?')) {
|
||
this.loadAutoSavedGame(savedGameState);
|
||
} else {
|
||
// Clear the auto-save if user declines
|
||
localStorage.removeItem('autoSaveGameState');
|
||
}
|
||
}, 500); // Small delay to ensure UI is ready
|
||
}
|
||
} catch (error) {
|
||
console.warn('Auto-resume check failed:', this.formatErrorMessage('auto-resume', error));
|
||
// Clear potentially corrupted auto-save data
|
||
localStorage.removeItem('autoSaveGameState');
|
||
}
|
||
}
|
||
|
||
autoSaveGameState() {
|
||
// Save current game state for auto-resume
|
||
if (this.gameState.isRunning || this.gameState.isPaused) {
|
||
const saveState = {
|
||
...this.gameState,
|
||
currentTask: this.gameState.currentTask,
|
||
savedAt: new Date().toISOString()
|
||
};
|
||
this.dataManager.set('autoSaveGameState', saveState);
|
||
}
|
||
}
|
||
|
||
loadAutoSavedGame(savedState) {
|
||
// Restore the saved game state
|
||
this.gameState = {
|
||
...this.gameState,
|
||
...savedState,
|
||
isRunning: false, // Will be set to true when resumed
|
||
isPaused: true
|
||
};
|
||
|
||
// Update UI to show the paused game
|
||
this.showScreen('game-screen');
|
||
this.updateDisplay();
|
||
|
||
// Show the resume notification
|
||
this.showNotification('Game session restored! Click Resume to continue.', 'success');
|
||
}
|
||
|
||
// Loading indicator methods
|
||
showButtonLoading(buttonId) {
|
||
const button = document.getElementById(buttonId);
|
||
if (button) {
|
||
button.disabled = true;
|
||
const textSpan = button.querySelector('.btn-text');
|
||
const loadingSpan = button.querySelector('.btn-loading');
|
||
if (textSpan && loadingSpan) {
|
||
textSpan.style.display = 'none';
|
||
loadingSpan.style.display = 'inline';
|
||
}
|
||
}
|
||
}
|
||
|
||
hideButtonLoading(buttonId) {
|
||
const button = document.getElementById(buttonId);
|
||
if (button) {
|
||
button.disabled = false;
|
||
const textSpan = button.querySelector('.btn-text');
|
||
const loadingSpan = button.querySelector('.btn-loading');
|
||
if (textSpan && loadingSpan) {
|
||
textSpan.style.display = 'inline';
|
||
loadingSpan.style.display = 'none';
|
||
}
|
||
}
|
||
}
|
||
|
||
toggleDifficultyDropdown() {
|
||
const taskType = document.getElementById('new-task-type').value;
|
||
const difficultyGroup = document.getElementById('difficulty-input-group');
|
||
|
||
if (taskType === 'main') {
|
||
difficultyGroup.style.display = 'block';
|
||
} else {
|
||
difficultyGroup.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
// Task Management Methods
|
||
showTaskManagement() {
|
||
this.showScreen('task-management-screen');
|
||
this.populateTaskLists();
|
||
this.toggleDifficultyDropdown(); // Set initial visibility
|
||
}
|
||
|
||
showTaskTab(tabType) {
|
||
// Update tab buttons
|
||
document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active'));
|
||
document.getElementById(`${tabType}-tasks-tab`).classList.add('active');
|
||
|
||
// Update task lists
|
||
document.querySelectorAll('.task-list').forEach(list => list.classList.remove('active'));
|
||
document.getElementById(`${tabType}-tasks-list`).classList.add('active');
|
||
}
|
||
|
||
populateTaskLists() {
|
||
this.populateTaskList('main', gameData.mainTasks);
|
||
this.populateTaskList('consequence', gameData.consequenceTasks);
|
||
}
|
||
|
||
populateTaskList(type, tasks) {
|
||
const listElement = document.getElementById(`${type}-tasks-list`);
|
||
|
||
if (tasks.length === 0) {
|
||
listElement.innerHTML = '<div class="empty-list">No tasks added yet. Add some tasks to get started!</div>';
|
||
return;
|
||
}
|
||
|
||
listElement.innerHTML = tasks.map((task, index) => {
|
||
let difficultyDisplay = '';
|
||
if (type === 'main' && task.difficulty) {
|
||
const emoji = this.getDifficultyEmoji(task.difficulty);
|
||
const points = this.getPointsForDifficulty(task.difficulty);
|
||
difficultyDisplay = `<div class="task-difficulty-display">${emoji} ${task.difficulty} (${points} ${points === 1 ? 'point' : 'points'})</div>`;
|
||
}
|
||
|
||
return `
|
||
<div class="task-item" data-type="${type}" data-index="${index}">
|
||
<div class="task-text-display">${task.text}</div>
|
||
${difficultyDisplay}
|
||
<div class="task-actions">
|
||
<button class="btn btn-info btn-small" onclick="game.editTask('${type}', ${index})">Edit</button>
|
||
<button class="btn btn-danger btn-small" onclick="game.deleteTask('${type}', ${index})">Delete</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
}
|
||
|
||
addNewTask() {
|
||
const taskText = document.getElementById('new-task-text').value.trim();
|
||
const taskType = document.getElementById('new-task-type').value;
|
||
const taskDifficulty = document.getElementById('new-task-difficulty').value;
|
||
|
||
if (!taskText) {
|
||
alert('Please enter a task description!');
|
||
return;
|
||
}
|
||
|
||
// Create new task object
|
||
const newTask = {
|
||
id: Date.now(), // Use timestamp as unique ID
|
||
text: taskText
|
||
};
|
||
|
||
// Add difficulty for main tasks only
|
||
if (taskType === 'main') {
|
||
newTask.difficulty = taskDifficulty;
|
||
}
|
||
|
||
// Add to appropriate array
|
||
if (taskType === 'main') {
|
||
gameData.mainTasks.push(newTask);
|
||
localStorage.setItem('customMainTasks', JSON.stringify(gameData.mainTasks));
|
||
} else {
|
||
gameData.consequenceTasks.push(newTask);
|
||
localStorage.setItem('customConsequenceTasks', JSON.stringify(gameData.consequenceTasks));
|
||
}
|
||
|
||
// Clear input and refresh list
|
||
document.getElementById('new-task-text').value = '';
|
||
document.getElementById('new-task-difficulty').value = 'Medium';
|
||
this.populateTaskLists();
|
||
|
||
// Switch to the appropriate tab
|
||
this.showTaskTab(taskType === 'main' ? 'main' : 'consequence');
|
||
|
||
console.log(`Added new ${taskType} task: ${taskText}`);
|
||
}
|
||
|
||
editTask(type, index) {
|
||
const tasks = type === 'main' ? gameData.mainTasks : gameData.consequenceTasks;
|
||
const task = tasks[index];
|
||
|
||
const newText = prompt(`Edit ${type} task:`, task.text);
|
||
if (newText !== null && newText.trim() !== '') {
|
||
tasks[index].text = newText.trim();
|
||
|
||
// Save to localStorage
|
||
const storageKey = type === 'main' ? 'customMainTasks' : 'customConsequenceTasks';
|
||
localStorage.setItem(storageKey, JSON.stringify(tasks));
|
||
|
||
// Refresh display
|
||
this.populateTaskLists();
|
||
console.log(`Edited ${type} task: ${newText}`);
|
||
}
|
||
}
|
||
|
||
deleteTask(type, index) {
|
||
const tasks = type === 'main' ? gameData.mainTasks : gameData.consequenceTasks;
|
||
const task = tasks[index];
|
||
|
||
if (confirm(`Are you sure you want to delete this ${type} task?\n\n"${task.text}"`)) {
|
||
tasks.splice(index, 1);
|
||
|
||
// Save to localStorage
|
||
const storageKey = type === 'main' ? 'customMainTasks' : 'customConsequenceTasks';
|
||
localStorage.setItem(storageKey, JSON.stringify(tasks));
|
||
|
||
// Refresh display
|
||
this.populateTaskLists();
|
||
console.log(`Deleted ${type} task: ${task.text}`);
|
||
}
|
||
}
|
||
|
||
resetToDefaultTasks() {
|
||
if (confirm('This will reset all tasks to the original defaults. Are you sure?')) {
|
||
// Clear localStorage
|
||
localStorage.removeItem('customMainTasks');
|
||
localStorage.removeItem('customConsequenceTasks');
|
||
|
||
// Reload the page to get fresh default tasks
|
||
location.reload();
|
||
}
|
||
}
|
||
|
||
changeTheme(themeName) {
|
||
// Remove any existing theme link
|
||
const existingThemeLink = document.getElementById('dynamic-theme');
|
||
if (existingThemeLink) {
|
||
existingThemeLink.remove();
|
||
}
|
||
|
||
// Remove old theme classes if they exist
|
||
const oldThemeClasses = ['theme-sunset', 'theme-forest', 'theme-midnight', 'theme-pastel', 'theme-neon', 'theme-autumn', 'theme-monochrome'];
|
||
document.body.classList.remove(...oldThemeClasses);
|
||
|
||
// Load the appropriate theme CSS file
|
||
if (themeName && themeName !== 'balanced-purple') {
|
||
const themeLink = document.createElement('link');
|
||
themeLink.id = 'dynamic-theme';
|
||
themeLink.rel = 'stylesheet';
|
||
themeLink.href = `${themeName}-theme.css`;
|
||
|
||
// Handle missing theme files gracefully
|
||
themeLink.onerror = () => {
|
||
console.warn(`Theme file ${themeName}-theme.css not found, falling back to default theme`);
|
||
themeLink.remove();
|
||
// Fall back to default theme
|
||
localStorage.setItem('gameTheme', 'balanced-purple');
|
||
};
|
||
|
||
document.head.appendChild(themeLink);
|
||
}
|
||
// balanced-purple is our default theme loaded in the HTML, so no need to load additional CSS
|
||
|
||
// Save theme preference
|
||
localStorage.setItem('gameTheme', themeName);
|
||
|
||
console.log(`Theme changed to: ${themeName}`);
|
||
}
|
||
|
||
loadSavedTheme() {
|
||
// Load saved theme or default to balanced-purple
|
||
const savedTheme = localStorage.getItem('gameTheme') || 'balanced-purple';
|
||
|
||
// Make sure the dropdown reflects the saved theme
|
||
const themeDropdown = document.getElementById('theme-dropdown');
|
||
if (themeDropdown) {
|
||
themeDropdown.value = savedTheme;
|
||
}
|
||
|
||
// Apply the theme
|
||
this.changeTheme(savedTheme);
|
||
}
|
||
|
||
showScreen(screenId) {
|
||
// Hide all screens
|
||
document.querySelectorAll('.screen').forEach(screen => {
|
||
screen.classList.remove('active');
|
||
});
|
||
|
||
// Show target screen
|
||
const targetScreen = document.getElementById(screenId);
|
||
if (targetScreen) {
|
||
targetScreen.classList.add('active');
|
||
} else {
|
||
console.error(`Screen element not found: ${screenId}`);
|
||
}
|
||
|
||
// Update start screen status when showing start screen
|
||
if (screenId === 'start-screen') {
|
||
this.updateStartScreenStatus();
|
||
}
|
||
}
|
||
|
||
updateStartScreenStatus() {
|
||
// Status message removed for cleaner home screen interface
|
||
const statusDiv = document.getElementById('start-screen-status');
|
||
if (statusDiv) {
|
||
statusDiv.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
toggleOptionsMenu() {
|
||
const optionsMenu = document.getElementById('options-menu');
|
||
const button = document.getElementById('options-menu-btn');
|
||
|
||
if (optionsMenu.style.display === 'none' || optionsMenu.style.display === '') {
|
||
optionsMenu.style.display = 'block';
|
||
button.textContent = '⚙️ Close Options';
|
||
} else {
|
||
optionsMenu.style.display = 'none';
|
||
button.textContent = '⚙️ Options';
|
||
}
|
||
}
|
||
|
||
toggleMusic() {
|
||
this.musicManager.toggle();
|
||
}
|
||
|
||
toggleLoop() {
|
||
this.musicManager.toggleLoopMode();
|
||
}
|
||
|
||
toggleShuffle() {
|
||
this.musicManager.toggleShuffleMode();
|
||
}
|
||
|
||
changeTrack(trackIndex) {
|
||
this.musicManager.changeTrack(trackIndex);
|
||
}
|
||
|
||
changeVolume(volume) {
|
||
this.musicManager.setVolume(volume);
|
||
}
|
||
|
||
// Data Management Methods
|
||
exportData() {
|
||
try {
|
||
this.showButtonLoading('export-btn');
|
||
|
||
// Small delay to show loading indicator
|
||
setTimeout(() => {
|
||
try {
|
||
this.dataManager.exportData(true);
|
||
this.showNotification('💾 Save file exported successfully!', 'success');
|
||
} catch (error) {
|
||
this.showNotification('❌ ' + this.formatErrorMessage('export', error), 'error');
|
||
} finally {
|
||
this.hideButtonLoading('export-btn');
|
||
}
|
||
}, 300);
|
||
} catch (error) {
|
||
this.showNotification('❌ ' + this.formatErrorMessage('export', error), 'error');
|
||
this.hideButtonLoading('export-btn');
|
||
}
|
||
}
|
||
|
||
importData() {
|
||
// Trigger the file input
|
||
document.getElementById('import-file').click();
|
||
}
|
||
|
||
async handleFileImport(event) {
|
||
const file = event.target.files[0];
|
||
if (!file) return;
|
||
|
||
this.showButtonLoading('import-btn');
|
||
|
||
try {
|
||
await this.dataManager.importData(file);
|
||
this.showNotification('📁 Save file imported successfully!', 'success');
|
||
|
||
// Refresh UI to show new data
|
||
this.musicManager.volume = this.dataManager.getSetting('music.volume') || 30;
|
||
this.musicManager.currentTrackIndex = this.dataManager.getSetting('music.currentTrack') || 0;
|
||
this.musicManager.loopMode = this.dataManager.getSetting('music.loopMode') || 0;
|
||
this.musicManager.shuffleMode = this.dataManager.getSetting('music.shuffle') || false;
|
||
this.musicManager.initializeVolumeUI();
|
||
|
||
// Update theme
|
||
const theme = this.dataManager.getSetting('theme') || 'balanced-purple';
|
||
this.changeTheme(theme);
|
||
|
||
} catch (error) {
|
||
this.showNotification('❌ ' + this.formatErrorMessage('import', error), 'error');
|
||
} finally {
|
||
this.hideButtonLoading('import-btn');
|
||
}
|
||
|
||
// Clear the file input
|
||
event.target.value = '';
|
||
}
|
||
|
||
showStats() {
|
||
const stats = this.dataManager.getStats();
|
||
|
||
// Update stat displays
|
||
document.getElementById('stat-games').textContent = stats.totalGamesPlayed;
|
||
document.getElementById('stat-completed').textContent = stats.totalTasksCompleted;
|
||
document.getElementById('stat-score').textContent = stats.bestScore;
|
||
document.getElementById('stat-streak').textContent = stats.currentStreak;
|
||
document.getElementById('stat-rate').textContent = stats.completionPercentage + '%';
|
||
document.getElementById('stat-hours').textContent = stats.hoursPlayed;
|
||
|
||
document.getElementById('stats-modal').style.display = 'block';
|
||
}
|
||
|
||
hideStats() {
|
||
document.getElementById('stats-modal').style.display = 'none';
|
||
}
|
||
|
||
showHelp() {
|
||
document.getElementById('help-modal').style.display = 'block';
|
||
}
|
||
|
||
hideHelp() {
|
||
document.getElementById('help-modal').style.display = 'none';
|
||
}
|
||
|
||
toggleHelp() {
|
||
const helpModal = document.getElementById('help-modal');
|
||
if (helpModal.style.display === 'none' || helpModal.style.display === '') {
|
||
this.showHelp();
|
||
} else {
|
||
this.hideHelp();
|
||
}
|
||
}
|
||
|
||
// Image Management Methods
|
||
showImageManagement() {
|
||
// Reset listener flag to allow fresh attachment
|
||
this.imageManagementListenersAttached = false;
|
||
|
||
this.showScreen('image-management-screen');
|
||
this.setupImageManagementEventListeners();
|
||
|
||
// Wait for image discovery to complete before loading gallery
|
||
if (!this.imageDiscoveryComplete) {
|
||
const gallery = document.getElementById('image-gallery');
|
||
gallery.innerHTML = '<div class="loading">Discovering images...</div>';
|
||
|
||
// Wait and try again
|
||
setTimeout(() => {
|
||
if (this.imageDiscoveryComplete) {
|
||
this.loadImageGallery();
|
||
} else {
|
||
gallery.innerHTML = '<div class="loading">Still discovering images... Please wait</div>';
|
||
setTimeout(() => this.loadImageGallery(), 1000);
|
||
}
|
||
}, 500);
|
||
} else {
|
||
this.loadImageGallery();
|
||
}
|
||
}
|
||
|
||
switchImageTab(tabType) {
|
||
// Update tab buttons
|
||
const taskTab = document.getElementById('task-images-tab');
|
||
const consequenceTab = document.getElementById('consequence-images-tab');
|
||
const taskGallery = document.getElementById('task-images-gallery');
|
||
const consequenceGallery = document.getElementById('consequence-images-gallery');
|
||
|
||
if (tabType === 'task') {
|
||
taskTab.classList.add('active');
|
||
consequenceTab.classList.remove('active');
|
||
taskGallery.classList.add('active');
|
||
consequenceGallery.classList.remove('active');
|
||
} else {
|
||
taskTab.classList.remove('active');
|
||
consequenceTab.classList.add('active');
|
||
taskGallery.classList.remove('active');
|
||
consequenceGallery.classList.add('active');
|
||
}
|
||
|
||
// Update gallery controls to work with current tab
|
||
this.updateImageGalleryControls(tabType);
|
||
}
|
||
|
||
updateImageGalleryControls(activeTab) {
|
||
// Update the select/deselect/delete buttons to work with the active tab
|
||
const selectAllBtn = document.getElementById('select-all-images-btn');
|
||
const deselectAllBtn = document.getElementById('deselect-all-images-btn');
|
||
const deleteBtn = document.getElementById('delete-selected-btn');
|
||
|
||
if (selectAllBtn) {
|
||
selectAllBtn.onclick = () => this.selectAllImages(activeTab);
|
||
}
|
||
|
||
if (deselectAllBtn) {
|
||
deselectAllBtn.onclick = () => this.deselectAllImages(activeTab);
|
||
}
|
||
|
||
if (deleteBtn) {
|
||
deleteBtn.onclick = () => this.deleteSelectedImages(activeTab);
|
||
}
|
||
}
|
||
|
||
setupImageManagementEventListeners() {
|
||
// Check if we already have listeners attached to prevent duplicates
|
||
if (this.imageManagementListenersAttached) {
|
||
return;
|
||
}
|
||
|
||
// Back button
|
||
const backBtn = document.getElementById('back-to-start-from-images-btn');
|
||
if (backBtn) {
|
||
backBtn.onclick = () => this.showScreen('start-screen');
|
||
}
|
||
|
||
// Desktop import buttons
|
||
const importTaskBtn = document.getElementById('import-task-images-btn');
|
||
if (importTaskBtn) {
|
||
importTaskBtn.onclick = async () => {
|
||
if (this.fileManager) {
|
||
await this.fileManager.selectAndImportImages('task');
|
||
this.loadImageGallery(); // Refresh the gallery to show new images
|
||
} else {
|
||
this.showNotification('Desktop file manager not available', 'warning');
|
||
}
|
||
};
|
||
}
|
||
|
||
const importConsequenceBtn = document.getElementById('import-consequence-images-btn');
|
||
if (importConsequenceBtn) {
|
||
importConsequenceBtn.onclick = async () => {
|
||
if (this.fileManager) {
|
||
await this.fileManager.selectAndImportImages('consequence');
|
||
this.loadImageGallery(); // Refresh the gallery to show new images
|
||
} else {
|
||
this.showNotification('Desktop file manager not available', 'warning');
|
||
}
|
||
};
|
||
}
|
||
|
||
// Storage info button
|
||
const storageInfoBtn = document.getElementById('storage-info-btn');
|
||
if (storageInfoBtn) {
|
||
storageInfoBtn.onclick = () => this.showStorageInfo();
|
||
}
|
||
|
||
// Tab buttons
|
||
const taskImagesTab = document.getElementById('task-images-tab');
|
||
if (taskImagesTab) {
|
||
taskImagesTab.onclick = () => this.switchImageTab('task');
|
||
}
|
||
|
||
const consequenceImagesTab = document.getElementById('consequence-images-tab');
|
||
if (consequenceImagesTab) {
|
||
consequenceImagesTab.onclick = () => this.switchImageTab('consequence');
|
||
}
|
||
|
||
// Upload input (fallback for web mode)
|
||
const uploadInput = document.getElementById('image-upload-input');
|
||
if (uploadInput) {
|
||
uploadInput.onchange = (e) => this.handleImageUpload(e);
|
||
}
|
||
|
||
// Web upload button (fallback)
|
||
const uploadBtn = document.getElementById('upload-images-btn');
|
||
if (uploadBtn) {
|
||
uploadBtn.onclick = () => this.uploadImages();
|
||
}
|
||
|
||
// Mark listeners as attached
|
||
this.imageManagementListenersAttached = true;
|
||
}
|
||
|
||
loadImageGallery() {
|
||
// Load both task and consequence image galleries
|
||
this.cleanupInvalidImages(); // Clean up invalid images first
|
||
this.loadTaskImages();
|
||
this.loadConsequenceImages();
|
||
this.updateImageCounts();
|
||
|
||
// Initialize with task tab active and update controls
|
||
this.updateImageGalleryControls('task');
|
||
}
|
||
|
||
loadTaskImages() {
|
||
const gallery = document.getElementById('task-images-gallery');
|
||
gallery.innerHTML = '<div class="loading">Loading task images...</div>';
|
||
|
||
// Get task images
|
||
const taskImages = gameData.discoveredTaskImages || [];
|
||
const customImages = this.dataManager.get('customImages') || { task: [], consequence: [] };
|
||
const disabledImages = this.dataManager.get('disabledImages') || [];
|
||
|
||
// Get custom task images (handle both old array format and new object format)
|
||
let customTaskImages = [];
|
||
if (Array.isArray(customImages)) {
|
||
// Old format - treat all as task images for backward compatibility
|
||
customTaskImages = customImages;
|
||
} else {
|
||
// New format - get task images specifically
|
||
customTaskImages = customImages.task || [];
|
||
}
|
||
|
||
const allTaskImages = [...taskImages, ...customTaskImages];
|
||
|
||
if (allTaskImages.length === 0) {
|
||
gallery.innerHTML = '<div class="no-images">No task images found. Upload or scan for task images!</div>';
|
||
return;
|
||
}
|
||
|
||
gallery.innerHTML = '';
|
||
this.renderImageItems(gallery, allTaskImages, disabledImages, customTaskImages);
|
||
}
|
||
|
||
loadConsequenceImages() {
|
||
const gallery = document.getElementById('consequence-images-gallery');
|
||
gallery.innerHTML = '<div class="loading">Loading consequence images...</div>';
|
||
|
||
// Get consequence images
|
||
const consequenceImages = gameData.discoveredConsequenceImages || [];
|
||
const customImages = this.dataManager.get('customImages') || { task: [], consequence: [] };
|
||
const disabledImages = this.dataManager.get('disabledImages') || [];
|
||
|
||
// Get custom consequence images (new format only)
|
||
let customConsequenceImages = [];
|
||
if (!Array.isArray(customImages)) {
|
||
customConsequenceImages = customImages.consequence || [];
|
||
}
|
||
|
||
const allConsequenceImages = [...consequenceImages, ...customConsequenceImages];
|
||
|
||
if (allConsequenceImages.length === 0) {
|
||
gallery.innerHTML = '<div class="no-images">No consequence images found. Upload or scan for consequence images!</div>';
|
||
return;
|
||
}
|
||
|
||
gallery.innerHTML = '';
|
||
this.renderImageItems(gallery, allConsequenceImages, disabledImages, customConsequenceImages);
|
||
}
|
||
|
||
renderImageItems(gallery, images, disabledImages, customImages) {
|
||
images.forEach((imageData, index) => {
|
||
const imageItem = document.createElement('div');
|
||
imageItem.className = 'image-item';
|
||
|
||
// Handle both old base64 format and new cached metadata format
|
||
let imagePath, displayName, imageSrc;
|
||
|
||
if (typeof imageData === 'string') {
|
||
// Old base64 format or regular file path
|
||
imagePath = imageData;
|
||
imageSrc = imageData;
|
||
displayName = this.getImageDisplayName(imageData);
|
||
} else {
|
||
// New cached metadata format
|
||
imagePath = imageData.cachedPath || imageData.originalName;
|
||
|
||
// Use dataUrl if available, otherwise show placeholder
|
||
if (imageData.dataUrl) {
|
||
imageSrc = imageData.dataUrl;
|
||
} else {
|
||
// Legacy cached image without dataUrl - show placeholder
|
||
imageSrc = this.createPlaceholderImage('Missing Data');
|
||
console.warn('Image missing dataUrl:', imageData.originalName);
|
||
}
|
||
|
||
displayName = imageData.originalName || 'Cached Image';
|
||
}
|
||
|
||
imageItem.dataset.imagePath = imagePath;
|
||
|
||
const isDisabled = disabledImages.includes(imagePath);
|
||
|
||
imageItem.innerHTML = `
|
||
<img src="${imageSrc}" alt="Image" class="image-preview" title="${displayName}" onerror="this.src='data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjE1MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjZGVlMmU2Ii8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxNCIgZmlsbD0iIzZjNzU3ZCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iPkZhaWxlZCB0byBsb2FkPC90ZXh0Pjwvc3ZnPg=='">
|
||
<div class="image-info">
|
||
<div class="image-controls">
|
||
<label>
|
||
<input type="checkbox" class="image-checkbox" ${isDisabled ? '' : 'checked'}>
|
||
Enable
|
||
</label>
|
||
<span class="image-status ${isDisabled ? 'image-disabled' : 'image-enabled'}">
|
||
${isDisabled ? 'Disabled' : 'Enabled'}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
// Add event listener for enable/disable
|
||
const checkbox = imageItem.querySelector('.image-checkbox');
|
||
checkbox.addEventListener('change', (e) => this.toggleImageEnabled(imagePath, e.target.checked));
|
||
|
||
// Add event listener for individual image selection (click to select/deselect)
|
||
imageItem.addEventListener('click', (e) => {
|
||
// Don't toggle selection if clicking on the checkbox
|
||
if (e.target.type !== 'checkbox') {
|
||
this.toggleImageSelection(imageItem);
|
||
}
|
||
});
|
||
|
||
gallery.appendChild(imageItem);
|
||
});
|
||
}
|
||
|
||
updateImageCounts() {
|
||
const imageCount = document.querySelector('.image-count');
|
||
const taskImages = gameData.discoveredTaskImages || [];
|
||
const consequenceImages = gameData.discoveredConsequenceImages || [];
|
||
const customImages = this.dataManager.get('customImages') || { task: [], consequence: [] };
|
||
const disabledImages = this.dataManager.get('disabledImages') || [];
|
||
|
||
// Handle both old array format and new object format
|
||
let customTaskCount = 0;
|
||
let customConsequenceCount = 0;
|
||
let totalCustomImages = 0;
|
||
|
||
if (Array.isArray(customImages)) {
|
||
// Old format - treat all as task images
|
||
customTaskCount = customImages.length;
|
||
totalCustomImages = customImages.length;
|
||
} else {
|
||
// New format
|
||
customTaskCount = (customImages.task || []).length;
|
||
customConsequenceCount = (customImages.consequence || []).length;
|
||
totalCustomImages = customTaskCount + customConsequenceCount;
|
||
}
|
||
|
||
const totalTaskImages = taskImages.length + customTaskCount;
|
||
const totalConsequenceImages = consequenceImages.length + customConsequenceCount;
|
||
const totalImages = totalTaskImages + totalConsequenceImages;
|
||
const enabledImages = totalImages - disabledImages.length;
|
||
|
||
if (imageCount) {
|
||
imageCount.textContent = `${totalImages} total images (${totalTaskImages} task, ${totalConsequenceImages} consequence, ${enabledImages} enabled)`;
|
||
}
|
||
}
|
||
|
||
getAllImages() {
|
||
// Return the current images from the game's discovery system
|
||
const taskImages = gameData.discoveredTaskImages || [];
|
||
const consequenceImages = gameData.discoveredConsequenceImages || [];
|
||
|
||
console.log('getAllImages - taskImages:', taskImages);
|
||
console.log('getAllImages - consequenceImages:', consequenceImages);
|
||
console.log('getAllImages - imageDiscoveryComplete:', this.imageDiscoveryComplete);
|
||
|
||
return [...taskImages, ...consequenceImages];
|
||
}
|
||
|
||
getImageDisplayName(imagePath) {
|
||
// Extract filename from path
|
||
const parts = imagePath.split('/');
|
||
return parts[parts.length - 1];
|
||
}
|
||
|
||
toggleImageEnabled(imagePath, enabled) {
|
||
let disabledImages = this.dataManager.get('disabledImages') || [];
|
||
|
||
if (enabled) {
|
||
disabledImages = disabledImages.filter(img => img !== imagePath);
|
||
} else {
|
||
if (!disabledImages.includes(imagePath)) {
|
||
disabledImages.push(imagePath);
|
||
}
|
||
}
|
||
|
||
this.dataManager.set('disabledImages', disabledImages);
|
||
|
||
// Update the status display
|
||
const imageItem = document.querySelector(`[data-image-path="${imagePath}"]`);
|
||
if (imageItem) {
|
||
const statusElement = imageItem.querySelector('.image-status');
|
||
statusElement.className = `image-status ${enabled ? 'image-enabled' : 'image-disabled'}`;
|
||
statusElement.textContent = enabled ? 'Enabled' : 'Disabled';
|
||
}
|
||
|
||
// Update count
|
||
this.updateImageCount();
|
||
|
||
this.showNotification(`Image ${enabled ? 'enabled' : 'disabled'} successfully!`, 'success');
|
||
}
|
||
|
||
toggleImageSelection(imageItem) {
|
||
imageItem.classList.toggle('selected');
|
||
}
|
||
|
||
uploadImages() {
|
||
document.getElementById('image-upload-input').click();
|
||
}
|
||
|
||
async handleImageUpload(event) {
|
||
const files = Array.from(event.target.files);
|
||
if (files.length === 0) return;
|
||
|
||
// Determine which tab is active to know where to save images
|
||
const activeTab = this.getActiveImageTab();
|
||
|
||
this.showNotification(`Processing ${files.length} images for ${activeTab} category...`, 'info');
|
||
let successCount = 0;
|
||
let failedCount = 0;
|
||
const maxImages = 50; // Reasonable limit to prevent storage overflow
|
||
|
||
// Check current image count
|
||
const customImages = this.dataManager.get('customImages') || { task: [], consequence: [] };
|
||
const currentTaskCount = Array.isArray(customImages) ? customImages.length : (customImages.task || []).length;
|
||
const currentConsequenceCount = Array.isArray(customImages) ? 0 : (customImages.consequence || []).length;
|
||
const totalCurrentImages = currentTaskCount + currentConsequenceCount;
|
||
|
||
if (totalCurrentImages >= maxImages) {
|
||
this.showNotification(`Image limit reached (${maxImages} max). Please delete some images first.`, 'error');
|
||
return;
|
||
}
|
||
|
||
for (const file of files) {
|
||
if (successCount + totalCurrentImages >= maxImages) {
|
||
this.showNotification(`Stopped at image limit (${maxImages} max). ${successCount} uploaded, ${files.length - successCount} skipped.`, 'warning');
|
||
break;
|
||
}
|
||
|
||
try {
|
||
const imageData = await this.processUploadedImage(file, activeTab);
|
||
if (imageData) {
|
||
// Try to save to localStorage with error handling
|
||
if (await this.saveImageWithQuotaCheck(imageData, activeTab)) {
|
||
successCount++;
|
||
} else {
|
||
failedCount++;
|
||
console.warn('Storage quota exceeded for image:', file.name);
|
||
break; // Stop processing if quota exceeded
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.warn('Failed to process image:', file.name, error);
|
||
failedCount++;
|
||
}
|
||
}
|
||
|
||
if (successCount > 0) {
|
||
this.showNotification(`${successCount} ${activeTab} image(s) uploaded successfully!${failedCount > 0 ? ` (${failedCount} failed due to storage limits)` : ''}`, successCount > failedCount ? 'success' : 'warning');
|
||
this.loadImageGallery(); // Refresh the gallery
|
||
this.cleanupInvalidImages(); // Clean up any images without dataUrl
|
||
} else {
|
||
this.showNotification(`Upload failed. ${failedCount > 0 ? 'Storage quota exceeded. Try deleting some images first.' : 'Please try again.'}`, 'error');
|
||
}
|
||
|
||
// Clear the input
|
||
event.target.value = '';
|
||
}
|
||
|
||
async saveImageWithQuotaCheck(imageData, activeTab) {
|
||
try {
|
||
let customImages = this.dataManager.get('customImages') || { task: [], consequence: [] };
|
||
|
||
// Ensure the structure exists
|
||
if (!customImages.task) customImages.task = [];
|
||
if (!customImages.consequence) customImages.consequence = [];
|
||
|
||
// Add to the appropriate category
|
||
customImages[activeTab].push(imageData);
|
||
|
||
// Try to save with quota checking
|
||
this.dataManager.set('customImages', customImages);
|
||
return true;
|
||
} catch (error) {
|
||
if (error.name === 'QuotaExceededError') {
|
||
console.error('Storage quota exceeded:', error);
|
||
return false;
|
||
} else {
|
||
console.error('Error saving image:', error);
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
|
||
getActiveImageTab() {
|
||
const taskTab = document.getElementById('task-images-tab');
|
||
return taskTab && taskTab.classList.contains('active') ? 'task' : 'consequence';
|
||
}
|
||
|
||
cleanupInvalidImages() {
|
||
const customImages = this.dataManager.get('customImages') || { task: [], consequence: [] };
|
||
let hasChanges = false;
|
||
|
||
if (Array.isArray(customImages)) {
|
||
// Old format - clean up and convert to new format
|
||
const validImages = customImages.filter(img => {
|
||
if (typeof img === 'string') {
|
||
return true; // Keep old base64 strings
|
||
} else {
|
||
return !!img.dataUrl; // Only keep metadata objects with dataUrl
|
||
}
|
||
});
|
||
|
||
if (validImages.length !== customImages.length) {
|
||
console.log(`Cleaned up ${customImages.length - validImages.length} invalid cached images`);
|
||
// Convert to new format - put all in task category for backward compatibility
|
||
this.dataManager.set('customImages', { task: validImages, consequence: [] });
|
||
hasChanges = true;
|
||
} else {
|
||
// Convert to new format without cleaning
|
||
this.dataManager.set('customImages', { task: customImages, consequence: [] });
|
||
hasChanges = true;
|
||
}
|
||
} else {
|
||
// New format - clean up both categories
|
||
const cleanCategory = (category) => {
|
||
const images = customImages[category] || [];
|
||
return images.filter(img => {
|
||
if (typeof img === 'string') {
|
||
return true; // Keep old base64 strings
|
||
} else {
|
||
return !!img.dataUrl; // Only keep metadata objects with dataUrl
|
||
}
|
||
});
|
||
};
|
||
|
||
const validTaskImages = cleanCategory('task');
|
||
const validConsequenceImages = cleanCategory('consequence');
|
||
|
||
const originalTaskCount = (customImages.task || []).length;
|
||
const originalConsequenceCount = (customImages.consequence || []).length;
|
||
|
||
if (validTaskImages.length !== originalTaskCount || validConsequenceImages.length !== originalConsequenceCount) {
|
||
const cleanedCount = (originalTaskCount + originalConsequenceCount) - (validTaskImages.length + validConsequenceImages.length);
|
||
console.log(`Cleaned up ${cleanedCount} invalid cached images`);
|
||
this.dataManager.set('customImages', { task: validTaskImages, consequence: validConsequenceImages });
|
||
hasChanges = true;
|
||
}
|
||
}
|
||
|
||
return hasChanges;
|
||
}
|
||
|
||
async scanForNewImages() {
|
||
this.showNotification('Scanning directories for new images...', 'info');
|
||
|
||
try {
|
||
// Get the current embedded manifest
|
||
let manifest = this.getEmbeddedManifest();
|
||
|
||
// Force a fresh scan by clearing any cached manifest
|
||
this.dataManager.set('cachedManifest', null);
|
||
|
||
// Perform comprehensive scan for new images
|
||
console.log('Performing user-requested scan for new images...');
|
||
manifest = await this.updateManifestWithNewImages(manifest);
|
||
|
||
// Update discovered images with new manifest
|
||
gameData.discoveredTaskImages = await this.verifyImagesFromManifest(manifest.tasks, 'images/tasks/');
|
||
gameData.discoveredConsequenceImages = await this.verifyImagesFromManifest(manifest.consequences, 'images/consequences/');
|
||
|
||
// Check how many new images were found
|
||
const totalImages = gameData.discoveredTaskImages.length + gameData.discoveredConsequenceImages.length;
|
||
|
||
this.showNotification(`Scan complete! Found ${totalImages} total images.`, 'success');
|
||
this.loadImageGallery(); // Refresh the gallery to show any new images
|
||
|
||
} catch (error) {
|
||
console.error('Directory scan failed:', error);
|
||
this.showNotification('Directory scan failed. Please try again.', 'error');
|
||
}
|
||
}
|
||
|
||
clearImageCache() {
|
||
// Clear the cached manifest to force a fresh scan next time
|
||
this.dataManager.set('cachedManifest', null);
|
||
this.showNotification('Image cache cleared. Next scan will be fresh.', 'success');
|
||
}
|
||
|
||
showStorageInfo() {
|
||
try {
|
||
const isDesktop = window.electronAPI !== undefined;
|
||
const customImages = this.dataManager.get('customImages') || { task: [], consequence: [] };
|
||
|
||
let totalCustomImages = 0;
|
||
let taskCount = 0;
|
||
let consequenceCount = 0;
|
||
|
||
if (Array.isArray(customImages)) {
|
||
// Old format
|
||
totalCustomImages = customImages.length;
|
||
taskCount = customImages.length;
|
||
consequenceCount = 0;
|
||
} else {
|
||
// New format - count both categories
|
||
taskCount = (customImages.task || []).length;
|
||
consequenceCount = (customImages.consequence || []).length;
|
||
totalCustomImages = taskCount + consequenceCount;
|
||
}
|
||
|
||
let message;
|
||
|
||
if (isDesktop) {
|
||
message = `
|
||
📊 Desktop Application Storage:
|
||
|
||
🖼️ Image Library:
|
||
├─ Task Images: ${taskCount}
|
||
└─ Consequence Images: ${consequenceCount}
|
||
Total Images: ${totalCustomImages}
|
||
|
||
💾 Storage Type: Native File System
|
||
📁 Storage Location: images/ folder in app directory
|
||
💡 Storage Capacity: Unlimited (depends on available disk space)
|
||
|
||
✅ Desktop Benefits:
|
||
• No browser storage limitations
|
||
• Full-resolution image support
|
||
• Native file system performance
|
||
• Automatic directory organization
|
||
• Cross-platform file compatibility
|
||
|
||
🎯 Image Management:
|
||
• Import images using native file dialogs
|
||
• Images automatically organized by category
|
||
• No compression or size restrictions
|
||
• Direct file system access for best performance
|
||
|
||
${totalCustomImages === 0 ? '📷 No custom images imported yet. Use the Import buttons to add your images!' : ''}
|
||
`.trim();
|
||
} else {
|
||
// Original web version info
|
||
const storageData = localStorage.getItem(this.dataManager.storageKey);
|
||
const storageSize = storageData ? storageData.length : 0;
|
||
const storageMB = (storageSize / (1024 * 1024)).toFixed(2);
|
||
const maxMB = 6;
|
||
const usagePercent = ((storageSize / (maxMB * 1024 * 1024)) * 100).toFixed(1);
|
||
|
||
message = `
|
||
📊 Browser Storage Information:
|
||
|
||
LocalStorage Used: ${storageMB} MB / ~${maxMB} MB (${usagePercent}%)
|
||
Total Custom Images: ${totalCustomImages}
|
||
|
||
${usagePercent > 85 ? '⚠️ Storage getting full - consider deleting some images' :
|
||
usagePercent > 70 ? '⚡ Storage usage is moderate' :
|
||
'✅ Storage usage is healthy'}
|
||
|
||
💡 Browser Limitations:
|
||
- Limited to ~6MB total storage
|
||
- Image compression required
|
||
- 50 image limit to prevent storage issues
|
||
|
||
💡 Consider upgrading to the desktop version for unlimited storage!
|
||
`.trim();
|
||
}
|
||
|
||
alert(message);
|
||
} catch (error) {
|
||
this.showNotification('Failed to get storage info', 'error');
|
||
}
|
||
}
|
||
|
||
async processUploadedImage(file, category = 'task') {
|
||
return new Promise(async (resolve, reject) => {
|
||
try {
|
||
// Validate file type
|
||
if (!file.type.startsWith('image/')) {
|
||
reject(new Error('Not an image file'));
|
||
return;
|
||
}
|
||
|
||
// Check file size before processing - 20MB limit
|
||
if (file.size > 20 * 1024 * 1024) {
|
||
reject(new Error('Image too large. Please use images smaller than 20MB.'));
|
||
return;
|
||
}
|
||
|
||
console.log(`Processing image: ${file.name}, size: ${(file.size / (1024 * 1024)).toFixed(2)}MB`);
|
||
|
||
// Generate unique filename for cached image
|
||
const timestamp = Date.now();
|
||
const fileExtension = file.name.split('.').pop().toLowerCase();
|
||
const cachedFileName = `cached_${timestamp}.${fileExtension}`;
|
||
const cachedImagePath = `images/cached/${cachedFileName}`;
|
||
|
||
// For web compatibility, we'll still compress but save as blob URL reference
|
||
const canvas = document.createElement('canvas');
|
||
const ctx = canvas.getContext('2d');
|
||
const img = new Image();
|
||
|
||
img.onload = async () => {
|
||
try {
|
||
// Calculate new dimensions (higher quality - 1600x1200)
|
||
let { width, height } = img;
|
||
const maxWidth = 1600;
|
||
const maxHeight = 1200;
|
||
|
||
console.log(`Original dimensions: ${width}x${height}`);
|
||
|
||
if (width > maxWidth || height > maxHeight) {
|
||
const ratio = Math.min(maxWidth / width, maxHeight / height);
|
||
width = Math.floor(width * ratio);
|
||
height = Math.floor(height * ratio);
|
||
console.log(`Resized dimensions: ${width}x${height}`);
|
||
}
|
||
|
||
// Set canvas size and draw high-quality image
|
||
canvas.width = width;
|
||
canvas.height = height;
|
||
ctx.drawImage(img, 0, 0, width, height);
|
||
|
||
// Convert to blob with high quality (0.95 for minimal compression)
|
||
canvas.toBlob(async (blob) => {
|
||
try {
|
||
// Save the blob as file reference
|
||
const imageData = await this.saveBlobToCache(blob, cachedFileName, file.name);
|
||
const compressedSize = (blob.size / (1024 * 1024)).toFixed(2);
|
||
console.log(`Cached image size: ${compressedSize}MB at ${cachedImagePath}`);
|
||
|
||
resolve(imageData);
|
||
} catch (error) {
|
||
console.error('Failed to save to cache:', error);
|
||
reject(error);
|
||
}
|
||
}, 'image/jpeg', 0.95);
|
||
|
||
} catch (error) {
|
||
console.error('Image processing error:', error);
|
||
reject(error);
|
||
}
|
||
};
|
||
|
||
img.onerror = () => reject(new Error('Failed to load image'));
|
||
|
||
// Create object URL for the image
|
||
const reader = new FileReader();
|
||
reader.onload = (e) => {
|
||
img.src = e.target.result;
|
||
};
|
||
reader.onerror = reject;
|
||
reader.readAsDataURL(file);
|
||
|
||
} catch (error) {
|
||
reject(error);
|
||
}
|
||
});
|
||
}
|
||
|
||
async saveBlobToCache(blob, cachedFileName, originalName) {
|
||
// Convert blob to base64 for reliable storage and display
|
||
return new Promise((resolve, reject) => {
|
||
const reader = new FileReader();
|
||
reader.onload = () => {
|
||
const imageMetadata = {
|
||
cachedFileName: cachedFileName,
|
||
originalName: originalName,
|
||
cachedPath: `images/cached/${cachedFileName}`,
|
||
dataUrl: reader.result, // Store the base64 data URL
|
||
size: blob.size,
|
||
type: blob.type,
|
||
timestamp: Date.now()
|
||
};
|
||
resolve(imageMetadata);
|
||
};
|
||
reader.onerror = reject;
|
||
reader.readAsDataURL(blob);
|
||
});
|
||
}
|
||
|
||
selectAllImages(tabType = 'task') {
|
||
const galleryId = tabType === 'task' ? 'task-images-gallery' : 'consequence-images-gallery';
|
||
const gallery = document.getElementById(galleryId);
|
||
const imageItems = gallery.querySelectorAll('.image-item');
|
||
imageItems.forEach(imageItem => {
|
||
imageItem.classList.add('selected');
|
||
});
|
||
}
|
||
|
||
deselectAllImages(tabType = 'task') {
|
||
const galleryId = tabType === 'task' ? 'task-images-gallery' : 'consequence-images-gallery';
|
||
const gallery = document.getElementById(galleryId);
|
||
const imageItems = gallery.querySelectorAll('.image-item');
|
||
imageItems.forEach(imageItem => {
|
||
imageItem.classList.remove('selected');
|
||
});
|
||
}
|
||
|
||
async deleteSelectedImages(tabType = 'task') {
|
||
const galleryId = tabType === 'task' ? 'task-images-gallery' : 'consequence-images-gallery';
|
||
const gallery = document.getElementById(galleryId);
|
||
const selectedItems = gallery.querySelectorAll('.image-item.selected');
|
||
|
||
if (selectedItems.length === 0) {
|
||
this.showNotification('No images selected for deletion.', 'warning');
|
||
return;
|
||
}
|
||
|
||
if (!confirm(`Are you sure you want to delete ${selectedItems.length} selected image(s)?`)) {
|
||
return;
|
||
}
|
||
|
||
let customImages = this.dataManager.get('customImages') || { task: [], consequence: [] };
|
||
let deletedCount = 0;
|
||
let fileDeletedCount = 0;
|
||
|
||
// Handle both old array format and new object format
|
||
if (Array.isArray(customImages)) {
|
||
// Old format - convert to new format first
|
||
customImages = { task: customImages, consequence: [] };
|
||
}
|
||
|
||
// If we're in desktop mode, also delete the actual files
|
||
const isDesktop = this.fileManager && this.fileManager.isElectron;
|
||
|
||
for (const item of selectedItems) {
|
||
const imagePath = item.dataset.imagePath;
|
||
|
||
// Determine which category this image belongs to
|
||
let imageCategory = 'task'; // default
|
||
if (imagePath.includes('consequences') || imagePath.includes('consequence')) {
|
||
imageCategory = 'consequence';
|
||
} else if (imagePath.includes('tasks') || imagePath.includes('task')) {
|
||
imageCategory = 'task';
|
||
}
|
||
|
||
// Delete the actual file if in desktop mode
|
||
if (isDesktop) {
|
||
try {
|
||
const fileDeleted = await this.fileManager.deleteImage(imagePath, imageCategory);
|
||
if (fileDeleted) {
|
||
fileDeletedCount++;
|
||
deletedCount++; // File manager handles storage cleanup
|
||
}
|
||
} catch (error) {
|
||
console.error('Error deleting file:', error);
|
||
}
|
||
} else {
|
||
// Web mode - only remove from storage
|
||
['task', 'consequence'].forEach(category => {
|
||
const initialLength = customImages[category].length;
|
||
customImages[category] = customImages[category].filter(img => {
|
||
if (typeof img === 'string') {
|
||
return img !== imagePath; // Old format: direct string comparison
|
||
} else {
|
||
// New format: compare against cachedPath or originalName
|
||
const imgPath = img.cachedPath || img.originalName;
|
||
return imgPath !== imagePath;
|
||
}
|
||
});
|
||
|
||
if (customImages[category].length < initialLength) {
|
||
deletedCount++;
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
// Update storage if not in desktop mode (desktop mode handles this in deleteImage)
|
||
if (!isDesktop && deletedCount > 0) {
|
||
this.dataManager.set('customImages', customImages);
|
||
}
|
||
|
||
// Clean up disabled image entries for deleted images
|
||
if (deletedCount > 0) {
|
||
let disabledImages = this.dataManager.get('disabledImages') || [];
|
||
const originalDisabledLength = disabledImages.length;
|
||
|
||
// Remove disabled entries for deleted images
|
||
selectedItems.forEach(item => {
|
||
const imagePath = item.dataset.imagePath;
|
||
disabledImages = disabledImages.filter(img => img !== imagePath);
|
||
});
|
||
|
||
if (disabledImages.length < originalDisabledLength) {
|
||
this.dataManager.set('disabledImages', disabledImages);
|
||
}
|
||
|
||
const message = isDesktop
|
||
? `${fileDeletedCount} image file(s) and references deleted successfully!`
|
||
: `${deletedCount} custom image(s) deleted successfully!`;
|
||
this.showNotification(message, 'success');
|
||
this.loadImageGallery(); // Refresh the gallery
|
||
} else {
|
||
this.showNotification('No custom images were selected. Default images cannot be deleted.', 'warning');
|
||
}
|
||
}
|
||
|
||
updateImageCount() {
|
||
const images = this.getAllImages();
|
||
const customImages = this.dataManager.get('customImages') || { task: [], consequence: [] };
|
||
const disabledImages = this.dataManager.get('disabledImages') || [];
|
||
|
||
// Handle both old and new customImages format
|
||
let customImagesArray = [];
|
||
if (Array.isArray(customImages)) {
|
||
// Old format - flat array
|
||
customImagesArray = customImages;
|
||
} else {
|
||
// New format - object with task and consequence arrays
|
||
customImagesArray = [...(customImages.task || []), ...(customImages.consequence || [])];
|
||
}
|
||
|
||
const allImages = [...images, ...customImagesArray];
|
||
|
||
const imageCount = document.querySelector('.image-count');
|
||
if (imageCount) {
|
||
imageCount.textContent = `${allImages.length} images (${allImages.length - disabledImages.length} enabled)`;
|
||
}
|
||
}
|
||
|
||
resetStats() {
|
||
if (this.dataManager.resetStats()) {
|
||
this.showNotification('📊 Statistics reset successfully!', 'success');
|
||
this.hideStats();
|
||
}
|
||
}
|
||
|
||
exportStatsOnly() {
|
||
try {
|
||
this.showButtonLoading('export-stats-btn');
|
||
|
||
// Small delay to show loading indicator
|
||
setTimeout(() => {
|
||
try {
|
||
this.dataManager.exportData(false);
|
||
this.showNotification('📊 Statistics exported successfully!', 'success');
|
||
} catch (error) {
|
||
this.showNotification('❌ ' + this.formatErrorMessage('stats-export', error), 'error');
|
||
} finally {
|
||
this.hideButtonLoading('export-stats-btn');
|
||
}
|
||
}, 300);
|
||
} catch (error) {
|
||
this.showNotification('❌ ' + this.formatErrorMessage('stats-export', error), 'error');
|
||
this.hideButtonLoading('export-stats-btn');
|
||
}
|
||
}
|
||
|
||
// Better error message formatting
|
||
formatErrorMessage(operation, error) {
|
||
const baseMessages = {
|
||
'export': 'Failed to export save data',
|
||
'import': 'Failed to import save data',
|
||
'stats-export': 'Failed to export statistics',
|
||
'auto-resume': 'Failed to restore previous session'
|
||
};
|
||
|
||
const specificErrors = {
|
||
'QuotaExceededError': 'Not enough storage space available',
|
||
'SecurityError': 'File access blocked by browser security',
|
||
'TypeError': 'Invalid file format detected',
|
||
'SyntaxError': 'Corrupted save file data'
|
||
};
|
||
|
||
const baseMessage = baseMessages[operation] || 'Operation failed';
|
||
const specificMessage = specificErrors[error.name] || error.message;
|
||
|
||
return `${baseMessage}: ${specificMessage}`;
|
||
}
|
||
|
||
// Audio Management Functions
|
||
showAudioManagement() {
|
||
// Reset listener flag to allow fresh attachment
|
||
this.audioManagementListenersAttached = false;
|
||
|
||
this.showScreen('audio-management-screen');
|
||
this.setupAudioManagementEventListeners();
|
||
|
||
// Wait for audio discovery to complete before loading gallery
|
||
if (!this.audioDiscoveryComplete) {
|
||
const galleries = document.querySelectorAll('.audio-gallery');
|
||
galleries.forEach(gallery => {
|
||
gallery.innerHTML = '<div class="loading">Discovering audio files...</div>';
|
||
});
|
||
|
||
// Wait and try again
|
||
setTimeout(() => {
|
||
if (this.audioDiscoveryComplete) {
|
||
this.loadAudioGallery();
|
||
} else {
|
||
galleries.forEach(gallery => {
|
||
gallery.innerHTML = '<div class="loading">Still discovering audio... Please wait</div>';
|
||
});
|
||
setTimeout(() => this.loadAudioGallery(), 1000);
|
||
}
|
||
}, 500);
|
||
} else {
|
||
this.loadAudioGallery();
|
||
}
|
||
}
|
||
|
||
switchAudioTab(tabType) {
|
||
// Update tab buttons
|
||
const backgroundTab = document.getElementById('background-audio-tab');
|
||
const ambientTab = document.getElementById('ambient-audio-tab');
|
||
const backgroundGallery = document.getElementById('background-audio-gallery');
|
||
const ambientGallery = document.getElementById('ambient-audio-gallery');
|
||
|
||
// Remove active class from all tabs and galleries
|
||
[backgroundTab, ambientTab].forEach(tab => tab && tab.classList.remove('active'));
|
||
[backgroundGallery, ambientGallery].forEach(gallery => gallery && gallery.classList.remove('active'));
|
||
|
||
// Add active class to selected tab and gallery
|
||
if (tabType === 'background') {
|
||
backgroundTab && backgroundTab.classList.add('active');
|
||
backgroundGallery && backgroundGallery.classList.add('active');
|
||
} else if (tabType === 'ambient') {
|
||
ambientTab && ambientTab.classList.add('active');
|
||
ambientGallery && ambientGallery.classList.add('active');
|
||
}
|
||
|
||
// Update gallery controls to work with current tab
|
||
this.updateAudioGalleryControls(tabType);
|
||
}
|
||
|
||
setupAudioManagementEventListeners() {
|
||
// Check if we already have listeners attached to prevent duplicates
|
||
if (this.audioManagementListenersAttached) {
|
||
return;
|
||
}
|
||
|
||
// Back button
|
||
const backBtn = document.getElementById('back-to-start-from-audio-btn');
|
||
if (backBtn) {
|
||
backBtn.onclick = () => this.showScreen('start-screen');
|
||
}
|
||
|
||
// Desktop import buttons
|
||
const importBackgroundBtn = document.getElementById('import-background-audio-btn');
|
||
if (importBackgroundBtn) {
|
||
importBackgroundBtn.onclick = async () => {
|
||
if (this.fileManager) {
|
||
await this.fileManager.selectAndImportAudio('background');
|
||
this.loadAudioGallery(); // Refresh the gallery to show new audio
|
||
} else {
|
||
this.showNotification('Desktop file manager not available', 'warning');
|
||
}
|
||
};
|
||
}
|
||
|
||
const importAmbientBtn = document.getElementById('import-ambient-audio-btn');
|
||
if (importAmbientBtn) {
|
||
importAmbientBtn.onclick = async () => {
|
||
if (this.fileManager) {
|
||
await this.fileManager.selectAndImportAudio('ambient');
|
||
this.loadAudioGallery(); // Refresh the gallery to show new audio
|
||
} else {
|
||
this.showNotification('Desktop file manager not available', 'warning');
|
||
}
|
||
};
|
||
}
|
||
|
||
// Audio storage info button
|
||
const audioStorageInfoBtn = document.getElementById('audio-storage-info-btn');
|
||
if (audioStorageInfoBtn) {
|
||
audioStorageInfoBtn.onclick = () => this.showAudioStorageInfo();
|
||
}
|
||
|
||
// Scan directories button
|
||
const scanAudioDirectoriesBtn = document.getElementById('scan-audio-directories-btn');
|
||
if (scanAudioDirectoriesBtn) {
|
||
scanAudioDirectoriesBtn.onclick = () => this.scanAudioDirectories();
|
||
}
|
||
|
||
// Tab buttons
|
||
const backgroundAudioTab = document.getElementById('background-audio-tab');
|
||
if (backgroundAudioTab) {
|
||
backgroundAudioTab.onclick = () => this.switchAudioTab('background');
|
||
}
|
||
|
||
const ambientAudioTab = document.getElementById('ambient-audio-tab');
|
||
if (ambientAudioTab) {
|
||
ambientAudioTab.onclick = () => this.switchAudioTab('ambient');
|
||
}
|
||
|
||
// Gallery control buttons - assign onclick directly to avoid expensive DOM operations
|
||
const selectAllAudioBtn = document.getElementById('select-all-audio-btn');
|
||
if (selectAllAudioBtn) {
|
||
selectAllAudioBtn.onclick = () => this.selectAllAudio();
|
||
}
|
||
|
||
const deselectAllAudioBtn = document.getElementById('deselect-all-audio-btn');
|
||
if (deselectAllAudioBtn) {
|
||
deselectAllAudioBtn.onclick = () => this.deselectAllAudio();
|
||
}
|
||
|
||
const deleteSelectedAudioBtn = document.getElementById('delete-selected-audio-btn');
|
||
if (deleteSelectedAudioBtn) {
|
||
deleteSelectedAudioBtn.onclick = () => this.deleteSelectedAudio();
|
||
}
|
||
|
||
const previewSelectedAudioBtn = document.getElementById('preview-selected-audio-btn');
|
||
if (previewSelectedAudioBtn) {
|
||
previewSelectedAudioBtn.onclick = () => this.previewSelectedAudio();
|
||
}
|
||
|
||
// Close preview button
|
||
const closePreviewBtn = document.getElementById('close-preview-btn');
|
||
if (closePreviewBtn) {
|
||
closePreviewBtn.onclick = () => this.closeAudioPreview();
|
||
}
|
||
|
||
// Mark listeners as attached
|
||
this.audioManagementListenersAttached = true;
|
||
}
|
||
|
||
loadAudioGallery() {
|
||
const backgroundGallery = document.getElementById('background-audio-gallery');
|
||
const ambientGallery = document.getElementById('ambient-audio-gallery');
|
||
|
||
if (!backgroundGallery || !ambientGallery) {
|
||
console.error('Audio gallery elements not found');
|
||
return;
|
||
}
|
||
|
||
// Get custom audio from storage
|
||
const customAudio = this.dataManager.get('customAudio') || { background: [], ambient: [] };
|
||
|
||
// Load each category
|
||
this.loadAudioCategory('background', backgroundGallery, customAudio.background);
|
||
this.loadAudioCategory('ambient', ambientGallery, customAudio.ambient);
|
||
|
||
// Update audio count
|
||
this.updateAudioCount();
|
||
|
||
// Setup initial gallery controls for the active tab
|
||
this.updateAudioGalleryControls('background');
|
||
|
||
// Refresh the music manager to include new custom tracks
|
||
if (this.musicManager) {
|
||
this.musicManager.refreshCustomTracks();
|
||
}
|
||
|
||
// Refresh the audio manager to include new audio files
|
||
if (this.audioManager) {
|
||
this.audioManager.refreshAudioLibrary();
|
||
}
|
||
}
|
||
|
||
loadAudioCategory(category, gallery, audioFiles) {
|
||
if (!audioFiles || audioFiles.length === 0) {
|
||
gallery.innerHTML = `<div class="no-audio">No ${category} audio files found. Use the import button to add some!</div>`;
|
||
return;
|
||
}
|
||
|
||
const audioItems = audioFiles.map(audio => {
|
||
return `
|
||
<div class="audio-item" data-category="${category}" data-filename="${audio.name}" onclick="game.toggleAudioSelection(this)">
|
||
<div class="audio-icon" data-category="${category}"></div>
|
||
<div class="audio-title">${audio.title}</div>
|
||
<div class="audio-filename">${audio.name}</div>
|
||
<div class="audio-controls">
|
||
<button class="audio-preview-btn" onclick="event.stopPropagation(); window.game.previewAudio(this.dataset.audioPath, this.dataset.audioTitle)" data-audio-path="${audio.path}" data-audio-title="${audio.title}">🎧 Preview</button>
|
||
<label class="audio-status" onclick="event.stopPropagation()">
|
||
<input type="checkbox" class="audio-checkbox" ${audio.enabled !== false ? 'checked' : ''}
|
||
onchange="game.toggleAudioEnabled('${category}', '${audio.name}', this.checked)">
|
||
<span class="${audio.enabled !== false ? 'audio-enabled' : 'audio-disabled'}">
|
||
${audio.enabled !== false ? 'Enabled' : 'Disabled'}
|
||
</span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
|
||
gallery.innerHTML = audioItems;
|
||
|
||
// Remove any existing event listeners to prevent duplicates
|
||
gallery.removeEventListener('click', gallery._previewHandler);
|
||
|
||
// Add event delegation for preview buttons to avoid path corruption in HTML attributes
|
||
gallery._previewHandler = (e) => {
|
||
if (e.target.classList.contains('audio-preview-btn')) {
|
||
const audioPath = e.target.getAttribute('data-audio-path');
|
||
const audioTitle = e.target.getAttribute('data-audio-title');
|
||
if (audioPath && audioTitle) {
|
||
console.log('Preview button clicked:', audioPath);
|
||
this.previewAudio(audioPath, audioTitle);
|
||
}
|
||
}
|
||
};
|
||
|
||
gallery.addEventListener('click', gallery._previewHandler);
|
||
}
|
||
|
||
updateAudioGalleryControls(activeCategory = 'background') {
|
||
// This will be called when the active tab changes to update controls
|
||
// for the current category
|
||
console.log(`Audio gallery controls updated for ${activeCategory} category`);
|
||
}
|
||
|
||
updateAudioCount() {
|
||
const customAudio = this.dataManager.get('customAudio') || { background: [], ambient: [] };
|
||
const backgroundCount = customAudio.background ? customAudio.background.length : 0;
|
||
const ambientCount = customAudio.ambient ? customAudio.ambient.length : 0;
|
||
const total = backgroundCount + ambientCount;
|
||
|
||
const audioCountElement = document.querySelector('.audio-count');
|
||
if (audioCountElement) {
|
||
audioCountElement.textContent = `${total} total audio files (${backgroundCount} background, ${ambientCount} ambient)`;
|
||
}
|
||
}
|
||
|
||
selectAllAudio() {
|
||
const activeGallery = document.querySelector('.audio-gallery.active');
|
||
if (activeGallery) {
|
||
const audioItems = activeGallery.querySelectorAll('.audio-item');
|
||
audioItems.forEach(item => item.classList.add('selected'));
|
||
}
|
||
}
|
||
|
||
deselectAllAudio() {
|
||
const activeGallery = document.querySelector('.audio-gallery.active');
|
||
if (activeGallery) {
|
||
const audioItems = activeGallery.querySelectorAll('.audio-item');
|
||
audioItems.forEach(item => item.classList.remove('selected'));
|
||
}
|
||
}
|
||
|
||
async deleteSelectedAudio() {
|
||
const activeGallery = document.querySelector('.audio-gallery.active');
|
||
if (!activeGallery) return;
|
||
|
||
const selectedItems = activeGallery.querySelectorAll('.audio-item.selected');
|
||
if (selectedItems.length === 0) {
|
||
this.showNotification('No audio files selected', 'warning');
|
||
return;
|
||
}
|
||
|
||
if (!confirm(`Are you sure you want to delete ${selectedItems.length} selected audio file(s)?`)) {
|
||
return;
|
||
}
|
||
|
||
let deletedCount = 0;
|
||
const isDesktop = window.electronAPI !== undefined;
|
||
|
||
// Use Promise.all to handle async operations properly
|
||
const deletionPromises = Array.from(selectedItems).map(async (item) => {
|
||
const category = item.dataset.category;
|
||
const filename = item.dataset.filename;
|
||
|
||
if (isDesktop && this.fileManager) {
|
||
// Desktop mode - delete actual file
|
||
// Need to find the full audio file path from storage
|
||
const customAudio = this.dataManager.get('customAudio') || { background: [], ambient: [], effects: [] };
|
||
console.log(`Looking for audio file in category ${category}:`, customAudio[category]);
|
||
console.log(`Searching for filename: ${filename}`);
|
||
|
||
const audioFile = customAudio[category].find(audio =>
|
||
(typeof audio === 'string' && audio.includes(filename)) ||
|
||
(typeof audio === 'object' && (audio.name === filename))
|
||
);
|
||
|
||
console.log(`Found audio file:`, audioFile);
|
||
|
||
if (audioFile) {
|
||
const audioPath = typeof audioFile === 'string' ? audioFile : audioFile.path;
|
||
console.log(`Attempting to delete audio file: ${audioPath}`);
|
||
const success = await this.fileManager.deleteAudio(audioPath, category);
|
||
if (success) {
|
||
console.log(`Successfully deleted: ${filename}`);
|
||
return true;
|
||
} else {
|
||
console.error(`Failed to delete audio file: ${filename}`);
|
||
return false;
|
||
}
|
||
} else {
|
||
console.error(`Audio file not found in storage: ${filename}`);
|
||
return false;
|
||
}
|
||
} else {
|
||
// Web mode - remove from storage only
|
||
this.removeAudioFromStorage(category, filename);
|
||
return true;
|
||
}
|
||
});
|
||
|
||
// Wait for all deletions to complete
|
||
try {
|
||
const results = await Promise.all(deletionPromises);
|
||
deletedCount = results.filter(result => result === true).length;
|
||
|
||
if (deletedCount > 0) {
|
||
const modeText = isDesktop ? 'file(s) deleted from disk' : 'reference(s) removed from storage';
|
||
this.showNotification(`${deletedCount} audio ${modeText}`, 'success');
|
||
this.loadAudioGallery(); // Refresh the gallery
|
||
} else {
|
||
this.showNotification('No audio files were deleted', 'warning');
|
||
}
|
||
} catch (error) {
|
||
console.error('Error during audio deletion:', error);
|
||
this.showNotification('Error occurred during deletion', 'error');
|
||
}
|
||
}
|
||
|
||
removeAudioFromStorage(category, filename) {
|
||
const customAudio = this.dataManager.get('customAudio') || { background: [], ambient: [], effects: [] };
|
||
if (customAudio[category]) {
|
||
customAudio[category] = customAudio[category].filter(audio => {
|
||
if (typeof audio === 'string') {
|
||
return !audio.includes(filename);
|
||
} else if (typeof audio === 'object') {
|
||
return audio.name !== filename;
|
||
}
|
||
return true;
|
||
});
|
||
this.dataManager.set('customAudio', customAudio);
|
||
}
|
||
}
|
||
|
||
previewSelectedAudio() {
|
||
const activeGallery = document.querySelector('.audio-gallery.active');
|
||
if (!activeGallery) return;
|
||
|
||
const selectedItems = activeGallery.querySelectorAll('.audio-item.selected');
|
||
if (selectedItems.length === 0) {
|
||
this.showNotification('No audio file selected for preview', 'warning');
|
||
return;
|
||
}
|
||
|
||
if (selectedItems.length > 1) {
|
||
this.showNotification('Please select only one audio file for preview', 'warning');
|
||
return;
|
||
}
|
||
|
||
const selectedItem = selectedItems[0];
|
||
const category = selectedItem.dataset.category;
|
||
const filename = selectedItem.dataset.filename;
|
||
|
||
// Find the audio file data
|
||
const customAudio = this.dataManager.get('customAudio') || { background: [], ambient: [], effects: [] };
|
||
const audioFile = customAudio[category].find(audio => audio.filename === filename);
|
||
|
||
if (audioFile) {
|
||
this.previewAudio(audioFile.path, audioFile.title);
|
||
}
|
||
}
|
||
|
||
previewAudio(audioPath, title) {
|
||
const previewSection = document.getElementById('audio-preview-section');
|
||
const audioPlayer = document.getElementById('audio-preview-player');
|
||
const previewFileName = document.getElementById('preview-file-name');
|
||
|
||
if (previewSection && audioPlayer && previewFileName) {
|
||
previewSection.style.display = 'block';
|
||
|
||
// Convert path to proper file URL for Electron
|
||
let audioSrc = audioPath;
|
||
if (window.electronAPI && audioPath.match(/^[A-Za-z]:\//)) {
|
||
// Absolute Windows path - convert to file:// URL
|
||
audioSrc = `file:///${audioPath}`;
|
||
console.log(`Preview: Converted to file URL: ${audioSrc}`);
|
||
} else if (window.electronAPI && !audioPath.startsWith('file://')) {
|
||
// Relative path in Electron - use as-is
|
||
audioSrc = audioPath;
|
||
console.log(`Preview: Using relative path: ${audioSrc}`);
|
||
}
|
||
|
||
audioPlayer.src = audioSrc;
|
||
previewFileName.textContent = title || 'Unknown';
|
||
|
||
// Scroll to preview section
|
||
previewSection.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||
}
|
||
}
|
||
|
||
closeAudioPreview() {
|
||
const previewSection = document.getElementById('audio-preview-section');
|
||
const audioPlayer = document.getElementById('audio-preview-player');
|
||
|
||
if (previewSection && audioPlayer) {
|
||
previewSection.style.display = 'none';
|
||
audioPlayer.pause();
|
||
audioPlayer.src = '';
|
||
}
|
||
}
|
||
|
||
toggleAudioEnabled(category, filename, enabled) {
|
||
const customAudio = this.dataManager.get('customAudio') || { background: [], ambient: [], effects: [] };
|
||
|
||
if (customAudio[category]) {
|
||
const audioFile = customAudio[category].find(audio => audio.name === filename);
|
||
if (audioFile) {
|
||
audioFile.enabled = enabled;
|
||
this.dataManager.set('customAudio', customAudio);
|
||
|
||
// Update the visual status
|
||
this.loadAudioGallery();
|
||
this.showNotification(`Audio ${enabled ? 'enabled' : 'disabled'}`, 'success');
|
||
}
|
||
}
|
||
}
|
||
|
||
toggleAudioSelection(audioItem) {
|
||
audioItem.classList.toggle('selected');
|
||
}
|
||
|
||
showAudioStorageInfo() {
|
||
try {
|
||
const isDesktop = window.electronAPI !== undefined;
|
||
const customAudio = this.dataManager.get('customAudio') || { background: [], ambient: [], effects: [] };
|
||
|
||
const backgroundCount = customAudio.background ? customAudio.background.length : 0;
|
||
const ambientCount = customAudio.ambient ? customAudio.ambient.length : 0;
|
||
const effectsCount = customAudio.effects ? customAudio.effects.length : 0;
|
||
const totalCustomAudio = backgroundCount + ambientCount + effectsCount;
|
||
|
||
let message = `📊 Audio Storage Information\n\n`;
|
||
|
||
if (isDesktop) {
|
||
message += `🖥️ Desktop Mode - Unlimited Storage\n`;
|
||
message += `Files stored in native file system\n\n`;
|
||
message += `📁 Audio Categories:\n`;
|
||
message += `🎵 Background Music: ${backgroundCount} files\n`;
|
||
message += `🌿 Ambient Sounds: ${ambientCount} files\n`;
|
||
message += `🔊 Sound Effects: ${effectsCount} files\n`;
|
||
message += `📊 Total Custom Audio: ${totalCustomAudio} files\n\n`;
|
||
message += `💾 Storage: Uses native file system\n`;
|
||
message += `📂 Location: audio/ folder in app directory\n`;
|
||
message += `🔄 Auto-scanned on startup`;
|
||
} else {
|
||
message += `🌐 Web Mode - Browser Storage\n`;
|
||
message += `Limited by browser storage quotas\n\n`;
|
||
message += `📁 Audio References:\n`;
|
||
message += `🎵 Background Music: ${backgroundCount} files\n`;
|
||
message += `🌿 Ambient Sounds: ${ambientCount} files\n`;
|
||
message += `🔊 Sound Effects: ${effectsCount} files\n`;
|
||
message += `📊 Total Custom Audio: ${totalCustomAudio} files\n\n`;
|
||
message += `💾 Storage: Browser localStorage\n`;
|
||
message += `⚠️ Subject to browser storage limits`;
|
||
}
|
||
|
||
alert(message);
|
||
} catch (error) {
|
||
console.error('Error showing audio storage info:', error);
|
||
alert('Error retrieving audio storage information.');
|
||
}
|
||
}
|
||
|
||
async scanAudioDirectories() {
|
||
try {
|
||
const isDesktop = window.electronAPI !== undefined;
|
||
|
||
if (!isDesktop) {
|
||
alert('🔍 Directory Scanning\n\nDirectory scanning is only available in desktop mode.\nTo use this feature, run the app with: npm start\n\nFor web mode, please use the Import buttons to add audio files manually.');
|
||
return;
|
||
}
|
||
|
||
// Show loading message
|
||
if (this.flashMessageManager) {
|
||
this.flashMessageManager.show('🔍 Scanning audio directories...', 'info');
|
||
}
|
||
|
||
console.log('🔍 Starting audio directory scan...');
|
||
|
||
// Call the AudioManager's scan method
|
||
if (this.audioManager) {
|
||
const scannedFiles = await this.audioManager.scanAudioDirectories();
|
||
|
||
if (scannedFiles.length > 0) {
|
||
// Refresh the audio gallery to show new files
|
||
this.loadAudioGallery();
|
||
|
||
console.log(`✅ Directory scan completed - found ${scannedFiles.length} files`);
|
||
alert(`🔍 Directory Scan Complete\n\nFound ${scannedFiles.length} audio files in the following directories:\n• audio/background\n• audio/ambient\n\nFiles have been added to your audio library and are ready to use!`);
|
||
} else {
|
||
console.log('ℹ️ Directory scan completed - no new files found');
|
||
alert('🔍 Directory Scan Complete\n\nNo new audio files were found in the audio directories.\n\nTo add audio files:\n1. Place them in the audio/ folder subdirectories\n2. Use the Import buttons to add files manually\n3. Restart the app to auto-scan');
|
||
}
|
||
} else {
|
||
console.error('AudioManager not available for directory scanning');
|
||
alert('❌ Error: Audio system not initialized.\nPlease try again or restart the app.');
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('Error during audio directory scan:', error);
|
||
|
||
if (this.flashMessageManager) {
|
||
this.flashMessageManager.show('❌ Directory scan failed', 'error');
|
||
}
|
||
|
||
alert(`❌ Directory Scan Failed\n\nError: ${error.message}\n\nPlease check the console for more details or try restarting the app.`);
|
||
}
|
||
}
|
||
|
||
showNotification(message, type = 'info') {
|
||
// Create notification element if it doesn't exist
|
||
let notification = document.getElementById('notification');
|
||
if (!notification) {
|
||
notification = document.createElement('div');
|
||
notification.id = 'notification';
|
||
notification.className = 'notification';
|
||
document.body.appendChild(notification);
|
||
}
|
||
|
||
notification.textContent = message;
|
||
notification.className = `notification ${type} show`;
|
||
|
||
// Hide after 3 seconds
|
||
setTimeout(() => {
|
||
notification.classList.remove('show');
|
||
}, 3000);
|
||
}
|
||
|
||
startGame() {
|
||
// 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() {
|
||
const availableTasks = gameData.consequenceTasks.filter(
|
||
task => !this.gameState.usedConsequenceTasks.includes(task.id)
|
||
);
|
||
|
||
if (availableTasks.length === 0) {
|
||
// Reset consequence tasks if all used
|
||
this.gameState.usedConsequenceTasks = [];
|
||
}
|
||
|
||
const tasksToChooseFrom = availableTasks.length > 0 ? availableTasks : gameData.consequenceTasks;
|
||
const randomIndex = Math.floor(Math.random() * tasksToChooseFrom.length);
|
||
this.gameState.currentTask = {
|
||
...tasksToChooseFrom[randomIndex],
|
||
image: this.getRandomImage(true) // true = consequence task image
|
||
};
|
||
this.gameState.isConsequenceTask = true;
|
||
}
|
||
|
||
getRandomImage(isConsequence = false) {
|
||
let imagePool;
|
||
let imageType;
|
||
let usedImagesArray;
|
||
|
||
if (isConsequence) {
|
||
imagePool = gameData.discoveredConsequenceImages;
|
||
imageType = 'consequence';
|
||
usedImagesArray = this.gameState.usedConsequenceImages;
|
||
} else {
|
||
imagePool = gameData.discoveredTaskImages;
|
||
imageType = 'task';
|
||
usedImagesArray = this.gameState.usedTaskImages;
|
||
}
|
||
|
||
// Add custom images to the pool
|
||
const customImages = this.dataManager.get('customImages') || { task: [], consequence: [] };
|
||
|
||
// Handle both old array format and new object format
|
||
let customImagePool = [];
|
||
if (Array.isArray(customImages)) {
|
||
// Old format - treat all as task images
|
||
customImagePool = isConsequence ? [] : customImages;
|
||
} else {
|
||
// New format - get appropriate category
|
||
customImagePool = customImages[imageType] || [];
|
||
}
|
||
|
||
imagePool = [...imagePool, ...customImagePool];
|
||
|
||
// Filter out disabled images - need to handle both formats
|
||
const disabledImages = this.dataManager.get('disabledImages') || [];
|
||
imagePool = imagePool.filter(img => {
|
||
// Handle both string paths and metadata objects
|
||
const imagePath = typeof img === 'string' ? img : (img.cachedPath || img.originalName);
|
||
return !disabledImages.includes(imagePath);
|
||
});
|
||
|
||
if (imagePool.length === 0) {
|
||
console.log(`No enabled ${imageType} images found, using placeholder`);
|
||
return this.createPlaceholderImage(isConsequence ? 'Consequence Image' : 'Task Image');
|
||
}
|
||
|
||
// Get image identifiers for tracking
|
||
const getImageId = (img) => {
|
||
return typeof img === 'string' ? img : (img.cachedPath || img.originalName);
|
||
};
|
||
|
||
// Filter out images that have already been used
|
||
let availableImages = imagePool.filter(img => {
|
||
const imageId = getImageId(img);
|
||
return !usedImagesArray.includes(imageId);
|
||
});
|
||
|
||
// If all images have been used, reset the used array and use all images again
|
||
if (availableImages.length === 0) {
|
||
console.log(`All ${imageType} images have been shown, resetting for repeat cycle`);
|
||
usedImagesArray.length = 0; // Clear the used images array
|
||
availableImages = imagePool; // Use all available images again
|
||
}
|
||
|
||
// Select a random image from available images
|
||
const randomIndex = Math.floor(Math.random() * availableImages.length);
|
||
const selectedImage = availableImages[randomIndex];
|
||
const selectedImageId = getImageId(selectedImage);
|
||
|
||
// Mark this image as used
|
||
usedImagesArray.push(selectedImageId);
|
||
|
||
// Convert to displayable format
|
||
const displayImage = this.getImageSrc(selectedImage);
|
||
console.log(`Selected ${imageType} image: ${typeof selectedImage === 'string' ? selectedImage : selectedImage.originalName} (${usedImagesArray.length}/${imagePool.length} used)`);
|
||
return displayImage;
|
||
}
|
||
|
||
getImageSrc(imageData) {
|
||
// Handle both old base64/path format and new cached metadata format
|
||
if (typeof imageData === 'string') {
|
||
return imageData; // Old format: base64 or file path
|
||
} else {
|
||
return imageData.dataUrl || imageData.cachedPath; // New format: use data URL or cached path
|
||
}
|
||
}
|
||
|
||
displayCurrentTask() {
|
||
const taskText = document.getElementById('task-text');
|
||
const taskImage = document.getElementById('task-image');
|
||
const taskImageContainer = document.querySelector('.task-image-container');
|
||
const taskContainer = document.querySelector('.task-container');
|
||
const taskTypeIndicator = document.getElementById('task-type-indicator');
|
||
const mercySkipBtn = document.getElementById('mercy-skip-btn');
|
||
const skipBtn = document.getElementById('skip-btn') || 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 = `
|
||
<div class="streak-bonus-content">
|
||
<div class="streak-icon">🔥</div>
|
||
<div class="streak-text">
|
||
<div class="streak-title">${streak} Task Streak!</div>
|
||
<div class="streak-bonus">+${bonusPoints} Bonus Points</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
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 = `
|
||
<div class="game-gallery-overlay">
|
||
<div class="game-gallery-container">
|
||
<div class="game-gallery-header">
|
||
<h3>🎉 Game Complete - Your Photo Session</h3>
|
||
<p>You captured ${allPhotos.length} photos during this game!</p>
|
||
</div>
|
||
|
||
<div class="game-gallery-grid">
|
||
${allPhotos.map((photo, index) => `
|
||
<div class="game-gallery-item" onclick="window.showGamePhoto(${index})">
|
||
<img src="${photo.dataURL}" alt="Game Photo ${index + 1}">
|
||
<div class="game-photo-number">${index + 1}</div>
|
||
<div class="game-photo-session">${photo.sessionType || 'Photo'}</div>
|
||
</div>
|
||
`).join('')}
|
||
</div>
|
||
|
||
<div class="game-gallery-controls">
|
||
<button id="continue-to-stats" class="game-gallery-continue">📊 View Final Stats</button>
|
||
</div>
|
||
|
||
<div class="game-gallery-note">
|
||
<p>🔒 All photos are stored locally and never uploaded</p>
|
||
<p>💾 Photos will be cleared when you start a new game</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
document.body.appendChild(gallery);
|
||
this.addGameGalleryStyles();
|
||
|
||
// Bind continue button
|
||
document.getElementById('continue-to-stats').addEventListener('click', () => {
|
||
gallery.remove();
|
||
onComplete();
|
||
});
|
||
|
||
// Make photo viewer available globally
|
||
window.showGamePhoto = (index) => {
|
||
this.showGameFullPhoto(index, allPhotos);
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Show full-size photo viewer for game photos
|
||
*/
|
||
showGameFullPhoto(index, photos) {
|
||
if (!photos || !photos[index]) return;
|
||
|
||
const photo = photos[index];
|
||
const viewer = document.createElement('div');
|
||
viewer.id = 'game-photo-viewer';
|
||
viewer.innerHTML = `
|
||
<div class="game-photo-viewer-overlay" onclick="this.parentElement.remove()">
|
||
<div class="game-photo-viewer-container">
|
||
<div class="game-photo-viewer-header">
|
||
<h4>Photo ${index + 1} of ${photos.length}</h4>
|
||
<div class="photo-session-info">${photo.sessionType || 'Photography Session'}</div>
|
||
<button class="game-photo-viewer-close" onclick="this.closest('#game-photo-viewer').remove()">×</button>
|
||
</div>
|
||
<div class="game-photo-viewer-content">
|
||
<img src="${photo.dataURL}" alt="Game Photo ${index + 1}">
|
||
</div>
|
||
<div class="game-photo-viewer-nav">
|
||
${index > 0 ? `<button onclick="window.showGamePhoto(${index - 1}); this.closest('#game-photo-viewer').remove();">← Previous</button>` : '<div></div>'}
|
||
${index < photos.length - 1 ? `<button onclick="window.showGamePhoto(${index + 1}); this.closest('#game-photo-viewer').remove();">Next →</button>` : '<div></div>'}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
document.body.appendChild(viewer);
|
||
}
|
||
|
||
/**
|
||
* Add styles for game photo gallery
|
||
*/
|
||
addGameGalleryStyles() {
|
||
if (document.getElementById('game-gallery-styles')) return;
|
||
|
||
const styles = document.createElement('style');
|
||
styles.id = 'game-gallery-styles';
|
||
styles.textContent = `
|
||
#game-photo-gallery .game-gallery-overlay {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100vw;
|
||
height: 100vh;
|
||
background: rgba(0, 0, 0, 0.95);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 11000;
|
||
}
|
||
|
||
#game-photo-gallery .game-gallery-container {
|
||
background: linear-gradient(135deg, #2a2a2a, #3a3a3a);
|
||
border-radius: 15px;
|
||
padding: 40px;
|
||
max-width: 95vw;
|
||
max-height: 95vh;
|
||
overflow-y: auto;
|
||
color: white;
|
||
box-shadow: 0 10px 30px rgba(0,0,0,0.5);
|
||
}
|
||
|
||
#game-photo-gallery .game-gallery-header {
|
||
text-align: center;
|
||
margin-bottom: 30px;
|
||
}
|
||
|
||
#game-photo-gallery .game-gallery-header h3 {
|
||
color: #ff6b6b;
|
||
font-size: 28px;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
#game-photo-gallery .game-gallery-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||
gap: 20px;
|
||
margin-bottom: 40px;
|
||
}
|
||
|
||
#game-photo-gallery .game-gallery-item {
|
||
position: relative;
|
||
cursor: pointer;
|
||
border-radius: 12px;
|
||
overflow: hidden;
|
||
transition: all 0.3s ease;
|
||
border: 3px solid #444;
|
||
background: #333;
|
||
}
|
||
|
||
#game-photo-gallery .game-gallery-item:hover {
|
||
transform: scale(1.08);
|
||
box-shadow: 0 8px 25px rgba(255, 107, 107, 0.4);
|
||
border-color: #ff6b6b;
|
||
}
|
||
|
||
#game-photo-gallery .game-gallery-item img {
|
||
width: 100%;
|
||
height: 180px;
|
||
object-fit: cover;
|
||
}
|
||
|
||
#game-photo-gallery .game-photo-number {
|
||
position: absolute;
|
||
top: 8px;
|
||
right: 8px;
|
||
background: linear-gradient(45deg, #ff6b6b, #ff8e53);
|
||
color: white;
|
||
border-radius: 50%;
|
||
width: 30px;
|
||
height: 30px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 14px;
|
||
font-weight: bold;
|
||
box-shadow: 0 2px 5px rgba(0,0,0,0.3);
|
||
}
|
||
|
||
#game-photo-gallery .game-photo-session {
|
||
position: absolute;
|
||
bottom: 0;
|
||
left: 0;
|
||
right: 0;
|
||
background: linear-gradient(transparent, rgba(0,0,0,0.8));
|
||
color: white;
|
||
padding: 8px;
|
||
font-size: 12px;
|
||
text-align: center;
|
||
}
|
||
|
||
#game-photo-gallery .game-gallery-controls {
|
||
text-align: center;
|
||
margin: 30px 0;
|
||
}
|
||
|
||
#game-photo-gallery .game-gallery-continue {
|
||
background: linear-gradient(45deg, #2ed573, #17a2b8);
|
||
color: white;
|
||
border: none;
|
||
padding: 18px 36px;
|
||
border-radius: 8px;
|
||
font-size: 18px;
|
||
font-weight: bold;
|
||
cursor: pointer;
|
||
transition: all 0.3s;
|
||
box-shadow: 0 4px 15px rgba(46, 213, 115, 0.3);
|
||
}
|
||
|
||
#game-photo-gallery .game-gallery-continue:hover {
|
||
transform: translateY(-3px);
|
||
box-shadow: 0 6px 20px rgba(46, 213, 115, 0.4);
|
||
}
|
||
|
||
#game-photo-gallery .game-gallery-note {
|
||
text-align: center;
|
||
color: #aaa;
|
||
font-size: 14px;
|
||
border-top: 1px solid #444;
|
||
padding-top: 20px;
|
||
}
|
||
|
||
/* Game Photo Viewer Styles */
|
||
#game-photo-viewer .game-photo-viewer-overlay {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100vw;
|
||
height: 100vh;
|
||
background: rgba(0, 0, 0, 0.98);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 11001;
|
||
}
|
||
|
||
#game-photo-viewer .game-photo-viewer-container {
|
||
background: linear-gradient(135deg, #2a2a2a, #3a3a3a);
|
||
border-radius: 15px;
|
||
max-width: 90vw;
|
||
max-height: 90vh;
|
||
color: white;
|
||
overflow: hidden;
|
||
box-shadow: 0 10px 30px rgba(0,0,0,0.5);
|
||
}
|
||
|
||
#game-photo-viewer .game-photo-viewer-header {
|
||
padding: 20px 25px;
|
||
background: linear-gradient(45deg, #ff6b6b, #ff8e53);
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
#game-photo-viewer .photo-session-info {
|
||
font-size: 14px;
|
||
opacity: 0.9;
|
||
}
|
||
|
||
#game-photo-viewer .game-photo-viewer-close {
|
||
background: none;
|
||
border: none;
|
||
color: white;
|
||
font-size: 28px;
|
||
cursor: pointer;
|
||
padding: 0;
|
||
width: 35px;
|
||
height: 35px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: 50%;
|
||
transition: background 0.3s;
|
||
}
|
||
|
||
#game-photo-viewer .game-photo-viewer-close:hover {
|
||
background: rgba(255,255,255,0.2);
|
||
}
|
||
|
||
#game-photo-viewer .game-photo-viewer-content {
|
||
padding: 30px;
|
||
text-align: center;
|
||
background: #2a2a2a;
|
||
}
|
||
|
||
#game-photo-viewer .game-photo-viewer-content img {
|
||
max-width: 100%;
|
||
max-height: 70vh;
|
||
border-radius: 8px;
|
||
box-shadow: 0 4px 15px rgba(0,0,0,0.3);
|
||
}
|
||
|
||
#game-photo-viewer .game-photo-viewer-nav {
|
||
padding: 20px 25px;
|
||
background: #3a3a3a;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
#game-photo-viewer .game-photo-viewer-nav button {
|
||
background: linear-gradient(45deg, #ff6b6b, #ff8e53);
|
||
color: white;
|
||
border: none;
|
||
padding: 12px 24px;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
transition: all 0.3s;
|
||
font-weight: bold;
|
||
}
|
||
|
||
#game-photo-viewer .game-photo-viewer-nav button:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 4px 12px rgba(255, 107, 107, 0.4);
|
||
}
|
||
`;
|
||
|
||
document.head.appendChild(styles);
|
||
}
|
||
|
||
resetGame() {
|
||
this.gameState = {
|
||
isRunning: false,
|
||
isPaused: false,
|
||
currentTask: null,
|
||
isConsequenceTask: false,
|
||
startTime: null,
|
||
pausedTime: 0,
|
||
totalPausedTime: 0,
|
||
completedCount: 0,
|
||
skippedCount: 0,
|
||
consequenceCount: 0,
|
||
score: 0,
|
||
lastSkippedTask: null,
|
||
usedMainTasks: [],
|
||
usedConsequenceTasks: [],
|
||
usedTaskImages: [],
|
||
usedConsequenceImages: [],
|
||
gameMode: 'complete-all',
|
||
timeLimit: 300,
|
||
xpTarget: 100,
|
||
currentStreak: 0,
|
||
totalStreakBonuses: 0,
|
||
lastStreakMilestone: 0
|
||
};
|
||
|
||
this.stopTimer();
|
||
this.showScreen('start-screen');
|
||
}
|
||
|
||
startTimer() {
|
||
this.timerInterval = setInterval(() => {
|
||
this.updateTimer();
|
||
}, 1000);
|
||
}
|
||
|
||
stopTimer() {
|
||
if (this.timerInterval) {
|
||
clearInterval(this.timerInterval);
|
||
this.timerInterval = null;
|
||
}
|
||
}
|
||
|
||
updateTimer() {
|
||
if (!this.gameState.isRunning || this.gameState.isPaused) return;
|
||
|
||
const currentTime = Date.now();
|
||
const elapsed = currentTime - this.gameState.startTime - this.gameState.totalPausedTime;
|
||
|
||
// Update XP every second
|
||
this.updateXp();
|
||
|
||
// Update scenario XP if in scenario mode
|
||
if (window.gameModeManager && window.gameModeManager.isScenarioMode()) {
|
||
this.updateScenarioTimeBasedXp();
|
||
this.updateScenarioFocusXp();
|
||
this.updateScenarioWebcamXp();
|
||
}
|
||
|
||
let formattedTime;
|
||
|
||
if (this.gameState.gameMode === 'timed') {
|
||
// Countdown timer for timed mode
|
||
const remainingMs = (this.gameState.timeLimit * 1000) - elapsed;
|
||
if (remainingMs <= 0) {
|
||
// Time's up!
|
||
formattedTime = '00:00';
|
||
document.getElementById('timer').textContent = formattedTime;
|
||
this.endGame('time');
|
||
return;
|
||
}
|
||
formattedTime = this.formatTime(remainingMs);
|
||
|
||
// Change color when time is running low (less than 30 seconds)
|
||
const timerElement = document.getElementById('timer') || document.getElementById('game-timer');
|
||
if (timerElement) {
|
||
if (remainingMs <= 30000) {
|
||
timerElement.style.color = '#ff4757';
|
||
timerElement.style.fontWeight = 'bold';
|
||
} else if (remainingMs <= 60000) {
|
||
timerElement.style.color = '#ffa502';
|
||
timerElement.style.fontWeight = 'bold';
|
||
} else {
|
||
timerElement.style.color = '';
|
||
timerElement.style.fontWeight = '';
|
||
}
|
||
}
|
||
} else {
|
||
// Normal elapsed timer for other modes
|
||
formattedTime = this.formatTime(elapsed);
|
||
}
|
||
|
||
const timerDisplayElement = document.getElementById('timer') || document.getElementById('game-timer');
|
||
if (timerDisplayElement) {
|
||
timerDisplayElement.textContent = formattedTime;
|
||
}
|
||
|
||
// Update timer status
|
||
const timerStatus = document.getElementById('timer-status');
|
||
if (timerStatus) {
|
||
if (this.gameState.isPaused) {
|
||
timerStatus.textContent = '(PAUSED)';
|
||
} else if (this.gameState.gameMode === 'timed') {
|
||
timerStatus.textContent = '(TIME LEFT)';
|
||
} else {
|
||
timerStatus.textContent = '';
|
||
}
|
||
}
|
||
}
|
||
|
||
formatTime(milliseconds) {
|
||
const totalSeconds = Math.floor(milliseconds / 1000);
|
||
const minutes = Math.floor(totalSeconds / 60);
|
||
const seconds = totalSeconds % 60;
|
||
|
||
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
||
}
|
||
|
||
updateStats() {
|
||
// Helper function to safely update element text content
|
||
const safeUpdateText = (id, value) => {
|
||
const element = document.getElementById(id);
|
||
if (element) {
|
||
element.textContent = value;
|
||
}
|
||
};
|
||
|
||
safeUpdateText('completed-count', this.gameState.completedCount);
|
||
safeUpdateText('skipped-count', this.gameState.skippedCount);
|
||
safeUpdateText('consequence-count', this.gameState.consequenceCount);
|
||
safeUpdateText('xp', this.gameState.xp || 0);
|
||
|
||
// Update streak display
|
||
const streakElement = document.getElementById('current-streak');
|
||
if (streakElement) {
|
||
streakElement.textContent = this.gameState.currentStreak;
|
||
|
||
// Add visual indicator for streak milestones
|
||
const streakContainer = streakElement.parentElement;
|
||
if (this.gameState.currentStreak >= 10) {
|
||
streakContainer.classList.add('streak-milestone');
|
||
} else {
|
||
streakContainer.classList.remove('streak-milestone');
|
||
}
|
||
}
|
||
}
|
||
|
||
updateOverallXpDisplay() {
|
||
// Update overall XP display on home screen header
|
||
const overallXpElement = document.getElementById('overall-xp-header');
|
||
if (overallXpElement) {
|
||
const overallXp = this.dataManager.get('overallXp') || 0;
|
||
overallXpElement.textContent = overallXp;
|
||
}
|
||
}
|
||
|
||
showFinalStats(reason = 'complete-all') {
|
||
const currentTime = Date.now();
|
||
const totalTime = currentTime - this.gameState.startTime - this.gameState.totalPausedTime;
|
||
const formattedTime = this.formatTime(totalTime);
|
||
|
||
// Update the game over title based on end reason
|
||
const gameOverTitle = document.querySelector('#game-over-screen h2');
|
||
const gameOverMessage = document.querySelector('#game-over-screen p');
|
||
|
||
// Only update elements if they exist (might not exist in Quick Play mode)
|
||
if (gameOverTitle && gameOverMessage) {
|
||
// Use game mode manager for completion message if available
|
||
if (window.gameModeManager && window.gameModeManager.isScenarioMode()) {
|
||
if (reason === 'scenario-complete') {
|
||
gameOverTitle.textContent = '🎭 Scenario Complete!';
|
||
gameOverMessage.textContent = window.gameModeManager.getCompletionMessage();
|
||
} else if (reason === 'scenario-quit') {
|
||
gameOverTitle.textContent = '😞 Scenario Abandoned';
|
||
gameOverMessage.textContent = 'You gave up on the scenario. Your progress was not saved.';
|
||
} else {
|
||
gameOverTitle.textContent = '🎉 Mode Complete!';
|
||
gameOverMessage.textContent = window.gameModeManager.getCompletionMessage();
|
||
}
|
||
} else {
|
||
switch (reason) {
|
||
case 'time':
|
||
gameOverTitle.textContent = '⏰ Time\'s Up!';
|
||
gameOverMessage.textContent = 'You ran out of time! See how many tasks you completed.';
|
||
break;
|
||
case 'target-reached':
|
||
gameOverTitle.textContent = '⭐ XP Target Reached!';
|
||
gameOverMessage.textContent = `Congratulations! You reached the target of ${this.gameState.xpTarget} XP!`;
|
||
break;
|
||
case 'complete-all':
|
||
default:
|
||
gameOverTitle.textContent = '🎉 All Tasks Complete!';
|
||
gameOverMessage.textContent = 'Congratulations! You\'ve completed all available tasks!';
|
||
break;
|
||
}
|
||
}
|
||
} else {
|
||
console.log('Game over screen elements not found - likely in Quick Play mode');
|
||
}
|
||
|
||
// Handle scenario vs regular game stats display
|
||
if (window.gameModeManager && window.gameModeManager.isScenarioMode()) {
|
||
this.displayScenarioStats(reason);
|
||
} else {
|
||
this.displayRegularGameStats();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Display scenario-specific stats
|
||
*/
|
||
displayScenarioStats(reason) {
|
||
const scenarioXp = this.gameState.scenarioXp || {};
|
||
const scenarioName = window.gameModeManager.getCurrentMode()?.name || 'Unknown Scenario';
|
||
|
||
// Calculate total scenario XP
|
||
const totalScenarioXp = (scenarioXp.timeBasedXp || 0) +
|
||
(scenarioXp.focusXp || 0) +
|
||
(scenarioXp.webcamXp || 0) +
|
||
(scenarioXp.photoXp || 0) +
|
||
(scenarioXp.stepCompletion || 0);
|
||
|
||
// Display scenario mode with null check
|
||
const finalGameModeElement = document.getElementById('final-game-mode');
|
||
if (finalGameModeElement) {
|
||
finalGameModeElement.textContent = scenarioName;
|
||
}
|
||
|
||
// Display scenario XP (separate from main game XP) with null check
|
||
const finalXpElement = document.getElementById('final-xp');
|
||
if (finalXpElement) {
|
||
finalXpElement.textContent = reason === 'scenario-quit' ? 0 : totalScenarioXp;
|
||
}
|
||
|
||
// Format time properly with null check
|
||
const finalTimeElement = document.getElementById('final-time');
|
||
if (finalTimeElement) {
|
||
const totalSeconds = Math.floor(this.gameState.timer);
|
||
const minutes = Math.floor(totalSeconds / 60);
|
||
const seconds = totalSeconds % 60;
|
||
const formattedTime = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
||
finalTimeElement.textContent = formattedTime;
|
||
}
|
||
|
||
// Hide irrelevant stats for scenarios
|
||
const statsToHide = ['final-completed', 'final-skipped', 'final-consequences', 'final-best-streak', 'final-streak-bonuses'];
|
||
statsToHide.forEach(statId => {
|
||
const element = document.getElementById(statId);
|
||
if (element && element.parentElement) {
|
||
element.parentElement.style.display = 'none';
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Display regular game stats
|
||
*/
|
||
displayRegularGameStats() {
|
||
// Show all stats elements for regular games
|
||
const statsToShow = ['final-completed', 'final-skipped', 'final-consequences', 'final-best-streak', 'final-streak-bonuses'];
|
||
statsToShow.forEach(statId => {
|
||
const element = document.getElementById(statId);
|
||
if (element && element.parentElement) {
|
||
element.parentElement.style.display = 'block';
|
||
}
|
||
});
|
||
|
||
// Update XP with null check
|
||
const finalXpElement = document.getElementById('final-xp');
|
||
if (finalXpElement) {
|
||
finalXpElement.textContent = this.gameState.xp || 0;
|
||
}
|
||
|
||
// Update time with null check
|
||
const finalTimeElement = document.getElementById('final-time');
|
||
if (finalTimeElement) {
|
||
const formattedTime = this.formatTime(this.gameState.timer);
|
||
finalTimeElement.textContent = formattedTime;
|
||
}
|
||
|
||
// Update stats with null checks
|
||
const finalCompletedElement = document.getElementById('final-completed');
|
||
const finalSkippedElement = document.getElementById('final-skipped');
|
||
const finalConsequencesElement = document.getElementById('final-consequences');
|
||
|
||
if (finalCompletedElement) finalCompletedElement.textContent = this.gameState.completedCount;
|
||
if (finalSkippedElement) finalSkippedElement.textContent = this.gameState.skippedCount;
|
||
if (finalConsequencesElement) finalConsequencesElement.textContent = this.gameState.consequenceCount;
|
||
|
||
// Update streak bonus stats
|
||
const bestStreakElement = document.getElementById('final-best-streak');
|
||
const streakBonusesElement = document.getElementById('final-streak-bonuses');
|
||
if (bestStreakElement) {
|
||
bestStreakElement.textContent = this.gameState.currentStreak;
|
||
}
|
||
if (streakBonusesElement) {
|
||
streakBonusesElement.textContent = this.gameState.totalStreakBonuses;
|
||
}
|
||
|
||
// Add game mode info to stats
|
||
let gameModeText = '';
|
||
switch (this.gameState.gameMode) {
|
||
case 'timed':
|
||
gameModeText = `Timed Challenge (${this.gameState.timeLimit / 60} minutes)`;
|
||
break;
|
||
case 'xp-target':
|
||
gameModeText = `XP Target (${this.gameState.xpTarget} XP)`;
|
||
break;
|
||
case 'complete-all':
|
||
default:
|
||
gameModeText = 'Complete All Tasks';
|
||
break;
|
||
}
|
||
|
||
// Show game mode info if element exists
|
||
const gameModeElement = document.getElementById('final-game-mode');
|
||
if (gameModeElement) {
|
||
gameModeElement.textContent = gameModeText;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Get total steps in current scenario
|
||
*/
|
||
getScenarioTotalSteps() {
|
||
const task = this.gameState.currentTask;
|
||
if (task && task.interactiveData && task.interactiveData.steps) {
|
||
return Object.keys(task.interactiveData.steps).length;
|
||
}
|
||
return 0;
|
||
}
|
||
}
|
||
|
||
// Unified Data Management System
|
||
class DataManager {
|
||
constructor() {
|
||
this.version = "1.0";
|
||
this.storageKey = "webGame-data";
|
||
this.data = this.loadData();
|
||
this.autoSaveInterval = null;
|
||
this.startAutoSave();
|
||
}
|
||
|
||
getDefaultData() {
|
||
return {
|
||
version: this.version,
|
||
timestamp: Date.now(),
|
||
settings: {
|
||
theme: "default",
|
||
difficulty: "medium",
|
||
music: {
|
||
volume: 30,
|
||
loopMode: 0,
|
||
shuffle: false,
|
||
currentTrack: 0
|
||
}
|
||
},
|
||
gameplay: {
|
||
totalGamesPlayed: 0,
|
||
totalTasksCompleted: 0,
|
||
totalTasksSkipped: 0,
|
||
bestScore: 0,
|
||
currentStreak: 0,
|
||
longestStreak: 0,
|
||
totalPlayTime: 0,
|
||
mercyPointsEarned: 0,
|
||
mercyPointsSpent: 0
|
||
},
|
||
customContent: {
|
||
tasks: {
|
||
main: [],
|
||
consequence: []
|
||
},
|
||
customImages: [],
|
||
disabledImages: []
|
||
},
|
||
statistics: {
|
||
taskCompletionRate: 0,
|
||
averageGameDuration: 0,
|
||
difficultyStats: {
|
||
easy: { played: 0, completed: 0 },
|
||
medium: { played: 0, completed: 0 },
|
||
hard: { played: 0, completed: 0 }
|
||
},
|
||
themeUsage: {},
|
||
lastPlayed: null
|
||
}
|
||
};
|
||
}
|
||
|
||
loadData() {
|
||
try {
|
||
// Try new unified format first
|
||
const unified = localStorage.getItem(this.storageKey);
|
||
if (unified) {
|
||
const data = JSON.parse(unified);
|
||
return this.migrateData(data);
|
||
}
|
||
|
||
// Migrate from old scattered localStorage
|
||
return this.migrateFromOldFormat();
|
||
} catch (error) {
|
||
console.warn('Data loading failed, using defaults:', error);
|
||
return this.getDefaultData();
|
||
}
|
||
}
|
||
|
||
migrateFromOldFormat() {
|
||
const data = this.getDefaultData();
|
||
|
||
// Migrate existing scattered data
|
||
try {
|
||
// Theme
|
||
const theme = localStorage.getItem('selectedTheme');
|
||
if (theme) data.settings.theme = theme;
|
||
|
||
// Music settings
|
||
const volume = localStorage.getItem('gameMusic-volume');
|
||
if (volume) data.settings.music.volume = parseInt(volume);
|
||
|
||
const track = localStorage.getItem('gameMusic-track');
|
||
if (track) data.settings.music.currentTrack = parseInt(track);
|
||
|
||
const loopMode = localStorage.getItem('gameMusic-loopMode');
|
||
if (loopMode) data.settings.music.loopMode = parseInt(loopMode);
|
||
|
||
const shuffle = localStorage.getItem('gameMusic-shuffleMode');
|
||
if (shuffle) data.settings.music.shuffle = shuffle === 'true';
|
||
|
||
// Custom tasks
|
||
const mainTasks = localStorage.getItem('customMainTasks');
|
||
if (mainTasks) data.customContent.tasks.main = JSON.parse(mainTasks);
|
||
|
||
const consequenceTasks = localStorage.getItem('customConsequenceTasks');
|
||
if (consequenceTasks) data.customContent.tasks.consequence = JSON.parse(consequenceTasks);
|
||
|
||
// Difficulty
|
||
const difficulty = localStorage.getItem('selectedDifficulty');
|
||
if (difficulty) data.settings.difficulty = difficulty;
|
||
|
||
this.saveData(); // Save in new format
|
||
this.cleanupOldStorage(); // Remove old keys
|
||
|
||
} catch (error) {
|
||
console.warn('Migration failed, using defaults:', error);
|
||
}
|
||
|
||
return data;
|
||
}
|
||
|
||
migrateData(data) {
|
||
// Handle version upgrades here
|
||
if (!data.version || data.version < this.version) {
|
||
console.log(`Upgrading data from ${data.version || 'unknown'} to ${this.version}`);
|
||
|
||
// Add any missing properties from default
|
||
const defaultData = this.getDefaultData();
|
||
data = this.deepMerge(defaultData, data);
|
||
data.version = this.version;
|
||
}
|
||
|
||
return data;
|
||
}
|
||
|
||
deepMerge(target, source) {
|
||
const result = { ...target };
|
||
|
||
for (const key in source) {
|
||
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
|
||
result[key] = this.deepMerge(target[key] || {}, source[key]);
|
||
} else {
|
||
result[key] = source[key];
|
||
}
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
cleanupOldStorage() {
|
||
// Remove old scattered localStorage keys
|
||
const oldKeys = [
|
||
'selectedTheme', 'gameMusic-volume', 'gameMusic-track',
|
||
'gameMusic-loopMode', 'gameMusic-shuffleMode',
|
||
'customMainTasks', 'customConsequenceTasks', 'selectedDifficulty'
|
||
];
|
||
|
||
oldKeys.forEach(key => localStorage.removeItem(key));
|
||
}
|
||
|
||
saveData() {
|
||
try {
|
||
if (!this.data) {
|
||
this.data = this.getDefaultData();
|
||
}
|
||
this.data.timestamp = Date.now();
|
||
localStorage.setItem(this.storageKey, JSON.stringify(this.data));
|
||
} catch (error) {
|
||
console.error('Failed to save data:', error);
|
||
throw error; // Re-throw so calling code can handle it
|
||
}
|
||
}
|
||
|
||
// Settings Management
|
||
getSetting(path) {
|
||
return this.getNestedValue(this.data.settings, path);
|
||
}
|
||
|
||
setSetting(path, value) {
|
||
this.setNestedValue(this.data.settings, path, value);
|
||
this.saveData();
|
||
}
|
||
|
||
// Generic data access
|
||
get(key) {
|
||
// Handle special cases first
|
||
if (key === 'autoSaveGameState') {
|
||
return JSON.parse(localStorage.getItem('autoSaveGameState') || 'null');
|
||
}
|
||
|
||
// For custom images and disabled images, store them in customContent
|
||
if (key === 'customImages') {
|
||
return this.data.customContent.customImages || [];
|
||
}
|
||
|
||
if (key === 'disabledImages') {
|
||
return this.data.customContent.disabledImages || [];
|
||
}
|
||
|
||
// Generic access to data properties
|
||
return this.data[key];
|
||
}
|
||
|
||
set(key, value) {
|
||
// Handle special cases first
|
||
if (key === 'autoSaveGameState') {
|
||
localStorage.setItem('autoSaveGameState', JSON.stringify(value));
|
||
return;
|
||
}
|
||
|
||
// For custom images and disabled images, store them in customContent
|
||
if (key === 'customImages') {
|
||
if (!this.data.customContent.customImages) {
|
||
this.data.customContent.customImages = [];
|
||
}
|
||
this.data.customContent.customImages = value;
|
||
this.saveData();
|
||
return;
|
||
}
|
||
|
||
if (key === 'disabledImages') {
|
||
if (!this.data.customContent.disabledImages) {
|
||
this.data.customContent.disabledImages = [];
|
||
}
|
||
this.data.customContent.disabledImages = value;
|
||
this.saveData();
|
||
return;
|
||
}
|
||
|
||
// Generic setter
|
||
this.data[key] = value;
|
||
this.saveData();
|
||
}
|
||
|
||
// Gameplay Statistics
|
||
recordGameStart() {
|
||
this.data.gameplay.totalGamesPlayed++;
|
||
this.data.statistics.lastPlayed = Date.now();
|
||
this.saveData();
|
||
}
|
||
|
||
recordTaskComplete(difficulty, points, isConsequence = false) {
|
||
this.data.gameplay.totalTasksCompleted++;
|
||
|
||
if (!isConsequence) {
|
||
this.data.gameplay.currentStreak++;
|
||
this.data.gameplay.longestStreak = Math.max(
|
||
this.data.gameplay.longestStreak,
|
||
this.data.gameplay.currentStreak
|
||
);
|
||
|
||
// Track difficulty stats
|
||
if (this.data.statistics.difficultyStats[difficulty]) {
|
||
this.data.statistics.difficultyStats[difficulty].completed++;
|
||
}
|
||
}
|
||
|
||
this.updateStatistics();
|
||
this.saveData();
|
||
}
|
||
|
||
recordTaskSkip(mercyPoints = 0) {
|
||
this.data.gameplay.totalTasksSkipped++;
|
||
this.data.gameplay.currentStreak = 0;
|
||
|
||
if (mercyPoints > 0) {
|
||
this.data.gameplay.mercyPointsSpent += mercyPoints;
|
||
}
|
||
|
||
this.updateStatistics();
|
||
this.saveData();
|
||
}
|
||
|
||
recordScore(score) {
|
||
this.data.gameplay.bestScore = Math.max(this.data.gameplay.bestScore, score);
|
||
this.saveData();
|
||
}
|
||
|
||
recordPlayTime(duration) {
|
||
this.data.gameplay.totalPlayTime += duration;
|
||
this.updateStatistics();
|
||
this.saveData();
|
||
}
|
||
|
||
updateStatistics() {
|
||
const { totalTasksCompleted, totalTasksSkipped } = this.data.gameplay;
|
||
const total = totalTasksCompleted + totalTasksSkipped;
|
||
|
||
this.data.statistics.taskCompletionRate = total > 0 ? totalTasksCompleted / total : 0;
|
||
|
||
if (this.data.gameplay.totalGamesPlayed > 0) {
|
||
this.data.statistics.averageGameDuration =
|
||
this.data.gameplay.totalPlayTime / this.data.gameplay.totalGamesPlayed;
|
||
}
|
||
}
|
||
|
||
// Custom Content Management
|
||
getCustomTasks(type = 'main') {
|
||
return this.data.customContent.tasks[type] || [];
|
||
}
|
||
|
||
addCustomTask(task, type = 'main') {
|
||
if (!this.data.customContent.tasks[type]) {
|
||
this.data.customContent.tasks[type] = [];
|
||
}
|
||
this.data.customContent.tasks[type].push(task);
|
||
this.saveData();
|
||
}
|
||
|
||
removeCustomTask(index, type = 'main') {
|
||
if (this.data.customContent.tasks[type]) {
|
||
this.data.customContent.tasks[type].splice(index, 1);
|
||
this.saveData();
|
||
}
|
||
}
|
||
|
||
resetCustomTasks(type = 'main') {
|
||
this.data.customContent.tasks[type] = [];
|
||
this.saveData();
|
||
}
|
||
|
||
// Export/Import System
|
||
exportData(includeStats = true) {
|
||
const exportData = {
|
||
version: this.version,
|
||
exportDate: new Date().toISOString(),
|
||
settings: this.data.settings,
|
||
customContent: this.data.customContent
|
||
};
|
||
|
||
if (includeStats) {
|
||
exportData.gameplay = this.data.gameplay;
|
||
exportData.statistics = this.data.statistics;
|
||
}
|
||
|
||
const dataStr = JSON.stringify(exportData, null, 2);
|
||
const blob = new Blob([dataStr], { type: 'application/json' });
|
||
const url = URL.createObjectURL(blob);
|
||
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = `webgame-save-${new Date().toISOString().split('T')[0]}.json`;
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
document.body.removeChild(a);
|
||
URL.revokeObjectURL(url);
|
||
|
||
return exportData;
|
||
}
|
||
|
||
importData(file) {
|
||
return new Promise((resolve, reject) => {
|
||
const reader = new FileReader();
|
||
|
||
reader.onload = (e) => {
|
||
try {
|
||
const importedData = JSON.parse(e.target.result);
|
||
|
||
// Validate import
|
||
if (!this.validateImportData(importedData)) {
|
||
throw new Error('Invalid save file format');
|
||
}
|
||
|
||
// Merge imported data
|
||
this.data = this.deepMerge(this.getDefaultData(), importedData);
|
||
this.data.version = this.version;
|
||
this.data.timestamp = Date.now();
|
||
|
||
this.saveData();
|
||
resolve(importedData);
|
||
|
||
} catch (error) {
|
||
reject(error);
|
||
}
|
||
};
|
||
|
||
reader.onerror = () => reject(new Error('Failed to read file'));
|
||
reader.readAsText(file);
|
||
});
|
||
}
|
||
|
||
validateImportData(data) {
|
||
// Basic validation
|
||
return data &&
|
||
typeof data === 'object' &&
|
||
(data.settings || data.customContent || data.gameplay);
|
||
}
|
||
|
||
// Utility functions
|
||
getNestedValue(obj, path) {
|
||
return path.split('.').reduce((current, key) => current && current[key], obj);
|
||
}
|
||
|
||
setNestedValue(obj, path, value) {
|
||
const keys = path.split('.');
|
||
const lastKey = keys.pop();
|
||
const target = keys.reduce((current, key) => {
|
||
if (!current[key]) current[key] = {};
|
||
return current[key];
|
||
}, obj);
|
||
target[lastKey] = value;
|
||
}
|
||
|
||
// Auto-save functionality
|
||
startAutoSave() {
|
||
this.autoSaveInterval = setInterval(() => {
|
||
this.saveData();
|
||
}, 30000); // Save every 30 seconds
|
||
}
|
||
|
||
stopAutoSave() {
|
||
if (this.autoSaveInterval) {
|
||
clearInterval(this.autoSaveInterval);
|
||
this.autoSaveInterval = null;
|
||
}
|
||
}
|
||
|
||
// Statistics getters for UI
|
||
getStats() {
|
||
return {
|
||
...this.data.gameplay,
|
||
...this.data.statistics,
|
||
hoursPlayed: Math.round(this.data.gameplay.totalPlayTime / 3600000 * 10) / 10,
|
||
completionPercentage: Math.round(this.data.statistics.taskCompletionRate * 100)
|
||
};
|
||
}
|
||
|
||
// Reset functionality
|
||
resetAllData() {
|
||
if (confirm('Are you sure you want to reset all data? This cannot be undone!')) {
|
||
this.data = this.getDefaultData();
|
||
this.saveData();
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
resetStats() {
|
||
if (confirm('Reset all statistics? Your settings and custom tasks will be kept.')) {
|
||
this.data.gameplay = this.getDefaultData().gameplay;
|
||
this.data.statistics = this.getDefaultData().statistics;
|
||
this.saveData();
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// Make DataManager available globally
|
||
window.DataManager = DataManager;
|
||
|
||
// Simple Music Management System with Playlist Controls
|
||
class MusicManager {
|
||
constructor(dataManager) {
|
||
this.dataManager = dataManager;
|
||
this.currentAudio = null;
|
||
this.isPlaying = false;
|
||
this.volume = this.dataManager.getSetting('music.volume') || 30;
|
||
this.loopMode = this.dataManager.getSetting('music.loopMode') || 0;
|
||
this.shuffleMode = this.dataManager.getSetting('music.shuffle') || false;
|
||
this.currentTrackIndex = this.dataManager.getSetting('music.currentTrack') || 0;
|
||
this.playHistory = [];
|
||
|
||
// Initialize empty tracks array - custom tracks will be loaded next
|
||
this.tracks = [];
|
||
|
||
// Load and add custom background music
|
||
this.loadCustomTracks();
|
||
|
||
this.updateUI();
|
||
this.initializeVolumeUI();
|
||
}
|
||
|
||
saveSettings() {
|
||
this.dataManager.setSetting('music.volume', this.volume);
|
||
this.dataManager.setSetting('music.currentTrack', this.currentTrackIndex);
|
||
this.dataManager.setSetting('music.loopMode', this.loopMode);
|
||
this.dataManager.setSetting('music.shuffle', this.shuffleMode);
|
||
}
|
||
|
||
loadCustomTracks() {
|
||
// Get custom background music from storage
|
||
const customAudio = this.dataManager.get('customAudio') || { background: [], ambient: [], effects: [] };
|
||
const backgroundMusic = customAudio.background || [];
|
||
|
||
// Add enabled custom background music to tracks
|
||
backgroundMusic.forEach(audio => {
|
||
if (audio.enabled !== false) { // Include if not explicitly disabled
|
||
this.tracks.push({
|
||
name: audio.title || audio.name,
|
||
file: audio.path,
|
||
isBuiltIn: false,
|
||
isCustom: true
|
||
});
|
||
}
|
||
});
|
||
|
||
console.log(`MusicManager: Loaded ${backgroundMusic.length} custom background tracks`);
|
||
}
|
||
|
||
refreshCustomTracks() {
|
||
// Clear all tracks since we only have custom tracks now
|
||
this.tracks = [];
|
||
|
||
// Reload custom tracks
|
||
this.loadCustomTracks();
|
||
|
||
// Update the UI to reflect new tracks
|
||
this.updateTrackSelector();
|
||
|
||
// If current track was custom and no longer exists, reset to first track
|
||
if (this.currentTrackIndex >= this.tracks.length) {
|
||
this.currentTrackIndex = 0;
|
||
this.saveSettings();
|
||
}
|
||
|
||
console.log(`MusicManager: Refreshed tracks, now have ${this.tracks.length} total tracks`);
|
||
}
|
||
|
||
initializeVolumeUI() {
|
||
const volumeSlider = document.getElementById('volume-slider');
|
||
const volumePercent = document.getElementById('volume-percent');
|
||
const trackSelector = document.getElementById('track-selector');
|
||
|
||
// Check if UI elements exist before updating them
|
||
if (!volumeSlider || !volumePercent || !trackSelector) {
|
||
// UI elements not available (probably in dedicated screen mode)
|
||
return;
|
||
}
|
||
|
||
volumeSlider.value = this.volume;
|
||
volumePercent.textContent = `${this.volume}%`;
|
||
|
||
// Build track selector with all tracks
|
||
this.updateTrackSelector();
|
||
|
||
this.updateLoopButton();
|
||
this.updateShuffleButton();
|
||
}
|
||
|
||
// Loop mode cycling: 0 -> 1 -> 2 -> 0
|
||
toggleLoopMode() {
|
||
this.loopMode = (this.loopMode + 1) % 3;
|
||
this.saveSettings();
|
||
this.updateLoopButton();
|
||
this.updateAudioLoop();
|
||
}
|
||
|
||
toggleShuffleMode() {
|
||
this.shuffleMode = !this.shuffleMode;
|
||
this.saveSettings();
|
||
this.updateShuffleButton();
|
||
if (this.shuffleMode) {
|
||
this.playHistory = [this.currentTrackIndex]; // Start tracking
|
||
}
|
||
}
|
||
|
||
updateLoopButton() {
|
||
const loopBtn = document.getElementById('loop-btn');
|
||
if (!loopBtn) return;
|
||
|
||
switch(this.loopMode) {
|
||
case 0:
|
||
loopBtn.classList.remove('active');
|
||
loopBtn.innerHTML = '🔁';
|
||
loopBtn.title = 'Loop: Off';
|
||
break;
|
||
case 1:
|
||
loopBtn.classList.add('active');
|
||
loopBtn.innerHTML = '🔁';
|
||
loopBtn.title = 'Loop: Playlist';
|
||
break;
|
||
case 2:
|
||
loopBtn.classList.add('active');
|
||
loopBtn.innerHTML = '🔂';
|
||
loopBtn.title = 'Loop: Current Track';
|
||
break;
|
||
}
|
||
}
|
||
|
||
updateShuffleButton() {
|
||
const shuffleBtn = document.getElementById('shuffle-btn');
|
||
if (!shuffleBtn) return;
|
||
|
||
if (this.shuffleMode) {
|
||
shuffleBtn.classList.add('active');
|
||
shuffleBtn.title = 'Shuffle: On';
|
||
} else {
|
||
shuffleBtn.classList.remove('active');
|
||
shuffleBtn.title = 'Shuffle: Off';
|
||
}
|
||
}
|
||
|
||
updateAudioLoop() {
|
||
if (this.currentAudio) {
|
||
this.currentAudio.loop = (this.loopMode === 2);
|
||
}
|
||
}
|
||
|
||
getNextTrack() {
|
||
if (this.shuffleMode) {
|
||
return this.getShuffledNextTrack();
|
||
} else {
|
||
return (this.currentTrackIndex + 1) % this.tracks.length;
|
||
}
|
||
}
|
||
|
||
getShuffledNextTrack() {
|
||
if (this.tracks.length <= 1) return 0;
|
||
|
||
let availableTracks = [];
|
||
for (let i = 0; i < this.tracks.length; i++) {
|
||
// Avoid last 2 played tracks if possible
|
||
if (this.playHistory.length < 2 || !this.playHistory.slice(-2).includes(i)) {
|
||
availableTracks.push(i);
|
||
}
|
||
}
|
||
|
||
// If no tracks available (shouldn't happen with 4+ tracks), use all except current
|
||
if (availableTracks.length === 0) {
|
||
availableTracks = this.tracks.map((_, i) => i).filter(i => i !== this.currentTrackIndex);
|
||
}
|
||
|
||
return availableTracks[Math.floor(Math.random() * availableTracks.length)];
|
||
}
|
||
|
||
// Called when track ends naturally
|
||
onTrackEnded() {
|
||
if (this.loopMode === 2) {
|
||
// Loop current track - handled by audio.loop = true
|
||
return;
|
||
} else if (this.loopMode === 1) {
|
||
// Loop playlist - go to next track
|
||
this.playNextTrack();
|
||
} else {
|
||
// No loop - stop playing
|
||
this.isPlaying = false;
|
||
this.updateUI();
|
||
}
|
||
}
|
||
|
||
playNextTrack() {
|
||
const nextIndex = this.getNextTrack();
|
||
this.changeTrack(nextIndex);
|
||
|
||
// Update play history for shuffle
|
||
if (this.shuffleMode) {
|
||
this.playHistory.push(nextIndex);
|
||
// Keep only last 3 tracks in history
|
||
if (this.playHistory.length > 3) {
|
||
this.playHistory.shift();
|
||
}
|
||
}
|
||
}
|
||
|
||
toggle() {
|
||
if (this.isPlaying) {
|
||
this.stop();
|
||
} else {
|
||
this.play();
|
||
}
|
||
}
|
||
|
||
play() {
|
||
try {
|
||
if (!this.currentAudio) {
|
||
this.loadCurrentTrack();
|
||
}
|
||
|
||
this.currentAudio.play();
|
||
this.isPlaying = true;
|
||
this.updateUI();
|
||
} catch (error) {
|
||
console.log('Audio playback failed:', error);
|
||
// Fallback to silent mode
|
||
this.isPlaying = true;
|
||
this.updateUI();
|
||
}
|
||
}
|
||
|
||
stop() {
|
||
if (this.currentAudio) {
|
||
this.currentAudio.pause();
|
||
this.currentAudio.currentTime = 0;
|
||
}
|
||
this.isPlaying = false;
|
||
this.updateUI();
|
||
}
|
||
|
||
pause() {
|
||
if (this.currentAudio && this.isPlaying) {
|
||
this.currentAudio.pause();
|
||
}
|
||
}
|
||
|
||
resume() {
|
||
if (this.currentAudio && this.isPlaying) {
|
||
try {
|
||
this.currentAudio.play();
|
||
} catch (error) {
|
||
console.log('Audio resume failed:', error);
|
||
}
|
||
}
|
||
}
|
||
|
||
changeTrack(trackIndex) {
|
||
const wasPlaying = this.isPlaying;
|
||
|
||
if (this.currentAudio) {
|
||
this.currentAudio.pause();
|
||
this.currentAudio = null;
|
||
}
|
||
|
||
this.currentTrackIndex = trackIndex;
|
||
this.saveSettings();
|
||
|
||
if (wasPlaying) {
|
||
this.play();
|
||
} else {
|
||
this.updateUI();
|
||
}
|
||
}
|
||
|
||
setVolume(volume) {
|
||
this.volume = volume;
|
||
this.saveSettings();
|
||
|
||
if (this.currentAudio) {
|
||
this.currentAudio.volume = volume / 100;
|
||
}
|
||
|
||
const volumePercent = document.getElementById('volume-percent');
|
||
volumePercent.textContent = `${volume}%`;
|
||
|
||
// Update volume icon based on level
|
||
const volumeIcon = document.querySelector('.volume-icon');
|
||
if (volume === 0) {
|
||
volumeIcon.textContent = '🔇';
|
||
} else if (volume < 30) {
|
||
volumeIcon.textContent = '🔉';
|
||
} else {
|
||
volumeIcon.textContent = '🔊';
|
||
}
|
||
}
|
||
|
||
loadCurrentTrack() {
|
||
const track = this.tracks[this.currentTrackIndex];
|
||
|
||
// Create audio element with fallback
|
||
this.currentAudio = new Audio();
|
||
this.currentAudio.volume = this.volume / 100;
|
||
this.updateAudioLoop();
|
||
|
||
// Set source with error handling
|
||
this.currentAudio.src = track.file;
|
||
|
||
// Handle loading errors gracefully
|
||
this.currentAudio.onerror = () => {
|
||
console.log(`Could not load audio: ${track.file}`);
|
||
// Continue without audio
|
||
};
|
||
|
||
// Handle successful loading
|
||
this.currentAudio.onloadeddata = () => {
|
||
console.log(`Loaded audio: ${track.name}`);
|
||
};
|
||
|
||
// Handle track ending
|
||
this.currentAudio.onended = () => {
|
||
if (this.isPlaying) {
|
||
this.onTrackEnded();
|
||
}
|
||
};
|
||
}
|
||
|
||
updateUI() {
|
||
const toggleBtn = document.getElementById('music-toggle');
|
||
const statusSpan = document.getElementById('music-status');
|
||
const trackSelector = document.getElementById('track-selector');
|
||
const currentTrack = this.tracks[this.currentTrackIndex];
|
||
|
||
// Check if UI elements exist before updating them
|
||
if (!toggleBtn || !statusSpan || !trackSelector) {
|
||
// UI elements not available (probably in dedicated screen mode)
|
||
return;
|
||
}
|
||
|
||
// Update track selector
|
||
trackSelector.value = this.currentTrackIndex;
|
||
|
||
if (this.isPlaying) {
|
||
toggleBtn.classList.add('playing');
|
||
toggleBtn.innerHTML = '⏸️';
|
||
|
||
// Show current mode in status
|
||
let modeText = '';
|
||
if (this.shuffleMode) modeText += ' 🔀';
|
||
if (this.loopMode === 1) modeText += ' 🔁';
|
||
else if (this.loopMode === 2) modeText += ' 🔂';
|
||
|
||
statusSpan.textContent = `♪ ${currentTrack.name}${modeText}`;
|
||
} else {
|
||
toggleBtn.classList.remove('playing');
|
||
toggleBtn.innerHTML = '🎵';
|
||
statusSpan.textContent = 'Music: Off';
|
||
}
|
||
}
|
||
|
||
updateTrackSelector() {
|
||
const trackSelector = document.getElementById('track-selector');
|
||
if (!trackSelector) return;
|
||
|
||
// Clear existing options
|
||
trackSelector.innerHTML = '';
|
||
|
||
// Add all tracks (built-in and custom)
|
||
this.tracks.forEach((track, index) => {
|
||
const option = document.createElement('option');
|
||
option.value = index;
|
||
option.textContent = track.isCustom ? `${track.name} (Custom)` : track.name;
|
||
trackSelector.appendChild(option);
|
||
});
|
||
|
||
// Set current selection
|
||
trackSelector.value = this.currentTrackIndex;
|
||
}
|
||
}
|
||
|
||
// Photo Gallery Management Methods
|
||
TaskChallengeGame.prototype.showPhotoGallery = function() {
|
||
this.showScreen('photo-gallery-screen');
|
||
this.setupPhotoGalleryEventListeners();
|
||
this.loadPhotoGallery();
|
||
};
|
||
|
||
TaskChallengeGame.prototype.setupPhotoGalleryEventListeners = function() {
|
||
// Back button
|
||
const backBtn = document.getElementById('back-to-start-from-photos-btn');
|
||
if (backBtn) {
|
||
backBtn.onclick = () => this.showScreen('start-screen');
|
||
}
|
||
|
||
// Action buttons
|
||
const downloadAllBtn = document.getElementById('download-all-photos-btn');
|
||
if (downloadAllBtn) {
|
||
downloadAllBtn.onclick = () => this.downloadAllPhotos();
|
||
}
|
||
|
||
const downloadSelectedBtn = document.getElementById('download-selected-photos-btn');
|
||
if (downloadSelectedBtn) {
|
||
downloadSelectedBtn.onclick = () => this.downloadSelectedPhotos();
|
||
}
|
||
|
||
const selectAllBtn = document.getElementById('select-all-photos-btn');
|
||
if (selectAllBtn) {
|
||
selectAllBtn.onclick = () => this.selectAllPhotos();
|
||
}
|
||
|
||
const deselectAllBtn = document.getElementById('deselect-all-photos-btn');
|
||
if (deselectAllBtn) {
|
||
deselectAllBtn.onclick = () => this.deselectAllPhotos();
|
||
}
|
||
|
||
const clearAllBtn = document.getElementById('clear-all-photos-btn');
|
||
if (clearAllBtn) {
|
||
clearAllBtn.onclick = () => this.clearAllPhotos();
|
||
}
|
||
|
||
const settingsBtn = document.getElementById('photo-storage-settings-btn');
|
||
if (settingsBtn) {
|
||
settingsBtn.onclick = () => this.showPhotoSettings();
|
||
}
|
||
|
||
// Filter controls
|
||
const sessionFilter = document.getElementById('photo-session-filter');
|
||
if (sessionFilter) {
|
||
sessionFilter.onchange = () => this.filterPhotos();
|
||
}
|
||
|
||
const sortOrder = document.getElementById('photo-sort-order');
|
||
if (sortOrder) {
|
||
sortOrder.onchange = () => this.filterPhotos();
|
||
}
|
||
|
||
// Modal close buttons
|
||
const closeDetailBtn = document.getElementById('close-photo-detail');
|
||
if (closeDetailBtn) {
|
||
closeDetailBtn.onclick = () => this.closePhotoDetail();
|
||
}
|
||
|
||
const closeSettingsBtn = document.getElementById('close-photo-settings');
|
||
if (closeSettingsBtn) {
|
||
closeSettingsBtn.onclick = () => this.closePhotoSettings();
|
||
}
|
||
|
||
// Photo detail modal actions
|
||
const downloadBtn = document.getElementById('download-photo-btn');
|
||
if (downloadBtn) {
|
||
downloadBtn.onclick = () => this.downloadCurrentPhoto();
|
||
}
|
||
|
||
const deleteBtn = document.getElementById('delete-photo-btn');
|
||
if (deleteBtn) {
|
||
deleteBtn.onclick = () => this.deleteCurrentPhoto();
|
||
}
|
||
|
||
// Settings modal actions
|
||
const saveSettingsBtn = document.getElementById('save-photo-settings-btn');
|
||
if (saveSettingsBtn) {
|
||
saveSettingsBtn.onclick = () => this.savePhotoSettings();
|
||
}
|
||
};
|
||
|
||
TaskChallengeGame.prototype.loadPhotoGallery = function() {
|
||
const stats = this.webcamManager.getPhotoStats();
|
||
this.updatePhotoStats(stats);
|
||
|
||
const photos = this.webcamManager.getSavedPhotos();
|
||
const grid = document.getElementById('photo-grid');
|
||
const noPhotosMsg = document.getElementById('no-photos-message');
|
||
|
||
if (photos.length === 0) {
|
||
grid.style.display = 'none';
|
||
noPhotosMsg.style.display = 'block';
|
||
return;
|
||
}
|
||
|
||
grid.style.display = 'grid';
|
||
noPhotosMsg.style.display = 'none';
|
||
|
||
this.renderPhotoGrid(photos);
|
||
};
|
||
|
||
TaskChallengeGame.prototype.updatePhotoStats = function(stats) {
|
||
const countDisplay = document.getElementById('photo-count-display');
|
||
const sizeDisplay = document.getElementById('storage-size-display');
|
||
|
||
if (countDisplay) {
|
||
countDisplay.textContent = `${stats.count} photos`;
|
||
}
|
||
|
||
if (sizeDisplay) {
|
||
const sizeInKB = (stats.totalSize / 1024).toFixed(1);
|
||
sizeDisplay.textContent = `${sizeInKB} KB used`;
|
||
}
|
||
};
|
||
|
||
TaskChallengeGame.prototype.renderPhotoGrid = function(photos) {
|
||
const grid = document.getElementById('photo-grid');
|
||
const sessionFilter = document.getElementById('photo-session-filter').value;
|
||
const sortOrder = document.getElementById('photo-sort-order').value;
|
||
|
||
// Initialize selected photos array if not exists
|
||
if (!this.selectedPhotos) {
|
||
this.selectedPhotos = new Set();
|
||
}
|
||
|
||
// Filter photos
|
||
let filteredPhotos = photos;
|
||
if (sessionFilter !== 'all') {
|
||
filteredPhotos = photos.filter(photo => photo.sessionType === sessionFilter);
|
||
}
|
||
|
||
// Sort photos
|
||
switch (sortOrder) {
|
||
case 'newest':
|
||
filteredPhotos.sort((a, b) => b.timestamp - a.timestamp);
|
||
break;
|
||
case 'oldest':
|
||
filteredPhotos.sort((a, b) => a.timestamp - b.timestamp);
|
||
break;
|
||
case 'session':
|
||
filteredPhotos.sort((a, b) => a.sessionType.localeCompare(b.sessionType));
|
||
break;
|
||
}
|
||
|
||
// Render photo items
|
||
grid.innerHTML = filteredPhotos.map(photo => this.createPhotoItem(photo)).join('');
|
||
|
||
// Add click listeners
|
||
grid.querySelectorAll('.photo-item').forEach(item => {
|
||
const photoId = item.dataset.photoId;
|
||
const checkbox = item.querySelector('.selection-checkbox');
|
||
|
||
// Checkbox click handler
|
||
checkbox.onchange = (e) => {
|
||
e.stopPropagation();
|
||
this.togglePhotoSelection(photoId, checkbox.checked);
|
||
};
|
||
|
||
// Click anywhere on item to select/deselect (like image gallery)
|
||
item.addEventListener('click', (e) => {
|
||
// Don't toggle selection if clicking on checkbox
|
||
if (e.target.type !== 'checkbox') {
|
||
const isCurrentlySelected = this.selectedPhotos.has(photoId);
|
||
this.togglePhotoSelection(photoId, !isCurrentlySelected);
|
||
checkbox.checked = !isCurrentlySelected;
|
||
}
|
||
});
|
||
|
||
// Update checkbox state if photo was previously selected
|
||
if (this.selectedPhotos.has(photoId)) {
|
||
checkbox.checked = true;
|
||
item.classList.add('selected');
|
||
}
|
||
});
|
||
|
||
// Update selection count
|
||
this.updateSelectionCount();
|
||
};
|
||
|
||
TaskChallengeGame.prototype.createPhotoItem = function(photo) {
|
||
const date = new Date(photo.timestamp).toLocaleDateString();
|
||
const sessionName = this.formatSessionName(photo.sessionType);
|
||
|
||
return `
|
||
<div class="photo-item" data-photo-id="${photo.id}">
|
||
<input type="checkbox" class="selection-checkbox" data-photo-id="${photo.id}">
|
||
<img src="${photo.dataURL}" alt="Captured photo" loading="lazy">
|
||
<div class="photo-item-info">
|
||
<div class="photo-item-date">${date}</div>
|
||
<div class="photo-item-session">${sessionName}</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
};
|
||
|
||
TaskChallengeGame.prototype.formatSessionName = function(sessionType) {
|
||
const sessionNames = {
|
||
'dress-up-photo': 'Dress-up',
|
||
'photography-studio': 'Studio',
|
||
'custom': 'Custom'
|
||
};
|
||
return sessionNames[sessionType] || sessionType;
|
||
};
|
||
|
||
TaskChallengeGame.prototype.filterPhotos = function() {
|
||
const photos = this.webcamManager.getSavedPhotos();
|
||
this.renderPhotoGrid(photos);
|
||
};
|
||
|
||
// Photo selection management
|
||
TaskChallengeGame.prototype.togglePhotoSelection = function(photoId, isSelected) {
|
||
if (!this.selectedPhotos) {
|
||
this.selectedPhotos = new Set();
|
||
}
|
||
|
||
const photoItem = document.querySelector(`.photo-item[data-photo-id="${photoId}"]`);
|
||
|
||
if (isSelected) {
|
||
this.selectedPhotos.add(photoId);
|
||
photoItem.classList.add('selected');
|
||
} else {
|
||
this.selectedPhotos.delete(photoId);
|
||
photoItem.classList.remove('selected');
|
||
}
|
||
|
||
this.updateSelectionCount();
|
||
};
|
||
|
||
TaskChallengeGame.prototype.selectAllPhotos = function() {
|
||
if (!this.selectedPhotos) {
|
||
this.selectedPhotos = new Set();
|
||
}
|
||
|
||
document.querySelectorAll('.photo-checkbox').forEach(checkbox => {
|
||
checkbox.checked = true;
|
||
this.selectedPhotos.add(checkbox.dataset.photoId);
|
||
checkbox.closest('.photo-item').classList.add('selected');
|
||
});
|
||
|
||
this.updateSelectionCount();
|
||
};
|
||
|
||
TaskChallengeGame.prototype.deselectAllPhotos = function() {
|
||
if (!this.selectedPhotos) {
|
||
this.selectedPhotos = new Set();
|
||
}
|
||
|
||
document.querySelectorAll('.photo-checkbox').forEach(checkbox => {
|
||
checkbox.checked = false;
|
||
checkbox.closest('.photo-item').classList.remove('selected');
|
||
});
|
||
|
||
this.selectedPhotos.clear();
|
||
this.updateSelectionCount();
|
||
};
|
||
|
||
TaskChallengeGame.prototype.updateSelectionCount = function() {
|
||
const count = this.selectedPhotos ? this.selectedPhotos.size : 0;
|
||
const downloadBtn = document.getElementById('download-selected-photos-btn');
|
||
|
||
if (downloadBtn) {
|
||
downloadBtn.textContent = count > 0 ? `📥 Download Selected (${count})` : '📥 Download Selected';
|
||
downloadBtn.disabled = count === 0;
|
||
|
||
if (count === 0) {
|
||
downloadBtn.classList.add('btn-disabled');
|
||
} else {
|
||
downloadBtn.classList.remove('btn-disabled');
|
||
}
|
||
}
|
||
};
|
||
|
||
TaskChallengeGame.prototype.downloadSelectedPhotos = function() {
|
||
if (!this.selectedPhotos || this.selectedPhotos.size === 0) {
|
||
this.showNotification('Please select photos to download', 'warning');
|
||
return;
|
||
}
|
||
|
||
const allPhotos = this.webcamManager.getSavedPhotos();
|
||
const selectedPhotos = allPhotos.filter(photo => this.selectedPhotos.has(photo.id));
|
||
|
||
this.webcamManager.downloadSelectedPhotos(selectedPhotos);
|
||
};
|
||
|
||
TaskChallengeGame.prototype.showPhotoDetail = function(photoId) {
|
||
const photos = this.webcamManager.getSavedPhotos();
|
||
const photo = photos.find(p => p.id === photoId);
|
||
if (!photo) return;
|
||
|
||
this.currentDetailPhoto = photo;
|
||
|
||
// Populate modal
|
||
document.getElementById('photo-detail-image').src = photo.dataURL;
|
||
document.getElementById('photo-detail-date').textContent = new Date(photo.timestamp).toLocaleString();
|
||
document.getElementById('photo-detail-session').textContent = this.formatSessionName(photo.sessionType);
|
||
document.getElementById('photo-detail-task').textContent = photo.taskId || 'Unknown';
|
||
document.getElementById('photo-detail-size').textContent = `${Math.round(photo.size / 1024)} KB`;
|
||
|
||
// Show modal
|
||
document.getElementById('photo-detail-modal').style.display = 'flex';
|
||
};
|
||
|
||
TaskChallengeGame.prototype.closePhotoDetail = function() {
|
||
document.getElementById('photo-detail-modal').style.display = 'none';
|
||
this.currentDetailPhoto = null;
|
||
};
|
||
|
||
TaskChallengeGame.prototype.downloadCurrentPhoto = function() {
|
||
if (this.currentDetailPhoto) {
|
||
this.webcamManager.downloadPhoto(this.currentDetailPhoto);
|
||
}
|
||
};
|
||
|
||
TaskChallengeGame.prototype.deleteCurrentPhoto = function() {
|
||
if (!this.currentDetailPhoto) return;
|
||
|
||
if (confirm('Are you sure you want to delete this photo? This cannot be undone.')) {
|
||
const success = this.webcamManager.deletePhoto(this.currentDetailPhoto.id);
|
||
if (success) {
|
||
this.closePhotoDetail();
|
||
this.loadPhotoGallery(); // Refresh the gallery
|
||
this.webcamManager.showNotification('Photo deleted', 'success');
|
||
} else {
|
||
this.webcamManager.showNotification('Failed to delete photo', 'error');
|
||
}
|
||
}
|
||
};
|
||
|
||
TaskChallengeGame.prototype.downloadAllPhotos = function() {
|
||
this.webcamManager.downloadAllPhotos();
|
||
};
|
||
|
||
TaskChallengeGame.prototype.clearAllPhotos = function() {
|
||
if (confirm('Are you sure you want to delete ALL photos? This cannot be undone.')) {
|
||
const success = this.webcamManager.clearAllPhotos();
|
||
if (success) {
|
||
this.loadPhotoGallery(); // Refresh the gallery
|
||
this.webcamManager.showNotification('All photos cleared', 'success');
|
||
} else {
|
||
this.webcamManager.showNotification('Failed to clear photos', 'error');
|
||
}
|
||
}
|
||
};
|
||
|
||
TaskChallengeGame.prototype.showPhotoSettings = function() {
|
||
const stats = this.webcamManager.getPhotoStats();
|
||
|
||
// Update consent radio buttons
|
||
const consentValue = stats.storageConsent;
|
||
document.getElementById('consent-enable').checked = consentValue === 'true';
|
||
document.getElementById('consent-disable').checked = consentValue === 'false';
|
||
|
||
// Update stats
|
||
document.getElementById('settings-photo-count').textContent = stats.count;
|
||
document.getElementById('settings-storage-size').textContent = `${(stats.totalSize / 1024).toFixed(1)} KB`;
|
||
document.getElementById('settings-oldest-photo').textContent =
|
||
stats.oldestPhoto ? new Date(stats.oldestPhoto).toLocaleDateString() : 'None';
|
||
|
||
// Show modal
|
||
document.getElementById('photo-settings-modal').style.display = 'flex';
|
||
};
|
||
|
||
TaskChallengeGame.prototype.closePhotoSettings = function() {
|
||
document.getElementById('photo-settings-modal').style.display = 'none';
|
||
};
|
||
|
||
TaskChallengeGame.prototype.savePhotoSettings = function() {
|
||
const consentValue = document.querySelector('input[name="photo-consent"]:checked').value;
|
||
localStorage.setItem('photoStorageConsent', consentValue);
|
||
|
||
this.closePhotoSettings();
|
||
this.webcamManager.showNotification('Photo settings saved', 'success');
|
||
};
|
||
|
||
// Annoyance Management Methods - Phase 2: Advanced Message Management
|
||
TaskChallengeGame.prototype.showAnnoyanceManagement = function() {
|
||
this.showScreen('annoyance-management-screen');
|
||
this.setupAnnoyanceManagementEventListeners();
|
||
this.loadAnnoyanceSettings();
|
||
this.showAnnoyanceTab('messages'); // Default to messages tab
|
||
};
|
||
|
||
TaskChallengeGame.prototype.setupAnnoyanceManagementEventListeners = function() {
|
||
// Back button
|
||
const backBtn = document.getElementById('back-to-start-from-annoyance-btn');
|
||
if (backBtn) {
|
||
backBtn.onclick = () => this.showScreen('start-screen');
|
||
}
|
||
|
||
// Save settings button
|
||
const saveBtn = document.getElementById('save-annoyance-settings');
|
||
if (saveBtn) {
|
||
saveBtn.onclick = () => this.saveAllAnnoyanceSettings();
|
||
}
|
||
|
||
// Tab navigation
|
||
document.getElementById('messages-tab').onclick = () => this.showAnnoyanceTab('messages');
|
||
document.getElementById('appearance-tab').onclick = () => this.showAnnoyanceTab('appearance');
|
||
document.getElementById('behavior-tab').onclick = () => this.showAnnoyanceTab('behavior');
|
||
document.getElementById('popup-images-tab').onclick = () => this.showAnnoyanceTab('popup-images');
|
||
document.getElementById('import-export-tab').onclick = () => this.showAnnoyanceTab('import-export');
|
||
document.getElementById('ai-tasks-tab').onclick = () => this.showAnnoyanceTab('ai-tasks');
|
||
|
||
this.setupMessagesTabListeners();
|
||
this.setupAppearanceTabListeners();
|
||
this.setupBehaviorTabListeners();
|
||
this.setupPopupImagesTabListeners();
|
||
this.setupImportExportTabListeners();
|
||
this.setupAITasksTabListeners();
|
||
};
|
||
|
||
TaskChallengeGame.prototype.showAnnoyanceTab = function(tabName) {
|
||
// Update tab buttons
|
||
document.querySelectorAll('.annoyance-tab').forEach(tab => tab.classList.remove('active'));
|
||
document.getElementById(`${tabName}-tab`).classList.add('active');
|
||
|
||
// Update tab content
|
||
document.querySelectorAll('.annoyance-tab-content').forEach(content => content.classList.remove('active'));
|
||
document.getElementById(`${tabName}-tab-content`).classList.add('active');
|
||
|
||
// Load tab-specific content
|
||
switch(tabName) {
|
||
case 'messages':
|
||
this.loadMessagesTab();
|
||
break;
|
||
case 'appearance':
|
||
this.loadAppearanceTab();
|
||
break;
|
||
case 'behavior':
|
||
this.loadBehaviorTab();
|
||
break;
|
||
case 'popup-images':
|
||
this.loadPopupImagesSettings();
|
||
break;
|
||
case 'import-export':
|
||
this.loadImportExportTab();
|
||
break;
|
||
case 'ai-tasks':
|
||
this.loadAITasksTab();
|
||
break;
|
||
}
|
||
};
|
||
|
||
// Messages Tab Management
|
||
TaskChallengeGame.prototype.setupMessagesTabListeners = function() {
|
||
const enabledCheckbox = document.getElementById('flash-messages-enabled');
|
||
const addBtn = document.getElementById('add-new-message-btn');
|
||
const closeEditorBtn = document.getElementById('close-editor-btn');
|
||
const saveMessageBtn = document.getElementById('save-message-btn');
|
||
const previewCurrentBtn = document.getElementById('preview-current-message-btn');
|
||
const cancelEditBtn = document.getElementById('cancel-edit-btn');
|
||
const categoryFilter = document.getElementById('category-filter');
|
||
const showDisabledCheckbox = document.getElementById('show-disabled-messages');
|
||
const messageTextarea = document.getElementById('message-text');
|
||
|
||
if (enabledCheckbox) {
|
||
enabledCheckbox.onchange = (e) => this.updateFlashMessageSetting('enabled', e.target.checked);
|
||
}
|
||
|
||
if (addBtn) {
|
||
addBtn.onclick = () => this.showMessageEditor();
|
||
}
|
||
|
||
if (closeEditorBtn) {
|
||
closeEditorBtn.onclick = () => this.hideMessageEditor();
|
||
}
|
||
|
||
if (saveMessageBtn) {
|
||
saveMessageBtn.onclick = () => this.saveCurrentMessage();
|
||
}
|
||
|
||
if (previewCurrentBtn) {
|
||
previewCurrentBtn.onclick = () => this.previewCurrentMessage();
|
||
}
|
||
|
||
if (cancelEditBtn) {
|
||
cancelEditBtn.onclick = () => this.hideMessageEditor();
|
||
}
|
||
|
||
if (categoryFilter) {
|
||
categoryFilter.onchange = () => this.refreshMessageList();
|
||
}
|
||
|
||
if (showDisabledCheckbox) {
|
||
showDisabledCheckbox.onchange = () => this.refreshMessageList();
|
||
}
|
||
|
||
if (messageTextarea) {
|
||
messageTextarea.oninput = () => this.updateCharacterCount();
|
||
}
|
||
};
|
||
|
||
TaskChallengeGame.prototype.loadMessagesTab = function() {
|
||
const config = this.flashMessageManager.getConfig();
|
||
const enabledCheckbox = document.getElementById('flash-messages-enabled');
|
||
if (enabledCheckbox) enabledCheckbox.checked = config.enabled;
|
||
|
||
this.refreshMessageList();
|
||
this.hideMessageEditor(); // Ensure editor is closed by default
|
||
};
|
||
|
||
TaskChallengeGame.prototype.refreshMessageList = function() {
|
||
const categoryFilter = document.getElementById('category-filter');
|
||
const showDisabled = document.getElementById('show-disabled-messages').checked;
|
||
const selectedCategory = categoryFilter.value;
|
||
|
||
let messages = this.flashMessageManager.getMessagesByCategory(selectedCategory);
|
||
|
||
if (!showDisabled) {
|
||
messages = messages.filter(msg => msg.enabled !== false);
|
||
}
|
||
|
||
this.renderMessageList(messages);
|
||
this.updateMessageStats();
|
||
};
|
||
|
||
TaskChallengeGame.prototype.renderMessageList = function(messages) {
|
||
const listElement = document.getElementById('message-list');
|
||
|
||
if (messages.length === 0) {
|
||
listElement.innerHTML = '<div style="padding: 20px; text-align: center; color: #666;">No messages match your criteria.</div>';
|
||
return;
|
||
}
|
||
|
||
listElement.innerHTML = messages.map(msg => `
|
||
<div class="message-item ${msg.enabled === false ? 'disabled' : ''}" data-message-id="${msg.id}">
|
||
<div class="message-content">
|
||
<div class="message-text">${msg.text}</div>
|
||
<div class="message-meta">
|
||
<span class="message-category ${msg.category}">${this.getCategoryEmoji(msg.category)} ${msg.category || 'Custom'}</span>
|
||
<span>Priority: ${msg.priority || 'Normal'}</span>
|
||
${msg.isCustom ? '<span>Custom</span>' : '<span>Default</span>'}
|
||
</div>
|
||
</div>
|
||
<div class="message-actions">
|
||
<div class="message-toggle ${msg.enabled !== false ? 'enabled' : ''}"
|
||
onclick="game.toggleMessage(${msg.id})"></div>
|
||
<button class="btn btn-info btn-small" onclick="game.editMessage(${msg.id})">Edit</button>
|
||
<button class="btn btn-success btn-small" onclick="game.previewMessage(${msg.id})">Preview</button>
|
||
${msg.isCustom ? `<button class="btn btn-danger btn-small" onclick="game.deleteMessage(${msg.id})">Delete</button>` : ''}
|
||
</div>
|
||
</div>
|
||
`).join('');
|
||
};
|
||
|
||
TaskChallengeGame.prototype.updateMessageStats = function() {
|
||
const stats = this.flashMessageManager.getMessageStats();
|
||
const statsElement = document.getElementById('message-stats');
|
||
if (statsElement) {
|
||
statsElement.textContent = `${stats.total} messages (${stats.enabled} enabled, ${stats.disabled} disabled)`;
|
||
}
|
||
};
|
||
|
||
TaskChallengeGame.prototype.showMessageEditor = function(messageData = null) {
|
||
const editor = document.getElementById('message-editor');
|
||
const title = document.getElementById('editor-title');
|
||
const textarea = document.getElementById('message-text');
|
||
const category = document.getElementById('message-category');
|
||
const priority = document.getElementById('message-priority');
|
||
|
||
if (messageData) {
|
||
title.textContent = 'Edit Message';
|
||
textarea.value = messageData.text || '';
|
||
category.value = messageData.category || 'custom';
|
||
priority.value = messageData.priority || 'normal';
|
||
editor.dataset.editingId = messageData.id;
|
||
} else {
|
||
title.textContent = 'Add New Message';
|
||
textarea.value = '';
|
||
category.value = 'custom';
|
||
priority.value = 'normal';
|
||
delete editor.dataset.editingId;
|
||
}
|
||
|
||
editor.style.display = 'block';
|
||
textarea.focus();
|
||
this.updateCharacterCount();
|
||
};
|
||
|
||
TaskChallengeGame.prototype.hideMessageEditor = function() {
|
||
const editor = document.getElementById('message-editor');
|
||
editor.style.display = 'none';
|
||
};
|
||
|
||
TaskChallengeGame.prototype.saveCurrentMessage = function() {
|
||
const textarea = document.getElementById('message-text');
|
||
const category = document.getElementById('message-category');
|
||
const priority = document.getElementById('message-priority');
|
||
const editor = document.getElementById('message-editor');
|
||
|
||
const messageText = textarea.value.trim();
|
||
if (!messageText) {
|
||
this.showNotification('Please enter a message text!', 'error');
|
||
return;
|
||
}
|
||
|
||
const messageData = {
|
||
text: messageText,
|
||
category: category.value,
|
||
priority: priority.value
|
||
};
|
||
|
||
if (editor.dataset.editingId) {
|
||
// Edit existing message
|
||
const updated = this.flashMessageManager.editMessage(parseInt(editor.dataset.editingId), messageData);
|
||
if (updated) {
|
||
this.showNotification('✅ Message updated successfully!', 'success');
|
||
} else {
|
||
this.showNotification('❌ Failed to update message', 'error');
|
||
}
|
||
} else {
|
||
// Add new message
|
||
const newMessage = this.flashMessageManager.addMessage(messageData);
|
||
this.showNotification('✅ Message added successfully!', 'success');
|
||
}
|
||
|
||
this.hideMessageEditor();
|
||
this.refreshMessageList();
|
||
};
|
||
|
||
TaskChallengeGame.prototype.previewCurrentMessage = function() {
|
||
const textarea = document.getElementById('message-text');
|
||
const messageText = textarea.value.trim();
|
||
|
||
if (!messageText) {
|
||
this.showNotification('Please enter a message to preview!', 'warning');
|
||
return;
|
||
}
|
||
|
||
this.flashMessageManager.previewMessage({ text: messageText });
|
||
};
|
||
|
||
TaskChallengeGame.prototype.updateCharacterCount = function() {
|
||
const textarea = document.getElementById('message-text');
|
||
const counter = document.getElementById('char-count');
|
||
const currentLength = textarea.value.length;
|
||
const maxLength = 200;
|
||
|
||
counter.textContent = currentLength;
|
||
|
||
const counterElement = counter.parentElement;
|
||
counterElement.className = 'char-counter';
|
||
|
||
if (currentLength > maxLength * 0.9) {
|
||
counterElement.className += ' warning';
|
||
}
|
||
if (currentLength > maxLength) {
|
||
counterElement.className += ' error';
|
||
}
|
||
};
|
||
|
||
// Message action methods
|
||
TaskChallengeGame.prototype.toggleMessage = function(messageId) {
|
||
const enabled = this.flashMessageManager.toggleMessageEnabled(messageId);
|
||
this.refreshMessageList();
|
||
this.showNotification(`Message ${enabled ? 'enabled' : 'disabled'}`, 'success');
|
||
};
|
||
|
||
TaskChallengeGame.prototype.editMessage = function(messageId) {
|
||
const messages = this.flashMessageManager.getAllMessages();
|
||
const message = messages.find(msg => msg.id === messageId);
|
||
if (message) {
|
||
this.showMessageEditor(message);
|
||
}
|
||
};
|
||
|
||
TaskChallengeGame.prototype.previewMessage = function(messageId) {
|
||
const messages = this.flashMessageManager.getAllMessages();
|
||
const message = messages.find(msg => msg.id === messageId);
|
||
if (message) {
|
||
this.flashMessageManager.previewMessage(message);
|
||
}
|
||
};
|
||
|
||
TaskChallengeGame.prototype.deleteMessage = function(messageId) {
|
||
const messages = this.flashMessageManager.getAllMessages();
|
||
const message = messages.find(msg => msg.id === messageId);
|
||
|
||
if (message && confirm(`Delete this message?\n\n"${message.text}"`)) {
|
||
const deleted = this.flashMessageManager.deleteMessage(messageId);
|
||
if (deleted) {
|
||
this.showNotification('✅ Message deleted', 'success');
|
||
this.refreshMessageList();
|
||
}
|
||
}
|
||
};
|
||
|
||
TaskChallengeGame.prototype.getCategoryEmoji = function(category) {
|
||
const emojis = {
|
||
motivational: '💪',
|
||
encouraging: '🌟',
|
||
achievement: '🏆',
|
||
persistence: '🔥',
|
||
custom: '✨'
|
||
};
|
||
return emojis[category] || '✨';
|
||
};
|
||
|
||
// Appearance Tab Management
|
||
TaskChallengeGame.prototype.setupAppearanceTabListeners = function() {
|
||
const controls = {
|
||
position: document.getElementById('message-position'),
|
||
animation: document.getElementById('animation-style'),
|
||
fontSize: document.getElementById('font-size'),
|
||
opacity: document.getElementById('message-opacity'),
|
||
textColor: document.getElementById('text-color'),
|
||
backgroundColor: document.getElementById('background-color'),
|
||
resetBtn: document.getElementById('reset-appearance-btn'),
|
||
previewBtn: document.getElementById('preview-appearance-btn')
|
||
};
|
||
|
||
if (controls.position) {
|
||
controls.position.onchange = (e) => this.updateFlashMessageSetting('position', e.target.value);
|
||
}
|
||
|
||
if (controls.animation) {
|
||
controls.animation.onchange = (e) => this.updateFlashMessageSetting('animation', e.target.value);
|
||
}
|
||
|
||
if (controls.fontSize) {
|
||
controls.fontSize.oninput = (e) => {
|
||
const value = parseInt(e.target.value);
|
||
document.getElementById('font-size-display').textContent = `${value}px`;
|
||
this.updateFlashMessageSetting('fontSize', `${value}px`);
|
||
};
|
||
}
|
||
|
||
if (controls.opacity) {
|
||
controls.opacity.oninput = (e) => {
|
||
const value = parseInt(e.target.value);
|
||
document.getElementById('opacity-display').textContent = `${value}%`;
|
||
const bgColor = this.hexToRgba(controls.backgroundColor.value, value / 100);
|
||
this.updateFlashMessageSetting('backgroundColor', bgColor);
|
||
};
|
||
}
|
||
|
||
if (controls.textColor) {
|
||
controls.textColor.onchange = (e) => this.updateFlashMessageSetting('color', e.target.value);
|
||
}
|
||
|
||
if (controls.backgroundColor) {
|
||
controls.backgroundColor.onchange = (e) => {
|
||
const opacity = parseInt(controls.opacity.value) / 100;
|
||
const bgColor = this.hexToRgba(e.target.value, opacity);
|
||
this.updateFlashMessageSetting('backgroundColor', bgColor);
|
||
};
|
||
}
|
||
|
||
if (controls.resetBtn) {
|
||
controls.resetBtn.onclick = () => this.resetAppearanceToDefaults();
|
||
}
|
||
|
||
if (controls.previewBtn) {
|
||
controls.previewBtn.onclick = () => this.previewAppearanceStyle();
|
||
}
|
||
};
|
||
|
||
TaskChallengeGame.prototype.loadAppearanceTab = function() {
|
||
const config = this.flashMessageManager.getConfig();
|
||
|
||
const controls = {
|
||
position: document.getElementById('message-position'),
|
||
animation: document.getElementById('animation-style'),
|
||
fontSize: document.getElementById('font-size'),
|
||
opacity: document.getElementById('message-opacity'),
|
||
textColor: document.getElementById('text-color'),
|
||
backgroundColor: document.getElementById('background-color')
|
||
};
|
||
|
||
if (controls.position) controls.position.value = config.position;
|
||
if (controls.animation) controls.animation.value = config.animation;
|
||
if (controls.fontSize) {
|
||
const fontSize = parseInt(config.fontSize) || 24;
|
||
controls.fontSize.value = fontSize;
|
||
document.getElementById('font-size-display').textContent = `${fontSize}px`;
|
||
}
|
||
|
||
// Extract opacity from backgroundColor if it's rgba
|
||
let opacity = 90;
|
||
let bgHex = '#007bff';
|
||
if (config.backgroundColor.includes('rgba')) {
|
||
const rgbaMatch = config.backgroundColor.match(/rgba\((\d+),\s*(\d+),\s*(\d+),\s*([01]?\.?\d*)\)/);
|
||
if (rgbaMatch) {
|
||
opacity = Math.round(parseFloat(rgbaMatch[4]) * 100);
|
||
bgHex = this.rgbToHex(parseInt(rgbaMatch[1]), parseInt(rgbaMatch[2]), parseInt(rgbaMatch[3]));
|
||
}
|
||
}
|
||
|
||
if (controls.opacity) {
|
||
controls.opacity.value = opacity;
|
||
document.getElementById('opacity-display').textContent = `${opacity}%`;
|
||
}
|
||
if (controls.textColor) controls.textColor.value = config.color || '#ffffff';
|
||
if (controls.backgroundColor) controls.backgroundColor.value = bgHex;
|
||
};
|
||
|
||
TaskChallengeGame.prototype.resetAppearanceToDefaults = function() {
|
||
const defaults = gameData.defaultFlashConfig;
|
||
this.flashMessageManager.updateConfig({
|
||
position: defaults.position,
|
||
animation: defaults.animation,
|
||
fontSize: defaults.fontSize,
|
||
color: defaults.color,
|
||
backgroundColor: defaults.backgroundColor
|
||
});
|
||
this.loadAppearanceTab();
|
||
this.showNotification('✅ Appearance reset to defaults', 'success');
|
||
};
|
||
|
||
TaskChallengeGame.prototype.previewAppearanceStyle = function() {
|
||
const messages = this.flashMessageManager.getMessages();
|
||
if (messages.length > 0) {
|
||
const sampleMessage = messages[0];
|
||
this.flashMessageManager.previewMessage(sampleMessage);
|
||
} else {
|
||
this.flashMessageManager.previewMessage({ text: "This is a preview of your style settings!" });
|
||
}
|
||
};
|
||
|
||
// Behavior Tab Management
|
||
TaskChallengeGame.prototype.setupBehaviorTabListeners = function() {
|
||
const controls = {
|
||
focusInterruption: document.getElementById('focus-interruption-chance'),
|
||
duration: document.getElementById('display-duration'),
|
||
interval: document.getElementById('interval-delay'),
|
||
variation: document.getElementById('time-variation'),
|
||
eventBased: document.getElementById('event-based-messages'),
|
||
pauseOnHover: document.getElementById('pause-on-hover'),
|
||
testBtn: document.getElementById('test-behavior-btn')
|
||
};
|
||
|
||
if (controls.focusInterruption) {
|
||
controls.focusInterruption.oninput = (e) => {
|
||
const value = parseInt(e.target.value);
|
||
document.getElementById('focus-interruption-display').textContent = `${value}%`;
|
||
this.gameState.focusInterruptionChance = value;
|
||
// Save to localStorage for persistence
|
||
this.dataManager.set('focusInterruptionChance', value);
|
||
console.log(`🧘 Focus interruption chance set to: ${value}%`);
|
||
};
|
||
}
|
||
|
||
if (controls.duration) {
|
||
controls.duration.oninput = (e) => {
|
||
const value = parseInt(e.target.value);
|
||
document.getElementById('duration-display').textContent = `${(value / 1000).toFixed(1)}s`;
|
||
this.updateFlashMessageSetting('displayDuration', value);
|
||
};
|
||
}
|
||
|
||
if (controls.interval) {
|
||
controls.interval.oninput = (e) => {
|
||
const value = parseInt(e.target.value);
|
||
document.getElementById('interval-display').textContent = `${Math.round(value / 1000)}s`;
|
||
this.updateFlashMessageSetting('intervalDelay', value);
|
||
};
|
||
}
|
||
|
||
if (controls.variation) {
|
||
controls.variation.oninput = (e) => {
|
||
const value = parseInt(e.target.value);
|
||
document.getElementById('variation-display').textContent = `±${Math.round(value / 1000)}s`;
|
||
this.updateFlashMessageSetting('timeVariation', value);
|
||
};
|
||
}
|
||
|
||
if (controls.eventBased) {
|
||
controls.eventBased.onchange = (e) => this.updateFlashMessageSetting('eventBasedMessages', e.target.checked);
|
||
}
|
||
|
||
if (controls.pauseOnHover) {
|
||
controls.pauseOnHover.onchange = (e) => this.updateFlashMessageSetting('pauseOnHover', e.target.checked);
|
||
}
|
||
|
||
if (controls.testBtn) {
|
||
controls.testBtn.onclick = () => this.testCurrentBehaviorSettings();
|
||
}
|
||
};
|
||
|
||
TaskChallengeGame.prototype.loadBehaviorTab = function() {
|
||
const config = this.flashMessageManager.getConfig();
|
||
|
||
const focusInterruptionSlider = document.getElementById('focus-interruption-chance');
|
||
const durationSlider = document.getElementById('display-duration');
|
||
const intervalSlider = document.getElementById('interval-delay');
|
||
const variationSlider = document.getElementById('time-variation');
|
||
const eventBasedCheckbox = document.getElementById('event-based-messages');
|
||
const pauseOnHoverCheckbox = document.getElementById('pause-on-hover');
|
||
|
||
// Load focus interruption setting
|
||
if (focusInterruptionSlider) {
|
||
const savedChance = this.dataManager.get('focusInterruptionChance') || 0;
|
||
this.gameState.focusInterruptionChance = savedChance;
|
||
focusInterruptionSlider.value = savedChance;
|
||
document.getElementById('focus-interruption-display').textContent = `${savedChance}%`;
|
||
console.log(`🧘 Focus interruption setting loaded: ${savedChance}%`);
|
||
}
|
||
|
||
if (durationSlider) {
|
||
durationSlider.value = config.displayDuration;
|
||
document.getElementById('duration-display').textContent = `${(config.displayDuration / 1000).toFixed(1)}s`;
|
||
}
|
||
if (intervalSlider) {
|
||
intervalSlider.value = config.intervalDelay;
|
||
document.getElementById('interval-display').textContent = `${Math.round(config.intervalDelay / 1000)}s`;
|
||
}
|
||
if (variationSlider) {
|
||
const variation = config.timeVariation || 5000;
|
||
variationSlider.value = variation;
|
||
document.getElementById('variation-display').textContent = `±${Math.round(variation / 1000)}s`;
|
||
}
|
||
if (eventBasedCheckbox) {
|
||
eventBasedCheckbox.checked = config.eventBasedMessages !== false;
|
||
}
|
||
if (pauseOnHoverCheckbox) {
|
||
pauseOnHoverCheckbox.checked = config.pauseOnHover || false;
|
||
}
|
||
};
|
||
|
||
TaskChallengeGame.prototype.testCurrentBehaviorSettings = function() {
|
||
this.showNotification('🧪 Testing behavior settings with 3 quick messages...', 'info');
|
||
|
||
let count = 0;
|
||
const showTestMessage = () => {
|
||
if (count >= 3) return;
|
||
|
||
const messages = this.flashMessageManager.getMessages();
|
||
if (messages.length > 0) {
|
||
const message = messages[Math.floor(Math.random() * messages.length)];
|
||
this.flashMessageManager.previewMessage(message);
|
||
}
|
||
|
||
count++;
|
||
if (count < 3) {
|
||
setTimeout(showTestMessage, 2000);
|
||
} else {
|
||
setTimeout(() => {
|
||
this.showNotification('✅ Behavior test complete!', 'success');
|
||
}, this.flashMessageManager.getConfig().displayDuration + 500);
|
||
}
|
||
};
|
||
|
||
setTimeout(showTestMessage, 500);
|
||
};
|
||
|
||
// Popup Images Tab Management
|
||
TaskChallengeGame.prototype.setupPopupImagesTabListeners = function() {
|
||
// Enable/disable toggle
|
||
const enabledCheckbox = document.getElementById('popup-images-enabled');
|
||
if (enabledCheckbox) {
|
||
enabledCheckbox.onchange = () => {
|
||
const config = this.popupImageManager.getConfig();
|
||
config.enabled = enabledCheckbox.checked;
|
||
this.popupImageManager.updateConfig(config);
|
||
this.updatePopupImagesInfo();
|
||
};
|
||
}
|
||
|
||
// Image count mode
|
||
const countModeSelect = document.getElementById('popup-count-mode');
|
||
if (countModeSelect) {
|
||
countModeSelect.onchange = () => {
|
||
this.updatePopupCountControls(countModeSelect.value);
|
||
const config = this.popupImageManager.getConfig();
|
||
config.imageCountMode = countModeSelect.value;
|
||
this.popupImageManager.updateConfig(config);
|
||
};
|
||
}
|
||
|
||
// Fixed count slider
|
||
const countSlider = document.getElementById('popup-image-count');
|
||
const countValue = document.getElementById('popup-image-count-value');
|
||
if (countSlider && countValue) {
|
||
countSlider.oninput = () => {
|
||
const count = parseInt(countSlider.value);
|
||
countValue.textContent = count;
|
||
const config = this.popupImageManager.getConfig();
|
||
config.imageCount = count;
|
||
this.popupImageManager.updateConfig(config);
|
||
this.updatePopupCountWarning(count);
|
||
};
|
||
}
|
||
|
||
// Range count inputs
|
||
const minCountInput = document.getElementById('popup-min-count');
|
||
const maxCountInput = document.getElementById('popup-max-count');
|
||
if (minCountInput) {
|
||
minCountInput.onchange = () => {
|
||
const config = this.popupImageManager.getConfig();
|
||
config.minCount = parseInt(minCountInput.value);
|
||
this.popupImageManager.updateConfig(config);
|
||
};
|
||
}
|
||
if (maxCountInput) {
|
||
maxCountInput.onchange = () => {
|
||
const config = this.popupImageManager.getConfig();
|
||
const maxCount = parseInt(maxCountInput.value);
|
||
config.maxCount = maxCount;
|
||
this.popupImageManager.updateConfig(config);
|
||
this.updatePopupCountWarning(maxCount);
|
||
};
|
||
}
|
||
|
||
// Duration mode
|
||
const durationModeSelect = document.getElementById('popup-duration-mode');
|
||
if (durationModeSelect) {
|
||
durationModeSelect.onchange = () => {
|
||
this.updatePopupDurationControls(durationModeSelect.value);
|
||
const config = this.popupImageManager.getConfig();
|
||
config.durationMode = durationModeSelect.value;
|
||
this.popupImageManager.updateConfig(config);
|
||
};
|
||
}
|
||
|
||
// Fixed duration slider
|
||
const durationSlider = document.getElementById('popup-display-duration');
|
||
const durationValue = document.getElementById('popup-display-duration-value');
|
||
if (durationSlider && durationValue) {
|
||
durationSlider.oninput = () => {
|
||
durationValue.textContent = durationSlider.value + 's';
|
||
const config = this.popupImageManager.getConfig();
|
||
config.displayDuration = parseInt(durationSlider.value) * 1000;
|
||
this.popupImageManager.updateConfig(config);
|
||
};
|
||
}
|
||
|
||
// Range duration inputs
|
||
const minDurationInput = document.getElementById('popup-min-duration');
|
||
const maxDurationInput = document.getElementById('popup-max-duration');
|
||
if (minDurationInput) {
|
||
minDurationInput.onchange = () => {
|
||
const config = this.popupImageManager.getConfig();
|
||
config.minDuration = parseInt(minDurationInput.value) * 1000;
|
||
this.popupImageManager.updateConfig(config);
|
||
};
|
||
}
|
||
if (maxDurationInput) {
|
||
maxDurationInput.onchange = () => {
|
||
const config = this.popupImageManager.getConfig();
|
||
config.maxDuration = parseInt(maxDurationInput.value) * 1000;
|
||
this.popupImageManager.updateConfig(config);
|
||
};
|
||
}
|
||
|
||
// Positioning
|
||
const positioningSelect = document.getElementById('popup-positioning');
|
||
if (positioningSelect) {
|
||
positioningSelect.onchange = () => {
|
||
const config = this.popupImageManager.getConfig();
|
||
config.positioning = positioningSelect.value;
|
||
this.popupImageManager.updateConfig(config);
|
||
};
|
||
}
|
||
|
||
// Visual effect checkboxes
|
||
const setupCheckbox = (id, configKey) => {
|
||
const checkbox = document.getElementById(id);
|
||
if (checkbox) {
|
||
checkbox.onchange = () => {
|
||
const config = this.popupImageManager.getConfig();
|
||
config[configKey] = checkbox.checked;
|
||
this.popupImageManager.updateConfig(config);
|
||
};
|
||
}
|
||
};
|
||
|
||
setupCheckbox('popup-allow-overlap', 'allowOverlap');
|
||
setupCheckbox('popup-fade-animation', 'fadeAnimation');
|
||
setupCheckbox('popup-blur-background', 'blurBackground');
|
||
setupCheckbox('popup-show-timer', 'showTimer');
|
||
setupCheckbox('popup-prevent-close', 'preventClose');
|
||
|
||
// Test buttons
|
||
const testSingleBtn = document.getElementById('test-popup-single');
|
||
if (testSingleBtn) {
|
||
testSingleBtn.onclick = () => {
|
||
this.popupImageManager.previewPunishmentPopups(1);
|
||
setTimeout(() => this.updatePopupImagesInfo(), 100);
|
||
};
|
||
}
|
||
|
||
const testMultipleBtn = document.getElementById('test-popup-multiple');
|
||
if (testMultipleBtn) {
|
||
testMultipleBtn.onclick = () => {
|
||
this.popupImageManager.triggerPunishmentPopups();
|
||
setTimeout(() => this.updatePopupImagesInfo(), 100);
|
||
};
|
||
}
|
||
|
||
const clearAllBtn = document.getElementById('clear-all-popups');
|
||
if (clearAllBtn) {
|
||
clearAllBtn.onclick = () => {
|
||
this.popupImageManager.clearAllPopups();
|
||
setTimeout(() => this.updatePopupImagesInfo(), 100);
|
||
};
|
||
}
|
||
|
||
// Size control listeners
|
||
const setupSizeSlider = (elementId, configKey, suffix = '') => {
|
||
const slider = document.getElementById(elementId);
|
||
const valueDisplay = document.getElementById(`${elementId}-value`);
|
||
if (slider && valueDisplay) {
|
||
slider.oninput = () => {
|
||
const value = parseInt(slider.value);
|
||
valueDisplay.textContent = value + suffix;
|
||
const config = this.popupImageManager.getConfig();
|
||
config[configKey] = configKey.includes('viewport') ? value / 100 : value;
|
||
this.popupImageManager.updateConfig(config);
|
||
};
|
||
}
|
||
};
|
||
|
||
const setupSizeInput = (elementId, configKey) => {
|
||
const input = document.getElementById(elementId);
|
||
if (input) {
|
||
input.onchange = () => {
|
||
const value = parseInt(input.value);
|
||
if (!isNaN(value)) {
|
||
const config = this.popupImageManager.getConfig();
|
||
config[configKey] = value;
|
||
this.popupImageManager.updateConfig(config);
|
||
}
|
||
};
|
||
}
|
||
};
|
||
|
||
setupSizeSlider('popup-viewport-width', 'viewportWidthRatio', '%');
|
||
setupSizeSlider('popup-viewport-height', 'viewportHeightRatio', '%');
|
||
setupSizeInput('popup-min-width', 'minWidth');
|
||
setupSizeInput('popup-max-width', 'maxWidth');
|
||
setupSizeInput('popup-min-height', 'minHeight');
|
||
setupSizeInput('popup-max-height', 'maxHeight');
|
||
};
|
||
|
||
TaskChallengeGame.prototype.updatePopupCountControls = function(mode) {
|
||
const fixedDiv = document.getElementById('popup-fixed-count');
|
||
const rangeDiv = document.getElementById('popup-range-count');
|
||
|
||
if (fixedDiv) fixedDiv.style.display = mode === 'fixed' ? 'block' : 'none';
|
||
if (rangeDiv) rangeDiv.style.display = mode === 'range' ? 'block' : 'none';
|
||
};
|
||
|
||
TaskChallengeGame.prototype.updatePopupDurationControls = function(mode) {
|
||
const fixedDiv = document.getElementById('popup-fixed-duration');
|
||
const rangeDiv = document.getElementById('popup-range-duration');
|
||
|
||
if (fixedDiv) fixedDiv.style.display = mode === 'fixed' ? 'block' : 'none';
|
||
if (rangeDiv) rangeDiv.style.display = mode === 'range' ? 'block' : 'none';
|
||
};
|
||
|
||
TaskChallengeGame.prototype.loadPopupImagesSettings = function() {
|
||
const config = this.popupImageManager.getConfig();
|
||
|
||
// Enable/disable
|
||
const enabledCheckbox = document.getElementById('popup-images-enabled');
|
||
if (enabledCheckbox) enabledCheckbox.checked = config.enabled;
|
||
|
||
// Count settings
|
||
const countModeSelect = document.getElementById('popup-count-mode');
|
||
if (countModeSelect) countModeSelect.value = config.imageCountMode;
|
||
|
||
const countSlider = document.getElementById('popup-image-count');
|
||
const countValue = document.getElementById('popup-image-count-value');
|
||
if (countSlider) countSlider.value = config.imageCount;
|
||
if (countValue) countValue.textContent = config.imageCount;
|
||
|
||
const minCountInput = document.getElementById('popup-min-count');
|
||
const maxCountInput = document.getElementById('popup-max-count');
|
||
if (minCountInput) minCountInput.value = config.minCount;
|
||
if (maxCountInput) maxCountInput.value = config.maxCount;
|
||
|
||
// Duration settings
|
||
const durationModeSelect = document.getElementById('popup-duration-mode');
|
||
if (durationModeSelect) durationModeSelect.value = config.durationMode;
|
||
|
||
const durationSlider = document.getElementById('popup-display-duration');
|
||
const durationValue = document.getElementById('popup-display-duration-value');
|
||
if (durationSlider) durationSlider.value = config.displayDuration / 1000;
|
||
if (durationValue) durationValue.textContent = (config.displayDuration / 1000) + 's';
|
||
|
||
const minDurationInput = document.getElementById('popup-min-duration');
|
||
const maxDurationInput = document.getElementById('popup-max-duration');
|
||
if (minDurationInput) minDurationInput.value = config.minDuration / 1000;
|
||
if (maxDurationInput) maxDurationInput.value = config.maxDuration / 1000;
|
||
|
||
// Positioning
|
||
const positioningSelect = document.getElementById('popup-positioning');
|
||
if (positioningSelect) positioningSelect.value = config.positioning;
|
||
|
||
// Visual effects
|
||
const checkboxes = {
|
||
'popup-allow-overlap': config.allowOverlap,
|
||
'popup-fade-animation': config.fadeAnimation,
|
||
'popup-blur-background': config.blurBackground,
|
||
'popup-show-timer': config.showTimer,
|
||
'popup-prevent-close': config.preventClose
|
||
};
|
||
|
||
Object.entries(checkboxes).forEach(([id, value]) => {
|
||
const checkbox = document.getElementById(id);
|
||
if (checkbox) checkbox.checked = value;
|
||
});
|
||
|
||
// Size settings
|
||
const viewportWidthSlider = document.getElementById('popup-viewport-width');
|
||
const viewportWidthValue = document.getElementById('popup-viewport-width-value');
|
||
if (viewportWidthSlider) viewportWidthSlider.value = (config.viewportWidthRatio || 0.35) * 100;
|
||
if (viewportWidthValue) viewportWidthValue.textContent = Math.round((config.viewportWidthRatio || 0.35) * 100) + '%';
|
||
|
||
const viewportHeightSlider = document.getElementById('popup-viewport-height');
|
||
const viewportHeightValue = document.getElementById('popup-viewport-height-value');
|
||
if (viewportHeightSlider) viewportHeightSlider.value = (config.viewportHeightRatio || 0.4) * 100;
|
||
if (viewportHeightValue) viewportHeightValue.textContent = Math.round((config.viewportHeightRatio || 0.4) * 100) + '%';
|
||
|
||
const sizeInputs = {
|
||
'popup-min-width': config.minWidth || 200,
|
||
'popup-max-width': config.maxWidth || 500,
|
||
'popup-min-height': config.minHeight || 150,
|
||
'popup-max-height': config.maxHeight || 400
|
||
};
|
||
|
||
Object.entries(sizeInputs).forEach(([id, value]) => {
|
||
const input = document.getElementById(id);
|
||
if (input) input.value = value;
|
||
});
|
||
|
||
// Update control visibility
|
||
this.updatePopupCountControls(config.imageCountMode);
|
||
this.updatePopupDurationControls(config.durationMode);
|
||
|
||
// Update info display and warnings
|
||
this.updatePopupImagesInfo();
|
||
|
||
// Check for high count warnings
|
||
const currentCount = config.imageCountMode === 'fixed' ? config.imageCount : config.maxCount;
|
||
this.updatePopupCountWarning(currentCount);
|
||
};
|
||
|
||
TaskChallengeGame.prototype.updatePopupImagesInfo = function() {
|
||
const availableCountEl = document.getElementById('available-images-count');
|
||
const activeCountEl = document.getElementById('active-popups-count');
|
||
|
||
if (availableCountEl) {
|
||
const availableImages = this.popupImageManager.getAvailableImages();
|
||
availableCountEl.textContent = availableImages.length;
|
||
}
|
||
|
||
if (activeCountEl) {
|
||
const activeCount = this.popupImageManager.getActiveCount();
|
||
activeCountEl.textContent = activeCount;
|
||
}
|
||
};
|
||
|
||
TaskChallengeGame.prototype.updatePopupCountWarning = function(count) {
|
||
const warningEl = document.getElementById('popup-warning');
|
||
if (warningEl) {
|
||
if (count > 20) {
|
||
warningEl.style.display = 'block';
|
||
} else {
|
||
warningEl.style.display = 'none';
|
||
}
|
||
}
|
||
};
|
||
|
||
// Import/Export Tab Management
|
||
TaskChallengeGame.prototype.setupImportExportTabListeners = function() {
|
||
const exportAllBtn = document.getElementById('export-all-messages-btn');
|
||
const exportEnabledBtn = document.getElementById('export-enabled-messages-btn');
|
||
const exportCustomBtn = document.getElementById('export-custom-messages-btn');
|
||
const importBtn = document.getElementById('import-messages-btn');
|
||
const importFile = document.getElementById('import-messages-file');
|
||
const resetDefaultsBtn = document.getElementById('reset-to-defaults-btn');
|
||
const clearAllBtn = document.getElementById('clear-all-messages-btn');
|
||
|
||
if (exportAllBtn) {
|
||
exportAllBtn.onclick = () => this.exportMessages('all');
|
||
}
|
||
if (exportEnabledBtn) {
|
||
exportEnabledBtn.onclick = () => this.exportMessages('enabled');
|
||
}
|
||
if (exportCustomBtn) {
|
||
exportCustomBtn.onclick = () => this.exportMessages('custom');
|
||
}
|
||
if (importBtn) {
|
||
importBtn.onclick = () => importFile.click();
|
||
}
|
||
if (importFile) {
|
||
importFile.onchange = (e) => this.handleMessageImport(e);
|
||
}
|
||
if (resetDefaultsBtn) {
|
||
resetDefaultsBtn.onclick = () => this.resetMessagesToDefaults();
|
||
}
|
||
if (clearAllBtn) {
|
||
clearAllBtn.onclick = () => this.clearAllMessages();
|
||
}
|
||
};
|
||
|
||
TaskChallengeGame.prototype.loadImportExportTab = function() {
|
||
// No specific loading needed for this tab
|
||
};
|
||
|
||
TaskChallengeGame.prototype.exportMessages = function(type) {
|
||
let includeDisabled = true;
|
||
let customOnly = false;
|
||
let filename = 'flash_messages_all.json';
|
||
|
||
switch(type) {
|
||
case 'enabled':
|
||
includeDisabled = false;
|
||
filename = 'flash_messages_enabled.json';
|
||
break;
|
||
case 'custom':
|
||
customOnly = true;
|
||
filename = 'flash_messages_custom.json';
|
||
break;
|
||
}
|
||
|
||
const exportData = this.flashMessageManager.exportMessages(includeDisabled, customOnly);
|
||
this.downloadFile(exportData, filename, 'application/json');
|
||
this.showNotification(`✅ Messages exported to ${filename}`, 'success');
|
||
};
|
||
|
||
TaskChallengeGame.prototype.handleMessageImport = function(event) {
|
||
const file = event.target.files[0];
|
||
if (!file) return;
|
||
|
||
const reader = new FileReader();
|
||
reader.onload = (e) => {
|
||
try {
|
||
const importMode = document.querySelector('input[name="importMode"]:checked').value;
|
||
const result = this.flashMessageManager.importMessages(e.target.result, importMode);
|
||
|
||
if (result.success) {
|
||
this.showNotification(`✅ Successfully imported ${result.imported} messages (${result.total} total)`, 'success');
|
||
if (this.getCurrentAnnoyanceTab() === 'messages') {
|
||
this.refreshMessageList();
|
||
}
|
||
} else {
|
||
this.showNotification(`❌ Import failed: ${result.error}`, 'error');
|
||
}
|
||
} catch (error) {
|
||
this.showNotification('❌ Invalid file format', 'error');
|
||
}
|
||
};
|
||
reader.readAsText(file);
|
||
|
||
// Clear the file input
|
||
event.target.value = '';
|
||
};
|
||
|
||
TaskChallengeGame.prototype.resetMessagesToDefaults = function() {
|
||
if (confirm('Reset to default messages? This will remove all custom messages and cannot be undone.')) {
|
||
const result = this.flashMessageManager.resetToDefaults();
|
||
this.showNotification(`✅ Reset to ${result.messages} default messages`, 'success');
|
||
if (this.getCurrentAnnoyanceTab() === 'messages') {
|
||
this.refreshMessageList();
|
||
}
|
||
}
|
||
};
|
||
|
||
TaskChallengeGame.prototype.clearAllMessages = function() {
|
||
if (confirm('Clear ALL messages? This will remove every message and cannot be undone. You will need to import messages to use the flash message system.')) {
|
||
this.flashMessageManager.updateMessages([]);
|
||
this.showNotification('⚠️ All messages cleared', 'warning');
|
||
if (this.getCurrentAnnoyanceTab() === 'messages') {
|
||
this.refreshMessageList();
|
||
}
|
||
}
|
||
};
|
||
|
||
// Utility Methods
|
||
TaskChallengeGame.prototype.updateFlashMessageSetting = function(setting, value) {
|
||
const currentConfig = this.flashMessageManager.getConfig();
|
||
const newConfig = { ...currentConfig, [setting]: value };
|
||
this.flashMessageManager.updateConfig(newConfig);
|
||
console.log(`Updated flash message setting: ${setting} = ${value}`);
|
||
};
|
||
|
||
TaskChallengeGame.prototype.saveAllAnnoyanceSettings = function() {
|
||
this.showNotification('✅ All annoyance settings saved!', 'success');
|
||
};
|
||
|
||
TaskChallengeGame.prototype.loadAnnoyanceSettings = function() {
|
||
// This method is called when the annoyance screen first loads
|
||
// Individual tabs will load their specific settings when shown
|
||
};
|
||
|
||
TaskChallengeGame.prototype.getCurrentAnnoyanceTab = function() {
|
||
const activeTab = document.querySelector('.annoyance-tab.active');
|
||
return activeTab ? activeTab.id.replace('-tab', '') : 'messages';
|
||
};
|
||
|
||
TaskChallengeGame.prototype.downloadFile = function(content, filename, mimeType) {
|
||
const blob = new Blob([content], { type: mimeType });
|
||
const url = URL.createObjectURL(blob);
|
||
const link = document.createElement('a');
|
||
link.href = url;
|
||
link.download = filename;
|
||
document.body.appendChild(link);
|
||
link.click();
|
||
document.body.removeChild(link);
|
||
URL.revokeObjectURL(url);
|
||
};
|
||
|
||
TaskChallengeGame.prototype.hexToRgba = function(hex, alpha) {
|
||
const r = parseInt(hex.slice(1, 3), 16);
|
||
const g = parseInt(hex.slice(3, 5), 16);
|
||
const b = parseInt(hex.slice(5, 7), 16);
|
||
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
||
};
|
||
|
||
TaskChallengeGame.prototype.rgbToHex = function(r, g, b) {
|
||
return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
|
||
};
|
||
|
||
// ========================================
|
||
// AI Tasks Tab Management
|
||
// ========================================
|
||
|
||
TaskChallengeGame.prototype.setupAITasksTabListeners = function() {
|
||
// Test Connection Button
|
||
const testConnectionBtn = document.getElementById('test-connection');
|
||
if (testConnectionBtn) {
|
||
testConnectionBtn.onclick = async () => {
|
||
const btn = testConnectionBtn;
|
||
const originalText = btn.textContent;
|
||
btn.textContent = 'Testing...';
|
||
btn.disabled = true;
|
||
|
||
try {
|
||
if (this.aiTaskManager) {
|
||
const isConnected = await this.aiTaskManager.testConnection();
|
||
this.updateConnectionStatus();
|
||
if (isConnected) {
|
||
this.flashMessageManager.showMessage('✅ Connected to Ollama successfully!');
|
||
} else {
|
||
this.flashMessageManager.showMessage('❌ Cannot connect to Ollama. Check if it\'s running.');
|
||
}
|
||
} else {
|
||
this.flashMessageManager.showMessage('⚠️ AI Manager not initialized');
|
||
}
|
||
} catch (error) {
|
||
this.flashMessageManager.showMessage('❌ Connection test failed: ' + error.message);
|
||
} finally {
|
||
btn.textContent = originalText;
|
||
btn.disabled = false;
|
||
}
|
||
};
|
||
}
|
||
|
||
// Generate Test Task Button
|
||
const generateTestBtn = document.getElementById('generate-test-task');
|
||
if (generateTestBtn) {
|
||
generateTestBtn.onclick = async () => {
|
||
const btn = generateTestBtn;
|
||
const preview = document.getElementById('test-task-output');
|
||
const originalText = btn.textContent;
|
||
|
||
if (!preview) {
|
||
console.error('Test task output element not found');
|
||
this.flashMessageManager.showMessage('❌ UI element not found');
|
||
return;
|
||
}
|
||
|
||
btn.textContent = 'Generating...';
|
||
btn.disabled = true;
|
||
preview.className = 'task-preview generating';
|
||
preview.textContent = 'AI is creating a personalized edging task...';
|
||
|
||
try {
|
||
if (this.aiTaskManager) {
|
||
const task = await this.aiTaskManager.generateEdgingTask();
|
||
preview.className = 'task-preview';
|
||
preview.textContent = task.instruction || task.description || task;
|
||
this.flashMessageManager.showMessage('🎯 Test task generated successfully!');
|
||
} else {
|
||
throw new Error('AI Manager not initialized');
|
||
}
|
||
} catch (error) {
|
||
preview.className = 'task-preview error';
|
||
preview.textContent = `Error generating task: ${error.message}`;
|
||
this.flashMessageManager.showMessage('❌ Failed to generate test task');
|
||
} finally {
|
||
btn.textContent = originalText;
|
||
btn.disabled = false;
|
||
}
|
||
};
|
||
}
|
||
|
||
// Model Selection Change
|
||
const modelSelect = document.getElementById('ai-model');
|
||
if (modelSelect) {
|
||
modelSelect.onchange = (e) => {
|
||
const model = e.target.value;
|
||
if (this.aiTaskManager) {
|
||
this.aiTaskManager.updateSettings({ model: model });
|
||
this.flashMessageManager.showMessage(`🔄 Switched to model: ${model}`);
|
||
}
|
||
};
|
||
}
|
||
|
||
// Temperature Slider
|
||
const tempSlider = document.getElementById('ai-temperature');
|
||
const tempValue = document.getElementById('temp-value');
|
||
if (tempSlider && tempValue) {
|
||
tempSlider.oninput = (e) => {
|
||
const temperature = parseFloat(e.target.value);
|
||
tempValue.textContent = temperature.toFixed(1);
|
||
if (this.aiTaskManager) {
|
||
this.aiTaskManager.updateSettings({ temperature: temperature });
|
||
}
|
||
};
|
||
}
|
||
|
||
// Max Tokens Input
|
||
const maxTokensInput = document.getElementById('ai-max-tokens');
|
||
if (maxTokensInput) {
|
||
maxTokensInput.onchange = (e) => {
|
||
const maxTokens = parseInt(e.target.value);
|
||
if (this.aiTaskManager) {
|
||
this.aiTaskManager.updateSettings({ maxTokens: maxTokens });
|
||
}
|
||
};
|
||
}
|
||
|
||
// Difficulty Level Change
|
||
const difficultySelect = document.getElementById('task-difficulty');
|
||
if (difficultySelect) {
|
||
difficultySelect.onchange = (e) => {
|
||
const difficulty = e.target.value;
|
||
if (this.aiTaskManager) {
|
||
this.aiTaskManager.updateSettings({ difficulty: difficulty });
|
||
this.flashMessageManager.showMessage(`🎯 Difficulty set to: ${difficulty}`);
|
||
}
|
||
};
|
||
}
|
||
|
||
// Personal Preferences Textarea
|
||
const prefsTextarea = document.getElementById('personal-preferences');
|
||
if (prefsTextarea) {
|
||
prefsTextarea.oninput = this.debounce((e) => {
|
||
const preferences = e.target.value;
|
||
if (this.aiTaskManager) {
|
||
this.aiTaskManager.updateSettings({ personalPreferences: preferences });
|
||
}
|
||
}, 500);
|
||
}
|
||
|
||
// Enable AI Toggle
|
||
const aiToggle = document.getElementById('enable-ai');
|
||
if (aiToggle) {
|
||
aiToggle.onchange = (e) => {
|
||
const enabled = e.target.checked;
|
||
if (this.aiTaskManager) {
|
||
this.aiTaskManager.updateSettings({ enabled: enabled });
|
||
this.flashMessageManager.showMessage(`🤖 AI Tasks ${enabled ? 'enabled' : 'disabled'}`);
|
||
|
||
// Update UI based on toggle
|
||
const configSection = document.querySelector('.ai-config');
|
||
if (configSection) {
|
||
configSection.style.opacity = enabled ? '1' : '0.6';
|
||
}
|
||
}
|
||
};
|
||
}
|
||
|
||
// Auto-generate Toggle
|
||
const autoGenToggle = document.getElementById('auto-generate');
|
||
if (autoGenToggle) {
|
||
autoGenToggle.onchange = (e) => {
|
||
const autoGenerate = e.target.checked;
|
||
if (this.aiTaskManager) {
|
||
this.aiTaskManager.updateSettings({ autoGenerate: autoGenerate });
|
||
this.flashMessageManager.showMessage(`🔄 Auto-generate ${autoGenerate ? 'enabled' : 'disabled'}`);
|
||
}
|
||
};
|
||
}
|
||
};
|
||
|
||
TaskChallengeGame.prototype.loadAITasksTab = function() {
|
||
if (!this.aiTaskManager) return;
|
||
|
||
const settings = this.aiTaskManager.getSettings();
|
||
|
||
// Load model selection
|
||
const modelSelect = document.getElementById('ai-model');
|
||
if (modelSelect) {
|
||
modelSelect.value = settings.model || 'llama3.2';
|
||
}
|
||
|
||
// Load temperature
|
||
const tempSlider = document.getElementById('ai-temperature');
|
||
const tempValue = document.getElementById('temp-value');
|
||
if (tempSlider && tempValue) {
|
||
tempSlider.value = settings.temperature || 0.7;
|
||
tempValue.textContent = (settings.temperature || 0.7).toFixed(1);
|
||
}
|
||
|
||
// Load max tokens
|
||
const maxTokensInput = document.getElementById('ai-max-tokens');
|
||
if (maxTokensInput) {
|
||
maxTokensInput.value = settings.maxTokens || 200;
|
||
}
|
||
|
||
// Load difficulty
|
||
const difficultySelect = document.getElementById('task-difficulty');
|
||
if (difficultySelect) {
|
||
difficultySelect.value = settings.difficulty || 'medium';
|
||
}
|
||
|
||
// Load personal preferences
|
||
const prefsTextarea = document.getElementById('personal-preferences');
|
||
if (prefsTextarea) {
|
||
prefsTextarea.value = settings.personalPreferences || '';
|
||
}
|
||
|
||
// Load toggles
|
||
const aiToggle = document.getElementById('enable-ai');
|
||
if (aiToggle) {
|
||
aiToggle.checked = settings.enabled !== false;
|
||
}
|
||
|
||
const autoGenToggle = document.getElementById('auto-generate');
|
||
if (autoGenToggle) {
|
||
autoGenToggle.checked = settings.autoGenerate === true;
|
||
}
|
||
|
||
// Update UI state
|
||
const configSection = document.querySelector('.ai-config');
|
||
if (configSection) {
|
||
configSection.style.opacity = settings.enabled !== false ? '1' : '0.6';
|
||
}
|
||
|
||
// Update connection status (async)
|
||
this.updateConnectionStatus().catch(error => {
|
||
console.error('Failed to update connection status:', error);
|
||
});
|
||
};
|
||
|
||
TaskChallengeGame.prototype.updateConnectionStatus = async function() {
|
||
const statusValue = document.getElementById('connection-status');
|
||
const modelStatus = document.getElementById('model-status');
|
||
|
||
if (!statusValue || !this.aiTaskManager) {
|
||
if (statusValue) {
|
||
statusValue.textContent = 'Not Available';
|
||
statusValue.className = 'status-value disconnected';
|
||
}
|
||
if (modelStatus) {
|
||
modelStatus.textContent = 'N/A';
|
||
modelStatus.className = 'status-value';
|
||
}
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// Set loading state
|
||
statusValue.textContent = 'Checking...';
|
||
statusValue.className = 'status-value';
|
||
|
||
// Force a fresh connection test
|
||
const isConnected = await this.aiTaskManager.testConnection();
|
||
const settings = this.aiTaskManager.getSettings();
|
||
|
||
if (isConnected) {
|
||
statusValue.textContent = 'Connected';
|
||
statusValue.className = 'status-value connected';
|
||
if (modelStatus) {
|
||
modelStatus.textContent = settings.model || this.aiTaskManager.currentModel || 'wizardlm-uncensored:13b';
|
||
modelStatus.className = 'status-value';
|
||
}
|
||
} else {
|
||
statusValue.textContent = 'Disconnected';
|
||
statusValue.className = 'status-value disconnected';
|
||
if (modelStatus) {
|
||
modelStatus.textContent = 'N/A';
|
||
modelStatus.className = 'status-value';
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('Error updating connection status:', error);
|
||
statusValue.textContent = 'Error';
|
||
statusValue.className = 'status-value disconnected';
|
||
if (modelStatus) {
|
||
modelStatus.textContent = 'N/A';
|
||
modelStatus.className = 'status-value';
|
||
}
|
||
}
|
||
};
|
||
|
||
// Utility function for debouncing input
|
||
TaskChallengeGame.prototype.debounce = function(func, wait) {
|
||
let timeout;
|
||
return function executedFunction(...args) {
|
||
const later = () => {
|
||
clearTimeout(timeout);
|
||
func.apply(this, args);
|
||
};
|
||
clearTimeout(timeout);
|
||
timeout = setTimeout(later, wait);
|
||
};
|
||
};
|
||
|
||
// Initialize game when page loads
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
console.log('🎮 Starting game initialization...');
|
||
|
||
// Show loading overlay immediately
|
||
const overlay = document.getElementById('loading-overlay');
|
||
if (overlay) {
|
||
overlay.style.display = 'flex';
|
||
overlay.classList.add('visible');
|
||
}
|
||
|
||
// Disable all interactive elements during initialization
|
||
const disableElements = () => {
|
||
const buttons = document.querySelectorAll('button');
|
||
const inputs = document.querySelectorAll('input, select, textarea');
|
||
const clickables = document.querySelectorAll('[onclick], .clickable, .tab-btn, .choice-btn');
|
||
|
||
[...buttons, ...inputs, ...clickables].forEach(element => {
|
||
element.disabled = true;
|
||
element.style.pointerEvents = 'none';
|
||
element.classList.add('loading-disabled');
|
||
});
|
||
};
|
||
|
||
// Re-enable all interactive elements after initialization
|
||
const enableElements = () => {
|
||
const buttons = document.querySelectorAll('button');
|
||
const inputs = document.querySelectorAll('input, select, textarea');
|
||
const clickables = document.querySelectorAll('[onclick], .clickable, .tab-btn, .choice-btn');
|
||
|
||
[...buttons, ...inputs, ...clickables].forEach(element => {
|
||
element.disabled = false;
|
||
element.style.pointerEvents = '';
|
||
element.classList.remove('loading-disabled');
|
||
});
|
||
};
|
||
|
||
disableElements();
|
||
|
||
// Validate dependencies before creating game
|
||
if (!validateDependencies()) {
|
||
console.error('❌ Critical dependencies missing, cannot start game');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
window.game = new TaskChallengeGame();
|
||
console.log('✅ Game instance created successfully');
|
||
|
||
// Wait for initialization to complete
|
||
const checkInitialization = () => {
|
||
if (window.game && window.game.isInitialized) {
|
||
enableElements();
|
||
console.log('✅ Game fully initialized and ready for interaction');
|
||
|
||
// Dispatch game ready event for other systems
|
||
window.dispatchEvent(new CustomEvent('gameReady', {
|
||
detail: { game: window.game }
|
||
}));
|
||
} else {
|
||
setTimeout(checkInitialization, 100);
|
||
}
|
||
};
|
||
checkInitialization();
|
||
|
||
} catch (error) {
|
||
console.error('❌ Failed to create game instance:', error);
|
||
enableElements(); // Re-enable on error
|
||
return;
|
||
}
|
||
|
||
// Initialize game mode manager after DOM is fully ready
|
||
setTimeout(() => {
|
||
if (window.gameModeManager) {
|
||
console.log('🎮 Initializing GameModeManager...');
|
||
window.gameModeManager.init();
|
||
console.log('🎮 Game Mode Manager initialization complete');
|
||
} else {
|
||
console.error('❌ GameModeManager not found on window');
|
||
}
|
||
}, 1000); // Give more time for DOM to be ready
|
||
}); |