training-academy/src/features/webcam/webcamManager.js

3345 lines
126 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* WebcamManager - Handles webcam integration for photography tasks
* Provides camera access, photo capture, and integration with interactive scenarios
*/
class WebcamManager {
constructor(game) {
this.game = game;
this.stream = null;
this.video = null;
this.canvas = null;
this.context = null;
this.isActive = false;
this.capturedPhotos = [];
this.currentPhotoSession = null;
this.preventClose = false; // Prevent camera closure during mirror sessions
console.log('🎥 WebcamManager initialized');
}
/**
* Initialize webcam functionality
*/
async init() {
console.log('🎥 Initializing webcam system...');
// Check if webcam is supported
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
console.warn('📷 Webcam not supported in this browser');
return false;
}
// Create video element for camera feed
this.video = document.createElement('video');
this.video.setAttribute('playsinline', true);
this.video.style.display = 'none';
document.body.appendChild(this.video);
// Create canvas for photo capture
this.canvas = document.createElement('canvas');
this.context = this.canvas.getContext('2d');
console.log('✅ Webcam system ready');
return true;
}
/**
* Request camera access from user
*/
async requestCameraAccess() {
console.log('📷 Requesting camera access...');
try {
const constraints = {
video: {
width: { ideal: 1280 },
height: { ideal: 720 },
facingMode: 'user' // Front-facing camera preferred
},
audio: false
};
this.stream = await navigator.mediaDevices.getUserMedia(constraints);
this.video.srcObject = this.stream;
return new Promise((resolve) => {
this.video.onloadedmetadata = () => {
this.video.play();
this.isActive = true;
console.log('✅ Camera access granted and active');
resolve(true);
};
});
} catch (error) {
console.error('❌ Camera access denied or failed:', error);
this.showCameraError(error);
return false;
}
}
/**
* Start photo session with progress tracking
*/
async startPhotoSessionWithProgress(sessionType, taskData) {
console.log(`📸 Starting photo session with progress: ${sessionType}`);
// Request camera if not already active
if (!this.isActive) {
const accessGranted = await this.requestCameraAccess();
if (!accessGranted) return false;
}
this.currentPhotoSession = {
type: sessionType,
taskData: taskData,
photos: [],
startTime: Date.now(),
requirements: taskData.requirements || { count: 1, description: 'Take photos to complete' },
photosNeeded: taskData.requirements?.count || 1
};
// Show camera interface with progress
this.showCameraInterfaceWithProgress();
return true;
}
/**
* Display camera interface with progress tracking
*/
showCameraInterfaceWithProgress() {
// Create camera overlay
const overlay = document.createElement('div');
overlay.id = 'camera-overlay';
overlay.innerHTML = `
<div class="camera-container">
<div class="camera-header">
<h3>📸 Photography Session</h3>
<p class="session-instruction">${this.currentPhotoSession.requirements.description}</p>
<div class="progress-indicator">
<span id="photos-taken">0</span> / <span id="photos-needed">${this.currentPhotoSession.photosNeeded}</span> photos
</div>
</div>
<div class="camera-preview">
<video id="camera-feed" autoplay playsinline></video>
<div class="camera-guidelines">
<div class="guideline horizontal"></div>
<div class="guideline vertical"></div>
</div>
</div>
<div class="camera-controls">
<button id="capture-photo" class="capture-btn">📷 Take Photo</button>
<button id="retake-photo" class="retake-btn" style="display: none;">🔄 Retake</button>
<button id="accept-photo" class="accept-btn" style="display: none;">✅ Accept Photo</button>
<button id="complete-session" class="complete-btn" style="display: none;">🎉 Complete Session</button>
</div>
<div class="photo-preview" id="photo-preview" style="display: none;">
<img id="captured-image" alt="Captured photo">
</div>
<div class="session-info">
<p class="privacy-note">⚠️ Photos are stored locally and not uploaded anywhere</p>
</div>
</div>
`;
document.body.appendChild(overlay);
// Connect video stream to preview
const cameraFeed = document.getElementById('camera-feed');
cameraFeed.srcObject = this.stream;
// Bind camera controls with progress
this.bindCameraControlsWithProgress();
// Add CSS styles
this.addCameraStyles();
}
/**
* Bind camera controls with progress tracking
*/
bindCameraControlsWithProgress() {
document.getElementById('capture-photo').addEventListener('click', () => {
this.startPhotoTimer();
});
document.getElementById('retake-photo').addEventListener('click', () => {
this.showCameraPreview();
});
document.getElementById('accept-photo').addEventListener('click', () => {
this.acceptPhotoWithProgress();
});
document.getElementById('complete-session').addEventListener('click', () => {
this.completePhotoSession();
});
}
/**
* Accept photo and update progress
*/
acceptPhotoWithProgress() {
const image = document.getElementById('captured-image');
const photoData = {
dataURL: image.src,
timestamp: Date.now(),
sessionType: this.currentPhotoSession.type,
taskData: this.currentPhotoSession.taskData
};
// Add to session photos
this.currentPhotoSession.photos.push(photoData);
this.capturedPhotos.push(photoData);
// Track photo for XP (1 XP per photo)
if (this.game && this.game.incrementPhotosTaken) {
this.game.incrementPhotosTaken();
}
// Update progress indicators
const photosTaken = this.currentPhotoSession.photos.length;
const photosNeeded = this.currentPhotoSession.photosNeeded;
document.getElementById('photos-taken').textContent = photosTaken;
// Save photo data
this.savePhotoData(photoData);
// Check if session is complete
if (photosTaken >= photosNeeded) {
// For single-photo sessions (like Quick Play), auto-complete
if (photosNeeded === 1) {
console.log(`✅ Single-photo session complete! Auto-completing...`);
// Small delay to let user see the completion message
setTimeout(() => {
this.completePhotoSession();
}, 1000);
} else {
// Show completion button for multi-photo sessions
document.getElementById('complete-session').style.display = 'inline-block';
document.getElementById('capture-photo').style.display = 'none';
document.getElementById('accept-photo').style.display = 'none';
// Update header
document.querySelector('.session-instruction').textContent = '🎉 Session complete! All photos taken.';
console.log(`✅ Photo session complete! ${photosTaken}/${photosNeeded} photos taken`);
}
} else {
// Return to camera view for more photos
this.showCameraPreview();
console.log(`📸 Photo accepted (${photosTaken}/${photosNeeded})`);
}
}
/**
* Complete the photo session
*/
completePhotoSession() {
console.log('🎉 Photo session completed successfully');
// Show photo gallery before completing
this.showPhotoGallery(() => {
// Notify task completion after gallery is closed
const event = new CustomEvent('photoSessionComplete', {
detail: {
photos: this.currentPhotoSession.photos,
sessionType: this.currentPhotoSession.type,
taskData: this.currentPhotoSession.taskData
}
});
document.dispatchEvent(event);
// End session
this.endPhotoSession();
});
}
/**
* Show photo gallery with all taken photos
*/
showPhotoGallery(onClose) {
if (!this.currentPhotoSession || this.currentPhotoSession.photos.length === 0) {
if (onClose) onClose();
return;
}
const gallery = document.createElement('div');
gallery.id = 'photo-gallery-overlay';
gallery.innerHTML = `
<div class="gallery-overlay">
<div class="gallery-container">
<div class="gallery-header">
<h3>📸 Session Complete - Photos Taken</h3>
<p>${this.currentPhotoSession.photos.length} photos captured</p>
</div>
<div class="gallery-grid" id="gallery-grid">
${this.currentPhotoSession.photos.map((photo, index) => `
<div class="gallery-item" onclick="window.showGalleryPhoto(${index})">
<img src="${photo.dataURL}" alt="Photo ${index + 1}">
<div class="photo-number">${index + 1}</div>
</div>
`).join('')}
</div>
<div class="gallery-controls">
<button id="close-gallery" class="gallery-close-btn">✅ Continue</button>
</div>
<div class="gallery-note">
<p>🔒 All photos are stored locally and never uploaded</p>
</div>
</div>
</div>
`;
document.body.appendChild(gallery);
this.addGalleryStyles();
// Bind close button
document.getElementById('close-gallery').addEventListener('click', () => {
gallery.remove();
if (onClose) onClose();
});
// Make photo viewer available globally
window.showGalleryPhoto = (index) => {
this.showFullPhoto(index);
};
}
/**
* Show full-size photo viewer
*/
showFullPhoto(index) {
if (!this.currentPhotoSession || !this.currentPhotoSession.photos[index]) return;
const photo = this.currentPhotoSession.photos[index];
const viewer = document.createElement('div');
viewer.id = 'photo-viewer-overlay';
viewer.innerHTML = `
<div class="photo-viewer-overlay" onclick="this.parentElement.remove()">
<div class="photo-viewer-container">
<div class="photo-viewer-header">
<h4>Photo ${index + 1} of ${this.currentPhotoSession.photos.length}</h4>
<button class="photo-viewer-close" onclick="this.closest('#photo-viewer-overlay').remove()">×</button>
</div>
<div class="photo-viewer-content">
<img src="${photo.dataURL}" alt="Photo ${index + 1}">
</div>
<div class="photo-viewer-nav">
${index > 0 ? `<button onclick="window.showGalleryPhoto(${index - 1}); this.closest('#photo-viewer-overlay').remove();">← Previous</button>` : '<div></div>'}
${index < this.currentPhotoSession.photos.length - 1 ? `<button onclick="window.showGalleryPhoto(${index + 1}); this.closest('#photo-viewer-overlay').remove();">Next →</button>` : '<div></div>'}
</div>
</div>
</div>
`;
document.body.appendChild(viewer);
}
/**
* Confirm ending session before requirements are met
*/
confirmEndSession() {
if (!this.currentPhotoSession) {
this.endPhotoSession();
return;
}
const photosTaken = this.currentPhotoSession.photos.length;
const photosNeeded = this.currentPhotoSession.photosNeeded;
if (photosTaken < photosNeeded) {
const confirmed = confirm(
`You have only taken ${photosTaken} out of ${photosNeeded} required photos.\n\n` +
`Ending now will not complete the photography task.\n\n` +
`Are you sure you want to end the session?`
);
if (!confirmed) {
return; // Don't end session
}
}
this.endPhotoSession();
}
/**
* Start a photo session for interactive tasks (original method - keep for compatibility)
*/
async startPhotoSession(sessionType, taskData) {
console.log(`📸 Starting photo session: ${sessionType}`);
// Request camera if not already active
if (!this.isActive) {
const accessGranted = await this.requestCameraAccess();
if (!accessGranted) return false;
}
this.currentPhotoSession = {
type: sessionType,
taskData: taskData,
photos: [],
startTime: Date.now()
};
// Show camera interface
this.showCameraInterface();
return true;
}
/**
* Display camera interface for photo tasks
*/
showCameraInterface() {
// Create camera overlay
const overlay = document.createElement('div');
overlay.id = 'camera-overlay';
overlay.innerHTML = `
<div class="camera-container">
<div class="camera-header">
<h3>📸 Photography Session</h3>
<p class="session-instruction">Position yourself according to the task instructions</p>
</div>
<div class="camera-preview">
<video id="camera-feed" autoplay playsinline></video>
<div class="camera-guidelines">
<div class="guideline horizontal"></div>
<div class="guideline vertical"></div>
</div>
</div>
<div class="camera-controls">
<button id="capture-photo" class="capture-btn">📷 Take Photo</button>
<button id="retake-photo" class="retake-btn" style="display: none;">🔄 Retake</button>
<button id="accept-photo" class="accept-btn" style="display: none;">✅ Accept Photo</button>
</div>
<div class="photo-preview" id="photo-preview" style="display: none;">
<img id="captured-image" alt="Captured photo">
</div>
<div class="session-info">
<p>Photos taken: <span id="photo-count">0</span></p>
<p class="privacy-note">⚠️ Photos are stored locally and not uploaded anywhere</p>
</div>
</div>
`;
document.body.appendChild(overlay);
// Connect video stream to preview
const cameraFeed = document.getElementById('camera-feed');
cameraFeed.srcObject = this.stream;
// Bind camera controls
this.bindCameraControls();
// Add CSS styles
this.addCameraStyles();
}
/**
* Bind camera control events
*/
bindCameraControls() {
document.getElementById('capture-photo').addEventListener('click', () => {
this.startPhotoTimer();
});
document.getElementById('retake-photo').addEventListener('click', () => {
this.showCameraPreview();
});
document.getElementById('accept-photo').addEventListener('click', () => {
this.acceptPhoto();
});
}
/**
* Capture a photo from the video stream
*/
capturePhoto() {
const video = document.getElementById('camera-feed');
// Set canvas size to video dimensions
this.canvas.width = video.videoWidth;
this.canvas.height = video.videoHeight;
// Draw video frame to canvas
this.context.drawImage(video, 0, 0);
// Add optional text overlay for training sessions
if (this.currentPhotoSession && this.currentPhotoSession.type === 'dress-up-session') {
this.addTrainingPhotoOverlay(this.context, this.canvas.width, this.canvas.height);
}
// Convert to image data
const imageDataURL = this.canvas.toDataURL('image/jpeg', 0.8);
// Show photo preview
this.showPhotoPreview(imageDataURL);
console.log('📸 Photo captured');
}
/**
* Start photo countdown timer
*/
startPhotoTimer(duration = 3) {
const captureBtn = document.getElementById('capture-photo');
const originalText = captureBtn.innerHTML;
let timeLeft = duration;
let cancelled = false;
// Disable capture button and show cancel option
captureBtn.disabled = true;
captureBtn.style.opacity = '0.7';
// Create large countdown overlay
const cameraPreview = document.querySelector('.camera-preview');
const countdownOverlay = document.createElement('div');
countdownOverlay.id = 'countdown-overlay';
countdownOverlay.style.cssText = `
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 10;
border-radius: 10px;
`;
const countdownText = document.createElement('div');
countdownText.id = 'countdown-text';
countdownText.style.cssText = `
font-size: 72px;
font-weight: bold;
color: white;
text-shadow: 2px 2px 4px rgba(0,0,0,0.8);
animation: pulse 1s infinite;
margin-bottom: 20px;
`;
const cancelBtn = document.createElement('button');
cancelBtn.textContent = '❌ Cancel';
cancelBtn.style.cssText = `
background: rgba(220, 53, 69, 0.8);
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
font-size: 16px;
cursor: pointer;
transition: all 0.3s;
`;
cancelBtn.addEventListener('click', () => {
cancelled = true;
countdownOverlay.remove();
captureBtn.innerHTML = originalText;
captureBtn.disabled = false;
captureBtn.style.opacity = '1';
console.log('📱 Photo timer cancelled');
});
countdownOverlay.appendChild(countdownText);
countdownOverlay.appendChild(cancelBtn);
cameraPreview.style.position = 'relative';
cameraPreview.appendChild(countdownOverlay);
// Add pulse animation
if (!document.getElementById('pulse-animation-style')) {
const style = document.createElement('style');
style.id = 'pulse-animation-style';
style.textContent = `
@keyframes pulse {
0% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.1); opacity: 0.8; }
100% { transform: scale(1); opacity: 1; }
}
`;
document.head.appendChild(style);
}
// Create countdown display
const updateCountdown = () => {
if (cancelled) return; // Stop if cancelled
if (timeLeft > 0) {
captureBtn.innerHTML = `📷 Get Ready...`;
countdownText.textContent = timeLeft;
timeLeft--;
setTimeout(updateCountdown, 1000);
} else {
// Show capture indicator
countdownText.textContent = '📸';
countdownText.style.fontSize = '96px';
cancelBtn.style.display = 'none'; // Hide cancel button
captureBtn.innerHTML = '📸 Capturing...';
setTimeout(() => {
if (!cancelled) {
// Take the photo
this.capturePhoto();
// Remove countdown overlay
countdownOverlay.remove();
// Reset button after capture
setTimeout(() => {
captureBtn.innerHTML = originalText;
captureBtn.disabled = false;
captureBtn.style.opacity = '1';
}, 500);
}
}, 500);
}
};
// Start countdown
updateCountdown();
console.log(`📱 Photo timer started: ${duration} seconds`);
}
/**
* Show captured photo preview
*/
showPhotoPreview(imageDataURL) {
const preview = document.getElementById('photo-preview');
const image = document.getElementById('captured-image');
image.src = imageDataURL;
preview.style.display = 'block';
// Update button visibility
document.getElementById('capture-photo').style.display = 'none';
document.getElementById('retake-photo').style.display = 'inline-block';
document.getElementById('accept-photo').style.display = 'inline-block';
}
/**
* Show camera preview (for retaking)
*/
showCameraPreview() {
const preview = document.getElementById('photo-preview');
preview.style.display = 'none';
// Update button visibility
document.getElementById('capture-photo').style.display = 'inline-block';
document.getElementById('retake-photo').style.display = 'none';
document.getElementById('accept-photo').style.display = 'none';
}
/**
* Accept and save the captured photo
*/
acceptPhoto() {
const image = document.getElementById('captured-image');
const photoData = {
dataURL: image.src,
timestamp: Date.now(),
sessionType: this.currentPhotoSession.type,
taskData: this.currentPhotoSession.taskData
};
// Add to session photos
this.currentPhotoSession.photos.push(photoData);
this.capturedPhotos.push(photoData);
// Track photo for XP (1 XP per photo)
if (this.game && this.game.incrementPhotosTaken) {
this.game.incrementPhotosTaken();
}
// Update photo count
document.getElementById('photo-count').textContent = this.currentPhotoSession.photos.length;
// Save to local storage (optional - user privacy)
this.savePhotoData(photoData);
// Return to camera view for more photos
this.showCameraPreview();
// Trigger task progression
this.notifyTaskComplete(photoData);
console.log(`✅ Photo accepted and saved (${this.currentPhotoSession.photos.length} total)`);
}
/**
* Save photo data (respecting privacy)
*/
savePhotoData(photoData) {
// Save metadata to session storage (temporary)
const metadata = {
timestamp: photoData.timestamp,
sessionType: photoData.sessionType,
taskId: photoData.taskData?.id
};
const sessionPhotos = JSON.parse(sessionStorage.getItem('photoSession') || '[]');
sessionPhotos.push(metadata);
sessionStorage.setItem('photoSession', JSON.stringify(sessionPhotos));
// Save actual photo data to localStorage with user consent
this.savePersistentPhoto(photoData);
}
/**
* Save photo data persistently with user consent
*/
savePersistentPhoto(photoData) {
try {
// Check if user has given consent for photo storage
const photoStorageConsent = localStorage.getItem('photoStorageConsent');
if (photoStorageConsent === null) {
// First time - ask for consent
this.requestPhotoStorageConsent(photoData);
return;
}
if (photoStorageConsent === 'true') {
// User has consented - save the photo
this.storePhotoInLocalStorage(photoData);
}
// If consent is 'false', don't save (but still allow session use)
} catch (error) {
console.warn('⚠️ Failed to save photo persistently:', error);
// Continue without persistent storage
}
}
/**
* Request user consent for photo storage
*/
requestPhotoStorageConsent(photoData) {
const modal = document.createElement('div');
modal.style.cssText = `
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.7); display: flex; align-items: center;
justify-content: center; z-index: 10000;
`;
modal.innerHTML = `
<div style="background: white; padding: 30px; border-radius: 15px; max-width: 500px; text-align: center;">
<h3>📸 Save Photos for Later?</h3>
<p>Would you like to save your captured photos so you can view and download them later?</p>
<p style="font-size: 0.9em; color: #666; margin: 15px 0;">
Photos are stored locally on your device only. You can change this setting anytime in options.
</p>
<div style="margin-top: 20px;">
<button id="consent-yes" style="background: #28a745; color: white; border: none; padding: 10px 20px; border-radius: 5px; margin: 0 10px; cursor: pointer;">
Yes, Save Photos
</button>
<button id="consent-no" style="background: #dc3545; color: white; border: none; padding: 10px 20px; border-radius: 5px; margin: 0 10px; cursor: pointer;">
No, Session Only
</button>
</div>
</div>
`;
document.body.appendChild(modal);
document.getElementById('consent-yes').onclick = () => {
localStorage.setItem('photoStorageConsent', 'true');
this.storePhotoInLocalStorage(photoData);
document.body.removeChild(modal);
this.showNotification('📸 Photos will be saved for later viewing!', 'success');
};
document.getElementById('consent-no').onclick = () => {
localStorage.setItem('photoStorageConsent', 'false');
document.body.removeChild(modal);
this.showNotification('Photos will only be available during this session', 'info');
};
}
/**
* Store photo in localStorage
*/
storePhotoInLocalStorage(photoData) {
try {
const savedPhotos = JSON.parse(localStorage.getItem('capturedPhotos') || '[]');
const photoToSave = {
id: `photo_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
dataURL: photoData.dataURL,
timestamp: photoData.timestamp,
sessionType: photoData.sessionType,
taskId: photoData.taskData?.id || 'unknown',
dateCreated: new Date(photoData.timestamp).toISOString(),
size: Math.round(photoData.dataURL.length * 0.75) // Approximate size in bytes
};
savedPhotos.push(photoToSave);
localStorage.setItem('capturedPhotos', JSON.stringify(savedPhotos));
console.log(`📸 Photo saved persistently (ID: ${photoToSave.id})`);
} catch (error) {
console.warn('⚠️ Failed to save photo to localStorage:', error);
if (error.name === 'QuotaExceededError') {
this.showNotification('⚠️ Storage full - consider downloading and clearing old photos', 'warning');
}
}
}
/**
* Show notification helper
*/
showNotification(message, type = 'info', duration = 3000) {
// Create notification if it doesn't exist
let notification = document.getElementById('photo-notification');
if (!notification) {
notification = document.createElement('div');
notification.id = 'photo-notification';
notification.style.cssText = `
position: fixed; top: 20px; right: 20px; padding: 15px 20px;
border-radius: 8px; color: white; z-index: 5000; font-weight: bold;
transition: all 0.3s ease; opacity: 0; transform: translateX(100%);
`;
document.body.appendChild(notification);
}
// Set message and style based on type
notification.textContent = message;
const colors = {
success: '#28a745',
warning: '#ffc107',
error: '#dc3545',
info: '#17a2b8'
};
notification.style.background = colors[type] || colors.info;
// Show notification
notification.style.opacity = '1';
notification.style.transform = 'translateX(0)';
// Hide after duration
setTimeout(() => {
notification.style.opacity = '0';
notification.style.transform = 'translateX(100%)';
}, duration);
}
/**
* Get all saved photos from localStorage
*/
getSavedPhotos() {
try {
return JSON.parse(localStorage.getItem('capturedPhotos') || '[]');
} catch (error) {
console.warn('⚠️ Failed to retrieve saved photos:', error);
return [];
}
}
/**
* Delete a specific photo by ID
*/
deletePhoto(photoId) {
try {
const savedPhotos = this.getSavedPhotos();
const filteredPhotos = savedPhotos.filter(photo => photo.id !== photoId);
localStorage.setItem('capturedPhotos', JSON.stringify(filteredPhotos));
return true;
} catch (error) {
console.warn('⚠️ Failed to delete photo:', error);
return false;
}
}
/**
* Clear all saved photos
*/
clearAllPhotos() {
try {
localStorage.removeItem('capturedPhotos');
return true;
} catch (error) {
console.warn('⚠️ Failed to clear photos:', error);
return false;
}
}
/**
* Download a photo as a file
*/
downloadPhoto(photo, filename = null) {
try {
const link = document.createElement('a');
link.download = filename || `photo_${new Date(photo.timestamp).toISOString().slice(0, 19).replace(/[:.]/g, '-')}.jpg`;
link.href = photo.dataURL;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
return true;
} catch (error) {
console.warn('⚠️ Failed to download photo:', error);
return false;
}
}
/**
* Download selected photos - single photo normally, multiple photos as zip
*/
async downloadSelectedPhotos(selectedPhotos) {
if (!selectedPhotos || selectedPhotos.length === 0) {
this.showNotification('No photos selected for download', 'warning');
return;
}
if (selectedPhotos.length === 1) {
// Single photo - download normally
const photo = selectedPhotos[0];
const filename = `photo_${new Date(photo.timestamp).toISOString().slice(0, 19).replace(/[:.]/g, '-')}.jpg`;
const success = this.downloadPhoto(photo, filename);
if (success) {
this.showNotification(`✅ Photo downloaded as ${filename}`, 'success');
console.log(`📸 Single photo download completed: ${filename}`);
}
} else {
// Multiple photos - create zip
await this.downloadPhotosAsZip(selectedPhotos);
}
}
/**
* Convert data URL to blob without using fetch (CSP-safe)
*/
dataURLToBlob(dataURL) {
try {
const [header, data] = dataURL.split(',');
const mime = header.match(/:(.*?);/)[1];
const binary = atob(data);
const array = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
array[i] = binary.charCodeAt(i);
}
return new Blob([array], { type: mime });
} catch (error) {
console.error('Failed to convert data URL to blob:', error);
return null;
}
}
/**
* Download photos as a zip file
*/
async downloadPhotosAsZip(photos, zipFilename = null) {
// Wait for JSZip to be available
let JSZipLibrary = null;
let attempts = 0;
const maxAttempts = 50; // 5 seconds max wait time
while (!JSZipLibrary && attempts < maxAttempts) {
if (typeof JSZip !== 'undefined') {
JSZipLibrary = JSZip;
break;
} else if (typeof window.JSZip !== 'undefined') {
JSZipLibrary = window.JSZip;
break;
}
// Wait 100ms before trying again
await new Promise(resolve => setTimeout(resolve, 100));
attempts++;
}
// Check if JSZip is available after waiting
if (!JSZipLibrary) {
console.error('JSZip library not available after waiting. Please ensure JSZip is loaded.');
this.showNotification('❌ Zip functionality not available. JSZip library failed to load.', 'error');
return;
}
try {
this.showNotification(`📦 Creating zip with ${photos.length} photos...`, 'info');
const zip = new JSZipLibrary();
// Add each photo to the zip
for (let i = 0; i < photos.length; i++) {
const photo = photos[i];
const timestamp = new Date(photo.timestamp).toISOString().slice(0, 19).replace(/[:.]/g, '-');
const filename = `photo_${i + 1}_${timestamp}.jpg`;
// Convert data URL to blob using helper function
const blob = this.dataURLToBlob(photo.dataURL);
if (!blob) {
console.warn(`Failed to convert photo ${i + 1} to blob, skipping`);
continue;
}
zip.file(filename, blob);
}
// Generate the zip file
const content = await zip.generateAsync({type: 'blob'});
// Create unique filename with date/time if not provided
let finalFilename = zipFilename;
if (!finalFilename) {
const now = new Date();
const dateStr = now.toISOString().slice(0, 10); // YYYY-MM-DD
const timeStr = now.toTimeString().slice(0, 8).replace(/:/g, '-'); // HH-MM-SS
finalFilename = `photos_${dateStr}_${timeStr}.zip`;
}
// Download the zip
const link = document.createElement('a');
link.href = URL.createObjectURL(content);
link.download = finalFilename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
// Clean up the object URL
setTimeout(() => URL.revokeObjectURL(link.href), 1000);
// Show success confirmation with filename
this.showNotification(`✅ Downloaded ${photos.length} photos as ${finalFilename}`, 'success');
console.log(`📦 Zip download completed: ${finalFilename} (${photos.length} photos)`);
} catch (error) {
console.error('Failed to create zip:', error);
this.showNotification('❌ Failed to create zip file', 'error');
}
}
/**
* Download all photos as a zip file
*/
async downloadAllPhotos() {
const photos = this.getSavedPhotos();
if (photos.length === 0) {
this.showNotification('No photos to download', 'warning');
return;
}
if (photos.length === 1) {
// Single photo - download normally
const photo = photos[0];
const filename = `photo_${new Date(photo.timestamp).toISOString().slice(0, 19).replace(/[:.]/g, '-')}.jpg`;
const success = this.downloadPhoto(photo, filename);
if (success) {
this.showNotification(`✅ Photo downloaded as ${filename}`, 'success');
console.log(`📸 Single photo download completed: ${filename}`);
}
} else {
// Multiple photos - create zip (filename will be auto-generated with timestamp)
await this.downloadPhotosAsZip(photos);
}
}
/**
* Get photo storage statistics
*/
getPhotoStats() {
const photos = this.getSavedPhotos();
const totalSize = photos.reduce((sum, photo) => sum + (photo.size || 0), 0);
return {
count: photos.length,
totalSize: totalSize,
oldestPhoto: photos.length > 0 ? Math.min(...photos.map(p => p.timestamp)) : null,
newestPhoto: photos.length > 0 ? Math.max(...photos.map(p => p.timestamp)) : null,
sessionTypes: [...new Set(photos.map(p => p.sessionType))],
storageConsent: localStorage.getItem('photoStorageConsent')
};
}
/**
* Notify the task system that a photo was taken
*/
notifyTaskComplete(photoData) {
if (this.game && this.game.interactiveTaskManager) {
// Trigger task completion or progression
const event = new CustomEvent('photoTaken', {
detail: {
photoData: photoData,
sessionType: this.currentPhotoSession.type
}
});
document.dispatchEvent(event);
}
}
/**
* End the current photo session
*/
endPhotoSession() {
console.log('🔚 Ending photo session');
// Remove camera overlay
const overlay = document.getElementById('camera-overlay');
if (overlay) {
overlay.remove();
}
// Stop camera stream
this.stopCamera();
// Clear current session
if (this.currentPhotoSession) {
console.log(`📊 Session ended: ${this.currentPhotoSession.photos.length} photos taken`);
// Check if this was a scenario-based photo session
const isScenarioSession = this.currentPhotoSession.taskData &&
this.currentPhotoSession.taskData.task &&
this.currentPhotoSession.taskData.task.scenarioState;
this.currentPhotoSession = null;
// Only call resumeFromCamera for non-scenario sessions
// Scenario sessions are handled by handlePhotoSessionCompletion
if (!isScenarioSession && this.game && this.game.interactiveTaskManager) {
console.log('📱 Non-scenario session - resuming to task interface');
this.game.interactiveTaskManager.resumeFromCamera();
} else if (isScenarioSession) {
console.log('🎭 Scenario session - completion handled by scenario system');
}
}
}
/**
* Start mirror mode - shows camera feed for self-viewing without taking photos
*/
async startMirrorMode(taskData) {
console.log('🪞 Starting webcam mirror mode');
// Request camera if not already active
if (!this.isActive) {
const accessGranted = await this.requestCameraAccess();
if (!accessGranted) {
console.warn('📷 Camera access required for mirror mode');
return false;
}
}
// Track webcam mirror start for XP (5 XP per minute)
if (this.game && this.game.trackWebcamMirror) {
this.game.trackWebcamMirror(true);
}
// Show mirror interface
this.showMirrorInterface(taskData);
return true;
}
/**
* Bank of verification positions for randomized instructions
*/
getPositionBank() {
return {
submissive: [
{
name: "Kneeling Submission",
instruction: "Kneel with hands behind your back, head bowed in submission",
description: "Classic submissive kneeling pose showing complete obedience"
},
{
name: "Present Position",
instruction: "On hands and knees, arch your back and present yourself for inspection",
description: "Degrading presentation pose for thorough examination"
},
{
name: "Worship Pose",
instruction: "Sit on your heels with hands on thighs, mouth open in worship position",
description: "Ready to worship and serve in proper position"
},
{
name: "Display Spread",
instruction: "Lying back with legs spread wide, hands above head in surrender",
description: "Complete exposure and vulnerability display"
},
{
name: "Begging Position",
instruction: "On knees with hands clasped together, looking up desperately",
description: "Pathetic begging pose showing your desperate need"
}
],
degrading: [
{
name: "Pet Crawl",
instruction: "On all fours like an animal, tongue out panting like a dog",
description: "Dehumanizing animal position for complete degradation"
},
{
name: "Toilet Position",
instruction: "Squatting low with mouth open wide, ready to be used",
description: "Ultimate degradation pose as a human toilet"
},
{
name: "Fuck-Doll Display",
instruction: "Lying with limbs spread, completely limp like a broken sex doll",
description: "Objectified as nothing but a mindless fuck toy"
},
{
name: "Cock-Socket Ready",
instruction: "Kneeling with mouth wide open, hands holding face open",
description: "Prepared to be nothing but a hole for use"
},
{
name: "Pain Slut Pose",
instruction: "Bent over with back arched, presenting for punishment",
description: "Ready to receive pain and discipline like the slut you are"
}
],
humiliating: [
{
name: "Embarrassed Display",
instruction: "Standing with hands covering face in shame while body exposed",
description: "Showing your humiliation and embarrassment"
},
{
name: "Pathetic Grovel",
instruction: "Face down on ground, ass up in the air, groveling pathetically",
description: "Ultimate humiliation and subservience position"
},
{
name: "Shame Spread",
instruction: "Squatting with legs wide, hands pulling yourself open",
description: "Shameful self-exposure for maximum humiliation"
},
{
name: "Desperate Beg",
instruction: "On back with legs in the air, begging desperately for attention",
description: "Pathetic attention-seeking humiliation pose"
},
{
name: "Slut Presentation",
instruction: "Bent forward touching toes, looking back with slutty expression",
description: "Presenting yourself like the desperate slut you are"
}
],
extreme: [
{
name: "Complete Surrender",
instruction: "Spread eagle on back, completely exposed and helpless",
description: "Total vulnerability and surrender to be used"
},
{
name: "Breeding Position",
instruction: "Face down ass up, legs spread wide for breeding access",
description: "Perfect position for being bred like an animal"
},
{
name: "Inspection Pose",
instruction: "Squatting with hands spreading yourself open for inspection",
description: "Allowing complete inspection of your worthless holes"
},
{
name: "Punishment Ready",
instruction: "Bent over grabbing ankles, presenting for harsh punishment",
description: "Ready to receive the brutal punishment you deserve"
},
{
name: "Broken Toy",
instruction: "Collapsed in heap, limbs akimbo like a discarded fuck toy",
description: "Completely broken and used up, nothing left but a toy"
}
]
};
}
/**
* Select random position from bank based on intensity
*/
selectRandomPosition(intensity = 'mixed') {
const positions = this.getPositionBank();
let availablePositions = [];
if (intensity === 'mixed') {
// Mix all categories
availablePositions = [
...positions.submissive,
...positions.degrading,
...positions.humiliating,
...positions.extreme
];
} else if (positions[intensity]) {
availablePositions = positions[intensity];
} else {
// Default to submissive if invalid intensity
availablePositions = positions.submissive;
}
const randomIndex = Math.floor(Math.random() * availablePositions.length);
return availablePositions[randomIndex];
}
/**
* Clean up localStorage to prevent quota exceeded errors
*/
cleanupStorageBeforeVerification() {
try {
// Clean up old verification photos
const verificationPhotos = JSON.parse(localStorage.getItem('verificationPhotos') || '[]');
if (verificationPhotos.length > 3) {
const recent = verificationPhotos.slice(-3);
localStorage.setItem('verificationPhotos', JSON.stringify(recent));
console.log(`🧹 Cleaned up verification photos: ${verificationPhotos.length}${recent.length}`);
}
// Clean up old captured photos
const capturedPhotos = JSON.parse(localStorage.getItem('capturedPhotos') || '[]');
if (capturedPhotos.length > 5) {
const recent = capturedPhotos.slice(-5);
localStorage.setItem('capturedPhotos', JSON.stringify(recent));
console.log(`🧹 Cleaned up captured photos: ${capturedPhotos.length}${recent.length}`);
}
// Clean up any old individual photo keys
const keys = Object.keys(localStorage);
const photoKeys = keys.filter(key => key.startsWith('verificationPhoto_'));
if (photoKeys.length > 10) {
photoKeys.slice(0, -10).forEach(key => {
const url = localStorage.getItem(key);
if (url && url.startsWith('blob:')) {
URL.revokeObjectURL(url);
}
localStorage.removeItem(key);
});
console.log(`🧹 Cleaned up ${photoKeys.length - 10} old photo keys`);
}
} catch (error) {
console.warn('⚠️ Error during storage cleanup:', error);
}
}
/**
* Start webcam verification mode for position verification
*/
async startVerificationMode(verificationData) {
console.log('🔍 Starting webcam verification mode');
// Clean up storage before starting to prevent quota issues
this.cleanupStorageBeforeVerification();
// Request camera if not already active
if (!this.isActive) {
const accessGranted = await this.requestCameraAccess();
if (!accessGranted) {
console.warn('📷 Camera access required for verification mode');
return false;
}
}
// Track verification start
if (this.game && this.game.trackWebcamVerification) {
this.game.trackWebcamVerification(true);
}
// Show verification interface
this.showVerificationInterface(verificationData);
return true;
}
/**
* Display verification interface - webcam feed with position verification
*/
showVerificationInterface(verificationData) {
// Select random position for this verification
const selectedPosition = this.selectRandomPosition(verificationData?.positionIntensity || 'mixed');
// Store selected position for use throughout verification
this.currentPosition = selectedPosition;
// Create verification overlay
const overlay = document.createElement('div');
overlay.id = 'verification-overlay';
overlay.innerHTML = `
<div class="verification-container">
<div class="verification-header">
<h3>🔍 Position Verification Required</h3>
<p>${verificationData?.instructions || 'Follow the position instructions and maintain pose'}</p>
</div>
<div class="verification-video-container">
<video id="verification-video" autoplay muted playsinline></video>
<div class="verification-overlay-text">
<div class="position-name" style="color: #ff6b6b; font-weight: bold; font-size: 1.2em; margin-bottom: 10px;">
${selectedPosition.name}
</div>
<div class="position-instructions">
${selectedPosition.instruction}
</div>
<div class="position-description" style="color: #ffd700; font-style: italic; font-size: 0.9em; margin-top: 10px;">
${selectedPosition.description}
</div>
</div>
</div>
<div class="verification-timer" id="verification-timer" style="display: none;">
<div class="verification-progress" id="verification-progress">
<div class="progress-bar" id="verification-progress-bar"></div>
</div>
<div class="timer-text">Verification time: <span id="verification-time">${verificationData?.verificationDuration || 30}</span>s</div>
<div class="verification-status" id="verification-status">Prepare for position verification...</div>
</div>
<div class="verification-controls">
<button id="verification-start-btn" class="btn btn-primary">
🔍 Start Verification
</button>
<button id="verification-close-btn" class="btn btn-secondary">
❌ Abandon Task
</button>
<button id="verification-force-close-btn" class="btn btn-danger" style="background: #dc3545; margin-left: 10px;">
🚨 Force Close
</button>
</div>
${verificationData?.verificationText ? `<div class="verification-task-text">${verificationData.verificationText}</div>` : ''}
</div>
`;
// Style the verification overlay
overlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.95);
display: flex;
justify-content: center;
align-items: center;
z-index: 99999;
font-family: Arial, sans-serif;
`;
const containerStyle = `
background: linear-gradient(135deg, #2c3e50, #34495e);
border: 2px solid #e74c3c;
border-radius: 15px;
padding: 30px;
max-width: 700px;
width: 95%;
text-align: center;
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.7);
color: #ecf0f1;
`;
const videoStyle = `
width: 100%;
max-width: 500px;
height: auto;
border-radius: 10px;
margin: 15px 0;
transform: scaleX(-1);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);
border: 2px solid #e74c3c;
position: relative;
`;
const buttonStyle = `
margin: 10px;
padding: 12px 24px;
border: none;
border-radius: 8px;
font-size: 16px;
cursor: pointer;
transition: all 0.3s ease;
background: linear-gradient(135deg, #e74c3c, #c0392b);
color: white;
font-weight: 600;
min-width: 140px;
`;
// Apply styles
overlay.querySelector('.verification-container').style.cssText = containerStyle;
overlay.querySelector('#verification-video').style.cssText = videoStyle;
overlay.querySelectorAll('button').forEach(btn => {
btn.style.cssText = buttonStyle;
});
// Style specific elements
const startBtn = overlay.querySelector('#verification-start-btn');
const closeBtn = overlay.querySelector('#verification-close-btn');
if (closeBtn) {
closeBtn.style.background = 'linear-gradient(135deg, #95a5a6, #7f8c8d)';
}
document.body.appendChild(overlay);
// Connect video stream to verification video element
const verificationVideo = overlay.querySelector('#verification-video');
if (this.stream && verificationVideo) {
verificationVideo.srcObject = this.stream;
}
// Add event listeners
startBtn.addEventListener('click', () => {
console.log('🔍 Starting verification process');
this.startVerificationTimer(verificationData, overlay);
});
closeBtn.addEventListener('click', () => {
console.log('❌ Verification abandoned - triggering game over');
this.closeVerificationMode();
this.showCondescendingGameOver();
});
// Add force close button handler
const forceCloseBtn = overlay.querySelector('#verification-force-close-btn');
if (forceCloseBtn) {
forceCloseBtn.addEventListener('click', () => {
console.log('🚨 Force closing verification mode');
this.closeVerificationMode();
// Don't trigger game over, just close
});
}
// Add escape key handler for emergency close
const escapeHandler = (event) => {
if (event.key === 'Escape') {
console.log('🚨 Emergency closing verification with Escape key');
document.removeEventListener('keydown', escapeHandler);
this.closeVerificationMode();
}
};
document.addEventListener('keydown', escapeHandler);
// Store escape handler reference for cleanup
overlay.escapeHandler = escapeHandler;
console.log('🔍 Verification interface displayed');
}
/**
* Start verification timer with countdown and auto-capture
*/
startVerificationTimer(verificationData, overlay) {
const duration = verificationData?.verificationDuration || 30;
const timerDisplay = overlay.querySelector('#verification-time');
const progressBar = overlay.querySelector('#verification-progress-bar');
const timerElement = overlay.querySelector('#verification-timer');
const statusElement = overlay.querySelector('#verification-status');
const startBtn = overlay.querySelector('#verification-start-btn');
// Hide start button and show timer
if (startBtn) startBtn.style.display = 'none';
if (timerElement) timerElement.style.display = 'block';
// Start preparation phase directly (no initial photo yet)
this.startPreparationPhase(duration, overlay);
}
/**
* Start preparation phase
*/
startPreparationPhase(duration, overlay) {
const statusElement = overlay.querySelector('#verification-status');
// Preparation phase (10 seconds)
let prepTime = 10;
const positionName = this.currentPosition ? this.currentPosition.name : 'required position';
if (statusElement) statusElement.textContent = `Get into ${positionName}! ${prepTime}s`;
const prepTimer = setInterval(() => {
try {
prepTime--;
console.log('📍 Preparation countdown:', prepTime);
if (statusElement) statusElement.textContent = `Get into ${positionName}! ${prepTime}s`;
if (prepTime <= 0) {
console.log('📍 Preparation complete, starting main verification...');
clearInterval(prepTimer);
this.preparationTimer = null;
this.startMainVerification(duration, overlay);
}
} catch (error) {
console.error('❌ Error in preparation timer:', error);
clearInterval(prepTimer);
this.preparationTimer = null;
// Emergency fallback: proceed to main verification anyway
this.startMainVerification(duration, overlay);
}
}, 1000);
// Store timer reference for cleanup
this.preparationTimer = prepTimer;
}
/**
* Start main verification phase with position holding
*/
startMainVerification(duration, overlay) {
console.log('🔍 Starting main verification phase with duration:', duration);
const timerDisplay = overlay.querySelector('#verification-time');
const progressBar = overlay.querySelector('#verification-progress-bar');
const statusElement = overlay.querySelector('#verification-status');
if (!overlay || !overlay.parentNode) {
console.error('❌ Overlay no longer exists, aborting verification');
return;
}
// Take START photo now that they're in position and verification begins
try {
this.capturePositionPhoto('start', overlay, () => {
// Continue with verification after START photo
console.log('📸 START photo captured, continuing verification...');
this.continueMainVerification(duration, overlay);
});
} catch (error) {
console.error('❌ Error capturing start photo:', error);
// Continue anyway without photo
this.continueMainVerification(duration, overlay);
}
}
/**
* Continue main verification phase after START photo
*/
continueMainVerification(duration, overlay) {
const timerDisplay = overlay.querySelector('#verification-time');
const progressBar = overlay.querySelector('#verification-progress-bar');
const statusElement = overlay.querySelector('#verification-status');
let timeLeft = duration;
if (statusElement) {
statusElement.textContent = 'HOLD POSITION - Being verified...';
statusElement.style.color = '#e74c3c';
statusElement.style.fontWeight = 'bold';
}
const timer = setInterval(() => {
timeLeft--;
// Update timer display
if (timerDisplay) {
timerDisplay.textContent = timeLeft;
}
// Update progress bar
if (progressBar) {
const progress = ((duration - timeLeft) / duration) * 100;
progressBar.style.width = progress + '%';
}
// Update status
if (statusElement && timeLeft > 0) {
statusElement.textContent = `HOLD POSITION - ${timeLeft}s remaining`;
}
// Verification complete
if (timeLeft <= 0) {
clearInterval(timer);
this.completeVerification(overlay);
}
}, 1000);
// Store timer reference for cleanup
this.verificationTimer = timer;
}
/**
* Complete verification process with final photo capture
*/
completeVerification(overlay) {
const statusElement = overlay.querySelector('#verification-status');
if (statusElement) {
statusElement.textContent = 'Position verification complete!';
statusElement.style.color = '#27ae60';
}
// Capture final position verification photo with degrading message
this.capturePositionPhoto('end', overlay, () => {
console.log('📸 Final photo captured, finalizing verification...');
// Reduced delay and added error handling
setTimeout(() => {
try {
this.finalizeVerification(overlay);
} catch (error) {
console.error('❌ Error during finalization:', error);
// Force close even if there's an error
this.closeVerificationMode();
const event = new CustomEvent('verificationComplete', {
detail: { success: true }
});
document.dispatchEvent(event);
}
}, 1000);
});
}
/**
* Capture position photo with degrading message
*/
capturePositionPhoto(phase, overlay, callback) {
console.log('📸 Starting position photo capture for phase:', phase);
// Try to find video element - check for both verification and test video IDs
let video = overlay.querySelector('#verification-video');
if (!video) {
video = overlay.querySelector('#test-video');
}
if (!video) {
video = overlay.querySelector('video');
}
const statusElement = overlay.querySelector('#verification-status') || overlay.querySelector('#test-status');
console.log('📸 Video element found:', !!video);
console.log('📸 Video ready:', video && video.videoWidth > 0);
console.log('📸 Video ID:', video?.id);
// Array of brutally degrading messages for different phases
const degradingMessages = {
start: [
"📸 *SNAP* - Look at this pathetic slut, already desperate to be used...",
"📸 *SNAP* - What a worthless whore, posing like the cum-hungry toy you are!",
"📸 *SNAP* - Smile for daddy, you filthy little cock sleeve!",
"📸 *SNAP* - Perfect positioning, you brain-dead fuck doll. Ready to be broken?",
"📸 *SNAP* - Such an eager little cum dump, already wet and waiting!",
"📸 *SNAP* - Look at yourself, you depraved slut... begging to be degraded!",
"📸 *SNAP* - Starting position: worthless whore ready for brutal training!",
"📸 *SNAP* - What a mindless gooning slut! Already dripping for abuse...",
"📸 *SNAP* - Beginning your transformation into a brainless fuck toy!",
"📸 *SNAP* - There's my good little pain slut, ready to be destroyed!",
"📸 *SNAP* - Such a desperate cock-hungry whore, posing like the slut you are!",
"📸 *SNAP* - Initial degradation documented. You look so fucking pathetic...",
"📸 *SNAP* - Starting your descent into complete cock slavery!",
"📸 *SNAP* - Look how excited this filthy slut is to be humiliated!",
"📸 *SNAP* - Perfect! My little cum bucket is ready for brutal conditioning!",
"📸 *SNAP* - Pre-training: one worthless whore about to be broken completely!",
"📸 *SNAP* - Ready to become daddy's mindless fuck doll? This says yes!",
"📸 *SNAP* - Starting pose: pathetic slut begging to be used and abused!",
"📸 *SNAP* - Initial submission: my cock-sleeve ready for brutal training!",
"📸 *SNAP* - Look at this eager little pain pig, so ready to suffer!",
"📸 *SNAP* - Fresh meat ready for psychological destruction! How delicious...",
"📸 *SNAP* - Another desperate whore volunteering for mind-rape! Perfect!",
"📸 *SNAP* - Look at those needy eyes... already begging to be owned!",
"📸 *SNAP* - What a disgusting little cum-addicted freak you are!",
"📸 *SNAP* - Time to shatter this worthless slut's mind completely!",
"📸 *SNAP* - Beginning the process of turning you into daddy's toilet!",
"📸 *SNAP* - Such a pathetic cock-worshipping bitch, ready to be ruined!",
"📸 *SNAP* - Starting documentation of your complete mental breakdown!",
"📸 *SNAP* - What an obedient little fuck-meat, posing for degradation!",
"📸 *SNAP* - Time to train this brain-dead slut into perfect submission!",
"📸 *SNAP* - Look at this worthless cum-rag, so eager to be destroyed!",
"📸 *SNAP* - Beginning your transformation from human to sex object!",
"📸 *SNAP* - Such a desperate attention whore, begging to be humiliated!",
"📸 *SNAP* - Ready to have your mind fucked beyond repair? Let's begin!",
"📸 *SNAP* - What a filthy pain-slut, already assuming the position!",
"📸 *SNAP* - Time to break this worthless toy and reprogram its brain!",
"📸 *SNAP* - Look at this cock-starved whore, so ready for brutal training!",
"📸 *SNAP* - Beginning the systematic destruction of your self-worth!",
"📸 *SNAP* - Such an eager little degradation junkie, ready for abuse!",
"📸 *SNAP* - Starting pose captured: one more slut ready for mind-fucking!"
],
end: [
"📸 *SNAP* - Final documentation: completely broken whore, perfectly trained!",
"📸 *SNAP* - There's proof you're nothing but a mindless cum receptacle!",
"📸 *SNAP* - Perfect! Another worthless slut successfully mind-fucked!",
"📸 *SNAP* - Look how well-broken you are now, my obedient cock toy!",
"📸 *SNAP* - Evidence captured: one more brain-dead fuck doll created!",
"📸 *SNAP* - Beautiful! You're now a perfectly trained pain slut!",
"📸 *SNAP* - Session complete: worthless whore successfully conditioned!",
"📸 *SNAP* - Final proof of your transformation into daddy's cum dump!",
"📸 *SNAP* - Post-training: completely mind-broken slut, mission accomplished!",
"📸 *SNAP* - There's how far you've fallen, you pathetic cock slave!",
"📸 *SNAP* - Training complete! Another mindless gooning slut created!",
"📸 *SNAP* - Final verification: worthless whore successfully brain-washed!",
"📸 *SNAP* - Look at you now... nothing but daddy's obedient fuck toy!",
"📸 *SNAP* - Session concluded: one more slut completely mind-fucked!",
"📸 *SNAP* - Perfect! My cock sleeve is now permanently brain-damaged!",
"📸 *SNAP* - End result: worthless cum dump, perfectly trained and broken!",
"📸 *SNAP* - Training complete: my pain pig is now completely obedient!",
"📸 *SNAP* - Final photo: proof of successful slut conditioning program!",
"📸 *SNAP* - Session finished: another mind-broken whore for daddy's use!",
"📸 *SNAP* - Conclusion: pathetic slut successfully turned into cock toy!",
"📸 *SNAP* - Mission accomplished: another human reduced to fuck-meat!",
"📸 *SNAP* - Final result: worthless toilet perfectly programmed for use!",
"📸 *SNAP* - Conditioning complete: mind successfully shattered and rebuilt!",
"📸 *SNAP* - Perfect destruction: another soul crushed into submission!",
"📸 *SNAP* - End state documented: completely dehumanized cock-socket!",
"📸 *SNAP* - Training success: pathetic worm transformed into sex object!",
"📸 *SNAP* - Final capture: broken toy ready for permanent ownership!",
"📸 *SNAP* - Session complete: mind successfully fucked beyond repair!",
"📸 *SNAP* - Beautiful ending: worthless slut now daddy's property forever!",
"📸 *SNAP* - Training finished: another human reduced to cum-receptacle!",
"📸 *SNAP* - Perfect conclusion: mind-rape successful, slut created!",
"📸 *SNAP* - Final documentation: psychological destruction 100% complete!",
"📸 *SNAP* - End result: pathetic worm now exists only to serve cock!",
"📸 *SNAP* - Mission success: another worthless whore properly broken!",
"📸 *SNAP* - Training complete: brain successfully melted into mush!",
"📸 *SNAP* - Final proof: transformation from person to fuck-object complete!",
"📸 *SNAP* - Perfect finale: another mind completely owned and destroyed!",
"📸 *SNAP* - Session concluded: worthless cum-rag properly conditioned!",
"📸 *SNAP* - End state: pathetic slut now permanently brain-fucked!",
"📸 *SNAP* - Final capture: successful creation of mindless sex-slave!"
]
};
if (video && video.videoWidth > 0) {
// Create canvas and capture frame
const canvas = document.createElement('canvas');
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
const ctx = canvas.getContext('2d');
// Flip the image horizontally (mirror effect)
ctx.scale(-1, 1);
ctx.drawImage(video, -canvas.width, 0);
// Get random degrading message
const messages = degradingMessages[phase] || degradingMessages.start;
const randomMessage = messages[Math.floor(Math.random() * messages.length)];
// Reset scale for text overlay
ctx.scale(-1, 1);
// Add text overlay with degrading message
this.addTextOverlayToCanvas(ctx, canvas.width, canvas.height, randomMessage, phase);
// Convert to base64
const photoData = canvas.toDataURL('image/jpeg', 0.9);
// Store verification photo with metadata
const verificationPhoto = {
timestamp: new Date().toISOString(),
data: photoData,
type: 'position_verification',
phase: phase,
message: randomMessage
};
// Save to localStorage (verification photos) with quota error handling
try {
const existingPhotos = JSON.parse(localStorage.getItem('verificationPhotos') || '[]');
// Add position info to verification photo
verificationPhoto.position = this.currentPosition ? {
name: this.currentPosition.name,
instruction: this.currentPosition.instruction,
description: this.currentPosition.description
} : null;
existingPhotos.push(verificationPhoto);
// Limit to last 5 verification photos to save space (reduced from 20)
if (existingPhotos.length > 5) {
existingPhotos.splice(0, existingPhotos.length - 5);
}
localStorage.setItem('verificationPhotos', JSON.stringify(existingPhotos));
console.log(`✅ Saved verification photo, total: ${existingPhotos.length}`);
} catch (storageError) {
if (storageError.name === 'QuotaExceededError') {
console.warn('⚠️ Storage quota exceeded, clearing verification photos and retrying...');
try {
localStorage.removeItem('verificationPhotos');
localStorage.setItem('verificationPhotos', JSON.stringify([verificationPhoto]));
console.log('✅ Saved verification photo after emergency cleanup');
} catch (retryError) {
console.error('❌ Failed to save verification photo even after cleanup:', retryError);
}
} else {
console.error('❌ Error saving verification photo:', storageError);
}
}
// ALSO store in main captured photos system for gallery integration
// Create photo data object outside try block to ensure scope
const mainPhotoData = {
timestamp: new Date().toISOString(),
data: photoData,
id: Date.now().toString(),
isWebcamCapture: true,
type: 'position_verification',
phase: phase,
message: randomMessage,
filename: `verification_${phase}_${Date.now()}.jpg`,
position: this.currentPosition ? {
name: this.currentPosition.name,
instruction: this.currentPosition.instruction,
description: this.currentPosition.description
} : null
};
try {
const mainCapturedPhotos = JSON.parse(localStorage.getItem('capturedPhotos') || '[]');
mainCapturedPhotos.push(mainPhotoData);
// Limit main photos to 10 total (reduced from 50 to save space)
if (mainCapturedPhotos.length > 10) {
mainCapturedPhotos.splice(0, mainCapturedPhotos.length - 10);
}
localStorage.setItem('capturedPhotos', JSON.stringify(mainCapturedPhotos));
console.log(`✅ Saved to main photos, total: ${mainCapturedPhotos.length}`);
} catch (storageError) {
if (storageError.name === 'QuotaExceededError') {
console.warn('⚠️ Main photos storage quota exceeded, clearing and retrying...');
try {
localStorage.removeItem('capturedPhotos');
localStorage.setItem('capturedPhotos', JSON.stringify([mainPhotoData]));
console.log('✅ Saved main photo after emergency cleanup');
} catch (retryError) {
console.error('❌ Failed to save main photo even after cleanup:', retryError);
}
} else {
console.error('❌ Error saving main photo:', storageError);
}
}
// Add to webcam manager's capturedPhotos array if available
if (this.capturedPhotos) {
this.capturedPhotos.push(mainPhotoData);
console.log('📸 Added to webcam manager capturedPhotos array. Total:', this.capturedPhotos.length);
} else {
console.log('📸 webcamManager capturedPhotos array not available');
}
console.log(`📸 ${phase} verification photo captured and stored in both systems:`, randomMessage);
console.log('📸 Verification photos in localStorage:', JSON.parse(localStorage.getItem('verificationPhotos') || '[]').length);
console.log('📸 Main captured photos in localStorage:', JSON.parse(localStorage.getItem('capturedPhotos') || '[]').length);
// Award XP for verification photo (same as regular photos)
if (this.game && this.game.incrementPhotosTaken) {
this.game.incrementPhotosTaken();
console.log(`📸 Awarded XP for ${phase} position verification photo`);
} else if (window.playerStats) {
// Fallback: award XP directly if game system not available
window.playerStats.awardXP(2, 'verification');
console.log(`📸 Awarded 2 XP directly for ${phase} position verification photo`);
}
// Show degrading message
if (statusElement) {
statusElement.innerHTML = randomMessage;
statusElement.style.color = '#e74c3c';
statusElement.style.fontSize = '16px';
statusElement.style.fontWeight = 'bold';
statusElement.style.textAlign = 'center';
statusElement.style.padding = '15px';
statusElement.style.background = 'rgba(231, 76, 60, 0.1)';
statusElement.style.borderRadius = '8px';
statusElement.style.border = '2px solid #e74c3c';
}
// Call callback after showing message for 3 seconds
setTimeout(() => {
if (callback) callback();
}, 3000);
} else {
console.error('❌ Failed to capture position photo');
if (statusElement) {
statusElement.textContent = 'Photo capture failed - camera not ready';
statusElement.style.color = '#e74c3c';
}
// Still call callback to continue
if (callback) callback();
}
}
/**
* Capture verification photo and complete process
*/
captureVerificationPhoto(overlay) {
const video = overlay.querySelector('#verification-video');
const statusElement = overlay.querySelector('#verification-status');
if (video && video.videoWidth > 0) {
// Create canvas and capture frame
const canvas = document.createElement('canvas');
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
const ctx = canvas.getContext('2d');
ctx.drawImage(video, 0, 0);
// Convert to base64
const photoData = canvas.toDataURL('image/jpeg', 0.8);
// Store verification photo
const verificationPhoto = {
timestamp: new Date().toISOString(),
data: photoData,
type: 'position_verification',
position: this.currentPosition ? {
name: this.currentPosition.name,
instruction: this.currentPosition.instruction,
description: this.currentPosition.description
} : null
};
// Save to localStorage with error handling
try {
const existingPhotos = JSON.parse(localStorage.getItem('verificationPhotos') || '[]');
existingPhotos.push(verificationPhoto);
// Limit to 5 most recent photos to prevent storage overflow
const limitedPhotos = existingPhotos.slice(-5);
localStorage.setItem('verificationPhotos', JSON.stringify(limitedPhotos));
} catch (storageError) {
if (storageError.name === 'QuotaExceededError') {
console.warn('⚠️ Storage quota exceeded for verification photos, clearing old data...');
try {
localStorage.removeItem('verificationPhotos');
localStorage.setItem('verificationPhotos', JSON.stringify([verificationPhoto]));
} catch (retryError) {
console.error('❌ Failed to save verification photo after cleanup:', retryError);
}
} else {
console.error('❌ Error saving verification photo array:', storageError);
}
}
console.log('📸 Verification photo captured and stored');
if (statusElement) {
statusElement.textContent = 'Verification COMPLETE - Position documented';
statusElement.style.color = '#27ae60';
}
// Complete verification after 1 second (reduced delay)
console.log('⏰ Starting verification completion timer...');
setTimeout(() => {
console.log('⏰ Verification timer fired, calling finalizeVerification...');
try {
this.finalizeVerification(overlay);
} catch (error) {
console.error('❌ Error in finalizeVerification:', error);
// Force close the modal even if there's an error
this.closeVerificationMode();
const event = new CustomEvent('verificationComplete', {
detail: { success: true }
});
document.dispatchEvent(event);
}
}, 1000);
} else {
console.error('❌ Failed to capture verification photo');
if (statusElement) {
statusElement.textContent = 'Verification failed - try again';
statusElement.style.color = '#e74c3c';
}
}
}
/**
* Finalize verification and proceed
*/
finalizeVerification(overlay) {
console.log('✅ Position verification completed successfully');
console.log('🔄 Finalizing verification process...');
try {
console.log('🔚 Calling closeVerificationMode...');
this.closeVerificationMode();
console.log('✅ Verification mode closed successfully');
// Notify completion with enhanced logging
console.log('📢 Dispatching verificationComplete event...');
const event = new CustomEvent('verificationComplete', {
detail: { success: true }
});
document.dispatchEvent(event);
console.log('✅ verificationComplete event dispatched successfully');
} catch (error) {
console.error('❌ Error during finalization:', error);
// Try to clean up anyway
try {
this.closeVerificationMode();
} catch (closeError) {
console.error('❌ Error during emergency close:', closeError);
}
// Dispatch event anyway
const event = new CustomEvent('verificationComplete', {
detail: { success: true, error: error.message }
});
document.dispatchEvent(event);
}
}
/**
* Close verification mode
*/
closeVerificationMode() {
console.log('🔚 Closing verification mode');
// Clear timer if running
if (this.verificationTimer) {
clearInterval(this.verificationTimer);
this.verificationTimer = null;
}
// Track verification end
if (this.game && this.game.trackWebcamVerification) {
this.game.trackWebcamVerification(false);
}
// Remove verification overlay - try multiple methods for safety
const overlay = document.getElementById('verification-overlay');
if (overlay) {
console.log('🔚 Removing verification overlay...');
// Remove escape key handler if it exists
if (overlay.escapeHandler) {
document.removeEventListener('keydown', overlay.escapeHandler);
console.log('🔚 Removed escape key handler');
}
overlay.remove();
}
// Additional safety: remove any stray verification overlays
const allVerificationOverlays = document.querySelectorAll('[id*="verification"], [class*="verification"]');
allVerificationOverlays.forEach(element => {
if (element.style.position === 'fixed' || element.style.zIndex > 9000) {
console.log('🔚 Removing stray verification element:', element.id || element.className);
element.remove();
}
});
// Stop camera stream
this.stopCamera();
console.log('✅ Verification mode closed completely');
}
/**
* Display mirror interface - webcam feed without photo capture
*/
showMirrorInterface(taskData) {
// Create mirror overlay
const overlay = document.createElement('div');
overlay.id = 'mirror-overlay';
overlay.innerHTML = `
<div class="mirror-container">
<div class="mirror-header">
<h3>🪞 Look at Yourself</h3>
<p>${taskData?.instructions || 'Use the webcam as your mirror'}</p>
</div>
<div class="mirror-video-container">
<video id="mirror-video" autoplay muted playsinline></video>
</div>
<div class="mirror-timer" id="mirror-timer" style="display: none;">
<div class="mirror-progress" id="mirror-progress">
<div class="progress-bar" id="mirror-progress-bar"></div>
</div>
<div class="timer-text">Time remaining: <span id="mirror-time">${taskData?.duration || 60}</span>s</div>
</div>
<div class="mirror-controls">
<button id="mirror-complete-btn" class="btn btn-primary" disabled>
⏱️ Complete Task (${taskData?.duration || 60}s)
</button>
<button id="mirror-close-btn" class="btn btn-secondary">
❌ Close Mirror
</button>
</div>
${taskData?.taskText ? `<div class="mirror-task-text">${taskData.taskText}</div>` : ''}
</div>
`;
// Style the mirror overlay
overlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.9);
display: flex;
justify-content: center;
align-items: center;
z-index: 10000;
`;
// Style the mirror container
const containerStyle = `
background: rgba(50, 50, 55, 0.95);
border: 2px solid #8a2be2;
border-radius: 15px;
padding: 20px;
max-width: 800px;
max-height: 90vh;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.7);
text-align: center;
overflow-y: auto;
color: #e0e0e0;
`;
const videoStyle = `
width: 100%;
max-width: 640px;
height: auto;
border-radius: 10px;
margin: 15px 0;
transform: scaleX(-1); /* Mirror effect */
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);
border: 2px solid #8a2be2;
`;
const buttonStyle = `
margin: 10px;
padding: 12px 24px;
border: none;
border-radius: 8px;
font-size: 16px;
cursor: pointer;
transition: all 0.3s ease;
background: linear-gradient(135deg, #8a2be2, #5e35b1);
color: white;
font-weight: 600;
min-width: 140px;
`;
// Apply styles
overlay.querySelector('.mirror-container').style.cssText = containerStyle;
overlay.querySelector('#mirror-video').style.cssText = videoStyle;
overlay.querySelectorAll('button').forEach(btn => {
btn.style.cssText = buttonStyle;
});
// Style disabled complete button
const completeBtn = overlay.querySelector('#mirror-complete-btn');
if (completeBtn && completeBtn.disabled) {
completeBtn.style.cssText += `
background: linear-gradient(135deg, #7f8c8d, #95a5a6);
cursor: not-allowed;
opacity: 0.6;
`;
}
// Style specific buttons
const closeBtn = overlay.querySelector('#mirror-close-btn');
console.log('🔍 Button elements found:', {
completeBtn: !!completeBtn,
closeBtn: !!closeBtn,
completeBtnText: completeBtn?.textContent,
closeBtnText: closeBtn?.textContent
});
completeBtn.style.cssText += 'background: linear-gradient(135deg, #8a2be2, #5e35b1); color: white;';
// Update close button based on preventClose state
if (this.preventClose) {
closeBtn.style.cssText += 'background: linear-gradient(135deg, #666666, #555555); color: #999999; cursor: not-allowed; opacity: 0.6;';
closeBtn.textContent = '🔒 Timer Active';
} else {
closeBtn.style.cssText += 'background: linear-gradient(135deg, #8a2be2, #5e35b1); color: white;';
}
// Style task text if present
const taskTextEl = overlay.querySelector('.mirror-task-text');
if (taskTextEl) {
taskTextEl.style.cssText = `
background: rgba(255, 255, 255, 0.1);
border-radius: 8px;
padding: 15px;
margin-top: 15px;
color: #e0e0e0;
font-style: italic;
border-left: 4px solid #8a2be2;
`;
}
// Style the timer and progress bar elements
const timerEl = overlay.querySelector('#mirror-timer');
const progressEl = overlay.querySelector('#mirror-progress');
const progressBar = overlay.querySelector('#mirror-progress-bar');
if (timerEl) {
timerEl.style.cssText = `
background: rgba(255, 255, 255, 0.1);
border-radius: 8px;
padding: 15px;
margin: 15px 0;
color: #e0e0e0;
text-align: center;
`;
}
if (progressEl) {
progressEl.style.cssText = `
width: 100%;
background: rgba(255, 255, 255, 0.1);
border-radius: 4px;
margin: 10px 0;
height: 8px;
overflow: hidden;
`;
}
if (progressBar) {
progressBar.style.cssText = `
width: 0%;
height: 100%;
background: linear-gradient(90deg, #8a2be2, #9c27b0);
border-radius: 4px;
transition: width 1s ease;
`;
}
document.body.appendChild(overlay);
// Style header elements for consistency
const headerH3 = overlay.querySelector('.mirror-header h3');
const headerP = overlay.querySelector('.mirror-header p');
if (headerH3) {
headerH3.style.cssText = `
color: #e0e0e0;
font-size: 1.5em;
margin-bottom: 15px;
text-shadow: none;
`;
}
if (headerP) {
headerP.style.cssText = `
color: #c0c0c0;
font-size: 1em;
margin-bottom: 20px;
line-height: 1.6;
`;
}
// Connect video stream to mirror video element
const mirrorVideo = overlay.querySelector('#mirror-video');
if (this.stream && mirrorVideo) {
mirrorVideo.srcObject = this.stream;
}
// Add event listeners
completeBtn.addEventListener('click', () => {
// Complete task - close mirror and continue scenario
console.log('✅ COMPLETE BUTTON CLICKED - Mirror task completed by user');
this.completeMirrorTask(taskData);
});
closeBtn.addEventListener('click', () => {
console.log('❌ CLOSE BUTTON CLICKED - Training task abandoned');
// Clear timer if running
if (this.mirrorTimer) {
clearInterval(this.mirrorTimer);
this.mirrorTimer = null;
}
// Close mirror interface
this.closeMirrorMode();
// Trigger game over for abandoning training task
console.log('💀 Training task abandoned - triggering game over');
if (this.game && this.game.triggerGameOver) {
this.game.triggerGameOver('Training task abandoned. You must complete your assigned tasks to progress.');
} else if (window.game && window.game.triggerGameOver) {
window.game.triggerGameOver('Training task abandoned. You must complete your assigned tasks to progress.');
} else {
// Create condescending styled game over dialog
this.showCondescendingGameOver();
}
});
// Start timer if duration is specified
if (taskData?.duration) {
this.startMirrorTimer(taskData.duration, overlay);
}
console.log('🪞 Mirror interface displayed');
}
/**
* Start mirror task timer with visual countdown
*/
startMirrorTimer(duration, overlay) {
console.log(`⏱️ Starting mirror timer for ${duration} seconds`);
const timerDisplay = overlay.querySelector('#mirror-time');
const progressBar = overlay.querySelector('#mirror-progress-bar');
const timerElement = overlay.querySelector('#mirror-timer');
// Show timer
if (timerElement) {
timerElement.style.display = 'block';
}
let timeLeft = duration;
const completeBtn = overlay.querySelector('#mirror-complete-btn');
const timer = setInterval(() => {
timeLeft--;
// Update timer display
if (timerDisplay) {
timerDisplay.textContent = timeLeft;
}
// Update button text with countdown
if (completeBtn && timeLeft > 0) {
completeBtn.textContent = `⏱️ Complete Task (${timeLeft}s)`;
}
// Update progress bar
if (progressBar) {
const progress = ((duration - timeLeft) / duration) * 100;
progressBar.style.width = progress + '%';
}
// Timer complete
if (timeLeft <= 0) {
clearInterval(timer);
console.log('⏰ Mirror timer completed');
// Enable complete button
if (completeBtn) {
completeBtn.disabled = false;
completeBtn.textContent = '✅ Time Complete - Click to Continue';
completeBtn.style.background = '#27ae60';
completeBtn.style.animation = 'pulse 1s infinite';
completeBtn.style.cursor = 'pointer';
}
}
}, 1000);
// Store timer reference for cleanup
this.mirrorTimer = timer;
}
/**
* Complete the mirror task
*/
completeMirrorTask(taskData) {
console.log('✅ Mirror task completed');
this.closeMirrorMode();
// Notify the game that the mirror task is complete
if (this.game && this.game.interactiveTaskManager) {
this.game.interactiveTaskManager.completeMirrorTask(taskData);
}
// Dispatch completion event
const event = new CustomEvent('mirrorTaskComplete', {
detail: { taskData }
});
document.dispatchEvent(event);
}
/**
* Show harsh confirmation dialog for abandoning mirror task
*/
showAbandonmentConfirmation() {
console.log('🛑 CREATING ABANDONMENT CONFIRMATION DIALOG');
// Create confirmation overlay
const confirmOverlay = document.createElement('div');
confirmOverlay.id = 'abandon-confirm-overlay';
confirmOverlay.innerHTML = `
<div class="abandon-confirm-container">
<div class="abandon-header">
<h3>🛑 Giving Up Already?</h3>
</div>
<div class="abandon-message">
<p>Are you sure you want to give up like a pathetic quitter?</p>
<p>Your weak attempt will be recorded as a <strong>FAILURE</strong>.</p>
<p>Everyone will know you couldn't even complete a simple mirror task.</p>
</div>
<div class="abandon-controls">
<button id="abandon-confirm-btn" class="btn btn-danger">
😤 Yes, I'm a Quitter
</button>
<button id="abandon-cancel-btn" class="btn btn-success">
💪 Continue Task
</button>
</div>
</div>
`;
// Style the confirmation overlay
confirmOverlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.95);
display: flex;
justify-content: center;
align-items: center;
z-index: 15000;
`;
// Style the confirmation container
const containerEl = confirmOverlay.querySelector('.abandon-confirm-container');
containerEl.style.cssText = `
background: rgba(60, 60, 60, 0.98);
border-radius: 15px;
padding: 30px;
max-width: 500px;
text-align: center;
border: 3px solid #ff4444;
box-shadow: 0 10px 30px rgba(255, 68, 68, 0.3);
`;
// Style the header
const headerEl = confirmOverlay.querySelector('.abandon-header h3');
headerEl.style.cssText = `
color: #ff4444;
margin-bottom: 20px;
font-size: 24px;
`;
// Style the message
const messageEl = confirmOverlay.querySelector('.abandon-message');
messageEl.style.cssText = `
color: #ffffff;
margin: 20px 0;
line-height: 1.6;
`;
// Style the buttons
const confirmBtn = confirmOverlay.querySelector('#abandon-confirm-btn');
const cancelBtn = confirmOverlay.querySelector('#abandon-cancel-btn');
confirmBtn.style.cssText = `
background: linear-gradient(135deg, #ff4444, #cc0000);
color: white;
border: none;
padding: 12px 24px;
margin: 10px;
border-radius: 8px;
font-size: 16px;
cursor: pointer;
`;
cancelBtn.style.cssText = `
background: linear-gradient(135deg, #44ff44, #00cc00);
color: white;
border: none;
padding: 12px 24px;
margin: 10px;
border-radius: 8px;
font-size: 16px;
cursor: pointer;
`;
document.body.appendChild(confirmOverlay);
// Add event listeners
confirmBtn.addEventListener('click', () => {
confirmOverlay.remove();
this.abandonMirrorTask();
});
cancelBtn.addEventListener('click', () => {
confirmOverlay.remove();
});
}
/**
* Handle mirror task abandonment
*/
abandonMirrorTask() {
console.log('❌ Mirror task abandoned by user');
// Reset prevent close
this.preventClose = false;
// Close mirror
this.closeMirrorMode();
// Notify game of abandonment
if (this.game && this.game.interactiveTaskManager) {
this.game.interactiveTaskManager.abandonMirrorTask();
}
// Dispatch abandonment event
const event = new CustomEvent('mirrorTaskAbandoned');
document.dispatchEvent(event);
}
/**
* Close mirror mode
*/
closeMirrorMode() {
console.log('🔚 Closing mirror mode');
// Clean up timer
if (this.mirrorTimer) {
clearInterval(this.mirrorTimer);
this.mirrorTimer = null;
console.log('⏱️ Mirror timer cleared');
}
// Track webcam mirror end for XP
if (this.game && this.game.trackWebcamMirror) {
this.game.trackWebcamMirror(false);
}
// Remove mirror overlay
const overlay = document.getElementById('mirror-overlay');
if (overlay) {
overlay.remove();
}
// Stop camera stream
this.stopCamera();
}
/**
* Show condescending game over dialog for training abandonment
*/
showCondescendingGameOver() {
const overlay = document.createElement('div');
overlay.id = 'condescending-game-over';
overlay.innerHTML = `
<div class="game-over-container">
<div class="game-over-header">
<h1>Training Abandoned</h1>
<p class="subtitle">Task Failed</p>
</div>
<div class="condescending-content">
<p class="main-message">
Really? You couldn't even handle a simple training exercise.
</p>
<div class="failure-details">
<p>• <strong>Status:</strong> Task abandoned before completion</p>
<p>• <strong>Reason:</strong> Lack of commitment and discipline</p>
<p>• <strong>Result:</strong> Complete failure to follow instructions</p>
</div>
<div class="harsh-verdict">
<p>This training was designed to test your dedication. You failed that test miserably.</p>
<p>If you can't handle basic mirror exercises, maybe you're not ready for real training. Consider whether you actually want to improve yourself or if you're just wasting everyone's time.</p>
</div>
<div class="consequence-section">
<p><strong>Consequence:</strong> Session will restart. Try to show some backbone next time.</p>
</div>
</div>
<div class="game-over-controls">
<button id="restart-session-btn" class="failure-btn">
Restart Session
</button>
</div>
</div>
`;
// Clean, condescending styling without excessive theatrics
overlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.9);
display: flex;
justify-content: center;
align-items: center;
z-index: 99999;
font-family: 'Arial', sans-serif;
`;
const container = overlay.querySelector('.game-over-container');
container.style.cssText = `
background: linear-gradient(135deg, #2c3e50, #34495e);
border: 2px solid #e74c3c;
border-radius: 12px;
padding: 40px;
max-width: 500px;
width: 90%;
text-align: center;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
`;
const header = overlay.querySelector('.game-over-header');
header.style.cssText = `
margin-bottom: 25px;
border-bottom: 1px solid #e74c3c;
padding-bottom: 15px;
`;
const title = overlay.querySelector('h1');
title.style.cssText = `
color: #e74c3c;
font-size: 1.8em;
margin: 0 0 10px 0;
font-weight: bold;
`;
const subtitle = overlay.querySelector('.subtitle');
subtitle.style.cssText = `
color: #bdc3c7;
font-size: 1em;
margin: 0;
`;
const content = overlay.querySelector('.condescending-content');
content.style.cssText = `
color: #ecf0f1;
line-height: 1.6;
font-size: 1em;
`;
const mainMessage = overlay.querySelector('.main-message');
mainMessage.style.cssText = `
font-size: 1.1em;
color: #e67e22;
margin-bottom: 20px;
font-weight: 500;
`;
const failureDetails = overlay.querySelector('.failure-details');
failureDetails.style.cssText = `
background: rgba(231, 76, 60, 0.1);
border-left: 3px solid #e74c3c;
padding: 15px;
margin: 20px 0;
text-align: left;
border-radius: 4px;
`;
const harshVerdict = overlay.querySelector('.harsh-verdict');
harshVerdict.style.cssText = `
background: rgba(0, 0, 0, 0.2);
padding: 15px;
border-radius: 6px;
margin: 20px 0;
border: 1px solid #34495e;
`;
const consequenceSection = overlay.querySelector('.consequence-section');
consequenceSection.style.cssText = `
background: rgba(231, 76, 60, 0.15);
padding: 15px;
border-radius: 6px;
margin: 20px 0;
border: 1px solid #e74c3c;
`;
const restartBtn = overlay.querySelector('#restart-session-btn');
restartBtn.style.cssText = `
background: linear-gradient(135deg, #e74c3c, #c0392b);
color: white;
border: none;
border-radius: 6px;
padding: 12px 24px;
font-size: 1em;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
margin-top: 20px;
`;
// Simple hover effect
restartBtn.addEventListener('mouseenter', () => {
restartBtn.style.background = 'linear-gradient(135deg, #c0392b, #a93226)';
});
restartBtn.addEventListener('mouseleave', () => {
restartBtn.style.background = 'linear-gradient(135deg, #e74c3c, #c0392b)';
});
// Add event listener for restart
restartBtn.addEventListener('click', () => {
console.log('🔄 Restarting session after training abandonment...');
window.location.reload();
});
document.body.appendChild(overlay);
console.log('💀 Condescending game over dialog displayed');
}
/**
* Stop camera stream
*/
stopCamera() {
if (this.stream) {
this.stream.getTracks().forEach(track => track.stop());
this.stream = null;
}
this.isActive = false;
console.log('📷 Camera stopped');
}
/**
* Show camera error message
*/
showCameraError(error) {
let message = 'Camera access failed. ';
if (error.name === 'NotAllowedError') {
message += 'Please allow camera access in your browser settings.';
} else if (error.name === 'NotFoundError') {
message += 'No camera found on this device.';
} else {
message += 'Please check your camera and try again.';
}
// Show error notification
if (this.game && this.game.showNotification) {
this.game.showNotification(message, 'error', 5000);
} else {
alert(message);
}
}
/**
* Add styles for photo gallery
*/
addGalleryStyles() {
if (document.getElementById('gallery-styles')) return;
const styles = document.createElement('style');
styles.id = 'gallery-styles';
styles.textContent = `
#photo-gallery-overlay .gallery-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.95);
display: flex;
align-items: center;
justify-content: center;
z-index: 10001;
}
#photo-gallery-overlay .gallery-container {
background: #2a2a2a;
border-radius: 10px;
padding: 30px;
max-width: 90vw;
max-height: 90vh;
overflow-y: auto;
color: white;
}
#photo-gallery-overlay .gallery-header {
text-align: center;
margin-bottom: 20px;
color: #ff6b6b;
}
#photo-gallery-overlay .gallery-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 15px;
margin-bottom: 30px;
}
#photo-gallery-overlay .gallery-item {
position: relative;
cursor: pointer;
border-radius: 8px;
overflow: hidden;
transition: transform 0.3s, box-shadow 0.3s;
border: 2px solid #444;
}
#photo-gallery-overlay .gallery-item:hover {
transform: scale(1.05);
box-shadow: 0 4px 15px rgba(255, 107, 107, 0.3);
}
#photo-gallery-overlay .gallery-item img {
width: 100%;
height: 150px;
object-fit: cover;
}
#photo-gallery-overlay .photo-number {
position: absolute;
top: 5px;
right: 5px;
background: rgba(255, 107, 107, 0.8);
color: white;
border-radius: 50%;
width: 25px;
height: 25px;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: bold;
}
#photo-gallery-overlay .gallery-controls {
text-align: center;
margin: 20px 0;
}
#photo-gallery-overlay .gallery-close-btn {
background: #2ed573;
color: white;
border: none;
padding: 15px 30px;
border-radius: 5px;
font-size: 16px;
cursor: pointer;
transition: all 0.3s;
}
#photo-gallery-overlay .gallery-close-btn:hover {
background: #27ae60;
transform: translateY(-2px);
}
#photo-gallery-overlay .gallery-note {
text-align: center;
color: #aaa;
font-size: 14px;
}
/* Photo Viewer Styles */
#photo-viewer-overlay .photo-viewer-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.98);
display: flex;
align-items: center;
justify-content: center;
z-index: 10002;
}
#photo-viewer-overlay .photo-viewer-container {
background: #2a2a2a;
border-radius: 10px;
max-width: 90vw;
max-height: 90vh;
color: white;
overflow: hidden;
}
#photo-viewer-overlay .photo-viewer-header {
padding: 15px 20px;
background: #3a3a3a;
display: flex;
justify-content: space-between;
align-items: center;
}
#photo-viewer-overlay .photo-viewer-close {
background: none;
border: none;
color: white;
font-size: 24px;
cursor: pointer;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
}
#photo-viewer-overlay .photo-viewer-content {
padding: 20px;
text-align: center;
}
#photo-viewer-overlay .photo-viewer-content img {
max-width: 100%;
max-height: 70vh;
border-radius: 5px;
}
#photo-viewer-overlay .photo-viewer-nav {
padding: 15px 20px;
background: #3a3a3a;
display: flex;
justify-content: space-between;
align-items: center;
}
#photo-viewer-overlay .photo-viewer-nav button {
background: #ff6b6b;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
transition: all 0.3s;
}
#photo-viewer-overlay .photo-viewer-nav button:hover {
background: #ff5252;
}
`;
document.head.appendChild(styles);
}
/**
* Add camera interface styles
*/
addCameraStyles() {
if (document.getElementById('camera-styles')) return;
const styles = document.createElement('style');
styles.id = 'camera-styles';
styles.textContent = `
#camera-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.95);
z-index: 10000;
display: flex;
align-items: center;
justify-content: center;
}
.camera-container {
background: #2a2a2a;
border-radius: 10px;
padding: 20px;
max-width: 90vw;
max-height: 90vh;
overflow-y: auto;
}
.camera-header {
text-align: center;
margin-bottom: 20px;
color: white;
}
.camera-preview {
position: relative;
margin-bottom: 20px;
border-radius: 10px;
overflow: hidden;
}
#camera-feed {
width: 100%;
max-width: 640px;
height: auto;
background: #000;
}
.camera-guidelines {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
}
.guideline {
position: absolute;
background: rgba(255, 255, 255, 0.3);
}
.guideline.horizontal {
left: 0;
right: 0;
top: 50%;
height: 1px;
transform: translateY(-50%);
}
.guideline.vertical {
top: 0;
bottom: 0;
left: 50%;
width: 1px;
transform: translateX(-50%);
}
.camera-controls {
text-align: center;
margin: 20px 0;
}
.camera-controls button {
margin: 0 10px;
padding: 12px 24px;
border: none;
border-radius: 5px;
font-size: 16px;
cursor: pointer;
transition: all 0.3s;
}
.capture-btn {
background: #ff4757;
color: white;
}
.retake-btn {
background: #ffa502;
color: white;
}
.accept-btn {
background: #2ed573;
color: white;
}
.complete-btn {
background: #2ed573;
color: white;
}
.end-btn {
background: #747d8c;
color: white;
}
.camera-controls button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.3);
}
.progress-indicator {
background: #3a3a3a;
padding: 10px;
border-radius: 5px;
margin: 10px 0;
font-size: 18px;
font-weight: bold;
color: #ff6b6b;
}
.photo-preview {
text-align: center;
margin: 20px 0;
}
#captured-image {
max-width: 100%;
max-height: 300px;
border-radius: 5px;
border: 2px solid #ddd;
}
.session-info {
text-align: center;
color: #ccc;
font-size: 14px;
}
.privacy-note {
color: #ffcccc;
font-style: italic;
margin-top: 10px;
}
`;
document.head.appendChild(styles);
}
/**
* Check if photography tasks should use webcam
*/
isPhotographyTask(task) {
if (!task || !task.interactiveType) return false;
const photoKeywords = ['photo', 'photograph', 'camera', 'picture', 'pose', 'submissive_photo'];
return photoKeywords.some(keyword =>
task.interactiveType.includes(keyword) ||
task.text?.toLowerCase().includes(keyword)
);
}
/**
* Get photo session type from task
*/
getSessionTypeFromTask(task) {
if (task.interactiveType?.includes('submissive')) return 'submissive_poses';
if (task.interactiveType?.includes('dress')) return 'dress_up_photos';
return 'general_photography';
}
/**
* Add text overlay to canvas with degrading message
*/
addTextOverlayToCanvas(ctx, canvasWidth, canvasHeight, message, phase) {
// Clean up the message (remove the *SNAP* part for overlay)
const cleanMessage = message.replace(/📸 \*SNAP\* - /, '');
// Set up text styling
const fontSize = Math.max(16, Math.floor(canvasWidth / 25));
ctx.font = `bold ${fontSize}px Arial`;
ctx.textAlign = 'center';
ctx.strokeStyle = 'black';
ctx.lineWidth = 3;
// Different colors for start/end phases
if (phase === 'start') {
ctx.fillStyle = '#4CAF50'; // Green for start
} else {
ctx.fillStyle = '#F44336'; // Red for end
}
// Add phase indicator at top
const phaseText = phase === 'start' ? '🔥 SLUT TRAINING BEGINS' : '💀 MIND-FUCKED & BROKEN';
const phaseY = 40;
// Draw phase indicator with outline
ctx.font = `bold ${Math.floor(fontSize * 1.2)}px Arial`;
ctx.strokeText(phaseText, canvasWidth / 2, phaseY);
ctx.fillText(phaseText, canvasWidth / 2, phaseY);
// Add timestamp
const timestamp = new Date().toLocaleString();
const timestampY = phaseY + 35;
ctx.font = `${Math.floor(fontSize * 0.8)}px Arial`;
ctx.fillStyle = '#FFFFFF';
ctx.strokeText(timestamp, canvasWidth / 2, timestampY);
ctx.fillText(timestamp, canvasWidth / 2, timestampY);
// Add degrading message at bottom
const messageY = canvasHeight - 60;
ctx.font = `bold ${fontSize}px Arial`;
ctx.fillStyle = '#FFD700'; // Gold color for the degrading message
// Handle text wrapping for long messages
const maxWidth = canvasWidth - 40;
const words = cleanMessage.split(' ');
let line = '';
let lineHeight = fontSize * 1.2;
let currentY = messageY;
for (let i = 0; i < words.length; i++) {
const testLine = line + words[i] + ' ';
const metrics = ctx.measureText(testLine);
if (metrics.width > maxWidth && i > 0) {
// Draw current line
ctx.strokeText(line.trim(), canvasWidth / 2, currentY);
ctx.fillText(line.trim(), canvasWidth / 2, currentY);
// Move to next line
line = words[i] + ' ';
currentY += lineHeight;
} else {
line = testLine;
}
}
// Draw the last line
if (line.trim()) {
ctx.strokeText(line.trim(), canvasWidth / 2, currentY);
ctx.fillText(line.trim(), canvasWidth / 2, currentY);
}
// Add decorative border
ctx.strokeStyle = phase === 'start' ? '#4CAF50' : '#F44336';
ctx.lineWidth = 4;
ctx.strokeRect(5, 5, canvasWidth - 10, canvasHeight - 10);
console.log(`📸 Added text overlay: ${phaseText} - ${cleanMessage}`);
}
/**
* Add text overlay for training session photos
*/
addTrainingPhotoOverlay(ctx, canvasWidth, canvasHeight) {
// Select a random message from regular training messages
const regularMessages = [
"Sissy in training",
"Dress up session",
"Learning to be feminine",
"Transformation in progress",
"Becoming the perfect sissy",
"Style practice",
"Outfit training",
"Femininity lesson",
"Beauty practice session",
"Sissy improvement time",
"Fashion training",
"Girly makeover session",
"Feminization progress",
"Pretty sissy practice",
"Style development session"
];
const message = regularMessages[Math.floor(Math.random() * regularMessages.length)];
const timestamp = new Date().toLocaleString();
// Setup text styling (simpler than verification photos)
ctx.font = "16px Arial";
ctx.textAlign = "left";
ctx.lineWidth = 3;
// Add subtle background overlay for readability
ctx.fillStyle = "rgba(0, 0, 0, 0.3)";
ctx.fillRect(10, 10, canvasWidth - 20, 60);
// Draw timestamp
ctx.strokeStyle = "black";
ctx.fillStyle = "white";
ctx.strokeText(timestamp, 20, 30);
ctx.fillText(timestamp, 20, 30);
// Draw message
ctx.strokeStyle = "black";
ctx.fillStyle = "#FFD700";
ctx.strokeText(message, 20, 55);
ctx.fillText(message, 20, 55);
}
/**
* Cleanup webcam resources
*/
cleanup() {
this.stopCamera();
if (this.video) {
this.video.remove();
this.video = null;
}
const overlay = document.getElementById('camera-overlay');
if (overlay) {
overlay.remove();
}
const styles = document.getElementById('camera-styles');
if (styles) {
styles.remove();
}
console.log('🧹 WebcamManager cleanup complete');
}
}
// Export for module usage
if (typeof module !== 'undefined' && module.exports) {
module.exports = WebcamManager;
}