Complete media library cleanup - consolidate duplicate gallery systems
Phase 1 - Video Library Consolidation: - Removed unified-video-gallery, loadUnifiedVideoGallery(), loadStandardVideoGallery(), loadLargeVideoGallery() - Removed loadVideoGalleryContent() and all category-video-gallery references - Removed setupVideoItemHandlers(), selectAllVideos(), deleteSelectedVideos(), deleteVideo() - Consolidated to single lib-video-gallery system Phase 2 - Image Library Consolidation: - Removed task-images-gallery, consequence-images-gallery elements and tabs - Removed obsolete image selection buttons - Added task/consequence/reward filtering to lib-image-gallery - Implemented directory-based category filtering Phase 3 - Audio Library Consolidation: - Removed background-audio-gallery, ambient-audio-gallery elements and tabs - Removed obsolete audio selection buttons - Enhanced lib-audio-gallery with background/ambient category filtering - Implemented efficient audio filtering system Impact: ~500+ lines removed, 12+ duplicate functions eliminated, 9+ obsolete UI elements cleaned Result: Single consolidated library system per media type with enhanced filtering capabilities
This commit is contained in:
parent
f1132822bf
commit
37ec5f0f1e
|
|
@ -18,9 +18,12 @@ The webGame uses an XP-based progression system where players advance through le
|
|||
- 1 XP for tasks completed in <5 minutes
|
||||
- 2 XP for tasks completed in 5-10 minutes
|
||||
- 3 XP for tasks completed in >10 minutes
|
||||
- **Session Time Bonus**: Additional XP for longer play sessions
|
||||
- **Session Time Bonus**: Additional 2 XP for every 15 minutes of continuous play. no partial time awarded.
|
||||
- **Session Recording Time Bonus**: Additional 1 XP for every 15 minutes of continuous play when the session is being recorded. no partial time awarded.
|
||||
- **Incomplete Tasks**: Players still earn XP even for incomplete tasks
|
||||
|
||||
- **Example**: Completing a 20-minute task earns 3 XP = 3 XP total
|
||||
- **Time bonus example**: A 45-minute quick play session earns 6 XP (3 x 15-minute intervals) = 6 XP total
|
||||
- **Recording bonus example**: A 45-minute recorded quick play session earns an additional 3 XP + session time bonus is an additional 6 XP (3 x 15-minute intervals) = 9 XP total
|
||||
### 🎭 Scenario Games
|
||||
- **Base Time XP**: 1 XP per 2 minutes of gameplay
|
||||
- **Webcam Bonus**: 1 XP per minute during webcam activities (x2 multiplier)
|
||||
|
|
|
|||
1076
index.html
1076
index.html
File diff suppressed because it is too large
Load Diff
|
|
@ -1,15 +1,15 @@
|
|||
{
|
||||
"name": "task-challenge-game",
|
||||
"name": "gooner-training-academy",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "task-challenge-game",
|
||||
"name": "gooner-training-academy",
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"electron": "^27.0.0",
|
||||
"electron": "^39.1.1",
|
||||
"electron-builder": "^24.6.4"
|
||||
}
|
||||
},
|
||||
|
|
@ -590,13 +590,13 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "18.19.127",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.127.tgz",
|
||||
"integrity": "sha512-gSjxjrnKXML/yo0BO099uPixMqfpJU0TKYjpfLU7TrtA2WWDki412Np/RSTPRil1saKBhvVVKzVx/p/6p94nVA==",
|
||||
"version": "22.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.0.tgz",
|
||||
"integrity": "sha512-xpr/lmLPQEj+TUnHmR+Ab91/glhJvsqcjB+yY0Ix9GO70H6Lb4FHH5GeqdOE5btAx7eIMwuHkp4H2MSkLcqWbA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/plist": {
|
||||
|
|
@ -1798,15 +1798,15 @@
|
|||
}
|
||||
},
|
||||
"node_modules/electron": {
|
||||
"version": "27.3.11",
|
||||
"resolved": "https://registry.npmjs.org/electron/-/electron-27.3.11.tgz",
|
||||
"integrity": "sha512-E1SiyEoI8iW5LW/MigCr7tJuQe7+0105UjqY7FkmCD12e2O6vtUbQ0j05HaBh2YgvkcEVgvQ2A8suIq5b5m6Gw==",
|
||||
"version": "39.1.1",
|
||||
"resolved": "https://registry.npmjs.org/electron/-/electron-39.1.1.tgz",
|
||||
"integrity": "sha512-VuFEI1yQ7BH3RYI5VZtwFlzGp4rpPRd5oEc26ZQIItVLcLTbXt4/O7o4hs+1fyg9Q3NvGAifgX5Vp5EBOIFpAg==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@electron/get": "^2.0.0",
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/node": "^22.7.7",
|
||||
"extract-zip": "^2.0.1"
|
||||
},
|
||||
"bin": {
|
||||
|
|
@ -3871,9 +3871,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "5.26.5",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -9,12 +9,13 @@
|
|||
"build-win": "electron-builder --win",
|
||||
"build-mac": "electron-builder --mac",
|
||||
"build-linux": "electron-builder --linux",
|
||||
"dev": "electron . --dev"
|
||||
"dev": "electron . --dev",
|
||||
"test-codecs": "electron . --dev --enable-logging"
|
||||
},
|
||||
"author": "Gooner Training Academy Developer",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"electron": "^27.0.0",
|
||||
"electron": "^39.1.1",
|
||||
"electron-builder": "^24.6.4"
|
||||
},
|
||||
"build": {
|
||||
|
|
@ -60,4 +61,4 @@
|
|||
"electron",
|
||||
"nsfw"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
615
quick-play.html
615
quick-play.html
|
|
@ -184,6 +184,16 @@
|
|||
Records your session with a small webcam viewer. All recordings are stored locally on your device.
|
||||
</div>
|
||||
<div class="webcam-sub-options" id="webcam-sub-options" style="display: none;">
|
||||
<div class="setting-row">
|
||||
<label for="webcam-output-directory">Output Directory:</label>
|
||||
<div class="directory-selector">
|
||||
<input type="text" id="webcam-output-path" readonly placeholder="Select directory for saving recordings..." style="flex: 1; margin-right: 10px;">
|
||||
<button type="button" id="select-webcam-directory" class="btn btn-secondary">📁 Browse</button>
|
||||
</div>
|
||||
<div class="setting-description">
|
||||
Choose where session recordings will be automatically saved. Directory will be remembered for future sessions. If no directory is selected, recordings will download automatically.
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<label for="webcam-position">Viewer Position:</label>
|
||||
<select id="webcam-position">
|
||||
|
|
@ -1198,9 +1208,6 @@
|
|||
<button id="new-settings" class="btn btn-secondary">
|
||||
⚙️ Change Settings
|
||||
</button>
|
||||
<button id="session-videos" class="btn btn-secondary">
|
||||
📹 Session Videos
|
||||
</button>
|
||||
<button id="back-to-home-results" class="btn btn-secondary">
|
||||
🏠 Back to Home
|
||||
</button>
|
||||
|
|
@ -1208,37 +1215,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Session Videos Gallery -->
|
||||
<div class="session-videos-gallery" id="session-videos-gallery" style="display: none;">
|
||||
<div class="videos-container">
|
||||
<div class="videos-header">
|
||||
<h2>📹 Session Videos</h2>
|
||||
<p>Your recorded Quick Play sessions</p>
|
||||
</div>
|
||||
|
||||
<div class="videos-content">
|
||||
<div class="videos-grid" id="videos-grid">
|
||||
<!-- Videos will be populated here -->
|
||||
</div>
|
||||
<div class="no-videos-message" id="no-videos-message" style="display: none;">
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">📹</div>
|
||||
<h3>No Session Videos Yet</h3>
|
||||
<p>Enable session recording in Quick Play setup to start recording your training sessions.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="videos-actions">
|
||||
<button id="clear-all-videos" class="btn btn-danger">
|
||||
🗑️ Clear All Videos
|
||||
</button>
|
||||
<button id="back-to-results" class="btn btn-secondary">
|
||||
⬅️ Back to Results
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Scripts -->
|
||||
|
|
@ -1285,6 +1262,7 @@
|
|||
videoOpacity: 0.7,
|
||||
// Webcam recording settings
|
||||
enableSessionRecording: false,
|
||||
webcamOutputDirectory: '',
|
||||
webcamPosition: 'bottom-right',
|
||||
webcamSize: 'small',
|
||||
// Task management
|
||||
|
|
@ -1310,13 +1288,41 @@
|
|||
xp: 0
|
||||
};
|
||||
|
||||
// Check MP4 codec support for Electron
|
||||
function checkMP4Support() {
|
||||
console.log('🎥 Checking MP4 codec support...');
|
||||
|
||||
const mp4Codecs = [
|
||||
'video/mp4;codecs=avc1.42E01E',
|
||||
'video/mp4;codecs=avc1.4D401E',
|
||||
'video/mp4;codecs=avc1.64001E',
|
||||
'video/mp4'
|
||||
];
|
||||
|
||||
const supportedCodecs = mp4Codecs.filter(codec => MediaRecorder.isTypeSupported(codec));
|
||||
|
||||
if (supportedCodecs.length > 0) {
|
||||
console.log('✅ MP4 codec support confirmed:', supportedCodecs);
|
||||
return true;
|
||||
} else {
|
||||
console.error('❌ No MP4 codecs supported. Available types:');
|
||||
console.error('WebM VP8:', MediaRecorder.isTypeSupported('video/webm;codecs=vp8'));
|
||||
console.error('WebM VP9:', MediaRecorder.isTypeSupported('video/webm;codecs=vp9'));
|
||||
console.error('WebM:', MediaRecorder.isTypeSupported('video/webm'));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize Quick Play when page loads
|
||||
document.addEventListener('DOMContentLoaded', async function() {
|
||||
console.log('⚡ Initializing Quick Play...');
|
||||
|
||||
try {
|
||||
// Check MP4 support first
|
||||
checkMP4Support();
|
||||
|
||||
// Load saved settings
|
||||
loadSavedSettings();
|
||||
await loadSavedSettings();
|
||||
console.log('✅ Settings loaded');
|
||||
|
||||
// Setup event listeners
|
||||
|
|
@ -1411,7 +1417,43 @@
|
|||
}
|
||||
});
|
||||
|
||||
function loadSavedSettings() {
|
||||
// Directory handle persistence functions
|
||||
async function storeDirectoryHandle(directoryHandle) {
|
||||
// Modern browsers with Origin Private File System API
|
||||
if ('storage' in navigator && 'persist' in navigator.storage) {
|
||||
try {
|
||||
const opfsRoot = await navigator.storage.getDirectory();
|
||||
const handleFile = await opfsRoot.getFileHandle('webcam-directory-handle', { create: true });
|
||||
const writable = await handleFile.createWritable();
|
||||
await writable.write(JSON.stringify({ name: directoryHandle.name }));
|
||||
await writable.close();
|
||||
return 'opfs-stored';
|
||||
} catch (error) {
|
||||
console.log('OPFS storage failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback - just return a reference ID
|
||||
return `handle-${Date.now()}`;
|
||||
}
|
||||
|
||||
async function loadStoredDirectory() {
|
||||
try {
|
||||
// Try to load saved directory name
|
||||
const savedDirectory = localStorage.getItem('webcamRecordingDirectory');
|
||||
if (savedDirectory) {
|
||||
quickPlaySettings.webcamOutputDirectory = savedDirectory;
|
||||
document.getElementById('webcam-output-path').value = savedDirectory;
|
||||
console.log('📁 Restored recording directory:', savedDirectory);
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading stored directory:', error);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async function loadSavedSettings() {
|
||||
try {
|
||||
const saved = localStorage.getItem('quickPlaySettings');
|
||||
if (saved) {
|
||||
|
|
@ -1424,6 +1466,10 @@
|
|||
} else {
|
||||
console.log('📂 No saved settings found, using defaults');
|
||||
}
|
||||
|
||||
// Also load saved directory
|
||||
await loadStoredDirectory();
|
||||
|
||||
} catch (error) {
|
||||
console.warn('Failed to load saved settings:', error);
|
||||
}
|
||||
|
|
@ -2057,37 +2103,7 @@
|
|||
});
|
||||
}
|
||||
|
||||
// Session videos button
|
||||
const sessionVideosBtn = document.getElementById('session-videos');
|
||||
if (sessionVideosBtn) {
|
||||
sessionVideosBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
console.log('Session videos button clicked');
|
||||
showSessionVideosGallery();
|
||||
});
|
||||
}
|
||||
|
||||
// Back to results from videos gallery
|
||||
const backToResultsBtn = document.getElementById('back-to-results');
|
||||
if (backToResultsBtn) {
|
||||
backToResultsBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
console.log('Back to results button clicked');
|
||||
hideSessionVideosGallery();
|
||||
});
|
||||
}
|
||||
|
||||
// Clear all videos button
|
||||
const clearAllVideosBtn = document.getElementById('clear-all-videos');
|
||||
if (clearAllVideosBtn) {
|
||||
clearAllVideosBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
console.log('Clear all videos button clicked');
|
||||
if (confirm('⚠️ Are you sure you want to delete all recorded session videos? This cannot be undone.')) {
|
||||
clearAllSessionVideos();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Force exit button
|
||||
const forceExitBtn = document.getElementById('force-exit');
|
||||
|
|
@ -2235,6 +2251,42 @@
|
|||
quickPlaySettings.enableSessionRecording = e.target.checked;
|
||||
updateWebcamOptionsVisibility();
|
||||
});
|
||||
document.getElementById('select-webcam-directory').addEventListener('click', async () => {
|
||||
try {
|
||||
const directoryHandle = await window.showDirectoryPicker();
|
||||
const directoryPath = directoryHandle.name;
|
||||
|
||||
// Save directory persistently
|
||||
quickPlaySettings.webcamOutputDirectory = directoryPath;
|
||||
quickPlaySettings.webcamDirectoryHandle = directoryHandle;
|
||||
|
||||
// Save to localStorage for persistence across sessions
|
||||
localStorage.setItem('selectedVideoDirectory', JSON.stringify({
|
||||
name: directoryPath,
|
||||
timestamp: Date.now()
|
||||
}));
|
||||
|
||||
// Also save to more persistent storage
|
||||
localStorage.setItem('webcamRecordingDirectory', directoryPath);
|
||||
|
||||
// Try to store directory handle reference (where supported)
|
||||
try {
|
||||
const handleId = await storeDirectoryHandle(directoryHandle);
|
||||
localStorage.setItem('webcamDirectoryHandleId', handleId);
|
||||
} catch (handleError) {
|
||||
console.log('📁 Directory handle storage not supported, will use fallback download');
|
||||
}
|
||||
|
||||
document.getElementById('webcam-output-path').value = directoryPath;
|
||||
console.log('📁 Webcam output directory selected and saved:', directoryPath);
|
||||
|
||||
if (window.flashMessageManager) {
|
||||
window.flashMessageManager.show(`📁 Recording directory set to: ${directoryPath}`, 'positive');
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('📁 Directory selection cancelled or failed:', error);
|
||||
}
|
||||
});
|
||||
document.getElementById('webcam-position').addEventListener('change', (e) => {
|
||||
quickPlaySettings.webcamPosition = e.target.value;
|
||||
});
|
||||
|
|
@ -2354,6 +2406,14 @@
|
|||
};
|
||||
console.log('📊 Session stats reset:', sessionStats);
|
||||
|
||||
// Start periodic XP display updates (every minute)
|
||||
if (window.xpDisplayInterval) {
|
||||
clearInterval(window.xpDisplayInterval);
|
||||
}
|
||||
window.xpDisplayInterval = setInterval(() => {
|
||||
updateSessionDisplay();
|
||||
}, 60000); // Update every minute
|
||||
|
||||
// Initialize webcam recording if enabled
|
||||
if (quickPlaySettings.enableSessionRecording) {
|
||||
initializeWebcamRecording();
|
||||
|
|
@ -3034,17 +3094,40 @@
|
|||
}
|
||||
}
|
||||
|
||||
function awardSessionTimeXP() {
|
||||
// Award XP for incomplete sessions based on time spent
|
||||
function calculateCurrentTimeBonus() {
|
||||
// Calculate current time bonus without awarding it (for display purposes)
|
||||
const sessionDurationMinutes = (Date.now() - sessionStats.started) / (1000 * 60);
|
||||
const timeBasedXP = Math.floor(sessionDurationMinutes / 2); // 1 XP per 2 minutes of session time
|
||||
const complete15MinIntervals = Math.floor(sessionDurationMinutes / 15);
|
||||
let timeBasedXP = complete15MinIntervals * 2; // 2 XP per complete 15-minute interval
|
||||
|
||||
// Add recording bonus if session recording is enabled
|
||||
if (quickPlaySettings.enableSessionRecording) {
|
||||
const recordingBonusXP = complete15MinIntervals * 1; // Additional 1 XP per interval when recording
|
||||
timeBasedXP += recordingBonusXP;
|
||||
}
|
||||
|
||||
return timeBasedXP;
|
||||
}
|
||||
|
||||
function awardSessionTimeXP() {
|
||||
// Award XP based on new system: 2 XP per complete 15-minute interval
|
||||
const sessionDurationMinutes = (Date.now() - sessionStats.started) / (1000 * 60);
|
||||
const complete15MinIntervals = Math.floor(sessionDurationMinutes / 15);
|
||||
let timeBasedXP = complete15MinIntervals * 2; // 2 XP per complete 15-minute interval
|
||||
|
||||
// Add recording bonus if session recording is enabled
|
||||
if (quickPlaySettings.enableSessionRecording) {
|
||||
const recordingBonusXP = complete15MinIntervals * 1; // Additional 1 XP per interval when recording
|
||||
timeBasedXP += recordingBonusXP;
|
||||
console.log(`📹 Recording bonus: ${recordingBonusXP} XP for ${complete15MinIntervals} complete 15-minute intervals`);
|
||||
}
|
||||
|
||||
if (timeBasedXP > 0) {
|
||||
sessionStats.xp += timeBasedXP;
|
||||
if (gameInstance && gameInstance.gameState) {
|
||||
gameInstance.gameState.xp = (gameInstance.gameState.xp || 0) + timeBasedXP;
|
||||
}
|
||||
console.log(`⏰ Awarded ${timeBasedXP} XP for ${sessionDurationMinutes.toFixed(1)} minutes of session time`);
|
||||
console.log(`⏰ Awarded ${timeBasedXP} XP for ${complete15MinIntervals} complete 15-minute intervals (${sessionDurationMinutes.toFixed(1)} minutes total)`);
|
||||
}
|
||||
|
||||
return timeBasedXP;
|
||||
|
|
@ -3669,6 +3752,13 @@
|
|||
console.log('endGame called - starting game termination');
|
||||
|
||||
try {
|
||||
// Clear XP display update interval
|
||||
if (window.xpDisplayInterval) {
|
||||
clearInterval(window.xpDisplayInterval);
|
||||
window.xpDisplayInterval = null;
|
||||
console.log('🔄 Cleared XP display update interval');
|
||||
}
|
||||
|
||||
// Immediately mute all videos to stop audio instantly
|
||||
const allVideos = document.querySelectorAll('video');
|
||||
allVideos.forEach(video => {
|
||||
|
|
@ -4072,10 +4162,12 @@
|
|||
tasksElement.textContent = sessionStats.completed;
|
||||
}
|
||||
|
||||
// Update XP display
|
||||
// Update XP display (include current time bonus)
|
||||
const xpElement = document.getElementById('current-xp');
|
||||
if (xpElement) {
|
||||
xpElement.textContent = sessionStats.xp;
|
||||
const currentTimeBonus = calculateCurrentTimeBonus();
|
||||
const totalCurrentXP = sessionStats.xp + currentTimeBonus;
|
||||
xpElement.textContent = totalCurrentXP;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -4449,9 +4541,15 @@
|
|||
console.log('🎥 Initializing webcam recording for session...');
|
||||
|
||||
try {
|
||||
// Request camera access
|
||||
// Request camera access with high quality 16:9 settings
|
||||
webcamStream = await navigator.mediaDevices.getUserMedia({
|
||||
video: true,
|
||||
video: {
|
||||
width: { ideal: 1920, min: 1280 },
|
||||
height: { ideal: 1080, min: 720 },
|
||||
aspectRatio: { ideal: 16/9 },
|
||||
frameRate: { ideal: 30, min: 24 },
|
||||
facingMode: 'user'
|
||||
},
|
||||
audio: false // Audio disabled for privacy
|
||||
});
|
||||
|
||||
|
|
@ -4464,20 +4562,42 @@
|
|||
// Apply user settings
|
||||
webcamViewer.className = `webcam-viewer active ${quickPlaySettings.webcamSize} ${quickPlaySettings.webcamPosition}`;
|
||||
|
||||
// Set up recording with lower quality for storage efficiency
|
||||
// Set up recording - Force MP4 format for Electron
|
||||
recordedChunks = [];
|
||||
const options = {
|
||||
mimeType: 'video/webm;codecs=vp8',
|
||||
videoBitsPerSecond: 250000 // 250kbps for smaller file size
|
||||
};
|
||||
|
||||
// Try different recording options if not supported
|
||||
if (MediaRecorder.isTypeSupported('video/webm;codecs=vp8')) {
|
||||
mediaRecorder = new MediaRecorder(webcamStream, options);
|
||||
} else if (MediaRecorder.isTypeSupported('video/webm')) {
|
||||
mediaRecorder = new MediaRecorder(webcamStream, { mimeType: 'video/webm' });
|
||||
} else {
|
||||
mediaRecorder = new MediaRecorder(webcamStream);
|
||||
// Try various MP4 codec combinations for Electron compatibility
|
||||
const mp4Codecs = [
|
||||
'video/mp4;codecs=avc1.42E01E', // H.264 Baseline
|
||||
'video/mp4;codecs=avc1.4D401E', // H.264 Main
|
||||
'video/mp4;codecs=avc1.64001E', // H.264 High
|
||||
'video/mp4', // Generic MP4
|
||||
];
|
||||
|
||||
let mediaRecorderCreated = false;
|
||||
|
||||
// Try each MP4 codec until one works
|
||||
for (const codec of mp4Codecs) {
|
||||
if (MediaRecorder.isTypeSupported(codec)) {
|
||||
try {
|
||||
mediaRecorder = new MediaRecorder(webcamStream, {
|
||||
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);
|
||||
mediaRecorderCreated = true;
|
||||
break;
|
||||
} catch (error) {
|
||||
console.warn('Failed to create MediaRecorder with codec:', codec, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no MP4 codec worked, throw error since user wants MP4 only
|
||||
if (!mediaRecorderCreated) {
|
||||
throw new Error('❌ MP4 recording not supported in this Electron version. Please update Electron or Chrome engine.');
|
||||
}
|
||||
|
||||
mediaRecorder.ondataavailable = (event) => {
|
||||
|
|
@ -4490,9 +4610,9 @@
|
|||
saveRecordedSession();
|
||||
};
|
||||
|
||||
// Start recording
|
||||
mediaRecorder.start();
|
||||
console.log('✅ Session recording started');
|
||||
// Start recording with optimized timeslice for quality
|
||||
mediaRecorder.start(1000); // Collect data every 1 second for better quality
|
||||
console.log('✅ High-quality session recording started at 4 Mbps');
|
||||
|
||||
// Set up viewer controls
|
||||
setupWebcamViewerControls();
|
||||
|
|
@ -4588,313 +4708,70 @@
|
|||
console.log('✅ Webcam recording stopped');
|
||||
}
|
||||
|
||||
function saveRecordedSession() {
|
||||
console.log('💾 Saving recorded session to gallery...');
|
||||
async function saveRecordedSession() {
|
||||
console.log('💾 Automatically saving recorded session...');
|
||||
|
||||
if (recordedChunks.length === 0) {
|
||||
console.warn('No recorded data to save');
|
||||
return;
|
||||
}
|
||||
|
||||
const blob = new Blob(recordedChunks, { type: 'video/webm' });
|
||||
const reader = new FileReader();
|
||||
// Use the recording format that was selected during setup
|
||||
const mimeType = quickPlaySettings.recordingMimeType || 'video/mp4';
|
||||
const extension = quickPlaySettings.recordingExtension || 'mp4';
|
||||
const blob = new Blob(recordedChunks, { type: mimeType });
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
const filename = `quick-play-session-${timestamp}.${extension}`;
|
||||
|
||||
reader.onload = function(event) {
|
||||
// Create video element to capture thumbnail
|
||||
const video = document.createElement('video');
|
||||
video.src = event.target.result;
|
||||
video.muted = true;
|
||||
video.currentTime = 2; // Seek to 2 seconds for thumbnail
|
||||
|
||||
video.addEventListener('seeked', function() {
|
||||
// Create canvas to capture thumbnail
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
canvas.width = 160; // Small thumbnail size
|
||||
canvas.height = 90; // 16:9 aspect ratio
|
||||
// Try to save to user-selected directory first (if available)
|
||||
if (quickPlaySettings.webcamDirectoryHandle) {
|
||||
try {
|
||||
const fileHandle = await quickPlaySettings.webcamDirectoryHandle.getFileHandle(filename, { create: true });
|
||||
const writable = await fileHandle.createWritable();
|
||||
await writable.write(blob);
|
||||
await writable.close();
|
||||
|
||||
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
||||
const thumbnailDataURL = canvas.toDataURL('image/jpeg', 0.7); // Compressed thumbnail
|
||||
console.log('✅ Session recording saved to directory:', filename);
|
||||
|
||||
const videoData = {
|
||||
id: `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||
thumbnail: thumbnailDataURL, // Store only thumbnail, not full video
|
||||
videoBlob: event.target.result, // Store full video separately for download
|
||||
timestamp: Date.now(),
|
||||
sessionType: 'quick-play-recording',
|
||||
duration: Date.now() - sessionStats.started,
|
||||
metadata: {
|
||||
type: 'session-recording',
|
||||
format: 'webm',
|
||||
source: 'quick-play',
|
||||
settings: {
|
||||
position: quickPlaySettings.webcamPosition,
|
||||
size: quickPlaySettings.webcamSize
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
// Try to save - if it fails due to quota, save without full video
|
||||
let capturedVideos = JSON.parse(localStorage.getItem('capturedVideos') || '[]');
|
||||
capturedVideos.push(videoData);
|
||||
|
||||
// Keep only last 5 recordings to manage storage
|
||||
if (capturedVideos.length > 5) {
|
||||
capturedVideos = capturedVideos.slice(-5);
|
||||
}
|
||||
|
||||
localStorage.setItem('capturedVideos', JSON.stringify(capturedVideos));
|
||||
console.log('✅ Session recording saved with full video:', videoData.id);
|
||||
|
||||
} catch (error) {
|
||||
console.warn('⚠️ Full video too large for storage, saving thumbnail only:', error);
|
||||
|
||||
// Fallback: Save without full video data
|
||||
const lightVideoData = {
|
||||
...videoData,
|
||||
videoBlob: null, // Remove full video data
|
||||
storageError: true
|
||||
};
|
||||
|
||||
try {
|
||||
let capturedVideos = JSON.parse(localStorage.getItem('capturedVideos') || '[]');
|
||||
capturedVideos.push(lightVideoData);
|
||||
|
||||
if (capturedVideos.length > 10) {
|
||||
capturedVideos = capturedVideos.slice(-10);
|
||||
}
|
||||
|
||||
localStorage.setItem('capturedVideos', JSON.stringify(capturedVideos));
|
||||
console.log('✅ Session recording saved (thumbnail only):', lightVideoData.id);
|
||||
|
||||
// Offer immediate download since storage failed
|
||||
if (confirm('📹 Video too large for gallery storage. Download now?')) {
|
||||
const a = document.createElement('a');
|
||||
a.href = event.target.result;
|
||||
a.download = `quick-play-session-${Date.now()}.webm`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
}
|
||||
|
||||
} catch (secondError) {
|
||||
console.error('❌ Failed to save even thumbnail:', secondError);
|
||||
// Last resort: offer immediate download
|
||||
const a = document.createElement('a');
|
||||
a.href = event.target.result;
|
||||
a.download = `quick-play-session-${Date.now()}.webm`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
}
|
||||
}
|
||||
|
||||
// Show completion message
|
||||
if (window.flashMessageManager) {
|
||||
window.flashMessageManager.show('📹 Session recording saved! Check library gallery to view.', 'positive');
|
||||
window.flashMessageManager.show(`📹 Recording saved to directory: ${filename}`, 'positive');
|
||||
}
|
||||
|
||||
// Clean up
|
||||
recordedChunks = [];
|
||||
video.remove();
|
||||
});
|
||||
|
||||
video.addEventListener('error', function() {
|
||||
console.warn('⚠️ Could not create thumbnail, saving without thumbnail');
|
||||
// Fallback without thumbnail
|
||||
const videoData = {
|
||||
id: `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||
thumbnail: null,
|
||||
videoBlob: event.target.result,
|
||||
timestamp: Date.now(),
|
||||
sessionType: 'quick-play-recording',
|
||||
duration: Date.now() - sessionStats.started,
|
||||
metadata: {
|
||||
type: 'session-recording',
|
||||
format: 'webm',
|
||||
source: 'quick-play',
|
||||
settings: {
|
||||
position: quickPlaySettings.webcamPosition,
|
||||
size: quickPlaySettings.webcamSize
|
||||
}
|
||||
}
|
||||
};
|
||||
return;
|
||||
|
||||
// Same storage logic as above...
|
||||
recordedChunks = [];
|
||||
});
|
||||
};
|
||||
|
||||
reader.readAsDataURL(blob);
|
||||
}
|
||||
|
||||
// Session Videos Gallery Functions
|
||||
function showSessionVideosGallery() {
|
||||
console.log('📹 Opening session videos gallery');
|
||||
|
||||
// Hide results screen, show videos gallery
|
||||
document.getElementById('quick-play-results').style.display = 'none';
|
||||
document.getElementById('session-videos-gallery').style.display = 'block';
|
||||
|
||||
// Load and display videos
|
||||
loadSessionVideos();
|
||||
}
|
||||
|
||||
function hideSessionVideosGallery() {
|
||||
console.log('📹 Closing session videos gallery');
|
||||
|
||||
// Hide videos gallery, show results screen
|
||||
document.getElementById('session-videos-gallery').style.display = 'none';
|
||||
document.getElementById('quick-play-results').style.display = 'block';
|
||||
}
|
||||
|
||||
function loadSessionVideos() {
|
||||
const capturedVideos = JSON.parse(localStorage.getItem('capturedVideos') || '[]');
|
||||
const videosGrid = document.getElementById('videos-grid');
|
||||
const noVideosMessage = document.getElementById('no-videos-message');
|
||||
|
||||
if (capturedVideos.length === 0) {
|
||||
videosGrid.innerHTML = '';
|
||||
noVideosMessage.style.display = 'block';
|
||||
return;
|
||||
} catch (error) {
|
||||
console.error('❌ Directory save failed, falling back to download:', error);
|
||||
}
|
||||
}
|
||||
|
||||
noVideosMessage.style.display = 'none';
|
||||
|
||||
// Sort by timestamp (newest first)
|
||||
capturedVideos.sort((a, b) => b.timestamp - a.timestamp);
|
||||
|
||||
videosGrid.innerHTML = capturedVideos.map(video => {
|
||||
const date = new Date(video.timestamp);
|
||||
const duration = formatDuration(video.duration);
|
||||
|
||||
return `
|
||||
<div class="video-item" data-video-id="${video.id}">
|
||||
<div class="video-preview">
|
||||
<video preload="metadata" muted>
|
||||
<source src="${video.dataURL}" type="video/webm">
|
||||
</video>
|
||||
<div class="video-overlay">
|
||||
<button class="play-btn" onclick="playVideo('${video.id}')">▶️</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="video-info">
|
||||
<div class="video-title">Session Recording</div>
|
||||
<div class="video-meta">
|
||||
<span class="video-date">${date.toLocaleDateString()} ${date.toLocaleTimeString()}</span>
|
||||
<span class="video-duration">${duration}</span>
|
||||
</div>
|
||||
<div class="video-settings">
|
||||
Position: ${video.metadata.settings.position} | Size: ${video.metadata.settings.size}
|
||||
</div>
|
||||
</div>
|
||||
<div class="video-actions">
|
||||
<button class="btn btn-sm btn-primary" onclick="downloadVideo('${video.id}')">
|
||||
💾 Download
|
||||
</button>
|
||||
<button class="btn btn-sm btn-danger" onclick="deleteVideo('${video.id}')">
|
||||
🗑️ Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function formatDuration(ms) {
|
||||
const seconds = Math.floor(ms / 1000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const remainingSeconds = seconds % 60;
|
||||
return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
function playVideo(videoId) {
|
||||
const capturedVideos = JSON.parse(localStorage.getItem('capturedVideos') || '[]');
|
||||
const video = capturedVideos.find(v => v.id === videoId);
|
||||
|
||||
if (!video) {
|
||||
console.warn('Video not found:', videoId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create fullscreen video player
|
||||
const overlay = document.createElement('div');
|
||||
overlay.className = 'video-player-overlay';
|
||||
overlay.innerHTML = `
|
||||
<div class="video-player-container">
|
||||
<video controls autoplay>
|
||||
<source src="${video.dataURL}" type="video/webm">
|
||||
</video>
|
||||
<button class="close-player" onclick="closeVideoPlayer()">✕</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(overlay);
|
||||
window.currentVideoOverlay = overlay;
|
||||
}
|
||||
|
||||
function closeVideoPlayer() {
|
||||
if (window.currentVideoOverlay) {
|
||||
document.body.removeChild(window.currentVideoOverlay);
|
||||
window.currentVideoOverlay = null;
|
||||
}
|
||||
}
|
||||
|
||||
function downloadVideo(videoId) {
|
||||
const capturedVideos = JSON.parse(localStorage.getItem('capturedVideos') || '[]');
|
||||
const video = capturedVideos.find(v => v.id === videoId);
|
||||
|
||||
if (!video) {
|
||||
console.warn('Video not found:', videoId);
|
||||
return;
|
||||
}
|
||||
// Automatic download fallback (no prompts)
|
||||
console.log('💾 Auto-downloading recording...');
|
||||
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = video.dataURL;
|
||||
a.download = `quick-play-session-${new Date(video.timestamp).toISOString().slice(0, 19)}.webm`;
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
a.style.display = 'none';
|
||||
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
console.log('📹 Video downloaded:', videoId);
|
||||
console.log('✅ Session recording auto-downloaded:', filename);
|
||||
|
||||
if (window.flashMessageManager) {
|
||||
window.flashMessageManager.show('📹 Video downloaded successfully!', 'positive');
|
||||
window.flashMessageManager.show(`📹 Recording downloaded: ${filename}`, 'positive');
|
||||
}
|
||||
|
||||
// Clean up
|
||||
recordedChunks = [];
|
||||
|
||||
|
||||
}
|
||||
|
||||
function deleteVideo(videoId) {
|
||||
if (!confirm('⚠️ Are you sure you want to delete this video? This cannot be undone.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
let capturedVideos = JSON.parse(localStorage.getItem('capturedVideos') || '[]');
|
||||
capturedVideos = capturedVideos.filter(v => v.id !== videoId);
|
||||
|
||||
localStorage.setItem('capturedVideos', JSON.stringify(capturedVideos));
|
||||
|
||||
console.log('🗑️ Video deleted:', videoId);
|
||||
|
||||
// Reload the gallery
|
||||
loadSessionVideos();
|
||||
|
||||
if (window.flashMessageManager) {
|
||||
window.flashMessageManager.show('🗑️ Video deleted successfully!', 'positive');
|
||||
}
|
||||
}
|
||||
|
||||
function clearAllSessionVideos() {
|
||||
localStorage.removeItem('capturedVideos');
|
||||
loadSessionVideos();
|
||||
|
||||
console.log('🗑️ All session videos cleared');
|
||||
|
||||
if (window.flashMessageManager) {
|
||||
window.flashMessageManager.show('🗑️ All videos cleared successfully!', 'positive');
|
||||
}
|
||||
}
|
||||
|
||||
function showErrorDialog(message) {
|
||||
alert(`❌ Error: ${message}`);
|
||||
|
|
@ -9827,17 +9704,17 @@
|
|||
/* Size variants */
|
||||
.webcam-viewer.small {
|
||||
width: 150px;
|
||||
height: 113px; /* 4:3 aspect ratio */
|
||||
height: 84px; /* 16:9 aspect ratio */
|
||||
}
|
||||
|
||||
.webcam-viewer.medium {
|
||||
width: 200px;
|
||||
height: 150px;
|
||||
height: 113px; /* 16:9 aspect ratio */
|
||||
}
|
||||
|
||||
.webcam-viewer.large {
|
||||
width: 250px;
|
||||
height: 188px;
|
||||
height: 141px; /* 16:9 aspect ratio */
|
||||
}
|
||||
|
||||
/* Position variants */
|
||||
|
|
|
|||
|
|
@ -2,6 +2,12 @@ const { app, BrowserWindow, dialog, ipcMain } = require('electron');
|
|||
const path = require('path');
|
||||
const fs = require('fs').promises;
|
||||
|
||||
// Enable MP4 codec support in Electron
|
||||
app.commandLine.appendSwitch('enable-features', 'VaapiVideoDecoder');
|
||||
app.commandLine.appendSwitch('use-file-for-fake-video-capture', 'false');
|
||||
app.commandLine.appendSwitch('enable-experimental-web-platform-features');
|
||||
app.commandLine.appendSwitch('enable-blink-features', 'MediaRecorder');
|
||||
|
||||
let mainWindow;
|
||||
|
||||
// Window state management
|
||||
|
|
@ -70,7 +76,10 @@ async function createWindow() {
|
|||
contextIsolation: true,
|
||||
enableRemoteModule: false,
|
||||
webSecurity: false, // Allow requests to localhost
|
||||
preload: path.join(__dirname, 'preload.js')
|
||||
preload: path.join(__dirname, 'preload.js'),
|
||||
// Enable media features for MP4 recording
|
||||
allowRunningInsecureContent: true,
|
||||
experimentalFeatures: true
|
||||
},
|
||||
icon: path.join(__dirname, 'assets', 'icon.png'),
|
||||
show: false,
|
||||
|
|
|
|||
|
|
@ -6949,4 +6949,152 @@ button#start-mirror-btn:disabled {
|
|||
.character-toggle-btn:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* Game Guide Styles */
|
||||
.game-guide-section {
|
||||
margin: 1.5rem 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn-guide {
|
||||
background: linear-gradient(135deg, #4a148c, #6a1b9a);
|
||||
color: white;
|
||||
border: 2px solid #9c27b0;
|
||||
padding: 0.8rem 2rem;
|
||||
border-radius: 25px;
|
||||
font-weight: bold;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 15px rgba(156, 39, 176, 0.3);
|
||||
}
|
||||
|
||||
.btn-guide:hover {
|
||||
background: linear-gradient(135deg, #6a1b9a, #8e24aa);
|
||||
border-color: #ba68c8;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(156, 39, 176, 0.4);
|
||||
}
|
||||
|
||||
.game-guide {
|
||||
max-width: 800px;
|
||||
margin: 1rem auto;
|
||||
background: rgba(26, 26, 26, 0.95);
|
||||
border: 2px solid #9c27b0;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 8px 32px rgba(156, 39, 176, 0.3);
|
||||
backdrop-filter: blur(10px);
|
||||
animation: slideDown 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.guide-content {
|
||||
padding: 2rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.guide-content h3 {
|
||||
color: #ba68c8;
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
text-align: center;
|
||||
text-shadow: 0 0 10px rgba(186, 104, 200, 0.5);
|
||||
}
|
||||
|
||||
.guide-section {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.guide-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
background: rgba(156, 39, 176, 0.1);
|
||||
border: 1px solid rgba(156, 39, 176, 0.3);
|
||||
border-radius: 10px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.guide-item:hover {
|
||||
background: rgba(156, 39, 176, 0.15);
|
||||
border-color: rgba(156, 39, 176, 0.5);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.guide-icon {
|
||||
font-size: 2rem;
|
||||
min-width: 60px;
|
||||
text-align: center;
|
||||
background: rgba(156, 39, 176, 0.2);
|
||||
border-radius: 50%;
|
||||
padding: 0.5rem;
|
||||
border: 2px solid rgba(156, 39, 176, 0.4);
|
||||
}
|
||||
|
||||
.guide-info h4 {
|
||||
color: #e1bee7;
|
||||
font-size: 1.2rem;
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.guide-info p {
|
||||
color: #cccccc;
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.guide-tips {
|
||||
background: rgba(76, 175, 80, 0.1);
|
||||
border: 1px solid rgba(76, 175, 80, 0.3);
|
||||
border-radius: 10px;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.guide-tips h4 {
|
||||
color: #81c784;
|
||||
font-size: 1.2rem;
|
||||
margin: 0 0 1rem 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.guide-tips ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.guide-tips li {
|
||||
color: #cccccc;
|
||||
padding: 0.5rem 0;
|
||||
padding-left: 1.5rem;
|
||||
position: relative;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.guide-tips li::before {
|
||||
content: "💡";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0.5rem;
|
||||
}
|
||||
|
||||
.guide-tips strong {
|
||||
color: #81c784;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
|
@ -36,6 +36,49 @@
|
|||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Scenario Status Bar */
|
||||
.scenario-status-bar {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: rgba(155, 89, 182, 0.1);
|
||||
border: 1px solid #9b59b6;
|
||||
border-radius: 10px;
|
||||
margin: 1rem;
|
||||
padding: 0.8rem;
|
||||
gap: 2rem;
|
||||
backdrop-filter: blur(5px);
|
||||
}
|
||||
|
||||
.scenario-status-bar .status-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.scenario-status-bar .status-label {
|
||||
color: #9b59b6;
|
||||
font-size: 0.85rem;
|
||||
font-weight: bold;
|
||||
margin-bottom: 0.2rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.scenario-status-bar .status-value {
|
||||
color: #ffffff;
|
||||
font-size: 1.1rem;
|
||||
font-weight: bold;
|
||||
text-shadow: 0 0 10px rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.scenario-status-bar .timer-display {
|
||||
font-family: 'Courier New', monospace;
|
||||
color: #00ff88;
|
||||
text-shadow: 0 0 10px rgba(0, 255, 136, 0.5);
|
||||
}
|
||||
|
||||
.training-controls {
|
||||
background: rgba(155, 89, 182, 0.1);
|
||||
border: 1px solid #9b59b6;
|
||||
|
|
@ -888,6 +931,22 @@
|
|||
<div class="subtitle">Master the Art of Dedicated Gooning</div>
|
||||
</div>
|
||||
|
||||
<!-- Game Status Bar -->
|
||||
<div class="scenario-status-bar" id="scenario-status-bar" style="display: none;">
|
||||
<div class="status-item">
|
||||
<span class="status-label">TIME:</span>
|
||||
<span id="scenario-timer" class="status-value timer-display">00:00</span>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<span class="status-label">XP:</span>
|
||||
<span id="scenario-current-xp" class="status-value">0</span>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<span class="status-label">ACTIVITY:</span>
|
||||
<span id="scenario-activity" class="status-value">Ready</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Library Status -->
|
||||
<div class="library-status" id="libraryStatus">
|
||||
<h3>📚 Library Status</h3>
|
||||
|
|
@ -2288,6 +2347,16 @@
|
|||
window.game.gameState.startTime = Date.now();
|
||||
window.game.gameState.xp = 0;
|
||||
|
||||
// Initialize scenario XP tracking
|
||||
if (window.game.initializeScenarioXp) {
|
||||
window.game.initializeScenarioXp();
|
||||
console.log('📊 Scenario XP system initialized');
|
||||
}
|
||||
|
||||
// Show status bar and start XP tracking
|
||||
showScenarioStatusBar();
|
||||
startScenarioXPTracking();
|
||||
|
||||
// Load the first training task directly
|
||||
if (trainingTasks.length > 0) {
|
||||
console.log('<27> Loading first training task directly...');
|
||||
|
|
@ -2432,6 +2501,13 @@
|
|||
window.game.gameState.isRunning = false;
|
||||
window.game.gameState.isPaused = false;
|
||||
|
||||
// Save scenario XP to player stats before clearing
|
||||
saveScenarioXPToPlayerStats();
|
||||
|
||||
// Hide status bar and stop XP tracking
|
||||
hideScenarioStatusBar();
|
||||
stopScenarioXPTracking();
|
||||
|
||||
// Clear current task
|
||||
window.game.gameState.currentTask = null;
|
||||
window.game.currentTask = null;
|
||||
|
|
@ -3469,6 +3545,136 @@
|
|||
|
||||
document.body.insertAdjacentHTML('beforeend', galleryHtml);
|
||||
}
|
||||
|
||||
// ===== SCENARIO XP STATUS BAR FUNCTIONS =====
|
||||
|
||||
function showScenarioStatusBar() {
|
||||
const statusBar = document.getElementById('scenario-status-bar');
|
||||
if (statusBar) {
|
||||
statusBar.style.display = 'flex';
|
||||
console.log('📊 Scenario status bar displayed');
|
||||
}
|
||||
}
|
||||
|
||||
function hideScenarioStatusBar() {
|
||||
const statusBar = document.getElementById('scenario-status-bar');
|
||||
if (statusBar) {
|
||||
statusBar.style.display = 'none';
|
||||
console.log('📊 Scenario status bar hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function updateScenarioStatusBar() {
|
||||
if (!window.game || !window.game.gameState) return;
|
||||
|
||||
// Update timer
|
||||
const timerElement = document.getElementById('scenario-timer');
|
||||
if (timerElement && window.game.gameState.startTime) {
|
||||
const elapsed = Date.now() - window.game.gameState.startTime;
|
||||
const minutes = Math.floor(elapsed / 60000);
|
||||
const seconds = Math.floor((elapsed % 60000) / 1000);
|
||||
timerElement.textContent = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
// Update XP display
|
||||
const xpElement = document.getElementById('scenario-current-xp');
|
||||
if (xpElement) {
|
||||
const currentXP = getCurrentScenarioXP();
|
||||
xpElement.textContent = currentXP;
|
||||
}
|
||||
|
||||
// Update activity status
|
||||
const activityElement = document.getElementById('scenario-activity');
|
||||
if (activityElement) {
|
||||
activityElement.textContent = getScenarioActivityStatus();
|
||||
}
|
||||
}
|
||||
|
||||
function getCurrentScenarioXP() {
|
||||
if (!window.game || !window.game.gameState || !window.game.gameState.scenarioXp) {
|
||||
return 0;
|
||||
}
|
||||
return window.game.gameState.scenarioXp.total || 0;
|
||||
}
|
||||
|
||||
function getScenarioActivityStatus() {
|
||||
if (!window.game || !window.game.gameState || !window.game.gameState.scenarioTracking) {
|
||||
return 'Ready';
|
||||
}
|
||||
|
||||
const tracking = window.game.gameState.scenarioTracking;
|
||||
if (tracking.isInWebcamActivity) {
|
||||
return 'Webcam Active';
|
||||
} else if (tracking.isInFocusActivity) {
|
||||
return 'Focus Task';
|
||||
} else if (window.game.gameState.isRunning) {
|
||||
return 'In Progress';
|
||||
} else {
|
||||
return 'Ready';
|
||||
}
|
||||
}
|
||||
|
||||
function startScenarioXPTracking() {
|
||||
// Clear any existing interval
|
||||
if (window.scenarioXPInterval) {
|
||||
clearInterval(window.scenarioXPInterval);
|
||||
}
|
||||
|
||||
// Update status bar every second and trigger XP calculations
|
||||
window.scenarioXPInterval = setInterval(() => {
|
||||
// Manually trigger scenario XP updates (since timer might be overridden)
|
||||
if (window.game && window.game.gameState && window.game.gameState.isRunning) {
|
||||
if (window.game.updateScenarioTimeBasedXp) {
|
||||
window.game.updateScenarioTimeBasedXp();
|
||||
}
|
||||
if (window.game.updateScenarioFocusXp) {
|
||||
window.game.updateScenarioFocusXp();
|
||||
}
|
||||
if (window.game.updateScenarioWebcamXp) {
|
||||
window.game.updateScenarioWebcamXp();
|
||||
}
|
||||
}
|
||||
|
||||
updateScenarioStatusBar();
|
||||
}, 1000);
|
||||
|
||||
console.log('📊 Started scenario XP tracking');
|
||||
}
|
||||
|
||||
function stopScenarioXPTracking() {
|
||||
if (window.scenarioXPInterval) {
|
||||
clearInterval(window.scenarioXPInterval);
|
||||
window.scenarioXPInterval = null;
|
||||
console.log('📊 Stopped scenario XP tracking');
|
||||
}
|
||||
}
|
||||
|
||||
function saveScenarioXPToPlayerStats() {
|
||||
if (!window.game || !window.game.gameState || !window.game.gameState.scenarioXp) {
|
||||
console.log('📊 No scenario XP to save');
|
||||
return;
|
||||
}
|
||||
|
||||
const totalScenarioXP = window.game.gameState.scenarioXp.total || 0;
|
||||
|
||||
if (totalScenarioXP > 0) {
|
||||
// Award XP to player stats system
|
||||
if (window.playerStats) {
|
||||
window.playerStats.awardXP(totalScenarioXP, 'scenario');
|
||||
console.log(`📊 Awarded ${totalScenarioXP} scenario XP to player stats`);
|
||||
}
|
||||
|
||||
// Also add to overall XP counter (for compatibility with existing system)
|
||||
if (window.game.dataManager) {
|
||||
const overallXp = window.game.dataManager.get('overallXp') || 0;
|
||||
const newOverallXp = overallXp + totalScenarioXP;
|
||||
window.game.dataManager.set('overallXp', newOverallXp);
|
||||
console.log(`📊 Added ${totalScenarioXP} XP to overall counter: ${overallXp} → ${newOverallXp}`);
|
||||
}
|
||||
} else {
|
||||
console.log('📊 No scenario XP earned this session');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in New Issue