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