Compare commits
18 Commits
3b01662d98
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 0493edf8e6 | |||
| c257128948 | |||
| aed7ba523d | |||
| 5444ec4047 | |||
| 66bc11099e | |||
| 25d54a4f5e | |||
| a00bdc5ff9 | |||
| d050db0d6e | |||
| a98783dbc4 | |||
| f2079261d1 | |||
| 0190fa3a06 | |||
| 09421c4bda | |||
| dcd204e476 | |||
| ca2d0d34bd | |||
| 8ffa53b69f | |||
| 68ffde0fc7 | |||
| a17a4c6f15 | |||
| 3a478d15d5 |
+10
-5
@@ -15,7 +15,8 @@ This module provides core functionality for Storm Games including:
|
|||||||
from .services import (
|
from .services import (
|
||||||
ConfigService,
|
ConfigService,
|
||||||
VolumeService,
|
VolumeService,
|
||||||
PathService
|
PathService,
|
||||||
|
SpeechHistoryService
|
||||||
)
|
)
|
||||||
|
|
||||||
# Import Sound class and functions
|
# Import Sound class and functions
|
||||||
@@ -39,7 +40,7 @@ from .sound import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Import Speech class and functions
|
# 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
|
# Import Scoreboard
|
||||||
from .scoreboard import Scoreboard
|
from .scoreboard import Scoreboard
|
||||||
@@ -51,7 +52,7 @@ from .input import get_input, check_for_exit, pause_game
|
|||||||
from .display import display_text, initialize_gui
|
from .display import display_text, initialize_gui
|
||||||
|
|
||||||
# Import menu functions
|
# Import menu functions
|
||||||
from .menu import game_menu, learn_sounds, instructions, credits, donate, exit_game
|
from .menu import game_menu, instruction_menu, learn_sounds, instructions, credits, donate, exit_game
|
||||||
|
|
||||||
# Update imports to reference Scoreboard methods
|
# Update imports to reference Scoreboard methods
|
||||||
high_scores = Scoreboard.display_high_scores
|
high_scores = Scoreboard.display_high_scores
|
||||||
@@ -78,7 +79,7 @@ __version__ = '2.0.0'
|
|||||||
# Make all symbols available at the package level
|
# Make all symbols available at the package level
|
||||||
__all__ = [
|
__all__ = [
|
||||||
# Services
|
# Services
|
||||||
'ConfigService', 'VolumeService', 'PathService',
|
'ConfigService', 'VolumeService', 'PathService', 'SpeechHistoryService',
|
||||||
|
|
||||||
# Sound
|
# Sound
|
||||||
'Sound',
|
'Sound',
|
||||||
@@ -102,6 +103,9 @@ __all__ = [
|
|||||||
'messagebox',
|
'messagebox',
|
||||||
'speak',
|
'speak',
|
||||||
'Speech',
|
'Speech',
|
||||||
|
'speak_previous',
|
||||||
|
'speak_current',
|
||||||
|
'speak_next',
|
||||||
|
|
||||||
# Scoreboard
|
# Scoreboard
|
||||||
'Scoreboard',
|
'Scoreboard',
|
||||||
@@ -113,7 +117,7 @@ __all__ = [
|
|||||||
'display_text', 'initialize_gui',
|
'display_text', 'initialize_gui',
|
||||||
|
|
||||||
# Menu
|
# Menu
|
||||||
'game_menu', 'learn_sounds', 'instructions', 'credits', 'donate', 'exit_game', 'high_scores', 'has_high_scores',
|
'game_menu', 'instruction_menu', 'learn_sounds', 'instructions', 'credits', 'donate', 'exit_game', 'high_scores', 'has_high_scores',
|
||||||
|
|
||||||
# Game class
|
# Game class
|
||||||
'Game',
|
'Game',
|
||||||
@@ -133,6 +137,7 @@ __all__ = [
|
|||||||
configService = ConfigService.get_instance()
|
configService = ConfigService.get_instance()
|
||||||
volumeService = VolumeService.get_instance()
|
volumeService = VolumeService.get_instance()
|
||||||
pathService = PathService.get_instance()
|
pathService = PathService.get_instance()
|
||||||
|
speechHistoryService = SpeechHistoryService.get_instance()
|
||||||
|
|
||||||
# Set up backward compatibility hooks for initialize_gui
|
# Set up backward compatibility hooks for initialize_gui
|
||||||
_originalInitializeGui = initialize_gui
|
_originalInitializeGui = initialize_gui
|
||||||
|
|||||||
+49
-3
@@ -88,13 +88,55 @@ def initialize_gui(gameTitle):
|
|||||||
Speech.get_instance().speak("Error loading sounds.", False)
|
Speech.get_instance().speak("Error loading sounds.", False)
|
||||||
soundData = {}
|
soundData = {}
|
||||||
|
|
||||||
# Play intro sound if available
|
# Play intro sound if available, optionally with visual logo
|
||||||
from .sound import cut_scene
|
from .sound import cut_scene
|
||||||
if 'game-intro' in soundData:
|
if 'game-intro' in soundData:
|
||||||
cut_scene(soundData, 'game-intro')
|
_show_logo_with_audio(soundData, 'game-intro')
|
||||||
|
|
||||||
return soundData
|
return soundData
|
||||||
|
|
||||||
|
def _show_logo_with_audio(soundData, audioKey):
|
||||||
|
"""Show visual logo while playing audio intro.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
soundData (dict): Dictionary of loaded sounds
|
||||||
|
audioKey (str): Key of the audio to play
|
||||||
|
"""
|
||||||
|
# Look for logo image files in common formats
|
||||||
|
logoFiles = ['logo.png', 'logo.jpg', 'logo.jpeg', 'logo.gif', 'logo.bmp']
|
||||||
|
logoImage = None
|
||||||
|
|
||||||
|
for logoFile in logoFiles:
|
||||||
|
if os.path.exists(logoFile):
|
||||||
|
try:
|
||||||
|
logoImage = pygame.image.load(logoFile)
|
||||||
|
break
|
||||||
|
except pygame.error:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if logoImage:
|
||||||
|
# Display logo while audio plays
|
||||||
|
screen = pygame.display.get_surface()
|
||||||
|
screenRect = screen.get_rect()
|
||||||
|
logoRect = logoImage.get_rect(center=screenRect.center)
|
||||||
|
|
||||||
|
# Clear screen to black
|
||||||
|
screen.fill((0, 0, 0))
|
||||||
|
screen.blit(logoImage, logoRect)
|
||||||
|
pygame.display.flip()
|
||||||
|
|
||||||
|
# Play audio and wait for it to finish
|
||||||
|
from .sound import cut_scene
|
||||||
|
cut_scene(soundData, audioKey)
|
||||||
|
|
||||||
|
# Clear screen after audio finishes
|
||||||
|
screen.fill((0, 0, 0))
|
||||||
|
pygame.display.flip()
|
||||||
|
else:
|
||||||
|
# No logo image found, just play audio
|
||||||
|
from .sound import cut_scene
|
||||||
|
cut_scene(soundData, audioKey)
|
||||||
|
|
||||||
def display_text(text):
|
def display_text(text):
|
||||||
"""Display and speak text with navigation controls.
|
"""Display and speak text with navigation controls.
|
||||||
|
|
||||||
@@ -136,6 +178,9 @@ def display_text(text):
|
|||||||
currentIndex = 0
|
currentIndex = 0
|
||||||
speech.speak(navText[currentIndex])
|
speech.speak(navText[currentIndex])
|
||||||
|
|
||||||
|
# Clear any pending events
|
||||||
|
pygame.event.clear()
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
event = pygame.event.wait()
|
event = pygame.event.wait()
|
||||||
if event.type == pygame.KEYDOWN:
|
if event.type == pygame.KEYDOWN:
|
||||||
@@ -188,5 +233,6 @@ def display_text(text):
|
|||||||
except:
|
except:
|
||||||
speech.speak("Failed to copy the text to the clipboard.")
|
speech.speak("Failed to copy the text to the clipboard.")
|
||||||
|
|
||||||
event = pygame.event.clear()
|
pygame.event.pump()
|
||||||
|
pygame.event.clear()
|
||||||
time.sleep(0.001)
|
time.sleep(0.001)
|
||||||
|
|||||||
@@ -10,11 +10,19 @@ Provides functionality for:
|
|||||||
|
|
||||||
import pygame
|
import pygame
|
||||||
import time
|
import time
|
||||||
import wx
|
|
||||||
from .speech import speak
|
from .speech import speak
|
||||||
|
|
||||||
def get_input(prompt="Enter text:", text=""):
|
def get_input(prompt="Enter text:", text=""):
|
||||||
"""Display a dialog box for text input.
|
"""Display an accessible text input dialog using pygame.
|
||||||
|
|
||||||
|
Features:
|
||||||
|
- Speaks each character as typed
|
||||||
|
- Left/Right arrows navigate and speak characters
|
||||||
|
- Up/Down arrows read full text content
|
||||||
|
- Backspace announces deletions
|
||||||
|
- Enter submits, Escape cancels
|
||||||
|
- Control key repeats the original prompt message
|
||||||
|
- Fully accessible without screen reader dependency
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
prompt (str): Prompt text to display (default: "Enter text:")
|
prompt (str): Prompt text to display (default: "Enter text:")
|
||||||
@@ -23,15 +31,179 @@ def get_input(prompt="Enter text:", text=""):
|
|||||||
Returns:
|
Returns:
|
||||||
str: User input text, or None if cancelled
|
str: User input text, or None if cancelled
|
||||||
"""
|
"""
|
||||||
app = wx.App(False)
|
|
||||||
dialog = wx.TextEntryDialog(None, prompt, "Input", text)
|
# Initialize text buffer and cursor
|
||||||
dialog.SetValue(text)
|
text_buffer = list(text) # Use list for easier character manipulation
|
||||||
if dialog.ShowModal() == wx.ID_OK:
|
cursor_pos = len(text_buffer) # Start at end of initial text
|
||||||
userInput = dialog.GetValue()
|
|
||||||
|
# Announce the prompt and initial text as a single message
|
||||||
|
if text:
|
||||||
|
initial_message = f"{prompt} Default text: {text}"
|
||||||
else:
|
else:
|
||||||
userInput = None
|
initial_message = f"{prompt} Empty text field"
|
||||||
dialog.Destroy()
|
speak(initial_message)
|
||||||
return userInput
|
|
||||||
|
# Clear any pending events
|
||||||
|
pygame.event.clear()
|
||||||
|
|
||||||
|
# Main input loop
|
||||||
|
while True:
|
||||||
|
event = pygame.event.wait()
|
||||||
|
|
||||||
|
if event.type == pygame.KEYDOWN:
|
||||||
|
if event.key == pygame.K_RETURN:
|
||||||
|
# Submit the input
|
||||||
|
result = ''.join(text_buffer)
|
||||||
|
speak(f"Submitted: {result if result else 'empty'}")
|
||||||
|
return result
|
||||||
|
|
||||||
|
elif event.key == pygame.K_ESCAPE:
|
||||||
|
# Cancel input
|
||||||
|
speak("Cancelled")
|
||||||
|
return None
|
||||||
|
|
||||||
|
elif event.key == pygame.K_BACKSPACE:
|
||||||
|
# Delete character before cursor
|
||||||
|
if cursor_pos > 0:
|
||||||
|
deleted_char = text_buffer.pop(cursor_pos - 1)
|
||||||
|
cursor_pos -= 1
|
||||||
|
speak(f"{deleted_char} deleted")
|
||||||
|
else:
|
||||||
|
speak("Nothing to delete")
|
||||||
|
|
||||||
|
elif event.key == pygame.K_DELETE:
|
||||||
|
# Delete character at cursor
|
||||||
|
if cursor_pos < len(text_buffer):
|
||||||
|
deleted_char = text_buffer.pop(cursor_pos)
|
||||||
|
speak(f"{deleted_char} deleted")
|
||||||
|
else:
|
||||||
|
speak("Nothing to delete")
|
||||||
|
|
||||||
|
elif event.key == pygame.K_LEFT:
|
||||||
|
# Move cursor left and speak character
|
||||||
|
if cursor_pos > 0:
|
||||||
|
cursor_pos -= 1
|
||||||
|
if cursor_pos == 0:
|
||||||
|
speak("Beginning of text")
|
||||||
|
else:
|
||||||
|
speak(text_buffer[cursor_pos])
|
||||||
|
else:
|
||||||
|
speak("Beginning of text")
|
||||||
|
|
||||||
|
elif event.key == pygame.K_RIGHT:
|
||||||
|
# Move cursor right and speak character
|
||||||
|
if cursor_pos < len(text_buffer):
|
||||||
|
speak(text_buffer[cursor_pos])
|
||||||
|
cursor_pos += 1
|
||||||
|
if cursor_pos == len(text_buffer):
|
||||||
|
speak("End of text")
|
||||||
|
else:
|
||||||
|
speak("End of text")
|
||||||
|
|
||||||
|
elif event.key == pygame.K_UP or event.key == pygame.K_DOWN:
|
||||||
|
# Read entire text content
|
||||||
|
if text_buffer:
|
||||||
|
speak(''.join(text_buffer))
|
||||||
|
else:
|
||||||
|
speak("Empty text field")
|
||||||
|
|
||||||
|
elif event.key == pygame.K_HOME:
|
||||||
|
# Move to beginning
|
||||||
|
cursor_pos = 0
|
||||||
|
speak("Beginning of text")
|
||||||
|
|
||||||
|
elif event.key == pygame.K_END:
|
||||||
|
# Move to end
|
||||||
|
cursor_pos = len(text_buffer)
|
||||||
|
speak("End of text")
|
||||||
|
|
||||||
|
elif event.key == pygame.K_LCTRL or event.key == pygame.K_RCTRL:
|
||||||
|
# Repeat the original prompt message
|
||||||
|
speak(initial_message)
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Handle regular character input
|
||||||
|
if event.unicode and event.unicode.isprintable():
|
||||||
|
char = event.unicode
|
||||||
|
# Insert character at cursor position
|
||||||
|
text_buffer.insert(cursor_pos, char)
|
||||||
|
cursor_pos += 1
|
||||||
|
|
||||||
|
# Speak the character name
|
||||||
|
if char == ' ':
|
||||||
|
speak("space")
|
||||||
|
elif char == '\\':
|
||||||
|
speak("backslash")
|
||||||
|
elif char == '/':
|
||||||
|
speak("slash")
|
||||||
|
elif char == '!':
|
||||||
|
speak("exclamation mark")
|
||||||
|
elif char == '"':
|
||||||
|
speak("quotation mark")
|
||||||
|
elif char == '#':
|
||||||
|
speak("hash")
|
||||||
|
elif char == '$':
|
||||||
|
speak("dollar sign")
|
||||||
|
elif char == '%':
|
||||||
|
speak("percent")
|
||||||
|
elif char == '&':
|
||||||
|
speak("ampersand")
|
||||||
|
elif char == "'":
|
||||||
|
speak("apostrophe")
|
||||||
|
elif char == '(':
|
||||||
|
speak("left parenthesis")
|
||||||
|
elif char == ')':
|
||||||
|
speak("right parenthesis")
|
||||||
|
elif char == '*':
|
||||||
|
speak("asterisk")
|
||||||
|
elif char == '+':
|
||||||
|
speak("plus")
|
||||||
|
elif char == ',':
|
||||||
|
speak("comma")
|
||||||
|
elif char == '-':
|
||||||
|
speak("minus")
|
||||||
|
elif char == '.':
|
||||||
|
speak("period")
|
||||||
|
elif char == ':':
|
||||||
|
speak("colon")
|
||||||
|
elif char == ';':
|
||||||
|
speak("semicolon")
|
||||||
|
elif char == '<':
|
||||||
|
speak("less than")
|
||||||
|
elif char == '=':
|
||||||
|
speak("equals")
|
||||||
|
elif char == '>':
|
||||||
|
speak("greater than")
|
||||||
|
elif char == '?':
|
||||||
|
speak("question mark")
|
||||||
|
elif char == '@':
|
||||||
|
speak("at sign")
|
||||||
|
elif char == '[':
|
||||||
|
speak("left bracket")
|
||||||
|
elif char == ']':
|
||||||
|
speak("right bracket")
|
||||||
|
elif char == '^':
|
||||||
|
speak("caret")
|
||||||
|
elif char == '_':
|
||||||
|
speak("underscore")
|
||||||
|
elif char == '`':
|
||||||
|
speak("grave accent")
|
||||||
|
elif char == '{':
|
||||||
|
speak("left brace")
|
||||||
|
elif char == '|':
|
||||||
|
speak("pipe")
|
||||||
|
elif char == '}':
|
||||||
|
speak("right brace")
|
||||||
|
elif char == '~':
|
||||||
|
speak("tilde")
|
||||||
|
else:
|
||||||
|
# For regular letters, numbers, and other characters
|
||||||
|
speak(char)
|
||||||
|
|
||||||
|
# Allow other events to be processed
|
||||||
|
pygame.event.pump()
|
||||||
|
pygame.event.clear()
|
||||||
|
time.sleep(0.001)
|
||||||
|
|
||||||
def pause_game():
|
def pause_game():
|
||||||
"""Pauses the game until user presses backspace."""
|
"""Pauses the game until user presses backspace."""
|
||||||
@@ -52,6 +224,10 @@ def pause_game():
|
|||||||
if event.type == pygame.KEYDOWN and event.key == pygame.K_BACKSPACE:
|
if event.type == pygame.KEYDOWN and event.key == pygame.K_BACKSPACE:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
pygame.event.pump()
|
||||||
|
pygame.event.clear()
|
||||||
|
time.sleep(0.001)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
pygame.mixer.unpause()
|
pygame.mixer.unpause()
|
||||||
except:
|
except:
|
||||||
|
|||||||
@@ -25,6 +25,94 @@ from .display import display_text
|
|||||||
from .scoreboard import Scoreboard
|
from .scoreboard import Scoreboard
|
||||||
from .services import PathService, ConfigService
|
from .services import PathService, ConfigService
|
||||||
|
|
||||||
|
def instruction_menu(sounds, instruction_text, *options):
|
||||||
|
"""Display a menu with an instruction announcement at the top.
|
||||||
|
|
||||||
|
The instruction text is announced as item 0 but is not selectable.
|
||||||
|
The actual menu items start at index 1, and navigation skips the instruction.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sounds (dict): Dictionary of sound objects
|
||||||
|
instruction_text (str): The instruction/context text to announce
|
||||||
|
*options: Menu options to display
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Selected menu option or None if cancelled
|
||||||
|
"""
|
||||||
|
# Get speech instance
|
||||||
|
speech = Speech.get_instance()
|
||||||
|
|
||||||
|
# Create combined list with instruction at index 0
|
||||||
|
all_items = [instruction_text] + list(options)
|
||||||
|
|
||||||
|
loop = True
|
||||||
|
pygame.mixer.stop()
|
||||||
|
current_index = 0 # Start at instruction
|
||||||
|
last_spoken = -1
|
||||||
|
|
||||||
|
# Clear any pending events
|
||||||
|
pygame.event.clear()
|
||||||
|
|
||||||
|
while loop:
|
||||||
|
if current_index != last_spoken:
|
||||||
|
speech.speak(all_items[current_index])
|
||||||
|
last_spoken = current_index
|
||||||
|
|
||||||
|
event = pygame.event.wait()
|
||||||
|
if event.type == pygame.KEYDOWN:
|
||||||
|
if event.key == pygame.K_ESCAPE:
|
||||||
|
return None
|
||||||
|
elif event.key in [pygame.K_DOWN, pygame.K_s]:
|
||||||
|
moved = False
|
||||||
|
if current_index == 0: # On instruction, go to first menu item
|
||||||
|
current_index = 1
|
||||||
|
moved = True
|
||||||
|
elif current_index < len(all_items) - 1: # Normal navigation
|
||||||
|
current_index += 1
|
||||||
|
moved = True
|
||||||
|
|
||||||
|
if moved:
|
||||||
|
try:
|
||||||
|
sounds['menu-move'].play()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
elif event.key in [pygame.K_UP, pygame.K_w]:
|
||||||
|
if current_index > 1: # Can move up from menu items (but not to instruction)
|
||||||
|
current_index -= 1
|
||||||
|
try:
|
||||||
|
sounds['menu-move'].play()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
elif event.key == pygame.K_HOME:
|
||||||
|
target_index = 1 if current_index != 1 else current_index # Go to first menu item
|
||||||
|
if target_index != current_index:
|
||||||
|
current_index = target_index
|
||||||
|
try:
|
||||||
|
sounds['menu-move'].play()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
elif event.key == pygame.K_END and current_index != len(all_items) - 1:
|
||||||
|
current_index = len(all_items) - 1
|
||||||
|
try:
|
||||||
|
sounds['menu-move'].play()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
elif event.key == pygame.K_RETURN:
|
||||||
|
if current_index == 0: # Can't select the instruction
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
sounds['menu-select'].play()
|
||||||
|
time.sleep(sounds['menu-select'].get_length())
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return options[current_index - 1] # Adjust for instruction offset
|
||||||
|
elif event.type == pygame.QUIT:
|
||||||
|
return None
|
||||||
|
|
||||||
|
pygame.event.pump()
|
||||||
|
pygame.event.clear()
|
||||||
|
time.sleep(0.001)
|
||||||
|
|
||||||
def game_menu(sounds, playCallback=None, *customOptions):
|
def game_menu(sounds, playCallback=None, *customOptions):
|
||||||
"""Display and handle the main game menu with standard and custom options.
|
"""Display and handle the main game menu with standard and custom options.
|
||||||
|
|
||||||
@@ -96,6 +184,7 @@ def game_menu(sounds, playCallback=None, *customOptions):
|
|||||||
pygame.mixer.stop()
|
pygame.mixer.stop()
|
||||||
currentIndex = 0
|
currentIndex = 0
|
||||||
lastSpoken = -1 # Track last spoken index
|
lastSpoken = -1 # Track last spoken index
|
||||||
|
pygame.event.clear()
|
||||||
|
|
||||||
while loop:
|
while loop:
|
||||||
if currentIndex != lastSpoken:
|
if currentIndex != lastSpoken:
|
||||||
@@ -249,7 +338,8 @@ def game_menu(sounds, playCallback=None, *customOptions):
|
|||||||
pass
|
pass
|
||||||
return allOptions[currentIndex]
|
return allOptions[currentIndex]
|
||||||
|
|
||||||
event = pygame.event.clear()
|
pygame.event.pump()
|
||||||
|
pygame.event.clear()
|
||||||
time.sleep(0.001)
|
time.sleep(0.001)
|
||||||
|
|
||||||
def learn_sounds(sounds):
|
def learn_sounds(sounds):
|
||||||
@@ -264,7 +354,7 @@ def learn_sounds(sounds):
|
|||||||
Excluded sounds:
|
Excluded sounds:
|
||||||
- Files in folders named 'ambience' (at any level)
|
- Files in folders named 'ambience' (at any level)
|
||||||
- Files in any directory starting with '.'
|
- 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:
|
Args:
|
||||||
sounds (dict): Dictionary of available sound objects
|
sounds (dict): Dictionary of available sound objects
|
||||||
@@ -284,8 +374,11 @@ def learn_sounds(sounds):
|
|||||||
|
|
||||||
# Process each sound key in the dictionary
|
# Process each sound key in the dictionary
|
||||||
for soundKey in sounds.keys():
|
for soundKey in sounds.keys():
|
||||||
# Skip if key has any excluded prefix
|
# Extract the filename (part after the last '/')
|
||||||
if any(soundKey.lower().startswith(prefix.lower()) for prefix in excludedPrefixes):
|
filename = soundKey.split('/')[-1]
|
||||||
|
|
||||||
|
# Skip if filename has any excluded prefix
|
||||||
|
if any(filename.lower().startswith(prefix.lower()) for prefix in excludedPrefixes):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Split key into path parts
|
# Split key into path parts
|
||||||
@@ -334,6 +427,7 @@ def learn_sounds(sounds):
|
|||||||
|
|
||||||
# Flag to track when to exit the loop
|
# Flag to track when to exit the loop
|
||||||
returnToMenu = False
|
returnToMenu = False
|
||||||
|
pygame.event.clear()
|
||||||
|
|
||||||
while not returnToMenu:
|
while not returnToMenu:
|
||||||
# Announce current sound
|
# Announce current sound
|
||||||
@@ -442,9 +536,9 @@ def credits():
|
|||||||
def donate():
|
def donate():
|
||||||
"""Open the donation webpage.
|
"""Open the donation webpage.
|
||||||
|
|
||||||
Opens the Ko-fi donation page.
|
Opens the donation page.
|
||||||
"""
|
"""
|
||||||
webbrowser.open('https://ko-fi.com/stormux')
|
webbrowser.open('https://www.paypal.com/donate/?business=stormdragon2976@gmail.com&no_recurring=0¤cy_code=USD')
|
||||||
messagebox("The donation page has been opened in your browser.")
|
messagebox("The donation page has been opened in your browser.")
|
||||||
|
|
||||||
def exit_game(fade=0):
|
def exit_game(fade=0):
|
||||||
@@ -485,6 +579,12 @@ def exit_game(fade=0):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Warning: Could not handle music during exit: {e}")
|
print(f"Warning: Could not handle music during exit: {e}")
|
||||||
|
|
||||||
|
# Clean up pygame mixer first
|
||||||
|
try:
|
||||||
|
pygame.mixer.quit()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Warning: Error during pygame.mixer.quit(): {e}")
|
||||||
|
|
||||||
# Clean up pygame
|
# Clean up pygame
|
||||||
try:
|
try:
|
||||||
pygame.quit()
|
pygame.quit()
|
||||||
|
|||||||
@@ -5,4 +5,3 @@ pyxdg>=0.27
|
|||||||
setproctitle>=1.2.0
|
setproctitle>=1.2.0
|
||||||
numpy>=1.19.0
|
numpy>=1.19.0
|
||||||
accessible-output2>=0.14
|
accessible-output2>=0.14
|
||||||
wxpython
|
|
||||||
|
|||||||
+2
-2
@@ -166,8 +166,8 @@ class Scoreboard:
|
|||||||
if name is None:
|
if name is None:
|
||||||
# Import get_input here to avoid circular imports
|
# Import get_input here to avoid circular imports
|
||||||
from .input import get_input
|
from .input import get_input
|
||||||
name = get_input("New high score! Enter your name:", "Player")
|
name = get_input("New high score! Enter your name:", "")
|
||||||
if name is None: # User cancelled
|
if name is None or name.strip() == "": # User cancelled or entered empty
|
||||||
name = "Player"
|
name = "Player"
|
||||||
|
|
||||||
# Insert new score at correct position
|
# Insert new score at correct position
|
||||||
|
|||||||
+164
@@ -6,10 +6,13 @@ Provides centralized services to replace global variables:
|
|||||||
- ConfigService: Manages game configuration
|
- ConfigService: Manages game configuration
|
||||||
- VolumeService: Handles volume settings
|
- VolumeService: Handles volume settings
|
||||||
- PathService: Manages file paths
|
- PathService: Manages file paths
|
||||||
|
- SpeechHistoryService: Manages speech history for navigation
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import configparser
|
import configparser
|
||||||
import os
|
import os
|
||||||
|
import time
|
||||||
|
from collections import deque
|
||||||
from xdg import BaseDirectory
|
from xdg import BaseDirectory
|
||||||
|
|
||||||
# For backward compatibility
|
# For backward compatibility
|
||||||
@@ -272,3 +275,164 @@ class PathService:
|
|||||||
globalPath = self.globalPath
|
globalPath = self.globalPath
|
||||||
|
|
||||||
return self
|
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']
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ import textwrap
|
|||||||
import time
|
import time
|
||||||
from sys import exit
|
from sys import exit
|
||||||
|
|
||||||
|
# Keep track of whether dialog instructions have been shown
|
||||||
|
dialogUsageInstructions = False
|
||||||
|
|
||||||
class Speech:
|
class Speech:
|
||||||
"""Handles text-to-speech functionality."""
|
"""Handles text-to-speech functionality."""
|
||||||
|
|
||||||
@@ -58,12 +61,14 @@ class Speech:
|
|||||||
# No speech providers found
|
# No speech providers found
|
||||||
print("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.
|
"""Speak text using the configured speech provider and display on screen.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
text (str): Text to speak and display
|
text (str): Text to speak and display
|
||||||
interrupt (bool): Whether to interrupt current speech (default: True)
|
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:
|
if not self.provider:
|
||||||
return
|
return
|
||||||
@@ -79,13 +84,46 @@ class Speech:
|
|||||||
self.lastSpoken["text"] = text
|
self.lastSpoken["text"] = text
|
||||||
self.lastSpoken["time"] = currentTime
|
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 self.providerName == "speechd":
|
||||||
if interrupt:
|
if interrupt:
|
||||||
self.spd.cancel()
|
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)
|
self.spd.say(text)
|
||||||
|
|
||||||
elif self.providerName == "accessible_output2":
|
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
|
# Display the text on screen
|
||||||
screen = pygame.display.get_surface()
|
screen = pygame.display.get_surface()
|
||||||
@@ -118,27 +156,45 @@ class Speech:
|
|||||||
# Global instance for backward compatibility
|
# Global instance for backward compatibility
|
||||||
_speechInstance = None
|
_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.
|
"""Speak text using the global speech instance.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
text (str): Text to speak and display
|
text (str): Text to speak and display
|
||||||
interrupt (bool): Whether to interrupt current speech (default: True)
|
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
|
global _speechInstance
|
||||||
if _speechInstance is None:
|
if _speechInstance is None:
|
||||||
_speechInstance = Speech.get_instance()
|
_speechInstance = Speech.get_instance()
|
||||||
_speechInstance.speak(text, interrupt)
|
_speechInstance.speak(text, interrupt, priority, add_to_history)
|
||||||
|
|
||||||
def messagebox(text):
|
def messagebox(text, sounds=None):
|
||||||
"""Display a simple message box with text.
|
"""Enhanced messagebox with dialog support.
|
||||||
|
|
||||||
Shows a message that can be repeated until the user chooses to continue.
|
|
||||||
|
|
||||||
Args:
|
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()
|
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.")
|
speech.speak(text + "\nPress any key to repeat or enter to continue.")
|
||||||
while True:
|
while True:
|
||||||
event = pygame.event.wait()
|
event = pygame.event.wait()
|
||||||
@@ -147,3 +203,200 @@ def messagebox(text):
|
|||||||
speech.speak(" ")
|
speech.speak(" ")
|
||||||
return
|
return
|
||||||
speech.speak(text + "\nPress any key to repeat or enter to continue.")
|
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)
|
||||||
|
|||||||
@@ -111,25 +111,122 @@ class Game:
|
|||||||
# Load sound effects
|
# Load sound effects
|
||||||
self.sound
|
self.sound
|
||||||
|
|
||||||
# Play intro sound if available
|
# Play intro sound if available, optionally with visual logo
|
||||||
if 'game-intro' in self.sound.sounds:
|
if 'game-intro' in self.sound.sounds:
|
||||||
self.sound.cut_scene('game-intro')
|
self._show_logo_with_audio('game-intro')
|
||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def speak(self, text, interrupt=True):
|
def _show_logo_with_audio(self, audioKey):
|
||||||
|
"""Show visual logo while playing audio intro.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
audioKey (str): Key of the audio to play
|
||||||
|
"""
|
||||||
|
# Look for logo image files in common formats
|
||||||
|
logoFiles = ['logo.png', 'logo.jpg', 'logo.jpeg', 'logo.gif', 'logo.bmp']
|
||||||
|
logoImage = None
|
||||||
|
|
||||||
|
for logoFile in logoFiles:
|
||||||
|
if os.path.exists(logoFile):
|
||||||
|
try:
|
||||||
|
logoImage = pygame.image.load(logoFile)
|
||||||
|
break
|
||||||
|
except pygame.error:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if logoImage:
|
||||||
|
# Display logo while audio plays
|
||||||
|
screen = pygame.display.get_surface()
|
||||||
|
screenRect = screen.get_rect()
|
||||||
|
logoRect = logoImage.get_rect(center=screenRect.center)
|
||||||
|
|
||||||
|
# Clear screen to black
|
||||||
|
screen.fill((0, 0, 0))
|
||||||
|
screen.blit(logoImage, logoRect)
|
||||||
|
pygame.display.flip()
|
||||||
|
|
||||||
|
# Play audio and wait for it to finish
|
||||||
|
self.sound.cut_scene(audioKey)
|
||||||
|
|
||||||
|
# Clear screen after audio finishes
|
||||||
|
screen.fill((0, 0, 0))
|
||||||
|
pygame.display.flip()
|
||||||
|
else:
|
||||||
|
# No logo image found, just play audio
|
||||||
|
self.sound.cut_scene(audioKey)
|
||||||
|
|
||||||
|
def speak(self, text, interrupt=True, priority="normal", add_to_history=True):
|
||||||
"""Speak text using the speech system.
|
"""Speak text using the speech system.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
text (str): Text to speak
|
text (str): Text to speak
|
||||||
interrupt (bool): Whether to interrupt current speech
|
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:
|
Returns:
|
||||||
Game: Self for method chaining
|
Game: Self for method chaining
|
||||||
"""
|
"""
|
||||||
self.speech.speak(text, interrupt)
|
self.speech.speak(text, interrupt, priority, add_to_history)
|
||||||
return self
|
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):
|
def play_bgm(self, musicFile):
|
||||||
"""Play background music.
|
"""Play background music.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user