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.
This commit is contained in:
dilgenfritz 2025-11-18 22:42:32 -06:00
parent ec45cf69f8
commit c40ed278e0
23 changed files with 3283 additions and 185 deletions

View File

@ -898,8 +898,20 @@ webGame/
├── quick-play.html # Quick Play game mode ├── quick-play.html # Quick Play game mode
├── training-academy.html # Training Academy mode ├── training-academy.html # Training Academy mode
├── porn-cinema.html # Video player system ├── porn-cinema.html # Video player system
├── hypno-gallery.html # Immersive slideshow system
├── player-stats.html # Statistics dashboard ├── player-stats.html # Statistics dashboard
├── user-profile.html # Profile and achievements ├── 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/ ├── src/
│ ├── core/ # Game engine and state management │ ├── core/ # Game engine and state management
│ ├── data/modes/ # Game mode configurations │ ├── data/modes/ # Game mode configurations

View File

@ -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

View File

@ -5,13 +5,13 @@
### **Method 1: Automated Setup (Recommended)** ### **Method 1: Automated Setup (Recommended)**
#### **Windows Users:** #### **Windows Users:**
1. Double-click `setup.bat` 1. Double-click `scripts/setup.bat`
2. Follow the prompts 2. Follow the prompts
3. Launch with `Start-webgame.bat` 3. Launch with `scripts/Start-webgame.bat`
#### **Mac/Linux Users:** #### **Mac/Linux Users:**
1. Open terminal in this folder 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` 3. Launch with: `npm start`
### **Method 2: Manual Setup** ### **Method 2: Manual Setup**
@ -103,9 +103,10 @@ Gooner-Training-Academy/
├── 📄 TESTER_GUIDE.md # Testing instructions ├── 📄 TESTER_GUIDE.md # Testing instructions
├── 📄 INSTALLATION_GUIDE.md # This file ├── 📄 INSTALLATION_GUIDE.md # This file
├── 📄 README.md # Full documentation ├── 📄 README.md # Full documentation
├── 🔧 setup.bat # Windows installer ├── 📁 scripts/ # Installation and launch scripts
├── 🔧 setup.sh # Mac/Linux installer │ ├── setup.bat # Windows installer
├── 🚀 Start-webgame.bat # Windows launcher │ ├── setup.sh # Mac/Linux installer
│ └── Start-webgame.bat # Windows launcher
├── 📄 package.json # Dependencies ├── 📄 package.json # Dependencies
├── 🌐 index.html # Main application ├── 🌐 index.html # Main application
├── 📁 src/ # Application code ├── 📁 src/ # Application code

67
docs/README.md Normal file
View File

@ -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.

File diff suppressed because it is too large Load Diff

View File

@ -7024,6 +7024,42 @@
return { success: false, reason: 'PlayerStats not available' }; 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);
};
</script> </script>

View File

@ -316,41 +316,305 @@
showExitConfirmationDialog(); 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() { function showExitConfirmationDialog() {
// Get sassy theater attendant content
const attendantDialog = getSassyTheaterAttendantDialog();
// Create modal dialog // Create modal dialog
const modal = document.createElement('div'); const modal = document.createElement('div');
modal.className = 'confirmation-modal'; modal.className = 'confirmation-modal cinema-attendant-modal';
modal.innerHTML = ` modal.innerHTML = `
<div class="confirmation-modal-content"> <div class="confirmation-modal-content">
<div class="confirmation-modal-header"> <div class="confirmation-modal-header cinema-attendant-header">
<h3>🏠 Return to Home</h3> <div class="attendant-avatar">${attendantDialog.avatar}</div>
<h3>${attendantDialog.title}</h3>
</div> </div>
<div class="confirmation-modal-body"> <div class="confirmation-modal-body cinema-attendant-body">
<p>Are you sure you want to return to the home screen?</p> <p class="attendant-main-message">${attendantDialog.message}</p>
<p class="warning-text">⚠️ Current playback will stop and any unsaved progress will be lost.</p> <div class="attendant-snark">
<span class="snark-icon">${attendantDialog.snarkIcon}</span>
<span class="snark-text">${attendantDialog.snark}</span>
</div>
</div> </div>
<div class="confirmation-modal-actions"> <div class="confirmation-modal-actions">
<button class="btn-confirm-exit">Yes, Go Home</button> <button class="btn-confirm-exit cinema-exit-btn">
<button class="btn-cancel-exit">Stay in Cinema</button> <span class="btn-icon">${attendantDialog.exitIcon}</span>
<span>${attendantDialog.exitText}</span>
</button>
<button class="btn-cancel-exit cinema-stay-btn">
<span class="btn-icon">${attendantDialog.stayIcon}</span>
<span>${attendantDialog.stayText}</span>
</button>
</div> </div>
</div> </div>
`; `;
// Add modal styles // Add enhanced modal styles for theater attendant
modal.style.cssText = ` modal.innerHTML += `
position: fixed; <style>
top: 0; .cinema-attendant-modal {
left: 0; position: fixed;
width: 100vw; top: 0;
height: 100vh; left: 0;
background: rgba(0, 0, 0, 0.8); width: 100vw;
display: flex; height: 100vh;
align-items: center; background: rgba(0, 0, 0, 0.85);
justify-content: center; display: flex;
z-index: 10003; align-items: center;
backdrop-filter: blur(5px); justify-content: center;
animation: fadeIn 0.3s ease-out; z-index: 10003;
backdrop-filter: blur(8px);
animation: fadeIn 0.4s ease-out;
}
.cinema-attendant-modal .confirmation-modal-content {
background: linear-gradient(145deg, #1a0d2e, #2d1b3d);
border: 3px solid #ff6b35;
border-radius: 20px;
padding: 2.5rem;
max-width: 500px;
width: 90%;
box-shadow: 0 25px 50px rgba(255, 107, 53, 0.3), 0 0 30px rgba(255, 107, 53, 0.2);
animation: modalBounceIn 0.5s ease-out;
text-align: center;
position: relative;
overflow: hidden;
}
.cinema-attendant-modal .confirmation-modal-content::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1), transparent);
animation: shimmer 3s infinite;
}
.cinema-attendant-header {
margin-bottom: 2rem;
position: relative;
z-index: 1;
}
.attendant-avatar {
font-size: 4rem;
margin-bottom: 0.5rem;
filter: drop-shadow(0 0 15px rgba(255, 107, 53, 0.6));
animation: pulse 2s infinite;
}
.cinema-attendant-header h3 {
color: #ff6b35;
font-size: 1.8rem;
font-weight: 700;
margin: 0;
text-shadow: 0 0 15px rgba(255, 107, 53, 0.4);
font-family: 'Audiowide', cursive;
}
.cinema-attendant-body {
margin-bottom: 2.5rem;
position: relative;
z-index: 1;
}
.attendant-main-message {
color: #f0f0f0;
font-size: 1.2rem;
margin-bottom: 1.5rem;
line-height: 1.6;
font-style: italic;
}
.attendant-snark {
background: linear-gradient(135deg, rgba(255, 107, 53, 0.15), rgba(255, 193, 7, 0.1));
border: 1px solid rgba(255, 107, 53, 0.4);
border-radius: 15px;
padding: 1rem;
display: flex;
align-items: center;
gap: 0.75rem;
font-size: 1rem;
color: #ffab91;
}
.snark-icon {
font-size: 1.3rem;
filter: drop-shadow(0 0 8px rgba(255, 107, 53, 0.5));
}
.snark-text {
flex: 1;
font-style: italic;
}
.cinema-attendant-modal .confirmation-modal-actions {
display: flex;
gap: 1.5rem;
justify-content: center;
position: relative;
z-index: 1;
}
.cinema-attendant-modal .confirmation-modal-actions button {
background: linear-gradient(135deg, #2d1b3d, #1a0d2e);
border: 2px solid;
border-radius: 15px;
padding: 1rem 1.5rem;
color: white;
font-size: 1.1rem;
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
gap: 0.5rem;
transition: all 0.3s ease;
min-width: 160px;
justify-content: center;
font-family: 'Audiowide', cursive;
}
.cinema-exit-btn {
border-color: #ff6b35 !important;
box-shadow: 0 0 20px rgba(255, 107, 53, 0.3);
}
.cinema-exit-btn:hover {
background: linear-gradient(135deg, #ff6b35, #ff5722) !important;
transform: translateY(-3px);
box-shadow: 0 8px 25px rgba(255, 107, 53, 0.5);
}
.cinema-stay-btn {
border-color: #4caf50 !important;
box-shadow: 0 0 20px rgba(76, 175, 80, 0.3);
}
.cinema-stay-btn:hover {
background: linear-gradient(135deg, #4caf50, #388e3c) !important;
transform: translateY(-3px);
box-shadow: 0 8px 25px rgba(76, 175, 80, 0.5);
}
.btn-icon {
font-size: 1.2rem;
filter: drop-shadow(0 0 8px currentColor);
}
@keyframes modalBounceIn {
0% {
transform: scale(0.3) translateY(-100px);
opacity: 0;
}
50% {
transform: scale(1.1) translateY(0);
}
100% {
transform: scale(1) translateY(0);
opacity: 1;
}
}
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.1); }
}
@keyframes shimmer {
0% { left: -100%; }
100% { left: 100%; }
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes fadeOut {
from { opacity: 1; }
to { opacity: 0; }
}
</style>
`; `;
modal.style.cssText = ``;
document.body.appendChild(modal); document.body.appendChild(modal);

View File

@ -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 // Initialize Quick Play when page loads
document.addEventListener('DOMContentLoaded', async function() { document.addEventListener('DOMContentLoaded', async function() {
console.log('⚡ Initializing Quick Play...'); console.log('⚡ Initializing Quick Play...');

View File

@ -43,9 +43,9 @@ copy README-DESKTOP.md "%OUTPUT_DIR%\"
:: Copy setup scripts :: Copy setup scripts
echo 🔧 Copying setup scripts... echo 🔧 Copying setup scripts...
copy setup.bat "%OUTPUT_DIR%\" copy scripts\setup.bat "%OUTPUT_DIR%\"
copy setup.sh "%OUTPUT_DIR%\" copy scripts\setup.sh "%OUTPUT_DIR%\"
copy Start-webgame.bat "%OUTPUT_DIR%\" copy scripts\Start-webgame.bat "%OUTPUT_DIR%\"
:: Copy source code (excluding user data) :: Copy source code (excluding user data)
echo 💻 Copying source code... echo 💻 Copying source code...

View File

@ -48,9 +48,9 @@ fi
# Copy setup scripts # Copy setup scripts
echo -e "${BLUE}🔧 Copying setup scripts...${NC}" echo -e "${BLUE}🔧 Copying setup scripts...${NC}"
cp setup.bat setup.sh "$OUTPUT_DIR/" cp scripts/setup.bat scripts/setup.sh "$OUTPUT_DIR/"
if [ -f Start-webgame.bat ]; then if [ -f scripts/Start-webgame.bat ]; then
cp Start-webgame.bat "$OUTPUT_DIR/" cp scripts/Start-webgame.bat "$OUTPUT_DIR/"
fi fi
# Copy source code (excluding user data) # Copy source code (excluding user data)

View File

@ -82,7 +82,7 @@ if errorlevel 0 (
echo 🚀 Ready to Launch! echo 🚀 Ready to Launch!
echo. echo.
echo 🎯 Quick Start Options: 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 2. Or run "npm start" in this folder
echo 3. Or open "index.html" in browser (limited features) echo 3. Or open "index.html" in browser (limited features)
echo. echo.
@ -111,7 +111,7 @@ if errorlevel 0 (
start "" npm start start "" npm start
) else ( ) else (
echo. echo.
echo 👍 Setup complete! Launch when ready with Start-webgame.bat echo 👍 Setup complete! Launch when ready with scripts/Start-webgame.bat
) )
) else ( ) else (

View File

@ -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) => { ipcMain.handle('copy-video', async (event, sourcePath, destPath) => {
try { try {
await fs.mkdir(path.dirname(destPath), { recursive: true }); await fs.mkdir(path.dirname(destPath), { recursive: true });

View File

@ -7,6 +7,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
selectImages: () => ipcRenderer.invoke('select-images'), selectImages: () => ipcRenderer.invoke('select-images'),
selectDirectory: () => ipcRenderer.invoke('select-directory'), selectDirectory: () => ipcRenderer.invoke('select-directory'),
readDirectory: (dirPath) => ipcRenderer.invoke('read-directory', dirPath), 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), copyImage: (sourcePath, destPath) => ipcRenderer.invoke('copy-image', sourcePath, destPath),
// Audio file operations // Audio file operations

View File

@ -289,6 +289,11 @@ class PlayerStats {
awardXP(amount, source = 'general') { awardXP(amount, source = 'general') {
console.log(`🏆 Awarded ${amount} XP from ${source}`); 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; this.stats.totalXP += amount;
// Track by source // Track by source
@ -307,13 +312,28 @@ class PlayerStats {
this.updateDailyStats('xp', amount); this.updateDailyStats('xp', amount);
this.saveStats(); 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 // Dispatch event to trigger display updates
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
window.dispatchEvent(new CustomEvent('playerStatsUpdated', { window.dispatchEvent(new CustomEvent('playerStatsUpdated', {
detail: { detail: {
totalXP: this.stats.totalXP, totalXP: this.stats.totalXP,
source: source, source: source,
amount: amount amount: amount,
oldLevel: oldLevel,
newLevel: newLevel,
leveledUp: newLevel.level > oldLevel.level
} }
})); }));
console.log(`📊 Dispatched playerStatsUpdated event: ${amount} XP from ${source}`); console.log(`📊 Dispatched playerStatsUpdated event: ${amount} XP from ${source}`);
@ -345,6 +365,198 @@ class PlayerStats {
this.updateDailyStats('tasksCompleted', 1); this.updateDailyStats('tasksCompleted', 1);
this.saveStats(); 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 ===== // ===== DAILY STATS TRACKING =====

View File

@ -902,6 +902,22 @@
</select> </select>
</div> </div>
<!-- TTS Controls -->
<div class="control-group">
<label class="control-label">🔊 Voice Narration</label>
<div class="control-row">
<button id="sidebar-tts-toggle" onclick="toggleTTS()" class="control-btn" title="Toggle voice narration on/off">
🔊 ON
</button>
<button id="sidebar-tts-stop" onclick="stopTTS()" class="control-btn" title="Stop current speech">
⏹️ Stop
</button>
</div>
<div id="sidebar-tts-status" class="control-label" style="margin-top: 5px; font-size: 0.75rem; color: #9CA3AF;">
Voice ready
</div>
</div>
<!-- Video Info --> <!-- Video Info -->
<div class="control-group"> <div class="control-group">
<div class="video-info"> <div class="video-info">
@ -1066,6 +1082,10 @@
let isVideoPlaying = true; let isVideoPlaying = true;
let videoControlListenersInitialized = false; let videoControlListenersInitialized = false;
// Scenario System Variables
let currentScenarioTask = null;
let currentScenarioStep = 'start';
// TTS Variables // TTS Variables
let voiceManager = null; let voiceManager = null;
let ttsEnabled = true; let ttsEnabled = true;
@ -1167,15 +1187,33 @@
function toggleTTS() { function toggleTTS() {
ttsEnabled = !ttsEnabled; 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) { if (ttsEnabled) {
toggleBtn.innerHTML = '🔊 Voice: ON'; if (headerToggleBtn) {
toggleBtn.style.background = 'linear-gradient(135deg, #8B5CF6, #EC4899)'; 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'); updateTTSStatus('Voice enabled');
speakText('Voice narration enabled.'); speakText('Voice narration enabled.');
} else { } else {
toggleBtn.innerHTML = '🔇 Voice: OFF'; if (headerToggleBtn) {
toggleBtn.style.background = 'linear-gradient(135deg, #6B7280, #9CA3AF)'; 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'); updateTTSStatus('Voice disabled');
stopTTS(); stopTTS();
} }
@ -1196,9 +1234,16 @@
// Update TTS status display // Update TTS status display
function updateTTSStatus(status) { function updateTTSStatus(status) {
const statusEl = document.getElementById('tts-status-text'); // Update header status
if (statusEl) { const headerStatusEl = document.getElementById('tts-status-text');
statusEl.textContent = status; 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 ===== // ===== END TTS FUNCTIONS =====
// Helper function to get format from file path // Helper function to get format from file path
@ -1878,6 +1947,9 @@
} }
}, 100); }, 100);
// Initialize sidebar TTS controls state
initializeSidebarTTSControls();
console.log('✅ Video control listeners set up successfully'); console.log('✅ Video control listeners set up successfully');
videoControlListenersInitialized = true; videoControlListenersInitialized = true;
} }
@ -1977,8 +2049,27 @@
const modeName = availableModes[selectedTrainingMode]?.name || selectedTrainingMode; const modeName = availableModes[selectedTrainingMode]?.name || selectedTrainingMode;
speakText(`Starting ${modeName} training session. Your journey begins now.`); speakText(`Starting ${modeName} training session. Your journey begins now.`);
// Hide setup interface // Hide most setup interface but keep TTS controls accessible
document.querySelector('.academy-header').style.display = 'none'; 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('.library-status').style.display = 'none';
document.querySelector('.training-controls').style.display = 'none'; document.querySelector('.training-controls').style.display = 'none';
document.querySelector('.academy-start-controls').style.display = 'none'; document.querySelector('.academy-start-controls').style.display = 'none';
@ -2462,7 +2553,7 @@
` : ''} ` : ''}
<div class="training-controls"> <div class="training-controls">
<button onclick="completeTrainingTask()" class="complete-btn">Complete Task</button> <button onclick="completeTrainingTask()" class="complete-btn">Complete Task</button>
<button onclick="loadNextTrainingTask()" class="next-btn">Next Task</button> <button onclick="showQuitConfirmation()" class="next-btn">Quit Training</button>
</div> </div>
</div> </div>
`; `;
@ -2532,7 +2623,7 @@
` : ''} ` : ''}
<div class="training-controls"> <div class="training-controls">
<button onclick="completeTrainingTask()" class="complete-btn">Complete Task</button> <button onclick="completeTrainingTask()" class="complete-btn">Complete Task</button>
<button onclick="loadNextTrainingTask()" class="next-btn">Next Task</button> <button onclick="showQuitConfirmation()" class="next-btn">Quit Training</button>
</div> </div>
</div> </div>
`; `;
@ -2712,6 +2803,372 @@
returnToModeSelection(); 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 = `
<div class="confirmation-backdrop"></div>
<div class="confirmation-modal">
<div class="confirmation-header">
<div class="confirmation-icon">${dialogContent.icon}</div>
<h3>${dialogContent.title}</h3>
</div>
<div class="confirmation-content">
<p>${dialogContent.message}</p>
<div class="confirmation-warning">
<span class="warning-icon">${dialogContent.warningIcon}</span>
<span>${dialogContent.warning}</span>
</div>
</div>
<div class="confirmation-actions">
<button class="confirm-btn quit-btn" onclick="confirmQuitTraining()">
<span class="btn-icon">${dialogContent.quitIcon || '🚪'}</span>
<span>${dialogContent.quitText || 'Quit Training'}</span>
</button>
<button class="confirm-btn continue-btn" onclick="cancelQuitTraining()">
<span class="btn-icon">${dialogContent.continueIcon || '💪'}</span>
<span>${dialogContent.continueText || 'Continue Training'}</span>
</button>
</div>
</div>
`;
// Add styles
confirmOverlay.innerHTML += `
<style>
#quit-confirmation-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 10000;
display: flex;
align-items: center;
justify-content: center;
animation: fadeIn 0.3s ease-out;
}
.confirmation-backdrop {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
backdrop-filter: blur(5px);
}
.confirmation-modal {
position: relative;
background: linear-gradient(145deg, #1a1a1a, #2d1b3d);
border: 2px solid #ff1744;
border-radius: 20px;
padding: 2rem;
max-width: 450px;
width: 90%;
box-shadow: 0 20px 40px rgba(255, 23, 68, 0.3), 0 0 20px rgba(255, 23, 68, 0.1);
animation: modalSlideIn 0.4s ease-out;
text-align: center;
}
.confirmation-header {
margin-bottom: 1.5rem;
}
.confirmation-icon {
font-size: 3rem;
margin-bottom: 0.5rem;
filter: drop-shadow(0 0 10px rgba(255, 193, 7, 0.5));
}
.confirmation-header h3 {
color: #ff1744;
font-size: 1.5rem;
font-weight: 700;
margin: 0;
text-shadow: 0 0 10px rgba(255, 23, 68, 0.3);
}
.confirmation-content {
margin-bottom: 2rem;
}
.confirmation-content p {
color: #e0e0e0;
font-size: 1.1rem;
margin-bottom: 1rem;
line-height: 1.5;
}
.confirmation-warning {
background: linear-gradient(135deg, rgba(255, 193, 7, 0.1), rgba(255, 152, 0, 0.1));
border: 1px solid rgba(255, 193, 7, 0.3);
border-radius: 10px;
padding: 0.75rem;
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.9rem;
color: #ffc107;
}
.warning-icon {
font-size: 1.2rem;
filter: drop-shadow(0 0 5px rgba(255, 193, 7, 0.5));
}
.confirmation-actions {
display: flex;
gap: 1rem;
justify-content: center;
}
.confirm-btn {
background: linear-gradient(135deg, #2d1b3d, #1a1a1a);
border: 2px solid;
border-radius: 15px;
padding: 0.75rem 1.5rem;
color: white;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
gap: 0.5rem;
transition: all 0.3s ease;
min-width: 150px;
justify-content: center;
}
.quit-btn {
border-color: #ff1744;
box-shadow: 0 0 15px rgba(255, 23, 68, 0.3);
}
.quit-btn:hover {
background: linear-gradient(135deg, #ff1744, #d81b60);
transform: translateY(-2px);
box-shadow: 0 5px 20px rgba(255, 23, 68, 0.5);
}
.continue-btn {
border-color: #4caf50;
box-shadow: 0 0 15px rgba(76, 175, 80, 0.3);
}
.continue-btn:hover {
background: linear-gradient(135deg, #4caf50, #388e3c);
transform: translateY(-2px);
box-shadow: 0 5px 20px rgba(76, 175, 80, 0.5);
}
.btn-icon {
font-size: 1.1rem;
filter: drop-shadow(0 0 5px currentColor);
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes modalSlideIn {
from {
transform: translateY(-50px) scale(0.9);
opacity: 0;
}
to {
transform: translateY(0) scale(1);
opacity: 1;
}
}
@keyframes fadeOut {
from { opacity: 1; }
to { opacity: 0; }
}
</style>
`;
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() { function returnToModeSelection() {
console.log('🔄 Returning to training mode selection...'); console.log('🔄 Returning to training mode selection...');
@ -2744,14 +3201,71 @@
} }
// Show setup interface again // Show setup interface again
document.querySelector('.academy-header').style.display = 'block'; const academyHeader = document.querySelector('.academy-header');
document.querySelector('.library-status').style.display = 'block'; if (academyHeader) {
document.querySelector('.training-controls').style.display = 'block'; // Restore all children visibility
document.querySelector('.academy-start-controls').style.display = 'block'; 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'); 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 // Reset training mode cards
document.querySelectorAll('.training-mode-card').forEach(card => { document.querySelectorAll('.training-mode-card').forEach(card => {
@ -2771,8 +3285,6 @@
} }
// Scenario Adventure Display System // Scenario Adventure Display System
let currentScenarioStep = 'start';
let currentScenarioTask = null;
function displayScenarioAdventure(container, task) { function displayScenarioAdventure(container, task) {
console.log('🎭 Displaying scenario adventure:', task.id); console.log('🎭 Displaying scenario adventure:', task.id);
@ -2793,6 +3305,16 @@
} }
function displayScenarioStep(container, stepId) { 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 scenario = currentScenarioTask.interactiveData;
const step = scenario.steps[stepId]; const step = scenario.steps[stepId];
@ -2821,7 +3343,7 @@
`).join('')} `).join('')}
</div> </div>
<div class="training-controls"> <div class="training-controls">
<button onclick="loadNextTrainingTask()" class="next-btn">Quit Scenario</button> <button onclick="showQuitConfirmation()" class="next-btn">Quit Training</button>
</div> </div>
</div> </div>
`; `;
@ -2834,7 +3356,7 @@
</div> </div>
<div class="training-controls"> <div class="training-controls">
<button onclick="selectScenarioChoice('${step.nextStep}')" class="action-btn">Continue</button> <button onclick="selectScenarioChoice('${step.nextStep}')" class="action-btn">Continue</button>
<button onclick="loadNextTrainingTask()" class="next-btn">Quit Training</button> <button onclick="showQuitConfirmation()" class="next-btn">Quit Training</button>
</div> </div>
</div> </div>
`; `;
@ -2856,7 +3378,7 @@
</div> </div>
<div class="training-controls"> <div class="training-controls">
<button onclick="startMirrorAction('${step.nextStep}', ${step.duration})" class="action-btn">Start Mirror Task</button> <button onclick="startMirrorAction('${step.nextStep}', ${step.duration})" class="action-btn">Start Mirror Task</button>
<button onclick="selectScenarioChoice('${step.nextStep}')" class="skip-btn">Quit Training</button> <button onclick="showQuitConfirmation()" class="skip-btn">Quit Training</button>
</div> </div>
</div> </div>
`; `;
@ -2911,7 +3433,7 @@
<button id="complete-action-btn" onclick="completeScenarioAction('${step.nextStep}')" class="complete-btn" disabled> <button id="complete-action-btn" onclick="completeScenarioAction('${step.nextStep}')" class="complete-btn" disabled>
Action Complete (<span id="btn-timer">${Math.floor(duration/60)}:${(duration%60).toString().padStart(2,'0')}</span>) Action Complete (<span id="btn-timer">${Math.floor(duration/60)}:${(duration%60).toString().padStart(2,'0')}</span>)
</button> </button>
<button onclick="selectScenarioChoice('${step.nextStep}')" class="next-btn">Quit Training</button> <button onclick="showQuitConfirmation()" class="next-btn">Quit Training</button>
</div> </div>
</div> </div>
`; `;
@ -2954,7 +3476,7 @@
</div> </div>
<div class="training-controls"> <div class="training-controls">
<button onclick="startPositionVerification('${step.nextStep}', ${step.verificationDuration}, '${step.verificationInstructions}', '${step.verificationText}')" class="action-btn">🔍 Start Position Verification</button> <button onclick="startPositionVerification('${step.nextStep}', ${step.verificationDuration}, '${step.verificationInstructions}', '${step.verificationText}')" class="action-btn">🔍 Start Position Verification</button>
<button onclick="selectScenarioChoice('${step.nextStep}')" class="skip-btn">Quit Training</button> <button onclick="showQuitConfirmation()" class="skip-btn">Quit Training</button>
</div> </div>
</div> </div>
`; `;
@ -2971,7 +3493,7 @@
</div> </div>
<div class="training-controls"> <div class="training-controls">
<button onclick="completeTrainingTask()" class="complete-btn">Complete Training</button> <button onclick="completeTrainingTask()" class="complete-btn">Complete Training</button>
<button onclick="loadNextTrainingTask()" class="next-btn">Next Task</button> <button onclick="showQuitConfirmation()" class="next-btn">Quit Training</button>
</div> </div>
</div> </div>
`; `;
@ -3375,7 +3897,7 @@
</div> </div>
<div class="training-controls"> <div class="training-controls">
<button onclick="startSimpleFocusSession(${task.duration || 60})" class="complete-btn">Start Focus Session</button> <button onclick="startSimpleFocusSession(${task.duration || 60})" class="complete-btn">Start Focus Session</button>
<button onclick="loadNextTrainingTask()" class="next-btn">Quit Training</button> <button onclick="showQuitConfirmation()" class="next-btn">Quit Training</button>
</div> </div>
</div> </div>
`; `;
@ -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 // Start initialization when DOM is loaded
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
console.log('🎓 Training Academy DOM loaded'); console.log('🎓 Training Academy DOM loaded');
@ -3557,6 +4091,26 @@
setTimeout(initializeTrainingAcademy, 1000); 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 // Listen for game ready events
window.addEventListener('gameReady', (event) => { window.addEventListener('gameReady', (event) => {
console.log('🎮 Game engine ready for training'); console.log('🎮 Game engine ready for training');
@ -3601,21 +4155,20 @@
throw new Error('Failed to start webcam session'); throw new Error('Failed to start webcam session');
} }
// Monitor the WebcamManager's captured photos array // Monitor photo progress for display only (completion handled by WebcamManager)
const originalCapturedPhotosLength = currentWebcamManager.capturedPhotos.length;
const checkPhotoProgress = setInterval(() => { const checkPhotoProgress = setInterval(() => {
const newPhotoCount = currentWebcamManager.capturedPhotos.length - originalCapturedPhotosLength; // Only update display, don't trigger completion
if (newPhotoCount > photosCaptured) { const sessionPhotos = currentWebcamManager.currentPhotoSession ?
currentWebcamManager.currentPhotoSession.photos.length : 0;
const newPhotoCount = sessionPhotos;
if (newPhotoCount !== photosCaptured) {
photosCaptured = newPhotoCount; photosCaptured = newPhotoCount;
updatePhotoProgress(); updatePhotoProgress();
// Check if we've reached the target // Don't auto-complete here - let WebcamManager handle it
if (photosCaptured >= photosNeeded) { // The photoSessionComplete event listener will handle completion
clearInterval(checkPhotoProgress);
setTimeout(completePhotoSession, 1500); // Delay to show final photo
}
} }
}, 500); // Check every 500ms }, 500); // Check every 500ms for display updates only
// Store the interval so we can clear it if needed // Store the interval so we can clear it if needed
window.photoProgressInterval = checkPhotoProgress; window.photoProgressInterval = checkPhotoProgress;
@ -3665,6 +4218,12 @@
} }
function completePhotoSession() { function completePhotoSession() {
// Prevent multiple calls
if (!photoSessionActive) {
console.log('📸 Photo session already completed, skipping');
return;
}
console.log('📸 Photo session completed'); console.log('📸 Photo session completed');
photoSessionActive = false; photoSessionActive = false;
@ -3674,8 +4233,9 @@
window.photoProgressInterval = null; window.photoProgressInterval = null;
} }
// Get newly captured photos from WebcamManager // Get newly captured photos from WebcamManager's current session
const capturedPhotos = currentWebcamManager ? currentWebcamManager.capturedPhotos : []; const capturedPhotos = currentWebcamManager && currentWebcamManager.currentPhotoSession ?
currentWebcamManager.currentPhotoSession.photos : [];
console.log(`📸 Found ${capturedPhotos.length} captured photos`); console.log(`📸 Found ${capturedPhotos.length} captured photos`);
// Add captured photos to training photo library // Add captured photos to training photo library
@ -3696,22 +4256,9 @@
`; `;
} }
// Close webcam interface // WebcamManager has already ended the session when firing photoSessionComplete event
if (currentWebcamManager) { // Just ensure we clean up our training academy state
// Stop the camera using WebcamManager's method console.log('📸 Photo session completion handled by training academy');
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;
}
} }
// Add captured photos to the training photo library // Add captured photos to the training photo library

View File

@ -689,8 +689,9 @@
document.getElementById('playlists-created').textContent = stats.playlistsCreated; document.getElementById('playlists-created').textContent = stats.playlistsCreated;
// Calculate level based on XP // Calculate level based on XP
const level = this.calculateLevel(rawStats.totalXP || 0); const levelData = this.calculateLevel(rawStats.totalXP || 0);
const levelInfo = this.getLevelInfo(level); const level = levelData.level;
const levelInfo = levelData;
document.getElementById('current-level').textContent = `${level} - ${levelInfo.name}`; document.getElementById('current-level').textContent = `${level} - ${levelInfo.name}`;
// Update level description // Update level description
@ -722,35 +723,79 @@
this.updateAchievements(rawStats); this.updateAchievements(rawStats);
} }
calculateLevel(totalXp) { // ===== EXPONENTIAL LEVEL SYSTEM (MATCHES PLAYERSTATS) =====
// Level up every 100 XP // Exponential XP scaling: starts at 10 XP for level 2, then grows exponentially
return Math.floor(totalXp / 100) + 1; 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) { getLevelInfo(level) {
const levelData = { const levelData = this.getLevelData();
1: { name: 'Virgin', icon: '🌱', description: 'Your first taste of pleasure awaits' }, const levelInfo = levelData.find(l => l.level === level);
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' }
};
// For levels beyond 20, continue with Omnipotent // For levels beyond 20, continue with Omnipotent
if (level > 20) { if (level > 20) {
return { return {
@ -760,20 +805,16 @@
}; };
} }
return levelData[level] || { name: 'Unknown', icon: '❓', description: 'Mysterious level' }; return levelInfo || { name: 'Unknown', icon: '❓', description: 'Mysterious level' };
} }
updateLevelProgress(totalXp) { updateLevelProgress(totalXp) {
const currentLevel = this.calculateLevel(totalXp); const levelInfo = this.calculateLevel(totalXp);
const currentLevelStart = (currentLevel - 1) * 100; const progressPercent = levelInfo.progressPercent;
const nextLevelStart = currentLevel * 100;
const progressInLevel = totalXp - currentLevelStart;
const levelDuration = nextLevelStart - currentLevelStart;
const progressPercent = (progressInLevel / levelDuration) * 100;
document.getElementById('level-fill').style.width = progressPercent + '%'; document.getElementById('level-fill').style.width = progressPercent + '%';
document.getElementById('level-text').textContent = Math.round(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() { initializeAchievements() {
@ -825,21 +866,36 @@
icon: '<27>', icon: '<27>',
title: 'Lustful Awakening', title: 'Lustful Awakening',
description: 'Reach Level 5 (Lustful)', 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', id: 'kinky-elite',
icon: '💎', icon: '💎',
title: 'Kinky Elite', title: 'Kinky Elite',
description: 'Reach Level 10 (Kinky)', 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', id: 'depraved-master',
icon: '🔮', icon: '🔮',
title: 'Depraved Master', title: 'Depraved Master',
description: 'Reach Level 12 (Depraved)', 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', id: 'collector',