feat: Add webcam recording task overlay system

- Implement canvas-based video compositing for task overlays
- Task information now burned directly into webcam recordings
- Show actual task content instead of generic descriptions
- Remove screen overlay in favor of webcam-embedded overlay
- Auto-enable task overlay for all webcam recordings
- Add real-time task text, type, and timer display on recordings
- Enhance training session documentation and review capabilities
This commit is contained in:
dilgenfritz 2025-11-17 10:07:19 -06:00
parent 676f06b2f1
commit bb27735b46
6 changed files with 2837 additions and 119 deletions

1513
hypno-gallery.html Normal file

File diff suppressed because it is too large Load Diff

View File

@ -187,6 +187,10 @@
<span class="feature-icon">🏆</span>
<span class="feature-text">Porn Cinema</span>
</button>
<button class="hero-feature btn-feature" id="hypno-gallery-btn">
<span class="feature-icon">🌀</span>
<span class="feature-text">Hypno Gallery</span>
</button>
<button class="hero-feature btn-feature" id="library-btn">
<span class="cassie-icon"></span>
<span class="feature-text">Library</span>
@ -316,6 +320,14 @@
</div>
</div>
<div class="guide-item">
<div class="guide-icon">🌀</div>
<div class="guide-info">
<h4>Hypno Gallery</h4>
<p>Immersive slideshow experience with configurable timing modes, visual effects, and transitions. View your image library in constant, random, or wave timing patterns with customizable durations and effects.</p>
</div>
</div>
<div class="guide-item">
<div class="guide-icon">📁</div>
<div class="guide-info">
@ -1641,12 +1653,22 @@
// Setup backup button handlers
setupBackupHandlers();
// Create initial backup
// Create initial backup with retry logic
try {
window.backupManager.createBackup(false);
console.log('🛡️ Initial backup created on startup');
} catch (error) {
console.warn('⚠️ Initial backup failed:', error);
console.warn('⚠️ Initial backup failed, attempting cleanup and retry:', error);
try {
// Perform emergency cleanup and try minimal backup
window.backupManager.performEmergencyCleanup();
const minimalBackup = window.backupManager.createMinimalBackup(false);
const backupKey = `${window.backupManager.backupPrefix}${Date.now()}`;
localStorage.setItem(backupKey, JSON.stringify(minimalBackup));
console.log('🛡️ Minimal backup created after cleanup');
} catch (retryError) {
console.error('❌ Even minimal backup failed:', retryError);
}
}
} else {
console.warn('⚠️ BackupManager not available');
@ -3595,7 +3617,15 @@
});
}
// Set up hypno gallery button (only once)
const hypnoGalleryBtn = document.getElementById('hypno-gallery-btn');
if (hypnoGalleryBtn && !hypnoGalleryBtn.hasAttribute('data-handler-attached')) {
hypnoGalleryBtn.setAttribute('data-handler-attached', 'true');
hypnoGalleryBtn.addEventListener('click', () => {
console.log('🌀 Opening Hypno Gallery...');
window.location.href = 'hypno-gallery.html';
});
}
// Set up user profile button (only once)
const userProfileBtn = document.getElementById('user-profile-btn');

View File

@ -27,6 +27,8 @@
</div>
</div>
<!-- Quick Play Header -->
<header class="quick-play-header">
<div class="quick-play-nav">
@ -52,6 +54,7 @@
</div>
</div>
<div class="nav-right quick-play-controls">
<button id="back-to-home" class="btn btn-secondary">🏠 Home</button>
<button id="quick-play-webcam-btn" class="btn btn-info" title="Take a photo with webcam">📸 Photo</button>
<button id="force-exit" class="btn btn-danger" title="Force close application">❌ Force Exit</button>
@ -1410,6 +1413,8 @@
loadingOverlay.style.display = 'none';
}
console.log('✅ Quick Play initialization complete');
}, 1000);
} catch (error) {
@ -2079,9 +2084,234 @@
console.log('📋 Video setup notification shown to user');
}
// Recording Overlay System - Always enabled for webcam recording
let recordingOverlayEnabled = true;
let recordingStartTime = Date.now();
let recordingTimerInterval = null;
function toggleRecordingOverlay() {
const webcamViewer = document.getElementById('webcam-viewer');
const button = document.getElementById('toggle-recording-overlay');
console.log('🎬 Toggle recording overlay called. Current state:', recordingOverlayEnabled);
console.log('🎬 Webcam viewer found:', !!webcamViewer);
console.log('🎬 Button element found:', !!button);
if (!recordingOverlayEnabled) {
// Enable webcam task overlay
recordingOverlayEnabled = true;
recordingStartTime = Date.now();
// Add recording class to webcam viewer to show task overlay
if (webcamViewer) {
webcamViewer.classList.add('recording');
console.log('🎬 Added recording class to webcam viewer');
}
button.textContent = '🔴 Task Overlay';
button.classList.remove('btn-info');
button.classList.add('btn-danger');
// Start timer
recordingTimerInterval = setInterval(updateRecordingTimer, 1000);
// Update with current task
updateRecordingOverlay();
console.log('🎬 Webcam task overlay enabled');
} else {
// Disable webcam task overlay
recordingOverlayEnabled = false;
recordingStartTime = null;
// Remove recording class from webcam viewer to hide task overlay
if (webcamViewer) {
webcamViewer.classList.remove('recording');
console.log('🎬 Removed recording class from webcam viewer');
}
button.textContent = '🎬 Task Overlay';
button.classList.remove('btn-danger');
button.classList.add('btn-info');
// Stop timer
if (recordingTimerInterval) {
clearInterval(recordingTimerInterval);
recordingTimerInterval = null;
}
console.log('🎬 Webcam task overlay disabled');
}
}
function updateRecordingTimer() {
if (!recordingStartTime) return;
const elapsed = Math.floor((Date.now() - recordingStartTime) / 1000);
const minutes = Math.floor(elapsed / 60);
const seconds = elapsed % 60;
const timeString = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
// Update both screen overlay and webcam overlay timestamps
const timestampElement = document.getElementById('recording-timestamp');
if (timestampElement) {
timestampElement.textContent = timeString;
}
// Update webcam overlay timer if no task timer is active
const webcamTimer = document.getElementById('webcam-task-timer');
const taskTimer = document.getElementById('task-timer');
if (webcamTimer && (!taskTimer || !taskTimer.textContent || taskTimer.textContent === '0:00')) {
webcamTimer.textContent = timeString;
}
}
function updateRecordingOverlay() {
if (!recordingOverlayEnabled) {
console.log('🎬 updateRecordingOverlay called but recording not enabled');
return;
}
console.log('🎬 Updating recording overlay content');
// Try multiple selectors to find task elements
let taskText = document.getElementById('task-text');
let taskTitle = document.getElementById('task-title');
let taskTimer = document.getElementById('task-timer');
// Fallback selectors if main ones not found
if (!taskText) taskText = document.querySelector('.task-text, [data-task-text]');
if (!taskTitle) taskTitle = document.querySelector('.task-title, [data-task-title], .current-task-title');
if (!taskTimer) taskTimer = document.querySelector('.task-timer, [data-task-timer], .timer-display');
console.log('🎬 Main task elements found:', {
taskText: !!taskText,
taskTitle: !!taskTitle,
taskTimer: !!taskTimer
});
const overlayTaskType = document.getElementById('webcam-task-type');
const overlayTaskText = document.getElementById('webcam-task-text');
const overlayTimer = document.getElementById('webcam-task-timer');
console.log('🎬 Webcam overlay elements found:', {
overlayTaskType: !!overlayTaskType,
overlayTaskText: !!overlayTaskText,
overlayTimer: !!overlayTimer
});
// Get task information from DOM or game state
let taskTitleText = '';
let taskTextContent = '';
let taskTimerContent = '';
if (taskTitle) {
taskTitleText = taskTitle.textContent || '';
} else if (window.gameInstance && window.gameInstance.gameState && window.gameInstance.gameState.currentTask) {
taskTitleText = window.gameInstance.gameState.currentTask.title || window.gameInstance.gameState.currentTask.text || '';
}
// Get actual task text - prefer title over description for actual task content
if (taskTitle) {
taskTextContent = taskTitle.textContent || '';
} else if (taskText) {
taskTextContent = taskText.textContent || '';
} else if (window.gameInstance && window.gameInstance.gameState && window.gameInstance.gameState.currentTask) {
taskTextContent = window.gameInstance.gameState.currentTask.title || window.gameInstance.gameState.currentTask.text || 'Training session in progress...';
}
if (taskTimer) {
taskTimerContent = taskTimer.textContent || '';
}
// Update webcam overlay with task information
if (overlayTaskType) {
console.log('🎬 Task title content:', taskTitleText);
if (taskTitleText.toLowerCase().includes('consequence')) {
overlayTaskType.textContent = 'CONSEQUENCE';
overlayTaskType.style.color = '#ff4757';
console.log('🎬 Set webcam overlay type to CONSEQUENCE');
} else if (taskTitleText.toLowerCase().includes('task')) {
overlayTaskType.textContent = 'TASK';
overlayTaskType.style.color = '#00d4ff';
console.log('🎬 Set webcam overlay type to TASK');
} else if (taskTitleText) {
overlayTaskType.textContent = 'SESSION';
overlayTaskType.style.color = '#ffd700';
console.log('🎬 Set webcam overlay type to SESSION');
} else {
overlayTaskType.textContent = 'TRAINING';
overlayTaskType.style.color = '#00ff00';
console.log('🎬 Set webcam overlay type to TRAINING (fallback)');
}
}
if (overlayTaskText) {
const text = taskTextContent || taskTitleText || 'Training session in progress...';
console.log('🎬 Updating webcam overlay text to:', text);
overlayTaskText.textContent = text;
}
if (overlayTimer) {
console.log('🎬 Task timer content:', taskTimerContent);
if (taskTimerContent && taskTimerContent !== '0:00' && taskTimerContent !== '') {
overlayTimer.textContent = taskTimerContent;
overlayTimer.style.display = 'inline';
console.log('🎬 Showing timer in webcam overlay:', taskTimerContent);
} else {
overlayTimer.textContent = '00:00';
overlayTimer.style.display = 'inline';
console.log('🎬 Set default timer in webcam overlay');
}
}
}
// Call this function whenever task content changes
function syncTaskWithOverlay() {
console.log('🎬 syncTaskWithOverlay called, recordingOverlayEnabled:', recordingOverlayEnabled);
if (recordingOverlayEnabled) {
updateRecordingOverlay();
// Also update webcam overlay elements directly for immediate display
updateWebcamOverlayElements();
}
}
function updateWebcamOverlayElements() {
const taskTitleElement = document.getElementById('task-title');
const webcamTaskText = document.getElementById('webcam-task-text');
const webcamTaskType = document.getElementById('webcam-task-type');
if (taskTitleElement && webcamTaskText && taskTitleElement.textContent.trim()) {
webcamTaskText.textContent = taskTitleElement.textContent.trim();
console.log('🎬 Updated webcam overlay with actual task:', taskTitleElement.textContent.trim());
}
if (webcamTaskType) {
const title = taskTitleElement?.textContent || '';
if (title.toLowerCase().includes('consequence')) {
webcamTaskType.textContent = 'CONSEQUENCE';
webcamTaskType.style.color = '#ff4757';
} else if (title.toLowerCase().includes('task')) {
webcamTaskType.textContent = 'TASK';
webcamTaskType.style.color = '#00d4ff';
} else if (title) {
webcamTaskType.textContent = 'SESSION';
webcamTaskType.style.color = '#ffd700';
} else {
webcamTaskType.textContent = 'TRAINING';
webcamTaskType.style.color = '#00ff00';
}
}
}
function setupEventListeners() {
console.log('Setting up event listeners...');
// Navigation buttons with error handling
const backToHomeBtn = document.getElementById('back-to-home');
if (backToHomeBtn) {
@ -3414,6 +3644,9 @@
taskText.style.display = 'none'; // Hide the description for consequence tasks
}
// Update recording overlay with new task
syncTaskWithOverlay();
// Hide skip button for consequence tasks
if (skipBtn) {
skipBtn.style.display = 'none';
@ -3551,6 +3784,9 @@
};
}
// Update recording overlay with new task
syncTaskWithOverlay();
// Set up button event handlers with proper error handling
if (completeBtn) {
completeBtn.style.display = 'inline-block';
@ -3725,6 +3961,9 @@
} else {
timerElement.style.color = '#00d4ff'; // Blue normally
}
// Update recording overlay when timer changes
syncTaskWithOverlay();
}
function stopTaskTimer() {
@ -4122,6 +4361,9 @@
taskText.textContent = 'Your task will appear here';
}
}
// Update recording overlay whenever task display changes
syncTaskWithOverlay();
}
function updateGameStatus(gameState) {
@ -4536,6 +4778,10 @@
let mediaRecorder = null;
let recordedChunks = [];
let webcamStream = null;
let recordingCanvas = null;
let recordingCanvasContext = null;
let compositeStream = null;
let animationFrameId = null;
async function initializeWebcamRecording() {
console.log('🎥 Initializing webcam recording for session...');
@ -4559,8 +4805,8 @@
webcamPreview.srcObject = webcamStream;
// Apply user settings
webcamViewer.className = `webcam-viewer active ${quickPlaySettings.webcamSize} ${quickPlaySettings.webcamPosition}`;
// Apply user settings and enable recording overlay by default
webcamViewer.className = `webcam-viewer active recording ${quickPlaySettings.webcamSize} ${quickPlaySettings.webcamPosition}`;
// Set up recording - Force MP4 format for Electron
recordedChunks = [];
@ -4575,18 +4821,41 @@
let mediaRecorderCreated = false;
// Set up canvas for compositing video with overlays
recordingCanvas = document.createElement('canvas');
recordingCanvas.width = 1920;
recordingCanvas.height = 1080;
recordingCanvasContext = recordingCanvas.getContext('2d');
// Create composite stream from canvas
compositeStream = recordingCanvas.captureStream(30); // 30 FPS
// Start compositing loop and recording timer
startVideoCompositing();
// Start recording timer for overlay
if (!recordingTimerInterval) {
recordingTimerInterval = setInterval(updateRecordingTimer, 1000);
}
// Initialize overlay with current task content
setTimeout(() => {
updateRecordingOverlay();
updateWebcamOverlayElements();
}, 1000);
// Try each MP4 codec until one works
for (const codec of mp4Codecs) {
if (MediaRecorder.isTypeSupported(codec)) {
try {
mediaRecorder = new MediaRecorder(webcamStream, {
mediaRecorder = new MediaRecorder(compositeStream, {
mimeType: codec,
videoBitsPerSecond: 4000000, // 4 Mbps for high quality 1080p
bitsPerSecond: 4000000 // Fallback for older browsers
});
quickPlaySettings.recordingMimeType = codec;
quickPlaySettings.recordingExtension = 'mp4';
console.log('🎥 Successfully using MP4 codec:', codec);
console.log('🎥 Successfully using MP4 codec with compositing:', codec);
mediaRecorderCreated = true;
break;
} catch (error) {
@ -4690,9 +4959,141 @@
});
}
function startVideoCompositing() {
console.log('🎨 Starting video compositing with task overlay...');
function drawFrame() {
if (!recordingCanvas || !recordingCanvasContext || !webcamStream) {
return;
}
const webcamPreview = document.getElementById('webcam-preview');
if (!webcamPreview || webcamPreview.videoWidth === 0) {
animationFrameId = requestAnimationFrame(drawFrame);
return;
}
// Clear canvas
recordingCanvasContext.clearRect(0, 0, recordingCanvas.width, recordingCanvas.height);
// Draw webcam video
recordingCanvasContext.drawImage(
webcamPreview,
0, 0,
recordingCanvas.width,
recordingCanvas.height
);
// Draw task overlay if enabled
if (recordingOverlayEnabled) {
drawTaskOverlayOnCanvas();
}
// Continue loop
animationFrameId = requestAnimationFrame(drawFrame);
}
drawFrame();
}
function drawTaskOverlayOnCanvas() {
const ctx = recordingCanvasContext;
if (!ctx) return;
// Get current task information - prioritize actual task title over generic description
const taskTitleElement = document.getElementById('task-title');
const taskTextElement = document.getElementById('task-text');
let actualTaskText = '';
if (taskTitleElement && taskTitleElement.textContent && taskTitleElement.textContent.trim()) {
actualTaskText = taskTitleElement.textContent.trim();
} else if (taskTextElement && taskTextElement.textContent && taskTextElement.textContent.trim()) {
actualTaskText = taskTextElement.textContent.trim();
} else {
actualTaskText = 'Training session in progress...';
}
const taskType = document.getElementById('webcam-task-type')?.textContent || 'SESSION';
const taskTimer = document.getElementById('webcam-task-timer')?.textContent || '00:00';
// Set up text styling
ctx.fillStyle = 'rgba(0, 0, 0, 0.8)';
ctx.strokeStyle = '#00ff00';
ctx.lineWidth = 2;
// Calculate overlay position (bottom area)
const overlayHeight = 120;
const overlayY = recordingCanvas.height - overlayHeight - 20;
const overlayX = 20;
const overlayWidth = recordingCanvas.width - 40;
// Draw overlay background
ctx.fillRect(overlayX, overlayY, overlayWidth, overlayHeight);
ctx.strokeRect(overlayX, overlayY, overlayWidth, overlayHeight);
// Draw task type and timer header
ctx.fillStyle = '#00ff00';
ctx.font = 'bold 28px Arial';
ctx.fillText(taskType, overlayX + 15, overlayY + 35);
ctx.fillStyle = '#ffa500';
ctx.font = 'bold 24px Arial';
const timerWidth = ctx.measureText(taskTimer).width;
ctx.fillText(taskTimer, overlayX + overlayWidth - timerWidth - 15, overlayY + 35);
// Draw task text (with word wrapping)
ctx.fillStyle = '#ffffff';
ctx.font = '22px Arial';
const maxWidth = overlayWidth - 30;
const lineHeight = 30;
const words = actualTaskText.split(' ');
let line = '';
let y = overlayY + 70;
for (let n = 0; n < words.length; n++) {
const testLine = line + words[n] + ' ';
const metrics = ctx.measureText(testLine);
const testWidth = metrics.width;
if (testWidth > maxWidth && n > 0) {
ctx.fillText(line, overlayX + 15, y);
line = words[n] + ' ';
y += lineHeight;
// Prevent overflow
if (y > overlayY + overlayHeight - 10) break;
} else {
line = testLine;
}
}
ctx.fillText(line, overlayX + 15, y);
}
function stopVideoCompositing() {
console.log('⏹️ Stopping video compositing...');
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
animationFrameId = null;
}
if (compositeStream) {
const tracks = compositeStream.getTracks();
tracks.forEach(track => track.stop());
compositeStream = null;
}
recordingCanvas = null;
recordingCanvasContext = null;
}
function stopWebcamRecording() {
console.log('⏹️ Stopping webcam recording...');
// Stop video compositing
stopVideoCompositing();
if (mediaRecorder && mediaRecorder.state === 'recording') {
mediaRecorder.stop();
}
@ -7086,10 +7487,49 @@
background: linear-gradient(135deg, #0f0f23 0%, #1a1a3a 50%, #0f0f23 100%);
min-height: 100vh;
font-family: 'Electrolize', sans-serif;
margin: 0;
padding: 0;
}
.recording-overlay-content {
display: flex;
flex-direction: column;
gap: 8px;
}
.recording-overlay-header {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
font-weight: bold;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.6; }
}
.recording-timestamp {
color: #ffd700;
font-family: 'Courier New', monospace;
}
.quick-play-header {
background: rgba(0, 0, 0, 0.9);
border-bottom: 1px solid #00d4ff;
@ -9737,12 +10177,78 @@
top: 20px;
left: 20px;
}
/* Webcam Task Overlay Styles */
.webcam-task-overlay {
position: absolute;
bottom: 8px;
left: 8px;
right: 8px;
background: rgba(0, 0, 0, 0.8);
border: 1px solid #00ff00;
border-radius: 6px;
padding: 8px 10px;
font-family: 'Arial', sans-serif;
font-size: 11px;
line-height: 1.3;
color: #ffffff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.6);
z-index: 10;
}
.webcam-task-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 4px;
font-size: 10px;
font-weight: bold;
}
.webcam-task-overlay #webcam-task-type {
color: #00ff00;
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.8);
}
.webcam-task-overlay #webcam-task-timer {
color: #ffa500;
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.8);
}
.webcam-task-overlay #webcam-task-text {
font-size: 12px;
font-weight: 500;
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.8);
word-wrap: break-word;
max-height: 3em;
overflow: hidden;
}
/* Hide webcam task overlay when not recording */
.webcam-viewer:not(.recording) .webcam-task-overlay {
display: none !important;
}
/* Show webcam task overlay when recording */
.webcam-viewer.recording .webcam-task-overlay {
display: block !important;
}
</style>
<!-- Floating Webcam Viewer -->
<div id="webcam-viewer" class="webcam-viewer">
<div class="recording-indicator">● REC</div>
<video id="webcam-preview" autoplay muted></video>
<!-- Task Overlay on Webcam Recording -->
<div id="webcam-task-overlay" class="webcam-task-overlay">
<div class="webcam-task-header">
<span id="webcam-task-type">SESSION</span>
<span id="webcam-task-timer">00:00</span>
</div>
<div id="webcam-task-text">Training session active...</div>
</div>
<div class="viewer-controls">
<button class="control-btn" id="toggle-webcam-viewer" title="Hide Webcam">👁️</button>
<button class="control-btn" id="stop-recording" title="Stop Recording">⏹️</button>

View File

@ -1147,12 +1147,197 @@ class WebcamManager {
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();
@ -1176,6 +1361,12 @@ class WebcamManager {
* 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';
@ -1188,8 +1379,14 @@ class WebcamManager {
<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">
${verificationData?.verificationInstructions || 'Assume required position'}
${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>
@ -1198,7 +1395,7 @@ class WebcamManager {
<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">Get in position...</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">
@ -1207,6 +1404,9 @@ class WebcamManager {
<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>
@ -1300,6 +1500,29 @@ class WebcamManager {
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');
}
@ -1330,32 +1553,62 @@ class WebcamManager {
// Preparation phase (10 seconds)
let prepTime = 10;
if (statusElement) statusElement.textContent = `Get in position now! ${prepTime}s`;
const positionName = this.currentPosition ? this.currentPosition.name : 'required position';
if (statusElement) statusElement.textContent = `Get into ${positionName}! ${prepTime}s`;
const prepTimer = setInterval(() => {
prepTime--;
if (statusElement) statusElement.textContent = `Get in position now! ${prepTime}s`;
if (prepTime <= 0) {
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
this.capturePositionPhoto('start', overlay, () => {
// Continue with verification after START photo
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);
});
}
}
/**
@ -1416,10 +1669,21 @@ class WebcamManager {
// Capture final position verification photo with degrading message
this.capturePositionPhoto('end', overlay, () => {
// Finalize verification after final photo
console.log('📸 Final photo captured, finalizing verification...');
// Reduced delay and added error handling
setTimeout(() => {
this.finalizeVerification(overlay);
}, 2000);
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);
});
}
@ -1444,51 +1708,91 @@ class WebcamManager {
console.log('📸 Video ready:', video && video.videoWidth > 0);
console.log('📸 Video ID:', video?.id);
// Array of degrading messages for different phases
// Array of brutally degrading messages for different phases
const degradingMessages = {
start: [
"📸 *SNAP* - Look at you, ready to humiliate yourself for training...",
"📸 *SNAP* - Another pathetic position documented. How degrading!",
"📸 *SNAP* - Smile for your shame! This is just the beginning...",
"📸 *SNAP* - Perfect. Now everyone can see how eager you are to submit.",
"📸 *SNAP* - Such a good little trainee, posing on command like that.",
"📸 *SNAP* - Look at yourself... so desperate for approval and training.",
"📸 *SNAP* - Starting position captured! Time to show your dedication...",
"📸 *SNAP* - What a good little gooner, already in position for training!",
"📸 *SNAP* - Beginning documentation of your descent into pleasure...",
"📸 *SNAP* - There we go! Ready to be molded into the perfect toy...",
"📸 *SNAP* - Such an eager student, posing so willingly for the camera!",
"📸 *SNAP* - Initial submission documented. You look so vulnerable...",
"📸 *SNAP* - Starting your journey to complete obedience. Smile!",
"📸 *SNAP* - Look how excited you are to begin your training session!",
"📸 *SNAP* - Perfect positioning! You're learning to follow orders well...",
"📸 *SNAP* - Pre-training photo captured. You look so innocent... for now.",
"📸 *SNAP* - Ready to begin your transformation? This photo says yes!",
"📸 *SNAP* - Starting pose documented. Let's see how far you'll go...",
"📸 *SNAP* - Initial compliance captured! Time to push your limits...",
"📸 *SNAP* - Beginning your session with such enthusiasm! How cute..."
"📸 *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 of your compliance. How obedient!",
"📸 *SNAP* - There's the proof of your dedication to degradation.",
"📸 *SNAP* - Perfect! Another successful training position documented.",
"📸 *SNAP* - Look how well-trained you've become. Such a good sub.",
"📸 *SNAP* - Evidence captured of your complete submission to training.",
"📸 *SNAP* - Beautiful! You held that humiliating pose perfectly.",
"📸 *SNAP* - Session complete! Look how thoroughly you've been trained...",
"📸 *SNAP* - Final proof of your transformation into a perfect gooner!",
"📸 *SNAP* - Post-training documentation shows your complete surrender...",
"📸 *SNAP* - There's the evidence of how far you've fallen! Magnificent!",
"📸 *SNAP* - Training complete! You've been so thoroughly conditioned...",
"📸 *SNAP* - Final verification: another successful session of submission!",
"📸 *SNAP* - Look at you now... completely molded to perfection!",
"📸 *SNAP* - Session concluded with total compliance documented!",
"📸 *SNAP* - Perfect! Your dedication to being trained is captured forever...",
"📸 *SNAP* - End result documented. You've exceeded all expectations!",
"📸 *SNAP* - Training session complete! Your obedience is beautiful...",
"📸 *SNAP* - Final photo proves your complete transformation! Well done...",
"📸 *SNAP* - Session finished! You've been such a good, compliant trainee...",
"📸 *SNAP* - Conclusion captured! Your journey to submission is documented..."
"📸 *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!"
]
};
@ -1525,19 +1829,41 @@ class WebcamManager {
message: randomMessage
};
// Save to localStorage (verification photos)
const existingPhotos = JSON.parse(localStorage.getItem('verificationPhotos') || '[]');
existingPhotos.push(verificationPhoto);
// Limit to last 20 verification photos to save space
if (existingPhotos.length > 20) {
existingPhotos.splice(0, existingPhotos.length - 20);
// 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);
}
}
localStorage.setItem('verificationPhotos', JSON.stringify(existingPhotos));
// ALSO store in main captured photos system for gallery integration
const mainCapturedPhotos = JSON.parse(localStorage.getItem('capturedPhotos') || '[]');
// Create photo data object outside try block to ensure scope
const mainPhotoData = {
timestamp: new Date().toISOString(),
data: photoData,
@ -1546,18 +1872,40 @@ class WebcamManager {
type: 'position_verification',
phase: phase,
message: randomMessage,
filename: `verification_${phase}_${Date.now()}.jpg`
filename: `verification_${phase}_${Date.now()}.jpg`,
position: this.currentPosition ? {
name: this.currentPosition.name,
instruction: this.currentPosition.instruction,
description: this.currentPosition.description
} : null
};
mainCapturedPhotos.push(mainPhotoData);
// Limit main photos to 50 total
if (mainCapturedPhotos.length > 50) {
mainCapturedPhotos.splice(0, mainCapturedPhotos.length - 50);
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);
}
}
localStorage.setItem('capturedPhotos', JSON.stringify(mainCapturedPhotos));
// Add to webcam manager's capturedPhotos array if available
if (this.capturedPhotos) {
this.capturedPhotos.push(mainPhotoData);
@ -1631,13 +1979,35 @@ class WebcamManager {
const verificationPhoto = {
timestamp: new Date().toISOString(),
data: photoData,
type: 'position_verification'
type: 'position_verification',
position: this.currentPosition ? {
name: this.currentPosition.name,
instruction: this.currentPosition.instruction,
description: this.currentPosition.description
} : null
};
// Save to localStorage
const existingPhotos = JSON.parse(localStorage.getItem('verificationPhotos') || '[]');
existingPhotos.push(verificationPhoto);
localStorage.setItem('verificationPhotos', JSON.stringify(existingPhotos));
// 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');
@ -1646,10 +2016,22 @@ class WebcamManager {
statusElement.style.color = '#27ae60';
}
// Complete verification after 2 seconds
// Complete verification after 1 second (reduced delay)
console.log('⏰ Starting verification completion timer...');
setTimeout(() => {
this.finalizeVerification(overlay);
}, 2000);
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) {
@ -1664,14 +2046,36 @@ class WebcamManager {
*/
finalizeVerification(overlay) {
console.log('✅ Position verification completed successfully');
console.log('🔄 Finalizing verification process...');
this.closeVerificationMode();
// Notify completion
const event = new CustomEvent('verificationComplete', {
detail: { success: true }
});
document.dispatchEvent(event);
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);
}
}
/**
@ -1691,14 +2095,33 @@ class WebcamManager {
this.game.trackWebcamVerification(false);
}
// Remove verification overlay
// 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');
}
/**
@ -2784,7 +3207,7 @@ class WebcamManager {
}
// Add phase indicator at top
const phaseText = phase === 'start' ? '🟢 START POSITION' : '🔴 END POSITION';
const phaseText = phase === 'start' ? '🔥 SLUT TRAINING BEGINS' : '💀 MIND-FUCKED & BROKEN';
const phaseY = 40;
// Draw phase indicator with outline

View File

@ -24,10 +24,22 @@ class BackupManager {
*/
createBackup(isAutomatic = false) {
try {
// Check storage usage before backup
const currentUsage = this.calculateStorageUsage();
const maxStorage = 5 * 1024 * 1024; // 5MB typical localStorage limit
console.log(`📊 Current storage usage: ${(currentUsage / 1024 / 1024).toFixed(2)}MB`);
if (currentUsage > maxStorage * 0.7) { // If over 70% usage
console.warn('⚠️ Storage usage high, performing preemptive cleanup...');
this.performEmergencyCleanup();
}
const timestamp = new Date().toISOString();
const backupData = this.gatherAllUserData();
const backup = {
// Check backup size and clean if too large
const testBackup = {
timestamp,
version: '1.0',
isAutomatic,
@ -38,12 +50,47 @@ class BackupManager {
appVersion: this.getAppVersion()
}
};
const backupJson = JSON.stringify(testBackup);
const backupSize = new Blob([backupJson]).size;
const maxSize = 1 * 1024 * 1024; // Reduced to 1MB limit for safety
console.log(`📊 Backup size: ${(backupSize / 1024 / 1024).toFixed(2)}MB`);
// Always clean photo data from backups to prevent quota issues
console.log('🧹 Proactively cleaning photo data from backup...');
backupData.capturedPhotos = this.cleanLargePhotoData(backupData.capturedPhotos);
backupData.photoGallery = this.cleanLargePhotoData(backupData.photoGallery);
backupData.verificationPhotos = this.cleanLargePhotoData(backupData.verificationPhotos);
// Update backup after cleaning
testBackup.data = backupData;
testBackup.metadata.totalItems = this.countDataItems(backupData);
testBackup.metadata.photosReduced = true;
// Recalculate size after cleaning
const cleanedSize = new Blob([JSON.stringify(testBackup)]).size;
console.log(`📊 Cleaned backup size: ${(cleanedSize / 1024 / 1024).toFixed(2)}MB`);
const backupKey = `${this.backupPrefix}${Date.now()}`;
localStorage.setItem(backupKey, JSON.stringify(backup));
try {
localStorage.setItem(backupKey, JSON.stringify(testBackup));
} catch (storageError) {
if (storageError.name === 'QuotaExceededError') {
console.warn('⚠️ Storage quota exceeded, performing emergency cleanup...');
this.performEmergencyCleanup();
// Try again with minimal backup
const minimalBackup = this.createMinimalBackup(isAutomatic);
localStorage.setItem(backupKey, JSON.stringify(minimalBackup));
} else {
throw storageError;
}
}
console.log(`🛡️ ${isAutomatic ? 'Auto' : 'Manual'} backup created: ${backupKey}`);
console.log(`📊 Backup contains ${backup.metadata.totalItems} data items`);
console.log(`📊 Backup contains ${testBackup.metadata.totalItems} data items`);
return backupKey;
} catch (error) {
@ -51,6 +98,135 @@ class BackupManager {
throw error;
}
}
/**
* Clean large photo data to reduce backup size
*/
cleanLargePhotoData(photoDataString) {
if (!photoDataString) return photoDataString;
try {
const photoData = JSON.parse(photoDataString);
if (Array.isArray(photoData)) {
// Keep only metadata, remove actual image data
const cleanedData = photoData.map(photo => ({
...photo,
data: photo.data ? '[IMAGE_DATA_REMOVED_FOR_BACKUP]' : photo.data,
dataUrl: photo.dataUrl ? '[IMAGE_DATA_REMOVED_FOR_BACKUP]' : photo.dataUrl
}));
return JSON.stringify(cleanedData);
}
} catch (error) {
console.warn('⚠️ Error cleaning photo data:', error);
}
return photoDataString;
}
/**
* Create minimal backup with only essential data
*/
createMinimalBackup(isAutomatic = false) {
const essentialData = {};
// Only include essential game data, no photos
const webGameData = localStorage.getItem('webGame-data');
if (webGameData) {
essentialData.webGameData = JSON.parse(webGameData);
}
essentialData.playerStats = localStorage.getItem('playerStats');
essentialData.achievements = localStorage.getItem('achievements');
essentialData.selectedTheme = localStorage.getItem('selectedTheme');
essentialData.userPreferences = localStorage.getItem('userPreferences');
essentialData.disabledTasks = localStorage.getItem('disabledTasks');
return {
timestamp: new Date().toISOString(),
version: '1.0',
isAutomatic,
data: essentialData,
metadata: {
totalItems: this.countDataItems(essentialData),
userAgent: navigator.userAgent,
appVersion: this.getAppVersion(),
isMinimal: true
}
};
}
/**
* Emergency cleanup when storage quota is exceeded
*/
performEmergencyCleanup() {
try {
console.log('🚨 Starting emergency storage cleanup...');
// Clean up old backups first - keep only 1
this.cleanupOldBackups(1);
// Clean up verification photos aggressively
const verificationPhotos = JSON.parse(localStorage.getItem('verificationPhotos') || '[]');
if (verificationPhotos.length > 2) {
const recent = verificationPhotos.slice(-2);
localStorage.setItem('verificationPhotos', JSON.stringify(recent));
console.log(`🧹 Emergency: Reduced verification photos to ${recent.length}`);
}
// Clean up captured photos aggressively
const capturedPhotos = JSON.parse(localStorage.getItem('capturedPhotos') || '[]');
if (capturedPhotos.length > 3) {
const recent = capturedPhotos.slice(-3);
localStorage.setItem('capturedPhotos', JSON.stringify(recent));
console.log(`🧹 Emergency: Reduced captured photos to ${recent.length}`);
}
// Clean up any old individual photo keys
const keys = Object.keys(localStorage);
const photoKeys = keys.filter(key =>
key.startsWith('verificationPhoto_') ||
key.startsWith('capturedPhoto_') ||
key.startsWith('photo_')
);
if (photoKeys.length > 5) {
const toRemove = photoKeys.slice(0, -5); // Keep only 5 most recent
toRemove.forEach(key => {
const value = localStorage.getItem(key);
if (value && value.startsWith('blob:')) {
URL.revokeObjectURL(value);
}
localStorage.removeItem(key);
});
console.log(`🧹 Emergency: Removed ${toRemove.length} old photo keys`);
}
// Calculate storage usage after cleanup
const usage = this.calculateStorageUsage();
console.log(`🧹 Emergency cleanup complete. Storage usage: ${(usage / 1024 / 1024).toFixed(2)}MB`);
} catch (error) {
console.error('❌ Emergency cleanup failed:', error);
}
}
/**
* Calculate approximate localStorage usage
*/
calculateStorageUsage() {
let totalSize = 0;
try {
for (let key in localStorage) {
if (localStorage.hasOwnProperty(key)) {
const value = localStorage.getItem(key);
totalSize += new Blob([key + value]).size;
}
}
} catch (error) {
console.warn('⚠️ Could not calculate storage usage:', error);
}
return totalSize;
}
/**
* Gather all user data from various storage systems
@ -81,9 +257,10 @@ class BackupManager {
data.achievements = localStorage.getItem('achievements');
data.levelProgress = localStorage.getItem('levelProgress');
// Photo gallery
// Photo gallery (metadata only, no image data)
data.photoGallery = localStorage.getItem('photoGallery');
data.capturedPhotos = localStorage.getItem('capturedPhotos');
data.verificationPhotos = localStorage.getItem('verificationPhotos');
// Custom content
data.customMainTasks = localStorage.getItem('customMainTasks');
@ -335,16 +512,17 @@ class BackupManager {
/**
* Clean up old automatic backups
*/
cleanupOldBackups() {
cleanupOldBackups(maxToKeep = null) {
const backups = this.listBackups();
const autoBackups = backups.filter(b => b.isAutomatic);
const maxBackups = maxToKeep || this.maxAutoBackups;
if (autoBackups.length > this.maxAutoBackups) {
const toDelete = autoBackups.slice(this.maxAutoBackups);
if (autoBackups.length > maxBackups) {
const toDelete = autoBackups.slice(maxBackups);
toDelete.forEach(backup => {
this.deleteBackup(backup.key);
});
console.log(`🧹 Cleaned up ${toDelete.length} old auto-backups`);
console.log(`🧹 Cleaned up ${toDelete.length} old auto-backups (keeping ${maxBackups})`);
}
}

View File

@ -3087,16 +3087,31 @@
// Store next step for completion callback
window.trainingAcademyNextStep = nextStep;
// Emergency timeout: force close if verification takes too long (5 minutes)
const emergencyTimeout = setTimeout(() => {
console.warn('🚨 Emergency timeout: Force closing stuck verification');
const overlay = document.getElementById('verification-overlay');
if (overlay) {
overlay.remove();
}
// Clear any verification timers
if (window.game?.webcamManager) {
window.game.webcamManager.closeVerificationMode();
}
alert('Verification timed out. Skipping to next step.');
proceedToNextStep(nextStep);
}, 5 * 60 * 1000); // 5 minutes
// Store timeout reference for cleanup
window.verificationEmergencyTimeout = emergencyTimeout;
// Create verification data for the webcam system
const verificationData = {
instructions: "Position verification required for training compliance",
verificationInstructions: instructions,
verificationText: verificationText,
verificationDuration: duration,
onComplete: function() {
console.log('🔍 Position verification completed, proceeding to:', window.trainingAcademyNextStep);
proceedToNextStep(window.trainingAcademyNextStep);
}
verificationDuration: duration
// Note: Removed onComplete callback to avoid conflicts with event system
};
// Use the webcam manager to start verification mode
@ -3108,9 +3123,20 @@
// Listen for verification completion
document.addEventListener('verificationComplete', function(event) {
console.log('📨 Verification complete event received:', event.detail);
// Clear emergency timeout
if (window.verificationEmergencyTimeout) {
clearTimeout(window.verificationEmergencyTimeout);
window.verificationEmergencyTimeout = null;
console.log('⏰ Cleared emergency timeout');
}
if (event.detail.success) {
console.log('✅ Verification completed successfully');
console.log('✅ Verification completed successfully, proceeding to:', nextStep);
proceedToNextStep(nextStep);
} else {
console.warn('⚠️ Verification failed or was abandoned');
}
}, { once: true });
@ -3920,16 +3946,53 @@
// Create a simple test overlay
const testOverlay = document.createElement('div');
testOverlay.id = 'verification-test-overlay';
testOverlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.9);
display: flex;
align-items: center;
justify-content: center;
z-index: 99999;
`;
testOverlay.innerHTML = `
<div style="position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(0,0,0,0.9); padding: 20px; border-radius: 10px; z-index: 99999;">
<h3 style="color: white; margin-bottom: 15px;">🧪 Testing Photo Capture</h3>
<div style="background: rgba(0,0,0,0.9); padding: 20px; border-radius: 10px; border: 2px solid #e74c3c;">
<h3 style="color: white; margin-bottom: 15px; text-align: center;">🧪 Testing Photo Capture</h3>
<video id="verification-video" autoplay muted playsinline style="width: 300px; height: 200px; border: 2px solid #e74c3c; border-radius: 8px; transform: scaleX(-1);"></video>
<div id="verification-status" style="color: white; margin-top: 10px; text-align: center;">Preparing test...</div>
<button onclick="this.parentElement.parentElement.remove()" style="margin-top: 15px; padding: 8px 16px; background: #f44336; color: white; border: none; border-radius: 5px; cursor: pointer;">Close Test</button>
<div style="text-align: center; margin-top: 15px;">
<button id="close-verification-test" style="padding: 8px 16px; background: #f44336; color: white; border: none; border-radius: 5px; cursor: pointer;">Close Test</button>
</div>
</div>
`;
document.body.appendChild(testOverlay);
// Add proper close button handler
const closeButton = testOverlay.querySelector('#close-verification-test');
closeButton.addEventListener('click', () => {
console.log('🧪 Closing verification test overlay...');
if (testOverlay.parentNode) {
testOverlay.parentNode.removeChild(testOverlay);
}
document.removeEventListener('keydown', escapeHandler);
});
// Add escape key handler for emergency close
const escapeHandler = (event) => {
if (event.key === 'Escape') {
console.log('🧪 Emergency closing verification test with Escape key...');
if (testOverlay && testOverlay.parentNode) {
testOverlay.parentNode.removeChild(testOverlay);
}
document.removeEventListener('keydown', escapeHandler);
}
};
document.addEventListener('keydown', escapeHandler);
const testVideo = testOverlay.querySelector('#verification-video');
if (window.game.webcamManager.stream) {
testVideo.srcObject = window.game.webcamManager.stream;
@ -3942,15 +4005,20 @@
console.log('🧪 Test photo captured successfully!');
const statusElement = testOverlay.querySelector('#verification-status');
if (statusElement) {
statusElement.innerHTML = '✅ Test photo captured! Check the gallery.';
statusElement.innerHTML = '✅ Test photo captured! Check the gallery.<br><small>This overlay will auto-close in 5 seconds...</small>';
statusElement.style.color = '#27ae60';
}
// Refresh gallery button after a delay
// Auto-close after showing success message
setTimeout(() => {
// Refresh the photo library to show new photos
console.log('🧪 Auto-closing test overlay...');
if (testOverlay && testOverlay.parentNode) {
testOverlay.parentNode.removeChild(testOverlay);
}
// Refresh gallery button after overlay is closed
initializePhotoLibrary();
}, 1000);
}, 5000);
});
}, 2000);
};