training-academy/popupImageManager.js

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