Implement simplified XP system with overall counter
- Replace scoring system with XP-based progression - Task-based XP: 2 XP per regular task, 5 XP per scenario step - Overall XP counter for rankings/leveling (persistent across sessions) - No overall XP awarded for quit/abandoned sessions (only completed sessions) - Session XP always displayed but only added to overall on completion - Simplified from time/activity bonuses to pure task completion rewards - Updated UI with dual XP display: Session XP / Total XP - Audio playlist system continues in background during gameplay
This commit is contained in:
parent
3e59456aff
commit
84b17a7930
|
|
@ -0,0 +1,80 @@
|
||||||
|
# Simplified Audio System Documentation
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
The audio system has been completely refactored to use a simple, dynamic approach with a streamlined directory structure.
|
||||||
|
|
||||||
|
## Simplified Directory Structure
|
||||||
|
|
||||||
|
### Current Structure:
|
||||||
|
```
|
||||||
|
audio/
|
||||||
|
├── background/ (Contains all background music/audio clips)
|
||||||
|
└── ambient/ (Empty for now, reserved for future ambient sounds)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Previous Complex Structure (REMOVED):
|
||||||
|
- ❌ `audio/effects/`
|
||||||
|
- ❌ `audio/tasks/teasing/`
|
||||||
|
- ❌ `audio/tasks/intense/`
|
||||||
|
- ❌ `audio/tasks/instructions/`
|
||||||
|
- ❌ `audio/punishments/denial/`
|
||||||
|
- ❌ `audio/punishments/mocking/`
|
||||||
|
- ❌ `audio/rewards/completion/`
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
### Single Background Audio Category
|
||||||
|
- **Before**: Multiple hardcoded categories (tasks, punishments, rewards, teasing, intense, etc.)
|
||||||
|
- **After**: One dynamic "background" category that uses user-managed audio files
|
||||||
|
|
||||||
|
### Dynamic Audio Discovery
|
||||||
|
- Audio files are loaded from the existing `customAudio` storage system
|
||||||
|
- Combines all user audio (background, ambient, effects) into one pool
|
||||||
|
- No more hardcoded file paths that can break
|
||||||
|
- Automatically refreshes when users add new audio files
|
||||||
|
|
||||||
|
### User Experience
|
||||||
|
1. **Add Audio**: Use "Manage Audio" menu to import audio files
|
||||||
|
2. **Auto-Play**: Audio automatically plays during tasks, rewards, and consequences
|
||||||
|
3. **Single Control**: All audio settings now control the same background audio
|
||||||
|
4. **No Errors**: No more "file not found" errors from missing hardcoded files
|
||||||
|
|
||||||
|
## Technical Changes
|
||||||
|
|
||||||
|
### AudioManager Simplification
|
||||||
|
- `playTaskAudio()` → `playBackgroundAudio()`
|
||||||
|
- `playPunishmentAudio()` → `playBackgroundAudio()`
|
||||||
|
- `playRewardAudio()` → `playBackgroundAudio()`
|
||||||
|
- Single audio category instead of multiple complex categories
|
||||||
|
|
||||||
|
### Game Integration
|
||||||
|
- All game events (task start, completion, skip) play from the same audio pool
|
||||||
|
- Audio automatically refreshes when users add files via "Manage Audio"
|
||||||
|
- Settings UI still shows separate controls but they all control the same audio
|
||||||
|
|
||||||
|
### Benefits
|
||||||
|
- ✅ No more audio file path errors
|
||||||
|
- ✅ User has full control over audio content
|
||||||
|
- ✅ Simple, reliable system
|
||||||
|
- ✅ Integrates with existing UI
|
||||||
|
- ✅ No hardcoded dependencies
|
||||||
|
|
||||||
|
## For Users
|
||||||
|
|
||||||
|
### To Add Background Audio:
|
||||||
|
1. Place your audio files in the `audio/background/` directory
|
||||||
|
2. Open the main menu and click "Manage Audio"
|
||||||
|
3. Click "🔍 Scan Directories" to discover the files
|
||||||
|
4. Audio will automatically play during the game
|
||||||
|
|
||||||
|
### Audio Controls:
|
||||||
|
- All volume sliders now control the same background audio
|
||||||
|
- Enable/disable toggles all control the same background audio
|
||||||
|
- Preview buttons all preview your background audio
|
||||||
|
|
||||||
|
## Migration Notes
|
||||||
|
- Complex directory structure has been simplified to just two folders
|
||||||
|
- Existing user audio files in "Manage Audio" will automatically work
|
||||||
|
- All audio files should be placed in `audio/background/` for now
|
||||||
|
- `audio/ambient/` is reserved for future ambient sounds (leave empty)
|
||||||
|
- System is now completely user-controlled and dynamic
|
||||||
|
|
@ -0,0 +1,113 @@
|
||||||
|
# Audio Directory Scanning Feature
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
The directory scanning feature automatically discovers audio files that are already present in your app's audio directory structure and adds them to your audio library.
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
### Automatic Scanning
|
||||||
|
- **On Startup**: The app automatically scans for audio files when it launches (desktop mode only)
|
||||||
|
- **Manual Scanning**: Use the "🔍 Scan Directories" button in the "Manage Audio" menu
|
||||||
|
|
||||||
|
### Scanned Directories
|
||||||
|
The scanner looks for audio files in these directories:
|
||||||
|
```
|
||||||
|
audio/
|
||||||
|
├── background/
|
||||||
|
└── ambient/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Supported Audio Formats
|
||||||
|
- **MP3** (most common)
|
||||||
|
- **WAV** (high quality)
|
||||||
|
- **OGG** (open source)
|
||||||
|
- **M4A** (Apple format)
|
||||||
|
- **AAC** (compressed)
|
||||||
|
- **FLAC** (lossless)
|
||||||
|
|
||||||
|
## Using Directory Scanning
|
||||||
|
|
||||||
|
### Desktop Mode (Recommended)
|
||||||
|
1. **Place Files**: Copy your audio files into the appropriate audio subdirectories
|
||||||
|
2. **Scan**: Click "🔍 Scan Directories" in the "Manage Audio" menu
|
||||||
|
3. **Auto-Use**: Scanned files are automatically added to your library and enabled
|
||||||
|
|
||||||
|
### File Organization
|
||||||
|
- **Background Music** → `audio/background/`
|
||||||
|
- **Ambient Sounds** → `audio/ambient/`
|
||||||
|
|
||||||
|
## Category Mapping
|
||||||
|
Due to the simplified audio system, all scanned files are categorized as follows:
|
||||||
|
- Files from `audio/ambient/` → **Ambient** category
|
||||||
|
- Files from `audio/background/` → **Background** category
|
||||||
|
|
||||||
|
## Benefits
|
||||||
|
|
||||||
|
### Easy Bulk Import
|
||||||
|
- Add many files at once by copying them to directories
|
||||||
|
- No need to import files one-by-one through the UI
|
||||||
|
- Perfect for organizing large audio collections
|
||||||
|
|
||||||
|
### Automatic Discovery
|
||||||
|
- New files are found automatically on app restart
|
||||||
|
- Manual scan button for immediate discovery
|
||||||
|
- No configuration required
|
||||||
|
|
||||||
|
### File Management
|
||||||
|
- Files stay in organized directory structure
|
||||||
|
- Easy to add/remove files outside the app
|
||||||
|
- Clear organization by purpose/category
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### No Files Found
|
||||||
|
- **Check Directories**: Ensure audio files are in the correct subdirectories
|
||||||
|
- **File Formats**: Verify files are in supported formats (MP3, WAV, OGG, M4A, AAC, FLAC)
|
||||||
|
- **Permissions**: Make sure the app has read access to the audio directories
|
||||||
|
- **File Names**: Avoid special characters in file names
|
||||||
|
|
||||||
|
### Duplicate Files
|
||||||
|
- The scanner checks for existing files before adding
|
||||||
|
- Files already in your library won't be duplicated
|
||||||
|
- If you see duplicates, use the "🧹 Cleanup" button
|
||||||
|
|
||||||
|
### Web Mode Limitations
|
||||||
|
- Directory scanning only works in desktop mode (Electron)
|
||||||
|
- Web browsers can't access file system directories
|
||||||
|
- Use the Import buttons for manual file upload in web mode
|
||||||
|
|
||||||
|
## Tips
|
||||||
|
|
||||||
|
### Organizing Your Audio
|
||||||
|
1. **Create Themed Folders**: Group similar audio files together
|
||||||
|
2. **Descriptive Names**: Use clear, descriptive file names
|
||||||
|
3. **Test Files**: Place a few test files first, then scan to verify
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
- Scanning is fast for hundreds of files
|
||||||
|
- Large files (>100MB) may take longer to process
|
||||||
|
- The app shows progress messages during scanning
|
||||||
|
|
||||||
|
### Backup Strategy
|
||||||
|
- Keep original files in the audio directories
|
||||||
|
- The app creates references, not copies
|
||||||
|
- Easy to backup/restore entire audio collection
|
||||||
|
|
||||||
|
## Integration
|
||||||
|
|
||||||
|
### With Dynamic Audio System
|
||||||
|
- Scanned files integrate seamlessly with the dynamic audio system
|
||||||
|
- All audio management features work with scanned files
|
||||||
|
- Enable/disable scanned files individually
|
||||||
|
|
||||||
|
### With Existing Audio
|
||||||
|
- Scanned files are added to existing audio library
|
||||||
|
- No conflicts with manually imported files
|
||||||
|
- Use both methods together for maximum flexibility
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. **Organize Files**: Place your audio files in the appropriate directories
|
||||||
|
2. **Scan**: Use the "🔍 Scan Directories" button
|
||||||
|
3. **Test**: Try playing audio during a game session
|
||||||
|
4. **Manage**: Use the audio management UI to enable/disable specific files
|
||||||
16
ROADMAP.md
16
ROADMAP.md
|
|
@ -8,11 +8,20 @@
|
||||||
- ✅ Scenario system with state management
|
- ✅ Scenario system with state management
|
||||||
- ✅ Loading overlay and initialization protection
|
- ✅ Loading overlay and initialization protection
|
||||||
- ✅ Desktop file management integration
|
- ✅ Desktop file management integration
|
||||||
|
- ✅ Continuous audio playlist system
|
||||||
|
- ✅ XP-based progression system (replaced scoring)
|
||||||
|
|
||||||
## 🚧 Active Development
|
## 🚧 Active Development
|
||||||
- Enhanced user experience improvements
|
- Enhanced user experience improvements
|
||||||
- Bug fixes and stability enhancements
|
- Bug fixes and stability enhancements
|
||||||
- Performance optimizations
|
- Performance optimizations
|
||||||
|
- **NEW XP System Implementation:**
|
||||||
|
- Time-based XP: 1 XP per 2 minutes of gameplay
|
||||||
|
- Focus session bonuses: 5 XP per minute
|
||||||
|
- Webcam mirror bonuses: 5 XP per minute
|
||||||
|
- Photo rewards: 1 XP per picture taken
|
||||||
|
- Main game XP conversion from old point system
|
||||||
|
- No XP awarded for abandoned/quit sessions
|
||||||
|
|
||||||
## 📋 Feature Backlog
|
## 📋 Feature Backlog
|
||||||
|
|
||||||
|
|
@ -40,6 +49,13 @@
|
||||||
- [ ] Media organization and tagging features
|
- [ ] Media organization and tagging features
|
||||||
- [ ] Import/export functionality for user content
|
- [ ] Import/export functionality for user content
|
||||||
- [ ] Content validation and quality checks
|
- [ ] Content validation and quality checks
|
||||||
|
- [ ] **Custom Audio Playlist Creation** - Allow users to create and save custom audio playlists with:
|
||||||
|
- Drag-and-drop playlist editor
|
||||||
|
- Named playlists (e.g., "Intense Session", "Relaxing Background")
|
||||||
|
- Shuffle/repeat options per playlist
|
||||||
|
- Playlist sharing and import/export
|
||||||
|
- Time-based playlist scheduling (different playlists for different game phases)
|
||||||
|
- Crossfade and transition effects between tracks
|
||||||
|
|
||||||
### 🔧 Technical Improvements
|
### 🔧 Technical Improvements
|
||||||
- [ ] Code refactoring and optimization
|
- [ ] Code refactoring and optimization
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,197 @@
|
||||||
|
/**
|
||||||
|
* Audio Compatibility Checker and Browser Mode Handler
|
||||||
|
* This script helps detect and handle audio limitations in browser vs Electron mode
|
||||||
|
*/
|
||||||
|
|
||||||
|
class AudioCompatibilityChecker {
|
||||||
|
constructor() {
|
||||||
|
this.isElectron = window.electronAPI !== undefined;
|
||||||
|
this.isFileProtocol = window.location.protocol === 'file:';
|
||||||
|
this.browserAudioSupported = this.checkBrowserAudioSupport();
|
||||||
|
}
|
||||||
|
|
||||||
|
checkBrowserAudioSupport() {
|
||||||
|
// Test if we can create audio elements
|
||||||
|
try {
|
||||||
|
const testAudio = new Audio();
|
||||||
|
return testAudio !== null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Browser does not support Audio API:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async testSingleAudioFile(path) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const audio = new Audio();
|
||||||
|
let resolved = false;
|
||||||
|
|
||||||
|
const cleanup = () => {
|
||||||
|
if (!resolved) {
|
||||||
|
resolved = true;
|
||||||
|
audio.pause();
|
||||||
|
audio.src = '';
|
||||||
|
audio.remove();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
audio.addEventListener('canplay', () => {
|
||||||
|
if (!resolved) {
|
||||||
|
cleanup();
|
||||||
|
resolve({ success: true, path, error: null });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
audio.addEventListener('error', (e) => {
|
||||||
|
if (!resolved) {
|
||||||
|
const errorInfo = {
|
||||||
|
code: e.target.error?.code,
|
||||||
|
message: e.target.error?.message,
|
||||||
|
networkState: e.target.networkState,
|
||||||
|
readyState: e.target.readyState,
|
||||||
|
src: e.target.src
|
||||||
|
};
|
||||||
|
cleanup();
|
||||||
|
resolve({ success: false, path, error: errorInfo });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Timeout after 3 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!resolved) {
|
||||||
|
cleanup();
|
||||||
|
resolve({ success: false, path, error: 'timeout' });
|
||||||
|
}
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
// Try to load the audio
|
||||||
|
try {
|
||||||
|
audio.src = path;
|
||||||
|
audio.load();
|
||||||
|
} catch (error) {
|
||||||
|
if (!resolved) {
|
||||||
|
cleanup();
|
||||||
|
resolve({ success: false, path, error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async quickAudioTest() {
|
||||||
|
console.log('🎵 Audio Compatibility Test Starting...');
|
||||||
|
console.log(`🎵 Environment: ${this.isElectron ? 'Electron' : 'Browser'}`);
|
||||||
|
console.log(`🎵 Protocol: ${window.location.protocol}`);
|
||||||
|
console.log(`🎵 Audio API Support: ${this.browserAudioSupported}`);
|
||||||
|
|
||||||
|
// Test a few known audio files
|
||||||
|
const testFiles = [
|
||||||
|
'audio/tasks/teasing/u.mp3',
|
||||||
|
'audio/rewards/completion/u.mp3'
|
||||||
|
];
|
||||||
|
|
||||||
|
const results = [];
|
||||||
|
for (const file of testFiles) {
|
||||||
|
console.log(`🎵 Testing: ${file}`);
|
||||||
|
const result = await this.testSingleAudioFile(file);
|
||||||
|
results.push(result);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
console.log(`✅ ${file} - Working`);
|
||||||
|
} else {
|
||||||
|
console.log(`❌ ${file} - Failed:`, result.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Small delay between tests
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
getRecommendations() {
|
||||||
|
const recommendations = [];
|
||||||
|
|
||||||
|
if (!this.isElectron && this.isFileProtocol) {
|
||||||
|
recommendations.push({
|
||||||
|
type: 'warning',
|
||||||
|
message: 'Running in browser with file:// protocol',
|
||||||
|
suggestion: 'Use "npm start" to run the Electron desktop version for full audio support'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.browserAudioSupported) {
|
||||||
|
recommendations.push({
|
||||||
|
type: 'error',
|
||||||
|
message: 'Browser does not support HTML5 Audio API',
|
||||||
|
suggestion: 'Try a different browser or update your current browser'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isElectron) {
|
||||||
|
recommendations.push({
|
||||||
|
type: 'success',
|
||||||
|
message: 'Running in Electron desktop mode',
|
||||||
|
suggestion: 'Audio should work without restrictions'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return recommendations;
|
||||||
|
}
|
||||||
|
|
||||||
|
async diagnoseAudioIssues() {
|
||||||
|
console.log('🎵 Audio Diagnostic Report');
|
||||||
|
console.log('=' .repeat(50));
|
||||||
|
|
||||||
|
const recommendations = this.getRecommendations();
|
||||||
|
recommendations.forEach(rec => {
|
||||||
|
const icon = rec.type === 'error' ? '❌' : rec.type === 'warning' ? '⚠️' : '✅';
|
||||||
|
console.log(`${icon} ${rec.message}`);
|
||||||
|
console.log(` 💡 ${rec.suggestion}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('\n🎵 Testing Audio Files...');
|
||||||
|
const testResults = await this.quickAudioTest();
|
||||||
|
|
||||||
|
const workingFiles = testResults.filter(r => r.success).length;
|
||||||
|
const totalFiles = testResults.length;
|
||||||
|
|
||||||
|
console.log('\n🎵 Summary:');
|
||||||
|
console.log(`✅ Working: ${workingFiles}/${totalFiles}`);
|
||||||
|
console.log(`❌ Failed: ${totalFiles - workingFiles}/${totalFiles}`);
|
||||||
|
|
||||||
|
if (workingFiles === 0) {
|
||||||
|
console.log('\n🎵 No audio files are working. This suggests:');
|
||||||
|
console.log(' - Browser CORS restrictions (use Electron app)');
|
||||||
|
console.log(' - Audio files are corrupted or missing');
|
||||||
|
console.log(' - Browser audio support issues');
|
||||||
|
} else if (workingFiles < totalFiles) {
|
||||||
|
console.log('\n🎵 Some audio files are working. Issues may be:');
|
||||||
|
console.log(' - Specific file corruption');
|
||||||
|
console.log(' - Filename/path issues');
|
||||||
|
console.log(' - Audio format compatibility');
|
||||||
|
} else {
|
||||||
|
console.log('\n🎵 All tested files are working! 🎉');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
environment: {
|
||||||
|
isElectron: this.isElectron,
|
||||||
|
isFileProtocol: this.isFileProtocol,
|
||||||
|
audioSupported: this.browserAudioSupported
|
||||||
|
},
|
||||||
|
testResults,
|
||||||
|
recommendations
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-run diagnostic if audio issues are detected
|
||||||
|
window.audioCompatibilityChecker = new AudioCompatibilityChecker();
|
||||||
|
|
||||||
|
// Quick check for common issues
|
||||||
|
if (!window.audioCompatibilityChecker.isElectron && window.audioCompatibilityChecker.isFileProtocol) {
|
||||||
|
console.log('🎵 Audio issues detected! Run window.audioCompatibilityChecker.diagnoseAudioIssues() for full report');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🎵 Audio Compatibility Checker loaded');
|
||||||
|
console.log('🎵 Run: window.audioCompatibilityChecker.diagnoseAudioIssues()');
|
||||||
|
|
@ -0,0 +1,143 @@
|
||||||
|
/**
|
||||||
|
* Audio File Validator
|
||||||
|
* This script tests which audio files can actually be loaded by the browser
|
||||||
|
*/
|
||||||
|
|
||||||
|
async function validateAudioFiles() {
|
||||||
|
console.log('🎵 Starting audio file validation...');
|
||||||
|
|
||||||
|
const audioStructure = {
|
||||||
|
tasks: {
|
||||||
|
teasing: [
|
||||||
|
'enjoying-a-giant-cock.mp3',
|
||||||
|
'horny-japanese-babe-japanese-sex.mp3',
|
||||||
|
'long-and-hard-moan.mp3',
|
||||||
|
'playing-her-pussy-with-a-dildo-japanese-sex.mp3',
|
||||||
|
'u.mp3'
|
||||||
|
],
|
||||||
|
intense: [
|
||||||
|
'bree-olson-screaming-orgasm-sound.mp3',
|
||||||
|
'carmela-bing-screaming-orgasm-sound.mp3',
|
||||||
|
'moaning-ringtone.mp3',
|
||||||
|
'multiple-orgasms-with-creamy-pussy.mp3'
|
||||||
|
],
|
||||||
|
instructions: [
|
||||||
|
'addict-to-my-tits-854x480p.mp3',
|
||||||
|
'deeper.mp3',
|
||||||
|
'you-love-to-goon.mp3'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
punishments: {
|
||||||
|
denial: [
|
||||||
|
'addicted-to-my-tits-tit-worship-mesmerize-joi-mov.mp3',
|
||||||
|
'dumb-gooner-bitch-4-big-tits.mp3',
|
||||||
|
'you-will-be-left-thoughtless.mp3'
|
||||||
|
],
|
||||||
|
mocking: [
|
||||||
|
'precise-denial.mp3',
|
||||||
|
'punishment-episode-cockring-causes-cumshots-cumshots-gameplay-only.mp3',
|
||||||
|
'quick-jerk-to-tits-in-pink-bra-1080p-ellie-idol.mp3',
|
||||||
|
'stroke-your-goon-bud.mp3'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
rewards: {
|
||||||
|
completion: ['u.mp3']
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const results = {
|
||||||
|
working: [],
|
||||||
|
failed: [],
|
||||||
|
total: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const [category, subcategories] of Object.entries(audioStructure)) {
|
||||||
|
for (const [subcategory, files] of Object.entries(subcategories)) {
|
||||||
|
for (const file of files) {
|
||||||
|
const audioPath = `audio/${category}/${subcategory}/${file}`;
|
||||||
|
results.total++;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const canLoad = await testAudioFile(audioPath);
|
||||||
|
if (canLoad) {
|
||||||
|
results.working.push(audioPath);
|
||||||
|
console.log(`✅ ${audioPath} - OK`);
|
||||||
|
} else {
|
||||||
|
results.failed.push(audioPath);
|
||||||
|
console.log(`❌ ${audioPath} - FAILED`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
results.failed.push(audioPath);
|
||||||
|
console.log(`❌ ${audioPath} - ERROR: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Small delay to prevent browser overload
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\n🎵 Audio Validation Results:');
|
||||||
|
console.log(`✅ Working files: ${results.working.length}/${results.total}`);
|
||||||
|
console.log(`❌ Failed files: ${results.failed.length}/${results.total}`);
|
||||||
|
|
||||||
|
if (results.failed.length > 0) {
|
||||||
|
console.log('\n❌ Failed files:');
|
||||||
|
results.failed.forEach(file => console.log(` - ${file}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (results.working.length > 0) {
|
||||||
|
console.log('\n✅ Working files:');
|
||||||
|
results.working.forEach(file => console.log(` - ${file}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testAudioFile(audioPath) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const audio = new Audio(audioPath);
|
||||||
|
let resolved = false;
|
||||||
|
|
||||||
|
const cleanup = () => {
|
||||||
|
if (!resolved) {
|
||||||
|
resolved = true;
|
||||||
|
audio.pause();
|
||||||
|
audio.src = '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
audio.addEventListener('canplay', () => {
|
||||||
|
if (!resolved) {
|
||||||
|
cleanup();
|
||||||
|
resolve(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
audio.addEventListener('error', () => {
|
||||||
|
if (!resolved) {
|
||||||
|
cleanup();
|
||||||
|
resolve(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Timeout after 5 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!resolved) {
|
||||||
|
cleanup();
|
||||||
|
resolve(false);
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
// Start loading
|
||||||
|
audio.load();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make functions available globally
|
||||||
|
window.audioValidator = {
|
||||||
|
validateAudioFiles,
|
||||||
|
testAudioFile
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('🎵 Audio validator loaded. Run: window.audioValidator.validateAudioFiles()');
|
||||||
|
|
@ -5,10 +5,8 @@ This is a placeholder for test audio.
|
||||||
## To test the audio system:
|
## To test the audio system:
|
||||||
|
|
||||||
1. **Add real audio files** to the appropriate directories:
|
1. **Add real audio files** to the appropriate directories:
|
||||||
- `/audio/tasks/teasing/` - Add MP3 files like `tease1.mp3`, `whisper1.mp3`
|
- `/audio/background/` - Add MP3 files for background music during tasks
|
||||||
- `/audio/punishments/denial/` - Add MP3 files like `denied.mp3`, `bad_boy.mp3`
|
- `/audio/ambient/` - Add MP3 files for ambient sounds (optional, leave empty for now)
|
||||||
- `/audio/punishments/mocking/` - Add MP3 files like `pathetic.mp3`, `weak.mp3`
|
|
||||||
- `/audio/rewards/completion/` - Add MP3 files like `good_boy.mp3`, `well_done.mp3`
|
|
||||||
|
|
||||||
2. **Test the controls**:
|
2. **Test the controls**:
|
||||||
- Open the game and go to Options ⚙️
|
- Open the game and go to Options ⚙️
|
||||||
|
|
@ -17,10 +15,10 @@ This is a placeholder for test audio.
|
||||||
- Use preview buttons to test audio
|
- Use preview buttons to test audio
|
||||||
|
|
||||||
3. **Test in gameplay**:
|
3. **Test in gameplay**:
|
||||||
- Start a task → Should play teasing/intense audio
|
- Start a task → Should play background audio
|
||||||
- Complete a task → Should play reward audio
|
- Complete a task → Should play background audio
|
||||||
- Skip a task → Should play mocking audio
|
- Skip a task → Should play background audio
|
||||||
- Get a consequence task → Should play denial audio
|
- All audio now comes from the background category
|
||||||
|
|
||||||
## Current Features Working:
|
## Current Features Working:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
// Audio Debug Script - Clear corrupted audio data and trigger fresh scan
|
||||||
|
|
||||||
|
console.log('🧹 Starting comprehensive audio cleanup...');
|
||||||
|
|
||||||
|
// Clear existing customAudio data completely and restart fresh
|
||||||
|
if (window.game && window.game.dataManager) {
|
||||||
|
console.log('🗑️ Clearing all existing audio data to start fresh...');
|
||||||
|
|
||||||
|
// Completely reset audio storage
|
||||||
|
window.game.dataManager.set('customAudio', { background: [], ambient: [] });
|
||||||
|
console.log('✅ Cleared all customAudio storage');
|
||||||
|
|
||||||
|
// Clear any cached audio library
|
||||||
|
if (window.game.audioManager) {
|
||||||
|
window.game.audioManager.audioLibrary = {
|
||||||
|
background: { general: [] }
|
||||||
|
};
|
||||||
|
console.log('✅ Cleared audio library cache');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now trigger a fresh directory scan if available
|
||||||
|
if (window.game.audioManager && window.game.audioManager.scanAudioDirectories) {
|
||||||
|
console.log('🔍 Triggering fresh directory scan...');
|
||||||
|
window.game.audioManager.scanAudioDirectories().then((scannedFiles) => {
|
||||||
|
console.log(`✅ Fresh scan completed - found ${scannedFiles.length} files`);
|
||||||
|
|
||||||
|
// Refresh the audio manager
|
||||||
|
return window.game.audioManager.refreshAudioLibrary();
|
||||||
|
}).then(() => {
|
||||||
|
console.log('✅ Audio library refreshed with clean data');
|
||||||
|
|
||||||
|
// Refresh the audio gallery
|
||||||
|
if (window.game.loadAudioGallery) {
|
||||||
|
window.game.loadAudioGallery();
|
||||||
|
console.log('✅ Audio gallery refreshed');
|
||||||
|
}
|
||||||
|
}).catch(error => {
|
||||||
|
console.error('❌ Error during fresh scan:', error);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log('⚠️ Directory scanning not available - try the "Scan Directories" button in Manage Audio');
|
||||||
|
|
||||||
|
// Just refresh what we have
|
||||||
|
if (window.game.audioManager) {
|
||||||
|
window.game.audioManager.refreshAudioLibrary().then(() => {
|
||||||
|
console.log('✅ Audio library refreshed');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
console.error('❌ Game or dataManager not available');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🎯 Cleanup script completed. Check the console for results and try playing audio.');
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
// Clear Audio Storage Script - Reset all audio data to start fresh
|
||||||
|
|
||||||
|
console.log('🧹 Clearing all audio storage data...');
|
||||||
|
|
||||||
|
// Clear all audio-related storage
|
||||||
|
if (window.game && window.game.dataManager) {
|
||||||
|
console.log('🗑️ Clearing customAudio storage...');
|
||||||
|
window.game.dataManager.set('customAudio', { background: [], ambient: [] });
|
||||||
|
|
||||||
|
console.log('🗑️ Clearing audio library cache...');
|
||||||
|
if (window.game.audioManager) {
|
||||||
|
window.game.audioManager.audioLibrary = {
|
||||||
|
background: { general: [] }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🔄 Refreshing audio library...');
|
||||||
|
if (window.game.audioManager) {
|
||||||
|
window.game.audioManager.refreshAudioLibrary().then(() => {
|
||||||
|
console.log('✅ Audio library refreshed');
|
||||||
|
|
||||||
|
// Refresh the UI
|
||||||
|
if (window.game.loadAudioGallery) {
|
||||||
|
window.game.loadAudioGallery();
|
||||||
|
console.log('✅ Audio gallery refreshed');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ All audio storage cleared successfully');
|
||||||
|
} else {
|
||||||
|
console.error('❌ Game or dataManager not available');
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,147 @@
|
||||||
|
/**
|
||||||
|
* Audio Overlap Debug Test Script
|
||||||
|
*
|
||||||
|
* This script helps test that background sounds stop properly when progressing through tasks.
|
||||||
|
*
|
||||||
|
* To use:
|
||||||
|
* 1. Start the game and begin playing
|
||||||
|
* 2. Open the browser console (F12)
|
||||||
|
* 3. Copy and paste this script into the console
|
||||||
|
* 4. Press Enter to run the test
|
||||||
|
*
|
||||||
|
* The test will simulate rapid task progression and monitor for audio overlaps.
|
||||||
|
*/
|
||||||
|
|
||||||
|
console.log('🎵 Starting Audio Overlap Test...');
|
||||||
|
|
||||||
|
// Function to test audio stopping
|
||||||
|
function testAudioStopping() {
|
||||||
|
if (!window.game || !window.game.audioManager) {
|
||||||
|
console.error('❌ Game or AudioManager not available');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const audioManager = window.game.audioManager;
|
||||||
|
|
||||||
|
console.log('🎵 Testing audio category stopping...');
|
||||||
|
|
||||||
|
// Test rapid audio switching
|
||||||
|
let testCount = 0;
|
||||||
|
const maxTests = 5;
|
||||||
|
|
||||||
|
const testInterval = setInterval(() => {
|
||||||
|
testCount++;
|
||||||
|
console.log(`🎵 Test ${testCount}/${maxTests}: Playing task audio...`);
|
||||||
|
|
||||||
|
// Stop all audio first
|
||||||
|
audioManager.stopAllImmediate();
|
||||||
|
|
||||||
|
// Wait a bit, then play new audio
|
||||||
|
setTimeout(() => {
|
||||||
|
audioManager.playTaskAudio('teasing', { fadeIn: 500, loop: true });
|
||||||
|
|
||||||
|
// Check how many audio elements are playing after a short delay
|
||||||
|
setTimeout(() => {
|
||||||
|
const currentlyPlaying = audioManager.getCurrentlyPlaying();
|
||||||
|
const playingCount = Object.keys(currentlyPlaying).length;
|
||||||
|
|
||||||
|
console.log(`🎵 Test ${testCount}: ${playingCount} audio tracks currently playing`);
|
||||||
|
console.log('🎵 Playing audio details:', currentlyPlaying);
|
||||||
|
|
||||||
|
if (playingCount > 1) {
|
||||||
|
console.warn(`⚠️ WARNING: ${playingCount} tracks playing simultaneously!`);
|
||||||
|
} else {
|
||||||
|
console.log('✅ Audio overlap test passed');
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
}, 50);
|
||||||
|
|
||||||
|
if (testCount >= maxTests) {
|
||||||
|
clearInterval(testInterval);
|
||||||
|
console.log('🎵 Audio overlap test completed');
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
setTimeout(() => {
|
||||||
|
audioManager.stopAllImmediate();
|
||||||
|
console.log('🎵 Test cleanup completed');
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to monitor audio during actual gameplay
|
||||||
|
function monitorGameplayAudio() {
|
||||||
|
if (!window.game || !window.game.audioManager) {
|
||||||
|
console.error('❌ Game or AudioManager not available');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🎵 Starting gameplay audio monitoring...');
|
||||||
|
console.log('🎵 Quickly complete or skip several tasks to test audio overlap prevention');
|
||||||
|
|
||||||
|
// Monitor audio every 500ms
|
||||||
|
const monitorInterval = setInterval(() => {
|
||||||
|
const currentlyPlaying = window.game.audioManager.getCurrentlyPlaying();
|
||||||
|
const playingCount = Object.keys(currentlyPlaying).length;
|
||||||
|
|
||||||
|
if (playingCount > 1) {
|
||||||
|
console.warn(`⚠️ AUDIO OVERLAP DETECTED: ${playingCount} tracks playing!`);
|
||||||
|
console.log('🎵 Overlapping audio details:', currentlyPlaying);
|
||||||
|
} else if (playingCount === 1) {
|
||||||
|
console.log(`✅ Single audio track playing (normal)`);
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
// Stop monitoring after 30 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
clearInterval(monitorInterval);
|
||||||
|
console.log('🎵 Audio monitoring stopped');
|
||||||
|
}, 30000);
|
||||||
|
|
||||||
|
return monitorInterval;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to test rapid task progression simulation
|
||||||
|
function simulateRapidProgression() {
|
||||||
|
if (!window.game || !window.game.gameState || !window.game.gameState.isRunning) {
|
||||||
|
console.error('❌ Game not running - start a game first');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🎵 Simulating rapid task progression...');
|
||||||
|
|
||||||
|
let progressCount = 0;
|
||||||
|
const maxProgress = 3;
|
||||||
|
|
||||||
|
const progressInterval = setInterval(() => {
|
||||||
|
progressCount++;
|
||||||
|
console.log(`🎵 Rapid progression test ${progressCount}/${maxProgress}`);
|
||||||
|
|
||||||
|
// Simulate completing a task (this should stop audio and start new audio)
|
||||||
|
if (window.game.gameState.isRunning) {
|
||||||
|
window.game.completeTask();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (progressCount >= maxProgress) {
|
||||||
|
clearInterval(progressInterval);
|
||||||
|
console.log('🎵 Rapid progression test completed');
|
||||||
|
}
|
||||||
|
}, 200); // Very fast progression to test overlap
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the tests
|
||||||
|
console.log('🎵 Audio Debug Test Menu:');
|
||||||
|
console.log('🎵 1. Run testAudioStopping() - Tests basic audio stopping');
|
||||||
|
console.log('🎵 2. Run monitorGameplayAudio() - Monitors for overlaps during gameplay');
|
||||||
|
console.log('🎵 3. Run simulateRapidProgression() - Simulates rapid task progression');
|
||||||
|
console.log('🎵 Example: testAudioStopping()');
|
||||||
|
|
||||||
|
// Make functions available globally for manual testing
|
||||||
|
window.audioDebugTest = {
|
||||||
|
testAudioStopping,
|
||||||
|
monitorGameplayAudio,
|
||||||
|
simulateRapidProgression
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('🎵 Functions available as window.audioDebugTest.*');
|
||||||
|
console.log('🎵 Quick test: window.audioDebugTest.testAudioStopping()');
|
||||||
|
|
@ -0,0 +1,105 @@
|
||||||
|
/**
|
||||||
|
* Electron Audio Path Debug Script
|
||||||
|
* Run this in the console to debug audio path issues in Electron
|
||||||
|
*/
|
||||||
|
|
||||||
|
function debugElectronAudioPaths() {
|
||||||
|
console.log('🔧 Electron Audio Path Debug');
|
||||||
|
console.log('=' .repeat(50));
|
||||||
|
|
||||||
|
console.log('Environment Info:');
|
||||||
|
console.log('- Is Electron:', window.electronAPI !== undefined);
|
||||||
|
console.log('- Current URL:', window.location.href);
|
||||||
|
console.log('- Current Protocol:', window.location.protocol);
|
||||||
|
console.log('- Current Directory:', window.location.href.replace('/index.html', ''));
|
||||||
|
|
||||||
|
// Test basic audio creation
|
||||||
|
console.log('\n🎵 Testing Basic Audio Creation:');
|
||||||
|
try {
|
||||||
|
const testAudio = new Audio();
|
||||||
|
console.log('✅ Audio element created successfully');
|
||||||
|
console.log('- Initial src:', testAudio.src);
|
||||||
|
|
||||||
|
// Test setting a simple relative path
|
||||||
|
testAudio.src = 'audio/tasks/teasing/u.mp3';
|
||||||
|
console.log('- After setting relative path:', testAudio.src);
|
||||||
|
|
||||||
|
// Test setting an absolute file path
|
||||||
|
const absolutePath = window.location.href.replace('/index.html', '') + '/audio/tasks/teasing/u.mp3';
|
||||||
|
testAudio.src = absolutePath;
|
||||||
|
console.log('- After setting absolute path:', testAudio.src);
|
||||||
|
|
||||||
|
testAudio.src = '';
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error creating audio element:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test actual file access
|
||||||
|
console.log('\n📁 Testing File Access:');
|
||||||
|
|
||||||
|
const testPaths = [
|
||||||
|
'audio/tasks/teasing/u.mp3',
|
||||||
|
'./audio/tasks/teasing/u.mp3',
|
||||||
|
window.location.href.replace('/index.html', '') + '/audio/tasks/teasing/u.mp3'
|
||||||
|
];
|
||||||
|
|
||||||
|
testPaths.forEach((path, index) => {
|
||||||
|
console.log(`\nTest ${index + 1}: ${path}`);
|
||||||
|
|
||||||
|
const audio = new Audio();
|
||||||
|
|
||||||
|
audio.addEventListener('loadstart', () => {
|
||||||
|
console.log(` ✅ Load started for: ${path}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
audio.addEventListener('canplay', () => {
|
||||||
|
console.log(` ✅ Can play: ${path}`);
|
||||||
|
audio.src = ''; // Clean up
|
||||||
|
});
|
||||||
|
|
||||||
|
audio.addEventListener('error', (e) => {
|
||||||
|
console.log(` ❌ Error loading: ${path}`);
|
||||||
|
console.log(` Error details:`, {
|
||||||
|
error: e.target.error,
|
||||||
|
src: e.target.src,
|
||||||
|
networkState: e.target.networkState,
|
||||||
|
readyState: e.target.readyState
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
audio.src = path;
|
||||||
|
setTimeout(() => {
|
||||||
|
if (audio.readyState === 0 && audio.networkState !== 2) {
|
||||||
|
console.log(` ⏰ Timeout for: ${path}`);
|
||||||
|
}
|
||||||
|
}, 2000);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(` ❌ Exception setting src: ${error.message}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test if electronAPI is available and working
|
||||||
|
if (window.electronAPI) {
|
||||||
|
console.log('\n⚡ Testing Electron API:');
|
||||||
|
|
||||||
|
window.electronAPI.fileExists('audio/tasks/teasing/u.mp3').then(exists => {
|
||||||
|
console.log('- File exists (relative):', exists);
|
||||||
|
}).catch(err => {
|
||||||
|
console.log('- Error checking file existence:', err);
|
||||||
|
});
|
||||||
|
|
||||||
|
window.electronAPI.getAppPath().then(appPath => {
|
||||||
|
console.log('- App path:', appPath);
|
||||||
|
}).catch(err => {
|
||||||
|
console.log('- Error getting app path:', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-run the debug
|
||||||
|
console.log('🔧 Electron Audio Debug loaded. Running automatic test...');
|
||||||
|
debugElectronAudioPaths();
|
||||||
|
|
||||||
|
// Make function available globally
|
||||||
|
window.debugElectronAudioPaths = debugElectronAudioPaths;
|
||||||
49
index.html
49
index.html
|
|
@ -130,22 +130,22 @@
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="game-mode-option">
|
<div class="game-mode-option">
|
||||||
<input type="radio" id="mode-score-target" name="gameMode" value="score-target">
|
<input type="radio" id="mode-xp-target" name="gameMode" value="xp-target">
|
||||||
<label for="mode-score-target">
|
<label for="mode-xp-target">
|
||||||
<strong>🏆 Score Target</strong>
|
<strong>🏆 XP Target</strong>
|
||||||
<p>Reach the target score to win</p>
|
<p>Reach the target XP to win</p>
|
||||||
<div class="mode-config" id="score-target-config" style="display: none;">
|
<div class="mode-config" id="xp-target-config" style="display: none;">
|
||||||
<label>Target Score:
|
<label>Target XP:
|
||||||
<select id="score-target-select">
|
<select id="xp-target-select">
|
||||||
<option value="100">100 points</option>
|
<option value="100">100 XP</option>
|
||||||
<option value="200">200 points</option>
|
<option value="200">200 XP</option>
|
||||||
<option value="300">300 points</option>
|
<option value="300">300 XP</option>
|
||||||
<option value="custom">Custom...</option>
|
<option value="custom">Custom...</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<div id="custom-score-input" style="display: none; margin-top: 10px;">
|
<div id="custom-xp-input" style="display: none; margin-top: 10px;">
|
||||||
<label>Custom target score:
|
<label>Custom target XP:
|
||||||
<input type="number" id="custom-score-value" min="50" max="10000" value="300" style="width: 80px;">
|
<input type="number" id="custom-xp-value" min="50" max="10000" value="300" style="width: 80px;">
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -395,7 +395,6 @@
|
||||||
<div class="upload-controls">
|
<div class="upload-controls">
|
||||||
<button id="import-background-audio-btn" class="btn btn-primary">🎶 Background Music</button>
|
<button id="import-background-audio-btn" class="btn btn-primary">🎶 Background Music</button>
|
||||||
<button id="import-ambient-audio-btn" class="btn btn-success">🌿 Ambient Sounds</button>
|
<button id="import-ambient-audio-btn" class="btn btn-success">🌿 Ambient Sounds</button>
|
||||||
<button id="import-effects-audio-btn" class="btn btn-warning">🔊 Sound Effects</button>
|
|
||||||
<input type="file" id="audio-upload-input" accept="audio/*" multiple style="display: none;">
|
<input type="file" id="audio-upload-input" accept="audio/*" multiple style="display: none;">
|
||||||
</div>
|
</div>
|
||||||
<div class="upload-info desktop-feature">
|
<div class="upload-info desktop-feature">
|
||||||
|
|
@ -405,6 +404,7 @@
|
||||||
<span>🌐 Web: Limited browser upload functionality</span>
|
<span>🌐 Web: Limited browser upload functionality</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="directory-controls">
|
<div class="directory-controls">
|
||||||
|
<button id="scan-audio-directories-btn" class="btn btn-primary">🔍 Scan Directories</button>
|
||||||
<button id="audio-storage-info-btn" class="btn btn-outline">📊 Storage Info</button>
|
<button id="audio-storage-info-btn" class="btn btn-outline">📊 Storage Info</button>
|
||||||
<button id="cleanup-invalid-audio-btn" class="btn btn-warning">🧹 Cleanup</button>
|
<button id="cleanup-invalid-audio-btn" class="btn btn-warning">🧹 Cleanup</button>
|
||||||
<button id="clear-all-audio-btn" class="btn btn-danger">🗑️ Clear All</button>
|
<button id="clear-all-audio-btn" class="btn btn-danger">🗑️ Clear All</button>
|
||||||
|
|
@ -429,7 +429,6 @@
|
||||||
<div class="audio-tabs">
|
<div class="audio-tabs">
|
||||||
<button id="background-audio-tab" class="tab-btn active">🎶 Background</button>
|
<button id="background-audio-tab" class="tab-btn active">🎶 Background</button>
|
||||||
<button id="ambient-audio-tab" class="tab-btn">🌿 Ambient</button>
|
<button id="ambient-audio-tab" class="tab-btn">🌿 Ambient</button>
|
||||||
<button id="effects-audio-tab" class="tab-btn">🔊 Effects</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="audio-galleries-container">
|
<div class="audio-galleries-container">
|
||||||
|
|
@ -440,10 +439,6 @@
|
||||||
<div id="ambient-audio-gallery" class="audio-gallery">
|
<div id="ambient-audio-gallery" class="audio-gallery">
|
||||||
<!-- Ambient audio files will be populated here -->
|
<!-- Ambient audio files will be populated here -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="effects-audio-gallery" class="audio-gallery">
|
|
||||||
<!-- Effects audio files will be populated here -->
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -1438,12 +1433,6 @@
|
||||||
<!-- Game Screen -->
|
<!-- Game Screen -->
|
||||||
<div id="game-screen" class="screen">
|
<div id="game-screen" class="screen">
|
||||||
<div class="task-container">
|
<div class="task-container">
|
||||||
<div class="task-type">
|
|
||||||
<span id="task-type-indicator">MAIN TASK</span>
|
|
||||||
<span id="task-difficulty" class="task-difficulty">MEDIUM</span>
|
|
||||||
<span id="task-points" class="task-points">+3 pts</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="task-image-container">
|
<div class="task-image-container">
|
||||||
<img id="task-image" src="" alt="Task Image" class="task-image">
|
<img id="task-image" src="" alt="Task Image" class="task-image">
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1465,8 +1454,12 @@
|
||||||
|
|
||||||
<div class="game-stats">
|
<div class="game-stats">
|
||||||
<div class="stat">
|
<div class="stat">
|
||||||
<span class="stat-label">Score:</span>
|
<span class="stat-label">Session XP:</span>
|
||||||
<span id="score" class="stat-value">0</span>
|
<span id="xp" class="stat-value">0</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat">
|
||||||
|
<span class="stat-label">Total XP:</span>
|
||||||
|
<span id="overall-xp" class="stat-value">0</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat">
|
<div class="stat">
|
||||||
<span class="stat-label">Completed:</span>
|
<span class="stat-label">Completed:</span>
|
||||||
|
|
@ -1501,7 +1494,7 @@
|
||||||
<p>Congratulations! You've completed all available tasks!</p>
|
<p>Congratulations! You've completed all available tasks!</p>
|
||||||
<div id="final-stats" class="final-stats">
|
<div id="final-stats" class="final-stats">
|
||||||
<p><strong>Game Mode:</strong> <span id="final-game-mode"></span></p>
|
<p><strong>Game Mode:</strong> <span id="final-game-mode"></span></p>
|
||||||
<p>Final Score: <span id="final-score"></span> points</p>
|
<p>Final XP: <span id="final-xp"></span> XP</p>
|
||||||
<p>Final Time: <span id="final-time"></span></p>
|
<p>Final Time: <span id="final-time"></span></p>
|
||||||
<p>Tasks Completed: <span id="final-completed"></span></p>
|
<p>Tasks Completed: <span id="final-completed"></span></p>
|
||||||
<p>Tasks Skipped: <span id="final-skipped"></span></p>
|
<p>Tasks Skipped: <span id="final-skipped"></span></p>
|
||||||
|
|
|
||||||
643
src/core/game.js
643
src/core/game.js
File diff suppressed because it is too large
Load Diff
|
|
@ -4,199 +4,140 @@ const gameData = {
|
||||||
mainTasks: [
|
mainTasks: [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
text: "Do 20 jumping jacks",
|
text: "Hold your breath for 30 seconds while maintaining focus",
|
||||||
difficulty: "Easy"
|
difficulty: "Easy"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
text: "Write down 3 things you're grateful for",
|
text: "Close your eyes and count to 20 without rushing",
|
||||||
difficulty: "Easy"
|
difficulty: "Easy"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 3,
|
id: 3,
|
||||||
text: "Call a friend or family member",
|
text: "Keep your hands completely still for 45 seconds",
|
||||||
difficulty: "Medium"
|
difficulty: "Medium"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 4,
|
id: 4,
|
||||||
text: "Organize your desk or workspace",
|
text: "Stare at a fixed point without blinking for 30 seconds",
|
||||||
difficulty: "Medium"
|
difficulty: "Medium"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 5,
|
id: 5,
|
||||||
text: "Take 10 deep breaths",
|
text: "Take 10 slow, deep breaths while focusing on control",
|
||||||
difficulty: "Easy"
|
difficulty: "Easy"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 6,
|
id: 6,
|
||||||
text: "Read for 15 minutes",
|
text: "Hold a stress position (arms extended) for 60 seconds",
|
||||||
difficulty: "Medium"
|
difficulty: "Medium"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 7,
|
id: 7,
|
||||||
text: "Do a 5-minute stretch routine",
|
text: "Repeat 'I am learning self-control' 10 times slowly",
|
||||||
difficulty: "Easy"
|
difficulty: "Easy"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 8,
|
id: 8,
|
||||||
text: "Write in a journal for 10 minutes",
|
text: "Maintain perfect posture for 2 minutes without moving",
|
||||||
difficulty: "Medium"
|
difficulty: "Medium"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 9,
|
id: 9,
|
||||||
text: "Listen to your favorite song and dance",
|
text: "Edge for 30 seconds then stop immediately",
|
||||||
difficulty: "Easy"
|
difficulty: "Easy"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 10,
|
id: 10,
|
||||||
text: "Make your bed perfectly",
|
text: "Close your eyes and focus on breathing for 90 seconds",
|
||||||
difficulty: "Easy"
|
difficulty: "Easy"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 11,
|
id: 11,
|
||||||
text: "Drink a full glass of water",
|
text: "Hold a challenging yoga pose for 60 seconds",
|
||||||
difficulty: "Easy"
|
difficulty: "Easy"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 12,
|
id: 12,
|
||||||
text: "Compliment someone genuinely",
|
text: "Count backwards from 100 by 7s without losing focus",
|
||||||
difficulty: "Medium"
|
difficulty: "Medium"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 13,
|
id: 13,
|
||||||
text: "Do 50 push-ups (or modified push-ups)",
|
text: "Edge for 2 minutes with 3 stops, then wait 30 seconds",
|
||||||
difficulty: "Hard"
|
difficulty: "Hard"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 14,
|
id: 14,
|
||||||
text: "Meditate for 20 minutes",
|
text: "Hold a plank position for 3 minutes while edging",
|
||||||
difficulty: "Hard"
|
difficulty: "Hard"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 15,
|
id: 15,
|
||||||
text: "Clean and organize an entire room",
|
text: "Practice edging with varying speeds for 5 minutes",
|
||||||
difficulty: "Hard"
|
difficulty: "Hard"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 16,
|
id: 16,
|
||||||
text: "Learn 10 new words in a foreign language",
|
text: "Complete a 10-minute endurance session with no release",
|
||||||
difficulty: "Hard"
|
difficulty: "Hard"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 17,
|
id: 17,
|
||||||
text: "Write a one-page story or essay",
|
text: "Edge to near climax 5 times, then stop for 2 minutes",
|
||||||
difficulty: "Hard"
|
difficulty: "Hard"
|
||||||
},
|
},
|
||||||
// Interactive Tasks
|
|
||||||
{
|
|
||||||
id: 18,
|
|
||||||
text: "Master the rhythm challenge",
|
|
||||||
difficulty: "Medium",
|
|
||||||
interactiveType: "rhythm-tap",
|
|
||||||
interactiveData: {
|
|
||||||
beats: 8,
|
|
||||||
bpm: 120,
|
|
||||||
tolerance: 300,
|
|
||||||
requiredAccuracy: 70
|
|
||||||
},
|
|
||||||
hint: "Listen to the beat and tap in rhythm. Don't worry about being perfect - 70% accuracy is enough!"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 19,
|
|
||||||
text: "Prove your focus with precision timing",
|
|
||||||
difficulty: "Hard",
|
|
||||||
interactiveType: "rhythm-tap",
|
|
||||||
interactiveData: {
|
|
||||||
beats: 12,
|
|
||||||
bpm: 140,
|
|
||||||
tolerance: 200,
|
|
||||||
requiredAccuracy: 85
|
|
||||||
},
|
|
||||||
hint: "This is a harder rhythm challenge. Stay focused and maintain the beat!"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 20,
|
|
||||||
text: "Quick rhythm warm-up",
|
|
||||||
difficulty: "Easy",
|
|
||||||
interactiveType: "rhythm-tap",
|
|
||||||
interactiveData: {
|
|
||||||
beats: 6,
|
|
||||||
bpm: 100,
|
|
||||||
tolerance: 400,
|
|
||||||
requiredAccuracy: 60
|
|
||||||
},
|
|
||||||
hint: "A gentle rhythm to get you started. Just follow the beat!"
|
|
||||||
},
|
|
||||||
// Add more interactive tasks for testing
|
|
||||||
{
|
|
||||||
id: 21,
|
|
||||||
text: "Test your rhythm skills",
|
|
||||||
difficulty: "Medium",
|
|
||||||
interactiveType: "rhythm-tap",
|
|
||||||
interactiveData: {
|
|
||||||
beats: 10,
|
|
||||||
bpm: 110,
|
|
||||||
tolerance: 350,
|
|
||||||
requiredAccuracy: 65
|
|
||||||
},
|
|
||||||
hint: "Keep steady and follow the beat!"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 22,
|
|
||||||
text: "Advanced rhythm challenge",
|
|
||||||
difficulty: "Hard",
|
|
||||||
interactiveType: "rhythm-tap",
|
|
||||||
interactiveData: {
|
|
||||||
beats: 16,
|
|
||||||
bpm: 160,
|
|
||||||
tolerance: 150,
|
|
||||||
requiredAccuracy: 90
|
|
||||||
},
|
|
||||||
hint: "This is intense! Focus and nail those beats!"
|
|
||||||
}
|
|
||||||
],
|
],
|
||||||
|
|
||||||
// Consequence tasks - these are triggered when main tasks are skipped
|
// Consequence tasks - these are triggered when main tasks are skipped
|
||||||
consequenceTasks: [
|
consequenceTasks: [
|
||||||
{
|
{
|
||||||
id: 101,
|
id: 101,
|
||||||
text: "Do 30 push-ups (or modified push-ups)"
|
text: "Hold the edge for 60 seconds without any movement"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 102,
|
id: 102,
|
||||||
text: "Clean the bathroom mirror and sink"
|
text: "Perform 50 squats while maintaining arousal"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 103,
|
id: 103,
|
||||||
text: "Stand on one foot for 2 minutes"
|
text: "Stay at the edge for 2 minutes with hands behind back"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 104,
|
id: 104,
|
||||||
text: "Write 'I will not skip tasks' 20 times"
|
text: "Write 'I will practice self-control' 25 times"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 105,
|
id: 105,
|
||||||
text: "Hold a plank position for 1 minute"
|
text: "Hold a wall sit for 3 minutes while edging"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 106,
|
id: 106,
|
||||||
text: "Recite the alphabet backwards"
|
text: "Edge for 30 seconds then cold shower for 1 minute"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 107,
|
id: 107,
|
||||||
text: "Do 50 jumping jacks"
|
text: "Maintain edge position for 5 minutes without touching"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 108,
|
id: 108,
|
||||||
text: "Wash 5 dishes by hand"
|
text: "Complete 100 push-ups in sets while staying aroused"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 109,
|
id: 109,
|
||||||
text: "Memorize and recite a short poem"
|
text: "Kneel in stress position for 10 minutes, no relief"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 110,
|
id: 110,
|
||||||
text: "Vacuum one room completely"
|
text: "Edge to near climax then stop for 5 minutes - repeat 3 times"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 111,
|
||||||
|
text: "Perform degrading poses while edging for 3 minutes"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 112,
|
||||||
|
text: "Worship your training material for 10 minutes, no touching"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|
@ -210,129 +151,129 @@ const gameData = {
|
||||||
// Placeholder images for tasks that don't have specific images
|
// Placeholder images for tasks that don't have specific images
|
||||||
defaultImage: "images/placeholder.jpg",
|
defaultImage: "images/placeholder.jpg",
|
||||||
|
|
||||||
// Flash Message System - Default encouraging messages
|
// Flash Message System - Training focused messages
|
||||||
defaultFlashMessages: [
|
defaultFlashMessages: [
|
||||||
// Motivational messages
|
// Motivational messages
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
text: "You're doing amazing! Keep going!",
|
text: "Your self-control is improving! Keep training!",
|
||||||
category: "motivational",
|
category: "motivational",
|
||||||
enabled: true
|
enabled: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
text: "Every task completed makes you stronger!",
|
text: "Every edge builds your endurance!",
|
||||||
category: "motivational",
|
category: "motivational",
|
||||||
enabled: true
|
enabled: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 3,
|
id: 3,
|
||||||
text: "Progress, not perfection!",
|
text: "Discipline over impulse!",
|
||||||
category: "motivational",
|
category: "motivational",
|
||||||
enabled: true
|
enabled: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 4,
|
id: 4,
|
||||||
text: "You've got this! Stay focused!",
|
text: "You're learning to control yourself! Stay focused!",
|
||||||
category: "motivational",
|
category: "motivational",
|
||||||
enabled: true
|
enabled: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 5,
|
id: 5,
|
||||||
text: "Small steps lead to big changes!",
|
text: "Small steps lead to better self-control!",
|
||||||
category: "motivational",
|
category: "motivational",
|
||||||
enabled: true
|
enabled: true
|
||||||
},
|
},
|
||||||
// Encouraging messages
|
// Encouraging messages
|
||||||
{
|
{
|
||||||
id: 6,
|
id: 6,
|
||||||
text: "Your dedication is inspiring!",
|
text: "Your dedication to training is impressive!",
|
||||||
category: "encouraging",
|
category: "encouraging",
|
||||||
enabled: true
|
enabled: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 7,
|
id: 7,
|
||||||
text: "Look how far you've come already!",
|
text: "Look how much your endurance has improved!",
|
||||||
category: "encouraging",
|
category: "encouraging",
|
||||||
enabled: true
|
enabled: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 8,
|
id: 8,
|
||||||
text: "You're building great habits!",
|
text: "You're building incredible self-discipline!",
|
||||||
category: "encouraging",
|
category: "encouraging",
|
||||||
enabled: true
|
enabled: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 9,
|
id: 9,
|
||||||
text: "Believe in yourself - you're capable of amazing things!",
|
text: "Master your desires - you have the strength!",
|
||||||
category: "encouraging",
|
category: "encouraging",
|
||||||
enabled: true
|
enabled: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 10,
|
id: 10,
|
||||||
text: "Your future self will thank you!",
|
text: "Your future self will appreciate this training!",
|
||||||
category: "encouraging",
|
category: "encouraging",
|
||||||
enabled: true
|
enabled: true
|
||||||
},
|
},
|
||||||
// Achievement messages
|
// Achievement messages
|
||||||
{
|
{
|
||||||
id: 11,
|
id: 11,
|
||||||
text: "Great job completing that task!",
|
text: "Excellent control on that task!",
|
||||||
category: "achievement",
|
category: "achievement",
|
||||||
enabled: true
|
enabled: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 12,
|
id: 12,
|
||||||
text: "You're on fire! Keep the streak alive!",
|
text: "You're on fire! Maintain that discipline!",
|
||||||
category: "achievement",
|
category: "achievement",
|
||||||
enabled: true
|
enabled: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 13,
|
id: 13,
|
||||||
text: "Another win in the books!",
|
text: "Another victory for self-control!",
|
||||||
category: "achievement",
|
category: "achievement",
|
||||||
enabled: true
|
enabled: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 14,
|
id: 14,
|
||||||
text: "Excellence in action!",
|
text: "Perfect execution!",
|
||||||
category: "achievement",
|
category: "achievement",
|
||||||
enabled: true
|
enabled: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 15,
|
id: 15,
|
||||||
text: "You're crushing your goals!",
|
text: "You're mastering your training goals!",
|
||||||
category: "achievement",
|
category: "achievement",
|
||||||
enabled: true
|
enabled: true
|
||||||
},
|
},
|
||||||
// Persistence messages
|
// Persistence messages
|
||||||
{
|
{
|
||||||
id: 16,
|
id: 16,
|
||||||
text: "Don't give up now - you're so close!",
|
text: "Don't give in now - show your discipline!",
|
||||||
category: "persistence",
|
category: "persistence",
|
||||||
enabled: true
|
enabled: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 17,
|
id: 17,
|
||||||
text: "Every challenge is an opportunity to grow!",
|
text: "Every challenge strengthens your self-control!",
|
||||||
category: "persistence",
|
category: "persistence",
|
||||||
enabled: true
|
enabled: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 18,
|
id: 18,
|
||||||
text: "Push through - greatness awaits!",
|
text: "Push through - mastery awaits!",
|
||||||
category: "persistence",
|
category: "persistence",
|
||||||
enabled: true
|
enabled: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 19,
|
id: 19,
|
||||||
text: "You're stronger than any excuse!",
|
text: "You're stronger than any urge!",
|
||||||
category: "persistence",
|
category: "persistence",
|
||||||
enabled: true
|
enabled: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 20,
|
id: 20,
|
||||||
text: "Champions are made in moments like this!",
|
text: "True control is forged in moments like this!",
|
||||||
category: "persistence",
|
category: "persistence",
|
||||||
enabled: true
|
enabled: true
|
||||||
}
|
}
|
||||||
|
|
@ -402,4 +343,4 @@ gameData.mainTasks.push({
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('🔍 gameData.mainTasks after:', gameData.mainTasks.length);
|
console.log('🔍 gameData.mainTasks after:', gameData.mainTasks.length);
|
||||||
console.log('✅ Mirror task added successfully');
|
console.log('✅ Mirror task added successfully');
|
||||||
|
|
|
||||||
|
|
@ -6,40 +6,21 @@ class AudioManager {
|
||||||
constructor(dataManager, game = null) {
|
constructor(dataManager, game = null) {
|
||||||
this.dataManager = dataManager;
|
this.dataManager = dataManager;
|
||||||
this.game = game; // Reference to game instance for state checking
|
this.game = game; // Reference to game instance for state checking
|
||||||
|
this.audioEnabled = true; // Global flag to disable all audio
|
||||||
|
this.isPlaylistMode = false; // New playlist mode flag
|
||||||
|
this.currentPlaylistAudio = null; // Current playlist audio element
|
||||||
|
this.playlistQueue = []; // Queue of audio files to play
|
||||||
|
this.currentPlaylistIndex = 0; // Current position in playlist
|
||||||
this.audioContext = null;
|
this.audioContext = null;
|
||||||
this.masterGain = null;
|
this.masterGain = null;
|
||||||
|
|
||||||
// Audio category configurations
|
// Audio category configurations - simplified to just background audio
|
||||||
this.categories = {
|
this.categories = {
|
||||||
tasks: {
|
background: {
|
||||||
volume: 0.7,
|
volume: 0.7,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
currentAudio: null,
|
currentAudio: null,
|
||||||
fadeTimeout: null
|
fadeTimeout: null
|
||||||
},
|
|
||||||
punishments: {
|
|
||||||
volume: 0.8,
|
|
||||||
enabled: true,
|
|
||||||
currentAudio: null,
|
|
||||||
fadeTimeout: null
|
|
||||||
},
|
|
||||||
ambient: {
|
|
||||||
volume: 0.3,
|
|
||||||
enabled: true,
|
|
||||||
currentAudio: null,
|
|
||||||
fadeTimeout: null
|
|
||||||
},
|
|
||||||
rewards: {
|
|
||||||
volume: 0.6,
|
|
||||||
enabled: true,
|
|
||||||
currentAudio: null,
|
|
||||||
fadeTimeout: null
|
|
||||||
},
|
|
||||||
instructions: {
|
|
||||||
volume: 0.9,
|
|
||||||
enabled: true,
|
|
||||||
currentAudio: null,
|
|
||||||
fadeTimeout: null
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -53,6 +34,23 @@ class AudioManager {
|
||||||
async init() {
|
async init() {
|
||||||
console.log('AudioManager initializing...');
|
console.log('AudioManager initializing...');
|
||||||
|
|
||||||
|
// Check if we're in a problematic browser environment
|
||||||
|
const isElectron = window.electronAPI !== undefined;
|
||||||
|
const isFileProtocol = window.location.protocol === 'file:';
|
||||||
|
|
||||||
|
if (!isElectron && isFileProtocol) {
|
||||||
|
console.warn('⚠️ Browser file:// mode detected - audio may not work due to CORS restrictions');
|
||||||
|
console.warn('💡 For full audio support, run: npm start (Electron desktop app)');
|
||||||
|
|
||||||
|
// Option to disable audio in browser mode
|
||||||
|
const disableAudio = localStorage.getItem('disableBrowserAudio') === 'true';
|
||||||
|
if (disableAudio) {
|
||||||
|
console.log('🔇 Audio disabled for browser mode');
|
||||||
|
this.isInitialized = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Load saved settings
|
// Load saved settings
|
||||||
this.loadSettings();
|
this.loadSettings();
|
||||||
|
|
||||||
|
|
@ -74,82 +72,146 @@ class AudioManager {
|
||||||
console.log('Available audio categories:', Object.keys(this.audioLibrary));
|
console.log('Available audio categories:', Object.keys(this.audioLibrary));
|
||||||
}
|
}
|
||||||
|
|
||||||
async discoverAudioLibrary() {
|
// Clean up corrupted audio data (paths with URL encoding issues)
|
||||||
// Define the actual file structure based on user's files
|
async cleanupCorruptedAudioData() {
|
||||||
const audioStructure = {
|
if (!window.game || !window.game.dataManager) {
|
||||||
tasks: {
|
return;
|
||||||
teasing: [
|
|
||||||
'enjoying-a-giant-cock.mp3',
|
|
||||||
'horny-japanese-babe-japanese-sex.mp3',
|
|
||||||
'long-and-hard-moan.mp3',
|
|
||||||
'orgasm-sounds.mp3',
|
|
||||||
'playing-her-pussy-with-a-dildo-japanese-sex.mp3',
|
|
||||||
'u.mp3'
|
|
||||||
],
|
|
||||||
intense: [], // Will use teasing files if empty
|
|
||||||
instructions: [] // Will use teasing files if empty
|
|
||||||
},
|
|
||||||
punishments: {
|
|
||||||
denial: [
|
|
||||||
'addicted-to-my-tits-tit-worship-mesmerize-joi-mov.mp3',
|
|
||||||
'dumb-gooner-bitch-4-big-tits.mp3',
|
|
||||||
'you-will-be-left-thoughtless.mp3'
|
|
||||||
],
|
|
||||||
mocking: [
|
|
||||||
'precise-denial.mp3',
|
|
||||||
'punishment-episode-cockring-causes-cumshots-cumshots-gameplay-only.mp3',
|
|
||||||
'quick-jerk-to-tits-in-pink-bra-1080p-ellie-idol.mp3',
|
|
||||||
'stroke-your-goon-bud.mp3'
|
|
||||||
]
|
|
||||||
},
|
|
||||||
ambient: {
|
|
||||||
background: []
|
|
||||||
},
|
|
||||||
rewards: {
|
|
||||||
completion: ['u.mp3']
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Initialize the library structure
|
|
||||||
this.audioLibrary = {};
|
|
||||||
|
|
||||||
for (const [category, subcategories] of Object.entries(audioStructure)) {
|
|
||||||
this.audioLibrary[category] = {};
|
|
||||||
|
|
||||||
for (const [subcategory, files] of Object.entries(subcategories)) {
|
|
||||||
this.audioLibrary[category][subcategory] = [];
|
|
||||||
|
|
||||||
// If subcategory is empty, fallback to teasing files for tasks
|
|
||||||
let filesToUse = files;
|
|
||||||
if (files.length === 0 && category === 'tasks') {
|
|
||||||
filesToUse = audioStructure.tasks.teasing;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check which files actually exist and add them
|
|
||||||
for (const file of filesToUse) {
|
|
||||||
const audioPath = `audio/${category}/${subcategory}/${file}`;
|
|
||||||
|
|
||||||
// For empty subcategories using fallback, use teasing path
|
|
||||||
const actualPath = (files.length === 0 && category === 'tasks')
|
|
||||||
? `audio/tasks/teasing/${file}`
|
|
||||||
: audioPath;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Add to library - we'll test loading when actually playing
|
|
||||||
this.audioLibrary[category][subcategory].push({
|
|
||||||
name: file.split('.')[0],
|
|
||||||
path: actualPath,
|
|
||||||
element: null // Will be created when needed
|
|
||||||
});
|
|
||||||
console.log(`Added audio file: ${actualPath}`);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(`Error adding audio file: ${actualPath}`, error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Audio library discovered:', this.audioLibrary);
|
const customAudio = window.game.dataManager.get('customAudio') || { background: [], ambient: [] };
|
||||||
|
let hasCorruption = false;
|
||||||
|
|
||||||
|
// Check for and fix corrupted paths
|
||||||
|
['background', 'ambient'].forEach(category => {
|
||||||
|
if (customAudio[category]) {
|
||||||
|
customAudio[category] = customAudio[category].filter(audioFile => {
|
||||||
|
// Remove files with corrupted paths (containing URL encoding artifacts)
|
||||||
|
if (audioFile.path && audioFile.path.includes('%08')) {
|
||||||
|
console.log(`Removing corrupted audio file: ${audioFile.path}`);
|
||||||
|
hasCorruption = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove any references to old 'effects' category
|
||||||
|
if (customAudio.effects) {
|
||||||
|
console.log('Removing deprecated effects category');
|
||||||
|
delete customAudio.effects;
|
||||||
|
hasCorruption = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasCorruption) {
|
||||||
|
console.log('🧹 Cleaned up corrupted audio data');
|
||||||
|
window.game.dataManager.set('customAudio', customAudio);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async discoverAudioLibrary() {
|
||||||
|
console.log('🎵 Discovering audio library from user-managed files...');
|
||||||
|
|
||||||
|
// Clean up any corrupted audio data first
|
||||||
|
await this.cleanupCorruptedAudioData();
|
||||||
|
|
||||||
|
// Instead of hardcoded files, use the dynamic audio management system
|
||||||
|
// This integrates with the existing "Manage Audio" menu that users can control
|
||||||
|
|
||||||
|
// Initialize the library structure
|
||||||
|
this.audioLibrary = {
|
||||||
|
background: {
|
||||||
|
general: [] // Single category for all background audio
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get audio files from the existing audio management system
|
||||||
|
if (window.game && window.game.dataManager) {
|
||||||
|
const customAudio = window.game.dataManager.get('customAudio') || { background: [], ambient: [] };
|
||||||
|
|
||||||
|
// Combine all audio categories into a single "background" category
|
||||||
|
const allAudioFiles = [
|
||||||
|
...(customAudio.background || []),
|
||||||
|
...(customAudio.ambient || [])
|
||||||
|
];
|
||||||
|
|
||||||
|
// Add user's audio files to the library (only if they actually exist)
|
||||||
|
const validFiles = [];
|
||||||
|
for (const audioFile of allAudioFiles) {
|
||||||
|
if (audioFile.enabled !== false) { // Only include enabled files
|
||||||
|
// Debug path corruption issue
|
||||||
|
console.log(`Processing audio file:`, audioFile);
|
||||||
|
console.log(`Path before adding:`, audioFile.path);
|
||||||
|
|
||||||
|
// Fix path corruption - decode any URL encoding and normalize path
|
||||||
|
let cleanPath = audioFile.path;
|
||||||
|
if (cleanPath.includes('%')) {
|
||||||
|
try {
|
||||||
|
cleanPath = decodeURIComponent(cleanPath);
|
||||||
|
console.log(`Decoded path: ${cleanPath}`);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`Failed to decode path: ${cleanPath}`, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For Electron with absolute Windows paths, keep them as-is
|
||||||
|
// Don't convert backslashes to forward slashes for Windows absolute paths
|
||||||
|
if (window.electronAPI && cleanPath.match(/^[A-Za-z]:\\/)) {
|
||||||
|
// Windows absolute path - convert backslashes to forward slashes for web compatibility
|
||||||
|
cleanPath = cleanPath.replace(/\\/g, '/');
|
||||||
|
console.log(`Normalized Windows path: ${cleanPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For Electron, validate file exists before adding
|
||||||
|
if (window.electronAPI && window.electronAPI.fs) {
|
||||||
|
try {
|
||||||
|
const exists = await window.electronAPI.fs.existsSync(cleanPath.replace(/\//g, '\\'));
|
||||||
|
if (!exists) {
|
||||||
|
console.warn(`⚠️ File does not exist, skipping: ${cleanPath}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`⚠️ Could not verify file existence: ${cleanPath}`, error);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const audioFileObj = {
|
||||||
|
name: audioFile.name || 'Unnamed Audio',
|
||||||
|
path: cleanPath,
|
||||||
|
element: null
|
||||||
|
};
|
||||||
|
|
||||||
|
this.audioLibrary.background.general.push(audioFileObj);
|
||||||
|
validFiles.push(audioFileObj);
|
||||||
|
console.log(`Added user audio file: ${cleanPath}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update storage to remove non-existent files
|
||||||
|
if (validFiles.length !== allAudioFiles.length && window.game && window.game.dataManager) {
|
||||||
|
console.log(`🧹 Removing ${allAudioFiles.length - validFiles.length} non-existent files from storage`);
|
||||||
|
const updatedCustomAudio = {
|
||||||
|
background: validFiles.filter(f => f.path.includes('/background/')),
|
||||||
|
ambient: validFiles.filter(f => f.path.includes('/ambient/'))
|
||||||
|
};
|
||||||
|
window.game.dataManager.set('customAudio', updatedCustomAudio);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`🎵 Loaded ${validFiles.length} user audio files`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no user audio is available, create empty structure
|
||||||
|
if (this.audioLibrary.background.general.length === 0) {
|
||||||
|
console.log('🎵 No user audio files found.');
|
||||||
|
console.log('🎵 To add background audio:');
|
||||||
|
console.log(' 1. Go to the main menu');
|
||||||
|
console.log(' 2. Click "Manage Audio"');
|
||||||
|
console.log(' 3. Import audio files using the "Import" buttons');
|
||||||
|
console.log(' 4. Audio will automatically play during tasks');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🎵 Audio library discovery completed:', this.audioLibrary);
|
||||||
}
|
}
|
||||||
|
|
||||||
loadSettings() {
|
loadSettings() {
|
||||||
|
|
@ -231,11 +293,29 @@ class AudioManager {
|
||||||
async playAudio(category, subcategory, fileName = null, options = {}) {
|
async playAudio(category, subcategory, fileName = null, options = {}) {
|
||||||
console.log(`PlayAudio called: ${category}/${subcategory}/${fileName || 'random'}`);
|
console.log(`PlayAudio called: ${category}/${subcategory}/${fileName || 'random'}`);
|
||||||
|
|
||||||
|
// Check if audio is globally disabled
|
||||||
|
if (!this.audioEnabled) {
|
||||||
|
console.log('Audio globally disabled - blocking playback');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we're in a skip audio state (for consequences)
|
||||||
|
if (this.game && this.game.gameState && this.game.gameState.skipAudioPlayed) {
|
||||||
|
console.log('Skip audio played flag set - blocking background audio during consequence transition');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.isInitialized) {
|
if (!this.isInitialized) {
|
||||||
console.warn('AudioManager not initialized yet');
|
console.warn('AudioManager not initialized yet');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if we're in browser mode and warn about limitations
|
||||||
|
const isElectron = window.electronAPI !== undefined;
|
||||||
|
if (!isElectron) {
|
||||||
|
console.warn('Running in browser mode - audio may have limitations due to file:// restrictions');
|
||||||
|
}
|
||||||
|
|
||||||
// Check if the game is still running - prevent audio after game ends
|
// Check if the game is still running - prevent audio after game ends
|
||||||
if (this.game && this.game.gameState && !this.game.gameState.isRunning) {
|
if (this.game && this.game.gameState && !this.game.gameState.isRunning) {
|
||||||
console.log(`Game is not running - blocking audio playback for ${category}/${subcategory}`);
|
console.log(`Game is not running - blocking audio playback for ${category}/${subcategory}`);
|
||||||
|
|
@ -275,31 +355,120 @@ class AudioManager {
|
||||||
|
|
||||||
console.log(`Selected audio file: ${audioFile.path}`);
|
console.log(`Selected audio file: ${audioFile.path}`);
|
||||||
|
|
||||||
|
// Validate the audio file path
|
||||||
|
if (!audioFile.path || audioFile.path.trim() === '') {
|
||||||
|
console.error(`Invalid audio file path: ${audioFile.path}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// Stop any currently playing audio in this category immediately and synchronously
|
// Stop any currently playing audio in this category immediately and synchronously
|
||||||
if (this.categories[category].currentAudio) {
|
if (this.categories[category].currentAudio) {
|
||||||
const existingAudio = this.categories[category].currentAudio;
|
const existingAudio = this.categories[category].currentAudio;
|
||||||
existingAudio.pause();
|
existingAudio.pause();
|
||||||
existingAudio.currentTime = 0;
|
existingAudio.currentTime = 0;
|
||||||
existingAudio.src = ''; // Clear source to fully stop
|
existingAudio._stopped = true; // Mark as stopped to prevent further playback
|
||||||
this.categories[category].currentAudio = null;
|
console.log(`[src debug] Stopping previous audio:`, existingAudio.src);
|
||||||
console.log(`Forcibly stopped existing audio in category: ${category}`);
|
// Don't clear src as it causes corruption - just pause and reset time
|
||||||
|
// Also remove from DOM if it exists
|
||||||
|
if (existingAudio.parentNode) {
|
||||||
|
existingAudio.parentNode.removeChild(existingAudio);
|
||||||
|
}
|
||||||
|
this.categories[category].currentAudio = null;
|
||||||
|
console.log(`Forcibly stopped existing audio in category: ${category}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear any pending fade timeout for this category
|
||||||
|
if (this.categories[category].fadeTimeout) {
|
||||||
|
clearTimeout(this.categories[category].fadeTimeout);
|
||||||
|
this.categories[category].fadeTimeout = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create and configure audio element
|
// Create and configure audio element
|
||||||
const audio = new Audio(audioFile.path);
|
const audio = new Audio();
|
||||||
|
audio._stopped = false; // Flag to prevent orphaned playback
|
||||||
|
|
||||||
|
// Set the source with proper error handling
|
||||||
|
try {
|
||||||
|
console.log(`Setting audio source to: ${audioFile.path}`);
|
||||||
|
console.log(`Audio file object:`, audioFile);
|
||||||
|
|
||||||
|
// Debug: Check if path is already corrupted
|
||||||
|
if (audioFile.path.includes('%')) {
|
||||||
|
console.error(`Path already contains URL encoding: ${audioFile.path}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For Electron, construct proper file:// URL for absolute paths
|
||||||
|
let audioSrc = audioFile.path;
|
||||||
|
if (window.electronAPI && audioFile.path.match(/^[A-Za-z]:\//)) {
|
||||||
|
// Absolute Windows path - convert to file:// URL
|
||||||
|
audioSrc = `file:///${audioFile.path}`;
|
||||||
|
console.log(`Converted to file URL: ${audioSrc}`);
|
||||||
|
} else if (window.electronAPI && !audioFile.path.startsWith('file://')) {
|
||||||
|
// Relative path in Electron - make it relative to app root
|
||||||
|
audioSrc = audioFile.path;
|
||||||
|
console.log(`Using relative path: ${audioSrc}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[src debug] Setting audio.src to:`, audioSrc);
|
||||||
|
audio.src = audioSrc;
|
||||||
|
console.log(`[src debug] audio.src after assignment:`, audio.src);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error setting audio source: ${audioFile.path}`, error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
audio.volume = this.categories[category].volume * this.masterVolume;
|
audio.volume = this.categories[category].volume * this.masterVolume;
|
||||||
audio.loop = options.loop || false;
|
audio.loop = options.loop || false;
|
||||||
|
|
||||||
console.log(`Audio volume set to: ${audio.volume} (category: ${this.categories[category].volume}, master: ${this.masterVolume})`);
|
console.log(`Audio volume set to: ${audio.volume} (category: ${this.categories[category].volume}, master: ${this.masterVolume})`);
|
||||||
|
|
||||||
// Add error handling
|
// Add comprehensive error handling with retry logic
|
||||||
audio.addEventListener('error', (e) => {
|
audio.addEventListener('error', (e) => {
|
||||||
console.error(`Audio file failed to load: ${audioFile.path}`, e);
|
console.error(`Audio file failed to load: ${audioFile.path}`, e);
|
||||||
console.error('Error details:', {
|
console.error('Error details:', {
|
||||||
error: e.target.error,
|
error: e.target.error,
|
||||||
networkState: e.target.networkState,
|
networkState: e.target.networkState,
|
||||||
readyState: e.target.readyState
|
readyState: e.target.readyState,
|
||||||
|
src: e.target.src,
|
||||||
|
srcAttribute: audio.getAttribute('src'),
|
||||||
|
srcProperty: audio.src,
|
||||||
|
errorCode: e.target.error?.code,
|
||||||
|
errorMessage: e.target.error?.message
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Clean up failed audio element
|
||||||
|
if (this.categories[category].currentAudio === audio) {
|
||||||
|
this.categories[category].currentAudio = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this was a specific file request and it failed, don't retry
|
||||||
|
if (fileName) {
|
||||||
|
console.log(`Specific audio file ${fileName} failed, not retrying`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For browser mode with file:// restrictions, disable retries to prevent spam
|
||||||
|
const isElectron = window.electronAPI !== undefined;
|
||||||
|
if (!isElectron && window.location.protocol === 'file:') {
|
||||||
|
console.warn('Browser file:// mode detected - disabling audio retries due to CORS restrictions');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For random selection, try a different file if available
|
||||||
|
const remainingFiles = audioFiles.filter(f => f !== audioFile);
|
||||||
|
if (remainingFiles.length > 0) {
|
||||||
|
console.log(`Retrying with different audio file from ${category}/${subcategory}`);
|
||||||
|
// Try again with a different random file after a short delay
|
||||||
|
setTimeout(() => {
|
||||||
|
// Check if game is still running before retrying
|
||||||
|
if (!window.game || !window.game.running) {
|
||||||
|
console.log('Game not running - canceling audio retry');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.playAudio(category, subcategory, null, options);
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
audio.addEventListener('loadstart', () => {
|
audio.addEventListener('loadstart', () => {
|
||||||
|
|
@ -307,9 +476,17 @@ class AudioManager {
|
||||||
});
|
});
|
||||||
|
|
||||||
audio.addEventListener('canplay', () => {
|
audio.addEventListener('canplay', () => {
|
||||||
|
if (audio._stopped) {
|
||||||
|
console.log(`Audio ready but stopped - not playing: ${audioFile.path}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
console.log(`Audio ready to play: ${audioFile.path}`);
|
console.log(`Audio ready to play: ${audioFile.path}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
audio.addEventListener('loadeddata', () => {
|
||||||
|
console.log(`Audio data loaded: ${audioFile.path}`);
|
||||||
|
});
|
||||||
|
|
||||||
// Set up event handlers
|
// Set up event handlers
|
||||||
audio.addEventListener('ended', () => {
|
audio.addEventListener('ended', () => {
|
||||||
if (this.categories[category].currentAudio === audio) {
|
if (this.categories[category].currentAudio === audio) {
|
||||||
|
|
@ -317,14 +494,23 @@ class AudioManager {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
audio.addEventListener('error', (e) => {
|
// Store reference
|
||||||
console.error(`Error playing audio ${audioFile.path}:`, e);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Store reference and play
|
|
||||||
this.categories[category].currentAudio = audio;
|
this.categories[category].currentAudio = audio;
|
||||||
|
|
||||||
|
// Use a small delay before attempting to play to ensure audio element is ready
|
||||||
try {
|
try {
|
||||||
|
// First, load the audio
|
||||||
|
audio.load();
|
||||||
|
|
||||||
|
// Small delay to ensure the audio element is properly initialized
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 50));
|
||||||
|
|
||||||
|
// Check if audio was stopped while loading
|
||||||
|
if (audio._stopped) {
|
||||||
|
console.log(`Audio was stopped while loading - not playing: ${audioFile.path}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
await audio.play();
|
await audio.play();
|
||||||
console.log(`Playing audio: ${category}/${subcategory}/${audioFile.name}`);
|
console.log(`Playing audio: ${category}/${subcategory}/${audioFile.name}`);
|
||||||
|
|
||||||
|
|
@ -336,6 +522,13 @@ class AudioManager {
|
||||||
return audio;
|
return audio;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to play audio: ${audioFile.path}`, error);
|
console.error(`Failed to play audio: ${audioFile.path}`, error);
|
||||||
|
console.error(`Play error details:`, {
|
||||||
|
name: error.name,
|
||||||
|
message: error.message,
|
||||||
|
audioSrc: audio.src,
|
||||||
|
audioReadyState: audio.readyState,
|
||||||
|
audioNetworkState: audio.networkState
|
||||||
|
});
|
||||||
this.categories[category].currentAudio = null;
|
this.categories[category].currentAudio = null;
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -349,17 +542,30 @@ class AudioManager {
|
||||||
|
|
||||||
const audio = this.categories[category].currentAudio;
|
const audio = this.categories[category].currentAudio;
|
||||||
console.log(`Stopping audio in category: ${category} with fadeOut: ${fadeOut}ms`);
|
console.log(`Stopping audio in category: ${category} with fadeOut: ${fadeOut}ms`);
|
||||||
|
|
||||||
if (fadeOut > 0) {
|
if (fadeOut > 0) {
|
||||||
this.fadeOut(audio, fadeOut, () => {
|
this.fadeOut(audio, fadeOut, () => {
|
||||||
audio.pause();
|
audio.pause();
|
||||||
audio.currentTime = 0; // Reset to beginning
|
audio.currentTime = 0; // Reset to beginning
|
||||||
|
console.log(`[src debug] Clearing src of stopped audio:`, audio.src);
|
||||||
|
audio.src = '';
|
||||||
|
console.log(`[src debug] src after clearing:`, audio.src);
|
||||||
|
// Also remove from DOM if it exists
|
||||||
|
if (audio.parentNode) {
|
||||||
|
audio.parentNode.removeChild(audio);
|
||||||
|
}
|
||||||
this.categories[category].currentAudio = null;
|
this.categories[category].currentAudio = null;
|
||||||
console.log(`Audio stopped in category: ${category}`);
|
console.log(`Audio stopped in category: ${category}`);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
audio.pause();
|
audio.pause();
|
||||||
audio.currentTime = 0; // Reset to beginning
|
audio.currentTime = 0; // Reset to beginning
|
||||||
|
audio._stopped = true; // Mark as stopped to prevent further playback
|
||||||
|
console.log(`[src debug] Stopping audio:`, audio.src);
|
||||||
|
// Don't clear src as it causes corruption - just pause and reset time
|
||||||
|
// Also remove from DOM if it exists
|
||||||
|
if (audio.parentNode) {
|
||||||
|
audio.parentNode.removeChild(audio);
|
||||||
|
}
|
||||||
this.categories[category].currentAudio = null;
|
this.categories[category].currentAudio = null;
|
||||||
console.log(`Audio immediately stopped in category: ${category}`);
|
console.log(`Audio immediately stopped in category: ${category}`);
|
||||||
}
|
}
|
||||||
|
|
@ -374,12 +580,26 @@ class AudioManager {
|
||||||
// Immediately stop all audio without fade
|
// Immediately stop all audio without fade
|
||||||
stopAllImmediate() {
|
stopAllImmediate() {
|
||||||
console.log('Stopping all audio immediately...');
|
console.log('Stopping all audio immediately...');
|
||||||
|
|
||||||
|
// Stop playlist first
|
||||||
|
this.stopPlaylist();
|
||||||
|
|
||||||
|
// Globally disable audio to prevent any new playback
|
||||||
|
this.audioEnabled = false;
|
||||||
|
|
||||||
for (const category in this.categories) {
|
for (const category in this.categories) {
|
||||||
if (this.categories[category].currentAudio) {
|
if (this.categories[category].currentAudio) {
|
||||||
const audio = this.categories[category].currentAudio;
|
const audio = this.categories[category].currentAudio;
|
||||||
audio.pause();
|
audio.pause();
|
||||||
audio.currentTime = 0;
|
audio.currentTime = 0;
|
||||||
audio.src = ''; // Clear source to fully stop
|
audio._stopped = true; // Mark as stopped to prevent further playback
|
||||||
|
// Don't clear src as it causes corruption - just pause and reset
|
||||||
|
|
||||||
|
// Also remove from DOM if it exists
|
||||||
|
if (audio.parentNode) {
|
||||||
|
audio.parentNode.removeChild(audio);
|
||||||
|
}
|
||||||
|
|
||||||
this.categories[category].currentAudio = null;
|
this.categories[category].currentAudio = null;
|
||||||
console.log(`Forcibly stopped audio in category: ${category}`);
|
console.log(`Forcibly stopped audio in category: ${category}`);
|
||||||
}
|
}
|
||||||
|
|
@ -392,6 +612,161 @@ class AudioManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Re-enable audio (call when game starts)
|
||||||
|
enableAudio() {
|
||||||
|
console.log('Audio re-enabled');
|
||||||
|
this.audioEnabled = true;
|
||||||
|
|
||||||
|
// Clear any stopped audio elements to prevent orphaned events
|
||||||
|
for (const category in this.categories) {
|
||||||
|
if (this.categories[category].currentAudio && this.categories[category].currentAudio._stopped) {
|
||||||
|
// Remove stopped audio elements completely
|
||||||
|
const stoppedAudio = this.categories[category].currentAudio;
|
||||||
|
if (stoppedAudio.parentNode) {
|
||||||
|
stoppedAudio.parentNode.removeChild(stoppedAudio);
|
||||||
|
}
|
||||||
|
this.categories[category].currentAudio = null;
|
||||||
|
console.log(`Cleared stopped audio element in category: ${category}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start continuous background audio playlist
|
||||||
|
startBackgroundPlaylist() {
|
||||||
|
if (!this.audioEnabled) {
|
||||||
|
console.log('Audio disabled - not starting playlist');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get background audio files
|
||||||
|
const backgroundFiles = this.audioLibrary.background?.general || [];
|
||||||
|
if (backgroundFiles.length === 0) {
|
||||||
|
console.log('No background audio files available for playlist');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shuffle the playlist
|
||||||
|
this.playlistQueue = [...backgroundFiles].sort(() => Math.random() - 0.5);
|
||||||
|
this.currentPlaylistIndex = 0;
|
||||||
|
this.isPlaylistMode = true;
|
||||||
|
|
||||||
|
console.log(`🎵 Starting background audio playlist with ${this.playlistQueue.length} tracks`);
|
||||||
|
this.playNextInPlaylist();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Play next track in playlist
|
||||||
|
playNextInPlaylist() {
|
||||||
|
if (!this.isPlaylistMode || !this.audioEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.playlistQueue.length === 0) {
|
||||||
|
console.log('Playlist is empty');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop current playlist audio if playing
|
||||||
|
if (this.currentPlaylistAudio) {
|
||||||
|
this.currentPlaylistAudio.pause();
|
||||||
|
this.currentPlaylistAudio.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get next track (loop back to start if at end)
|
||||||
|
if (this.currentPlaylistIndex >= this.playlistQueue.length) {
|
||||||
|
this.currentPlaylistIndex = 0;
|
||||||
|
// Re-shuffle for variety
|
||||||
|
this.playlistQueue = [...this.playlistQueue].sort(() => Math.random() - 0.5);
|
||||||
|
console.log('🔄 Playlist completed, reshuffling and restarting');
|
||||||
|
}
|
||||||
|
|
||||||
|
const audioFile = this.playlistQueue[this.currentPlaylistIndex];
|
||||||
|
console.log(`🎵 Playing playlist track ${this.currentPlaylistIndex + 1}/${this.playlistQueue.length}: ${audioFile.name}`);
|
||||||
|
|
||||||
|
// Create audio element
|
||||||
|
this.currentPlaylistAudio = new Audio();
|
||||||
|
const audio = this.currentPlaylistAudio;
|
||||||
|
|
||||||
|
// Set up event handlers
|
||||||
|
audio.addEventListener('ended', () => {
|
||||||
|
console.log(`🎵 Track ended: ${audioFile.name}`);
|
||||||
|
this.currentPlaylistIndex++;
|
||||||
|
// Small delay before next track
|
||||||
|
setTimeout(() => this.playNextInPlaylist(), 500);
|
||||||
|
});
|
||||||
|
|
||||||
|
audio.addEventListener('error', (e) => {
|
||||||
|
console.error(`🎵 Playlist track failed: ${audioFile.name}`, e);
|
||||||
|
this.currentPlaylistIndex++;
|
||||||
|
// Try next track after error
|
||||||
|
setTimeout(() => this.playNextInPlaylist(), 1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set source and play
|
||||||
|
try {
|
||||||
|
// Use the same file URL conversion logic as the main playAudio method
|
||||||
|
let audioSrc = audioFile.path;
|
||||||
|
if (window.electronAPI && audioFile.path.match(/^[A-Za-z]:\//)) {
|
||||||
|
// Absolute Windows path - convert to file:// URL
|
||||||
|
audioSrc = `file:///${audioFile.path}`;
|
||||||
|
console.log(`🎵 Converted to file URL: ${audioSrc}`);
|
||||||
|
} else if (window.electronAPI && !audioFile.path.startsWith('file://')) {
|
||||||
|
// Relative path in Electron - make it relative to app root
|
||||||
|
audioSrc = audioFile.path;
|
||||||
|
console.log(`🎵 Using relative path: ${audioSrc}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
audio.src = audioSrc;
|
||||||
|
audio.volume = this.getMasterVolume() * (this.getCategoryVolume('background') || 0.7);
|
||||||
|
audio.play().then(() => {
|
||||||
|
console.log(`🎵 Now playing: ${audioFile.name}`);
|
||||||
|
}).catch(error => {
|
||||||
|
console.error(`🎵 Failed to play playlist track: ${audioFile.name}`, error);
|
||||||
|
this.currentPlaylistIndex++;
|
||||||
|
setTimeout(() => this.playNextInPlaylist(), 1000);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`🎵 Error setting up playlist track: ${audioFile.name}`, error);
|
||||||
|
this.currentPlaylistIndex++;
|
||||||
|
setTimeout(() => this.playNextInPlaylist(), 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pause playlist
|
||||||
|
pausePlaylist() {
|
||||||
|
if (this.currentPlaylistAudio && !this.currentPlaylistAudio.paused) {
|
||||||
|
this.currentPlaylistAudio.pause();
|
||||||
|
console.log('🎵 Playlist paused');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resume playlist
|
||||||
|
resumePlaylist() {
|
||||||
|
if (this.currentPlaylistAudio && this.currentPlaylistAudio.paused) {
|
||||||
|
this.currentPlaylistAudio.play().then(() => {
|
||||||
|
console.log('🎵 Playlist resumed');
|
||||||
|
}).catch(error => {
|
||||||
|
console.error('🎵 Failed to resume playlist', error);
|
||||||
|
// Try next track if current one fails
|
||||||
|
this.currentPlaylistIndex++;
|
||||||
|
setTimeout(() => this.playNextInPlaylist(), 500);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop playlist completely
|
||||||
|
stopPlaylist() {
|
||||||
|
this.isPlaylistMode = false;
|
||||||
|
if (this.currentPlaylistAudio) {
|
||||||
|
this.currentPlaylistAudio.pause();
|
||||||
|
this.currentPlaylistAudio.src = '';
|
||||||
|
this.currentPlaylistAudio.remove();
|
||||||
|
this.currentPlaylistAudio = null;
|
||||||
|
}
|
||||||
|
this.playlistQueue = [];
|
||||||
|
this.currentPlaylistIndex = 0;
|
||||||
|
console.log('🎵 Playlist stopped');
|
||||||
|
}
|
||||||
|
|
||||||
// Stop all audio
|
// Stop all audio
|
||||||
stopAll(fadeOut = 0) {
|
stopAll(fadeOut = 0) {
|
||||||
for (const category in this.categories) {
|
for (const category in this.categories) {
|
||||||
|
|
@ -504,21 +879,175 @@ class AudioManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convenient methods for specific audio types
|
// Convenient methods for specific audio types - now all use background audio
|
||||||
playTaskAudio(intensity = 'teasing', options = {}) {
|
playTaskAudio(intensity = 'general', options = {}) {
|
||||||
return this.playAudio('tasks', intensity, null, options);
|
return this.playBackgroundAudio(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
playPunishmentAudio(type = 'denial', options = {}) {
|
playPunishmentAudio(type = 'general', options = {}) {
|
||||||
return this.playAudio('punishments', type, null, options);
|
return this.playBackgroundAudio(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
playRewardAudio(options = {}) {
|
playRewardAudio(options = {}) {
|
||||||
return this.playAudio('rewards', 'completion', null, options);
|
return this.playBackgroundAudio(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
playInstructionAudio(instruction, options = {}) {
|
playInstructionAudio(instruction, options = {}) {
|
||||||
return this.playAudio('instructions', 'commands', instruction, options);
|
return this.playBackgroundAudio(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main background audio playing method
|
||||||
|
playBackgroundAudio(options = {}) {
|
||||||
|
return this.playAudio('background', 'general', null, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enhanced playAudio with fallback support - simplified since we only have one category
|
||||||
|
async playAudioWithFallback(category, primarySubcategory, fallbackSubcategory, fileName = null, options = {}) {
|
||||||
|
try {
|
||||||
|
return await this.playAudio(category, primarySubcategory, fileName, options);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`Audio playback failed for ${category}/${primarySubcategory}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan audio directories for files
|
||||||
|
async scanAudioDirectories() {
|
||||||
|
console.log('🔍 Scanning audio directories...');
|
||||||
|
|
||||||
|
const scannedFiles = [];
|
||||||
|
|
||||||
|
// Simplified directory structure - only background and ambient
|
||||||
|
const audioDirectories = [
|
||||||
|
'audio/background',
|
||||||
|
'audio/ambient'
|
||||||
|
];
|
||||||
|
|
||||||
|
// For Electron environment, we can use the existing readAudioDirectory API
|
||||||
|
if (window.electronAPI && window.electronAPI.readAudioDirectory) {
|
||||||
|
try {
|
||||||
|
for (const directory of audioDirectories) {
|
||||||
|
console.log(`Scanning directory: ${directory}`);
|
||||||
|
|
||||||
|
// Get the full path to the directory
|
||||||
|
const appPath = await window.electronAPI.getAppPath();
|
||||||
|
const fullPath = await window.electronAPI.pathJoin(appPath, directory);
|
||||||
|
|
||||||
|
// Check if directory exists before trying to read it
|
||||||
|
const dirExists = await window.electronAPI.fileExists(fullPath);
|
||||||
|
if (!dirExists) {
|
||||||
|
console.log(`Directory does not exist: ${fullPath}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = await window.electronAPI.readAudioDirectory(fullPath);
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
scannedFiles.push({
|
||||||
|
name: file.name,
|
||||||
|
path: file.path,
|
||||||
|
title: file.title || file.name,
|
||||||
|
directory: directory,
|
||||||
|
enabled: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`🔍 Found ${scannedFiles.length} audio files in directories`);
|
||||||
|
|
||||||
|
// Add scanned files to the audio library if they're not already in customAudio
|
||||||
|
if (scannedFiles.length > 0) {
|
||||||
|
await this.addScannedFilesToLibrary(scannedFiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
return scannedFiles;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error scanning audio directories:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn('Directory scanning not available - requires Electron environment');
|
||||||
|
console.log('💡 To use directory scanning:');
|
||||||
|
console.log(' 1. Run the app with: npm start');
|
||||||
|
console.log(' 2. Or manually add audio files via "Manage Audio" menu');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add scanned files to the audio library
|
||||||
|
async addScannedFilesToLibrary(scannedFiles) {
|
||||||
|
if (!window.game || !window.game.dataManager) {
|
||||||
|
console.warn('DataManager not available for adding scanned files');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const customAudio = window.game.dataManager.get('customAudio') || {
|
||||||
|
background: [],
|
||||||
|
ambient: []
|
||||||
|
};
|
||||||
|
|
||||||
|
let addedCount = 0;
|
||||||
|
|
||||||
|
for (const file of scannedFiles) {
|
||||||
|
// Check if file is already in the library
|
||||||
|
const existsInBackground = customAudio.background.some(f => f.path === file.path);
|
||||||
|
const existsInAmbient = customAudio.ambient.some(f => f.path === file.path);
|
||||||
|
|
||||||
|
if (!existsInBackground && !existsInAmbient) {
|
||||||
|
// Determine which category to add to based on directory
|
||||||
|
let category = 'background'; // Default category
|
||||||
|
|
||||||
|
if (file.directory.includes('ambient')) {
|
||||||
|
category = 'ambient';
|
||||||
|
}
|
||||||
|
// Note: Since we removed effects, tasks, etc., everything else goes to background
|
||||||
|
|
||||||
|
customAudio[category].push({
|
||||||
|
name: file.name,
|
||||||
|
path: file.path,
|
||||||
|
enabled: true,
|
||||||
|
source: 'directory_scan'
|
||||||
|
});
|
||||||
|
|
||||||
|
addedCount++;
|
||||||
|
console.log(`Added ${file.name} to ${category} category`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addedCount > 0) {
|
||||||
|
// Save updated audio library
|
||||||
|
window.game.dataManager.set('customAudio', customAudio);
|
||||||
|
|
||||||
|
// Refresh the audio library to include new files
|
||||||
|
await this.discoverAudioLibrary();
|
||||||
|
|
||||||
|
console.log(`✅ Added ${addedCount} new audio files to library`);
|
||||||
|
|
||||||
|
// Show success message to user
|
||||||
|
if (window.game && window.game.flashMessageManager) {
|
||||||
|
window.game.flashMessageManager.show(
|
||||||
|
`Audio Scan Complete: Added ${addedCount} audio files to library`,
|
||||||
|
'success'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('ℹ️ No new audio files found during scan');
|
||||||
|
|
||||||
|
if (window.game && window.game.flashMessageManager) {
|
||||||
|
window.game.flashMessageManager.show(
|
||||||
|
'Audio scan complete - no new files found',
|
||||||
|
'info'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh audio library - useful when user adds new audio files
|
||||||
|
async refreshAudioLibrary() {
|
||||||
|
console.log('🎵 Refreshing audio library...');
|
||||||
|
await this.discoverAudioLibrary();
|
||||||
|
console.log('🎵 Audio library refreshed');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get settings for UI
|
// Get settings for UI
|
||||||
|
|
@ -530,4 +1059,10 @@ class AudioManager {
|
||||||
isInitialized: this.isInitialized
|
isInitialized: this.isInitialized
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Public method to trigger directory scan (for UI buttons)
|
||||||
|
async triggerDirectoryScan() {
|
||||||
|
console.log('🔍 Triggering audio directory scan from UI...');
|
||||||
|
return await this.scanAudioDirectories();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1183,6 +1183,11 @@ class InteractiveTaskManager {
|
||||||
startBtn.addEventListener('click', () => {
|
startBtn.addEventListener('click', () => {
|
||||||
console.log('🧘 Starting focus session');
|
console.log('🧘 Starting focus session');
|
||||||
|
|
||||||
|
// Track focus session start for XP (5 XP per minute)
|
||||||
|
if (this.game && this.game.trackFocusSession) {
|
||||||
|
this.game.trackFocusSession(true);
|
||||||
|
}
|
||||||
|
|
||||||
// Hide start button and show progress
|
// Hide start button and show progress
|
||||||
startBtn.style.display = 'none';
|
startBtn.style.display = 'none';
|
||||||
progressContainer.style.display = 'block';
|
progressContainer.style.display = 'block';
|
||||||
|
|
@ -1214,6 +1219,11 @@ class InteractiveTaskManager {
|
||||||
if (timeLeft < 0) {
|
if (timeLeft < 0) {
|
||||||
clearInterval(focusInterval);
|
clearInterval(focusInterval);
|
||||||
|
|
||||||
|
// Track focus session end for XP
|
||||||
|
if (this.game && this.game.trackFocusSession) {
|
||||||
|
this.game.trackFocusSession(false);
|
||||||
|
}
|
||||||
|
|
||||||
// Focus session completed
|
// Focus session completed
|
||||||
timerDisplay.textContent = 'Complete!';
|
timerDisplay.textContent = 'Complete!';
|
||||||
progressText.textContent = '✓ Focus session completed';
|
progressText.textContent = '✓ Focus session completed';
|
||||||
|
|
@ -1523,6 +1533,11 @@ class InteractiveTaskManager {
|
||||||
console.log('🧘 🎬 Stopping focus video playback...');
|
console.log('🧘 🎬 Stopping focus video playback...');
|
||||||
this.focusVideoActive = false;
|
this.focusVideoActive = false;
|
||||||
|
|
||||||
|
// End focus session tracking for XP (if still active)
|
||||||
|
if (this.game && this.game.trackFocusSession) {
|
||||||
|
this.game.trackFocusSession(false);
|
||||||
|
}
|
||||||
|
|
||||||
if (this.focusVideoPlayer) {
|
if (this.focusVideoPlayer) {
|
||||||
// Pause and clear the video
|
// Pause and clear the video
|
||||||
this.focusVideoPlayer.pause();
|
this.focusVideoPlayer.pause();
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,22 @@ class FlashMessageManager {
|
||||||
this.messages = gameData.defaultFlashMessages.filter(msg => msg.enabled !== false);
|
this.messages = gameData.defaultFlashMessages.filter(msg => msg.enabled !== false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Public show method for compatibility with game.js
|
||||||
|
* @param {string|object} message - Message text or object
|
||||||
|
* @param {string} type - Message type (info, error, etc.)
|
||||||
|
*/
|
||||||
|
show(message, type = 'info') {
|
||||||
|
// Optionally style based on type
|
||||||
|
let config = {};
|
||||||
|
if (type === 'error') {
|
||||||
|
config = { color: '#fff', backgroundColor: '#d32f2f' };
|
||||||
|
} else if (type === 'info') {
|
||||||
|
config = { color: '#fff', backgroundColor: '#1976d2' };
|
||||||
|
}
|
||||||
|
this.showMessage(message, config);
|
||||||
|
}
|
||||||
|
|
||||||
createMessageElement() {
|
createMessageElement() {
|
||||||
// Create the overlay element for flash messages
|
// Create the overlay element for flash messages
|
||||||
|
|
|
||||||
|
|
@ -197,6 +197,11 @@ class WebcamManager {
|
||||||
this.currentPhotoSession.photos.push(photoData);
|
this.currentPhotoSession.photos.push(photoData);
|
||||||
this.capturedPhotos.push(photoData);
|
this.capturedPhotos.push(photoData);
|
||||||
|
|
||||||
|
// Track photo for XP (1 XP per photo)
|
||||||
|
if (this.game && this.game.incrementPhotosTaken) {
|
||||||
|
this.game.incrementPhotosTaken();
|
||||||
|
}
|
||||||
|
|
||||||
// Update progress indicators
|
// Update progress indicators
|
||||||
const photosTaken = this.currentPhotoSession.photos.length;
|
const photosTaken = this.currentPhotoSession.photos.length;
|
||||||
const photosNeeded = this.currentPhotoSession.photosNeeded;
|
const photosNeeded = this.currentPhotoSession.photosNeeded;
|
||||||
|
|
@ -643,6 +648,11 @@ class WebcamManager {
|
||||||
this.currentPhotoSession.photos.push(photoData);
|
this.currentPhotoSession.photos.push(photoData);
|
||||||
this.capturedPhotos.push(photoData);
|
this.capturedPhotos.push(photoData);
|
||||||
|
|
||||||
|
// Track photo for XP (1 XP per photo)
|
||||||
|
if (this.game && this.game.incrementPhotosTaken) {
|
||||||
|
this.game.incrementPhotosTaken();
|
||||||
|
}
|
||||||
|
|
||||||
// Update photo count
|
// Update photo count
|
||||||
document.getElementById('photo-count').textContent = this.currentPhotoSession.photos.length;
|
document.getElementById('photo-count').textContent = this.currentPhotoSession.photos.length;
|
||||||
|
|
||||||
|
|
@ -1113,6 +1123,11 @@ class WebcamManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Track webcam mirror start for XP (5 XP per minute)
|
||||||
|
if (this.game && this.game.trackWebcamMirror) {
|
||||||
|
this.game.trackWebcamMirror(true);
|
||||||
|
}
|
||||||
|
|
||||||
// Show mirror interface
|
// Show mirror interface
|
||||||
this.showMirrorInterface(taskData);
|
this.showMirrorInterface(taskData);
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -1500,6 +1515,11 @@ class WebcamManager {
|
||||||
closeMirrorMode() {
|
closeMirrorMode() {
|
||||||
console.log('🔚 Closing mirror mode');
|
console.log('🔚 Closing mirror mode');
|
||||||
|
|
||||||
|
// Track webcam mirror end for XP
|
||||||
|
if (this.game && this.game.trackWebcamMirror) {
|
||||||
|
this.game.trackWebcamMirror(false);
|
||||||
|
}
|
||||||
|
|
||||||
// Remove mirror overlay
|
// Remove mirror overlay
|
||||||
const overlay = document.getElementById('mirror-overlay');
|
const overlay = document.getElementById('mirror-overlay');
|
||||||
if (overlay) {
|
if (overlay) {
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,7 @@ class DesktopFileManager {
|
||||||
};
|
};
|
||||||
this.audioDirectories = {
|
this.audioDirectories = {
|
||||||
background: null,
|
background: null,
|
||||||
ambient: null,
|
ambient: null
|
||||||
effects: null
|
|
||||||
};
|
};
|
||||||
this.videoDirectories = {
|
this.videoDirectories = {
|
||||||
background: null,
|
background: null,
|
||||||
|
|
@ -35,7 +34,6 @@ class DesktopFileManager {
|
||||||
|
|
||||||
this.audioDirectories.background = await window.electronAPI.pathJoin(this.appPath, 'audio', 'background');
|
this.audioDirectories.background = await window.electronAPI.pathJoin(this.appPath, 'audio', 'background');
|
||||||
this.audioDirectories.ambient = await window.electronAPI.pathJoin(this.appPath, 'audio', 'ambient');
|
this.audioDirectories.ambient = await window.electronAPI.pathJoin(this.appPath, 'audio', 'ambient');
|
||||||
this.audioDirectories.effects = await window.electronAPI.pathJoin(this.appPath, 'audio', 'effects');
|
|
||||||
|
|
||||||
this.videoDirectories.background = await window.electronAPI.pathJoin(this.appPath, 'videos', 'background');
|
this.videoDirectories.background = await window.electronAPI.pathJoin(this.appPath, 'videos', 'background');
|
||||||
this.videoDirectories.tasks = await window.electronAPI.pathJoin(this.appPath, 'videos', 'tasks');
|
this.videoDirectories.tasks = await window.electronAPI.pathJoin(this.appPath, 'videos', 'tasks');
|
||||||
|
|
@ -48,7 +46,6 @@ class DesktopFileManager {
|
||||||
|
|
||||||
await window.electronAPI.createDirectory(this.audioDirectories.background);
|
await window.electronAPI.createDirectory(this.audioDirectories.background);
|
||||||
await window.electronAPI.createDirectory(this.audioDirectories.ambient);
|
await window.electronAPI.createDirectory(this.audioDirectories.ambient);
|
||||||
await window.electronAPI.createDirectory(this.audioDirectories.effects);
|
|
||||||
|
|
||||||
await window.electronAPI.createDirectory(this.videoDirectories.background);
|
await window.electronAPI.createDirectory(this.videoDirectories.background);
|
||||||
await window.electronAPI.createDirectory(this.videoDirectories.tasks);
|
await window.electronAPI.createDirectory(this.videoDirectories.tasks);
|
||||||
|
|
@ -490,22 +487,20 @@ class DesktopFileManager {
|
||||||
async scanAllAudioDirectories() {
|
async scanAllAudioDirectories() {
|
||||||
if (!this.isElectron) {
|
if (!this.isElectron) {
|
||||||
this.showNotification('Audio directory scanning only available in desktop version', 'warning');
|
this.showNotification('Audio directory scanning only available in desktop version', 'warning');
|
||||||
return { background: [], ambient: [], effects: [] };
|
return { background: [], ambient: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
const backgroundAudio = await this.scanDirectoryForAudio('background');
|
const backgroundAudio = await this.scanDirectoryForAudio('background');
|
||||||
const ambientAudio = await this.scanDirectoryForAudio('ambient');
|
const ambientAudio = await this.scanDirectoryForAudio('ambient');
|
||||||
const effectsAudio = await this.scanDirectoryForAudio('effects');
|
|
||||||
|
|
||||||
const results = {
|
const results = {
|
||||||
background: backgroundAudio,
|
background: backgroundAudio,
|
||||||
ambient: ambientAudio,
|
ambient: ambientAudio
|
||||||
effects: effectsAudio
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update game audio storage
|
// Update game audio storage
|
||||||
if (backgroundAudio.length > 0 || ambientAudio.length > 0 || effectsAudio.length > 0) {
|
if (backgroundAudio.length > 0 || ambientAudio.length > 0) {
|
||||||
await this.updateAudioStorage([...backgroundAudio, ...ambientAudio, ...effectsAudio]);
|
await this.updateAudioStorage([...backgroundAudio, ...ambientAudio]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return results;
|
return results;
|
||||||
|
|
@ -517,7 +512,7 @@ class DesktopFileManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get existing audio
|
// Get existing audio
|
||||||
let customAudio = this.dataManager.get('customAudio') || { background: [], ambient: [], effects: [] };
|
let customAudio = this.dataManager.get('customAudio') || { background: [], ambient: [] };
|
||||||
|
|
||||||
// Add new audio files (avoid duplicates)
|
// Add new audio files (avoid duplicates)
|
||||||
for (const audio of audioFiles) {
|
for (const audio of audioFiles) {
|
||||||
|
|
@ -562,7 +557,7 @@ class DesktopFileManager {
|
||||||
const success = await window.electronAPI.deleteFile(audioPath);
|
const success = await window.electronAPI.deleteFile(audioPath);
|
||||||
if (success) {
|
if (success) {
|
||||||
// Remove from storage
|
// Remove from storage
|
||||||
let customAudio = this.dataManager.get('customAudio') || { background: [], ambient: [], effects: [] };
|
let customAudio = this.dataManager.get('customAudio') || { background: [], ambient: [] };
|
||||||
|
|
||||||
if (customAudio[category]) {
|
if (customAudio[category]) {
|
||||||
customAudio[category] = customAudio[category].filter(audio => {
|
customAudio[category] = customAudio[category].filter(audio => {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue