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
├── 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

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)**
#### **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

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' };
};
// ===== 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>

View File

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

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

View File

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

View File

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

View File

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

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) => {
try {
await fs.mkdir(path.dirname(destPath), { recursive: true });

View File

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

View File

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

View File

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

View File

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