/** * Flash Message Manager - Handles displaying encouraging messages during gameplay * Part of the Annoyance system for customizable user motivation */ class FlashMessageManager { constructor(dataManager) { this.dataManager = dataManager; this.isActive = false; this.isPaused = false; this.currentTimeout = null; this.messageElement = null; this.config = null; this.messages = []; this.lastMessageIndex = -1; this.messageQueue = []; this.init(); } init() { // Load configuration and messages this.loadConfiguration(); this.loadMessages(); this.createMessageElement(); console.log('FlashMessageManager initialized'); console.log(`Loaded ${this.messages.length} flash messages`); } loadConfiguration() { // Get saved config or use defaults const savedConfig = this.dataManager.get('flashMessageConfig'); this.config = { ...gameData.defaultFlashConfig, ...(savedConfig || {}) }; } loadMessages() { // Get custom messages or use defaults const customMessages = this.dataManager.get('customFlashMessages'); if (customMessages && customMessages.length > 0) { this.messages = customMessages.filter(msg => msg.enabled !== false); } else { // Use default messages this.messages = gameData.defaultFlashMessages.filter(msg => msg.enabled !== false); } } /** * Public show method for compatibility with game.js * @param {string|object} message - Message text or object * @param {string} type - Message type (info, error, etc.) */ show(message, type = 'info') { // Optionally style based on type let config = {}; if (type === 'error') { config = { color: '#fff', backgroundColor: '#d32f2f' }; } else if (type === 'info') { config = { color: '#fff', backgroundColor: '#1976d2' }; } this.showMessage(message, config); } createMessageElement() { // Create the overlay element for flash messages this.messageElement = document.createElement('div'); this.messageElement.id = 'flash-message-overlay'; this.messageElement.className = 'flash-message-overlay'; this.messageElement.style.display = 'none'; // Apply default styles this.applyMessageStyles(); // Add to body document.body.appendChild(this.messageElement); } applyMessageStyles() { if (!this.messageElement) return; // Base styles that are always applied Object.assign(this.messageElement.style, { position: 'fixed', display: 'none', fontSize: this.config.fontSize, fontWeight: this.config.fontWeight, color: this.config.color, backgroundColor: this.config.backgroundColor, borderRadius: this.config.borderRadius, padding: this.config.padding, maxWidth: this.config.maxWidth, zIndex: this.config.zIndex, textAlign: 'center', wordWrap: 'break-word', boxShadow: '0 4px 20px rgba(0, 0, 0, 0.3)', backdropFilter: 'blur(5px)', border: '2px solid rgba(255, 255, 255, 0.2)', transition: 'opacity 0.5s ease-in-out, transform 0.5s ease-in-out' }); // Position-based styles this.applyPositionStyles(); } applyPositionStyles() { if (!this.messageElement) return; // Reset position styles this.messageElement.style.top = ''; this.messageElement.style.bottom = ''; this.messageElement.style.left = ''; this.messageElement.style.right = ''; this.messageElement.style.transform = ''; switch (this.config.position) { case 'top-left': this.messageElement.style.top = '20px'; this.messageElement.style.left = '20px'; break; case 'top-center': this.messageElement.style.top = '20px'; this.messageElement.style.left = '50%'; this.messageElement.style.transform = 'translateX(-50%)'; break; case 'top-right': this.messageElement.style.top = '20px'; this.messageElement.style.right = '20px'; break; case 'center-left': this.messageElement.style.top = '50%'; this.messageElement.style.left = '20px'; this.messageElement.style.transform = 'translateY(-50%)'; break; case 'center': default: this.messageElement.style.top = '50%'; this.messageElement.style.left = '50%'; this.messageElement.style.transform = 'translate(-50%, -50%)'; break; case 'center-right': this.messageElement.style.top = '50%'; this.messageElement.style.right = '20px'; this.messageElement.style.transform = 'translateY(-50%)'; break; case 'bottom-left': this.messageElement.style.bottom = '20px'; this.messageElement.style.left = '20px'; break; case 'bottom-center': this.messageElement.style.bottom = '20px'; this.messageElement.style.left = '50%'; this.messageElement.style.transform = 'translateX(-50%)'; break; case 'bottom-right': this.messageElement.style.bottom = '20px'; this.messageElement.style.right = '20px'; break; } } start() { if (!this.config.enabled || this.messages.length === 0) { console.log('Flash messages disabled or no messages available'); return; } this.isActive = true; this.isPaused = false; this.scheduleNext(); } stop() { this.isActive = false; this.isPaused = false; this.clearCurrentTimeout(); this.hideCurrentMessage(); console.log('Flash message system stopped'); } pause() { if (this.isActive) { this.isPaused = true; this.clearCurrentTimeout(); this.hideCurrentMessage(); console.log('Flash message system paused'); } } resume() { if (this.isActive && this.isPaused) { this.isPaused = false; this.scheduleNext(); console.log('Flash message system resumed'); } } scheduleNext() { if (!this.isActive || this.isPaused) return; // Clear any existing timeout this.clearCurrentTimeout(); // Schedule the next message const delay = this.config.intervalDelay + (Math.random() * 10000 - 5000); // ±5 seconds variation this.currentTimeout = setTimeout(() => { this.showRandomMessage(); this.scheduleNext(); // Schedule the next one }, Math.max(delay, 5000)); // Minimum 5 seconds between messages } clearCurrentTimeout() { if (this.currentTimeout) { clearTimeout(this.currentTimeout); this.currentTimeout = null; } } showRandomMessage() { if (!this.isActive || this.isPaused || this.messages.length === 0) return; // Pick a random message (avoid repeating the last one if possible) let messageIndex; if (this.messages.length > 1) { do { messageIndex = Math.floor(Math.random() * this.messages.length); } while (messageIndex === this.lastMessageIndex); } else { messageIndex = 0; } const message = this.messages[messageIndex]; this.lastMessageIndex = messageIndex; this.showMessage(message); } showMessage(messageObj, customConfig = {}) { if (!this.messageElement) return; // Apply any custom configuration temporarily const tempConfig = { ...this.config, ...customConfig }; // Set the message text this.messageElement.textContent = messageObj.text || messageObj; // Apply styles (in case config changed) this.applyMessageStyles(); // Show the message with animation this.messageElement.style.display = 'block'; this.messageElement.style.opacity = '0'; // Trigger animation based on config requestAnimationFrame(() => { this.applyShowAnimation(tempConfig.animation); }); // Hide the message after the display duration setTimeout(() => { this.hideCurrentMessage(tempConfig.animation); }, tempConfig.displayDuration); } applyShowAnimation(animationType = 'fade') { if (!this.messageElement) return; switch (animationType) { case 'slide': this.messageElement.style.opacity = '1'; this.messageElement.style.transform = this.messageElement.style.transform.replace('translateY(50px)', ''); break; case 'bounce': this.messageElement.style.opacity = '1'; this.messageElement.style.animation = 'flashBounceIn 0.6s ease-out'; break; case 'pulse': this.messageElement.style.opacity = '1'; this.messageElement.style.animation = 'flashPulseIn 0.8s ease-out'; break; case 'fade': default: this.messageElement.style.opacity = '1'; break; } } hideCurrentMessage(animationType = 'fade') { if (!this.messageElement) return; switch (animationType) { case 'slide': this.messageElement.style.transform += ' translateY(-50px)'; this.messageElement.style.opacity = '0'; break; case 'bounce': case 'pulse': this.messageElement.style.animation = 'flashFadeOut 0.5s ease-in'; break; case 'fade': default: this.messageElement.style.opacity = '0'; break; } // Hide the element completely after animation setTimeout(() => { if (this.messageElement) { this.messageElement.style.display = 'none'; this.messageElement.style.animation = ''; } }, 500); } // Manual message triggering for specific events triggerEventMessage(eventType, customMessage = null) { if (!this.isActive || this.isPaused) return; let message = customMessage; if (!message) { // Find messages for specific event types const eventMessages = this.messages.filter(msg => { switch (eventType) { case 'taskComplete': return msg.category === 'achievement'; case 'taskSkip': return msg.category === 'persistence'; case 'gameStart': return msg.category === 'motivational'; case 'streak': return msg.category === 'achievement'; default: return msg.category === 'encouraging'; } }); if (eventMessages.length > 0) { message = eventMessages[Math.floor(Math.random() * eventMessages.length)]; } else { // Fallback to any available message message = this.messages[Math.floor(Math.random() * this.messages.length)]; } } if (message) { // Clear current schedule temporarily to show event message this.clearCurrentTimeout(); this.showMessage(message); // Resume normal scheduling after a short delay setTimeout(() => { if (this.isActive && !this.isPaused) { this.scheduleNext(); } }, this.config.displayDuration + 2000); } } // Configuration management updateConfig(newConfig) { this.config = { ...this.config, ...newConfig }; this.dataManager.set('flashMessageConfig', this.config); // Apply new styles this.applyMessageStyles(); // Restart if active to apply new timing if (this.isActive) { this.stop(); this.start(); } console.log('Flash message config updated:', newConfig); } updateMessages(newMessages) { this.messages = newMessages.filter(msg => msg.enabled !== false); this.dataManager.set('customFlashMessages', newMessages); console.log(`Updated messages: ${this.messages.length} active messages`); } // Enhanced message management addMessage(messageData) { const customMessages = this.dataManager.get('customFlashMessages') || [...gameData.defaultFlashMessages]; const newMessage = { id: Date.now(), text: messageData.text, category: messageData.category || 'custom', priority: messageData.priority || 'normal', enabled: true, isCustom: true, createdAt: new Date().toISOString() }; customMessages.push(newMessage); this.updateMessages(customMessages); return newMessage; } editMessage(messageId, updates) { const customMessages = this.dataManager.get('customFlashMessages') || [...gameData.defaultFlashMessages]; const messageIndex = customMessages.findIndex(msg => msg.id === messageId); if (messageIndex !== -1) { customMessages[messageIndex] = { ...customMessages[messageIndex], ...updates }; this.updateMessages(customMessages); return customMessages[messageIndex]; } return null; } deleteMessage(messageId) { const customMessages = this.dataManager.get('customFlashMessages') || [...gameData.defaultFlashMessages]; const filteredMessages = customMessages.filter(msg => msg.id !== messageId); this.updateMessages(filteredMessages); return filteredMessages.length < customMessages.length; } toggleMessageEnabled(messageId) { const customMessages = this.dataManager.get('customFlashMessages') || [...gameData.defaultFlashMessages]; const messageIndex = customMessages.findIndex(msg => msg.id === messageId); if (messageIndex !== -1) { customMessages[messageIndex].enabled = !customMessages[messageIndex].enabled; this.updateMessages(customMessages); return customMessages[messageIndex].enabled; } return false; } // Advanced filtering and categorization getMessagesByCategory(category) { if (category === 'all') return this.getAllMessages(); return this.getAllMessages().filter(msg => msg.category === category); } getMessageStats() { const allMessages = this.getAllMessages(); const enabledMessages = allMessages.filter(msg => msg.enabled !== false); const disabledMessages = allMessages.filter(msg => msg.enabled === false); const categories = {}; allMessages.forEach(msg => { const cat = msg.category || 'custom'; categories[cat] = (categories[cat] || 0) + 1; }); return { total: allMessages.length, enabled: enabledMessages.length, disabled: disabledMessages.length, custom: allMessages.filter(msg => msg.isCustom).length, categories: categories }; } // Import/Export functionality exportMessages(includeDisabled = true, customOnly = false) { const allMessages = this.getAllMessages(); let messagesToExport = allMessages; if (!includeDisabled) { messagesToExport = messagesToExport.filter(msg => msg.enabled !== false); } if (customOnly) { messagesToExport = messagesToExport.filter(msg => msg.isCustom); } const exportData = { messages: messagesToExport, config: this.getConfig(), exportedAt: new Date().toISOString(), version: "2.0" }; return JSON.stringify(exportData, null, 2); } importMessages(jsonData, mode = 'merge') { try { const importData = JSON.parse(jsonData); const importedMessages = importData.messages || []; let finalMessages = []; if (mode === 'replace') { // Replace all messages finalMessages = importedMessages.map(msg => ({ ...msg, id: msg.id || Date.now() + Math.random(), importedAt: new Date().toISOString() })); } else { // Merge with existing messages const existingMessages = this.getAllMessages(); const existingIds = new Set(existingMessages.map(msg => msg.id)); finalMessages = [...existingMessages]; importedMessages.forEach(msg => { if (!existingIds.has(msg.id)) { finalMessages.push({ ...msg, id: msg.id || Date.now() + Math.random(), importedAt: new Date().toISOString() }); } }); } this.updateMessages(finalMessages); // Optionally import config if (importData.config) { this.updateConfig(importData.config); } return { success: true, imported: importedMessages.length, total: finalMessages.length }; } catch (error) { return { success: false, error: error.message }; } } resetToDefaults() { this.dataManager.set('customFlashMessages', null); this.dataManager.set('flashMessageConfig', null); this.loadConfiguration(); this.loadMessages(); return { messages: this.getAllMessages().length, config: this.getConfig() }; } // Utility methods isEnabled() { return this.config.enabled; } getConfig() { return { ...this.config }; } getMessages() { return [...this.messages]; } getAllMessages() { // Return both enabled and disabled messages for management const customMessages = this.dataManager.get('customFlashMessages'); return customMessages || [...gameData.defaultFlashMessages]; } // Preview method for testing previewMessage(messageObj, customConfig = {}) { const wasActive = this.isActive; this.isActive = true; // Temporarily enable to show preview this.showMessage(messageObj, customConfig); this.isActive = wasActive; // Restore previous state } // Cleanup destroy() { this.stop(); if (this.messageElement && this.messageElement.parentNode) { this.messageElement.parentNode.removeChild(this.messageElement); } this.messageElement = null; } }