Fix: Critical syntax error and storage quota issues
- Fixed missing arrow (=>) in interactiveTaskManager.js line 5677 - Added null checks for DOM elements in async intervals (dynamic captions, sensory overload tasks) - Disabled auto-backup to prevent localStorage quota exceeded errors - Implemented aggressive emergency cleanup (removes all backups and photo data) - Added global QuotaExceededError handler with automatic cleanup - Added BackupManager to training-academy.html with initialization - Enhanced preference save error handling with user feedback - Save validation prevents level progression if preferences fail to save - All photo data now removed from localStorage to conserve space
This commit is contained in:
parent
49fc81418d
commit
b5856b21ae
Binary file not shown.
|
|
@ -0,0 +1,212 @@
|
|||
# Training Academy - Phase 1: Campaign Foundation ✅ COMPLETE
|
||||
|
||||
**Completion Date:** November 29, 2025
|
||||
**Status:** All subphases implemented, tested, and integrated
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 Overview
|
||||
|
||||
Phase 1 establishes the core foundation systems for The Academy training mode, including campaign progression, user preferences, media library management, and UI integration.
|
||||
|
||||
### Subphase 1.1: Campaign Progression System ✅
|
||||
**File:** `src/features/academy/campaignManager.js` (359 lines)
|
||||
|
||||
**Implemented Features:**
|
||||
- 30-level campaign structure across 6 story arcs
|
||||
- Checkpoint detection system (levels 5, 10, 15, 20, 25, 30)
|
||||
- Level unlock/lock system with persistence
|
||||
- Arc progression tracking
|
||||
- Campaign state management (localStorage persistence)
|
||||
- Test suite with all core functionality validated
|
||||
|
||||
**Test Results:** ✅ All tests passed
|
||||
- Level progression working correctly
|
||||
- Can't proceed to Level 2 until Level 1 complete (intended behavior)
|
||||
- Checkpoint detection accurate
|
||||
- State persists across sessions
|
||||
|
||||
---
|
||||
|
||||
### Subphase 1.2: Preference Management System ✅
|
||||
**File:** `src/features/academy/preferenceManager.js` (475 lines)
|
||||
|
||||
**Implemented Features:**
|
||||
- 8 preference categories:
|
||||
- Content themes (16 options)
|
||||
- Visual styles (12 options)
|
||||
- Intensity levels (7 options)
|
||||
- Audio preferences (9 options)
|
||||
- Duration preferences (6 options)
|
||||
- Gender preferences (8 options)
|
||||
- Specialty content (10 options)
|
||||
- Quality preferences (6 options)
|
||||
- Checkpoint preference updates with confirmation modals
|
||||
- Content filtering system for media selection
|
||||
- Preference validation and defaults
|
||||
- Persistence with localStorage
|
||||
|
||||
**Test Results:** ✅ Seems fine for now
|
||||
- All preference categories working
|
||||
- Checkpoint modals displaying correctly
|
||||
- Preferences saved and loaded successfully
|
||||
|
||||
---
|
||||
|
||||
### Subphase 1.3: Library Tagging System ✅
|
||||
**File:** `src/features/academy/libraryManager.js` (711 lines)
|
||||
|
||||
**Implemented Features:**
|
||||
- **Integration with existing video library:**
|
||||
- `syncWithExistingLibrary()` - Imports videos from `desktopFileManager.getAllVideos()`
|
||||
- Fallback to `localStorage.unifiedVideoLibrary`
|
||||
- Preserves Academy metadata (tags, ratings) when re-syncing
|
||||
- Updates source metadata (duration, fileSize, format)
|
||||
- Tag management system:
|
||||
- 74 predefined tags across 6 categories
|
||||
- Add/remove tags from media
|
||||
- Bulk tag operations
|
||||
- Tag usage statistics
|
||||
- Rating system (1-5 stars)
|
||||
- Favorites tracking
|
||||
- Collections/playlists
|
||||
- Search and filter by tags, ratings, duration
|
||||
- Media statistics and analytics
|
||||
- Preference-based media selection
|
||||
|
||||
**Test Results:** ✅ Populated
|
||||
- Library synced with existing videos
|
||||
- Tags and ratings working
|
||||
- Search functionality operational
|
||||
|
||||
**Integration Notes:**
|
||||
- Academy library is a **metadata layer** on top of existing video library
|
||||
- `desktopFileManager` manages files, `academyLibrary` manages training metadata
|
||||
- No duplicate library systems - single source of truth
|
||||
- Auto-syncs when checkpoint modals opened or training sessions started
|
||||
|
||||
---
|
||||
|
||||
### Subphase 1.4: UI Integration ✅
|
||||
**File:** `src/features/academy/academyUI.js` (594 lines)
|
||||
**Styles:** `src/styles/academy-ui.css` (full styling)
|
||||
|
||||
**Implemented Features:**
|
||||
- Level selection screen with 30-level grid
|
||||
- Visual arc indicators and checkpoint badges
|
||||
- Arc navigation (6 story arcs)
|
||||
- Level info display (title, description, objectives, rewards)
|
||||
- Checkpoint modals with 4 tabs:
|
||||
- Preferences editor
|
||||
- Library statistics
|
||||
- Progress summary
|
||||
- Rewards preview
|
||||
- Integration with campaign, preference, and library managers
|
||||
- Level start screen with session preparation
|
||||
- Content filtering based on user preferences
|
||||
- Responsive design matching game's dark theme
|
||||
|
||||
**Test Results:** ✅ All of this seems to work
|
||||
- Level select UI rendering correctly
|
||||
- Checkpoint modals displaying all tabs
|
||||
- Preferences can be updated at checkpoints
|
||||
- Library stats showing synced videos
|
||||
- Arc progression visual feedback working
|
||||
|
||||
---
|
||||
|
||||
## Integration with Existing Systems
|
||||
|
||||
### Files Modified:
|
||||
1. **training-academy.html**
|
||||
- Added Academy module script tags:
|
||||
- `src/features/academy/campaignManager.js`
|
||||
- `src/features/academy/preferenceManager.js`
|
||||
- `src/features/academy/libraryManager.js`
|
||||
- `src/features/academy/academyUI.js`
|
||||
- Scripts load after `gameData.js` (required dependency)
|
||||
|
||||
### Dependencies:
|
||||
- `window.gameData` - Campaign state, preferences, library metadata
|
||||
- `window.desktopFileManager` - Video/photo file management (primary source)
|
||||
- `localStorage.unifiedVideoLibrary` - Fallback video source
|
||||
- Existing video player systems (videoPlayerManager, overlayVideoPlayer)
|
||||
|
||||
### Data Flow:
|
||||
```
|
||||
desktopFileManager.getAllVideos()
|
||||
↓
|
||||
libraryManager.syncWithExistingLibrary()
|
||||
↓
|
||||
academyLibrary.media (adds tags/ratings metadata)
|
||||
↓
|
||||
preferenceManager.getContentFilter()
|
||||
↓
|
||||
libraryManager.getMediaForPreferences()
|
||||
↓
|
||||
Training session video selection
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Test Files Created
|
||||
|
||||
**Test files for isolated module testing:**
|
||||
- `test-campaign-manager.html` - Campaign progression tests
|
||||
- `test-preference-manager.html` - Preference system tests
|
||||
- `test-library-manager.html` - Library tagging/search tests
|
||||
- `test-academy-ui.html` - UI component tests
|
||||
|
||||
**Note:** Test files require manual script loading (Academy modules not auto-initialized). This is expected - the main `training-academy.html` page has proper initialization.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 Deliverables Summary
|
||||
|
||||
| Component | File | Lines | Status |
|
||||
|-----------|------|-------|--------|
|
||||
| Campaign Manager | campaignManager.js | 359 | ✅ Complete |
|
||||
| Preference Manager | preferenceManager.js | 475 | ✅ Complete |
|
||||
| Library Manager | libraryManager.js | 711 | ✅ Complete |
|
||||
| Academy UI | academyUI.js | 594 | ✅ Complete |
|
||||
| UI Styles | academy-ui.css | ~600 | ✅ Complete |
|
||||
| **Total** | **5 files** | **~2,739 lines** | **100% Complete** |
|
||||
|
||||
---
|
||||
|
||||
## Next Steps: Phase 2
|
||||
|
||||
**Phase 2: Level Data & Content Configuration**
|
||||
|
||||
Main deliverable: `academyLevelData.js` with detailed configuration for all 30 levels including:
|
||||
- Task types and variations per level
|
||||
- Media requirements and filters
|
||||
- Difficulty curves and progression
|
||||
- Story elements and dialogue
|
||||
- Objectives and success criteria
|
||||
|
||||
**Estimated Time:** 15-20 hours
|
||||
**Ready to begin:** Yes - Phase 1 foundation complete
|
||||
|
||||
---
|
||||
|
||||
## Known Issues & Future Enhancements
|
||||
|
||||
### Current Limitations:
|
||||
1. **Training session integration:** `startTrainingSession()` currently just calls existing function - needs full integration with level config, preference filtering, and library selection
|
||||
2. **Test pages:** Don't auto-initialize Academy modules - by design, main page handles initialization
|
||||
3. **Persistence:** All using localStorage - may need IndexedDB for larger media libraries (future optimization)
|
||||
|
||||
### Phase 1 Success Criteria Met:
|
||||
- ✅ Campaign structure with 30 levels
|
||||
- ✅ User preference management at checkpoints
|
||||
- ✅ Media library tagging and organization
|
||||
- ✅ Integration with existing video library
|
||||
- ✅ UI for level selection and checkpoint modals
|
||||
- ✅ All systems persist state correctly
|
||||
- ✅ User tested and approved all subphases
|
||||
|
||||
---
|
||||
|
||||
**Phase 1 Status: COMPLETE ✅**
|
||||
**Ready for Phase 2: YES ✅**
|
||||
|
|
@ -0,0 +1,536 @@
|
|||
# Dress-Up Game Complete Redesign: Inventory-Based Photo Progression
|
||||
|
||||
## Core Concept
|
||||
|
||||
Player takes a **questionnaire** at the start to declare what items they have available. The game then creates a **personalized photo challenge progression** that escalates in intensity, using only the items the player owns.
|
||||
|
||||
**Flow:**
|
||||
1. Questionnaire → Inventory Assessment
|
||||
2. Custom Path Generation → Based on available items
|
||||
3. Progressive Photo Challenges → Escalating intensity
|
||||
4. Certificate Completion → Document transformation
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Initial Questionnaire
|
||||
|
||||
### Item Categories to Check
|
||||
|
||||
#### Clothing Items:
|
||||
- **Panties** (none / basic / sexy / multiple)
|
||||
- **Bras** (none / sports / regular / sexy / multiple)
|
||||
- **Dresses** (none / casual / formal / slutty / multiple)
|
||||
- **Skirts** (none / basic / mini / micro / multiple)
|
||||
- **Pantyhose/Stockings** (none / nude / fishnet / multiple)
|
||||
- **Heels/Shoes** (none / flats / heels / platform / boots)
|
||||
- **Wigs** (none / short / long / colored / multiple)
|
||||
- **Lingerie** (none / basic / sexy / extreme)
|
||||
|
||||
#### Accessories:
|
||||
- **Makeup** (none / basic / full kit)
|
||||
- **Jewelry** (none / basic / feminine / collar)
|
||||
- **Nail Polish** (none / basic / colors)
|
||||
- **Props** (mirror / phone stand / selfie stick)
|
||||
|
||||
#### Toys:
|
||||
- **Dildos** (none / small / medium / large / multiple)
|
||||
- **Butt Plugs** (none / small / medium / large / tail plug)
|
||||
- **Chastity Device** (none / cage / belt)
|
||||
- **Restraints** (none / cuffs / rope / spreader bar)
|
||||
- **Gags** (none / ball gag / ring gag)
|
||||
- **Nipple Clamps** (none / basic / weighted / chain)
|
||||
|
||||
#### Environment:
|
||||
- **Mirror** (yes / no)
|
||||
- **Full-length mirror** (yes / no)
|
||||
- **Private space** (yes / no)
|
||||
- **Camera/phone stand** (yes / no)
|
||||
|
||||
### Questionnaire UI
|
||||
|
||||
```javascript
|
||||
questionnaire_start: {
|
||||
type: 'inventory-check',
|
||||
story: "Before we begin your transformation photo session, I need to know what you have available. Be honest - this determines how intense we can make your experience.",
|
||||
categories: {
|
||||
clothing: {
|
||||
title: "👗 Clothing & Feminization Items",
|
||||
items: [
|
||||
{ id: 'panties', label: 'Panties', options: ['none', 'basic', 'sexy', 'multiple'] },
|
||||
{ id: 'bras', label: 'Bras', options: ['none', 'sports', 'regular', 'sexy', 'multiple'] },
|
||||
{ id: 'dresses', label: 'Dresses', options: ['none', 'casual', 'slutty', 'multiple'] },
|
||||
{ id: 'skirts', label: 'Skirts', options: ['none', 'basic', 'mini', 'micro', 'multiple'] },
|
||||
{ id: 'pantyhose', label: 'Pantyhose/Stockings', options: ['none', 'nude', 'fishnet', 'multiple'] },
|
||||
{ id: 'heels', label: 'Heels/Shoes', options: ['none', 'flats', 'heels', 'platforms'] },
|
||||
{ id: 'wigs', label: 'Wigs', options: ['none', 'short', 'long', 'colored'] },
|
||||
{ id: 'lingerie', label: 'Lingerie Sets', options: ['none', 'basic', 'sexy', 'extreme'] }
|
||||
]
|
||||
},
|
||||
accessories: {
|
||||
title: "💄 Makeup & Accessories",
|
||||
items: [
|
||||
{ id: 'makeup', label: 'Makeup Kit', options: ['none', 'basic', 'full'] },
|
||||
{ id: 'jewelry', label: 'Jewelry', options: ['none', 'basic', 'feminine', 'collar'] },
|
||||
{ id: 'nailPolish', label: 'Nail Polish', options: ['none', 'basic', 'colors'] }
|
||||
]
|
||||
},
|
||||
toys: {
|
||||
title: "🔞 Toys & Restraints",
|
||||
items: [
|
||||
{ id: 'dildos', label: 'Dildos', options: ['none', 'small', 'medium', 'large', 'multiple'] },
|
||||
{ id: 'plugs', label: 'Butt Plugs', options: ['none', 'small', 'medium', 'large', 'tail'] },
|
||||
{ id: 'chastity', label: 'Chastity Device', options: ['none', 'cage', 'belt'] },
|
||||
{ id: 'restraints', label: 'Restraints', options: ['none', 'cuffs', 'rope', 'spreader'] },
|
||||
{ id: 'gags', label: 'Gags', options: ['none', 'ball', 'ring'] },
|
||||
{ id: 'nippleClamps', label: 'Nipple Clamps', options: ['none', 'basic', 'weighted', 'chain'] }
|
||||
]
|
||||
},
|
||||
environment: {
|
||||
title: "📸 Setup & Environment",
|
||||
items: [
|
||||
{ id: 'mirror', label: 'Mirror Available', type: 'boolean' },
|
||||
{ id: 'fullMirror', label: 'Full-Length Mirror', type: 'boolean' },
|
||||
{ id: 'privateSpace', label: 'Private Space', type: 'boolean' },
|
||||
{ id: 'phoneStand', label: 'Phone/Camera Stand', type: 'boolean' }
|
||||
]
|
||||
}
|
||||
},
|
||||
nextStep: 'inventory_summary'
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Inventory Assessment & Path Generation
|
||||
|
||||
After questionnaire, the game analyzes inventory and generates a custom photo challenge path.
|
||||
|
||||
### Intensity Tiers Based on Inventory
|
||||
|
||||
**Tier 1 - Minimal Items (0-3 items):**
|
||||
- Basic poses, limited clothing
|
||||
- Focus on body positions and edging
|
||||
- Example: Just panties → 5-photo progression
|
||||
|
||||
**Tier 2 - Basic Wardrobe (4-7 items):**
|
||||
- Simple feminization path
|
||||
- Clothing combinations
|
||||
- Example: Panties + bra + skirt → 10-photo progression
|
||||
|
||||
**Tier 3 - Full Feminization (8-12 items):**
|
||||
- Complete outfit transformations
|
||||
- Makeup and accessories
|
||||
- Example: Full sissy transformation → 15-photo progression
|
||||
|
||||
**Tier 4 - Toy Integration (13-17 items):**
|
||||
- Feminization + toy usage
|
||||
- Edging with props
|
||||
- Example: Dressed + plugged + edging → 20-photo progression
|
||||
|
||||
**Tier 5 - Extreme Collection (18+ items):**
|
||||
- Complete transformation + restraints
|
||||
- Maximum degradation potential
|
||||
- Example: Full sissy in chastity, plugged, restrained → 25-photo progression
|
||||
|
||||
### Dynamic Path Creation
|
||||
|
||||
```javascript
|
||||
inventory_summary: {
|
||||
type: 'path-generation',
|
||||
story: "Based on what you have, here's your personalized transformation journey...",
|
||||
|
||||
// System generates custom progression based on inventory
|
||||
pathGeneration: {
|
||||
calculateTier: (inventory) => {
|
||||
const itemCount = Object.values(inventory).filter(v => v !== 'none').length;
|
||||
if (itemCount >= 18) return 5;
|
||||
if (itemCount >= 13) return 4;
|
||||
if (itemCount >= 8) return 3;
|
||||
if (itemCount >= 4) return 2;
|
||||
return 1;
|
||||
},
|
||||
|
||||
generatePhotoSteps: (inventory, tier) => {
|
||||
// Returns array of photo challenge steps customized to inventory
|
||||
}
|
||||
},
|
||||
|
||||
choices: [
|
||||
{
|
||||
text: "Begin my photo transformation journey",
|
||||
nextStep: "photo_challenge_1"
|
||||
},
|
||||
{
|
||||
text: "I need to adjust my inventory",
|
||||
nextStep: "questionnaire_start"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Progressive Photo Challenges
|
||||
|
||||
Each photo challenge escalates based on available items.
|
||||
|
||||
### Photo Challenge Structure
|
||||
|
||||
```javascript
|
||||
photo_challenge_X: {
|
||||
type: 'photo-verification',
|
||||
mood: 'escalating',
|
||||
|
||||
// Story adapts based on inventory tier
|
||||
story: "[Generated based on tier and items]",
|
||||
|
||||
// Photo requirements generated from inventory
|
||||
photoRequirements: {
|
||||
items: ['panties', 'bra'], // Items player must wear/use
|
||||
pose: 'selected_from_pose_bank', // Pose from inventory-aware bank
|
||||
edging: true/false, // Whether to edge during photo
|
||||
count: 3, // Number of photos required
|
||||
timer: 30 // Time limit per pose
|
||||
},
|
||||
|
||||
// Variations based on player's progress
|
||||
storyVariations: {
|
||||
first_time: "Let's start simple...",
|
||||
progressing: "You're doing well, now let's increase intensity...",
|
||||
advanced: "Time to push your limits..."
|
||||
},
|
||||
|
||||
nextStep: "photo_challenge_X+1"
|
||||
}
|
||||
```
|
||||
|
||||
### Example Progression Paths
|
||||
|
||||
#### Tier 1 Path (Minimal - Just Panties):
|
||||
1. **Photo 1**: Wearing panties, basic standing pose
|
||||
2. **Photo 2**: Panties + bent over display pose
|
||||
3. **Photo 3**: Panties + edging while posed
|
||||
4. **Photo 4**: Panties around ankles, exposed
|
||||
5. **Photo 5**: Panties + mirror shame (if mirror available)
|
||||
|
||||
#### Tier 3 Path (Full Feminization - Panties, Bra, Dress, Heels, Wig, Makeup):
|
||||
1. **Photo 1**: Panties + bra only, basic feminine pose
|
||||
2. **Photo 2**: Add dress, twirl pose
|
||||
3. **Photo 3**: Add heels, standing elegantly
|
||||
4. **Photo 4**: Add wig, full sissy display
|
||||
5. **Photo 5**: Add makeup, admiring in mirror
|
||||
6. **Photo 6**: Full outfit + curtsy pose
|
||||
7. **Photo 7**: Full outfit + sitting ladylike
|
||||
8. **Photo 8**: Full outfit + edging while dressed
|
||||
9. **Photo 9**: Dress hiked up, exposed beneath
|
||||
10. **Photo 10**: Lifting dress, showing panties shamefully
|
||||
11. **Photo 11**: On knees in full outfit
|
||||
12. **Photo 12**: Bent over in heels and dress
|
||||
13. **Photo 13**: Full outfit + presenting position
|
||||
14. **Photo 14**: Edging desperately while fully feminized
|
||||
15. **Photo 15**: Final transformation documentation
|
||||
|
||||
#### Tier 5 Path (Extreme - Full Collection):
|
||||
1-15: (Same as Tier 3)
|
||||
16. **Photo 16**: Add chastity cage, display locked state
|
||||
17. **Photo 17**: Add butt plug, show inserted
|
||||
18. **Photo 18**: Add dildo, pose with toy
|
||||
19. **Photo 19**: Add restraints, bound and helpless
|
||||
20. **Photo 20**: Add gag, silenced sissy
|
||||
21. **Photo 21**: Add nipple clamps, decorated
|
||||
22. **Photo 22**: Edging while fully equipped
|
||||
23. **Photo 23**: Toy usage demonstration
|
||||
24. **Photo 24**: Complete degradation display
|
||||
25. **Photo 25**: Final extreme documentation
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Dynamic Pose Bank System
|
||||
|
||||
Poses adapt to available inventory.
|
||||
|
||||
### Pose Categories by Item Type
|
||||
|
||||
#### Basic Poses (No items needed):
|
||||
- Standing neutral
|
||||
- Kneeling submission
|
||||
- Bent over display
|
||||
- Spread eagle
|
||||
- Squatting exposed
|
||||
|
||||
#### Panty Poses:
|
||||
- Panties on display
|
||||
- Panties around ankles
|
||||
- Pulling panties aside
|
||||
- Panty peek (bent over)
|
||||
- Panty edge (rubbing through fabric)
|
||||
|
||||
#### Bra Poses:
|
||||
- Bra adjustment
|
||||
- Bra display (cupping)
|
||||
- Straps showing
|
||||
- Sports bra flex
|
||||
- Bra reveal
|
||||
|
||||
#### Dress/Skirt Poses:
|
||||
- Twirl and spin
|
||||
- Dress lift (showing panties)
|
||||
- Sitting with legs crossed
|
||||
- Curtsy
|
||||
- Dress adjust (showing body)
|
||||
|
||||
#### Heel Poses:
|
||||
- Standing in heels
|
||||
- Walking pose
|
||||
- Heel up on surface
|
||||
- Kneeling in heels
|
||||
- Bent over in heels
|
||||
|
||||
#### Toy Poses:
|
||||
- Holding dildo to mouth
|
||||
- Toy insertion display
|
||||
- Plug reveal (bent over)
|
||||
- Chastity cage show
|
||||
- Restrained position
|
||||
- Gagged silence
|
||||
- Clamped nipples display
|
||||
|
||||
#### Edging Poses (any outfit):
|
||||
- Edge while standing
|
||||
- Edge while kneeling
|
||||
- Edge while bent over
|
||||
- Edge with toy
|
||||
- Edge in mirror
|
||||
- Hands-free edge (thigh squeeze)
|
||||
|
||||
### Pose Selection Logic
|
||||
|
||||
```javascript
|
||||
selectPoseForChallenge(inventory, challengeNumber, tier) {
|
||||
// Get available pose categories based on inventory
|
||||
const availableCategories = this.getAvailableCategories(inventory);
|
||||
|
||||
// Escalate intensity based on challenge number
|
||||
const intensity = this.calculateIntensity(challengeNumber, tier);
|
||||
|
||||
// Select appropriate pose
|
||||
const pose = this.getPoseByIntensity(availableCategories, intensity);
|
||||
|
||||
// Ensure no repeats
|
||||
if (this.usedPoses.has(pose.name)) {
|
||||
return this.selectPoseForChallenge(inventory, challengeNumber, tier);
|
||||
}
|
||||
|
||||
this.usedPoses.add(pose.name);
|
||||
return pose;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: Certificate Endings
|
||||
|
||||
Endings adapt to tier and items used.
|
||||
|
||||
### Tier-Based Certificates
|
||||
|
||||
**Tier 1 Certificate - 🩲 Basic Exposure:**
|
||||
- Pink/white gradient
|
||||
- "EXPOSED AND DOCUMENTED"
|
||||
- Shows items used: "Panties"
|
||||
- Photo count: 5 photos
|
||||
|
||||
**Tier 2 Certificate - 👗 Feminization Beginner:**
|
||||
- Purple/pink gradient
|
||||
- "FEMINIZATION JOURNEY DOCUMENTED"
|
||||
- Shows items used: "Panties, Bra, Skirt"
|
||||
- Photo count: 10 photos
|
||||
|
||||
**Tier 3 Certificate - 🎀 Complete Sissy Transformation:**
|
||||
- Hot pink/gold gradient
|
||||
- "COMPLETE SISSY TRANSFORMATION ACHIEVED"
|
||||
- Shows items used: Full outfit list
|
||||
- Photo count: 15 photos
|
||||
|
||||
**Tier 4 Certificate - 🔞 Toy Integration Master:**
|
||||
- Red/black gradient
|
||||
- "DEGRADATION WITH TOYS DOCUMENTED"
|
||||
- Shows items used: Clothing + toys list
|
||||
- Photo count: 20 photos
|
||||
|
||||
**Tier 5 Certificate - ⛓️ Ultimate Sissy Slut:**
|
||||
- Black/neon pink gradient
|
||||
- "ULTIMATE TRANSFORMATION: COMPLETE DEGRADATION"
|
||||
- Shows all items used
|
||||
- Photo count: 25 photos
|
||||
- Special badge: "EXTREME COLLECTION MASTER"
|
||||
|
||||
### Certificate Template
|
||||
|
||||
```javascript
|
||||
ending_tier_X: {
|
||||
type: 'ending',
|
||||
mood: 'transformation_complete',
|
||||
endingTitle: "🎀 [TIER TITLE]",
|
||||
endingText: `
|
||||
<div style="background: linear-gradient(135deg, [COLORS]); border: 4px solid [COLOR]; border-radius: 20px; padding: 40px;">
|
||||
<h1 style="font-size: 2.5em;">[EMOJI] [TITLE] [EMOJI]</h1>
|
||||
|
||||
<h2 style="font-size: 2em; margin: 30px 0;">[ACHIEVEMENT]</h2>
|
||||
|
||||
<div style="text-align: left; margin: 30px 0;">
|
||||
<h3>📸 Photos Taken: ${photoCount}</h3>
|
||||
<h3>👗 Items Used:</h3>
|
||||
<ul>
|
||||
${itemsList}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<p style="font-size: 1.8em; margin: 30px 0; font-weight: bold;">
|
||||
Final State: [STATUS]
|
||||
</p>
|
||||
|
||||
<p style="font-size: 1.2em; font-style: italic;">
|
||||
[CLOSING MESSAGE]
|
||||
</p>
|
||||
</div>
|
||||
`,
|
||||
outcome: "tier_X_complete"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementation Files
|
||||
|
||||
### New Files to Create:
|
||||
1. **`src/utils/inventoryManager.js`** - Handles inventory tracking and path generation
|
||||
2. **`src/data/poseBanks/inventoryPoseBank.js`** - Pose library organized by item type
|
||||
|
||||
### Files to Modify:
|
||||
1. **`src/data/modes/dressUpGameData.js`** - Complete overhaul with new structure
|
||||
2. **`src/features/tasks/interactiveTaskManager.js`** - Add inventory-check and path-generation step types
|
||||
3. **`src/features/webcam/webcamManager.js`** - Integrate inventory-aware pose selection
|
||||
4. **`training-academy.html`** - Add questionnaire UI rendering
|
||||
|
||||
---
|
||||
|
||||
## Implementation Priority
|
||||
|
||||
### Phase 1 (Core Functionality):
|
||||
1. ✅ Create questionnaire system
|
||||
2. ✅ Inventory tracking
|
||||
3. ✅ Basic tier calculation
|
||||
4. ✅ Generate simple progression (Tier 1 path)
|
||||
|
||||
### Phase 2 (Path Expansion):
|
||||
5. Generate all 5 tier paths
|
||||
6. Create inventory-aware pose bank
|
||||
7. Implement pose selection logic
|
||||
8. Add no-repeat tracking
|
||||
|
||||
### Phase 3 (Polish):
|
||||
9. Dynamic story text based on inventory
|
||||
10. Certificate endings for all tiers
|
||||
11. Item combination bonuses
|
||||
12. Progress tracking UI
|
||||
|
||||
---
|
||||
|
||||
## Data Structure Example
|
||||
|
||||
### Stored Inventory Object:
|
||||
```javascript
|
||||
playerInventory = {
|
||||
clothing: {
|
||||
panties: 'sexy',
|
||||
bras: 'multiple',
|
||||
dresses: 'slutty',
|
||||
skirts: 'mini',
|
||||
pantyhose: 'fishnet',
|
||||
heels: 'platforms',
|
||||
wigs: 'long',
|
||||
lingerie: 'extreme'
|
||||
},
|
||||
accessories: {
|
||||
makeup: 'full',
|
||||
jewelry: 'feminine',
|
||||
nailPolish: 'colors'
|
||||
},
|
||||
toys: {
|
||||
dildos: 'multiple',
|
||||
plugs: 'large',
|
||||
chastity: 'cage',
|
||||
restraints: 'cuffs',
|
||||
gags: 'ball',
|
||||
nippleClamps: 'chain'
|
||||
},
|
||||
environment: {
|
||||
mirror: true,
|
||||
fullMirror: true,
|
||||
privateSpace: true,
|
||||
phoneStand: true
|
||||
},
|
||||
tier: 5,
|
||||
totalItems: 19
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Success Metrics
|
||||
|
||||
### Personalization:
|
||||
- Each player gets unique progression based on their items
|
||||
- No impossible challenges (asking for items they don't have)
|
||||
- Appropriate difficulty scaling
|
||||
|
||||
### Engagement:
|
||||
- Players motivated to acquire more items to unlock higher tiers
|
||||
- Clear progression visible in photo count
|
||||
- Certificate shows exactly what they accomplished
|
||||
|
||||
### Technical:
|
||||
- Accurate inventory tracking
|
||||
- Correct tier calculation
|
||||
- No pose repeats within session
|
||||
- Proper item requirement enforcement
|
||||
|
||||
---
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
### Tier Unlocks:
|
||||
- "Next time, try adding [item] to unlock [tier]!"
|
||||
- Shopping list for next tier
|
||||
- Item combination bonuses
|
||||
|
||||
### Photo Gallery:
|
||||
- Save progression photos
|
||||
- Before/after comparisons
|
||||
- Shareable certificates
|
||||
|
||||
### Advanced Paths:
|
||||
- Timed challenges
|
||||
- Multi-step transformations
|
||||
- Partner/Mistress mode (someone else controls progression)
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
### Design Philosophy:
|
||||
The game should feel **personal and achievable** rather than impossible. Every player gets a challenge suited to what they actually own, with clear progression toward a rewarding certificate ending.
|
||||
|
||||
### Safety & Consent:
|
||||
- Players only declare what they're comfortable with
|
||||
- Can restart questionnaire anytime
|
||||
- No pressure to acquire items they don't want
|
||||
- Clear "what you'll be asked to do" preview before starting
|
||||
|
||||
### Replayability:
|
||||
- Players can retake questionnaire with different items
|
||||
- Higher tiers unlock as collection grows
|
||||
- Different poses each session due to randomization
|
||||
- Multiple endings based on tier achieved
|
||||
|
||||
|
|
@ -0,0 +1,241 @@
|
|||
# Dress-Up Game Testing Guide
|
||||
|
||||
## Overview
|
||||
The dress-up game has been completely redesigned with an inventory-based photo progression system. Players take a questionnaire declaring what items they own, then get a personalized photo journey based on their tier.
|
||||
|
||||
## System Files Created/Modified
|
||||
|
||||
### New Files
|
||||
1. **src/utils/inventoryManager.js** - Core tier calculation and progression generation logic
|
||||
2. **src/data/poseBanks/inventoryPoseBank.js** - 36 poses across 7 categories
|
||||
3. **docs/DRESS_UP_GAME_REDESIGN.md** - Complete system documentation
|
||||
4. **docs/DRESS_UP_TESTING_GUIDE.md** - This file
|
||||
|
||||
### Modified Files
|
||||
1. **src/data/modes/dressUpGameData.js** - Complete rewrite with questionnaire and 5 tier endings
|
||||
2. **src/features/tasks/interactiveTaskManager.js** - Added inventory-check and path-generation handlers
|
||||
3. **index.html** - Added script tags for inventory utilities
|
||||
4. **src/styles/styles.css** - Added inventory questionnaire and summary styles
|
||||
|
||||
## Tier System
|
||||
|
||||
### Tier Thresholds
|
||||
- **Tier 1**: 0-3 items = 5 photos
|
||||
- **Tier 2**: 4-7 items = 10 photos
|
||||
- **Tier 3**: 8-12 items = 15 photos
|
||||
- **Tier 4**: 13-17 items = 20 photos
|
||||
- **Tier 5**: 18+ items = 25 photos
|
||||
|
||||
### Item Categories (21 total)
|
||||
|
||||
**Clothing (8):**
|
||||
- Panties (none/basic/sexy/multiple)
|
||||
- Bras (none/sports/regular/sexy/multiple)
|
||||
- Dresses (none/casual/slutty/multiple)
|
||||
- Skirts (none/basic/mini/micro/multiple)
|
||||
- Pantyhose/Stockings (none/nude/fishnet/multiple)
|
||||
- Heels/Shoes (none/flats/heels/platforms)
|
||||
- Wigs (none/short/long/colored)
|
||||
- Lingerie Sets (none/basic/sexy/extreme)
|
||||
|
||||
**Accessories (3):**
|
||||
- Makeup Kit (none/basic/full)
|
||||
- Jewelry (none/basic/feminine/collar)
|
||||
- Nail Polish (none/basic/colors)
|
||||
|
||||
**Toys (6):**
|
||||
- Dildos (none/small/medium/large/multiple)
|
||||
- Butt Plugs (none/small/medium/large/tail)
|
||||
- Chastity Device (none/cage/belt)
|
||||
- Restraints (none/cuffs/rope/spreader)
|
||||
- Gags (none/ball/ring)
|
||||
- Nipple Clamps (none/basic/weighted/chain)
|
||||
|
||||
**Environment (4):**
|
||||
- Mirror Available (checkbox)
|
||||
- Full-Length Mirror (checkbox)
|
||||
- Private Space (checkbox)
|
||||
- Phone/Camera Stand (checkbox)
|
||||
|
||||
## Testing Scenarios
|
||||
|
||||
### Test 1: Minimal Inventory (Tier 1)
|
||||
**Items to select:**
|
||||
- Panties: basic
|
||||
- Mirror Available: ✓
|
||||
- Private Space: ✓
|
||||
|
||||
**Expected Results:**
|
||||
- Tier 1 calculated
|
||||
- 5 photo challenges generated
|
||||
- Only panties required in poses
|
||||
- Certificate: "🩲 CERTIFICATE OF BASIC EXPOSURE"
|
||||
- Pink/white gradient, pink border
|
||||
|
||||
### Test 2: Medium Inventory (Tier 2)
|
||||
**Items to select:**
|
||||
- Panties: sexy
|
||||
- Bras: regular
|
||||
- Dresses: casual
|
||||
- Heels: flats
|
||||
- Mirror Available: ✓
|
||||
- Full-Length Mirror: ✓
|
||||
- Private Space: ✓
|
||||
|
||||
**Expected Results:**
|
||||
- Tier 2 calculated (7 items)
|
||||
- 10 photo challenges
|
||||
- Progressive item addition (starts with panties, adds bra/dress/heels)
|
||||
- Edging introduced around photo 3-4
|
||||
- Certificate: "👗 CERTIFICATE OF FEMINIZATION BEGINNER"
|
||||
- Purple/pink gradient
|
||||
|
||||
### Test 3: Full Feminization (Tier 3)
|
||||
**Items to select:**
|
||||
- Panties: multiple
|
||||
- Bras: sexy
|
||||
- Dresses: slutty
|
||||
- Skirts: mini
|
||||
- Pantyhose: fishnet
|
||||
- Heels: heels
|
||||
- Wigs: long
|
||||
- Lingerie: sexy
|
||||
- Makeup: basic
|
||||
- Jewelry: feminine
|
||||
- Mirror Available: ✓
|
||||
- Full-Length Mirror: ✓
|
||||
|
||||
**Expected Results:**
|
||||
- Tier 3 calculated (12 items)
|
||||
- 15 photo challenges
|
||||
- Full feminization progression
|
||||
- Moderate edging frequency
|
||||
- Certificate: "🎀 CERTIFICATE OF COMPLETE SISSY TRANSFORMATION"
|
||||
- Hot pink gradient with gold border
|
||||
|
||||
### Test 4: Toy Integration (Tier 4)
|
||||
**Items to select:**
|
||||
- All Tier 3 items PLUS:
|
||||
- Dildos: medium
|
||||
- Butt Plugs: small
|
||||
- Nail Polish: colors
|
||||
- Lingerie: extreme
|
||||
- Restraints: cuffs
|
||||
|
||||
**Expected Results:**
|
||||
- Tier 4 calculated (17 items)
|
||||
- 20 photo challenges
|
||||
- Toys introduced at 65% progression (photo 13)
|
||||
- Frequent edging (every 2nd photo)
|
||||
- Certificate: "🔞 CERTIFICATE OF TOY INTEGRATION MASTER"
|
||||
- Red/black gradient
|
||||
|
||||
### Test 5: Ultimate Collection (Tier 5)
|
||||
**Items to select:**
|
||||
- ALL items at maximum levels
|
||||
- All checkboxes checked
|
||||
|
||||
**Expected Results:**
|
||||
- Tier 5 calculated (21 items)
|
||||
- 25 photo challenges
|
||||
- All items progressively added
|
||||
- Extreme toys at end (chastity, gag, clamps)
|
||||
- Almost constant edging (>80%)
|
||||
- Certificate: "⛓️ CERTIFICATE OF ULTIMATE SISSY SLUT"
|
||||
- Black/neon pink gradient with green border, glowing effect
|
||||
|
||||
## Key Features to Verify
|
||||
|
||||
### ✅ Questionnaire
|
||||
- [ ] All 21 items display correctly
|
||||
- [ ] Dropdowns show correct options
|
||||
- [ ] Checkboxes toggle properly
|
||||
- [ ] Submit button collects all values
|
||||
- [ ] Navigation to summary works
|
||||
|
||||
### ✅ Inventory Summary
|
||||
- [ ] Correct tier displayed
|
||||
- [ ] Item count accurate
|
||||
- [ ] Photo count matches tier
|
||||
- [ ] Item lists show selected items organized by category
|
||||
- [ ] "Begin transformation" navigates to first challenge
|
||||
- [ ] "Adjust inventory" returns to questionnaire
|
||||
|
||||
### ✅ Photo Challenges
|
||||
- [ ] Correct number generated (5/10/15/20/25)
|
||||
- [ ] Items required match availability
|
||||
- [ ] Progressive item addition works
|
||||
- [ ] Edging frequency escalates appropriately
|
||||
- [ ] Pose selection matches items
|
||||
- [ ] All challenges navigable in sequence
|
||||
|
||||
### ✅ Certificate Endings
|
||||
- [ ] Correct certificate for tier
|
||||
- [ ] Tier badge displays with correct styling
|
||||
- [ ] Photo count shown accurately
|
||||
- [ ] Item list populated with used items
|
||||
- [ ] Visual styling matches tier (gradients, borders)
|
||||
- [ ] Final state text appropriate
|
||||
|
||||
## Common Issues to Check
|
||||
|
||||
### Issue: Blank screen after questionnaire
|
||||
**Cause:** inventoryManager.js not loaded
|
||||
**Fix:** Verify script tag in index.html
|
||||
|
||||
### Issue: No poses selected
|
||||
**Cause:** inventoryPoseBank.js not loaded
|
||||
**Fix:** Verify script tag in index.html
|
||||
|
||||
### Issue: Items list empty in certificate
|
||||
**Cause:** populateInventoryItemsList not called
|
||||
**Fix:** Check ending step type === 'ending' and scenarioState has tier
|
||||
|
||||
### Issue: Wrong number of photos
|
||||
**Cause:** Tier calculation incorrect
|
||||
**Fix:** Verify countTotalItems() logic in inventoryManager.js
|
||||
|
||||
### Issue: Items required but not owned
|
||||
**Cause:** Progressive addition logic error
|
||||
**Fix:** Check selectItemsForChallenge() in inventoryManager.js
|
||||
|
||||
## Console Debugging
|
||||
|
||||
Enable console logging to track progression:
|
||||
```javascript
|
||||
// In browser console:
|
||||
window.localStorage.setItem('debug', 'true');
|
||||
```
|
||||
|
||||
Look for these log messages:
|
||||
- `📋 Displaying inventory questionnaire`
|
||||
- `📦 Collected inventory: {...}`
|
||||
- `🎯 Generating inventory-based path`
|
||||
- `📊 Tier X: Y items, Z photos`
|
||||
- `📸 Generated N photo challenges`
|
||||
- `✅ Added N challenge steps to scenario`
|
||||
|
||||
## Manual Testing Steps
|
||||
|
||||
1. Open game in browser
|
||||
2. Navigate to Training Academy
|
||||
3. Select "Dress-Up/Photography Challenges"
|
||||
4. Choose "Inventory-Based Photo Transformation" scenario
|
||||
5. Click "Begin inventory questionnaire"
|
||||
6. Fill out questionnaire (use test scenarios above)
|
||||
7. Click "Submit Inventory"
|
||||
8. Verify summary displays correctly
|
||||
9. Click "Begin my personalized photo journey"
|
||||
10. Complete photo challenges
|
||||
11. Verify certificate displays with correct items
|
||||
12. Click "Complete Task" to return
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- ✅ All 5 tiers generate correct photo counts
|
||||
- ✅ Item requirements never exceed available items
|
||||
- ✅ Edging frequency escalates appropriately
|
||||
- ✅ Pose selection matches required items
|
||||
- ✅ Certificates display with accurate item lists
|
||||
- ✅ No errors in console
|
||||
- ✅ Smooth progression from questionnaire → challenges → certificate
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,617 @@
|
|||
# The Academy - Project Overview
|
||||
## **Campaign-Style Progressive Training System**
|
||||
|
||||
> **Vision**: The Academy is a 30-level campaign where users progress from curious beginner to perfect gooner. Each level introduces new features, increases intensity, and challenges mastery. Users build and curate their media library, set personal preferences, and complete increasingly complex training sessions. This is where transformation happens.
|
||||
|
||||
---
|
||||
|
||||
## 📋 Project Documents
|
||||
|
||||
| Document | Description | Status |
|
||||
|----------|-------------|--------|
|
||||
| [OVERVIEW.md](./OVERVIEW.md) | This document - Project overview and roadmap | ✅ Current |
|
||||
| [CAMPAIGN-STRUCTURE.md](./CAMPAIGN-STRUCTURE.md) | Complete 30-level campaign design | ✅ Complete |
|
||||
| [PHASE-1.md](./PHASE-1.md) | Campaign Foundation & Core Systems | 📝 Needs Update |
|
||||
| [PHASE-2.md](./PHASE-2.md) | Levels 1-10 (Foundation & Feature Discovery Arcs) | 📝 Needs Update |
|
||||
| [PHASE-3.md](./PHASE-3.md) | Levels 11-20 (Mind & Body + Advanced Training Arcs) | 📝 Needs Update |
|
||||
| [PHASE-4.md](./PHASE-4.md) | Levels 21-25 (Path Specialization Arc) | 📝 Needs Update |
|
||||
| [PHASE-5.md](./PHASE-5.md) | Levels 26-30 (Ultimate Mastery Arc) + Graduation | 📝 Needs Update |
|
||||
| [PHASE-6.md](./PHASE-6.md) | Progression, Stats, Achievements & Library System | 📝 Needs Update |
|
||||
| [PHASE-7.md](./PHASE-7.md) | Polish, UI/UX, Accessibility & Testing | 📝 Needs Update |
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Project Overview
|
||||
|
||||
### **What Changed from Original Design:**
|
||||
|
||||
**Original Concept**: Training game with 4 paths, tier selection, and feature integration.
|
||||
|
||||
**New Concept**: **The Academy** - A 30-level progressive campaign with:
|
||||
- 6 story arcs (5 levels each)
|
||||
- Locked level progression (complete N to unlock N+1)
|
||||
- Feature unlocking (gated by level completion)
|
||||
- Preference checkpoints at Levels 1, 5, 10, 15, 20, 25
|
||||
- Media library curation as core training
|
||||
- Comprehensive tagging system (directory + individual file tagging)
|
||||
- Path specialization at Level 21
|
||||
- 5-hour graduation session at Level 30
|
||||
- Post-graduation freeplay and Ascended Mode
|
||||
|
||||
### **Core Philosophy:**
|
||||
|
||||
1. **Progressive Complexity**: Each level builds on previous knowledge
|
||||
2. **Library Curation = Discipline**: Organizing media is training in mindfulness and control
|
||||
3. **Personalization**: Preferences + tags = uniquely tailored experience
|
||||
4. **Achievement Through Mastery**: Can't skip levels, must prove competence
|
||||
5. **Transformation Journey**: Story of becoming a "perfect gooner"
|
||||
|
||||
---
|
||||
|
||||
## 📊 Campaign Structure Summary
|
||||
|
||||
### **6 Story Arcs:**
|
||||
|
||||
| Arc | Levels | Focus | Unlocks |
|
||||
|-----|--------|-------|---------|
|
||||
| Foundation | 1-5 | Basic edging, rhythm, video, library setup | Standard tier, directory tagging, video features |
|
||||
| Feature Discovery | 6-10 | Webcam, dual/quad video, TTS, metronome | Extended tier, all basic features, file tagging |
|
||||
| Mind & Body | 11-15 | Hypno, captions, TTS+spiral, sensory overload | Marathon tier, advanced features, theme tags |
|
||||
| Advanced Training | 16-20 | Interruptions, denial, popups, gauntlet | All features, smart playlists |
|
||||
| Path Specialization | 21-25 | Choose path, deep dive into specialty | Path mastery, Master Librarian |
|
||||
| Ultimate Mastery | 26-30 | 2hr → 3hr → 4hr → 5hr graduation | Freeplay, Ascended Mode, graduation |
|
||||
|
||||
### **Key Campaign Mechanics:**
|
||||
|
||||
**Level Unlocking**: Linear progression, must complete level N to unlock N+1
|
||||
|
||||
**Feature Gating**:
|
||||
- Webcam: Unlocked Level 5
|
||||
- Dual video: Level 7
|
||||
- TTS: Level 8
|
||||
- Quad video: Level 9
|
||||
- Hypno: Level 10
|
||||
- Captions/Popups: Level 12
|
||||
- Interruptions: Level 15
|
||||
- All features: Level 20
|
||||
|
||||
**Duration Tiers**:
|
||||
- Quick (1x): Available from start
|
||||
- Standard (5x): Level 2
|
||||
- Extended (10x): Level 10
|
||||
- Marathon (15x): Level 15
|
||||
|
||||
**Preference Checkpoints**: Levels 1, 5, 10, 15, 20, 25
|
||||
- Update content themes (sissy, humiliation, worship, denial, etc.)
|
||||
- Adjust visual preferences
|
||||
- Set intensity levels
|
||||
- Configure caption tone
|
||||
|
||||
**Library Checkpoints**: Levels 2, 3, 5, 10, 15, 20, 25
|
||||
- Tag directories and files
|
||||
- Improve tag coverage (90% → 95% → 98% → 100%)
|
||||
- Advance curator rank (Novice → Master)
|
||||
- Build smart playlists
|
||||
- Quality/diversity scoring
|
||||
|
||||
**Failure Conditions**:
|
||||
- Cumming = instant fail, restart level
|
||||
- Closing features early = fail
|
||||
- Abandoning session = fail
|
||||
- Failing interruptions (late-game) = fail
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Revised Implementation Phases
|
||||
|
||||
### Phase 1: Campaign Foundation & Core Systems
|
||||
**Priority: HIGHEST** | **Effort: 12-16 hours** | **Status: Not Started**
|
||||
|
||||
Build the foundational systems required for the entire campaign.
|
||||
|
||||
**Key Deliverables:**
|
||||
- Campaign progression system (level unlocking, failure states)
|
||||
- Preference management system (checkpoints, filtering)
|
||||
- Media library tagging system (directory + file tagging)
|
||||
- Tag-based content filtering
|
||||
- Basic UI for level selection
|
||||
- Data persistence (campaign progress, preferences, library data)
|
||||
|
||||
**Measurable Test:**
|
||||
- [ ] Can select Level 1, complete it, unlock Level 2
|
||||
- [ ] Preference form appears at Level 1, saves correctly
|
||||
- [ ] Can add directory, tag it, tags save to libraryData
|
||||
- [ ] Can tag individual files, tags persist
|
||||
- [ ] Tag filtering works (preference filters content selection)
|
||||
- [ ] Progress persists across page refreshes
|
||||
- [ ] Campaign state displays correctly (unlocked levels, current level, stats)
|
||||
|
||||
**[📄 View Phase 1 Details](./PHASE-1.md)**
|
||||
|
||||
---
|
||||
|
||||
### Phase 2: Levels 1-10 Implementation
|
||||
**Priority: HIGH** | **Effort: 16-20 hours** | **Status: Not Started**
|
||||
|
||||
Implement Foundation Arc (Levels 1-5) and Feature Discovery Arc (Levels 6-10).
|
||||
|
||||
**Key Deliverables:**
|
||||
- Level 1: First Edge (basic timer, 5 min)
|
||||
- Level 2: Building Stamina (10 min, first directory tagging)
|
||||
- Level 3: Rhythm Training (metronome, individual file tagging)
|
||||
- Level 4: Visual Stimulation (single video, tag filtering)
|
||||
- Level 5: Foundation Complete (video + metronome, first checkpoint)
|
||||
- Level 6: Mirror Introduction (webcam mirror, auto-capture)
|
||||
- Level 7: Dual Video Experience
|
||||
- Level 8: Voice of Command (TTS coaching)
|
||||
- Level 9: The Quad (quad video layout)
|
||||
- Level 10: Discovery Complete (multi-feature, second checkpoint)
|
||||
- All action handlers for Levels 1-10
|
||||
- Library UI (directory tagging, file tagging, stats dashboard)
|
||||
- Preference UI (checkbox forms at checkpoints)
|
||||
|
||||
**Measurable Test:**
|
||||
- [ ] Can complete all 10 levels sequentially
|
||||
- [ ] Each level unlocks the next upon completion
|
||||
- [ ] Preference checkpoints work at L1, L5, L10
|
||||
- [ ] Library checkpoints work at L2, L3, L5, L10
|
||||
- [ ] All features unlock correctly (webcam L5, TTS L8, quad L9)
|
||||
- [ ] Tag coverage tracking works
|
||||
- [ ] Curator rank advances
|
||||
- [ ] Cumming causes level restart
|
||||
- [ ] All 10 levels tested end-to-end without errors
|
||||
|
||||
**[📄 View Phase 2 Details](./PHASE-2.md)**
|
||||
|
||||
---
|
||||
|
||||
### Phase 3: Levels 11-20 Implementation
|
||||
**Priority: HIGH** | **Effort: 18-24 hours** | **Status: Not Started**
|
||||
|
||||
Implement Mind & Body Arc (Levels 11-15) and Advanced Training Arc (Levels 16-20).
|
||||
|
||||
**Key Deliverables:**
|
||||
- Level 11: Spiral Descent (hypno spirals, subliminal)
|
||||
- Level 12: Caption Conditioning (preference-based captions)
|
||||
- Level 13: Voice + Mind (TTS + hypno, mantras)
|
||||
- Level 14: Multi-Sensory Assault (quad + spiral + TTS + metronome)
|
||||
- Level 15: Mind & Body Complete (max features, third checkpoint)
|
||||
- Level 16: The Interruption (10 interruptions)
|
||||
- Level 17: Denial Introduction (45 min denial)
|
||||
- Level 18: Popups & Distractions (20 random popups)
|
||||
- Level 19: The Gauntlet (60 min, all features, 15 interruptions)
|
||||
- Level 20: Advanced Training Complete (90 min marathon, fourth checkpoint)
|
||||
- Hypno spiral display system
|
||||
- Caption rotation system (preference-based)
|
||||
- Interruption system (hands-off, verification, humiliation)
|
||||
- Popup image system
|
||||
- Denial timer/messaging
|
||||
|
||||
**Measurable Test:**
|
||||
- [ ] Can complete all levels 11-20 sequentially
|
||||
- [ ] Hypno spirals display correctly with subliminal messages
|
||||
- [ ] Caption content matches user preferences
|
||||
- [ ] Mantras adapt to preferences
|
||||
- [ ] TTS + hypno coordinate without conflicts
|
||||
- [ ] Interruptions trigger correctly and pause session
|
||||
- [ ] Popup images display randomly as specified
|
||||
- [ ] 90-minute marathon completes successfully
|
||||
- [ ] Preference checkpoint at L15, library checkpoint at L15
|
||||
- [ ] All features coordinate in complex combinations
|
||||
- [ ] Performance remains acceptable with all features active
|
||||
|
||||
**[📄 View Phase 3 Details](./PHASE-3.md)**
|
||||
|
||||
---
|
||||
|
||||
### Phase 4: Levels 21-25 Implementation (Path Specialization)
|
||||
**Priority: MEDIUM** | **Effort: 20-26 hours** | **Status: Not Started**
|
||||
|
||||
Implement path selection and 6 specialized training paths.
|
||||
|
||||
**Key Deliverables:**
|
||||
- Level 21: Path Selection (questionnaire UI)
|
||||
- Path filtering based on preferences (e.g., Humiliation requires preference)
|
||||
- 6 Path Implementations (Levels 22-25 for each):
|
||||
- **Endurance**: 60min → 90min → 120min → 150min stamina tests
|
||||
- **Denial**: 7-day → ruined orgasm → 14-day → 30-day challenges
|
||||
- **Humiliation**: Self-degradation → positions → public fantasy → ultimate degradation
|
||||
- **Obedience**: 30 interruptions → command sequences → precision timing → perfect exam
|
||||
- **Sensitivity**: Minimal stim → audio-only → visual triggers → hair-trigger mastery
|
||||
- **Multi-Sensory**: Triple → quad → five features → all-features overload
|
||||
- Detailed scenarios for each path level
|
||||
- Path-specific content and challenges
|
||||
- Library checkpoint at L25
|
||||
|
||||
**Measurable Test:**
|
||||
- [ ] Path selection questionnaire displays at L21
|
||||
- [ ] Only available paths shown (based on preferences)
|
||||
- [ ] Can select a path and progress through L22-25
|
||||
- [ ] Each path has unique content/challenges
|
||||
- [ ] Endurance path increases duration correctly
|
||||
- [ ] Denial path tracks denial streak
|
||||
- [ ] Humiliation path respects preference setting
|
||||
- [ ] Obedience path handles complex interruptions
|
||||
- [ ] Sensitivity path works with minimal stimulation
|
||||
- [ ] Multi-Sensory path coordinates all features
|
||||
- [ ] All 6 paths testable end-to-end
|
||||
- [ ] Path completion unlocks Level 26
|
||||
- [ ] Library statistics show Master Librarian achievement
|
||||
|
||||
**[📄 View Phase 4 Details](./PHASE-4.md)**
|
||||
|
||||
---
|
||||
|
||||
### Phase 5: Levels 26-30 Implementation (Ultimate Mastery + Graduation)
|
||||
**Priority: MEDIUM** | **Effort: 16-22 hours** | **Status: Not Started**
|
||||
|
||||
Implement ultimate mastery arc and graduation ceremony.
|
||||
|
||||
**Key Deliverables:**
|
||||
- Level 26: Return to Foundation (120 min, user-selected features)
|
||||
- Level 27: The Ascension (150 min, all features, progressive intensity)
|
||||
- Level 28: Edge of Infinity (180 min, dynamic difficulty)
|
||||
- Level 29: The Final Crucible (240 min, 4-stage intensity, 40 interruptions)
|
||||
- Level 30: Graduation (300 min, ultimate test)
|
||||
- Progressive intensity system (4 stages: Warm-Up → Building → Peak → Overload)
|
||||
- Dynamic difficulty adjustment
|
||||
- Milestone photo captures (every 30 min or at % checkpoints)
|
||||
- Graduation ceremony system:
|
||||
- Stats summary display
|
||||
- Photo gallery compilation
|
||||
- Achievement showcase
|
||||
- Library curation report (files, tags, curator rank, quality score)
|
||||
- Preference profile summary
|
||||
- Graduation oath (preference-based)
|
||||
- Certificate generation
|
||||
- Freeplay mode unlock
|
||||
- Ascended Mode unlock (secret post-graduation)
|
||||
|
||||
**Measurable Test:**
|
||||
- [ ] Level 26 allows user feature selection
|
||||
- [ ] Level 27 progressive intensity works (4 stages)
|
||||
- [ ] Level 28 dynamic difficulty adjusts based on performance
|
||||
- [ ] Level 29 handles 240 minutes with 40 interruptions
|
||||
- [ ] Level 30 completes full 300-minute session
|
||||
- [ ] Milestone photos capture correctly
|
||||
- [ ] Graduation ceremony displays all stats
|
||||
- [ ] Library report shows accurate data
|
||||
- [ ] Graduation oath adapts to preferences
|
||||
- [ ] Certificate generates with correct info
|
||||
- [ ] Freeplay mode unlocks after graduation
|
||||
- [ ] Ascended Mode unlocks and functions
|
||||
- [ ] Can replay any completed level
|
||||
- [ ] All 30 levels tested in sequence from L1 → L30
|
||||
|
||||
**[📄 View Phase 5 Details](./PHASE-5.md)**
|
||||
|
||||
---
|
||||
|
||||
### Phase 6: Progression, Stats, Achievements & Library System
|
||||
**Priority: HIGH** | **Effort: 14-18 hours** | **Status: Not Started**
|
||||
|
||||
Build comprehensive tracking systems that span entire campaign.
|
||||
|
||||
**Key Deliverables:**
|
||||
- `trainingStats` data structure (sessions, time, levels, feature usage)
|
||||
- 25+ achievement system:
|
||||
- Campaign achievements (complete levels/arcs)
|
||||
- Library achievements (tag coverage, file count, curator rank)
|
||||
- Feature achievements (use specific features)
|
||||
- Ultimate achievements (flawless denial, all photos, etc.)
|
||||
- Achievement notification system (modals, toasts)
|
||||
- Stats dashboard UI:
|
||||
- Campaign progress (levels completed, current arc)
|
||||
- Time statistics (total time, per-level time)
|
||||
- Library statistics (files, tags, coverage, quality score)
|
||||
- Feature usage tracking
|
||||
- Achievement gallery
|
||||
- Curator rank display
|
||||
- Persistent data management (save/load across sessions)
|
||||
- Denial streak tracking
|
||||
- Library quality scoring algorithm
|
||||
- Diversity scoring algorithm
|
||||
- Smart playlist system from tag combinations
|
||||
|
||||
**Measurable Test:**
|
||||
- [ ] Stats persist across page refreshes
|
||||
- [ ] Completing level updates stats correctly
|
||||
- [ ] Achievements unlock at correct triggers
|
||||
- [ ] Achievement notifications display correctly
|
||||
- [ ] Stats dashboard shows accurate data
|
||||
- [ ] Library statistics calculate correctly
|
||||
- [ ] Tag coverage percentage accurate
|
||||
- [ ] Curator rank advances based on tag coverage
|
||||
- [ ] Quality score reflects library organization
|
||||
- [ ] Diversity score reflects content variety
|
||||
- [ ] Denial streak tracks correctly
|
||||
- [ ] Smart playlists filter by tags correctly
|
||||
- [ ] Can export/import campaign data
|
||||
- [ ] All 25+ achievements are obtainable
|
||||
|
||||
**[📄 View Phase 6 Details](./PHASE-6.md)**
|
||||
|
||||
---
|
||||
|
||||
### Phase 7: Polish, UI/UX, Accessibility & Testing
|
||||
**Priority: MEDIUM-LOW** | **Effort: 12-16 hours** | **Status: Not Started**
|
||||
|
||||
Final polish, quality of life features, and comprehensive testing.
|
||||
|
||||
**Key Deliverables:**
|
||||
- UI/UX improvements:
|
||||
- Dark mode / theme customization
|
||||
- Smooth transitions and animations
|
||||
- Responsive design (mobile consideration)
|
||||
- Loading states and progress indicators
|
||||
- Onboarding tutorial for new users
|
||||
- Accessibility features:
|
||||
- Safe word / emergency stop button
|
||||
- Keyboard navigation
|
||||
- Pause system (pause during interruptions, resume)
|
||||
- Session recovery (browser crash recovery)
|
||||
- Adjustable text size
|
||||
- Quality of life:
|
||||
- Quick session retry (restart failed level)
|
||||
- Skip tutorial option (for replays)
|
||||
- Custom session duration multiplier
|
||||
- Library bulk operations (bulk tag, bulk import)
|
||||
- Export library metadata
|
||||
- Performance optimization:
|
||||
- Lazy loading of videos/images
|
||||
- Tag search indexing
|
||||
- Efficient DOM updates
|
||||
- Comprehensive testing:
|
||||
- L1 → L30 full playthrough
|
||||
- All 6 paths tested
|
||||
- All features tested in isolation and combination
|
||||
- Edge cases (browser refresh, tab switching, etc.)
|
||||
- Performance testing (library with 10,000+ files)
|
||||
- Bug fixes and error handling
|
||||
- Documentation (in-game help, tooltips)
|
||||
|
||||
**Measurable Test:**
|
||||
- [ ] Dark mode toggles correctly
|
||||
- [ ] All UI transitions smooth (no jank)
|
||||
- [ ] Safe word immediately stops session
|
||||
- [ ] Keyboard shortcuts work throughout
|
||||
- [ ] Pause/resume works correctly
|
||||
- [ ] Session recovery restores state after crash
|
||||
- [ ] Can retry failed level immediately
|
||||
- [ ] Tutorial can be skipped on replay
|
||||
- [ ] Bulk tagging 100 files works quickly
|
||||
- [ ] Library with 5,000 files performs well
|
||||
- [ ] Tag search returns results in <100ms
|
||||
- [ ] No console errors during full playthrough
|
||||
- [ ] No memory leaks during long sessions
|
||||
- [ ] All tooltips/help text accurate
|
||||
- [ ] Full L1→L30 playthrough successful
|
||||
- [ ] All 6 paths completable
|
||||
- [ ] All features functional in all combinations
|
||||
|
||||
**[📄 View Phase 7 Details](./PHASE-7.md)**
|
||||
|
||||
---
|
||||
|
||||
## ⏱️ Total Estimated Effort
|
||||
|
||||
| Phase | Priority | Hours | Status | Dependencies |
|
||||
|-------|----------|-------|--------|--------------|
|
||||
| Phase 1 | HIGHEST | 12-16 | Not Started | None |
|
||||
| Phase 2 | HIGH | 16-20 | Not Started | Phase 1 |
|
||||
| Phase 3 | HIGH | 18-24 | Not Started | Phase 1, 2 |
|
||||
| Phase 4 | MEDIUM | 20-26 | Not Started | Phase 1, 2, 3 |
|
||||
| Phase 5 | MEDIUM | 16-22 | Not Started | Phase 1, 2, 3, 4 |
|
||||
| Phase 6 | HIGH | 14-18 | Not Started | Phase 1 |
|
||||
| Phase 7 | MEDIUM-LOW | 12-16 | Not Started | All |
|
||||
| **TOTAL** | | **108-142** | **0% Complete** | |
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Recommended Implementation Order
|
||||
|
||||
**Parallel Track Strategy:**
|
||||
|
||||
**Track A (Campaign Content):**
|
||||
1. Phase 1 (Foundation) → Phase 2 (L1-10) → Phase 3 (L11-20) → Phase 4 (L21-25) → Phase 5 (L26-30)
|
||||
|
||||
**Track B (Systems):**
|
||||
1. Phase 6 (Stats/Achievements/Library) - Can start after Phase 1
|
||||
|
||||
**Track C (Polish):**
|
||||
1. Phase 7 (UI/UX/Testing) - Continuous throughout, finalize at end
|
||||
|
||||
**Recommended Sequence:**
|
||||
1. **Phase 1** (12-16 hours) - Foundation systems (MUST complete first)
|
||||
2. **Phase 6** (14-18 hours) - Stats/library systems (start after Phase 1, work in parallel)
|
||||
3. **Phase 2** (16-20 hours) - Levels 1-10 (can start during Phase 6)
|
||||
4. **Phase 3** (18-24 hours) - Levels 11-20
|
||||
5. **Phase 4** (20-26 hours) - Levels 21-25 (Path specialization)
|
||||
6. **Phase 5** (16-22 hours) - Levels 26-30 (Ultimate + Graduation)
|
||||
7. **Phase 7** (12-16 hours) - Polish and final testing
|
||||
|
||||
**Rationale**: Build foundation first, then systems and content in parallel, polish at end.
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Technical Architecture
|
||||
|
||||
### New Files to Create:
|
||||
|
||||
**Campaign Management:**
|
||||
- `src/features/academy/campaignManager.js` - Level unlocking, progression, failure states
|
||||
- `src/features/academy/preferenceManager.js` - User preferences, content filtering
|
||||
- `src/features/academy/libraryManager.js` - Media library, tagging, filtering
|
||||
- `src/features/academy/tagManager.js` - Tag CRUD operations, suggestions, validation
|
||||
|
||||
**Data Structures:**
|
||||
- `src/data/modes/academyLevelData.js` - All 30 level definitions
|
||||
- `src/data/academy/achievements.js` - Achievement definitions
|
||||
- `src/data/academy/pathData.js` - 6 specialized path data
|
||||
|
||||
**Systems:**
|
||||
- `src/features/academy/progressionManager.js` - Stats, achievements, levels
|
||||
- `src/features/academy/graduationManager.js` - Ceremony, certificate, unlocks
|
||||
- `src/features/academy/intensityManager.js` - Progressive intensity scaling
|
||||
|
||||
### Files to Update:
|
||||
|
||||
**Core:**
|
||||
- `src/features/tasks/interactiveTaskManager.js` - New action handlers for all level types
|
||||
- `src/core/gameDataManager.js` - Add academyProgress, preferences, libraryData persistence
|
||||
- `training-academy.html` - Complete UI overhaul for campaign
|
||||
|
||||
**Integration:**
|
||||
- `src/features/video/videoPlayerManager.js` - Tag-based video selection
|
||||
- `src/features/images/popupImageManager.js` - Tag-based image selection
|
||||
- `src/features/webcam/webcamManager.js` - Milestone photo captures
|
||||
- `src/features/tts/voiceManager.js` - Preference-based script generation
|
||||
|
||||
### Data Flow:
|
||||
|
||||
```
|
||||
User launches Academy
|
||||
↓
|
||||
campaignManager loads academyProgress
|
||||
↓
|
||||
Display level select UI (show unlocked levels)
|
||||
↓
|
||||
User selects level N
|
||||
↓
|
||||
If checkpoint level (1,5,10,15,20,25):
|
||||
→ Show preference form
|
||||
→ Show library maintenance UI
|
||||
↓
|
||||
Load level data from academyLevelData.js
|
||||
↓
|
||||
Initialize required features based on level
|
||||
↓
|
||||
Run level session (interactiveTaskManager)
|
||||
↓
|
||||
Monitor for:
|
||||
- Cumming (fail)
|
||||
- Feature closure (fail)
|
||||
- Interruptions (handle)
|
||||
- Time milestones (photos)
|
||||
↓
|
||||
Session completes:
|
||||
- Update stats (time, features used)
|
||||
- Check achievements
|
||||
- Update library stats (if tagging done)
|
||||
- Unlock next level
|
||||
- Save all data
|
||||
↓
|
||||
Display completion screen
|
||||
↓
|
||||
If Level 30:
|
||||
→ Launch graduation ceremony
|
||||
→ Generate certificate
|
||||
→ Unlock freeplay/ascended
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💡 Key Integration Principles
|
||||
|
||||
1. **Feature Modularity**: Each feature can be enabled/disabled independently
|
||||
2. **Graceful Degradation**: If webcam unavailable, session continues without it
|
||||
3. **Progressive Disclosure**: Features unlock gradually, preventing overwhelm
|
||||
4. **User Control**: Preferences shape content, tags filter media
|
||||
5. **Performance Awareness**: Lazy load media, efficient tag indexing
|
||||
6. **Data Privacy**: All webcam photos stay local, user controls deletion
|
||||
7. **Accessibility**: Safe word, keyboard nav, pause system always available
|
||||
8. **Testing**: Test each level individually, then full campaign playthrough
|
||||
9. **Progression Gating**: Can't skip levels, ensures proper learning curve
|
||||
10. **Replay Value**: Can replay any completed level for better stats/photos
|
||||
|
||||
---
|
||||
|
||||
## 🎓 Feature Unlocking Schedule
|
||||
|
||||
| Level | Feature Unlocked | Why |
|
||||
|-------|-----------------|-----|
|
||||
| 1 | Basic timer | Foundation |
|
||||
| 2 | Directory tagging | Library curation begins |
|
||||
| 3 | Audio metronome, File tagging | Rhythm + organization |
|
||||
| 4 | Video player (single) | Visual stimulation |
|
||||
| 5 | Webcam mirror | Self-awareness |
|
||||
| 6 | Auto-capture photos | Documentation |
|
||||
| 7 | Dual video | Increased stimulation |
|
||||
| 8 | TTS coaching | Voice control |
|
||||
| 9 | Quad video | Maximum visual overload |
|
||||
| 10 | Extended tier, Smart playlists | Longer sessions, better organization |
|
||||
| 11 | Hypno spirals | Mind control |
|
||||
| 12 | Captions, Popups | Mental conditioning |
|
||||
| 13 | TTS+Hypno combo | Deep trance |
|
||||
| 14 | Multi-feature coordination | Complex orchestration |
|
||||
| 15 | Interruptions, Marathon tier | Ultimate control testing |
|
||||
| 20 | All features unlocked | Complete arsenal |
|
||||
| 21 | Path selection | Specialization |
|
||||
| 26 | Custom feature selection | Mastery through choice |
|
||||
| 30 | Freeplay, Ascended Mode | Post-graduation freedom |
|
||||
|
||||
---
|
||||
|
||||
## 📈 Success Criteria (Overall Project)
|
||||
|
||||
### Minimum Viable Campaign (MVC):
|
||||
- [ ] All 30 levels implemented
|
||||
- [ ] Level progression works (L1 → L30)
|
||||
- [ ] All features unlock correctly
|
||||
- [ ] Preference system functional
|
||||
- [ ] Library tagging system functional
|
||||
- [ ] Stats persist across sessions
|
||||
- [ ] Can complete full campaign (L1 → L30)
|
||||
- [ ] Graduation ceremony works
|
||||
|
||||
### Full Campaign (Ideal):
|
||||
- [ ] All above + measurable tests from each phase pass
|
||||
- [ ] All 6 paths functional
|
||||
- [ ] All 25+ achievements obtainable
|
||||
- [ ] Library quality/diversity scoring works
|
||||
- [ ] Smart playlists functional
|
||||
- [ ] Freeplay and Ascended Mode work
|
||||
- [ ] No performance issues with large libraries
|
||||
- [ ] Full UI polish complete
|
||||
- [ ] All accessibility features work
|
||||
|
||||
---
|
||||
|
||||
## 🚦 Current Status
|
||||
|
||||
**Last Updated**: November 29, 2025
|
||||
**Current Phase**: Design Complete, Implementation Not Started
|
||||
**Overall Progress**: 0%
|
||||
**Documentation Status**: Campaign structure complete, phase docs need updating
|
||||
|
||||
**Next Actions**:
|
||||
1. Review and approve campaign structure
|
||||
2. Update all 7 phase documents to align with campaign
|
||||
3. Begin Phase 1 implementation (foundation systems)
|
||||
|
||||
---
|
||||
|
||||
## 📞 Quick Reference
|
||||
|
||||
**Campaign Design**: [CAMPAIGN-STRUCTURE.md](./CAMPAIGN-STRUCTURE.md)
|
||||
**Phase Documents**: See table at top of this document
|
||||
**Current Files**:
|
||||
- Training data: `src/data/modes/trainingGameData.js` (will be replaced/supplemented)
|
||||
- Task manager: `src/features/tasks/interactiveTaskManager.js` (will be updated)
|
||||
- UI: `training-academy.html` (will be overhauled)
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Vision Statement
|
||||
|
||||
**The Academy is the crowning jewel of the entire project.**
|
||||
|
||||
It transforms random edging sessions into a structured journey of growth and mastery. Users don't just edge—they progress through a carefully designed campaign that teaches discipline through library curation, self-awareness through webcam mirror sessions, endurance through progressively longer levels, and obedience through interruption challenges.
|
||||
|
||||
Every level has purpose. Every feature unlocks at the right moment. Every preference matters. Every tag contributes to a perfectly curated experience.
|
||||
|
||||
Users enter as curious beginners. They leave as perfect gooners with impeccably organized libraries, fully customized preferences, and complete mastery of all features.
|
||||
|
||||
**This is not just a training game. This is The Academy. This is transformation.**
|
||||
|
||||
Let's build it.
|
||||
|
|
@ -0,0 +1,500 @@
|
|||
# Phase 1: Campaign Foundation & Core Systems
|
||||
|
||||
**Priority**: HIGHEST - Must complete first
|
||||
**Estimated Effort**: 12-16 hours
|
||||
**Status**: In Progress
|
||||
**Dependencies**: None
|
||||
|
||||
---
|
||||
|
||||
## 📝 Subphase Tracking
|
||||
|
||||
- [x] **Subphase 1.1**: Campaign Progression System (2-3 hrs) - ✅ COMPLETE - User tested and confirmed
|
||||
- [x] **Subphase 1.2**: Preference Management System (2-3 hrs) - ✅ COMPLETE - User tested and confirmed
|
||||
- [x] **Subphase 1.3**: Library Tagging System (2-3 hrs) - ✅ COMPLETE - User tested and confirmed
|
||||
- [x] **Subphase 1.4**: Content Filtering & UI (2-3 hrs) - ✅ COMPLETE - Awaiting user test
|
||||
|
||||
**⚠️ IMPORTANT**: After each subphase implementation, WAIT for user to test and confirm before proceeding to next subphase.
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Phase Goals
|
||||
|
||||
Build the foundational systems required for The Academy's 30-level campaign structure, including campaign progression, preference management, and media library tagging.
|
||||
|
||||
---
|
||||
|
||||
## 📋 What This Phase Delivers
|
||||
|
||||
### Core Systems:
|
||||
1. **Campaign Progression System** - Level unlocking, completion tracking, failure states
|
||||
2. **Preference Management System** - User preferences with checkpoint updates
|
||||
3. **Media Library Tagging System** - Directory and individual file tagging
|
||||
4. **Tag-Based Content Filtering** - Filter videos/images by preferences and tags
|
||||
5. **Data Persistence** - Save/load campaign progress, preferences, library data
|
||||
6. **Basic UI Infrastructure** - Level selection, preference forms, library management
|
||||
|
||||
**End Result**: Foundation that all 30 levels will build upon.
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ Implementation Tasks
|
||||
|
||||
### Task 1: Campaign Progression System (4-5 hours)
|
||||
|
||||
**File to Create**: `src/features/academy/campaignManager.js`
|
||||
|
||||
**Responsibilities**:
|
||||
- Track which levels are unlocked (start with only Level 1)
|
||||
- Handle level completion (unlock next level)
|
||||
- Track current level, highest completed level
|
||||
- Handle failure states (cumming, abandoning session, feature closure)
|
||||
- Manage level restart on failure
|
||||
- Save/load campaign state
|
||||
|
||||
**Data Structure** (in `gameData.js`):
|
||||
```javascript
|
||||
academyProgress: {
|
||||
version: 1,
|
||||
currentLevel: 1,
|
||||
highestUnlockedLevel: 1,
|
||||
completedLevels: [], // array of level numbers completed
|
||||
currentArc: 'Foundation', // Foundation, Feature Discovery, etc.
|
||||
failedAttempts: {}, // { levelNum: count }
|
||||
totalSessionTime: 0, // seconds across all levels
|
||||
lastPlayedLevel: null,
|
||||
lastPlayedDate: null,
|
||||
graduationCompleted: false,
|
||||
freeplayUnlocked: false,
|
||||
ascendedModeUnlocked: false
|
||||
}
|
||||
```
|
||||
|
||||
**Key Methods**:
|
||||
```javascript
|
||||
class CampaignManager {
|
||||
constructor() {
|
||||
this.loadProgress();
|
||||
}
|
||||
|
||||
// Get list of unlocked levels
|
||||
getUnlockedLevels() {
|
||||
// Returns array: [1, 2, 3, ...highestUnlockedLevel]
|
||||
}
|
||||
|
||||
// Check if level is unlocked
|
||||
isLevelUnlocked(levelNum) {
|
||||
// Returns true if levelNum <= highestUnlockedLevel
|
||||
}
|
||||
|
||||
// Start a level
|
||||
startLevel(levelNum) {
|
||||
// Validates level is unlocked
|
||||
// Loads level data
|
||||
// Returns level config or error
|
||||
}
|
||||
|
||||
// Complete a level
|
||||
completeLevel(levelNum, sessionData) {
|
||||
// Add to completedLevels
|
||||
// Unlock next level
|
||||
// Update stats
|
||||
// Check for arc completion
|
||||
// Save progress
|
||||
// Return: { nextLevelUnlocked, achievements, arcComplete }
|
||||
}
|
||||
|
||||
// Fail a level
|
||||
failLevel(levelNum, reason) {
|
||||
// Increment failedAttempts
|
||||
// Log failure reason ('cumming', 'abandoned', 'feature-closed')
|
||||
// Save progress
|
||||
}
|
||||
|
||||
// Get current arc
|
||||
getCurrentArc() {
|
||||
// Based on current level, return arc name
|
||||
// L1-5: Foundation
|
||||
// L6-10: Feature Discovery
|
||||
// L11-15: Mind & Body
|
||||
// L16-20: Advanced Training
|
||||
// L21-25: Path Specialization
|
||||
// L26-30: Ultimate Mastery
|
||||
}
|
||||
|
||||
// Check if level is a checkpoint
|
||||
isCheckpointLevel(levelNum) {
|
||||
// Returns true for levels 1, 5, 10, 15, 20, 25
|
||||
}
|
||||
|
||||
// Save/load
|
||||
saveProgress() { /* save to gameData */ }
|
||||
loadProgress() { /* load from gameData */ }
|
||||
}
|
||||
```
|
||||
|
||||
**Testing Checklist**:
|
||||
- [ ] Can start Level 1
|
||||
- [ ] Completing L1 unlocks L2
|
||||
- [ ] Cannot start L2 before completing L1
|
||||
- [ ] Failing L1 allows restart
|
||||
- [ ] Progress persists across page refresh
|
||||
- [ ] Arc detection works correctly
|
||||
- [ ] Checkpoint detection works for L1, L5, L10, etc.
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Preference Management System (3-4 hours)
|
||||
|
||||
**File to Create**: `src/features/academy/preferenceManager.js`
|
||||
|
||||
**Responsibilities**:
|
||||
- Display preference form at checkpoint levels
|
||||
- Save user preferences
|
||||
- Filter content based on preferences
|
||||
- Generate preference-based text (captions, mantras, oaths)
|
||||
|
||||
**Data Structure** (in `gameData.js`):
|
||||
```javascript
|
||||
academyPreferences: {
|
||||
lastUpdatedLevel: 1,
|
||||
contentThemes: {
|
||||
sissy: false,
|
||||
humiliation: false,
|
||||
worship: false,
|
||||
denial: false,
|
||||
obedience: true,
|
||||
bimbo: false,
|
||||
findom: false,
|
||||
edging: true // always true
|
||||
},
|
||||
visualPreferences: {
|
||||
amateur: true,
|
||||
professional: true,
|
||||
hentai: false,
|
||||
bodyTypes: ['curvy', 'athletic'],
|
||||
specificContent: [] // 'feet', 'BBC', etc.
|
||||
},
|
||||
intensity: {
|
||||
soft: false,
|
||||
moderate: true,
|
||||
harsh: false,
|
||||
extreme: false
|
||||
},
|
||||
captionTone: {
|
||||
neutral: true,
|
||||
masculine: false,
|
||||
feminine: false,
|
||||
motivational: true,
|
||||
degrading: false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key Methods**:
|
||||
```javascript
|
||||
class PreferenceManager {
|
||||
// Display preference form (at checkpoints)
|
||||
showPreferenceForm(levelNum) {
|
||||
// Generate HTML form with checkboxes
|
||||
// Pre-fill with current preferences
|
||||
// Return promise that resolves when user submits
|
||||
}
|
||||
|
||||
// Save preferences
|
||||
savePreferences(prefs) {
|
||||
// Update academyPreferences in gameData
|
||||
// Set lastUpdatedLevel
|
||||
// Save to storage
|
||||
}
|
||||
|
||||
// Get preferences
|
||||
getPreferences() {
|
||||
// Return current preferences
|
||||
}
|
||||
|
||||
// Check if theme is enabled
|
||||
isThemeEnabled(theme) {
|
||||
// Returns true if contentThemes[theme] === true
|
||||
}
|
||||
|
||||
// Get enabled themes
|
||||
getEnabledThemes() {
|
||||
// Returns array of enabled theme names
|
||||
}
|
||||
|
||||
// Generate preference-based text
|
||||
generateMantra() {
|
||||
// Based on enabled themes, return appropriate mantra
|
||||
// e.g., sissy enabled: "I am a sissy gooner..."
|
||||
}
|
||||
|
||||
generateCaptionTags() {
|
||||
// Returns array of caption tags based on themes
|
||||
// Used to filter caption images
|
||||
}
|
||||
|
||||
// Filter content by preferences
|
||||
matchesPreferences(contentTags) {
|
||||
// Given array of tags, check if matches user preferences
|
||||
// Returns true if compatible
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**UI Components Needed**:
|
||||
```html
|
||||
<!-- Preference Form (shown at L1, L5, L10, 15, 20, 25) -->
|
||||
<div id="preference-form">
|
||||
<h2>Set Your Preferences</h2>
|
||||
<p>Customize your Academy experience.</p>
|
||||
|
||||
<section>
|
||||
<h3>Content Themes</h3>
|
||||
<label><input type="checkbox" name="theme-sissy"> Sissy/Feminization</label>
|
||||
<label><input type="checkbox" name="theme-humiliation"> Humiliation/Degradation</label>
|
||||
<label><input type="checkbox" name="theme-worship"> Worship/Devotion</label>
|
||||
<label><input type="checkbox" name="theme-denial"> Denial/Chastity</label>
|
||||
<label><input type="checkbox" name="theme-obedience" checked> Obedience/Control</label>
|
||||
<label><input type="checkbox" name="theme-bimbo"> Bimbo/Dumbification</label>
|
||||
<label><input type="checkbox" name="theme-findom"> Findom/Tribute</label>
|
||||
</section>
|
||||
|
||||
<!-- Visual Preferences, Intensity, Caption Tone sections similar -->
|
||||
|
||||
<button id="save-preferences">Save Preferences</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Testing Checklist**:
|
||||
- [ ] Preference form appears at L1
|
||||
- [ ] Can select/deselect preferences
|
||||
- [ ] Preferences save to gameData
|
||||
- [ ] Preferences persist across refresh
|
||||
- [ ] Content filtering respects preferences
|
||||
- [ ] Mantra generation adapts to preferences
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Media Library Tagging System (4-5 hours)
|
||||
|
||||
**Files to Create**:
|
||||
- `src/features/academy/libraryManager.js`
|
||||
- `src/features/academy/tagManager.js`
|
||||
|
||||
**Responsibilities**:
|
||||
- Add directories to library
|
||||
- Tag directories (bulk tags for all files in directory)
|
||||
- Tag individual files (specific tags per file)
|
||||
- Calculate tag coverage percentage
|
||||
- Track curator rank
|
||||
- Display library statistics
|
||||
|
||||
**Data Structure** (in `gameData.js`):
|
||||
```javascript
|
||||
libraryData: {
|
||||
directories: [
|
||||
{
|
||||
path: '/assets/BBC/',
|
||||
tags: ['bbc', 'interracial'],
|
||||
addedAtLevel: 2,
|
||||
fileCount: 47,
|
||||
subdirectories: [...]
|
||||
}
|
||||
],
|
||||
individualFiles: [
|
||||
{
|
||||
path: '/assets/BBC/amateur/video01.mp4',
|
||||
type: 'video',
|
||||
duration: 272,
|
||||
quality: '1080p',
|
||||
directoryTags: ['bbc', 'interracial', 'amateur'],
|
||||
customTags: ['pov', 'blowjob'],
|
||||
allTags: ['bbc', 'interracial', 'amateur', 'pov', 'blowjob'],
|
||||
taggedAtLevel: 3,
|
||||
timesPlayed: 12
|
||||
}
|
||||
],
|
||||
statistics: {
|
||||
totalFiles: 1247,
|
||||
taggedFiles: 1200,
|
||||
untaggedFiles: 47,
|
||||
tagCoverage: 96.2,
|
||||
curatorRank: 'Expert',
|
||||
totalTags: 2847,
|
||||
uniqueTags: 87,
|
||||
topTags: [
|
||||
{ tag: 'amateur', count: 423 }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key Methods**:
|
||||
|
||||
```javascript
|
||||
class LibraryManager {
|
||||
addDirectory(path, tags = []) { /* Add to directories array */ }
|
||||
tagDirectory(path, tags) { /* Add tags to directory */ }
|
||||
tagFile(filePath, tags) { /* Add custom tags to specific file */ }
|
||||
getFileTags(filePath) { /* Return allTags for a file */ }
|
||||
getFilesByTags(tags, matchMode = 'AND') { /* Filter files by tags */ }
|
||||
updateStatistics() { /* Calculate totals, coverage %, curator rank */ }
|
||||
getCuratorRank() {
|
||||
// <50%: Novice, 50-75%: Apprentice, 75-90%: Journeyman
|
||||
// 90-98%: Expert, 98-100%: Master
|
||||
}
|
||||
}
|
||||
|
||||
class TagManager {
|
||||
getAllTags() { /* Return comprehensive tag list */ }
|
||||
suggestTags(filename) { /* Parse filename for keywords */ }
|
||||
isValidTag(tag) { /* Check if tag exists in catalog */ }
|
||||
addCustomTag(tag) { /* Allow user-defined tags */ }
|
||||
}
|
||||
```
|
||||
|
||||
**UI Components**:
|
||||
- Directory tagging form
|
||||
- File tagging form
|
||||
- Library statistics dashboard
|
||||
|
||||
**Testing Checklist**:
|
||||
- [ ] Can add directory with tags
|
||||
- [ ] Can tag individual file
|
||||
- [ ] File inherits directory tags
|
||||
- [ ] Tag coverage calculates correctly
|
||||
- [ ] Curator rank advances based on coverage
|
||||
- [ ] Statistics persist across refresh
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Tag-Based Content Filtering (2-3 hours)
|
||||
|
||||
**File to Update**: `src/features/video/videoLibrary.js`
|
||||
|
||||
**New Methods**:
|
||||
```javascript
|
||||
class VideoLibrary {
|
||||
getVideosByTags(tags, matchMode = 'AND') {
|
||||
// Use LibraryManager to get matching files
|
||||
// Filter by type === 'video'
|
||||
}
|
||||
|
||||
getVideosByPreferences(preferences) {
|
||||
// Convert preferences to tags
|
||||
// Use getVideosByTags
|
||||
}
|
||||
|
||||
getRandomVideo(tags = null, preferences = null) {
|
||||
// Filter by tags/preferences, return random
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Testing Checklist**:
|
||||
- [ ] Video filtering by tags works
|
||||
- [ ] Preference-based filtering works
|
||||
- [ ] Returns null gracefully if no matches
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Data Persistence (1-2 hours)
|
||||
|
||||
**File to Update**: `src/core/gameDataManager.js`
|
||||
|
||||
Add academy data structures and ensure save/load works.
|
||||
|
||||
**Testing Checklist**:
|
||||
- [ ] All academy data saves
|
||||
- [ ] All academy data loads on refresh
|
||||
- [ ] Existing saves migrate without errors
|
||||
|
||||
---
|
||||
|
||||
### Task 6: Basic UI Infrastructure (2-3 hours)
|
||||
|
||||
**File to Update**: `training-academy.html`
|
||||
|
||||
Add:
|
||||
- Level select screen (30 level buttons)
|
||||
- Checkpoint modal (preferences/library tabs)
|
||||
- Level completion screen
|
||||
|
||||
**Testing Checklist**:
|
||||
- [ ] Level select screen displays
|
||||
- [ ] Locked levels appear disabled
|
||||
- [ ] Checkpoint modal appears correctly
|
||||
- [ ] Level completion screen shows stats
|
||||
|
||||
---
|
||||
|
||||
## 📏 Measurable Test Criteria
|
||||
|
||||
After Phase 1, ALL of these must pass:
|
||||
|
||||
### Campaign Progression:
|
||||
- [ ] Can select and start Level 1
|
||||
- [ ] Completing Level 1 unlocks Level 2
|
||||
- [ ] Cannot start Level 2 before completing Level 1
|
||||
- [ ] Failing Level 1 allows restart
|
||||
- [ ] Progress persists across refresh
|
||||
- [ ] Current arc displays correctly
|
||||
|
||||
### Preferences:
|
||||
- [ ] Preference form appears at Level 1
|
||||
- [ ] Preferences save and persist
|
||||
- [ ] Preference form appears at Level 5
|
||||
|
||||
### Library Tagging:
|
||||
- [ ] Can add directory with tags at Level 2
|
||||
- [ ] Can tag individual file at Level 3
|
||||
- [ ] Tag coverage calculates correctly
|
||||
- [ ] Curator rank displays
|
||||
- [ ] Library statistics persist
|
||||
|
||||
### Content Filtering:
|
||||
- [ ] Can filter videos by tags
|
||||
- [ ] Preference filtering works
|
||||
|
||||
### Data Persistence:
|
||||
- [ ] All data saves and loads correctly
|
||||
- [ ] No console errors
|
||||
|
||||
### UI:
|
||||
- [ ] Level select renders 30 levels
|
||||
- [ ] Checkpoint modal functional
|
||||
- [ ] Level completion screen displays
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Success Criteria
|
||||
|
||||
**Phase 1 Complete When:**
|
||||
1. All 6 tasks implemented
|
||||
2. All measurable tests pass
|
||||
3. No console errors
|
||||
4. Can complete L1 → unlock L2 → checkpoint at L5
|
||||
5. Library tagging works at L2-3
|
||||
6. All data persists
|
||||
|
||||
---
|
||||
|
||||
## 📂 Files Created/Modified
|
||||
|
||||
### New Files:
|
||||
- `src/features/academy/campaignManager.js`
|
||||
- `src/features/academy/preferenceManager.js`
|
||||
- `src/features/academy/libraryManager.js`
|
||||
- `src/features/academy/tagManager.js`
|
||||
|
||||
### Modified Files:
|
||||
- `src/core/gameDataManager.js`
|
||||
- `src/features/video/videoLibrary.js`
|
||||
- `training-academy.html`
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Next Phase
|
||||
|
||||
After Phase 1: **Phase 2: Levels 1-10 Implementation**
|
||||
|
|
@ -0,0 +1,678 @@
|
|||
# Phase 2: Levels 1-10 Implementation
|
||||
|
||||
**Priority**: HIGH - Core tutorial and early progression
|
||||
**Estimated Effort**: 16-20 hours
|
||||
**Status**: Not Started
|
||||
**Dependencies**: Phase 1 (Campaign Foundation)
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Phase Goals
|
||||
|
||||
Implement the first 10 levels of The Academy, covering the **Foundation Arc** (L1-5) and **Feature Discovery Arc** (L6-10). These levels introduce basic edging, build to complex feature combinations, and include the first preference checkpoint.
|
||||
|
||||
---
|
||||
|
||||
## 📋 What This Phase Delivers
|
||||
|
||||
### Arcs Implemented:
|
||||
1. **Foundation Arc (L1-5)**: Basic edging, rhythm, video introduction, library setup
|
||||
2. **Feature Discovery Arc (L6-10)**: Webcam, dual video, TTS, quad video, hypno spiral
|
||||
|
||||
### Level-by-Level:
|
||||
- **L1**: Edge Training 101 (5min) - Basic edges
|
||||
- **L2**: Rhythm & Control (10min) - Rhythm patterns + library addition
|
||||
- **L3**: Visual Immersion (15min) - First porn video + file tagging
|
||||
- **L4**: Multi-Tasking Challenge (20min) - Multiple actions, rhythm + video
|
||||
- **L5**: Foundation Checkpoint (25min) - Preference update + L1-4 recap
|
||||
- **L6**: The Observer (30min) - Webcam unlock
|
||||
- **L7**: Dual Focus (40min) - Picture-in-picture dual video
|
||||
- **L8**: Vocal Commands (50min) - TTS unlock
|
||||
- **L9**: Sensory Array (60min) - Quad video unlock
|
||||
- **L10**: Hypnotic Gateway (70min) - Hypno spiral unlock + Checkpoint
|
||||
|
||||
**End Result**: Fully functional early campaign with progressive feature unlocking.
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ Implementation Tasks
|
||||
|
||||
### Task 1: Action Handler Extensions (6-8 hours)
|
||||
|
||||
**File to Update**: `src/features/tasks/interactiveTaskManager.js`
|
||||
|
||||
Add handlers for new actions used in L1-10:
|
||||
|
||||
#### New Action Types Needed:
|
||||
|
||||
**Basic Actions (L1-5)**:
|
||||
```javascript
|
||||
{
|
||||
type: 'edge',
|
||||
params: {
|
||||
count: 5,
|
||||
instruction: 'Edge 5 times in a row'
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
type: 'rhythm',
|
||||
params: {
|
||||
pattern: 'slow-fast-slow', // or 'fast-slow-fast', 'steady'
|
||||
duration: 120
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
type: 'add-library-directory',
|
||||
params: {
|
||||
directory: '/assets/amateur/',
|
||||
suggestedTags: ['amateur', 'solo']
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
type: 'tag-files',
|
||||
params: {
|
||||
directory: '/assets/amateur/',
|
||||
minFiles: 10,
|
||||
suggestedTags: ['pov', 'blowjob', 'riding']
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Feature Unlock Actions (L6-10)**:
|
||||
```javascript
|
||||
{
|
||||
type: 'enable-webcam',
|
||||
params: {
|
||||
instruction: 'Turn on your webcam to watch yourself'
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
type: 'dual-video',
|
||||
params: {
|
||||
mainVideo: 'focus',
|
||||
pipVideo: 'overlay',
|
||||
pipPosition: 'bottom-right'
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
type: 'tts-command',
|
||||
params: {
|
||||
text: 'Good gooner. Edge again.',
|
||||
voice: 'feminine'
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
type: 'quad-video',
|
||||
params: {
|
||||
layout: 'grid', // or 'cascade'
|
||||
videos: ['random', 'random', 'random', 'random']
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
type: 'hypno-spiral',
|
||||
params: {
|
||||
duration: 120,
|
||||
overlay: true,
|
||||
opacity: 0.5
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Handler Implementation:
|
||||
|
||||
```javascript
|
||||
class InteractiveTaskManager {
|
||||
// ... existing code ...
|
||||
|
||||
async handleEdgeAction(action) {
|
||||
// Show edge counter UI
|
||||
// Wait for user to edge X times
|
||||
// Track edge count
|
||||
// Return when complete
|
||||
}
|
||||
|
||||
async handleRhythmAction(action) {
|
||||
// Display rhythm pattern indicator
|
||||
// Play metronome sounds
|
||||
// Track user compliance
|
||||
}
|
||||
|
||||
async handleAddLibraryAction(action) {
|
||||
// Call LibraryManager.addDirectory
|
||||
// Show success message
|
||||
}
|
||||
|
||||
async handleTagFilesAction(action) {
|
||||
// Open tagging UI
|
||||
// Pre-fill directory and suggestions
|
||||
// Wait for user to tag N files
|
||||
// Update library stats
|
||||
}
|
||||
|
||||
async handleEnableWebcamAction(action) {
|
||||
// Call WebcamManager.start()
|
||||
// Show webcam in overlay
|
||||
// Mark webcam as unlocked
|
||||
}
|
||||
|
||||
async handleDualVideoAction(action) {
|
||||
// Load main video in FocusVideoPlayer
|
||||
// Load PiP video in OverlayVideoPlayer
|
||||
// Position PiP
|
||||
}
|
||||
|
||||
async handleTTSAction(action) {
|
||||
// Call VoiceManager.speak()
|
||||
// Wait for speech to finish
|
||||
}
|
||||
|
||||
async handleQuadVideoAction(action) {
|
||||
// Initialize QuadVideoPlayer
|
||||
// Load 4 random videos
|
||||
// Display in grid layout
|
||||
}
|
||||
|
||||
async handleHypnoSpiralAction(action) {
|
||||
// Show hypno spiral overlay
|
||||
// Animate spiral
|
||||
// Play hypno audio
|
||||
// Run for duration
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Testing Checklist**:
|
||||
- [ ] Edge action tracks count correctly
|
||||
- [ ] Rhythm action displays pattern
|
||||
- [ ] Library directory adds successfully
|
||||
- [ ] File tagging UI works
|
||||
- [ ] Webcam enables correctly
|
||||
- [ ] Dual video displays properly
|
||||
- [ ] TTS speaks commands
|
||||
- [ ] Quad video grid renders
|
||||
- [ ] Hypno spiral animates
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Level Data Definitions (4-5 hours)
|
||||
|
||||
**File to Create**: `src/data/modes/academyLevelData.js`
|
||||
|
||||
Define all 30 levels. For Phase 2, implement L1-10 in detail:
|
||||
|
||||
```javascript
|
||||
const academyLevels = {
|
||||
1: {
|
||||
name: "Edge Training 101",
|
||||
arc: "Foundation",
|
||||
duration: 300, // 5 minutes
|
||||
requirements: {
|
||||
minLevel: 1,
|
||||
featuresRequired: []
|
||||
},
|
||||
unlocks: {
|
||||
level: 2,
|
||||
features: []
|
||||
},
|
||||
sessionStructure: {
|
||||
warmup: [
|
||||
{ type: 'caption', text: 'Welcome to The Academy, gooner.' },
|
||||
{ type: 'instruction', text: 'Today you learn the basics of edging.' }
|
||||
],
|
||||
main: [
|
||||
{ type: 'edge', params: { count: 5, instruction: 'Edge 5 times slowly' } },
|
||||
{ type: 'wait', duration: 30 },
|
||||
{ type: 'edge', params: { count: 3, instruction: 'Edge 3 more times' } }
|
||||
],
|
||||
cooldown: [
|
||||
{ type: 'caption', text: 'You completed your first level!' },
|
||||
{ type: 'save-progress' }
|
||||
]
|
||||
},
|
||||
failureConditions: {
|
||||
cumming: true,
|
||||
closingFeatures: false, // no features to close yet
|
||||
abandoningSession: true
|
||||
}
|
||||
},
|
||||
|
||||
2: {
|
||||
name: "Rhythm & Control",
|
||||
arc: "Foundation",
|
||||
duration: 600, // 10 minutes
|
||||
requirements: {
|
||||
minLevel: 2,
|
||||
completedLevels: [1]
|
||||
},
|
||||
unlocks: {
|
||||
level: 3,
|
||||
features: []
|
||||
},
|
||||
sessionStructure: {
|
||||
warmup: [
|
||||
{ type: 'caption', text: 'Level 2: Learning rhythm and building your library.' }
|
||||
],
|
||||
main: [
|
||||
{ type: 'rhythm', params: { pattern: 'slow-fast-slow', duration: 180 } },
|
||||
{ type: 'add-library-directory', params: {
|
||||
directory: '/assets/amateur/',
|
||||
suggestedTags: ['amateur', 'solo']
|
||||
}},
|
||||
{ type: 'edge', params: { count: 10 } }
|
||||
],
|
||||
cooldown: [
|
||||
{ type: 'caption', text: 'Your library is growing...' }
|
||||
]
|
||||
},
|
||||
failureConditions: {
|
||||
cumming: true,
|
||||
abandoningSession: true
|
||||
}
|
||||
},
|
||||
|
||||
3: {
|
||||
name: "Visual Immersion",
|
||||
arc: "Foundation",
|
||||
duration: 900, // 15 minutes
|
||||
requirements: {
|
||||
minLevel: 3,
|
||||
completedLevels: [2]
|
||||
},
|
||||
unlocks: {
|
||||
level: 4,
|
||||
features: ['video']
|
||||
},
|
||||
sessionStructure: {
|
||||
warmup: [
|
||||
{ type: 'caption', text: 'Time to add visuals to your training.' }
|
||||
],
|
||||
main: [
|
||||
{ type: 'video-start', params: { player: 'focus', tags: ['amateur'] } },
|
||||
{ type: 'tag-files', params: {
|
||||
directory: '/assets/amateur/',
|
||||
minFiles: 10,
|
||||
suggestedTags: ['pov', 'blowjob', 'riding']
|
||||
}},
|
||||
{ type: 'edge', params: { count: 15 } },
|
||||
{ type: 'video-stop' }
|
||||
],
|
||||
cooldown: []
|
||||
},
|
||||
failureConditions: {
|
||||
cumming: true,
|
||||
abandoningSession: true,
|
||||
closingFeatures: ['video']
|
||||
}
|
||||
},
|
||||
|
||||
4: {
|
||||
name: "Multi-Tasking Challenge",
|
||||
arc: "Foundation",
|
||||
duration: 1200, // 20 minutes
|
||||
requirements: {
|
||||
minLevel: 4,
|
||||
completedLevels: [3]
|
||||
},
|
||||
unlocks: {
|
||||
level: 5
|
||||
},
|
||||
sessionStructure: {
|
||||
warmup: [],
|
||||
main: [
|
||||
{ type: 'video-start', params: { player: 'focus' } },
|
||||
{ type: 'rhythm', params: { pattern: 'fast-slow-fast', duration: 240 } },
|
||||
{ type: 'edge', params: { count: 20 } },
|
||||
{ type: 'caption', text: 'Keep going... faster...' }
|
||||
],
|
||||
cooldown: []
|
||||
}
|
||||
},
|
||||
|
||||
5: {
|
||||
name: "Foundation Checkpoint",
|
||||
arc: "Foundation",
|
||||
duration: 1500, // 25 minutes
|
||||
isCheckpoint: true,
|
||||
requirements: {
|
||||
minLevel: 5,
|
||||
completedLevels: [4]
|
||||
},
|
||||
unlocks: {
|
||||
level: 6,
|
||||
features: [],
|
||||
arcsCompleted: ['Foundation']
|
||||
},
|
||||
sessionStructure: {
|
||||
warmup: [
|
||||
{ type: 'show-preferences', reason: 'checkpoint' }
|
||||
],
|
||||
main: [
|
||||
// Recap of L1-4 techniques
|
||||
{ type: 'edge', params: { count: 5 } },
|
||||
{ type: 'rhythm', params: { pattern: 'slow-fast-slow', duration: 120 } },
|
||||
{ type: 'video-start' },
|
||||
{ type: 'edge', params: { count: 25 } }
|
||||
],
|
||||
cooldown: [
|
||||
{ type: 'show-arc-complete', arc: 'Foundation' }
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
6: {
|
||||
name: "The Observer",
|
||||
arc: "Feature Discovery",
|
||||
duration: 1800, // 30 minutes
|
||||
requirements: {
|
||||
minLevel: 6,
|
||||
completedLevels: [5]
|
||||
},
|
||||
unlocks: {
|
||||
level: 7,
|
||||
features: ['webcam']
|
||||
},
|
||||
sessionStructure: {
|
||||
warmup: [
|
||||
{ type: 'caption', text: 'Welcome to Feature Discovery. Time to watch yourself.' }
|
||||
],
|
||||
main: [
|
||||
{ type: 'enable-webcam' },
|
||||
{ type: 'video-start' },
|
||||
{ type: 'caption', text: 'Watch yourself gooning...' },
|
||||
{ type: 'edge', params: { count: 30 } }
|
||||
],
|
||||
cooldown: []
|
||||
},
|
||||
failureConditions: {
|
||||
cumming: true,
|
||||
abandoningSession: true,
|
||||
closingFeatures: ['webcam', 'video']
|
||||
}
|
||||
},
|
||||
|
||||
7: {
|
||||
name: "Dual Focus",
|
||||
arc: "Feature Discovery",
|
||||
duration: 2400, // 40 minutes
|
||||
requirements: {
|
||||
minLevel: 7,
|
||||
completedLevels: [6],
|
||||
featuresUnlocked: ['webcam']
|
||||
},
|
||||
unlocks: {
|
||||
level: 8,
|
||||
features: ['dual-video']
|
||||
},
|
||||
sessionStructure: {
|
||||
main: [
|
||||
{ type: 'dual-video', params: { mainVideo: 'focus', pipVideo: 'overlay' } },
|
||||
{ type: 'edge', params: { count: 40 } }
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
8: {
|
||||
name: "Vocal Commands",
|
||||
arc: "Feature Discovery",
|
||||
duration: 3000, // 50 minutes
|
||||
requirements: {
|
||||
minLevel: 8,
|
||||
completedLevels: [7],
|
||||
featuresUnlocked: ['dual-video']
|
||||
},
|
||||
unlocks: {
|
||||
level: 9,
|
||||
features: ['tts']
|
||||
},
|
||||
sessionStructure: {
|
||||
main: [
|
||||
{ type: 'enable-tts' },
|
||||
{ type: 'video-start' },
|
||||
{ type: 'tts-command', params: { text: 'Edge for me, gooner.' } },
|
||||
{ type: 'edge', params: { count: 50 } },
|
||||
{ type: 'tts-command', params: { text: 'Good gooner. Keep going.' } }
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
9: {
|
||||
name: "Sensory Array",
|
||||
arc: "Feature Discovery",
|
||||
duration: 3600, // 60 minutes
|
||||
requirements: {
|
||||
minLevel: 9,
|
||||
completedLevels: [8],
|
||||
featuresUnlocked: ['tts']
|
||||
},
|
||||
unlocks: {
|
||||
level: 10,
|
||||
features: ['quad-video']
|
||||
},
|
||||
sessionStructure: {
|
||||
main: [
|
||||
{ type: 'quad-video', params: { layout: 'grid' } },
|
||||
{ type: 'tts-command', params: { text: 'Watch all four screens...' } },
|
||||
{ type: 'edge', params: { count: 60 } }
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
10: {
|
||||
name: "Hypnotic Gateway",
|
||||
arc: "Feature Discovery",
|
||||
duration: 4200, // 70 minutes
|
||||
isCheckpoint: true,
|
||||
requirements: {
|
||||
minLevel: 10,
|
||||
completedLevels: [9],
|
||||
featuresUnlocked: ['quad-video']
|
||||
},
|
||||
unlocks: {
|
||||
level: 11,
|
||||
features: ['hypno'],
|
||||
arcsCompleted: ['Feature Discovery']
|
||||
},
|
||||
sessionStructure: {
|
||||
warmup: [
|
||||
{ type: 'show-preferences', reason: 'checkpoint' }
|
||||
],
|
||||
main: [
|
||||
{ type: 'hypno-spiral', params: { duration: 120, overlay: true } },
|
||||
{ type: 'quad-video' },
|
||||
{ type: 'tts-command', params: { text: 'Deeper... watch the spiral...' } },
|
||||
{ type: 'edge', params: { count: 70 } }
|
||||
],
|
||||
cooldown: [
|
||||
{ type: 'show-arc-complete', arc: 'Feature Discovery' }
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default academyLevels;
|
||||
```
|
||||
|
||||
**Testing Checklist**:
|
||||
- [ ] All 10 levels defined
|
||||
- [ ] Level requirements check correctly
|
||||
- [ ] Unlocks work as specified
|
||||
- [ ] Checkpoint levels trigger preferences
|
||||
|
||||
---
|
||||
|
||||
### Task 3: UI Components for Levels (3-4 hours)
|
||||
|
||||
**File to Update**: `training-academy.html`
|
||||
|
||||
Add UI elements:
|
||||
|
||||
```html
|
||||
<!-- Level Completion Screen -->
|
||||
<div id="level-complete-modal" class="modal hidden">
|
||||
<h2>Level Complete!</h2>
|
||||
<div id="level-stats">
|
||||
<p>Time: <span id="session-time"></span></p>
|
||||
<p>Edges: <span id="edge-count"></span></p>
|
||||
<p>Next Level Unlocked: <span id="next-level"></span></p>
|
||||
</div>
|
||||
<button id="continue-btn">Continue</button>
|
||||
</div>
|
||||
|
||||
<!-- Arc Complete Screen -->
|
||||
<div id="arc-complete-modal" class="modal hidden">
|
||||
<h2>Arc Complete: <span id="arc-name"></span></h2>
|
||||
<p id="arc-description"></p>
|
||||
<button id="continue-arc-btn">Continue to Next Arc</button>
|
||||
</div>
|
||||
|
||||
<!-- Level Select Screen Updates -->
|
||||
<div id="level-grid">
|
||||
<!-- Generate 30 level cards -->
|
||||
<!-- L1-10 should be functional after this phase -->
|
||||
</div>
|
||||
```
|
||||
|
||||
**Testing Checklist**:
|
||||
- [ ] Level complete modal displays stats
|
||||
- [ ] Arc complete modal shows at L5 and L10
|
||||
- [ ] Level select shows L1-10 unlockable
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Feature Unlock Integration (2-3 hours)
|
||||
|
||||
Connect action handlers to actual feature managers:
|
||||
|
||||
**Webcam Unlock** → `WebcamManager.start()`
|
||||
**Dual Video** → `FocusVideoPlayer` + `OverlayVideoPlayer`
|
||||
**TTS** → `VoiceManager.speak()`
|
||||
**Quad Video** → `QuadVideoPlayer.initialize()`
|
||||
**Hypno** → Create hypno spiral overlay
|
||||
|
||||
**Testing Checklist**:
|
||||
- [ ] Webcam activates at L6
|
||||
- [ ] Dual video works at L7
|
||||
- [ ] TTS speaks at L8
|
||||
- [ ] Quad video displays at L9
|
||||
- [ ] Hypno spiral shows at L10
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Library/Preference Integration (1-2 hours)
|
||||
|
||||
Ensure checkpoints work:
|
||||
|
||||
**At L1**: Show preference form
|
||||
**At L5**: Show preference form + library stats
|
||||
**At L10**: Show preference form + library stats
|
||||
|
||||
**Testing Checklist**:
|
||||
- [ ] L1 shows preferences
|
||||
- [ ] L5 shows preferences and library
|
||||
- [ ] L10 shows preferences and library
|
||||
- [ ] Preferences affect content selection
|
||||
|
||||
---
|
||||
|
||||
## 📏 Measurable Test Criteria
|
||||
|
||||
After Phase 2, ALL of these must pass:
|
||||
|
||||
### Level 1:
|
||||
- [ ] Can start L1
|
||||
- [ ] Preference form appears
|
||||
- [ ] Edge counter works
|
||||
- [ ] Completing L1 unlocks L2
|
||||
|
||||
### Level 2:
|
||||
- [ ] Rhythm pattern displays
|
||||
- [ ] Library directory adds
|
||||
- [ ] Completing L2 unlocks L3
|
||||
|
||||
### Level 3:
|
||||
- [ ] Video plays
|
||||
- [ ] Tagging UI works
|
||||
- [ ] Tag at least 10 files
|
||||
- [ ] Completing L3 unlocks L4
|
||||
|
||||
### Level 4:
|
||||
- [ ] Video + rhythm work simultaneously
|
||||
- [ ] Completing L4 unlocks L5
|
||||
|
||||
### Level 5 (Checkpoint):
|
||||
- [ ] Preference form appears
|
||||
- [ ] Library stats display
|
||||
- [ ] Foundation Arc completes
|
||||
- [ ] Completing L5 unlocks L6
|
||||
|
||||
### Level 6:
|
||||
- [ ] Webcam enables
|
||||
- [ ] Webcam feed displays
|
||||
- [ ] Completing L6 unlocks L7
|
||||
|
||||
### Level 7:
|
||||
- [ ] Dual video displays (main + PiP)
|
||||
- [ ] PiP positions correctly
|
||||
- [ ] Completing L7 unlocks L8
|
||||
|
||||
### Level 8:
|
||||
- [ ] TTS speaks commands
|
||||
- [ ] Voice is audible
|
||||
- [ ] Completing L8 unlocks L9
|
||||
|
||||
### Level 9:
|
||||
- [ ] Quad video grid displays
|
||||
- [ ] All 4 videos play
|
||||
- [ ] Completing L9 unlocks L10
|
||||
|
||||
### Level 10 (Checkpoint):
|
||||
- [ ] Hypno spiral displays
|
||||
- [ ] Spiral animates
|
||||
- [ ] Preference form appears
|
||||
- [ ] Feature Discovery Arc completes
|
||||
- [ ] Completing L10 unlocks L11
|
||||
|
||||
### Overall:
|
||||
- [ ] All levels playable
|
||||
- [ ] No console errors
|
||||
- [ ] Progress saves after each level
|
||||
- [ ] Feature unlocks persist
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Success Criteria
|
||||
|
||||
**Phase 2 Complete When:**
|
||||
1. Levels 1-10 fully functional
|
||||
2. Foundation + Feature Discovery arcs complete
|
||||
3. All measurable tests pass
|
||||
4. Checkpoints at L5 and L10 work
|
||||
5. Feature unlocking progresses correctly
|
||||
6. Can play L1 → L10 sequentially
|
||||
|
||||
---
|
||||
|
||||
## 📂 Files Created/Modified
|
||||
|
||||
### New Files:
|
||||
- `src/data/modes/academyLevelData.js`
|
||||
|
||||
### Modified Files:
|
||||
- `src/features/tasks/interactiveTaskManager.js`
|
||||
- `training-academy.html`
|
||||
- `src/features/webcam/webcamManager.js`
|
||||
- `src/features/video/videoPlayerManager.js`
|
||||
- `src/features/tts/voiceManager.js`
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Next Phase
|
||||
|
||||
After Phase 2: **Phase 3: Levels 11-20 Implementation** (Mind & Body + Advanced Training arcs)
|
||||
|
|
@ -0,0 +1,785 @@
|
|||
# Phase 3: Levels 11-20 Implementation
|
||||
|
||||
**Priority**: MEDIUM-HIGH - Advanced features and complexity
|
||||
**Estimated Effort**: 18-24 hours
|
||||
**Status**: Not Started
|
||||
**Dependencies**: Phase 2 (Levels 1-10)
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Phase Goals
|
||||
|
||||
Implement levels 11-20, covering the **Mind & Body Arc** (L11-15) and **Advanced Training Arc** (L16-20). These levels introduce hypno overlays, captions, interruptions, popups, and complex multi-feature coordination.
|
||||
|
||||
---
|
||||
|
||||
## 📋 What This Phase Delivers
|
||||
|
||||
### Arcs Implemented:
|
||||
1. **Mind & Body Arc (L11-15)**: Hypno overlays, captions, TTS+spiral combos, sensory overload
|
||||
2. **Advanced Training Arc (L16-20)**: Interruptions, denial training, popup images, 90-minute marathon
|
||||
|
||||
### Level-by-Level:
|
||||
- **L11**: Mind Sync (75min) - Hypno + captions
|
||||
- **L12**: Caption Conditioning (80min) - Dynamic captions based on preferences
|
||||
- **L13**: Voice in the Spiral (85min) - TTS + hypno synchronized
|
||||
- **L14**: Sensory Overload (90min) - Quad + hypno + TTS + captions
|
||||
- **L15**: Mind & Body Checkpoint (100min) - Preference update + arc recap
|
||||
- **L16**: Controlled Chaos (110min) - Interruptions unlock
|
||||
- **L17**: The Denial Dance (120min) - Forced denial training
|
||||
- **L18**: Popup Distraction (130min) - Random popup images
|
||||
- **L19**: Total Immersion (140min) - All features combined
|
||||
- **L20**: Ultimate Checkpoint (150min) - All features unlocked, preference update
|
||||
|
||||
**End Result**: Full feature suite unlocked, players mastering complex multi-feature sessions.
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ Implementation Tasks
|
||||
|
||||
### Task 1: Advanced Action Handlers (7-9 hours)
|
||||
|
||||
**File to Update**: `src/features/tasks/interactiveTaskManager.js`
|
||||
|
||||
Add handlers for L11-20 actions:
|
||||
|
||||
#### New Action Types:
|
||||
|
||||
**Hypno + Caption Combos (L11-14)**:
|
||||
```javascript
|
||||
{
|
||||
type: 'hypno-caption-combo',
|
||||
params: {
|
||||
hypnoDuration: 180,
|
||||
captions: [
|
||||
{ text: 'You are a good gooner', delay: 10 },
|
||||
{ text: 'Edging is pleasure', delay: 30 }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
type: 'dynamic-captions',
|
||||
params: {
|
||||
duration: 300,
|
||||
captionSource: 'preference-based', // generates based on user preferences
|
||||
frequency: 15 // new caption every 15 seconds
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
type: 'tts-hypno-sync',
|
||||
params: {
|
||||
spiralDuration: 120,
|
||||
ttsCommands: [
|
||||
{ text: 'Watch the spiral...', delay: 10 },
|
||||
{ text: 'Deeper...', delay: 60 }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
type: 'sensory-overload',
|
||||
params: {
|
||||
features: ['quad-video', 'hypno', 'tts', 'captions'],
|
||||
duration: 300
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Interruption System (L16)**:
|
||||
```javascript
|
||||
{
|
||||
type: 'enable-interruptions',
|
||||
params: {
|
||||
types: ['edge', 'pose', 'mantra'],
|
||||
frequency: 'medium', // low/medium/high
|
||||
randomize: true
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
type: 'interruption',
|
||||
params: {
|
||||
interruptionType: 'edge', // 'edge', 'pose', 'mantra', 'stop-stroking'
|
||||
duration: 30,
|
||||
instruction: 'STOP! Edge right now!'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Denial Training (L17)**:
|
||||
```javascript
|
||||
{
|
||||
type: 'denial-training',
|
||||
params: {
|
||||
denialPeriods: [
|
||||
{ duration: 120, instruction: 'No touching for 2 minutes' },
|
||||
{ allowStroking: 60 },
|
||||
{ duration: 180, instruction: 'Hands off again' }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
type: 'stop-stroking',
|
||||
params: {
|
||||
duration: 120,
|
||||
showTimer: true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Popup System (L18)**:
|
||||
```javascript
|
||||
{
|
||||
type: 'enable-popups',
|
||||
params: {
|
||||
frequency: 'medium',
|
||||
sources: ['tasks', 'consequences'], // image folders
|
||||
duration: 10, // seconds per popup
|
||||
randomize: true
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
type: 'popup-image',
|
||||
params: {
|
||||
imagePath: '/images/tasks/edge.png',
|
||||
duration: 10,
|
||||
position: 'center' // or random
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Handler Implementation:
|
||||
|
||||
```javascript
|
||||
class InteractiveTaskManager {
|
||||
// ... existing handlers ...
|
||||
|
||||
async handleHypnoCaptionCombo(action) {
|
||||
// Start hypno spiral
|
||||
// Schedule captions at specified delays
|
||||
// Run both simultaneously
|
||||
}
|
||||
|
||||
async handleDynamicCaptions(action) {
|
||||
// Get user preferences
|
||||
// Generate captions based on themes
|
||||
// Display new caption every N seconds
|
||||
// Track caption history
|
||||
}
|
||||
|
||||
async handleTTSHypnoSync(action) {
|
||||
// Start spiral
|
||||
// Schedule TTS commands
|
||||
// Synchronize timing
|
||||
}
|
||||
|
||||
async handleSensoryOverload(action) {
|
||||
// Activate all specified features
|
||||
// Quad video + hypno + TTS + captions
|
||||
// Coordinate timing
|
||||
}
|
||||
|
||||
async handleEnableInterruptions(action) {
|
||||
// Set up random interruption scheduler
|
||||
// Based on frequency, schedule random interruptions
|
||||
// Store interruption state
|
||||
}
|
||||
|
||||
async handleInterruption(action) {
|
||||
// Pause current activities
|
||||
// Show interruption overlay (STOP! EDGE!)
|
||||
// Wait for user to complete
|
||||
// Resume activities
|
||||
}
|
||||
|
||||
async handleDenialTraining(action) {
|
||||
// Loop through denial periods
|
||||
// Enforce "hands off" periods with timer
|
||||
// Allow stroking periods
|
||||
}
|
||||
|
||||
async handleStopStroking(action) {
|
||||
// Display "HANDS OFF" timer
|
||||
// Count down duration
|
||||
// Detect if user fails (optional)
|
||||
}
|
||||
|
||||
async handleEnablePopups(action) {
|
||||
// Set up random popup scheduler
|
||||
// Based on frequency, show random images
|
||||
}
|
||||
|
||||
async handlePopupImage(action) {
|
||||
// Display image overlay
|
||||
// Auto-hide after duration
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Testing Checklist**:
|
||||
- [ ] Hypno + captions work simultaneously
|
||||
- [ ] Dynamic captions generate from preferences
|
||||
- [ ] TTS + hypno synchronize correctly
|
||||
- [ ] Sensory overload coordinates all features
|
||||
- [ ] Interruptions trigger randomly
|
||||
- [ ] Denial periods enforce "hands off"
|
||||
- [ ] Popups display at correct frequency
|
||||
- [ ] All advanced handlers work without conflicts
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Caption System (4-5 hours)
|
||||
|
||||
**File to Create**: `src/features/captions/captionManager.js`
|
||||
|
||||
**Responsibilities**:
|
||||
- Generate captions based on preferences
|
||||
- Display captions with timing
|
||||
- Track caption history
|
||||
- Support manual and automatic captions
|
||||
|
||||
**Data Structure**:
|
||||
```javascript
|
||||
const captionTemplates = {
|
||||
edging: [
|
||||
"You are a good gooner",
|
||||
"Edge for me",
|
||||
"Stroke faster",
|
||||
"Slower now..."
|
||||
],
|
||||
sissy: [
|
||||
"You're such a sissy",
|
||||
"Good girl",
|
||||
"Embrace your feminine side"
|
||||
],
|
||||
humiliation: [
|
||||
"You're addicted",
|
||||
"You can't stop",
|
||||
"This is what you are"
|
||||
],
|
||||
worship: [
|
||||
"Worship this body",
|
||||
"You live to serve",
|
||||
"Obey"
|
||||
]
|
||||
// ... more themes
|
||||
};
|
||||
```
|
||||
|
||||
**Methods**:
|
||||
```javascript
|
||||
class CaptionManager {
|
||||
generateCaption(themes) {
|
||||
// Pick random template from enabled themes
|
||||
}
|
||||
|
||||
displayCaption(text, duration = 5) {
|
||||
// Show caption overlay
|
||||
// Auto-hide after duration
|
||||
}
|
||||
|
||||
startAutoCaptions(frequency, themes) {
|
||||
// Generate and display captions every N seconds
|
||||
}
|
||||
|
||||
stopAutoCaptions() {
|
||||
// Stop auto-caption loop
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**UI Component**:
|
||||
```html
|
||||
<div id="caption-overlay" class="hidden">
|
||||
<p id="caption-text"></p>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Testing Checklist**:
|
||||
- [ ] Captions generate from templates
|
||||
- [ ] Captions respect preferences
|
||||
- [ ] Auto-captions cycle correctly
|
||||
- [ ] Manual captions display properly
|
||||
- [ ] Caption overlay styled correctly
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Interruption System (3-4 hours)
|
||||
|
||||
**File to Create**: `src/features/interruptions/interruptionManager.js`
|
||||
|
||||
**Responsibilities**:
|
||||
- Schedule random interruptions
|
||||
- Display interruption overlays
|
||||
- Pause/resume current activities
|
||||
- Track interruption compliance
|
||||
|
||||
**Interruption Types**:
|
||||
- **Edge**: "STOP! Edge right now!"
|
||||
- **Pose**: "Hold this pose for 30 seconds"
|
||||
- **Mantra**: "Repeat: I am a good gooner"
|
||||
- **Stop Stroking**: "Hands off for 2 minutes"
|
||||
|
||||
**Methods**:
|
||||
```javascript
|
||||
class InterruptionManager {
|
||||
enable(frequency = 'medium') {
|
||||
// Schedule random interruptions
|
||||
// frequency: low (5-10min), medium (3-5min), high (1-3min)
|
||||
}
|
||||
|
||||
disable() {
|
||||
// Stop interruption scheduler
|
||||
}
|
||||
|
||||
triggerInterruption(type, params) {
|
||||
// Pause current activities
|
||||
// Show interruption overlay
|
||||
// Execute interruption
|
||||
// Resume activities
|
||||
}
|
||||
|
||||
scheduleNext() {
|
||||
// Schedule next random interruption
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**UI Component**:
|
||||
```html
|
||||
<div id="interruption-overlay" class="modal hidden">
|
||||
<h2>INTERRUPTION!</h2>
|
||||
<p id="interruption-instruction"></p>
|
||||
<div id="interruption-timer"></div>
|
||||
<button id="interruption-complete">Done</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Testing Checklist**:
|
||||
- [ ] Interruptions trigger at correct frequency
|
||||
- [ ] Interruption overlay displays
|
||||
- [ ] Current activities pause
|
||||
- [ ] Activities resume after interruption
|
||||
- [ ] All interruption types work
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Popup Image System (2-3 hours)
|
||||
|
||||
**File to Update**: `src/features/images/popupImageManager.js`
|
||||
|
||||
**Enhancements**:
|
||||
- Random scheduling based on frequency
|
||||
- Tag-based image selection
|
||||
- Auto-hide after duration
|
||||
|
||||
**Methods**:
|
||||
```javascript
|
||||
class PopupImageManager {
|
||||
enableAutoPopups(frequency, tags) {
|
||||
// Schedule random popups
|
||||
// Filter images by tags
|
||||
}
|
||||
|
||||
disableAutoPopups() {
|
||||
// Stop popup scheduler
|
||||
}
|
||||
|
||||
showPopup(imagePath, duration = 10) {
|
||||
// Display popup
|
||||
// Auto-hide after duration
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Testing Checklist**:
|
||||
- [ ] Popups trigger at correct frequency
|
||||
- [ ] Images filtered by tags
|
||||
- [ ] Popups auto-hide
|
||||
- [ ] Multiple popups don't overlap
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Level Data for L11-20 (3-4 hours)
|
||||
|
||||
**File to Update**: `src/data/modes/academyLevelData.js`
|
||||
|
||||
Add levels 11-20:
|
||||
|
||||
```javascript
|
||||
const academyLevels = {
|
||||
// ... L1-10 ...
|
||||
|
||||
11: {
|
||||
name: "Mind Sync",
|
||||
arc: "Mind & Body",
|
||||
duration: 4500, // 75 minutes
|
||||
requirements: {
|
||||
minLevel: 11,
|
||||
completedLevels: [10],
|
||||
featuresUnlocked: ['hypno']
|
||||
},
|
||||
unlocks: {
|
||||
level: 12,
|
||||
features: ['captions']
|
||||
},
|
||||
sessionStructure: {
|
||||
main: [
|
||||
{ type: 'hypno-caption-combo', params: {
|
||||
hypnoDuration: 180,
|
||||
captions: [
|
||||
{ text: 'You are a good gooner', delay: 10 },
|
||||
{ text: 'Edging is your purpose', delay: 60 }
|
||||
]
|
||||
}},
|
||||
{ type: 'quad-video' },
|
||||
{ type: 'edge', params: { count: 75 } }
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
12: {
|
||||
name: "Caption Conditioning",
|
||||
arc: "Mind & Body",
|
||||
duration: 4800, // 80 minutes
|
||||
requirements: {
|
||||
minLevel: 12,
|
||||
completedLevels: [11],
|
||||
featuresUnlocked: ['captions']
|
||||
},
|
||||
unlocks: {
|
||||
level: 13
|
||||
},
|
||||
sessionStructure: {
|
||||
main: [
|
||||
{ type: 'dynamic-captions', params: {
|
||||
duration: 4800,
|
||||
captionSource: 'preference-based',
|
||||
frequency: 20
|
||||
}},
|
||||
{ type: 'video-start' },
|
||||
{ type: 'edge', params: { count: 80 } }
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
13: {
|
||||
name: "Voice in the Spiral",
|
||||
arc: "Mind & Body",
|
||||
duration: 5100, // 85 minutes
|
||||
requirements: {
|
||||
minLevel: 13,
|
||||
completedLevels: [12]
|
||||
},
|
||||
unlocks: {
|
||||
level: 14
|
||||
},
|
||||
sessionStructure: {
|
||||
main: [
|
||||
{ type: 'tts-hypno-sync', params: {
|
||||
spiralDuration: 300,
|
||||
ttsCommands: [
|
||||
{ text: 'Watch the spiral...', delay: 10 },
|
||||
{ text: 'Deeper and deeper...', delay: 120 },
|
||||
{ text: 'You are under my control...', delay: 240 }
|
||||
]
|
||||
}},
|
||||
{ type: 'edge', params: { count: 85 } }
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
14: {
|
||||
name: "Sensory Overload",
|
||||
arc: "Mind & Body",
|
||||
duration: 5400, // 90 minutes
|
||||
requirements: {
|
||||
minLevel: 14,
|
||||
completedLevels: [13]
|
||||
},
|
||||
unlocks: {
|
||||
level: 15
|
||||
},
|
||||
sessionStructure: {
|
||||
main: [
|
||||
{ type: 'sensory-overload', params: {
|
||||
features: ['quad-video', 'hypno', 'tts', 'captions'],
|
||||
duration: 5400
|
||||
}},
|
||||
{ type: 'edge', params: { count: 90 } }
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
15: {
|
||||
name: "Mind & Body Checkpoint",
|
||||
arc: "Mind & Body",
|
||||
duration: 6000, // 100 minutes
|
||||
isCheckpoint: true,
|
||||
requirements: {
|
||||
minLevel: 15,
|
||||
completedLevels: [14]
|
||||
},
|
||||
unlocks: {
|
||||
level: 16,
|
||||
features: [],
|
||||
arcsCompleted: ['Mind & Body']
|
||||
},
|
||||
sessionStructure: {
|
||||
warmup: [
|
||||
{ type: 'show-preferences', reason: 'checkpoint' }
|
||||
],
|
||||
main: [
|
||||
{ type: 'sensory-overload', params: { features: ['quad-video', 'hypno', 'tts', 'captions'], duration: 5400 } },
|
||||
{ type: 'edge', params: { count: 100 } }
|
||||
],
|
||||
cooldown: [
|
||||
{ type: 'show-arc-complete', arc: 'Mind & Body' }
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
16: {
|
||||
name: "Controlled Chaos",
|
||||
arc: "Advanced Training",
|
||||
duration: 6600, // 110 minutes
|
||||
requirements: {
|
||||
minLevel: 16,
|
||||
completedLevels: [15]
|
||||
},
|
||||
unlocks: {
|
||||
level: 17,
|
||||
features: ['interruptions']
|
||||
},
|
||||
sessionStructure: {
|
||||
main: [
|
||||
{ type: 'enable-interruptions', params: { types: ['edge', 'pose'], frequency: 'medium' } },
|
||||
{ type: 'quad-video' },
|
||||
{ type: 'edge', params: { count: 110 } }
|
||||
]
|
||||
},
|
||||
failureConditions: {
|
||||
cumming: true,
|
||||
abandoningSession: true,
|
||||
closingFeatures: ['quad-video'],
|
||||
ignoringInterruptions: true
|
||||
}
|
||||
},
|
||||
|
||||
17: {
|
||||
name: "The Denial Dance",
|
||||
arc: "Advanced Training",
|
||||
duration: 7200, // 120 minutes
|
||||
requirements: {
|
||||
minLevel: 17,
|
||||
completedLevels: [16],
|
||||
featuresUnlocked: ['interruptions']
|
||||
},
|
||||
unlocks: {
|
||||
level: 18
|
||||
},
|
||||
sessionStructure: {
|
||||
main: [
|
||||
{ type: 'denial-training', params: {
|
||||
denialPeriods: [
|
||||
{ duration: 300, instruction: 'Hands off for 5 minutes' },
|
||||
{ allowStroking: 120 },
|
||||
{ duration: 600, instruction: 'Hands off for 10 minutes' }
|
||||
]
|
||||
}},
|
||||
{ type: 'edge', params: { count: 120 } }
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
18: {
|
||||
name: "Popup Distraction",
|
||||
arc: "Advanced Training",
|
||||
duration: 7800, // 130 minutes
|
||||
requirements: {
|
||||
minLevel: 18,
|
||||
completedLevels: [17]
|
||||
},
|
||||
unlocks: {
|
||||
level: 19,
|
||||
features: ['popups']
|
||||
},
|
||||
sessionStructure: {
|
||||
main: [
|
||||
{ type: 'enable-popups', params: { frequency: 'medium', sources: ['tasks', 'consequences'] } },
|
||||
{ type: 'quad-video' },
|
||||
{ type: 'edge', params: { count: 130 } }
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
19: {
|
||||
name: "Total Immersion",
|
||||
arc: "Advanced Training",
|
||||
duration: 8400, // 140 minutes
|
||||
requirements: {
|
||||
minLevel: 19,
|
||||
completedLevels: [18],
|
||||
featuresUnlocked: ['popups']
|
||||
},
|
||||
unlocks: {
|
||||
level: 20
|
||||
},
|
||||
sessionStructure: {
|
||||
main: [
|
||||
{ type: 'enable-interruptions', params: { frequency: 'high' } },
|
||||
{ type: 'enable-popups', params: { frequency: 'high' } },
|
||||
{ type: 'sensory-overload', params: { features: ['quad-video', 'hypno', 'tts', 'captions'] } },
|
||||
{ type: 'edge', params: { count: 140 } }
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
20: {
|
||||
name: "Ultimate Checkpoint",
|
||||
arc: "Advanced Training",
|
||||
duration: 9000, // 150 minutes
|
||||
isCheckpoint: true,
|
||||
requirements: {
|
||||
minLevel: 20,
|
||||
completedLevels: [19]
|
||||
},
|
||||
unlocks: {
|
||||
level: 21,
|
||||
features: [], // All features now unlocked
|
||||
arcsCompleted: ['Advanced Training'],
|
||||
durationTiers: ['Quick', 'Standard', 'Extended'] // Marathon at L25
|
||||
},
|
||||
sessionStructure: {
|
||||
warmup: [
|
||||
{ type: 'show-preferences', reason: 'checkpoint' }
|
||||
],
|
||||
main: [
|
||||
{ type: 'enable-interruptions', params: { frequency: 'high' } },
|
||||
{ type: 'enable-popups', params: { frequency: 'high' } },
|
||||
{ type: 'sensory-overload', params: { features: ['quad-video', 'hypno', 'tts', 'captions'] } },
|
||||
{ type: 'edge', params: { count: 150 } }
|
||||
],
|
||||
cooldown: [
|
||||
{ type: 'show-arc-complete', arc: 'Advanced Training' },
|
||||
{ type: 'show-message', text: 'All features unlocked! Duration tiers available.' }
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**Testing Checklist**:
|
||||
- [ ] All L11-20 defined
|
||||
- [ ] Requirements check correctly
|
||||
- [ ] Feature unlocks work
|
||||
- [ ] Checkpoints at L15 and L20 trigger
|
||||
|
||||
---
|
||||
|
||||
## 📏 Measurable Test Criteria
|
||||
|
||||
After Phase 3, ALL of these must pass:
|
||||
|
||||
### Level 11:
|
||||
- [ ] Hypno spiral displays
|
||||
- [ ] Captions appear with spiral
|
||||
- [ ] Quad video plays
|
||||
- [ ] Completing L11 unlocks L12
|
||||
|
||||
### Level 12:
|
||||
- [ ] Dynamic captions generate
|
||||
- [ ] Captions respect preferences
|
||||
- [ ] Captions cycle every 20 seconds
|
||||
- [ ] Completing L12 unlocks L13
|
||||
|
||||
### Level 13:
|
||||
- [ ] TTS + hypno synchronize
|
||||
- [ ] Voice commands match spiral timing
|
||||
- [ ] Completing L13 unlocks L14
|
||||
|
||||
### Level 14:
|
||||
- [ ] All features activate (quad + hypno + TTS + captions)
|
||||
- [ ] No feature conflicts
|
||||
- [ ] Completing L14 unlocks L15
|
||||
|
||||
### Level 15 (Checkpoint):
|
||||
- [ ] Preference form appears
|
||||
- [ ] Library stats display
|
||||
- [ ] Mind & Body Arc completes
|
||||
- [ ] Completing L15 unlocks L16
|
||||
|
||||
### Level 16:
|
||||
- [ ] Interruptions enable
|
||||
- [ ] Interruptions trigger randomly
|
||||
- [ ] Interruption overlay displays
|
||||
- [ ] Activities pause/resume
|
||||
- [ ] Completing L16 unlocks L17
|
||||
|
||||
### Level 17:
|
||||
- [ ] Denial periods enforce "hands off"
|
||||
- [ ] Timer displays for denial periods
|
||||
- [ ] Completing L17 unlocks L18
|
||||
|
||||
### Level 18:
|
||||
- [ ] Popups enable
|
||||
- [ ] Popups display randomly
|
||||
- [ ] Images filtered correctly
|
||||
- [ ] Completing L18 unlocks L19
|
||||
|
||||
### Level 19:
|
||||
- [ ] All features coordinate (interruptions + popups + sensory overload)
|
||||
- [ ] High frequency works without conflicts
|
||||
- [ ] Completing L19 unlocks L20
|
||||
|
||||
### Level 20 (Checkpoint):
|
||||
- [ ] Preference form appears
|
||||
- [ ] Advanced Training Arc completes
|
||||
- [ ] Duration tiers unlock message displays
|
||||
- [ ] All features confirmed unlocked
|
||||
- [ ] Completing L20 unlocks L21
|
||||
|
||||
### Overall:
|
||||
- [ ] All levels L11-20 playable
|
||||
- [ ] No console errors
|
||||
- [ ] Progress saves after each level
|
||||
- [ ] Feature unlocks persist
|
||||
- [ ] Can play L11 → L20 sequentially
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Success Criteria
|
||||
|
||||
**Phase 3 Complete When:**
|
||||
1. Levels 11-20 fully functional
|
||||
2. Mind & Body + Advanced Training arcs complete
|
||||
3. All measurable tests pass
|
||||
4. Caption system working
|
||||
5. Interruption system working
|
||||
6. Popup system working
|
||||
7. All features coordinate without conflicts
|
||||
8. Checkpoints at L15 and L20 work
|
||||
|
||||
---
|
||||
|
||||
## 📂 Files Created/Modified
|
||||
|
||||
### New Files:
|
||||
- `src/features/captions/captionManager.js`
|
||||
- `src/features/interruptions/interruptionManager.js`
|
||||
|
||||
### Modified Files:
|
||||
- `src/features/tasks/interactiveTaskManager.js`
|
||||
- `src/features/images/popupImageManager.js`
|
||||
- `src/data/modes/academyLevelData.js`
|
||||
- `training-academy.html`
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Next Phase
|
||||
|
||||
After Phase 3: **Phase 4: Levels 21-25 Path Specialization**
|
||||
|
|
@ -0,0 +1,797 @@
|
|||
# Phase 4: Levels 21-25 Path Specialization
|
||||
|
||||
**Priority**: MEDIUM - Path differentiation and specialization
|
||||
**Estimated Effort**: 20-26 hours
|
||||
**Status**: Not Started
|
||||
**Dependencies**: Phase 3 (Levels 11-20)
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Phase Goals
|
||||
|
||||
Implement levels 21-25, covering the **Path Specialization Arc**. At Level 21, players choose one of 6 specialized paths based on their preferences and goals. Levels 22-25 provide path-specific scenarios and challenges.
|
||||
|
||||
---
|
||||
|
||||
## 📋 What This Phase Delivers
|
||||
|
||||
### Arc Implemented:
|
||||
**Path Specialization Arc (L21-25)**: Path selection + 4 path-specific levels
|
||||
|
||||
### The 6 Paths:
|
||||
1. **Endurance Path**: Stamina building, marathon sessions, long-duration challenges
|
||||
2. **Denial Path**: Extended denial, edging without release, frustration training
|
||||
3. **Humiliation Path**: Degradation scenarios, humiliation tasks, ego dissolution
|
||||
4. **Obedience Path**: Command following, submission training, control surrender
|
||||
5. **Sensitivity Path**: Heightened arousal, minimal touch, extreme sensitivity
|
||||
6. **Multi-Sensory Path**: All features maxed, overwhelming stimulation, sensory mastery
|
||||
|
||||
### Level-by-Level:
|
||||
- **L21**: Path Selection (165min) - Questionnaire + path choice
|
||||
- **L22**: Path Level 1 (180min) - Introduction to chosen path
|
||||
- **L23**: Path Level 2 (195min) - Intermediate path training
|
||||
- **L24**: Path Level 3 (210min) - Advanced path challenges
|
||||
- **L25**: Path Mastery Checkpoint (225min) - Path graduation + preference update
|
||||
|
||||
**End Result**: Personalized experience based on user's path choice, preparing for Ultimate Mastery arc.
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ Implementation Tasks
|
||||
|
||||
### Task 1: Path Selection System (5-6 hours)
|
||||
|
||||
**File to Create**: `src/features/academy/pathSelector.js`
|
||||
|
||||
**Responsibilities**:
|
||||
- Display path selection questionnaire
|
||||
- Calculate recommended path based on answers
|
||||
- Allow manual path override
|
||||
- Save selected path
|
||||
- Generate path-specific content
|
||||
|
||||
**Questionnaire Questions**:
|
||||
```javascript
|
||||
const pathQuestions = [
|
||||
{
|
||||
id: 1,
|
||||
question: "What aspect of gooning excites you most?",
|
||||
answers: [
|
||||
{ text: "Going for hours without stopping", path: "Endurance", weight: 3 },
|
||||
{ text: "Being denied release", path: "Denial", weight: 3 },
|
||||
{ text: "Being degraded and humiliated", path: "Humiliation", weight: 3 },
|
||||
{ text: "Following commands obediently", path: "Obedience", weight: 3 },
|
||||
{ text: "Increasing my sensitivity", path: "Sensitivity", weight: 3 },
|
||||
{ text: "Overwhelming my senses", path: "Multi-Sensory", weight: 3 }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
question: "How do you prefer to edge?",
|
||||
answers: [
|
||||
{ text: "Slow and steady for maximum duration", path: "Endurance", weight: 2 },
|
||||
{ text: "Right to the edge without going over", path: "Denial", weight: 2 },
|
||||
{ text: "While being verbally degraded", path: "Humiliation", weight: 2 },
|
||||
{ text: "Only when commanded", path: "Obedience", weight: 2 },
|
||||
{ text: "With minimal touch", path: "Sensitivity", weight: 2 },
|
||||
{ text: "With all features active", path: "Multi-Sensory", weight: 2 }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
question: "What is your ultimate gooning goal?",
|
||||
answers: [
|
||||
{ text: "Goon for 5+ hours straight", path: "Endurance", weight: 3 },
|
||||
{ text: "Edge for days without cumming", path: "Denial", weight: 3 },
|
||||
{ text: "Embrace total humiliation", path: "Humiliation", weight: 3 },
|
||||
{ text: "Become perfectly obedient", path: "Obedience", weight: 3 },
|
||||
{ text: "Cum from minimal stimulation", path: "Sensitivity", weight: 3 },
|
||||
{ text: "Master sensory overload", path: "Multi-Sensory", weight: 3 }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
question: "Which feature do you enjoy most?",
|
||||
answers: [
|
||||
{ text: "Long video marathons", path: "Endurance", weight: 1 },
|
||||
{ text: "Denial and stop-stroking commands", path: "Denial", weight: 1 },
|
||||
{ text: "Humiliating captions and tasks", path: "Humiliation", weight: 1 },
|
||||
{ text: "TTS commands and instructions", path: "Obedience", weight: 1 },
|
||||
{ text: "Hypno spirals and edge control", path: "Sensitivity", weight: 1 },
|
||||
{ text: "Everything at once", path: "Multi-Sensory", weight: 1 }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
question: "How intense should your training be?",
|
||||
answers: [
|
||||
{ text: "Moderate but long-lasting", path: "Endurance", weight: 2 },
|
||||
{ text: "Intensely frustrating", path: "Denial", weight: 2 },
|
||||
{ text: "Psychologically challenging", path: "Humiliation", weight: 2 },
|
||||
{ text: "Strictly controlled", path: "Obedience", weight: 2 },
|
||||
{ text: "Physically overwhelming", path: "Sensitivity", weight: 2 },
|
||||
{ text: "Maximum intensity across all dimensions", path: "Multi-Sensory", weight: 2 }
|
||||
]
|
||||
}
|
||||
];
|
||||
```
|
||||
|
||||
**Path Calculation**:
|
||||
```javascript
|
||||
class PathSelector {
|
||||
calculateRecommendedPath(answers) {
|
||||
// Sum weights for each path
|
||||
// Return path with highest score
|
||||
}
|
||||
|
||||
showPathSelection(recommendedPath) {
|
||||
// Display questionnaire results
|
||||
// Show recommended path with description
|
||||
// Allow user to override choice
|
||||
// Return selected path
|
||||
}
|
||||
|
||||
savePath(path) {
|
||||
// Save to academyProgress.selectedPath
|
||||
}
|
||||
|
||||
getPathDescription(path) {
|
||||
// Return detailed description of path
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Path Descriptions**:
|
||||
```javascript
|
||||
const pathDescriptions = {
|
||||
Endurance: {
|
||||
name: "Endurance Path",
|
||||
tagline: "Master of marathons",
|
||||
description: "Build stamina for ultra-long sessions. Train to goon for hours without breaks.",
|
||||
focus: "Duration, stamina, marathon sessions",
|
||||
graduation: "5-hour session with minimal breaks"
|
||||
},
|
||||
Denial: {
|
||||
name: "Denial Path",
|
||||
tagline: "Master of frustration",
|
||||
description: "Learn to edge endlessly without release. Embrace permanent denial.",
|
||||
focus: "Denial periods, edging without cumming, frustration tolerance",
|
||||
graduation: "4-hour denial session with 100+ edges"
|
||||
},
|
||||
Humiliation: {
|
||||
name: "Humiliation Path",
|
||||
tagline: "Master of degradation",
|
||||
description: "Embrace humiliation and ego dissolution. Become the perfect degraded gooner.",
|
||||
focus: "Humiliating tasks, degrading captions, ego death",
|
||||
graduation: "Humiliation gauntlet with extreme tasks"
|
||||
},
|
||||
Obedience: {
|
||||
name: "Obedience Path",
|
||||
tagline: "Master of submission",
|
||||
description: "Perfect command following. Learn total obedience and control surrender.",
|
||||
focus: "TTS commands, instant obedience, submission",
|
||||
graduation: "Perfect obedience test with complex commands"
|
||||
},
|
||||
Sensitivity: {
|
||||
name: "Sensitivity Path",
|
||||
tagline: "Master of sensation",
|
||||
description: "Heighten your sensitivity to extreme levels. Minimal touch, maximum pleasure.",
|
||||
focus: "Sensitivity training, minimal stimulation, edge control",
|
||||
graduation: "Hands-free or minimal-touch edge marathon"
|
||||
},
|
||||
MultiSensory: {
|
||||
name: "Multi-Sensory Path",
|
||||
tagline: "Master of everything",
|
||||
description: "All features, maximum intensity. Become a master of sensory overload.",
|
||||
focus: "All features combined, maximum complexity, total immersion",
|
||||
graduation: "Ultimate sensory overload challenge"
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**Testing Checklist**:
|
||||
- [ ] Questionnaire displays
|
||||
- [ ] Answers calculate recommended path
|
||||
- [ ] Path descriptions display
|
||||
- [ ] Can override recommendation
|
||||
- [ ] Selected path saves
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Path-Specific Action Handlers (6-8 hours)
|
||||
|
||||
**File to Update**: `src/features/tasks/interactiveTaskManager.js`
|
||||
|
||||
Add handlers for path-specific actions:
|
||||
|
||||
#### Endurance Path Actions:
|
||||
```javascript
|
||||
{
|
||||
type: 'marathon-warmup',
|
||||
params: {
|
||||
duration: 600, // 10-minute warmup
|
||||
instruction: 'Prepare your body and mind for the long haul'
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
type: 'stamina-building',
|
||||
params: {
|
||||
edgeCycles: 5,
|
||||
cycleLength: 300, // 5-minute cycles
|
||||
intensity: 'moderate'
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
type: 'endurance-test',
|
||||
params: {
|
||||
duration: 3600, // 1-hour test
|
||||
minEdges: 30,
|
||||
breaks: 'none'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Denial Path Actions:
|
||||
```javascript
|
||||
{
|
||||
type: 'extended-denial',
|
||||
params: {
|
||||
denialDuration: 1800, // 30-minute denial
|
||||
allowedTouching: 'none',
|
||||
showTimer: true
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
type: 'frustration-building',
|
||||
params: {
|
||||
edgesToBrink: 20,
|
||||
forcedStops: 5
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
type: 'denial-oath',
|
||||
params: {
|
||||
oath: 'I will not cum. I am a denied gooner.',
|
||||
repetitions: 10
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Humiliation Path Actions:
|
||||
```javascript
|
||||
{
|
||||
type: 'humiliation-task',
|
||||
params: {
|
||||
task: 'Write "I am a pathetic gooner" 10 times',
|
||||
verification: 'photo' // optional
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
type: 'degrading-mantra',
|
||||
params: {
|
||||
mantra: 'I am addicted. I am weak. I am a gooner.',
|
||||
duration: 300,
|
||||
tts: true
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
type: 'ego-dissolution',
|
||||
params: {
|
||||
captions: 'degrading',
|
||||
hypno: true,
|
||||
duration: 600
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Obedience Path Actions:
|
||||
```javascript
|
||||
{
|
||||
type: 'command-drill',
|
||||
params: {
|
||||
commands: [
|
||||
{ command: 'Edge now', delay: 10 },
|
||||
{ command: 'Stop', delay: 5 },
|
||||
{ command: 'Edge again', delay: 10 }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
type: 'instant-obedience',
|
||||
params: {
|
||||
randomCommands: true,
|
||||
commandFrequency: 30, // every 30 seconds
|
||||
duration: 600
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
type: 'obedience-oath',
|
||||
params: {
|
||||
oath: 'I obey. I submit. I am controlled.',
|
||||
repetitions: 10
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Sensitivity Path Actions:
|
||||
```javascript
|
||||
{
|
||||
type: 'sensitivity-training',
|
||||
params: {
|
||||
touchIntensity: 'minimal',
|
||||
edges: 20,
|
||||
instruction: 'Use only your fingertips'
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
type: 'slow-edge',
|
||||
params: {
|
||||
duration: 600, // 10-minute slow edge
|
||||
maxStrokes: 100
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
type: 'hands-free-attempt',
|
||||
params: {
|
||||
duration: 300,
|
||||
allowedStimulation: 'visual-only'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Multi-Sensory Path Actions:
|
||||
```javascript
|
||||
{
|
||||
type: 'total-overload',
|
||||
params: {
|
||||
features: ['quad-video', 'hypno', 'tts', 'captions', 'interruptions', 'popups', 'webcam'],
|
||||
duration: 900,
|
||||
intensity: 'maximum'
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
type: 'feature-cascade',
|
||||
params: {
|
||||
startWith: ['video'],
|
||||
addEvery: 60, // add feature every 60 seconds
|
||||
features: ['tts', 'hypno', 'captions', 'quad-video', 'popups', 'interruptions']
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
type: 'sensory-mastery',
|
||||
params: {
|
||||
allFeatures: true,
|
||||
complexTasks: true,
|
||||
duration: 1200
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Handler Implementation**:
|
||||
```javascript
|
||||
class InteractiveTaskManager {
|
||||
// ... existing handlers ...
|
||||
|
||||
// Endurance handlers
|
||||
async handleMarathonWarmup(action) { /* Warmup routine */ }
|
||||
async handleStaminaBuilding(action) { /* Edge cycles */ }
|
||||
async handleEnduranceTest(action) { /* Long duration test */ }
|
||||
|
||||
// Denial handlers
|
||||
async handleExtendedDenial(action) { /* Long denial period */ }
|
||||
async handleFrustrationBuilding(action) { /* Edge to brink repeatedly */ }
|
||||
async handleDenialOath(action) { /* Repeat oath */ }
|
||||
|
||||
// Humiliation handlers
|
||||
async handleHumiliationTask(action) { /* Display and verify task */ }
|
||||
async handleDegradingMantra(action) { /* TTS mantra loop */ }
|
||||
async handleEgoDissolution(action) { /* Hypno + degrading captions */ }
|
||||
|
||||
// Obedience handlers
|
||||
async handleCommandDrill(action) { /* Sequence of commands */ }
|
||||
async handleInstantObedience(action) { /* Random rapid commands */ }
|
||||
async handleObedienceOath(action) { /* Repeat oath */ }
|
||||
|
||||
// Sensitivity handlers
|
||||
async handleSensitivityTraining(action) { /* Minimal touch edges */ }
|
||||
async handleSlowEdge(action) { /* Slow, controlled edge */ }
|
||||
async handleHandsFreeAttempt(action) { /* No touch challenge */ }
|
||||
|
||||
// Multi-Sensory handlers
|
||||
async handleTotalOverload(action) { /* Activate all features */ }
|
||||
async handleFeatureCascade(action) { /* Add features progressively */ }
|
||||
async handleSensoryMastery(action) { /* Ultimate challenge */ }
|
||||
}
|
||||
```
|
||||
|
||||
**Testing Checklist**:
|
||||
- [ ] All 6 paths have unique actions
|
||||
- [ ] Path-specific handlers work correctly
|
||||
- [ ] Actions respect path focus
|
||||
- [ ] No conflicts between path actions
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Path-Specific Level Data (4-5 hours)
|
||||
|
||||
**File to Update**: `src/data/modes/academyLevelData.js`
|
||||
|
||||
Add levels 21-25 with path variations:
|
||||
|
||||
```javascript
|
||||
const academyLevels = {
|
||||
// ... L1-20 ...
|
||||
|
||||
21: {
|
||||
name: "Path Selection",
|
||||
arc: "Path Specialization",
|
||||
duration: 9900, // 165 minutes
|
||||
requirements: {
|
||||
minLevel: 21,
|
||||
completedLevels: [20]
|
||||
},
|
||||
unlocks: {
|
||||
level: 22
|
||||
},
|
||||
sessionStructure: {
|
||||
warmup: [
|
||||
{ type: 'show-path-questionnaire' }
|
||||
],
|
||||
main: [
|
||||
{ type: 'quad-video' },
|
||||
{ type: 'edge', params: { count: 165 } }
|
||||
],
|
||||
cooldown: [
|
||||
{ type: 'show-path-results' },
|
||||
{ type: 'save-path-selection' }
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
22: {
|
||||
name: "Path Level 1",
|
||||
arc: "Path Specialization",
|
||||
duration: 10800, // 180 minutes
|
||||
requirements: {
|
||||
minLevel: 22,
|
||||
completedLevels: [21],
|
||||
pathSelected: true
|
||||
},
|
||||
unlocks: {
|
||||
level: 23
|
||||
},
|
||||
sessionStructure: {
|
||||
// Different structure per path
|
||||
Endurance: {
|
||||
main: [
|
||||
{ type: 'marathon-warmup', params: { duration: 600 } },
|
||||
{ type: 'stamina-building', params: { edgeCycles: 5, cycleLength: 300 } },
|
||||
{ type: 'edge', params: { count: 100 } }
|
||||
]
|
||||
},
|
||||
Denial: {
|
||||
main: [
|
||||
{ type: 'extended-denial', params: { denialDuration: 1800 } },
|
||||
{ type: 'frustration-building', params: { edgesToBrink: 30 } },
|
||||
{ type: 'denial-oath', params: { repetitions: 10 } }
|
||||
]
|
||||
},
|
||||
Humiliation: {
|
||||
main: [
|
||||
{ type: 'humiliation-task', params: { task: 'Write degrading affirmations' } },
|
||||
{ type: 'degrading-mantra', params: { duration: 600, tts: true } },
|
||||
{ type: 'edge', params: { count: 80 } }
|
||||
]
|
||||
},
|
||||
Obedience: {
|
||||
main: [
|
||||
{ type: 'command-drill', params: { commands: [...] } },
|
||||
{ type: 'instant-obedience', params: { duration: 900 } },
|
||||
{ type: 'obedience-oath', params: { repetitions: 10 } }
|
||||
]
|
||||
},
|
||||
Sensitivity: {
|
||||
main: [
|
||||
{ type: 'sensitivity-training', params: { edges: 50 } },
|
||||
{ type: 'slow-edge', params: { duration: 1200 } },
|
||||
{ type: 'hands-free-attempt', params: { duration: 300 } }
|
||||
]
|
||||
},
|
||||
MultiSensory: {
|
||||
main: [
|
||||
{ type: 'feature-cascade', params: { startWith: ['video'], addEvery: 120 } },
|
||||
{ type: 'total-overload', params: { duration: 1800 } }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
23: {
|
||||
name: "Path Level 2",
|
||||
arc: "Path Specialization",
|
||||
duration: 11700, // 195 minutes
|
||||
requirements: {
|
||||
minLevel: 23,
|
||||
completedLevels: [22]
|
||||
},
|
||||
unlocks: {
|
||||
level: 24
|
||||
},
|
||||
sessionStructure: {
|
||||
// Intermediate path training (more intense)
|
||||
// Similar structure to L22 but harder
|
||||
}
|
||||
},
|
||||
|
||||
24: {
|
||||
name: "Path Level 3",
|
||||
arc: "Path Specialization",
|
||||
duration: 12600, // 210 minutes
|
||||
requirements: {
|
||||
minLevel: 24,
|
||||
completedLevels: [23]
|
||||
},
|
||||
unlocks: {
|
||||
level: 25
|
||||
},
|
||||
sessionStructure: {
|
||||
// Advanced path challenges
|
||||
// Similar structure to L23 but hardest
|
||||
}
|
||||
},
|
||||
|
||||
25: {
|
||||
name: "Path Mastery",
|
||||
arc: "Path Specialization",
|
||||
duration: 13500, // 225 minutes
|
||||
isCheckpoint: true,
|
||||
requirements: {
|
||||
minLevel: 25,
|
||||
completedLevels: [24]
|
||||
},
|
||||
unlocks: {
|
||||
level: 26,
|
||||
arcsCompleted: ['Path Specialization'],
|
||||
durationTiers: ['Marathon'] // Unlock Marathon tier (15x)
|
||||
},
|
||||
sessionStructure: {
|
||||
warmup: [
|
||||
{ type: 'show-preferences', reason: 'checkpoint' }
|
||||
],
|
||||
main: {
|
||||
// Path graduation challenge
|
||||
// Hardest test for each path
|
||||
Endurance: [
|
||||
{ type: 'endurance-test', params: { duration: 7200, minEdges: 100 } }
|
||||
],
|
||||
Denial: [
|
||||
{ type: 'extended-denial', params: { denialDuration: 3600 } },
|
||||
{ type: 'frustration-building', params: { edgesToBrink: 100 } }
|
||||
],
|
||||
Humiliation: [
|
||||
{ type: 'ego-dissolution', params: { duration: 3600 } },
|
||||
{ type: 'humiliation-task', params: { task: 'Ultimate humiliation gauntlet' } }
|
||||
],
|
||||
Obedience: [
|
||||
{ type: 'instant-obedience', params: { duration: 3600, commandFrequency: 15 } }
|
||||
],
|
||||
Sensitivity: [
|
||||
{ type: 'hands-free-attempt', params: { duration: 1800 } },
|
||||
{ type: 'slow-edge', params: { duration: 1800, maxStrokes: 50 } }
|
||||
],
|
||||
MultiSensory: [
|
||||
{ type: 'sensory-mastery', params: { duration: 3600, allFeatures: true } }
|
||||
]
|
||||
},
|
||||
cooldown: [
|
||||
{ type: 'show-arc-complete', arc: 'Path Specialization' },
|
||||
{ type: 'show-path-graduation', path: 'selected' }
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**Testing Checklist**:
|
||||
- [ ] L21 questionnaire works
|
||||
- [ ] Path selection saves
|
||||
- [ ] L22-24 load correct path structure
|
||||
- [ ] L25 graduation challenge matches path
|
||||
- [ ] Marathon tier unlocks at L25
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Path Data Management (2-3 hours)
|
||||
|
||||
**File to Create**: `src/data/academy/pathData.js`
|
||||
|
||||
**Path Definitions**:
|
||||
```javascript
|
||||
export const paths = {
|
||||
Endurance: {
|
||||
// ... full path definition ...
|
||||
},
|
||||
// ... other 5 paths ...
|
||||
};
|
||||
|
||||
export const pathQuestionnaire = [ /* questions */ ];
|
||||
```
|
||||
|
||||
**File to Update**: `src/core/gameDataManager.js`
|
||||
|
||||
Add `selectedPath` to `academyProgress`:
|
||||
```javascript
|
||||
academyProgress: {
|
||||
// ... existing fields ...
|
||||
selectedPath: null, // 'Endurance', 'Denial', etc.
|
||||
pathQuestionnaireAnswers: [],
|
||||
pathProgress: {
|
||||
level22Complete: false,
|
||||
level23Complete: false,
|
||||
level24Complete: false,
|
||||
level25Complete: false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Testing Checklist**:
|
||||
- [ ] Path data loads correctly
|
||||
- [ ] Selected path persists
|
||||
- [ ] Path progress tracks
|
||||
|
||||
---
|
||||
|
||||
### Task 5: UI for Path Selection (3-4 hours)
|
||||
|
||||
**File to Update**: `training-academy.html`
|
||||
|
||||
Add path selection UI:
|
||||
|
||||
```html
|
||||
<!-- Path Questionnaire Modal -->
|
||||
<div id="path-questionnaire-modal" class="modal hidden">
|
||||
<h2>Choose Your Path</h2>
|
||||
<p>Answer these questions to find your ideal training path.</p>
|
||||
|
||||
<div id="questionnaire-container">
|
||||
<!-- Questions rendered here -->
|
||||
</div>
|
||||
|
||||
<button id="calculate-path">Calculate Recommended Path</button>
|
||||
</div>
|
||||
|
||||
<!-- Path Results Modal -->
|
||||
<div id="path-results-modal" class="modal hidden">
|
||||
<h2>Your Recommended Path</h2>
|
||||
|
||||
<div id="recommended-path">
|
||||
<h3 id="path-name"></h3>
|
||||
<p id="path-tagline"></p>
|
||||
<p id="path-description"></p>
|
||||
<p><strong>Focus:</strong> <span id="path-focus"></span></p>
|
||||
<p><strong>Graduation:</strong> <span id="path-graduation"></span></p>
|
||||
</div>
|
||||
|
||||
<div id="all-paths">
|
||||
<h3>Or choose a different path:</h3>
|
||||
<!-- 6 path cards -->
|
||||
</div>
|
||||
|
||||
<button id="confirm-path">Confirm Path</button>
|
||||
</div>
|
||||
|
||||
<!-- Path Graduation Modal -->
|
||||
<div id="path-graduation-modal" class="modal hidden">
|
||||
<h2>🎓 Path Mastery Complete!</h2>
|
||||
<p>You have mastered the <span id="graduated-path"></span>.</p>
|
||||
<p>Marathon tier (15x) now unlocked!</p>
|
||||
<button id="continue-graduation">Continue to Ultimate Mastery</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Testing Checklist**:
|
||||
- [ ] Questionnaire renders
|
||||
- [ ] Results display correctly
|
||||
- [ ] All 6 paths selectable
|
||||
- [ ] Graduation modal displays
|
||||
|
||||
---
|
||||
|
||||
## 📏 Measurable Test Criteria
|
||||
|
||||
After Phase 4, ALL of these must pass:
|
||||
|
||||
### Level 21 (Path Selection):
|
||||
- [ ] Questionnaire displays with all 5 questions
|
||||
- [ ] Can answer all questions
|
||||
- [ ] Recommended path calculates correctly
|
||||
- [ ] Path descriptions display
|
||||
- [ ] Can override recommendation
|
||||
- [ ] Selected path saves
|
||||
- [ ] Completing L21 unlocks L22
|
||||
|
||||
### Level 22 (Path Level 1):
|
||||
- [ ] Correct path structure loads
|
||||
- [ ] Endurance path: Marathon warmup + stamina building work
|
||||
- [ ] Denial path: Extended denial + frustration work
|
||||
- [ ] Humiliation path: Tasks + degrading mantras work
|
||||
- [ ] Obedience path: Commands + instant obedience work
|
||||
- [ ] Sensitivity path: Sensitivity training + slow edges work
|
||||
- [ ] Multi-Sensory path: Feature cascade + total overload work
|
||||
- [ ] Completing L22 unlocks L23
|
||||
|
||||
### Level 23 (Path Level 2):
|
||||
- [ ] Intermediate difficulty loads
|
||||
- [ ] Path-specific content works
|
||||
- [ ] Completing L23 unlocks L24
|
||||
|
||||
### Level 24 (Path Level 3):
|
||||
- [ ] Advanced difficulty loads
|
||||
- [ ] Path-specific content works
|
||||
- [ ] Completing L24 unlocks L25
|
||||
|
||||
### Level 25 (Path Mastery):
|
||||
- [ ] Preference form appears
|
||||
- [ ] Path graduation challenge loads
|
||||
- [ ] Endurance: 2-hour test works
|
||||
- [ ] Denial: 1-hour denial + 100 edges work
|
||||
- [ ] Humiliation: Ego dissolution + gauntlet work
|
||||
- [ ] Obedience: 1-hour instant obedience works
|
||||
- [ ] Sensitivity: Hands-free + slow edge work
|
||||
- [ ] Multi-Sensory: Ultimate overload works
|
||||
- [ ] Path graduation modal displays
|
||||
- [ ] Marathon tier unlocks
|
||||
- [ ] Path Specialization Arc completes
|
||||
- [ ] Completing L25 unlocks L26
|
||||
|
||||
### Overall:
|
||||
- [ ] All 6 paths functional
|
||||
- [ ] Path selection persists
|
||||
- [ ] No console errors
|
||||
- [ ] Can complete L21 → L25 for any path
|
||||
- [ ] Marathon tier confirmed unlocked
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Success Criteria
|
||||
|
||||
**Phase 4 Complete When:**
|
||||
1. Levels 21-25 fully functional
|
||||
2. All 6 paths implemented
|
||||
3. Path selection system works
|
||||
4. All path-specific actions functional
|
||||
5. All measurable tests pass
|
||||
6. Path graduation displays correctly
|
||||
7. Marathon tier unlocks at L25
|
||||
|
||||
---
|
||||
|
||||
## 📂 Files Created/Modified
|
||||
|
||||
### New Files:
|
||||
- `src/features/academy/pathSelector.js`
|
||||
- `src/data/academy/pathData.js`
|
||||
|
||||
### Modified Files:
|
||||
- `src/features/tasks/interactiveTaskManager.js`
|
||||
- `src/data/modes/academyLevelData.js`
|
||||
- `src/core/gameDataManager.js`
|
||||
- `training-academy.html`
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Next Phase
|
||||
|
||||
After Phase 4: **Phase 5: Levels 26-30 + Graduation** (Ultimate Mastery arc)
|
||||
|
|
@ -0,0 +1,693 @@
|
|||
# Phase 5: Levels 26-30 + Graduation
|
||||
|
||||
**Priority**: MEDIUM - Campaign finale and graduation
|
||||
**Estimated Effort**: 16-22 hours
|
||||
**Status**: Not Started
|
||||
**Dependencies**: Phase 4 (Levels 21-25)
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Phase Goals
|
||||
|
||||
Implement levels 26-30, covering the **Ultimate Mastery Arc**. These are the final five levels of The Academy, culminating in the epic Level 30 graduation ceremony. Implement progressive intensity scaling, dynamic difficulty, and the graduation system.
|
||||
|
||||
---
|
||||
|
||||
## 📋 What This Phase Delivers
|
||||
|
||||
### Arc Implemented:
|
||||
**Ultimate Mastery Arc (L26-30)**: Final challenges leading to graduation
|
||||
|
||||
### Level-by-Level:
|
||||
- **L26**: Ultimate Test I (240min / 4 hours) - Intensity Stage 1
|
||||
- **L27**: Ultimate Test II (255min / 4.25 hours) - Intensity Stage 2
|
||||
- **L28**: Ultimate Test III (270min / 4.5 hours) - Intensity Stage 3
|
||||
- **L29**: Ultimate Test IV (285min / 4.75 hours) - Intensity Stage 4
|
||||
- **L30**: Graduation (300min / 5 hours) - Final exam + ceremony
|
||||
|
||||
### New Systems:
|
||||
1. **Progressive Intensity System**: 4 stages (Moderate → Challenging → Intense → Extreme)
|
||||
2. **Dynamic Difficulty Adjustment**: Adapts to user's path and preferences
|
||||
3. **Graduation Ceremony**: Oath, certificate, stats report, photo gallery, library report
|
||||
4. **Freeplay Mode**: Unlocked after graduation
|
||||
5. **Ascended Mode**: Ultra-hard post-graduation mode
|
||||
|
||||
**End Result**: Complete campaign with satisfying conclusion and post-game content.
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ Implementation Tasks
|
||||
|
||||
### Task 1: Progressive Intensity System (5-6 hours)
|
||||
|
||||
**File to Create**: `src/features/academy/intensityManager.js`
|
||||
|
||||
**Responsibilities**:
|
||||
- Calculate intensity based on level (26-30)
|
||||
- Adjust feature frequency/complexity
|
||||
- Generate intensity-scaled tasks
|
||||
- Track intensity progression
|
||||
|
||||
**Intensity Stages**:
|
||||
```javascript
|
||||
const intensityStages = {
|
||||
1: {
|
||||
level: 26,
|
||||
name: "Moderate",
|
||||
description: "Warm up for the ultimate tests",
|
||||
multipliers: {
|
||||
edgeCount: 1.0,
|
||||
interruptionFrequency: 1.0,
|
||||
popupFrequency: 1.0,
|
||||
denialDuration: 1.0,
|
||||
featureCount: 4 // max 4 features at once
|
||||
}
|
||||
},
|
||||
2: {
|
||||
level: 27,
|
||||
name: "Challenging",
|
||||
description: "Ramping up the pressure",
|
||||
multipliers: {
|
||||
edgeCount: 1.2,
|
||||
interruptionFrequency: 1.3,
|
||||
popupFrequency: 1.3,
|
||||
denialDuration: 1.2,
|
||||
featureCount: 5
|
||||
}
|
||||
},
|
||||
3: {
|
||||
level: 28,
|
||||
name: "Intense",
|
||||
description: "Testing your limits",
|
||||
multipliers: {
|
||||
edgeCount: 1.4,
|
||||
interruptionFrequency: 1.6,
|
||||
popupFrequency: 1.6,
|
||||
denialDuration: 1.4,
|
||||
featureCount: 6
|
||||
}
|
||||
},
|
||||
4: {
|
||||
level: 29,
|
||||
name: "Extreme",
|
||||
description: "Maximum challenge before graduation",
|
||||
multipliers: {
|
||||
edgeCount: 1.6,
|
||||
interruptionFrequency: 2.0,
|
||||
popupFrequency: 2.0,
|
||||
denialDuration: 1.6,
|
||||
featureCount: 7 // all features
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**Methods**:
|
||||
```javascript
|
||||
class IntensityManager {
|
||||
getIntensityStage(level) {
|
||||
// Return intensity config for level 26-30
|
||||
}
|
||||
|
||||
scaleAction(action, intensityStage) {
|
||||
// Apply multipliers to action params
|
||||
// e.g., edge count, interruption frequency
|
||||
}
|
||||
|
||||
generateIntensifiedSession(level, baseTasks) {
|
||||
// Take base task list
|
||||
// Apply intensity scaling
|
||||
// Return intensified task list
|
||||
}
|
||||
|
||||
calculateDifficulty(level, path, preferences) {
|
||||
// Combine level, path focus, and preferences
|
||||
// Return difficulty score
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Testing Checklist**:
|
||||
- [ ] Intensity stages defined
|
||||
- [ ] Multipliers apply correctly
|
||||
- [ ] Actions scale with intensity
|
||||
- [ ] Difficulty calculation works
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Dynamic Difficulty Adjustment (3-4 hours)
|
||||
|
||||
**File to Update**: `src/features/academy/intensityManager.js`
|
||||
|
||||
**Responsibilities**:
|
||||
- Adjust difficulty based on path
|
||||
- Respect user preferences
|
||||
- Generate path-specific challenges for L26-30
|
||||
|
||||
**Path-Specific Adjustments**:
|
||||
```javascript
|
||||
const pathIntensityAdjustments = {
|
||||
Endurance: {
|
||||
focus: 'duration',
|
||||
adjustments: {
|
||||
// Longer sessions, more edges
|
||||
edgeCount: +20,
|
||||
breakDuration: -30, // shorter breaks
|
||||
sessionPacing: 'steady'
|
||||
}
|
||||
},
|
||||
Denial: {
|
||||
focus: 'frustration',
|
||||
adjustments: {
|
||||
denialDuration: +300, // +5 minutes
|
||||
edgesToBrink: +10,
|
||||
releaseProhibited: true
|
||||
}
|
||||
},
|
||||
Humiliation: {
|
||||
focus: 'psychological',
|
||||
adjustments: {
|
||||
degradingCaptions: true,
|
||||
humiliationTasks: +5,
|
||||
egoReduction: 'maximum'
|
||||
}
|
||||
},
|
||||
Obedience: {
|
||||
focus: 'control',
|
||||
adjustments: {
|
||||
commandFrequency: +10, // more commands
|
||||
instantCompliance: true,
|
||||
complexCommands: true
|
||||
}
|
||||
},
|
||||
Sensitivity: {
|
||||
focus: 'sensation',
|
||||
adjustments: {
|
||||
minimalTouch: true,
|
||||
edgeControl: 'precise',
|
||||
slowEdging: true
|
||||
}
|
||||
},
|
||||
MultiSensory: {
|
||||
focus: 'overload',
|
||||
adjustments: {
|
||||
allFeatures: true,
|
||||
simultaneousFeatures: 7,
|
||||
complexityMax: true
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**Methods**:
|
||||
```javascript
|
||||
class IntensityManager {
|
||||
// ... existing methods ...
|
||||
|
||||
applyPathAdjustments(baseTasks, path) {
|
||||
// Modify tasks based on path focus
|
||||
}
|
||||
|
||||
generatePathSpecificChallenge(level, path) {
|
||||
// Create unique challenge for path at this level
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Testing Checklist**:
|
||||
- [ ] Path adjustments apply
|
||||
- [ ] Each path has unique L26-30 experience
|
||||
- [ ] Adjustments respect preferences
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Graduation System (5-7 hours)
|
||||
|
||||
**File to Create**: `src/features/academy/graduationManager.js`
|
||||
|
||||
**Responsibilities**:
|
||||
- Execute Level 30 (5-hour final exam)
|
||||
- Generate graduation ceremony
|
||||
- Create certificate
|
||||
- Compile stats report
|
||||
- Curate photo gallery
|
||||
- Compile library report
|
||||
- Unlock freeplay and Ascended mode
|
||||
- Display graduation modal
|
||||
|
||||
**Graduation Ceremony Components**:
|
||||
|
||||
**1. Final Oath**:
|
||||
```javascript
|
||||
const graduationOath = {
|
||||
base: "I am a graduate of The Academy.",
|
||||
custom: (stats) => `
|
||||
I have completed ${stats.totalLevels} levels.
|
||||
I have edged ${stats.totalEdges} times.
|
||||
I have gooned for ${stats.totalHours} hours.
|
||||
I have mastered ${stats.selectedPath}.
|
||||
I am a dedicated gooner.
|
||||
I will continue to goon.
|
||||
I embrace my addiction.
|
||||
`
|
||||
};
|
||||
```
|
||||
|
||||
**2. Certificate**:
|
||||
```javascript
|
||||
const certificate = {
|
||||
title: "Certificate of Completion",
|
||||
subtitle: "The Academy",
|
||||
body: `
|
||||
This certifies that [USERNAME] has successfully completed
|
||||
The Academy's comprehensive gooner training program.
|
||||
|
||||
Path: [PATH]
|
||||
Total Time: [HOURS] hours
|
||||
Total Edges: [EDGES]
|
||||
Library Size: [VIDEOS] videos, [IMAGES] images
|
||||
Curator Rank: [RANK]
|
||||
|
||||
Graduated: [DATE]
|
||||
`
|
||||
};
|
||||
```
|
||||
|
||||
**3. Stats Report**:
|
||||
```javascript
|
||||
const statsReport = {
|
||||
general: {
|
||||
totalLevels: 30,
|
||||
totalSessionTime: 0, // seconds
|
||||
totalEdges: 0,
|
||||
averageSessionLength: 0,
|
||||
longestSession: 0,
|
||||
failedAttempts: 0
|
||||
},
|
||||
features: {
|
||||
webcamTimeUsed: 0,
|
||||
ttsCommandsHeard: 0,
|
||||
hypnoTimeWatched: 0,
|
||||
captionsDisplayed: 0,
|
||||
interruptionsCompleted: 0,
|
||||
popupsViewed: 0
|
||||
},
|
||||
library: {
|
||||
totalVideos: 0,
|
||||
totalImages: 0,
|
||||
taggedFiles: 0,
|
||||
tagCoverage: 0, // %
|
||||
curatorRank: 'Master',
|
||||
topTags: []
|
||||
},
|
||||
path: {
|
||||
selectedPath: '',
|
||||
pathLevelsCompleted: 4, // L22-25
|
||||
pathSpecificStats: {} // depends on path
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**4. Photo Gallery**:
|
||||
```javascript
|
||||
// Collect photos from:
|
||||
// - Dress-up game completions
|
||||
// - Webcam snapshots (if enabled)
|
||||
// - Achievement unlocks
|
||||
// Display as gallery during ceremony
|
||||
```
|
||||
|
||||
**5. Library Report**:
|
||||
```javascript
|
||||
const libraryReport = {
|
||||
summary: "Your curated library contains X videos and Y images.",
|
||||
coverage: "Z% of your library is tagged.",
|
||||
curatorRank: "You achieved Master rank as a curator.",
|
||||
topCategories: ["amateur", "POV", "edging"],
|
||||
recommendations: "Continue curating to reach 100% coverage."
|
||||
};
|
||||
```
|
||||
|
||||
**Methods**:
|
||||
```javascript
|
||||
class GraduationManager {
|
||||
async executeGraduationLevel() {
|
||||
// Run 5-hour Level 30 session
|
||||
// Track all stats during session
|
||||
}
|
||||
|
||||
async startCeremony() {
|
||||
// Show graduation modal
|
||||
// Display oath → certificate → stats → photos → library
|
||||
}
|
||||
|
||||
generateCertificate(stats) {
|
||||
// Fill template with user stats
|
||||
// Return HTML certificate
|
||||
}
|
||||
|
||||
compileStatsReport() {
|
||||
// Aggregate all stats from academyProgress
|
||||
}
|
||||
|
||||
compilePhotoGallery() {
|
||||
// Collect photos from dress-up, webcam, achievements
|
||||
}
|
||||
|
||||
compileLibraryReport() {
|
||||
// Generate summary from libraryData
|
||||
}
|
||||
|
||||
unlockPostGraduationContent() {
|
||||
// Unlock freeplay mode
|
||||
// Unlock Ascended mode
|
||||
// Save to gameData
|
||||
}
|
||||
|
||||
saveCertificate() {
|
||||
// Optionally save certificate as image/PDF
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Graduation Modal UI**:
|
||||
```html
|
||||
<div id="graduation-modal" class="modal">
|
||||
<h1>🎓 CONGRATULATIONS! 🎓</h1>
|
||||
<h2>You have graduated from The Academy!</h2>
|
||||
|
||||
<div id="graduation-oath">
|
||||
<h3>Recite Your Oath:</h3>
|
||||
<p id="oath-text"></p>
|
||||
<button id="recite-oath">I Recite This Oath</button>
|
||||
</div>
|
||||
|
||||
<div id="graduation-certificate" class="hidden">
|
||||
<h3>Your Certificate</h3>
|
||||
<div id="certificate-content"></div>
|
||||
<button id="download-certificate">Download Certificate</button>
|
||||
</div>
|
||||
|
||||
<div id="graduation-stats" class="hidden">
|
||||
<h3>Your Academy Statistics</h3>
|
||||
<div id="stats-report"></div>
|
||||
</div>
|
||||
|
||||
<div id="graduation-photos" class="hidden">
|
||||
<h3>Your Journey</h3>
|
||||
<div id="photo-gallery"></div>
|
||||
</div>
|
||||
|
||||
<div id="graduation-library" class="hidden">
|
||||
<h3>Your Curated Library</h3>
|
||||
<div id="library-report"></div>
|
||||
</div>
|
||||
|
||||
<div id="post-graduation" class="hidden">
|
||||
<h3>What's Next?</h3>
|
||||
<p>You have unlocked:</p>
|
||||
<ul>
|
||||
<li>✅ Freeplay Mode - Create custom sessions</li>
|
||||
<li>✅ Ascended Mode - Ultra-hard challenges</li>
|
||||
<li>✅ All duration tiers (Quick, Standard, Extended, Marathon)</li>
|
||||
</ul>
|
||||
<button id="enter-freeplay">Enter Freeplay</button>
|
||||
<button id="try-ascended">Try Ascended Mode</button>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Testing Checklist**:
|
||||
- [ ] Level 30 runs full 5-hour session
|
||||
- [ ] Graduation ceremony triggers
|
||||
- [ ] Oath displays and is recitable
|
||||
- [ ] Certificate generates with correct stats
|
||||
- [ ] Stats report compiles accurately
|
||||
- [ ] Photo gallery displays
|
||||
- [ ] Library report displays
|
||||
- [ ] Freeplay unlocks
|
||||
- [ ] Ascended mode unlocks
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Level Data for L26-30 (2-3 hours)
|
||||
|
||||
**File to Update**: `src/data/modes/academyLevelData.js`
|
||||
|
||||
Add levels 26-30:
|
||||
|
||||
```javascript
|
||||
const academyLevels = {
|
||||
// ... L1-25 ...
|
||||
|
||||
26: {
|
||||
name: "Ultimate Test I",
|
||||
arc: "Ultimate Mastery",
|
||||
duration: 14400, // 240 minutes (4 hours)
|
||||
intensityStage: 1,
|
||||
requirements: {
|
||||
minLevel: 26,
|
||||
completedLevels: [25]
|
||||
},
|
||||
unlocks: {
|
||||
level: 27
|
||||
},
|
||||
sessionStructure: {
|
||||
main: [
|
||||
{ type: 'apply-intensity', stage: 1 },
|
||||
{ type: 'path-specific-challenge', level: 26 },
|
||||
{ type: 'sensory-overload', duration: 14400 },
|
||||
{ type: 'edge', params: { count: 200 } }
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
27: {
|
||||
name: "Ultimate Test II",
|
||||
arc: "Ultimate Mastery",
|
||||
duration: 15300, // 255 minutes
|
||||
intensityStage: 2,
|
||||
requirements: {
|
||||
minLevel: 27,
|
||||
completedLevels: [26]
|
||||
},
|
||||
unlocks: {
|
||||
level: 28
|
||||
},
|
||||
sessionStructure: {
|
||||
main: [
|
||||
{ type: 'apply-intensity', stage: 2 },
|
||||
{ type: 'path-specific-challenge', level: 27 },
|
||||
{ type: 'sensory-overload', duration: 15300 },
|
||||
{ type: 'edge', params: { count: 240 } }
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
28: {
|
||||
name: "Ultimate Test III",
|
||||
arc: "Ultimate Mastery",
|
||||
duration: 16200, // 270 minutes
|
||||
intensityStage: 3,
|
||||
requirements: {
|
||||
minLevel: 28,
|
||||
completedLevels: [27]
|
||||
},
|
||||
unlocks: {
|
||||
level: 29
|
||||
},
|
||||
sessionStructure: {
|
||||
main: [
|
||||
{ type: 'apply-intensity', stage: 3 },
|
||||
{ type: 'path-specific-challenge', level: 28 },
|
||||
{ type: 'sensory-overload', duration: 16200 },
|
||||
{ type: 'edge', params: { count: 280 } }
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
29: {
|
||||
name: "Ultimate Test IV",
|
||||
arc: "Ultimate Mastery",
|
||||
duration: 17100, // 285 minutes
|
||||
intensityStage: 4,
|
||||
requirements: {
|
||||
minLevel: 29,
|
||||
completedLevels: [28]
|
||||
},
|
||||
unlocks: {
|
||||
level: 30
|
||||
},
|
||||
sessionStructure: {
|
||||
main: [
|
||||
{ type: 'apply-intensity', stage: 4 },
|
||||
{ type: 'path-specific-challenge', level: 29 },
|
||||
{ type: 'sensory-overload', duration: 17100 },
|
||||
{ type: 'edge', params: { count: 320 } }
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
30: {
|
||||
name: "Graduation",
|
||||
arc: "Ultimate Mastery",
|
||||
duration: 18000, // 300 minutes (5 hours)
|
||||
intensityStage: 4,
|
||||
isGraduation: true,
|
||||
requirements: {
|
||||
minLevel: 30,
|
||||
completedLevels: [29]
|
||||
},
|
||||
unlocks: {
|
||||
graduation: true,
|
||||
freeplay: true,
|
||||
ascendedMode: true,
|
||||
arcsCompleted: ['Ultimate Mastery']
|
||||
},
|
||||
sessionStructure: {
|
||||
warmup: [
|
||||
{ type: 'caption', text: 'This is it. Your final test.' }
|
||||
],
|
||||
main: [
|
||||
{ type: 'apply-intensity', stage: 4 },
|
||||
{ type: 'path-specific-challenge', level: 30 },
|
||||
{ type: 'sensory-overload', duration: 18000 },
|
||||
{ type: 'edge', params: { count: 300 } }
|
||||
],
|
||||
cooldown: [
|
||||
{ type: 'start-graduation-ceremony' }
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**Testing Checklist**:
|
||||
- [ ] L26-30 defined
|
||||
- [ ] Intensity stages apply
|
||||
- [ ] L30 triggers graduation
|
||||
- [ ] Graduation unlocks saved
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Freeplay & Ascended Mode Foundations (1-2 hours)
|
||||
|
||||
**Basic Implementation** (full features in Phase 6/7):
|
||||
|
||||
**Freeplay Mode**:
|
||||
- Allow users to create custom sessions
|
||||
- Select any features, any duration
|
||||
- No progression requirements
|
||||
|
||||
**Ascended Mode**:
|
||||
- Ultra-hard version of any level
|
||||
- 2x intensity multipliers
|
||||
- Additional challenges
|
||||
- For post-graduation mastery
|
||||
|
||||
**Data Structure**:
|
||||
```javascript
|
||||
academyProgress: {
|
||||
// ... existing ...
|
||||
freeplayUnlocked: false,
|
||||
ascendedModeUnlocked: false,
|
||||
ascendedLevelsCompleted: []
|
||||
}
|
||||
```
|
||||
|
||||
**Testing Checklist**:
|
||||
- [ ] Freeplay mode accessible after graduation
|
||||
- [ ] Ascended mode accessible after graduation
|
||||
- [ ] Basic functionality works
|
||||
|
||||
---
|
||||
|
||||
## 📏 Measurable Test Criteria
|
||||
|
||||
After Phase 5, ALL of these must pass:
|
||||
|
||||
### Level 26 (Ultimate Test I):
|
||||
- [ ] 4-hour session runs
|
||||
- [ ] Intensity Stage 1 applies (1.0x multipliers)
|
||||
- [ ] Path-specific challenge loads
|
||||
- [ ] Sensory overload coordinated
|
||||
- [ ] Completing L26 unlocks L27
|
||||
|
||||
### Level 27 (Ultimate Test II):
|
||||
- [ ] 4.25-hour session runs
|
||||
- [ ] Intensity Stage 2 applies (1.2-1.3x multipliers)
|
||||
- [ ] Difficulty increase noticeable
|
||||
- [ ] Completing L27 unlocks L28
|
||||
|
||||
### Level 28 (Ultimate Test III):
|
||||
- [ ] 4.5-hour session runs
|
||||
- [ ] Intensity Stage 3 applies (1.4-1.6x multipliers)
|
||||
- [ ] Difficulty significantly harder
|
||||
- [ ] Completing L28 unlocks L29
|
||||
|
||||
### Level 29 (Ultimate Test IV):
|
||||
- [ ] 4.75-hour session runs
|
||||
- [ ] Intensity Stage 4 applies (1.6-2.0x multipliers)
|
||||
- [ ] Maximum difficulty before graduation
|
||||
- [ ] Completing L29 unlocks L30
|
||||
|
||||
### Level 30 (Graduation):
|
||||
- [ ] 5-hour final session runs
|
||||
- [ ] Path-specific final challenge loads
|
||||
- [ ] All features coordinate perfectly
|
||||
- [ ] Session completes successfully
|
||||
- [ ] Graduation ceremony triggers
|
||||
|
||||
### Graduation Ceremony:
|
||||
- [ ] Oath displays correctly
|
||||
- [ ] Certificate generates with accurate stats
|
||||
- [ ] Stats report compiles (30 levels, total time, edges, path)
|
||||
- [ ] Photo gallery displays (if photos exist)
|
||||
- [ ] Library report displays (tag coverage, curator rank)
|
||||
- [ ] Certificate downloadable
|
||||
- [ ] Freeplay unlocks
|
||||
- [ ] Ascended mode unlocks
|
||||
- [ ] Ultimate Mastery arc completes
|
||||
- [ ] `graduationCompleted` flag set to true
|
||||
|
||||
### Overall:
|
||||
- [ ] All levels L26-30 playable
|
||||
- [ ] Intensity progression works
|
||||
- [ ] Path adjustments apply
|
||||
- [ ] No console errors
|
||||
- [ ] Progress saves after each level
|
||||
- [ ] Can complete L26 → L30 sequentially
|
||||
- [ ] Graduation persistent after refresh
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Success Criteria
|
||||
|
||||
**Phase 5 Complete When:**
|
||||
1. Levels 26-30 fully functional
|
||||
2. Ultimate Mastery arc complete
|
||||
3. Progressive intensity system works
|
||||
4. Graduation ceremony executes perfectly
|
||||
5. Certificate, stats, photos, library all display
|
||||
6. Freeplay and Ascended mode unlock
|
||||
7. All measurable tests pass
|
||||
8. Campaign finale satisfying and complete
|
||||
|
||||
---
|
||||
|
||||
## 📂 Files Created/Modified
|
||||
|
||||
### New Files:
|
||||
- `src/features/academy/intensityManager.js`
|
||||
- `src/features/academy/graduationManager.js`
|
||||
|
||||
### Modified Files:
|
||||
- `src/data/modes/academyLevelData.js`
|
||||
- `src/core/gameDataManager.js`
|
||||
- `training-academy.html`
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Next Phase
|
||||
|
||||
After Phase 5: **Phase 6: Stats/Achievements/Library Systems** (Cross-cutting systems)
|
||||
|
|
@ -0,0 +1,755 @@
|
|||
# Phase 6: Stats/Achievements/Library Systems
|
||||
|
||||
**Priority**: MEDIUM - Cross-cutting systems for engagement
|
||||
**Estimated Effort**: 14-18 hours
|
||||
**Status**: Not Started
|
||||
**Dependencies**: Phase 1 (for library foundation), can run parallel with Phase 2-5
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Phase Goals
|
||||
|
||||
Implement comprehensive statistics tracking, achievement system, and advanced library management. These cross-cutting systems enhance engagement throughout the entire campaign and provide depth for post-graduation content.
|
||||
|
||||
---
|
||||
|
||||
## 📋 What This Phase Delivers
|
||||
|
||||
### Core Systems:
|
||||
1. **Statistics Dashboard** - Track all Academy metrics
|
||||
2. **Achievement System** - 25+ achievements with unlock notifications
|
||||
3. **Advanced Library Management** - Quality scoring, diversity scoring, smart playlists
|
||||
4. **Curator Rank Progression** - Novice → Master with benefits
|
||||
5. **Stats Visualization** - Charts, graphs, progress bars
|
||||
6. **Export/Import** - Save/load library data, share achievements
|
||||
|
||||
**End Result**: Rich meta-game layer that rewards progress and curation.
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ Implementation Tasks
|
||||
|
||||
### Task 1: Achievement System (5-6 hours)
|
||||
|
||||
**File to Create**: `src/features/academy/progressionManager.js`
|
||||
|
||||
**Responsibilities**:
|
||||
- Define all achievements
|
||||
- Track achievement progress
|
||||
- Unlock achievements
|
||||
- Display achievement notifications
|
||||
- Achievement showcase
|
||||
|
||||
**Achievement Definitions**:
|
||||
```javascript
|
||||
const achievements = [
|
||||
// Progression Achievements
|
||||
{
|
||||
id: 'first_level',
|
||||
name: 'First Steps',
|
||||
description: 'Complete Level 1',
|
||||
icon: '🎓',
|
||||
category: 'progression',
|
||||
condition: (stats) => stats.completedLevels.includes(1),
|
||||
reward: null
|
||||
},
|
||||
{
|
||||
id: 'foundation_complete',
|
||||
name: 'Foundation Graduate',
|
||||
description: 'Complete the Foundation Arc (L1-5)',
|
||||
icon: '🏛️',
|
||||
category: 'progression',
|
||||
condition: (stats) => stats.completedLevels.length >= 5,
|
||||
reward: null
|
||||
},
|
||||
{
|
||||
id: 'halfway',
|
||||
name: 'Halfway There',
|
||||
description: 'Complete 15 levels',
|
||||
icon: '🔥',
|
||||
category: 'progression',
|
||||
condition: (stats) => stats.completedLevels.length >= 15,
|
||||
reward: null
|
||||
},
|
||||
{
|
||||
id: 'all_30',
|
||||
name: 'Academy Graduate',
|
||||
description: 'Complete all 30 levels',
|
||||
icon: '🎓',
|
||||
category: 'progression',
|
||||
condition: (stats) => stats.graduationCompleted,
|
||||
reward: 'Certificate of Completion'
|
||||
},
|
||||
|
||||
// Edging Achievements
|
||||
{
|
||||
id: 'edge_100',
|
||||
name: 'Century Club',
|
||||
description: 'Edge 100 times total',
|
||||
icon: '💯',
|
||||
category: 'edging',
|
||||
condition: (stats) => stats.totalEdges >= 100,
|
||||
reward: null
|
||||
},
|
||||
{
|
||||
id: 'edge_500',
|
||||
name: 'Edge Master',
|
||||
description: 'Edge 500 times total',
|
||||
icon: '🔥',
|
||||
category: 'edging',
|
||||
condition: (stats) => stats.totalEdges >= 500,
|
||||
reward: null
|
||||
},
|
||||
{
|
||||
id: 'edge_1000',
|
||||
name: 'Edge Legend',
|
||||
description: 'Edge 1000 times total',
|
||||
icon: '⭐',
|
||||
category: 'edging',
|
||||
condition: (stats) => stats.totalEdges >= 1000,
|
||||
reward: null
|
||||
},
|
||||
|
||||
// Time Achievements
|
||||
{
|
||||
id: 'time_10h',
|
||||
name: '10 Hours Strong',
|
||||
description: 'Goon for 10 hours total',
|
||||
icon: '⏰',
|
||||
category: 'time',
|
||||
condition: (stats) => stats.totalSessionTime >= 36000, // 10 hours in seconds
|
||||
reward: null
|
||||
},
|
||||
{
|
||||
id: 'time_50h',
|
||||
name: 'Dedicated Gooner',
|
||||
description: 'Goon for 50 hours total',
|
||||
icon: '⏱️',
|
||||
category: 'time',
|
||||
condition: (stats) => stats.totalSessionTime >= 180000, // 50 hours
|
||||
reward: null
|
||||
},
|
||||
{
|
||||
id: 'time_100h',
|
||||
name: 'Gooner for Life',
|
||||
description: 'Goon for 100 hours total',
|
||||
icon: '🏆',
|
||||
category: 'time',
|
||||
condition: (stats) => stats.totalSessionTime >= 360000, // 100 hours
|
||||
reward: 'Honorary Title: Eternal Gooner'
|
||||
},
|
||||
|
||||
// Feature Achievements
|
||||
{
|
||||
id: 'webcam_unlock',
|
||||
name: 'The Observer',
|
||||
description: 'Unlock webcam feature',
|
||||
icon: '📹',
|
||||
category: 'features',
|
||||
condition: (stats) => stats.featuresUnlocked.includes('webcam'),
|
||||
reward: null
|
||||
},
|
||||
{
|
||||
id: 'all_features',
|
||||
name: 'Feature Complete',
|
||||
description: 'Unlock all features',
|
||||
icon: '🎯',
|
||||
category: 'features',
|
||||
condition: (stats) => stats.featuresUnlocked.length >= 10,
|
||||
reward: null
|
||||
},
|
||||
|
||||
// Library Achievements
|
||||
{
|
||||
id: 'first_directory',
|
||||
name: 'Curator Initiate',
|
||||
description: 'Add your first directory to the library',
|
||||
icon: '📁',
|
||||
category: 'library',
|
||||
condition: (stats) => stats.library.directories.length >= 1,
|
||||
reward: null
|
||||
},
|
||||
{
|
||||
id: 'tag_100',
|
||||
name: 'Tagger',
|
||||
description: 'Tag 100 files',
|
||||
icon: '🏷️',
|
||||
category: 'library',
|
||||
condition: (stats) => stats.library.taggedFiles >= 100,
|
||||
reward: null
|
||||
},
|
||||
{
|
||||
id: 'tag_500',
|
||||
name: 'Master Tagger',
|
||||
description: 'Tag 500 files',
|
||||
icon: '🏷️',
|
||||
category: 'library',
|
||||
condition: (stats) => stats.library.taggedFiles >= 500,
|
||||
reward: null
|
||||
},
|
||||
{
|
||||
id: 'coverage_90',
|
||||
name: 'Meticulous Curator',
|
||||
description: 'Reach 90% tag coverage',
|
||||
icon: '✨',
|
||||
category: 'library',
|
||||
condition: (stats) => stats.library.tagCoverage >= 90,
|
||||
reward: null
|
||||
},
|
||||
{
|
||||
id: 'curator_master',
|
||||
name: 'Master Curator',
|
||||
description: 'Achieve Master curator rank',
|
||||
icon: '👑',
|
||||
category: 'library',
|
||||
condition: (stats) => stats.library.curatorRank === 'Master',
|
||||
reward: 'Access to advanced library features'
|
||||
},
|
||||
|
||||
// Challenge Achievements
|
||||
{
|
||||
id: 'marathon_first',
|
||||
name: 'Marathon Runner',
|
||||
description: 'Complete your first marathon session (15x)',
|
||||
icon: '🏃',
|
||||
category: 'challenge',
|
||||
condition: (stats) => stats.marathonSessionsCompleted >= 1,
|
||||
reward: null
|
||||
},
|
||||
{
|
||||
id: 'no_failures',
|
||||
name: 'Flawless',
|
||||
description: 'Complete 10 levels without any failures',
|
||||
icon: '💎',
|
||||
category: 'challenge',
|
||||
condition: (stats) => stats.consecutiveLevelsWithoutFailure >= 10,
|
||||
reward: null
|
||||
},
|
||||
{
|
||||
id: 'path_master',
|
||||
name: 'Path Master',
|
||||
description: 'Complete your chosen path (L22-25)',
|
||||
icon: '🛤️',
|
||||
category: 'challenge',
|
||||
condition: (stats) => stats.pathLevelsCompleted >= 4,
|
||||
reward: null
|
||||
},
|
||||
|
||||
// Hidden Achievements
|
||||
{
|
||||
id: 'secret_ascended',
|
||||
name: 'Ascended',
|
||||
description: 'Complete 5 Ascended Mode levels',
|
||||
icon: '🌟',
|
||||
category: 'hidden',
|
||||
hidden: true,
|
||||
condition: (stats) => stats.ascendedLevelsCompleted >= 5,
|
||||
reward: 'Secret reward'
|
||||
},
|
||||
{
|
||||
id: 'secret_perfectionist',
|
||||
name: 'Perfectionist',
|
||||
description: 'Complete all 30 levels without any failures',
|
||||
icon: '🏆',
|
||||
category: 'hidden',
|
||||
hidden: true,
|
||||
condition: (stats) => stats.completedLevels.length === 30 && stats.totalFailedAttempts === 0,
|
||||
reward: 'Ultimate gooner title'
|
||||
}
|
||||
|
||||
// Total: 25+ achievements
|
||||
];
|
||||
```
|
||||
|
||||
**Methods**:
|
||||
```javascript
|
||||
class ProgressionManager {
|
||||
checkAchievements(stats) {
|
||||
// Loop through all achievements
|
||||
// Check conditions
|
||||
// Unlock new achievements
|
||||
// Return newly unlocked achievements
|
||||
}
|
||||
|
||||
unlockAchievement(achievementId) {
|
||||
// Set achievement as unlocked
|
||||
// Show notification
|
||||
// Save to gameData
|
||||
}
|
||||
|
||||
getUnlockedAchievements() {
|
||||
// Return list of unlocked achievements
|
||||
}
|
||||
|
||||
getAchievementProgress(achievementId) {
|
||||
// For progressive achievements, return % progress
|
||||
}
|
||||
|
||||
displayAchievementNotification(achievement) {
|
||||
// Show toast notification
|
||||
// "Achievement Unlocked: [NAME]"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Achievement Notification UI**:
|
||||
```html
|
||||
<div id="achievement-toast" class="toast hidden">
|
||||
<div class="achievement-icon"></div>
|
||||
<div class="achievement-content">
|
||||
<h4>Achievement Unlocked!</h4>
|
||||
<p class="achievement-name"></p>
|
||||
<p class="achievement-description"></p>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Testing Checklist**:
|
||||
- [ ] All 25+ achievements defined
|
||||
- [ ] Achievement conditions check correctly
|
||||
- [ ] Achievements unlock at right moments
|
||||
- [ ] Notifications display
|
||||
- [ ] Achievements persist
|
||||
- [ ] Hidden achievements stay hidden until unlocked
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Statistics Dashboard (4-5 hours)
|
||||
|
||||
**File to Update**: `src/features/academy/progressionManager.js`
|
||||
**File to Create**: `src/features/academy/statsVisualizer.js`
|
||||
|
||||
**Stats to Track**:
|
||||
```javascript
|
||||
const academyStats = {
|
||||
// Progression
|
||||
currentLevel: 1,
|
||||
highestUnlockedLevel: 1,
|
||||
completedLevels: [],
|
||||
arcsCompleted: [],
|
||||
|
||||
// Time
|
||||
totalSessionTime: 0, // seconds
|
||||
longestSession: 0, // seconds
|
||||
averageSessionLength: 0, // seconds
|
||||
lastPlayedDate: null,
|
||||
|
||||
// Edging
|
||||
totalEdges: 0,
|
||||
edgesPerLevel: {}, // { level: count }
|
||||
averageEdgesPerSession: 0,
|
||||
|
||||
// Features
|
||||
featuresUnlocked: [],
|
||||
webcamTimeUsed: 0,
|
||||
ttsCommandsHeard: 0,
|
||||
hypnoTimeWatched: 0,
|
||||
captionsDisplayed: 0,
|
||||
interruptionsCompleted: 0,
|
||||
popupsViewed: 0,
|
||||
|
||||
// Library
|
||||
library: {
|
||||
totalFiles: 0,
|
||||
taggedFiles: 0,
|
||||
tagCoverage: 0,
|
||||
curatorRank: 'Novice',
|
||||
directoriesAdded: 0
|
||||
},
|
||||
|
||||
// Path
|
||||
selectedPath: null,
|
||||
pathLevelsCompleted: 0,
|
||||
|
||||
// Failures
|
||||
totalFailedAttempts: 0,
|
||||
failuresByReason: {
|
||||
cumming: 0,
|
||||
abandoned: 0,
|
||||
featureClosed: 0
|
||||
},
|
||||
consecutiveLevelsWithoutFailure: 0,
|
||||
|
||||
// Achievements
|
||||
achievementsUnlocked: [],
|
||||
achievementProgress: {}
|
||||
};
|
||||
```
|
||||
|
||||
**Stats Dashboard UI**:
|
||||
```html
|
||||
<div id="stats-dashboard">
|
||||
<h2>Academy Statistics</h2>
|
||||
|
||||
<section id="progression-stats">
|
||||
<h3>Progression</h3>
|
||||
<p>Current Level: <strong id="current-level"></strong></p>
|
||||
<p>Completed Levels: <strong id="completed-levels"></strong> / 30</p>
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" id="level-progress"></div>
|
||||
</div>
|
||||
<p>Arcs Completed: <strong id="arcs-completed"></strong></p>
|
||||
</section>
|
||||
|
||||
<section id="time-stats">
|
||||
<h3>Time Investment</h3>
|
||||
<p>Total Time: <strong id="total-time"></strong></p>
|
||||
<p>Longest Session: <strong id="longest-session"></strong></p>
|
||||
<p>Average Session: <strong id="average-session"></strong></p>
|
||||
</section>
|
||||
|
||||
<section id="edge-stats">
|
||||
<h3>Edging Statistics</h3>
|
||||
<p>Total Edges: <strong id="total-edges"></strong></p>
|
||||
<p>Average per Session: <strong id="avg-edges"></strong></p>
|
||||
<canvas id="edges-chart"></canvas>
|
||||
</section>
|
||||
|
||||
<section id="feature-stats">
|
||||
<h3>Feature Usage</h3>
|
||||
<ul>
|
||||
<li>Webcam Time: <span id="webcam-time"></span></li>
|
||||
<li>TTS Commands: <span id="tts-commands"></span></li>
|
||||
<li>Hypno Time: <span id="hypno-time"></span></li>
|
||||
<li>Captions Shown: <span id="captions"></span></li>
|
||||
<li>Interruptions: <span id="interruptions"></span></li>
|
||||
<li>Popups Viewed: <span id="popups"></span></li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section id="library-stats">
|
||||
<h3>Library Curation</h3>
|
||||
<p>Total Files: <strong id="library-files"></strong></p>
|
||||
<p>Tagged Files: <strong id="tagged-files"></strong></p>
|
||||
<p>Tag Coverage: <strong id="tag-coverage"></strong>%</p>
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" id="coverage-bar"></div>
|
||||
</div>
|
||||
<p>Curator Rank: <strong id="curator-rank"></strong></p>
|
||||
</section>
|
||||
|
||||
<section id="achievement-showcase">
|
||||
<h3>Achievements</h3>
|
||||
<div id="achievement-grid">
|
||||
<!-- Achievement cards -->
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Visualization Methods**:
|
||||
```javascript
|
||||
class StatsVisualizer {
|
||||
renderProgressBar(percent, elementId) {
|
||||
// Update progress bar width
|
||||
}
|
||||
|
||||
renderEdgeChart(edgesPerLevel) {
|
||||
// Use Chart.js or similar to render edge graph
|
||||
}
|
||||
|
||||
formatTime(seconds) {
|
||||
// Convert seconds to "Xh Ym" format
|
||||
}
|
||||
|
||||
renderAchievementGrid(achievements) {
|
||||
// Display unlocked + locked achievements
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Testing Checklist**:
|
||||
- [ ] All stats track correctly
|
||||
- [ ] Dashboard displays all sections
|
||||
- [ ] Progress bars update
|
||||
- [ ] Charts render correctly
|
||||
- [ ] Time formats correctly
|
||||
- [ ] Achievement grid displays
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Advanced Library Management (3-4 hours)
|
||||
|
||||
**File to Update**: `src/features/academy/libraryManager.js`
|
||||
|
||||
**New Features**:
|
||||
1. **Quality Scoring** - Rate files 1-5 stars
|
||||
2. **Diversity Scoring** - Measure tag variety
|
||||
3. **Smart Playlists** - Auto-generate playlists from tags
|
||||
|
||||
**Quality Scoring**:
|
||||
```javascript
|
||||
class LibraryManager {
|
||||
// ... existing methods ...
|
||||
|
||||
rateFile(filePath, rating) {
|
||||
// rating: 1-5 stars
|
||||
// Save to individualFiles
|
||||
}
|
||||
|
||||
getHighestRatedFiles(count = 10) {
|
||||
// Return top-rated files
|
||||
}
|
||||
|
||||
calculateAverageQuality() {
|
||||
// Average rating across all rated files
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Diversity Scoring**:
|
||||
```javascript
|
||||
class LibraryManager {
|
||||
// ... existing ...
|
||||
|
||||
calculateDiversityScore() {
|
||||
// Count unique tags
|
||||
// Compare to total tags in catalog
|
||||
// Return % (0-100)
|
||||
}
|
||||
|
||||
getUnderrepresentedTags() {
|
||||
// Return tags with < 10 files
|
||||
// Suggest adding content with these tags
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Smart Playlists**:
|
||||
```javascript
|
||||
class LibraryManager {
|
||||
// ... existing ...
|
||||
|
||||
createSmartPlaylist(name, tagCombination, matchMode = 'AND') {
|
||||
// tagCombination: ['amateur', 'pov']
|
||||
// matchMode: 'AND' or 'OR'
|
||||
// Return playlist of matching files
|
||||
}
|
||||
|
||||
getSuggestedPlaylists(preferences) {
|
||||
// Based on user preferences, suggest playlists
|
||||
// e.g., "Sissy + Humiliation", "Denial + Edging"
|
||||
}
|
||||
|
||||
savePlaylist(playlist) {
|
||||
// Save to gameData
|
||||
}
|
||||
|
||||
loadPlaylists() {
|
||||
// Return all saved playlists
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Smart Playlist UI**:
|
||||
```html
|
||||
<div id="smart-playlists">
|
||||
<h3>Smart Playlists</h3>
|
||||
<button id="create-playlist">Create Playlist</button>
|
||||
|
||||
<div id="playlist-creator" class="hidden">
|
||||
<input type="text" id="playlist-name" placeholder="Playlist Name">
|
||||
<div id="tag-selector">
|
||||
<!-- Multi-select tags -->
|
||||
</div>
|
||||
<select id="match-mode">
|
||||
<option value="AND">Match ALL tags</option>
|
||||
<option value="OR">Match ANY tag</option>
|
||||
</select>
|
||||
<button id="save-playlist">Save Playlist</button>
|
||||
</div>
|
||||
|
||||
<div id="playlist-list">
|
||||
<!-- Saved playlists -->
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Testing Checklist**:
|
||||
- [ ] Can rate files 1-5 stars
|
||||
- [ ] Quality score calculates
|
||||
- [ ] Diversity score calculates
|
||||
- [ ] Smart playlists create correctly
|
||||
- [ ] Playlists save and load
|
||||
- [ ] Suggested playlists generate
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Curator Rank Benefits (2-3 hours)
|
||||
|
||||
**Curator Rank System**:
|
||||
```javascript
|
||||
const curatorRanks = {
|
||||
Novice: {
|
||||
minCoverage: 0,
|
||||
benefits: ['Basic tagging']
|
||||
},
|
||||
Apprentice: {
|
||||
minCoverage: 50,
|
||||
benefits: ['Bulk tagging', 'Tag suggestions']
|
||||
},
|
||||
Journeyman: {
|
||||
minCoverage: 75,
|
||||
benefits: ['Smart playlists', 'Quality rating']
|
||||
},
|
||||
Expert: {
|
||||
minCoverage: 90,
|
||||
benefits: ['Advanced filters', 'Diversity insights']
|
||||
},
|
||||
Master: {
|
||||
minCoverage: 98,
|
||||
benefits: ['Full library analytics', 'Export/import', 'Custom tags']
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**Rank-Gated Features**:
|
||||
- **Bulk Tagging**: Apprentice+ can tag multiple files at once
|
||||
- **Smart Playlists**: Journeyman+ can create smart playlists
|
||||
- **Advanced Filters**: Expert+ can filter by multiple criteria
|
||||
- **Export/Import**: Master can export library data
|
||||
|
||||
**Testing Checklist**:
|
||||
- [ ] Rank calculates based on coverage
|
||||
- [ ] Benefits unlock at correct ranks
|
||||
- [ ] Features gate correctly
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Export/Import System (2-3 hours)
|
||||
|
||||
**File to Create**: `src/features/academy/dataExporter.js`
|
||||
|
||||
**Responsibilities**:
|
||||
- Export library data as JSON
|
||||
- Export achievements as JSON
|
||||
- Import library data
|
||||
- Share library configurations
|
||||
|
||||
**Methods**:
|
||||
```javascript
|
||||
class DataExporter {
|
||||
exportLibrary() {
|
||||
// Return JSON of libraryData
|
||||
}
|
||||
|
||||
exportAchievements() {
|
||||
// Return JSON of unlocked achievements
|
||||
}
|
||||
|
||||
importLibrary(jsonData) {
|
||||
// Merge imported data with existing library
|
||||
// Avoid duplicates
|
||||
}
|
||||
|
||||
downloadAsFile(data, filename) {
|
||||
// Trigger browser download
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**UI**:
|
||||
```html
|
||||
<div id="export-import">
|
||||
<h3>Data Management</h3>
|
||||
<button id="export-library">Export Library</button>
|
||||
<button id="export-achievements">Export Achievements</button>
|
||||
<input type="file" id="import-file" accept=".json">
|
||||
<button id="import-library">Import Library</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Testing Checklist**:
|
||||
- [ ] Library exports as JSON
|
||||
- [ ] Achievements export as JSON
|
||||
- [ ] Import merges correctly
|
||||
- [ ] No duplicate entries after import
|
||||
|
||||
---
|
||||
|
||||
## 📏 Measurable Test Criteria
|
||||
|
||||
After Phase 6, ALL of these must pass:
|
||||
|
||||
### Achievement System:
|
||||
- [ ] All 25+ achievements defined
|
||||
- [ ] "First Steps" unlocks after L1
|
||||
- [ ] "Foundation Graduate" unlocks after L5
|
||||
- [ ] "Century Club" unlocks at 100 edges
|
||||
- [ ] "10 Hours Strong" unlocks at 10 hours
|
||||
- [ ] "Curator Initiate" unlocks after adding first directory
|
||||
- [ ] Achievement notifications display
|
||||
- [ ] Achievements persist across refresh
|
||||
- [ ] Hidden achievements stay hidden
|
||||
|
||||
### Statistics Dashboard:
|
||||
- [ ] Dashboard displays all sections
|
||||
- [ ] Current level displays correctly
|
||||
- [ ] Completed levels count correct
|
||||
- [ ] Progress bar updates
|
||||
- [ ] Total time calculates correctly
|
||||
- [ ] Total edges track correctly
|
||||
- [ ] Feature usage stats accurate
|
||||
- [ ] Library stats display
|
||||
- [ ] Achievement grid shows unlocked + locked
|
||||
|
||||
### Library Management:
|
||||
- [ ] Can rate files 1-5 stars
|
||||
- [ ] Average quality calculates
|
||||
- [ ] Diversity score calculates
|
||||
- [ ] Smart playlists create
|
||||
- [ ] Playlists save and load
|
||||
- [ ] Tag suggestions work
|
||||
|
||||
### Curator Ranks:
|
||||
- [ ] Novice at 0% coverage
|
||||
- [ ] Apprentice at 50% coverage
|
||||
- [ ] Journeyman at 75% coverage
|
||||
- [ ] Expert at 90% coverage
|
||||
- [ ] Master at 98% coverage
|
||||
- [ ] Benefits unlock correctly
|
||||
|
||||
### Export/Import:
|
||||
- [ ] Library exports as valid JSON
|
||||
- [ ] Achievements export as valid JSON
|
||||
- [ ] Import merges without duplicates
|
||||
- [ ] Download triggers correctly
|
||||
|
||||
### Overall:
|
||||
- [ ] No console errors
|
||||
- [ ] All systems persist data
|
||||
- [ ] Performance acceptable with large library
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Success Criteria
|
||||
|
||||
**Phase 6 Complete When:**
|
||||
1. Achievement system fully functional (25+ achievements)
|
||||
2. Stats dashboard displays all metrics
|
||||
3. Advanced library features work (quality, diversity, playlists)
|
||||
4. Curator rank progression works
|
||||
5. Export/import functional
|
||||
6. All measurable tests pass
|
||||
7. Meta-game layer enhances engagement
|
||||
|
||||
---
|
||||
|
||||
## 📂 Files Created/Modified
|
||||
|
||||
### New Files:
|
||||
- `src/features/academy/progressionManager.js`
|
||||
- `src/features/academy/statsVisualizer.js`
|
||||
- `src/features/academy/dataExporter.js`
|
||||
- `src/data/academy/achievements.js`
|
||||
|
||||
### Modified Files:
|
||||
- `src/features/academy/libraryManager.js`
|
||||
- `src/core/gameDataManager.js`
|
||||
- `training-academy.html`
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Next Phase
|
||||
|
||||
After Phase 6: **Phase 7: Polish/UI/UX/Testing** (Final refinements and QA)
|
||||
|
|
@ -0,0 +1,752 @@
|
|||
# Phase 7: Polish, UI/UX & Testing
|
||||
|
||||
**Priority**: HIGH - Final quality assurance
|
||||
**Estimated Effort**: 12-16 hours
|
||||
**Status**: Not Started
|
||||
**Dependencies**: Phases 1-6 complete
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Phase Goals
|
||||
|
||||
Final polish, UI/UX improvements, comprehensive testing, and quality assurance. Ensure The Academy is polished, accessible, performant, and bug-free before launch.
|
||||
|
||||
---
|
||||
|
||||
## 📋 What This Phase Delivers
|
||||
|
||||
### Polish Areas:
|
||||
1. **UI/UX Refinements** - Dark mode, animations, responsive design
|
||||
2. **Accessibility Features** - Safe word, keyboard navigation, pause functionality
|
||||
3. **Performance Optimization** - Loading times, video buffering, memory management
|
||||
4. **Error Handling** - Graceful failures, user-friendly error messages
|
||||
5. **Comprehensive Testing** - L1→L30 playthrough, all features, all paths
|
||||
6. **Documentation** - User guide, tooltips, onboarding
|
||||
|
||||
**End Result**: Production-ready Academy with excellent user experience.
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ Implementation Tasks
|
||||
|
||||
### Task 1: UI/UX Refinements (4-5 hours)
|
||||
|
||||
#### 1.1 Dark Mode Enhancement
|
||||
**Current**: Basic dark styling
|
||||
**Goal**: Polished dark theme with proper contrast
|
||||
|
||||
**Updates Needed**:
|
||||
```css
|
||||
/* src/styles/styles-dark-edgy.css */
|
||||
|
||||
:root {
|
||||
--bg-primary: #0a0a0a;
|
||||
--bg-secondary: #1a1a1a;
|
||||
--bg-tertiary: #2a2a2a;
|
||||
--text-primary: #e0e0e0;
|
||||
--text-secondary: #a0a0a0;
|
||||
--accent-primary: #ff006e;
|
||||
--accent-secondary: #8338ec;
|
||||
--border-color: #333;
|
||||
--success-color: #06ffa5;
|
||||
--warning-color: #ffbe0b;
|
||||
--error-color: #ff006e;
|
||||
}
|
||||
|
||||
/* Smooth transitions */
|
||||
* {
|
||||
transition: background-color 0.2s ease, color 0.2s ease;
|
||||
}
|
||||
|
||||
/* Card styling */
|
||||
.level-card {
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.level-card:hover {
|
||||
background: var(--bg-tertiary);
|
||||
border-color: var(--accent-primary);
|
||||
box-shadow: 0 6px 12px rgba(255, 0, 110, 0.2);
|
||||
}
|
||||
|
||||
/* Button styling */
|
||||
button {
|
||||
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 12px 24px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(255, 0, 110, 0.4);
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.2 Animations
|
||||
Add smooth transitions for modals, level unlocks, achievements:
|
||||
|
||||
```css
|
||||
/* Modal animations */
|
||||
.modal {
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: scale(0.9); }
|
||||
to { opacity: 1; transform: scale(1); }
|
||||
}
|
||||
|
||||
/* Level unlock animation */
|
||||
.level-card.unlocked {
|
||||
animation: unlock 0.5s ease;
|
||||
}
|
||||
|
||||
@keyframes unlock {
|
||||
0% { transform: scale(1); }
|
||||
50% { transform: scale(1.05); }
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
|
||||
/* Achievement toast */
|
||||
.achievement-toast {
|
||||
animation: slideInRight 0.5s ease;
|
||||
}
|
||||
|
||||
@keyframes slideInRight {
|
||||
from { transform: translateX(400px); opacity: 0; }
|
||||
to { transform: translateX(0); opacity: 1; }
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.3 Responsive Design
|
||||
Ensure mobile/tablet compatibility:
|
||||
|
||||
```css
|
||||
/* Mobile adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.level-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.quad-video {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.stats-dashboard {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
/* Tablet adjustments */
|
||||
@media (min-width: 769px) and (max-width: 1024px) {
|
||||
.level-grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Testing Checklist**:
|
||||
- [ ] Dark mode looks polished
|
||||
- [ ] All buttons have hover states
|
||||
- [ ] Modals animate smoothly
|
||||
- [ ] Level unlocks animate
|
||||
- [ ] Achievement toasts slide in
|
||||
- [ ] Responsive on mobile (320px+)
|
||||
- [ ] Responsive on tablet (768px+)
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Accessibility Features (3-4 hours)
|
||||
|
||||
#### 2.1 Safe Word System
|
||||
**Critical safety feature** for immediate session stop:
|
||||
|
||||
```javascript
|
||||
// src/features/academy/safeWordManager.js
|
||||
class SafeWordManager {
|
||||
constructor() {
|
||||
this.safeWord = localStorage.getItem('safeWord') || 'STOP';
|
||||
this.listening = false;
|
||||
}
|
||||
|
||||
setSafeWord(word) {
|
||||
this.safeWord = word.toUpperCase();
|
||||
localStorage.setItem('safeWord', this.safeWord);
|
||||
}
|
||||
|
||||
startListening() {
|
||||
this.listening = true;
|
||||
document.addEventListener('keydown', this.handleKeypress.bind(this));
|
||||
}
|
||||
|
||||
stopListening() {
|
||||
this.listening = false;
|
||||
document.removeEventListener('keydown', this.handleKeypress.bind(this));
|
||||
}
|
||||
|
||||
handleKeypress(event) {
|
||||
// Listen for safe word typed
|
||||
// If detected, immediately stop session
|
||||
if (this.detectSafeWord(event.key)) {
|
||||
this.triggerEmergencyStop();
|
||||
}
|
||||
}
|
||||
|
||||
triggerEmergencyStop() {
|
||||
// Stop all media
|
||||
// Close all overlays
|
||||
// Return to level select
|
||||
// Show "Session stopped safely" message
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**UI**:
|
||||
```html
|
||||
<div id="safe-word-settings">
|
||||
<h3>Safe Word</h3>
|
||||
<p>Set a safe word to immediately stop any session.</p>
|
||||
<input type="text" id="safe-word-input" placeholder="Enter safe word">
|
||||
<button id="save-safe-word">Save</button>
|
||||
<p><small>Current: <strong id="current-safe-word">STOP</strong></small></p>
|
||||
</div>
|
||||
|
||||
<div id="emergency-stop-notice" class="hidden">
|
||||
<h2>⚠️ Session Stopped</h2>
|
||||
<p>Safe word detected. All activities stopped.</p>
|
||||
<button id="return-to-menu">Return to Menu</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
#### 2.2 Keyboard Navigation
|
||||
Full keyboard accessibility:
|
||||
|
||||
```javascript
|
||||
// Add keyboard shortcuts
|
||||
const keyboardShortcuts = {
|
||||
'Escape': () => pauseSession(),
|
||||
'Space': () => togglePlayPause(),
|
||||
'p': () => openPreferences(),
|
||||
's': () => openStats(),
|
||||
'l': () => openLibrary(),
|
||||
'1-9': (num) => quickSelectLevel(num)
|
||||
};
|
||||
|
||||
// Focus management
|
||||
function manageFocus() {
|
||||
// Ensure modals trap focus
|
||||
// Tab through interactive elements
|
||||
// Return focus on modal close
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.3 Pause Functionality
|
||||
Allow pausing any session:
|
||||
|
||||
```javascript
|
||||
class SessionManager {
|
||||
pause() {
|
||||
// Pause all videos
|
||||
// Stop TTS
|
||||
// Pause timers
|
||||
// Show pause overlay
|
||||
}
|
||||
|
||||
resume() {
|
||||
// Resume all activities
|
||||
// Continue timers from pause point
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**UI**:
|
||||
```html
|
||||
<button id="pause-session" class="floating-button">⏸️ Pause</button>
|
||||
|
||||
<div id="pause-overlay" class="modal hidden">
|
||||
<h2>Session Paused</h2>
|
||||
<p>Time Remaining: <span id="time-remaining"></span></p>
|
||||
<button id="resume-session">Resume</button>
|
||||
<button id="abandon-session">Abandon Session</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Testing Checklist**:
|
||||
- [ ] Safe word can be set
|
||||
- [ ] Safe word triggers emergency stop
|
||||
- [ ] All features stop immediately
|
||||
- [ ] Keyboard shortcuts work
|
||||
- [ ] Tab navigation works in modals
|
||||
- [ ] Pause button accessible
|
||||
- [ ] Pause/resume works correctly
|
||||
- [ ] Timer resumes from pause point
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Performance Optimization (2-3 hours)
|
||||
|
||||
#### 3.1 Video Buffering
|
||||
Pre-load videos to prevent stuttering:
|
||||
|
||||
```javascript
|
||||
class VideoPreloader {
|
||||
preloadVideo(videoPath) {
|
||||
const video = document.createElement('video');
|
||||
video.src = videoPath;
|
||||
video.preload = 'auto';
|
||||
video.load();
|
||||
return video;
|
||||
}
|
||||
|
||||
preloadNextLevelVideos(nextLevel) {
|
||||
// Pre-load videos for next level
|
||||
// Do this during current level
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.2 Memory Management
|
||||
Clean up resources:
|
||||
|
||||
```javascript
|
||||
class ResourceManager {
|
||||
cleanup() {
|
||||
// Stop all videos
|
||||
// Clear intervals/timeouts
|
||||
// Remove event listeners
|
||||
// Garbage collect large objects
|
||||
}
|
||||
|
||||
cleanupAfterLevel() {
|
||||
// Remove unused videos from DOM
|
||||
// Clear temporary data
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.3 Loading States
|
||||
Show loading indicators:
|
||||
|
||||
```html
|
||||
<div id="loading-overlay" class="hidden">
|
||||
<div class="spinner"></div>
|
||||
<p>Loading level...</p>
|
||||
<p id="loading-progress">0%</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Testing Checklist**:
|
||||
- [ ] Videos buffer before playback
|
||||
- [ ] No stuttering during playback
|
||||
- [ ] Memory usage stays stable
|
||||
- [ ] No memory leaks over long sessions
|
||||
- [ ] Loading indicators show during waits
|
||||
- [ ] Resource cleanup works
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Error Handling (2-3 hours)
|
||||
|
||||
#### 4.1 Graceful Failures
|
||||
Handle errors without crashing:
|
||||
|
||||
```javascript
|
||||
class ErrorHandler {
|
||||
handleVideoLoadError(videoPath) {
|
||||
console.error(`Video failed to load: ${videoPath}`);
|
||||
// Try alternate video
|
||||
// Or skip video action
|
||||
// Show user-friendly message
|
||||
showMessage('Video unavailable. Using alternative...');
|
||||
}
|
||||
|
||||
handleFeatureError(feature) {
|
||||
console.error(`Feature failed: ${feature}`);
|
||||
// Disable feature gracefully
|
||||
// Continue session without it
|
||||
showMessage(`${feature} unavailable. Continuing...`);
|
||||
}
|
||||
|
||||
handleDataSaveError() {
|
||||
console.error('Failed to save progress');
|
||||
// Retry save
|
||||
// Offer manual export
|
||||
showMessage('Warning: Progress may not be saved. Export data manually.');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 4.2 User-Friendly Error Messages
|
||||
Replace technical errors:
|
||||
|
||||
```javascript
|
||||
const errorMessages = {
|
||||
'VIDEO_NOT_FOUND': 'The video file could not be found. Check your media library.',
|
||||
'SAVE_FAILED': 'Could not save your progress. Try exporting your data.',
|
||||
'WEBCAM_DENIED': 'Webcam access denied. Enable in browser settings.',
|
||||
'FEATURE_UNAVAILABLE': 'This feature is currently unavailable.',
|
||||
'NETWORK_ERROR': 'Network error. Some features may not work.'
|
||||
};
|
||||
|
||||
function showError(errorCode) {
|
||||
const message = errorMessages[errorCode] || 'An error occurred.';
|
||||
showModal('Error', message);
|
||||
}
|
||||
```
|
||||
|
||||
**Testing Checklist**:
|
||||
- [ ] Missing video handled gracefully
|
||||
- [ ] Feature failures don't crash app
|
||||
- [ ] Data save errors handled
|
||||
- [ ] User-friendly error messages shown
|
||||
- [ ] Errors logged to console for debugging
|
||||
- [ ] Can recover from errors without refresh
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Comprehensive Testing (4-6 hours)
|
||||
|
||||
#### 5.1 Full Campaign Playthrough
|
||||
Test every level sequentially:
|
||||
|
||||
**Test Plan**:
|
||||
```
|
||||
✅ Level 1-5 (Foundation Arc)
|
||||
- [ ] L1: Edge training works
|
||||
- [ ] L2: Rhythm + library addition works
|
||||
- [ ] L3: Video + file tagging works
|
||||
- [ ] L4: Multi-tasking works
|
||||
- [ ] L5: Checkpoint + preferences work
|
||||
|
||||
✅ Level 6-10 (Feature Discovery Arc)
|
||||
- [ ] L6: Webcam enables
|
||||
- [ ] L7: Dual video works
|
||||
- [ ] L8: TTS works
|
||||
- [ ] L9: Quad video works
|
||||
- [ ] L10: Hypno + checkpoint work
|
||||
|
||||
✅ Level 11-15 (Mind & Body Arc)
|
||||
- [ ] L11: Hypno + captions work
|
||||
- [ ] L12: Dynamic captions work
|
||||
- [ ] L13: TTS + hypno sync works
|
||||
- [ ] L14: Sensory overload works
|
||||
- [ ] L15: Checkpoint works
|
||||
|
||||
✅ Level 16-20 (Advanced Training Arc)
|
||||
- [ ] L16: Interruptions work
|
||||
- [ ] L17: Denial training works
|
||||
- [ ] L18: Popups work
|
||||
- [ ] L19: Total immersion works
|
||||
- [ ] L20: Ultimate checkpoint works
|
||||
|
||||
✅ Level 21-25 (Path Specialization Arc)
|
||||
- [ ] L21: Path selection works
|
||||
- [ ] L22-24: Path-specific content works for all 6 paths
|
||||
- [ ] L25: Path graduation works
|
||||
|
||||
✅ Level 26-30 (Ultimate Mastery Arc)
|
||||
- [ ] L26-29: Intensity progression works
|
||||
- [ ] L30: Graduation ceremony works
|
||||
- [ ] Certificate generates
|
||||
- [ ] Stats report accurate
|
||||
- [ ] Freeplay unlocks
|
||||
- [ ] Ascended mode unlocks
|
||||
```
|
||||
|
||||
#### 5.2 Feature Testing
|
||||
Test all features independently:
|
||||
|
||||
```
|
||||
✅ Features
|
||||
- [ ] Webcam: Starts, displays, stops
|
||||
- [ ] Video players: Focus, overlay, quad all work
|
||||
- [ ] TTS: Speaks commands correctly
|
||||
- [ ] Hypno spiral: Animates smoothly
|
||||
- [ ] Captions: Display, auto-generate, theme-based
|
||||
- [ ] Interruptions: Random, pause/resume, all types
|
||||
- [ ] Popups: Random, filtered, auto-hide
|
||||
- [ ] Audio: Background music, metronome, hypno tracks
|
||||
```
|
||||
|
||||
#### 5.3 Data Persistence Testing
|
||||
Ensure all data saves/loads:
|
||||
|
||||
```
|
||||
✅ Data Persistence
|
||||
- [ ] Campaign progress saves
|
||||
- [ ] Preferences save
|
||||
- [ ] Library data saves
|
||||
- [ ] Achievements save
|
||||
- [ ] Stats save
|
||||
- [ ] All data loads on refresh
|
||||
- [ ] No data loss on browser close
|
||||
```
|
||||
|
||||
#### 5.4 Edge Case Testing
|
||||
Test unusual scenarios:
|
||||
|
||||
```
|
||||
✅ Edge Cases
|
||||
- [ ] Complete L1 → immediately refresh → L2 still unlocked
|
||||
- [ ] Fail L5 → restart → preferences re-appear
|
||||
- [ ] Close webcam mid-session → graceful failure
|
||||
- [ ] Change preferences at all 6 checkpoints → preferences apply
|
||||
- [ ] Tag 1000+ files → performance acceptable
|
||||
- [ ] Complete all 30 levels → all features unlocked
|
||||
- [ ] Try Ascended mode → ultra-hard works
|
||||
- [ ] Export library → import on fresh profile → data intact
|
||||
```
|
||||
|
||||
#### 5.5 Cross-Browser Testing
|
||||
Test in multiple browsers:
|
||||
|
||||
```
|
||||
✅ Browsers
|
||||
- [ ] Chrome/Edge (Chromium)
|
||||
- [ ] Firefox
|
||||
- [ ] Safari (if applicable)
|
||||
- [ ] Mobile browsers (Chrome, Safari)
|
||||
```
|
||||
|
||||
#### 5.6 Accessibility Testing
|
||||
Test with accessibility tools:
|
||||
|
||||
```
|
||||
✅ Accessibility
|
||||
- [ ] Screen reader compatibility (basic)
|
||||
- [ ] Keyboard-only navigation works
|
||||
- [ ] Color contrast sufficient (WCAG AA)
|
||||
- [ ] Focus indicators visible
|
||||
- [ ] Safe word accessible via keyboard
|
||||
```
|
||||
|
||||
**Testing Checklist**:
|
||||
- [ ] All 30 levels playable L1→L30
|
||||
- [ ] All features functional
|
||||
- [ ] All data persists
|
||||
- [ ] No console errors in normal flow
|
||||
- [ ] Works in Chrome/Edge
|
||||
- [ ] Works in Firefox
|
||||
- [ ] Mobile-responsive
|
||||
- [ ] Keyboard navigation works
|
||||
- [ ] Safe word works
|
||||
- [ ] No crashes or freezes
|
||||
|
||||
---
|
||||
|
||||
### Task 6: Documentation & Onboarding (1-2 hours)
|
||||
|
||||
#### 6.1 In-App Help
|
||||
Add tooltips and help text:
|
||||
|
||||
```html
|
||||
<!-- Tooltips -->
|
||||
<div class="tooltip">
|
||||
Hover over ? icons for help
|
||||
</div>
|
||||
|
||||
<!-- Level descriptions -->
|
||||
<div class="level-info">
|
||||
<h4>Level 1: Edge Training 101</h4>
|
||||
<p>Learn the basics of edging. Duration: 5 minutes.</p>
|
||||
<p><strong>Requirements:</strong> None</p>
|
||||
<p><strong>Unlocks:</strong> Level 2</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
#### 6.2 First-Time User Onboarding
|
||||
Guide new users:
|
||||
|
||||
```html
|
||||
<div id="welcome-modal" class="modal">
|
||||
<h2>Welcome to The Academy</h2>
|
||||
<p>You are about to begin a comprehensive 30-level gooner training program.</p>
|
||||
|
||||
<h3>Getting Started:</h3>
|
||||
<ul>
|
||||
<li>✅ Complete levels in order</li>
|
||||
<li>✅ Set your preferences at checkpoints</li>
|
||||
<li>✅ Build your media library</li>
|
||||
<li>✅ Unlock features progressively</li>
|
||||
<li>✅ Graduate after Level 30</li>
|
||||
</ul>
|
||||
|
||||
<h3>Safety:</h3>
|
||||
<p>Set a safe word in settings to immediately stop any session.</p>
|
||||
|
||||
<button id="start-academy">Start Level 1</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
#### 6.3 User Guide (Markdown)
|
||||
Create `docs/USER-GUIDE.md`:
|
||||
|
||||
```markdown
|
||||
# The Academy - User Guide
|
||||
|
||||
## Overview
|
||||
The Academy is a 30-level progressive gooner training program...
|
||||
|
||||
## Getting Started
|
||||
1. Start with Level 1
|
||||
2. Complete each level to unlock the next
|
||||
3. Set preferences at checkpoints (L1, 5, 10, 15, 20, 25)
|
||||
|
||||
## Features
|
||||
- Webcam: Watch yourself (unlocks L6)
|
||||
- TTS: Voice commands (unlocks L8)
|
||||
...
|
||||
|
||||
## Safety
|
||||
- Set a safe word in settings
|
||||
- Press Escape to pause any session
|
||||
...
|
||||
|
||||
## FAQ
|
||||
Q: Can I skip levels?
|
||||
A: No, levels must be completed in order.
|
||||
...
|
||||
```
|
||||
|
||||
**Testing Checklist**:
|
||||
- [ ] Tooltips display
|
||||
- [ ] Level info shows
|
||||
- [ ] Welcome modal appears for new users
|
||||
- [ ] User guide complete and accurate
|
||||
|
||||
---
|
||||
|
||||
## 📏 Measurable Test Criteria
|
||||
|
||||
After Phase 7, ALL of these must pass:
|
||||
|
||||
### UI/UX:
|
||||
- [ ] Dark mode polished
|
||||
- [ ] All animations smooth
|
||||
- [ ] Responsive on mobile (320px+)
|
||||
- [ ] Responsive on tablet (768px+)
|
||||
- [ ] All buttons have hover states
|
||||
- [ ] Modals animate in/out
|
||||
- [ ] Level unlocks animate
|
||||
|
||||
### Accessibility:
|
||||
- [ ] Safe word can be set
|
||||
- [ ] Safe word stops session immediately
|
||||
- [ ] Keyboard shortcuts work (Escape, Space, p, s, l)
|
||||
- [ ] Tab navigation works
|
||||
- [ ] Pause/resume functional
|
||||
- [ ] Focus management correct in modals
|
||||
|
||||
### Performance:
|
||||
- [ ] Videos buffer before playback
|
||||
- [ ] No stuttering during 5-hour session
|
||||
- [ ] Memory stable over long sessions
|
||||
- [ ] Loading indicators show
|
||||
- [ ] Resource cleanup works
|
||||
|
||||
### Error Handling:
|
||||
- [ ] Missing videos handled gracefully
|
||||
- [ ] Feature failures don't crash app
|
||||
- [ ] User-friendly error messages
|
||||
- [ ] Can recover from errors
|
||||
|
||||
### Comprehensive Testing:
|
||||
- [ ] All 30 levels playable sequentially
|
||||
- [ ] All features work
|
||||
- [ ] All data persists
|
||||
- [ ] Works in Chrome/Edge
|
||||
- [ ] Works in Firefox
|
||||
- [ ] Mobile-responsive
|
||||
- [ ] No critical bugs
|
||||
|
||||
### Documentation:
|
||||
- [ ] Tooltips helpful
|
||||
- [ ] Welcome modal informative
|
||||
- [ ] User guide complete
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Success Criteria
|
||||
|
||||
**Phase 7 Complete When:**
|
||||
1. UI/UX polished and responsive
|
||||
2. Accessibility features functional (safe word, keyboard nav, pause)
|
||||
3. Performance optimized (no stuttering, stable memory)
|
||||
4. Error handling graceful
|
||||
5. Full L1→L30 playthrough successful
|
||||
6. All features tested and working
|
||||
7. No critical bugs
|
||||
8. Cross-browser compatible
|
||||
9. Documentation complete
|
||||
10. **Ready for launch**
|
||||
|
||||
---
|
||||
|
||||
## 📂 Files Created/Modified
|
||||
|
||||
### New Files:
|
||||
- `src/features/academy/safeWordManager.js`
|
||||
- `src/utils/errorHandler.js`
|
||||
- `src/utils/performanceMonitor.js`
|
||||
- `docs/USER-GUIDE.md`
|
||||
|
||||
### Modified Files:
|
||||
- `src/styles/styles-dark-edgy.css` (polish)
|
||||
- `training-academy.html` (accessibility, onboarding)
|
||||
- All manager files (error handling)
|
||||
|
||||
---
|
||||
|
||||
## 🎉 Launch Readiness
|
||||
|
||||
After Phase 7, The Academy is **production-ready**:
|
||||
|
||||
✅ 30 levels fully functional
|
||||
✅ 6 arcs complete
|
||||
✅ All features unlocked progressively
|
||||
✅ Preference system working
|
||||
✅ Library tagging complete
|
||||
✅ 25+ achievements
|
||||
✅ Stats dashboard
|
||||
✅ Graduation ceremony
|
||||
✅ Freeplay & Ascended modes
|
||||
✅ Polished UI/UX
|
||||
✅ Accessible & safe
|
||||
✅ Performant & stable
|
||||
✅ Well-documented
|
||||
|
||||
**Total Development Time**: 108-142 hours
|
||||
**Total Levels**: 30
|
||||
**Total Features**: 10+
|
||||
**Total Achievements**: 25+
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Post-Launch
|
||||
|
||||
**Future Enhancements** (optional):
|
||||
- Community features (share achievements, playlists)
|
||||
- Additional paths
|
||||
- Seasonal events
|
||||
- Advanced analytics
|
||||
- Mobile app version
|
||||
|
||||
**Maintenance**:
|
||||
- Monitor for bugs
|
||||
- Update media library
|
||||
- Add new achievements
|
||||
- Respond to user feedback
|
||||
|
|
@ -0,0 +1,223 @@
|
|||
# Phase 8: Polish & Refinement
|
||||
|
||||
**Priority**: MEDIUM - Quality of Life improvements
|
||||
**Estimated Effort**: 8-12 hours
|
||||
**Status**: Not Started
|
||||
**Dependencies**: Phases 1-7 complete
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Phase Goals
|
||||
|
||||
Polish and refine The Academy experience based on testing feedback. Focus on UI/UX improvements, feature enhancements, and quality of life updates that emerged during implementation and testing.
|
||||
|
||||
---
|
||||
|
||||
## 📋 Action Items
|
||||
|
||||
### UI/UX Improvements
|
||||
|
||||
#### Scenario Display Enhancement
|
||||
- [ ] **Improve typography and spacing**
|
||||
- Better font hierarchy (titles, body, instructions)
|
||||
- Consistent padding/margins throughout scenarios
|
||||
- Improved line height and readability
|
||||
|
||||
- [ ] **Visual design upgrades**
|
||||
- More engaging card/panel designs for story steps
|
||||
- Better use of colors for different step types (story, action, choice)
|
||||
- Add subtle animations/transitions between steps
|
||||
- Enhance button styling (hover states, active states, disabled states)
|
||||
|
||||
- [ ] **Story presentation**
|
||||
- More immersive story text formatting
|
||||
- Better visual separation between story and instructions
|
||||
- Add instructor avatar or themed visual elements
|
||||
- Consider different visual themes per arc
|
||||
|
||||
#### Action Button States
|
||||
- [ ] **Clear visual feedback**
|
||||
- Distinct hover states
|
||||
- Clear disabled state (grayed out with reason)
|
||||
- Loading/processing state for async actions
|
||||
- Success state animation when completing actions
|
||||
|
||||
- [ ] **Button labeling**
|
||||
- More descriptive button text (e.g., "Continue to Next Challenge" vs "Continue")
|
||||
- Show what happens next on buttons when possible
|
||||
- Display time remaining on timed action buttons
|
||||
|
||||
### Feature Enhancements
|
||||
|
||||
#### Library Tagging Verification (Level 2)
|
||||
- [ ] **Create new action type: `library-tag-verification`**
|
||||
- Parameters:
|
||||
```javascript
|
||||
{
|
||||
type: 'library-tag-verification',
|
||||
params: {
|
||||
requiredImages: 5,
|
||||
requiredVideos: 5,
|
||||
suggestedTags: ['amateur', 'solo', 'pov']
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Build in-game tagging interface**
|
||||
- Display user's library files (images and videos separately)
|
||||
- File preview thumbnails
|
||||
- Tag input field with suggestions
|
||||
- Apply tags to multiple files at once
|
||||
|
||||
- [ ] **Live progress tracking**
|
||||
- Counter display: "Images tagged: 3/5 | Videos tagged: 2/5"
|
||||
- Visual progress bars for each type
|
||||
- Green checkmark when threshold reached
|
||||
- Disable "Complete" button until both thresholds met
|
||||
|
||||
- [ ] **Integration with libraryManager**
|
||||
- Use existing `tagFile()` and `tagDirectory()` methods
|
||||
- Sync tags to localStorage
|
||||
- Validate minimum tags applied before allowing progression
|
||||
|
||||
#### General Quality of Life
|
||||
- [ ] **Keyboard shortcuts**
|
||||
- Space/Enter to continue on story steps
|
||||
- ESC to open quit confirmation
|
||||
- Number keys for multiple choice selections
|
||||
|
||||
- [ ] **Session persistence**
|
||||
- Save mid-level progress (not just level completion)
|
||||
- Resume option if session interrupted
|
||||
- "Last played" timestamp display
|
||||
|
||||
- [ ] **Progress indicators**
|
||||
- Show "Step X of Y" in scenarios with multiple steps
|
||||
- Arc progress bar (e.g., "Foundation Arc: 3/5 levels complete")
|
||||
- Overall campaign progress percentage
|
||||
|
||||
### Accessibility Improvements
|
||||
- [ ] **Screen reader support**
|
||||
- Proper ARIA labels on all interactive elements
|
||||
- Announce step changes to screen readers
|
||||
- Keyboard navigation for all features
|
||||
|
||||
- [ ] **Visual accessibility**
|
||||
- High contrast mode option
|
||||
- Adjustable text size
|
||||
- Color-blind friendly color schemes
|
||||
|
||||
- [ ] **Audio accessibility**
|
||||
- TTS volume controls more prominent
|
||||
- Option to adjust TTS speed
|
||||
- Visual captions for TTS announcements
|
||||
|
||||
### Performance Optimizations
|
||||
- [ ] **Reduce load times**
|
||||
- Lazy load level data (only load current + next 2 levels)
|
||||
- Optimize scenario step rendering
|
||||
- Cache frequently used images/assets
|
||||
|
||||
- [ ] **Smooth transitions**
|
||||
- Add loading states between levels
|
||||
- Prevent UI flicker during step changes
|
||||
- Optimize video player initialization
|
||||
|
||||
### Bug Fixes & Edge Cases
|
||||
- [ ] **Error handling**
|
||||
- Graceful fallback if webcam unavailable
|
||||
- Better messaging if video library empty
|
||||
- Handle missing preferences gracefully
|
||||
|
||||
- [ ] **Edge cases**
|
||||
- What happens if user completes level without saving?
|
||||
- Handle browser refresh mid-level
|
||||
- Prevent double-clicking completion buttons
|
||||
|
||||
---
|
||||
|
||||
## 📂 Files to Modify
|
||||
|
||||
### Primary Files:
|
||||
- `training-academy.html` - UI improvements, new action handler
|
||||
- `src/features/tasks/interactiveTaskManager.js` - Library tag verification action
|
||||
- `src/features/academy/libraryManager.js` - Tagging interface integration
|
||||
- `src/styles/academy-ui.css` - Visual enhancements
|
||||
|
||||
### Supporting Files:
|
||||
- `src/features/academy/academyUI.js` - Progress indicators
|
||||
- `src/data/modes/trainingGameData.js` - Update Level 2 to use new tagging action
|
||||
- `src/features/ui/flashMessageManager.js` - Better user feedback
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Success Criteria
|
||||
|
||||
**Phase 8 Complete When:**
|
||||
1. UI/UX improvements implemented and tested
|
||||
2. Library tagging verification working in-game
|
||||
3. All quality of life features functional
|
||||
4. Accessibility requirements met
|
||||
5. Performance optimizations applied
|
||||
6. Bug fixes verified
|
||||
7. User testing shows improved experience
|
||||
|
||||
---
|
||||
|
||||
## 📝 Testing Checklist
|
||||
|
||||
### UI/UX Testing:
|
||||
- [ ] All scenario steps render with improved styling
|
||||
- [ ] Button states clearly visible and functional
|
||||
- [ ] Transitions smooth and performant
|
||||
- [ ] Story text engaging and well-formatted
|
||||
|
||||
### Feature Testing:
|
||||
- [ ] Library tagging verification tracks correctly
|
||||
- [ ] Both image and video thresholds work
|
||||
- [ ] Progress saves and persists
|
||||
- [ ] Keyboard shortcuts functional
|
||||
|
||||
### Accessibility Testing:
|
||||
- [ ] Screen reader announces all changes
|
||||
- [ ] Full keyboard navigation works
|
||||
- [ ] High contrast mode readable
|
||||
- [ ] TTS controls accessible
|
||||
|
||||
### Performance Testing:
|
||||
- [ ] Levels load quickly
|
||||
- [ ] No UI lag during step transitions
|
||||
- [ ] Video playback smooth
|
||||
- [ ] Memory usage acceptable
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Implementation Priority
|
||||
|
||||
**High Priority** (Do First):
|
||||
1. UI/UX improvements (most visible impact)
|
||||
2. Library tagging verification (core feature gap)
|
||||
3. Button state improvements (user feedback)
|
||||
|
||||
**Medium Priority** (Do Second):
|
||||
1. Keyboard shortcuts
|
||||
2. Progress indicators
|
||||
3. Session persistence
|
||||
|
||||
**Low Priority** (Polish):
|
||||
1. Accessibility enhancements
|
||||
2. Performance optimizations
|
||||
3. Advanced visual theming
|
||||
|
||||
---
|
||||
|
||||
## 💡 Future Considerations
|
||||
|
||||
Items to consider for post-Phase 8:
|
||||
- Custom instructor personas (different voices/styles)
|
||||
- Achievement system for completing challenges
|
||||
- Leaderboard/community features
|
||||
- Custom level editor
|
||||
- Mobile responsiveness
|
||||
- Dark/light theme toggle
|
||||
- Export/import progress data
|
||||
|
|
@ -17,4 +17,9 @@ Thoughts so far:
|
|||
I'm liking it, I'm using it a couple times every gooning session, I can def see improvements in the gaming tracking (maybe some more achievements)
|
||||
I haven't gone through ALL functions but will keep you updated...
|
||||
|
||||
Also think would be interesting to have a kind of "Story Mode", like narrative driven gooning... Sounds fun, would love to help here too
|
||||
Also think would be interesting to have a kind of "Story Mode", like narrative driven gooning... Sounds fun, would love to help here too
|
||||
|
||||
|
||||
|
||||
|
||||
I would like to lift and shift all the library code from index.html to it's own page(library.html). Please mirror it to all the other pages with a header that matches the style and all the css variables to ensure the theme is continuous throughout.
|
||||
45
index.html
45
index.html
|
|
@ -196,6 +196,22 @@
|
|||
<span class="cassie-icon"></span>
|
||||
<span class="feature-text">Library</span>
|
||||
</button>
|
||||
<button class="hero-feature btn-feature" id="test-campaign-btn" onclick="window.location.href='test-campaign-manager.html'" style="background: linear-gradient(135deg, #8338ec, #ff006e);">
|
||||
<span class="feature-icon">🧪</span>
|
||||
<span class="feature-text">Test Campaign</span>
|
||||
</button>
|
||||
<button class="hero-feature btn-feature" id="test-preferences-btn" onclick="window.location.href='test-preference-manager.html'" style="background: linear-gradient(135deg, #ff006e, #8338ec);">
|
||||
<span class="feature-icon">🎯</span>
|
||||
<span class="feature-text">Test Preferences</span>
|
||||
</button>
|
||||
<button class="hero-feature btn-feature" id="test-library-btn" onclick="window.location.href='test-library-manager.html'" style="background: linear-gradient(135deg, #8338ec, #ff006e);">
|
||||
<span class="feature-icon">📚</span>
|
||||
<span class="feature-text">Test Library</span>
|
||||
</button>
|
||||
<button class="hero-feature btn-feature" id="test-academy-ui-btn" onclick="window.location.href='test-academy-ui.html'" style="background: linear-gradient(135deg, #ff006e, #8338ec);">
|
||||
<span class="feature-icon">🎓</span>
|
||||
<span class="feature-text">Test Academy UI</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -894,6 +910,10 @@
|
|||
<script src="src/data/modes/dressUpGameData.js"></script>
|
||||
<script src="src/data/gameDataManager.js"></script>
|
||||
|
||||
<!-- Inventory System Utilities -->
|
||||
<script src="src/utils/inventoryManager.js"></script>
|
||||
<script src="src/data/poseBanks/inventoryPoseBank.js"></script>
|
||||
|
||||
<!-- Backup System -->
|
||||
<script src="src/utils/backupManager.js"></script>
|
||||
|
||||
|
|
@ -3424,6 +3444,31 @@
|
|||
|
||||
// Initialize backup system
|
||||
initializeBackupSystem();
|
||||
|
||||
// Set up global localStorage quota handler
|
||||
const originalSetItem = localStorage.setItem.bind(localStorage);
|
||||
localStorage.setItem = function(key, value) {
|
||||
try {
|
||||
originalSetItem(key, value);
|
||||
} catch (error) {
|
||||
if (error.name === 'QuotaExceededError') {
|
||||
console.error('🚨 LocalStorage quota exceeded! Performing emergency cleanup...');
|
||||
if (window.backupManager) {
|
||||
window.backupManager.performEmergencyCleanup();
|
||||
}
|
||||
// Try again after cleanup
|
||||
try {
|
||||
originalSetItem(key, value);
|
||||
console.log('✅ Save succeeded after cleanup');
|
||||
} catch (retryError) {
|
||||
console.error('❌ Save failed even after cleanup:', retryError);
|
||||
alert('⚠️ Storage is full! Photo data has been cleared. Please restart the app.');
|
||||
}
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
} // Set up video management button (only once)
|
||||
const videoManageBtn = document.getElementById('manage-video-btn');
|
||||
if (videoManageBtn && !videoManageBtn.hasAttribute('data-handler-attached')) {
|
||||
|
|
|
|||
|
|
@ -38,9 +38,9 @@ copy package.json "%OUTPUT_DIR%\"
|
|||
:: Copy documentation
|
||||
echo 📚 Copying documentation...
|
||||
copy README.md "%OUTPUT_DIR%\"
|
||||
copy TESTER_GUIDE.md "%OUTPUT_DIR%\"
|
||||
copy INSTALLATION_GUIDE.md "%OUTPUT_DIR%\"
|
||||
copy README-DESKTOP.md "%OUTPUT_DIR%\"
|
||||
@REM copy TESTER_GUIDE.md "%OUTPUT_DIR%\"
|
||||
@REM copy INSTALLATION_GUIDE.md "%OUTPUT_DIR%\"
|
||||
@REM copy README-DESKTOP.md "%OUTPUT_DIR%\"
|
||||
|
||||
:: Copy setup scripts
|
||||
echo 🔧 Copying setup scripts...
|
||||
|
|
@ -58,11 +58,11 @@ xcopy assets "%OUTPUT_DIR%\assets\" /e /i /q
|
|||
|
||||
:: Copy empty directories for user content
|
||||
echo 📁 Creating user content directories...
|
||||
mkdir "%OUTPUT_DIR%\images"
|
||||
mkdir "%OUTPUT_DIR%\images\tasks"
|
||||
mkdir "%OUTPUT_DIR%\images\consequences"
|
||||
mkdir "%OUTPUT_DIR%\photos"
|
||||
mkdir "%OUTPUT_DIR%\photos\captured"
|
||||
mkdir "%OUTPUT_DIR%\audio"
|
||||
mkdir "%OUTPUT_DIR%\videos"
|
||||
mkdir "%OUTPUT_DIR%\videos\recorded"
|
||||
mkdir "%OUTPUT_DIR%\backups"
|
||||
|
||||
:: Copy sample audio (if exists)
|
||||
|
|
@ -71,29 +71,11 @@ if exist audio\*.mp3 (
|
|||
xcopy audio "%OUTPUT_DIR%\audio\" /e /i /q /y
|
||||
)
|
||||
|
||||
:: Create placeholder files
|
||||
echo 📝 Creating placeholder files...
|
||||
echo # User Task Images > "%OUTPUT_DIR%\images\tasks\README.md"
|
||||
echo Upload your custom task images here. Supported formats: JPG, PNG, WebP >> "%OUTPUT_DIR%\images\tasks\README.md"
|
||||
echo Maximum 50 images, recommended resolution: 1600x1200 >> "%OUTPUT_DIR%\images\tasks\README.md"
|
||||
|
||||
echo # User Consequence Images > "%OUTPUT_DIR%\images\consequences\README.md"
|
||||
echo Upload your custom consequence images here. Supported formats: JPG, PNG, WebP >> "%OUTPUT_DIR%\images\consequences\README.md"
|
||||
echo These images are used for punishment scenarios. >> "%OUTPUT_DIR%\images\consequences\README.md"
|
||||
|
||||
echo # Background Music > "%OUTPUT_DIR%\audio\README.md"
|
||||
echo Place your background music files here. Supported formats: MP3, WAV, OGG >> "%OUTPUT_DIR%\audio\README.md"
|
||||
echo Files will be automatically detected and added to the playlist. >> "%OUTPUT_DIR%\audio\README.md"
|
||||
|
||||
echo # Video Directory > "%OUTPUT_DIR%\videos\README.md"
|
||||
echo Link external video directories here or place video files directly. >> "%OUTPUT_DIR%\videos\README.md"
|
||||
echo Supported formats: MP4, WebM, AVI, MOV >> "%OUTPUT_DIR%\videos\README.md"
|
||||
echo For best performance, use MP4 format with H.264 codec. >> "%OUTPUT_DIR%\videos\README.md"
|
||||
|
||||
:: Create distribution info file
|
||||
echo 📋 Creating distribution info...
|
||||
echo # Gooner Training Academy - Distribution Info > "%OUTPUT_DIR%\DISTRIBUTION_INFO.txt"
|
||||
echo Version: 4.0 Beta >> "%OUTPUT_DIR%\DISTRIBUTION_INFO.txt"
|
||||
echo Version: 4.1 Beta >> "%OUTPUT_DIR%\DISTRIBUTION_INFO.txt"
|
||||
echo Build Date: %BUILD_DATE% >> "%OUTPUT_DIR%\DISTRIBUTION_INFO.txt"
|
||||
echo Package Type: Beta Testing Build >> "%OUTPUT_DIR%\DISTRIBUTION_INFO.txt"
|
||||
echo. >> "%OUTPUT_DIR%\DISTRIBUTION_INFO.txt"
|
||||
|
|
|
|||
|
|
@ -1,292 +1,296 @@
|
|||
/**
|
||||
* Dress-Up/Photography Game Mode Data
|
||||
* Webcam photography and dressing challenges
|
||||
* Inventory-based progressive photo challenges
|
||||
*/
|
||||
|
||||
const dressUpGameData = {
|
||||
// Photography and dress-up tasks
|
||||
mainTasks: [
|
||||
{
|
||||
id: 'photo-session-1',
|
||||
text: "Basic Photo Session",
|
||||
difficulty: "Easy",
|
||||
interactiveType: "photo-challenge",
|
||||
story: "Take photos of yourself following the specified requirements.",
|
||||
photoRequirements: {
|
||||
count: 3,
|
||||
poses: ["standing", "sitting", "profile"],
|
||||
timer: 30
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'outfit-challenge-1',
|
||||
text: "Outfit Coordination Challenge",
|
||||
difficulty: "Medium",
|
||||
interactiveType: "choice-challenge",
|
||||
story: "Choose and model different outfit combinations.",
|
||||
choices: [
|
||||
{ text: "Casual wear", value: "casual", points: 10 },
|
||||
{ text: "Formal attire", value: "formal", points: 15 },
|
||||
{ text: "Creative costume", value: "creative", points: 20 }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'pose-sequence-1',
|
||||
text: "Pose Sequence Challenge",
|
||||
difficulty: "Hard",
|
||||
interactiveType: "photo-challenge",
|
||||
story: "Execute a sequence of poses for a professional photo shoot.",
|
||||
photoRequirements: {
|
||||
count: 6,
|
||||
poses: ["elegant", "playful", "serious", "dynamic", "artistic", "confident"],
|
||||
timer: 45
|
||||
}
|
||||
}
|
||||
],
|
||||
mainTasks: [],
|
||||
|
||||
// Dress-up scenarios
|
||||
// Consequence tasks for dress-up mode
|
||||
consequenceTasks: [],
|
||||
|
||||
// Inventory-based progression scenario
|
||||
scenarios: [
|
||||
{
|
||||
id: 'scenario-dress-up-photo',
|
||||
text: "Sissy Dress-Up Photo Session",
|
||||
difficulty: "Medium",
|
||||
id: 'scenario-inventory-progression',
|
||||
text: "Personal Transformation Photo Journey",
|
||||
difficulty: "Variable",
|
||||
interactiveType: "scenario-adventure",
|
||||
interactiveData: {
|
||||
title: "Feminization Photography Studio",
|
||||
title: "Inventory-Based Photo Transformation",
|
||||
steps: {
|
||||
start: {
|
||||
type: 'choice',
|
||||
mood: 'dominant',
|
||||
story: "You've been brought to a private studio for a special photo session. The photographer looks you up and down with a smirk. 'Today we're going to transform you into the perfect little sissy and document every humiliating moment. Which type of feminization session should we start with?'",
|
||||
mood: 'introduction',
|
||||
story: "Welcome to your personalized transformation photo session. I'll guide you through a progressive photo journey tailored to what you have available. First, I need to know what items you have at your disposal. Be honest - this determines your entire experience.",
|
||||
choices: [
|
||||
{
|
||||
text: "Forced feminization makeover",
|
||||
type: "feminization",
|
||||
preview: "Complete transformation into a sissy",
|
||||
nextStep: "feminization_path"
|
||||
},
|
||||
{
|
||||
text: "Humiliating sissy maid outfit",
|
||||
type: "maid",
|
||||
preview: "Degrading maid costume and poses",
|
||||
nextStep: "maid_path"
|
||||
},
|
||||
{
|
||||
text: "Slutty schoolgirl transformation",
|
||||
type: "schoolgirl",
|
||||
preview: "Inappropriate schoolgirl outfit and poses",
|
||||
nextStep: "schoolgirl_path"
|
||||
},
|
||||
{
|
||||
text: "Bimbo princess dress-up",
|
||||
type: "bimbo",
|
||||
preview: "Over-the-top feminine and degrading",
|
||||
nextStep: "bimbo_path"
|
||||
text: "Begin inventory questionnaire",
|
||||
nextStep: "questionnaire"
|
||||
}
|
||||
]
|
||||
},
|
||||
feminization_path: {
|
||||
type: 'action',
|
||||
mood: 'humiliating',
|
||||
story: "The photographer forces you into a frilly pink dress, applies makeup to your face, and puts a blonde wig on your head. 'Look at what a pathetic little sissy you make! Edge while I take pictures of your humiliation. The more aroused you get, the more feminine and submissive you'll look in the photos.'",
|
||||
actionText: "Edge while being feminized and photographed",
|
||||
duration: 240,
|
||||
nextStep: "feminization_progression"
|
||||
|
||||
questionnaire: {
|
||||
type: 'inventory-check',
|
||||
mood: 'assessment',
|
||||
story: "Let's assess what you have available. Select the appropriate option for each item category.",
|
||||
inventoryCategories: {
|
||||
clothing: {
|
||||
title: "👗 Clothing & Feminization Items",
|
||||
items: {
|
||||
panties: { label: 'Panties', options: ['none', 'basic', 'sexy', 'multiple'] },
|
||||
bras: { label: 'Bras', options: ['none', 'sports', 'regular', 'sexy', 'multiple'] },
|
||||
dresses: { label: 'Dresses', options: ['none', 'casual', 'slutty', 'multiple'] },
|
||||
skirts: { label: 'Skirts', options: ['none', 'basic', 'mini', 'micro', 'multiple'] },
|
||||
pantyhose: { label: 'Pantyhose/Stockings', options: ['none', 'nude', 'fishnet', 'multiple'] },
|
||||
heels: { label: 'Heels/Shoes', options: ['none', 'flats', 'heels', 'platforms'] },
|
||||
wigs: { label: 'Wigs', options: ['none', 'short', 'long', 'colored'] },
|
||||
lingerie: { label: 'Lingerie Sets', options: ['none', 'basic', 'sexy', 'extreme'] }
|
||||
}
|
||||
},
|
||||
accessories: {
|
||||
title: "💄 Makeup & Accessories",
|
||||
items: {
|
||||
makeup: { label: 'Makeup Kit', options: ['none', 'basic', 'full'] },
|
||||
jewelry: { label: 'Jewelry', options: ['none', 'basic', 'feminine', 'collar'] },
|
||||
nailPolish: { label: 'Nail Polish', options: ['none', 'basic', 'colors'] }
|
||||
}
|
||||
},
|
||||
toys: {
|
||||
title: "🔞 Toys & Restraints",
|
||||
items: {
|
||||
dildos: { label: 'Dildos', options: ['none', 'small', 'medium', 'large', 'multiple'] },
|
||||
plugs: { label: 'Butt Plugs', options: ['none', 'small', 'medium', 'large', 'tail'] },
|
||||
chastity: { label: 'Chastity Device', options: ['none', 'cage', 'belt'] },
|
||||
restraints: { label: 'Restraints', options: ['none', 'cuffs', 'rope', 'spreader'] },
|
||||
gags: { label: 'Gags', options: ['none', 'ball', 'ring'] },
|
||||
nippleClamps: { label: 'Nipple Clamps', options: ['none', 'basic', 'weighted', 'chain'] }
|
||||
}
|
||||
},
|
||||
environment: {
|
||||
title: "📸 Setup & Environment",
|
||||
items: {
|
||||
mirror: { label: 'Mirror Available', type: 'boolean' },
|
||||
fullMirror: { label: 'Full-Length Mirror', type: 'boolean' },
|
||||
privateSpace: { label: 'Private Space', type: 'boolean' },
|
||||
phoneStand: { label: 'Phone/Camera Stand', type: 'boolean' }
|
||||
}
|
||||
}
|
||||
},
|
||||
nextStep: "inventory_summary"
|
||||
},
|
||||
maid_path: {
|
||||
type: 'action',
|
||||
mood: 'degrading',
|
||||
story: "The photographer hands you a skimpy maid outfit with a short frilly skirt. 'Put this on, sissy maid. You're going to pose like the submissive little servant you are. Edge while I photograph your humiliation. Show me how pathetic you look in that outfit.'",
|
||||
actionText: "Edge while posing as a degraded sissy maid",
|
||||
duration: 300,
|
||||
nextStep: "maid_progression"
|
||||
},
|
||||
schoolgirl_path: {
|
||||
type: 'action',
|
||||
mood: 'inappropriate',
|
||||
story: "The photographer tosses you a slutty schoolgirl outfit - tiny plaid skirt, tight white shirt, and pigtails. 'Time to play dress-up, little girl. You're going to pose like the naughty schoolgirl slut you are. Edge while I capture your shame.'",
|
||||
actionText: "Edge while posing as a slutty schoolgirl",
|
||||
duration: 180,
|
||||
nextStep: "schoolgirl_progression"
|
||||
},
|
||||
bimbo_path: {
|
||||
type: 'action',
|
||||
mood: 'bimbo',
|
||||
story: "The photographer pulls out the most degrading outfit yet - a hot pink mini dress, massive fake breasts, blonde bimbo wig, and platform heels. 'Time to complete your transformation into a brainless bimbo slut. Edge while I photograph how pathetic and desperate you look. Show me what a dumb little bimbo you are.'",
|
||||
actionText: "Edge while posing as a humiliated bimbo",
|
||||
duration: 360,
|
||||
nextStep: "bimbo_progression"
|
||||
},
|
||||
feminization_progression: {
|
||||
type: 'choice',
|
||||
mood: 'sissy_humiliation',
|
||||
story: "The photographer reviews the humiliating feminization photos. 'Look at these pictures - you make such a pathetic little sissy! The camera captured every moment of your shame. Ready to take it further?'",
|
||||
|
||||
inventory_summary: {
|
||||
type: 'path-generation',
|
||||
mood: 'planning',
|
||||
story: "Processing your inventory... Calculating your transformation tier...",
|
||||
choices: [
|
||||
{
|
||||
text: "Take sissy humiliation photos now",
|
||||
type: "photography",
|
||||
preview: "Capture your feminized shame",
|
||||
nextStep: "feminization_photo_session"
|
||||
text: "Begin my personalized photo journey",
|
||||
nextStep: "challenge_1"
|
||||
},
|
||||
{
|
||||
text: "More extreme feminization",
|
||||
type: "extreme_feminization",
|
||||
preview: "Push your sissy transformation further",
|
||||
nextStep: "extreme_feminization_path"
|
||||
text: "Adjust my inventory answers",
|
||||
nextStep: "questionnaire"
|
||||
}
|
||||
]
|
||||
},
|
||||
maid_progression: {
|
||||
type: 'choice',
|
||||
mood: 'maid_humiliation',
|
||||
story: "The photographer laughs at the photos of you in the maid outfit. 'Such a pathetic little servant! You look so degraded and humiliated. But maybe we should explore other forms of humiliation too...'",
|
||||
choices: [
|
||||
{
|
||||
text: "Take maid humiliation photos now",
|
||||
type: "photography",
|
||||
preview: "Document your maid degradation",
|
||||
nextStep: "maid_photo_session"
|
||||
},
|
||||
{
|
||||
text: "Force you to clean while dressed as maid",
|
||||
type: "cleaning",
|
||||
preview: "Humiliating cleaning tasks",
|
||||
nextStep: "cleaning_path"
|
||||
}
|
||||
]
|
||||
},
|
||||
schoolgirl_progression: {
|
||||
type: 'choice',
|
||||
mood: 'schoolgirl_shame',
|
||||
story: "The photographer reviews the slutty schoolgirl photos. 'Look how inappropriate you look! Such a naughty little schoolgirl slut. But I think we can make you even more shameful...'",
|
||||
choices: [
|
||||
{
|
||||
text: "Take slutty schoolgirl photos now",
|
||||
type: "photography",
|
||||
preview: "Capture your schoolgirl shame",
|
||||
nextStep: "schoolgirl_photo_session"
|
||||
},
|
||||
{
|
||||
text: "Pose in even more inappropriate positions",
|
||||
type: "inappropriate",
|
||||
preview: "More degrading schoolgirl poses",
|
||||
nextStep: "inappropriate_path"
|
||||
}
|
||||
]
|
||||
},
|
||||
bimbo_progression: {
|
||||
type: 'choice',
|
||||
mood: 'bimbo_degradation',
|
||||
story: "The photographer can't stop laughing at how ridiculous you look as a bimbo. 'You're the perfect brainless slut! So dumb and desperate. But maybe we can combine this with other humiliating styles...'",
|
||||
choices: [
|
||||
{
|
||||
text: "Take bimbo humiliation photos now",
|
||||
type: "photography",
|
||||
preview: "Capture your bimbo transformation",
|
||||
nextStep: "bimbo_photo_session"
|
||||
},
|
||||
{
|
||||
text: "Force you to act like a brainless bimbo",
|
||||
type: "bimbo_act",
|
||||
preview: "Humiliating bimbo behavior",
|
||||
nextStep: "bimbo_act_path"
|
||||
}
|
||||
]
|
||||
},
|
||||
feminization_photo_session: {
|
||||
|
||||
// Photo challenges will be dynamically generated based on tier
|
||||
// Tier 1: challenge_1 through challenge_5
|
||||
// Tier 2: challenge_1 through challenge_10
|
||||
// Tier 3: challenge_1 through challenge_15
|
||||
// Tier 4: challenge_1 through challenge_20
|
||||
// Tier 5: challenge_1 through challenge_25
|
||||
|
||||
// These will be created dynamically by the system
|
||||
// Each challenge follows this structure:
|
||||
/*
|
||||
challenge_X: {
|
||||
type: 'photo-verification',
|
||||
mood: 'progressive',
|
||||
story: "Generated based on tier, items, and progression",
|
||||
photoRequirements: {
|
||||
items: ['panties', 'bra'], // Required items
|
||||
pose: 'category_name', // Pose category
|
||||
edging: false, // Whether to edge
|
||||
count: 3 // Number of photos
|
||||
},
|
||||
nextStep: "challenge_X+1" or "tier_X_ending"
|
||||
}
|
||||
*/
|
||||
|
||||
// Tier 1 ending (5 photos)
|
||||
tier_1_ending: {
|
||||
type: 'ending',
|
||||
mood: 'sissy_documented',
|
||||
story: "The photographer reviews the humiliating sissy photos. 'Perfect shots! Every photo captures your feminization and humiliation beautifully. You're documented as a complete sissy now.'",
|
||||
endingText: "Feminization photography completed. Final state: Arousal HIGH, Control VARIABLE. Your sissy transformation has been documented.",
|
||||
outcome: "documented_sissy"
|
||||
mood: 'completion',
|
||||
endingTitle: "🩲 CERTIFICATE OF BASIC EXPOSURE",
|
||||
endingText: `
|
||||
<div style="background: linear-gradient(135deg, #ffc0cb 0%, #ffffff 100%); border: 4px solid #ff69b4; border-radius: 20px; padding: 40px; max-width: 600px; margin: 20px auto; box-shadow: 0 10px 40px rgba(0,0,0,0.5);">
|
||||
<h1 style="color: #ff1493; text-align: center; font-size: 2.5em; margin-bottom: 30px; text-shadow: 2px 2px 4px rgba(0,0,0,0.3);">🩲 CERTIFICATE OF BASIC EXPOSURE 🩲</h1>
|
||||
|
||||
<div style="background: rgba(255,255,255,0.6); border-radius: 10px; padding: 30px; margin: 20px 0;">
|
||||
<p style="color: #d6336c; font-size: 2em; text-align: center; margin: 20px 0; font-weight: bold;">
|
||||
EXPOSED AND<br/>
|
||||
<span style="color: #ff1493; font-size: 1.5em;">DOCUMENTED</span>
|
||||
</p>
|
||||
|
||||
<div style="text-align: left; margin: 30px 0; color: #333;">
|
||||
<h3 style="color: #d6336c;">📸 Photos Taken: 5</h3>
|
||||
<h3 style="color: #d6336c;">👗 Items Used:</h3>
|
||||
<div id="tier-1-items-list" style="margin-left: 20px;"></div>
|
||||
</div>
|
||||
|
||||
<p style="color: #d6336c; font-size: 1.8em; text-align: center; margin: 30px 0; font-weight: bold; border-top: 2px solid #ff69b4; padding-top: 20px;">
|
||||
Final State: EXPOSED BEGINNER
|
||||
</p>
|
||||
|
||||
<p style="color: #666; font-size: 1.1em; text-align: center; font-style: italic;">
|
||||
Your journey has been documented. Consider acquiring more items to unlock higher tiers.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
outcome: "tier_1_complete"
|
||||
},
|
||||
maid_photo_session: {
|
||||
|
||||
// Tier 2 ending (10 photos)
|
||||
tier_2_ending: {
|
||||
type: 'ending',
|
||||
mood: 'maid_documented',
|
||||
story: "The photographer looks through the degrading maid photos. 'Excellent! These photos show what a pathetic little servant you are. Your humiliation is perfectly captured.'",
|
||||
endingText: "Maid humiliation photography completed. Final state: Arousal HIGH, Control VARIABLE. Your maid degradation has been documented.",
|
||||
outcome: "documented_maid"
|
||||
mood: 'completion',
|
||||
endingTitle: "👗 CERTIFICATE OF FEMINIZATION BEGINNER",
|
||||
endingText: `
|
||||
<div style="background: linear-gradient(135deg, #ba68c8 0%, #f48fb1 100%); border: 4px solid #9c27b0; border-radius: 20px; padding: 40px; max-width: 600px; margin: 20px auto; box-shadow: 0 10px 40px rgba(0,0,0,0.5);">
|
||||
<h1 style="color: #ffffff; text-align: center; font-size: 2.5em; margin-bottom: 30px; text-shadow: 2px 2px 4px rgba(0,0,0,0.5);">👗 CERTIFICATE OF FEMINIZATION BEGINNER 👗</h1>
|
||||
|
||||
<div style="background: rgba(255,255,255,0.2); border-radius: 10px; padding: 30px; margin: 20px 0;">
|
||||
<p style="color: #ffffff; font-size: 2em; text-align: center; margin: 20px 0; font-weight: bold;">
|
||||
FEMINIZATION JOURNEY<br/>
|
||||
<span style="font-size: 1.5em;">DOCUMENTED</span>
|
||||
</p>
|
||||
|
||||
<div style="text-align: left; margin: 30px 0; color: #ffffff;">
|
||||
<h3>📸 Photos Taken: 10</h3>
|
||||
<h3>👗 Items Used:</h3>
|
||||
<div id="tier-2-items-list" style="margin-left: 20px;"></div>
|
||||
</div>
|
||||
|
||||
<p style="color: #ffffff; font-size: 1.8em; text-align: center; margin: 30px 0; font-weight: bold; border-top: 2px solid #ffffff; padding-top: 20px;">
|
||||
Final State: FEMINIZATION INITIATED
|
||||
</p>
|
||||
|
||||
<p style="color: #f3e5f5; font-size: 1.1em; text-align: center; font-style: italic;">
|
||||
You've taken your first steps into feminization and it's all documented.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
outcome: "tier_2_complete"
|
||||
},
|
||||
schoolgirl_photo_session: {
|
||||
|
||||
// Tier 3 ending (15 photos)
|
||||
tier_3_ending: {
|
||||
type: 'ending',
|
||||
mood: 'schoolgirl_documented',
|
||||
story: "The photographer reviews the slutty schoolgirl photos. 'Incredible shots! You look so inappropriate and shameful. Every photo captures your schoolgirl slut transformation.'",
|
||||
endingText: "Schoolgirl photography completed. Final state: Arousal HIGH, Control VARIABLE. Your schoolgirl shame has been documented.",
|
||||
outcome: "documented_schoolgirl"
|
||||
mood: 'completion',
|
||||
endingTitle: "🎀 CERTIFICATE OF COMPLETE SISSY TRANSFORMATION",
|
||||
endingText: `
|
||||
<div style="background: linear-gradient(135deg, #ff1493 0%, #ffc0cb 50%, #ff69b4 100%); border: 4px solid #ffd700; border-radius: 20px; padding: 40px; max-width: 600px; margin: 20px auto; box-shadow: 0 10px 40px rgba(0,0,0,0.5);">
|
||||
<h1 style="color: #ffffff; text-align: center; font-size: 2.5em; margin-bottom: 30px; text-shadow: 2px 2px 4px rgba(0,0,0,0.5);">🎀 CERTIFICATE OF COMPLETE SISSY TRANSFORMATION 🎀</h1>
|
||||
|
||||
<div style="background: rgba(255,255,255,0.3); border-radius: 10px; padding: 30px; margin: 20px 0;">
|
||||
<p style="color: #ffffff; font-size: 2em; text-align: center; margin: 20px 0; font-weight: bold;">
|
||||
COMPLETE SISSY<br/>
|
||||
<span style="font-size: 1.5em; color: #ffd700;">TRANSFORMATION ACHIEVED</span>
|
||||
</p>
|
||||
|
||||
<div style="text-align: left; margin: 30px 0; color: #ffffff;">
|
||||
<h3>📸 Photos Taken: 15</h3>
|
||||
<h3>👗 Items Used:</h3>
|
||||
<div id="tier-3-items-list" style="margin-left: 20px;"></div>
|
||||
</div>
|
||||
|
||||
<p style="color: #ffd700; font-size: 1.8em; text-align: center; margin: 30px 0; font-weight: bold; border-top: 2px solid #ffd700; padding-top: 20px;">
|
||||
Final State: FULLY FEMINIZED SISSY
|
||||
</p>
|
||||
|
||||
<p style="color: #ffffff; font-size: 1.1em; text-align: center; font-style: italic;">
|
||||
Your complete transformation into a sissy has been thoroughly documented.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
outcome: "tier_3_complete"
|
||||
},
|
||||
bimbo_photo_session: {
|
||||
|
||||
// Tier 4 ending (20 photos)
|
||||
tier_4_ending: {
|
||||
type: 'ending',
|
||||
mood: 'bimbo_documented',
|
||||
story: "The photographer can't stop laughing at the bimbo photos. 'These are perfect! You look like such a brainless slut. Every photo captures your complete bimbo transformation.'",
|
||||
endingText: "Bimbo photography completed. Final state: Arousal HIGH, Control VARIABLE. Your bimbo transformation has been documented.",
|
||||
outcome: "documented_bimbo"
|
||||
mood: 'completion',
|
||||
endingTitle: "🔞 CERTIFICATE OF TOY INTEGRATION MASTER",
|
||||
endingText: `
|
||||
<div style="background: linear-gradient(135deg, #b71c1c 0%, #000000 100%); border: 4px solid #ff5252; border-radius: 20px; padding: 40px; max-width: 600px; margin: 20px auto; box-shadow: 0 10px 40px rgba(0,0,0,0.5);">
|
||||
<h1 style="color: #ff5252; text-align: center; font-size: 2.5em; margin-bottom: 30px; text-shadow: 2px 2px 4px rgba(0,0,0,0.7);">🔞 CERTIFICATE OF TOY INTEGRATION MASTER 🔞</h1>
|
||||
|
||||
<div style="background: rgba(255,255,255,0.1); border-radius: 10px; padding: 30px; margin: 20px 0;">
|
||||
<p style="color: #ff5252; font-size: 2em; text-align: center; margin: 20px 0; font-weight: bold;">
|
||||
DEGRADATION WITH TOYS<br/>
|
||||
<span style="font-size: 1.5em; color: #ffffff;">DOCUMENTED</span>
|
||||
</p>
|
||||
|
||||
<div style="text-align: left; margin: 30px 0; color: #ffffff;">
|
||||
<h3 style="color: #ff5252;">📸 Photos Taken: 20</h3>
|
||||
<h3 style="color: #ff5252;">👗 Items Used:</h3>
|
||||
<div id="tier-4-items-list" style="margin-left: 20px;"></div>
|
||||
</div>
|
||||
|
||||
<p style="color: #ff5252; font-size: 1.8em; text-align: center; margin: 30px 0; font-weight: bold; border-top: 2px solid #ff5252; padding-top: 20px;">
|
||||
Final State: TOY-TRAINED SISSY SLUT
|
||||
</p>
|
||||
|
||||
<p style="color: #ffcdd2; font-size: 1.1em; text-align: center; font-style: italic;">
|
||||
Your feminization combined with toy usage has been completely documented.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
outcome: "tier_4_complete"
|
||||
},
|
||||
extreme_feminization_path: {
|
||||
type: 'action',
|
||||
mood: 'extreme_humiliation',
|
||||
story: "The photographer pulls out even more degrading feminine items - a frilly baby doll dress, excessive makeup, and humiliating accessories. 'Time to push your feminization to the extreme! You're going to become the most pathetic sissy possible. Edge while I document your complete transformation into a humiliated sissy slut.'",
|
||||
actionText: "Edge while undergoing extreme feminization",
|
||||
duration: 300,
|
||||
nextStep: "extreme_feminization_completion"
|
||||
},
|
||||
extreme_feminization_completion: {
|
||||
|
||||
// Tier 5 ending (25 photos)
|
||||
tier_5_ending: {
|
||||
type: 'ending',
|
||||
mood: 'extreme_sissy_documented',
|
||||
story: "The photographer reviews the extreme feminization photos with delight. 'Absolutely perfect! These photos show your complete transformation into the most pathetic sissy possible. Every shot captures your extreme feminization and total humiliation.'",
|
||||
endingText: "Extreme feminization completed. You have been transformed into the ultimate sissy and it's all documented.",
|
||||
outcome: "extreme_sissy_documented"
|
||||
},
|
||||
cleaning_path: {
|
||||
type: 'action',
|
||||
mood: 'maid_degradation',
|
||||
story: "The photographer hands you cleaning supplies. 'Time to put that maid outfit to use! You're going to clean this studio while I photograph your humiliation. Edge while you work like the pathetic little servant you are.'",
|
||||
actionText: "Edge while performing humiliating cleaning tasks",
|
||||
duration: 240,
|
||||
nextStep: "cleaning_completion"
|
||||
},
|
||||
cleaning_completion: {
|
||||
type: 'ending',
|
||||
mood: 'maid_servant_documented',
|
||||
story: "The photographer laughs at the photos of you cleaning while dressed as a maid. 'Perfect! These photos show what a degraded little servant you are. Your humiliation while doing menial labor is beautifully captured.'",
|
||||
endingText: "Maid cleaning session completed. Your degradation as a servant has been documented.",
|
||||
outcome: "maid_servant_documented"
|
||||
},
|
||||
inappropriate_path: {
|
||||
type: 'action',
|
||||
mood: 'schoolgirl_degradation',
|
||||
story: "The photographer directs you into increasingly inappropriate and shameful poses. 'Spread your legs wider! Bend over more! Show me how slutty you can be in that schoolgirl outfit. Edge while I capture every shameful moment.'",
|
||||
actionText: "Edge while posing in degrading schoolgirl positions",
|
||||
duration: 180,
|
||||
nextStep: "inappropriate_completion"
|
||||
},
|
||||
inappropriate_completion: {
|
||||
type: 'ending',
|
||||
mood: 'schoolgirl_slut_documented',
|
||||
story: "The photographer reviews the inappropriate schoolgirl photos with satisfaction. 'These are incredibly shameful! Every photo captures how much of a slutty schoolgirl you are. Your inappropriate behavior is perfectly documented.'",
|
||||
endingText: "Inappropriate schoolgirl session completed. Final state: Arousal HIGH, Control VARIABLE. Your shameful schoolgirl behavior has been documented.",
|
||||
outcome: "schoolgirl_slut_documented"
|
||||
},
|
||||
bimbo_act_path: {
|
||||
type: 'action',
|
||||
mood: 'bimbo_behavior',
|
||||
story: "The photographer forces you to act like a brainless bimbo. 'Talk like a dumb slut! Giggle constantly! Show me how stupid you can be! Edge while acting like the brainless bimbo you've become while I photograph your pathetic behavior.'",
|
||||
actionText: "Edge while acting like a brainless bimbo",
|
||||
duration: 300,
|
||||
nextStep: "bimbo_act_completion"
|
||||
},
|
||||
bimbo_act_completion: {
|
||||
type: 'ending',
|
||||
mood: 'bimbo_behavior_documented',
|
||||
story: "The photographer can't stop laughing at the photos of you acting like a brainless bimbo. 'These are hilarious! Every photo captures how stupid and desperate you are. Your complete bimbo transformation is perfectly documented.'",
|
||||
endingText: "Bimbo behavior session completed. Your brainless bimbo behavior has been documented.",
|
||||
outcome: "bimbo_behavior_documented"
|
||||
},
|
||||
completion: {
|
||||
type: 'ending',
|
||||
mood: 'satisfied',
|
||||
story: "The photographer reviews the humiliating photos with satisfaction. 'Perfect! These photos capture your complete transformation and degradation. You're now documented as the sissy you truly are.'",
|
||||
endingText: "Photo session complete. Your feminization has been permanently documented.",
|
||||
outcome: "feminized_documented"
|
||||
mood: 'completion',
|
||||
endingTitle: "⛓️ CERTIFICATE OF ULTIMATE SISSY SLUT",
|
||||
endingText: `
|
||||
<div style="background: linear-gradient(135deg, #000000 0%, #ff1493 50%, #000000 100%); border: 4px solid #00ff00; border-radius: 20px; padding: 40px; max-width: 600px; margin: 20px auto; box-shadow: 0 10px 40px rgba(255,20,147,0.7);">
|
||||
<h1 style="color: #ff1493; text-align: center; font-size: 2.5em; margin-bottom: 30px; text-shadow: 0 0 10px #ff1493;">⛓️ CERTIFICATE OF ULTIMATE SISSY SLUT ⛓️</h1>
|
||||
|
||||
<div style="background: rgba(255,20,147,0.2); border: 2px solid #ff1493; border-radius: 10px; padding: 30px; margin: 20px 0;">
|
||||
<p style="color: #ff1493; font-size: 2em; text-align: center; margin: 20px 0; font-weight: bold;">
|
||||
ULTIMATE TRANSFORMATION:<br/>
|
||||
<span style="font-size: 1.5em; color: #00ff00;">COMPLETE DEGRADATION</span>
|
||||
</p>
|
||||
|
||||
<div style="text-align: left; margin: 30px 0; color: #ffffff;">
|
||||
<h3 style="color: #ff1493;">📸 Photos Taken: 25</h3>
|
||||
<h3 style="color: #ff1493;">👗 Full Collection Used:</h3>
|
||||
<div id="tier-5-items-list" style="margin-left: 20px;"></div>
|
||||
<p style="margin-top: 20px; color: #00ff00; font-weight: bold;">🏆 EXTREME COLLECTION MASTER</p>
|
||||
</div>
|
||||
|
||||
<p style="color: #00ff00; font-size: 1.8em; text-align: center; margin: 30px 0; font-weight: bold; border-top: 2px solid #ff1493; padding-top: 20px;">
|
||||
Final State: ULTIMATE DEGRADED SISSY
|
||||
</p>
|
||||
|
||||
<p style="color: #ff69b4; font-size: 1.1em; text-align: center; font-style: italic;">
|
||||
Your complete transformation and degradation using every item has been permanently documented. You are the ultimate sissy slut.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
outcome: "tier_5_complete"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -295,18 +299,10 @@ const dressUpGameData = {
|
|||
|
||||
// Configuration for dress-up mode
|
||||
config: {
|
||||
photoSettings: {
|
||||
defaultTimer: 30,
|
||||
maxPhotos: 10,
|
||||
qualityRequirements: {
|
||||
lighting: "good",
|
||||
focus: "sharp",
|
||||
composition: "centered"
|
||||
}
|
||||
},
|
||||
themes: ["elegant", "playful", "artistic", "professional", "creative"],
|
||||
mood: "creative",
|
||||
theme: "expression"
|
||||
inventoryBased: true,
|
||||
dynamicProgression: true,
|
||||
mood: "transformation",
|
||||
theme: "personalization"
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,250 @@
|
|||
/**
|
||||
* Inventory-Based Pose Bank
|
||||
* Poses organized by item type and intensity
|
||||
*/
|
||||
|
||||
const inventoryPoseBank = {
|
||||
// Basic poses (no items required)
|
||||
basic: [
|
||||
{
|
||||
name: "Standing Neutral",
|
||||
instruction: "Stand naturally with hands at sides",
|
||||
description: "Basic standing position for documentation"
|
||||
},
|
||||
{
|
||||
name: "Kneeling Submission",
|
||||
instruction: "Kneel with hands on thighs, head slightly bowed",
|
||||
description: "Submissive kneeling position"
|
||||
},
|
||||
{
|
||||
name: "Sitting Display",
|
||||
instruction: "Sit with legs together, hands in lap",
|
||||
description: "Modest sitting position"
|
||||
}
|
||||
],
|
||||
|
||||
// Panty-specific poses
|
||||
panty: [
|
||||
{
|
||||
name: "Panty Display",
|
||||
instruction: "Standing, pull waistband to show panties clearly",
|
||||
description: "Display your panties for the camera"
|
||||
},
|
||||
{
|
||||
name: "Panty Peek",
|
||||
instruction: "Bent over, looking back at camera to show panties",
|
||||
description: "Bend to reveal your panties"
|
||||
},
|
||||
{
|
||||
name: "Panties Down",
|
||||
instruction: "Pull panties down to mid-thigh, exposed",
|
||||
description: "Lower your panties to expose yourself"
|
||||
},
|
||||
{
|
||||
name: "Panty Adjustment",
|
||||
instruction: "Hands adjusting panties, showing waistband",
|
||||
description: "Adjust your panties for the camera"
|
||||
}
|
||||
],
|
||||
|
||||
// Bra-specific poses
|
||||
bra: [
|
||||
{
|
||||
name: "Bra Display",
|
||||
instruction: "Arms behind back, chest forward to display bra",
|
||||
description: "Show off your bra"
|
||||
},
|
||||
{
|
||||
name: "Bra Adjustment",
|
||||
instruction: "Hands adjusting bra straps or cups",
|
||||
description: "Adjust your bra for proper fit"
|
||||
},
|
||||
{
|
||||
name: "Strap Show",
|
||||
instruction: "Pull strap to show bra clearly",
|
||||
description: "Display your bra strap"
|
||||
}
|
||||
],
|
||||
|
||||
// Dress/Skirt poses
|
||||
dress: [
|
||||
{
|
||||
name: "Twirl Display",
|
||||
instruction: "Spin in circle, dress/skirt fanning out",
|
||||
description: "Twirl to show off your outfit"
|
||||
},
|
||||
{
|
||||
name: "Dress Lift",
|
||||
instruction: "Lift hem to show what's underneath",
|
||||
description: "Lift your dress to reveal beneath"
|
||||
},
|
||||
{
|
||||
name: "Sitting Ladylike",
|
||||
instruction: "Sit with legs crossed, smoothing dress",
|
||||
description: "Sit properly in your feminine outfit"
|
||||
},
|
||||
{
|
||||
name: "Curtsy",
|
||||
instruction: "Hold dress edges, cross leg behind, bow head",
|
||||
description: "Perform a proper feminine curtsy"
|
||||
},
|
||||
{
|
||||
name: "Dress Adjust",
|
||||
instruction: "Standing, adjust dress while posing",
|
||||
description: "Adjust your dress demurely"
|
||||
}
|
||||
],
|
||||
|
||||
// Heel-specific poses
|
||||
heel: [
|
||||
{
|
||||
name: "Heel Display Standing",
|
||||
instruction: "Standing with one foot forward to show heels",
|
||||
description: "Display your heels while standing"
|
||||
},
|
||||
{
|
||||
name: "Walking Pose",
|
||||
instruction: "Mid-step pose showing heel and posture",
|
||||
description: "Pose as if walking in heels"
|
||||
},
|
||||
{
|
||||
name: "Kneeling in Heels",
|
||||
instruction: "Kneel while keeping heels on",
|
||||
description: "Kneel without removing your heels"
|
||||
},
|
||||
{
|
||||
name: "Bent Over in Heels",
|
||||
instruction: "Bend at waist while wearing heels",
|
||||
description: "Bend over while in heels"
|
||||
}
|
||||
],
|
||||
|
||||
// Toy poses
|
||||
toy: [
|
||||
{
|
||||
name: "Dildo to Mouth",
|
||||
instruction: "Hold dildo near or touching mouth",
|
||||
description: "Display toy near your mouth"
|
||||
},
|
||||
{
|
||||
name: "Plug Display",
|
||||
instruction: "Bent over to show inserted plug",
|
||||
description: "Reveal your inserted plug"
|
||||
},
|
||||
{
|
||||
name: "Chastity Show",
|
||||
instruction: "Standing or sitting, display locked cage/belt",
|
||||
description: "Show your chastity device"
|
||||
},
|
||||
{
|
||||
name: "Restrained Position",
|
||||
instruction: "Pose with wrists/ankles in restraints",
|
||||
description: "Display yourself in restraints"
|
||||
},
|
||||
{
|
||||
name: "Gagged Display",
|
||||
instruction: "Head up, show gag clearly",
|
||||
description: "Display your gag"
|
||||
},
|
||||
{
|
||||
name: "Clamped Nipples",
|
||||
instruction: "Chest forward to display nipple clamps",
|
||||
description: "Show your clamped nipples"
|
||||
},
|
||||
{
|
||||
name: "Toy Insertion Pose",
|
||||
instruction: "Positioned to show toy insertion",
|
||||
description: "Pose showing toy being used"
|
||||
}
|
||||
],
|
||||
|
||||
// Edging poses
|
||||
edging: [
|
||||
{
|
||||
name: "Standing Edge",
|
||||
instruction: "Standing while edging yourself",
|
||||
description: "Edge while standing upright"
|
||||
},
|
||||
{
|
||||
name: "Kneeling Edge",
|
||||
instruction: "Kneeling while edging, maintain posture",
|
||||
description: "Edge while in kneeling position"
|
||||
},
|
||||
{
|
||||
name: "Bent Over Edge",
|
||||
instruction: "Bent over while edging yourself",
|
||||
description: "Edge while bent at the waist"
|
||||
},
|
||||
{
|
||||
name: "Spread Eagle Edge",
|
||||
instruction: "Lying back, legs spread, edging",
|
||||
description: "Edge while fully spread"
|
||||
},
|
||||
{
|
||||
name: "Squatting Edge",
|
||||
instruction: "Deep squat while edging, legs wide",
|
||||
description: "Edge in degrading squat position"
|
||||
},
|
||||
{
|
||||
name: "Mirror Edge",
|
||||
instruction: "Edge while watching yourself in mirror",
|
||||
description: "Edge while seeing your reflection"
|
||||
},
|
||||
{
|
||||
name: "Thigh Squeeze Edge",
|
||||
instruction: "Sitting or lying, squeeze thighs to edge",
|
||||
description: "Edge by squeezing thighs together"
|
||||
},
|
||||
{
|
||||
name: "On Back Edge",
|
||||
instruction: "Lying on back with legs spread, edge yourself",
|
||||
description: "Edge while on your back"
|
||||
},
|
||||
{
|
||||
name: "Dressed Edge",
|
||||
instruction: "Edge while fully dressed in feminine outfit",
|
||||
description: "Edge while wearing your outfit"
|
||||
},
|
||||
{
|
||||
name: "Exposed Edge",
|
||||
instruction: "Standing against wall, spread, edge while exposed",
|
||||
description: "Edge while fully exposed"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
/**
|
||||
* Select pose based on category
|
||||
*/
|
||||
function selectPoseForCategory(category, usedPoses = new Set()) {
|
||||
const poses = inventoryPoseBank[category] || inventoryPoseBank.basic;
|
||||
|
||||
// Filter out used poses
|
||||
const availablePoses = poses.filter(pose => !usedPoses.has(pose.name));
|
||||
|
||||
// If all poses used, reset
|
||||
if (availablePoses.length === 0) {
|
||||
console.log(`🔄 All ${category} poses used - resetting`);
|
||||
return selectPoseForCategory(category, new Set());
|
||||
}
|
||||
|
||||
// Select random pose
|
||||
const randomIndex = Math.floor(Math.random() * availablePoses.length);
|
||||
const selectedPose = availablePoses[randomIndex];
|
||||
|
||||
console.log(`📸 Selected ${category} pose: ${selectedPose.name}`);
|
||||
|
||||
return selectedPose;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all poses for a category
|
||||
*/
|
||||
function getPosesByCategory(category) {
|
||||
return inventoryPoseBank[category] || inventoryPoseBank.basic;
|
||||
}
|
||||
|
||||
// Make available globally
|
||||
window.inventoryPoseBank = inventoryPoseBank;
|
||||
window.selectPoseForCategory = selectPoseForCategory;
|
||||
window.getPosesByCategory = getPosesByCategory;
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,590 @@
|
|||
/**
|
||||
* Academy UI Manager
|
||||
* Handles level selection, checkpoint modals, and integration with campaign/preference/library systems
|
||||
*/
|
||||
|
||||
class AcademyUI {
|
||||
constructor() {
|
||||
this.currentModal = null;
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
console.log('🎓 Academy UI Manager initialized');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show level selection screen
|
||||
* Displays all 30 levels with locked/unlocked states
|
||||
*/
|
||||
showLevelSelect() {
|
||||
const unlockedLevels = window.campaignManager.getUnlockedLevels();
|
||||
const completedLevels = window.gameData.academyProgress.completedLevels;
|
||||
const currentLevel = window.gameData.academyProgress.currentLevel;
|
||||
|
||||
let html = `
|
||||
<div class="level-select-screen">
|
||||
<div class="level-select-header">
|
||||
<h2>🎓 The Academy - Campaign</h2>
|
||||
<p class="progress-subtitle">Select your next level</p>
|
||||
<div class="campaign-progress">
|
||||
<div class="progress-bar-container">
|
||||
<div class="progress-bar-fill" style="width: ${(completedLevels.length / 30 * 100).toFixed(1)}%"></div>
|
||||
</div>
|
||||
<p class="progress-text">${completedLevels.length}/30 Levels Complete</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="level-grid">
|
||||
`;
|
||||
|
||||
// Generate 30 level buttons
|
||||
for (let i = 1; i <= 30; i++) {
|
||||
const isUnlocked = unlockedLevels.includes(i);
|
||||
const isCompleted = completedLevels.includes(i);
|
||||
const isCurrent = i === currentLevel;
|
||||
const isCheckpoint = window.campaignManager.isCheckpointLevel(i);
|
||||
const arc = window.campaignManager.getArcForLevel(i);
|
||||
|
||||
const statusClass = isCompleted ? 'completed' :
|
||||
isUnlocked ? 'unlocked' : 'locked';
|
||||
|
||||
const icon = isCompleted ? '✓' :
|
||||
isCheckpoint ? '⚡' :
|
||||
isUnlocked ? '▶' : '🔒';
|
||||
|
||||
html += `
|
||||
<button
|
||||
class="level-button ${statusClass} ${isCurrent ? 'current' : ''}"
|
||||
onclick="window.academyUI.selectLevel(${i})"
|
||||
${!isUnlocked ? 'disabled' : ''}
|
||||
title="${arc} Arc${isCheckpoint ? ' - Checkpoint' : ''}${isCompleted ? ' - Completed' : ''}">
|
||||
<div class="level-number">${i}</div>
|
||||
<div class="level-icon">${icon}</div>
|
||||
${isCheckpoint ? '<div class="checkpoint-badge">⚡</div>' : ''}
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
html += `
|
||||
</div>
|
||||
|
||||
<div class="level-info-panel" id="level-info-panel">
|
||||
<p class="info-hint">Select a level to view details</p>
|
||||
</div>
|
||||
|
||||
<div class="level-select-footer">
|
||||
<button onclick="window.location.href='index.html'" class="btn-back">🏠 Back to Menu</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Show in the academy setup container
|
||||
const setupContainer = document.getElementById('academy-setup');
|
||||
if (setupContainer) {
|
||||
setupContainer.innerHTML = html;
|
||||
setupContainer.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a level and show its details
|
||||
* @param {number} levelNum - Level to select
|
||||
*/
|
||||
selectLevel(levelNum) {
|
||||
const arc = window.campaignManager.getArcForLevel(levelNum);
|
||||
const isCheckpoint = window.campaignManager.isCheckpointLevel(levelNum);
|
||||
const isCompleted = window.campaignManager.isLevelCompleted(levelNum);
|
||||
|
||||
// Show level details in info panel
|
||||
const infoPanelContent = this.getLevelInfoHTML(levelNum, arc, isCheckpoint, isCompleted);
|
||||
const infoPanel = document.getElementById('level-info-panel');
|
||||
if (infoPanel) {
|
||||
infoPanel.innerHTML = infoPanelContent;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get level info HTML
|
||||
* @param {number} levelNum - Level number
|
||||
* @param {string} arc - Arc name
|
||||
* @param {boolean} isCheckpoint - Is checkpoint level
|
||||
* @param {boolean} isCompleted - Is completed
|
||||
* @returns {string} HTML content
|
||||
*/
|
||||
getLevelInfoHTML(levelNum, arc, isCheckpoint, isCompleted) {
|
||||
return `
|
||||
<div class="level-details">
|
||||
<h3>Level ${levelNum}: ${this.getLevelTitle(levelNum)}</h3>
|
||||
<div class="level-meta">
|
||||
<span class="arc-badge">${arc} Arc</span>
|
||||
${isCheckpoint ? '<span class="checkpoint-badge-large">⚡ Checkpoint</span>' : ''}
|
||||
${isCompleted ? '<span class="completed-badge">✓ Completed</span>' : ''}
|
||||
</div>
|
||||
<p class="level-description">${this.getLevelDescription(levelNum)}</p>
|
||||
${isCheckpoint ? '<p class="checkpoint-notice">💡 You will be asked to update your preferences before starting this level.</p>' : ''}
|
||||
<button onclick="window.academyUI.startLevel(${levelNum})" class="btn-start-level">
|
||||
${isCompleted ? '🔄 Replay Level' : '▶️ Start Level'}
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get level title based on number
|
||||
* @param {number} levelNum - Level number
|
||||
* @returns {string} Level title
|
||||
*/
|
||||
getLevelTitle(levelNum) {
|
||||
const titles = {
|
||||
1: "First Edge",
|
||||
5: "Foundation Complete",
|
||||
10: "Feature Discovery Complete",
|
||||
15: "Deepening",
|
||||
20: "Mastery",
|
||||
25: "Final Challenge",
|
||||
30: "Graduation"
|
||||
};
|
||||
return titles[levelNum] || `Training Session ${levelNum}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get level description
|
||||
* @param {number} levelNum - Level number
|
||||
* @returns {string} Level description
|
||||
*/
|
||||
getLevelDescription(levelNum) {
|
||||
if (levelNum === 1) return "Welcome to The Academy. Begin your journey with basic edging training.";
|
||||
if (levelNum === 5) return "Complete the Foundation arc. Test your progress with a checkpoint session.";
|
||||
if (levelNum === 10) return "Feature Discovery complete. Time to optimize your preferences.";
|
||||
if (levelNum === 15) return "Halfway through. Deepen your training with advanced techniques.";
|
||||
if (levelNum === 20) return "Enter the Mastery arc. Push your limits further.";
|
||||
if (levelNum === 25) return "Final checkpoint before graduation. Prepare for the ultimate test.";
|
||||
if (levelNum === 30) return "The final test. Graduate from The Academy.";
|
||||
return "Continue your training journey with this session.";
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a level (with checkpoint modal if needed)
|
||||
* @param {number} levelNum - Level to start
|
||||
*/
|
||||
startLevel(levelNum) {
|
||||
console.log(`🎯 Starting Level ${levelNum}...`);
|
||||
|
||||
// Check if this is a checkpoint level
|
||||
if (window.campaignManager.isCheckpointLevel(levelNum)) {
|
||||
this.showCheckpointModal(levelNum);
|
||||
} else {
|
||||
this.beginLevel(levelNum);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show checkpoint preference modal
|
||||
* @param {number} levelNum - Checkpoint level number
|
||||
*/
|
||||
showCheckpointModal(levelNum) {
|
||||
const config = window.preferenceManager.getCheckpointModalConfig(levelNum);
|
||||
|
||||
if (!config) {
|
||||
console.error('No checkpoint config for level', levelNum);
|
||||
this.beginLevel(levelNum);
|
||||
return;
|
||||
}
|
||||
|
||||
const modalHTML = this.buildCheckpointModalHTML(levelNum, config);
|
||||
|
||||
// Create modal overlay
|
||||
const overlay = document.createElement('div');
|
||||
overlay.className = 'checkpoint-modal-overlay';
|
||||
overlay.innerHTML = modalHTML;
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
this.currentModal = overlay;
|
||||
|
||||
// Initialize tabs
|
||||
this.initCheckpointModalTabs(levelNum, config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build checkpoint modal HTML
|
||||
* @param {number} levelNum - Level number
|
||||
* @param {Object} config - Modal configuration
|
||||
* @returns {string} Modal HTML
|
||||
*/
|
||||
buildCheckpointModalHTML(levelNum, config) {
|
||||
return `
|
||||
<div class="checkpoint-modal">
|
||||
<div class="checkpoint-header">
|
||||
<h2>${config.title}</h2>
|
||||
<p>${config.description}</p>
|
||||
</div>
|
||||
|
||||
<div class="checkpoint-tabs">
|
||||
<button class="tab-btn active" data-tab="preferences">🎯 Preferences</button>
|
||||
<button class="tab-btn" data-tab="library">📚 Library</button>
|
||||
</div>
|
||||
|
||||
<div class="checkpoint-content">
|
||||
<div class="tab-panel active" id="preferences-panel">
|
||||
<div id="preferences-editor"></div>
|
||||
</div>
|
||||
<div class="tab-panel" id="library-panel">
|
||||
<div id="library-manager"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="checkpoint-footer">
|
||||
<button onclick="window.academyUI.closeCheckpointModal()" class="btn-secondary">
|
||||
Skip for Now
|
||||
</button>
|
||||
<button onclick="window.academyUI.saveCheckpointAndStart(${levelNum})" class="btn-primary">
|
||||
Save & Start Level
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize checkpoint modal tabs
|
||||
* @param {number} levelNum - Level number
|
||||
* @param {Object} config - Modal configuration
|
||||
*/
|
||||
initCheckpointModalTabs(levelNum, config) {
|
||||
// Tab switching
|
||||
const tabBtns = document.querySelectorAll('.tab-btn');
|
||||
tabBtns.forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const tabName = btn.dataset.tab;
|
||||
|
||||
// Update active states
|
||||
tabBtns.forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
|
||||
document.querySelectorAll('.tab-panel').forEach(panel => {
|
||||
panel.classList.remove('active');
|
||||
});
|
||||
document.getElementById(`${tabName}-panel`).classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
// Populate preferences editor
|
||||
this.populatePreferencesEditor(config.categories);
|
||||
|
||||
// Populate library manager
|
||||
this.populateLibraryManager();
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate preferences editor
|
||||
* @param {Array<string>} categories - Categories to show
|
||||
*/
|
||||
populatePreferencesEditor(categories) {
|
||||
const container = document.getElementById('preferences-editor');
|
||||
if (!container) return;
|
||||
|
||||
let html = '';
|
||||
|
||||
categories.forEach(category => {
|
||||
const prefs = window.preferenceManager.getCategoryPreferences(category);
|
||||
html += `<div class="pref-category">`;
|
||||
html += `<h3>${this.getCategoryDisplayName(category)}</h3>`;
|
||||
html += this.buildCategoryEditor(category, prefs);
|
||||
html += `</div>`;
|
||||
});
|
||||
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get category display name
|
||||
* @param {string} category - Category key
|
||||
* @returns {string} Display name
|
||||
*/
|
||||
getCategoryDisplayName(category) {
|
||||
const names = {
|
||||
contentThemes: '🎭 Content Themes',
|
||||
visualPreferences: '👁️ Visual Preferences',
|
||||
intensity: '⚡ Intensity Levels',
|
||||
captionTone: '💬 Caption Tone',
|
||||
audioPreferences: '🔊 Audio Preferences',
|
||||
sessionDuration: '⏱️ Session Duration',
|
||||
featurePreferences: '🎮 Feature Preferences',
|
||||
boundaries: '🛡️ Boundaries'
|
||||
};
|
||||
return names[category] || category;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build category editor HTML
|
||||
* @param {string} category - Category name
|
||||
* @param {Object} prefs - Preferences object
|
||||
* @returns {string} Editor HTML
|
||||
*/
|
||||
buildCategoryEditor(category, prefs) {
|
||||
if (category === 'intensity') {
|
||||
return this.buildIntensitySliders(prefs);
|
||||
} else if (category === 'sessionDuration') {
|
||||
return this.buildDurationSelector(prefs);
|
||||
} else {
|
||||
return this.buildCheckboxes(category, prefs);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build intensity sliders
|
||||
* @param {Object} prefs - Intensity preferences
|
||||
* @returns {string} HTML
|
||||
*/
|
||||
buildIntensitySliders(prefs) {
|
||||
let html = '';
|
||||
for (const [key, value] of Object.entries(prefs)) {
|
||||
html += `
|
||||
<div class="slider-group">
|
||||
<label>${key.replace(/([A-Z])/g, ' $1').trim()}: <span id="${key}-value">${value}</span></label>
|
||||
<input type="range" min="1" max="5" value="${value}"
|
||||
data-category="intensity" data-key="${key}"
|
||||
oninput="document.getElementById('${key}-value').textContent = this.value">
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build duration selector
|
||||
* @param {Object} prefs - Duration preferences
|
||||
* @returns {string} HTML
|
||||
*/
|
||||
buildDurationSelector(prefs) {
|
||||
const options = ['short', 'medium', 'long', 'marathon'];
|
||||
let html = '<select data-category="sessionDuration" data-key="preferred">';
|
||||
options.forEach(opt => {
|
||||
html += `<option value="${opt}" ${prefs.preferred === opt ? 'selected' : ''}>${opt}</option>`;
|
||||
});
|
||||
html += '</select>';
|
||||
return html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build checkboxes for category
|
||||
* @param {string} category - Category name
|
||||
* @param {Object} prefs - Preferences object
|
||||
* @returns {string} HTML
|
||||
*/
|
||||
buildCheckboxes(category, prefs) {
|
||||
let html = '<div class="checkbox-grid">';
|
||||
for (const [key, value] of Object.entries(prefs)) {
|
||||
if (value !== null) { // Skip undiscovered features
|
||||
html += `
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" ${value ? 'checked' : ''}
|
||||
data-category="${category}" data-key="${key}">
|
||||
${key}
|
||||
</label>
|
||||
`;
|
||||
}
|
||||
}
|
||||
html += '</div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate library manager
|
||||
*/
|
||||
populateLibraryManager() {
|
||||
const container = document.getElementById('library-manager');
|
||||
if (!container) return;
|
||||
|
||||
// Sync with existing library first
|
||||
const syncResult = window.libraryManager.syncWithExistingLibrary();
|
||||
const stats = window.libraryManager.getLibraryStats();
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="library-stats">
|
||||
<h3>📊 Library Statistics</h3>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Total Videos</span>
|
||||
<span class="stat-value">${stats.totalItems}</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Tagged</span>
|
||||
<span class="stat-value">${stats.taggedItems}</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Avg Rating</span>
|
||||
<span class="stat-value">${stats.averageRating}⭐</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Favorites</span>
|
||||
<span class="stat-value">${stats.totalFavorites}</span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="library-hint">
|
||||
💡 Your existing video library (${syncResult.total} videos) is automatically imported.
|
||||
You can add tags and ratings to organize your content.
|
||||
</p>
|
||||
<p class="library-hint">
|
||||
📚 Manage your full library from the main menu Library tab for more detailed controls.
|
||||
</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save checkpoint preferences and start level
|
||||
* @param {number} levelNum - Level to start
|
||||
*/
|
||||
saveCheckpointAndStart(levelNum) {
|
||||
console.log('💾 Saving checkpoint preferences...');
|
||||
|
||||
// Collect all preference changes
|
||||
const updates = {};
|
||||
|
||||
// Get all checkboxes
|
||||
document.querySelectorAll('input[type="checkbox"][data-category]').forEach(checkbox => {
|
||||
const category = checkbox.dataset.category;
|
||||
const key = checkbox.dataset.key;
|
||||
|
||||
if (!updates[category]) updates[category] = {};
|
||||
updates[category][key] = checkbox.checked;
|
||||
});
|
||||
|
||||
// Get all sliders
|
||||
document.querySelectorAll('input[type="range"][data-category]').forEach(slider => {
|
||||
const category = slider.dataset.category;
|
||||
const key = slider.dataset.key;
|
||||
|
||||
if (!updates[category]) updates[category] = {};
|
||||
updates[category][key] = parseInt(slider.value);
|
||||
});
|
||||
|
||||
// Get all selects
|
||||
document.querySelectorAll('select[data-category]').forEach(select => {
|
||||
const category = select.dataset.category;
|
||||
const key = select.dataset.key;
|
||||
|
||||
if (!updates[category]) updates[category] = {};
|
||||
updates[category][key] = select.value;
|
||||
});
|
||||
|
||||
// Save to preferenceManager
|
||||
let saved = true;
|
||||
if (Object.keys(updates).length > 0) {
|
||||
saved = window.preferenceManager.updateMultipleCategories(updates, levelNum);
|
||||
if (saved) {
|
||||
console.log('✅ Preferences updated:', updates);
|
||||
} else {
|
||||
console.error('❌ Failed to save preferences');
|
||||
alert('⚠️ Unable to save preferences. Storage may be full.\n\nPlease restart the app and try again.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Close modal and begin level
|
||||
this.closeCheckpointModal();
|
||||
this.beginLevel(levelNum);
|
||||
}
|
||||
|
||||
/**
|
||||
* Close checkpoint modal
|
||||
*/
|
||||
closeCheckpointModal() {
|
||||
if (this.currentModal) {
|
||||
this.currentModal.remove();
|
||||
this.currentModal = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Begin level (after checkpoint or directly)
|
||||
* @param {number} levelNum - Level to start
|
||||
*/
|
||||
beginLevel(levelNum) {
|
||||
console.log(`🚀 Beginning Level ${levelNum}...`);
|
||||
|
||||
// Use campaignManager to start level
|
||||
const levelConfig = window.campaignManager.startLevel(levelNum);
|
||||
|
||||
if (!levelConfig) {
|
||||
alert('Unable to start this level. Please check requirements.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Hide level select screen
|
||||
const setupContainer = document.getElementById('academy-setup');
|
||||
if (setupContainer) {
|
||||
setupContainer.style.display = 'none';
|
||||
}
|
||||
|
||||
// Start the actual training session
|
||||
// This would integrate with existing training-academy.html session logic
|
||||
this.startTrainingSession(levelConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start training session with level config
|
||||
* @param {Object} levelConfig - Level configuration
|
||||
*/
|
||||
startTrainingSession(levelConfig) {
|
||||
console.log('🎓 Starting training session with config:', levelConfig);
|
||||
|
||||
// Get detailed level configuration from academyLevelData
|
||||
const detailedConfig = window.academyLevelData.getLevelConfig(levelConfig.levelNumber);
|
||||
if (!detailedConfig) {
|
||||
console.error('⚠️ No level data found for level:', levelConfig.levelNumber);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('📊 Detailed level config:', detailedConfig);
|
||||
|
||||
// Get content filter based on user preferences
|
||||
const preferenceFilter = window.preferenceManager.getContentFilter();
|
||||
|
||||
// Sync with existing library to ensure we have latest videos
|
||||
window.libraryManager.syncWithExistingLibrary();
|
||||
|
||||
// Get media requirements for this level
|
||||
const mediaRequirements = window.academyLevelData.getMediaForLevel(
|
||||
levelConfig.levelNumber,
|
||||
window.preferenceManager.getPreferences()
|
||||
);
|
||||
|
||||
console.log('🎬 Media requirements:', mediaRequirements);
|
||||
|
||||
// Get filtered media from library
|
||||
const mediaItems = window.libraryManager.getMediaForPreferences(
|
||||
mediaRequirements.videoFilter,
|
||||
mediaRequirements.videoCount
|
||||
);
|
||||
|
||||
console.log('🎬 Loaded media for session:', mediaItems.length, 'items');
|
||||
console.log('🎯 Preference filter:', preferenceFilter);
|
||||
console.log('⏱️ Session duration:', detailedConfig.session.duration);
|
||||
console.log('📋 Tasks:', detailedConfig.tasks.length);
|
||||
console.log('🎯 Objectives:', detailedConfig.objectives);
|
||||
|
||||
// If we have media items, log their details
|
||||
if (mediaItems.length > 0) {
|
||||
console.log('📹 Sample media item:', mediaItems[0]);
|
||||
} else {
|
||||
console.warn('⚠️ No media items matched preferences. Will use all available videos.');
|
||||
// Fallback to all videos if no matches
|
||||
const allVideos = window.libraryManager.getAllVideosWithMetadata();
|
||||
console.log('📹 Using all videos as fallback:', allVideos.length);
|
||||
}
|
||||
|
||||
// TODO: Integrate with existing startTrainingSession() in training-academy.html
|
||||
// For now, just call the existing function if available
|
||||
if (typeof window.startTrainingSession === 'function') {
|
||||
window.startTrainingSession();
|
||||
} else {
|
||||
console.error('⚠️ startTrainingSession() not found in window scope');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create global instance
|
||||
window.academyUI = new AcademyUI();
|
||||
|
|
@ -0,0 +1,290 @@
|
|||
/**
|
||||
* Campaign Manager
|
||||
* Handles The Academy's 30-level campaign progression system
|
||||
*/
|
||||
|
||||
class CampaignManager {
|
||||
constructor() {
|
||||
this.initializeProgress();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize academy progress if it doesn't exist
|
||||
*/
|
||||
initializeProgress() {
|
||||
// gameData is available globally via window.gameData
|
||||
if (!window.gameData.academyProgress) {
|
||||
window.gameData.academyProgress = {
|
||||
version: 1,
|
||||
currentLevel: 1,
|
||||
highestUnlockedLevel: 1,
|
||||
completedLevels: [],
|
||||
currentArc: 'Foundation',
|
||||
failedAttempts: {},
|
||||
totalSessionTime: 0,
|
||||
lastPlayedLevel: null,
|
||||
lastPlayedDate: null,
|
||||
graduationCompleted: false,
|
||||
freeplayUnlocked: false,
|
||||
ascendedModeUnlocked: false,
|
||||
selectedPath: null,
|
||||
featuresUnlocked: []
|
||||
};
|
||||
this.saveProgress();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save progress to localStorage
|
||||
*/
|
||||
saveProgress() {
|
||||
localStorage.setItem('webGame-data', JSON.stringify(window.gameData));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of unlocked levels
|
||||
* @returns {Array<number>} Array of unlocked level numbers
|
||||
*/
|
||||
getUnlockedLevels() {
|
||||
const unlocked = [];
|
||||
const highest = window.gameData.academyProgress.highestUnlockedLevel;
|
||||
for (let i = 1; i <= highest; i++) {
|
||||
unlocked.push(i);
|
||||
}
|
||||
return unlocked;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a level is unlocked
|
||||
* @param {number} levelNum - Level number to check
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isLevelUnlocked(levelNum) {
|
||||
return levelNum <= window.gameData.academyProgress.highestUnlockedLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a level is completed
|
||||
* @param {number} levelNum - Level number to check
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isLevelCompleted(levelNum) {
|
||||
return window.gameData.academyProgress.completedLevels.includes(levelNum);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a level
|
||||
* @param {number} levelNum - Level number to start
|
||||
* @returns {Object|null} Level config or null if invalid
|
||||
*/
|
||||
startLevel(levelNum) {
|
||||
// Validate level is unlocked
|
||||
if (!this.isLevelUnlocked(levelNum)) {
|
||||
console.error(`Level ${levelNum} is locked. Complete previous levels first.`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Update current level
|
||||
window.gameData.academyProgress.currentLevel = levelNum;
|
||||
window.gameData.academyProgress.lastPlayedLevel = levelNum;
|
||||
window.gameData.academyProgress.lastPlayedDate = new Date().toISOString();
|
||||
|
||||
// Update current arc
|
||||
window.gameData.academyProgress.currentArc = this.getArcForLevel(levelNum);
|
||||
|
||||
this.saveProgress();
|
||||
|
||||
console.log(`Started Level ${levelNum} (${gameData.academyProgress.currentArc} Arc)`);
|
||||
|
||||
return {
|
||||
levelNum,
|
||||
arc: window.gameData.academyProgress.currentArc,
|
||||
isCheckpoint: this.isCheckpointLevel(levelNum)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete a level
|
||||
* @param {number} levelNum - Level number completed
|
||||
* @param {Object} sessionData - Data from the session
|
||||
* @returns {Object} Completion results
|
||||
*/
|
||||
completeLevel(levelNum, sessionData = {}) {
|
||||
const progress = window.gameData.academyProgress;
|
||||
|
||||
// Add to completed levels if not already there
|
||||
if (!progress.completedLevels.includes(levelNum)) {
|
||||
progress.completedLevels.push(levelNum);
|
||||
}
|
||||
|
||||
// Unlock next level
|
||||
const nextLevel = levelNum + 1;
|
||||
let nextLevelUnlocked = false;
|
||||
if (nextLevel <= 30 && nextLevel > progress.highestUnlockedLevel) {
|
||||
progress.highestUnlockedLevel = nextLevel;
|
||||
nextLevelUnlocked = true;
|
||||
console.log(`🔓 Level ${nextLevel} unlocked!`);
|
||||
}
|
||||
|
||||
// Update session time
|
||||
if (sessionData.duration) {
|
||||
progress.totalSessionTime += sessionData.duration;
|
||||
}
|
||||
|
||||
// Check for arc completion
|
||||
const arcComplete = this.checkArcCompletion(levelNum);
|
||||
|
||||
// Reset consecutive failures on success
|
||||
progress.consecutiveLevelsWithoutFailure =
|
||||
(progress.consecutiveLevelsWithoutFailure || 0) + 1;
|
||||
|
||||
this.saveProgress();
|
||||
|
||||
console.log(`✅ Level ${levelNum} completed!`);
|
||||
|
||||
return {
|
||||
levelCompleted: levelNum,
|
||||
nextLevelUnlocked,
|
||||
nextLevel: nextLevelUnlocked ? nextLevel : null,
|
||||
arcComplete,
|
||||
completedArc: arcComplete ? this.getArcForLevel(levelNum) : null
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fail a level
|
||||
* @param {number} levelNum - Level that was failed
|
||||
* @param {string} reason - Failure reason ('cumming', 'abandoned', 'feature-closed')
|
||||
*/
|
||||
failLevel(levelNum, reason) {
|
||||
const progress = window.gameData.academyProgress;
|
||||
|
||||
// Increment failed attempts for this level
|
||||
if (!progress.failedAttempts[levelNum]) {
|
||||
progress.failedAttempts[levelNum] = 0;
|
||||
}
|
||||
progress.failedAttempts[levelNum]++;
|
||||
|
||||
// Track total failures
|
||||
if (!progress.totalFailedAttempts) {
|
||||
progress.totalFailedAttempts = 0;
|
||||
}
|
||||
progress.totalFailedAttempts++;
|
||||
|
||||
// Track failure by reason
|
||||
if (!progress.failuresByReason) {
|
||||
progress.failuresByReason = {
|
||||
cumming: 0,
|
||||
abandoned: 0,
|
||||
featureClosed: 0
|
||||
};
|
||||
}
|
||||
if (progress.failuresByReason[reason] !== undefined) {
|
||||
progress.failuresByReason[reason]++;
|
||||
}
|
||||
|
||||
// Reset consecutive success streak
|
||||
progress.consecutiveLevelsWithoutFailure = 0;
|
||||
|
||||
this.saveProgress();
|
||||
|
||||
console.log(`❌ Level ${levelNum} failed (${reason}). Attempts: ${progress.failedAttempts[levelNum]}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the arc name for a level
|
||||
* @param {number} levelNum - Level number
|
||||
* @returns {string} Arc name
|
||||
*/
|
||||
getArcForLevel(levelNum) {
|
||||
if (levelNum >= 1 && levelNum <= 5) return 'Foundation';
|
||||
if (levelNum >= 6 && levelNum <= 10) return 'Feature Discovery';
|
||||
if (levelNum >= 11 && levelNum <= 15) return 'Mind & Body';
|
||||
if (levelNum >= 16 && levelNum <= 20) return 'Advanced Training';
|
||||
if (levelNum >= 21 && levelNum <= 25) return 'Path Specialization';
|
||||
if (levelNum >= 26 && levelNum <= 30) return 'Ultimate Mastery';
|
||||
return 'Unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current arc
|
||||
* @returns {string} Current arc name
|
||||
*/
|
||||
getCurrentArc() {
|
||||
return window.gameData.academyProgress.currentArc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if level is a checkpoint (1, 5, 10, 15, 20, 25)
|
||||
* @param {number} levelNum - Level number
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isCheckpointLevel(levelNum) {
|
||||
return [1, 5, 10, 15, 20, 25].includes(levelNum);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an arc is complete
|
||||
* @param {number} levelNum - Level just completed
|
||||
* @returns {boolean}
|
||||
*/
|
||||
checkArcCompletion(levelNum) {
|
||||
const arcEndLevels = [5, 10, 15, 20, 25, 30];
|
||||
return arcEndLevels.includes(levelNum);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get progress statistics
|
||||
* @returns {Object} Progress stats
|
||||
*/
|
||||
getProgressStats() {
|
||||
const progress = window.gameData.academyProgress;
|
||||
return {
|
||||
currentLevel: progress.currentLevel,
|
||||
highestUnlockedLevel: progress.highestUnlockedLevel,
|
||||
completedLevels: progress.completedLevels.length,
|
||||
totalLevels: 30,
|
||||
percentComplete: (progress.completedLevels.length / 30) * 100,
|
||||
currentArc: progress.currentArc,
|
||||
totalSessionTime: progress.totalSessionTime,
|
||||
failedAttempts: Object.values(progress.failedAttempts).reduce((a, b) => a + b, 0),
|
||||
consecutiveSuccesses: progress.consecutiveLevelsWithoutFailure || 0
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset progress (for testing or fresh start)
|
||||
*/
|
||||
resetProgress() {
|
||||
window.gameData.academyProgress = {
|
||||
version: 1,
|
||||
currentLevel: 1,
|
||||
highestUnlockedLevel: 1,
|
||||
completedLevels: [],
|
||||
currentArc: 'Foundation',
|
||||
failedAttempts: {},
|
||||
totalSessionTime: 0,
|
||||
lastPlayedLevel: null,
|
||||
lastPlayedDate: null,
|
||||
graduationCompleted: false,
|
||||
freeplayUnlocked: false,
|
||||
ascendedModeUnlocked: false,
|
||||
selectedPath: null,
|
||||
featuresUnlocked: [],
|
||||
totalFailedAttempts: 0,
|
||||
failuresByReason: {
|
||||
cumming: 0,
|
||||
abandoned: 0,
|
||||
featureClosed: 0
|
||||
},
|
||||
consecutiveLevelsWithoutFailure: 0
|
||||
};
|
||||
this.saveProgress();
|
||||
console.log('🔄 Academy progress reset');
|
||||
}
|
||||
}
|
||||
|
||||
// Create singleton instance and expose globally
|
||||
window.campaignManager = new CampaignManager();
|
||||
|
||||
console.log('🎓 Campaign Manager initialized');
|
||||
|
|
@ -0,0 +1,710 @@
|
|||
/**
|
||||
* Library Management System for The Academy
|
||||
* Handles media tagging, organization, quality ratings, and preference-based filtering
|
||||
*/
|
||||
|
||||
class LibraryManager {
|
||||
constructor() {
|
||||
this.initializeLibrary();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize library structure in gameData if it doesn't exist
|
||||
* Integrates with existing desktopFileManager video/photo system
|
||||
*/
|
||||
initializeLibrary() {
|
||||
if (!window.gameData.academyLibrary) {
|
||||
window.gameData.academyLibrary = {
|
||||
version: 1,
|
||||
lastUpdated: new Date().toISOString(),
|
||||
|
||||
// Media items with tags and metadata
|
||||
// Keys are file paths from desktopFileManager.getAllVideos()
|
||||
media: {
|
||||
// Key: file path from existing library, Value: metadata object
|
||||
// Example: "C:/Users/drew/Videos/video1.mp4": { tags: [...], rating: 5, ... }
|
||||
},
|
||||
|
||||
// Tag definitions and usage counts
|
||||
tags: {
|
||||
// Content theme tags
|
||||
contentThemes: ['dominance', 'submission', 'humiliation', 'worship', 'edging', 'denial',
|
||||
'cei', 'sissy', 'bbc', 'feet', 'femdom', 'maledom', 'lesbian', 'gay',
|
||||
'trans', 'hentai', 'pov', 'joi', 'gooning', 'mindbreak'],
|
||||
|
||||
// Visual style tags
|
||||
visualStyles: ['solo', 'couples', 'group', 'amateur', 'professional', 'animated',
|
||||
'real', 'closeup', 'fullbody', 'pov', 'compilation', 'pmv', 'slowmo',
|
||||
'artistic', 'raw'],
|
||||
|
||||
// Intensity tags
|
||||
intensity: ['soft', 'moderate', 'intense', 'extreme', 'hardcore', 'gentle', 'rough'],
|
||||
|
||||
// Audio tags
|
||||
audio: ['music', 'talking', 'moaning', 'silent', 'asmr', 'binaural', 'soundfx',
|
||||
'voiceover', 'ambient'],
|
||||
|
||||
// Quality tags
|
||||
quality: ['lowres', 'sd', 'hd', '720p', '1080p', '4k', 'vr'],
|
||||
|
||||
// Duration tags
|
||||
duration: ['short', 'medium', 'long', 'loop', 'extended'],
|
||||
|
||||
// Actor/performer tags (will be populated dynamically)
|
||||
performers: [],
|
||||
|
||||
// Custom user tags
|
||||
custom: []
|
||||
},
|
||||
|
||||
// Tag usage statistics
|
||||
tagStats: {
|
||||
// Tag name -> count of media items with that tag
|
||||
},
|
||||
|
||||
// Quality ratings by user
|
||||
ratings: {
|
||||
// File path -> rating (1-5 stars)
|
||||
},
|
||||
|
||||
// User's favorite/disliked items
|
||||
favorites: [],
|
||||
disliked: [],
|
||||
|
||||
// Collection system (playlists/categories)
|
||||
collections: {
|
||||
// Collection name -> array of file paths
|
||||
},
|
||||
|
||||
// Scan history (track what's been tagged)
|
||||
scanHistory: {
|
||||
lastFullScan: null,
|
||||
scannedFolders: [],
|
||||
totalItemsScanned: 0,
|
||||
totalItemsTagged: 0
|
||||
}
|
||||
};
|
||||
|
||||
this.saveLibrary();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the full library object
|
||||
* @returns {Object} Library object
|
||||
*/
|
||||
getLibrary() {
|
||||
return window.gameData.academyLibrary;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add or update media item with tags and metadata
|
||||
* @param {string} filePath - Path to media file
|
||||
* @param {Object} metadata - Metadata object { tags, rating, duration, etc. }
|
||||
* @returns {boolean} Success status
|
||||
*/
|
||||
addMediaItem(filePath, metadata = {}) {
|
||||
const library = this.getLibrary();
|
||||
|
||||
// Create or update media item
|
||||
if (!library.media[filePath]) {
|
||||
library.media[filePath] = {
|
||||
filePath: filePath,
|
||||
tags: [],
|
||||
rating: 0,
|
||||
timesPlayed: 0,
|
||||
lastPlayed: null,
|
||||
addedDate: new Date().toISOString(),
|
||||
duration: null,
|
||||
fileSize: null,
|
||||
format: this.getFileFormat(filePath)
|
||||
};
|
||||
}
|
||||
|
||||
// Merge metadata
|
||||
Object.assign(library.media[filePath], metadata);
|
||||
|
||||
// Update tag statistics
|
||||
if (metadata.tags) {
|
||||
this.updateTagStats(metadata.tags);
|
||||
}
|
||||
|
||||
library.lastUpdated = new Date().toISOString();
|
||||
this.saveLibrary();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add tags to a media item
|
||||
* @param {string} filePath - Path to media file
|
||||
* @param {Array<string>} tags - Array of tag names
|
||||
* @returns {boolean} Success status
|
||||
*/
|
||||
addTags(filePath, tags) {
|
||||
const library = this.getLibrary();
|
||||
|
||||
if (!library.media[filePath]) {
|
||||
this.addMediaItem(filePath);
|
||||
}
|
||||
|
||||
const item = library.media[filePath];
|
||||
|
||||
// Add new tags (avoid duplicates)
|
||||
tags.forEach(tag => {
|
||||
if (!item.tags.includes(tag)) {
|
||||
item.tags.push(tag);
|
||||
}
|
||||
});
|
||||
|
||||
// Update tag statistics
|
||||
this.updateTagStats(tags);
|
||||
|
||||
library.lastUpdated = new Date().toISOString();
|
||||
this.saveLibrary();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove tags from a media item
|
||||
* @param {string} filePath - Path to media file
|
||||
* @param {Array<string>} tags - Array of tag names to remove
|
||||
* @returns {boolean} Success status
|
||||
*/
|
||||
removeTags(filePath, tags) {
|
||||
const library = this.getLibrary();
|
||||
|
||||
if (!library.media[filePath]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const item = library.media[filePath];
|
||||
|
||||
tags.forEach(tag => {
|
||||
const index = item.tags.indexOf(tag);
|
||||
if (index > -1) {
|
||||
item.tags.splice(index, 1);
|
||||
|
||||
// Decrement tag stat
|
||||
if (library.tagStats[tag]) {
|
||||
library.tagStats[tag] = Math.max(0, library.tagStats[tag] - 1);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
library.lastUpdated = new Date().toISOString();
|
||||
this.saveLibrary();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk tag multiple files at once
|
||||
* @param {Array<string>} filePaths - Array of file paths
|
||||
* @param {Array<string>} tags - Array of tags to add to all files
|
||||
* @returns {Object} Result object with success count
|
||||
*/
|
||||
bulkAddTags(filePaths, tags) {
|
||||
let successCount = 0;
|
||||
|
||||
filePaths.forEach(filePath => {
|
||||
if (this.addTags(filePath, tags)) {
|
||||
successCount++;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
filesTagged: successCount,
|
||||
totalFiles: filePaths.length,
|
||||
tags: tags
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Set quality rating for a media item
|
||||
* @param {string} filePath - Path to media file
|
||||
* @param {number} rating - Rating (1-5)
|
||||
* @returns {boolean} Success status
|
||||
*/
|
||||
setRating(filePath, rating) {
|
||||
if (rating < 1 || rating > 5) {
|
||||
console.error('Rating must be between 1 and 5');
|
||||
return false;
|
||||
}
|
||||
|
||||
const library = this.getLibrary();
|
||||
|
||||
if (!library.media[filePath]) {
|
||||
this.addMediaItem(filePath);
|
||||
}
|
||||
|
||||
library.media[filePath].rating = rating;
|
||||
library.ratings[filePath] = rating;
|
||||
|
||||
library.lastUpdated = new Date().toISOString();
|
||||
this.saveLibrary();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add media item to favorites
|
||||
* @param {string} filePath - Path to media file
|
||||
* @returns {boolean} Success status
|
||||
*/
|
||||
addToFavorites(filePath) {
|
||||
const library = this.getLibrary();
|
||||
|
||||
if (!library.favorites.includes(filePath)) {
|
||||
library.favorites.push(filePath);
|
||||
|
||||
// Remove from disliked if present
|
||||
const dislikedIndex = library.disliked.indexOf(filePath);
|
||||
if (dislikedIndex > -1) {
|
||||
library.disliked.splice(dislikedIndex, 1);
|
||||
}
|
||||
|
||||
library.lastUpdated = new Date().toISOString();
|
||||
this.saveLibrary();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add media item to disliked list
|
||||
* @param {string} filePath - Path to media file
|
||||
* @returns {boolean} Success status
|
||||
*/
|
||||
addToDisliked(filePath) {
|
||||
const library = this.getLibrary();
|
||||
|
||||
if (!library.disliked.includes(filePath)) {
|
||||
library.disliked.push(filePath);
|
||||
|
||||
// Remove from favorites if present
|
||||
const favIndex = library.favorites.indexOf(filePath);
|
||||
if (favIndex > -1) {
|
||||
library.favorites.splice(favIndex, 1);
|
||||
}
|
||||
|
||||
library.lastUpdated = new Date().toISOString();
|
||||
this.saveLibrary();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search/filter media by tags and criteria
|
||||
* @param {Object} filters - Filter configuration
|
||||
* @returns {Array<Object>} Array of matching media items
|
||||
*/
|
||||
searchMedia(filters = {}) {
|
||||
const library = this.getLibrary();
|
||||
let results = Object.values(library.media);
|
||||
|
||||
// Filter by required tags (item must have ALL these tags)
|
||||
if (filters.requiredTags && filters.requiredTags.length > 0) {
|
||||
results = results.filter(item =>
|
||||
filters.requiredTags.every(tag => item.tags.includes(tag))
|
||||
);
|
||||
}
|
||||
|
||||
// Filter by any tags (item must have AT LEAST ONE of these tags)
|
||||
if (filters.anyTags && filters.anyTags.length > 0) {
|
||||
results = results.filter(item =>
|
||||
filters.anyTags.some(tag => item.tags.includes(tag))
|
||||
);
|
||||
}
|
||||
|
||||
// Filter by excluded tags (item must NOT have any of these tags)
|
||||
if (filters.excludedTags && filters.excludedTags.length > 0) {
|
||||
results = results.filter(item =>
|
||||
!filters.excludedTags.some(tag => item.tags.includes(tag))
|
||||
);
|
||||
}
|
||||
|
||||
// Filter by minimum rating
|
||||
if (filters.minRating) {
|
||||
results = results.filter(item => item.rating >= filters.minRating);
|
||||
}
|
||||
|
||||
// Filter by favorites only
|
||||
if (filters.favoritesOnly) {
|
||||
results = results.filter(item => library.favorites.includes(item.filePath));
|
||||
}
|
||||
|
||||
// Exclude disliked
|
||||
if (filters.excludeDisliked) {
|
||||
results = results.filter(item => !library.disliked.includes(item.filePath));
|
||||
}
|
||||
|
||||
// Filter by format
|
||||
if (filters.format) {
|
||||
results = results.filter(item => item.format === filters.format);
|
||||
}
|
||||
|
||||
// Sort results
|
||||
if (filters.sortBy) {
|
||||
results = this.sortResults(results, filters.sortBy, filters.sortOrder || 'desc');
|
||||
}
|
||||
|
||||
// Limit results
|
||||
if (filters.limit) {
|
||||
results = results.slice(0, filters.limit);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get media items matching user's preferences (from preferenceManager)
|
||||
* @param {Object} preferenceFilter - Filter from preferenceManager.getContentFilter()
|
||||
* @param {number} limit - Maximum number of items to return
|
||||
* @returns {Array<Object>} Array of matching media items
|
||||
*/
|
||||
getMediaForPreferences(preferenceFilter, limit = 10) {
|
||||
const filters = {
|
||||
anyTags: [
|
||||
...preferenceFilter.themes,
|
||||
...preferenceFilter.visuals,
|
||||
...preferenceFilter.audio,
|
||||
...preferenceFilter.tones
|
||||
],
|
||||
excludedTags: [
|
||||
...preferenceFilter.exclude.hardLimits,
|
||||
...preferenceFilter.exclude.softLimits
|
||||
],
|
||||
excludeDisliked: true,
|
||||
sortBy: 'rating',
|
||||
sortOrder: 'desc',
|
||||
limit: limit
|
||||
};
|
||||
|
||||
return this.searchMedia(filters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a collection (playlist/category)
|
||||
* @param {string} name - Collection name
|
||||
* @param {Array<string>} filePaths - Array of file paths
|
||||
* @returns {boolean} Success status
|
||||
*/
|
||||
createCollection(name, filePaths = []) {
|
||||
const library = this.getLibrary();
|
||||
|
||||
library.collections[name] = filePaths;
|
||||
|
||||
library.lastUpdated = new Date().toISOString();
|
||||
this.saveLibrary();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add items to a collection
|
||||
* @param {string} collectionName - Collection name
|
||||
* @param {Array<string>} filePaths - File paths to add
|
||||
* @returns {boolean} Success status
|
||||
*/
|
||||
addToCollection(collectionName, filePaths) {
|
||||
const library = this.getLibrary();
|
||||
|
||||
if (!library.collections[collectionName]) {
|
||||
this.createCollection(collectionName, filePaths);
|
||||
} else {
|
||||
filePaths.forEach(filePath => {
|
||||
if (!library.collections[collectionName].includes(filePath)) {
|
||||
library.collections[collectionName].push(filePath);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
library.lastUpdated = new Date().toISOString();
|
||||
this.saveLibrary();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all items in a collection
|
||||
* @param {string} collectionName - Collection name
|
||||
* @returns {Array<Object>} Array of media items
|
||||
*/
|
||||
getCollection(collectionName) {
|
||||
const library = this.getLibrary();
|
||||
|
||||
if (!library.collections[collectionName]) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return library.collections[collectionName].map(filePath =>
|
||||
library.media[filePath]
|
||||
).filter(item => item !== undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get library statistics
|
||||
* @returns {Object} Stats object
|
||||
*/
|
||||
getLibraryStats() {
|
||||
const library = this.getLibrary();
|
||||
|
||||
const totalItems = Object.keys(library.media).length;
|
||||
const taggedItems = Object.values(library.media).filter(item => item.tags.length > 0).length;
|
||||
const ratedItems = Object.values(library.media).filter(item => item.rating > 0).length;
|
||||
|
||||
// Calculate average rating
|
||||
const ratings = Object.values(library.media).map(item => item.rating).filter(r => r > 0);
|
||||
const avgRating = ratings.length > 0 ? (ratings.reduce((a, b) => a + b, 0) / ratings.length).toFixed(2) : 0;
|
||||
|
||||
return {
|
||||
totalItems: totalItems,
|
||||
taggedItems: taggedItems,
|
||||
untaggedItems: totalItems - taggedItems,
|
||||
ratedItems: ratedItems,
|
||||
averageRating: parseFloat(avgRating),
|
||||
totalFavorites: library.favorites.length,
|
||||
totalDisliked: library.disliked.length,
|
||||
totalCollections: Object.keys(library.collections).length,
|
||||
totalTags: Object.keys(library.tagStats).length,
|
||||
mostUsedTags: this.getMostUsedTags(5),
|
||||
lastUpdated: library.lastUpdated,
|
||||
scanHistory: library.scanHistory
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get most used tags
|
||||
* @param {number} limit - Number of tags to return
|
||||
* @returns {Array<Object>} Array of {tag, count} objects
|
||||
*/
|
||||
getMostUsedTags(limit = 10) {
|
||||
const library = this.getLibrary();
|
||||
|
||||
return Object.entries(library.tagStats)
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.slice(0, limit)
|
||||
.map(([tag, count]) => ({ tag, count }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all available tags by category
|
||||
* @returns {Object} Tags organized by category
|
||||
*/
|
||||
getAllTags() {
|
||||
return this.getLibrary().tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync with existing video library from desktopFileManager
|
||||
* Imports videos from existing library and preserves existing Academy tags/ratings
|
||||
*/
|
||||
syncWithExistingLibrary() {
|
||||
console.log('🔄 Syncing with existing video library...');
|
||||
|
||||
let allVideos = [];
|
||||
|
||||
// Get videos from desktopFileManager (primary source)
|
||||
if (window.desktopFileManager && typeof window.desktopFileManager.getAllVideos === 'function') {
|
||||
allVideos = window.desktopFileManager.getAllVideos();
|
||||
console.log(`📹 Found ${allVideos.length} videos from desktopFileManager`);
|
||||
}
|
||||
|
||||
// Fallback to unifiedVideoLibrary in localStorage
|
||||
if (allVideos.length === 0) {
|
||||
const unifiedData = JSON.parse(localStorage.getItem('unifiedVideoLibrary') || '{}');
|
||||
allVideos = unifiedData.allVideos || [];
|
||||
console.log(`📹 Found ${allVideos.length} videos from unifiedVideoLibrary`);
|
||||
}
|
||||
|
||||
// Import videos into Academy library while preserving existing Academy metadata
|
||||
const library = this.getLibrary();
|
||||
let newCount = 0;
|
||||
let existingCount = 0;
|
||||
|
||||
allVideos.forEach(video => {
|
||||
const filePath = video.path || video.name;
|
||||
|
||||
if (!library.media[filePath]) {
|
||||
// New video - add with minimal metadata
|
||||
library.media[filePath] = {
|
||||
filePath: filePath,
|
||||
name: video.name,
|
||||
tags: [],
|
||||
rating: 0,
|
||||
timesPlayed: 0,
|
||||
lastPlayed: null,
|
||||
addedDate: new Date().toISOString(),
|
||||
duration: video.duration || null,
|
||||
fileSize: video.size || null,
|
||||
format: video.format || this.getFileFormat(filePath),
|
||||
// Preserve original library metadata
|
||||
originalSource: video.directory || video.source || 'Unknown',
|
||||
resolution: video.resolution || null,
|
||||
thumbnail: video.thumbnail || null
|
||||
};
|
||||
newCount++;
|
||||
} else {
|
||||
// Existing video - update non-Academy fields only
|
||||
library.media[filePath].name = video.name;
|
||||
library.media[filePath].duration = video.duration || library.media[filePath].duration;
|
||||
library.media[filePath].fileSize = video.size || library.media[filePath].fileSize;
|
||||
library.media[filePath].format = video.format || library.media[filePath].format;
|
||||
library.media[filePath].resolution = video.resolution || library.media[filePath].resolution;
|
||||
library.media[filePath].thumbnail = video.thumbnail || library.media[filePath].thumbnail;
|
||||
existingCount++;
|
||||
}
|
||||
});
|
||||
|
||||
library.lastUpdated = new Date().toISOString();
|
||||
library.scanHistory.lastFullScan = new Date().toISOString();
|
||||
library.scanHistory.totalItemsScanned = allVideos.length;
|
||||
library.scanHistory.totalItemsTagged = Object.values(library.media).filter(item => item.tags.length > 0).length;
|
||||
|
||||
this.saveLibrary();
|
||||
|
||||
console.log(`✅ Sync complete: ${newCount} new, ${existingCount} existing, ${allVideos.length} total videos`);
|
||||
|
||||
return {
|
||||
total: allVideos.length,
|
||||
new: newCount,
|
||||
existing: existingCount
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all videos from existing library (convenience method)
|
||||
* @returns {Array<Object>} Array of video objects with Academy metadata
|
||||
*/
|
||||
getAllVideosWithMetadata() {
|
||||
const library = this.getLibrary();
|
||||
return Object.values(library.media);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get video by file path
|
||||
* @param {string} filePath - Path to video file
|
||||
* @returns {Object|null} Video object with metadata
|
||||
*/
|
||||
getVideoByPath(filePath) {
|
||||
const library = this.getLibrary();
|
||||
return library.media[filePath] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a custom tag
|
||||
* @param {string} tagName - Custom tag name
|
||||
* @returns {boolean} Success status
|
||||
*/
|
||||
addCustomTag(tagName) {
|
||||
const library = this.getLibrary();
|
||||
|
||||
if (!library.tags.custom.includes(tagName)) {
|
||||
library.tags.custom.push(tagName);
|
||||
library.lastUpdated = new Date().toISOString();
|
||||
this.saveLibrary();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Record that a media item was played
|
||||
* @param {string} filePath - Path to media file
|
||||
* @returns {boolean} Success status
|
||||
*/
|
||||
recordPlay(filePath) {
|
||||
const library = this.getLibrary();
|
||||
|
||||
if (!library.media[filePath]) {
|
||||
this.addMediaItem(filePath);
|
||||
}
|
||||
|
||||
library.media[filePath].timesPlayed = (library.media[filePath].timesPlayed || 0) + 1;
|
||||
library.media[filePath].lastPlayed = new Date().toISOString();
|
||||
|
||||
this.saveLibrary();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recently played items
|
||||
* @param {number} limit - Number of items to return
|
||||
* @returns {Array<Object>} Array of media items
|
||||
*/
|
||||
getRecentlyPlayed(limit = 10) {
|
||||
const library = this.getLibrary();
|
||||
|
||||
return Object.values(library.media)
|
||||
.filter(item => item.lastPlayed !== null)
|
||||
.sort((a, b) => new Date(b.lastPlayed) - new Date(a.lastPlayed))
|
||||
.slice(0, limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset library to empty state
|
||||
* @returns {boolean} Success status
|
||||
*/
|
||||
resetLibrary() {
|
||||
delete window.gameData.academyLibrary;
|
||||
this.initializeLibrary();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
|
||||
/**
|
||||
* Update tag statistics
|
||||
* @param {Array<string>} tags - Tags to increment
|
||||
*/
|
||||
updateTagStats(tags) {
|
||||
const library = this.getLibrary();
|
||||
|
||||
tags.forEach(tag => {
|
||||
library.tagStats[tag] = (library.tagStats[tag] || 0) + 1;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get file format from path
|
||||
* @param {string} filePath - File path
|
||||
* @returns {string} File format (mp4, webm, etc.)
|
||||
*/
|
||||
getFileFormat(filePath) {
|
||||
const ext = filePath.split('.').pop().toLowerCase();
|
||||
return ext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort results by field
|
||||
* @param {Array<Object>} results - Results to sort
|
||||
* @param {string} field - Field to sort by
|
||||
* @param {string} order - 'asc' or 'desc'
|
||||
* @returns {Array<Object>} Sorted results
|
||||
*/
|
||||
sortResults(results, field, order = 'desc') {
|
||||
return results.sort((a, b) => {
|
||||
if (field === 'rating' || field === 'timesPlayed') {
|
||||
return order === 'desc' ? b[field] - a[field] : a[field] - b[field];
|
||||
} else if (field === 'lastPlayed' || field === 'addedDate') {
|
||||
const dateA = new Date(a[field]);
|
||||
const dateB = new Date(b[field]);
|
||||
return order === 'desc' ? dateB - dateA : dateA - dateB;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Save library to localStorage
|
||||
*/
|
||||
saveLibrary() {
|
||||
try {
|
||||
localStorage.setItem('webGame-data', JSON.stringify(window.gameData));
|
||||
} catch (error) {
|
||||
console.error('Failed to save library:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create global instance
|
||||
window.libraryManager = new LibraryManager();
|
||||
|
|
@ -0,0 +1,482 @@
|
|||
/**
|
||||
* Preference Management System for The Academy
|
||||
* Handles user preferences across 8 categories, checkpoint modals, and preference-based filtering
|
||||
*/
|
||||
|
||||
class PreferenceManager {
|
||||
constructor() {
|
||||
this.initializePreferences();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize preferences structure in gameData if it doesn't exist
|
||||
*/
|
||||
initializePreferences() {
|
||||
if (!window.gameData.academyPreferences) {
|
||||
window.gameData.academyPreferences = {
|
||||
version: 1,
|
||||
lastUpdated: new Date().toISOString(),
|
||||
checkpointHistory: [], // Track when preferences were updated
|
||||
|
||||
// Category 1: Content Themes (multi-select)
|
||||
contentThemes: {
|
||||
dominance: false,
|
||||
submission: false,
|
||||
humiliation: false,
|
||||
worship: false,
|
||||
edging: false,
|
||||
denial: false,
|
||||
cei: false,
|
||||
sissy: false,
|
||||
bbc: false,
|
||||
feet: false,
|
||||
femdom: false,
|
||||
maledom: false,
|
||||
lesbian: false,
|
||||
gay: false,
|
||||
trans: false,
|
||||
hentai: false,
|
||||
pov: false,
|
||||
joi: false,
|
||||
gooning: false,
|
||||
mindbreak: false
|
||||
},
|
||||
|
||||
// Category 2: Visual Preferences (multi-select)
|
||||
visualPreferences: {
|
||||
solo: false,
|
||||
couples: false,
|
||||
group: false,
|
||||
amateur: false,
|
||||
professional: false,
|
||||
animated: false,
|
||||
realPorn: false,
|
||||
closeups: false,
|
||||
fullBody: false,
|
||||
pov: false,
|
||||
compilation: false,
|
||||
pmv: false,
|
||||
slowmo: false,
|
||||
artistic: false,
|
||||
raw: false
|
||||
},
|
||||
|
||||
// Category 3: Intensity Levels (slider 1-5)
|
||||
intensity: {
|
||||
visualIntensity: 3, // How explicit/hardcore
|
||||
paceIntensity: 3, // How fast/aggressive
|
||||
mentalIntensity: 3, // How mind-breaking
|
||||
audioIntensity: 3, // How loud/overwhelming
|
||||
taskDifficulty: 3 // How challenging
|
||||
},
|
||||
|
||||
// Category 4: Caption/Text Tone (multi-select)
|
||||
captionTone: {
|
||||
encouraging: false,
|
||||
mocking: false,
|
||||
commanding: false,
|
||||
seductive: false,
|
||||
degrading: false,
|
||||
playful: false,
|
||||
serious: false,
|
||||
casual: false,
|
||||
formal: false,
|
||||
extreme: false
|
||||
},
|
||||
|
||||
// Category 5: Audio Preferences (multi-select)
|
||||
audioPreferences: {
|
||||
femaleVoice: false,
|
||||
maleVoice: false,
|
||||
moaning: false,
|
||||
talking: false,
|
||||
music: false,
|
||||
ambience: false,
|
||||
asmr: false,
|
||||
binaural: false,
|
||||
silence: false,
|
||||
soundEffects: false
|
||||
},
|
||||
|
||||
// Category 6: Session Duration (single-select)
|
||||
sessionDuration: {
|
||||
preferred: 'medium', // 'short' (5-15m), 'medium' (15-30m), 'long' (30-60m), 'marathon' (60m+)
|
||||
allowFlexibility: true // Can go shorter/longer based on performance
|
||||
},
|
||||
|
||||
// Category 7: Feature Preferences (multi-select - unlocks as features discovered)
|
||||
featurePreferences: {
|
||||
webcam: null, // null = not yet discovered, true/false after discovery
|
||||
tts: null,
|
||||
interactiveTasks: null,
|
||||
edgingTimer: null,
|
||||
denialLocks: null,
|
||||
punishments: null,
|
||||
rewards: null,
|
||||
progressTracking: null,
|
||||
mediaLibrary: null,
|
||||
customPlaylists: null
|
||||
},
|
||||
|
||||
// Category 8: Boundaries (multi-select for hard limits)
|
||||
boundaries: {
|
||||
hardLimits: [], // User-specified content to NEVER show
|
||||
softLimits: [], // User-specified content to show sparingly
|
||||
triggerWarnings: [], // User wants warnings before certain content
|
||||
requireConfirmation: [] // User wants to confirm before certain tasks
|
||||
}
|
||||
};
|
||||
|
||||
this.savePreferences();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all current preferences
|
||||
* @returns {Object} Full preferences object
|
||||
*/
|
||||
getPreferences() {
|
||||
return window.gameData.academyPreferences;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get preferences for a specific category
|
||||
* @param {string} category - Category name (contentThemes, visualPreferences, etc.)
|
||||
* @returns {Object} Category preferences
|
||||
*/
|
||||
getCategoryPreferences(category) {
|
||||
const prefs = this.getPreferences();
|
||||
return prefs[category] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update preferences for a specific category
|
||||
* @param {string} category - Category name
|
||||
* @param {Object} updates - Object with preference updates
|
||||
* @param {number} checkpointLevel - Level number where update occurred (optional)
|
||||
* @returns {boolean} Success status
|
||||
*/
|
||||
updateCategoryPreferences(category, updates, checkpointLevel = null) {
|
||||
const prefs = this.getPreferences();
|
||||
|
||||
if (!prefs[category]) {
|
||||
console.error(`Invalid category: ${category}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Update the category
|
||||
Object.assign(prefs[category], updates);
|
||||
|
||||
// Update metadata
|
||||
prefs.lastUpdated = new Date().toISOString();
|
||||
|
||||
// Track checkpoint history
|
||||
if (checkpointLevel !== null) {
|
||||
prefs.checkpointHistory.push({
|
||||
level: checkpointLevel,
|
||||
category: category,
|
||||
timestamp: new Date().toISOString(),
|
||||
changes: updates
|
||||
});
|
||||
}
|
||||
|
||||
this.savePreferences();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk update multiple categories at once (used at checkpoints)
|
||||
* @param {Object} categoryUpdates - Object with category names as keys, updates as values
|
||||
* @param {number} checkpointLevel - Level number where update occurred
|
||||
* @returns {boolean} Success status
|
||||
*/
|
||||
updateMultipleCategories(categoryUpdates, checkpointLevel) {
|
||||
const prefs = this.getPreferences();
|
||||
|
||||
for (const [category, updates] of Object.entries(categoryUpdates)) {
|
||||
if (!prefs[category]) {
|
||||
console.error(`Invalid category: ${category}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
Object.assign(prefs[category], updates);
|
||||
}
|
||||
|
||||
// Update metadata
|
||||
prefs.lastUpdated = new Date().toISOString();
|
||||
|
||||
// Track checkpoint history
|
||||
prefs.checkpointHistory.push({
|
||||
level: checkpointLevel,
|
||||
categories: Object.keys(categoryUpdates),
|
||||
timestamp: new Date().toISOString(),
|
||||
changes: categoryUpdates
|
||||
});
|
||||
|
||||
return this.savePreferences();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get content filter based on current preferences
|
||||
* Used to filter media library for level content selection
|
||||
* @returns {Object} Filter configuration
|
||||
*/
|
||||
getContentFilter() {
|
||||
const prefs = this.getPreferences();
|
||||
|
||||
return {
|
||||
// Theme filters
|
||||
themes: this.getActivePreferences(prefs.contentThemes),
|
||||
|
||||
// Visual filters
|
||||
visuals: this.getActivePreferences(prefs.visualPreferences),
|
||||
|
||||
// Audio filters
|
||||
audio: this.getActivePreferences(prefs.audioPreferences),
|
||||
|
||||
// Tone filters
|
||||
tones: this.getActivePreferences(prefs.captionTone),
|
||||
|
||||
// Intensity filters
|
||||
intensity: {
|
||||
visual: prefs.intensity.visualIntensity,
|
||||
pace: prefs.intensity.paceIntensity,
|
||||
mental: prefs.intensity.mentalIntensity,
|
||||
audio: prefs.intensity.audioIntensity,
|
||||
difficulty: prefs.intensity.taskDifficulty
|
||||
},
|
||||
|
||||
// Duration preference
|
||||
duration: prefs.sessionDuration.preferred,
|
||||
allowFlexibility: prefs.sessionDuration.allowFlexibility,
|
||||
|
||||
// Boundaries (exclude these)
|
||||
exclude: {
|
||||
hardLimits: prefs.boundaries.hardLimits,
|
||||
softLimits: prefs.boundaries.softLimits
|
||||
},
|
||||
|
||||
// Warnings needed
|
||||
warnings: prefs.boundaries.triggerWarnings,
|
||||
|
||||
// Confirmation required
|
||||
confirmations: prefs.boundaries.requireConfirmation
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of active (true) preferences from a category object
|
||||
* @param {Object} categoryPrefs - Category preferences object
|
||||
* @returns {Array<string>} Array of active preference names
|
||||
*/
|
||||
getActivePreferences(categoryPrefs) {
|
||||
return Object.entries(categoryPrefs)
|
||||
.filter(([key, value]) => value === true)
|
||||
.map(([key]) => key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a checkpoint level should show preference modal
|
||||
* @param {number} levelNum - Level number
|
||||
* @returns {boolean} True if checkpoint level
|
||||
*/
|
||||
isCheckpointLevel(levelNum) {
|
||||
return [1, 5, 10, 15, 20, 25].includes(levelNum);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get checkpoint modal configuration for a specific level
|
||||
* @param {number} levelNum - Level number
|
||||
* @returns {Object} Modal configuration with categories to show
|
||||
*/
|
||||
getCheckpointModalConfig(levelNum) {
|
||||
const configs = {
|
||||
1: {
|
||||
title: "Welcome to The Academy",
|
||||
description: "Let's set up your initial preferences. You can always change these later at checkpoints (L5, 10, 15, 20, 25).",
|
||||
categories: ['contentThemes', 'visualPreferences', 'sessionDuration'],
|
||||
isInitial: true
|
||||
},
|
||||
5: {
|
||||
title: "Checkpoint: Refine Your Experience",
|
||||
description: "You've completed the Foundation arc. Let's refine your preferences based on what you've experienced.",
|
||||
categories: ['contentThemes', 'visualPreferences', 'intensity', 'captionTone'],
|
||||
isInitial: false
|
||||
},
|
||||
10: {
|
||||
title: "Checkpoint: Feature Discovery Complete",
|
||||
description: "Now that you've explored all features, let's optimize your sessions.",
|
||||
categories: ['featurePreferences', 'audioPreferences', 'intensity', 'sessionDuration'],
|
||||
isInitial: false
|
||||
},
|
||||
15: {
|
||||
title: "Checkpoint: Deepening Your Training",
|
||||
description: "You're halfway through. Time to deepen your preferences.",
|
||||
categories: ['contentThemes', 'captionTone', 'intensity', 'boundaries'],
|
||||
isInitial: false
|
||||
},
|
||||
20: {
|
||||
title: "Checkpoint: Advanced Personalization",
|
||||
description: "You're in the Mastery arc. Let's perfect your experience.",
|
||||
categories: ['visualPreferences', 'audioPreferences', 'featurePreferences', 'intensity'],
|
||||
isInitial: false
|
||||
},
|
||||
25: {
|
||||
title: "Checkpoint: Final Refinement",
|
||||
description: "Almost at graduation. Final chance to perfect your preferences.",
|
||||
categories: ['contentThemes', 'visualPreferences', 'intensity', 'captionTone', 'boundaries'],
|
||||
isInitial: false
|
||||
}
|
||||
};
|
||||
|
||||
return configs[levelNum] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get checkpoint history
|
||||
* @returns {Array} Array of checkpoint update records
|
||||
*/
|
||||
getCheckpointHistory() {
|
||||
return this.getPreferences().checkpointHistory || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a feature as discovered (unlocks it in featurePreferences)
|
||||
* @param {string} featureName - Feature name (webcam, tts, etc.)
|
||||
* @returns {boolean} Success status
|
||||
*/
|
||||
discoverFeature(featureName) {
|
||||
const prefs = this.getPreferences();
|
||||
|
||||
if (!prefs.featurePreferences.hasOwnProperty(featureName)) {
|
||||
console.error(`Invalid feature: ${featureName}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Set to false initially (discovered but not enabled)
|
||||
// User will choose at next checkpoint
|
||||
if (prefs.featurePreferences[featureName] === null) {
|
||||
prefs.featurePreferences[featureName] = false;
|
||||
prefs.lastUpdated = new Date().toISOString();
|
||||
this.savePreferences();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of discovered features
|
||||
* @returns {Array<string>} Array of discovered feature names
|
||||
*/
|
||||
getDiscoveredFeatures() {
|
||||
const prefs = this.getPreferences();
|
||||
return Object.entries(prefs.featurePreferences)
|
||||
.filter(([key, value]) => value !== null)
|
||||
.map(([key]) => key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of enabled features
|
||||
* @returns {Array<string>} Array of enabled feature names
|
||||
*/
|
||||
getEnabledFeatures() {
|
||||
const prefs = this.getPreferences();
|
||||
return Object.entries(prefs.featurePreferences)
|
||||
.filter(([key, value]) => value === true)
|
||||
.map(([key]) => key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a hard limit (content to never show)
|
||||
* @param {string} limitTag - Tag to add to hard limits
|
||||
* @returns {boolean} Success status
|
||||
*/
|
||||
addHardLimit(limitTag) {
|
||||
const prefs = this.getPreferences();
|
||||
if (!prefs.boundaries.hardLimits.includes(limitTag)) {
|
||||
prefs.boundaries.hardLimits.push(limitTag);
|
||||
prefs.lastUpdated = new Date().toISOString();
|
||||
this.savePreferences();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a hard limit
|
||||
* @param {string} limitTag - Tag to remove from hard limits
|
||||
* @returns {boolean} Success status
|
||||
*/
|
||||
removeHardLimit(limitTag) {
|
||||
const prefs = this.getPreferences();
|
||||
const index = prefs.boundaries.hardLimits.indexOf(limitTag);
|
||||
if (index > -1) {
|
||||
prefs.boundaries.hardLimits.splice(index, 1);
|
||||
prefs.lastUpdated = new Date().toISOString();
|
||||
this.savePreferences();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get statistics about preferences
|
||||
* @returns {Object} Stats object
|
||||
*/
|
||||
getPreferenceStats() {
|
||||
const prefs = this.getPreferences();
|
||||
|
||||
return {
|
||||
totalCheckpoints: prefs.checkpointHistory.length,
|
||||
lastUpdated: prefs.lastUpdated,
|
||||
activeThemes: this.getActivePreferences(prefs.contentThemes).length,
|
||||
activeVisuals: this.getActivePreferences(prefs.visualPreferences).length,
|
||||
activeAudio: this.getActivePreferences(prefs.audioPreferences).length,
|
||||
activeTones: this.getActivePreferences(prefs.captionTone).length,
|
||||
discoveredFeatures: this.getDiscoveredFeatures().length,
|
||||
enabledFeatures: this.getEnabledFeatures().length,
|
||||
averageIntensity: this.getAverageIntensity(),
|
||||
hardLimitsSet: prefs.boundaries.hardLimits.length,
|
||||
softLimitsSet: prefs.boundaries.softLimits.length,
|
||||
preferredDuration: prefs.sessionDuration.preferred
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate average intensity across all intensity categories
|
||||
* @returns {number} Average intensity (1-5)
|
||||
*/
|
||||
getAverageIntensity() {
|
||||
const prefs = this.getPreferences();
|
||||
const intensities = Object.values(prefs.intensity);
|
||||
const sum = intensities.reduce((acc, val) => acc + val, 0);
|
||||
return (sum / intensities.length).toFixed(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset all preferences to defaults
|
||||
* @returns {boolean} Success status
|
||||
*/
|
||||
resetPreferences() {
|
||||
delete window.gameData.academyPreferences;
|
||||
this.initializePreferences();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save preferences to localStorage
|
||||
*/
|
||||
savePreferences() {
|
||||
try {
|
||||
localStorage.setItem('webGame-data', JSON.stringify(window.gameData));
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Failed to save preferences:', error);
|
||||
if (error.name === 'QuotaExceededError') {
|
||||
alert('⚠️ Storage full! Preferences could not be saved.\n\nThe app has cleared old data. Please try again.');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create global instance
|
||||
window.preferenceManager = new PreferenceManager();
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -366,12 +366,22 @@ class VideoPlayerManager {
|
|||
}
|
||||
|
||||
/**
|
||||
* Get full video path
|
||||
* Get full path for video
|
||||
*/
|
||||
getVideoPath(videoPath) {
|
||||
if (videoPath.startsWith('http') || videoPath.startsWith('file:') || videoPath.startsWith('video/')) {
|
||||
// Already a full URL
|
||||
if (videoPath.startsWith('http') || videoPath.startsWith('file:')) {
|
||||
return videoPath;
|
||||
}
|
||||
|
||||
// Absolute path (Windows: C:\... or E:\..., Unix: /...)
|
||||
if (videoPath.match(/^[A-Z]:\\/i) || videoPath.startsWith('/')) {
|
||||
// Convert to file:// URL for Electron
|
||||
const normalizedPath = videoPath.replace(/\\/g, '/');
|
||||
return `file:///${normalizedPath}`;
|
||||
}
|
||||
|
||||
// Relative path - add video/ prefix
|
||||
return `video/${videoPath}`;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -213,29 +213,19 @@ class WebcamManager {
|
|||
this.savePhotoData(photoData);
|
||||
|
||||
// Check if session is complete
|
||||
console.log(`📊 Checking completion: ${photosTaken} >= ${photosNeeded}?`, photosTaken >= photosNeeded);
|
||||
if (photosTaken >= photosNeeded) {
|
||||
// For single-photo sessions (like Quick Play), auto-complete
|
||||
if (photosNeeded === 1) {
|
||||
console.log(`✅ Single-photo session complete! Auto-completing...`);
|
||||
// Small delay to let user see the completion message
|
||||
setTimeout(() => {
|
||||
this.completePhotoSession();
|
||||
}, 1000);
|
||||
} else {
|
||||
// Show completion button for multi-photo sessions
|
||||
document.getElementById('complete-session').style.display = 'inline-block';
|
||||
document.getElementById('capture-photo').style.display = 'none';
|
||||
document.getElementById('accept-photo').style.display = 'none';
|
||||
|
||||
// Update header
|
||||
document.querySelector('.session-instruction').textContent = '🎉 Session complete! All photos taken.';
|
||||
|
||||
console.log(`✅ Photo session complete! ${photosTaken}/${photosNeeded} photos taken`);
|
||||
}
|
||||
console.log(`✅ Photo session complete! ${photosTaken}/${photosNeeded} photos taken - auto-completing in 1 second...`);
|
||||
|
||||
// Auto-complete session when required photos are captured
|
||||
setTimeout(() => {
|
||||
console.log('⏰ Timeout fired - calling completePhotoSession()');
|
||||
this.completePhotoSession();
|
||||
}, 1000);
|
||||
} else {
|
||||
// Return to camera view for more photos
|
||||
this.showCameraPreview();
|
||||
console.log(`📸 Photo accepted (${photosTaken}/${photosNeeded})`);
|
||||
console.log(`📸 Photo accepted (${photosTaken}/${photosNeeded}) - continuing session`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -244,10 +234,17 @@ class WebcamManager {
|
|||
*/
|
||||
completePhotoSession() {
|
||||
console.log('🎉 Photo session completed successfully');
|
||||
console.log('📊 Current session:', this.currentPhotoSession);
|
||||
|
||||
// Show photo gallery before completing
|
||||
this.showPhotoGallery(() => {
|
||||
// Notify task completion after gallery is closed
|
||||
// For single-photo sessions (like progression challenges), skip gallery
|
||||
const isSinglePhotoSession = this.currentPhotoSession.photosNeeded === 1;
|
||||
console.log(`🔍 Single-photo session? ${isSinglePhotoSession} (photosNeeded: ${this.currentPhotoSession.photosNeeded})`);
|
||||
|
||||
if (isSinglePhotoSession) {
|
||||
console.log('📸 Single-photo session - skipping gallery, ending immediately');
|
||||
|
||||
// Notify task completion immediately
|
||||
console.log('🔔 Dispatching photoSessionComplete event...');
|
||||
const event = new CustomEvent('photoSessionComplete', {
|
||||
detail: {
|
||||
photos: this.currentPhotoSession.photos,
|
||||
|
|
@ -256,10 +253,31 @@ class WebcamManager {
|
|||
}
|
||||
});
|
||||
document.dispatchEvent(event);
|
||||
console.log('✅ Event dispatched');
|
||||
|
||||
// End session
|
||||
console.log('🔚 Calling endPhotoSession()...');
|
||||
this.endPhotoSession();
|
||||
});
|
||||
console.log('✅ endPhotoSession() completed');
|
||||
} else {
|
||||
console.log('📸 Multi-photo session - showing gallery first');
|
||||
// Show photo gallery for multi-photo sessions before completing
|
||||
this.showPhotoGallery(() => {
|
||||
console.log('📸 Gallery closed - dispatching event and ending session');
|
||||
// Notify task completion after gallery is closed
|
||||
const event = new CustomEvent('photoSessionComplete', {
|
||||
detail: {
|
||||
photos: this.currentPhotoSession.photos,
|
||||
sessionType: this.currentPhotoSession.type,
|
||||
taskData: this.currentPhotoSession.taskData
|
||||
}
|
||||
});
|
||||
document.dispatchEvent(event);
|
||||
|
||||
// End session
|
||||
this.endPhotoSession();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -686,7 +704,7 @@ class WebcamManager {
|
|||
/**
|
||||
* Save photo data (respecting privacy)
|
||||
*/
|
||||
savePhotoData(photoData) {
|
||||
async savePhotoData(photoData) {
|
||||
// Save metadata to session storage (temporary)
|
||||
const metadata = {
|
||||
timestamp: photoData.timestamp,
|
||||
|
|
@ -698,8 +716,36 @@ class WebcamManager {
|
|||
sessionPhotos.push(metadata);
|
||||
sessionStorage.setItem('photoSession', JSON.stringify(sessionPhotos));
|
||||
|
||||
// Save actual photo data to localStorage with user consent
|
||||
this.savePersistentPhoto(photoData);
|
||||
// Save actual photo to file system if running in Electron
|
||||
if (this.game && this.game.fileManager && this.game.fileManager.isElectron) {
|
||||
await this.savePhotoToFileSystem(photoData);
|
||||
} else {
|
||||
// Browser mode - save to localStorage with user consent
|
||||
this.savePersistentPhoto(photoData);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save photo to file system (Electron only)
|
||||
*/
|
||||
async savePhotoToFileSystem(photoData) {
|
||||
try {
|
||||
const savedPhoto = await this.game.fileManager.savePhoto(
|
||||
photoData.dataURL,
|
||||
photoData.sessionType
|
||||
);
|
||||
|
||||
if (savedPhoto) {
|
||||
console.log(`📸 Photo saved to file system: ${savedPhoto.filename}`);
|
||||
this.showNotification('📸 Photo saved to file system!', 'success');
|
||||
} else {
|
||||
console.warn('📸 Failed to save photo to file system');
|
||||
this.showNotification('⚠️ Failed to save photo', 'warning');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('📸 Error saving photo to file system:', error);
|
||||
this.showNotification('⚠️ Failed to save photo', 'warning');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1094,12 +1140,19 @@ class WebcamManager {
|
|||
|
||||
// Remove camera overlay
|
||||
const overlay = document.getElementById('camera-overlay');
|
||||
console.log('🔍 Camera overlay element:', overlay);
|
||||
if (overlay) {
|
||||
console.log('🗑️ Removing camera overlay...');
|
||||
overlay.remove();
|
||||
console.log('✅ Camera overlay removed');
|
||||
} else {
|
||||
console.log('⚠️ No camera overlay found to remove');
|
||||
}
|
||||
|
||||
// Stop camera stream
|
||||
console.log('📹 Stopping camera stream...');
|
||||
this.stopCamera();
|
||||
console.log('✅ Camera stream stopped');
|
||||
|
||||
// Clear current session
|
||||
if (this.currentPhotoSession) {
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -5194,6 +5194,224 @@ input[type="number"]:focus, input[type="text"]:focus, select:focus {
|
|||
100% { transform: scale(1); }
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
INVENTORY QUESTIONNAIRE STYLING
|
||||
=========================================== */
|
||||
|
||||
.inventory-questionnaire {
|
||||
max-width: 800px;
|
||||
margin: 20px auto;
|
||||
}
|
||||
|
||||
.inventory-category {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.category-title {
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 2px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.inventory-items {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
gap: 15px;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.inventory-item {
|
||||
padding: 10px;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border-radius: 8px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.inventory-item:hover {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
cursor: pointer;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.inventory-checkbox {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.select-label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.inventory-select {
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.inventory-select:hover {
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
border-color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
.inventory-select:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2);
|
||||
}
|
||||
|
||||
/* Inventory Summary Styles */
|
||||
.inventory-summary {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 15px;
|
||||
padding: 30px;
|
||||
margin: 20px 0;
|
||||
border: 2px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.inventory-summary h3 {
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
font-size: 24px;
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.tier-badge {
|
||||
display: inline-block;
|
||||
padding: 10px 30px;
|
||||
border-radius: 25px;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
margin: 10px 0 20px;
|
||||
}
|
||||
|
||||
.tier-badge.tier-1 {
|
||||
background: linear-gradient(135deg, #ffc0cb 0%, #ffffff 100%);
|
||||
color: #ff1493;
|
||||
border: 2px solid #ff69b4;
|
||||
}
|
||||
|
||||
.tier-badge.tier-2 {
|
||||
background: linear-gradient(135deg, #ba68c8 0%, #f48fb1 100%);
|
||||
color: #ffffff;
|
||||
border: 2px solid #9c27b0;
|
||||
}
|
||||
|
||||
.tier-badge.tier-3 {
|
||||
background: linear-gradient(135deg, #ff1493 0%, #ffc0cb 50%, #ff69b4 100%);
|
||||
color: #ffffff;
|
||||
border: 2px solid #ffd700;
|
||||
}
|
||||
|
||||
.tier-badge.tier-4 {
|
||||
background: linear-gradient(135deg, #b71c1c 0%, #000000 100%);
|
||||
color: #ff5252;
|
||||
border: 2px solid #ff5252;
|
||||
}
|
||||
|
||||
.tier-badge.tier-5 {
|
||||
background: linear-gradient(135deg, #000000 0%, #ff1493 50%, #000000 100%);
|
||||
color: #ff1493;
|
||||
border: 2px solid #00ff00;
|
||||
box-shadow: 0 0 20px rgba(255, 20, 147, 0.5);
|
||||
}
|
||||
|
||||
.summary-stats {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
margin: 20px 0;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.summary-stats .stat {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
padding: 15px 25px;
|
||||
border-radius: 10px;
|
||||
text-align: center;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
display: block;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-size: 14px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
display: block;
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.inventory-lists {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 20px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.item-category {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 10px;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.item-category h4 {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-size: 16px;
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.item-category ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.item-category li {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
font-size: 14px;
|
||||
padding: 5px 0;
|
||||
padding-left: 15px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.item-category li:before {
|
||||
content: "✓";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: #28a745;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
INTERLUDE INTERFACE STYLING (Webcam Mirror, Focus Session, etc.)
|
||||
=========================================== */
|
||||
|
|
|
|||
|
|
@ -15,8 +15,12 @@ class BackupManager {
|
|||
|
||||
init() {
|
||||
console.log('🛡️ Backup Manager initialized');
|
||||
this.startAutoBackup();
|
||||
// Perform aggressive cleanup first
|
||||
this.performEmergencyCleanup();
|
||||
this.cleanupOldBackups();
|
||||
// Disable auto-backup to prevent quota issues
|
||||
// this.startAutoBackup();
|
||||
console.log('⚠️ Auto-backup disabled to prevent storage quota issues');
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -162,44 +166,33 @@ class BackupManager {
|
|||
try {
|
||||
console.log('🚨 Starting emergency storage cleanup...');
|
||||
|
||||
// Clean up old backups first - keep only 1
|
||||
this.cleanupOldBackups(1);
|
||||
// Delete ALL backups immediately
|
||||
const keys = Object.keys(localStorage);
|
||||
const backupKeys = keys.filter(key => key.startsWith(this.backupPrefix));
|
||||
backupKeys.forEach(key => localStorage.removeItem(key));
|
||||
console.log(`🧹 Emergency: Removed ${backupKeys.length} backups`);
|
||||
|
||||
// Clean up verification photos aggressively
|
||||
const verificationPhotos = JSON.parse(localStorage.getItem('verificationPhotos') || '[]');
|
||||
if (verificationPhotos.length > 2) {
|
||||
const recent = verificationPhotos.slice(-2);
|
||||
localStorage.setItem('verificationPhotos', JSON.stringify(recent));
|
||||
console.log(`🧹 Emergency: Reduced verification photos to ${recent.length}`);
|
||||
}
|
||||
|
||||
// Clean up captured photos aggressively
|
||||
const capturedPhotos = JSON.parse(localStorage.getItem('capturedPhotos') || '[]');
|
||||
if (capturedPhotos.length > 3) {
|
||||
const recent = capturedPhotos.slice(-3);
|
||||
localStorage.setItem('capturedPhotos', JSON.stringify(recent));
|
||||
console.log(`🧹 Emergency: Reduced captured photos to ${recent.length}`);
|
||||
}
|
||||
// Clean up ALL photos - don't keep any in localStorage
|
||||
localStorage.removeItem('verificationPhotos');
|
||||
localStorage.removeItem('capturedPhotos');
|
||||
localStorage.removeItem('photoGallery');
|
||||
console.log(`🧹 Emergency: Removed all photo data from localStorage`);
|
||||
|
||||
// Clean up any old individual photo keys
|
||||
const keys = Object.keys(localStorage);
|
||||
const photoKeys = keys.filter(key =>
|
||||
key.startsWith('verificationPhoto_') ||
|
||||
key.startsWith('capturedPhoto_') ||
|
||||
key.startsWith('photo_')
|
||||
);
|
||||
|
||||
if (photoKeys.length > 5) {
|
||||
const toRemove = photoKeys.slice(0, -5); // Keep only 5 most recent
|
||||
toRemove.forEach(key => {
|
||||
const value = localStorage.getItem(key);
|
||||
if (value && value.startsWith('blob:')) {
|
||||
URL.revokeObjectURL(value);
|
||||
}
|
||||
localStorage.removeItem(key);
|
||||
});
|
||||
console.log(`🧹 Emergency: Removed ${toRemove.length} old photo keys`);
|
||||
}
|
||||
photoKeys.forEach(key => {
|
||||
const value = localStorage.getItem(key);
|
||||
if (value && value.startsWith('blob:')) {
|
||||
URL.revokeObjectURL(value);
|
||||
}
|
||||
localStorage.removeItem(key);
|
||||
});
|
||||
console.log(`🧹 Emergency: Removed ${photoKeys.length} old photo keys`);
|
||||
|
||||
// Calculate storage usage after cleanup
|
||||
const usage = this.calculateStorageUsage();
|
||||
|
|
|
|||
|
|
@ -514,6 +514,11 @@ class DesktopFileManager {
|
|||
if (!this.isElectron) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.dataManager) {
|
||||
console.warn('⚠️ DataManager not available, skipping linked directories load');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const data = this.dataManager.get('linkedVideoDirectories');
|
||||
|
|
|
|||
|
|
@ -0,0 +1,626 @@
|
|||
/**
|
||||
* InventoryManager - Handles player inventory tracking and progression path generation
|
||||
* Determines tier based on available items and creates personalized photo challenges
|
||||
*/
|
||||
|
||||
class InventoryManager {
|
||||
constructor() {
|
||||
this.inventory = null;
|
||||
this.tier = 0;
|
||||
this.totalItems = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store player's inventory from questionnaire
|
||||
*/
|
||||
setInventory(inventoryData) {
|
||||
this.inventory = inventoryData;
|
||||
this.totalItems = this.countTotalItems();
|
||||
this.tier = this.calculateTier();
|
||||
|
||||
console.log('📦 Inventory set:', this.totalItems, 'items, Tier', this.tier);
|
||||
return {
|
||||
inventory: this.inventory,
|
||||
tier: this.tier,
|
||||
totalItems: this.totalItems
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Count total non-none items across all categories
|
||||
*/
|
||||
countTotalItems() {
|
||||
let count = 0;
|
||||
|
||||
// Count clothing items
|
||||
for (const [key, value] of Object.entries(this.inventory.clothing || {})) {
|
||||
if (value && value !== 'none') count++;
|
||||
}
|
||||
|
||||
// Count accessories
|
||||
for (const [key, value] of Object.entries(this.inventory.accessories || {})) {
|
||||
if (value && value !== 'none') count++;
|
||||
}
|
||||
|
||||
// Count toys
|
||||
for (const [key, value] of Object.entries(this.inventory.toys || {})) {
|
||||
if (value && value !== 'none') count++;
|
||||
}
|
||||
|
||||
// Count environment items (booleans)
|
||||
for (const [key, value] of Object.entries(this.inventory.environment || {})) {
|
||||
if (value === true) count++;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate tier based on total items
|
||||
*/
|
||||
calculateTier() {
|
||||
if (this.totalItems >= 18) return 5;
|
||||
if (this.totalItems >= 13) return 4;
|
||||
if (this.totalItems >= 8) return 3;
|
||||
if (this.totalItems >= 4) return 2;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available items as array
|
||||
*/
|
||||
getAvailableItems() {
|
||||
const items = [];
|
||||
|
||||
// Collect clothing items
|
||||
for (const [key, value] of Object.entries(this.inventory.clothing || {})) {
|
||||
if (value && value !== 'none') {
|
||||
items.push({ category: 'clothing', type: key, level: value });
|
||||
}
|
||||
}
|
||||
|
||||
// Collect accessories
|
||||
for (const [key, value] of Object.entries(this.inventory.accessories || {})) {
|
||||
if (value && value !== 'none') {
|
||||
items.push({ category: 'accessories', type: key, level: value });
|
||||
}
|
||||
}
|
||||
|
||||
// Collect toys
|
||||
for (const [key, value] of Object.entries(this.inventory.toys || {})) {
|
||||
if (value && value !== 'none') {
|
||||
items.push({ category: 'toys', type: key, level: value });
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if player has specific item
|
||||
*/
|
||||
hasItem(itemType) {
|
||||
const allItems = {
|
||||
...this.inventory.clothing,
|
||||
...this.inventory.accessories,
|
||||
...this.inventory.toys,
|
||||
...this.inventory.environment
|
||||
};
|
||||
|
||||
return allItems[itemType] && allItems[itemType] !== 'none';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get item level (none/basic/sexy/multiple, etc.)
|
||||
*/
|
||||
getItemLevel(itemType) {
|
||||
const allItems = {
|
||||
...this.inventory.clothing,
|
||||
...this.inventory.accessories,
|
||||
...this.inventory.toys
|
||||
};
|
||||
|
||||
return allItems[itemType] || 'none';
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate photo progression based on tier
|
||||
*/
|
||||
generatePhotoProgression() {
|
||||
const availableItems = this.getAvailableItems();
|
||||
const photoCount = this.getPhotoCountForTier();
|
||||
|
||||
console.log(`📸 Generating ${photoCount} photo challenges for Tier ${this.tier}`);
|
||||
|
||||
const progression = [];
|
||||
|
||||
for (let i = 1; i <= photoCount; i++) {
|
||||
const challenge = this.generatePhotoChallenge(i, photoCount, availableItems);
|
||||
progression.push(challenge);
|
||||
}
|
||||
|
||||
return progression;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get photo count based on tier
|
||||
*/
|
||||
getPhotoCountForTier() {
|
||||
const counts = {
|
||||
1: 5,
|
||||
2: 10,
|
||||
3: 15,
|
||||
4: 20,
|
||||
5: 25
|
||||
};
|
||||
return counts[this.tier] || 5;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate individual photo challenge
|
||||
*/
|
||||
generatePhotoChallenge(challengeNumber, totalChallenges, availableItems) {
|
||||
const intensity = this.calculateChallengeIntensity(challengeNumber, totalChallenges);
|
||||
const requiredItems = this.selectItemsForChallenge(challengeNumber, availableItems);
|
||||
const includesEdging = this.shouldIncludeEdging(challengeNumber, totalChallenges, requiredItems);
|
||||
const poseCategory = this.determinePoseCategory(requiredItems, includesEdging);
|
||||
|
||||
// Generate instruction text
|
||||
const instruction = this.generateInstructionText(challengeNumber, totalChallenges, requiredItems, poseCategory, includesEdging);
|
||||
|
||||
return {
|
||||
number: challengeNumber,
|
||||
intensity: intensity,
|
||||
requiredItems: requiredItems,
|
||||
edging: includesEdging,
|
||||
poseCategory: poseCategory,
|
||||
instruction: instruction
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate instruction text for photo challenge
|
||||
*/
|
||||
generateInstructionText(challengeNumber, totalChallenges, requiredItems, poseCategory, includesEdging) {
|
||||
// Build item list with specific instructions for special items
|
||||
const itemDescriptions = [];
|
||||
const specialInstructions = [];
|
||||
|
||||
requiredItems.forEach(item => {
|
||||
if (item === 'dildos-oral') {
|
||||
itemDescriptions.push('dildo');
|
||||
specialInstructions.push('suck the dildo or slap your face with it');
|
||||
} else {
|
||||
itemDescriptions.push(item);
|
||||
}
|
||||
});
|
||||
|
||||
const progressText = `Photo ${challengeNumber} of ${totalChallenges}`;
|
||||
const edgingText = includesEdging ? ' Edge while posing.' : '';
|
||||
|
||||
// Handle nude photos
|
||||
if (requiredItems.length === 0) {
|
||||
const nudePoses = [
|
||||
'Pose nude for the camera',
|
||||
'Strike a confident nude pose',
|
||||
'Show yourself completely exposed',
|
||||
'Display your naked body',
|
||||
'Present yourself with nothing on',
|
||||
'Kneel nude and look at the camera',
|
||||
'Bend over and show everything',
|
||||
'Lie on your back nude',
|
||||
'Stand with legs spread',
|
||||
'Sit nude and spread your legs'
|
||||
];
|
||||
const nudeText = nudePoses[challengeNumber % nudePoses.length];
|
||||
return `${progressText} - ${nudeText}.${edgingText}`;
|
||||
}
|
||||
|
||||
const itemList = itemDescriptions.join(', ');
|
||||
|
||||
// Check if user has multiple of any items and suggest variety
|
||||
const varietyItems = this.getVarietyItems(requiredItems);
|
||||
const varietyText = varietyItems.length > 0
|
||||
? ` Try a different ${varietyItems.join(' or ')} than last time.`
|
||||
: '';
|
||||
|
||||
// Special instructions for specific items
|
||||
const specialText = specialInstructions.length > 0
|
||||
? ` ${specialInstructions.join('. ')}.`
|
||||
: '';
|
||||
|
||||
// Detailed pose instructions based on category
|
||||
const poseInstructions = {
|
||||
'standing': [
|
||||
'Stand with hands on hips',
|
||||
'Stand with arms behind your head',
|
||||
'Strike a confident standing pose',
|
||||
'Stand and show off your outfit',
|
||||
'Stand with one leg forward'
|
||||
],
|
||||
'sitting': [
|
||||
'Sit with legs crossed',
|
||||
'Sit and spread your legs',
|
||||
'Sit on the edge of a chair',
|
||||
'Sit with hands on your knees',
|
||||
'Recline while seated'
|
||||
],
|
||||
'kneeling': [
|
||||
'Kneel with hands behind your back',
|
||||
'Kneel and look up at the camera',
|
||||
'Kneel with legs spread',
|
||||
'Kneel and present yourself',
|
||||
'Kneel on all fours'
|
||||
],
|
||||
'lying': [
|
||||
'Lie on your back',
|
||||
'Lie on your side',
|
||||
'Lie face down',
|
||||
'Recline and show off',
|
||||
'Lie with legs spread'
|
||||
],
|
||||
'mirror': [
|
||||
'Admire yourself in the mirror',
|
||||
'Take a mirror selfie',
|
||||
'Pose in front of the mirror',
|
||||
'Check yourself out in the mirror',
|
||||
'Show your outfit in the mirror'
|
||||
],
|
||||
'provocative': [
|
||||
'Bend over and look back',
|
||||
'Arch your back',
|
||||
'Strike a seductive pose',
|
||||
'Pose provocatively',
|
||||
'Show off your assets'
|
||||
],
|
||||
'submissive': [
|
||||
'Kneel submissively',
|
||||
'Lower your head in submission',
|
||||
'Present yourself submissively',
|
||||
'Assume a submissive position',
|
||||
'Display your obedience'
|
||||
],
|
||||
'edging': [
|
||||
'Edge while looking at the camera',
|
||||
'Edge and pose',
|
||||
'Touch yourself while posing',
|
||||
'Edge in position',
|
||||
'Stroke while holding the pose'
|
||||
],
|
||||
'toy': [
|
||||
'Display your toys',
|
||||
'Pose with toys visible',
|
||||
'Show off your toy collection',
|
||||
'Present your toys',
|
||||
'Hold your toys for the camera'
|
||||
],
|
||||
'heel': [
|
||||
'Show off your heels - legs extended',
|
||||
'Stand on tiptoes in your heels',
|
||||
'Sit and display your heels',
|
||||
'Pose to highlight your heels',
|
||||
'Walk pose in your heels'
|
||||
],
|
||||
'dress': [
|
||||
'Twirl in your dress/skirt',
|
||||
'Lift your dress/skirt slightly',
|
||||
'Model your outfit',
|
||||
'Show off your dress/skirt',
|
||||
'Pose to display your outfit'
|
||||
],
|
||||
'bra': [
|
||||
'Show off your bra',
|
||||
'Display your lingerie',
|
||||
'Cup your chest',
|
||||
'Present your bra',
|
||||
'Pose to highlight your lingerie'
|
||||
],
|
||||
'panty': [
|
||||
'Show off your panties',
|
||||
'Display what you\'re wearing',
|
||||
'Pull your panties to the side',
|
||||
'Present yourself in panties',
|
||||
'Bend over in your panties'
|
||||
]
|
||||
};
|
||||
|
||||
// Get pose text - use challenge number to rotate through options
|
||||
const poseOptions = poseInstructions[poseCategory] || [
|
||||
'Strike a confident pose',
|
||||
'Pose seductively',
|
||||
'Show everything to the camera',
|
||||
'Present yourself',
|
||||
'Display yourself'
|
||||
];
|
||||
const poseText = poseOptions[challengeNumber % poseOptions.length];
|
||||
|
||||
return `${progressText} - ${poseText} wearing ${itemList}.${specialText}${varietyText}${edgingText}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get items that user has multiple of for variety suggestions
|
||||
*/
|
||||
getVarietyItems(requiredItems) {
|
||||
const varietyItems = [];
|
||||
|
||||
requiredItems.forEach(itemKey => {
|
||||
const level = this.getItemLevel(itemKey);
|
||||
if (level === 'multiple') {
|
||||
// Convert item key to friendly name
|
||||
const friendlyNames = {
|
||||
'panties': 'pair of panties',
|
||||
'bras': 'bra',
|
||||
'lingerie': 'lingerie set',
|
||||
'dresses': 'dress',
|
||||
'skirts': 'skirt',
|
||||
'pantyhose': 'pantyhose',
|
||||
'heels': 'heels',
|
||||
'wigs': 'wig'
|
||||
};
|
||||
if (friendlyNames[itemKey]) {
|
||||
varietyItems.push(friendlyNames[itemKey]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return varietyItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate intensity (0-1) based on progression
|
||||
*/
|
||||
calculateChallengeIntensity(challengeNumber, totalChallenges) {
|
||||
return challengeNumber / totalChallenges;
|
||||
}
|
||||
|
||||
/**
|
||||
* Select items to use for this specific challenge
|
||||
* Considers item compatibility and mutual exclusivity
|
||||
*/
|
||||
selectItemsForChallenge(challengeNumber, availableItems) {
|
||||
const items = [];
|
||||
const intensity = challengeNumber / this.getPhotoCountForTier();
|
||||
|
||||
// Track what's already selected to avoid conflicts
|
||||
let hasUnderwear = false;
|
||||
let hasAnalToy = false;
|
||||
let hasChastity = false;
|
||||
|
||||
// Early photos (0-15%) - start nude or minimal
|
||||
if (intensity < 0.15) {
|
||||
// Nude poses to start
|
||||
return [];
|
||||
}
|
||||
|
||||
// Lingerie is a complete set - if selected, use it instead of bra/panties
|
||||
if (intensity >= 0.25 && this.hasItem('lingerie')) {
|
||||
items.push('lingerie');
|
||||
hasUnderwear = true;
|
||||
} else {
|
||||
// Otherwise build from bra and panties separately
|
||||
if (intensity >= 0.15 && this.hasItem('panties')) {
|
||||
items.push('panties');
|
||||
hasUnderwear = true;
|
||||
}
|
||||
|
||||
if (intensity >= 0.2 && this.hasItem('bras')) {
|
||||
items.push('bras');
|
||||
}
|
||||
}
|
||||
|
||||
// Add dress/skirt around 30% (goes over underwear)
|
||||
if (intensity >= 0.3) {
|
||||
if (this.hasItem('dresses')) items.push('dresses');
|
||||
else if (this.hasItem('skirts')) items.push('skirts');
|
||||
}
|
||||
|
||||
// Add pantyhose around 40% (under dress/skirt, over panties)
|
||||
if (intensity >= 0.4 && this.hasItem('pantyhose')) {
|
||||
items.push('pantyhose');
|
||||
}
|
||||
|
||||
// Add heels around 45%
|
||||
if (intensity >= 0.45 && this.hasItem('heels')) {
|
||||
items.push('heels');
|
||||
}
|
||||
|
||||
// Add wig around 55%
|
||||
if (intensity >= 0.55 && this.hasItem('wigs')) {
|
||||
items.push('wigs');
|
||||
}
|
||||
|
||||
// Add makeup around 60%
|
||||
if (intensity >= 0.6 && this.hasItem('makeup')) {
|
||||
items.push('makeup');
|
||||
}
|
||||
|
||||
// Add jewelry around 65%
|
||||
if (intensity >= 0.65 && this.hasItem('jewelry')) {
|
||||
items.push('jewelry');
|
||||
}
|
||||
|
||||
// Chastity check - if wearing chastity, mark it
|
||||
if (intensity >= 0.5 && this.hasItem('chastity')) {
|
||||
items.push('chastity');
|
||||
hasChastity = true;
|
||||
}
|
||||
|
||||
// Anal toys - only ONE at a time (plug OR dildo, not both)
|
||||
// Prefer plug first, then switch to dildo later
|
||||
if (intensity >= 0.5 && intensity < 0.7 && !hasAnalToy) {
|
||||
if (this.hasItem('plugs')) {
|
||||
items.push('plugs');
|
||||
hasAnalToy = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Hand-held toys (can use if no current anal plug, or for specific actions)
|
||||
// Around 70%, switch from plug to dildo with specific actions
|
||||
if (intensity >= 0.7 && this.hasItem('dildos')) {
|
||||
// If we had a plug earlier, remove it and add dildo instead
|
||||
const plugIndex = items.indexOf('plugs');
|
||||
if (plugIndex > -1) {
|
||||
items.splice(plugIndex, 1);
|
||||
}
|
||||
// Dildo with specific action
|
||||
items.push('dildos-oral');
|
||||
hasAnalToy = true;
|
||||
}
|
||||
|
||||
// Vibrator (can be used regardless of other toys)
|
||||
if (intensity >= 0.75 && this.hasItem('vibrators') && !hasChastity) {
|
||||
items.push('vibrators');
|
||||
}
|
||||
|
||||
// Restraints around 78%
|
||||
if (intensity >= 0.78 && this.hasItem('restraints')) {
|
||||
items.push('restraints');
|
||||
}
|
||||
|
||||
// Nipple clamps around 82%
|
||||
if (intensity >= 0.82 && this.hasItem('nippleClamps')) {
|
||||
items.push('nippleClamps');
|
||||
}
|
||||
|
||||
// Gags around 88%
|
||||
if (intensity >= 0.88 && this.hasItem('gags')) {
|
||||
items.push('gags');
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if challenge should include edging
|
||||
* Cannot edge if wearing chastity cage
|
||||
*/
|
||||
shouldIncludeEdging(challengeNumber, totalChallenges, requiredItems) {
|
||||
// No edging if chastity cage is involved
|
||||
if (requiredItems.includes('chastity')) return false;
|
||||
|
||||
const intensity = challengeNumber / totalChallenges;
|
||||
|
||||
// No edging in first 20%
|
||||
if (intensity < 0.2) return false;
|
||||
|
||||
// Occasional edging in middle
|
||||
if (intensity < 0.6) return challengeNumber % 3 === 0;
|
||||
|
||||
// Frequent edging in later stages
|
||||
if (intensity < 0.8) return challengeNumber % 2 === 0;
|
||||
|
||||
// Almost always edging in final stages
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine pose category based on items
|
||||
*/
|
||||
determinePoseCategory(requiredItems, includesEdging) {
|
||||
// If edging, select edging pose category
|
||||
if (includesEdging) return 'edging';
|
||||
|
||||
// If toys present, toy poses
|
||||
if (requiredItems.some(item => ['plugs', 'dildos', 'chastity', 'gags', 'nippleClamps'].includes(item))) {
|
||||
return 'toy';
|
||||
}
|
||||
|
||||
// If heels, heel poses
|
||||
if (requiredItems.includes('heels')) return 'heel';
|
||||
|
||||
// If dress/skirt, dress poses
|
||||
if (requiredItems.includes('dresses') || requiredItems.includes('skirts')) {
|
||||
return 'dress';
|
||||
}
|
||||
|
||||
// If bra, bra poses
|
||||
if (requiredItems.includes('bras')) return 'bra';
|
||||
|
||||
// If just panties, panty poses
|
||||
if (requiredItems.includes('panties')) return 'panty';
|
||||
|
||||
// Default to basic
|
||||
return 'basic';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get inventory summary for display
|
||||
*/
|
||||
getInventorySummary() {
|
||||
const summary = {
|
||||
tier: this.tier,
|
||||
totalItems: this.totalItems,
|
||||
photoCount: this.getPhotoCountForTier(),
|
||||
itemsByCategory: {
|
||||
clothing: [],
|
||||
accessories: [],
|
||||
toys: [],
|
||||
environment: []
|
||||
}
|
||||
};
|
||||
|
||||
// Collect clothing
|
||||
for (const [key, value] of Object.entries(this.inventory.clothing || {})) {
|
||||
if (value && value !== 'none') {
|
||||
summary.itemsByCategory.clothing.push(`${this.formatItemName(key)}: ${value}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Collect accessories
|
||||
for (const [key, value] of Object.entries(this.inventory.accessories || {})) {
|
||||
if (value && value !== 'none') {
|
||||
summary.itemsByCategory.accessories.push(`${this.formatItemName(key)}: ${value}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Collect toys
|
||||
for (const [key, value] of Object.entries(this.inventory.toys || {})) {
|
||||
if (value && value !== 'none') {
|
||||
summary.itemsByCategory.toys.push(`${this.formatItemName(key)}: ${value}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Collect environment
|
||||
for (const [key, value] of Object.entries(this.inventory.environment || {})) {
|
||||
if (value === true) {
|
||||
summary.itemsByCategory.environment.push(this.formatItemName(key));
|
||||
}
|
||||
}
|
||||
|
||||
return summary;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format item name for display
|
||||
*/
|
||||
formatItemName(key) {
|
||||
const names = {
|
||||
panties: 'Panties',
|
||||
bras: 'Bras',
|
||||
dresses: 'Dresses',
|
||||
skirts: 'Skirts',
|
||||
pantyhose: 'Pantyhose/Stockings',
|
||||
heels: 'Heels',
|
||||
wigs: 'Wigs',
|
||||
lingerie: 'Lingerie',
|
||||
makeup: 'Makeup',
|
||||
jewelry: 'Jewelry',
|
||||
nailPolish: 'Nail Polish',
|
||||
dildos: 'Dildos',
|
||||
plugs: 'Butt Plugs',
|
||||
chastity: 'Chastity Device',
|
||||
restraints: 'Restraints',
|
||||
gags: 'Gags',
|
||||
nippleClamps: 'Nipple Clamps',
|
||||
mirror: 'Mirror',
|
||||
fullMirror: 'Full-Length Mirror',
|
||||
privateSpace: 'Private Space',
|
||||
phoneStand: 'Phone/Camera Stand'
|
||||
};
|
||||
|
||||
return names[key] || key;
|
||||
}
|
||||
}
|
||||
|
||||
// Make available globally
|
||||
window.InventoryManager = InventoryManager;
|
||||
|
|
@ -0,0 +1,200 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Academy UI Integration Test</title>
|
||||
<link rel="stylesheet" href="src/styles/academy-ui.css">
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: #1a1a1a;
|
||||
color: #e0e0e0;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
color: #ff006e;
|
||||
margin-bottom: 30px;
|
||||
font-size: 2.5em;
|
||||
}
|
||||
|
||||
.test-section {
|
||||
background: #2a2a2a;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.test-section h2 {
|
||||
color: #8338ec;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(135deg, #ff006e, #8338ec);
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.status-box {
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.status-box.success {
|
||||
background: rgba(0, 255, 136, 0.1);
|
||||
border: 1px solid #00ff88;
|
||||
color: #00ff88;
|
||||
}
|
||||
|
||||
.status-box.info {
|
||||
background: rgba(131, 56, 236, 0.1);
|
||||
border: 1px solid #8338ec;
|
||||
color: #8338ec;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🎓 Academy UI Integration Test</h1>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>Manual Tests</h2>
|
||||
<div class="button-group">
|
||||
<button onclick="setupSampleData()">📦 Setup Sample Data</button>
|
||||
<button onclick="showLevelSelect()">📋 Show Level Select</button>
|
||||
<button onclick="testCheckpointModal(1)">🎯 Test L1 Checkpoint</button>
|
||||
<button onclick="testCheckpointModal(5)">⚡ Test L5 Checkpoint</button>
|
||||
<button onclick="testCheckpointModal(10)">💫 Test L10 Checkpoint</button>
|
||||
<button onclick="resetData()">🔄 Reset Data</button>
|
||||
</div>
|
||||
<div id="output"></div>
|
||||
</div>
|
||||
|
||||
<!-- Academy Setup Container (where UI will render) -->
|
||||
<div id="academy-setup"></div>
|
||||
</div>
|
||||
|
||||
<!-- Load Dependencies -->
|
||||
<script src="src/data/gameData.js"></script>
|
||||
<script src="src/features/academy/campaignManager.js"></script>
|
||||
<script src="src/features/academy/preferenceManager.js"></script>
|
||||
<script src="src/features/academy/libraryManager.js"></script>
|
||||
<script src="src/features/academy/academyUI.js"></script>
|
||||
|
||||
<script>
|
||||
function setupSampleData() {
|
||||
console.log('📦 Setting up sample data...');
|
||||
|
||||
// Unlock first 10 levels
|
||||
for (let i = 1; i <= 10; i++) {
|
||||
if (i <= 7) {
|
||||
// Complete first 7 levels
|
||||
window.campaignManager.startLevel(i);
|
||||
window.campaignManager.completeLevel(i, { duration: 600 });
|
||||
}
|
||||
}
|
||||
|
||||
// Add sample preferences
|
||||
window.preferenceManager.updateCategoryPreferences('contentThemes', {
|
||||
edging: true,
|
||||
denial: true,
|
||||
pov: true
|
||||
}, 1);
|
||||
|
||||
window.preferenceManager.updateCategoryPreferences('intensity', {
|
||||
visualIntensity: 4,
|
||||
mentalIntensity: 3,
|
||||
taskDifficulty: 3
|
||||
}, 5);
|
||||
|
||||
// Add sample library items
|
||||
const sampleFiles = [
|
||||
{ path: 'assets/pornstars/video1.mp4', tags: ['solo', 'pov', 'professional'], rating: 5 },
|
||||
{ path: 'assets/pornstars/video2.mp4', tags: ['couples', 'hd', 'hardcore'], rating: 4 },
|
||||
{ path: 'assets/feet/video1.mp4', tags: ['feet', 'pov', 'fetish'], rating: 4 },
|
||||
{ path: 'assets/hentai/video1.mp4', tags: ['hentai', 'animated'], rating: 3 },
|
||||
{ path: 'assets/BBC/video1.mp4', tags: ['bbc', 'couples'], rating: 5 }
|
||||
];
|
||||
|
||||
sampleFiles.forEach(file => {
|
||||
window.libraryManager.addMediaItem(file.path, {
|
||||
tags: file.tags,
|
||||
rating: file.rating
|
||||
});
|
||||
});
|
||||
|
||||
showOutput('✅ Sample data created: 7 levels complete, preferences set, 5 media items added', 'success');
|
||||
}
|
||||
|
||||
function showLevelSelect() {
|
||||
console.log('📋 Showing level select screen...');
|
||||
window.academyUI.showLevelSelect();
|
||||
showOutput('✅ Level select screen displayed', 'success');
|
||||
}
|
||||
|
||||
function testCheckpointModal(levelNum) {
|
||||
console.log(`⚡ Testing checkpoint modal for Level ${levelNum}...`);
|
||||
window.academyUI.showCheckpointModal(levelNum);
|
||||
showOutput(`✅ Checkpoint modal shown for Level ${levelNum}`, 'success');
|
||||
}
|
||||
|
||||
function resetData() {
|
||||
if (confirm('Reset all academy data?')) {
|
||||
window.campaignManager.resetProgress();
|
||||
window.preferenceManager.resetPreferences();
|
||||
window.libraryManager.resetLibrary();
|
||||
|
||||
// Clear the academy setup container
|
||||
const setupContainer = document.getElementById('academy-setup');
|
||||
if (setupContainer) {
|
||||
setupContainer.innerHTML = '';
|
||||
}
|
||||
|
||||
showOutput('🔄 All data reset', 'info');
|
||||
}
|
||||
}
|
||||
|
||||
function showOutput(message, type = 'info') {
|
||||
const outputDiv = document.getElementById('output');
|
||||
outputDiv.innerHTML = `<div class="status-box ${type}">${message}</div>`;
|
||||
}
|
||||
|
||||
// Initialize on load
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
showOutput('🎓 Academy UI Integration ready. Click "Setup Sample Data" to begin.', 'info');
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,293 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Campaign Manager Test - Subphase 1.1</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
max-width: 800px;
|
||||
margin: 50px auto;
|
||||
padding: 20px;
|
||||
background: #1a1a1a;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
.test-section {
|
||||
background: #2a2a2a;
|
||||
padding: 20px;
|
||||
margin: 20px 0;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #333;
|
||||
}
|
||||
h1 { color: #ff006e; }
|
||||
h2 { color: #8338ec; margin-top: 0; }
|
||||
button {
|
||||
background: linear-gradient(135deg, #ff006e, #8338ec);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
margin: 5px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
}
|
||||
button:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(255, 0, 110, 0.4);
|
||||
}
|
||||
.status {
|
||||
padding: 10px;
|
||||
margin: 10px 0;
|
||||
border-radius: 4px;
|
||||
background: #333;
|
||||
}
|
||||
.success { border-left: 4px solid #06ffa5; }
|
||||
.error { border-left: 4px solid #ff006e; }
|
||||
.info { border-left: 4px solid #8338ec; }
|
||||
pre {
|
||||
background: #0a0a0a;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.test-result {
|
||||
margin: 10px 0;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.pass { background: #06ffa533; border-left: 4px solid #06ffa5; }
|
||||
.fail { background: #ff006e33; border-left: 4px solid #ff006e; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>🎓 Campaign Manager Test - Subphase 1.1</h1>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>Campaign Progression System</h2>
|
||||
<p>Testing basic level unlocking, completion tracking, and failure states.</p>
|
||||
|
||||
<button onclick="runAllTests()">🧪 Run All Tests</button>
|
||||
<button onclick="resetProgress()">🔄 Reset Progress</button>
|
||||
<button onclick="showProgress()">📊 Show Progress</button>
|
||||
|
||||
<div id="test-results"></div>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>Manual Testing</h2>
|
||||
<button onclick="startLevel(1)">Start Level 1</button>
|
||||
<button onclick="completeLevel(1)">Complete Level 1</button>
|
||||
<button onclick="failLevel(1, 'cumming')">Fail Level 1 (cumming)</button>
|
||||
<button onclick="startLevel(2)">Start Level 2 (should fail if L1 not complete)</button>
|
||||
|
||||
<div id="manual-results"></div>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>Current Progress</h2>
|
||||
<div id="progress-display"></div>
|
||||
</div>
|
||||
|
||||
<!-- Load dependencies -->
|
||||
<script src="src/data/gameData.js"></script>
|
||||
<script src="src/features/academy/campaignManager.js"></script>
|
||||
<script>
|
||||
// campaignManager is now available globally
|
||||
|
||||
// Test functions
|
||||
window.runAllTests = function() {
|
||||
const results = document.getElementById('test-results');
|
||||
results.innerHTML = '<h3>Running Tests...</h3>';
|
||||
|
||||
const tests = [];
|
||||
|
||||
// Test 1: Initial state
|
||||
campaignManager.resetProgress();
|
||||
const stats = campaignManager.getProgressStats();
|
||||
tests.push({
|
||||
name: 'Initial state - L1 unlocked, L2 locked',
|
||||
pass: stats.currentLevel === 1 && stats.highestUnlockedLevel === 1,
|
||||
details: `Current: ${stats.currentLevel}, Highest: ${stats.highestUnlockedLevel}`
|
||||
});
|
||||
|
||||
// Test 2: Can start L1
|
||||
const l1Start = campaignManager.startLevel(1);
|
||||
tests.push({
|
||||
name: 'Can start Level 1',
|
||||
pass: l1Start !== null && l1Start.levelNum === 1,
|
||||
details: l1Start ? `Started ${l1Start.arc} arc` : 'Failed to start'
|
||||
});
|
||||
|
||||
// Test 3: Cannot start L2 before completing L1
|
||||
const l2Locked = campaignManager.startLevel(2);
|
||||
tests.push({
|
||||
name: 'Cannot start Level 2 (locked)',
|
||||
pass: l2Locked === null,
|
||||
details: l2Locked === null ? 'Correctly blocked' : 'ERROR: Started locked level'
|
||||
});
|
||||
|
||||
// Test 4: Complete L1
|
||||
const l1Complete = campaignManager.completeLevel(1, { duration: 300 });
|
||||
tests.push({
|
||||
name: 'Complete Level 1 unlocks Level 2',
|
||||
pass: l1Complete.nextLevelUnlocked && l1Complete.nextLevel === 2,
|
||||
details: `Next level: ${l1Complete.nextLevel}, Unlocked: ${l1Complete.nextLevelUnlocked}`
|
||||
});
|
||||
|
||||
// Test 5: Can now start L2
|
||||
const l2Start = campaignManager.startLevel(2);
|
||||
tests.push({
|
||||
name: 'Can start Level 2 after completing L1',
|
||||
pass: l2Start !== null && l2Start.levelNum === 2,
|
||||
details: l2Start ? `Started level ${l2Start.levelNum}` : 'Failed to start'
|
||||
});
|
||||
|
||||
// Test 6: Fail L2
|
||||
campaignManager.failLevel(2, 'cumming');
|
||||
const statsAfterFail = campaignManager.getProgressStats();
|
||||
tests.push({
|
||||
name: 'Failing L2 allows restart',
|
||||
pass: statsAfterFail.failedAttempts > 0,
|
||||
details: `Failed attempts: ${statsAfterFail.failedAttempts}`
|
||||
});
|
||||
|
||||
// Test 7: Progress persists
|
||||
const beforeReload = campaignManager.getProgressStats();
|
||||
// Simulate reload by getting fresh data
|
||||
const savedData = JSON.parse(localStorage.getItem('webGame-data'));
|
||||
tests.push({
|
||||
name: 'Progress persists in localStorage',
|
||||
pass: savedData.academyProgress !== undefined && savedData.academyProgress.highestUnlockedLevel === 2,
|
||||
details: `Highest unlocked: ${savedData.academyProgress.highestUnlockedLevel}`
|
||||
});
|
||||
|
||||
// Test 8: Arc detection
|
||||
const l1Arc = campaignManager.getArcForLevel(1);
|
||||
const l6Arc = campaignManager.getArcForLevel(6);
|
||||
tests.push({
|
||||
name: 'Arc detection works',
|
||||
pass: l1Arc === 'Foundation' && l6Arc === 'Feature Discovery',
|
||||
details: `L1: ${l1Arc}, L6: ${l6Arc}`
|
||||
});
|
||||
|
||||
// Test 9: Checkpoint detection
|
||||
const isCheckpoint1 = campaignManager.isCheckpointLevel(1);
|
||||
const isCheckpoint2 = campaignManager.isCheckpointLevel(2);
|
||||
const isCheckpoint5 = campaignManager.isCheckpointLevel(5);
|
||||
tests.push({
|
||||
name: 'Checkpoint detection works',
|
||||
pass: isCheckpoint1 && !isCheckpoint2 && isCheckpoint5,
|
||||
details: `L1: ${isCheckpoint1}, L2: ${isCheckpoint2}, L5: ${isCheckpoint5}`
|
||||
});
|
||||
|
||||
// Display results
|
||||
let html = '<h3>Test Results</h3>';
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
tests.forEach(test => {
|
||||
if (test.pass) passed++;
|
||||
else failed++;
|
||||
|
||||
html += `
|
||||
<div class="test-result ${test.pass ? 'pass' : 'fail'}">
|
||||
<strong>${test.pass ? '✅' : '❌'} ${test.name}</strong><br>
|
||||
<small>${test.details}</small>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
html += `<div class="status ${failed === 0 ? 'success' : 'error'}">
|
||||
<strong>Results: ${passed}/${tests.length} tests passed</strong>
|
||||
</div>`;
|
||||
|
||||
results.innerHTML = html;
|
||||
|
||||
// Update progress display
|
||||
showProgress();
|
||||
};
|
||||
|
||||
window.resetProgress = function() {
|
||||
campaignManager.resetProgress();
|
||||
document.getElementById('manual-results').innerHTML =
|
||||
'<div class="status success">✅ Progress reset to Level 1</div>';
|
||||
showProgress();
|
||||
};
|
||||
|
||||
window.startLevel = function(levelNum) {
|
||||
const result = campaignManager.startLevel(levelNum);
|
||||
const resultsDiv = document.getElementById('manual-results');
|
||||
|
||||
if (result) {
|
||||
resultsDiv.innerHTML = `
|
||||
<div class="status success">
|
||||
✅ Started Level ${result.levelNum}<br>
|
||||
Arc: ${result.arc}<br>
|
||||
Checkpoint: ${result.isCheckpoint}
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
resultsDiv.innerHTML = `
|
||||
<div class="status error">
|
||||
❌ Cannot start Level ${levelNum} - Level is locked
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
showProgress();
|
||||
};
|
||||
|
||||
window.completeLevel = function(levelNum) {
|
||||
const result = campaignManager.completeLevel(levelNum, { duration: 300 });
|
||||
const resultsDiv = document.getElementById('manual-results');
|
||||
|
||||
resultsDiv.innerHTML = `
|
||||
<div class="status success">
|
||||
✅ Level ${result.levelCompleted} completed!<br>
|
||||
${result.nextLevelUnlocked ? `🔓 Level ${result.nextLevel} unlocked!` : ''}<br>
|
||||
${result.arcComplete ? `🎉 ${result.completedArc} Arc complete!` : ''}
|
||||
</div>
|
||||
`;
|
||||
showProgress();
|
||||
};
|
||||
|
||||
window.failLevel = function(levelNum, reason) {
|
||||
campaignManager.failLevel(levelNum, reason);
|
||||
const resultsDiv = document.getElementById('manual-results');
|
||||
|
||||
resultsDiv.innerHTML = `
|
||||
<div class="status error">
|
||||
❌ Level ${levelNum} failed (${reason})<br>
|
||||
You can restart this level.
|
||||
</div>
|
||||
`;
|
||||
showProgress();
|
||||
};
|
||||
|
||||
window.showProgress = function() {
|
||||
const stats = campaignManager.getProgressStats();
|
||||
const progress = window.gameData.academyProgress;
|
||||
|
||||
document.getElementById('progress-display').innerHTML = `
|
||||
<pre>${JSON.stringify(stats, null, 2)}</pre>
|
||||
<h3>Full Progress Data:</h3>
|
||||
<pre>${JSON.stringify(progress, null, 2)}</pre>
|
||||
`;
|
||||
};
|
||||
|
||||
// Initial display
|
||||
showProgress();
|
||||
|
||||
console.log('✅ Campaign Manager Test Page Ready');
|
||||
console.log('📝 Run runAllTests() to execute automated tests');
|
||||
</script>
|
||||
|
||||
<!-- Electron preload (if needed) -->
|
||||
<script>
|
||||
// Check if running in Electron
|
||||
if (typeof require !== 'undefined') {
|
||||
console.log('🖥️ Running in Electron');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,609 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Library Manager Test Suite</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: #1a1a1a;
|
||||
color: #e0e0e0;
|
||||
padding: 20px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
color: #ff006e;
|
||||
margin-bottom: 30px;
|
||||
font-size: 2.5em;
|
||||
}
|
||||
|
||||
.test-section {
|
||||
background: #2a2a2a;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.test-section h2 {
|
||||
color: #8338ec;
|
||||
margin-bottom: 15px;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.test-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.test-result {
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
background: #1a1a1a;
|
||||
border-left: 4px solid #666;
|
||||
}
|
||||
|
||||
.test-result.pass {
|
||||
border-left-color: #00ff88;
|
||||
background: rgba(0, 255, 136, 0.1);
|
||||
}
|
||||
|
||||
.test-result.fail {
|
||||
border-left-color: #ff006e;
|
||||
background: rgba(255, 0, 110, 0.1);
|
||||
}
|
||||
|
||||
.test-result h3 {
|
||||
font-size: 1em;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.test-result.pass h3 { color: #00ff88; }
|
||||
.test-result.fail h3 { color: #ff006e; }
|
||||
|
||||
.test-result p {
|
||||
font-size: 0.9em;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(135deg, #ff006e, #8338ec);
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
button:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.status-box {
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.status-box.success {
|
||||
background: rgba(0, 255, 136, 0.1);
|
||||
border: 1px solid #00ff88;
|
||||
color: #00ff88;
|
||||
}
|
||||
|
||||
.status-box.error {
|
||||
background: rgba(255, 0, 110, 0.1);
|
||||
border: 1px solid #ff006e;
|
||||
color: #ff006e;
|
||||
}
|
||||
|
||||
.status-box.info {
|
||||
background: rgba(131, 56, 236, 0.1);
|
||||
border: 1px solid #8338ec;
|
||||
color: #8338ec;
|
||||
}
|
||||
|
||||
.json-display {
|
||||
background: #0a0a0a;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
overflow-x: auto;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.85em;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.media-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
gap: 15px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.media-card {
|
||||
background: #1a1a1a;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
border: 1px solid #333;
|
||||
}
|
||||
|
||||
.media-card h4 {
|
||||
color: #ff006e;
|
||||
font-size: 0.9em;
|
||||
margin-bottom: 10px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tag-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.tag {
|
||||
background: rgba(131, 56, 236, 0.3);
|
||||
padding: 3px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75em;
|
||||
color: #8338ec;
|
||||
}
|
||||
|
||||
.rating {
|
||||
color: #ffb700;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>📚 Library Manager Test Suite</h1>
|
||||
|
||||
<!-- Automated Tests Section -->
|
||||
<div class="test-section">
|
||||
<h2>Automated Tests</h2>
|
||||
<button onclick="runAllTests()">🧪 Run All Tests</button>
|
||||
<div id="test-results" class="test-grid"></div>
|
||||
</div>
|
||||
|
||||
<!-- Manual Testing Section -->
|
||||
<div class="test-section">
|
||||
<h2>Manual Testing</h2>
|
||||
<div class="button-group">
|
||||
<button onclick="syncWithLibrary()">🔄 Sync with Existing Library</button>
|
||||
<button onclick="addSampleMedia()">➕ Add Sample Media (Test Only)</button>
|
||||
<button onclick="testBulkTagging()">🏷️ Test Bulk Tagging</button>
|
||||
<button onclick="testSearch()">🔍 Test Search</button>
|
||||
<button onclick="testRatings()">⭐ Test Ratings</button>
|
||||
<button onclick="testCollections()">📁 Test Collections</button>
|
||||
<button onclick="showStats()">📈 Show Stats</button>
|
||||
<button onclick="showAllMedia()">📚 Show All Media</button>
|
||||
<button onclick="resetLibrary()">🔄 Reset Library</button>
|
||||
</div>
|
||||
<div id="manual-output"></div>
|
||||
</div>
|
||||
|
||||
<!-- Library Display -->
|
||||
<div class="test-section">
|
||||
<h2>Current Library State</h2>
|
||||
<div id="library-display" class="json-display"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Load Dependencies -->
|
||||
<script src="src/data/gameData.js"></script>
|
||||
<script src="src/features/academy/libraryManager.js"></script>
|
||||
|
||||
<script>
|
||||
// Test runner
|
||||
function runAllTests() {
|
||||
const tests = [
|
||||
testInitialization,
|
||||
testAddMedia,
|
||||
testTagging,
|
||||
testBulkTagging,
|
||||
testRating,
|
||||
testFavorites,
|
||||
testSearch,
|
||||
testCollections,
|
||||
testStats,
|
||||
testPersistence
|
||||
];
|
||||
|
||||
const resultsDiv = document.getElementById('test-results');
|
||||
resultsDiv.innerHTML = '';
|
||||
|
||||
tests.forEach(test => {
|
||||
const result = test();
|
||||
const resultDiv = document.createElement('div');
|
||||
resultDiv.className = `test-result ${result.pass ? 'pass' : 'fail'}`;
|
||||
resultDiv.innerHTML = `
|
||||
<h3>${result.pass ? '✓' : '✗'} ${result.name}</h3>
|
||||
<p>${result.message}</p>
|
||||
`;
|
||||
resultsDiv.appendChild(resultDiv);
|
||||
});
|
||||
}
|
||||
|
||||
// Test 1: Initialization
|
||||
function testInitialization() {
|
||||
const library = window.libraryManager.getLibrary();
|
||||
const hasRequiredFields = library.media && library.tags && library.collections;
|
||||
|
||||
return {
|
||||
name: 'Initialization Test',
|
||||
pass: hasRequiredFields && library.version === 1,
|
||||
message: hasRequiredFields ? 'Library initialized with all required structures' : 'Missing fields'
|
||||
};
|
||||
}
|
||||
|
||||
// Test 2: Add Media
|
||||
function testAddMedia() {
|
||||
const success = window.libraryManager.addMediaItem('assets/test/video1.mp4', {
|
||||
tags: ['edging', 'pov', 'hd'],
|
||||
rating: 4
|
||||
});
|
||||
|
||||
const library = window.libraryManager.getLibrary();
|
||||
const item = library.media['assets/test/video1.mp4'];
|
||||
|
||||
return {
|
||||
name: 'Add Media Test',
|
||||
pass: success && item && item.tags.length === 3,
|
||||
message: success ? 'Media item added with tags and rating' : 'Failed to add media'
|
||||
};
|
||||
}
|
||||
|
||||
// Test 3: Tagging
|
||||
function testTagging() {
|
||||
window.libraryManager.addMediaItem('assets/test/video2.mp4');
|
||||
window.libraryManager.addTags('assets/test/video2.mp4', ['femdom', 'worship']);
|
||||
|
||||
const library = window.libraryManager.getLibrary();
|
||||
const item = library.media['assets/test/video2.mp4'];
|
||||
|
||||
return {
|
||||
name: 'Tagging Test',
|
||||
pass: item.tags.includes('femdom') && item.tags.includes('worship'),
|
||||
message: 'Tags added successfully'
|
||||
};
|
||||
}
|
||||
|
||||
// Test 4: Bulk Tagging
|
||||
function testBulkTagging() {
|
||||
const files = ['assets/test/video3.mp4', 'assets/test/video4.mp4', 'assets/test/video5.mp4'];
|
||||
const result = window.libraryManager.bulkAddTags(files, ['compilation', 'pmv']);
|
||||
|
||||
return {
|
||||
name: 'Bulk Tagging Test',
|
||||
pass: result.filesTagged === 3,
|
||||
message: `Bulk tagged ${result.filesTagged} files with ${result.tags.length} tags`
|
||||
};
|
||||
}
|
||||
|
||||
// Test 5: Rating
|
||||
function testRating() {
|
||||
window.libraryManager.setRating('assets/test/video1.mp4', 5);
|
||||
const library = window.libraryManager.getLibrary();
|
||||
const rating = library.media['assets/test/video1.mp4'].rating;
|
||||
|
||||
return {
|
||||
name: 'Rating Test',
|
||||
pass: rating === 5,
|
||||
message: `Rating set to ${rating}/5`
|
||||
};
|
||||
}
|
||||
|
||||
// Test 6: Favorites
|
||||
function testFavorites() {
|
||||
window.libraryManager.addToFavorites('assets/test/video1.mp4');
|
||||
const library = window.libraryManager.getLibrary();
|
||||
|
||||
return {
|
||||
name: 'Favorites Test',
|
||||
pass: library.favorites.includes('assets/test/video1.mp4'),
|
||||
message: 'Item added to favorites'
|
||||
};
|
||||
}
|
||||
|
||||
// Test 7: Search
|
||||
function testSearch() {
|
||||
const results = window.libraryManager.searchMedia({
|
||||
anyTags: ['edging', 'femdom'],
|
||||
minRating: 3
|
||||
});
|
||||
|
||||
return {
|
||||
name: 'Search Test',
|
||||
pass: results.length > 0,
|
||||
message: `Found ${results.length} items matching search criteria`
|
||||
};
|
||||
}
|
||||
|
||||
// Test 8: Collections
|
||||
function testCollections() {
|
||||
window.libraryManager.createCollection('My Playlist', [
|
||||
'assets/test/video1.mp4',
|
||||
'assets/test/video2.mp4'
|
||||
]);
|
||||
|
||||
const items = window.libraryManager.getCollection('My Playlist');
|
||||
|
||||
return {
|
||||
name: 'Collections Test',
|
||||
pass: items.length === 2,
|
||||
message: `Collection created with ${items.length} items`
|
||||
};
|
||||
}
|
||||
|
||||
// Test 9: Stats
|
||||
function testStats() {
|
||||
const stats = window.libraryManager.getLibraryStats();
|
||||
|
||||
return {
|
||||
name: 'Statistics Test',
|
||||
pass: stats.totalItems > 0 && stats.hasOwnProperty('averageRating'),
|
||||
message: `${stats.totalItems} items, ${stats.taggedItems} tagged, avg rating ${stats.averageRating}`
|
||||
};
|
||||
}
|
||||
|
||||
// Test 10: Persistence
|
||||
function testPersistence() {
|
||||
window.libraryManager.addMediaItem('assets/test/persistence.mp4', { rating: 3 });
|
||||
|
||||
const savedData = JSON.parse(localStorage.getItem('webGame-data'));
|
||||
const exists = savedData.academyLibrary.media['assets/test/persistence.mp4'] !== undefined;
|
||||
|
||||
return {
|
||||
name: 'Persistence Test',
|
||||
pass: exists,
|
||||
message: exists ? 'Library persists to localStorage' : 'Persistence failed'
|
||||
};
|
||||
}
|
||||
|
||||
// Manual testing functions
|
||||
function syncWithLibrary() {
|
||||
const result = window.libraryManager.syncWithExistingLibrary();
|
||||
const outputDiv = document.getElementById('manual-output');
|
||||
outputDiv.innerHTML = `<div class="status-box success">
|
||||
✅ Synced with existing library:<br>
|
||||
- Total videos: ${result.total}<br>
|
||||
- New videos: ${result.new}<br>
|
||||
- Existing videos: ${result.existing}
|
||||
</div>`;
|
||||
|
||||
// Show the media
|
||||
showAllMedia();
|
||||
}
|
||||
|
||||
function addSampleMedia() {
|
||||
const sampleFiles = [
|
||||
{ path: 'assets/pornstars/riley-reid-1.mp4', tags: ['solo', 'pov', 'professional'], rating: 5 },
|
||||
{ path: 'assets/pornstars/mia-khalifa-2.mp4', tags: ['group', 'hd', 'hardcore'], rating: 4 },
|
||||
{ path: 'assets/feet/footjob-1.mp4', tags: ['feet', 'pov', 'fetish'], rating: 4 },
|
||||
{ path: 'assets/hentai/anime-1.mp4', tags: ['hentai', 'animated', 'mindbreak'], rating: 3 },
|
||||
{ path: 'assets/BBC/interracial-1.mp4', tags: ['bbc', 'couples', 'intense'], rating: 5 }
|
||||
];
|
||||
|
||||
sampleFiles.forEach(file => {
|
||||
window.libraryManager.addMediaItem(file.path, {
|
||||
tags: file.tags,
|
||||
rating: file.rating
|
||||
});
|
||||
});
|
||||
|
||||
const outputDiv = document.getElementById('manual-output');
|
||||
outputDiv.innerHTML = `<div class="status-box success">
|
||||
Added ${sampleFiles.length} sample media items to library
|
||||
</div>`;
|
||||
|
||||
showAllMedia();
|
||||
}
|
||||
|
||||
function testBulkTagging() {
|
||||
const library = window.libraryManager.getLibrary();
|
||||
const allFiles = Object.keys(library.media);
|
||||
|
||||
if (allFiles.length === 0) {
|
||||
const outputDiv = document.getElementById('manual-output');
|
||||
outputDiv.innerHTML = `<div class="status-box error">No media in library. Add sample media first.</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
const result = window.libraryManager.bulkAddTags(allFiles.slice(0, 3), ['test-tag', 'bulk-tagged']);
|
||||
|
||||
const outputDiv = document.getElementById('manual-output');
|
||||
outputDiv.innerHTML = `<div class="status-box success">
|
||||
Bulk tagged ${result.filesTagged} files with tags: ${result.tags.join(', ')}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function testSearch() {
|
||||
const results = window.libraryManager.searchMedia({
|
||||
anyTags: ['pov', 'solo'],
|
||||
minRating: 3,
|
||||
sortBy: 'rating',
|
||||
limit: 5
|
||||
});
|
||||
|
||||
const outputDiv = document.getElementById('manual-output');
|
||||
|
||||
if (results.length === 0) {
|
||||
outputDiv.innerHTML = `<div class="status-box error">No results found. Add sample media first.</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
let html = `<div class="status-box success">Found ${results.length} items</div>`;
|
||||
html += '<div class="media-grid">';
|
||||
|
||||
results.forEach(item => {
|
||||
html += `
|
||||
<div class="media-card">
|
||||
<h4>${item.filePath.split('/').pop()}</h4>
|
||||
<div class="rating">⭐ ${item.rating}/5</div>
|
||||
<div class="tag-list">
|
||||
${item.tags.map(tag => `<span class="tag">${tag}</span>`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
html += '</div>';
|
||||
outputDiv.innerHTML = html;
|
||||
}
|
||||
|
||||
function testRatings() {
|
||||
const library = window.libraryManager.getLibrary();
|
||||
const allFiles = Object.keys(library.media);
|
||||
|
||||
if (allFiles.length === 0) {
|
||||
const outputDiv = document.getElementById('manual-output');
|
||||
outputDiv.innerHTML = `<div class="status-box error">No media in library. Add sample media first.</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
// Set random ratings
|
||||
allFiles.forEach(file => {
|
||||
window.libraryManager.setRating(file, Math.floor(Math.random() * 5) + 1);
|
||||
});
|
||||
|
||||
const stats = window.libraryManager.getLibraryStats();
|
||||
const outputDiv = document.getElementById('manual-output');
|
||||
outputDiv.innerHTML = `<div class="status-box success">
|
||||
Rated ${stats.ratedItems} items<br>
|
||||
Average rating: ${stats.averageRating}/5
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function testCollections() {
|
||||
const library = window.libraryManager.getLibrary();
|
||||
const allFiles = Object.keys(library.media);
|
||||
|
||||
if (allFiles.length < 3) {
|
||||
const outputDiv = document.getElementById('manual-output');
|
||||
outputDiv.innerHTML = `<div class="status-box error">Need at least 3 items. Add sample media first.</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
window.libraryManager.createCollection('Test Collection', allFiles.slice(0, 3));
|
||||
const items = window.libraryManager.getCollection('Test Collection');
|
||||
|
||||
const outputDiv = document.getElementById('manual-output');
|
||||
outputDiv.innerHTML = `<div class="status-box success">
|
||||
Created collection with ${items.length} items
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function showStats() {
|
||||
const stats = window.libraryManager.getLibraryStats();
|
||||
const outputDiv = document.getElementById('manual-output');
|
||||
outputDiv.innerHTML = `<div class="status-box info">Library Statistics:</div>` +
|
||||
`<div class="json-display">${JSON.stringify(stats, null, 2)}</div>`;
|
||||
}
|
||||
|
||||
function showAllMedia() {
|
||||
const library = window.libraryManager.getLibrary();
|
||||
const allMedia = Object.values(library.media);
|
||||
|
||||
if (allMedia.length === 0) {
|
||||
const outputDiv = document.getElementById('manual-output');
|
||||
outputDiv.innerHTML = `<div class="status-box info">Library is empty. Add some media items.</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
const outputDiv = document.getElementById('manual-output');
|
||||
let html = `<div class="status-box info">Total items: ${allMedia.length}</div>`;
|
||||
html += '<div class="media-grid">';
|
||||
|
||||
allMedia.forEach(item => {
|
||||
html += `
|
||||
<div class="media-card">
|
||||
<h4>${item.filePath.split('/').pop()}</h4>
|
||||
<div class="rating">⭐ ${item.rating}/5</div>
|
||||
<div class="tag-list">
|
||||
${item.tags.map(tag => `<span class="tag">${tag}</span>`).join('')}
|
||||
</div>
|
||||
<div style="margin-top: 8px; font-size: 0.8em; color: #888;">
|
||||
Plays: ${item.timesPlayed || 0}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
html += '</div>';
|
||||
outputDiv.innerHTML = html;
|
||||
}
|
||||
|
||||
function resetLibrary() {
|
||||
if (confirm('Reset entire library? This will delete all media items and tags.')) {
|
||||
window.libraryManager.resetLibrary();
|
||||
const outputDiv = document.getElementById('manual-output');
|
||||
outputDiv.innerHTML = `<div class="status-box success">Library reset to empty state</div>`;
|
||||
updateLibraryDisplay();
|
||||
}
|
||||
}
|
||||
|
||||
function updateLibraryDisplay() {
|
||||
const library = window.libraryManager.getLibrary();
|
||||
document.getElementById('library-display').textContent = JSON.stringify(library, null, 2);
|
||||
}
|
||||
|
||||
// Initialize on load
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
updateLibraryDisplay();
|
||||
|
||||
// Auto-sync with existing library if available
|
||||
if (window.desktopFileManager) {
|
||||
setTimeout(() => {
|
||||
const result = window.libraryManager.syncWithExistingLibrary();
|
||||
if (result.total > 0) {
|
||||
const outputDiv = document.getElementById('manual-output');
|
||||
outputDiv.innerHTML = `<div class="status-box info">
|
||||
📹 Auto-synced ${result.total} videos from existing library.
|
||||
Use "🔄 Sync with Existing Library" to refresh.
|
||||
</div>`;
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// Auto-refresh progress display every 2 seconds
|
||||
setInterval(updateLibraryDisplay, 2000);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,526 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Preference Manager Test Suite</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: #1a1a1a;
|
||||
color: #e0e0e0;
|
||||
padding: 20px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
color: #ff006e;
|
||||
margin-bottom: 30px;
|
||||
font-size: 2.5em;
|
||||
}
|
||||
|
||||
.test-section {
|
||||
background: #2a2a2a;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.test-section h2 {
|
||||
color: #8338ec;
|
||||
margin-bottom: 15px;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.test-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.test-result {
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
background: #1a1a1a;
|
||||
border-left: 4px solid #666;
|
||||
}
|
||||
|
||||
.test-result.pass {
|
||||
border-left-color: #00ff88;
|
||||
background: rgba(0, 255, 136, 0.1);
|
||||
}
|
||||
|
||||
.test-result.fail {
|
||||
border-left-color: #ff006e;
|
||||
background: rgba(255, 0, 110, 0.1);
|
||||
}
|
||||
|
||||
.test-result h3 {
|
||||
font-size: 1em;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.test-result.pass h3 { color: #00ff88; }
|
||||
.test-result.fail h3 { color: #ff006e; }
|
||||
|
||||
.test-result p {
|
||||
font-size: 0.9em;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(135deg, #ff006e, #8338ec);
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
button:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.status-box {
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.status-box.success {
|
||||
background: rgba(0, 255, 136, 0.1);
|
||||
border: 1px solid #00ff88;
|
||||
color: #00ff88;
|
||||
}
|
||||
|
||||
.status-box.error {
|
||||
background: rgba(255, 0, 110, 0.1);
|
||||
border: 1px solid #ff006e;
|
||||
color: #ff006e;
|
||||
}
|
||||
|
||||
.status-box.info {
|
||||
background: rgba(131, 56, 236, 0.1);
|
||||
border: 1px solid #8338ec;
|
||||
color: #8338ec;
|
||||
}
|
||||
|
||||
.json-display {
|
||||
background: #0a0a0a;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
overflow-x: auto;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.85em;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.preference-editor {
|
||||
background: #2a2a2a;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.pref-category {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.pref-category h3 {
|
||||
color: #ff006e;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.checkbox-group {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
padding: 5px;
|
||||
border-radius: 4px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.checkbox-label:hover {
|
||||
background: rgba(255, 0, 110, 0.1);
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.slider-container {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.slider-label {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
input[type="range"] {
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
border-radius: 3px;
|
||||
background: #444;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input[type="range"]::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
background: #ff006e;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input[type="range"]::-moz-range-thumb {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
background: #ff006e;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🎯 Preference Manager Test Suite</h1>
|
||||
|
||||
<!-- Automated Tests Section -->
|
||||
<div class="test-section">
|
||||
<h2>Automated Tests</h2>
|
||||
<button onclick="runAllTests()">🧪 Run All Tests</button>
|
||||
<div id="test-results" class="test-grid"></div>
|
||||
</div>
|
||||
|
||||
<!-- Manual Testing Section -->
|
||||
<div class="test-section">
|
||||
<h2>Manual Testing</h2>
|
||||
<div class="button-group">
|
||||
<button onclick="showPreferences()">📊 Show Current Preferences</button>
|
||||
<button onclick="testCheckpointModal(1)">🎬 Test L1 Checkpoint</button>
|
||||
<button onclick="testCheckpointModal(5)">🎯 Test L5 Checkpoint</button>
|
||||
<button onclick="testCheckpointModal(10)">⚡ Test L10 Checkpoint</button>
|
||||
<button onclick="simulatePreferenceUpdate()">✏️ Simulate Update</button>
|
||||
<button onclick="testContentFilter()">🔍 Show Content Filter</button>
|
||||
<button onclick="showStats()">📈 Show Stats</button>
|
||||
<button onclick="resetPreferences()">🔄 Reset All</button>
|
||||
</div>
|
||||
<div id="manual-output"></div>
|
||||
</div>
|
||||
|
||||
<!-- Interactive Preference Editor -->
|
||||
<div class="test-section">
|
||||
<h2>Interactive Preference Editor</h2>
|
||||
<div id="preference-editor"></div>
|
||||
</div>
|
||||
|
||||
<!-- Progress Display -->
|
||||
<div class="test-section">
|
||||
<h2>Current State</h2>
|
||||
<div id="progress-display" class="json-display"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Load Dependencies -->
|
||||
<script src="src/data/gameData.js"></script>
|
||||
<script src="src/features/academy/preferenceManager.js"></script>
|
||||
|
||||
<script>
|
||||
// Test runner
|
||||
function runAllTests() {
|
||||
const tests = [
|
||||
testInitialization,
|
||||
testCategoryUpdate,
|
||||
testBulkUpdate,
|
||||
testContentFilter,
|
||||
testCheckpointDetection,
|
||||
testFeatureDiscovery,
|
||||
testBoundaries,
|
||||
testPersistence,
|
||||
testStats,
|
||||
testReset
|
||||
];
|
||||
|
||||
const resultsDiv = document.getElementById('test-results');
|
||||
resultsDiv.innerHTML = '';
|
||||
|
||||
tests.forEach(test => {
|
||||
const result = test();
|
||||
const resultDiv = document.createElement('div');
|
||||
resultDiv.className = `test-result ${result.pass ? 'pass' : 'fail'}`;
|
||||
resultDiv.innerHTML = `
|
||||
<h3>${result.pass ? '✓' : '✗'} ${result.name}</h3>
|
||||
<p>${result.message}</p>
|
||||
`;
|
||||
resultsDiv.appendChild(resultDiv);
|
||||
});
|
||||
}
|
||||
|
||||
// Test 1: Initialization
|
||||
function testInitialization() {
|
||||
const prefs = window.preferenceManager.getPreferences();
|
||||
const hasAllCategories = prefs.contentThemes && prefs.visualPreferences &&
|
||||
prefs.intensity && prefs.captionTone &&
|
||||
prefs.audioPreferences && prefs.sessionDuration &&
|
||||
prefs.featurePreferences && prefs.boundaries;
|
||||
|
||||
return {
|
||||
name: 'Initialization Test',
|
||||
pass: hasAllCategories && prefs.version === 1,
|
||||
message: hasAllCategories ? 'All 8 categories initialized correctly' : 'Missing categories'
|
||||
};
|
||||
}
|
||||
|
||||
// Test 2: Category Update
|
||||
function testCategoryUpdate() {
|
||||
const success = window.preferenceManager.updateCategoryPreferences('contentThemes', {
|
||||
edging: true,
|
||||
denial: true
|
||||
}, 1);
|
||||
|
||||
const prefs = window.preferenceManager.getCategoryPreferences('contentThemes');
|
||||
|
||||
return {
|
||||
name: 'Category Update Test',
|
||||
pass: success && prefs.edging === true && prefs.denial === true,
|
||||
message: success ? 'Successfully updated contentThemes' : 'Update failed'
|
||||
};
|
||||
}
|
||||
|
||||
// Test 3: Bulk Update
|
||||
function testBulkUpdate() {
|
||||
const updates = {
|
||||
contentThemes: { gooning: true },
|
||||
intensity: { visualIntensity: 4 }
|
||||
};
|
||||
|
||||
const success = window.preferenceManager.updateMultipleCategories(updates, 5);
|
||||
const prefs = window.preferenceManager.getPreferences();
|
||||
|
||||
return {
|
||||
name: 'Bulk Update Test',
|
||||
pass: success && prefs.contentThemes.gooning === true && prefs.intensity.visualIntensity === 4,
|
||||
message: success ? 'Successfully updated multiple categories' : 'Bulk update failed'
|
||||
};
|
||||
}
|
||||
|
||||
// Test 4: Content Filter
|
||||
function testContentFilter() {
|
||||
const filter = window.preferenceManager.getContentFilter();
|
||||
const hasRequiredFields = filter.themes && filter.visuals && filter.intensity && filter.exclude;
|
||||
|
||||
return {
|
||||
name: 'Content Filter Test',
|
||||
pass: hasRequiredFields && Array.isArray(filter.themes),
|
||||
message: hasRequiredFields ? 'Content filter generated correctly' : 'Filter generation failed'
|
||||
};
|
||||
}
|
||||
|
||||
// Test 5: Checkpoint Detection
|
||||
function testCheckpointDetection() {
|
||||
const isL1Checkpoint = window.preferenceManager.isCheckpointLevel(1);
|
||||
const isL5Checkpoint = window.preferenceManager.isCheckpointLevel(5);
|
||||
const isL3NotCheckpoint = !window.preferenceManager.isCheckpointLevel(3);
|
||||
|
||||
return {
|
||||
name: 'Checkpoint Detection Test',
|
||||
pass: isL1Checkpoint && isL5Checkpoint && isL3NotCheckpoint,
|
||||
message: 'Correctly identifies checkpoint levels (1,5,10,15,20,25)'
|
||||
};
|
||||
}
|
||||
|
||||
// Test 6: Feature Discovery
|
||||
function testFeatureDiscovery() {
|
||||
window.preferenceManager.discoverFeature('webcam');
|
||||
window.preferenceManager.discoverFeature('tts');
|
||||
|
||||
const discovered = window.preferenceManager.getDiscoveredFeatures();
|
||||
|
||||
return {
|
||||
name: 'Feature Discovery Test',
|
||||
pass: discovered.includes('webcam') && discovered.includes('tts'),
|
||||
message: `Discovered ${discovered.length} features: ${discovered.join(', ')}`
|
||||
};
|
||||
}
|
||||
|
||||
// Test 7: Boundaries
|
||||
function testBoundaries() {
|
||||
window.preferenceManager.addHardLimit('extreme-content');
|
||||
const prefs = window.preferenceManager.getPreferences();
|
||||
const hasLimit = prefs.boundaries.hardLimits.includes('extreme-content');
|
||||
|
||||
return {
|
||||
name: 'Boundaries Test',
|
||||
pass: hasLimit,
|
||||
message: hasLimit ? 'Hard limit added successfully' : 'Failed to add hard limit'
|
||||
};
|
||||
}
|
||||
|
||||
// Test 8: Persistence
|
||||
function testPersistence() {
|
||||
window.preferenceManager.updateCategoryPreferences('visualPreferences', { pov: true });
|
||||
const beforeSave = window.preferenceManager.getCategoryPreferences('visualPreferences').pov;
|
||||
|
||||
// Simulate reload
|
||||
const savedData = JSON.parse(localStorage.getItem('webGame-data'));
|
||||
const afterLoad = savedData.academyPreferences.visualPreferences.pov;
|
||||
|
||||
return {
|
||||
name: 'Persistence Test',
|
||||
pass: beforeSave === true && afterLoad === true,
|
||||
message: beforeSave === afterLoad ? 'Preferences persist to localStorage' : 'Persistence failed'
|
||||
};
|
||||
}
|
||||
|
||||
// Test 9: Stats
|
||||
function testStats() {
|
||||
const stats = window.preferenceManager.getPreferenceStats();
|
||||
const hasRequiredStats = stats.hasOwnProperty('totalCheckpoints') &&
|
||||
stats.hasOwnProperty('activeThemes') &&
|
||||
stats.hasOwnProperty('averageIntensity');
|
||||
|
||||
return {
|
||||
name: 'Statistics Test',
|
||||
pass: hasRequiredStats,
|
||||
message: `Generated stats: ${stats.activeThemes} themes, avg intensity ${stats.averageIntensity}`
|
||||
};
|
||||
}
|
||||
|
||||
// Test 10: Reset
|
||||
function testReset() {
|
||||
const beforeReset = window.preferenceManager.getPreferences().contentThemes.edging;
|
||||
window.preferenceManager.resetPreferences();
|
||||
const afterReset = window.preferenceManager.getPreferences().contentThemes.edging;
|
||||
|
||||
return {
|
||||
name: 'Reset Test',
|
||||
pass: beforeReset === true && afterReset === false,
|
||||
message: afterReset === false ? 'Preferences reset to defaults' : 'Reset failed'
|
||||
};
|
||||
}
|
||||
|
||||
// Manual testing functions
|
||||
function showPreferences() {
|
||||
const prefs = window.preferenceManager.getPreferences();
|
||||
const outputDiv = document.getElementById('manual-output');
|
||||
outputDiv.innerHTML = `<div class="status-box info">Current Preferences:</div>` +
|
||||
`<div class="json-display">${JSON.stringify(prefs, null, 2)}</div>`;
|
||||
}
|
||||
|
||||
function testCheckpointModal(level) {
|
||||
const config = window.preferenceManager.getCheckpointModalConfig(level);
|
||||
const outputDiv = document.getElementById('manual-output');
|
||||
|
||||
if (config) {
|
||||
outputDiv.innerHTML = `<div class="status-box success">
|
||||
<strong>Level ${level} Checkpoint Config:</strong><br>
|
||||
Title: ${config.title}<br>
|
||||
Description: ${config.description}<br>
|
||||
Categories to show: ${config.categories.join(', ')}<br>
|
||||
Is Initial: ${config.isInitial}
|
||||
</div>`;
|
||||
} else {
|
||||
outputDiv.innerHTML = `<div class="status-box error">Level ${level} is not a checkpoint</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function simulatePreferenceUpdate() {
|
||||
const updates = {
|
||||
contentThemes: { worship: true, femdom: true },
|
||||
intensity: { mentalIntensity: 4, taskDifficulty: 3 },
|
||||
captionTone: { commanding: true, seductive: true }
|
||||
};
|
||||
|
||||
window.preferenceManager.updateMultipleCategories(updates, 10);
|
||||
|
||||
const outputDiv = document.getElementById('manual-output');
|
||||
outputDiv.innerHTML = `<div class="status-box success">
|
||||
Simulated L10 checkpoint update:<br>
|
||||
- Enabled worship & femdom themes<br>
|
||||
- Set mental intensity to 4<br>
|
||||
- Enabled commanding & seductive tones
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function testContentFilter() {
|
||||
const filter = window.preferenceManager.getContentFilter();
|
||||
const outputDiv = document.getElementById('manual-output');
|
||||
outputDiv.innerHTML = `<div class="status-box info">Generated Content Filter:</div>` +
|
||||
`<div class="json-display">${JSON.stringify(filter, null, 2)}</div>`;
|
||||
}
|
||||
|
||||
function showStats() {
|
||||
const stats = window.preferenceManager.getPreferenceStats();
|
||||
const outputDiv = document.getElementById('manual-output');
|
||||
outputDiv.innerHTML = `<div class="status-box info">Preference Statistics:</div>` +
|
||||
`<div class="json-display">${JSON.stringify(stats, null, 2)}</div>`;
|
||||
}
|
||||
|
||||
function resetPreferences() {
|
||||
if (confirm('Reset all preferences to defaults?')) {
|
||||
window.preferenceManager.resetPreferences();
|
||||
const outputDiv = document.getElementById('manual-output');
|
||||
outputDiv.innerHTML = `<div class="status-box success">All preferences reset to defaults</div>`;
|
||||
updateProgressDisplay();
|
||||
}
|
||||
}
|
||||
|
||||
function updateProgressDisplay() {
|
||||
const prefs = window.preferenceManager.getPreferences();
|
||||
document.getElementById('progress-display').textContent = JSON.stringify(prefs, null, 2);
|
||||
}
|
||||
|
||||
// Initialize on load
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
updateProgressDisplay();
|
||||
|
||||
// Auto-refresh progress display every 2 seconds
|
||||
setInterval(updateProgressDisplay, 2000);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue