4919 lines
193 KiB
JavaScript
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();
|
|
}); |