Implement complete webcam photo gallery system

- Add WebcamManager for camera access and photo capture
- Replace timer steps with camera buttons for photography tasks
- Add photo count validation and progress tracking
- Remove quit options to ensure photo requirements are met
- Implement game-end photo gallery with full-size viewer
- Add Photography Studio game mode with dedicated scenarios
- Include responsive design with professional styling
- Maintain local photo storage for privacy

Features:
- Step-based photo requirements with visual progress
- Interactive photo gallery with navigation
- Comprehensive session metadata tracking
- Privacy-focused local-only photo storage
This commit is contained in:
dilgenfritz 2025-10-27 12:27:04 -05:00
parent 395c79363c
commit 6752097979
5 changed files with 1973 additions and 17 deletions

397
game.js
View File

@ -7,6 +7,9 @@ class TaskChallengeGame {
// Initialize interactive task manager
this.interactiveTaskManager = new InteractiveTaskManager(this);
// Initialize webcam manager for photography tasks
this.webcamManager = new WebcamManager(this);
// Initialize desktop features early
this.initDesktopFeatures();
@ -47,6 +50,7 @@ class TaskChallengeGame {
// Initialize Popup Image System (Punishment for skips)
this.popupImageManager = new PopupImageManager(this.dataManager);
window.popupImageManager = this.popupImageManager; // Make available globally for HTML onclick handlers
// Initialize AI Task Generation System
this.aiTaskManager = new AITaskManager(this.dataManager);
@ -58,6 +62,9 @@ class TaskChallengeGame {
this.gameModeManager = new GameModeManager();
window.gameModeManager = this.gameModeManager;
// Initialize webcam for photography tasks
this.initializeWebcam();
this.initializeEventListeners();
this.setupKeyboardShortcuts();
this.setupWindowResizeHandling();
@ -1646,6 +1653,65 @@ class TaskChallengeGame {
// Load saved theme
this.loadSavedTheme();
}
/**
* Initialize webcam system for photography tasks
*/
async initializeWebcam() {
console.log('🎥 Initializing webcam system...');
try {
const initialized = await this.webcamManager.init();
if (initialized) {
console.log('✅ Webcam system ready for photography tasks');
// Listen for photo task events
document.addEventListener('photoTaken', (event) => {
this.handlePhotoTaken(event.detail);
});
// Listen for photo session completion
document.addEventListener('photoSessionComplete', (event) => {
this.handlePhotoSessionComplete(event.detail);
});
} else {
console.log('📷 Webcam not available - photography tasks will use standard mode');
}
} catch (error) {
console.error('❌ Webcam initialization failed:', error);
}
}
/**
* Handle photo taken event from webcam manager
*/
handlePhotoTaken(detail) {
console.log('📸 Photo taken for task:', detail.sessionType);
// Show confirmation message
this.showNotification('Photo captured successfully! 📸', 'success', 3000);
// Progress the interactive task if applicable
if (this.interactiveTaskManager && this.interactiveTaskManager.currentTask) {
this.interactiveTaskManager.handlePhotoCompletion(detail);
}
}
/**
* Handle photo session completion
*/
handlePhotoSessionComplete(detail) {
console.log('🎉 Photo session completed:', detail.sessionType, `(${detail.photos.length} photos)`);
// Show completion message
this.showNotification(`Photography session completed! ${detail.photos.length} photos taken 📸`, 'success', 4000);
// Progress the scenario task
if (this.interactiveTaskManager && this.interactiveTaskManager.currentInteractiveTask) {
this.interactiveTaskManager.handlePhotoSessionCompletion(detail);
}
}
initializeAudioControls() {
// Volume sliders
@ -4615,8 +4681,335 @@ ${usagePercent > 85 ? '⚠️ Storage getting full - consider deleting some imag
console.log('Game ended - stopping all audio');
this.audioManager.stopAllImmediate();
this.showFinalStats(reason);
this.showScreen('game-over-screen');
// Show photo gallery if any photos were taken during the game
this.showGamePhotoGallery(() => {
this.showFinalStats(reason);
this.showScreen('game-over-screen');
});
}
/**
* Show photo gallery with all photos taken during the game session
*/
showGamePhotoGallery(onComplete) {
// Get all photos from webcam manager
const allPhotos = this.webcamManager.capturedPhotos || [];
if (allPhotos.length === 0) {
// No photos taken, proceed directly to final stats
onComplete();
return;
}
console.log(`📸 Showing game photo gallery with ${allPhotos.length} photos`);
const gallery = document.createElement('div');
gallery.id = 'game-photo-gallery';
gallery.innerHTML = `
<div class="game-gallery-overlay">
<div class="game-gallery-container">
<div class="game-gallery-header">
<h3>🎉 Game Complete - Your Photo Session</h3>
<p>You captured ${allPhotos.length} photos during this game!</p>
</div>
<div class="game-gallery-grid">
${allPhotos.map((photo, index) => `
<div class="game-gallery-item" onclick="window.showGamePhoto(${index})">
<img src="${photo.dataURL}" alt="Game Photo ${index + 1}">
<div class="game-photo-number">${index + 1}</div>
<div class="game-photo-session">${photo.sessionType || 'Photo'}</div>
</div>
`).join('')}
</div>
<div class="game-gallery-controls">
<button id="continue-to-stats" class="game-gallery-continue">📊 View Final Stats</button>
</div>
<div class="game-gallery-note">
<p>🔒 All photos are stored locally and never uploaded</p>
<p>💾 Photos will be cleared when you start a new game</p>
</div>
</div>
</div>
`;
document.body.appendChild(gallery);
this.addGameGalleryStyles();
// Bind continue button
document.getElementById('continue-to-stats').addEventListener('click', () => {
gallery.remove();
onComplete();
});
// Make photo viewer available globally
window.showGamePhoto = (index) => {
this.showGameFullPhoto(index, allPhotos);
};
}
/**
* Show full-size photo viewer for game photos
*/
showGameFullPhoto(index, photos) {
if (!photos || !photos[index]) return;
const photo = photos[index];
const viewer = document.createElement('div');
viewer.id = 'game-photo-viewer';
viewer.innerHTML = `
<div class="game-photo-viewer-overlay" onclick="this.parentElement.remove()">
<div class="game-photo-viewer-container">
<div class="game-photo-viewer-header">
<h4>Photo ${index + 1} of ${photos.length}</h4>
<div class="photo-session-info">${photo.sessionType || 'Photography Session'}</div>
<button class="game-photo-viewer-close" onclick="this.closest('#game-photo-viewer').remove()">×</button>
</div>
<div class="game-photo-viewer-content">
<img src="${photo.dataURL}" alt="Game Photo ${index + 1}">
</div>
<div class="game-photo-viewer-nav">
${index > 0 ? `<button onclick="window.showGamePhoto(${index - 1}); this.closest('#game-photo-viewer').remove();">← Previous</button>` : '<div></div>'}
${index < photos.length - 1 ? `<button onclick="window.showGamePhoto(${index + 1}); this.closest('#game-photo-viewer').remove();">Next →</button>` : '<div></div>'}
</div>
</div>
</div>
`;
document.body.appendChild(viewer);
}
/**
* Add styles for game photo gallery
*/
addGameGalleryStyles() {
if (document.getElementById('game-gallery-styles')) return;
const styles = document.createElement('style');
styles.id = 'game-gallery-styles';
styles.textContent = `
#game-photo-gallery .game-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: 11000;
}
#game-photo-gallery .game-gallery-container {
background: linear-gradient(135deg, #2a2a2a, #3a3a3a);
border-radius: 15px;
padding: 40px;
max-width: 95vw;
max-height: 95vh;
overflow-y: auto;
color: white;
box-shadow: 0 10px 30px rgba(0,0,0,0.5);
}
#game-photo-gallery .game-gallery-header {
text-align: center;
margin-bottom: 30px;
}
#game-photo-gallery .game-gallery-header h3 {
color: #ff6b6b;
font-size: 28px;
margin-bottom: 10px;
}
#game-photo-gallery .game-gallery-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 40px;
}
#game-photo-gallery .game-gallery-item {
position: relative;
cursor: pointer;
border-radius: 12px;
overflow: hidden;
transition: all 0.3s ease;
border: 3px solid #444;
background: #333;
}
#game-photo-gallery .game-gallery-item:hover {
transform: scale(1.08);
box-shadow: 0 8px 25px rgba(255, 107, 107, 0.4);
border-color: #ff6b6b;
}
#game-photo-gallery .game-gallery-item img {
width: 100%;
height: 180px;
object-fit: cover;
}
#game-photo-gallery .game-photo-number {
position: absolute;
top: 8px;
right: 8px;
background: linear-gradient(45deg, #ff6b6b, #ff8e53);
color: white;
border-radius: 50%;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: bold;
box-shadow: 0 2px 5px rgba(0,0,0,0.3);
}
#game-photo-gallery .game-photo-session {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: linear-gradient(transparent, rgba(0,0,0,0.8));
color: white;
padding: 8px;
font-size: 12px;
text-align: center;
}
#game-photo-gallery .game-gallery-controls {
text-align: center;
margin: 30px 0;
}
#game-photo-gallery .game-gallery-continue {
background: linear-gradient(45deg, #2ed573, #17a2b8);
color: white;
border: none;
padding: 18px 36px;
border-radius: 8px;
font-size: 18px;
font-weight: bold;
cursor: pointer;
transition: all 0.3s;
box-shadow: 0 4px 15px rgba(46, 213, 115, 0.3);
}
#game-photo-gallery .game-gallery-continue:hover {
transform: translateY(-3px);
box-shadow: 0 6px 20px rgba(46, 213, 115, 0.4);
}
#game-photo-gallery .game-gallery-note {
text-align: center;
color: #aaa;
font-size: 14px;
border-top: 1px solid #444;
padding-top: 20px;
}
/* Game Photo Viewer Styles */
#game-photo-viewer .game-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: 11001;
}
#game-photo-viewer .game-photo-viewer-container {
background: linear-gradient(135deg, #2a2a2a, #3a3a3a);
border-radius: 15px;
max-width: 90vw;
max-height: 90vh;
color: white;
overflow: hidden;
box-shadow: 0 10px 30px rgba(0,0,0,0.5);
}
#game-photo-viewer .game-photo-viewer-header {
padding: 20px 25px;
background: linear-gradient(45deg, #ff6b6b, #ff8e53);
display: flex;
justify-content: space-between;
align-items: center;
}
#game-photo-viewer .photo-session-info {
font-size: 14px;
opacity: 0.9;
}
#game-photo-viewer .game-photo-viewer-close {
background: none;
border: none;
color: white;
font-size: 28px;
cursor: pointer;
padding: 0;
width: 35px;
height: 35px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: background 0.3s;
}
#game-photo-viewer .game-photo-viewer-close:hover {
background: rgba(255,255,255,0.2);
}
#game-photo-viewer .game-photo-viewer-content {
padding: 30px;
text-align: center;
background: #2a2a2a;
}
#game-photo-viewer .game-photo-viewer-content img {
max-width: 100%;
max-height: 70vh;
border-radius: 8px;
box-shadow: 0 4px 15px rgba(0,0,0,0.3);
}
#game-photo-viewer .game-photo-viewer-nav {
padding: 20px 25px;
background: #3a3a3a;
display: flex;
justify-content: space-between;
align-items: center;
}
#game-photo-viewer .game-photo-viewer-nav button {
background: linear-gradient(45deg, #ff6b6b, #ff8e53);
color: white;
border: none;
padding: 12px 24px;
border-radius: 6px;
cursor: pointer;
transition: all 0.3s;
font-weight: bold;
}
#game-photo-viewer .game-photo-viewer-nav button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(255, 107, 107, 0.4);
}
`;
document.head.appendChild(styles);
}
resetGame() {

View File

@ -22,6 +22,11 @@ class GameModeManager {
description: 'Reach a target score by completing tasks based on difficulty',
icon: '🎯'
},
'photography-studio': {
name: 'Photography Studio',
description: 'Dedicated webcam photography and dressing sessions',
icon: '📸'
},
'scenario-adventures': {
name: 'Scenario Adventures',
description: 'Interactive Choose Your Own Adventure scenarios',
@ -46,13 +51,15 @@ class GameModeManager {
// Scenario collections for different modes - EXPANDED CONTENT
this.scenarioCollections = {
'photography-studio': [
'scenario-dress-up-photo'
],
'scenario-adventures': [
'scenario-training-regimen',
'scenario-punishment-session',
'scenario-humiliation-challenge',
'scenario-edging-marathon',
'scenario-obedience-training',
'scenario-dress-up-photo',
'scenario-creative-tasks'
],
'training-academy': [
@ -62,8 +69,7 @@ class GameModeManager {
],
'punishment-gauntlet': [
'scenario-punishment-session',
'scenario-humiliation-challenge',
'scenario-dress-up-photo'
'scenario-humiliation-challenge'
],
'endurance-trials': [
'scenario-edging-marathon',
@ -2236,6 +2242,117 @@ class GameModeManager {
duration: 140,
effects: { arousal: 30, control: -18 },
nextStep: "visual_completion"
},
expression_photography: {
type: 'action',
mood: 'photographer_demanding',
story: "Your instructor focuses on capturing your expressions. 'Perfect. Now show me humiliation in your face. Blush, look ashamed, show me desperation. Each expression will be documented.'",
actionText: "Pose for degrading expression photography (3 photos required)",
photoCount: 3,
effects: { arousal: 25, control: -15 },
nextStep: "expression_photo_review"
},
action_photography: {
type: 'action',
mood: 'photographer_creative',
story: "Your instructor directs action shots. 'Now we capture you in motion during your tasks. Edge while I photograph you in action. Show submission through movement.'",
actionText: "Perform tasks while being photographed (4 photos required)",
photoCount: 4,
effects: { arousal: 30, control: -20 },
nextStep: "action_photo_completion"
},
expression_photo_review: {
type: 'choice',
mood: 'reviewing_humiliation',
story: "Your instructor reviews the expression photos. 'Excellent capture of your shame and desperation. Now choose how to conclude this emotional documentation.'",
choices: [
{
text: "Take more extreme expressions",
preview: "Push emotional boundaries",
effects: { arousal: 20, control: -15 },
nextStep: "extreme_expression_session"
},
{
text: "Complete the expression session",
preview: "Finish emotional photography",
effects: { control: -5 },
nextStep: "visual_completion"
}
]
},
action_photo_completion: {
type: 'choice',
mood: 'action_documented',
story: "Your instructor reviews the action shots. 'Perfect documentation of submission in motion. Your degradation has been captured dynamically.'",
choices: [
{
text: "Continue with more action photos",
preview: "Extended action documentation",
effects: { arousal: 25, control: -12 },
nextStep: "extended_action_photos"
},
{
text: "Complete the action session",
preview: "Finish action photography",
effects: { control: -5 },
nextStep: "visual_completion"
}
]
},
extreme_expression_session: {
type: 'action',
mood: 'emotionally_intense',
story: "Your instructor pushes for more extreme expressions. 'Show me deeper shame, more intense desperation. Let every emotion show for the camera.'",
actionText: "Display extreme emotional expressions for photography (2 photos required)",
photoCount: 2,
effects: { arousal: 35, control: -25 },
nextStep: "visual_completion"
},
extended_action_photos: {
type: 'action',
mood: 'action_intensive',
story: "Your instructor extends the action photography. 'More dynamic shots. Show submission through every movement and gesture.'",
actionText: "Extended action photography session (3 photos required)",
photoCount: 3,
effects: { arousal: 30, control: -18 },
nextStep: "visual_completion"
},
progressive_photo_sequence: {
type: 'action',
mood: 'progressively_degrading',
story: "Your instructor sets up a progressive sequence. 'We'll document your increasing degradation step by step. Each photo will show you becoming more humiliated than the last.'",
actionText: "Progressive degradation photo sequence (5 photos required)",
photoCount: 5,
effects: { arousal: 40, control: -30 },
nextStep: "progressive_review"
},
progressive_review: {
type: 'choice',
mood: 'reviewing_progression',
story: "Your instructor reviews the progressive sequence. 'Perfect documentation of your degradation journey. Each photo shows deeper submission than the last.'",
choices: [
{
text: "Review the progression photos",
preview: "See your degradation documented",
effects: { arousal: 25, control: -15 },
nextStep: "progression_viewing"
},
{
text: "Complete the session",
preview: "Finish progressive documentation",
effects: { control: -5 },
nextStep: "visual_completion"
}
]
},
progression_viewing: {
type: 'action',
mood: 'self_reviewing',
story: "Your instructor shows you the progressive photos. 'Look at your journey from modest to completely degraded. Edge while viewing your documented transformation.'",
actionText: "Edge while viewing your progressive degradation photos",
duration: 120,
effects: { arousal: 35, control: -20 },
nextStep: "visual_completion"
}
}
}

View File

@ -1212,6 +1212,7 @@
<script src="image-discovery-fix.js"></script>
<script src="gameModeManager.js"></script>
<script src="interactiveTaskManager.js"></script>
<script src="webcamManager.js"></script>
<script src="desktop-file-manager.js"></script>
<script src="game.js"></script>
<script>

View File

@ -261,7 +261,20 @@ class InteractiveTaskManager {
feedback.className = 'interactive-feedback';
const container = document.getElementById('interactive-task-container');
container.appendChild(feedback);
if (container) {
container.appendChild(feedback);
} else {
// If no interactive container, try to append to task display
const taskDisplay = document.querySelector('.task-display-container') ||
document.querySelector('.task-display') ||
document.body;
if (taskDisplay) {
taskDisplay.appendChild(feedback);
} else {
console.warn('No suitable container found for feedback message');
return; // Exit early if no container available
}
}
}
feedback.className = `interactive-feedback feedback-${type}`;
@ -475,19 +488,36 @@ class InteractiveTaskManager {
choicesEl.appendChild(choiceBtn);
});
} else if (step.type === 'action') {
// Display action requirements
const actionBtn = document.createElement('button');
actionBtn.className = 'scenario-action';
actionBtn.innerHTML = `
<div class="action-text">${step.actionText}</div>
<div class="action-timer" id="action-timer">${step.duration || 30}s</div>
`;
// Check if this is a photography task first
if (this.isPhotographyStep(step) && this.game.webcamManager) {
// For photography tasks, show camera button instead of timer
const cameraBtn = document.createElement('button');
cameraBtn.className = 'scenario-camera-btn';
cameraBtn.innerHTML = `
<div class="camera-text">📸 ${step.actionText}</div>
<div class="camera-info">Take ${step.photoCount || 3} photos to complete this task</div>
`;
actionBtn.addEventListener('click', () => {
this.startScenarioAction(task, scenario, step);
});
cameraBtn.addEventListener('click', () => {
this.startPhotographySession(task, scenario, step);
});
choicesEl.appendChild(actionBtn);
choicesEl.appendChild(cameraBtn);
} else {
// For non-photography tasks, show regular action button with timer
const actionBtn = document.createElement('button');
actionBtn.className = 'scenario-action';
actionBtn.innerHTML = `
<div class="action-text">${step.actionText}</div>
<div class="action-timer" id="action-timer">${step.duration || 30}s</div>
`;
actionBtn.addEventListener('click', () => {
this.startScenarioAction(task, scenario, step);
});
choicesEl.appendChild(actionBtn);
}
} else if (step.type === 'ending') {
// Display ending and completion
task.scenarioState.completed = true;
@ -673,6 +703,385 @@ class InteractiveTaskManager {
async validateScenarioTask(task) {
return task.scenarioState && task.scenarioState.completed;
}
/**
* Check if a scenario step involves photography
*/
isPhotographyStep(step) {
if (!step) return false;
const photoKeywords = ['photo', 'photograph', 'camera', 'picture', 'pose', 'submissive_photo', 'dress.*photo'];
const textToCheck = `${step.actionText || ''} ${step.story || ''}`.toLowerCase();
return photoKeywords.some(keyword => {
if (keyword.includes('.*')) {
// Handle regex patterns
const regex = new RegExp(keyword);
return regex.test(textToCheck);
}
return textToCheck.includes(keyword);
});
}
/**
* Start photography session for scenario step
*/
async startPhotographySession(task, scenario, step) {
console.log('📸 Starting photography session for scenario step');
// Determine photo requirements from step
const photoRequirements = this.getPhotoRequirements(step);
const sessionType = this.getSessionTypeFromStep(step);
const success = await this.game.webcamManager.startPhotoSessionWithProgress(sessionType, {
task: task,
scenario: scenario,
step: step,
requirements: photoRequirements
});
if (!success) {
this.showFeedback('error', 'Camera not available. Please complete the task manually.');
}
}
/**
* Get photo requirements from step
*/
getPhotoRequirements(step) {
// First check if step has explicit photoCount property
if (step.photoCount) {
return {
count: step.photoCount,
description: `Take ${step.photoCount} photos to complete this task`
};
}
// Otherwise parse step content to determine how many photos are needed
const actionText = step.actionText?.toLowerCase() || '';
const story = step.story?.toLowerCase() || '';
// Look for photo count hints in the text
if (actionText.includes('series') || story.includes('multiple') || story.includes('several')) {
return { count: 3, description: 'Take 3 photos in different poses' };
} else if (actionText.includes('photo session') || story.includes('photo shoot')) {
return { count: 5, description: 'Complete photo session (5 photos)' };
} else {
return { count: 1, description: 'Take 1 photo to complete task' };
}
}
/**
* Get session type from scenario step
*/
getSessionTypeFromStep(step) {
const content = `${step.actionText || ''} ${step.story || ''}`.toLowerCase();
if (content.includes('submissive') || content.includes('pose')) {
return 'submissive_poses';
} else if (content.includes('dress') || content.includes('costume')) {
return 'dress_up_photos';
} else if (content.includes('humil') || content.includes('degrad')) {
return 'humiliation_photos';
} else {
return 'general_photography';
}
}
/**
* Offer webcam option for photography tasks
*/
async offerWebcamOption(task) {
return new Promise((resolve) => {
// Create modal dialog
const modal = document.createElement('div');
modal.id = 'webcam-offer-modal';
modal.innerHTML = `
<div class="modal-overlay">
<div class="modal-content">
<h3>📸 Photography Task Detected</h3>
<p>This task involves photography. Would you like to use your webcam for a more immersive experience?</p>
<div class="task-preview">
<strong>Task:</strong> ${task.text}
</div>
<div class="modal-buttons">
<button id="use-webcam-btn" class="btn-primary">📷 Use Webcam</button>
<button id="skip-webcam-btn" class="btn-secondary">📝 Text Only</button>
</div>
<div class="privacy-note">
<small>🔒 Photos are processed locally and never uploaded</small>
</div>
</div>
</div>
`;
document.body.appendChild(modal);
this.addWebcamModalStyles();
// Handle button clicks
document.getElementById('use-webcam-btn').addEventListener('click', () => {
modal.remove();
resolve(true);
});
document.getElementById('skip-webcam-btn').addEventListener('click', () => {
modal.remove();
resolve(false);
});
});
}
/**
* Start webcam photo session for photography task
*/
async startWebcamPhotoSession(task) {
console.log('📸 Starting webcam photo session for task:', task.interactiveType);
const sessionType = this.game.webcamManager.getSessionTypeFromTask(task);
const success = await this.game.webcamManager.startPhotoSession(sessionType, task);
if (success) {
console.log('✅ Webcam photo session started successfully');
return true;
} else {
console.log('❌ Webcam photo session failed to start, falling back to text mode');
// Fall back to regular interactive task display
return this.displayRegularInteractiveTask(task);
}
}
/**
* Handle photo session completion (multiple photos)
*/
handlePhotoSessionCompletion(detail) {
console.log('🎉 Photo session completion handled:', detail.sessionType, `(${detail.photos.length} photos)`);
// Show task completion message
if (this.game.showNotification) {
this.game.showNotification(`Photography completed! ${detail.photos.length} photos taken 📸`, 'success', 3000);
}
// Progress the scenario to next step
if (this.currentInteractiveTask && detail.taskData) {
const { task, scenario, step } = detail.taskData;
// Apply step effects
if (step.effects) {
this.applyEffects(step.effects, task.scenarioState);
}
// Move to next step
const nextStep = step.nextStep || 'completion';
setTimeout(() => {
this.displayScenarioStep(task, scenario, nextStep);
}, 1000);
}
}
/**
* Handle photo completion from webcam (single photo - keep for compatibility)
*/
handlePhotoCompletion(detail) {
console.log('📸 Photo completion handled:', detail.sessionType);
// Show task completion message
if (this.game.showNotification) {
this.game.showNotification('Photography task completed! 📸', 'success', 3000);
}
// Mark current task as completed after a delay (but don't use completeInteractiveTask to avoid container issues)
setTimeout(() => {
if (this.currentInteractiveTask) {
this.cleanupInteractiveTask();
this.game.completeTask();
}
}, 2000);
}
/**
* Resume from camera back to task
*/
resumeFromCamera() {
console.log('📱 Resuming from camera to task interface');
// Show completion message and progress to next task
if (this.currentInteractiveTask) {
// Mark task as completed without trying to show feedback in missing containers
this.cleanupInteractiveTask();
// Show success notification through game system instead
if (this.game && this.game.showNotification) {
this.game.showNotification('Photography task completed successfully! 📸', 'success', 3000);
}
// Complete the task directly
setTimeout(() => {
this.game.completeTask();
}, 1000);
}
}
/**
* Display regular interactive task (fallback from webcam)
*/
async displayRegularInteractiveTask(task) {
// Continue with regular interactive task display
// (This is the original displayInteractiveTask logic)
// Handle task image for interactive tasks
const taskImage = document.getElementById('task-image');
if (taskImage && task.image) {
console.log(`🖼️ Setting interactive task image: ${task.image}`);
taskImage.src = task.image;
taskImage.onerror = () => {
console.log('Interactive task image failed to load:', task.image);
taskImage.src = this.createPlaceholderImage('Interactive Task');
};
} else if (taskImage) {
console.log(`⚠️ No image provided for interactive task, creating placeholder`);
taskImage.src = this.createPlaceholderImage('Interactive Task');
}
// Continue with rest of task display logic...
const taskContainer = this.taskContainer || document.querySelector('.task-display-container');
// Add interactive container
const interactiveContainer = document.createElement('div');
interactiveContainer.id = 'interactive-task-container';
interactiveContainer.innerHTML = `
<div class="interactive-task-content">
<h3>Interactive Task: ${task.interactiveType}</h3>
<p>${task.text}</p>
<div class="task-completion">
<button id="complete-photo-task" class="btn-primary">📸 Complete Photography Task</button>
</div>
</div>
`;
if (taskContainer) {
taskContainer.appendChild(interactiveContainer);
}
// Bind completion button
document.getElementById('complete-photo-task').addEventListener('click', () => {
this.completeInteractiveTask();
});
return true;
}
/**
* Add styles for webcam modal
*/
addWebcamModalStyles() {
if (document.getElementById('webcam-modal-styles')) return;
const styles = document.createElement('style');
styles.id = 'webcam-modal-styles';
styles.textContent = `
#webcam-offer-modal .modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
#webcam-offer-modal .modal-content {
background: #2a2a2a;
padding: 30px;
border-radius: 10px;
max-width: 500px;
text-align: center;
color: white;
box-shadow: 0 4px 20px rgba(0,0,0,0.5);
}
#webcam-offer-modal h3 {
margin-bottom: 20px;
color: #ff6b6b;
}
#webcam-offer-modal .task-preview {
background: #3a3a3a;
padding: 15px;
border-radius: 5px;
margin: 20px 0;
font-style: italic;
}
#webcam-offer-modal .modal-buttons {
margin: 20px 0;
}
#webcam-offer-modal .modal-buttons button {
margin: 0 10px;
padding: 12px 24px;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
transition: all 0.3s;
}
#webcam-offer-modal .btn-primary {
background: #ff6b6b;
color: white;
}
#webcam-offer-modal .btn-secondary {
background: #6c757d;
color: white;
}
#webcam-offer-modal .modal-buttons button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.3);
}
#webcam-offer-modal .privacy-note {
color: #aaa;
margin-top: 15px;
}
/* Scenario camera button styles */
.scenario-camera-btn {
background: #ff6b6b;
color: white;
border: none;
border-radius: 8px;
padding: 15px 20px;
margin: 10px 0;
width: 100%;
cursor: pointer;
transition: all 0.3s;
font-size: 16px;
}
.scenario-camera-btn:hover {
background: #ff5252;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(255, 107, 107, 0.3);
}
.camera-text {
font-weight: bold;
margin-bottom: 5px;
}
.camera-info {
font-size: 14px;
opacity: 0.9;
}
`;
document.head.appendChild(styles);
}
}
// Export for use in other modules

1036
webcamManager.js Normal file

File diff suppressed because it is too large Load Diff