diff --git a/sound.py b/sound.py index 9de6be4..7fb130e 100644 --- a/sound.py +++ b/sound.py @@ -120,24 +120,25 @@ class Sound: pygame.event.clear() pygame.mixer.stop() - # Play the sound - channel = self.sounds[soundName].play(-1 if loop else 0) - if not channel: - return None - # Apply appropriate volume settings sfx_volume = self.volumeService.get_sfx_volume() - # Handle positional audio if positions are provided + # Handle positional audio if positions are provided - check range BEFORE starting sound if playerPos is not None and objPos is not None: # Calculate stereo panning left_vol, right_vol = self._get_stereo_panning(playerPos, objPos, centerDistance) # Don't play if out of range if left_vol == 0 and right_vol == 0: - channel.stop() return None + # Play the sound + channel = self.sounds[soundName].play(-1 if loop else 0) + if not channel: + return None + + # Apply volume settings + if playerPos is not None and objPos is not None: # Apply positional volume adjustments channel.set_volume(volume * left_vol * sfx_volume, volume * right_vol * sfx_volume) else: @@ -351,6 +352,10 @@ def _find_matching_sound(soundPattern, sounds): keys = [k for k in sounds.keys() if re.match("^" + soundPattern + ".*", k)] return random.choice(keys) if keys else None +def get_available_channel(): + """Get an available channel for playing sounds.""" + return pygame.mixer.find_channel() + # Global functions for backward compatibility def play_bgm(musicFile): """Play background music with proper volume settings.""" @@ -425,20 +430,22 @@ def play_sound(sound_or_name, volume=1.0, loop=False, playerPos=None, objPos=Non # Case 4: Sound name with dictionary elif isinstance(sounds, dict) and isinstance(sound_or_name, str) and sound_or_name in sounds: + # Apply volume settings + sfx_vol = volumeService.get_sfx_volume() + + # Handle positional audio - check range BEFORE starting sound + if playerPos is not None and objPos is not None: + left_vol, right_vol = _get_stereo_panning(playerPos, objPos, centerDistance) + if left_vol == 0 and right_vol == 0: + return None # Don't start sound if out of range + # Play the sound channel = sounds[sound_or_name].play(-1 if loop else 0) if not channel: return None # Apply volume settings - sfx_vol = volumeService.get_sfx_volume() - - # Handle positional audio if playerPos is not None and objPos is not None: - left_vol, right_vol = _get_stereo_panning(playerPos, objPos, centerDistance) - if left_vol == 0 and right_vol == 0: - channel.stop() - return None channel.set_volume(volume * left_vol * sfx_vol, volume * right_vol * sfx_vol) else: channel.set_volume(volume * sfx_vol) diff --git a/speech.py b/speech.py index 4da3f28..8c44d6e 100644 --- a/speech.py +++ b/speech.py @@ -130,15 +130,31 @@ def speak(text, interrupt=True): _speechInstance = Speech.get_instance() _speechInstance.speak(text, interrupt) -def messagebox(text): - """Display a simple message box with text. - - Shows a message that can be repeated until the user chooses to continue. +def messagebox(text, sounds=None): + """Enhanced messagebox with dialog support. Args: - text (str): Message to display + text (str or dict): Simple string message or dialog configuration dict + sounds (Sound object, optional): Sound system for playing dialog audio """ speech = Speech.get_instance() + + # Handle simple string (backward compatibility) + if isinstance(text, str): + _show_simple_message(speech, text) + return + + # Handle dialog format + if isinstance(text, dict) and "entries" in text: + _show_dialog_sequence(speech, text, sounds) + return + + # Fallback to simple message if format not recognized + _show_simple_message(speech, str(text)) + + +def _show_simple_message(speech, text): + """Show a simple text message (original messagebox behavior).""" speech.speak(text + "\nPress any key to repeat or enter to continue.") while True: event = pygame.event.wait() @@ -147,3 +163,148 @@ def messagebox(text): speech.speak(" ") return speech.speak(text + "\nPress any key to repeat or enter to continue.") + + +def _show_dialog_sequence(speech, dialog_config, sounds): + """Show a dialog sequence with character speech and optional sounds. + + Args: + speech: Speech instance for text-to-speech + dialog_config (dict): Dialog configuration with entries list and optional settings + sounds: Sound system for playing audio files + """ + entries = dialog_config.get("entries", []) + allow_skip = dialog_config.get("allow_skip", False) + dialog_sound = dialog_config.get("sound", None) + + if not entries: + return + + entry_index = 0 + while entry_index < len(entries): + entry = entries[entry_index] + + # Play sound before showing dialog + _play_dialog_sound(entry, dialog_config, sounds) + + # Format and show the dialog text + formatted_text = _format_dialog_entry(entry) + if not formatted_text: + entry_index += 1 + continue + + # Show dialog with appropriate controls (only on first entry) + if entry_index == 0: + if allow_skip: + control_text = "\nPress any key to repeat, enter for next, or escape to skip all." + else: + control_text = "\nPress any key to repeat or enter for next." + else: + control_text = "" # No instructions after first entry + + speech.speak(formatted_text + control_text) + + # Handle user input + while True: + event = pygame.event.wait() + if event.type == pygame.KEYDOWN: + if event.key == pygame.K_ESCAPE: + if allow_skip: + speech.speak(" ") + return # Skip entire dialog sequence + else: + # Escape acts like enter if skip not allowed + speech.speak(" ") + entry_index += 1 + break + elif event.key == pygame.K_RETURN: + speech.speak(" ") + entry_index += 1 + break + else: + # Repeat current entry (include instructions only on first entry) + repeat_text = formatted_text + if entry_index == 0: + repeat_text += control_text + speech.speak(repeat_text) + + +def _format_dialog_entry(entry): + """Format a dialog entry for display. + + Args: + entry (dict): Dialog entry with text, optional speaker, and optional narrative flag + + Returns: + str: Formatted text for speech + """ + text = entry.get("text", "") + speaker = entry.get("speaker", None) + is_narrative = entry.get("narrative", False) + + if not text: + return "" + + if is_narrative: + # Narrative text - no speaker name + return text + elif speaker: + # Character dialog - include speaker name + return f"{speaker}: \"{text}\"" + else: + # Plain text - no special formatting + return text + + +def _play_dialog_sound(entry, dialog_config, sounds): + """Play appropriate sound for a dialog entry and wait for it to complete. + + Args: + entry (dict): Dialog entry that may have a sound + dialog_config (dict): Dialog configuration that may have a default sound + sounds: Sound system (either Sound class instance or dictionary of sounds) + """ + if not sounds: + return + + sound_to_play = None + + # Determine which sound to play (priority order) + if entry.get("sound"): + # Entry-specific sound (highest priority) + sound_to_play = entry["sound"] + elif dialog_config.get("sound"): + # Dialog-specific sound (medium priority) + sound_to_play = dialog_config["sound"] + else: + # Default dialogue.ogg (lowest priority) + sound_to_play = "dialogue" # Will look for dialogue.ogg + + if sound_to_play: + try: + # Handle both Sound class instances and sound dictionaries + if hasattr(sounds, 'sounds') and sound_to_play in sounds.sounds: + # Sound class instance (like from libstormgames Sound class) + sound_obj = sounds.sounds[sound_to_play] + from .sound import get_available_channel + channel = get_available_channel() + channel.play(sound_obj) + sound_duration = sound_obj.get_length() + if sound_duration > 0: + pygame.time.wait(int(sound_duration * 1000)) + elif isinstance(sounds, dict) and sound_to_play in sounds: + # Dictionary of pygame sound objects (like from initialize_gui) + sound_obj = sounds[sound_to_play] + from .sound import get_available_channel + channel = get_available_channel() + channel.play(sound_obj) + sound_duration = sound_obj.get_length() + if sound_duration > 0: + pygame.time.wait(int(sound_duration * 1000)) + elif hasattr(sounds, 'play'): + # Try using a play method if available + sounds.play(sound_to_play) + pygame.time.wait(500) # Default delay if can't get duration + except Exception: + # Sound missing or error - continue silently without crashing + pass