diff --git a/__init__.py b/__init__.py index 99a3bf8..4759845 100755 --- a/__init__.py +++ b/__init__.py @@ -15,7 +15,8 @@ This module provides core functionality for Storm Games including: from .services import ( ConfigService, VolumeService, - PathService + PathService, + SpeechHistoryService ) # Import Sound class and functions @@ -39,7 +40,7 @@ from .sound import ( ) # Import Speech class and functions -from .speech import messagebox, speak, Speech +from .speech import messagebox, speak, Speech, speak_previous, speak_current, speak_next # Import Scoreboard from .scoreboard import Scoreboard @@ -78,7 +79,7 @@ __version__ = '2.0.0' # Make all symbols available at the package level __all__ = [ # Services - 'ConfigService', 'VolumeService', 'PathService', + 'ConfigService', 'VolumeService', 'PathService', 'SpeechHistoryService', # Sound 'Sound', @@ -102,6 +103,9 @@ __all__ = [ 'messagebox', 'speak', 'Speech', + 'speak_previous', + 'speak_current', + 'speak_next', # Scoreboard 'Scoreboard', @@ -133,6 +137,7 @@ __all__ = [ configService = ConfigService.get_instance() volumeService = VolumeService.get_instance() pathService = PathService.get_instance() +speechHistoryService = SpeechHistoryService.get_instance() # Set up backward compatibility hooks for initialize_gui _originalInitializeGui = initialize_gui diff --git a/services.py b/services.py index dd2fd7a..a9b32e4 100644 --- a/services.py +++ b/services.py @@ -6,10 +6,13 @@ Provides centralized services to replace global variables: - ConfigService: Manages game configuration - VolumeService: Handles volume settings - PathService: Manages file paths +- SpeechHistoryService: Manages speech history for navigation """ import configparser import os +import time +from collections import deque from xdg import BaseDirectory # For backward compatibility @@ -272,3 +275,134 @@ class PathService: globalPath = self.globalPath return self + + +class SpeechHistoryService: + """Speech history management service for message navigation.""" + + _instance = None + + @classmethod + def get_instance(cls): + """Get or create the singleton instance.""" + if cls._instance is None: + cls._instance = SpeechHistoryService() + return cls._instance + + def __init__(self, max_size=10): + """Initialize speech history. + + Args: + max_size (int): Maximum number of messages to keep in history + """ + self.history = deque(maxlen=max_size) + self.current_index = -1 + self.max_size = max_size + + def add_message(self, text): + """Add a message to the speech history. + + Args: + text (str): The spoken message to add to history + """ + if not text or not text.strip(): + return + + # Add message with timestamp + message_entry = { + 'text': text.strip(), + 'timestamp': time.time() + } + + self.history.append(message_entry) + # Reset current index when new message is added + self.current_index = -1 + + def get_current(self): + """Get the current message in history. + + Returns: + str or None: Current message text, or None if no history + """ + if not self.history: + return None + + if self.current_index == -1: + # Return most recent message + return self.history[-1]['text'] + else: + # Return message at current index + if 0 <= self.current_index < len(self.history): + return self.history[self.current_index]['text'] + + return None + + def move_previous(self): + """Navigate to the previous message in history. + + Returns: + str or None: Previous message text, or None if at beginning + """ + if not self.history: + return None + + if self.current_index == -1: + # Start from the most recent message + self.current_index = len(self.history) - 1 + elif self.current_index > 0: + # Move backward in history + self.current_index -= 1 + else: + # Already at the beginning, wrap to the end + self.current_index = len(self.history) - 1 + + return self.get_current() + + def move_next(self): + """Navigate to the next message in history. + + Returns: + str or None: Next message text, or None if at end + """ + if not self.history: + return None + + if self.current_index == -1: + # Already at most recent, wrap to beginning + self.current_index = 0 + elif self.current_index < len(self.history) - 1: + # Move forward in history + self.current_index += 1 + else: + # At the end, wrap to beginning + self.current_index = 0 + + return self.get_current() + + def clear_history(self): + """Clear all messages from history.""" + self.history.clear() + self.current_index = -1 + + def set_max_size(self, max_size): + """Change the maximum history size. + + Args: + max_size (int): New maximum size for history buffer + """ + if max_size > 0: + self.max_size = max_size + # Create new deque with new max size, preserving recent messages + old_history = list(self.history) + self.history = deque(old_history[-max_size:], maxlen=max_size) + # Adjust current index if needed + if self.current_index >= len(self.history): + self.current_index = len(self.history) - 1 + + def get_history_size(self): + """Get the current number of messages in history. + + Returns: + int: Number of messages in history + """ + return len(self.history) diff --git a/speech.py b/speech.py index 7bc6b4f..5aac126 100644 --- a/speech.py +++ b/speech.py @@ -61,12 +61,13 @@ class Speech: # No speech providers found print("No speech providers found.") - def speak(self, text, interrupt=True): + def speak(self, text, interrupt=True, priority="normal"): """Speak text using the configured speech provider and display on screen. Args: text (str): Text to speak and display interrupt (bool): Whether to interrupt current speech (default: True) + priority (str): Speech priority - "important", "normal", or "notification" """ if not self.provider: return @@ -74,7 +75,7 @@ class Speech: currentTime = pygame.time.get_ticks() # Check if this is the same text within the delay window - if (self.lastSpoken["text"] == text and + if (self.lastSpoken["text"] == text and currentTime - self.lastSpoken["time"] < self.speechDelay): return @@ -82,13 +83,45 @@ class Speech: self.lastSpoken["text"] = text self.lastSpoken["time"] = currentTime - # Proceed with speech + # Add to speech history (import here to avoid circular imports) + try: + from .services import SpeechHistoryService + history_service = SpeechHistoryService.get_instance() + history_service.add_message(text) + except ImportError: + pass + + # Proceed with speech based on provider and priority if self.providerName == "speechd": if interrupt: self.spd.cancel() + + # Set priority for speechd + if priority == "important": + try: + import speechd + self.spd.set_priority(speechd.Priority.IMPORTANT) + except (ImportError, AttributeError): + pass + elif priority == "notification": + try: + import speechd + self.spd.set_priority(speechd.Priority.NOTIFICATION) + except (ImportError, AttributeError): + pass + else: # normal + try: + import speechd + self.spd.set_priority(speechd.Priority.TEXT) + except (ImportError, AttributeError): + pass + self.spd.say(text) + elif self.providerName == "accessible_output2": - self.ao2.speak(text, interrupt=interrupt) + # For accessible_output2, use interrupt for important messages + use_interrupt = interrupt or (priority == "important") + self.ao2.speak(text, interrupt=use_interrupt) # Display the text on screen screen = pygame.display.get_surface() @@ -121,17 +154,18 @@ class Speech: # Global instance for backward compatibility _speechInstance = None -def speak(text, interrupt=True): +def speak(text, interrupt=True, priority="normal"): """Speak text using the global speech instance. Args: text (str): Text to speak and display interrupt (bool): Whether to interrupt current speech (default: True) + priority (str): Speech priority - "important", "normal", or "notification" """ global _speechInstance if _speechInstance is None: _speechInstance = Speech.get_instance() - _speechInstance.speak(text, interrupt) + _speechInstance.speak(text, interrupt, priority) def messagebox(text, sounds=None): """Enhanced messagebox with dialog support. @@ -313,3 +347,45 @@ def _play_dialog_sound(entry, dialog_config, sounds): except Exception: # Sound missing or error - continue silently without crashing pass + + +def speak_previous(): + """Navigate to and speak the previous message in speech history.""" + try: + from .services import SpeechHistoryService + history_service = SpeechHistoryService.get_instance() + message = history_service.move_previous() + if message: + speak(message, interrupt=True, priority="important") + else: + speak("No previous messages", interrupt=True, priority="important") + except ImportError: + speak("Speech history not available", interrupt=True, priority="important") + + +def speak_current(): + """Repeat the current message in speech history.""" + try: + from .services import SpeechHistoryService + history_service = SpeechHistoryService.get_instance() + message = history_service.get_current() + if message: + speak(message, interrupt=True, priority="important") + else: + speak("No current message", interrupt=True, priority="important") + except ImportError: + speak("Speech history not available", interrupt=True, priority="important") + + +def speak_next(): + """Navigate to and speak the next message in speech history.""" + try: + from .services import SpeechHistoryService + history_service = SpeechHistoryService.get_instance() + message = history_service.move_next() + if message: + speak(message, interrupt=True, priority="important") + else: + speak("No next messages", interrupt=True, priority="important") + except ImportError: + speak("Speech history not available", interrupt=True, priority="important") diff --git a/utils.py b/utils.py index 9b06611..dde5c78 100644 --- a/utils.py +++ b/utils.py @@ -156,19 +156,76 @@ class Game: # No logo image found, just play audio self.sound.cut_scene(audioKey) - def speak(self, text, interrupt=True): + def speak(self, text, interrupt=True, priority="normal"): """Speak text using the speech system. Args: text (str): Text to speak interrupt (bool): Whether to interrupt current speech + priority (str): Speech priority - "important", "normal", or "notification" Returns: Game: Self for method chaining """ - self.speech.speak(text, interrupt) + self.speech.speak(text, interrupt, priority) return self + def speak_previous(self): + """Navigate to and speak the previous message in speech history. + + Returns: + Game: Self for method chaining + """ + from .speech import speak_previous + speak_previous() + return self + + def speak_current(self): + """Repeat the current message in speech history. + + Returns: + Game: Self for method chaining + """ + from .speech import speak_current + speak_current() + return self + + def speak_next(self): + """Navigate to and speak the next message in speech history. + + Returns: + Game: Self for method chaining + """ + from .speech import speak_next + speak_next() + return self + + def setup_speech_history_keys(self, previous_key=None, current_key=None, next_key=None): + """Set up convenient key bindings for speech history navigation. + + Args: + previous_key (int, optional): Key code for previous message (default: F1) + current_key (int, optional): Key code for current message (default: F2) + next_key (int, optional): Key code for next message (default: F3) + + Returns: + dict: Dictionary mapping key codes to speech history functions + """ + # Set default keys if not provided + if previous_key is None: + previous_key = pygame.K_F1 + if current_key is None: + current_key = pygame.K_F2 + if next_key is None: + next_key = pygame.K_F3 + + # Return a dictionary that games can use in their event loops + return { + previous_key: self.speak_previous, + current_key: self.speak_current, + next_key: self.speak_next + } + def play_bgm(self, musicFile): """Play background music.