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:
parent
478b5884fb
commit
395c79363c
|
|
@ -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
|
||||
134
audio/README.md
134
audio/README.md
|
|
@ -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!
|
||||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
67
gameData.js
67
gameData.js
|
|
@ -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!"
|
||||
}
|
||||
],
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -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;
|
||||
}
|
||||
90
index.html
90
index.html
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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() {
|
||||
|
|
|
|||
655
styles.css
655
styles.css
|
|
@ -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); }
|
||||
}
|
||||
Loading…
Reference in New Issue