/** * WebcamManager - Handles webcam integration for photography tasks * Provides camera access, photo capture, and integration with interactive scenarios */ class WebcamManager { constructor(game) { this.game = game; this.stream = null; this.video = null; this.canvas = null; this.context = null; this.isActive = false; this.capturedPhotos = []; this.currentPhotoSession = null; this.preventClose = false; // Prevent camera closure during mirror sessions console.log('๐ŸŽฅ WebcamManager initialized'); } /** * Initialize webcam functionality */ async init() { console.log('๐ŸŽฅ Initializing webcam system...'); // Check if webcam is supported if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { console.warn('๐Ÿ“ท Webcam not supported in this browser'); return false; } // Create video element for camera feed this.video = document.createElement('video'); this.video.setAttribute('playsinline', true); this.video.style.display = 'none'; document.body.appendChild(this.video); // Create canvas for photo capture this.canvas = document.createElement('canvas'); this.context = this.canvas.getContext('2d'); console.log('โœ… Webcam system ready'); return true; } /** * Request camera access from user */ async requestCameraAccess() { console.log('๐Ÿ“ท Requesting camera access...'); try { const constraints = { video: { width: { ideal: 1280 }, height: { ideal: 720 }, facingMode: 'user' // Front-facing camera preferred }, audio: false }; this.stream = await navigator.mediaDevices.getUserMedia(constraints); this.video.srcObject = this.stream; return new Promise((resolve) => { this.video.onloadedmetadata = () => { this.video.play(); this.isActive = true; console.log('โœ… Camera access granted and active'); resolve(true); }; }); } catch (error) { console.error('โŒ Camera access denied or failed:', error); this.showCameraError(error); return false; } } /** * Start photo session with progress tracking */ async startPhotoSessionWithProgress(sessionType, taskData) { console.log(`๐Ÿ“ธ Starting photo session with progress: ${sessionType}`); // Request camera if not already active if (!this.isActive) { const accessGranted = await this.requestCameraAccess(); if (!accessGranted) return false; } this.currentPhotoSession = { type: sessionType, taskData: taskData, photos: [], startTime: Date.now(), requirements: taskData.requirements || { count: 1, description: 'Take photos to complete' }, photosNeeded: taskData.requirements?.count || 1 }; // Show camera interface with progress this.showCameraInterfaceWithProgress(); return true; } /** * Display camera interface with progress tracking */ showCameraInterfaceWithProgress() { // Create camera overlay const overlay = document.createElement('div'); overlay.id = 'camera-overlay'; overlay.innerHTML = `

๐Ÿ“ธ 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 if (photosTaken >= photosNeeded) { // Show completion button document.getElementById('complete-session').style.display = 'inline-block'; document.getElementById('capture-photo').style.display = 'none'; document.getElementById('accept-photo').style.display = 'none'; // Update header document.querySelector('.session-instruction').textContent = '๐ŸŽ‰ Session complete! All photos taken.'; console.log(`โœ… Photo session complete! ${photosTaken}/${photosNeeded} photos taken`); } else { // Return to camera view for more photos this.showCameraPreview(); console.log(`๐Ÿ“ธ Photo accepted (${photosTaken}/${photosNeeded})`); } } /** * Complete the photo session */ completePhotoSession() { console.log('๐ŸŽ‰ Photo session completed successfully'); // Show photo gallery before completing this.showPhotoGallery(() => { // Notify task completion after gallery is closed const event = new CustomEvent('photoSessionComplete', { detail: { photos: this.currentPhotoSession.photos, sessionType: this.currentPhotoSession.type, taskData: this.currentPhotoSession.taskData } }); document.dispatchEvent(event); // End session this.endPhotoSession(); }); } /** * Show photo gallery with all taken photos */ showPhotoGallery(onClose) { if (!this.currentPhotoSession || this.currentPhotoSession.photos.length === 0) { if (onClose) onClose(); return; } const gallery = document.createElement('div'); gallery.id = 'photo-gallery-overlay'; gallery.innerHTML = ` `; 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); // Convert to image data const imageDataURL = this.canvas.toDataURL('image/jpeg', 0.8); // Show photo preview this.showPhotoPreview(imageDataURL); console.log('๐Ÿ“ธ Photo captured'); } /** * Start photo countdown timer */ startPhotoTimer(duration = 3) { const captureBtn = document.getElementById('capture-photo'); const originalText = captureBtn.innerHTML; let timeLeft = duration; let cancelled = false; // Disable capture button and show cancel option captureBtn.disabled = true; captureBtn.style.opacity = '0.7'; // Create large countdown overlay const cameraPreview = document.querySelector('.camera-preview'); const countdownOverlay = document.createElement('div'); countdownOverlay.id = 'countdown-overlay'; countdownOverlay.style.cssText = ` position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); display: flex; flex-direction: column; align-items: center; justify-content: center; z-index: 10; border-radius: 10px; `; const countdownText = document.createElement('div'); countdownText.id = 'countdown-text'; countdownText.style.cssText = ` font-size: 72px; font-weight: bold; color: white; text-shadow: 2px 2px 4px rgba(0,0,0,0.8); animation: pulse 1s infinite; margin-bottom: 20px; `; const cancelBtn = document.createElement('button'); cancelBtn.textContent = 'โŒ Cancel'; cancelBtn.style.cssText = ` background: rgba(220, 53, 69, 0.8); color: white; border: none; padding: 10px 20px; border-radius: 5px; font-size: 16px; cursor: pointer; transition: all 0.3s; `; cancelBtn.addEventListener('click', () => { cancelled = true; countdownOverlay.remove(); captureBtn.innerHTML = originalText; captureBtn.disabled = false; captureBtn.style.opacity = '1'; console.log('๐Ÿ“ฑ Photo timer cancelled'); }); countdownOverlay.appendChild(countdownText); countdownOverlay.appendChild(cancelBtn); cameraPreview.style.position = 'relative'; cameraPreview.appendChild(countdownOverlay); // Add pulse animation if (!document.getElementById('pulse-animation-style')) { const style = document.createElement('style'); style.id = 'pulse-animation-style'; style.textContent = ` @keyframes pulse { 0% { transform: scale(1); opacity: 1; } 50% { transform: scale(1.1); opacity: 0.8; } 100% { transform: scale(1); opacity: 1; } } `; document.head.appendChild(style); } // Create countdown display const updateCountdown = () => { if (cancelled) return; // Stop if cancelled if (timeLeft > 0) { captureBtn.innerHTML = `๐Ÿ“ท Get Ready...`; countdownText.textContent = timeLeft; timeLeft--; setTimeout(updateCountdown, 1000); } else { // Show capture indicator countdownText.textContent = '๐Ÿ“ธ'; countdownText.style.fontSize = '96px'; cancelBtn.style.display = 'none'; // Hide cancel button captureBtn.innerHTML = '๐Ÿ“ธ Capturing...'; setTimeout(() => { if (!cancelled) { // Take the photo this.capturePhoto(); // Remove countdown overlay countdownOverlay.remove(); // Reset button after capture setTimeout(() => { captureBtn.innerHTML = originalText; captureBtn.disabled = false; captureBtn.style.opacity = '1'; }, 500); } }, 500); } }; // Start countdown updateCountdown(); console.log(`๐Ÿ“ฑ Photo timer started: ${duration} seconds`); } /** * Show captured photo preview */ showPhotoPreview(imageDataURL) { const preview = document.getElementById('photo-preview'); const image = document.getElementById('captured-image'); image.src = imageDataURL; preview.style.display = 'block'; // Update button visibility document.getElementById('capture-photo').style.display = 'none'; document.getElementById('retake-photo').style.display = 'inline-block'; document.getElementById('accept-photo').style.display = 'inline-block'; } /** * Show camera preview (for retaking) */ showCameraPreview() { const preview = document.getElementById('photo-preview'); preview.style.display = 'none'; // Update button visibility document.getElementById('capture-photo').style.display = 'inline-block'; document.getElementById('retake-photo').style.display = 'none'; document.getElementById('accept-photo').style.display = 'none'; } /** * Accept and save the captured photo */ acceptPhoto() { const image = document.getElementById('captured-image'); const photoData = { dataURL: image.src, timestamp: Date.now(), sessionType: this.currentPhotoSession.type, taskData: this.currentPhotoSession.taskData }; // Add to session photos this.currentPhotoSession.photos.push(photoData); this.capturedPhotos.push(photoData); // Track photo for XP (1 XP per photo) if (this.game && this.game.incrementPhotosTaken) { this.game.incrementPhotosTaken(); } // Update photo count document.getElementById('photo-count').textContent = this.currentPhotoSession.photos.length; // Save to local storage (optional - user privacy) this.savePhotoData(photoData); // Return to camera view for more photos this.showCameraPreview(); // Trigger task progression this.notifyTaskComplete(photoData); console.log(`โœ… Photo accepted and saved (${this.currentPhotoSession.photos.length} total)`); } /** * Save photo data (respecting privacy) */ savePhotoData(photoData) { // Save metadata to session storage (temporary) const metadata = { timestamp: photoData.timestamp, sessionType: photoData.sessionType, taskId: photoData.taskData?.id }; const sessionPhotos = JSON.parse(sessionStorage.getItem('photoSession') || '[]'); sessionPhotos.push(metadata); sessionStorage.setItem('photoSession', JSON.stringify(sessionPhotos)); // Save actual photo data to localStorage with user consent this.savePersistentPhoto(photoData); } /** * Save photo data persistently with user consent */ savePersistentPhoto(photoData) { try { // Check if user has given consent for photo storage const photoStorageConsent = localStorage.getItem('photoStorageConsent'); if (photoStorageConsent === null) { // First time - ask for consent this.requestPhotoStorageConsent(photoData); return; } if (photoStorageConsent === 'true') { // User has consented - save the photo this.storePhotoInLocalStorage(photoData); } // If consent is 'false', don't save (but still allow session use) } catch (error) { console.warn('โš ๏ธ Failed to save photo persistently:', error); // Continue without persistent storage } } /** * Request user consent for photo storage */ requestPhotoStorageConsent(photoData) { const modal = document.createElement('div'); modal.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.7); display: flex; align-items: center; justify-content: center; z-index: 10000; `; modal.innerHTML = `

๐Ÿ“ธ 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'); if (overlay) { overlay.remove(); } // Stop camera stream this.stopCamera(); // Clear current session if (this.currentPhotoSession) { console.log(`๐Ÿ“Š Session ended: ${this.currentPhotoSession.photos.length} photos taken`); // Check if this was a scenario-based photo session const isScenarioSession = this.currentPhotoSession.taskData && this.currentPhotoSession.taskData.task && this.currentPhotoSession.taskData.task.scenarioState; this.currentPhotoSession = null; // Only call resumeFromCamera for non-scenario sessions // Scenario sessions are handled by handlePhotoSessionCompletion if (!isScenarioSession && this.game && this.game.interactiveTaskManager) { console.log('๐Ÿ“ฑ Non-scenario session - resuming to task interface'); this.game.interactiveTaskManager.resumeFromCamera(); } else if (isScenarioSession) { console.log('๐ŸŽญ Scenario session - completion handled by scenario system'); } } } /** * Start mirror mode - shows camera feed for self-viewing without taking photos */ async startMirrorMode(taskData) { console.log('๐Ÿชž Starting webcam mirror mode'); // Request camera if not already active if (!this.isActive) { const accessGranted = await this.requestCameraAccess(); if (!accessGranted) { console.warn('๐Ÿ“ท Camera access required for mirror mode'); return false; } } // Track webcam mirror start for XP (5 XP per minute) if (this.game && this.game.trackWebcamMirror) { this.game.trackWebcamMirror(true); } // Show mirror interface this.showMirrorInterface(taskData); return true; } /** * 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 specific buttons const completeBtn = overlay.querySelector('#mirror-complete-btn'); 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 - Attempting to close mirror'); if (this.preventClose) { // Show warning that camera cannot be closed during timer if (this.game && this.game.showNotification) { this.game.showNotification('Cannot close mirror during active session. Please wait for timer to complete.', 'warning'); } else { console.log('โš ๏ธ Cannot close mirror during active session'); } return; } // Show harsh confirmation dialog for abandonment console.log('๐Ÿ›‘ Showing abandonment confirmation dialog'); this.showAbandonmentConfirmation(); }); console.log('๐Ÿชž Mirror interface displayed'); } /** * 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'); // 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(); } /** * 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'; } /** * 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; }