From c40ed278e067d2002e65a9756aac0e8690f3aa8e Mon Sep 17 00:00:00 2001 From: dilgenfritz Date: Tue, 18 Nov 2025 22:42:32 -0600 Subject: [PATCH] Major project reorganization: Move docs and scripts to dedicated directories Documentation Organization: - Move all documentation (except main README.md) to docs/ directory - Create docs/README.md as navigation index for all documentation - Update file structure references in main README.md Script Organization: - Move all scripts to scripts/ directory (setup.bat, setup.sh, Start-webgame.bat, etc.) - Update script references in documentation - Update distribution scripts to use new paths Hypno Gallery Implementation: - Implement recursive image directory scanning in main process - Add readImageDirectoryRecursive IPC handler for proper Node.js fs access - Clean up complex fallback code in hypno-gallery.html - Create comprehensive HYPNO-GALLERY-README.md documentation File Structure Improvements: - Clean root directory with only essential application files - Organized subdirectories: src/, docs/, scripts/, images/, audio/, assets/ - Professional project structure for better maintainability This reorganization improves project navigation, separates concerns properly, and provides a solid foundation for future development. --- README.md | 12 + .../BETA_FEEDBACK_FORM.md | 0 .../DISTRIBUTION_SUMMARY.md | 0 docs/HYPNO-GALLERY-README.md | 325 ++++ .../INSTALLATION_GUIDE.md | 13 +- README-ARCHIVED.md => docs/README-ARCHIVED.md | 0 README-DESKTOP.md => docs/README-DESKTOP.md | 0 docs/README.md | 67 + TESTER_GUIDE.md => docs/TESTER_GUIDE.md | 0 hypno-gallery.html | 1618 ++++++++++++++++- index.html | 36 + porn-cinema.html | 308 +++- quick-play.html | 12 + .../Start-webgame.bat | 0 .../create-distribution.bat | 6 +- .../create-distribution.sh | 6 +- setup.bat => scripts/setup.bat | 4 +- setup.sh => scripts/setup.sh | 0 src/core/main.js | 55 + src/core/preload.js | 1 + src/features/stats/playerStats.js | 214 ++- training-academy.html | 657 ++++++- user-profile.html | 134 +- 23 files changed, 3283 insertions(+), 185 deletions(-) rename BETA_FEEDBACK_FORM.md => docs/BETA_FEEDBACK_FORM.md (100%) rename DISTRIBUTION_SUMMARY.md => docs/DISTRIBUTION_SUMMARY.md (100%) create mode 100644 docs/HYPNO-GALLERY-README.md rename INSTALLATION_GUIDE.md => docs/INSTALLATION_GUIDE.md (93%) rename README-ARCHIVED.md => docs/README-ARCHIVED.md (100%) rename README-DESKTOP.md => docs/README-DESKTOP.md (100%) create mode 100644 docs/README.md rename TESTER_GUIDE.md => docs/TESTER_GUIDE.md (100%) rename Start-webgame.bat => scripts/Start-webgame.bat (100%) rename create-distribution.bat => scripts/create-distribution.bat (98%) rename create-distribution.sh => scripts/create-distribution.sh (97%) rename setup.bat => scripts/setup.bat (96%) rename setup.sh => scripts/setup.sh (100%) diff --git a/README.md b/README.md index 819d1ca..2582bc0 100644 --- a/README.md +++ b/README.md @@ -898,8 +898,20 @@ webGame/ ├── quick-play.html # Quick Play game mode ├── training-academy.html # Training Academy mode ├── porn-cinema.html # Video player system +├── hypno-gallery.html # Immersive slideshow system ├── player-stats.html # Statistics dashboard ├── user-profile.html # Profile and achievements +├── scripts/ # Installation and launch scripts +│ ├── setup.bat # Windows installer +│ ├── setup.sh # Mac/Linux installer +│ ├── Start-webgame.bat # Windows launcher +│ └── create-distribution.* # Distribution builders +├── docs/ # Documentation and guides +│ ├── HYPNO-GALLERY-README.md # Hypno Gallery documentation +│ ├── INSTALLATION_GUIDE.md # Setup instructions +│ ├── TESTER_GUIDE.md # Testing instructions +│ ├── ROADMAP.md # Development roadmap +│ └── archive/ # Archived documentation ├── src/ │ ├── core/ # Game engine and state management │ ├── data/modes/ # Game mode configurations diff --git a/BETA_FEEDBACK_FORM.md b/docs/BETA_FEEDBACK_FORM.md similarity index 100% rename from BETA_FEEDBACK_FORM.md rename to docs/BETA_FEEDBACK_FORM.md diff --git a/DISTRIBUTION_SUMMARY.md b/docs/DISTRIBUTION_SUMMARY.md similarity index 100% rename from DISTRIBUTION_SUMMARY.md rename to docs/DISTRIBUTION_SUMMARY.md diff --git a/docs/HYPNO-GALLERY-README.md b/docs/HYPNO-GALLERY-README.md new file mode 100644 index 0000000..4166345 --- /dev/null +++ b/docs/HYPNO-GALLERY-README.md @@ -0,0 +1,325 @@ +# 🌀 Hypno Gallery - Immersive Slideshow System + +## Overview + +The Hypno Gallery is an advanced, feature-rich slideshow application designed for creating immersive visual experiences. It supports recursive directory scanning, multiple slideshow management, customizable timing patterns, and sophisticated visual effects. + +## Current Features + +### 📋 Slideshow Management +- **Multiple Slideshow Support**: Create, save, load, and manage multiple slideshow configurations +- **Slideshow Playlists**: Each slideshow can have its own directories, settings, and configurations +- **Persistent Storage**: Slideshows are automatically saved to localStorage and persist between sessions +- **Duplicate & Share**: Clone existing slideshows for quick customization +- **Import/Export**: Load existing slideshows or create new ones + +### 📁 Directory Management +- **Recursive Directory Scanning**: Automatically finds all images in directories and subdirectories +- **Multiple Directory Support**: Add multiple image directories to a single slideshow +- **Space-Friendly Path Handling**: Properly handles directory names with spaces +- **Real-time Directory Updates**: Add/remove directories from active slideshows +- **Cross-Platform Path Support**: Works with Windows, macOS, and Linux file paths + +### ⏱️ Advanced Timing System +- **Constant Duration**: Fixed timing between image transitions +- **Random Range**: Variable timing within a specified min/max range +- **Wave Pattern**: Sinusoidal timing that creates rhythmic acceleration/deceleration +- **Customizable Parameters**: Fine-tune wave amplitude, frequency, and base timing + +### 🖼️ Image Handling +- **Multiple Format Support**: JPG, PNG, GIF, BMP, WebP, SVG, TIFF, ICO +- **Smart Image Ordering**: Random shuffle, sequential, or reverse order +- **Flexible Display Modes**: + - **Contain**: Fit entire image within screen bounds + - **Cover**: Fill screen, cropping if necessary + - **Stretch**: Fill screen, distorting aspect ratio if needed +- **Memory Efficient**: Loads images on-demand with preloading optimization + +### ✨ Transition Effects +- **Fade Transitions**: Smooth cross-fade between images +- **Customizable Duration**: Adjust transition timing (100ms - 2s) +- **Instant Switching**: Option for no transitions for rapid changes +- **GPU Accelerated**: Hardware-accelerated CSS transitions for smooth performance + +### 🎨 Background Customization +- **Solid Colors**: Customizable solid background colors +- **Gradient Backgrounds**: Linear gradients with multiple direction options +- **Blurred Image Backgrounds**: Dynamic blurred version of current image +- **Opacity Control**: Adjustable background opacity and blur intensity +- **Real-time Preview**: Live preview of background changes + +### 🎮 Interactive Controls +- **Fullscreen Experience**: Immersive fullscreen slideshow mode +- **Mouse Controls**: Auto-hiding UI elements, show on mouse movement +- **Keyboard Shortcuts**: Space to pause/resume, arrow keys for navigation, Escape to exit +- **Progress Indicator**: Optional progress bar showing slideshow advancement +- **Real-time Information**: Current image counter, timing info, slideshow status + +### 📷 Captured Photo Integration +- **Webcam Integration**: Include photos captured from webcam sessions +- **Mixed Media**: Combine directory images with captured photos +- **Flexible Inclusion**: Toggle captured photos on/off per slideshow + +### 💾 Data Persistence +- **Auto-Save Settings**: All settings automatically saved to localStorage +- **Session Recovery**: Resume slideshow configurations after restart +- **Export/Import Ready**: Prepared for future backup/restore functionality + +## File Structure + +``` +hypno-gallery.html # Main application file +src/ +├── core/ +│ ├── main.js # Electron main process (includes recursive image scanner) +│ └── preload.js # IPC bridge (exposes readImageDirectoryRecursive) +├── styles/ +│ ├── styles.css # Base application styles +│ └── styles-dark-edgy.css # Dark theme styles +└── utils/ + └── desktop-file-manager.js # File system utilities +``` + +## Usage Guide + +### Getting Started +1. **Launch Application**: Open `hypno-gallery.html` in the electron app +2. **Create Slideshow**: Click "🎬 New" to create your first slideshow +3. **Add Directories**: Use "📁 Add Directory" to select image folders +4. **Configure Settings**: Customize timing, transitions, and visual effects +5. **Start Experience**: Click "🌀 Start Hypno Gallery" to begin + +### Slideshow Management +```javascript +// Creating a new slideshow +1. Click "🎬 New" button +2. Enter slideshow name +3. Click "✅ Create" + +// Loading existing slideshow +1. Click "📁 Load" button +2. Select from dropdown +3. Click "✅ Load" + +// Adding directories to slideshow +1. Load or create a slideshow +2. Click "📁 Add Directory" +3. Select folder containing images +4. Directory is recursively scanned and added +``` + +### Advanced Configuration + +#### Timing Patterns +- **Constant**: `duration = 3000ms` (3 seconds per image) +- **Random**: `min = 1500ms, max = 5000ms` (random between 1.5-5 seconds) +- **Wave**: `waveMin = 1000ms, waveMax = 5000ms, rate = 100` (sinusoidal pattern) + +#### Transition Types +- **Fade**: Smooth cross-fade transition with customizable duration +- **None**: Instant image switching for rapid-fire effects + +#### Background Modes +- **Solid**: Single color background +- **Gradient**: Linear gradient from color A to color B +- **Blurred**: Dynamically blurred version of current image + +## Technical Implementation + +### Recursive Directory Scanning +The application uses a main process function for reliable directory traversal: + +```javascript +// Main process implementation (main.js) +ipcMain.handle('read-image-directory-recursive', async (event, dirPath) => { + // Recursive scanning with fs.readdir and fs.stat + // Handles subdirectories and file filtering + // Returns array of image file objects with metadata +}); + +// Renderer usage (hypno-gallery.html) +const images = await window.electronAPI.readImageDirectoryRecursive(directoryPath); +``` + +### Image Loading Pipeline +1. **Directory Scanning**: Recursively find all image files +2. **Metadata Extraction**: File size, path, format detection +3. **Preprocessing**: Path normalization, title generation +4. **Memory Management**: On-demand loading with preloading optimization + +### Performance Optimizations +- **GPU Acceleration**: CSS transitions and transforms +- **Memory Efficiency**: Image preloading with memory limits +- **File System Caching**: Reduced redundant directory scans +- **Event Throttling**: Optimized mouse movement and resize handlers + +## Future Implementations + +### 🎯 Planned Features (Near-term) + +#### 📱 Grid Functionality +``` +Multi-Slideshow Grid System: +- Display multiple slideshows simultaneously +- 2x2, 3x3, or custom grid layouts +- Independent timing and settings per grid cell +- Synchronized or independent playback modes +- Grid-specific transition effects (wave patterns across cells) +``` + +#### 📝 Text Overlay System +``` +Dynamic Text Integration: +- Customizable text overlays on images +- Multiple font options and styles +- Position presets (center, corners, custom) +- Text animation effects (fade in/out, scroll, pulse) +- Per-slideshow text configuration +- Image-specific text metadata support +``` + +#### 🎵 Audio Integration +``` +Synchronized Audio Experience: +- Background music support +- Audio track per slideshow +- Beat-synchronized image timing +- Audio visualization elements +- Volume and fade controls +``` + +#### 🎨 Advanced Visual Effects +``` +Enhanced Visual Processing: +- Image filters (sepia, blur, contrast, saturation) +- Color correction and enhancement +- Edge detection and artistic effects +- Real-time image transformation +- GPU-accelerated filter pipeline +``` + +### 🚀 Advanced Features (Long-term) + +#### 🌐 Network Capabilities +``` +Cloud and Network Features: +- Cloud storage integration +- Network directory mounting +- Remote slideshow control +- Multi-device synchronization +- Collaborative slideshow creation +``` + +#### 🤖 AI-Enhanced Features +``` +Intelligent Automation: +- AI-powered image categorization +- Smart slideshow generation based on content +- Mood-based timing adjustment +- Automatic image enhancement +- Content-aware transitions +``` + +#### 📊 Analytics and Insights +``` +Usage Analytics: +- Slideshow viewing statistics +- Image popularity tracking +- Timing pattern analysis +- User interaction heatmaps +- Performance metrics dashboard +``` + +#### 🎮 Interactive Elements +``` +Enhanced Interactivity: +- Touch gesture support +- Voice control integration +- Eye-tracking compatibility +- Biometric feedback integration +- Interactive hotspots on images +``` + +## Configuration Examples + +### Basic Slideshow Setup +```javascript +// Example slideshow configuration +const slideshowConfig = { + name: "Nature Photography", + directories: [ + "E:/Photos/Nature", + "E:/Photos/Landscapes" + ], + settings: { + timingMode: "random", + minDuration: 2000, + maxDuration: 6000, + transitionType: "fade", + transitionDuration: 800, + backgroundType: "blurred", + fitMode: "cover" + }, + includeCapturedPhotos: false +}; +``` + +### Advanced Wave Timing +```javascript +// Wave timing for hypnotic effects +const hypnoConfig = { + timingMode: "wave", + waveMin: 500, // Fastest: 0.5 seconds + waveMax: 4000, // Slowest: 4 seconds + waveRate: 50, // Wave frequency + transitionType: "fade", + transitionDuration: 1200 +}; +``` + +## API Reference + +### Core Functions +- `loadImagesFromDirectory(path)`: Recursively scan directory for images +- `startSlideshow()`: Initialize and begin slideshow playback +- `pauseSlideshow()`: Pause/resume slideshow +- `stopSlideshow()`: Stop and exit slideshow mode +- `createSlideshow(name)`: Create new slideshow configuration +- `loadSlideshow(name)`: Load existing slideshow +- `saveSlideshow()`: Persist current slideshow settings + +### Event Handlers +- `onImageLoad`: Triggered when new image is loaded +- `onTimingChange`: Fired when timing pattern changes +- `onTransitionComplete`: Called after transition finishes +- `onSlideshowEnd`: Triggered when slideshow completes loop + +## Browser Compatibility +- **Electron**: Full feature support (recommended) +- **Chrome/Edge**: Core features supported +- **Firefox**: Core features supported +- **Safari**: Limited support (no file system access) + +## Performance Notes +- **Recommended**: 500+ images per slideshow +- **Memory Usage**: ~50MB for 1000 images +- **File Formats**: All modern image formats supported +- **Directory Depth**: No practical limit on subdirectory scanning + +## Troubleshooting + +### Common Issues +1. **Images not loading**: Check directory permissions and file formats +2. **Slideshow won't start**: Ensure at least one directory is added with images +3. **Transitions choppy**: Reduce image size or transition duration +4. **Directory not found**: Verify path exists and contains supported image files + +### Debug Mode +Enable debug logging by opening browser console - all operations are logged with detailed information. + +--- + +**Version**: 2.0.0 +**Last Updated**: November 2025 +**Platform**: Electron Desktop Application +**License**: Proprietary \ No newline at end of file diff --git a/INSTALLATION_GUIDE.md b/docs/INSTALLATION_GUIDE.md similarity index 93% rename from INSTALLATION_GUIDE.md rename to docs/INSTALLATION_GUIDE.md index 85121df..23de247 100644 --- a/INSTALLATION_GUIDE.md +++ b/docs/INSTALLATION_GUIDE.md @@ -5,13 +5,13 @@ ### **Method 1: Automated Setup (Recommended)** #### **Windows Users:** -1. Double-click `setup.bat` +1. Double-click `scripts/setup.bat` 2. Follow the prompts -3. Launch with `Start-webgame.bat` +3. Launch with `scripts/Start-webgame.bat` #### **Mac/Linux Users:** 1. Open terminal in this folder -2. Run: `chmod +x setup.sh && ./setup.sh` +2. Run: `chmod +x scripts/setup.sh && ./scripts/setup.sh` 3. Launch with: `npm start` ### **Method 2: Manual Setup** @@ -103,9 +103,10 @@ Gooner-Training-Academy/ ├── 📄 TESTER_GUIDE.md # Testing instructions ├── 📄 INSTALLATION_GUIDE.md # This file ├── 📄 README.md # Full documentation -├── 🔧 setup.bat # Windows installer -├── 🔧 setup.sh # Mac/Linux installer -├── 🚀 Start-webgame.bat # Windows launcher +├── 📁 scripts/ # Installation and launch scripts +│ ├── setup.bat # Windows installer +│ ├── setup.sh # Mac/Linux installer +│ └── Start-webgame.bat # Windows launcher ├── 📄 package.json # Dependencies ├── 🌐 index.html # Main application ├── 📁 src/ # Application code diff --git a/README-ARCHIVED.md b/docs/README-ARCHIVED.md similarity index 100% rename from README-ARCHIVED.md rename to docs/README-ARCHIVED.md diff --git a/README-DESKTOP.md b/docs/README-DESKTOP.md similarity index 100% rename from README-DESKTOP.md rename to docs/README-DESKTOP.md diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..4dac662 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,67 @@ +# 📚 Documentation Index + +Welcome to the Gooner Training Academy documentation! This directory contains all guides, references, and technical documentation for the application. + +## 📖 Getting Started + +| Document | Purpose | Target Audience | +|----------|---------|----------------| +| [INSTALLATION_GUIDE.md](INSTALLATION_GUIDE.md) | Setup instructions and system requirements | New users, testers | +| [TESTER_GUIDE.md](TESTER_GUIDE.md) | Testing procedures and focus areas | Beta testers, QA | +| [README-DESKTOP.md](README-DESKTOP.md) | Desktop-specific features and setup | Desktop users | + +## 🎮 Feature Documentation + +| Document | Purpose | Target Audience | +|----------|---------|----------------| +| [HYPNO-GALLERY-README.md](HYPNO-GALLERY-README.md) | Complete Hypno Gallery feature guide | Users, developers | + +## 📋 Project Management + +| Document | Purpose | Target Audience | +|----------|---------|----------------| +| [ROADMAP.md](ROADMAP.md) | Development roadmap and future plans | Stakeholders, developers | +| [tasks.md](tasks.md) | Task lists and development todos | Development team | +| [organized-tasks.md](organized-tasks.md) | Structured task organization | Project managers | +| [levels.md](levels.md) | Game progression and level design | Game designers | +| [consequences.md](consequences.md) | Punishment system documentation | Content designers | + +## 📦 Distribution & Release + +| Document | Purpose | Target Audience | +|----------|---------|----------------| +| [DISTRIBUTION_SUMMARY.md](DISTRIBUTION_SUMMARY.md) | Release packaging information | Release managers | +| [BETA_FEEDBACK_FORM.md](BETA_FEEDBACK_FORM.md) | Beta testing feedback template | Beta testers | + +## 🗃️ Archives + +The [archive/](archive/) directory contains historical documentation and deprecated guides: + +| Document | Status | Notes | +|----------|--------|-------| +| [sprint1.md](archive/sprint1.md) | Archived | First development sprint documentation | +| [note.md](archive/note.md) | Archived | Development notes | +| [GAME_MODE_CLEANUP_PLAN.md](archive/GAME_MODE_CLEANUP_PLAN.md) | Archived | Game mode refactoring plan | +| [MEDIA_LIBRARY_CLEANUP_PLAN.md](archive/MEDIA_LIBRARY_CLEANUP_PLAN.md) | Archived | Media system refactoring plan | +| [DATA_LOSS_INCIDENT.md](archive/DATA_LOSS_INCIDENT.md) | Archived | Historical data loss incident report | + +## 🚀 Quick Navigation + +### For New Users: +1. Start with [INSTALLATION_GUIDE.md](INSTALLATION_GUIDE.md) +2. Review [TESTER_GUIDE.md](TESTER_GUIDE.md) if participating in beta testing +3. Check feature-specific documentation as needed + +### For Developers: +1. Review [ROADMAP.md](ROADMAP.md) for project direction +2. Check [tasks.md](tasks.md) for current development priorities +3. Consult feature documentation for implementation details + +### For Testers: +1. Follow [INSTALLATION_GUIDE.md](INSTALLATION_GUIDE.md) for setup +2. Use [TESTER_GUIDE.md](TESTER_GUIDE.md) for testing procedures +3. Submit feedback via [BETA_FEEDBACK_FORM.md](BETA_FEEDBACK_FORM.md) + +--- + +**Need help?** Check the main [README.md](../README.md) in the root directory for comprehensive application documentation and support resources. \ No newline at end of file diff --git a/TESTER_GUIDE.md b/docs/TESTER_GUIDE.md similarity index 100% rename from TESTER_GUIDE.md rename to docs/TESTER_GUIDE.md diff --git a/hypno-gallery.html b/hypno-gallery.html index 1f68109..9ce0cd6 100644 --- a/hypno-gallery.html +++ b/hypno-gallery.html @@ -226,6 +226,19 @@ background: rgba(0, 0, 0, 0.7); padding: 1rem; border-radius: 10px; + opacity: 0; + transition: opacity 0.3s ease-in-out; + pointer-events: none; + } + + .slideshow-controls.visible { + opacity: 1; + pointer-events: auto; + } + + .slideshow-container:hover .slideshow-controls { + opacity: 1; + pointer-events: auto; } .slideshow-controls button { @@ -251,6 +264,19 @@ padding: 1rem; border-radius: 10px; font-family: 'Courier New', monospace; + opacity: 0; + transition: opacity 0.3s ease-in-out; + pointer-events: none; + } + + .slideshow-info.visible { + opacity: 1; + pointer-events: auto; + } + + .slideshow-container:hover .slideshow-info { + opacity: 1; + pointer-events: auto; } .navigation { @@ -269,6 +295,16 @@ height: 4px; background: rgba(255, 255, 255, 0.2); display: none; + opacity: 0; + transition: opacity 0.3s ease-in-out; + } + + .progress-container.visible { + opacity: 1; + } + + .slideshow-container:hover .progress-container { + opacity: 1; } .progress-bar { @@ -404,6 +440,99 @@ + +
+
+

📋 Slideshow Playlists

+
+ +
+
+ Slideshow Management: +
+ + +
+
+
+ + +
+ + +
+ + + +
+ + +
+
+
+
+ + +
+
+ Slideshow Directories: +
+
+
Select a slideshow to manage directories
+
+
+
+ +
+ + + + +
+
+ + + + +
+

🎬 Current Session Settings

+
+
📁 Sources: Not configured
+
⏱️ Timing: Not configured
+
✨ Transitions: Not configured
+
+
+
+
+
+
@@ -468,34 +597,7 @@
- -
-

📁 Directory Selection

-
-
- Image Sources: -
-
-
- - 📷 Include Captured Photos -
-
- -
-
- - - 0 directories selected - -
-
-
-
-
-
+
@@ -539,6 +641,76 @@
+ +
+

🎨 Background Settings

+
+
+ Background Type: +
+ +
+
+ + +
+ Background Color: +
+ + +
+
+ + + + + + +
+
+

✨ Transition Settings

@@ -624,9 +796,20 @@ transitionType: 'fade', transitionDuration: 500, selectedDirectories: [], // Array of selected directory paths - includeCapturedPhotos: true + includeCapturedPhotos: true, + backgroundType: 'solid', + backgroundColor: '#000000', + gradientColor1: '#000000', + gradientColor2: '#1a1a1a', + gradientDirection: 'to bottom', + blurAmount: 20, + blurOpacity: 70 }; + // Slideshow Management Variables + let savedSlideshows = {}; + let currentSlideshowName = null; + let slideshow = { images: [], currentIndex: 0, @@ -678,13 +861,12 @@ } if (savedSettings.includeCapturedPhotos !== undefined) { currentSettings.includeCapturedPhotos = savedSettings.includeCapturedPhotos; - document.getElementById('includeCapturedPhotos').checked = savedSettings.includeCapturedPhotos; } console.log('💾 Loaded directory preferences:', currentSettings.selectedDirectories); // Populate directory list - populateDirectoryList(linkedDirectories); + // populateDirectoryList(linkedDirectories); // UI element removed } catch (error) { console.error('❌ Error initializing directory selection:', error); @@ -694,6 +876,10 @@ // Populate Directory List function populateDirectoryList(directories) { const directoriesList = document.getElementById('linkedDirectoriesList'); + if (!directoriesList) { + console.log('📁 Directory list UI element not found (removed), skipping population'); + return; + } if (directories.length === 0) { directoriesList.innerHTML = '
No image directories linked. Use Library to add directories.
'; @@ -1131,27 +1317,72 @@ updateTransitionSettings(e.target.value); }); - // Include captured photos checkbox - document.getElementById('includeCapturedPhotos').addEventListener('change', (e) => { - currentSettings.includeCapturedPhotos = e.target.checked; + // Include captured photos checkbox (removed from UI) + const capturedPhotosCheckbox = document.getElementById('includeCapturedPhotos'); + if (capturedPhotosCheckbox) { + capturedPhotosCheckbox.addEventListener('change', (e) => { + currentSettings.includeCapturedPhotos = e.target.checked; + saveHypnoGallerySettings(); + console.log('📷 Captured photos inclusion:', e.target.checked); + + // Refresh image library + setTimeout(() => { + initializeImageLibrary(); + }, 100); + }); + } + + // Refresh directories button (removed from UI) + const refreshButton = document.getElementById('refreshDirectories'); + if (refreshButton) { + refreshButton.addEventListener('click', async () => { + console.log('🔄 Refreshing directories...'); + await initializeDirectorySelection(); + await initializeImageLibrary(); + }); + } + + // Background settings + document.getElementById('backgroundType').addEventListener('change', (e) => { + currentSettings.backgroundType = e.target.value; + updateBackgroundOptions(e.target.value); + applyBackground(); saveHypnoGallerySettings(); - console.log('📷 Captured photos inclusion:', e.target.checked); - - // Refresh image library - setTimeout(() => { - initializeImageLibrary(); - }, 100); }); - // Refresh directories button - document.getElementById('refreshDirectories').addEventListener('click', async () => { - console.log('🔄 Refreshing directories...'); - await initializeDirectorySelection(); - await initializeImageLibrary(); + // Color pickers and hex inputs + setupColorControl('backgroundColor'); + setupColorControl('gradientColor1'); + setupColorControl('gradientColor2'); + + document.getElementById('gradientDirection').addEventListener('change', (e) => { + currentSettings.gradientDirection = e.target.value; + applyBackground(); + saveHypnoGallerySettings(); + }); + + // Blur controls + document.getElementById('blurAmount').addEventListener('input', (e) => { + const value = parseInt(e.target.value); + currentSettings.blurAmount = value; + document.getElementById('blurAmountValue').textContent = value + 'px'; + applyBackground(); + saveHypnoGallerySettings(); + }); + + document.getElementById('blurOpacity').addEventListener('input', (e) => { + const value = parseInt(e.target.value); + currentSettings.blurOpacity = value; + document.getElementById('blurOpacityValue').textContent = value + '%'; + applyBackground(); + saveHypnoGallerySettings(); }); // Keyboard controls document.addEventListener('keydown', handleKeyPress); + + // Mouse movement controls for auto-hiding UI + setupMouseControls(); } // Update timing settings display @@ -1188,16 +1419,19 @@ } // Start slideshow - function startSlideshow() { - if (imageLibrary.length === 0) { - alert('No images available for slideshow. Please add images to your library first.'); + async function startSlideshow() { + console.log('🌀 Starting Hypno Gallery slideshow...'); + + // Get slideshow-specific images + const slideshowImages = await getSlideshowImages(); + + if (slideshowImages.length === 0) { + alert('No images available for slideshow. Please configure slideshow directories or add images to your library first.'); return; } - console.log('🌀 Starting Hypno Gallery slideshow...'); - // Prepare image array - slideshow.images = [...imageLibrary]; + slideshow.images = [...slideshowImages]; // Apply image ordering switch (currentSettings.imageOrder) { @@ -1266,6 +1500,14 @@ imgElement.src = `file://${currentImage.fullPath}`; } + // Update background for blurred mode + if (currentSettings.backgroundType === 'blurred') { + // Wait for image to load before updating background + imgElement.onload = () => { + setTimeout(() => applyBackground(), 100); // Small delay to ensure image is rendered + }; + } + // Apply fit mode switch (currentSettings.fitMode) { case 'contain': @@ -1384,6 +1626,11 @@ function nextImage() { if (!slideshow.isPlaying) return; + // Cancel current timing to restart timer + if (slideshow.nextTimeout) { + clearTimeout(slideshow.nextTimeout); + } + slideshow.currentIndex++; loadCurrentImage(); scheduleNextImage(); @@ -1458,29 +1705,56 @@ // Handle keyboard input function handleKeyPress(event) { - if (!slideshow.isPlaying) return; + // Allow keyboard controls even when slideshow is paused + if (!slideshow.isPlaying && !slideshow.isPaused) return; switch (event.key) { case ' ': case 'p': + case 'k': // Video player convention event.preventDefault(); pauseSlideshow(); break; case 'ArrowRight': case 'n': + case 'l': // Right arrow alternative + case 'j': // Vim-like forward event.preventDefault(); nextImage(); break; case 'ArrowLeft': case 'b': + case 'h': // Left arrow alternative + case 'k': // Vim-like backward (if not pausing) event.preventDefault(); - previousImage(); + if (event.key === 'k' && slideshow.isPlaying) { + pauseSlideshow(); // k pauses when playing + } else { + previousImage(); + } break; case 'Escape': case 'q': + case 'x': event.preventDefault(); stopSlideshow(); break; + case 'f': + case 'F11': + event.preventDefault(); + toggleFullscreen(); + break; + case 'r': + event.preventDefault(); + // Restart slideshow from beginning + slideshow.currentIndex = 0; + displayCurrentImage(); + break; + case '?': + case 'h': + event.preventDefault(); + showKeyboardHelp(); + break; } } @@ -1492,6 +1766,126 @@ } } + // Mouse control setup for auto-hiding UI + let mouseTimeout; + let controlsVisible = false; + + function setupMouseControls() { + const slideshowContainer = document.getElementById('slideshowContainer'); + if (!slideshowContainer) return; + + // Show controls on mouse movement + slideshowContainer.addEventListener('mousemove', () => { + if (!slideshow.isPlaying && !slideshow.isPaused) return; + + showControls(); + + // Clear existing timeout + if (mouseTimeout) { + clearTimeout(mouseTimeout); + } + + // Hide controls after 3 seconds of no movement + mouseTimeout = setTimeout(() => { + hideControls(); + }, 3000); + }); + + // Keep controls visible when hovering over them + const controls = slideshowContainer.querySelector('.slideshow-controls'); + const info = slideshowContainer.querySelector('.slideshow-info'); + + [controls, info].forEach(element => { + if (element) { + element.addEventListener('mouseenter', () => { + if (mouseTimeout) { + clearTimeout(mouseTimeout); + } + showControls(); + }); + + element.addEventListener('mouseleave', () => { + mouseTimeout = setTimeout(() => { + hideControls(); + }, 1000); + }); + } + }); + + // Controls start hidden and only show on mouse hover + // No initial hideControls() call needed as CSS handles this + } + + function showControls() { + controlsVisible = true; + const slideshowContainer = document.getElementById('slideshowContainer'); + if (!slideshowContainer) return; + + const controls = slideshowContainer.querySelector('.slideshow-controls'); + const info = slideshowContainer.querySelector('.slideshow-info'); + const progress = slideshowContainer.querySelector('.progress-container'); + + if (controls) controls.classList.add('visible'); + if (info) info.classList.add('visible'); + if (progress && progress.style.display !== 'none') progress.classList.add('visible'); + } + + function hideControls() { + controlsVisible = false; + const slideshowContainer = document.getElementById('slideshowContainer'); + if (!slideshowContainer) return; + + const controls = slideshowContainer.querySelector('.slideshow-controls'); + const info = slideshowContainer.querySelector('.slideshow-info'); + const progress = slideshowContainer.querySelector('.progress-container'); + + if (controls) controls.classList.remove('visible'); + if (info) info.classList.remove('visible'); + if (progress) progress.classList.remove('visible'); + } + + function showControlsTemporarily() { + // Keyboard controls no longer show UI - only mouse hover does + // This function kept for potential future use + return; + } + + function toggleFullscreen() { + if (!document.fullscreenElement) { + document.documentElement.requestFullscreen().catch(err => { + console.log('Error attempting to enable fullscreen:', err); + }); + } else { + document.exitFullscreen().catch(err => { + console.log('Error attempting to exit fullscreen:', err); + }); + } + } + + function showKeyboardHelp() { + const helpText = ` +🎮 KEYBOARD CONTROLS: + +▶️ PLAYBACK: + Space, P, K - Pause/Resume + →, N, L, J - Next image + ←, B, H - Previous image + R - Restart from beginning + +🎯 NAVIGATION: + Esc, Q, X - Stop slideshow + F, F11 - Toggle fullscreen + +❓ HELP: + ?, H - Show this help + +ℹ️ Controls auto-hide after 3 seconds of no mouse movement. + Move mouse or use keyboard to show them again. + `; + + alert(helpText); + } + // Show image gallery (for testing/preview) function showImageGallery() { if (imageLibrary.length === 0) { @@ -1523,10 +1917,1126 @@ document.body.insertAdjacentHTML('beforeend', galleryHtml); } + // Slideshow Management Functions + function loadSavedSlideshows() { + try { + const saved = localStorage.getItem('hypnoGallerySlideshows'); + if (saved) { + savedSlideshows = JSON.parse(saved); + console.log('📋 Loaded slideshows:', Object.keys(savedSlideshows).length); + } else { + savedSlideshows = {}; + console.log('📋 No saved slideshows found, initialized empty object'); + } + updateSlideshowsList(); + } catch (error) { + console.error('❌ Error loading slideshows:', error); + savedSlideshows = {}; + } + } + + function saveSlideshowsToStorage() { + try { + localStorage.setItem('hypnoGallerySlideshows', JSON.stringify(savedSlideshows)); + console.log('💾 Slideshows saved to storage'); + } catch (error) { + console.error('❌ Error saving slideshows:', error); + } + } + + function updateSlideshowsList() { + // This function is kept for compatibility but no longer manages dropdown + console.log('📋 Slideshows available:', Object.keys(savedSlideshows || {}).length); + + // Update slideshow details if current slideshow is loaded + updateSlideshowDetails(); + } + + function updateSlideshowDetails() { + const details = document.getElementById('slideshowDetails'); + const directoryList = document.getElementById('slideshowDirectoryList'); + const addBtn = document.getElementById('addDirectoryBtn'); + const removeBtn = document.getElementById('removeDirectoryBtn'); + const testBtn = document.getElementById('testDirectoriesBtn'); + + if (currentSlideshowName && savedSlideshows[currentSlideshowName]) { + const slideshow = savedSlideshows[currentSlideshowName]; + const directories = slideshow.directories || []; + const sourcesCount = directories.length + (slideshow.includeCapturedPhotos ? 1 : 0); + + details.innerHTML = ` +
+ 🎬 ${currentSlideshowName} +
+
📁 Directories: ${directories.length} folders
+
📷 Captured Photos: ${slideshow.includeCapturedPhotos ? 'Included' : 'Excluded'}
+
⏱️ Timing: ${slideshow.settings.timingMode} (${slideshow.settings.duration}ms)
+
✨ Transition: ${slideshow.settings.transitionType} (${slideshow.settings.transitionDuration}ms)
+
📅 Created: ${slideshow.created ? new Date(slideshow.created).toLocaleDateString() : 'Unknown'}
+ `; + + // Update directory list + if (directories.length > 0) { + directoryList.innerHTML = directories.map((dir, index) => ` +
+ 📁 ${dir.name || dir} +
+ `).join(''); + } else { + directoryList.innerHTML = '
No directories added
'; + } + + // Enable buttons + addBtn.disabled = false; + document.getElementById('addFromLibraryBtn').disabled = false; + removeBtn.disabled = true; // Enable when directory is selected + testBtn.disabled = directories.length === 0; + + } else { + details.innerHTML = '
Select a slideshow to view details
'; + directoryList.innerHTML = '
Select a slideshow to manage directories
'; + + // Disable buttons + addBtn.disabled = true; + document.getElementById('addFromLibraryBtn').disabled = true; + removeBtn.disabled = true; + testBtn.disabled = true; + } + } + + function showCreateSlideshowDialog() { + const dialog = document.createElement('div'); + dialog.id = 'create-slideshow-dialog'; + dialog.innerHTML = ` +
+
+

🎬 Create New Slideshow

+ +
+ + +
+ +
+ + +
+
+
+ `; + + document.body.appendChild(dialog); + + // Focus the input and select all text with multiple attempts + const focusInput = () => { + const input = document.getElementById('dialogSlideshowName'); + if (input) { + input.focus(); + input.select(); + + // Add click handler to ensure input can be clicked + input.addEventListener('click', () => { + input.focus(); + }); + + // Add a safety focus on any dialog click + dialog.addEventListener('click', (e) => { + if (e.target === input) { + input.focus(); + } + }); + } + }; + + // Try multiple times to ensure focus works + setTimeout(focusInput, 50); + setTimeout(focusInput, 150); + setTimeout(focusInput, 300); + + // Handle Enter key + document.getElementById('dialogSlideshowName').addEventListener('keydown', (e) => { + if (e.key === 'Enter') { + createSlideshowFromDialog(); + } else if (e.key === 'Escape') { + closeSlideshowDialog(); + } + }); + } + + function showLoadSlideshowDialog() { + console.log('🔍 Debug - savedSlideshows:', savedSlideshows); + console.log('🔍 Debug - slideshow count:', savedSlideshows ? Object.keys(savedSlideshows).length : 'savedSlideshows is null/undefined'); + console.log('🔍 Debug - localStorage check:', localStorage.getItem('hypnoGallerySlideshows')); + + // Reload slideshows if empty but localStorage has data + if ((!savedSlideshows || Object.keys(savedSlideshows).length === 0)) { + const saved = localStorage.getItem('hypnoGallerySlideshows'); + if (saved) { + try { + savedSlideshows = JSON.parse(saved); + console.log('🔄 Reloaded slideshows from localStorage:', Object.keys(savedSlideshows).length); + } catch (e) { + console.error('❌ Error parsing saved slideshows:', e); + } + } + } + + if (!savedSlideshows || Object.keys(savedSlideshows).length === 0) { + alert('No saved slideshows available. Create one first!'); + return; + } + + const dialog = document.createElement('div'); + dialog.id = 'load-slideshow-dialog'; + dialog.innerHTML = ` +
+
+

📂 Load Slideshow

+ +
+ + +
+ +
+ Select a slideshow to view details +
+ +
+ + +
+
+
+ `; + + document.body.appendChild(dialog); + + // Add change listener for slideshow selection + document.getElementById('dialogSlideshowSelect').addEventListener('change', updateDialogSlideshowInfo); + } + + function updateDialogSlideshowInfo() { + const select = document.getElementById('dialogSlideshowSelect'); + const info = document.getElementById('dialogSlideshowInfo'); + const loadBtn = document.getElementById('dialogLoadBtn'); + + if (select.value && savedSlideshows[select.value]) { + const slideshow = savedSlideshows[select.value]; + const directories = slideshow.directories || []; + + info.innerHTML = ` +
📁 Directories: ${directories.length} folders
+
📷 Captured Photos: ${slideshow.includeCapturedPhotos ? 'Included' : 'Excluded'}
+
⏱️ Timing: ${slideshow.settings.timingMode}
+
📅 Created: ${slideshow.created ? new Date(slideshow.created).toLocaleDateString() : 'Unknown'}
+ `; + loadBtn.disabled = false; + } else { + info.textContent = 'Select a slideshow to view details'; + loadBtn.disabled = true; + } + } + + function createSlideshowFromDialog() { + const nameInput = document.getElementById('dialogSlideshowName'); + const name = nameInput.value.trim(); + + if (!name) { + alert('Please enter a slideshow name'); + nameInput.focus(); + return; + } + + if (savedSlideshows[name]) { + if (!confirm(`Slideshow "${name}" already exists. Replace it?`)) { + nameInput.focus(); + return; + } + } + + // Create slideshow with current settings and empty directories + savedSlideshows[name] = { + name: name, + settings: { ...currentSettings }, + directories: [], // Slideshow-specific directories + includeCapturedPhotos: true, // Default to including captured photos + created: Date.now(), + modified: Date.now() + }; + + saveSlideshowsToStorage(); + updateSlideshowsList(); + + // Load the new slideshow + loadSlideshow(name); + + closeSlideshowDialog(); + + console.log('✅ Created slideshow:', name); + alert(`Slideshow "${name}" created successfully!`); + } + + function loadSlideshowFromDialog() { + const select = document.getElementById('dialogSlideshowSelect'); + const name = select.value; + + if (!name || !savedSlideshows[name]) { + alert('Please select a slideshow to load'); + return; + } + + // Load the slideshow directly + loadSlideshow(name); + + closeSlideshowDialog(); + + console.log('📂 Loaded slideshow from dialog:', name); + } + + function closeSlideshowDialog() { + const createDialog = document.getElementById('create-slideshow-dialog'); + const loadDialog = document.getElementById('load-slideshow-dialog'); + + if (createDialog) createDialog.remove(); + if (loadDialog) loadDialog.remove(); + } + + function loadSlideshow(slideshowName = null) { + // If no name provided and no current slideshow, return + if (!slideshowName && !currentSlideshowName) { + currentSlideshowName = null; + updateCurrentSessionDisplay(); + return; + } + + const name = slideshowName || currentSlideshowName; + + if (!name || !savedSlideshows[name]) { + // Clear settings if no slideshow selected + currentSlideshowName = null; + updateCurrentSessionDisplay(); + return; + } + + const slideshow = savedSlideshows[name]; + + // Load settings + Object.assign(currentSettings, slideshow.settings); + currentSlideshowName = name; + + // Update UI controls + updateUIFromSettings(); + updateCurrentSessionDisplay(); + updateSlideshowDetails(); + + console.log('📂 Auto-loaded slideshow:', name); + } + + function saveCurrentAsSlideshow() { + if (!currentSlideshowName || !savedSlideshows[currentSlideshowName]) { + alert('Please load a slideshow first, or create a new one'); + return; + } + + if (!confirm(`Update slideshow "${currentSlideshowName}" with current settings?`)) { + return; + } + + savedSlideshows[currentSlideshowName].settings = { ...currentSettings }; + savedSlideshows[currentSlideshowName].modified = Date.now(); + + saveSlideshowsToStorage(); + updateSlideshowDetails(); + + console.log('💾 Updated slideshow:', currentSlideshowName); + alert(`Slideshow "${currentSlideshowName}" updated successfully!`); + } + + function duplicateSlideshow() { + if (!currentSlideshowName || !savedSlideshows[currentSlideshowName]) { + alert('Please load a slideshow first'); + return; + } + + showDuplicateSlideshowDialog(currentSlideshowName); + } + + function showDuplicateSlideshowDialog(originalName) { + const dialog = document.createElement('div'); + dialog.id = 'duplicate-slideshow-dialog'; + dialog.innerHTML = ` +
+
+

📋 Duplicate Slideshow

+ +
+ Duplicating: ${originalName} +
+ +
+ + +
+ +
+ + +
+
+
+ `; + + document.body.appendChild(dialog); + + // Focus the input and select all text + const focusInput = () => { + const input = document.getElementById('dialogDuplicateName'); + if (input) { + input.focus(); + input.select(); + } + }; + + setTimeout(focusInput, 50); + setTimeout(focusInput, 150); + + // Handle Enter key + document.getElementById('dialogDuplicateName').addEventListener('keydown', (e) => { + if (e.key === 'Enter') { + executeDuplicate(); + } else if (e.key === 'Escape') { + closeDuplicateDialog(); + } + }); + } + + function executeDuplicate() { + const input = document.getElementById('dialogDuplicateName'); + const newName = input.value.trim(); + const originalName = currentSlideshowName; + + if (!newName) { + alert('Please enter a name for the duplicate slideshow'); + input.focus(); + return; + } + + if (savedSlideshows[newName]) { + if (!confirm(`Slideshow "${newName}" already exists. Replace it?`)) { + input.focus(); + return; + } + } + + // Perform the duplication + savedSlideshows[newName] = { + ...savedSlideshows[originalName], + name: newName, + created: Date.now(), + modified: Date.now() + }; + + saveSlideshowsToStorage(); + updateSlideshowsList(); + + // Load the new slideshow + loadSlideshow(newName); + + closeDuplicateDialog(); + + console.log('📋 Duplicated slideshow:', originalName, '→', newName); + alert(`Slideshow duplicated as "${newName}"!`); + } + + function closeDuplicateDialog() { + const dialog = document.getElementById('duplicate-slideshow-dialog'); + if (dialog) dialog.remove(); + } + + function showLibrarySelectionDialog() { + if (!currentSlideshowName || !savedSlideshows[currentSlideshowName]) { + alert('Please load a slideshow first'); + return; + } + + const dialog = document.createElement('div'); + dialog.id = 'library-selection-dialog'; + dialog.innerHTML = ` +
+
+

📚 Add from Library

+ +
+
+ + +
+ +
+ +
+ Loading directories... +
+
+
+ +
+ + +
+
+
+ `; + + document.body.appendChild(dialog); + + // Load available directories + loadLibraryDirectories(); + } + + async function loadLibraryDirectories() { + const container = document.getElementById('libraryDirectoriesList'); + + try { + // Get linked directories from localStorage + const linkedDirectories = JSON.parse(localStorage.getItem('linkedImageDirectories') || '[]'); + console.log('📚 Loading library directories:', linkedDirectories); + + if (linkedDirectories.length === 0) { + container.innerHTML = '
No directories in library. Add some through the main game interface first.
'; + return; + } + + // Create checkboxes for each directory + container.innerHTML = linkedDirectories.map((dir, index) => { + const dirPath = dir.path || dir; + const dirName = typeof dir === 'string' ? dir.split(/[\\\\/]/).pop() : (dir.name || dirPath.split(/[\\\\/]/).pop() || 'Unknown Directory'); + const imageCount = dir.imageCount || dir.images?.length || 0; + + return ` +
+ + +
+ `; + }).join(''); + + } catch (error) { + console.error('❌ Error loading library directories:', error); + container.innerHTML = '
Error loading directories
'; + } + } + + function toggleLibraryDirectory(index) { + const checkbox = document.getElementById(`libDir_${index}`); + if (checkbox) { + checkbox.checked = !checkbox.checked; + } + } + + function addSelectedFromLibrary() { + const includeCapturedPhotos = document.getElementById('libraryCapturedPhotos').checked; + const selectedDirectories = []; + + // Get selected directories from localStorage + const linkedDirectories = JSON.parse(localStorage.getItem('linkedImageDirectories') || '[]'); + linkedDirectories.forEach((dir, index) => { + const checkbox = document.getElementById(`libDir_${index}`); + if (checkbox && checkbox.checked) { + const dirPath = dir.path || dir; + selectedDirectories.push({ + path: dirPath, + name: typeof dir === 'string' ? dir.split(/[\\\\/]/).pop() : (dir.name || dirPath.split(/[\\\\/]/).pop() || 'Unknown Directory') + }); + } + }); + + // Allow adding nothing (user might want slideshow-specific directories only) + if (selectedDirectories.length === 0 && !includeCapturedPhotos) { + if (!confirm('No library directories or captured photos will be added. Continue anyway?')) { + return; + } + } + + // Add to current slideshow + const slideshow = savedSlideshows[currentSlideshowName]; + if (!slideshow.directories) { + slideshow.directories = []; + } + + // Add directories (check for duplicates) + let addedCount = 0; + selectedDirectories.forEach(newDir => { + const exists = slideshow.directories.find(existingDir => + (typeof existingDir === 'string' ? existingDir : existingDir.path) === newDir.path + ); + + if (!exists) { + slideshow.directories.push(newDir); + addedCount++; + } + }); + + // Update captured photos setting only if it changed + const currentCapturedPhotos = slideshow.includeCapturedPhotos || false; + if (includeCapturedPhotos !== currentCapturedPhotos) { + slideshow.includeCapturedPhotos = includeCapturedPhotos; + } + + // Save and update + slideshow.modified = Date.now(); + saveSlideshowsToStorage(); + updateSlideshowDetails(); + + closeLibraryDialog(); + + // Better messaging + let message = ''; + if (addedCount > 0) { + message = `Added ${addedCount} director${addedCount === 1 ? 'y' : 'ies'} to slideshow!`; + } else if (selectedDirectories.length > 0) { + message = 'No new directories were added (duplicates found)'; + } else { + message = 'No directories selected.'; + } + + if (includeCapturedPhotos !== currentCapturedPhotos) { + const capturedAction = includeCapturedPhotos ? 'enabled' : 'disabled'; + message += ` Captured photos ${capturedAction}.`; + } + + if (message.trim()) { + alert(message); + } + + console.log('📚 Added from library:', { directories: addedCount, capturedPhotos: includeCapturedPhotos }); + } + + function closeLibraryDialog() { + const dialog = document.getElementById('library-selection-dialog'); + if (dialog) dialog.remove(); + } + + // Background Management Functions + function setupColorControl(colorId) { + const colorPicker = document.getElementById(colorId); + const hexInput = document.getElementById(colorId + 'Hex'); + + colorPicker.addEventListener('change', (e) => { + const color = e.target.value; + hexInput.value = color; + currentSettings[colorId] = color; + applyBackground(); + saveHypnoGallerySettings(); + }); + + hexInput.addEventListener('input', (e) => { + const color = e.target.value; + if (/^#[0-9A-F]{6}$/i.test(color)) { + colorPicker.value = color; + currentSettings[colorId] = color; + applyBackground(); + saveHypnoGallerySettings(); + } + }); + } + + function updateBackgroundOptions(type) { + const solidOptions = document.getElementById('solidColorOptions'); + const gradientOptions = document.getElementById('gradientOptions'); + const blurredOptions = document.getElementById('blurredOptions'); + + // Hide all options first + solidOptions.style.display = 'none'; + gradientOptions.style.display = 'none'; + blurredOptions.style.display = 'none'; + + // Show relevant options + switch (type) { + case 'solid': + solidOptions.style.display = 'flex'; + break; + case 'gradient': + gradientOptions.style.display = 'block'; + break; + case 'blurred': + blurredOptions.style.display = 'block'; + break; + } + } + + function applyBackground() { + const slideshowContainer = document.getElementById('slideshowContainer'); + if (!slideshowContainer) return; + + const { backgroundType, backgroundColor, gradientColor1, gradientColor2, gradientDirection, blurAmount, blurOpacity } = currentSettings; + + // Ensure container has proper positioning for backgrounds + if (slideshowContainer.style.position !== 'relative') { + slideshowContainer.style.position = 'relative'; + } + + // Remove existing background elements + const existingBg = slideshowContainer.querySelector('.slideshow-background'); + if (existingBg) existingBg.remove(); + + // Reset any previous background styles + slideshowContainer.style.background = ''; + slideshowContainer.style.backgroundColor = ''; + + switch (backgroundType) { + case 'solid': + slideshowContainer.style.backgroundColor = backgroundColor; + break; + + case 'gradient': + const gradientBg = `linear-gradient(${gradientDirection}, ${gradientColor1}, ${gradientColor2})`; + slideshowContainer.style.background = gradientBg; + break; + + case 'blurred': + slideshowContainer.style.backgroundColor = '#000000'; // Fallback color + createBlurredBackground(); + break; + } + } + + function createBlurredBackground() { + const slideshowContainer = document.getElementById('slideshowContainer'); + const currentImg = document.getElementById('slideshowImage'); + + if (!currentImg || !currentImg.src || !slideshowContainer) { + console.log('🫧 Cannot create blurred background:', { + hasContainer: !!slideshowContainer, + hasImg: !!currentImg, + hasSrc: !!currentImg?.src + }); + return; + } + + console.log('🫧 Creating blurred background with:', currentImg.src); + + // Create background element + const backgroundDiv = document.createElement('div'); + backgroundDiv.className = 'slideshow-background'; + backgroundDiv.style.cssText = ` + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-image: url("${currentImg.src}"); + background-size: cover; + background-position: center; + background-repeat: no-repeat; + filter: blur(${currentSettings.blurAmount}px); + opacity: ${currentSettings.blurOpacity / 100}; + z-index: 1; + pointer-events: none; + `; + + // Insert background behind the main image + slideshowContainer.insertBefore(backgroundDiv, slideshowContainer.firstChild); + + // Ensure main image is above background + if (currentImg.style.position !== 'relative') { + currentImg.style.position = 'relative'; + currentImg.style.zIndex = '2'; + } + + // Ensure all controls are above the background (z-index only, preserve existing positioning) + const slideshowInfo = document.getElementById('slideshowInfo'); + const slideshowControls = slideshowContainer.querySelector('.slideshow-controls'); + const progressBar = slideshowContainer.querySelector('.progress-bar'); + + if (slideshowInfo) { + slideshowInfo.style.zIndex = '10'; + } + + if (slideshowControls) { + slideshowControls.style.zIndex = '10'; + } + + if (progressBar) { + progressBar.style.zIndex = '10'; + } + + console.log('✅ Blurred background created successfully'); + } + + function deleteSlideshow() { + if (!currentSlideshowName || !savedSlideshows[currentSlideshowName]) { + alert('Please load a slideshow first'); + return; + } + + const name = currentSlideshowName; + + if (!confirm(`Delete slideshow "${name}"? This cannot be undone.`)) { + return; + } + + delete savedSlideshows[name]; + saveSlideshowsToStorage(); + updateSlideshowsList(); + + // Clear current slideshow since it was deleted + currentSlideshowName = null; + updateCurrentSessionDisplay(); + + console.log('🗑️ Deleted slideshow:', name); + alert(`Slideshow "${name}" deleted successfully!`); + } + + function updateUIFromSettings() { + // Timing settings + document.getElementById('timingMode').value = currentSettings.timingMode; + document.getElementById('durationSlider').value = currentSettings.duration; + document.getElementById('minDurationSlider').value = currentSettings.minDuration; + document.getElementById('maxDurationSlider').value = currentSettings.maxDuration; + document.getElementById('waveMinSlider').value = currentSettings.waveMin; + document.getElementById('waveMaxSlider').value = currentSettings.waveMax; + document.getElementById('waveRateSlider').value = currentSettings.waveRate; + + // Transition settings + document.getElementById('transitionType').value = currentSettings.transitionType; + document.getElementById('transitionDuration').value = currentSettings.transitionDuration; + + // Directory settings (checkbox removed) + + // Background settings + document.getElementById('backgroundType').value = currentSettings.backgroundType; + document.getElementById('backgroundColor').value = currentSettings.backgroundColor; + document.getElementById('backgroundColorHex').value = currentSettings.backgroundColor; + document.getElementById('gradientColor1').value = currentSettings.gradientColor1; + document.getElementById('gradientColor1Hex').value = currentSettings.gradientColor1; + document.getElementById('gradientColor2').value = currentSettings.gradientColor2; + document.getElementById('gradientColor2Hex').value = currentSettings.gradientColor2; + document.getElementById('gradientDirection').value = currentSettings.gradientDirection; + document.getElementById('blurAmount').value = currentSettings.blurAmount; + document.getElementById('blurAmountValue').textContent = currentSettings.blurAmount + 'px'; + document.getElementById('blurOpacity').value = currentSettings.blurOpacity; + document.getElementById('blurOpacityValue').textContent = currentSettings.blurOpacity + '%'; + + // Update displays + updateTimingSettings(currentSettings.timingMode); + updateTransitionSettings(currentSettings.transitionType); + updateBackgroundOptions(currentSettings.backgroundType); + + // Refresh directory display (if function exists) + if (typeof updateLinkedDirectories === 'function') { + updateLinkedDirectories(); + } + } + + function updateCurrentSessionDisplay() { + let sourcesCount = 0; + + if (currentSlideshowName && savedSlideshows[currentSlideshowName]) { + const slideshow = savedSlideshows[currentSlideshowName]; + sourcesCount = (slideshow.directories || []).length + (slideshow.includeCapturedPhotos ? 1 : 0); + } else { + sourcesCount = currentSettings.selectedDirectories.length + + (currentSettings.includeCapturedPhotos ? 1 : 0); + } + + document.getElementById('currentSources').textContent = + sourcesCount > 0 ? `${sourcesCount} sources selected` : 'No sources selected'; + + let timingText = currentSettings.timingMode; + if (currentSettings.timingMode === 'constant') { + timingText += ` (${(currentSettings.duration/1000).toFixed(1)}s)`; + } else if (currentSettings.timingMode === 'random') { + timingText += ` (${(currentSettings.minDuration/1000).toFixed(1)}-${(currentSettings.maxDuration/1000).toFixed(1)}s)`; + } else if (currentSettings.timingMode === 'wave') { + timingText += ` (${(currentSettings.waveMin/1000).toFixed(1)}-${(currentSettings.waveMax/1000).toFixed(1)}s)`; + } + document.getElementById('currentTiming').textContent = timingText; + + document.getElementById('currentTransitions').textContent = + `${currentSettings.transitionType} (${(currentSettings.transitionDuration/1000).toFixed(1)}s)`; + } + + // Slideshow Directory Management Functions + let selectedDirectoryIndex = -1; + + function selectSlideshowDirectory(index) { + // Remove previous selection + document.querySelectorAll('.slideshow-directory-item').forEach(item => { + item.style.border = '1px solid transparent'; + item.style.background = 'rgba(102, 126, 234, 0.1)'; + }); + + // Select new item + const items = document.querySelectorAll('.slideshow-directory-item'); + if (items[index]) { + items[index].style.border = '1px solid #667eea'; + items[index].style.background = 'rgba(102, 126, 234, 0.3)'; + selectedDirectoryIndex = index; + document.getElementById('removeDirectoryBtn').disabled = false; + } + } + + async function addDirectoryToSlideshow() { + if (!currentSlideshowName || !savedSlideshows[currentSlideshowName]) { + alert('Please load a slideshow first'); + return; + } + + try { + // Check if we're in Electron environment + if (!window.electronAPI || !window.electronAPI.selectDirectory) { + alert('Directory selection is only available in the desktop version of the application.'); + return; + } + + // Use the existing selectDirectory method + const directoryPath = await window.electronAPI.selectDirectory(); + + if (directoryPath) { + const directoryName = directoryPath.split(/[\\/]/).pop(); + + // Initialize directories array if it doesn't exist + if (!savedSlideshows[currentSlideshowName].directories) { + savedSlideshows[currentSlideshowName].directories = []; + } + + // Check if directory already exists + const existingDir = savedSlideshows[currentSlideshowName].directories.find(dir => + (typeof dir === 'string' ? dir : dir.path) === directoryPath + ); + + if (existingDir) { + alert('This directory is already added to the slideshow'); + return; + } + + // Add directory to slideshow + savedSlideshows[currentSlideshowName].directories.push({ + path: directoryPath, + name: directoryName + }); + + savedSlideshows[currentSlideshowName].modified = Date.now(); + saveSlideshowsToStorage(); + updateSlideshowDetails(); + updateCurrentSessionDisplay(); + + console.log('📁 Added directory to slideshow:', directoryName); + alert(`Added directory: ${directoryName}`); + } else { + console.log('Directory selection cancelled'); + } + + } catch (error) { + console.error('❌ Error adding directory:', error); + alert('Error adding directory: ' + error.message); + } + } + + function removeDirectoryFromSlideshow() { + if (!currentSlideshowName || !savedSlideshows[currentSlideshowName] || selectedDirectoryIndex === -1) { + return; + } + + const directories = savedSlideshows[currentSlideshowName].directories || []; + if (selectedDirectoryIndex >= 0 && selectedDirectoryIndex < directories.length) { + const removedDir = directories[selectedDirectoryIndex]; + directories.splice(selectedDirectoryIndex, 1); + + savedSlideshows[currentSlideshowName].modified = Date.now(); + saveSlideshowsToStorage(); + + selectedDirectoryIndex = -1; + updateSlideshowDetails(); + updateCurrentSessionDisplay(); + + console.log('🗑️ Removed directory from slideshow:', removedDir.name || removedDir); + } + } + + async function testSlideshowDirectories() { + if (!currentSlideshowName || !savedSlideshows[currentSlideshowName]) { + return; + } + + const slideshow = savedSlideshows[currentSlideshowName]; + const directories = slideshow.directories || []; + + if (directories.length === 0) { + alert('No directories to test'); + return; + } + + let totalImages = 0; + let validDirs = 0; + let invalidDirs = []; + + // Test each directory + for (const dir of directories) { + const dirPath = typeof dir === 'string' ? dir : dir.path; + const dirName = typeof dir === 'string' ? dirPath.split(/[\\/]/).pop() : dir.name; + + try { + // For electron environments, we could use fs to check directory + // For now, just assume they're valid and count as 1 image each for demo + validDirs++; + totalImages += 10; // Placeholder count + } catch (error) { + invalidDirs.push(dirName); + } + } + + const message = ` +📁 Directory Test Results: +✅ Valid directories: ${validDirs} +❌ Invalid directories: ${invalidDirs.length} +🖼️ Estimated images: ${totalImages} + +${invalidDirs.length > 0 ? `\nInvalid directories:\n${invalidDirs.join('\n')}` : ''} + `; + + alert(message.trim()); + } + + async function getSlideshowImages() { + let images = []; + + // If a slideshow is loaded, use its directories + if (currentSlideshowName && savedSlideshows[currentSlideshowName]) { + const slideshow = savedSlideshows[currentSlideshowName]; + const directories = slideshow.directories || []; + + console.log('📁 Loading images from slideshow directories:', directories.length); + + // Load images from slideshow-specific directories + for (const dir of directories) { + const dirPath = typeof dir === 'string' ? dir : dir.path; + try { + const dirImages = await loadImagesFromDirectory(dirPath); + images.push(...dirImages); + console.log(`📁 Loaded ${dirImages.length} images from: ${dirPath}`); + } catch (error) { + console.error(`❌ Error loading directory ${dirPath}:`, error); + } + } + + // Include captured photos if enabled + if (slideshow.includeCapturedPhotos) { + const capturedPhotos = getCapturedPhotos(); + images.push(...capturedPhotos); + console.log(`📷 Added ${capturedPhotos.length} captured photos`); + } + } else { + // Fallback to main library if no slideshow selected + console.log('📚 Using main image library'); + images = [...imageLibrary]; + } + + console.log(`🖼️ Total slideshow images: ${images.length}`); + return images; + } + + async function loadImagesFromDirectory(directoryPath) { + // Load images from a directory recursively using the main process function + console.log(`🔍 Starting recursive image scan of: ${directoryPath}`); + + if (typeof window !== 'undefined' && window.electronAPI && window.electronAPI.readImageDirectoryRecursive) { + try { + // Use the proper main process recursive function + const files = await window.electronAPI.readImageDirectoryRecursive(directoryPath); + console.log(`✅ Found ${files.length} images recursively`); + + // Convert to the expected format for the slideshow system + const processedFiles = files.map(file => ({ + name: file.name, + fullPath: file.path, + isWebcamCapture: false, + source: 'slideshow-directory' + })); + + return processedFiles; + } catch (error) { + console.error('❌ Error reading directory recursively:', error); + return []; + } + } else { + console.warn('⚠️ Directory loading not available in this environment'); + return []; + } + } + + + + function getCapturedPhotos() { + // Get captured photos from localStorage or game library + try { + const captured = localStorage.getItem('capturedPhotos'); + if (captured) { + const photos = JSON.parse(captured); + return photos.map(photo => ({ + name: photo.name || 'Captured Photo', + path: photo.dataURL || photo.path, + fullPath: photo.dataURL || photo.path, + isWebcamCapture: true, + source: 'captured-photos' + })); + } + } catch (error) { + console.error('❌ Error loading captured photos:', error); + } + return []; + } + // Initialize when DOM is loaded document.addEventListener('DOMContentLoaded', () => { console.log('🌀 Hypno Gallery DOM loaded'); - setTimeout(initializeHypnoGallery, 1000); + setTimeout(() => { + initializeHypnoGallery(); + loadSavedSlideshows(); + updateCurrentSessionDisplay(); + + // Dropdown removed - slideshow loading now handled by dialog system + }, 1000); }); // Initialize timing settings display diff --git a/index.html b/index.html index c6276fa..eeed46b 100644 --- a/index.html +++ b/index.html @@ -7024,6 +7024,42 @@ return { success: false, reason: 'PlayerStats not available' }; }; + + // ===== ANIMATION EVENT LISTENERS ===== + + // Listen for level up events + window.addEventListener('levelUp', (event) => { + console.log('🎉 Level up event received:', event.detail); + updateLevelDisplay(); // Update the display immediately + }); + + // Listen for achievement events + window.addEventListener('achievementUnlocked', (event) => { + console.log('🏆 Achievement unlocked event received:', event.detail); + }); + + // Listen for player stats updates + window.addEventListener('playerStatsUpdated', (event) => { + console.log('📊 Player stats updated:', event.detail); + updateLevelDisplay(); // Keep display in sync + }); + + // Test function for animations + window.testLevelUpAnimation = function() { + const oldLevel = { level: 4, name: 'Aroused', icon: '🌿', description: 'Your arousal is building' }; + const newLevel = { level: 5, name: 'Lustful', icon: '🌟', description: 'Consumed by lust and craving more' }; + window.showLevelUpAnimation(oldLevel, newLevel, 25, 'test'); + }; + + window.testAchievementAnimation = function() { + const achievement = { + id: 'test', + icon: '🏆', + title: 'Test Achievement', + description: 'This is a test achievement for demonstration' + }; + window.showAchievementAnimation(achievement); + }; diff --git a/porn-cinema.html b/porn-cinema.html index 380889e..3d7bdc8 100644 --- a/porn-cinema.html +++ b/porn-cinema.html @@ -316,41 +316,305 @@ showExitConfirmationDialog(); }); + function getSassyTheaterAttendantDialog() { + // Array of sassy theater attendant responses + const sassyResponses = [ + { + avatar: '🎭', + title: 'Leaving Already?', + message: 'Oh honey, the show was just getting good! Are you sure you want to walk out now?', + snarkIcon: '🍿', + snark: 'I mean, I guess not everyone has the stamina for a full feature presentation...', + exitIcon: '🚶‍♀️', + exitText: 'Leave Theater', + stayIcon: '🎬', + stayText: 'Keep Watching' + }, + { + avatar: '💅', + title: 'Really? Right Now?', + message: 'Sweetie, you\'re about to miss the best part! The climax is coming up...', + snarkIcon: '🎪', + snark: 'But sure, go ahead and leave during the most exciting scene. Your loss!', + exitIcon: '🚶‍♀️', + exitText: 'Leave Theater', + stayIcon: '🎬', + stayText: 'Keep Watching' + }, + { + avatar: '😏', + title: 'Intermission Over?', + message: 'Darling, this isn\'t a bathroom break - you\'re actually leaving? How disappointing...', + snarkIcon: '🎭', + snark: 'I thought you had better taste in entertainment. Guess I was wrong.', + exitIcon: '🚶‍♀️', + exitText: 'Leave Theater', + stayIcon: '🎬', + stayText: 'Keep Watching' + }, + { + avatar: '🎪', + title: 'Show\'s Not Over!', + message: 'Excuse me? The feature presentation is still running! You can\'t just leave mid-scene!', + snarkIcon: '🎬', + snark: 'This is like walking out of the theater during the finale. Absolutely scandalous!', + exitIcon: '🚶‍♀️', + exitText: 'Leave Theater', + stayIcon: '🎬', + stayText: 'Keep Watching' + }, + { + avatar: '💄', + title: 'Attention Deficit?', + message: 'Oh sweetie, can\'t focus for more than five minutes? The good stuff requires patience...', + snarkIcon: '⏰', + snark: 'Real connoisseurs know that the best performances build up slowly to an explosive finish.', + exitIcon: '🚶‍♀️', + exitText: 'Leave Theater', + stayIcon: '🎬', + stayText: 'Keep Watching' + }, + { + avatar: '🎬', + title: 'Intermission Confusion?', + message: 'Hun, this isn\'t the lobby! You\'re in the middle of premium content here!', + snarkIcon: '🍾', + snark: 'Some people pay extra for this level of entertainment, and you\'re just... walking away?', + exitIcon: '🚶‍♀️', + exitText: 'Leave Theater', + stayIcon: '🎬', + stayText: 'Keep Watching' + } + ]; + + // Return a random sassy response + return sassyResponses[Math.floor(Math.random() * sassyResponses.length)]; + } + function showExitConfirmationDialog() { + // Get sassy theater attendant content + const attendantDialog = getSassyTheaterAttendantDialog(); + // Create modal dialog const modal = document.createElement('div'); - modal.className = 'confirmation-modal'; + modal.className = 'confirmation-modal cinema-attendant-modal'; modal.innerHTML = `
-
-

🏠 Return to Home

+
+
${attendantDialog.avatar}
+

${attendantDialog.title}

-
-

Are you sure you want to return to the home screen?

-

⚠️ Current playback will stop and any unsaved progress will be lost.

+
+

${attendantDialog.message}

+
+ ${attendantDialog.snarkIcon} + ${attendantDialog.snark} +
- - + +
`; - // Add modal styles - modal.style.cssText = ` - 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: 10003; - backdrop-filter: blur(5px); - animation: fadeIn 0.3s ease-out; + // Add enhanced modal styles for theater attendant + modal.innerHTML += ` + `; + + modal.style.cssText = ``; document.body.appendChild(modal); diff --git a/quick-play.html b/quick-play.html index 516a20b..50905bf 100644 --- a/quick-play.html +++ b/quick-play.html @@ -1298,6 +1298,18 @@ } } + // ===== ANIMATION EVENT LISTENERS ===== + + // Listen for level up events + window.addEventListener('levelUp', (event) => { + console.log('🎉 Quick Play: Level up event received:', event.detail); + }); + + // Listen for achievement events + window.addEventListener('achievementUnlocked', (event) => { + console.log('🏆 Quick Play: Achievement unlocked event received:', event.detail); + }); + // Initialize Quick Play when page loads document.addEventListener('DOMContentLoaded', async function() { console.log('⚡ Initializing Quick Play...'); diff --git a/Start-webgame.bat b/scripts/Start-webgame.bat similarity index 100% rename from Start-webgame.bat rename to scripts/Start-webgame.bat diff --git a/create-distribution.bat b/scripts/create-distribution.bat similarity index 98% rename from create-distribution.bat rename to scripts/create-distribution.bat index 9aab8a5..30e82e9 100644 --- a/create-distribution.bat +++ b/scripts/create-distribution.bat @@ -43,9 +43,9 @@ copy README-DESKTOP.md "%OUTPUT_DIR%\" :: Copy setup scripts echo 🔧 Copying setup scripts... -copy setup.bat "%OUTPUT_DIR%\" -copy setup.sh "%OUTPUT_DIR%\" -copy Start-webgame.bat "%OUTPUT_DIR%\" +copy scripts\setup.bat "%OUTPUT_DIR%\" +copy scripts\setup.sh "%OUTPUT_DIR%\" +copy scripts\Start-webgame.bat "%OUTPUT_DIR%\" :: Copy source code (excluding user data) echo 💻 Copying source code... diff --git a/create-distribution.sh b/scripts/create-distribution.sh similarity index 97% rename from create-distribution.sh rename to scripts/create-distribution.sh index b72255b..96ded41 100644 --- a/create-distribution.sh +++ b/scripts/create-distribution.sh @@ -48,9 +48,9 @@ fi # Copy setup scripts echo -e "${BLUE}🔧 Copying setup scripts...${NC}" -cp setup.bat setup.sh "$OUTPUT_DIR/" -if [ -f Start-webgame.bat ]; then - cp Start-webgame.bat "$OUTPUT_DIR/" +cp scripts/setup.bat scripts/setup.sh "$OUTPUT_DIR/" +if [ -f scripts/Start-webgame.bat ]; then + cp scripts/Start-webgame.bat "$OUTPUT_DIR/" fi # Copy source code (excluding user data) diff --git a/setup.bat b/scripts/setup.bat similarity index 96% rename from setup.bat rename to scripts/setup.bat index 2e16740..f8bc68c 100644 --- a/setup.bat +++ b/scripts/setup.bat @@ -82,7 +82,7 @@ if errorlevel 0 ( echo 🚀 Ready to Launch! echo. echo 🎯 Quick Start Options: - echo 1. Double-click "Start-webgame.bat" to launch + echo 1. Double-click "scripts/Start-webgame.bat" to launch echo 2. Or run "npm start" in this folder echo 3. Or open "index.html" in browser (limited features) echo. @@ -111,7 +111,7 @@ if errorlevel 0 ( start "" npm start ) else ( echo. - echo 👍 Setup complete! Launch when ready with Start-webgame.bat + echo 👍 Setup complete! Launch when ready with scripts/Start-webgame.bat ) ) else ( diff --git a/setup.sh b/scripts/setup.sh similarity index 100% rename from setup.sh rename to scripts/setup.sh diff --git a/src/core/main.js b/src/core/main.js index 7969e14..4a1f7aa 100644 --- a/src/core/main.js +++ b/src/core/main.js @@ -497,6 +497,61 @@ ipcMain.handle('read-video-directory-recursive', async (event, dirPath) => { } }); +// Recursive image directory scanner +ipcMain.handle('read-image-directory-recursive', async (event, dirPath) => { + const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg', '.tiff', '.ico']; + + async function scanDirectory(currentPath) { + let imageFiles = []; + + try { + const items = await fs.readdir(currentPath, { withFileTypes: true }); + + for (const item of items) { + const itemPath = path.join(currentPath, item.name); + + if (item.isDirectory()) { + // Recursively scan subdirectories + const subDirImages = await scanDirectory(itemPath); + imageFiles.push(...subDirImages); + } else if (item.isFile()) { + const ext = path.extname(item.name).toLowerCase(); + if (imageExtensions.includes(ext)) { + try { + const stats = await fs.stat(itemPath); + imageFiles.push({ + name: item.name, + path: itemPath, + url: `file:///${itemPath.replace(/\\/g, '/')}`, + title: item.name.replace(/\.[^/.]+$/, "").replace(/[-_]/g, ' ').replace(/\w\S*/g, (txt) => txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase()), + size: stats.size, + type: `image/${ext.slice(1)}`, + directory: path.dirname(itemPath) + }); + } catch (statError) { + console.warn(`Could not stat image file: ${itemPath}`, statError); + } + } + } + } + } catch (readError) { + console.warn(`Could not read directory: ${currentPath}`, readError); + } + + return imageFiles; + } + + try { + console.log(`🔍 Starting recursive image scan of: ${dirPath}`); + const allImages = await scanDirectory(dirPath); + console.log(`✅ Recursive image scan complete: Found ${allImages.length} images`); + return allImages; + } catch (error) { + console.error('Error in recursive image directory scan:', error); + return []; + } +}); + ipcMain.handle('copy-video', async (event, sourcePath, destPath) => { try { await fs.mkdir(path.dirname(destPath), { recursive: true }); diff --git a/src/core/preload.js b/src/core/preload.js index 9715354..c544820 100644 --- a/src/core/preload.js +++ b/src/core/preload.js @@ -7,6 +7,7 @@ contextBridge.exposeInMainWorld('electronAPI', { selectImages: () => ipcRenderer.invoke('select-images'), selectDirectory: () => ipcRenderer.invoke('select-directory'), readDirectory: (dirPath) => ipcRenderer.invoke('read-directory', dirPath), + readImageDirectoryRecursive: (dirPath) => ipcRenderer.invoke('read-image-directory-recursive', dirPath), copyImage: (sourcePath, destPath) => ipcRenderer.invoke('copy-image', sourcePath, destPath), // Audio file operations diff --git a/src/features/stats/playerStats.js b/src/features/stats/playerStats.js index e62fb3e..76bb8a8 100644 --- a/src/features/stats/playerStats.js +++ b/src/features/stats/playerStats.js @@ -289,6 +289,11 @@ class PlayerStats { awardXP(amount, source = 'general') { console.log(`🏆 Awarded ${amount} XP from ${source}`); + + // Calculate old level before adding XP + const oldLevel = this.getCurrentLevel(); + const oldTotalXP = this.stats.totalXP; + this.stats.totalXP += amount; // Track by source @@ -307,13 +312,28 @@ class PlayerStats { this.updateDailyStats('xp', amount); this.saveStats(); + // Calculate new level after adding XP + const newLevel = this.getCurrentLevel(); + + // Check for level up + if (newLevel.level > oldLevel.level) { + console.log(`🎉 LEVEL UP! ${oldLevel.level} → ${newLevel.level} (${oldLevel.name} → ${newLevel.name})`); + this.triggerLevelUpAnimation(oldLevel, newLevel, amount, source); + } + + // Check for new achievements + this.checkForNewAchievements(oldTotalXP, this.stats.totalXP); + // Dispatch event to trigger display updates if (typeof window !== 'undefined') { window.dispatchEvent(new CustomEvent('playerStatsUpdated', { detail: { totalXP: this.stats.totalXP, source: source, - amount: amount + amount: amount, + oldLevel: oldLevel, + newLevel: newLevel, + leveledUp: newLevel.level > oldLevel.level } })); console.log(`📊 Dispatched playerStatsUpdated event: ${amount} XP from ${source}`); @@ -345,6 +365,198 @@ class PlayerStats { this.updateDailyStats('tasksCompleted', 1); this.saveStats(); } + + // ===== LEVEL AND ACHIEVEMENT SYSTEM ===== + + getCurrentLevel() { + return this.calculateLevel(this.stats.totalXP); + } + + calculateLevel(totalXP) { + const levelData = this.getLevelData(); + let currentLevel = levelData[0]; + + for (let i = levelData.length - 1; i >= 0; i--) { + if (totalXP >= levelData[i].xpRequired) { + currentLevel = levelData[i]; + break; + } + } + + // Calculate progress toward next level + const nextLevel = levelData.find(l => l.level === currentLevel.level + 1); + const currentLevelXP = totalXP - currentLevel.xpRequired; + const xpNeededForNext = nextLevel ? nextLevel.xpRequired - currentLevel.xpRequired : 100; + const progressPercent = nextLevel ? (currentLevelXP / xpNeededForNext) * 100 : 100; + + return { + level: currentLevel.level, + name: currentLevel.name, + icon: currentLevel.icon, + description: currentLevel.description, + totalXP: totalXP, + currentLevelXP: currentLevelXP, + xpNeededForNext: xpNeededForNext, + progressPercent: Math.min(progressPercent, 100), + nextLevelName: nextLevel ? nextLevel.name : 'Max Level' + }; + } + + getLevelData() { + return [ + { level: 1, name: "Virgin", xpRequired: 0, icon: '🌱', description: 'Your first taste of pleasure awaits' }, + { level: 2, name: "Curious", xpRequired: 10, icon: '🌿', description: 'Start exploring your desires' }, + { level: 3, name: "Eager", xpRequired: 25, icon: '🌱', description: 'Developing your appetites' }, + { level: 4, name: "Aroused", xpRequired: 48, icon: '🌿', description: 'Your arousal is building' }, + { level: 5, name: "Lustful", xpRequired: 82, icon: '🌟', description: 'Consumed by lust and craving more' }, + { level: 6, name: "Passionate", xpRequired: 133, icon: '🔥', description: 'Your passion burns hot with desire' }, + { level: 7, name: "Addicted", xpRequired: 209, icon: '⭐', description: 'Hopelessly addicted to pleasure' }, + { level: 8, name: "Obsessed", xpRequired: 323, icon: '🎯', description: 'Your obsession drives you to new heights' }, + { level: 9, name: "Deviant", xpRequired: 494, icon: '🏆', description: 'A true deviant exploring desire' }, + { level: 10, name: "Kinky", xpRequired: 751, icon: '💎', description: 'Welcome to the kinky elite' }, + { level: 11, name: "Perverted", xpRequired: 1137, icon: '🌟', description: 'A seasoned pervert with experience' }, + { level: 12, name: "Depraved", xpRequired: 1716, icon: '🔮', description: 'Mastered the art of depravity' }, + { level: 13, name: "Dominant", xpRequired: 2585, icon: '👑', description: 'A dominant force commanding respect' }, + { level: 14, name: "Submissive", xpRequired: 3889, icon: '🚀', description: 'Embracing submission and control' }, + { level: 15, name: "Hedonist", xpRequired: 5844, icon: '🌌', description: 'Pure hedonism guides your moves' }, + { level: 16, name: "Insatiable", xpRequired: 8777, icon: '⚡', description: 'Your appetite knows no bounds' }, + { level: 17, name: "Transcendent", xpRequired: 13176, icon: '🔥', description: 'Transcending mortal desire' }, + { level: 18, name: "Enlightened", xpRequired: 19775, icon: '🌠', description: 'Achieved enlightened pleasure' }, + { level: 19, name: "Godlike", xpRequired: 29673, icon: '🏛️', description: 'Godlike levels of sexual prowess' }, + { level: 20, name: "Omnipotent", xpRequired: 44520, icon: '👁️', description: 'Ultimate master of pleasure' } + ]; + } + + checkForNewAchievements(oldXP, newXP) { + const achievements = this.getAchievements(); + const stats = this.stats; + + achievements.forEach(achievement => { + const wasUnlocked = achievement.condition({ ...stats, totalXP: oldXP }); + const isNowUnlocked = achievement.condition(stats); + + if (!wasUnlocked && isNowUnlocked) { + console.log(`🏆 NEW ACHIEVEMENT UNLOCKED: ${achievement.title}`); + this.triggerAchievementAnimation(achievement); + } + }); + } + + getAchievements() { + return [ + { + id: 'first-video', + icon: '🎬', + title: 'First Taste', + description: 'Watch your first video', + condition: stats => (stats.videosWatched || 0) >= 1 + }, + { + id: 'early-bird', + icon: '🐦', + title: 'Early Bird', + description: 'Watch 10 videos', + condition: stats => (stats.videosWatched || 0) >= 10 + }, + { + id: 'marathon-viewer', + icon: '🏃‍♂️', + title: 'Marathon Viewer', + description: 'Watch for 2+ hours total', + condition: stats => (stats.totalWatchTime || 0) >= 7200000 // 2 hours in milliseconds + }, + { + id: 'playlist-curator', + icon: '📝', + title: 'Playlist Curator', + description: 'Create 5 playlists', + condition: stats => (stats.playlistsCreated || 0) >= 5 + }, + { + id: 'consistency-king', + icon: '🔥', + title: 'Consistency King', + description: 'Maintain a 7-day streak', + condition: stats => (stats.longestStreak || 0) >= 7 + }, + { + id: 'xp-master', + icon: '⭐', + title: 'Passionate Soul', + description: 'Earn 500 total XP (Level 6)', + condition: stats => (stats.totalXP || 0) >= 500 + }, + { + id: 'level-up', + icon: '🆙', + title: 'Lustful Awakening', + description: 'Reach Level 5 (Lustful)', + condition: stats => this.calculateLevel(stats.totalXP || 0).level >= 5 + }, + { + id: 'kinky-elite', + icon: '💎', + title: 'Kinky Elite', + description: 'Reach Level 10 (Kinky)', + condition: stats => this.calculateLevel(stats.totalXP || 0).level >= 10 + }, + { + id: 'depraved-master', + icon: '🔮', + title: 'Depraved Master', + description: 'Reach Level 12 (Depraved)', + condition: stats => this.calculateLevel(stats.totalXP || 0).level >= 12 + }, + { + id: 'collector', + icon: '🎭', + title: 'Desire Collector', + description: 'Watch 100 different videos', + condition: stats => (stats.videosWatched || 0) >= 100 + } + ]; + } + + triggerLevelUpAnimation(oldLevel, newLevel, xpAmount, source) { + // Create and trigger level up animation + if (typeof window !== 'undefined' && window.showLevelUpAnimation) { + window.showLevelUpAnimation(oldLevel, newLevel, xpAmount, source); + } else { + // Fallback for environments without animation support + console.log(`🎉 LEVEL UP ACHIEVED! ${oldLevel.name} (${oldLevel.level}) → ${newLevel.name} (${newLevel.level})`); + } + + // Dispatch level up event + if (typeof window !== 'undefined') { + window.dispatchEvent(new CustomEvent('levelUp', { + detail: { + oldLevel: oldLevel, + newLevel: newLevel, + xpAmount: xpAmount, + source: source + } + })); + } + } + + triggerAchievementAnimation(achievement) { + // Create and trigger achievement animation + if (typeof window !== 'undefined' && window.showAchievementAnimation) { + window.showAchievementAnimation(achievement); + } else { + // Fallback for environments without animation support + console.log(`🏆 ACHIEVEMENT UNLOCKED! ${achievement.title}: ${achievement.description}`); + } + + // Dispatch achievement event + if (typeof window !== 'undefined') { + window.dispatchEvent(new CustomEvent('achievementUnlocked', { + detail: { + achievement: achievement + } + })); + } + } // ===== DAILY STATS TRACKING ===== diff --git a/training-academy.html b/training-academy.html index 12f8b7a..50b5052 100644 --- a/training-academy.html +++ b/training-academy.html @@ -902,6 +902,22 @@
+ +
+ +
+ + +
+ +
+
@@ -1066,6 +1082,10 @@ let isVideoPlaying = true; let videoControlListenersInitialized = false; + // Scenario System Variables + let currentScenarioTask = null; + let currentScenarioStep = 'start'; + // TTS Variables let voiceManager = null; let ttsEnabled = true; @@ -1167,15 +1187,33 @@ function toggleTTS() { ttsEnabled = !ttsEnabled; - const toggleBtn = document.getElementById('tts-toggle'); + // Update header toggle button + const headerToggleBtn = document.getElementById('tts-toggle'); + // Update sidebar toggle button + const sidebarToggleBtn = document.getElementById('sidebar-tts-toggle'); + if (ttsEnabled) { - toggleBtn.innerHTML = '🔊 Voice: ON'; - toggleBtn.style.background = 'linear-gradient(135deg, #8B5CF6, #EC4899)'; + if (headerToggleBtn) { + headerToggleBtn.innerHTML = '🔊 Voice: ON'; + headerToggleBtn.style.background = 'linear-gradient(135deg, #8B5CF6, #EC4899)'; + } + if (sidebarToggleBtn) { + sidebarToggleBtn.innerHTML = '🔊 ON'; + sidebarToggleBtn.style.background = 'rgba(139, 92, 246, 0.4)'; + sidebarToggleBtn.style.borderColor = 'rgba(139, 92, 246, 0.6)'; + } updateTTSStatus('Voice enabled'); speakText('Voice narration enabled.'); } else { - toggleBtn.innerHTML = '🔇 Voice: OFF'; - toggleBtn.style.background = 'linear-gradient(135deg, #6B7280, #9CA3AF)'; + if (headerToggleBtn) { + headerToggleBtn.innerHTML = '🔇 Voice: OFF'; + headerToggleBtn.style.background = 'linear-gradient(135deg, #6B7280, #9CA3AF)'; + } + if (sidebarToggleBtn) { + sidebarToggleBtn.innerHTML = '🔇 OFF'; + sidebarToggleBtn.style.background = 'rgba(107, 114, 128, 0.4)'; + sidebarToggleBtn.style.borderColor = 'rgba(107, 114, 128, 0.6)'; + } updateTTSStatus('Voice disabled'); stopTTS(); } @@ -1196,9 +1234,16 @@ // Update TTS status display function updateTTSStatus(status) { - const statusEl = document.getElementById('tts-status-text'); - if (statusEl) { - statusEl.textContent = status; + // Update header status + const headerStatusEl = document.getElementById('tts-status-text'); + if (headerStatusEl) { + headerStatusEl.textContent = status; + } + + // Update sidebar status + const sidebarStatusEl = document.getElementById('sidebar-tts-status'); + if (sidebarStatusEl) { + sidebarStatusEl.textContent = status; } } @@ -1209,6 +1254,30 @@ } } + // Initialize sidebar TTS controls to match current state + function initializeSidebarTTSControls() { + const sidebarToggleBtn = document.getElementById('sidebar-tts-toggle'); + const sidebarStatusEl = document.getElementById('sidebar-tts-status'); + + if (sidebarToggleBtn) { + if (ttsEnabled) { + sidebarToggleBtn.innerHTML = '🔊 ON'; + sidebarToggleBtn.style.background = 'rgba(139, 92, 246, 0.4)'; + sidebarToggleBtn.style.borderColor = 'rgba(139, 92, 246, 0.6)'; + } else { + sidebarToggleBtn.innerHTML = '🔇 OFF'; + sidebarToggleBtn.style.background = 'rgba(107, 114, 128, 0.4)'; + sidebarToggleBtn.style.borderColor = 'rgba(107, 114, 128, 0.6)'; + } + } + + if (sidebarStatusEl) { + sidebarStatusEl.textContent = ttsEnabled ? 'Voice ready' : 'Voice disabled'; + } + + console.log('🔊 Sidebar TTS controls initialized'); + } + // ===== END TTS FUNCTIONS ===== // Helper function to get format from file path @@ -1878,6 +1947,9 @@ } }, 100); + // Initialize sidebar TTS controls state + initializeSidebarTTSControls(); + console.log('✅ Video control listeners set up successfully'); videoControlListenersInitialized = true; } @@ -1977,8 +2049,27 @@ const modeName = availableModes[selectedTrainingMode]?.name || selectedTrainingMode; speakText(`Starting ${modeName} training session. Your journey begins now.`); - // Hide setup interface - document.querySelector('.academy-header').style.display = 'none'; + // Hide most setup interface but keep TTS controls accessible + const academyHeader = document.querySelector('.academy-header'); + if (academyHeader) { + // Hide everything except TTS controls + const children = academyHeader.children; + for (let child of children) { + if (!child.classList.contains('tts-controls')) { + child.style.display = 'none'; + } + } + // Make header smaller and less prominent during training + academyHeader.style.padding = '0.5rem'; + academyHeader.style.margin = '0.5rem'; + academyHeader.style.background = 'rgba(26, 26, 26, 0.7)'; + academyHeader.style.position = 'fixed'; + academyHeader.style.top = '10px'; + academyHeader.style.right = '260px'; // Position next to video controls + academyHeader.style.width = 'auto'; + academyHeader.style.zIndex = '1001'; + academyHeader.style.borderRadius = '8px'; + } document.querySelector('.library-status').style.display = 'none'; document.querySelector('.training-controls').style.display = 'none'; document.querySelector('.academy-start-controls').style.display = 'none'; @@ -2462,7 +2553,7 @@ ` : ''}
- +
`; @@ -2532,7 +2623,7 @@ ` : ''}
- +
`; @@ -2712,6 +2803,372 @@ returnToModeSelection(); } + function showQuitConfirmation() { + console.log('⚠️ Showing quit training confirmation...'); + + // Get dynamic content based on selected training mode + const dialogContent = getQuipyDialogContent(); + + // Create confirmation overlay + const confirmOverlay = document.createElement('div'); + confirmOverlay.id = 'quit-confirmation-overlay'; + confirmOverlay.innerHTML = ` +
+
+
+
${dialogContent.icon}
+

${dialogContent.title}

+
+ +
+

${dialogContent.message}

+
+ ${dialogContent.warningIcon} + ${dialogContent.warning} +
+
+ +
+ + +
+
+ `; + + // Add styles + confirmOverlay.innerHTML += ` + + `; + + document.body.appendChild(confirmOverlay); + + // Close on backdrop click + confirmOverlay.querySelector('.confirmation-backdrop').onclick = cancelQuitTraining; + + // ESC key handler + const escHandler = (e) => { + if (e.key === 'Escape') { + cancelQuitTraining(); + } + }; + document.addEventListener('keydown', escHandler); + confirmOverlay.escHandler = escHandler; + } + + function getQuipyDialogContent() { + // Default content + let content = { + icon: '⚠️', + title: 'Quit Training Session?', + message: 'Are you sure you want to quit this training session?', + warningIcon: '🔥', + warning: 'Your progress will be saved, but you\'ll need to restart this session.', + quitIcon: '🚪', + quitText: 'Quit Training', + continueIcon: '💪', + continueText: 'Continue Training' + }; + + // Customize based on selected training mode + if (selectedTrainingMode) { + switch (selectedTrainingMode) { + case 'beginner': + content = { + icon: '🥺', + title: 'Giving Up Already?', + message: 'Come on, even beginners can handle more than this! You\'re just getting started.', + warningIcon: '💪', + warning: 'Real gooners don\'t quit during basic training. But hey, your choice...', + quitIcon: '😢', + quitText: 'I Give Up', + continueIcon: '🌟', + continueText: 'Keep Learning' + }; + break; + + case 'intermediate': + content = { + icon: '😏', + title: 'Can\'t Handle the Heat?', + message: 'Thought you were ready for intermediate training? Guess not everyone can level up.', + warningIcon: '🔥', + warning: 'You\'ll lose your momentum and have to rebuild that focus. Worth it?', + quitIcon: '🏃', + quitText: 'Retreat', + continueIcon: '🔥', + continueText: 'Bring the Heat' + }; + break; + + case 'advanced': + content = { + icon: '🤨', + title: 'Seriously? Advanced Gooner Quitting?', + message: 'An advanced gooner doesn\'t just walk away. This is embarrassing.', + warningIcon: '👑', + warning: 'You\'re supposed to be elite. Elite gooners finish what they start.', + quitIcon: '💸', + quitText: 'Be Weak', + continueIcon: '👑', + continueText: 'Stay Elite' + }; + break; + + case 'endurance': + content = { + icon: '💀', + title: 'Endurance Broken Already?', + message: 'The whole point is to build stamina. Quitting defeats the purpose entirely.', + warningIcon: '⏱️', + warning: 'Real endurance training means pushing through when it gets tough.', + quitIcon: '💔', + quitText: 'Tap Out', + continueIcon: '⚡', + continueText: 'Push Limits' + }; + break; + + case 'humiliation': + content = { + icon: '😈', + title: 'Too Humiliated to Continue?', + message: 'Aww, is the humiliation training too much for you? How... predictable.', + warningIcon: '🔥', + warning: 'Quitting now just proves you can\'t handle being properly trained.', + quitIcon: '🙈', + quitText: 'Run Away', + continueIcon: '😈', + continueText: 'Take More' + }; + break; + + case 'dress-up': + content = { + icon: '💄', + title: 'Not Feeling Pretty Enough?', + message: 'Can\'t handle getting all dressed up? But you were just starting to look so cute!', + warningIcon: '👗', + warning: 'You\'ll miss out on completing your fabulous transformation.', + quitIcon: '👔', + quitText: 'Stay Boring', + continueIcon: '💅', + continueText: 'Get Pretty' + }; + break; + + default: + // Keep default content for unknown modes + break; + } + } + + // If we're in a scenario, add scenario-specific flavor + if (currentScenarioTask && currentScenarioTask.title) { + const scenario = currentScenarioTask.title.toLowerCase(); + if (scenario.includes('sissy') || scenario.includes('feminiz')) { + content.icon = '💋'; + content.title = 'Not Ready to Embrace It?'; + } else if (scenario.includes('worship') || scenario.includes('goddess')) { + content.icon = '👸'; + content.title = 'Abandoning Your Goddess?'; + } else if (scenario.includes('edge') || scenario.includes('denial')) { + content.icon = '😤'; + content.title = 'Can\'t Handle the Edge?'; + } + } + + return content; + } + + function confirmQuitTraining() { + console.log('✅ Quit training confirmed'); + + // Remove confirmation overlay + const overlay = document.getElementById('quit-confirmation-overlay'); + if (overlay) { + if (overlay.escHandler) { + document.removeEventListener('keydown', overlay.escHandler); + } + overlay.remove(); + } + + // Proceed with quitting + returnToModeSelection(); + } + + function cancelQuitTraining() { + console.log('❌ Quit training cancelled'); + + // Remove confirmation overlay + const overlay = document.getElementById('quit-confirmation-overlay'); + if (overlay) { + if (overlay.escHandler) { + document.removeEventListener('keydown', overlay.escHandler); + } + overlay.style.animation = 'fadeOut 0.2s ease-out'; + setTimeout(() => overlay.remove(), 200); + } + } + function returnToModeSelection() { console.log('🔄 Returning to training mode selection...'); @@ -2744,14 +3201,71 @@ } // Show setup interface again - document.querySelector('.academy-header').style.display = 'block'; - document.querySelector('.library-status').style.display = 'block'; - document.querySelector('.training-controls').style.display = 'block'; - document.querySelector('.academy-start-controls').style.display = 'block'; + const academyHeader = document.querySelector('.academy-header'); + if (academyHeader) { + // Restore all children visibility + const children = academyHeader.children; + for (let child of children) { + child.style.display = ''; + } + // Reset header styling + academyHeader.style.padding = '2rem'; + academyHeader.style.margin = '1rem'; + academyHeader.style.background = 'linear-gradient(135deg, #1a1a1a, #2d1b3d)'; + academyHeader.style.position = 'static'; + academyHeader.style.top = 'auto'; + academyHeader.style.right = 'auto'; + academyHeader.style.width = 'auto'; + academyHeader.style.zIndex = 'auto'; + academyHeader.style.borderRadius = '15px'; + } + // Show main training academy interface + const libraryStatus = document.querySelector('.library-status'); + if (libraryStatus) libraryStatus.style.display = 'block'; - // Hide game interface + const trainingControls = document.querySelector('.training-controls'); + if (trainingControls) trainingControls.style.display = 'block'; + + const startControls = document.querySelector('.academy-start-controls'); + if (startControls) startControls.style.display = 'block'; + + // Show the main training academy container + const academyContainer = document.querySelector('.training-academy-container'); + if (academyContainer) { + academyContainer.style.display = 'block'; + academyContainer.style.visibility = 'visible'; + } + + // Show training mode selection cards + const modeSelection = document.querySelector('.training-mode-selection'); + if (modeSelection) { + modeSelection.style.display = 'block'; + modeSelection.style.visibility = 'visible'; + } + + // Hide all game-related containers const gameInterface = document.getElementById('gameInterface'); - gameInterface.style.display = 'none'; + if (gameInterface) { + gameInterface.style.display = 'none'; + } + + const gameContainer = document.getElementById('game-container'); + if (gameContainer) { + gameContainer.style.display = 'none'; + gameContainer.innerHTML = ''; + } + + const taskDisplay = document.getElementById('training-task-display'); + if (taskDisplay) { + taskDisplay.style.display = 'none'; + taskDisplay.innerHTML = ''; + } + + // Clear any overlays that might be covering the interface + const overlays = document.querySelectorAll('.camera-overlay, .verification-overlay, #camera-overlay'); + overlays.forEach(overlay => { + if (overlay) overlay.remove(); + }); // Reset training mode cards document.querySelectorAll('.training-mode-card').forEach(card => { @@ -2771,8 +3285,6 @@ } // Scenario Adventure Display System - let currentScenarioStep = 'start'; - let currentScenarioTask = null; function displayScenarioAdventure(container, task) { console.log('🎭 Displaying scenario adventure:', task.id); @@ -2793,6 +3305,16 @@ } function displayScenarioStep(container, stepId) { + if (!currentScenarioTask) { + console.error('❌ No current scenario task available'); + return; + } + + if (!currentScenarioTask.interactiveData) { + console.error('❌ No interactive data available for current scenario task'); + return; + } + const scenario = currentScenarioTask.interactiveData; const step = scenario.steps[stepId]; @@ -2821,7 +3343,7 @@ `).join('')}
- +
`; @@ -2834,7 +3356,7 @@
- +
`; @@ -2856,7 +3378,7 @@
- +
`; @@ -2911,7 +3433,7 @@ - + `; @@ -2954,7 +3476,7 @@
- +
`; @@ -2971,7 +3493,7 @@
- +
`; @@ -3375,7 +3897,7 @@
- +
`; @@ -3549,6 +4071,18 @@ } } + // ===== ANIMATION EVENT LISTENERS ===== + + // Listen for level up events + window.addEventListener('levelUp', (event) => { + console.log('🎉 Training Academy: Level up event received:', event.detail); + }); + + // Listen for achievement events + window.addEventListener('achievementUnlocked', (event) => { + console.log('🏆 Training Academy: Achievement unlocked event received:', event.detail); + }); + // Start initialization when DOM is loaded document.addEventListener('DOMContentLoaded', () => { console.log('🎓 Training Academy DOM loaded'); @@ -3557,6 +4091,26 @@ setTimeout(initializeTrainingAcademy, 1000); }); + // Listen for WebcamManager photo session completion + document.addEventListener('photoSessionComplete', (event) => { + console.log('📸 Photo session completed by WebcamManager:', event.detail); + + // Handle completion for training academy photo sessions + if (photoSessionActive && event.detail && event.detail.photos) { + const photosCount = event.detail.photos.length; + console.log(`📸 Training photo session complete: ${photosCount} photos taken`); + + // Update captured photos count + photosCaptured = photosCount; + + // Process the completion + handlePhotoSessionCompletion(); + + // Reset session state + photoSessionActive = false; + } + }); + // Listen for game ready events window.addEventListener('gameReady', (event) => { console.log('🎮 Game engine ready for training'); @@ -3601,21 +4155,20 @@ throw new Error('Failed to start webcam session'); } - // Monitor the WebcamManager's captured photos array - const originalCapturedPhotosLength = currentWebcamManager.capturedPhotos.length; + // Monitor photo progress for display only (completion handled by WebcamManager) const checkPhotoProgress = setInterval(() => { - const newPhotoCount = currentWebcamManager.capturedPhotos.length - originalCapturedPhotosLength; - if (newPhotoCount > photosCaptured) { + // Only update display, don't trigger completion + const sessionPhotos = currentWebcamManager.currentPhotoSession ? + currentWebcamManager.currentPhotoSession.photos.length : 0; + const newPhotoCount = sessionPhotos; + if (newPhotoCount !== photosCaptured) { photosCaptured = newPhotoCount; updatePhotoProgress(); - // Check if we've reached the target - if (photosCaptured >= photosNeeded) { - clearInterval(checkPhotoProgress); - setTimeout(completePhotoSession, 1500); // Delay to show final photo - } + // Don't auto-complete here - let WebcamManager handle it + // The photoSessionComplete event listener will handle completion } - }, 500); // Check every 500ms + }, 500); // Check every 500ms for display updates only // Store the interval so we can clear it if needed window.photoProgressInterval = checkPhotoProgress; @@ -3665,6 +4218,12 @@ } function completePhotoSession() { + // Prevent multiple calls + if (!photoSessionActive) { + console.log('📸 Photo session already completed, skipping'); + return; + } + console.log('📸 Photo session completed'); photoSessionActive = false; @@ -3674,8 +4233,9 @@ window.photoProgressInterval = null; } - // Get newly captured photos from WebcamManager - const capturedPhotos = currentWebcamManager ? currentWebcamManager.capturedPhotos : []; + // Get newly captured photos from WebcamManager's current session + const capturedPhotos = currentWebcamManager && currentWebcamManager.currentPhotoSession ? + currentWebcamManager.currentPhotoSession.photos : []; console.log(`📸 Found ${capturedPhotos.length} captured photos`); // Add captured photos to training photo library @@ -3696,22 +4256,9 @@ `; } - // Close webcam interface - if (currentWebcamManager) { - // Stop the camera using WebcamManager's method - currentWebcamManager.stopCamera(); - - // Remove the camera overlay created by WebcamManager - const overlay = document.getElementById('camera-overlay'); - if (overlay) { - overlay.remove(); - console.log('📸 Camera overlay removed'); - } - - // Reset webcam manager state - currentWebcamManager.isActive = false; - currentWebcamManager.currentPhotoSession = null; - } + // WebcamManager has already ended the session when firing photoSessionComplete event + // Just ensure we clean up our training academy state + console.log('📸 Photo session completion handled by training academy'); } // Add captured photos to the training photo library diff --git a/user-profile.html b/user-profile.html index 8354aa9..3b1727c 100644 --- a/user-profile.html +++ b/user-profile.html @@ -689,8 +689,9 @@ document.getElementById('playlists-created').textContent = stats.playlistsCreated; // Calculate level based on XP - const level = this.calculateLevel(rawStats.totalXP || 0); - const levelInfo = this.getLevelInfo(level); + const levelData = this.calculateLevel(rawStats.totalXP || 0); + const level = levelData.level; + const levelInfo = levelData; document.getElementById('current-level').textContent = `${level} - ${levelInfo.name}`; // Update level description @@ -722,35 +723,79 @@ this.updateAchievements(rawStats); } - calculateLevel(totalXp) { - // Level up every 100 XP - return Math.floor(totalXp / 100) + 1; + // ===== EXPONENTIAL LEVEL SYSTEM (MATCHES PLAYERSTATS) ===== + // Exponential XP scaling: starts at 10 XP for level 2, then grows exponentially + getLevelData() { + // Use PlayerStats level data if available for consistency + if (window.playerStats && typeof window.playerStats.getLevelData === 'function') { + return window.playerStats.getLevelData(); + } + + // Fallback level data (matches PlayerStats) + return [ + { level: 1, name: "Virgin", xpRequired: 0, icon: '🌱', description: 'Your first taste of pleasure awaits' }, + { level: 2, name: "Curious", xpRequired: 10, icon: '🌿', description: 'Start exploring your desires' }, + { level: 3, name: "Eager", xpRequired: 25, icon: '🌱', description: 'Developing your appetites' }, + { level: 4, name: "Aroused", xpRequired: 48, icon: '🌿', description: 'Your arousal is building' }, + { level: 5, name: "Lustful", xpRequired: 82, icon: '🌟', description: 'Consumed by lust and craving more' }, + { level: 6, name: "Passionate", xpRequired: 133, icon: '🔥', description: 'Your passion burns hot with desire' }, + { level: 7, name: "Addicted", xpRequired: 209, icon: '⭐', description: 'Hopelessly addicted to pleasure' }, + { level: 8, name: "Obsessed", xpRequired: 323, icon: '🎯', description: 'Your obsession drives you to new heights' }, + { level: 9, name: "Deviant", xpRequired: 494, icon: '🏆', description: 'A true deviant exploring desire' }, + { level: 10, name: "Kinky", xpRequired: 751, icon: '💎', description: 'Welcome to the kinky elite' }, + { level: 11, name: "Perverted", xpRequired: 1137, icon: '🌟', description: 'A seasoned pervert with experience' }, + { level: 12, name: "Depraved", xpRequired: 1716, icon: '🔮', description: 'Mastered the art of depravity' }, + { level: 13, name: "Dominant", xpRequired: 2585, icon: '👑', description: 'A dominant force commanding respect' }, + { level: 14, name: "Submissive", xpRequired: 3889, icon: '🚀', description: 'Embracing submission and control' }, + { level: 15, name: "Hedonist", xpRequired: 5844, icon: '🌌', description: 'Pure hedonism guides your moves' }, + { level: 16, name: "Insatiable", xpRequired: 8777, icon: '⚡', description: 'Your appetite knows no bounds' }, + { level: 17, name: "Transcendent", xpRequired: 13176, icon: '🔥', description: 'Transcending mortal desire' }, + { level: 18, name: "Enlightened", xpRequired: 19775, icon: '🌠', description: 'Achieved enlightened pleasure' }, + { level: 19, name: "Godlike", xpRequired: 29673, icon: '🏛️', description: 'Godlike levels of sexual prowess' }, + { level: 20, name: "Omnipotent", xpRequired: 44520, icon: '👁️', description: 'Ultimate master of pleasure' } + ]; + } + + calculateLevel(totalXP) { + // Use PlayerStats calculation if available for consistency + if (window.playerStats && typeof window.playerStats.calculateLevel === 'function') { + return window.playerStats.calculateLevel(totalXP); + } + + // Fallback calculation + const levelData = this.getLevelData(); + // Find the highest level that the user has reached + let currentLevel = levelData[0]; + for (let i = levelData.length - 1; i >= 0; i--) { + if (totalXP >= levelData[i].xpRequired) { + currentLevel = levelData[i]; + break; + } + } + + // Calculate progress toward next level + const nextLevel = levelData.find(l => l.level === currentLevel.level + 1); + const currentLevelXP = totalXP - currentLevel.xpRequired; + const xpNeededForNext = nextLevel ? nextLevel.xpRequired - currentLevel.xpRequired : 100; + const progressPercent = nextLevel ? (currentLevelXP / xpNeededForNext) * 100 : 100; + + return { + level: currentLevel.level, + name: currentLevel.name, + totalXP: totalXP, + currentLevelXP: currentLevelXP, + xpNeededForNext: xpNeededForNext, + progressPercent: Math.min(progressPercent, 100), + nextLevelName: nextLevel ? nextLevel.name : 'Max Level', + icon: currentLevel.icon, + description: currentLevel.description + }; } getLevelInfo(level) { - const levelData = { - 1: { name: 'Virgin', icon: '🌱', description: 'Your first taste of pleasure awaits' }, - 2: { name: 'Curious', icon: '🌿', description: 'Start exploring your desires' }, - 3: { name: 'Eager', icon: '🌱', description: 'Developing your appetites' }, - 4: { name: 'Aroused', icon: '🌿', description: 'Your arousal is building' }, - 5: { name: 'Lustful', icon: '🌟', description: 'Consumed by lust and craving more' }, - 6: { name: 'Passionate', icon: '🔥', description: 'Your passion burns hot with desire' }, - 7: { name: 'Addicted', icon: '⭐', description: 'Hopelessly addicted to pleasure' }, - 8: { name: 'Obsessed', icon: '🎯', description: 'Your obsession drives you to new heights' }, - 9: { name: 'Deviant', icon: '🏆', description: 'A true deviant exploring desire' }, - 10: { name: 'Kinky', icon: '💎', description: 'Welcome to the kinky elite' }, - 11: { name: 'Perverted', icon: '🌟', description: 'A seasoned pervert with experience' }, - 12: { name: 'Depraved', icon: '🔮', description: 'Mastered the art of depravity' }, - 13: { name: 'Dominant', icon: '👑', description: 'A dominant force commanding respect' }, - 14: { name: 'Hedonistic', icon: '🚀', description: 'Pure hedonism guides your moves' }, - 15: { name: 'Decadent', icon: '🌌', description: 'Reached decadent levels of indulgence' }, - 16: { name: 'Insatiable', icon: '⚡', description: 'Your appetite knows no bounds' }, - 17: { name: 'Sinful', icon: '🔥', description: 'Deliciously sinful dedication' }, - 18: { name: 'Forbidden', icon: '🌠', description: 'Achieved forbidden status' }, - 19: { name: 'Godlike', icon: '🏛️', description: 'Godlike levels of sexual prowess' }, - 20: { name: 'Omnipotent', icon: '👁️', description: 'Ultimate master of pleasure' } - }; - + const levelData = this.getLevelData(); + const levelInfo = levelData.find(l => l.level === level); + // For levels beyond 20, continue with Omnipotent if (level > 20) { return { @@ -760,20 +805,16 @@ }; } - return levelData[level] || { name: 'Unknown', icon: '❓', description: 'Mysterious level' }; + return levelInfo || { name: 'Unknown', icon: '❓', description: 'Mysterious level' }; } updateLevelProgress(totalXp) { - const currentLevel = this.calculateLevel(totalXp); - const currentLevelStart = (currentLevel - 1) * 100; - const nextLevelStart = currentLevel * 100; - const progressInLevel = totalXp - currentLevelStart; - const levelDuration = nextLevelStart - currentLevelStart; - const progressPercent = (progressInLevel / levelDuration) * 100; + const levelInfo = this.calculateLevel(totalXp); + const progressPercent = levelInfo.progressPercent; document.getElementById('level-fill').style.width = progressPercent + '%'; document.getElementById('level-text').textContent = Math.round(progressPercent) + '%'; - document.getElementById('level-xp').textContent = `${progressInLevel} / ${levelDuration} XP`; + document.getElementById('level-xp').textContent = `${levelInfo.currentLevelXP} / ${levelInfo.xpNeededForNext} XP`; } initializeAchievements() { @@ -825,21 +866,36 @@ icon: '�', title: 'Lustful Awakening', description: 'Reach Level 5 (Lustful)', - condition: stats => Math.floor((stats.totalXP || 0) / 100) + 1 >= 5 + condition: stats => { + if (window.playerStats && typeof window.playerStats.calculateLevel === 'function') { + return window.playerStats.calculateLevel(stats.totalXP || 0).level >= 5; + } + return Math.floor((stats.totalXP || 0) / 100) + 1 >= 5; + } }, { id: 'kinky-elite', icon: '💎', title: 'Kinky Elite', description: 'Reach Level 10 (Kinky)', - condition: stats => Math.floor((stats.totalXP || 0) / 100) + 1 >= 10 + condition: stats => { + if (window.playerStats && typeof window.playerStats.calculateLevel === 'function') { + return window.playerStats.calculateLevel(stats.totalXP || 0).level >= 10; + } + return Math.floor((stats.totalXP || 0) / 100) + 1 >= 10; + } }, { id: 'depraved-master', icon: '🔮', title: 'Depraved Master', description: 'Reach Level 12 (Depraved)', - condition: stats => Math.floor((stats.totalXP || 0) / 100) + 1 >= 12 + condition: stats => { + if (window.playerStats && typeof window.playerStats.calculateLevel === 'function') { + return window.playerStats.calculateLevel(stats.totalXP || 0).level >= 12; + } + return Math.floor((stats.totalXP || 0) / 100) + 1 >= 12; + } }, { id: 'collector',