feat: Add dynamic aspect ratio sizing to punishment popups

- Modified PopupImageManager to calculate popup size based on image aspect ratio
- Added size configuration options (min/max width/height, viewport ratios)
- Enhanced popup positioning to handle variable-sized popups with collision detection
- Added comprehensive size controls to Annoyance Management interface
- Implemented asynchronous image loading to determine proper dimensions
- Added padding between popups to prevent visual overlap
- Updated default configuration with size constraints and viewport ratio settings
This commit is contained in:
dilgenfritz 2025-09-28 21:53:58 -05:00
parent 6fad5af73f
commit 22a1642faa
6 changed files with 1361 additions and 0 deletions

311
game.js
View File

@ -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');

View File

@ -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
}
};

View File

@ -352,6 +352,7 @@
<button id="messages-tab" class="annoyance-tab active">💬 Messages</button>
<button id="appearance-tab" class="annoyance-tab">🎨 Appearance</button>
<button id="behavior-tab" class="annoyance-tab">⚡ Behavior</button>
<button id="popup-images-tab" class="annoyance-tab">🖼️ Popup Images</button>
<button id="import-export-tab" class="annoyance-tab">📁 Import/Export</button>
</div>
@ -539,6 +540,216 @@
</div>
</div>
<!-- Popup Images Tab -->
<div id="popup-images-tab-content" class="annoyance-tab-content">
<div class="annoyance-section">
<h3>🖼️ Punishment Popups</h3>
<p class="help-text">Configure consequence images that appear when tasks are skipped</p>
<!-- Enable/Disable -->
<div class="control-section">
<div class="control-group">
<label class="switch-label">
<input type="checkbox" id="popup-images-enabled" />
<span class="switch"></span>
Enable Punishment Popups
</label>
</div>
</div>
<!-- Image Count Settings -->
<div class="control-section">
<h4>📊 Number of Images</h4>
<div class="control-group">
<label for="popup-count-mode">Count Mode:</label>
<select id="popup-count-mode">
<option value="fixed">Fixed Amount</option>
<option value="random">Random (1-5)</option>
<option value="range">Custom Range</option>
</select>
</div>
<div id="popup-fixed-count" class="control-group">
<label for="popup-image-count">Number of Images:</label>
<input type="range" id="popup-image-count" min="1" max="8" value="3" />
<span id="popup-image-count-value">3</span>
</div>
<div id="popup-range-count" class="control-group" style="display: none;">
<div class="range-inputs">
<div>
<label for="popup-min-count">Minimum:</label>
<input type="number" id="popup-min-count" min="1" max="5" value="2" />
</div>
<div>
<label for="popup-max-count">Maximum:</label>
<input type="number" id="popup-max-count" min="2" max="10" value="5" />
</div>
</div>
</div>
</div>
<!-- Display Duration Settings -->
<div class="control-section">
<h4>⏱️ Display Duration</h4>
<div class="control-group">
<label for="popup-duration-mode">Duration Mode:</label>
<select id="popup-duration-mode">
<option value="fixed">Fixed Duration</option>
<option value="random">Random (5-15s)</option>
<option value="range">Custom Range</option>
</select>
</div>
<div id="popup-fixed-duration" class="control-group">
<label for="popup-display-duration">Duration (seconds):</label>
<input type="range" id="popup-display-duration" min="3" max="30" value="8" />
<span id="popup-display-duration-value">8s</span>
</div>
<div id="popup-range-duration" class="control-group" style="display: none;">
<div class="range-inputs">
<div>
<label for="popup-min-duration">Min (seconds):</label>
<input type="number" id="popup-min-duration" min="2" max="20" value="5" />
</div>
<div>
<label for="popup-max-duration">Max (seconds):</label>
<input type="number" id="popup-max-duration" min="5" max="60" value="15" />
</div>
</div>
</div>
</div>
<!-- Positioning & Appearance -->
<div class="control-section">
<h4>🎯 Positioning</h4>
<div class="control-group">
<label for="popup-positioning">Layout Style:</label>
<select id="popup-positioning">
<option value="random">Random Positions</option>
<option value="cascade">Cascading</option>
<option value="grid">Grid Layout</option>
<option value="center">Centered (stacked)</option>
</select>
</div>
<div class="control-group">
<label class="switch-label">
<input type="checkbox" id="popup-allow-overlap" />
<span class="switch"></span>
Allow Overlapping
</label>
</div>
</div>
<!-- Size Settings -->
<div class="control-section">
<h4>📏 Size Settings</h4>
<p class="help-text">Popups automatically size to match image proportions within these limits</p>
<div class="control-group">
<label for="popup-viewport-width">Max Viewport Width:</label>
<input type="range" id="popup-viewport-width" min="20" max="60" value="35" />
<span id="popup-viewport-width-value">35%</span>
</div>
<div class="control-group">
<label for="popup-viewport-height">Max Viewport Height:</label>
<input type="range" id="popup-viewport-height" min="20" max="60" value="40" />
<span id="popup-viewport-height-value">40%</span>
</div>
<div class="control-group">
<div class="range-inputs">
<div>
<label for="popup-min-width">Min Width (px):</label>
<input type="number" id="popup-min-width" min="150" max="400" value="200" />
</div>
<div>
<label for="popup-max-width">Max Width (px):</label>
<input type="number" id="popup-max-width" min="300" max="800" value="500" />
</div>
</div>
</div>
<div class="control-group">
<div class="range-inputs">
<div>
<label for="popup-min-height">Min Height (px):</label>
<input type="number" id="popup-min-height" min="100" max="300" value="150" />
</div>
<div>
<label for="popup-max-height">Max Height (px):</label>
<input type="number" id="popup-max-height" min="200" max="600" value="400" />
</div>
</div>
</div>
</div>
<!-- Visual Effects -->
<div class="control-section">
<h4>✨ Visual Effects</h4>
<div class="control-group">
<label class="switch-label">
<input type="checkbox" id="popup-fade-animation" />
<span class="switch"></span>
Fade In/Out Animation
</label>
</div>
<div class="control-group">
<label class="switch-label">
<input type="checkbox" id="popup-blur-background" />
<span class="switch"></span>
Blur Background
</label>
</div>
<div class="control-group">
<label class="switch-label">
<input type="checkbox" id="popup-show-timer" />
<span class="switch"></span>
Show Countdown Timer
</label>
</div>
<div class="control-group">
<label class="switch-label">
<input type="checkbox" id="popup-prevent-close" />
<span class="switch"></span>
Prevent Manual Close
</label>
</div>
</div>
<!-- Test & Preview -->
<div class="control-section">
<h4>🧪 Testing</h4>
<div class="test-buttons">
<button id="test-popup-single" class="btn btn-info">Test 1 Popup</button>
<button id="test-popup-multiple" class="btn btn-primary">Test Multiple</button>
<button id="clear-all-popups" class="btn btn-danger">Clear All</button>
</div>
<p class="help-text">Test your popup settings to see how they look</p>
</div>
<!-- Status & Info -->
<div class="control-section">
<div class="info-display">
<div class="info-item">
<span class="info-label">Available Images:</span>
<span id="available-images-count">0</span>
</div>
<div class="info-item">
<span class="info-label">Active Popups:</span>
<span id="active-popups-count">0</span>
</div>
</div>
</div>
</div>
</div>
<!-- Import/Export Tab -->
<div id="import-export-tab-content" class="annoyance-tab-content">
<div class="annoyance-section">
@ -667,6 +878,7 @@
<script src="gameData.js"></script>
<script src="flashMessageManager.js"></script>
<script src="popupImageManager.js"></script>
<script src="desktop-file-manager.js"></script>
<script src="game.js"></script>
<!-- Statistics Modal -->

559
popupImageManager.js Normal file
View File

@ -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
};
}
}

View File

@ -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;
}

8
webGame.code-workspace Normal file
View File

@ -0,0 +1,8 @@
{
"folders": [
{
"path": "."
}
],
"settings": {}
}