/** * 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 this.usedPositions = new Set(); // Track positions used in this session to prevent repeats 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 = `

๐Ÿ“ธ Photography Session

${this.currentPhotoSession.requirements.description}

0 / ${this.currentPhotoSession.photosNeeded} photos

โš ๏ธ Photos are stored locally and not uploaded anywhere

`; 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 console.log(`๐Ÿ“Š Checking completion: ${photosTaken} >= ${photosNeeded}?`, photosTaken >= photosNeeded); if (photosTaken >= photosNeeded) { console.log(`โœ… Photo session complete! ${photosTaken}/${photosNeeded} photos taken - auto-completing in 1 second...`); // Auto-complete session when required photos are captured setTimeout(() => { console.log('โฐ Timeout fired - calling completePhotoSession()'); this.completePhotoSession(); }, 1000); } else { // Return to camera view for more photos this.showCameraPreview(); console.log(`๐Ÿ“ธ Photo accepted (${photosTaken}/${photosNeeded}) - continuing session`); } } /** * Complete the photo session */ completePhotoSession() { console.log('๐ŸŽ‰ Photo session completed successfully'); console.log('๐Ÿ“Š Current session:', this.currentPhotoSession); // For single-photo sessions (like progression challenges), skip gallery const isSinglePhotoSession = this.currentPhotoSession.photosNeeded === 1; console.log(`๐Ÿ” Single-photo session? ${isSinglePhotoSession} (photosNeeded: ${this.currentPhotoSession.photosNeeded})`); if (isSinglePhotoSession) { console.log('๐Ÿ“ธ Single-photo session - skipping gallery, ending immediately'); // Notify task completion immediately console.log('๐Ÿ”” Dispatching photoSessionComplete event...'); const event = new CustomEvent('photoSessionComplete', { detail: { photos: this.currentPhotoSession.photos, sessionType: this.currentPhotoSession.type, taskData: this.currentPhotoSession.taskData } }); document.dispatchEvent(event); console.log('โœ… Event dispatched'); // End session console.log('๐Ÿ”š Calling endPhotoSession()...'); this.endPhotoSession(); console.log('โœ… endPhotoSession() completed'); } else { console.log('๐Ÿ“ธ Multi-photo session - showing gallery first'); // Show photo gallery for multi-photo sessions before completing this.showPhotoGallery(() => { console.log('๐Ÿ“ธ Gallery closed - dispatching event and ending session'); // 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 = ` `; 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 = `

Photo ${index + 1} of ${this.currentPhotoSession.photos.length}

Photo ${index + 1}
${index > 0 ? `` : '
'} ${index < this.currentPhotoSession.photos.length - 1 ? `` : '
'}
`; 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 = `

๐Ÿ“ธ Photography Session

Position yourself according to the task instructions

Photos taken: 0

โš ๏ธ Photos are stored locally and not uploaded anywhere

`; 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) */ async 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 to file system if running in Electron if (this.game && this.game.fileManager && this.game.fileManager.isElectron) { await this.savePhotoToFileSystem(photoData); } else { // Browser mode - save to localStorage with user consent this.savePersistentPhoto(photoData); } } /** * Save photo to file system (Electron only) */ async savePhotoToFileSystem(photoData) { try { const savedPhoto = await this.game.fileManager.savePhoto( photoData.dataURL, photoData.sessionType ); if (savedPhoto) { console.log(`๐Ÿ“ธ Photo saved to file system: ${savedPhoto.filename}`); this.showNotification('๐Ÿ“ธ Photo saved to file system!', 'success'); } else { console.warn('๐Ÿ“ธ Failed to save photo to file system'); this.showNotification('โš ๏ธ Failed to save photo', 'warning'); } } catch (error) { console.error('๐Ÿ“ธ Error saving photo to file system:', error); this.showNotification('โš ๏ธ Failed to save photo', 'warning'); } } /** * 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 = `

๐Ÿ“ธ Save Photos for Later?

Would you like to save your captured photos so you can view and download them later?

Photos are stored locally on your device only. You can change this setting anytime in options.

`; 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'); console.log('๐Ÿ” Camera overlay element:', overlay); if (overlay) { console.log('๐Ÿ—‘๏ธ Removing camera overlay...'); overlay.remove(); console.log('โœ… Camera overlay removed'); } else { console.log('โš ๏ธ No camera overlay found to remove'); } // Stop camera stream console.log('๐Ÿ“น Stopping camera stream...'); this.stopCamera(); console.log('โœ… Camera stream stopped'); // 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" }, { name: "Submissive Edge", instruction: "Kneeling upright while edging slowly, maintain proper posture", description: "Edge yourself while maintaining submissive kneeling position" }, { name: "Crawling Service", instruction: "On hands and knees in crawling position, ready to serve", description: "Degrading crawl position showing you're nothing but an animal" } ], degrading: [ { name: "Object Display", instruction: "On your back with legs pulled up and spread, completely exposed like a piece of furniture", description: "Reduced to nothing but an object for use and display" }, { name: "Mouth Service Position", instruction: "Squatting low with mouth open wide, ready to be used", description: "Ultimate degradation pose prepared to service" }, { 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" }, { name: "Degraded Edge", instruction: "Squatting with legs wide, edging yourself while keeping balance", description: "Edge in the most degrading squat position possible" }, { name: "Toilet Position", instruction: "Squatting low over imaginary toilet, holding degrading pose", description: "Ultimate degradation - reduced to using toilet position" }, { name: "Face-Down Edge", instruction: "Face pressed to floor, ass up, edge yourself in this humiliating position", description: "Edge while face-down in the most degrading pose" } ], 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" }, { name: "Humiliated Edge", instruction: "On your back, legs spread, edge while looking at yourself in shame", description: "Edge yourself while watching your own humiliation" }, { name: "Exposed Edge", instruction: "Standing spread eagle against wall, edge yourself while fully exposed", description: "Edge while completely exposed and vulnerable against the wall" }, { name: "Mirror Shame", instruction: "Squat facing mirror, edge while watching your pathetic reflection", description: "Edge while forced to watch yourself in the mirror" }, { name: "Legs Spread Display", instruction: "Sitting with legs in wide V-shape, edge while staying spread open", description: "Maintain humiliating spread position while edging" } ], 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" }, { name: "Pinned Edge", instruction: "On back with legs pinned wide open by your hands, edge while holding yourself spread", description: "Edge while using your own hands to hold yourself in maximum exposure" }, { name: "Breeding Edge", instruction: "Face down ass up, edge yourself while presenting like an animal in heat", description: "Edge while in breeding position - complete animalistic degradation" }, { name: "Exposed Hole Edge", instruction: "On back, legs pulled all the way back to shoulders, edge while fully exposed", description: "Edge while exposing everything in the most vulnerable position" }, { name: "Thigh Squeeze Edge", instruction: "Sitting or lying, squeeze thighs together rhythmically to edge without using hands", description: "Edge by squeezing your thighs together - pathetic and desperate" }, { name: "Squatting Edge", instruction: "Deep squat with legs spread wide, edge while maintaining this degrading position", description: "Edge while in a deep, exposed squat - thighs burning with effort" }, { name: "Worship Edge", instruction: "Kneeling with forehead to ground, ass up, edge yourself in worship position", description: "Edge while in ultimate submission worship pose" } ] }; } /** * Select random position from bank based on intensity * Ensures no repeats within a single session */ 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; } // Filter out already used positions const unusedPositions = availablePositions.filter(pos => !this.usedPositions.has(pos.name)); // If all positions have been used, reset the tracker if (unusedPositions.length === 0) { console.log('๐Ÿ”„ All positions used - resetting position tracker'); this.usedPositions.clear(); return this.selectRandomPosition(intensity); // Recursive call with fresh pool } // Select random position from unused ones const randomIndex = Math.floor(Math.random() * unusedPositions.length); const selectedPosition = unusedPositions[randomIndex]; // Mark this position as used this.usedPositions.add(selectedPosition.name); console.log(`๐Ÿ“ Selected position: ${selectedPosition.name} (${this.usedPositions.size} used so far)`); return selectedPosition; } /** * 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 = `

๐Ÿ” Position Verification Required

${verificationData?.instructions || 'Follow the position instructions and maintain pose'}

${selectedPosition.name}
${selectedPosition.instruction}
${selectedPosition.description}
${verificationData?.verificationText ? `
${verificationData.verificationText}
` : ''}
`; // 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'); const skipBtn = overlay.querySelector('#verification-skip-btn'); // Hide start button and show timer if (startBtn) startBtn.style.display = 'none'; if (timerElement) timerElement.style.display = 'block'; // Show skip button for testing if (skipBtn) { skipBtn.style.display = 'inline-block'; skipBtn.addEventListener('click', () => { console.log('โฉ Skip button clicked - completing verification timer'); // Clear all timers if (this.preparationTimer) clearInterval(this.preparationTimer); if (this.verificationTimer) clearInterval(this.verificationTimer); // Complete verification immediately this.completeVerification(overlay); }); } // 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 = `

๐Ÿชž Look at Yourself

${taskData?.instructions || 'Use the webcam as your mirror'}

${taskData?.taskText ? `
${taskData.taskText}
` : ''}
`; // 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 skipBtn = overlay.querySelector('#skip-mirror-overlay-timer-btn'); // Add skip button handler if (skipBtn) { skipBtn.addEventListener('click', () => { console.log('โฉ Skip button clicked - completing timer'); timeLeft = 0; // This will trigger completion on next interval }); // Style the skip button skipBtn.style.cssText = ` background: linear-gradient(135deg, #ff9800 0%, #f57c00 100%); color: white; border: 2px solid rgba(255, 255, 255, 0.2); border-radius: 12px; padding: 10px 20px; font-size: 14px; font-weight: bold; cursor: pointer; transition: all 0.3s ease; box-shadow: 0 4px 12px rgba(255, 152, 0, 0.4); margin-top: 10px; `; skipBtn.addEventListener('mouseenter', () => { skipBtn.style.background = 'linear-gradient(135deg, #fb8c00 0%, #e65100 100%)'; skipBtn.style.transform = 'translateY(-2px)'; skipBtn.style.boxShadow = '0 6px 16px rgba(255, 152, 0, 0.5)'; }); skipBtn.addEventListener('mouseleave', () => { skipBtn.style.background = 'linear-gradient(135deg, #ff9800 0%, #f57c00 100%)'; skipBtn.style.transform = 'translateY(0)'; skipBtn.style.boxShadow = '0 4px 12px rgba(255, 152, 0, 0.4)'; }); } 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 = `

๐Ÿ›‘ Giving Up Already?

Are you sure you want to give up like a pathetic quitter?

Your weak attempt will be recorded as a FAILURE.

Everyone will know you couldn't even complete a simple mirror task.

`; // 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 = `

Training Abandoned

Task Failed

Really? You couldn't even handle a simple training exercise.

โ€ข Status: Task abandoned before completion

โ€ข Reason: Lack of commitment and discipline

โ€ข Result: Complete failure to follow instructions

This training was designed to test your dedication. You failed that test miserably.

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.

Consequence: Session will restart. Try to show some backbone next time.

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