Restore original game modes and fix missing scenario steps - Added back Timed Challenge and Score Target modes alongside scenario modes - Updated gameModeManager to map new mode names to original engine functionality - Removed interactive tasks from standard/timed/scored modes as requested - Fixed missing scenario continuation steps (slutty_continuation, photo_series_continuation) - Added complete branching paths for dress-up photo scenarios - All 7 game modes now working: Standard, Timed, Scored, and 4 scenario modes - Maintained periodic popup system across all modes - Preserved original time/score configuration UI

This commit is contained in:
dilgenfritz 2025-10-27 11:51:38 -05:00
parent 478b5884fb
commit 395c79363c
12 changed files with 6778 additions and 114 deletions

17
Start-webgame.bat Normal file
View File

@ -0,0 +1,17 @@
@echo off
REM --- Start Ollama Service ---
echo Starting Ollama AI service...
start /min ollama serve
REM --- Wait a few seconds for Ollama to initialize ---
timeout /t 3 /nobreak >nul
REM --- Start the Edge & Punishment webGame ---
echo Launching Edge & Punishment...
REM If you use npm:
npm start
REM If you use a different command (like electron .), replace above with:
REM electron .
pause

View File

@ -1,31 +1,119 @@
# Audio Files for Task Challenge Game
# Audio Library for Edge & Punishment
## Current Audio Tracks
This directory contains all audio files used in the game. The AudioManager will automatically discover and categorize audio files placed in the appropriate subdirectories.
This directory contains 4 background music tracks:
## Background Music (Legacy)
1. **Colorful-Flowers(chosic.com).mp3** - Upbeat and energetic music
2. **New-Beginnings-chosic.com_.mp3** - Ambient and inspiring music
3. **storm-clouds-purpple-cat(chosic.com).mp3** - Intense and dramatic music
4. **Tokyo-Music-Walker-Brunch-For-Two-chosic.com_.mp3** - Calm and peaceful music
The existing background music tracks are still available:
- **Colorful-Flowers(chosic.com).mp3** - Upbeat and energetic music
- **New-Beginnings-chosic.com_.mp3** - Ambient and inspiring music
- **storm-clouds-purpple-cat(chosic.com).mp3** - Intense and dramatic music
- **Tokyo-Music-Walker-Brunch-For-Two-chosic.com_.mp3** - Calm and peaceful music
## Audio Integration
## New Audio System Structure
These tracks are automatically integrated into the game's music system:
- **Colorful Flowers**: Motivating upbeat track for energy
- **New Beginnings**: Ambient background for focus
- **Storm Clouds**: Intense track for challenge moments
- **Brunch For Two**: Calm and relaxing atmosphere
```
/audio/
/tasks/ # Audio that plays during edging tasks
/teasing/ # Light, teasing audio for building arousal
- tease1.mp3 # Soft whispers, light moaning
- tease2.mp3 # Gentle encouragement, sultry voices
- whisper1.mp3 # ASMR-style teasing whispers
/intense/ # More aggressive audio for advanced tasks
- intense1.mp3 # Heavy breathing, intense moaning
- intense2.mp3 # Urgent whispers, building intensity
- heavy_breathing.mp3
/instructions/ # Verbal JOI-style commands
- stroke_slow.mp3 # "Stroke slowly for me..."
- stroke_fast.mp3 # "Faster! Don't stop!"
- stop_now.mp3 # "Stop! Hands off!"
## Music Controls
/punishments/ # Audio when punishment popups appear
/denial/ # Frustration and denial audio
- no_no_no.mp3 # "No, no, no! You don't get to cum!"
- denied.mp3 # "Denied! Try again!"
- bad_boy.mp3 # "Bad boy! No touching!"
/mocking/ # Teasing/mocking for skippers
- pathetic.mp3 # "How pathetic..."
- weak.mp3 # "Too weak to continue?"
- try_again.mp3 # "Try again, if you can handle it"
Players can:
- Toggle music on/off with the 🎵 button
- See the current track name in the header
- Music automatically loops and pauses with game state
/rewards/ # Celebration audio for task completion
/completion/
- good_boy.mp3 # "Good boy! Well done!"
- well_done.mp3 # "Excellent work!"
- reward1.mp3 # Positive reinforcement audio
## Audio Specifications
- Format: MP3
- Files loop automatically during gameplay
- Volume: Game sets to 30% by default for comfortable listening
- Source: chosic.com (royalty-free music)
/ambient/ # Background/atmospheric audio (optional)
/background/
- ambient1.mp3 # Subtle background audio
- ambient2.mp3 # Atmospheric soundscapes
```
## Supported Formats
- **MP3**: Recommended (best compatibility)
- **WAV**: High quality, larger files
- **OGG**: Good compression, modern browsers
## File Naming Conventions
- Use descriptive names (e.g., `soft_whisper.mp3`, `intense_breathing.mp3`)
- Avoid spaces (use underscores: `stroke_fast.mp3`)
- Keep names concise but clear
- Use lowercase for consistency
## Audio Guidelines
### Task Audio (`/tasks/`)
- **Duration**: 30 seconds to 3 minutes
- **Volume**: Recorded at consistent levels
- **Content**: Encouraging, teasing, instructional
- **Quality**: Clear audio, minimal background noise
### Punishment Audio (`/punishments/`)
- **Duration**: 5-30 seconds (punchy and immediate)
- **Content**: Denial, mocking, correction
- **Tone**: Dominant, disapproving, teasing
### Reward Audio (`/rewards/`)
- **Duration**: 5-15 seconds
- **Content**: Praise, encouragement, satisfaction
- **Tone**: Positive, affirming, proud
## Usage in Game
The AudioManager will:
1. **Auto-discover** all audio files in these directories
2. **Randomly select** from available files in each category
3. **Respect volume settings** and user preferences
4. **Fade in/out** for smooth transitions
5. **Allow previewing** in the settings menu
## Getting Started
1. **Add your first audio files** to `/tasks/teasing/` and `/punishments/denial/`
2. **Test in-game** - audio will be discovered automatically
3. **Adjust volumes** in the game's Options > Audio menu
4. **Add more files** as needed for variety
## Pro Tips
- **Layer experiences**: Combine teasing task audio with denial punishment audio
- **Build tension**: Use progression from teasing to intense categories
- **Create variety**: Multiple files in each category prevent repetition
- **Test volumes**: Ensure audio doesn't overpower or get lost
- **Consider timing**: Match audio duration to typical task lengths
## Technical Notes
- Audio files are loaded on-demand (not preloaded)
- Web Audio API used for precise volume control
- Cross-fade capabilities for smooth transitions
- Category-based enable/disable for user control
- Preview functionality for settings menu
Add your audio files to the appropriate directories and they'll be automatically integrated into the game experience!

39
audio/TESTING.md Normal file
View File

@ -0,0 +1,39 @@
# Test Audio File
This is a placeholder for test audio.
## To test the audio system:
1. **Add real audio files** to the appropriate directories:
- `/audio/tasks/teasing/` - Add MP3 files like `tease1.mp3`, `whisper1.mp3`
- `/audio/punishments/denial/` - Add MP3 files like `denied.mp3`, `bad_boy.mp3`
- `/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**:
- Open the game and go to Options ⚙️
- Adjust volume sliders
- Toggle audio categories on/off
- Use preview buttons to test audio
3. **Test in gameplay**:
- Start a task → Should play teasing/intense audio
- Complete a task → Should play reward audio
- Skip a task → Should play mocking audio
- Get a consequence task → Should play denial audio
## Current Features Working:
**AudioManager class** - Centralized audio control
**Volume controls** - Master + category volumes
**Audio discovery** - Auto-detects files in directories
**Game integration** - Audio triggers during gameplay
**Fade effects** - Smooth audio transitions
**Settings persistence** - Saves user preferences
**Preview system** - Test audio in options menu
## Phase 1 Complete! 🎵
The core audio infrastructure is now in place. Add your audio files and start enjoying the enhanced experience!
**Next Phase**: Add more audio categories, JOI instructions, and advanced features.

506
audioManager.js Normal file
View File

@ -0,0 +1,506 @@
/**
* Audio Manager - Centralized Audio System for Edge & Punishment
* Handles multiple audio categories with volume controls and fade effects
*/
class AudioManager {
constructor(dataManager) {
this.dataManager = dataManager;
this.audioContext = null;
this.masterGain = null;
// Audio category configurations
this.categories = {
tasks: {
volume: 0.7,
enabled: true,
currentAudio: 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
}
};
this.masterVolume = 0.7;
this.isInitialized = false;
this.audioLibrary = {};
this.init();
}
async init() {
console.log('AudioManager initializing...');
// Load saved settings
this.loadSettings();
// Initialize Web Audio API for better control
try {
this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
this.masterGain = this.audioContext.createGain();
this.masterGain.connect(this.audioContext.destination);
this.masterGain.gain.value = this.masterVolume;
} catch (error) {
console.warn('Web Audio API not available, falling back to HTML5 audio:', error);
}
// Discover available audio files
await this.discoverAudioLibrary();
this.isInitialized = true;
console.log('AudioManager initialized successfully');
console.log('Available audio categories:', Object.keys(this.audioLibrary));
}
async discoverAudioLibrary() {
// Define the actual file structure based on user's files
const audioStructure = {
tasks: {
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);
}
loadSettings() {
const savedSettings = this.dataManager.get('audioSettings') || {};
// Load master volume
this.masterVolume = savedSettings.masterVolume || 0.7;
// Load category settings
for (const category in this.categories) {
if (savedSettings.categories && savedSettings.categories[category]) {
this.categories[category] = {
...this.categories[category],
...savedSettings.categories[category]
};
}
}
}
saveSettings() {
const settings = {
masterVolume: this.masterVolume,
categories: this.categories
};
this.dataManager.set('audioSettings', settings);
}
// Master volume control
setMasterVolume(volume) {
this.masterVolume = Math.max(0, Math.min(1, volume));
if (this.masterGain) {
this.masterGain.gain.value = this.masterVolume;
}
this.saveSettings();
}
getMasterVolume() {
return this.masterVolume;
}
// Category volume control
setCategoryVolume(category, volume) {
if (this.categories[category]) {
this.categories[category].volume = Math.max(0, Math.min(1, volume));
// Update currently playing audio if any
if (this.categories[category].currentAudio) {
this.categories[category].currentAudio.volume =
this.categories[category].volume * this.masterVolume;
}
this.saveSettings();
}
}
getCategoryVolume(category) {
return this.categories[category]?.volume || 0;
}
// Enable/disable categories
setCategoryEnabled(category, enabled) {
if (this.categories[category]) {
this.categories[category].enabled = enabled;
// Stop currently playing audio if disabled
if (!enabled && this.categories[category].currentAudio) {
this.stopCategory(category);
}
this.saveSettings();
}
}
isCategoryEnabled(category) {
return this.categories[category]?.enabled || false;
}
// Play audio from library
async playAudio(category, subcategory, fileName = null, options = {}) {
console.log(`PlayAudio called: ${category}/${subcategory}/${fileName || 'random'}`);
if (!this.isInitialized) {
console.warn('AudioManager not initialized yet');
return null;
}
if (!this.isCategoryEnabled(category)) {
console.log(`Audio category ${category} is disabled`);
return null;
}
const categoryLib = this.audioLibrary[category];
if (!categoryLib || !categoryLib[subcategory]) {
console.warn(`Audio not found: ${category}/${subcategory}. Available:`, Object.keys(this.audioLibrary));
console.warn(`Category structure:`, categoryLib);
return null;
}
const audioFiles = categoryLib[subcategory];
if (audioFiles.length === 0) {
console.warn(`No audio files available in ${category}/${subcategory}`);
return null;
}
// Select audio file
let audioFile;
if (fileName) {
audioFile = audioFiles.find(f => f.name === fileName);
if (!audioFile) {
console.warn(`Specific audio file not found: ${fileName}. Available:`, audioFiles.map(f => f.name));
return null;
}
} else {
// Random selection
audioFile = audioFiles[Math.floor(Math.random() * audioFiles.length)];
}
console.log(`Selected audio file: ${audioFile.path}`);
// Stop any currently playing audio in this category
this.stopCategory(category);
// Create and configure audio element
const audio = new Audio(audioFile.path);
audio.volume = this.categories[category].volume * this.masterVolume;
audio.loop = options.loop || false;
console.log(`Audio volume set to: ${audio.volume} (category: ${this.categories[category].volume}, master: ${this.masterVolume})`);
// Add error handling
audio.addEventListener('error', (e) => {
console.error(`Audio file failed to load: ${audioFile.path}`, e);
console.error('Error details:', {
error: e.target.error,
networkState: e.target.networkState,
readyState: e.target.readyState
});
});
audio.addEventListener('loadstart', () => {
console.log(`Started loading: ${audioFile.path}`);
});
audio.addEventListener('canplay', () => {
console.log(`Audio ready to play: ${audioFile.path}`);
});
// Set up event handlers
audio.addEventListener('ended', () => {
if (this.categories[category].currentAudio === audio) {
this.categories[category].currentAudio = null;
}
});
audio.addEventListener('error', (e) => {
console.error(`Error playing audio ${audioFile.path}:`, e);
});
// Store reference and play
this.categories[category].currentAudio = audio;
try {
await audio.play();
console.log(`Playing audio: ${category}/${subcategory}/${audioFile.name}`);
// Handle fade in
if (options.fadeIn) {
this.fadeIn(audio, options.fadeIn);
}
return audio;
} catch (error) {
console.error(`Failed to play audio: ${audioFile.path}`, error);
this.categories[category].currentAudio = null;
return null;
}
}
// Stop audio in specific category
stopCategory(category, fadeOut = 0) {
if (!this.categories[category] || !this.categories[category].currentAudio) {
return;
}
const audio = this.categories[category].currentAudio;
console.log(`Stopping audio in category: ${category} with fadeOut: ${fadeOut}ms`);
if (fadeOut > 0) {
this.fadeOut(audio, fadeOut, () => {
audio.pause();
audio.currentTime = 0; // Reset to beginning
this.categories[category].currentAudio = null;
console.log(`Audio stopped in category: ${category}`);
});
} else {
audio.pause();
audio.currentTime = 0; // Reset to beginning
this.categories[category].currentAudio = null;
console.log(`Audio immediately stopped in category: ${category}`);
}
// Clear any pending fade timeout
if (this.categories[category].fadeTimeout) {
clearTimeout(this.categories[category].fadeTimeout);
this.categories[category].fadeTimeout = null;
}
}
// Immediately stop all audio without fade
stopAllImmediate() {
console.log('Stopping all audio immediately...');
for (const category in this.categories) {
this.stopCategory(category, 0); // No fade, immediate stop
}
}
// Stop all audio
stopAll(fadeOut = 0) {
for (const category in this.categories) {
this.stopCategory(category, fadeOut);
}
}
// Fade effects
fadeIn(audio, duration) {
const targetVolume = audio.volume;
audio.volume = 0;
const steps = 20;
const stepSize = targetVolume / steps;
const stepDelay = duration / steps;
let currentStep = 0;
const fadeInterval = setInterval(() => {
currentStep++;
audio.volume = Math.min(targetVolume, stepSize * currentStep);
if (currentStep >= steps) {
clearInterval(fadeInterval);
audio.volume = targetVolume;
}
}, stepDelay);
}
fadeOut(audio, duration, callback = null) {
const startVolume = audio.volume;
const steps = 20;
const stepSize = startVolume / steps;
const stepDelay = duration / steps;
let currentStep = 0;
const fadeInterval = setInterval(() => {
currentStep++;
audio.volume = Math.max(0, startVolume - (stepSize * currentStep));
if (currentStep >= steps) {
clearInterval(fadeInterval);
audio.volume = 0;
if (callback) callback();
}
}, stepDelay);
}
// Get current playing status
isPlaying(category) {
return this.categories[category]?.currentAudio &&
!this.categories[category].currentAudio.paused;
}
getCurrentlyPlaying() {
const playing = {};
for (const [category, config] of Object.entries(this.categories)) {
if (this.isPlaying(category)) {
playing[category] = {
src: config.currentAudio.src,
currentTime: config.currentAudio.currentTime,
duration: config.currentAudio.duration
};
}
}
return playing;
}
// Audio library info
getAvailableAudio() {
const available = {};
for (const [category, subcategories] of Object.entries(this.audioLibrary)) {
available[category] = {};
for (const [subcategory, files] of Object.entries(subcategories)) {
available[category][subcategory] = files.map(f => f.name);
}
}
return available;
}
// Preview audio (for settings menu)
async previewAudio(category, subcategory, fileName) {
// Stop any currently playing preview
this.stopCategory('preview');
// Play audio with temporary category
const audio = await this.playAudio(category, subcategory, fileName);
if (audio) {
// Auto-stop after 3 seconds for preview
setTimeout(() => {
if (audio && !audio.paused) {
audio.pause();
}
}, 3000);
}
return audio;
}
// Test audio playback - useful for debugging
async testAudio() {
console.log('Testing audio system...');
console.log('Audio library:', this.audioLibrary);
// Test a teasing audio file
if (this.audioLibrary.tasks && this.audioLibrary.tasks.teasing && this.audioLibrary.tasks.teasing.length > 0) {
console.log('Testing teasing audio...');
await this.playAudio('tasks', 'teasing');
} else {
console.log('No teasing audio files found');
}
}
// Convenient methods for specific audio types
playTaskAudio(intensity = 'teasing', options = {}) {
return this.playAudio('tasks', intensity, null, options);
}
playPunishmentAudio(type = 'denial', options = {}) {
return this.playAudio('punishments', type, null, options);
}
playRewardAudio(options = {}) {
return this.playAudio('rewards', 'completion', null, options);
}
playInstructionAudio(instruction, options = {}) {
return this.playAudio('instructions', 'commands', instruction, options);
}
// Get settings for UI
getSettings() {
return {
masterVolume: this.masterVolume,
categories: { ...this.categories },
available: this.getAvailableAudio(),
isInitialized: this.isInitialized
};
}
}

1880
game.js

File diff suppressed because it is too large Load Diff

View File

@ -86,6 +86,73 @@ const gameData = {
id: 17,
text: "Write a one-page story or essay",
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!"
}
],

2413
gameModeManager.js Normal file

File diff suppressed because it is too large Load Diff

64
image-discovery-fix.js Normal file
View File

@ -0,0 +1,64 @@
// Image Discovery Fix - Force Override Version
// This is a complete fix that forcefully overrides the broken discoverImages method
// Emergency fix to override broken methods
function forceFixGame() {
console.log('<27> Applying emergency game fix...');
// Override the broken discoverImages completely
if (window.game) {
// Force mark image discovery as complete
window.game.imageDiscoveryComplete = true;
// Set basic image arrays
if (!gameData.discoveredTaskImages) gameData.discoveredTaskImages = [];
if (!gameData.discoveredConsequenceImages) gameData.discoveredConsequenceImages = [];
// Try to get images from desktop file manager if available
if (window.game.dataManager) {
const customImages = window.game.dataManager.get('customImages') || { task: [], consequence: [] };
let taskImages = [];
let consequenceImages = [];
if (Array.isArray(customImages)) {
taskImages = customImages;
} else {
taskImages = customImages.task || [];
consequenceImages = customImages.consequence || [];
}
if (taskImages.length > 0 || consequenceImages.length > 0) {
gameData.discoveredTaskImages = taskImages.map(img => typeof img === 'string' ? img : img.name);
gameData.discoveredConsequenceImages = consequenceImages.map(img => typeof img === 'string' ? img : img.name);
console.log(`<EFBFBD> Emergency fix - Found ${gameData.discoveredTaskImages.length} task images, ${gameData.discoveredConsequenceImages.length} consequence images`);
}
}
// Override the broken discoverImages method
window.game.discoverImages = async function() {
console.log('<27> Using emergency fixed discoverImages method');
this.imageDiscoveryComplete = true;
return Promise.resolve();
};
console.log('✅ Emergency fix applied successfully');
}
}
// Apply fix immediately if game exists
if (typeof window !== 'undefined') {
// Try to apply fix multiple times until it works
const tryFix = () => {
if (window.game) {
forceFixGame();
} else {
setTimeout(tryFix, 100);
}
};
// Start trying to fix
setTimeout(tryFix, 500);
// Also expose for manual use
window.forceFixGame = forceFixGame;
}

View File

@ -144,6 +144,69 @@
</select>
</div>
<!-- Periodic Popup Controls -->
<div class="option-item periodic-popup-controls">
<div class="periodic-popup-header">
<label class="option-label">💫 Periodic Image Popups</label>
<button id="test-periodic-popup" class="btn btn-mini">🔍 Test Popup</button>
</div>
<div class="periodic-popup-group">
<div class="periodic-control">
<label><input type="checkbox" id="enable-periodic-popups" checked> Enable Periodic Popups</label>
</div>
<div class="periodic-control">
<label for="popup-min-interval">Min Interval (seconds):</label>
<input type="number" id="popup-min-interval" min="10" max="300" value="30" class="number-input">
</div>
<div class="periodic-control">
<label for="popup-max-interval">Max Interval (seconds):</label>
<input type="number" id="popup-max-interval" min="30" max="600" value="120" class="number-input">
</div>
<div class="periodic-control">
<label for="popup-display-duration">Display Duration (seconds):</label>
<input type="number" id="popup-display-duration" min="2" max="30" value="5" class="number-input">
</div>
</div>
</div>
<!-- Audio Controls -->
<div class="option-item audio-controls">
<div class="audio-control-header">
<label class="option-label">🎵 Audio Settings</label>
<button id="debug-audio" class="btn btn-mini">🔧 Debug Audio</button>
</div>
<div class="audio-control-group">
<div class="audio-control">
<label for="master-volume">Master Volume:</label>
<input type="range" id="master-volume" min="0" max="100" value="70" class="volume-slider">
<span id="master-volume-display">70%</span>
</div>
<div class="audio-control">
<label for="task-audio-volume">Task Audio:</label>
<input type="range" id="task-audio-volume" min="0" max="100" value="70" class="volume-slider">
<span id="task-audio-volume-display">70%</span>
<button id="preview-task-audio" class="btn btn-mini">▶️</button>
</div>
<div class="audio-control">
<label for="punishment-audio-volume">Punishment Audio:</label>
<input type="range" id="punishment-audio-volume" min="0" max="100" value="80" class="volume-slider">
<span id="punishment-audio-volume-display">80%</span>
<button id="preview-punishment-audio" class="btn btn-mini">▶️</button>
</div>
<div class="audio-control">
<label for="reward-audio-volume">Reward Audio:</label>
<input type="range" id="reward-audio-volume" min="0" max="100" value="60" class="volume-slider">
<span id="reward-audio-volume-display">60%</span>
<button id="preview-reward-audio" class="btn btn-mini">▶️</button>
</div>
</div>
<div class="audio-toggles">
<label><input type="checkbox" id="enable-task-audio" checked> Enable Task Audio</label>
<label><input type="checkbox" id="enable-punishment-audio" checked> Enable Punishment Audio</label>
<label><input type="checkbox" id="enable-reward-audio" checked> Enable Reward Audio</label>
</div>
</div>
<!-- Data Management -->
<div class="option-item data-controls">
<button id="import-btn" class="btn btn-option" title="Import Save File">
@ -1019,11 +1082,6 @@
</div>
<script src="gameData.js"></script>
<script src="flashMessageManager.js"></script>
<script src="popupImageManager.js"></script>
<script src="aiTaskManager.js"></script>
<script src="desktop-file-manager.js"></script>
<script src="game.js"></script>
<!-- Statistics Modal -->
<div id="stats-modal" class="modal" style="display: none;">
<div class="modal-content">
@ -1145,4 +1203,26 @@
</div>
</body>
<!-- Script Loading Order -->
<script src="flashMessageManager.js"></script>
<script src="popupImageManager.js"></script>
<script src="aiTaskManager.js"></script>
<script src="audioManager.js"></script>
<script src="image-discovery-fix.js"></script>
<script src="gameModeManager.js"></script>
<script src="interactiveTaskManager.js"></script>
<script src="desktop-file-manager.js"></script>
<script src="game.js"></script>
<script>
// Force apply emergency fix
window.addEventListener('load', () => {
console.log('🚨 Page loaded - applying emergency fixes...');
setTimeout(() => {
if (window.forceFixGame) {
window.forceFixGame();
}
}, 2000);
});
</script>
</html>

681
interactiveTaskManager.js Normal file
View File

@ -0,0 +1,681 @@
/**
* Interactive Task Manager - Handles advanced task types with user interaction
* Supports mini-games, interactive elements, progress tracking, and dynamic content
*/
class InteractiveTaskManager {
constructor(gameInstance) {
this.game = gameInstance;
this.currentInteractiveTask = null;
this.taskContainer = null;
this.isInteractiveTaskActive = false;
// Task type registry
this.taskTypes = new Map();
this.registerBuiltInTaskTypes();
}
/**
* Register all built-in interactive task types
*/
registerBuiltInTaskTypes() {
// Rhythm tasks removed per user request
// Input Tasks
this.registerTaskType('text-input', {
name: 'Text Input Challenge',
description: 'Enter specific text or answer questions',
handler: this.createTextInputTask.bind(this),
validator: this.validateTextInputTask.bind(this)
});
this.registerTaskType('slider-challenge', {
name: 'Precision Slider',
description: 'Adjust sliders to exact values',
handler: this.createSliderTask.bind(this),
validator: this.validateSliderTask.bind(this)
});
this.registerTaskType('choice-challenge', {
name: 'Multiple Choice',
description: 'Make the correct choices under pressure',
handler: this.createChoiceTask.bind(this),
validator: this.validateChoiceTask.bind(this)
});
// Timed Challenges
this.registerTaskType('speed-challenge', {
name: 'Speed Challenge',
description: 'Complete actions within time limits',
handler: this.createSpeedTask.bind(this),
validator: this.validateSpeedTask.bind(this)
});
// Focus/Concentration Tasks
this.registerTaskType('focus-hold', {
name: 'Focus Hold',
description: 'Maintain focus on a target for set duration',
handler: this.createFocusTask.bind(this),
validator: this.validateFocusTask.bind(this)
});
// Scenario Builder Tasks
this.registerTaskType('scenario-adventure', {
name: 'Choose Your Own Adventure',
description: 'Interactive storylines with meaningful choices',
handler: this.createScenarioTask.bind(this),
validator: this.validateScenarioTask.bind(this)
});
}
/**
* Register a new task type
*/
registerTaskType(typeId, config) {
this.taskTypes.set(typeId, {
id: typeId,
name: config.name,
description: config.description,
handler: config.handler,
validator: config.validator,
cleanup: config.cleanup || (() => {})
});
console.log(`Registered interactive task type: ${typeId}`);
}
/**
* Check if a task is interactive
*/
isInteractiveTask(task) {
return task.hasOwnProperty('interactiveType') && this.taskTypes.has(task.interactiveType);
}
/**
* Display an interactive task
*/
async displayInteractiveTask(task) {
if (!this.isInteractiveTask(task)) {
console.warn('Task is not interactive:', task);
return false;
}
console.log(`Displaying interactive task: ${task.interactiveType}`);
this.currentInteractiveTask = task;
this.isInteractiveTaskActive = true;
// Handle task image for interactive tasks
const taskImage = document.getElementById('task-image');
if (taskImage && task.image) {
console.log(`🖼️ Setting interactive task image: ${task.image}`);
taskImage.src = task.image;
taskImage.onerror = () => {
console.log('Interactive task image failed to load:', task.image);
// Create placeholder if image fails
taskImage.src = this.createPlaceholderImage('Interactive Task');
};
} else if (taskImage) {
console.log(`⚠️ No image provided for interactive task, creating placeholder`);
taskImage.src = this.createPlaceholderImage('Interactive Task');
}
// Get the task container
this.taskContainer = document.querySelector('.task-display-container');
// Add interactive container
const interactiveContainer = document.createElement('div');
interactiveContainer.id = 'interactive-task-container';
interactiveContainer.className = 'interactive-task-container';
// Insert after task text
const taskTextContainer = document.querySelector('.task-text-container');
taskTextContainer.parentNode.insertBefore(interactiveContainer, taskTextContainer.nextSibling);
// Get task type config and create the interactive element
const taskType = this.taskTypes.get(task.interactiveType);
if (taskType && taskType.handler) {
await taskType.handler(task, interactiveContainer);
}
// Hide normal action buttons and show interactive controls
this.showInteractiveControls();
return true;
}
/**
* Create placeholder image for interactive tasks
*/
createPlaceholderImage(text) {
const canvas = document.createElement('canvas');
canvas.width = 400;
canvas.height = 300;
const ctx = canvas.getContext('2d');
// Create gradient background
const gradient = ctx.createLinearGradient(0, 0, 400, 300);
gradient.addColorStop(0, '#667eea');
gradient.addColorStop(1, '#764ba2');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 400, 300);
// Add text
ctx.fillStyle = 'white';
ctx.font = '24px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(text, 200, 150);
return canvas.toDataURL();
}
/**
* Show interactive controls and hide standard buttons
*/
showInteractiveControls() {
const actionButtons = document.querySelector('.action-buttons');
const originalButtons = actionButtons.querySelectorAll('button:not(.interactive-btn)');
// Hide original buttons
originalButtons.forEach(btn => btn.style.display = 'none');
// Add interactive control buttons
const interactiveControls = document.createElement('div');
interactiveControls.className = 'interactive-controls';
interactiveControls.innerHTML = `
<button id="interactive-complete-btn" class="btn btn-success interactive-btn" disabled>
Complete Challenge
</button>
<button id="interactive-give-up-btn" class="btn btn-danger interactive-btn">
Give Up
</button>
`;
actionButtons.appendChild(interactiveControls);
// Add event listeners
document.getElementById('interactive-complete-btn').addEventListener('click', () => this.completeInteractiveTask());
document.getElementById('interactive-give-up-btn').addEventListener('click', () => this.giveUpInteractiveTask());
}
/**
* Validate and complete interactive task
*/
async completeInteractiveTask() {
if (!this.currentInteractiveTask) return;
const taskType = this.taskTypes.get(this.currentInteractiveTask.interactiveType);
if (taskType && taskType.validator) {
const isValid = await taskType.validator(this.currentInteractiveTask);
if (isValid) {
// Task completed successfully
this.showFeedback('success', 'Challenge completed successfully! 🎉');
setTimeout(() => {
this.cleanupInteractiveTask();
this.game.completeTask();
}, 1500);
} else {
// Task failed validation
this.showFeedback('error', 'Not quite right. Try again! 🤔');
this.enableInteractiveControls();
}
}
}
/**
* Give up on interactive task (counts as skip)
*/
giveUpInteractiveTask() {
this.showFeedback('warning', 'Challenge abandoned. This counts as a skip.');
setTimeout(() => {
this.cleanupInteractiveTask();
this.game.skipTask();
}, 1000);
}
/**
* Show hint for current task
*/
showHint() {
if (!this.currentInteractiveTask || !this.currentInteractiveTask.hint) {
this.showFeedback('info', 'No hint available for this challenge.');
return;
}
this.showFeedback('info', `💡 Hint: ${this.currentInteractiveTask.hint}`);
}
/**
* Show feedback message
*/
showFeedback(type, message) {
// Create or update feedback element
let feedback = document.getElementById('interactive-feedback');
if (!feedback) {
feedback = document.createElement('div');
feedback.id = 'interactive-feedback';
feedback.className = 'interactive-feedback';
const container = document.getElementById('interactive-task-container');
container.appendChild(feedback);
}
feedback.className = `interactive-feedback feedback-${type}`;
feedback.textContent = message;
feedback.style.display = 'block';
// Auto-hide after 3 seconds for non-error messages
if (type !== 'error') {
setTimeout(() => {
if (feedback) feedback.style.display = 'none';
}, 3000);
}
}
/**
* Enable/disable interactive controls
*/
enableInteractiveControls(enabled = true) {
const completeBtn = document.getElementById('interactive-complete-btn');
if (completeBtn) {
completeBtn.disabled = !enabled;
}
}
/**
* Clean up interactive task elements
*/
cleanupInteractiveTask() {
// Remove interactive container
const container = document.getElementById('interactive-task-container');
if (container) {
container.remove();
}
// Remove interactive controls
const controls = document.querySelector('.interactive-controls');
if (controls) {
controls.remove();
}
// Show original buttons
const actionButtons = document.querySelector('.action-buttons');
const originalButtons = actionButtons.querySelectorAll('button:not(.interactive-btn)');
originalButtons.forEach(btn => btn.style.display = '');
// Run task-specific cleanup
if (this.currentInteractiveTask) {
const taskType = this.taskTypes.get(this.currentInteractiveTask.interactiveType);
if (taskType && taskType.cleanup) {
taskType.cleanup();
}
}
this.currentInteractiveTask = null;
this.isInteractiveTaskActive = false;
}
// =============================================================================
// TASK TYPE IMPLEMENTATIONS
// =============================================================================
// Rhythm task implementations removed per user request
// Placeholder implementations for other task types
async createTextInputTask(task, container) {
// Implementation for text input tasks
console.log('Text input task created');
}
async validateTextInputTask(task) {
return true; // Placeholder
}
async createSliderTask(task, container) {
// Implementation for slider tasks
console.log('Slider task created');
}
async validateSliderTask(task) {
return true; // Placeholder
}
async createChoiceTask(task, container) {
// Implementation for choice tasks
console.log('Choice task created');
}
async validateChoiceTask(task) {
return true; // Placeholder
}
async createSpeedTask(task, container) {
// Implementation for speed tasks
console.log('Speed task created');
}
async validateSpeedTask(task) {
return true; // Placeholder
}
async createFocusTask(task, container) {
// Implementation for focus tasks
console.log('Focus task created');
}
async validateFocusTask(task) {
return true; // Placeholder
}
// =============================================================================
// SCENARIO BUILDER IMPLEMENTATION
// =============================================================================
/**
* Create a choose your own adventure scenario task
*/
async createScenarioTask(task, container) {
const scenario = task.interactiveData || {};
container.innerHTML = `
<div class="scenario-task">
<div class="scenario-header">
<h4>📖 ${scenario.title || 'Interactive Scenario'}</h4>
<div class="scenario-progress">
Step <span id="scenario-step">1</span> of <span id="scenario-total">${this.getScenarioStepCount(scenario)}</span>
</div>
</div>
<div class="scenario-content">
<div id="scenario-story" class="scenario-story"></div>
<div id="scenario-choices" class="scenario-choices"></div>
</div>
<div class="scenario-status">
<div class="scenario-stats">
<span class="stat">Arousal: <span id="scenario-arousal">Unknown</span></span>
<span class="stat">Control: <span id="scenario-control">Unknown</span></span>
<span class="stat">Intensity: <span id="scenario-intensity">Low</span></span>
</div>
</div>
</div>
`;
// Initialize scenario state
this.initializeScenario(task, scenario);
}
initializeScenario(task, scenario) {
// Scenario state tracking
task.scenarioState = {
currentStep: 'start',
stepNumber: 1,
totalSteps: this.getScenarioStepCount(scenario),
choices: [],
arousal: 50, // 0-100 scale
control: 50, // 0-100 scale (higher = more controlled)
intensity: 1, // 1-3 scale
completed: false,
outcome: null
};
// Start the scenario
this.displayScenarioStep(task, scenario, 'start');
}
displayScenarioStep(task, scenario, stepId) {
const step = scenario.steps[stepId];
if (!step) {
console.error('Scenario step not found:', stepId);
return;
}
const storyEl = document.getElementById('scenario-story');
const choicesEl = document.getElementById('scenario-choices');
const stepEl = document.getElementById('scenario-step');
// Update step counter
stepEl.textContent = task.scenarioState.stepNumber;
// Display story text with dynamic content
const storyText = this.processScenarioText(step.story, task.scenarioState);
storyEl.innerHTML = `
<div class="story-text ${step.mood || 'neutral'}">
${storyText}
</div>
`;
// Clear previous choices
choicesEl.innerHTML = '';
if (step.type === 'choice') {
// Display choices
step.choices.forEach((choice, index) => {
const isAvailable = this.isChoiceAvailable(choice, task.scenarioState);
const choiceBtn = document.createElement('button');
choiceBtn.className = `scenario-choice ${choice.type || 'normal'} ${!isAvailable ? 'disabled' : ''}`;
choiceBtn.disabled = !isAvailable;
choiceBtn.innerHTML = `
<div class="choice-text">${choice.text}</div>
<div class="choice-preview">${choice.preview || ''}</div>
${choice.requirements ? `<div class="choice-requirements">${choice.requirements}</div>` : ''}
`;
if (isAvailable) {
choiceBtn.addEventListener('click', () => {
this.makeScenarioChoice(task, scenario, choice, index);
});
}
choicesEl.appendChild(choiceBtn);
});
} else if (step.type === 'action') {
// Display action requirements
const actionBtn = document.createElement('button');
actionBtn.className = 'scenario-action';
actionBtn.innerHTML = `
<div class="action-text">${step.actionText}</div>
<div class="action-timer" id="action-timer">${step.duration || 30}s</div>
`;
actionBtn.addEventListener('click', () => {
this.startScenarioAction(task, scenario, step);
});
choicesEl.appendChild(actionBtn);
} else if (step.type === 'ending') {
// Display ending and completion
task.scenarioState.completed = true;
task.scenarioState.outcome = step.outcome;
const endingDiv = document.createElement('div');
endingDiv.className = 'scenario-ending';
endingDiv.innerHTML = `
<div class="ending-text">${step.endingText}</div>
<div class="ending-outcome ${step.outcome}">${this.getOutcomeText(step.outcome)}</div>
`;
choicesEl.appendChild(endingDiv);
// Enable completion button
setTimeout(() => {
this.enableInteractiveControls(true);
}, 2000);
}
// Update stats display
this.updateScenarioStats(task.scenarioState);
// Apply scenario effects (audio, visual effects, etc.)
this.applyScenarioEffects(step, task.scenarioState);
}
makeScenarioChoice(task, scenario, choice, choiceIndex) {
// Record the choice
task.scenarioState.choices.push({
step: task.scenarioState.currentStep,
choice: choiceIndex,
text: choice.text
});
// Apply choice effects
this.applyChoiceEffects(choice, task.scenarioState);
// Move to next step
const nextStep = choice.nextStep || this.getDefaultNextStep(task.scenarioState);
task.scenarioState.currentStep = nextStep;
task.scenarioState.stepNumber++;
// Display next step
this.displayScenarioStep(task, scenario, nextStep);
}
startScenarioAction(task, scenario, step) {
const duration = step.duration || 30;
const timerEl = document.getElementById('action-timer');
let timeLeft = duration;
// Disable the action button
const actionBtn = document.querySelector('.scenario-action');
actionBtn.disabled = true;
actionBtn.classList.add('active');
const countdown = setInterval(() => {
timeLeft--;
timerEl.textContent = `${timeLeft}s`;
if (timeLeft <= 0) {
clearInterval(countdown);
// Action completed
actionBtn.classList.remove('active');
actionBtn.classList.add('completed');
// Apply action effects
this.applyActionEffects(step, task.scenarioState);
// Move to next step
setTimeout(() => {
const nextStep = step.nextStep || this.getDefaultNextStep(task.scenarioState);
task.scenarioState.currentStep = nextStep;
task.scenarioState.stepNumber++;
this.displayScenarioStep(task, scenario, nextStep);
}, 1500);
}
}, 1000);
}
applyChoiceEffects(choice, state) {
if (choice.effects) {
if (choice.effects.arousal) state.arousal = Math.max(0, Math.min(100, state.arousal + choice.effects.arousal));
if (choice.effects.control) state.control = Math.max(0, Math.min(100, state.control + choice.effects.control));
if (choice.effects.intensity) state.intensity = Math.max(1, Math.min(3, state.intensity + choice.effects.intensity));
}
}
applyActionEffects(step, state) {
if (step.effects) {
if (step.effects.arousal) state.arousal = Math.max(0, Math.min(100, state.arousal + step.effects.arousal));
if (step.effects.control) state.control = Math.max(0, Math.min(100, state.control + step.effects.control));
if (step.effects.intensity) state.intensity = Math.max(1, Math.min(3, state.intensity + step.effects.intensity));
}
}
applyScenarioEffects(step, state) {
// Could add visual effects, audio changes, etc. based on step properties
if (step.bgColor) {
document.querySelector('.interactive-task-container').style.background = step.bgColor;
}
}
updateScenarioStats(state) {
const arousalEl = document.getElementById('scenario-arousal');
const controlEl = document.getElementById('scenario-control');
const intensityEl = document.getElementById('scenario-intensity');
if (arousalEl) arousalEl.textContent = this.getArousalText(state.arousal);
if (controlEl) controlEl.textContent = this.getControlText(state.control);
if (intensityEl) intensityEl.textContent = this.getIntensityText(state.intensity);
}
getArousalText(arousal) {
if (arousal < 20) return 'Very Low';
if (arousal < 40) return 'Low';
if (arousal < 60) return 'Moderate';
if (arousal < 80) return 'High';
return 'Very High';
}
getControlText(control) {
if (control < 20) return 'Lost';
if (control < 40) return 'Struggling';
if (control < 60) return 'Managing';
if (control < 80) return 'Good';
return 'Excellent';
}
getIntensityText(intensity) {
return ['Low', 'Medium', 'High'][intensity - 1] || 'Low';
}
getOutcomeText(outcome) {
const outcomes = {
'success': '🎉 Scenario Completed Successfully!',
'partial': '⚡ Partially Successful',
'failure': '🔥 Overwhelmed by Desire',
'denied': '🚫 Denied and Frustrated',
'reward': '🏆 Earned a Special Reward',
'punishment': '⛓️ Earned Punishment'
};
return outcomes[outcome] || 'Scenario Complete';
}
isChoiceAvailable(choice, state) {
if (!choice.conditions) return true;
for (const [key, value] of Object.entries(choice.conditions)) {
if (key === 'arousal' && state.arousal < value) return false;
if (key === 'control' && state.control < value) return false;
if (key === 'intensity' && state.intensity < value) return false;
if (key === 'minArousal' && state.arousal < value) return false;
if (key === 'maxArousal' && state.arousal > value) return false;
}
return true;
}
processScenarioText(text, state) {
// Replace placeholders with current state values
return text
.replace(/\{arousal\}/g, this.getArousalText(state.arousal))
.replace(/\{control\}/g, this.getControlText(state.control))
.replace(/\{intensity\}/g, this.getIntensityText(state.intensity));
}
getScenarioStepCount(scenario) {
return Object.keys(scenario.steps || {}).length;
}
getDefaultNextStep(state) {
// Simple logic to determine next step based on state
// Can be overridden by specific choice nextStep properties
return `step${state.stepNumber + 1}`;
}
/**
* Validate scenario task completion
*/
async validateScenarioTask(task) {
return task.scenarioState && task.scenarioState.completed;
}
}
// Export for use in other modules
if (typeof module !== 'undefined' && module.exports) {
module.exports = InteractiveTaskManager;
}

View File

@ -1,6 +1,6 @@
/**
* Popup Image Manager - Handles punishment popups when tasks are skipped
* Part of the Annoyance system for consequence enforcement
* Popup Image Manager - Handles both punishment popups and periodic random image popups
* Part of the Annoyance system for consequence enforcement + periodic visual enhancement
*/
class PopupImageManager {
constructor(dataManager) {
@ -9,12 +9,352 @@ class PopupImageManager {
this.config = null;
this.isEnabled = true;
// Periodic popup system
this.periodicSystem = {
isActive: false,
interval: null,
minInterval: 30000, // 30 seconds
maxInterval: 120000, // 2 minutes
displayDuration: 5000, // 5 seconds
history: [],
maxHistorySize: 20
};
this.init();
}
init() {
this.loadConfiguration();
console.log('PopupImageManager initialized');
console.log('PopupImageManager initialized with periodic system');
}
/**
* Start periodic random image popups
*/
startPeriodicPopups() {
if (this.periodicSystem.isActive) {
console.log('⚠️ Periodic popup system already active');
return;
}
this.periodicSystem.isActive = true;
this.scheduleNextPeriodicPopup();
console.log('🚀 Periodic image popup system started');
}
/**
* Stop periodic random image popups
*/
stopPeriodicPopups() {
if (!this.periodicSystem.isActive) {
return;
}
this.periodicSystem.isActive = false;
if (this.periodicSystem.interval) {
clearTimeout(this.periodicSystem.interval);
this.periodicSystem.interval = null;
}
console.log('🛑 Periodic image popup system stopped');
}
/**
* Schedule the next periodic popup
*/
scheduleNextPeriodicPopup() {
if (!this.periodicSystem.isActive) {
return;
}
const { minInterval, maxInterval } = this.periodicSystem;
const interval = Math.random() * (maxInterval - minInterval) + minInterval;
this.periodicSystem.interval = setTimeout(() => {
this.showPeriodicPopup();
this.scheduleNextPeriodicPopup();
}, interval);
console.log(`⏰ Next periodic popup in ${Math.round(interval / 1000)} seconds`);
}
/**
* Show a periodic random popup
*/
showPeriodicPopup() {
if (!this.periodicSystem.isActive) {
return;
}
const image = this.getRandomPeriodicImage();
if (!image) {
console.log('⚠️ No images available for periodic popup');
return;
}
this.displayPeriodicPopup(image);
}
/**
* Get random image for periodic popups (from both folders)
*/
getRandomPeriodicImage() {
const taskImages = gameData?.discoveredTaskImages || [];
const consequenceImages = gameData?.discoveredConsequenceImages || [];
const allImages = [...taskImages, ...consequenceImages];
if (allImages.length === 0) {
return null;
}
// Filter out disabled images and recent history
const disabledImages = this.dataManager.get('disabledImages') || [];
const { history } = this.periodicSystem;
const availableImages = allImages.filter(img => {
const imagePath = typeof img === 'string' ? img : (img.cachedPath || img.originalName);
return !disabledImages.includes(imagePath) && !history.includes(imagePath);
});
// If all images are recent, clear history
if (availableImages.length === 0) {
this.periodicSystem.history = [];
const nonDisabledImages = allImages.filter(img => {
const imagePath = typeof img === 'string' ? img : (img.cachedPath || img.originalName);
return !disabledImages.includes(imagePath);
});
if (nonDisabledImages.length === 0) {
return null;
}
const randomIndex = Math.floor(Math.random() * nonDisabledImages.length);
return nonDisabledImages[randomIndex];
}
const randomIndex = Math.floor(Math.random() * availableImages.length);
const selectedImage = availableImages[randomIndex];
// Add to history
const imagePath = typeof selectedImage === 'string' ? selectedImage : (selectedImage.cachedPath || selectedImage.originalName);
this.periodicSystem.history.push(imagePath);
if (this.periodicSystem.history.length > this.periodicSystem.maxHistorySize) {
this.periodicSystem.history.shift();
}
return selectedImage;
}
/**
* Display periodic popup
*/
displayPeriodicPopup(imageData) {
const imageSrc = this.getImageSrc(imageData);
const imageName = typeof imageData === 'string' ? 'Random Image' : (imageData.originalName || 'Random Image');
const popup = document.createElement('div');
popup.className = 'periodic-popup-container';
popup.innerHTML = `
<div class="periodic-popup-backdrop" onclick="window.popupImageManager.hidePeriodicPopup(this)">
<div class="periodic-popup-content" onclick="event.stopPropagation()">
<div class="periodic-popup-header">
<span class="periodic-popup-title">💫 Random Visual</span>
<button class="periodic-popup-close" onclick="window.popupImageManager.hidePeriodicPopup(this.closest('.periodic-popup-container'))">×</button>
</div>
<div class="periodic-popup-image-wrapper">
<img src="${imageSrc}" alt="Random Popup" class="periodic-popup-image">
</div>
<div class="periodic-popup-footer">
<small>${imageName}</small>
</div>
</div>
</div>
`;
this.ensurePeriodicStyles();
document.body.appendChild(popup);
// Animate in
setTimeout(() => {
popup.classList.add('periodic-visible');
}, 50);
// Auto-hide
setTimeout(() => {
this.hidePeriodicPopup(popup);
}, this.periodicSystem.displayDuration);
console.log(`🖼️ Showing periodic popup: ${imageName}`);
}
/**
* Hide periodic popup
*/
hidePeriodicPopup(popup) {
if (!popup || !popup.parentNode) {
return;
}
popup.classList.add('periodic-hiding');
setTimeout(() => {
if (popup && popup.parentNode) {
popup.parentNode.removeChild(popup);
}
}, 300);
}
/**
* Ensure periodic popup styles
*/
ensurePeriodicStyles() {
if (document.querySelector('#periodic-popup-styles')) {
return;
}
const styles = document.createElement('style');
styles.id = 'periodic-popup-styles';
styles.textContent = `
.periodic-popup-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 10001;
opacity: 0;
transition: opacity 0.3s ease;
pointer-events: none;
}
.periodic-popup-container.periodic-visible {
opacity: 1;
pointer-events: all;
}
.periodic-popup-container.periodic-hiding {
opacity: 0;
pointer-events: none;
}
.periodic-popup-backdrop {
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
.periodic-popup-content {
background: white;
border-radius: 12px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
max-width: 70vw;
max-height: 70vh;
display: flex;
flex-direction: column;
cursor: default;
animation: periodicSlideIn 0.3s ease-out;
}
@keyframes periodicSlideIn {
from {
transform: translateY(-50px) scale(0.9);
opacity: 0;
}
to {
transform: translateY(0) scale(1);
opacity: 1;
}
}
.periodic-popup-header {
padding: 12px 18px;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 12px 12px 0 0;
}
.periodic-popup-title {
font-weight: bold;
font-size: 14px;
}
.periodic-popup-close {
background: none;
border: none;
color: white;
font-size: 20px;
cursor: pointer;
padding: 0;
width: 25px;
height: 25px;
border-radius: 50%;
transition: background-color 0.2s;
}
.periodic-popup-close:hover {
background-color: rgba(255, 255, 255, 0.2);
}
.periodic-popup-image-wrapper {
padding: 15px;
display: flex;
justify-content: center;
align-items: center;
min-height: 150px;
}
.periodic-popup-image {
max-width: 100%;
max-height: 50vh;
border-radius: 6px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.periodic-popup-footer {
padding: 8px 18px;
border-top: 1px solid #eee;
text-align: center;
color: #666;
font-size: 12px;
border-radius: 0 0 12px 12px;
background-color: #f8f9fa;
}
@media (max-width: 768px) {
.periodic-popup-content {
margin: 20px;
max-width: calc(100vw - 40px);
max-height: calc(100vh - 40px);
}
}
`;
document.head.appendChild(styles);
}
/**
* Update periodic popup settings
*/
updatePeriodicSettings(settings) {
if (settings.minInterval) this.periodicSystem.minInterval = settings.minInterval * 1000;
if (settings.maxInterval) this.periodicSystem.maxInterval = settings.maxInterval * 1000;
if (settings.displayDuration) this.periodicSystem.displayDuration = settings.displayDuration * 1000;
console.log('🔧 Periodic popup settings updated:', {
minInterval: this.periodicSystem.minInterval / 1000 + 's',
maxInterval: this.periodicSystem.maxInterval / 1000 + 's',
displayDuration: this.periodicSystem.displayDuration / 1000 + 's'
});
}
loadConfiguration() {

View File

@ -1168,6 +1168,158 @@ body {
display: none;
}
/* New Game Mode Grid Styles */
.game-mode-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 15px;
margin: 20px 0;
}
.game-mode-card {
background: white;
border: 2px solid #ddd;
border-radius: 12px;
padding: 20px;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
display: flex;
align-items: center;
gap: 15px;
min-height: 100px;
}
.game-mode-card:hover {
border-color: #4facfe;
transform: translateY(-3px);
box-shadow: 0 8px 25px rgba(79, 172, 254, 0.2);
}
.game-mode-card.selected {
border-color: #4facfe;
background: linear-gradient(135deg, #4facfe15, #00f2fe08);
box-shadow: 0 8px 25px rgba(79, 172, 254, 0.3);
}
.game-mode-card input[type="radio"] {
position: absolute;
opacity: 0;
top: 15px;
right: 15px;
cursor: pointer;
}
.mode-icon {
font-size: 2.5em;
flex-shrink: 0;
width: 60px;
height: 60px;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #f8f9fa, #e9ecef);
border-radius: 50%;
transition: all 0.3s ease;
}
.game-mode-card:hover .mode-icon {
background: linear-gradient(135deg, #4facfe, #00f2fe);
color: white;
transform: scale(1.1);
}
.game-mode-card.selected .mode-icon {
background: linear-gradient(135deg, #4facfe, #00f2fe);
color: white;
}
.mode-info {
flex: 1;
}
.mode-info h4 {
color: #333;
margin: 0 0 8px 0;
font-size: 1.2em;
font-weight: 600;
}
.mode-info p {
color: #666;
margin: 0;
font-size: 0.95em;
line-height: 1.4;
}
/* Mode Options Styles */
.mode-options {
margin-top: 20px;
}
.config-section {
background: #f8f9fa;
border-radius: 8px;
padding: 15px;
margin-bottom: 15px;
border: 1px solid #e9ecef;
}
.config-section h4 {
color: #495057;
margin: 0 0 12px 0;
font-size: 1em;
display: flex;
align-items: center;
gap: 8px;
}
.checkbox-label {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
cursor: pointer;
font-size: 0.9em;
color: #495057;
}
.checkbox-label input[type="checkbox"] {
margin: 0;
cursor: pointer;
}
.frequency-control {
margin-left: 20px;
padding: 10px;
background: white;
border-radius: 6px;
border: 1px solid #dee2e6;
}
.frequency-control label {
display: block;
font-size: 0.85em;
color: #6c757d;
margin-bottom: 5px;
}
.frequency-control input[type="range"] {
width: 100%;
margin: 5px 0;
}
.frequency-control small {
font-size: 0.75em;
color: #6c757d;
font-style: italic;
}
#frequency-display {
font-weight: bold;
color: #4facfe;
}
.custom-time-input:focus,
.custom-score-input:focus {
border-color: #007bff;
@ -3076,8 +3228,509 @@ body.theme-monochrome {
align-items: flex-start;
gap: 5px;
}
.status-value {
align-self: flex-end;
}
}
/* Audio Controls Styling */
.audio-controls {
border-top: 1px solid var(--border-color);
padding-top: 10px;
margin-top: 10px;
}
.audio-control-header {
margin-bottom: 10px;
}
.audio-control-group {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 10px;
}
.audio-control {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
}
.audio-control label {
min-width: 100px;
font-weight: 500;
}
.volume-slider {
flex: 1;
height: 4px;
background: var(--border-color);
border-radius: 2px;
outline: none;
-webkit-appearance: none;
}
.volume-slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 16px;
height: 16px;
background: var(--primary-color);
border-radius: 50%;
cursor: pointer;
transition: transform 0.2s;
}
.volume-slider::-webkit-slider-thumb:hover {
transform: scale(1.1);
}
.volume-slider::-moz-range-thumb {
width: 16px;
height: 16px;
background: var(--primary-color);
border-radius: 50%;
cursor: pointer;
border: none;
}
.audio-control span {
min-width: 35px;
text-align: right;
font-weight: 500;
color: var(--primary-color);
}
.btn-mini {
padding: 2px 6px;
font-size: 10px;
min-width: 24px;
height: 20px;
line-height: 1;
}
.audio-toggles {
display: flex;
flex-direction: column;
gap: 4px;
padding-top: 8px;
border-top: 1px solid var(--border-color-light);
}
.audio-toggles label {
display: flex;
align-items: center;
gap: 6px;
font-size: 11px;
cursor: pointer;
}
.audio-toggles input[type="checkbox"] {
margin: 0;
}
/* ===== INTERACTIVE TASK STYLES ===== */
/* Interactive Task Container */
.interactive-task-container {
margin: 20px 0;
padding: 20px;
background: rgba(255, 255, 255, 0.1);
border-radius: 12px;
border: 2px solid rgba(255, 255, 255, 0.2);
backdrop-filter: blur(10px);
}
.interactive-controls {
margin-top: 15px;
display: flex;
gap: 10px;
justify-content: center;
flex-wrap: wrap;
}
.interactive-feedback {
margin-top: 15px;
padding: 12px;
border-radius: 8px;
text-align: center;
font-weight: bold;
display: none;
animation: feedback-slide-in 0.3s ease-out;
}
@keyframes feedback-slide-in {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.feedback-success {
background: rgba(40, 167, 69, 0.9);
color: white;
}
.feedback-error {
background: rgba(220, 53, 69, 0.9);
color: white;
}
.feedback-warning {
background: rgba(255, 193, 7, 0.9);
color: #856404;
}
.feedback-info {
background: rgba(23, 162, 184, 0.9);
color: white;
}
/* ===== RHYTHM TASK STYLES ===== */
.rhythm-task {
text-align: center;
}
.rhythm-task h4 {
color: rgba(255, 255, 255, 0.9);
margin-bottom: 10px;
}
.rhythm-task p {
color: rgba(255, 255, 255, 0.8);
margin-bottom: 20px;
}
.rhythm-display {
margin: 20px 0;
}
.beat-indicator {
width: 100px;
height: 100px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.3);
margin: 0 auto 15px;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
transition: all 0.1s ease;
border: 3px solid rgba(255, 255, 255, 0.5);
}
.beat-indicator.beat-flash {
background: rgba(255, 107, 107, 0.8);
transform: scale(1.1);
box-shadow: 0 0 20px rgba(255, 107, 107, 0.6);
border-color: rgba(255, 107, 107, 0.9);
}
.rhythm-progress {
width: 100%;
height: 8px;
background: rgba(255, 255, 255, 0.2);
border-radius: 4px;
overflow: hidden;
margin-bottom: 20px;
}
.progress-bar {
height: 100%;
background: linear-gradient(90deg, #ff6b6b, #ee5a24);
width: 0%;
transition: width 0.2s ease;
}
.rhythm-tap-btn {
width: 150px;
height: 150px;
border-radius: 50%;
background: linear-gradient(135deg, #667eea, #764ba2);
border: 3px solid rgba(255, 255, 255, 0.3);
color: white;
font-size: 20px;
font-weight: bold;
cursor: pointer;
transition: all 0.1s ease;
margin: 20px 0;
box-shadow: 0 8px 15px rgba(0, 0, 0, 0.2);
}
.rhythm-tap-btn:hover {
background: linear-gradient(135deg, #764ba2, #667eea);
transform: scale(1.05);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.3);
}
.rhythm-tap-btn:active,
.rhythm-tap-btn.tapped {
background: linear-gradient(135deg, #ff6b6b, #ee5a24);
transform: scale(0.95);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
}
.rhythm-stats {
display: flex;
justify-content: space-around;
margin-top: 15px;
font-size: 16px;
font-weight: bold;
}
.rhythm-stats span {
color: rgba(255, 255, 255, 0.9);
background: rgba(255, 255, 255, 0.1);
padding: 8px 12px;
border-radius: 6px;
}
/* ===== SCENARIO BUILDER STYLES ===== */
.scenario-task {
text-align: left;
max-width: 100%;
}
.scenario-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding: 15px;
background: rgba(255, 255, 255, 0.1);
border-radius: 8px;
}
.scenario-header h4 {
color: rgba(255, 255, 255, 0.95);
margin: 0;
font-size: 18px;
}
.scenario-progress {
color: rgba(255, 255, 255, 0.8);
font-size: 14px;
background: rgba(0, 0, 0, 0.2);
padding: 5px 10px;
border-radius: 12px;
}
.scenario-content {
margin-bottom: 20px;
}
.scenario-story {
margin-bottom: 20px;
}
.story-text {
background: rgba(255, 255, 255, 0.08);
padding: 20px;
border-radius: 10px;
border-left: 4px solid rgba(255, 255, 255, 0.3);
color: rgba(255, 255, 255, 0.9);
line-height: 1.6;
font-size: 16px;
}
.story-text.seductive {
border-left-color: #ff6b6b;
background: rgba(255, 107, 107, 0.1);
}
.story-text.intense {
border-left-color: #ee5a24;
background: rgba(238, 90, 36, 0.1);
}
.story-text.playful {
border-left-color: #667eea;
background: rgba(102, 126, 234, 0.1);
}
.story-text.dominant {
border-left-color: #764ba2;
background: rgba(118, 75, 162, 0.1);
}
.scenario-choices {
display: flex;
flex-direction: column;
gap: 12px;
}
.scenario-choice {
background: linear-gradient(135deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.05));
border: 2px solid rgba(255, 255, 255, 0.2);
border-radius: 10px;
padding: 15px;
color: white;
cursor: pointer;
transition: all 0.3s ease;
text-align: left;
min-height: auto;
width: 100%;
}
.scenario-choice:hover:not(.disabled) {
background: linear-gradient(135deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0.1));
border-color: rgba(255, 255, 255, 0.4);
transform: translateY(-2px);
}
.scenario-choice.disabled {
opacity: 0.5;
cursor: not-allowed;
background: rgba(0, 0, 0, 0.2);
}
.scenario-choice.submissive {
border-color: rgba(255, 107, 107, 0.5);
background: linear-gradient(135deg, rgba(255, 107, 107, 0.1), rgba(255, 107, 107, 0.05));
}
.scenario-choice.dominant {
border-color: rgba(118, 75, 162, 0.5);
background: linear-gradient(135deg, rgba(118, 75, 162, 0.1), rgba(118, 75, 162, 0.05));
}
.scenario-choice.risky {
border-color: rgba(238, 90, 36, 0.5);
background: linear-gradient(135deg, rgba(238, 90, 36, 0.1), rgba(238, 90, 36, 0.05));
}
.choice-text {
font-size: 16px;
font-weight: bold;
margin-bottom: 5px;
}
.choice-preview {
font-size: 14px;
color: rgba(255, 255, 255, 0.7);
font-style: italic;
margin-bottom: 5px;
}
.choice-requirements {
font-size: 12px;
color: rgba(255, 193, 7, 0.8);
background: rgba(255, 193, 7, 0.1);
padding: 4px 8px;
border-radius: 4px;
display: inline-block;
}
.scenario-action {
background: linear-gradient(135deg, #667eea, #764ba2);
border: none;
border-radius: 10px;
padding: 20px;
color: white;
cursor: pointer;
transition: all 0.3s ease;
width: 100%;
font-size: 16px;
font-weight: bold;
}
.scenario-action:hover {
background: linear-gradient(135deg, #764ba2, #667eea);
transform: scale(1.02);
}
.scenario-action.active {
background: linear-gradient(135deg, #ff6b6b, #ee5a24);
animation: pulse 1s infinite;
}
.scenario-action.completed {
background: linear-gradient(135deg, #28a745, #20c997);
cursor: default;
}
.action-text {
font-size: 18px;
margin-bottom: 10px;
}
.action-timer {
font-size: 24px;
font-weight: bold;
color: rgba(255, 255, 255, 0.9);
}
.scenario-ending {
text-align: center;
padding: 20px;
background: rgba(255, 255, 255, 0.1);
border-radius: 10px;
}
.ending-text {
font-size: 16px;
color: rgba(255, 255, 255, 0.9);
margin-bottom: 15px;
line-height: 1.6;
}
.ending-outcome {
font-size: 18px;
font-weight: bold;
padding: 10px;
border-radius: 8px;
}
.ending-outcome.success {
background: rgba(40, 167, 69, 0.2);
color: #28a745;
}
.ending-outcome.failure {
background: rgba(220, 53, 69, 0.2);
color: #dc3545;
}
.ending-outcome.reward {
background: rgba(255, 193, 7, 0.2);
color: #ffc107;
}
.scenario-status {
margin-top: 20px;
padding: 15px;
background: rgba(0, 0, 0, 0.2);
border-radius: 8px;
}
.scenario-stats {
display: flex;
justify-content: space-around;
flex-wrap: wrap;
gap: 10px;
}
.scenario-stats .stat {
background: rgba(255, 255, 255, 0.1);
padding: 8px 12px;
border-radius: 6px;
font-size: 14px;
color: rgba(255, 255, 255, 0.9);
text-align: center;
min-width: 80px;
}
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.05); }
100% { transform: scale(1); }
}