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 |
+23
-18
@@ -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
|
||||
@@ -51,7 +52,7 @@ from .input import get_input, check_for_exit, pause_game
|
||||
from .display import display_text, initialize_gui
|
||||
|
||||
# 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
|
||||
high_scores = Scoreboard.display_high_scores
|
||||
@@ -78,8 +79,8 @@ __version__ = '2.0.0'
|
||||
# Make all symbols available at the package level
|
||||
__all__ = [
|
||||
# Services
|
||||
'ConfigService', 'VolumeService', 'PathService',
|
||||
|
||||
'ConfigService', 'VolumeService', 'PathService', 'SpeechHistoryService',
|
||||
|
||||
# Sound
|
||||
'Sound',
|
||||
'play_bgm',
|
||||
@@ -97,32 +98,35 @@ __all__ = [
|
||||
'cut_scene',
|
||||
'play_random_falling',
|
||||
'calculate_volume_and_pan',
|
||||
|
||||
|
||||
# Speech
|
||||
'messagebox',
|
||||
'speak',
|
||||
'Speech',
|
||||
|
||||
'speak_previous',
|
||||
'speak_current',
|
||||
'speak_next',
|
||||
|
||||
# Scoreboard
|
||||
'Scoreboard',
|
||||
|
||||
|
||||
# Input
|
||||
'get_input', 'check_for_exit', 'pause_game',
|
||||
|
||||
|
||||
# Display
|
||||
'display_text', 'initialize_gui',
|
||||
|
||||
|
||||
# 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',
|
||||
|
||||
|
||||
# Utils
|
||||
'check_for_updates', 'get_version_tuple', 'check_compatibility',
|
||||
'sanitize_filename', 'lerp', 'smooth_step', 'distance_2d',
|
||||
'x_powerbar', 'y_powerbar', 'generate_tone',
|
||||
|
||||
|
||||
# Re-exported functions from pygame, math, random
|
||||
'get_ticks', 'delay', 'wait',
|
||||
'sin', 'cos', 'sqrt', 'floor', 'ceil',
|
||||
@@ -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
|
||||
@@ -141,10 +146,10 @@ def initialize_gui_with_services(gameTitle):
|
||||
"""Wrapper around initialize_gui that initializes services."""
|
||||
# Initialize path service
|
||||
pathService.initialize(gameTitle)
|
||||
|
||||
|
||||
# Connect config service to path service
|
||||
configService.set_game_info(gameTitle, pathService)
|
||||
|
||||
|
||||
# Call original initialize_gui
|
||||
return _originalInitializeGui(gameTitle)
|
||||
|
||||
@@ -159,11 +164,11 @@ def scoreboard_init_with_services(self, score=0, configService=None, speech=None
|
||||
# Use global services if not specified
|
||||
if configService is None:
|
||||
configService = ConfigService.get_instance()
|
||||
|
||||
|
||||
# Ensure pathService is connected if using defaults
|
||||
if not hasattr(configService, 'pathService') and pathService.game_path is not None:
|
||||
configService.pathService = pathService
|
||||
|
||||
|
||||
# Call original init with services
|
||||
_originalScoreboardInit(self, score, configService, speech)
|
||||
|
||||
|
||||
@@ -13,10 +13,10 @@ from xdg import BaseDirectory
|
||||
|
||||
class Config:
|
||||
"""Configuration management class for Storm Games."""
|
||||
|
||||
|
||||
def __init__(self, gameTitle):
|
||||
"""Initialize configuration system for a game.
|
||||
|
||||
|
||||
Args:
|
||||
gameTitle (str): Title of the game
|
||||
"""
|
||||
@@ -24,19 +24,19 @@ class Config:
|
||||
self.globalPath = os.path.join(BaseDirectory.xdg_config_home, "storm-games")
|
||||
self.gamePath = os.path.join(self.globalPath,
|
||||
str.lower(str.replace(gameTitle, " ", "-")))
|
||||
|
||||
|
||||
# Create game directory if it doesn't exist
|
||||
if not os.path.exists(self.gamePath):
|
||||
os.makedirs(self.gamePath)
|
||||
|
||||
|
||||
# Initialize config parsers
|
||||
self.localConfig = configparser.ConfigParser()
|
||||
self.globalConfig = configparser.ConfigParser()
|
||||
|
||||
|
||||
# Load existing configurations
|
||||
self.read_local_config()
|
||||
self.read_global_config()
|
||||
|
||||
|
||||
def read_local_config(self):
|
||||
"""Read local configuration from file."""
|
||||
try:
|
||||
@@ -44,7 +44,7 @@ class Config:
|
||||
self.localConfig.read_file(configFile)
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
def read_global_config(self):
|
||||
"""Read global configuration from file."""
|
||||
try:
|
||||
@@ -52,12 +52,12 @@ class Config:
|
||||
self.globalConfig.read_file(configFile)
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
def write_local_config(self):
|
||||
"""Write local configuration to file."""
|
||||
with open(os.path.join(self.gamePath, "config.ini"), 'w') as configFile:
|
||||
self.localConfig.write(configFile)
|
||||
|
||||
|
||||
def write_global_config(self):
|
||||
"""Write global configuration to file."""
|
||||
with open(os.path.join(self.globalPath, "config.ini"), 'w') as configFile:
|
||||
@@ -71,7 +71,7 @@ globalPath = ""
|
||||
|
||||
def write_config(writeGlobal=False):
|
||||
"""Write configuration to file.
|
||||
|
||||
|
||||
Args:
|
||||
writeGlobal (bool): If True, write to global config, otherwise local (default: False)
|
||||
"""
|
||||
@@ -84,7 +84,7 @@ def write_config(writeGlobal=False):
|
||||
|
||||
def read_config(readGlobal=False):
|
||||
"""Read configuration from file.
|
||||
|
||||
|
||||
Args:
|
||||
readGlobal (bool): If True, read global config, otherwise local (default: False)
|
||||
"""
|
||||
|
||||
+78
-32
@@ -23,81 +23,123 @@ displayTextUsageInstructions = False
|
||||
|
||||
def initialize_gui(gameTitle):
|
||||
"""Initialize the game GUI and sound system.
|
||||
|
||||
|
||||
Args:
|
||||
gameTitle (str): Title of the game
|
||||
|
||||
|
||||
Returns:
|
||||
dict: Dictionary of loaded sound objects
|
||||
"""
|
||||
# Initialize path service with game title
|
||||
pathService = PathService.get_instance().initialize(gameTitle)
|
||||
|
||||
|
||||
# Seed the random generator to the clock
|
||||
random.seed()
|
||||
|
||||
|
||||
# Set game's name
|
||||
setproctitle(str.lower(str.replace(gameTitle, " ", "-")))
|
||||
|
||||
|
||||
# Initialize pygame
|
||||
pygame.init()
|
||||
pygame.display.set_mode((800, 600))
|
||||
pygame.display.set_caption(gameTitle)
|
||||
|
||||
|
||||
# Set up audio system
|
||||
pygame.mixer.pre_init(44100, -16, 2, 1024)
|
||||
pygame.mixer.init()
|
||||
pygame.mixer.set_num_channels(32)
|
||||
pygame.mixer.set_reserved(0) # Reserve channel for cut scenes
|
||||
|
||||
|
||||
# Enable key repeat for volume controls
|
||||
pygame.key.set_repeat(500, 100)
|
||||
|
||||
|
||||
# Load sound files recursively including subdirectories
|
||||
soundData = {}
|
||||
try:
|
||||
import os
|
||||
|
||||
|
||||
soundDir = "sounds/"
|
||||
# Walk through directory tree
|
||||
for dirPath, dirNames, fileNames in os.walk(soundDir):
|
||||
# Get relative path from soundDir
|
||||
relPath = os.path.relpath(dirPath, soundDir)
|
||||
|
||||
|
||||
# Process each file
|
||||
for fileName in fileNames:
|
||||
# Check if file is a valid sound file
|
||||
if fileName.lower().endswith(('.ogg', '.wav')):
|
||||
# Full path to the sound file
|
||||
fullPath = os.path.join(dirPath, fileName)
|
||||
|
||||
|
||||
# Create sound key (remove extension)
|
||||
baseName = os.path.splitext(fileName)[0]
|
||||
|
||||
|
||||
# If in root sounds dir, just use basename
|
||||
if relPath == '.':
|
||||
soundKey = baseName
|
||||
else:
|
||||
# Otherwise use relative path + basename, normalized with forward slashes
|
||||
soundKey = os.path.join(relPath, baseName).replace('\\', '/')
|
||||
|
||||
|
||||
# Load the sound
|
||||
soundData[soundKey] = pygame.mixer.Sound(fullPath)
|
||||
except Exception as e:
|
||||
print("Error loading sounds:", e)
|
||||
Speech.get_instance().speak("Error loading sounds.", False)
|
||||
soundData = {}
|
||||
|
||||
# Play intro sound if available
|
||||
|
||||
# Play intro sound if available, optionally with visual logo
|
||||
from .sound import cut_scene
|
||||
if 'game-intro' in soundData:
|
||||
cut_scene(soundData, 'game-intro')
|
||||
|
||||
_show_logo_with_audio(soundData, 'game-intro')
|
||||
|
||||
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):
|
||||
"""Display and speak text with navigation controls.
|
||||
|
||||
|
||||
Allows users to:
|
||||
- Navigate text line by line with arrow keys (skipping blank lines)
|
||||
- Listen to full text with space
|
||||
@@ -107,20 +149,20 @@ def display_text(text):
|
||||
- Alt+PageUp/PageDown: Master volume up/down
|
||||
- Alt+Home/End: Background music volume up/down
|
||||
- Alt+Insert/Delete: Sound effects volume up/down
|
||||
|
||||
|
||||
Args:
|
||||
text (list): List of text lines to display
|
||||
"""
|
||||
# Get service instances
|
||||
speech = Speech.get_instance()
|
||||
volumeService = VolumeService.get_instance()
|
||||
|
||||
|
||||
# Store original text with blank lines for copying
|
||||
originalText = text.copy()
|
||||
|
||||
|
||||
# Create navigation text by filtering out blank lines
|
||||
navText = [line for line in text if line.strip()]
|
||||
|
||||
|
||||
# Add instructions at the start on the first display
|
||||
global displayTextUsageInstructions
|
||||
if not displayTextUsageInstructions:
|
||||
@@ -129,20 +171,23 @@ def display_text(text):
|
||||
"or t to copy the entire text. Press enter or escape when you are done reading.")
|
||||
navText.insert(0, instructions)
|
||||
displayTextUsageInstructions = True
|
||||
|
||||
|
||||
# Add end marker
|
||||
navText.append("End of text.")
|
||||
|
||||
|
||||
currentIndex = 0
|
||||
speech.speak(navText[currentIndex])
|
||||
|
||||
# Clear any pending events
|
||||
pygame.event.clear()
|
||||
|
||||
while True:
|
||||
event = pygame.event.wait()
|
||||
if event.type == pygame.KEYDOWN:
|
||||
# Check for Alt modifier
|
||||
mods = pygame.key.get_mods()
|
||||
altPressed = mods & pygame.KMOD_ALT
|
||||
|
||||
|
||||
# Volume controls (require Alt)
|
||||
if altPressed:
|
||||
if event.key == pygame.K_PAGEUP:
|
||||
@@ -160,26 +205,26 @@ def display_text(text):
|
||||
else:
|
||||
if event.key in (pygame.K_ESCAPE, pygame.K_RETURN):
|
||||
return
|
||||
|
||||
|
||||
if event.key in [pygame.K_DOWN, pygame.K_s] and currentIndex < len(navText) - 1:
|
||||
currentIndex += 1
|
||||
speech.speak(navText[currentIndex])
|
||||
|
||||
|
||||
if event.key in [pygame.K_UP, pygame.K_w] and currentIndex > 0:
|
||||
currentIndex -= 1
|
||||
speech.speak(navText[currentIndex])
|
||||
|
||||
|
||||
if event.key == pygame.K_SPACE:
|
||||
# Join with newlines to preserve spacing in speech
|
||||
speech.speak('\n'.join(originalText[1:-1]))
|
||||
|
||||
|
||||
if event.key == pygame.K_c:
|
||||
try:
|
||||
pyperclip.copy(navText[currentIndex])
|
||||
speech.speak("Copied " + navText[currentIndex] + " to the clipboard.")
|
||||
except:
|
||||
speech.speak("Failed to copy the text to the clipboard.")
|
||||
|
||||
|
||||
if event.key == pygame.K_t:
|
||||
try:
|
||||
# Join with newlines to preserve blank lines in full text
|
||||
@@ -187,6 +232,7 @@ def display_text(text):
|
||||
speech.speak("Copied entire message to the clipboard.")
|
||||
except:
|
||||
speech.speak("Failed to copy the text to the clipboard.")
|
||||
|
||||
event = pygame.event.clear()
|
||||
|
||||
pygame.event.pump()
|
||||
pygame.event.clear()
|
||||
time.sleep(0.001)
|
||||
|
||||
@@ -10,28 +10,200 @@ Provides functionality for:
|
||||
|
||||
import pygame
|
||||
import time
|
||||
import wx
|
||||
from .speech import speak
|
||||
|
||||
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:
|
||||
prompt (str): Prompt text to display (default: "Enter text:")
|
||||
text (str): Initial text in input box (default: "")
|
||||
|
||||
|
||||
Returns:
|
||||
str: User input text, or None if cancelled
|
||||
"""
|
||||
app = wx.App(False)
|
||||
dialog = wx.TextEntryDialog(None, prompt, "Input", text)
|
||||
dialog.SetValue(text)
|
||||
if dialog.ShowModal() == wx.ID_OK:
|
||||
userInput = dialog.GetValue()
|
||||
|
||||
# Initialize text buffer and cursor
|
||||
text_buffer = list(text) # Use list for easier character manipulation
|
||||
cursor_pos = len(text_buffer) # Start at end of initial text
|
||||
|
||||
# Announce the prompt and initial text as a single message
|
||||
if text:
|
||||
initial_message = f"{prompt} Default text: {text}"
|
||||
else:
|
||||
userInput = None
|
||||
dialog.Destroy()
|
||||
return userInput
|
||||
initial_message = f"{prompt} Empty text field"
|
||||
speak(initial_message)
|
||||
|
||||
# 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():
|
||||
"""Pauses the game until user presses backspace."""
|
||||
@@ -51,6 +223,10 @@ def pause_game():
|
||||
event = pygame.event.wait()
|
||||
if event.type == pygame.KEYDOWN and event.key == pygame.K_BACKSPACE:
|
||||
break
|
||||
|
||||
pygame.event.pump()
|
||||
pygame.event.clear()
|
||||
time.sleep(0.001)
|
||||
|
||||
try:
|
||||
pygame.mixer.unpause()
|
||||
@@ -66,7 +242,7 @@ def pause_game():
|
||||
|
||||
def check_for_exit():
|
||||
"""Check if user has pressed escape key.
|
||||
|
||||
|
||||
Returns:
|
||||
bool: True if escape was pressed, False otherwise
|
||||
"""
|
||||
|
||||
@@ -25,9 +25,97 @@ from .display import display_text
|
||||
from .scoreboard import Scoreboard
|
||||
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):
|
||||
"""Display and handle the main game menu with standard and custom options.
|
||||
|
||||
|
||||
Standard menu structure:
|
||||
1. Play (always first)
|
||||
2. High Scores
|
||||
@@ -37,53 +125,53 @@ def game_menu(sounds, playCallback=None, *customOptions):
|
||||
6. Credits (if available)
|
||||
7. Donate
|
||||
8. Exit
|
||||
|
||||
|
||||
Handles navigation with:
|
||||
- Up/Down arrows for selection
|
||||
- Home/End for first/last option
|
||||
- Enter to select
|
||||
- Escape to exit
|
||||
- Volume controls (with Alt modifier)
|
||||
|
||||
|
||||
Args:
|
||||
sounds (dict): Dictionary of sound objects
|
||||
playCallback (function, optional): Callback function for the "play" option.
|
||||
If None, "play" is returned as a string like other options.
|
||||
*customOptions: Additional custom options to include after play but before standard ones
|
||||
|
||||
|
||||
Returns:
|
||||
str: Selected menu option or "exit" if user pressed escape
|
||||
"""
|
||||
# Get speech instance
|
||||
speech = Speech.get_instance()
|
||||
|
||||
|
||||
# Start with Play option
|
||||
allOptions = ["play"]
|
||||
|
||||
|
||||
# Add high scores option if scores exist
|
||||
if Scoreboard.has_high_scores():
|
||||
allOptions.append("high_scores")
|
||||
|
||||
|
||||
# Add custom options (other menu items, etc.)
|
||||
allOptions.extend(customOptions)
|
||||
|
||||
|
||||
# Add standard options in preferred order
|
||||
allOptions.append("learn_sounds")
|
||||
|
||||
|
||||
# Check for instructions file
|
||||
if os.path.isfile('files/instructions.txt'):
|
||||
allOptions.append("instructions")
|
||||
|
||||
|
||||
# Check for credits file
|
||||
if os.path.isfile('files/credits.txt'):
|
||||
allOptions.append("credits")
|
||||
|
||||
|
||||
# Final options
|
||||
allOptions.extend(["donate", "exit_game"])
|
||||
|
||||
|
||||
# Track if music was previously playing
|
||||
musicWasPlaying = pygame.mixer.music.get_busy()
|
||||
|
||||
|
||||
# Only start menu music if no music is currently playing
|
||||
if not musicWasPlaying:
|
||||
try:
|
||||
@@ -91,23 +179,24 @@ def game_menu(sounds, playCallback=None, *customOptions):
|
||||
play_bgm("sounds/music_menu.ogg")
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
loop = True
|
||||
pygame.mixer.stop()
|
||||
currentIndex = 0
|
||||
lastSpoken = -1 # Track last spoken index
|
||||
|
||||
pygame.event.clear()
|
||||
|
||||
while loop:
|
||||
if currentIndex != lastSpoken:
|
||||
speech.speak(allOptions[currentIndex])
|
||||
lastSpoken = currentIndex
|
||||
|
||||
|
||||
event = pygame.event.wait()
|
||||
if event.type == pygame.KEYDOWN:
|
||||
# Check for Alt modifier
|
||||
mods = pygame.key.get_mods()
|
||||
altPressed = mods & pygame.KMOD_ALT
|
||||
|
||||
|
||||
# Volume controls (require Alt)
|
||||
if altPressed:
|
||||
if event.key == pygame.K_PAGEUP:
|
||||
@@ -169,9 +258,9 @@ def game_menu(sounds, playCallback=None, *customOptions):
|
||||
time.sleep(sounds['menu-select'].get_length())
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
selectedOption = allOptions[currentIndex]
|
||||
|
||||
|
||||
# Special case for exit_game with fade
|
||||
if selectedOption == "exit_game":
|
||||
exit_game(500 if pygame.mixer.music.get_busy() else 0)
|
||||
@@ -202,7 +291,7 @@ def game_menu(sounds, playCallback=None, *customOptions):
|
||||
pygame.mixer.music.pause()
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
# Handle standard options
|
||||
if selectedOption == "instructions":
|
||||
instructions()
|
||||
@@ -214,7 +303,7 @@ def game_menu(sounds, playCallback=None, *customOptions):
|
||||
Scoreboard.display_high_scores()
|
||||
elif selectedOption == "donate":
|
||||
donate()
|
||||
|
||||
|
||||
# Unpause music after function returns
|
||||
try:
|
||||
# Check if music is actually paused before trying to unpause
|
||||
@@ -248,105 +337,110 @@ def game_menu(sounds, playCallback=None, *customOptions):
|
||||
except:
|
||||
pass
|
||||
return allOptions[currentIndex]
|
||||
|
||||
event = pygame.event.clear()
|
||||
|
||||
pygame.event.pump()
|
||||
pygame.event.clear()
|
||||
time.sleep(0.001)
|
||||
|
||||
def learn_sounds(sounds):
|
||||
"""Interactive menu for learning game sounds.
|
||||
|
||||
|
||||
Allows users to:
|
||||
- Navigate through available sounds with up/down arrows
|
||||
- Navigate between sound categories (folders) using Page Up/Page Down or Left/Right arrows
|
||||
- Play selected sounds with Enter
|
||||
- Return to menu with Escape
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
Returns:
|
||||
str: "menu" if user exits with escape
|
||||
"""
|
||||
# Get speech instance
|
||||
speech = Speech.get_instance()
|
||||
|
||||
|
||||
# Define exclusion criteria
|
||||
excludedPrefixes = ["game-intro", "music_menu", "_"]
|
||||
excludedDirs = ["ambience", "."]
|
||||
|
||||
|
||||
# Organize sounds by directory
|
||||
soundsByDir = {}
|
||||
|
||||
|
||||
# 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
|
||||
parts = soundKey.split('/')
|
||||
|
||||
|
||||
# Skip if any part of the path is an excluded directory
|
||||
if any(part.lower() == dirName.lower() or part.startswith('.') for part in parts for dirName in excludedDirs):
|
||||
continue
|
||||
|
||||
|
||||
# Determine the directory
|
||||
if '/' in soundKey:
|
||||
directory = soundKey.split('/')[0]
|
||||
else:
|
||||
directory = 'root' # Root directory sounds
|
||||
|
||||
|
||||
# Add to sounds by directory
|
||||
if directory not in soundsByDir:
|
||||
soundsByDir[directory] = []
|
||||
soundsByDir[directory].append(soundKey)
|
||||
|
||||
|
||||
# Sort each directory's sounds
|
||||
for directory in soundsByDir:
|
||||
soundsByDir[directory].sort()
|
||||
|
||||
|
||||
# If no sounds found, inform the user and return
|
||||
if not soundsByDir:
|
||||
speech.speak("No sounds available to learn.")
|
||||
return "menu"
|
||||
|
||||
|
||||
# Get list of directories in sorted order
|
||||
directories = sorted(soundsByDir.keys())
|
||||
|
||||
|
||||
# Start with first directory
|
||||
currentDirIndex = 0
|
||||
currentDir = directories[currentDirIndex]
|
||||
currentSoundKeys = soundsByDir[currentDir]
|
||||
currentSoundIndex = 0
|
||||
|
||||
|
||||
# Display appropriate message based on number of directories
|
||||
if len(directories) > 1:
|
||||
messagebox(f"Starting with {currentDir if currentDir != 'root' else 'root directory'} sounds. Use left and right arrows or page up and page down to navigate categories.")
|
||||
|
||||
|
||||
# Track last spoken to avoid repetition
|
||||
lastSpoken = -1
|
||||
directoryChanged = True # Flag to track if directory just changed
|
||||
|
||||
|
||||
# Flag to track when to exit the loop
|
||||
returnToMenu = False
|
||||
|
||||
pygame.event.clear()
|
||||
|
||||
while not returnToMenu:
|
||||
# Announce current sound
|
||||
if currentSoundIndex != lastSpoken:
|
||||
totalSounds = len(currentSoundKeys)
|
||||
soundName = currentSoundKeys[currentSoundIndex]
|
||||
|
||||
|
||||
# Remove directory prefix if present
|
||||
if '/' in soundName:
|
||||
displayName = '/'.join(soundName.split('/')[1:])
|
||||
else:
|
||||
displayName = soundName
|
||||
|
||||
|
||||
# If directory just changed, include directory name in announcement
|
||||
if directoryChanged:
|
||||
dirDescription = "Root directory" if currentDir == 'root' else currentDir
|
||||
@@ -354,24 +448,24 @@ def learn_sounds(sounds):
|
||||
directoryChanged = False # Reset flag after announcement
|
||||
else:
|
||||
announcement = f"{displayName}, {currentSoundIndex + 1} of {totalSounds}"
|
||||
|
||||
|
||||
speech.speak(announcement)
|
||||
lastSpoken = currentSoundIndex
|
||||
|
||||
|
||||
event = pygame.event.wait()
|
||||
if event.type == pygame.KEYDOWN:
|
||||
if event.key == pygame.K_ESCAPE:
|
||||
returnToMenu = True
|
||||
|
||||
|
||||
# Sound navigation
|
||||
elif event.key in [pygame.K_DOWN, pygame.K_s] and currentSoundIndex < len(currentSoundKeys) - 1:
|
||||
pygame.mixer.stop()
|
||||
currentSoundIndex += 1
|
||||
|
||||
|
||||
elif event.key in [pygame.K_UP, pygame.K_w] and currentSoundIndex > 0:
|
||||
pygame.mixer.stop()
|
||||
currentSoundIndex -= 1
|
||||
|
||||
|
||||
# Directory navigation
|
||||
elif event.key in [pygame.K_PAGEDOWN, pygame.K_RIGHT] and currentDirIndex < len(directories) - 1:
|
||||
pygame.mixer.stop()
|
||||
@@ -381,7 +475,7 @@ def learn_sounds(sounds):
|
||||
currentSoundIndex = 0
|
||||
directoryChanged = True # Set flag on directory change
|
||||
lastSpoken = -1 # Force announcement
|
||||
|
||||
|
||||
elif event.key in [pygame.K_PAGEUP, pygame.K_LEFT] and currentDirIndex > 0:
|
||||
pygame.mixer.stop()
|
||||
currentDirIndex -= 1
|
||||
@@ -390,7 +484,7 @@ def learn_sounds(sounds):
|
||||
currentSoundIndex = 0
|
||||
directoryChanged = True # Set flag on directory change
|
||||
lastSpoken = -1 # Force announcement
|
||||
|
||||
|
||||
# Play sound
|
||||
elif event.key == pygame.K_RETURN:
|
||||
try:
|
||||
@@ -400,16 +494,16 @@ def learn_sounds(sounds):
|
||||
except Exception as e:
|
||||
print(f"Error playing sound: {e}")
|
||||
speech.speak("Could not play sound.")
|
||||
|
||||
|
||||
event = pygame.event.clear()
|
||||
pygame.event.pump() # Process pygame's internal events
|
||||
time.sleep(0.001)
|
||||
|
||||
|
||||
return "menu"
|
||||
|
||||
def instructions():
|
||||
"""Display game instructions from file.
|
||||
|
||||
|
||||
Reads and displays instructions from 'files/instructions.txt'.
|
||||
If file is missing, displays an error message.
|
||||
"""
|
||||
@@ -441,28 +535,28 @@ def credits():
|
||||
|
||||
def donate():
|
||||
"""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.")
|
||||
|
||||
def exit_game(fade=0):
|
||||
"""Clean up and exit the game properly.
|
||||
|
||||
|
||||
Args:
|
||||
fade (int): Milliseconds to fade out music before exiting.
|
||||
0 means stop immediately (default)
|
||||
"""
|
||||
# Force clear any pending events to prevent hanging
|
||||
pygame.event.clear()
|
||||
|
||||
|
||||
# Stop all mixer channels first
|
||||
try:
|
||||
pygame.mixer.stop()
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not stop mixer channels: {e}")
|
||||
|
||||
|
||||
# Get speech instance and handle all providers
|
||||
try:
|
||||
speech = Speech.get_instance()
|
||||
@@ -473,7 +567,7 @@ def exit_game(fade=0):
|
||||
print(f"Warning: Could not close speech: {e}")
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not get speech instance: {e}")
|
||||
|
||||
|
||||
# Handle music based on fade parameter
|
||||
try:
|
||||
if fade > 0 and pygame.mixer.music.get_busy():
|
||||
@@ -484,13 +578,19 @@ def exit_game(fade=0):
|
||||
pygame.mixer.music.stop()
|
||||
except Exception as 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
|
||||
try:
|
||||
pygame.quit()
|
||||
except Exception as e:
|
||||
print(f"Warning: Error during pygame.quit(): {e}")
|
||||
|
||||
|
||||
# Use os._exit for immediate termination
|
||||
import os
|
||||
os._exit(0)
|
||||
|
||||
@@ -5,4 +5,3 @@ pyxdg>=0.27
|
||||
setproctitle>=1.2.0
|
||||
numpy>=1.19.0
|
||||
accessible-output2>=0.14
|
||||
wxpython
|
||||
|
||||
+56
-56
@@ -18,10 +18,10 @@ from .config import localConfig, write_config, read_config
|
||||
|
||||
class Scoreboard:
|
||||
"""Handles high score tracking with player names."""
|
||||
|
||||
|
||||
def __init__(self, score=0, configService=None, speech=None):
|
||||
"""Initialize scoreboard.
|
||||
|
||||
|
||||
Args:
|
||||
score (int): Initial score (default: 0)
|
||||
configService (ConfigService): Config service (default: global instance)
|
||||
@@ -29,15 +29,15 @@ class Scoreboard:
|
||||
"""
|
||||
# Ensure services are properly initialized
|
||||
self._ensure_services()
|
||||
|
||||
|
||||
self.configService = configService or ConfigService.get_instance()
|
||||
self.speech = speech or Speech.get_instance()
|
||||
self.currentScore = score
|
||||
self.highScores = []
|
||||
|
||||
|
||||
# For backward compatibility
|
||||
read_config()
|
||||
|
||||
|
||||
try:
|
||||
# Try to use configService
|
||||
self.configService.localConfig.add_section("scoreboard")
|
||||
@@ -47,7 +47,7 @@ class Scoreboard:
|
||||
localConfig.add_section("scoreboard")
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
# Load existing high scores
|
||||
for i in range(1, 11):
|
||||
try:
|
||||
@@ -72,15 +72,15 @@ class Scoreboard:
|
||||
'name': "Player",
|
||||
'score': 0
|
||||
})
|
||||
|
||||
|
||||
# Sort high scores by score value in descending order
|
||||
self.highScores.sort(key=lambda x: x['score'], reverse=True)
|
||||
|
||||
|
||||
def _ensure_services(self):
|
||||
"""Ensure PathService and ConfigService are properly initialized."""
|
||||
# Get PathService and make sure it has a game name
|
||||
pathService = PathService.get_instance()
|
||||
|
||||
|
||||
# If no game name yet, try to get from pygame window title
|
||||
if not pathService.gameName:
|
||||
try:
|
||||
@@ -89,55 +89,55 @@ class Scoreboard:
|
||||
pathService.gameName = pygame.display.get_caption()[0]
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
# Initialize path service if we have a game name but no paths set up
|
||||
if pathService.gameName and not pathService.gamePath:
|
||||
pathService.initialize(pathService.gameName)
|
||||
|
||||
|
||||
# Get ConfigService and connect to PathService
|
||||
configService = ConfigService.get_instance()
|
||||
if not hasattr(configService, 'pathService') or not configService.pathService:
|
||||
if pathService.gameName:
|
||||
configService.set_game_info(pathService.gameName, pathService)
|
||||
|
||||
|
||||
# Ensure the game directory exists
|
||||
if pathService.gamePath and not os.path.exists(pathService.gamePath):
|
||||
try:
|
||||
os.makedirs(pathService.gamePath)
|
||||
except Exception as e:
|
||||
print(f"Error creating game directory: {e}")
|
||||
|
||||
|
||||
def get_score(self):
|
||||
"""Get current score."""
|
||||
return self.currentScore
|
||||
|
||||
|
||||
def get_high_scores(self):
|
||||
"""Get list of high scores."""
|
||||
return self.highScores
|
||||
|
||||
|
||||
def decrease_score(self, points=1):
|
||||
"""Decrease the current score."""
|
||||
self.currentScore -= int(points)
|
||||
return self
|
||||
|
||||
|
||||
def increase_score(self, points=1):
|
||||
"""Increase the current score."""
|
||||
self.currentScore += int(points)
|
||||
return self
|
||||
|
||||
|
||||
def set_score(self, score):
|
||||
"""Set the current score to a specific value."""
|
||||
self.currentScore = int(score)
|
||||
return self
|
||||
|
||||
|
||||
def reset_score(self):
|
||||
"""Reset the current score to zero."""
|
||||
self.currentScore = 0
|
||||
return self
|
||||
|
||||
|
||||
def check_high_score(self):
|
||||
"""Check if current score qualifies as a high score.
|
||||
|
||||
|
||||
Returns:
|
||||
int: Position (1-10) if high score, None if not
|
||||
"""
|
||||
@@ -145,47 +145,47 @@ class Scoreboard:
|
||||
if self.currentScore > entry['score']:
|
||||
return i + 1
|
||||
return None
|
||||
|
||||
|
||||
def add_high_score(self, name=None):
|
||||
"""Add current score to high scores if it qualifies.
|
||||
|
||||
|
||||
Args:
|
||||
name (str): Player name (if None, will prompt user)
|
||||
|
||||
|
||||
Returns:
|
||||
bool: True if score was added, False if not
|
||||
"""
|
||||
# Ensure services are properly set up
|
||||
self._ensure_services()
|
||||
|
||||
|
||||
position = self.check_high_score()
|
||||
if position is None:
|
||||
return False
|
||||
|
||||
|
||||
# Get player name
|
||||
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
|
||||
self.highScores.insert(position - 1, {
|
||||
'name': name,
|
||||
'score': self.currentScore
|
||||
})
|
||||
|
||||
|
||||
# Keep only top 10
|
||||
self.highScores = self.highScores[:10]
|
||||
|
||||
|
||||
# Save to config - try both methods for maximum compatibility
|
||||
try:
|
||||
# Try new method first
|
||||
for i, entry in enumerate(self.highScores):
|
||||
self.configService.localConfig.set("scoreboard", f"score_{i+1}", str(entry['score']))
|
||||
self.configService.localConfig.set("scoreboard", f"name_{i+1}", entry['name'])
|
||||
|
||||
|
||||
# Try to write with configService
|
||||
try:
|
||||
self.configService.write_local_config()
|
||||
@@ -196,7 +196,7 @@ class Scoreboard:
|
||||
localConfig.set("scoreboard", f"score_{i+1}", str(entry['score']))
|
||||
localConfig.set("scoreboard", f"name_{i+1}", entry['name'])
|
||||
write_config()
|
||||
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error writing high scores: {e}")
|
||||
# If all else fails, try direct old method
|
||||
@@ -204,7 +204,7 @@ class Scoreboard:
|
||||
localConfig.set("scoreboard", f"score_{i+1}", str(entry['score']))
|
||||
localConfig.set("scoreboard", f"name_{i+1}", entry['name'])
|
||||
write_config()
|
||||
|
||||
|
||||
# Announce success
|
||||
try:
|
||||
self.speech.messagebox(f"Congratulations {name}! You got position {position} on the scoreboard!")
|
||||
@@ -212,14 +212,14 @@ class Scoreboard:
|
||||
# Fallback to global speak function
|
||||
from .speech import speak
|
||||
speak(f"Congratulations {name}! You got position {position} on the scoreboard!")
|
||||
|
||||
|
||||
time.sleep(1)
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def has_high_scores():
|
||||
"""Check if the current game has any high scores.
|
||||
|
||||
|
||||
Returns:
|
||||
bool: True if at least one high score exists, False otherwise
|
||||
"""
|
||||
@@ -227,7 +227,7 @@ class Scoreboard:
|
||||
# Get PathService to access game name
|
||||
pathService = PathService.get_instance()
|
||||
gameName = pathService.gameName
|
||||
|
||||
|
||||
# If no game name, try to get from window title
|
||||
if not gameName:
|
||||
try:
|
||||
@@ -237,31 +237,31 @@ class Scoreboard:
|
||||
pathService.gameName = gameName
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
# Ensure path service is properly initialized
|
||||
if gameName and not pathService.gamePath:
|
||||
pathService.initialize(gameName)
|
||||
|
||||
|
||||
# Get the config file path
|
||||
configPath = os.path.join(pathService.gamePath, "config.ini")
|
||||
|
||||
|
||||
# If config file doesn't exist, there are no scores
|
||||
if not os.path.exists(configPath):
|
||||
return False
|
||||
|
||||
|
||||
# Ensure config service is properly connected to path service
|
||||
configService = ConfigService.get_instance()
|
||||
configService.set_game_info(gameName, pathService)
|
||||
|
||||
|
||||
# Create scoreboard using the properly initialized services
|
||||
board = Scoreboard(0, configService)
|
||||
|
||||
|
||||
# Force a read of local config to ensure fresh data
|
||||
configService.read_local_config()
|
||||
|
||||
|
||||
# Get high scores
|
||||
scores = board.get_high_scores()
|
||||
|
||||
|
||||
# Check if any score is greater than zero
|
||||
return any(score['score'] > 0 for score in scores)
|
||||
except Exception as e:
|
||||
@@ -271,7 +271,7 @@ class Scoreboard:
|
||||
@staticmethod
|
||||
def display_high_scores():
|
||||
"""Display high scores for the current game.
|
||||
|
||||
|
||||
Reads the high scores from Scoreboard class.
|
||||
Shows the game name at the top followed by the available scores.
|
||||
"""
|
||||
@@ -279,7 +279,7 @@ class Scoreboard:
|
||||
# Get PathService to access game name
|
||||
pathService = PathService.get_instance()
|
||||
gameName = pathService.gameName
|
||||
|
||||
|
||||
# If no game name, try to get from window title
|
||||
if not gameName:
|
||||
try:
|
||||
@@ -289,30 +289,30 @@ class Scoreboard:
|
||||
pathService.gameName = gameName
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
# Ensure path service is properly initialized
|
||||
if gameName and not pathService.gamePath:
|
||||
pathService.initialize(gameName)
|
||||
|
||||
|
||||
# Ensure config service is properly connected to path service
|
||||
configService = ConfigService.get_instance()
|
||||
configService.set_game_info(gameName, pathService)
|
||||
|
||||
|
||||
# Create scoreboard using the properly initialized services
|
||||
board = Scoreboard(0, configService)
|
||||
|
||||
|
||||
# Force a read of local config to ensure fresh data
|
||||
configService.read_local_config()
|
||||
|
||||
|
||||
# Get high scores
|
||||
scores = board.get_high_scores()
|
||||
|
||||
|
||||
# Filter out scores with zero points
|
||||
validScores = [score for score in scores if score['score'] > 0]
|
||||
|
||||
|
||||
# Prepare the lines to display
|
||||
lines = [f"High Scores for {gameName}:"]
|
||||
|
||||
|
||||
# Add scores to the display list
|
||||
if validScores:
|
||||
for i, entry in enumerate(validScores, 1):
|
||||
@@ -320,13 +320,13 @@ class Scoreboard:
|
||||
lines.append(scoreStr)
|
||||
else:
|
||||
lines.append("No high scores yet.")
|
||||
|
||||
|
||||
# Display the high scores
|
||||
display_text(lines)
|
||||
except Exception as e:
|
||||
print(f"Error displaying high scores: {e}")
|
||||
info = ["Could not display high scores."]
|
||||
display_text(info)
|
||||
|
||||
|
||||
# For backward compatibility with older code that might call displayHigh_scores
|
||||
displayHigh_scores = display_high_scores
|
||||
|
||||
+200
-36
@@ -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
|
||||
@@ -17,37 +20,37 @@ from .config import gamePath, globalPath, write_config, read_config
|
||||
|
||||
class ConfigService:
|
||||
"""Configuration management service."""
|
||||
|
||||
|
||||
_instance = None
|
||||
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls):
|
||||
"""Get or create the singleton instance."""
|
||||
if cls._instance is None:
|
||||
cls._instance = ConfigService()
|
||||
return cls._instance
|
||||
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize configuration parsers."""
|
||||
self.localConfig = configparser.ConfigParser()
|
||||
self.globalConfig = configparser.ConfigParser()
|
||||
self.gameTitle = None
|
||||
self.pathService = None
|
||||
|
||||
|
||||
def set_game_info(self, gameTitle, pathService):
|
||||
"""Set game information and initialize configs.
|
||||
|
||||
|
||||
Args:
|
||||
gameTitle (str): Title of the game
|
||||
pathService (PathService): Path service instance
|
||||
"""
|
||||
self.gameTitle = gameTitle
|
||||
self.pathService = pathService
|
||||
|
||||
|
||||
# Load existing configurations
|
||||
self.read_local_config()
|
||||
self.read_global_config()
|
||||
|
||||
|
||||
def read_local_config(self):
|
||||
"""Read local configuration from file."""
|
||||
try:
|
||||
@@ -66,7 +69,7 @@ class ConfigService:
|
||||
self.localConfig.read_dict(globals().get('localConfig', {}))
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
def read_global_config(self):
|
||||
"""Read global configuration from file."""
|
||||
try:
|
||||
@@ -85,7 +88,7 @@ class ConfigService:
|
||||
self.globalConfig.read_dict(globals().get('globalConfig', {}))
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
def write_local_config(self):
|
||||
"""Write local configuration to file."""
|
||||
try:
|
||||
@@ -104,7 +107,7 @@ class ConfigService:
|
||||
write_config(False)
|
||||
except Exception as e:
|
||||
print(f"Warning: Failed to write local config: {e}")
|
||||
|
||||
|
||||
def write_global_config(self):
|
||||
"""Write global configuration to file."""
|
||||
try:
|
||||
@@ -127,37 +130,37 @@ class ConfigService:
|
||||
|
||||
class VolumeService:
|
||||
"""Volume management service."""
|
||||
|
||||
|
||||
_instance = None
|
||||
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls):
|
||||
"""Get or create the singleton instance."""
|
||||
if cls._instance is None:
|
||||
cls._instance = VolumeService()
|
||||
return cls._instance
|
||||
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize volume settings."""
|
||||
self.bgmVolume = 0.75 # Default background music volume
|
||||
self.sfxVolume = 1.0 # Default sound effects volume
|
||||
self.masterVolume = 1.0 # Default master volume
|
||||
|
||||
|
||||
def adjust_master_volume(self, change, pygameMixer=None):
|
||||
"""Adjust the master volume for all sounds.
|
||||
|
||||
|
||||
Args:
|
||||
change (float): Amount to change volume by (positive or negative)
|
||||
pygameMixer: Optional pygame.mixer module for real-time updates
|
||||
"""
|
||||
self.masterVolume = max(0.0, min(1.0, self.masterVolume + change))
|
||||
|
||||
|
||||
# Update real-time audio if pygame mixer is provided
|
||||
if pygameMixer:
|
||||
# Update music volume
|
||||
if pygameMixer.music.get_busy():
|
||||
pygameMixer.music.set_volume(self.bgmVolume * self.masterVolume)
|
||||
|
||||
|
||||
# Update all sound channels
|
||||
for i in range(pygameMixer.get_num_channels()):
|
||||
channel = pygameMixer.Channel(i)
|
||||
@@ -170,29 +173,29 @@ class VolumeService:
|
||||
# Stereo audio
|
||||
left, right = currentVolume
|
||||
channel.set_volume(left * self.masterVolume, right * self.masterVolume)
|
||||
|
||||
|
||||
def adjust_bgm_volume(self, change, pygameMixer=None):
|
||||
"""Adjust only the background music volume.
|
||||
|
||||
|
||||
Args:
|
||||
change (float): Amount to change volume by (positive or negative)
|
||||
pygameMixer: Optional pygame.mixer module for real-time updates
|
||||
"""
|
||||
self.bgmVolume = max(0.0, min(1.0, self.bgmVolume + change))
|
||||
|
||||
|
||||
# Update real-time audio if pygame mixer is provided
|
||||
if pygameMixer and pygameMixer.music.get_busy():
|
||||
pygameMixer.music.set_volume(self.bgmVolume * self.masterVolume)
|
||||
|
||||
|
||||
def adjust_sfx_volume(self, change, pygameMixer=None):
|
||||
"""Adjust volume for sound effects only.
|
||||
|
||||
|
||||
Args:
|
||||
change (float): Amount to change volume by (positive or negative)
|
||||
pygameMixer: Optional pygame.mixer module for real-time updates
|
||||
"""
|
||||
self.sfxVolume = max(0.0, min(1.0, self.sfxVolume + change))
|
||||
|
||||
|
||||
# Update real-time audio if pygame mixer is provided
|
||||
if pygameMixer:
|
||||
# Update all sound channels except reserved ones
|
||||
@@ -208,18 +211,18 @@ class VolumeService:
|
||||
left, right = currentVolume
|
||||
channel.set_volume(left * self.sfxVolume * self.masterVolume,
|
||||
right * self.sfxVolume * self.masterVolume)
|
||||
|
||||
|
||||
def get_bgm_volume(self):
|
||||
"""Get the current BGM volume with master adjustment.
|
||||
|
||||
|
||||
Returns:
|
||||
float: Current adjusted BGM volume
|
||||
"""
|
||||
return self.bgmVolume * self.masterVolume
|
||||
|
||||
|
||||
def get_sfx_volume(self):
|
||||
"""Get the current SFX volume with master adjustment.
|
||||
|
||||
|
||||
Returns:
|
||||
float: Current adjusted SFX volume
|
||||
"""
|
||||
@@ -228,32 +231,32 @@ class VolumeService:
|
||||
|
||||
class PathService:
|
||||
"""Path management service."""
|
||||
|
||||
|
||||
_instance = None
|
||||
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls):
|
||||
"""Get or create the singleton instance."""
|
||||
if cls._instance is None:
|
||||
cls._instance = PathService()
|
||||
return cls._instance
|
||||
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize path variables."""
|
||||
self.globalPath = None
|
||||
self.gamePath = None
|
||||
self.gameName = None
|
||||
|
||||
|
||||
# Try to initialize from global variables for backward compatibility
|
||||
global gamePath, globalPath
|
||||
if gamePath:
|
||||
self.gamePath = gamePath
|
||||
if globalPath:
|
||||
self.globalPath = globalPath
|
||||
|
||||
|
||||
def initialize(self, gameTitle):
|
||||
"""Initialize paths for a game.
|
||||
|
||||
|
||||
Args:
|
||||
gameTitle (str): Title of the game
|
||||
"""
|
||||
@@ -261,14 +264,175 @@ class PathService:
|
||||
self.globalPath = os.path.join(BaseDirectory.xdg_config_home, "storm-games")
|
||||
self.gamePath = os.path.join(self.globalPath,
|
||||
str.lower(str.replace(gameTitle, " ", "-")))
|
||||
|
||||
|
||||
# Create game directory if it doesn't exist
|
||||
if not os.path.exists(self.gamePath):
|
||||
os.makedirs(self.gamePath)
|
||||
|
||||
|
||||
# Update global variables for backward compatibility
|
||||
global gamePath, globalPath
|
||||
gamePath = self.gamePath
|
||||
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']
|
||||
|
||||
@@ -13,28 +13,31 @@ 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."""
|
||||
|
||||
|
||||
_instance = None
|
||||
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls):
|
||||
"""Get or create the singleton instance."""
|
||||
if cls._instance is None:
|
||||
cls._instance = Speech()
|
||||
return cls._instance
|
||||
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize speech system with available provider."""
|
||||
# Handle speech delays so we don't get stuttering
|
||||
self.lastSpoken = {"text": None, "time": 0}
|
||||
self.speechDelay = 250 # ms
|
||||
|
||||
|
||||
# Try to initialize a speech provider
|
||||
self.provider = None
|
||||
self.providerName = None
|
||||
|
||||
|
||||
# Try speechd first
|
||||
try:
|
||||
import speechd
|
||||
@@ -44,7 +47,7 @@ class Speech:
|
||||
return
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
# Try accessible_output2 next
|
||||
try:
|
||||
import accessible_output2.outputs.auto
|
||||
@@ -54,44 +57,79 @@ class Speech:
|
||||
return
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
# Update last spoken tracking
|
||||
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()
|
||||
if not screen:
|
||||
return
|
||||
|
||||
|
||||
font = pygame.font.Font(None, 36)
|
||||
# Wrap the text
|
||||
maxWidth = screen.get_width() - 40 # Leave a 20-pixel margin on each side
|
||||
@@ -109,7 +147,7 @@ class Speech:
|
||||
screen.blit(surface, textRect)
|
||||
currentY += surface.get_height()
|
||||
pygame.display.flip()
|
||||
|
||||
|
||||
def close(self):
|
||||
"""Clean up speech resources."""
|
||||
if self.providerName == "speechd":
|
||||
@@ -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)
|
||||
|
||||
@@ -26,137 +26,234 @@ from .scoreboard import Scoreboard
|
||||
|
||||
class Game:
|
||||
"""Central class to manage all game systems."""
|
||||
|
||||
|
||||
def __init__(self, title):
|
||||
"""Initialize a new game.
|
||||
|
||||
|
||||
Args:
|
||||
title (str): Title of the game
|
||||
"""
|
||||
self.title = title
|
||||
|
||||
|
||||
# Initialize services
|
||||
self.pathService = PathService.get_instance().initialize(title)
|
||||
self.configService = ConfigService.get_instance()
|
||||
self.configService.set_game_info(title, self.pathService)
|
||||
self.volumeService = VolumeService.get_instance()
|
||||
|
||||
|
||||
# Initialize game components (lazy loaded)
|
||||
self._speech = None
|
||||
self._sound = None
|
||||
self._scoreboard = None
|
||||
|
||||
|
||||
# Display text instructions flag
|
||||
self.displayTextUsageInstructions = False
|
||||
|
||||
|
||||
@property
|
||||
def speech(self):
|
||||
"""Get the speech system (lazy loaded).
|
||||
|
||||
|
||||
Returns:
|
||||
Speech: Speech system instance
|
||||
"""
|
||||
if not self._speech:
|
||||
self._speech = Speech.get_instance()
|
||||
return self._speech
|
||||
|
||||
|
||||
@property
|
||||
def sound(self):
|
||||
"""Get the sound system (lazy loaded).
|
||||
|
||||
|
||||
Returns:
|
||||
Sound: Sound system instance
|
||||
"""
|
||||
if not self._sound:
|
||||
self._sound = Sound("sounds/", self.volumeService)
|
||||
return self._sound
|
||||
|
||||
|
||||
@property
|
||||
def scoreboard(self):
|
||||
"""Get the scoreboard (lazy loaded).
|
||||
|
||||
|
||||
Returns:
|
||||
Scoreboard: Scoreboard instance
|
||||
"""
|
||||
if not self._scoreboard:
|
||||
self._scoreboard = Scoreboard(self.configService)
|
||||
return self._scoreboard
|
||||
|
||||
|
||||
def initialize(self):
|
||||
"""Initialize the game GUI and sound system.
|
||||
|
||||
|
||||
Returns:
|
||||
Game: Self for method chaining
|
||||
"""
|
||||
# Set process title
|
||||
setproctitle(str.lower(str.replace(self.title, " ", "")))
|
||||
|
||||
|
||||
# Seed the random generator
|
||||
random.seed()
|
||||
|
||||
|
||||
# Initialize pygame
|
||||
pygame.init()
|
||||
pygame.display.set_mode((800, 600))
|
||||
pygame.display.set_caption(self.title)
|
||||
|
||||
|
||||
# Set up audio system
|
||||
pygame.mixer.pre_init(44100, -16, 2, 1024)
|
||||
pygame.mixer.init()
|
||||
pygame.mixer.set_num_channels(32)
|
||||
pygame.mixer.set_reserved(0) # Reserve channel for cut scenes
|
||||
|
||||
|
||||
# Enable key repeat for volume controls
|
||||
pygame.key.set_repeat(500, 100)
|
||||
|
||||
|
||||
# Load sound effects
|
||||
self.sound
|
||||
|
||||
# Play intro sound if available
|
||||
|
||||
# Play intro sound if available, optionally with visual logo
|
||||
if 'game-intro' in self.sound.sounds:
|
||||
self.sound.cut_scene('game-intro')
|
||||
|
||||
self._show_logo_with_audio('game-intro')
|
||||
|
||||
return self
|
||||
|
||||
def speak(self, text, interrupt=True):
|
||||
"""Speak text using the speech system.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
|
||||
Args:
|
||||
musicFile (str): Path to music file
|
||||
|
||||
|
||||
Returns:
|
||||
Game: Self for method chaining
|
||||
"""
|
||||
self.sound.play_bgm(musicFile)
|
||||
return self
|
||||
|
||||
|
||||
def display_text(self, textLines):
|
||||
"""Display text with navigation controls.
|
||||
|
||||
|
||||
Args:
|
||||
textLines (list): List of text lines
|
||||
|
||||
|
||||
Returns:
|
||||
Game: Self for method chaining
|
||||
"""
|
||||
# Store original text with blank lines for copying
|
||||
originalText = textLines.copy()
|
||||
|
||||
|
||||
# Create navigation text by filtering out blank lines
|
||||
navText = [line for line in textLines if line.strip()]
|
||||
|
||||
|
||||
# Add instructions at the start on the first display
|
||||
if not self.displayTextUsageInstructions:
|
||||
instructions = ("Press space to read the whole text. Use up and down arrows to navigate "
|
||||
@@ -164,20 +261,20 @@ class Game:
|
||||
"or t to copy the entire text. Press enter or escape when you are done reading.")
|
||||
navText.insert(0, instructions)
|
||||
self.displayTextUsageInstructions = True
|
||||
|
||||
|
||||
# Add end marker
|
||||
navText.append("End of text.")
|
||||
|
||||
|
||||
currentIndex = 0
|
||||
self.speech.speak(navText[currentIndex])
|
||||
|
||||
|
||||
while True:
|
||||
event = pygame.event.wait()
|
||||
if event.type == pygame.KEYDOWN:
|
||||
# Check for Alt modifier
|
||||
mods = pygame.key.get_mods()
|
||||
altPressed = mods & pygame.KMOD_ALT
|
||||
|
||||
|
||||
# Volume controls (require Alt)
|
||||
if altPressed:
|
||||
if event.key == pygame.K_PAGEUP:
|
||||
@@ -195,19 +292,19 @@ class Game:
|
||||
else:
|
||||
if event.key in (pygame.K_ESCAPE, pygame.K_RETURN):
|
||||
return self
|
||||
|
||||
|
||||
if event.key in [pygame.K_DOWN, pygame.K_s] and currentIndex < len(navText) - 1:
|
||||
currentIndex += 1
|
||||
self.speech.speak(navText[currentIndex])
|
||||
|
||||
|
||||
if event.key in [pygame.K_UP, pygame.K_w] and currentIndex > 0:
|
||||
currentIndex -= 1
|
||||
self.speech.speak(navText[currentIndex])
|
||||
|
||||
|
||||
if event.key == pygame.K_SPACE:
|
||||
# Join with newlines to preserve spacing in speech
|
||||
self.speech.speak('\n'.join(originalText[1:-1]))
|
||||
|
||||
|
||||
if event.key == pygame.K_c:
|
||||
try:
|
||||
import pyperclip
|
||||
@@ -215,7 +312,7 @@ class Game:
|
||||
self.speech.speak("Copied " + navText[currentIndex] + " to the clipboard.")
|
||||
except:
|
||||
self.speech.speak("Failed to copy the text to the clipboard.")
|
||||
|
||||
|
||||
if event.key == pygame.K_t:
|
||||
try:
|
||||
import pyperclip
|
||||
@@ -224,10 +321,10 @@ class Game:
|
||||
self.speech.speak("Copied entire message to the clipboard.")
|
||||
except:
|
||||
self.speech.speak("Failed to copy the text to the clipboard.")
|
||||
|
||||
|
||||
pygame.event.clear()
|
||||
time.sleep(0.001)
|
||||
|
||||
|
||||
def exit(self):
|
||||
"""Clean up and exit the game."""
|
||||
if self._speech and self.speech.providerName == "speechd":
|
||||
@@ -241,12 +338,12 @@ class Game:
|
||||
|
||||
def check_for_updates(currentVersion, gameName, url):
|
||||
"""Check for game updates.
|
||||
|
||||
|
||||
Args:
|
||||
currentVersion (str): Current version string (e.g. "1.0.0")
|
||||
gameName (str): Name of the game
|
||||
url (str): URL to check for updates
|
||||
|
||||
|
||||
Returns:
|
||||
dict: Update information or None if no update available
|
||||
"""
|
||||
@@ -266,10 +363,10 @@ def check_for_updates(currentVersion, gameName, url):
|
||||
|
||||
def get_version_tuple(versionStr):
|
||||
"""Convert version string to comparable tuple.
|
||||
|
||||
|
||||
Args:
|
||||
versionStr (str): Version string (e.g. "1.0.0")
|
||||
|
||||
|
||||
Returns:
|
||||
tuple: Version as tuple of integers
|
||||
"""
|
||||
@@ -277,11 +374,11 @@ def get_version_tuple(versionStr):
|
||||
|
||||
def check_compatibility(requiredVersion, currentVersion):
|
||||
"""Check if current version meets minimum required version.
|
||||
|
||||
|
||||
Args:
|
||||
requiredVersion (str): Minimum required version string
|
||||
currentVersion (str): Current version string
|
||||
|
||||
|
||||
Returns:
|
||||
bool: True if compatible, False otherwise
|
||||
"""
|
||||
@@ -291,10 +388,10 @@ def check_compatibility(requiredVersion, currentVersion):
|
||||
|
||||
def sanitize_filename(filename):
|
||||
"""Sanitize a filename to be safe for all operating systems.
|
||||
|
||||
|
||||
Args:
|
||||
filename (str): Original filename
|
||||
|
||||
|
||||
Returns:
|
||||
str: Sanitized filename
|
||||
"""
|
||||
@@ -309,12 +406,12 @@ def sanitize_filename(filename):
|
||||
|
||||
def lerp(start, end, factor):
|
||||
"""Linear interpolation between two values.
|
||||
|
||||
|
||||
Args:
|
||||
start (float): Start value
|
||||
end (float): End value
|
||||
factor (float): Interpolation factor (0.0-1.0)
|
||||
|
||||
|
||||
Returns:
|
||||
float: Interpolated value
|
||||
"""
|
||||
@@ -322,12 +419,12 @@ def lerp(start, end, factor):
|
||||
|
||||
def smooth_step(edge0, edge1, x):
|
||||
"""Hermite interpolation between two values.
|
||||
|
||||
|
||||
Args:
|
||||
edge0 (float): Start edge
|
||||
edge1 (float): End edge
|
||||
x (float): Value to interpolate
|
||||
|
||||
|
||||
Returns:
|
||||
float: Interpolated value with smooth step
|
||||
"""
|
||||
@@ -338,13 +435,13 @@ def smooth_step(edge0, edge1, x):
|
||||
|
||||
def distance_2d(x1, y1, x2, y2):
|
||||
"""Calculate Euclidean distance between two 2D points.
|
||||
|
||||
|
||||
Args:
|
||||
x1 (float): X coordinate of first point
|
||||
y1 (float): Y coordinate of first point
|
||||
x2 (float): X coordinate of second point
|
||||
y2 (float): Y coordinate of second point
|
||||
|
||||
|
||||
Returns:
|
||||
float: Distance between points
|
||||
"""
|
||||
@@ -352,17 +449,17 @@ def distance_2d(x1, y1, x2, y2):
|
||||
|
||||
def generate_tone(frequency, duration=0.1, sampleRate=44100, volume=0.2):
|
||||
"""Generate a tone at the specified frequency.
|
||||
|
||||
|
||||
Args:
|
||||
frequency (float): Frequency in Hz
|
||||
duration (float): Duration in seconds (default: 0.1)
|
||||
sampleRate (int): Sample rate in Hz (default: 44100)
|
||||
volume (float): Volume from 0.0 to 1.0 (default: 0.2)
|
||||
|
||||
|
||||
Returns:
|
||||
pygame.mixer.Sound: Sound object with the generated tone
|
||||
"""
|
||||
|
||||
|
||||
t = np.linspace(0, duration, int(sampleRate * duration), False)
|
||||
tone = np.sin(2 * np.pi * frequency * t)
|
||||
stereoTone = np.vstack((tone, tone)).T # Create a 2D array for stereo
|
||||
@@ -372,17 +469,17 @@ def generate_tone(frequency, duration=0.1, sampleRate=44100, volume=0.2):
|
||||
|
||||
def x_powerbar():
|
||||
"""Sound based horizontal power bar
|
||||
|
||||
|
||||
Returns:
|
||||
int: Selected position between -50 and 50
|
||||
"""
|
||||
|
||||
|
||||
clock = pygame.time.Clock()
|
||||
screen = pygame.display.get_surface()
|
||||
position = -50 # Start from the leftmost position
|
||||
direction = 1 # Move right initially
|
||||
barHeight = 20
|
||||
|
||||
|
||||
while True:
|
||||
frequency = 440 # A4 note
|
||||
leftVolume = (50 - position) / 100
|
||||
@@ -390,7 +487,7 @@ def x_powerbar():
|
||||
tone = generate_tone(frequency)
|
||||
channel = tone.play()
|
||||
channel.set_volume(leftVolume, rightVolume)
|
||||
|
||||
|
||||
# Visual representation
|
||||
screen.fill((0, 0, 0))
|
||||
barWidth = screen.get_width() - 40 # Leave 20px margin on each side
|
||||
@@ -398,13 +495,13 @@ def x_powerbar():
|
||||
markerPos = int(20 + (position + 50) / 100 * barWidth)
|
||||
pygame.draw.rect(screen, (255, 0, 0), (markerPos - 5, screen.get_height() // 2 - barHeight, 10, barHeight * 2))
|
||||
pygame.display.flip()
|
||||
|
||||
|
||||
for event in pygame.event.get():
|
||||
check_for_exit()
|
||||
if event.type == pygame.KEYDOWN and event.key == pygame.K_SPACE:
|
||||
channel.stop()
|
||||
return position # This will return a value between -50 and 50
|
||||
|
||||
|
||||
position += direction
|
||||
if position > 50:
|
||||
position = 50
|
||||
@@ -412,27 +509,27 @@ def x_powerbar():
|
||||
elif position < -50:
|
||||
position = -50
|
||||
direction = 1
|
||||
|
||||
|
||||
clock.tick(40) # Speed of bar
|
||||
|
||||
def y_powerbar():
|
||||
"""Sound based vertical power bar
|
||||
|
||||
|
||||
Returns:
|
||||
int: Selected power level between 0 and 100
|
||||
"""
|
||||
|
||||
|
||||
clock = pygame.time.Clock()
|
||||
screen = pygame.display.get_surface()
|
||||
power = 0
|
||||
direction = 1 # 1 for increasing, -1 for decreasing
|
||||
barWidth = 20
|
||||
|
||||
|
||||
while True:
|
||||
frequency = 220 + (power * 5) # Adjust these values to change the pitch range
|
||||
tone = generate_tone(frequency)
|
||||
channel = tone.play()
|
||||
|
||||
|
||||
# Visual representation
|
||||
screen.fill((0, 0, 0))
|
||||
barHeight = screen.get_height() - 40 # Leave 20px margin on top and bottom
|
||||
@@ -440,15 +537,15 @@ def y_powerbar():
|
||||
markerPos = int(20 + (100 - power) / 100 * barHeight)
|
||||
pygame.draw.rect(screen, (255, 0, 0), (screen.get_width() // 2 - barWidth, markerPos - 5, barWidth * 2, 10))
|
||||
pygame.display.flip()
|
||||
|
||||
|
||||
for event in pygame.event.get():
|
||||
check_for_exit()
|
||||
if event.type == pygame.KEYDOWN and event.key == pygame.K_SPACE:
|
||||
channel.stop()
|
||||
return power
|
||||
|
||||
|
||||
power += direction
|
||||
if power >= 100 or power <= 0:
|
||||
direction *= -1 # Reverse direction at limits
|
||||
|
||||
|
||||
clock.tick(40)
|
||||
|
||||
Reference in New Issue
Block a user