Compare commits
9 Commits
385e5a81ed
...
master
Author | SHA1 | Date | |
---|---|---|---|
|
0493edf8e6 | ||
|
c257128948 | ||
|
aed7ba523d | ||
|
5444ec4047 | ||
|
66bc11099e | ||
|
25d54a4f5e | ||
|
a00bdc5ff9 | ||
|
d050db0d6e | ||
|
a98783dbc4 |
11
__init__.py
11
__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
|
||||
|
9
menu.py
9
menu.py
@@ -354,7 +354,7 @@ def learn_sounds(sounds):
|
||||
Excluded sounds:
|
||||
- Files in folders named 'ambience' (at any level)
|
||||
- Files in any directory starting with '.'
|
||||
- Files starting with 'game-intro', 'music_menu', or '_'
|
||||
- Files whose filename starts with 'game-intro', 'music_menu', or '_' (regardless of directory)
|
||||
|
||||
Args:
|
||||
sounds (dict): Dictionary of available sound objects
|
||||
@@ -374,8 +374,11 @@ def learn_sounds(sounds):
|
||||
|
||||
# Process each sound key in the dictionary
|
||||
for soundKey in sounds.keys():
|
||||
# Skip if key has any excluded prefix
|
||||
if any(soundKey.lower().startswith(prefix.lower()) for prefix in excludedPrefixes):
|
||||
# Extract the filename (part after the last '/')
|
||||
filename = soundKey.split('/')[-1]
|
||||
|
||||
# Skip if filename has any excluded prefix
|
||||
if any(filename.lower().startswith(prefix.lower()) for prefix in excludedPrefixes):
|
||||
continue
|
||||
|
||||
# Split key into path parts
|
||||
|
@@ -166,8 +166,8 @@ class Scoreboard:
|
||||
if name is None:
|
||||
# Import get_input here to avoid circular imports
|
||||
from .input import get_input
|
||||
name = get_input("New high score! Enter your name:", "Player")
|
||||
if name is None: # User cancelled
|
||||
name = get_input("New high score! Enter your name:", "")
|
||||
if name is None or name.strip() == "": # User cancelled or entered empty
|
||||
name = "Player"
|
||||
|
||||
# Insert new score at correct position
|
||||
|
164
services.py
164
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,164 @@ 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)
|
||||
|
||||
def is_at_first(self):
|
||||
"""Check if currently at the first (oldest) message in history.
|
||||
|
||||
Returns:
|
||||
bool: True if at the first message
|
||||
"""
|
||||
if not self.history:
|
||||
return False
|
||||
return self.current_index == 0
|
||||
|
||||
def is_at_last(self):
|
||||
"""Check if currently at the last (newest) message in history.
|
||||
|
||||
Returns:
|
||||
bool: True if at the last message or viewing most recent
|
||||
"""
|
||||
if not self.history:
|
||||
return False
|
||||
return self.current_index == -1 or self.current_index == len(self.history) - 1
|
||||
|
||||
def get_most_recent(self):
|
||||
"""Get the most recent message (what F2 should always say).
|
||||
|
||||
Returns:
|
||||
str or None: Most recent message text, or None if no history
|
||||
"""
|
||||
if not self.history:
|
||||
return None
|
||||
return self.history[-1]['text']
|
||||
|
35
sound.py
35
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)
|
||||
|
275
speech.py
275
speech.py
@@ -13,6 +13,9 @@ import textwrap
|
||||
import time
|
||||
from sys import exit
|
||||
|
||||
# Keep track of whether dialog instructions have been shown
|
||||
dialogUsageInstructions = False
|
||||
|
||||
class Speech:
|
||||
"""Handles text-to-speech functionality."""
|
||||
|
||||
@@ -58,12 +61,14 @@ 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", add_to_history=True):
|
||||
"""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"
|
||||
add_to_history (bool): Whether to add this message to speech history (default: True)
|
||||
"""
|
||||
if not self.provider:
|
||||
return
|
||||
@@ -71,7 +76,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
|
||||
|
||||
@@ -79,13 +84,46 @@ class Speech:
|
||||
self.lastSpoken["text"] = text
|
||||
self.lastSpoken["time"] = currentTime
|
||||
|
||||
# Proceed with speech
|
||||
# Add to speech history (import here to avoid circular imports)
|
||||
if add_to_history:
|
||||
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()
|
||||
@@ -118,27 +156,45 @@ class Speech:
|
||||
# Global instance for backward compatibility
|
||||
_speechInstance = None
|
||||
|
||||
def speak(text, interrupt=True):
|
||||
def speak(text, interrupt=True, priority="normal", add_to_history=True):
|
||||
"""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"
|
||||
add_to_history (bool): Whether to add this message to speech history (default: True)
|
||||
"""
|
||||
global _speechInstance
|
||||
if _speechInstance is None:
|
||||
_speechInstance = Speech.get_instance()
|
||||
_speechInstance.speak(text, interrupt)
|
||||
_speechInstance.speak(text, interrupt, priority, add_to_history)
|
||||
|
||||
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 +203,200 @@ 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 dialog of the game)
|
||||
global dialogUsageInstructions
|
||||
if not dialogUsageInstructions and 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."
|
||||
dialogUsageInstructions = True
|
||||
else:
|
||||
control_text = "" # No instructions after first dialog
|
||||
|
||||
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 (no instructions when repeating)
|
||||
speech.speak(formatted_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))
|
||||
pygame.event.clear() # Clear all events queued during sound playback
|
||||
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))
|
||||
pygame.event.clear() # Clear all events queued during sound playback
|
||||
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
|
||||
pygame.event.pump() # Clear any events queued during sound playback
|
||||
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:
|
||||
# Add position indicator
|
||||
prefix = ""
|
||||
if history_service.is_at_first():
|
||||
prefix = "First: "
|
||||
speak(prefix + message, interrupt=True, priority="important", add_to_history=False)
|
||||
else:
|
||||
speak("No previous messages", interrupt=True, priority="important", add_to_history=False)
|
||||
except ImportError:
|
||||
speak("Speech history not available", interrupt=True, priority="important", add_to_history=False)
|
||||
|
||||
|
||||
def speak_current():
|
||||
"""Repeat the most recent message in speech history (F2 always speaks last message)."""
|
||||
try:
|
||||
from .services import SpeechHistoryService
|
||||
history_service = SpeechHistoryService.get_instance()
|
||||
message = history_service.get_most_recent()
|
||||
if message:
|
||||
speak(message, interrupt=True, priority="important", add_to_history=False)
|
||||
else:
|
||||
speak("No messages in history", interrupt=True, priority="important", add_to_history=False)
|
||||
except ImportError:
|
||||
speak("Speech history not available", interrupt=True, priority="important", add_to_history=False)
|
||||
|
||||
|
||||
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:
|
||||
# Add position indicator
|
||||
prefix = ""
|
||||
if history_service.is_at_last():
|
||||
prefix = "Last: "
|
||||
speak(prefix + message, interrupt=True, priority="important", add_to_history=False)
|
||||
else:
|
||||
speak("No next messages", interrupt=True, priority="important", add_to_history=False)
|
||||
except ImportError:
|
||||
speak("Speech history not available", interrupt=True, priority="important", add_to_history=False)
|
||||
|
62
utils.py
62
utils.py
@@ -156,19 +156,77 @@ 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", add_to_history=True):
|
||||
"""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"
|
||||
add_to_history (bool): Whether to add this message to speech history (default: True)
|
||||
|
||||
Returns:
|
||||
Game: Self for method chaining
|
||||
"""
|
||||
self.speech.speak(text, interrupt)
|
||||
self.speech.speak(text, interrupt, priority, add_to_history)
|
||||
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.
|
||||
|
||||
|
Reference in New Issue
Block a user