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:
parent
ec45cf69f8
commit
c40ed278e0
12
README.md
12
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
1618
hypno-gallery.html
1618
hypno-gallery.html
File diff suppressed because it is too large
Load Diff
36
index.html
36
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);
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
|
|
|
|||
308
porn-cinema.html
308
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 = `
|
||||
<div class="confirmation-modal-content">
|
||||
<div class="confirmation-modal-header">
|
||||
<h3>🏠 Return to Home</h3>
|
||||
<div class="confirmation-modal-header cinema-attendant-header">
|
||||
<div class="attendant-avatar">${attendantDialog.avatar}</div>
|
||||
<h3>${attendantDialog.title}</h3>
|
||||
</div>
|
||||
<div class="confirmation-modal-body">
|
||||
<p>Are you sure you want to return to the home screen?</p>
|
||||
<p class="warning-text">⚠️ Current playback will stop and any unsaved progress will be lost.</p>
|
||||
<div class="confirmation-modal-body cinema-attendant-body">
|
||||
<p class="attendant-main-message">${attendantDialog.message}</p>
|
||||
<div class="attendant-snark">
|
||||
<span class="snark-icon">${attendantDialog.snarkIcon}</span>
|
||||
<span class="snark-text">${attendantDialog.snark}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="confirmation-modal-actions">
|
||||
<button class="btn-confirm-exit">Yes, Go Home</button>
|
||||
<button class="btn-cancel-exit">Stay in Cinema</button>
|
||||
<button class="btn-confirm-exit cinema-exit-btn">
|
||||
<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>
|
||||
`;
|
||||
|
||||
// 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 += `
|
||||
<style>
|
||||
.cinema-attendant-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: rgba(0, 0, 0, 0.85);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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...');
|
||||
|
|
|
|||
|
|
@ -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...
|
||||
|
|
@ -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)
|
||||
|
|
@ -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 (
|
||||
|
|
@ -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 });
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 =====
|
||||
|
||||
|
|
|
|||
|
|
@ -902,6 +902,22 @@
|
|||
</select>
|
||||
</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 -->
|
||||
<div class="control-group">
|
||||
<div class="video-info">
|
||||
|
|
@ -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 @@
|
|||
` : ''}
|
||||
<div class="training-controls">
|
||||
<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>
|
||||
`;
|
||||
|
|
@ -2532,7 +2623,7 @@
|
|||
` : ''}
|
||||
<div class="training-controls">
|
||||
<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>
|
||||
`;
|
||||
|
|
@ -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 = `
|
||||
<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() {
|
||||
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('')}
|
||||
</div>
|
||||
<div class="training-controls">
|
||||
<button onclick="loadNextTrainingTask()" class="next-btn">Quit Scenario</button>
|
||||
<button onclick="showQuitConfirmation()" class="next-btn">Quit Training</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -2834,7 +3356,7 @@
|
|||
</div>
|
||||
<div class="training-controls">
|
||||
<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>
|
||||
`;
|
||||
|
|
@ -2856,7 +3378,7 @@
|
|||
</div>
|
||||
<div class="training-controls">
|
||||
<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>
|
||||
`;
|
||||
|
|
@ -2911,7 +3433,7 @@
|
|||
<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>)
|
||||
</button>
|
||||
<button onclick="selectScenarioChoice('${step.nextStep}')" class="next-btn">Quit Training</button>
|
||||
<button onclick="showQuitConfirmation()" class="next-btn">Quit Training</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -2954,7 +3476,7 @@
|
|||
</div>
|
||||
<div class="training-controls">
|
||||
<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>
|
||||
`;
|
||||
|
|
@ -2971,7 +3493,7 @@
|
|||
</div>
|
||||
<div class="training-controls">
|
||||
<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>
|
||||
`;
|
||||
|
|
@ -3375,7 +3897,7 @@
|
|||
</div>
|
||||
<div class="training-controls">
|
||||
<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>
|
||||
`;
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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: '<27>',
|
||||
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',
|
||||
|
|
|
|||
Loading…
Reference in New Issue