/** * Popup Image Manager - Handles both punishment popups and periodic random image popups * Part of the Annoyance system for consequence enforcement + periodic visual enhancement */ class PopupImageManager { constructor(dataManager) { this.dataManager = dataManager; this.activePopups = []; this.config = null; this.isEnabled = true; // Periodic popup system this.periodicSystem = { isActive: false, interval: null, minInterval: 30000, // 30 seconds maxInterval: 120000, // 2 minutes displayDuration: 5000, // 5 seconds history: [], maxHistorySize: 20 }; this.init(); } init() { this.loadConfiguration(); console.log('PopupImageManager initialized with periodic system'); } /** * Start periodic random image popups */ startPeriodicPopups() { if (this.periodicSystem.isActive) { console.log('⚠️ Periodic popup system already active'); return; } this.periodicSystem.isActive = true; this.scheduleNextPeriodicPopup(); console.log('🚀 Periodic image popup system started'); } /** * Stop periodic random image popups */ stopPeriodicPopups() { if (!this.periodicSystem.isActive) { return; } this.periodicSystem.isActive = false; if (this.periodicSystem.interval) { clearTimeout(this.periodicSystem.interval); this.periodicSystem.interval = null; } console.log('🛑 Periodic image popup system stopped'); } /** * Schedule the next periodic popup */ scheduleNextPeriodicPopup() { if (!this.periodicSystem.isActive) { return; } const { minInterval, maxInterval } = this.periodicSystem; const interval = Math.random() * (maxInterval - minInterval) + minInterval; this.periodicSystem.interval = setTimeout(() => { this.showPeriodicPopup(); this.scheduleNextPeriodicPopup(); }, interval); console.log(`⏰ Next periodic popup in ${Math.round(interval / 1000)} seconds`); } /** * Show a periodic random popup */ showPeriodicPopup() { if (!this.periodicSystem.isActive) { return; } const image = this.getRandomPeriodicImage(); if (!image) { console.log('⚠️ No images available for periodic popup'); return; } this.displayPeriodicPopup(image); } /** * Get random image for periodic popups (from both folders) */ getRandomPeriodicImage() { const taskImages = gameData?.discoveredTaskImages || []; const consequenceImages = gameData?.discoveredConsequenceImages || []; const allImages = [...taskImages, ...consequenceImages]; if (allImages.length === 0) { return null; } // Filter out disabled images and recent history const disabledImages = this.dataManager.get('disabledImages') || []; const { history } = this.periodicSystem; const availableImages = allImages.filter(img => { const imagePath = typeof img === 'string' ? img : (img.cachedPath || img.originalName); return !disabledImages.includes(imagePath) && !history.includes(imagePath); }); // If all images are recent, clear history if (availableImages.length === 0) { this.periodicSystem.history = []; const nonDisabledImages = allImages.filter(img => { const imagePath = typeof img === 'string' ? img : (img.cachedPath || img.originalName); return !disabledImages.includes(imagePath); }); if (nonDisabledImages.length === 0) { return null; } const randomIndex = Math.floor(Math.random() * nonDisabledImages.length); return nonDisabledImages[randomIndex]; } const randomIndex = Math.floor(Math.random() * availableImages.length); const selectedImage = availableImages[randomIndex]; // Add to history const imagePath = typeof selectedImage === 'string' ? selectedImage : (selectedImage.cachedPath || selectedImage.originalName); this.periodicSystem.history.push(imagePath); if (this.periodicSystem.history.length > this.periodicSystem.maxHistorySize) { this.periodicSystem.history.shift(); } return selectedImage; } /** * Display periodic popup */ displayPeriodicPopup(imageData) { const imageSrc = this.getImageSrc(imageData); const imageName = typeof imageData === 'string' ? 'Random Image' : (imageData.originalName || 'Random Image'); const popup = document.createElement('div'); popup.className = 'periodic-popup-container'; popup.innerHTML = `
💫 Random Visual
Random Popup
`; this.ensurePeriodicStyles(); document.body.appendChild(popup); // Animate in setTimeout(() => { popup.classList.add('periodic-visible'); }, 50); // Auto-hide setTimeout(() => { this.hidePeriodicPopup(popup); }, this.periodicSystem.displayDuration); console.log(`🖼️ Showing periodic popup: ${imageName}`); } /** * Hide periodic popup */ hidePeriodicPopup(popup) { if (!popup || !popup.parentNode) { return; } popup.classList.add('periodic-hiding'); setTimeout(() => { if (popup && popup.parentNode) { popup.parentNode.removeChild(popup); } }, 300); } /** * Ensure periodic popup styles */ ensurePeriodicStyles() { if (document.querySelector('#periodic-popup-styles')) { return; } const styles = document.createElement('style'); styles.id = 'periodic-popup-styles'; styles.textContent = ` .periodic-popup-container { position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 10001; opacity: 0; transition: opacity 0.3s ease; pointer-events: none; } .periodic-popup-container.periodic-visible { opacity: 1; pointer-events: all; } .periodic-popup-container.periodic-hiding { opacity: 0; pointer-events: none; } .periodic-popup-backdrop { width: 100%; height: 100%; background: rgba(0, 0, 0, 0.6); display: flex; align-items: center; justify-content: center; cursor: pointer; } .periodic-popup-content { background: white; border-radius: 12px; box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3); max-width: 70vw; max-height: 70vh; display: flex; flex-direction: column; cursor: default; animation: periodicSlideIn 0.3s ease-out; } @keyframes periodicSlideIn { from { transform: translateY(-50px) scale(0.9); opacity: 0; } to { transform: translateY(0) scale(1); opacity: 1; } } .periodic-popup-header { padding: 12px 18px; border-bottom: 1px solid #eee; display: flex; justify-content: space-between; align-items: center; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border-radius: 12px 12px 0 0; } .periodic-popup-title { font-weight: bold; font-size: 14px; } .periodic-popup-close { background: none; border: none; color: white; font-size: 20px; cursor: pointer; padding: 0; width: 25px; height: 25px; border-radius: 50%; transition: background-color 0.2s; } .periodic-popup-close:hover { background-color: rgba(255, 255, 255, 0.2); } .periodic-popup-image-wrapper { padding: 15px; display: flex; justify-content: center; align-items: center; min-height: 150px; } .periodic-popup-image { max-width: 100%; max-height: 50vh; border-radius: 6px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); } .periodic-popup-footer { padding: 8px 18px; border-top: 1px solid #eee; text-align: center; color: #666; font-size: 12px; border-radius: 0 0 12px 12px; background-color: #f8f9fa; } @media (max-width: 768px) { .periodic-popup-content { margin: 20px; max-width: calc(100vw - 40px); max-height: calc(100vh - 40px); } } `; document.head.appendChild(styles); } /** * Update periodic popup settings */ updatePeriodicSettings(settings) { if (settings.minInterval) this.periodicSystem.minInterval = settings.minInterval * 1000; if (settings.maxInterval) this.periodicSystem.maxInterval = settings.maxInterval * 1000; if (settings.displayDuration) this.periodicSystem.displayDuration = settings.displayDuration * 1000; console.log('🔧 Periodic popup settings updated:', { minInterval: this.periodicSystem.minInterval / 1000 + 's', maxInterval: this.periodicSystem.maxInterval / 1000 + 's', displayDuration: this.periodicSystem.displayDuration / 1000 + 's' }); } loadConfiguration() { // Get saved config or use defaults const savedConfig = this.dataManager.get('popupImageConfig'); const defaultConfig = gameData.defaultPopupImageConfig || { enabled: true, imageCount: 3, imageCountMode: 'fixed', minCount: 2, maxCount: 5, displayDuration: 8000, durationMode: 'fixed', minDuration: 5000, maxDuration: 15000, positioning: 'random', allowOverlap: true, fadeAnimation: true, blurBackground: true, preventClose: true, showTimer: true, triggerOnSkip: true, intensity: 'medium' }; this.config = { ...defaultConfig, ...(savedConfig || {}) }; } updateConfig(newConfig) { this.config = { ...this.config, ...newConfig }; this.dataManager.set('popupImageConfig', this.config); console.log('Popup image config updated:', newConfig); } getConfig() { return { ...this.config }; } // Main method to trigger punishment popups triggerPunishmentPopups() { if (!this.config.enabled) { console.log('Punishment popups disabled'); return; } // Get consequence images const images = this.getAvailableImages(); if (images.length === 0) { console.log('No consequence images available for punishment popups'); return; } // Determine how many images to show const imageCount = this.calculateImageCount(); // Clear any existing popups first this.clearAllPopups(); // Create background blur if enabled if (this.config.blurBackground) { this.createBackgroundBlur(); } // Generate popup configurations const popupConfigs = this.generatePopupConfigs(imageCount, images); // Create and show popups with smart delays based on popup count const baseDelay = Math.max(100, Math.min(300, 3000 / popupConfigs.length)); // Dynamic delay: faster for more popups popupConfigs.forEach((config, index) => { setTimeout(() => { this.createPopup(config); }, index * baseDelay); }); console.log(`Triggered ${imageCount} punishment popups`); } getAvailableImages() { // Get consequence images from the game's discovery system const discoveredImages = gameData.discoveredConsequenceImages || []; // Get custom consequence images const customImages = this.dataManager.get('customImages') || { task: [], consequence: [] }; let customConsequenceImages = []; if (!Array.isArray(customImages)) { customConsequenceImages = customImages.consequence || []; } // Get disabled images to filter out const disabledImages = this.dataManager.get('disabledImages') || []; // Combine and filter images const allImages = [...discoveredImages, ...customConsequenceImages]; const availableImages = allImages.filter(img => { const imagePath = typeof img === 'string' ? img : (img.cachedPath || img.originalName); return !disabledImages.includes(imagePath); }); return availableImages; } calculateImageCount() { switch (this.config.imageCountMode) { case 'random': return Math.floor(Math.random() * 10) + 1; // 1-10 images (increased from 1-5) case 'range': const min = this.config.minCount; const max = this.config.maxCount; return Math.floor(Math.random() * (max - min + 1)) + min; case 'fixed': default: return this.config.imageCount; } } calculateDuration() { switch (this.config.durationMode) { case 'random': return Math.floor(Math.random() * 10000) + 5000; // 5-15 seconds case 'range': const min = this.config.minDuration; const max = this.config.maxDuration; return Math.floor(Math.random() * (max - min + 1)) + min; case 'fixed': default: return this.config.displayDuration; } } generatePopupConfigs(count, images) { const configs = []; const usedImages = []; const usedPositions = []; for (let i = 0; i < count; i++) { // Select a random image (avoid repeats if possible) let selectedImage; let attempts = 0; do { selectedImage = images[Math.floor(Math.random() * images.length)]; attempts++; } while (usedImages.includes(selectedImage) && attempts < 10 && images.length > 1); usedImages.push(selectedImage); // Get image aspect ratio by loading it temporarily const config = { image: selectedImage, position: null, // Will be set after aspect ratio is determined duration: this.calculateDuration(), index: i, id: `punishment-popup-${Date.now()}-${i}`, aspectRatio: 1.33 // Default aspect ratio }; configs.push(config); } return configs; } generatePosition(index, totalCount, usedPositions, imageAspectRatio = 1.33) { const viewport = { width: window.innerWidth, height: window.innerHeight }; // Calculate popup size based on image aspect ratio and config constraints const maxWidth = Math.min( this.config.maxWidth || 500, viewport.width * (this.config.viewportWidthRatio || 0.35) ); const maxHeight = Math.min( this.config.maxHeight || 400, viewport.height * (this.config.viewportHeightRatio || 0.4) ); const minWidth = this.config.minWidth || 200; const minHeight = this.config.minHeight || 150; let popupSize; if (imageAspectRatio > 1) { // Landscape image - constrain by width const width = Math.max(minWidth, Math.min(maxWidth, maxWidth)); const height = Math.max(minHeight, Math.min(width / imageAspectRatio + 40, maxHeight)); // +40 for header popupSize = { width, height }; } else { // Portrait or square image - constrain by height const height = Math.max(minHeight, Math.min(maxHeight, maxHeight)); const width = Math.max(minWidth, Math.min((height - 40) * imageAspectRatio, maxWidth)); // -40 for header popupSize = { width, height }; } switch (this.config.positioning) { case 'cascade': return { left: 100 + (index * 30), top: 100 + (index * 30), width: popupSize.width, height: popupSize.height }; case 'grid': const cols = Math.ceil(Math.sqrt(totalCount)); const row = Math.floor(index / cols); const col = index % cols; return { left: (viewport.width / cols) * col + (viewport.width / cols - popupSize.width) / 2, top: (viewport.height / cols) * row + (viewport.height / cols - popupSize.height) / 2, width: popupSize.width, height: popupSize.height }; case 'center': const offset = (index - Math.floor(totalCount / 2)) * 50; return { left: (viewport.width - popupSize.width) / 2 + offset, top: (viewport.height - popupSize.height) / 2 + offset, width: popupSize.width, height: popupSize.height }; case 'random': default: let position; let attempts = 0; do { position = { left: Math.random() * (viewport.width - popupSize.width), top: Math.random() * (viewport.height - popupSize.height), width: popupSize.width, height: popupSize.height }; attempts++; } while (!this.config.allowOverlap && this.overlapsExisting(position, usedPositions) && attempts < 20); usedPositions.push(position); return position; } } overlapsExisting(newPos, existingPositions) { // Add padding to prevent popups from being too close const padding = 20; return existingPositions.some(pos => { return !(newPos.left > pos.left + pos.width + padding || newPos.left + newPos.width + padding < pos.left || newPos.top > pos.top + pos.height + padding || newPos.top + newPos.height + padding < pos.top); }); } createBackgroundBlur() { const blur = document.createElement('div'); blur.id = 'punishment-popup-blur'; blur.className = 'punishment-popup-blur'; blur.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.3); backdrop-filter: blur(3px); z-index: 9999; pointer-events: none; `; document.body.appendChild(blur); if (this.config.fadeAnimation) { blur.style.opacity = '0'; requestAnimationFrame(() => { blur.style.transition = 'opacity 0.3s ease-in-out'; blur.style.opacity = '1'; }); } } createPopup(config) { // First, load the image to get its aspect ratio const tempImg = new Image(); const imageSrc = this.getImageSrc(config.image); tempImg.onload = () => { // Calculate aspect ratio const aspectRatio = tempImg.width / tempImg.height; config.aspectRatio = aspectRatio; // Generate position with proper aspect ratio const usedPositions = this.activePopups.map(p => p.config.position); config.position = this.generatePosition(config.index, 1, usedPositions, aspectRatio); // Now create the actual popup this.createPopupElement(config); }; tempImg.onerror = () => { // If image fails to load, use default aspect ratio const usedPositions = this.activePopups.map(p => p.config.position); config.position = this.generatePosition(config.index, 1, usedPositions, 1.33); this.createPopupElement(config); }; tempImg.src = imageSrc; } createPopupElement(config) { const popup = document.createElement('div'); popup.id = config.id; popup.className = 'punishment-popup'; popup.dataset.index = config.index; // Style the popup popup.style.cssText = ` position: fixed; left: ${config.position.left}px; top: ${config.position.top}px; width: ${config.position.width}px; height: ${config.position.height}px; z-index: 10000; background: white; border: 3px solid #dc3545; border-radius: 10px; box-shadow: 0 8px 32px rgba(220, 53, 69, 0.4); overflow: hidden; display: flex; flex-direction: column; pointer-events: ${this.config.preventClose ? 'none' : 'auto'}; `; // Create header with timer if (this.config.showTimer || !this.config.preventClose) { const header = document.createElement('div'); header.className = 'punishment-popup-header'; header.style.cssText = ` background: #dc3545; color: white; padding: 8px 12px; font-size: 12px; font-weight: bold; display: flex; justify-content: space-between; align-items: center; `; if (this.config.showTimer) { const timer = document.createElement('span'); timer.className = 'punishment-popup-timer'; timer.textContent = `${Math.ceil(config.duration / 1000)}s`; header.appendChild(timer); } const title = document.createElement('span'); title.textContent = 'Consequence'; header.appendChild(title); popup.appendChild(header); } // Create image container const imageContainer = document.createElement('div'); imageContainer.style.cssText = ` flex: 1; display: flex; align-items: center; justify-content: center; background: #f8f9fa; padding: 10px; `; // Create image element const img = document.createElement('img'); img.style.cssText = ` max-width: 100%; max-height: 100%; object-fit: contain; border-radius: 5px; `; // Set image source const imageSrc = this.getImageSrc(config.image); img.src = imageSrc; img.alt = 'Consequence Image'; imageContainer.appendChild(img); popup.appendChild(imageContainer); // Add to DOM document.body.appendChild(popup); // Add fade-in animation if (this.config.fadeAnimation) { popup.style.opacity = '0'; popup.style.transform = 'scale(0.8)'; popup.style.transition = 'opacity 0.3s ease-out, transform 0.3s ease-out'; requestAnimationFrame(() => { popup.style.opacity = '1'; popup.style.transform = 'scale(1)'; }); } // Add to active popups list this.activePopups.push({ element: popup, config: config, startTime: Date.now() }); // Start timer countdown if enabled if (this.config.showTimer) { this.startTimer(popup, config.duration); } // Schedule removal setTimeout(() => { this.removePopup(config.id); }, config.duration); } startTimer(popup, duration) { const timerElement = popup.querySelector('.punishment-popup-timer'); if (!timerElement) return; const startTime = Date.now(); const updateTimer = () => { const elapsed = Date.now() - startTime; const remaining = Math.max(0, duration - elapsed); const seconds = Math.ceil(remaining / 1000); timerElement.textContent = `${seconds}s`; if (remaining > 0) { requestAnimationFrame(updateTimer); } }; requestAnimationFrame(updateTimer); } removePopup(popupId) { const popupData = this.activePopups.find(p => p.config.id === popupId); if (!popupData) return; const popup = popupData.element; if (this.config.fadeAnimation) { popup.style.transition = 'opacity 0.3s ease-in-out, transform 0.3s ease-in-out'; popup.style.opacity = '0'; popup.style.transform = 'scale(0.9)'; setTimeout(() => { if (popup.parentNode) { popup.parentNode.removeChild(popup); } }, 300); } else { if (popup.parentNode) { popup.parentNode.removeChild(popup); } } // Remove from active popups this.activePopups = this.activePopups.filter(p => p.config.id !== popupId); // Remove background blur if no more popups if (this.activePopups.length === 0) { this.removeBackgroundBlur(); } } removeBackgroundBlur() { const blur = document.getElementById('punishment-popup-blur'); if (blur) { if (this.config.fadeAnimation) { blur.style.transition = 'opacity 0.3s ease-in-out'; blur.style.opacity = '0'; setTimeout(() => { if (blur.parentNode) { blur.parentNode.removeChild(blur); } }, 300); } else { blur.parentNode.removeChild(blur); } } } clearAllPopups() { // Remove all active popups immediately this.activePopups.forEach(popupData => { if (popupData.element.parentNode) { popupData.element.parentNode.removeChild(popupData.element); } }); this.activePopups = []; this.removeBackgroundBlur(); } getImageSrc(imageData) { // Handle both old path format and new cached metadata format if (typeof imageData === 'string') { return imageData; } else if (imageData.dataUrl) { return imageData.dataUrl; } else { return imageData.cachedPath || imageData.originalName || ''; } } // Preview functionality for testing previewPunishmentPopups(count = 1) { const oldConfig = { ...this.config }; this.config.imageCount = count; this.config.imageCountMode = 'fixed'; this.triggerPunishmentPopups(); // Restore config this.config = oldConfig; } // Utility methods isActive() { return this.activePopups.length > 0; } getActiveCount() { return this.activePopups.length; } getStats() { return { active: this.activePopups.length, config: this.getConfig(), availableImages: this.getAvailableImages().length }; } }