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:
parent
395c79363c
commit
6752097979
397
game.js
397
game.js
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue