559 lines
18 KiB
JavaScript
559 lines
18 KiB
JavaScript
/**
|
|
* 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
|
|
};
|
|
}
|
|
} |