Browse Source

Implement seasonal themes and audio system enhancements

- Add seasonal theme system with winter, spring, summer, autumn themes
- Implement comprehensive audio system with menu and in-game music
- Update theme manager with seasonal color schemes and animations
- Add seasonal audio tracks for menu and gameplay
- Enhance UI components with theme-aware styling
- Update documentation for sound system and seasonal implementation
- Add audio conversion scripts for asset optimization
- Improve settings screen with theme selection
- Update stats screen with seasonal theming
- Refine game screen and main menu with seasonal elements
Fszontagh 4 months ago
parent
commit
fc8f3a0474

BIN
assets/audio/ingame/ingame_autumn_001.mp3


BIN
assets/audio/ingame/ingame_default_001.mp3


BIN
assets/audio/ingame/ingame_spring_001.mp3


BIN
assets/audio/ingame/ingame_summer_001.mp3


BIN
assets/audio/ingame/ingame_winter_001.mp3


BIN
assets/audio/ingame/ingame_winter_002.mp3


BIN
assets/audio/menu/menu_autumn.mp3


BIN
assets/audio/menu/menu_default.mp3


BIN
assets/audio/menu/menu_spring.mp3


BIN
assets/audio/menu/menu_summer.mp3


BIN
assets/audio/menu/menu_winter.mp3


+ 21 - 1
docs/IMPLEMENTATION_SUMMARY.md

@@ -1,4 +1,24 @@
-# Google Play Games Integration Summary
+# ZenTap Implementation Summary
+
+## Latest Updates (v1.0.4+)
+
+### 🎵 Theme-Based Music System
+- **Menu Music**: Automatically adapts to selected seasonal theme
+- **Ingame Music**: Random selection from theme-appropriate tracks
+- **Smart Switching**: Music changes when themes are selected in settings
+- **Multi-Track Support**: System supports multiple tracks per theme (winter has 2 tracks)
+
+### 🎨 Theme System Improvements
+- **Hidden Default Theme**: Default theme is hidden from selector in release builds
+- **Fixed Overflow**: Theme selector is now scrollable and responsive
+- **Immediate Music Feedback**: Theme changes instantly update background music
+
+### 📱 UI Enhancements
+- **Responsive Theme Dialog**: Properly sized and scrollable theme selection
+- **Better Landscape Support**: Theme selector works in all orientations
+- **Context-Aware Music**: Different music for menu vs gameplay contexts
+
+## Google Play Games Integration Summary
 
 ## ✅ Successfully Implemented
 

+ 10 - 1
docs/SEASONAL_THEMES_IMPLEMENTATION.md

@@ -173,13 +173,22 @@ Widget buildSeasonalIcon() {
 4. **Consistency**: All UI elements adapt cohesively to selected theme
 5. **Performance**: Efficient color system with minimal overhead
 
-## Future Enhancements
+## Recent Enhancements (v1.0.4+)
+
+### ✅ Implemented Features
+- **Hide Default Theme in Release**: Default theme is hidden from theme selector in production builds
+- **Theme-Based Music System**: Menu and ingame music automatically adapt to selected theme
+- **Scrollable Theme Selector**: Fixed overflow issues in theme selection dialog
+- **Smart Music Switching**: Music changes instantly when theme is selected
+
+### 🔄 Future Enhancements
 
 1. **Auto Theme Selection**: Based on system time/season
 2. **Custom Themes**: User-defined color schemes
 3. **Theme Animations**: Smooth transitions between themes
 4. **Adaptive Brightness**: Theme variants for different lighting
 5. **Theme Previews**: Live preview in selection dialog
+6. **Theme-Based Sound Effects**: Seasonal bubble pop sounds to match themes
 
 ## Testing Recommendations
 

+ 115 - 66
docs/SOUND.md

@@ -9,29 +9,59 @@ All audio files should be placed in the [`assets/audio/`](assets/audio/) directo
 ```
 assets/
 └── audio/
-    ├── bubble_pop.wav          # Primary bubble pop sound
-    ├── bubble_pop_alt.wav      # Alternative bubble pop sound
-    ├── ambient_background.mp3  # Main background music
-    ├── zen_mode_background.mp3 # Zen mode ambient music
-    └── ui_click.wav           # UI button click sound (optional)
+    ├── source/
+    │   ├── bubble_pop.mp3          # Primary bubble pop sound
+    │   ├── bubble_pop_alt.mp3      # Alternative bubble pop sound
+    │   └── ambient_background.mp3  # Fallback background music
+    ├── menu/
+    │   ├── menu_default.mp3        # Default theme menu music
+    │   ├── menu_spring.mp3         # Spring theme menu music
+    │   ├── menu_summer.mp3         # Summer theme menu music
+    │   ├── menu_autumn.mp3         # Autumn theme menu music
+    │   └── menu_winter.mp3         # Winter theme menu music
+    ├── ingame/
+    │   ├── ingame_default_001.mp3  # Default theme ingame music
+    │   ├── ingame_spring_001.mp3   # Spring theme ingame music
+    │   ├── ingame_summer_001.mp3   # Summer theme ingame music
+    │   ├── ingame_autumn_001.mp3   # Autumn theme ingame music
+    │   ├── ingame_winter_001.mp3   # Winter theme ingame music (track 1)
+    │   └── ingame_winter_002.mp3   # Winter theme ingame music (track 2)
+    └── compressed/
+        ├── bubble_pop.wav          # Compressed bubble pop sound
+        ├── bubble_pop_alt.wav      # Compressed alternative pop sound
+        └── ambient_background.mp3  # Compressed background music
 ```
 
 ## 🎵 Required Audio Files
 
-### Sound Effects (WAV format recommended)
+### Sound Effects (MP3 format)
 
 | File Name | Purpose | Duration | Description |
 |-----------|---------|----------|-------------|
-| `bubble_pop.wav` | Primary bubble pop sound | 0.1-0.3s | Satisfying pop sound when bubbles are tapped |
-| `bubble_pop_alt.wav` | Alternative pop sound | 0.1-0.3s | Variation for audio diversity (optional) |
-| `ui_click.wav` | UI interaction | 0.05-0.1s | Button press feedback (optional) |
+| `source/bubble_pop.mp3` | Primary bubble pop sound | 0.1-0.3s | Satisfying pop sound when bubbles are tapped |
+| `source/bubble_pop_alt.mp3` | Alternative pop sound | 0.1-0.3s | Variation for audio diversity |
+| `source/ambient_background.mp3` | Fallback background | 2-5 minutes | Legacy background music file |
 
-### Background Music (MP3 format recommended)
+### Theme-Based Menu Music (MP3 format)
 
 | File Name | Purpose | Duration | Description |
 |-----------|---------|----------|-------------|
-| `ambient_background.mp3` | Play mode background | 2-5 minutes | Calm, looping ambient music for gameplay |
-| `zen_mode_background.mp3` | Zen mode background | 2-5 minutes | Even more relaxing track for zen mode |
+| `menu/menu_default.mp3` | Default theme menu | 2-5 minutes | Menu music for default/fSociety theme |
+| `menu/menu_spring.mp3` | Spring theme menu | 2-5 minutes | Light, fresh menu music for spring |
+| `menu/menu_summer.mp3` | Summer theme menu | 2-5 minutes | Warm, bright menu music for summer |
+| `menu/menu_autumn.mp3` | Autumn theme menu | 2-5 minutes | Cozy, warm menu music for autumn |
+| `menu/menu_winter.mp3` | Winter theme menu | 2-5 minutes | Cool, serene menu music for winter |
+
+### Theme-Based Ingame Music (MP3 format)
+
+| File Name | Purpose | Duration | Description |
+|-----------|---------|----------|-------------|
+| `ingame/ingame_default_001.mp3` | Default theme gameplay | 3-8 minutes | Gameplay music for default theme |
+| `ingame/ingame_spring_001.mp3` | Spring theme gameplay | 3-8 minutes | Fresh, energetic gameplay music |
+| `ingame/ingame_summer_001.mp3` | Summer theme gameplay | 3-8 minutes | Upbeat, sunny gameplay music |
+| `ingame/ingame_autumn_001.mp3` | Autumn theme gameplay | 3-8 minutes | Contemplative autumn gameplay music |
+| `ingame/ingame_winter_001.mp3` | Winter theme gameplay (track 1) | 3-8 minutes | Calm winter gameplay music |
+| `ingame/ingame_winter_002.mp3` | Winter theme gameplay (track 2) | 3-8 minutes | Alternative winter gameplay music |
 
 ## 📋 Audio Specifications
 
@@ -64,56 +94,44 @@ flutter:
     - assets/audio/
 ```
 
-### 3. Update AudioManager
-Modify [`lib/game/audio/audio_manager.dart`](lib/game/audio/audio_manager.dart) to load the actual files:
+### 3. Audio System Features
 
+The AudioManager now supports:
+
+#### Theme-Based Music System
+- **Menu Music**: Automatically selects music based on current seasonal theme
+- **Ingame Music**: Random selection from theme-appropriate tracks during gameplay
+- **Context Switching**: Different music for menu vs. gameplay contexts
+
+#### Smart Music Management
 ```dart
-// Replace the placeholder methods with actual file loading:
-
-Future<void> initialize() async {
-  try {
-    // Load background music
-    await _backgroundPlayer.setAsset('assets/audio/ambient_background.mp3');
-    await _backgroundPlayer.setLoopMode(LoopMode.one);
-  } catch (e) {
-    print('Audio initialization failed: $e');
-  }
-}
-
-Future<void> playBubblePop() async {
-  if (!_soundEnabled) return;
-  
-  try {
-    // Load and play bubble pop sound
-    await _effectPlayer.setAsset('assets/audio/bubble_pop.wav');
-    await _effectPlayer.play();
-    
-    // Add haptic feedback
-    if (_hapticEnabled) {
-      await _playHapticFeedback();
-    }
-  } catch (e) {
-    print('Failed to play bubble pop sound: $e');
-  }
-}
-
-Future<void> playBackgroundMusic() async {
-  if (!_musicEnabled) return;
-  
-  try {
-    // Switch music based on zen mode
-    String musicFile = isZenMode 
-        ? 'assets/audio/zen_mode_background.mp3'
-        : 'assets/audio/ambient_background.mp3';
-        
-    await _backgroundPlayer.setAsset(musicFile);
-    await _backgroundPlayer.play();
-  } catch (e) {
-    print('Failed to play background music: $e');
-  }
-}
+// Theme-aware menu music
+await AudioManager().playMenuMusic();
+
+// Theme-aware ingame music with random selection
+await AudioManager().playIngameMusic();
+
+// Legacy compatibility method
+await AudioManager().playBackgroundMusic(); // Falls back to menu music
+```
+
+#### Multiple Track Support
+The system supports multiple tracks per theme for variety:
+```dart
+// Example: Winter theme has 2 ingame tracks
+static const Map<String, List<String>> _ingameMusicTracks = {
+  'winter': ['ingame/ingame_winter_001.mp3', 'ingame/ingame_winter_002.mp3'],
+  'spring': ['ingame/ingame_spring_001.mp3'],
+  // ... other themes
+};
 ```
 
+#### Automatic Integration
+- **Main Menu**: Starts theme-appropriate menu music
+- **Game Screen**: Switches to theme-appropriate ingame music
+- **Settings**: Updates music when theme is changed
+- **Stats Screen**: Returns to menu music
+
 ### 4. Test Audio Integration
 After adding files and updating the code:
 
@@ -175,14 +193,45 @@ flutter analyze
 - **Web**: Use MP3 for broader browser support
 - **Desktop**: Full format support
 
-## 🎯 Future Enhancements
-
-- Multiple bubble pop sound variations
-- Dynamic music layers based on gameplay
-- Sound effect randomization for variety
-- Volume sliders for SFX and music separately
-- Sound preference persistence
+## 🎯 Recent Enhancements (v1.0.4+)
+
+### ✅ Implemented Features
+- **Theme-Based Music System**: Menu and ingame music automatically adapt to seasonal themes
+- **Random Track Selection**: Multiple ingame tracks per theme with random selection
+- **Context-Aware Audio**: Different music for menu vs. gameplay contexts
+- **Smart Music Switching**: Music changes when themes are selected in settings
+- **Multi-Track Support**: System supports multiple tracks per theme (winter has 2 tracks)
+
+### 🔄 Future Enhancements
+- Multiple bubble pop sound variations per theme
+- Dynamic music layers based on gameplay intensity
+- Music fade-in/fade-out transitions
+- Per-theme volume balancing
+- Music crossfading during theme changes
+- Seasonal sound effects to match themes
+
+## 🎨 Theme Audio Design Guidelines
+
+### Spring Theme
+- **Menu Music**: Light, fresh, awakening sounds
+- **Ingame Music**: Gentle, optimistic, growing energy
+- **Mood**: Renewal, hope, gentle energy
+
+### Summer Theme
+- **Menu Music**: Warm, bright, uplifting
+- **Ingame Music**: Energetic but not overwhelming
+- **Mood**: Joy, vitality, warmth
+
+### Autumn Theme
+- **Menu Music**: Cozy, contemplative, warm
+- **Ingame Music**: Reflective, comfortable, grounding
+- **Mood**: Reflection, coziness, preparation
+
+### Winter Theme
+- **Menu Music**: Serene, crisp, peaceful
+- **Ingame Music**: Calm, focused, crystalline clarity
+- **Mood**: Tranquility, focus, inner peace
 
 ---
 
-**Note**: Current implementation uses system sounds as placeholders. Follow this guide to replace them with custom audio files for the complete ZenTap experience.
+**Note**: The theme-based music system is now fully implemented. Replace the placeholder audio files with theme-appropriate music to complete the immersive ZenTap experience.

+ 140 - 40
lib/game/audio/audio_manager.dart

@@ -2,6 +2,7 @@ import 'package:flutter/services.dart';
 import 'package:flame_audio/flame_audio.dart';
 import 'dart:math';
 import 'dart:io' show Platform;
+import '../../utils/theme_manager.dart';
 
 class AudioManager {
   static final AudioManager _instance = AudioManager._internal();
@@ -9,7 +10,7 @@ class AudioManager {
   AudioManager._internal();
 
   final Random _random = Random();
-  
+
   bool _soundEnabled = true;
   bool _musicEnabled = true;
   bool _hapticEnabled = true;
@@ -21,6 +22,20 @@ class AudioManager {
   AudioPool? _popPool;
   AudioPool? _popAltPool;
 
+  // Current playing music context
+  String? _currentMenuMusic;
+  String? _currentIngameMusic;
+  bool _isMenuMusic = false;
+
+  // Available ingame music tracks by theme
+  static const Map<String, List<String>> _ingameMusicTracks = {
+    'default': ['ingame/ingame_default_001.mp3'],
+    'spring': ['ingame/ingame_spring_001.mp3'],
+    'summer': ['ingame/ingame_summer_001.mp3'],
+    'autumn': ['ingame/ingame_autumn_001.mp3'],
+    'winter': ['ingame/ingame_winter_001.mp3', 'ingame/ingame_winter_002.mp3'],
+  };
+
   bool get soundEnabled => _soundEnabled;
   bool get musicEnabled => _musicEnabled;
   bool get hapticEnabled => _hapticEnabled;
@@ -31,16 +46,33 @@ class AudioManager {
 
   Future<void> initialize() async {
     await _detectPlatformCapabilities();
-    
+
     if (_isAudioSupported) {
       // Preload audio assets
       try {
+        // Basic sound effects
         await FlameAudio.audioCache.loadAll([
           'source/ambient_background.mp3',
           'source/bubble_pop.mp3',
-          'source/bubble_pop_alt.mp3'
+          'source/bubble_pop_alt.mp3',
+        ]);
+
+        // Menu music files
+        await FlameAudio.audioCache.loadAll([
+          'menu/menu_default.mp3',
+          'menu/menu_spring.mp3',
+          'menu/menu_summer.mp3',
+          'menu/menu_autumn.mp3',
+          'menu/menu_winter.mp3',
         ]);
-        
+
+        // Ingame music files
+        final allIngameTracks = <String>[];
+        for (final tracks in _ingameMusicTracks.values) {
+          allIngameTracks.addAll(tracks);
+        }
+        await FlameAudio.audioCache.loadAll(allIngameTracks);
+
         // Create sound pools for efficient playback
         _popPool = await FlameAudio.createPool(
           'source/bubble_pop.mp3',
@@ -54,18 +86,20 @@ class AudioManager {
         print('Failed to load audio assets or create pools: $e');
       }
     }
-    
+
     print('AudioManager initialized for ${Platform.operatingSystem}');
   }
 
   Future<void> _detectPlatformCapabilities() async {
     // Check audio support
     _isAudioSupported = _checkAudioSupport();
-    
+
     // Check vibration support
     _isVibrationSupported = _checkVibrationSupport();
-    
-    print('Platform capabilities - Audio: $_isAudioSupported, Vibration: $_isVibrationSupported');
+
+    print(
+      'Platform capabilities - Audio: $_isAudioSupported, Vibration: $_isVibrationSupported',
+    );
   }
 
   bool _checkAudioSupport() {
@@ -94,26 +128,28 @@ class AudioManager {
 
   void playBubblePop() {
     if (!_soundEnabled) return;
-    
+
     // Make audio playback async to avoid blocking main thread
     _playBubblePopAsync();
-    
+
     // Handle haptic feedback asynchronously
     if (_hapticEnabled && _isVibrationSupported) {
       _playHapticFeedbackAsync();
     }
   }
-  
+
   void _playBubblePopAsync() async {
     if (_isAudioSupported) {
       // Use Flame Audio for bubble pop
-      final sound = _random.nextBool()
-          ? 'source/bubble_pop.mp3'
-          : 'source/bubble_pop_alt.mp3';
+      final sound =
+          _random.nextBool()
+              ? 'source/bubble_pop.mp3'
+              : 'source/bubble_pop_alt.mp3';
       try {
         if (sound == 'source/bubble_pop.mp3' && _popPool != null) {
           _popPool!.start(volume: _sfxVolume);
-        } else if (sound == 'source/bubble_pop_alt.mp3' && _popAltPool != null) {
+        } else if (sound == 'source/bubble_pop_alt.mp3' &&
+            _popAltPool != null) {
           _popAltPool!.start(volume: _sfxVolume);
         } else {
           // Fallback to direct playback if pools fail
@@ -127,7 +163,7 @@ class AudioManager {
       _playSystemSoundAsync();
     }
   }
-  
+
   void _playSystemSoundAsync() async {
     try {
       // Vary the system sound for some variety
@@ -146,13 +182,13 @@ class AudioManager {
       print('Failed to play system sound: $e');
     }
   }
-  
+
   void _playHapticFeedbackAsync() async {
     if (!_isVibrationSupported) {
       // Silently skip haptic feedback on unsupported platforms
       return;
     }
-    
+
     try {
       // Use Flutter's built-in HapticFeedback instead of vibration plugin
       await HapticFeedback.lightImpact();
@@ -163,33 +199,91 @@ class AudioManager {
     }
   }
 
+  Future<void> playMenuMusic() async {
+    if (!_musicEnabled || !_isAudioSupported) {
+      if (!_isAudioSupported) {
+        print('Menu music not available - using system audio only');
+      }
+      return;
+    }
+
+    try {
+      // Get current theme
+      final currentTheme = ThemeManager.effectiveTheme;
+      final themeId = currentTheme.id;
+      final menuMusicPath = 'menu/menu_$themeId.mp3';
 
-  Future<void> playBackgroundMusic() async {
+      // Only change music if it's different from current
+      if (_currentMenuMusic == menuMusicPath && _isMenuMusic) {
+        print('Menu music already playing: $menuMusicPath');
+        return;
+      }
+
+      // Stop any existing BGM first
+      await FlameAudio.bgm.stop();
+
+      // Play theme-based menu music
+      FlameAudio.bgm.play(menuMusicPath, volume: _bgmVolume);
+      _currentMenuMusic = menuMusicPath;
+      _isMenuMusic = true;
+      _currentIngameMusic = null;
+
+      print('Menu music started: $menuMusicPath');
+    } catch (e) {
+      print('Failed to play menu music: $e');
+    }
+  }
+
+  Future<void> playIngameMusic() async {
     if (!_musicEnabled || !_isAudioSupported) {
       if (!_isAudioSupported) {
-        print('Background music not available - using system audio only');
+        print('Ingame music not available - using system audio only');
       }
       return;
     }
-    
+
     try {
+      // Get current theme
+      final currentTheme = ThemeManager.effectiveTheme;
+      final themeId = currentTheme.id;
+
+      // Get available tracks for this theme
+      final availableTracks =
+          _ingameMusicTracks[themeId] ?? _ingameMusicTracks['default']!;
+
+      // Randomly select a track
+      final selectedTrack =
+          availableTracks[_random.nextInt(availableTracks.length)];
+
+      // Only change music if it's different from current
+      if (_currentIngameMusic == selectedTrack && !_isMenuMusic) {
+        print('Ingame music already playing: $selectedTrack');
+        return;
+      }
+
       // Stop any existing BGM first
       await FlameAudio.bgm.stop();
-      
-      // Play with updated volume
-      FlameAudio.bgm.play(
-        'source/ambient_background.mp3',
-        volume: _bgmVolume
-      );
-      print('Background music started');
+
+      // Play randomly selected ingame music
+      FlameAudio.bgm.play(selectedTrack, volume: _bgmVolume);
+      _currentIngameMusic = selectedTrack;
+      _isMenuMusic = false;
+      _currentMenuMusic = null;
+
+      print('Ingame music started: $selectedTrack (theme: $themeId)');
     } catch (e) {
-      print('Failed to play background music: $e');
+      print('Failed to play ingame music: $e');
     }
   }
 
+  Future<void> playBackgroundMusic() async {
+    // Fallback method - defaults to menu music for compatibility
+    await playMenuMusic();
+  }
+
   Future<void> stopBackgroundMusic() async {
     if (!_isAudioSupported) return;
-    
+
     try {
       FlameAudio.bgm.stop();
     } catch (e) {
@@ -199,7 +293,7 @@ class AudioManager {
 
   Future<void> pauseBackgroundMusic() async {
     if (!_isAudioSupported) return;
-    
+
     try {
       FlameAudio.bgm.pause();
     } catch (e) {
@@ -209,14 +303,13 @@ class AudioManager {
 
   Future<void> resumeBackgroundMusic() async {
     if (!_musicEnabled || !_isAudioSupported) return;
-    
+
     try {
       FlameAudio.bgm.resume();
     } catch (e) {
       print('Failed to resume background music: $e');
     }
   }
-  
 
   void setSoundEnabled(bool enabled) {
     _soundEnabled = enabled;
@@ -227,7 +320,13 @@ class AudioManager {
     if (!enabled) {
       stopBackgroundMusic();
     } else {
-      playBackgroundMusic();
+      // Resume appropriate music based on context
+      if (_isMenuMusic ||
+          (_currentMenuMusic != null && _currentIngameMusic == null)) {
+        playMenuMusic();
+      } else {
+        playIngameMusic();
+      }
     }
   }
 
@@ -251,13 +350,13 @@ class AudioManager {
     } catch (e) {
       print('Failed to dispose background player: $e');
     }
-    
+
     try {
       await _popPool?.dispose();
     } catch (e) {
       print('Failed to dispose popPool: $e');
     }
-    
+
     try {
       await _popAltPool?.dispose();
     } catch (e) {
@@ -267,9 +366,10 @@ class AudioManager {
 
   // Utility method to get platform info for debugging
   String getPlatformInfo() {
-    final audioType = _isAudioSupported ? 'Full Audio Support' : 'System Sounds Only';
+    final audioType =
+        _isAudioSupported ? 'Full Audio Support' : 'System Sounds Only';
     return 'Platform: ${Platform.operatingSystem}, '
-           'Audio: $audioType, '
-           'Vibration: $_isVibrationSupported';
+        'Audio: $audioType, '
+        'Vibration: $_isVibrationSupported';
   }
-}
+}

+ 78 - 79
lib/ui/game_screen.dart

@@ -8,16 +8,14 @@ import '../utils/colors.dart';
 import '../utils/settings_manager.dart';
 import '../utils/haptic_utils.dart';
 import '../utils/theme_notifier.dart';
+import '../game/audio/audio_manager.dart';
 import 'components/animated_background.dart';
 import '../utils/app_lifecycle_manager.dart';
 
 class GameScreen extends StatefulWidget {
   final bool isZenMode;
 
-  const GameScreen({
-    super.key,
-    required this.isZenMode,
-  });
+  const GameScreen({super.key, required this.isZenMode});
 
   @override
   State<GameScreen> createState() => _GameScreenState();
@@ -34,10 +32,10 @@ class _GameScreenState extends State<GameScreen> {
     super.initState();
     game = ZenTapGame();
     game.setZenMode(widget.isZenMode);
-    
+
     // Register the game instance for app lifecycle management
     AppLifecycleManager.instance.setCurrentGame(game);
-    
+
     // Hide system UI for immersive experience
     SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
   }
@@ -45,7 +43,7 @@ class _GameScreenState extends State<GameScreen> {
   @override
   void didChangeDependencies() {
     super.didChangeDependencies();
-    
+
     // Set localized strings for the game
     final l10n = AppLocalizations.of(context)!;
     game.setLocalizedStrings(
@@ -53,14 +51,19 @@ class _GameScreenState extends State<GameScreen> {
       relaxationPoints: l10n.relaxationPoints,
       time: l10n.time,
     );
-    
+
     // Initialize shake detector only on mobile platforms
     _initializeShakeDetector();
+
+    // Start ingame music when entering game
+    WidgetsBinding.instance.addPostFrameCallback((_) {
+      AudioManager().playIngameMusic();
+    });
   }
 
   void _initializeShakeDetector() async {
     if (_shakeDetector != null) return; // Already initialized
-    
+
     try {
       // Only initialize shake detector on mobile platforms
       if (Theme.of(context).platform == TargetPlatform.android ||
@@ -86,15 +89,15 @@ class _GameScreenState extends State<GameScreen> {
     } catch (e) {
       // Shake detector might not be initialized on desktop
     }
-    
+
     // Unregister the game instance from lifecycle management
     AppLifecycleManager.instance.setCurrentGame(null);
-    
+
     // Restore system UI
     SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
     super.dispose();
   }
-  
+
   void _onPhoneShake() async {
     if (!_isPaused) {
       // Trigger background shake animation
@@ -102,10 +105,10 @@ class _GameScreenState extends State<GameScreen> {
       if (backgroundState != null) {
         (backgroundState as dynamic).triggerShake();
       }
-      
+
       // Trigger game shake effects (spawn bubbles and shake existing ones)
       game.handleShake();
-      
+
       // Haptic feedback
       if (SettingsManager.isHapticsEnabled) {
         await HapticUtils.vibrate(duration: 100);
@@ -125,54 +128,57 @@ class _GameScreenState extends State<GameScreen> {
                 focusNode: FocusNode()..requestFocus(),
                 onKeyEvent: (event) {
                   // Add keyboard shortcut for shake simulation on desktop
-                  if (event.logicalKey == LogicalKeyboardKey.space && event is KeyDownEvent) {
+                  if (event.logicalKey == LogicalKeyboardKey.space &&
+                      event is KeyDownEvent) {
                     _onPhoneShake();
                   }
                 },
                 child: Stack(
-                children: [
-                  // Animated Background
-                  AnimatedBackground(
-                    key: _backgroundKey,
-                    isZenMode: widget.isZenMode,
-                    onShake: () {}, // Background handles its own shake animation
-                    child: Container(), // Empty container just for background
-                  ),
-                  
-                  // Game Widget with tap detection (transparent background)
-                  GestureDetector(
-                    onTapDown: (details) async {
-                      // Add haptic feedback on tap
-                      if (SettingsManager.isHapticsEnabled) {
-                        await HapticUtils.vibrate(duration: 30);
-                      }
-                      
-                      // Convert screen position to game position
-                      final position = Vector2(
-                        details.localPosition.dx,
-                        details.localPosition.dy,
-                      );
-                      game.handleTap(position);
-                    },
-                    child: GameWidget<ZenTapGame>.controlled(
-                      gameFactory: () => game,
+                  children: [
+                    // Animated Background
+                    AnimatedBackground(
+                      key: _backgroundKey,
+                      isZenMode: widget.isZenMode,
+                      onShake:
+                          () {}, // Background handles its own shake animation
+                      child: Container(), // Empty container just for background
                     ),
-                  ),
-                  
-                  // Top UI Overlay - adaptive to orientation
-                  SafeArea(
-                    child: Padding(
-                      padding: const EdgeInsets.all(16.0),
-                      child: orientation == Orientation.portrait
-                          ? _buildPortraitUI()
-                          : _buildLandscapeUI(),
+
+                    // Game Widget with tap detection (transparent background)
+                    GestureDetector(
+                      onTapDown: (details) async {
+                        // Add haptic feedback on tap
+                        if (SettingsManager.isHapticsEnabled) {
+                          await HapticUtils.vibrate(duration: 30);
+                        }
+
+                        // Convert screen position to game position
+                        final position = Vector2(
+                          details.localPosition.dx,
+                          details.localPosition.dy,
+                        );
+                        game.handleTap(position);
+                      },
+                      child: GameWidget<ZenTapGame>.controlled(
+                        gameFactory: () => game,
+                      ),
                     ),
-                  ),
-                  
-                  // Pause Overlay
-                  if (_isPaused) _buildPauseOverlay(),
-                ],
-               ),
+
+                    // Top UI Overlay - adaptive to orientation
+                    SafeArea(
+                      child: Padding(
+                        padding: const EdgeInsets.all(16.0),
+                        child:
+                            orientation == Orientation.portrait
+                                ? _buildPortraitUI()
+                                : _buildLandscapeUI(),
+                      ),
+                    ),
+
+                    // Pause Overlay
+                    if (_isPaused) _buildPauseOverlay(),
+                  ],
+                ),
               );
             },
           ),
@@ -198,13 +204,10 @@ class _GameScreenState extends State<GameScreen> {
           ),
         ),
         const Spacer(),
-        
+
         // Mode Indicator
         Container(
-          padding: const EdgeInsets.symmetric(
-            horizontal: 16,
-            vertical: 8,
-          ),
+          padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
           decoration: BoxDecoration(
             color: ZenColors.black.withValues(alpha: 0.3),
             borderRadius: BorderRadius.circular(20),
@@ -222,7 +225,7 @@ class _GameScreenState extends State<GameScreen> {
           ),
         ),
         const Spacer(),
-        
+
         // Pause Button
         IconButton(
           onPressed: _togglePause,
@@ -257,13 +260,10 @@ class _GameScreenState extends State<GameScreen> {
           ),
         ),
         const SizedBox(width: 16),
-        
+
         // Mode Indicator (smaller in landscape)
         Container(
-          padding: const EdgeInsets.symmetric(
-            horizontal: 12,
-            vertical: 6,
-          ),
+          padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
           decoration: BoxDecoration(
             color: ZenColors.black.withValues(alpha: 0.3),
             borderRadius: BorderRadius.circular(16),
@@ -281,7 +281,7 @@ class _GameScreenState extends State<GameScreen> {
           ),
         ),
         const Spacer(),
-        
+
         // Pause Button
         IconButton(
           onPressed: _togglePause,
@@ -330,7 +330,7 @@ class _GameScreenState extends State<GameScreen> {
                 ),
               ),
               const SizedBox(height: 30),
-              
+
               // Resume Button
               ElevatedButton(
                 onPressed: _togglePause,
@@ -347,10 +347,7 @@ class _GameScreenState extends State<GameScreen> {
                 ),
                 child: Text(
                   AppLocalizations.of(context)!.resume,
-                  style: TextStyle(
-                    fontSize: 18,
-                    fontWeight: FontWeight.w600,
-                  ),
+                  style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
                 ),
               ),
             ],
@@ -364,7 +361,7 @@ class _GameScreenState extends State<GameScreen> {
     if (SettingsManager.isHapticsEnabled) {
       await HapticUtils.vibrate(duration: 50);
     }
-    
+
     setState(() {
       _isPaused = !_isPaused;
       if (_isPaused) {
@@ -377,13 +374,13 @@ class _GameScreenState extends State<GameScreen> {
 
   void _showExitDialog() async {
     if (!mounted) return;
-    
+
     if (SettingsManager.isHapticsEnabled) {
       await HapticUtils.vibrate(duration: 50);
     }
-    
+
     if (!mounted) return;
-    
+
     showDialog(
       context: context,
       builder: (BuildContext context) {
@@ -408,7 +405,9 @@ class _GameScreenState extends State<GameScreen> {
             ElevatedButton(
               onPressed: () {
                 Navigator.of(context).pop(); // Close dialog
-                Navigator.of(context).pop(true); // Return to main menu with result
+                Navigator.of(
+                  context,
+                ).pop(true); // Return to main menu with result
               },
               style: ElevatedButton.styleFrom(
                 backgroundColor: ZenColors.red,
@@ -421,4 +420,4 @@ class _GameScreenState extends State<GameScreen> {
       },
     );
   }
-}
+}

+ 41 - 56
lib/ui/main_menu.dart

@@ -6,6 +6,7 @@ import '../utils/haptic_utils.dart';
 import '../utils/settings_manager.dart';
 import '../utils/score_manager.dart';
 import '../utils/theme_notifier.dart';
+import '../game/audio/audio_manager.dart';
 import 'game_screen.dart';
 import 'settings_screen.dart';
 import 'stats_screen.dart';
@@ -28,6 +29,11 @@ class _MainMenuState extends State<MainMenu> {
     super.initState();
     _checkTutorial();
     _initializeAndLoadScore();
+
+    // Start menu music when entering main menu
+    WidgetsBinding.instance.addPostFrameCallback((_) {
+      AudioManager().playMenuMusic();
+    });
   }
 
   @override
@@ -73,12 +79,13 @@ class _MainMenuState extends State<MainMenu> {
                     SafeArea(
                       child: Padding(
                         padding: const EdgeInsets.all(20.0),
-                        child: orientation == Orientation.portrait
-                            ? _buildPortraitLayout()
-                            : _buildLandscapeLayout(),
+                        child:
+                            orientation == Orientation.portrait
+                                ? _buildPortraitLayout()
+                                : _buildLandscapeLayout(),
                       ),
                     ),
-                    
+
                     // Tutorial overlay
                     if (_showTutorial)
                       TutorialOverlay(
@@ -119,7 +126,7 @@ class _MainMenuState extends State<MainMenu> {
             ),
           ],
         ),
-        
+
         // Main content
         Expanded(
           child: Column(
@@ -156,7 +163,7 @@ class _MainMenuState extends State<MainMenu> {
             ],
           ),
         ),
-        
+
         // Right side - Menu buttons and settings
         Expanded(
           flex: 3,
@@ -180,15 +187,13 @@ class _MainMenuState extends State<MainMenu> {
                   ),
                 ],
               ),
-              
+
               // Menu buttons
               Expanded(
                 child: SingleChildScrollView(
                   child: Column(
                     mainAxisAlignment: MainAxisAlignment.center,
-                    children: [
-                      _buildResponsiveMenuButtons(),
-                    ],
+                    children: [_buildResponsiveMenuButtons()],
                   ),
                 ),
               ),
@@ -213,7 +218,7 @@ class _MainMenuState extends State<MainMenu> {
           ),
         ),
         const SizedBox(height: 10),
-        
+
         // Subtitle
         Text(
           AppLocalizations.of(context)!.appSubtitle,
@@ -229,10 +234,7 @@ class _MainMenuState extends State<MainMenu> {
 
   Widget _buildTodayScore() {
     return Container(
-      padding: const EdgeInsets.symmetric(
-        horizontal: 20,
-        vertical: 12,
-      ),
+      padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
       decoration: BoxDecoration(
         color: ZenColors.black.withValues(alpha: 0.3),
         borderRadius: BorderRadius.circular(15),
@@ -265,7 +267,7 @@ class _MainMenuState extends State<MainMenu> {
           () => _navigateToGame(context, false),
         ),
         const SizedBox(height: 20),
-        
+
         // Zen Mode Button
         _buildMenuButton(
           context,
@@ -275,7 +277,7 @@ class _MainMenuState extends State<MainMenu> {
           () => _navigateToGame(context, true),
         ),
         const SizedBox(height: 20),
-        
+
         // Stats Button
         _buildMenuButton(
           context,
@@ -285,7 +287,7 @@ class _MainMenuState extends State<MainMenu> {
           _openStats,
         ),
         const SizedBox(height: 20),
-        
+
         // Exit Button
         _buildExitButton(
           context,
@@ -301,10 +303,7 @@ class _MainMenuState extends State<MainMenu> {
   Widget _buildSettingsHint() {
     return Text(
       AppLocalizations.of(context)!.tapToFeelCalm,
-      style: TextStyle(
-        color: ZenColors.currentMutedText,
-        fontSize: 14,
-      ),
+      style: TextStyle(color: ZenColors.currentMutedText, fontSize: 14),
     );
   }
 
@@ -313,7 +312,7 @@ class _MainMenuState extends State<MainMenu> {
       builder: (context, orientation) {
         final buttonSpacing = orientation == Orientation.portrait ? 20.0 : 12.0;
         final l10n = AppLocalizations.of(context)!;
-        
+
         return Column(
           children: [
             // Play Button
@@ -325,7 +324,7 @@ class _MainMenuState extends State<MainMenu> {
               () => _navigateToGame(context, false),
             ),
             SizedBox(height: buttonSpacing),
-            
+
             // Zen Mode Button
             _buildMenuButton(
               context,
@@ -335,7 +334,7 @@ class _MainMenuState extends State<MainMenu> {
               () => _navigateToGame(context, true),
             ),
             SizedBox(height: buttonSpacing),
-            
+
             // Stats Button
             _buildMenuButton(
               context,
@@ -345,7 +344,7 @@ class _MainMenuState extends State<MainMenu> {
               _openStats,
             ),
             SizedBox(height: buttonSpacing),
-            
+
             // Exit Button
             _buildExitButton(
               context,
@@ -383,11 +382,7 @@ class _MainMenuState extends State<MainMenu> {
         ),
         child: Row(
           children: [
-            Icon(
-              icon,
-              size: 32,
-              color: ZenColors.currentButtonText,
-            ),
+            Icon(icon, size: 32, color: ZenColors.currentButtonText),
             const SizedBox(width: 20),
             Expanded(
               child: Column(
@@ -446,11 +441,7 @@ class _MainMenuState extends State<MainMenu> {
         ),
         child: Row(
           children: [
-            Icon(
-              icon,
-              size: 32,
-              color: ZenColors.white,
-            ),
+            Icon(icon, size: 32, color: ZenColors.white),
             const SizedBox(width: 20),
             Expanded(
               child: Column(
@@ -490,7 +481,7 @@ class _MainMenuState extends State<MainMenu> {
     if (SettingsManager.isHapticsEnabled) {
       await HapticUtils.vibrate(duration: 50);
     }
-    
+
     // Close the app
     SystemNavigator.pop();
   }
@@ -499,27 +490,23 @@ class _MainMenuState extends State<MainMenu> {
     if (SettingsManager.isHapticsEnabled) {
       await HapticUtils.vibrate(duration: 50);
     }
-    
+
     if (!mounted) return;
-    Navigator.of(context).push(
-      MaterialPageRoute(
-        builder: (context) => const SettingsScreen(),
-      ),
-    );
+    Navigator.of(
+      context,
+    ).push(MaterialPageRoute(builder: (context) => const SettingsScreen()));
   }
 
   void _openStats() async {
     if (SettingsManager.isHapticsEnabled) {
       await HapticUtils.vibrate(duration: 50);
     }
-    
+
     if (!mounted) return;
-    final result = await Navigator.of(context).push(
-      MaterialPageRoute(
-        builder: (context) => const StatsScreen(),
-      ),
-    );
-    
+    final result = await Navigator.of(
+      context,
+    ).push(MaterialPageRoute(builder: (context) => const StatsScreen()));
+
     // Refresh score when returning from stats
     if (result == true) {
       _loadTodayScore();
@@ -530,17 +517,15 @@ class _MainMenuState extends State<MainMenu> {
     if (SettingsManager.isHapticsEnabled) {
       await HapticUtils.vibrate(duration: 50);
     }
-    
+
     if (!mounted) return;
     final result = await Navigator.of(context).push(
-      MaterialPageRoute(
-        builder: (context) => GameScreen(isZenMode: isZenMode),
-      ),
+      MaterialPageRoute(builder: (context) => GameScreen(isZenMode: isZenMode)),
     );
-    
+
     // Refresh score when returning from game
     if (mounted && result == true) {
       _loadTodayScore();
     }
   }
-}
+}

+ 91 - 77
lib/ui/settings_screen.dart

@@ -10,6 +10,7 @@ import '../utils/theme_manager.dart';
 import '../utils/theme_notifier.dart';
 import 'components/animated_background.dart';
 import 'google_play_games_widget.dart';
+import '../game/audio/audio_manager.dart';
 
 class SettingsScreen extends StatefulWidget {
   const SettingsScreen({super.key});
@@ -825,91 +826,100 @@ class _SettingsScreenState extends State<SettingsScreen> {
           fontWeight: FontWeight.bold,
         ),
       ),
-      content: Column(
-        mainAxisSize: MainAxisSize.min,
-        children:
-            SeasonalTheme.values
-                .where((theme) {
-                  // Hide default theme in release builds
-                  if (!kDebugMode && theme == SeasonalTheme.default_) {
-                    return false;
-                  }
-                  return true;
-                })
-                .map((theme) {
-                  final isSelected = theme == currentTheme;
-                  final themeName = _getLocalizedThemeName(theme);
-                  final themeIcon = ThemeManager.getThemeIcon(theme);
-
-                  return Container(
-                    margin: const EdgeInsets.only(bottom: 8),
-                    child: Material(
-                      color:
-                          isSelected
-                              ? ZenColors.currentButtonBackground.withValues(
-                                alpha: 0.1,
-                              )
-                              : Colors.transparent,
-                      borderRadius: BorderRadius.circular(12),
-                      child: InkWell(
-                        onTap: () => _selectTheme(theme),
-                        borderRadius: BorderRadius.circular(12),
-                        child: Container(
-                          padding: const EdgeInsets.all(12),
-                          decoration: BoxDecoration(
+      content: SizedBox(
+        width: double.maxFinite,
+        child: Column(
+          mainAxisSize: MainAxisSize.min,
+          children: [
+            // Use a constraint container with max height for scrolling
+            ConstrainedBox(
+              constraints: BoxConstraints(
+                maxHeight: MediaQuery.of(context).size.height * 0.5,
+              ),
+              child: SingleChildScrollView(
+                child: Column(
+                  mainAxisSize: MainAxisSize.min,
+                  children:
+                      ThemeManager.availableThemes.map((theme) {
+                        final isSelected = theme == currentTheme;
+                        final themeName = _getLocalizedThemeName(theme);
+                        final themeIcon = ThemeManager.getThemeIcon(theme);
+
+                        return Container(
+                          margin: const EdgeInsets.only(bottom: 8),
+                          child: Material(
+                            color:
+                                isSelected
+                                    ? ZenColors.currentButtonBackground
+                                        .withValues(alpha: 0.1)
+                                    : Colors.transparent,
                             borderRadius: BorderRadius.circular(12),
-                            border: Border.all(
-                              color:
-                                  isSelected
-                                      ? ZenColors.currentButtonBackground
-                                          .withValues(alpha: 0.3)
-                                      : ZenColors.currentUiElements.withValues(
-                                        alpha: 0.2,
-                                      ),
-                              width: 1,
-                            ),
-                          ),
-                          child: Row(
-                            children: [
-                              Icon(
-                                themeIcon,
-                                color:
-                                    isSelected
-                                        ? ZenColors.currentButtonBackground
-                                        : ZenColors.currentPrimaryText,
-                                size: 20,
-                              ),
-                              const SizedBox(width: 12),
-                              Expanded(
-                                child: Text(
-                                  themeName,
-                                  style: TextStyle(
+                            child: InkWell(
+                              onTap: () => _selectTheme(theme),
+                              borderRadius: BorderRadius.circular(12),
+                              child: Container(
+                                padding: const EdgeInsets.all(12),
+                                decoration: BoxDecoration(
+                                  borderRadius: BorderRadius.circular(12),
+                                  border: Border.all(
                                     color:
                                         isSelected
                                             ? ZenColors.currentButtonBackground
-                                            : ZenColors.currentPrimaryText,
-                                    fontSize: 16,
-                                    fontWeight:
-                                        isSelected
-                                            ? FontWeight.w600
-                                            : FontWeight.normal,
+                                                .withValues(alpha: 0.3)
+                                            : ZenColors.currentUiElements
+                                                .withValues(alpha: 0.2),
+                                    width: 1,
                                   ),
                                 ),
-                              ),
-                              if (isSelected)
-                                Icon(
-                                  Icons.check,
-                                  color: ZenColors.currentButtonBackground,
-                                  size: 20,
+                                child: Row(
+                                  children: [
+                                    Icon(
+                                      themeIcon,
+                                      color:
+                                          isSelected
+                                              ? ZenColors
+                                                  .currentButtonBackground
+                                              : ZenColors.currentPrimaryText,
+                                      size: 20,
+                                    ),
+                                    const SizedBox(width: 12),
+                                    Expanded(
+                                      child: Text(
+                                        themeName,
+                                        style: TextStyle(
+                                          color:
+                                              isSelected
+                                                  ? ZenColors
+                                                      .currentButtonBackground
+                                                  : ZenColors
+                                                      .currentPrimaryText,
+                                          fontSize: 16,
+                                          fontWeight:
+                                              isSelected
+                                                  ? FontWeight.w600
+                                                  : FontWeight.normal,
+                                        ),
+                                      ),
+                                    ),
+                                    if (isSelected)
+                                      Icon(
+                                        Icons.check,
+                                        color:
+                                            ZenColors.currentButtonBackground,
+                                        size: 20,
+                                      ),
+                                  ],
                                 ),
-                            ],
+                              ),
+                            ),
                           ),
-                        ),
-                      ),
-                    ),
-                  );
-                })
-                .toList(),
+                        );
+                      }).toList(),
+                ),
+              ),
+            ),
+          ],
+        ),
       ),
       actions: [
         TextButton(
@@ -1134,6 +1144,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
     }
 
     await SettingsManager.setSelectedTheme(theme);
+
+    // Switch menu music to match new theme
+    AudioManager().playMenuMusic();
+
     if (mounted) {
       Navigator.of(context).pop(); // Close theme dialog
       setState(() {}); // Refresh the settings screen to show new theme

+ 6 - 0
lib/ui/stats_screen.dart

@@ -4,6 +4,7 @@ import '../l10n/app_localizations.dart';
 import '../utils/colors.dart';
 import '../utils/score_manager.dart';
 import '../utils/theme_notifier.dart';
+import '../game/audio/audio_manager.dart';
 
 class StatsScreen extends StatefulWidget {
   const StatsScreen({super.key});
@@ -22,6 +23,11 @@ class _StatsScreenState extends State<StatsScreen>
     super.initState();
     _tabController = TabController(length: 2, vsync: this);
     _loadData();
+
+    // Start menu music when entering stats screen
+    WidgetsBinding.instance.addPostFrameCallback((_) {
+      AudioManager().playMenuMusic();
+    });
   }
 
   @override

+ 12 - 0
lib/utils/theme_manager.dart

@@ -1,3 +1,4 @@
+import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
 import 'package:shared_preferences/shared_preferences.dart';
 import 'theme_notifier.dart';
@@ -193,6 +194,17 @@ class ThemeManager {
     }
   }
 
+  /// Get available themes for selection (excluding default in release mode)
+  static List<SeasonalTheme> get availableThemes {
+    return SeasonalTheme.values.where((theme) {
+      // Hide default theme in release builds
+      if (!kDebugMode && theme == SeasonalTheme.default_) {
+        return false;
+      }
+      return true;
+    }).toList();
+  }
+
   /// Default fSociety theme (current colors)
   static const SeasonalColors _defaultColors = SeasonalColors(
     appBackground: Color(0xFF000000),

+ 2 - 0
pubspec.yaml

@@ -111,6 +111,8 @@ flutter:
     - assets/images/
     - assets/audio/
     - assets/audio/source/
+    - assets/audio/menu/
+    - assets/audio/ingame/
 
   # An image asset can refer to one or more resolution-specific "variants", see
   # https://flutter.dev/to/resolution-aware-images

+ 27 - 7
scripts/README.md

@@ -28,16 +28,36 @@ Builds APK and AAB files for Android in debug and release modes.
 ### How it works
 1. Looks for source audio files in `assets/audio/source/`
 2. Converts them according to specifications in [`assets/audio/README.md`](../assets/audio/README.md):
-   - **Sound Effects**: WAV format, 44.1 kHz, 16-bit, mono, 0.1-0.5 seconds
-   - **Background Music**: MP3 format, 44.1 kHz, 192 kbps, stereo, 2-5 minutes
-3. Outputs converted files to `assets/audio/`
+   - **Sound Effects**: MP3 format, 44.1 kHz, 128 kbps, mono, 0.1-0.5 seconds
+   - **Background Music**: MP3 format, 44.1 kHz, 192 kbps, stereo, 2-8 minutes
+3. Outputs converted files to appropriate directories:
+   - Sound effects: `assets/audio/source/`
+   - Menu music: `assets/audio/menu/`
+   - Ingame music: `assets/audio/ingame/`
+   - Compressed versions: `assets/audio/compressed/`
 4. Validates file durations and reports status
 
 ### Required Files
-- `bubble_pop.wav` - Primary bubble pop sound effect
-- `bubble_pop_alt.wav` - Alternative bubble pop sound effect
-- `ambient_background.mp3` - Main gameplay background music
-- `zen_mode_background.mp3` - Zen mode background music
+
+#### Sound Effects (source/)
+- `bubble_pop.mp3` - Primary bubble pop sound effect
+- `bubble_pop_alt.mp3` - Alternative bubble pop sound effect
+- `ambient_background.mp3` - Legacy background music
+
+#### Theme-Based Menu Music (menu/)
+- `menu_default.mp3` - Default theme menu music
+- `menu_spring.mp3` - Spring theme menu music
+- `menu_summer.mp3` - Summer theme menu music
+- `menu_autumn.mp3` - Autumn theme menu music
+- `menu_winter.mp3` - Winter theme menu music
+
+#### Theme-Based Ingame Music (ingame/)
+- `ingame_default_001.mp3` - Default theme gameplay music
+- `ingame_spring_001.mp3` - Spring theme gameplay music
+- `ingame_summer_001.mp3` - Summer theme gameplay music
+- `ingame_autumn_001.mp3` - Autumn theme gameplay music
+- `ingame_winter_001.mp3` - Winter theme gameplay music (track 1)
+- `ingame_winter_002.mp3` - Winter theme gameplay music (track 2)
 
 ### Supported Source Formats
 WAV, MP3, OGG, FLAC, M4A

+ 84 - 25
scripts/convert_audio.sh

@@ -15,6 +15,9 @@ NC='\033[0m' # No Color
 # Directories
 AUDIO_DIR="assets/audio"
 SOURCE_DIR="assets/audio/source"
+MENU_DIR="assets/audio/menu"
+INGAME_DIR="assets/audio/ingame"
+COMPRESSED_DIR="assets/audio/compressed"
 OUTPUT_DIR="assets/audio"
 
 echo -e "${BLUE}ZenTap Audio Conversion Script${NC}"
@@ -32,14 +35,18 @@ fi
 
 # Create directories if they don't exist
 mkdir -p "$SOURCE_DIR"
+mkdir -p "$MENU_DIR"
+mkdir -p "$INGAME_DIR"
+mkdir -p "$COMPRESSED_DIR"
 mkdir -p "$OUTPUT_DIR"
 
 echo -e "${YELLOW}Audio Conversion Specifications:${NC}"
-echo "- Sound Effects: WAV format, 44.1 kHz, 16-bit, 0.1-0.5 seconds"
-echo "- Background Music: MP3 format, 44.1 kHz, 128-320 kbps, 2-5 minutes"
+echo "- Sound Effects: MP3 format, 44.1 kHz, 128 kbps, mono, 0.1-0.5 seconds"
+echo "- Menu Music: MP3 format, 44.1 kHz, 192 kbps, stereo, 2-5 minutes"
+echo "- Ingame Music: MP3 format, 44.1 kHz, 192 kbps, stereo, 3-8 minutes"
 echo ""
 
-# Function to convert sound effects to WAV
+# Function to convert sound effects to MP3
 convert_sound_effect() {
     local input_file="$1"
     local output_file="$2"
@@ -48,7 +55,7 @@ convert_sound_effect() {
     
     ffmpeg -i "$input_file" \
         -ar 44100 \
-        -sample_fmt s16 \
+        -b:a 128k \
         -ac 1 \
         -t 0.5 \
         -y \
@@ -107,16 +114,22 @@ echo -e "${YELLOW}Looking for source files in $SOURCE_DIR${NC}"
 echo ""
 
 # Convert sound effects
-for sound_effect in "bubble_pop" "bubble_pop_alt"; do
+for sound_effect in "bubble_pop" "bubble_pop_alt" "ambient_background"; do
     # Look for various input formats
     for ext in wav mp3 ogg flac m4a; do
         source_file="$SOURCE_DIR/${sound_effect}.$ext"
         if [ -f "$source_file" ]; then
-            output_file="$OUTPUT_DIR/${sound_effect}.wav"
-            convert_sound_effect "$source_file" "$output_file"
-            
-            if [ -f "$output_file" ]; then
-                validate_duration "$output_file" 0 1
+            output_file="$SOURCE_DIR/${sound_effect}.mp3"
+            if [[ "$sound_effect" == "ambient_background" ]]; then
+                convert_background_music "$source_file" "$output_file"
+                if [ -f "$output_file" ]; then
+                    validate_duration "$output_file" 120 300  # 2-5 minutes
+                fi
+            else
+                convert_sound_effect "$source_file" "$output_file"
+                if [ -f "$output_file" ]; then
+                    validate_duration "$output_file" 0 1
+                fi
             fi
             echo ""
             break
@@ -124,13 +137,13 @@ for sound_effect in "bubble_pop" "bubble_pop_alt"; do
     done
 done
 
-# Convert background music
-for music_track in "ambient_background" "zen_mode_background"; do
-    # Look for various input formats
+# Convert menu music files
+echo -e "${YELLOW}Converting menu music files...${NC}"
+for theme in "default" "spring" "summer" "autumn" "winter"; do
     for ext in wav mp3 ogg flac m4a; do
-        source_file="$SOURCE_DIR/${music_track}.$ext"
+        source_file="$SOURCE_DIR/menu_${theme}.$ext"
         if [ -f "$source_file" ]; then
-            output_file="$OUTPUT_DIR/${music_track}.mp3"
+            output_file="$MENU_DIR/menu_${theme}.mp3"
             convert_background_music "$source_file" "$output_file"
             
             if [ -f "$output_file" ]; then
@@ -142,19 +155,60 @@ for music_track in "ambient_background" "zen_mode_background"; do
     done
 done
 
+# Convert ingame music files
+echo -e "${YELLOW}Converting ingame music files...${NC}"
+for theme in "default" "spring" "summer" "autumn" "winter"; do
+    for track_num in "001" "002" "003"; do
+        for ext in wav mp3 ogg flac m4a; do
+            source_file="$SOURCE_DIR/ingame_${theme}_${track_num}.$ext"
+            if [ -f "$source_file" ]; then
+                output_file="$INGAME_DIR/ingame_${theme}_${track_num}.mp3"
+                convert_background_music "$source_file" "$output_file"
+                
+                if [ -f "$output_file" ]; then
+                    validate_duration "$output_file" 180 480  # 3-8 minutes
+                fi
+                echo ""
+                break
+            fi
+        done
+    done
+done
+
 # Check if we have any missing files
 echo -e "${YELLOW}Checking for required audio files:${NC}"
 
-required_files=(
-    "bubble_pop.wav"
-    "bubble_pop_alt.wav"
-    "ambient_background.mp3"
-    "zen_mode_background.mp3"
+echo -e "${BLUE}Sound Effects (source/):${NC}"
+sound_effect_files=(
+    "source/bubble_pop.mp3"
+    "source/bubble_pop_alt.mp3"
+    "source/ambient_background.mp3"
 )
 
+echo -e "${BLUE}Menu Music (menu/):${NC}"
+menu_music_files=(
+    "menu/menu_default.mp3"
+    "menu/menu_spring.mp3"
+    "menu/menu_summer.mp3"
+    "menu/menu_autumn.mp3"
+    "menu/menu_winter.mp3"
+)
+
+echo -e "${BLUE}Ingame Music (ingame/):${NC}"
+ingame_music_files=(
+    "ingame/ingame_default_001.mp3"
+    "ingame/ingame_spring_001.mp3"
+    "ingame/ingame_summer_001.mp3"
+    "ingame/ingame_autumn_001.mp3"
+    "ingame/ingame_winter_001.mp3"
+    "ingame/ingame_winter_002.mp3"
+)
+
+all_files=("${sound_effect_files[@]}" "${menu_music_files[@]}" "${ingame_music_files[@]}")
 missing_files=()
-for file in "${required_files[@]}"; do
-    if [ -f "$OUTPUT_DIR/$file" ]; then
+
+for file in "${all_files[@]}"; do
+    if [ -f "$AUDIO_DIR/$file" ]; then
         echo -e "${GREEN}✓ $file${NC}"
     else
         echo -e "${RED}✗ $file (missing)${NC}"
@@ -165,14 +219,19 @@ done
 echo ""
 
 if [ ${#missing_files[@]} -eq 0 ]; then
-    echo -e "${GREEN}All required audio files are present!${NC}"
+    echo -e "${GREEN}All audio files are present!${NC}"
 else
-    echo -e "${YELLOW}Missing files: ${missing_files[*]}${NC}"
+    echo -e "${YELLOW}Missing files: ${#missing_files[@]} out of ${#all_files[@]}${NC}"
     echo ""
     echo -e "${BLUE}To add missing files:${NC}"
-    echo "1. Place source audio files in $SOURCE_DIR/"
+    echo "1. Place source audio files in $SOURCE_DIR/ with appropriate names"
     echo "2. Run this script again"
     echo ""
+    echo -e "${BLUE}Expected source file naming:${NC}"
+    echo "- Sound effects: bubble_pop.*, bubble_pop_alt.*, ambient_background.*"
+    echo "- Menu music: menu_default.*, menu_spring.*, menu_summer.*, menu_autumn.*, menu_winter.*"
+    echo "- Ingame music: ingame_<theme>_<track>.* (e.g., ingame_winter_001.mp3)"
+    echo ""
     echo -e "${BLUE}Supported source formats:${NC} WAV, MP3, OGG, FLAC, M4A"
 fi