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