training-academy/game.js

3014 lines
117 KiB
JavaScript

// Game state management
class TaskChallengeGame {
constructor() {
// Initialize data management system first
this.dataManager = new DataManager();
// Initialize desktop features early
this.initDesktopFeatures();
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: []
};
this.timerInterval = null;
this.imageDiscoveryComplete = false;
this.musicManager = new MusicManager(this.dataManager);
this.initializeEventListeners();
this.setupKeyboardShortcuts();
this.initializeCustomTasks();
this.discoverImages().then(() => {
this.showScreen('start-screen');
});
// Check for auto-resume after initialization
this.checkAutoResume();
}
async initDesktopFeatures() {
// Initialize desktop file manager
if (typeof DesktopFileManager !== 'undefined') {
this.fileManager = new DesktopFileManager(this.dataManager);
window.desktopFileManager = this.fileManager;
// Auto-scan directories on startup
setTimeout(async () => {
if (this.fileManager && this.fileManager.isElectron) {
console.log('🔍 Auto-scanning directories on startup...');
await this.fileManager.scanAllDirectories();
}
}, 1000); // Wait 1 second for initialization to complete
}
// Check if we're in Electron and update UI accordingly
setTimeout(() => {
const isElectron = window.electronAPI !== undefined;
if (isElectron) {
document.body.classList.add('desktop-mode');
// Show desktop-specific features
document.querySelectorAll('.desktop-only').forEach(el => el.style.display = '');
document.querySelectorAll('.desktop-feature').forEach(el => el.style.display = '');
document.querySelectorAll('.web-feature').forEach(el => el.style.display = 'none');
console.log('🖥️ Desktop mode activated');
} else {
document.body.classList.add('web-mode');
// Hide desktop-only features
document.querySelectorAll('.desktop-only').forEach(el => el.style.display = 'none');
document.querySelectorAll('.desktop-feature').forEach(el => el.style.display = 'none');
document.querySelectorAll('.web-feature').forEach(el => el.style.display = '');
console.log('🌐 Web mode activated');
}
}, 100);
}
initializeCustomTasks() {
// Load custom tasks from localStorage or use defaults
const savedMainTasks = localStorage.getItem('customMainTasks');
const savedConsequenceTasks = localStorage.getItem('customConsequenceTasks');
if (savedMainTasks) {
gameData.mainTasks = JSON.parse(savedMainTasks);
}
if (savedConsequenceTasks) {
gameData.consequenceTasks = JSON.parse(savedConsequenceTasks);
}
console.log(`Loaded ${gameData.mainTasks.length} main tasks and ${gameData.consequenceTasks.length} consequence tasks`);
}
async discoverImages() {
try {
console.log('Loading embedded image manifest...');
// Get the current embedded manifest (empty by default)
let manifest = this.getEmbeddedManifest();
// Only scan if the user has explicitly requested it via the scan button
// No automatic scanning on startup
console.log('Skipping automatic scan - user must manually scan or upload images');
// Build full paths and verify images exist
gameData.discoveredTaskImages = await this.verifyImagesFromManifest(manifest.tasks, 'images/tasks/');
gameData.discoveredConsequenceImages = await this.verifyImagesFromManifest(manifest.consequences, 'images/consequences/');
console.log(`Task images found: ${gameData.discoveredTaskImages.length}`);
console.log(`Consequence images found: ${gameData.discoveredConsequenceImages.length}`);
} catch (error) {
console.log('Image manifest loading failed:', error);
// Don't fallback to pattern detection - keep empty
gameData.discoveredTaskImages = [];
gameData.discoveredConsequenceImages = [];
}
this.imageDiscoveryComplete = true;
console.log('Image discovery completed');
}
getEmbeddedManifest() {
// Empty manifest - users must upload or scan for their own images
return {
"tasks": [],
"consequences": []
};
}
async updateManifestWithNewImages(manifest) {
console.log('Scanning for new images...');
// Get cached manifest or current one
let cachedManifest = this.dataManager.get('cachedManifest');
if (!cachedManifest) {
cachedManifest = {...manifest}; // Copy original
}
// Scan for new images using pattern detection
const newTaskImages = await this.scanDirectoryForNewImages('images/tasks/', cachedManifest.tasks);
const newConsequenceImages = await this.scanDirectoryForNewImages('images/consequences/', cachedManifest.consequences);
// Add any new images found
if (newTaskImages.length > 0) {
cachedManifest.tasks = [...new Set([...cachedManifest.tasks, ...newTaskImages])]; // Remove duplicates
console.log(`Found ${newTaskImages.length} new task images:`, newTaskImages);
}
if (newConsequenceImages.length > 0) {
cachedManifest.consequences = [...new Set([...cachedManifest.consequences, ...newConsequenceImages])]; // Remove duplicates
console.log(`Found ${newConsequenceImages.length} new consequence images:`, newConsequenceImages);
}
// Save updated manifest to localStorage
this.dataManager.set('cachedManifest', cachedManifest);
return cachedManifest;
}
async scanDirectoryForNewImages(directory, knownImages) {
const newImages = [];
// Comprehensive pattern scan for new images
const patterns = [
// Numbers 1-100
...Array.from({length: 100}, (_, i) => (i + 1).toString()),
// Image patterns
...Array.from({length: 50}, (_, i) => `image${i + 1}`),
...Array.from({length: 50}, (_, i) => `img${i + 1}`),
...Array.from({length: 50}, (_, i) => `photo${i + 1}`),
...Array.from({length: 50}, (_, i) => `pic${i + 1}`),
// Date-based patterns (common camera formats)
...Array.from({length: 50}, (_, i) => `IMG_${20210101 + i}`),
...Array.from({length: 50}, (_, i) => `DSC${String(i + 1).padStart(4, '0')}`),
// Letters a-z
...Array.from({length: 26}, (_, i) => String.fromCharCode(97 + i)),
// Random common names
'new', 'test', 'sample', 'demo', 'example', 'temp', 'screenshot', 'capture',
// UUID-like patterns
...Array.from({length: 20}, (_, i) => `img_${Date.now() + i}`)
];
console.log(`Scanning ${directory} for new images (${patterns.length} patterns)...`);
// Test each pattern with each supported format
let checkedCount = 0;
const maxChecks = 500; // Reasonable limit to prevent hanging
for (const pattern of patterns) {
if (checkedCount >= maxChecks) {
console.log(`Reached maximum scan limit (${maxChecks} checks)`);
break;
}
for (const format of gameData.supportedImageFormats) {
checkedCount++;
const filename = `${pattern}${format}`;
// Skip if we already know about this image
if (knownImages.includes(filename)) {
continue;
}
const imagePath = `${directory}${filename}`;
const exists = await this.checkImageExists(imagePath);
if (exists) {
newImages.push(filename);
console.log('✓ Found NEW image:', filename);
}
}
}
console.log(`Scanned ${checkedCount} possibilities, found ${newImages.length} new images`);
return newImages;
}
async verifyImagesFromManifest(imageList, directory) {
const validImages = [];
for (const imageName of imageList) {
const imagePath = `${directory}${imageName}`;
const exists = await this.checkImageExists(imagePath);
if (exists) {
validImages.push(imagePath);
console.log('✓ Verified image:', imagePath);
} else {
console.log('✗ Missing image:', imagePath);
}
}
return validImages;
}
async fallbackImageDiscovery() {
// Fallback to simplified pattern detection if manifest fails
console.log('Using fallback pattern detection...');
const commonPatterns = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10'];
gameData.discoveredTaskImages = await this.findImagesWithPatterns('images/tasks/', commonPatterns);
gameData.discoveredConsequenceImages = await this.findImagesWithPatterns('images/consequences/', commonPatterns);
// If still no images found, show helpful message but don't use placeholders
if (gameData.discoveredTaskImages.length === 0 && gameData.discoveredConsequenceImages.length === 0) {
console.log('No images found. Users will need to upload or scan for images.');
gameData.discoveredTaskImages = [];
gameData.discoveredConsequenceImages = [];
}
}
async findImagesWithPatterns(directory, patterns) {
const foundImages = [];
for (const pattern of patterns) {
for (const format of gameData.supportedImageFormats) {
const imagePath = `${directory}${pattern}${format}`;
const exists = await this.checkImageExists(imagePath);
if (exists) {
foundImages.push(imagePath);
console.log('✓ Found image:', imagePath);
}
}
}
return foundImages;
}
setupPlaceholderImages() {
gameData.discoveredTaskImages = [this.createPlaceholderImage('Task Image')];
gameData.discoveredConsequenceImages = [this.createPlaceholderImage('Consequence Image')];
}
checkImageExists(imagePath) {
return new Promise((resolve) => {
const img = new Image();
img.onload = () => {
console.log(`✓ Found image: ${imagePath}`);
resolve(true);
};
img.onerror = () => {
console.log(`✗ Missing image: ${imagePath}`);
resolve(false);
};
img.src = imagePath;
// Timeout after 1 second (faster discovery)
setTimeout(() => {
console.log(`⏰ Timeout for image: ${imagePath}`);
resolve(false);
}, 1000);
});
}
createPlaceholderImage(label = 'Task Image') {
const encodedLabel = encodeURIComponent(label);
return `data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjZjBmMGYwIiBzdHJva2U9IiNjY2MiIHN0cm9rZS13aWR0aD0iMiIvPjx0ZXh0IHg9IjUwJSIgeT0iNDAlIiBmb250LXNpemU9IjE2IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBkeT0iLjNlbSIgZmlsbD0iIzY2NiI+JHtsYWJlbH08L3RleHQ+PHRleHQgeD0iNTAlIiB5PSI2MCUiIGZvbnQtc2l6ZT0iMTIiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGR5PSIuM2VtIiBmaWxsPSIjOTk5Ij5BZGQgaW1hZ2VzIHRvIGZvbGRlcjwvdGV4dD48L3N2Zz4=`;
}
initializeEventListeners() {
// Screen navigation
document.getElementById('start-btn').addEventListener('click', () => this.startGame());
document.getElementById('resume-btn').addEventListener('click', () => this.resumeGame());
document.getElementById('quit-btn').addEventListener('click', () => this.quitGame());
document.getElementById('play-again-btn').addEventListener('click', () => this.resetGame());
// Game actions
document.getElementById('complete-btn').addEventListener('click', () => this.completeTask());
document.getElementById('skip-btn').addEventListener('click', () => this.skipTask());
document.getElementById('mercy-skip-btn').addEventListener('click', () => this.mercySkip());
document.getElementById('pause-btn').addEventListener('click', () => this.pauseGame());
// Theme selector
document.getElementById('theme-dropdown').addEventListener('change', (e) => this.changeTheme(e.target.value));
// Music controls
document.getElementById('music-toggle').addEventListener('click', () => this.toggleMusic());
document.getElementById('loop-btn').addEventListener('click', () => this.toggleLoop());
document.getElementById('shuffle-btn').addEventListener('click', () => this.toggleShuffle());
document.getElementById('track-selector').addEventListener('change', (e) => this.changeTrack(parseInt(e.target.value)));
document.getElementById('volume-slider').addEventListener('input', (e) => this.changeVolume(parseInt(e.target.value)));
// Task management
document.getElementById('manage-tasks-btn').addEventListener('click', () => this.showTaskManagement());
document.getElementById('back-to-start-btn').addEventListener('click', () => this.showScreen('start-screen'));
document.getElementById('add-task-btn').addEventListener('click', () => this.addNewTask());
document.getElementById('reset-tasks-btn').addEventListener('click', () => this.resetToDefaultTasks());
document.getElementById('main-tasks-tab').addEventListener('click', () => this.showTaskTab('main'));
document.getElementById('consequence-tasks-tab').addEventListener('click', () => this.showTaskTab('consequence'));
document.getElementById('new-task-type').addEventListener('change', () => this.toggleDifficultyDropdown());
// Data management
document.getElementById('export-btn').addEventListener('click', () => this.exportData());
document.getElementById('import-btn').addEventListener('click', () => this.importData());
document.getElementById('import-file').addEventListener('change', (e) => this.handleFileImport(e));
document.getElementById('stats-btn').addEventListener('click', () => this.showStats());
document.getElementById('help-btn').addEventListener('click', () => this.showHelp());
document.getElementById('close-stats').addEventListener('click', () => this.hideStats());
document.getElementById('close-help').addEventListener('click', () => this.hideHelp());
document.getElementById('reset-stats-btn').addEventListener('click', () => this.resetStats());
document.getElementById('export-stats-btn').addEventListener('click', () => this.exportStatsOnly());
// Image management - only the main button, others will be attached when screen is shown
document.getElementById('manage-images-btn').addEventListener('click', () => this.showImageManagement());
// Load saved theme
this.loadSavedTheme();
}
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;
}
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();
// 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();
}
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)');
}
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 all existing theme classes
const themeClasses = ['theme-ocean', 'theme-sunset', 'theme-forest', 'theme-midnight', 'theme-pastel', 'theme-neon', 'theme-autumn', 'theme-monochrome'];
document.body.classList.remove(...themeClasses);
// Add new theme class (ocean is default, no class needed)
if (themeName !== 'ocean') {
document.body.classList.add(`theme-${themeName}`);
}
// Save theme preference
localStorage.setItem('gameTheme', themeName);
console.log(`Theme changed to: ${themeName}`);
}
loadSavedTheme() {
const savedTheme = localStorage.getItem('gameTheme') || 'ocean';
document.getElementById('theme-dropdown').value = savedTheme;
this.changeTheme(savedTheme);
}
showScreen(screenId) {
console.log(`showScreen(${screenId}) called`);
// Hide all screens
document.querySelectorAll('.screen').forEach(screen => {
screen.classList.remove('active');
console.log(`Removed active class from screen:`, screen.id);
});
// Show target screen
const targetScreen = document.getElementById(screenId);
if (targetScreen) {
targetScreen.classList.add('active');
console.log(`Added active class to screen: ${screenId}`);
} else {
console.error(`Screen element not found: ${screenId}`);
}
// Update start screen status when showing start screen
if (screenId === 'start-screen') {
this.updateStartScreenStatus();
}
}
updateStartScreenStatus() {
const statusDiv = document.getElementById('start-screen-status');
if (!statusDiv) return;
const totalImages = gameData.discoveredTaskImages.length + gameData.discoveredConsequenceImages.length;
const customImages = this.dataManager.get('customImages') || [];
const totalAvailable = totalImages + customImages.length;
if (totalAvailable === 0) {
statusDiv.innerHTML = `
<div class="status-warning">
⚠️ No images found!
<br>Go to "Manage Images" to upload images or scan directories.
</div>
`;
statusDiv.style.display = 'block';
} else {
statusDiv.innerHTML = `
<div class="status-success">
✅ Ready to play! Found ${totalAvailable} images
(${totalImages} from directories, ${customImages.length} uploaded)
</div>
`;
statusDiv.style.display = 'block';
}
}
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') || 'ocean';
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() {
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) {
const newSelectAllBtn = selectAllBtn.cloneNode(true);
selectAllBtn.parentNode.replaceChild(newSelectAllBtn, selectAllBtn);
newSelectAllBtn.addEventListener('click', () => this.selectAllImages(activeTab));
}
if (deselectAllBtn) {
const newDeselectAllBtn = deselectAllBtn.cloneNode(true);
deselectAllBtn.parentNode.replaceChild(newDeselectAllBtn, deselectAllBtn);
newDeselectAllBtn.addEventListener('click', () => this.deselectAllImages(activeTab));
}
if (deleteBtn) {
const newDeleteBtn = deleteBtn.cloneNode(true);
deleteBtn.parentNode.replaceChild(newDeleteBtn, deleteBtn);
newDeleteBtn.addEventListener('click', () => this.deleteSelectedImages(activeTab));
}
}
setupImageManagementEventListeners() {
// Remove any existing listeners to prevent duplicates
const backBtn = document.getElementById('back-to-start-from-images-btn');
const uploadBtn = document.getElementById('upload-images-btn');
const uploadInput = document.getElementById('image-upload-input');
const storageInfoBtn = document.getElementById('storage-info-btn');
const selectAllBtn = document.getElementById('select-all-images-btn');
const deselectAllBtn = document.getElementById('deselect-all-images-btn');
const deleteBtn = document.getElementById('delete-selected-btn');
const taskImagesTab = document.getElementById('task-images-tab');
const consequenceImagesTab = document.getElementById('consequence-images-tab');
// Desktop-specific buttons
const importTaskBtn = document.getElementById('import-task-images-btn');
const importConsequenceBtn = document.getElementById('import-consequence-images-btn');
// Clone and replace to remove existing listeners
if (backBtn) {
const newBackBtn = backBtn.cloneNode(true);
backBtn.parentNode.replaceChild(newBackBtn, backBtn);
newBackBtn.addEventListener('click', () => this.showScreen('start-screen'));
}
// Desktop import buttons
if (importTaskBtn) {
const newImportTaskBtn = importTaskBtn.cloneNode(true);
importTaskBtn.parentNode.replaceChild(newImportTaskBtn, importTaskBtn);
newImportTaskBtn.addEventListener('click', 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');
}
});
}
if (importConsequenceBtn) {
const newImportConsequenceBtn = importConsequenceBtn.cloneNode(true);
importConsequenceBtn.parentNode.replaceChild(newImportConsequenceBtn, importConsequenceBtn);
newImportConsequenceBtn.addEventListener('click', 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');
}
});
}
// Original web upload button (fallback)
if (uploadBtn) {
const newUploadBtn = uploadBtn.cloneNode(true);
uploadBtn.parentNode.replaceChild(newUploadBtn, uploadBtn);
newUploadBtn.addEventListener('click', () => this.uploadImages());
}
if (storageInfoBtn) {
const newStorageInfoBtn = storageInfoBtn.cloneNode(true);
storageInfoBtn.parentNode.replaceChild(newStorageInfoBtn, storageInfoBtn);
newStorageInfoBtn.addEventListener('click', () => this.showStorageInfo());
}
if (taskImagesTab) {
const newTaskImagesTab = taskImagesTab.cloneNode(true);
taskImagesTab.parentNode.replaceChild(newTaskImagesTab, taskImagesTab);
newTaskImagesTab.addEventListener('click', () => this.switchImageTab('task'));
}
if (consequenceImagesTab) {
const newConsequenceImagesTab = consequenceImagesTab.cloneNode(true);
consequenceImagesTab.parentNode.replaceChild(newConsequenceImagesTab, consequenceImagesTab);
newConsequenceImagesTab.addEventListener('click', () => this.switchImageTab('consequence'));
}
if (uploadInput) {
const newUploadInput = uploadInput.cloneNode(true);
uploadInput.parentNode.replaceChild(newUploadInput, uploadInput);
newUploadInput.addEventListener('change', (e) => this.handleImageUpload(e));
}
if (selectAllBtn) {
const newSelectAllBtn = selectAllBtn.cloneNode(true);
selectAllBtn.parentNode.replaceChild(newSelectAllBtn, selectAllBtn);
newSelectAllBtn.addEventListener('click', () => this.selectAllImages());
}
if (deselectAllBtn) {
const newDeselectAllBtn = deselectAllBtn.cloneNode(true);
deselectAllBtn.parentNode.replaceChild(newDeselectAllBtn, deselectAllBtn);
newDeselectAllBtn.addEventListener('click', () => this.deselectAllImages());
}
if (deleteBtn) {
const newDeleteBtn = deleteBtn.cloneNode(true);
deleteBtn.parentNode.replaceChild(newDeleteBtn, deleteBtn);
newDeleteBtn.addEventListener('click', () => this.deleteSelectedImages());
}
}
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}`;
}
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() {
console.log('startGame() called');
if (!this.imageDiscoveryComplete) {
console.log('Image discovery not complete, retrying in 500ms...');
// Wait a bit longer for image discovery
setTimeout(() => this.startGame(), 500);
return;
}
// Check if we have any images available
const totalImages = gameData.discoveredTaskImages.length + gameData.discoveredConsequenceImages.length;
const customImages = this.dataManager.get('customImages') || [];
if (totalImages === 0 && customImages.length === 0) {
// No images available - guide user to add images
this.showNotification('No images found! Please upload images or scan directories first.', 'error', 5000);
this.showScreen('image-management-screen');
return;
}
console.log('Starting game initialization...');
this.gameState.isRunning = true;
this.gameState.isPaused = false;
this.gameState.startTime = Date.now();
this.gameState.totalPausedTime = 0;
this.gameState.score = 0;
this.gameState.lastSkippedTask = null;
console.log('Recording game start in data manager...');
// Record game start in data manager
this.dataManager.recordGameStart();
console.log('Starting timer...');
this.startTimer();
console.log('Loading next task...');
this.loadNextTask();
console.log('Showing game screen...');
this.showScreen('game-screen');
console.log('Updating stats...');
this.updateStats();
console.log('startGame() completed successfully');
}
loadNextTask() {
if (this.gameState.isConsequenceTask) {
// Load a consequence task
this.loadConsequenceTask();
} else {
// Load a main task
this.loadMainTask();
}
this.displayCurrentTask();
}
loadMainTask() {
const availableTasks = gameData.mainTasks.filter(
task => !this.gameState.usedMainTasks.includes(task.id)
);
if (availableTasks.length === 0) {
// All main tasks completed - end game
this.endGame();
return;
}
// Select random task and random image from task pool
const randomIndex = Math.floor(Math.random() * availableTasks.length);
this.gameState.currentTask = {
...availableTasks[randomIndex],
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;
if (isConsequence) {
imagePool = gameData.discoveredConsequenceImages;
imageType = 'consequence';
} else {
imagePool = gameData.discoveredTaskImages;
imageType = 'task';
}
// 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');
}
const randomIndex = Math.floor(Math.random() * imagePool.length);
const selectedImage = imagePool[randomIndex];
// Convert to displayable format
const displayImage = this.getImageSrc(selectedImage);
console.log(`Selected ${imageType} image: ${typeof selectedImage === 'string' ? selectedImage : selectedImage.originalName}`);
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 taskTypeIndicator = document.getElementById('task-type-indicator');
const taskDifficulty = document.getElementById('task-difficulty');
const taskPoints = document.getElementById('task-points');
const mercySkipBtn = document.getElementById('mercy-skip-btn');
const skipBtn = document.getElementById('skip-btn');
taskText.textContent = this.gameState.currentTask.text;
// Set image with error handling
taskImage.src = this.gameState.currentTask.image;
taskImage.onerror = () => {
console.log('Image failed to load:', this.gameState.currentTask.image);
taskImage.src = this.createPlaceholderImage();
};
// Update task type indicator and button visibility
if (this.gameState.isConsequenceTask) {
taskTypeIndicator.textContent = 'CONSEQUENCE TASK';
taskTypeIndicator.classList.add('consequence');
// Hide difficulty/points for consequence tasks
taskDifficulty.style.display = 'none';
taskPoints.style.display = 'none';
// Hide regular skip button for consequence tasks
skipBtn.style.display = 'none';
// Show mercy skip button for consequence tasks
const originalTask = this.findOriginalSkippedTask();
if (originalTask) {
const mercyCost = this.getPointsForDifficulty(originalTask.difficulty || 'Medium') * 2;
const canAfford = this.gameState.score >= mercyCost;
if (canAfford) {
mercySkipBtn.style.display = 'block';
document.getElementById('mercy-skip-cost').textContent = `-${mercyCost} pts`;
mercySkipBtn.disabled = false;
} else {
mercySkipBtn.style.display = 'block';
document.getElementById('mercy-skip-cost').textContent = `-${mercyCost} pts (Not enough!)`;
mercySkipBtn.disabled = true;
}
} else {
mercySkipBtn.style.display = 'none';
}
} else {
taskTypeIndicator.textContent = 'MAIN TASK';
taskTypeIndicator.classList.remove('consequence');
// Show regular skip button for main tasks
skipBtn.style.display = 'block';
// Hide mercy skip button for main tasks
mercySkipBtn.style.display = 'none';
// Show difficulty and points for main tasks
const difficulty = this.gameState.currentTask.difficulty || 'Medium';
const points = this.getPointsForDifficulty(difficulty);
const difficultyEmoji = this.getDifficultyEmoji(difficulty);
taskDifficulty.textContent = `${difficultyEmoji} ${difficulty}`;
taskPoints.textContent = `${points} ${points === 1 ? 'point' : 'points'}`;
taskDifficulty.style.display = 'block';
taskPoints.style.display = 'inline-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;
const mercyCost = this.getPointsForDifficulty(originalTask.difficulty || 'Medium') * 2;
if (this.gameState.score < mercyCost) {
alert(`Not enough points! You need ${mercyCost} points but only have ${this.gameState.score}.`);
return;
}
// Confirm the mercy skip
const confirmed = confirm(
`Use Mercy Skip?\n\n` +
`This will cost you ${mercyCost} points from your score.\n` +
`Your score will go from ${this.gameState.score} to ${this.gameState.score - mercyCost}.\n\n` +
`Are you sure you want to skip this consequence task?`
);
if (!confirmed) return;
// Deduct points and skip the consequence task
this.gameState.score -= 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 '🟡';
}
}
completeTask() {
if (!this.gameState.isRunning || this.gameState.isPaused) return;
// 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
} else {
this.gameState.usedMainTasks.push(this.gameState.currentTask.id);
this.gameState.completedCount++;
// Award points for completing main tasks
const difficulty = this.gameState.currentTask.difficulty || 'Medium';
const points = this.getPointsForDifficulty(difficulty);
this.gameState.score += points;
}
// 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;
if (this.gameState.isConsequenceTask) {
// Can't skip consequence tasks - they must be completed
alert("Consequence tasks cannot be skipped!");
return;
}
// 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++;
// Load a consequence task
this.gameState.isConsequenceTask = true;
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.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.showScreen('game-screen');
}
quitGame() {
this.endGame();
}
endGame() {
this.gameState.isRunning = false;
this.stopTimer();
this.showFinalStats();
this.showScreen('game-over-screen');
}
resetGame() {
this.gameState = {
isRunning: false,
isPaused: false,
currentTask: null,
isConsequenceTask: false,
startTime: null,
pausedTime: 0,
totalPausedTime: 0,
completedCount: 0,
skippedCount: 0,
consequenceCount: 0,
usedMainTasks: [],
usedConsequenceTasks: []
};
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;
const formattedTime = this.formatTime(elapsed);
document.getElementById('timer').textContent = formattedTime;
// Update timer status
const timerStatus = document.getElementById('timer-status');
if (this.gameState.isPaused) {
timerStatus.textContent = '(PAUSED)';
} else {
timerStatus.textContent = '';
}
}
formatTime(milliseconds) {
const totalSeconds = Math.floor(milliseconds / 1000);
const minutes = Math.floor(totalSeconds / 60);
const seconds = totalSeconds % 60;
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
}
updateStats() {
document.getElementById('completed-count').textContent = this.gameState.completedCount;
document.getElementById('skipped-count').textContent = this.gameState.skippedCount;
document.getElementById('consequence-count').textContent = this.gameState.consequenceCount;
document.getElementById('score').textContent = this.gameState.score;
}
showFinalStats() {
const currentTime = Date.now();
const totalTime = currentTime - this.gameState.startTime - this.gameState.totalPausedTime;
const formattedTime = this.formatTime(totalTime);
document.getElementById('final-score').textContent = this.gameState.score;
document.getElementById('final-time').textContent = formattedTime;
document.getElementById('final-completed').textContent = this.gameState.completedCount;
document.getElementById('final-skipped').textContent = this.gameState.skippedCount;
document.getElementById('final-consequences').textContent = this.gameState.consequenceCount;
}
}
// Unified Data Management System
class DataManager {
constructor() {
this.version = "1.0";
this.storageKey = "webGame-data";
this.data = this.loadData();
this.autoSaveInterval = null;
this.startAutoSave();
}
getDefaultData() {
return {
version: this.version,
timestamp: Date.now(),
settings: {
theme: "default",
difficulty: "medium",
music: {
volume: 30,
loopMode: 0,
shuffle: false,
currentTrack: 0
}
},
gameplay: {
totalGamesPlayed: 0,
totalTasksCompleted: 0,
totalTasksSkipped: 0,
bestScore: 0,
currentStreak: 0,
longestStreak: 0,
totalPlayTime: 0,
mercyPointsEarned: 0,
mercyPointsSpent: 0
},
customContent: {
tasks: {
main: [],
consequence: []
},
customImages: [],
disabledImages: []
},
statistics: {
taskCompletionRate: 0,
averageGameDuration: 0,
difficultyStats: {
easy: { played: 0, completed: 0 },
medium: { played: 0, completed: 0 },
hard: { played: 0, completed: 0 }
},
themeUsage: {},
lastPlayed: null
}
};
}
loadData() {
try {
// Try new unified format first
const unified = localStorage.getItem(this.storageKey);
if (unified) {
const data = JSON.parse(unified);
return this.migrateData(data);
}
// Migrate from old scattered localStorage
return this.migrateFromOldFormat();
} catch (error) {
console.warn('Data loading failed, using defaults:', error);
return this.getDefaultData();
}
}
migrateFromOldFormat() {
const data = this.getDefaultData();
// Migrate existing scattered data
try {
// Theme
const theme = localStorage.getItem('selectedTheme');
if (theme) data.settings.theme = theme;
// Music settings
const volume = localStorage.getItem('gameMusic-volume');
if (volume) data.settings.music.volume = parseInt(volume);
const track = localStorage.getItem('gameMusic-track');
if (track) data.settings.music.currentTrack = parseInt(track);
const loopMode = localStorage.getItem('gameMusic-loopMode');
if (loopMode) data.settings.music.loopMode = parseInt(loopMode);
const shuffle = localStorage.getItem('gameMusic-shuffleMode');
if (shuffle) data.settings.music.shuffle = shuffle === 'true';
// Custom tasks
const mainTasks = localStorage.getItem('customMainTasks');
if (mainTasks) data.customContent.tasks.main = JSON.parse(mainTasks);
const consequenceTasks = localStorage.getItem('customConsequenceTasks');
if (consequenceTasks) data.customContent.tasks.consequence = JSON.parse(consequenceTasks);
// Difficulty
const difficulty = localStorage.getItem('selectedDifficulty');
if (difficulty) data.settings.difficulty = difficulty;
console.log('Migrated data from old format');
this.saveData(); // Save in new format
this.cleanupOldStorage(); // Remove old keys
} catch (error) {
console.warn('Migration failed, using defaults:', error);
}
return data;
}
migrateData(data) {
// Handle version upgrades here
if (!data.version || data.version < this.version) {
console.log(`Upgrading data from ${data.version || 'unknown'} to ${this.version}`);
// Add any missing properties from default
const defaultData = this.getDefaultData();
data = this.deepMerge(defaultData, data);
data.version = this.version;
}
return data;
}
deepMerge(target, source) {
const result = { ...target };
for (const key in source) {
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
result[key] = this.deepMerge(target[key] || {}, source[key]);
} else {
result[key] = source[key];
}
}
return result;
}
cleanupOldStorage() {
// Remove old scattered localStorage keys
const oldKeys = [
'selectedTheme', 'gameMusic-volume', 'gameMusic-track',
'gameMusic-loopMode', 'gameMusic-shuffleMode',
'customMainTasks', 'customConsequenceTasks', 'selectedDifficulty'
];
oldKeys.forEach(key => localStorage.removeItem(key));
console.log('Cleaned up old storage keys');
}
saveData() {
try {
if (!this.data) {
this.data = this.getDefaultData();
}
this.data.timestamp = Date.now();
localStorage.setItem(this.storageKey, JSON.stringify(this.data));
} catch (error) {
console.error('Failed to save data:', error);
throw error; // Re-throw so calling code can handle it
}
}
// Settings Management
getSetting(path) {
return this.getNestedValue(this.data.settings, path);
}
setSetting(path, value) {
this.setNestedValue(this.data.settings, path, value);
this.saveData();
}
// Generic data access
get(key) {
// Handle special cases first
if (key === 'autoSaveGameState') {
return JSON.parse(localStorage.getItem('autoSaveGameState') || 'null');
}
// For custom images and disabled images, store them in customContent
if (key === 'customImages') {
return this.data.customContent.customImages || [];
}
if (key === 'disabledImages') {
return this.data.customContent.disabledImages || [];
}
// Generic access to data properties
return this.data[key];
}
set(key, value) {
// Handle special cases first
if (key === 'autoSaveGameState') {
localStorage.setItem('autoSaveGameState', JSON.stringify(value));
return;
}
// For custom images and disabled images, store them in customContent
if (key === 'customImages') {
if (!this.data.customContent.customImages) {
this.data.customContent.customImages = [];
}
this.data.customContent.customImages = value;
this.saveData();
return;
}
if (key === 'disabledImages') {
if (!this.data.customContent.disabledImages) {
this.data.customContent.disabledImages = [];
}
this.data.customContent.disabledImages = value;
this.saveData();
return;
}
// Generic setter
this.data[key] = value;
this.saveData();
}
// Gameplay Statistics
recordGameStart() {
this.data.gameplay.totalGamesPlayed++;
this.data.statistics.lastPlayed = Date.now();
this.saveData();
}
recordTaskComplete(difficulty, points, isConsequence = false) {
this.data.gameplay.totalTasksCompleted++;
if (!isConsequence) {
this.data.gameplay.currentStreak++;
this.data.gameplay.longestStreak = Math.max(
this.data.gameplay.longestStreak,
this.data.gameplay.currentStreak
);
// Track difficulty stats
if (this.data.statistics.difficultyStats[difficulty]) {
this.data.statistics.difficultyStats[difficulty].completed++;
}
}
this.updateStatistics();
this.saveData();
}
recordTaskSkip(mercyPoints = 0) {
this.data.gameplay.totalTasksSkipped++;
this.data.gameplay.currentStreak = 0;
if (mercyPoints > 0) {
this.data.gameplay.mercyPointsSpent += mercyPoints;
}
this.updateStatistics();
this.saveData();
}
recordScore(score) {
this.data.gameplay.bestScore = Math.max(this.data.gameplay.bestScore, score);
this.saveData();
}
recordPlayTime(duration) {
this.data.gameplay.totalPlayTime += duration;
this.updateStatistics();
this.saveData();
}
updateStatistics() {
const { totalTasksCompleted, totalTasksSkipped } = this.data.gameplay;
const total = totalTasksCompleted + totalTasksSkipped;
this.data.statistics.taskCompletionRate = total > 0 ? totalTasksCompleted / total : 0;
if (this.data.gameplay.totalGamesPlayed > 0) {
this.data.statistics.averageGameDuration =
this.data.gameplay.totalPlayTime / this.data.gameplay.totalGamesPlayed;
}
}
// Custom Content Management
getCustomTasks(type = 'main') {
return this.data.customContent.tasks[type] || [];
}
addCustomTask(task, type = 'main') {
if (!this.data.customContent.tasks[type]) {
this.data.customContent.tasks[type] = [];
}
this.data.customContent.tasks[type].push(task);
this.saveData();
}
removeCustomTask(index, type = 'main') {
if (this.data.customContent.tasks[type]) {
this.data.customContent.tasks[type].splice(index, 1);
this.saveData();
}
}
resetCustomTasks(type = 'main') {
this.data.customContent.tasks[type] = [];
this.saveData();
}
// Export/Import System
exportData(includeStats = true) {
const exportData = {
version: this.version,
exportDate: new Date().toISOString(),
settings: this.data.settings,
customContent: this.data.customContent
};
if (includeStats) {
exportData.gameplay = this.data.gameplay;
exportData.statistics = this.data.statistics;
}
const dataStr = JSON.stringify(exportData, null, 2);
const blob = new Blob([dataStr], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `webgame-save-${new Date().toISOString().split('T')[0]}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
return exportData;
}
importData(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => {
try {
const importedData = JSON.parse(e.target.result);
// Validate import
if (!this.validateImportData(importedData)) {
throw new Error('Invalid save file format');
}
// Merge imported data
this.data = this.deepMerge(this.getDefaultData(), importedData);
this.data.version = this.version;
this.data.timestamp = Date.now();
this.saveData();
resolve(importedData);
} catch (error) {
reject(error);
}
};
reader.onerror = () => reject(new Error('Failed to read file'));
reader.readAsText(file);
});
}
validateImportData(data) {
// Basic validation
return data &&
typeof data === 'object' &&
(data.settings || data.customContent || data.gameplay);
}
// Utility functions
getNestedValue(obj, path) {
return path.split('.').reduce((current, key) => current && current[key], obj);
}
setNestedValue(obj, path, value) {
const keys = path.split('.');
const lastKey = keys.pop();
const target = keys.reduce((current, key) => {
if (!current[key]) current[key] = {};
return current[key];
}, obj);
target[lastKey] = value;
}
// Auto-save functionality
startAutoSave() {
this.autoSaveInterval = setInterval(() => {
this.saveData();
}, 30000); // Save every 30 seconds
}
stopAutoSave() {
if (this.autoSaveInterval) {
clearInterval(this.autoSaveInterval);
this.autoSaveInterval = null;
}
}
// Statistics getters for UI
getStats() {
return {
...this.data.gameplay,
...this.data.statistics,
hoursPlayed: Math.round(this.data.gameplay.totalPlayTime / 3600000 * 10) / 10,
completionPercentage: Math.round(this.data.statistics.taskCompletionRate * 100)
};
}
// Reset functionality
resetAllData() {
if (confirm('Are you sure you want to reset all data? This cannot be undone!')) {
this.data = this.getDefaultData();
this.saveData();
return true;
}
return false;
}
resetStats() {
if (confirm('Reset all statistics? Your settings and custom tasks will be kept.')) {
this.data.gameplay = this.getDefaultData().gameplay;
this.data.statistics = this.getDefaultData().statistics;
this.saveData();
return true;
}
return false;
}
}
// 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 = [];
this.tracks = [
{ name: 'Colorful Flowers', file: 'audio/Colorful-Flowers(chosic.com).mp3' },
{ name: 'New Beginnings', file: 'audio/New-Beginnings-chosic.com_.mp3' },
{ name: 'Storm Clouds', file: 'audio/storm-clouds-purpple-cat(chosic.com).mp3' },
{ name: 'Brunch For Two', file: 'audio/Tokyo-Music-Walker-Brunch-For-Two-chosic.com_.mp3' }
];
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);
}
initializeVolumeUI() {
const volumeSlider = document.getElementById('volume-slider');
const volumePercent = document.getElementById('volume-percent');
const trackSelector = document.getElementById('track-selector');
volumeSlider.value = this.volume;
volumePercent.textContent = `${this.volume}%`;
trackSelector.value = this.currentTrackIndex;
this.updateLoopButton();
this.updateShuffleButton();
}
// Loop mode cycling: 0 -> 1 -> 2 -> 0
toggleLoopMode() {
this.loopMode = (this.loopMode + 1) % 3;
this.saveSettings();
this.updateLoopButton();
this.updateAudioLoop();
}
toggleShuffleMode() {
this.shuffleMode = !this.shuffleMode;
this.saveSettings();
this.updateShuffleButton();
if (this.shuffleMode) {
this.playHistory = [this.currentTrackIndex]; // Start tracking
}
}
updateLoopButton() {
const loopBtn = document.getElementById('loop-btn');
switch(this.loopMode) {
case 0:
loopBtn.classList.remove('active');
loopBtn.innerHTML = '🔁';
loopBtn.title = 'Loop: Off';
break;
case 1:
loopBtn.classList.add('active');
loopBtn.innerHTML = '🔁';
loopBtn.title = 'Loop: Playlist';
break;
case 2:
loopBtn.classList.add('active');
loopBtn.innerHTML = '🔂';
loopBtn.title = 'Loop: Current Track';
break;
}
}
updateShuffleButton() {
const shuffleBtn = document.getElementById('shuffle-btn');
if (this.shuffleMode) {
shuffleBtn.classList.add('active');
shuffleBtn.title = 'Shuffle: On';
} else {
shuffleBtn.classList.remove('active');
shuffleBtn.title = 'Shuffle: Off';
}
}
updateAudioLoop() {
if (this.currentAudio) {
this.currentAudio.loop = (this.loopMode === 2);
}
}
getNextTrack() {
if (this.shuffleMode) {
return this.getShuffledNextTrack();
} else {
return (this.currentTrackIndex + 1) % this.tracks.length;
}
}
getShuffledNextTrack() {
if (this.tracks.length <= 1) return 0;
let availableTracks = [];
for (let i = 0; i < this.tracks.length; i++) {
// Avoid last 2 played tracks if possible
if (this.playHistory.length < 2 || !this.playHistory.slice(-2).includes(i)) {
availableTracks.push(i);
}
}
// If no tracks available (shouldn't happen with 4+ tracks), use all except current
if (availableTracks.length === 0) {
availableTracks = this.tracks.map((_, i) => i).filter(i => i !== this.currentTrackIndex);
}
return availableTracks[Math.floor(Math.random() * availableTracks.length)];
}
// Called when track ends naturally
onTrackEnded() {
if (this.loopMode === 2) {
// Loop current track - handled by audio.loop = true
return;
} else if (this.loopMode === 1) {
// Loop playlist - go to next track
this.playNextTrack();
} else {
// No loop - stop playing
this.isPlaying = false;
this.updateUI();
}
}
playNextTrack() {
const nextIndex = this.getNextTrack();
this.changeTrack(nextIndex);
// Update play history for shuffle
if (this.shuffleMode) {
this.playHistory.push(nextIndex);
// Keep only last 3 tracks in history
if (this.playHistory.length > 3) {
this.playHistory.shift();
}
}
}
toggle() {
if (this.isPlaying) {
this.stop();
} else {
this.play();
}
}
play() {
try {
if (!this.currentAudio) {
this.loadCurrentTrack();
}
this.currentAudio.play();
this.isPlaying = true;
this.updateUI();
} catch (error) {
console.log('Audio playback failed:', error);
// Fallback to silent mode
this.isPlaying = true;
this.updateUI();
}
}
stop() {
if (this.currentAudio) {
this.currentAudio.pause();
this.currentAudio.currentTime = 0;
}
this.isPlaying = false;
this.updateUI();
}
pause() {
if (this.currentAudio && this.isPlaying) {
this.currentAudio.pause();
}
}
resume() {
if (this.currentAudio && this.isPlaying) {
try {
this.currentAudio.play();
} catch (error) {
console.log('Audio resume failed:', error);
}
}
}
changeTrack(trackIndex) {
const wasPlaying = this.isPlaying;
if (this.currentAudio) {
this.currentAudio.pause();
this.currentAudio = null;
}
this.currentTrackIndex = trackIndex;
this.saveSettings();
if (wasPlaying) {
this.play();
} else {
this.updateUI();
}
}
setVolume(volume) {
this.volume = volume;
this.saveSettings();
if (this.currentAudio) {
this.currentAudio.volume = volume / 100;
}
const volumePercent = document.getElementById('volume-percent');
volumePercent.textContent = `${volume}%`;
// Update volume icon based on level
const volumeIcon = document.querySelector('.volume-icon');
if (volume === 0) {
volumeIcon.textContent = '🔇';
} else if (volume < 30) {
volumeIcon.textContent = '🔉';
} else {
volumeIcon.textContent = '🔊';
}
}
loadCurrentTrack() {
const track = this.tracks[this.currentTrackIndex];
// Create audio element with fallback
this.currentAudio = new Audio();
this.currentAudio.volume = this.volume / 100;
this.updateAudioLoop();
// Set source with error handling
this.currentAudio.src = track.file;
// Handle loading errors gracefully
this.currentAudio.onerror = () => {
console.log(`Could not load audio: ${track.file}`);
// Continue without audio
};
// Handle successful loading
this.currentAudio.onloadeddata = () => {
console.log(`Loaded audio: ${track.name}`);
};
// Handle track ending
this.currentAudio.onended = () => {
if (this.isPlaying) {
this.onTrackEnded();
}
};
}
updateUI() {
const toggleBtn = document.getElementById('music-toggle');
const statusSpan = document.getElementById('music-status');
const trackSelector = document.getElementById('track-selector');
const currentTrack = this.tracks[this.currentTrackIndex];
// Update track selector
trackSelector.value = this.currentTrackIndex;
if (this.isPlaying) {
toggleBtn.classList.add('playing');
toggleBtn.innerHTML = '⏸️';
// Show current mode in status
let modeText = '';
if (this.shuffleMode) modeText += ' 🔀';
if (this.loopMode === 1) modeText += ' 🔁';
else if (this.loopMode === 2) modeText += ' 🔂';
statusSpan.textContent = `${currentTrack.name}${modeText}`;
} else {
toggleBtn.classList.remove('playing');
toggleBtn.innerHTML = '🎵';
statusSpan.textContent = 'Music: Off';
}
}
}
// Initialize game when page loads
document.addEventListener('DOMContentLoaded', () => {
window.game = new TaskChallengeGame();
});