training-academy/game.js

4919 lines
193 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: [],
usedTaskImages: [], // Track which task images have been shown
usedConsequenceImages: [], // Track which consequence images have been shown
gameMode: 'complete-all', // Game mode: 'complete-all', 'timed', 'score-target'
timeLimit: 300, // Time limit in seconds (5 minutes default)
scoreTarget: 1000, // Score target (default 1000 points)
currentStreak: 0, // Track consecutive completed regular tasks
totalStreakBonuses: 0, // Track total streak bonus points earned
lastStreakMilestone: 0 // Track the last streak milestone reached
};
this.timerInterval = null;
this.imageDiscoveryComplete = false;
this.imageManagementListenersAttached = false;
this.audioDiscoveryComplete = false;
this.audioManagementListenersAttached = false;
this.musicManager = new MusicManager(this.dataManager);
// Initialize Flash Message System
this.flashMessageManager = new FlashMessageManager(this.dataManager);
// Initialize Popup Image System (Punishment for skips)
this.popupImageManager = new PopupImageManager(this.dataManager);
this.initializeEventListeners();
this.setupKeyboardShortcuts();
this.setupWindowResizeHandling();
this.initializeCustomTasks();
this.discoverImages().then(() => {
this.showScreen('start-screen');
});
this.discoverAudio();
// 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=`;
}
// 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');
await this.fileManager.scanDirectoryForAudio('effects');
console.log('Desktop audio discovery completed');
} else {
console.log('Web mode - audio discovery skipped');
}
} catch (error) {
console.log('Audio discovery failed:', error);
}
this.audioDiscoveryComplete = true;
console.log('Audio discovery completed');
}
initializeEventListeners() {
// Screen navigation
document.getElementById('start-btn').addEventListener('click', () => this.startGame());
document.getElementById('resume-btn').addEventListener('click', () => this.resumeGame());
document.getElementById('quit-btn').addEventListener('click', () => this.quitGame());
document.getElementById('play-again-btn').addEventListener('click', () => this.resetGame());
// Game mode selection
this.initializeGameModeListeners();
// Game actions
document.getElementById('complete-btn').addEventListener('click', () => this.completeTask());
document.getElementById('skip-btn').addEventListener('click', () => this.skipTask());
document.getElementById('mercy-skip-btn').addEventListener('click', () => this.mercySkip());
document.getElementById('pause-btn').addEventListener('click', () => this.pauseGame());
// Theme selector
document.getElementById('theme-dropdown').addEventListener('change', (e) => this.changeTheme(e.target.value));
// Options menu toggle
document.getElementById('options-menu-btn').addEventListener('click', () => this.toggleOptionsMenu());
// Music controls
document.getElementById('music-toggle').addEventListener('click', () => this.toggleMusic());
document.getElementById('music-toggle-compact').addEventListener('click', (e) => {
e.stopPropagation(); // Prevent event bubbling
// The hover panel will show automatically, just indicate it's interactive
});
document.getElementById('loop-btn').addEventListener('click', () => this.toggleLoop());
document.getElementById('shuffle-btn').addEventListener('click', () => this.toggleShuffle());
document.getElementById('track-selector').addEventListener('change', (e) => this.changeTrack(parseInt(e.target.value)));
document.getElementById('volume-slider').addEventListener('input', (e) => this.changeVolume(parseInt(e.target.value)));
// Task management
document.getElementById('manage-tasks-btn').addEventListener('click', () => this.showTaskManagement());
document.getElementById('back-to-start-btn').addEventListener('click', () => this.showScreen('start-screen'));
document.getElementById('add-task-btn').addEventListener('click', () => this.addNewTask());
document.getElementById('reset-tasks-btn').addEventListener('click', () => this.resetToDefaultTasks());
document.getElementById('main-tasks-tab').addEventListener('click', () => this.showTaskTab('main'));
document.getElementById('consequence-tasks-tab').addEventListener('click', () => this.showTaskTab('consequence'));
document.getElementById('new-task-type').addEventListener('change', () => this.toggleDifficultyDropdown());
// Data management
document.getElementById('export-btn').addEventListener('click', () => this.exportData());
document.getElementById('import-btn').addEventListener('click', () => this.importData());
document.getElementById('import-file').addEventListener('change', (e) => this.handleFileImport(e));
document.getElementById('stats-btn').addEventListener('click', () => this.showStats());
document.getElementById('help-btn').addEventListener('click', () => this.showHelp());
document.getElementById('close-stats').addEventListener('click', () => this.hideStats());
document.getElementById('close-help').addEventListener('click', () => this.hideHelp());
document.getElementById('reset-stats-btn').addEventListener('click', () => this.resetStats());
document.getElementById('export-stats-btn').addEventListener('click', () => this.exportStatsOnly());
// Image management - only the main button, others will be attached when screen is shown
document.getElementById('manage-images-btn').addEventListener('click', () => this.showImageManagement());
// Audio management - only the main button, others will be attached when screen is shown
document.getElementById('manage-audio-btn').addEventListener('click', () => this.showAudioManagement());
// Annoyance management - main button and basic controls
document.getElementById('manage-annoyance-btn').addEventListener('click', () => this.showAnnoyanceManagement());
// Load saved theme
this.loadSavedTheme();
}
initializeGameModeListeners() {
const gameModeRadios = document.querySelectorAll('input[name="gameMode"]');
gameModeRadios.forEach(radio => {
radio.addEventListener('change', () => this.handleGameModeChange());
});
// Add listeners for dropdown changes
document.getElementById('time-limit-select').addEventListener('change', () => {
this.handleTimeLimitChange();
});
document.getElementById('score-target-select').addEventListener('change', () => {
this.handleScoreTargetChange();
});
// Add listeners for custom input changes
document.getElementById('custom-time-input').addEventListener('input', () => {
this.handleCustomTimeChange();
});
document.getElementById('custom-score-input').addEventListener('input', () => {
this.handleCustomScoreChange();
});
// Initialize with default mode
this.handleGameModeChange();
}
handleGameModeChange() {
const selectedMode = document.querySelector('input[name="gameMode"]:checked').value;
this.gameState.gameMode = selectedMode;
// Show/hide configuration options based on selected mode
document.getElementById('timed-config').style.display =
selectedMode === 'timed' ? 'block' : 'none';
document.getElementById('score-target-config').style.display =
selectedMode === 'score-target' ? 'block' : 'none';
// Update game state with selected values
if (selectedMode === 'timed') {
this.handleTimeLimitChange();
} else if (selectedMode === 'score-target') {
this.handleScoreTargetChange();
}
console.log(`Game mode changed to: ${selectedMode}`, this.gameState);
}
handleTimeLimitChange() {
const timeLimitSelect = document.getElementById('time-limit-select');
const customTimeInput = document.getElementById('custom-time-input');
const selectedValue = timeLimitSelect.value;
if (selectedValue === 'custom') {
customTimeInput.style.display = 'block';
this.handleCustomTimeChange();
} else {
customTimeInput.style.display = 'none';
this.gameState.timeLimit = parseInt(selectedValue);
}
}
handleScoreTargetChange() {
const scoreTargetSelect = document.getElementById('score-target-select');
const customScoreInput = document.getElementById('custom-score-input');
const selectedValue = scoreTargetSelect.value;
if (selectedValue === 'custom') {
customScoreInput.style.display = 'block';
this.handleCustomScoreChange();
} else {
customScoreInput.style.display = 'none';
this.gameState.scoreTarget = parseInt(selectedValue);
}
}
handleCustomTimeChange() {
const customTimeInput = document.getElementById('custom-time-input');
const minutes = parseInt(customTimeInput.value) || 15;
this.gameState.timeLimit = minutes * 60; // Convert minutes to seconds
}
handleCustomScoreChange() {
const customScoreInput = document.getElementById('custom-score-input');
const score = parseInt(customScoreInput.value) || 300;
this.gameState.scoreTarget = score;
}
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)');
}
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 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() {
// 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') || '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() {
// 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 effectsTab = document.getElementById('effects-audio-tab');
const backgroundGallery = document.getElementById('background-audio-gallery');
const ambientGallery = document.getElementById('ambient-audio-gallery');
const effectsGallery = document.getElementById('effects-audio-gallery');
// Remove active class from all tabs and galleries
[backgroundTab, ambientTab, effectsTab].forEach(tab => tab && tab.classList.remove('active'));
[backgroundGallery, ambientGallery, effectsGallery].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');
} else if (tabType === 'effects') {
effectsTab && effectsTab.classList.add('active');
effectsGallery && effectsGallery.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');
}
};
}
const importEffectsBtn = document.getElementById('import-effects-audio-btn');
if (importEffectsBtn) {
importEffectsBtn.onclick = async () => {
if (this.fileManager) {
await this.fileManager.selectAndImportAudio('effects');
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();
}
// 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');
}
const effectsAudioTab = document.getElementById('effects-audio-tab');
if (effectsAudioTab) {
effectsAudioTab.onclick = () => this.switchAudioTab('effects');
}
// 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');
const effectsGallery = document.getElementById('effects-audio-gallery');
if (!backgroundGallery || !ambientGallery || !effectsGallery) {
console.error('Audio gallery elements not found');
return;
}
// Get custom audio from storage
const customAudio = this.dataManager.get('customAudio') || { background: [], ambient: [], effects: [] };
// Load each category
this.loadAudioCategory('background', backgroundGallery, customAudio.background);
this.loadAudioCategory('ambient', ambientGallery, customAudio.ambient);
this.loadAudioCategory('effects', effectsGallery, customAudio.effects);
// 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();
}
}
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(); game.previewAudio('${audio.path}', '${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;
}
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: [], 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 total = backgroundCount + ambientCount + effectsCount;
const audioCountElement = document.querySelector('.audio-count');
if (audioCountElement) {
audioCountElement.textContent = `${total} total audio files (${backgroundCount} background, ${ambientCount} ambient, ${effectsCount} effects)`;
}
}
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';
audioPlayer.src = audioPath;
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.');
}
}
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') || { 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;
}
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('Starting flash message system...');
this.flashMessageManager.start();
this.flashMessageManager.triggerEventMessage('gameStart');
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
if (this.gameState.gameMode === 'complete-all') {
// Only end game in complete-all mode
this.endGame('complete-all');
return;
} else {
// In timed and score-target modes, reset used tasks and continue
console.log(`All tasks completed in ${this.gameState.gameMode} mode, cycling through tasks again`);
this.gameState.usedMainTasks = [];
// Recursively call loadMainTask to select from reset pool
this.loadMainTask();
return;
}
}
// 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;
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 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 '🟡';
}
}
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 10; // 10 bonus points per streak milestone
}
return 0;
}
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;
// 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++;
// Increment streak for regular tasks
this.gameState.currentStreak++;
// Award points for completing main tasks
const difficulty = this.gameState.currentTask.difficulty || 'Medium';
const basePoints = this.getPointsForDifficulty(difficulty);
this.gameState.score += basePoints;
// Check for streak bonus (every 10 consecutive completed tasks)
const streakBonus = this.checkStreakBonus();
if (streakBonus > 0) {
this.gameState.score += streakBonus;
this.gameState.totalStreakBonuses += streakBonus;
this.showStreakBonusNotification(this.gameState.currentStreak, streakBonus);
// Trigger special streak message
this.flashMessageManager.triggerEventMessage('streak');
} else {
// Trigger regular completion message
this.flashMessageManager.triggerEventMessage('taskComplete');
}
// Check for score target win condition
if (this.gameState.gameMode === 'score-target' && this.gameState.score >= this.gameState.scoreTarget) {
this.updateStats(); // Update stats before ending game
this.endGame('score-target');
return;
}
}
// 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++;
// 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.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();
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();
this.showScreen('game-screen');
}
quitGame() {
this.endGame();
}
endGame(reason = 'complete-all') {
this.gameState.isRunning = false;
this.stopTimer();
this.flashMessageManager.stop();
this.showFinalStats(reason);
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,
score: 0,
lastSkippedTask: null,
usedMainTasks: [],
usedConsequenceTasks: [],
usedTaskImages: [],
usedConsequenceImages: [],
gameMode: 'complete-all',
timeLimit: 300,
scoreTarget: 1000,
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;
let formattedTime;
if (this.gameState.gameMode === 'timed') {
// Countdown timer for timed mode
const remainingMs = (this.gameState.timeLimit * 1000) - elapsed;
if (remainingMs <= 0) {
// Time's up!
formattedTime = '00:00';
document.getElementById('timer').textContent = formattedTime;
this.endGame('time');
return;
}
formattedTime = this.formatTime(remainingMs);
// Change color when time is running low (less than 30 seconds)
const timerElement = document.getElementById('timer');
if (remainingMs <= 30000) {
timerElement.style.color = '#ff4757';
timerElement.style.fontWeight = 'bold';
} else if (remainingMs <= 60000) {
timerElement.style.color = '#ffa502';
timerElement.style.fontWeight = 'bold';
} else {
timerElement.style.color = '';
timerElement.style.fontWeight = '';
}
} else {
// Normal elapsed timer for other modes
formattedTime = this.formatTime(elapsed);
}
document.getElementById('timer').textContent = formattedTime;
// Update timer status
const timerStatus = document.getElementById('timer-status');
if (this.gameState.isPaused) {
timerStatus.textContent = '(PAUSED)';
} else if (this.gameState.gameMode === 'timed') {
timerStatus.textContent = '(TIME LEFT)';
} else {
timerStatus.textContent = '';
}
}
formatTime(milliseconds) {
const totalSeconds = Math.floor(milliseconds / 1000);
const minutes = Math.floor(totalSeconds / 60);
const seconds = totalSeconds % 60;
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
}
updateStats() {
document.getElementById('completed-count').textContent = this.gameState.completedCount;
document.getElementById('skipped-count').textContent = this.gameState.skippedCount;
document.getElementById('consequence-count').textContent = this.gameState.consequenceCount;
document.getElementById('score').textContent = this.gameState.score;
// 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');
}
}
}
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');
switch (reason) {
case 'time':
gameOverTitle.textContent = '⏰ Time\'s Up!';
gameOverMessage.textContent = 'You ran out of time! See how many tasks you completed.';
break;
case 'score-target':
gameOverTitle.textContent = '🏆 Target Reached!';
gameOverMessage.textContent = `Congratulations! You reached the target score of ${this.gameState.scoreTarget} points!`;
break;
case 'complete-all':
default:
gameOverTitle.textContent = '🎉 All Tasks Complete!';
gameOverMessage.textContent = 'Congratulations! You\'ve completed all available tasks!';
break;
}
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;
// 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 'score-target':
gameModeText = `Score Target (${this.gameState.scoreTarget} points)`;
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;
}
}
}
// 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 = [];
// Initialize empty tracks array - custom tracks will be loaded next
this.tracks = [];
// Load and add custom background music
this.loadCustomTracks();
this.updateUI();
this.initializeVolumeUI();
}
saveSettings() {
this.dataManager.setSetting('music.volume', this.volume);
this.dataManager.setSetting('music.currentTrack', this.currentTrackIndex);
this.dataManager.setSetting('music.loopMode', this.loopMode);
this.dataManager.setSetting('music.shuffle', this.shuffleMode);
}
loadCustomTracks() {
// Get custom background music from storage
const customAudio = this.dataManager.get('customAudio') || { background: [], ambient: [], effects: [] };
const backgroundMusic = customAudio.background || [];
// Add enabled custom background music to tracks
backgroundMusic.forEach(audio => {
if (audio.enabled !== false) { // Include if not explicitly disabled
this.tracks.push({
name: audio.title || audio.name,
file: audio.path,
isBuiltIn: false,
isCustom: true
});
}
});
console.log(`MusicManager: Loaded ${backgroundMusic.length} custom background tracks`);
}
refreshCustomTracks() {
// Clear all tracks since we only have custom tracks now
this.tracks = [];
// Reload custom tracks
this.loadCustomTracks();
// Update the UI to reflect new tracks
this.updateTrackSelector();
// If current track was custom and no longer exists, reset to first track
if (this.currentTrackIndex >= this.tracks.length) {
this.currentTrackIndex = 0;
this.saveSettings();
}
console.log(`MusicManager: Refreshed tracks, now have ${this.tracks.length} total tracks`);
}
initializeVolumeUI() {
const volumeSlider = document.getElementById('volume-slider');
const volumePercent = document.getElementById('volume-percent');
const trackSelector = document.getElementById('track-selector');
volumeSlider.value = this.volume;
volumePercent.textContent = `${this.volume}%`;
// Build track selector with all tracks
this.updateTrackSelector();
this.updateLoopButton();
this.updateShuffleButton();
}
// Loop mode cycling: 0 -> 1 -> 2 -> 0
toggleLoopMode() {
this.loopMode = (this.loopMode + 1) % 3;
this.saveSettings();
this.updateLoopButton();
this.updateAudioLoop();
}
toggleShuffleMode() {
this.shuffleMode = !this.shuffleMode;
this.saveSettings();
this.updateShuffleButton();
if (this.shuffleMode) {
this.playHistory = [this.currentTrackIndex]; // Start tracking
}
}
updateLoopButton() {
const loopBtn = document.getElementById('loop-btn');
switch(this.loopMode) {
case 0:
loopBtn.classList.remove('active');
loopBtn.innerHTML = '🔁';
loopBtn.title = 'Loop: Off';
break;
case 1:
loopBtn.classList.add('active');
loopBtn.innerHTML = '🔁';
loopBtn.title = 'Loop: Playlist';
break;
case 2:
loopBtn.classList.add('active');
loopBtn.innerHTML = '🔂';
loopBtn.title = 'Loop: Current Track';
break;
}
}
updateShuffleButton() {
const shuffleBtn = document.getElementById('shuffle-btn');
if (this.shuffleMode) {
shuffleBtn.classList.add('active');
shuffleBtn.title = 'Shuffle: On';
} else {
shuffleBtn.classList.remove('active');
shuffleBtn.title = 'Shuffle: Off';
}
}
updateAudioLoop() {
if (this.currentAudio) {
this.currentAudio.loop = (this.loopMode === 2);
}
}
getNextTrack() {
if (this.shuffleMode) {
return this.getShuffledNextTrack();
} else {
return (this.currentTrackIndex + 1) % this.tracks.length;
}
}
getShuffledNextTrack() {
if (this.tracks.length <= 1) return 0;
let availableTracks = [];
for (let i = 0; i < this.tracks.length; i++) {
// Avoid last 2 played tracks if possible
if (this.playHistory.length < 2 || !this.playHistory.slice(-2).includes(i)) {
availableTracks.push(i);
}
}
// If no tracks available (shouldn't happen with 4+ tracks), use all except current
if (availableTracks.length === 0) {
availableTracks = this.tracks.map((_, i) => i).filter(i => i !== this.currentTrackIndex);
}
return availableTracks[Math.floor(Math.random() * availableTracks.length)];
}
// Called when track ends naturally
onTrackEnded() {
if (this.loopMode === 2) {
// Loop current track - handled by audio.loop = true
return;
} else if (this.loopMode === 1) {
// Loop playlist - go to next track
this.playNextTrack();
} else {
// No loop - stop playing
this.isPlaying = false;
this.updateUI();
}
}
playNextTrack() {
const nextIndex = this.getNextTrack();
this.changeTrack(nextIndex);
// Update play history for shuffle
if (this.shuffleMode) {
this.playHistory.push(nextIndex);
// Keep only last 3 tracks in history
if (this.playHistory.length > 3) {
this.playHistory.shift();
}
}
}
toggle() {
if (this.isPlaying) {
this.stop();
} else {
this.play();
}
}
play() {
try {
if (!this.currentAudio) {
this.loadCurrentTrack();
}
this.currentAudio.play();
this.isPlaying = true;
this.updateUI();
} catch (error) {
console.log('Audio playback failed:', error);
// Fallback to silent mode
this.isPlaying = true;
this.updateUI();
}
}
stop() {
if (this.currentAudio) {
this.currentAudio.pause();
this.currentAudio.currentTime = 0;
}
this.isPlaying = false;
this.updateUI();
}
pause() {
if (this.currentAudio && this.isPlaying) {
this.currentAudio.pause();
}
}
resume() {
if (this.currentAudio && this.isPlaying) {
try {
this.currentAudio.play();
} catch (error) {
console.log('Audio resume failed:', error);
}
}
}
changeTrack(trackIndex) {
const wasPlaying = this.isPlaying;
if (this.currentAudio) {
this.currentAudio.pause();
this.currentAudio = null;
}
this.currentTrackIndex = trackIndex;
this.saveSettings();
if (wasPlaying) {
this.play();
} else {
this.updateUI();
}
}
setVolume(volume) {
this.volume = volume;
this.saveSettings();
if (this.currentAudio) {
this.currentAudio.volume = volume / 100;
}
const volumePercent = document.getElementById('volume-percent');
volumePercent.textContent = `${volume}%`;
// Update volume icon based on level
const volumeIcon = document.querySelector('.volume-icon');
if (volume === 0) {
volumeIcon.textContent = '🔇';
} else if (volume < 30) {
volumeIcon.textContent = '🔉';
} else {
volumeIcon.textContent = '🔊';
}
}
loadCurrentTrack() {
const track = this.tracks[this.currentTrackIndex];
// Create audio element with fallback
this.currentAudio = new Audio();
this.currentAudio.volume = this.volume / 100;
this.updateAudioLoop();
// Set source with error handling
this.currentAudio.src = track.file;
// Handle loading errors gracefully
this.currentAudio.onerror = () => {
console.log(`Could not load audio: ${track.file}`);
// Continue without audio
};
// Handle successful loading
this.currentAudio.onloadeddata = () => {
console.log(`Loaded audio: ${track.name}`);
};
// Handle track ending
this.currentAudio.onended = () => {
if (this.isPlaying) {
this.onTrackEnded();
}
};
}
updateUI() {
const toggleBtn = document.getElementById('music-toggle');
const statusSpan = document.getElementById('music-status');
const trackSelector = document.getElementById('track-selector');
const currentTrack = this.tracks[this.currentTrackIndex];
// Update track selector
trackSelector.value = this.currentTrackIndex;
if (this.isPlaying) {
toggleBtn.classList.add('playing');
toggleBtn.innerHTML = '⏸️';
// Show current mode in status
let modeText = '';
if (this.shuffleMode) modeText += ' 🔀';
if (this.loopMode === 1) modeText += ' 🔁';
else if (this.loopMode === 2) modeText += ' 🔂';
statusSpan.textContent = `${currentTrack.name}${modeText}`;
} else {
toggleBtn.classList.remove('playing');
toggleBtn.innerHTML = '🎵';
statusSpan.textContent = 'Music: Off';
}
}
updateTrackSelector() {
const trackSelector = document.getElementById('track-selector');
if (!trackSelector) return;
// Clear existing options
trackSelector.innerHTML = '';
// Add all tracks (built-in and custom)
this.tracks.forEach((track, index) => {
const option = document.createElement('option');
option.value = index;
option.textContent = track.isCustom ? `${track.name} (Custom)` : track.name;
trackSelector.appendChild(option);
});
// Set current selection
trackSelector.value = this.currentTrackIndex;
}
}
// 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');
this.setupMessagesTabListeners();
this.setupAppearanceTabListeners();
this.setupBehaviorTabListeners();
this.setupPopupImagesTabListeners();
this.setupImportExportTabListeners();
};
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;
}
};
// 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 = {
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.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 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');
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 = () => {
countValue.textContent = countSlider.value;
const config = this.popupImageManager.getConfig();
config.imageCount = parseInt(countSlider.value);
this.popupImageManager.updateConfig(config);
};
}
// 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();
config.maxCount = parseInt(maxCountInput.value);
this.popupImageManager.updateConfig(config);
};
}
// 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
this.updatePopupImagesInfo();
};
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;
}
};
// 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);
};
// Initialize game when page loads
document.addEventListener('DOMContentLoaded', () => {
window.game = new TaskChallengeGame();
});