diff --git a/game.js b/game.js index 8695de6..e746bcc 100644 --- a/game.js +++ b/game.js @@ -42,6 +42,9 @@ class TaskChallengeGame { // 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(); @@ -2875,6 +2878,9 @@ ${usagePercent > 85 ? 'โš ๏ธ Storage getting full - consider deleting some imag // 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(); @@ -3937,11 +3943,13 @@ TaskChallengeGame.prototype.setupAnnoyanceManagementEventListeners = function() 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(); }; @@ -3965,6 +3973,9 @@ TaskChallengeGame.prototype.showAnnoyanceTab = function(tabName) { case 'behavior': this.loadBehaviorTab(); break; + case 'popup-images': + this.loadPopupImagesSettings(); + break; case 'import-export': this.loadImportExportTab(); break; @@ -4451,6 +4462,306 @@ TaskChallengeGame.prototype.testCurrentBehaviorSettings = function() { 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'); diff --git a/gameData.js b/gameData.js index 1f99d92..f7d92d2 100644 --- a/gameData.js +++ b/gameData.js @@ -286,5 +286,33 @@ const gameData = { padding: '20px 30px', maxWidth: '400px', zIndex: 10000 + }, + + // Default Popup Image Configuration (Punishment System) + defaultPopupImageConfig: { + enabled: true, + imageCount: 3, // Number of images to show + imageCountMode: 'fixed', // 'fixed', 'random', 'range' + minCount: 2, // For range mode + maxCount: 5, // For range mode + displayDuration: 8000, // 8 seconds default + durationMode: 'fixed', // 'fixed', 'random', 'range' + minDuration: 5000, // For range mode (5s) + maxDuration: 15000, // For range mode (15s) + positioning: 'random', // 'random', 'cascade', 'grid', 'center' + allowOverlap: true, + fadeAnimation: true, + blurBackground: true, + preventClose: true, // Users cannot close these + showTimer: true, // Show countdown timer + triggerOnSkip: true, // Trigger when tasks are skipped + intensity: 'medium', // 'low', 'medium', 'high' - affects default values + // Size constraints for dynamic sizing + minWidth: 200, + maxWidth: 500, + minHeight: 150, + maxHeight: 400, + viewportWidthRatio: 0.35, // Max 35% of viewport width + viewportHeightRatio: 0.4 // Max 40% of viewport height } }; \ No newline at end of file diff --git a/index.html b/index.html index 7927a61..77c5180 100644 --- a/index.html +++ b/index.html @@ -352,6 +352,7 @@ + @@ -539,6 +540,216 @@ + + +
@@ -667,6 +878,7 @@ + diff --git a/popupImageManager.js b/popupImageManager.js new file mode 100644 index 0000000..43eec2a --- /dev/null +++ b/popupImageManager.js @@ -0,0 +1,559 @@ +/** + * Popup Image Manager - Handles punishment popups when tasks are skipped + * Part of the Annoyance system for consequence enforcement + */ +class PopupImageManager { + constructor(dataManager) { + this.dataManager = dataManager; + this.activePopups = []; + this.config = null; + this.isEnabled = true; + + this.init(); + } + + init() { + this.loadConfiguration(); + console.log('PopupImageManager initialized'); + } + + 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 slight delays + popupConfigs.forEach((config, index) => { + setTimeout(() => { + this.createPopup(config); + }, index * 300); // 300ms delay between each popup to allow for image loading + }); + + 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() * 5) + 1; // 1-5 images + 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 + }; + } +} \ No newline at end of file diff --git a/styles.css b/styles.css index 06f23ae..eda749f 100644 --- a/styles.css +++ b/styles.css @@ -2666,4 +2666,247 @@ body.theme-monochrome { .annoyance-section { padding: 15px; } +} + +/* ====================================== + Punishment Popup System Styles + ====================================== */ + +/* Background blur for punishment popups */ +.punishment-popup-blur { + 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; +} + +/* Individual punishment popup */ +.punishment-popup { + position: fixed; + 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; + font-family: var(--font-family); + min-width: 200px; + min-height: 150px; + max-width: 500px; + max-height: 400px; +} + +/* Popup header with timer and title */ +.punishment-popup-header { + background: #dc3545; + color: white; + padding: 8px 12px; + font-size: 12px; + font-weight: bold; + display: flex; + justify-content: space-between; + align-items: center; + user-select: none; +} + +.punishment-popup-timer { + background: rgba(255, 255, 255, 0.2); + padding: 2px 6px; + border-radius: 4px; + font-family: monospace; + font-size: 11px; + min-width: 30px; + text-align: center; +} + +/* Image container within popup */ +.punishment-popup img { + max-width: 100%; + max-height: 100%; + object-fit: contain; + border-radius: 5px; +} + +/* Popup Images Tab specific styles */ +.range-inputs { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 15px; + margin-top: 10px; +} + +.range-inputs > div { + display: flex; + flex-direction: column; + gap: 5px; +} + +.range-inputs input[type="number"] { + padding: 8px; + border: 1px solid #ddd; + border-radius: 5px; + font-size: 14px; + background: white; + color: #333; +} + +.range-inputs input[type="number"]:focus { + outline: none; + border-color: #007bff; + box-shadow: 0 0 5px rgba(0, 123, 255, 0.3); +} + +.test-buttons { + display: flex; + gap: 10px; + flex-wrap: wrap; + margin-top: 10px; +} + +.test-buttons .btn { + flex: 1; + min-width: 120px; + padding: 10px 15px; + font-size: 14px; + font-weight: 500; + border: none; + border-radius: 6px; + cursor: pointer; + transition: all 0.2s ease; + text-align: center; +} + +.test-buttons .btn-info { + background: #17a2b8; + color: white; +} + +.test-buttons .btn-info:hover { + background: #138496; + transform: translateY(-1px); +} + +.test-buttons .btn-primary { + background: #007bff; + color: white; +} + +.test-buttons .btn-primary:hover { + background: #0056b3; + transform: translateY(-1px); +} + +.test-buttons .btn-danger { + background: #dc3545; + color: white; +} + +.test-buttons .btn-danger:hover { + background: #c82333; + transform: translateY(-1px); +} + +.info-display { + background: #f8f9fa; + padding: 15px; + border-radius: 8px; + border-left: 4px solid #007bff; + margin-top: 10px; +} + +.info-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 5px 0; + border-bottom: 1px solid rgba(0, 0, 0, 0.1); +} + +.info-item:last-child { + border-bottom: none; +} + +.info-label { + font-weight: 500; + color: #495057; +} + +.info-item span:last-child { + font-weight: bold; + color: #007bff; + background: rgba(0, 123, 255, 0.1); + padding: 2px 8px; + border-radius: 12px; + font-size: 12px; + min-width: 30px; + text-align: center; +} + +/* Responsive styles for popup images tab */ +@media (max-width: 768px) { + .range-inputs { + grid-template-columns: 1fr; + gap: 10px; + } + + .test-buttons { + flex-direction: column; + } + + .test-buttons .btn { + min-width: 100%; + } + + .punishment-popup { + max-width: 90vw; + max-height: 80vh; + } + + .info-item { + flex-direction: column; + align-items: flex-start; + gap: 5px; + } + + .info-item span:last-child { + align-self: flex-end; + } +} + +/* Animation for popup appearance */ +@keyframes popupFadeIn { + from { + opacity: 0; + transform: scale(0.8); + } + to { + opacity: 1; + transform: scale(1); + } +} + +@keyframes popupFadeOut { + from { + opacity: 1; + transform: scale(1); + } + to { + opacity: 0; + transform: scale(0.9); + } +} + +.punishment-popup { + animation: popupFadeIn 0.3s ease-out; +} + +.punishment-popup.fade-out { + animation: popupFadeOut 0.3s ease-in; } \ No newline at end of file diff --git a/webGame.code-workspace b/webGame.code-workspace new file mode 100644 index 0000000..876a149 --- /dev/null +++ b/webGame.code-workspace @@ -0,0 +1,8 @@ +{ + "folders": [ + { + "path": "." + } + ], + "settings": {} +} \ No newline at end of file