Code cleanup and sound consolidation.

This commit is contained in:
Storm Dragon 2025-03-22 17:34:35 -04:00
parent 3a478d15d5
commit a17a4c6f15
10 changed files with 732 additions and 986 deletions

View File

@ -79,7 +79,7 @@ __version__ = '2.0.0'
__all__ = [ __all__ = [
# Services # Services
'ConfigService', 'VolumeService', 'PathService', 'ConfigService', 'VolumeService', 'PathService',
# Sound # Sound
'Sound', 'Sound',
'play_bgm', 'play_bgm',
@ -97,32 +97,32 @@ __all__ = [
'cut_scene', 'cut_scene',
'play_random_falling', 'play_random_falling',
'calculate_volume_and_pan', 'calculate_volume_and_pan',
# Speech # Speech
'messagebox', 'messagebox',
'speak', 'speak',
'Speech', 'Speech',
# Scoreboard # Scoreboard
'Scoreboard', 'Scoreboard',
# Input # Input
'get_input', 'check_for_exit', 'pause_game', 'get_input', 'check_for_exit', 'pause_game',
# Display # Display
'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', 'learn_sounds', 'instructions', 'credits', 'donate', 'exit_game', 'high_scores', 'has_high_scores',
# Game class # Game class
'Game', 'Game',
# Utils # Utils
'check_for_updates', 'get_version_tuple', 'check_compatibility', 'check_for_updates', 'get_version_tuple', 'check_compatibility',
'sanitize_filename', 'lerp', 'smooth_step', 'distance_2d', 'sanitize_filename', 'lerp', 'smooth_step', 'distance_2d',
'x_powerbar', 'y_powerbar', 'generate_tone', 'x_powerbar', 'y_powerbar', 'generate_tone',
# Re-exported functions from pygame, math, random # Re-exported functions from pygame, math, random
'get_ticks', 'delay', 'wait', 'get_ticks', 'delay', 'wait',
'sin', 'cos', 'sqrt', 'floor', 'ceil', 'sin', 'cos', 'sqrt', 'floor', 'ceil',
@ -141,10 +141,10 @@ def initialize_gui_with_services(gameTitle):
"""Wrapper around initialize_gui that initializes services.""" """Wrapper around initialize_gui that initializes services."""
# Initialize path service # Initialize path service
pathService.initialize(gameTitle) pathService.initialize(gameTitle)
# Connect config service to path service # Connect config service to path service
configService.set_game_info(gameTitle, pathService) configService.set_game_info(gameTitle, pathService)
# Call original initialize_gui # Call original initialize_gui
return _originalInitializeGui(gameTitle) return _originalInitializeGui(gameTitle)
@ -159,11 +159,11 @@ def scoreboard_init_with_services(self, score=0, configService=None, speech=None
# Use global services if not specified # Use global services if not specified
if configService is None: if configService is None:
configService = ConfigService.get_instance() configService = ConfigService.get_instance()
# Ensure pathService is connected if using defaults # Ensure pathService is connected if using defaults
if not hasattr(configService, 'pathService') and pathService.game_path is not None: if not hasattr(configService, 'pathService') and pathService.game_path is not None:
configService.pathService = pathService configService.pathService = pathService
# Call original init with services # Call original init with services
_originalScoreboardInit(self, score, configService, speech) _originalScoreboardInit(self, score, configService, speech)

View File

@ -13,10 +13,10 @@ from xdg import BaseDirectory
class Config: class Config:
"""Configuration management class for Storm Games.""" """Configuration management class for Storm Games."""
def __init__(self, gameTitle): def __init__(self, gameTitle):
"""Initialize configuration system for a game. """Initialize configuration system for a game.
Args: Args:
gameTitle (str): Title of the game gameTitle (str): Title of the game
""" """
@ -24,19 +24,19 @@ class Config:
self.globalPath = os.path.join(BaseDirectory.xdg_config_home, "storm-games") self.globalPath = os.path.join(BaseDirectory.xdg_config_home, "storm-games")
self.gamePath = os.path.join(self.globalPath, self.gamePath = os.path.join(self.globalPath,
str.lower(str.replace(gameTitle, " ", "-"))) str.lower(str.replace(gameTitle, " ", "-")))
# Create game directory if it doesn't exist # Create game directory if it doesn't exist
if not os.path.exists(self.gamePath): if not os.path.exists(self.gamePath):
os.makedirs(self.gamePath) os.makedirs(self.gamePath)
# Initialize config parsers # Initialize config parsers
self.localConfig = configparser.ConfigParser() self.localConfig = configparser.ConfigParser()
self.globalConfig = configparser.ConfigParser() self.globalConfig = configparser.ConfigParser()
# Load existing configurations # Load existing configurations
self.read_local_config() self.read_local_config()
self.read_global_config() self.read_global_config()
def read_local_config(self): def read_local_config(self):
"""Read local configuration from file.""" """Read local configuration from file."""
try: try:
@ -44,7 +44,7 @@ class Config:
self.localConfig.read_file(configFile) self.localConfig.read_file(configFile)
except: except:
pass pass
def read_global_config(self): def read_global_config(self):
"""Read global configuration from file.""" """Read global configuration from file."""
try: try:
@ -52,12 +52,12 @@ class Config:
self.globalConfig.read_file(configFile) self.globalConfig.read_file(configFile)
except: except:
pass pass
def write_local_config(self): def write_local_config(self):
"""Write local configuration to file.""" """Write local configuration to file."""
with open(os.path.join(self.gamePath, "config.ini"), 'w') as configFile: with open(os.path.join(self.gamePath, "config.ini"), 'w') as configFile:
self.localConfig.write(configFile) self.localConfig.write(configFile)
def write_global_config(self): def write_global_config(self):
"""Write global configuration to file.""" """Write global configuration to file."""
with open(os.path.join(self.globalPath, "config.ini"), 'w') as configFile: with open(os.path.join(self.globalPath, "config.ini"), 'w') as configFile:
@ -71,7 +71,7 @@ globalPath = ""
def write_config(writeGlobal=False): def write_config(writeGlobal=False):
"""Write configuration to file. """Write configuration to file.
Args: Args:
writeGlobal (bool): If True, write to global config, otherwise local (default: False) 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): def read_config(readGlobal=False):
"""Read configuration from file. """Read configuration from file.
Args: Args:
readGlobal (bool): If True, read global config, otherwise local (default: False) readGlobal (bool): If True, read global config, otherwise local (default: False)
""" """

View File

@ -23,81 +23,81 @@ displayTextUsageInstructions = False
def initialize_gui(gameTitle): def initialize_gui(gameTitle):
"""Initialize the game GUI and sound system. """Initialize the game GUI and sound system.
Args: Args:
gameTitle (str): Title of the game gameTitle (str): Title of the game
Returns: Returns:
dict: Dictionary of loaded sound objects dict: Dictionary of loaded sound objects
""" """
# Initialize path service with game title # Initialize path service with game title
pathService = PathService.get_instance().initialize(gameTitle) pathService = PathService.get_instance().initialize(gameTitle)
# Seed the random generator to the clock # Seed the random generator to the clock
random.seed() random.seed()
# Set game's name # Set game's name
setproctitle(str.lower(str.replace(gameTitle, " ", "-"))) setproctitle(str.lower(str.replace(gameTitle, " ", "-")))
# Initialize pygame # Initialize pygame
pygame.init() pygame.init()
pygame.display.set_mode((800, 600)) pygame.display.set_mode((800, 600))
pygame.display.set_caption(gameTitle) pygame.display.set_caption(gameTitle)
# Set up audio system # Set up audio system
pygame.mixer.pre_init(44100, -16, 2, 1024) pygame.mixer.pre_init(44100, -16, 2, 1024)
pygame.mixer.init() pygame.mixer.init()
pygame.mixer.set_num_channels(32) pygame.mixer.set_num_channels(32)
pygame.mixer.set_reserved(0) # Reserve channel for cut scenes pygame.mixer.set_reserved(0) # Reserve channel for cut scenes
# Enable key repeat for volume controls # Enable key repeat for volume controls
pygame.key.set_repeat(500, 100) pygame.key.set_repeat(500, 100)
# Load sound files recursively including subdirectories # Load sound files recursively including subdirectories
soundData = {} soundData = {}
try: try:
import os import os
soundDir = "sounds/" soundDir = "sounds/"
# Walk through directory tree # Walk through directory tree
for dirPath, dirNames, fileNames in os.walk(soundDir): for dirPath, dirNames, fileNames in os.walk(soundDir):
# Get relative path from soundDir # Get relative path from soundDir
relPath = os.path.relpath(dirPath, soundDir) relPath = os.path.relpath(dirPath, soundDir)
# Process each file # Process each file
for fileName in fileNames: for fileName in fileNames:
# Check if file is a valid sound file # Check if file is a valid sound file
if fileName.lower().endswith(('.ogg', '.wav')): if fileName.lower().endswith(('.ogg', '.wav')):
# Full path to the sound file # Full path to the sound file
fullPath = os.path.join(dirPath, fileName) fullPath = os.path.join(dirPath, fileName)
# Create sound key (remove extension) # Create sound key (remove extension)
baseName = os.path.splitext(fileName)[0] baseName = os.path.splitext(fileName)[0]
# If in root sounds dir, just use basename # If in root sounds dir, just use basename
if relPath == '.': if relPath == '.':
soundKey = baseName soundKey = baseName
else: else:
# Otherwise use relative path + basename, normalized with forward slashes # Otherwise use relative path + basename, normalized with forward slashes
soundKey = os.path.join(relPath, baseName).replace('\\', '/') soundKey = os.path.join(relPath, baseName).replace('\\', '/')
# Load the sound # Load the sound
soundData[soundKey] = pygame.mixer.Sound(fullPath) soundData[soundKey] = pygame.mixer.Sound(fullPath)
except Exception as e: except Exception as e:
print("Error loading sounds:", e) print("Error loading sounds:", e)
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
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') cut_scene(soundData, 'game-intro')
return soundData return soundData
def display_text(text): def display_text(text):
"""Display and speak text with navigation controls. """Display and speak text with navigation controls.
Allows users to: Allows users to:
- Navigate text line by line with arrow keys (skipping blank lines) - Navigate text line by line with arrow keys (skipping blank lines)
- Listen to full text with space - Listen to full text with space
@ -107,20 +107,20 @@ def display_text(text):
- Alt+PageUp/PageDown: Master volume up/down - Alt+PageUp/PageDown: Master volume up/down
- Alt+Home/End: Background music volume up/down - Alt+Home/End: Background music volume up/down
- Alt+Insert/Delete: Sound effects volume up/down - Alt+Insert/Delete: Sound effects volume up/down
Args: Args:
text (list): List of text lines to display text (list): List of text lines to display
""" """
# Get service instances # Get service instances
speech = Speech.get_instance() speech = Speech.get_instance()
volumeService = VolumeService.get_instance() volumeService = VolumeService.get_instance()
# Store original text with blank lines for copying # Store original text with blank lines for copying
originalText = text.copy() originalText = text.copy()
# Create navigation text by filtering out blank lines # Create navigation text by filtering out blank lines
navText = [line for line in text if line.strip()] navText = [line for line in text if line.strip()]
# Add instructions at the start on the first display # Add instructions at the start on the first display
global displayTextUsageInstructions global displayTextUsageInstructions
if not displayTextUsageInstructions: if not displayTextUsageInstructions:
@ -129,20 +129,20 @@ def display_text(text):
"or t to copy the entire text. Press enter or escape when you are done reading.") "or t to copy the entire text. Press enter or escape when you are done reading.")
navText.insert(0, instructions) navText.insert(0, instructions)
displayTextUsageInstructions = True displayTextUsageInstructions = True
# Add end marker # Add end marker
navText.append("End of text.") navText.append("End of text.")
currentIndex = 0 currentIndex = 0
speech.speak(navText[currentIndex]) speech.speak(navText[currentIndex])
while True: while True:
event = pygame.event.wait() event = pygame.event.wait()
if event.type == pygame.KEYDOWN: if event.type == pygame.KEYDOWN:
# Check for Alt modifier # Check for Alt modifier
mods = pygame.key.get_mods() mods = pygame.key.get_mods()
altPressed = mods & pygame.KMOD_ALT altPressed = mods & pygame.KMOD_ALT
# Volume controls (require Alt) # Volume controls (require Alt)
if altPressed: if altPressed:
if event.key == pygame.K_PAGEUP: if event.key == pygame.K_PAGEUP:
@ -160,26 +160,26 @@ def display_text(text):
else: else:
if event.key in (pygame.K_ESCAPE, pygame.K_RETURN): if event.key in (pygame.K_ESCAPE, pygame.K_RETURN):
return return
if event.key in [pygame.K_DOWN, pygame.K_s] and currentIndex < len(navText) - 1: if event.key in [pygame.K_DOWN, pygame.K_s] and currentIndex < len(navText) - 1:
currentIndex += 1 currentIndex += 1
speech.speak(navText[currentIndex]) speech.speak(navText[currentIndex])
if event.key in [pygame.K_UP, pygame.K_w] and currentIndex > 0: if event.key in [pygame.K_UP, pygame.K_w] and currentIndex > 0:
currentIndex -= 1 currentIndex -= 1
speech.speak(navText[currentIndex]) speech.speak(navText[currentIndex])
if event.key == pygame.K_SPACE: if event.key == pygame.K_SPACE:
# Join with newlines to preserve spacing in speech # Join with newlines to preserve spacing in speech
speech.speak('\n'.join(originalText[1:-1])) speech.speak('\n'.join(originalText[1:-1]))
if event.key == pygame.K_c: if event.key == pygame.K_c:
try: try:
pyperclip.copy(navText[currentIndex]) pyperclip.copy(navText[currentIndex])
speech.speak("Copied " + navText[currentIndex] + " to the clipboard.") speech.speak("Copied " + navText[currentIndex] + " to the clipboard.")
except: except:
speech.speak("Failed to copy the text to the clipboard.") speech.speak("Failed to copy the text to the clipboard.")
if event.key == pygame.K_t: if event.key == pygame.K_t:
try: try:
# Join with newlines to preserve blank lines in full text # Join with newlines to preserve blank lines in full text
@ -187,6 +187,6 @@ def display_text(text):
speech.speak("Copied entire message to the clipboard.") speech.speak("Copied entire message to the clipboard.")
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() event = pygame.event.clear()
time.sleep(0.001) time.sleep(0.001)

View File

@ -15,11 +15,11 @@ 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 a dialog box for text input.
Args: Args:
prompt (str): Prompt text to display (default: "Enter text:") prompt (str): Prompt text to display (default: "Enter text:")
text (str): Initial text in input box (default: "") text (str): Initial text in input box (default: "")
Returns: Returns:
str: User input text, or None if cancelled str: User input text, or None if cancelled
""" """
@ -66,7 +66,7 @@ def pause_game():
def check_for_exit(): def check_for_exit():
"""Check if user has pressed escape key. """Check if user has pressed escape key.
Returns: Returns:
bool: True if escape was pressed, False otherwise bool: True if escape was pressed, False otherwise
""" """

120
menu.py
View File

@ -27,7 +27,7 @@ from .services import PathService, ConfigService
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.
Standard menu structure: Standard menu structure:
1. Play (always first) 1. Play (always first)
2. High Scores 2. High Scores
@ -37,53 +37,53 @@ def game_menu(sounds, playCallback=None, *customOptions):
6. Credits (if available) 6. Credits (if available)
7. Donate 7. Donate
8. Exit 8. Exit
Handles navigation with: Handles navigation with:
- Up/Down arrows for selection - Up/Down arrows for selection
- Home/End for first/last option - Home/End for first/last option
- Enter to select - Enter to select
- Escape to exit - Escape to exit
- Volume controls (with Alt modifier) - Volume controls (with Alt modifier)
Args: Args:
sounds (dict): Dictionary of sound objects sounds (dict): Dictionary of sound objects
playCallback (function, optional): Callback function for the "play" option. playCallback (function, optional): Callback function for the "play" option.
If None, "play" is returned as a string like other options. If None, "play" is returned as a string like other options.
*customOptions: Additional custom options to include after play but before standard ones *customOptions: Additional custom options to include after play but before standard ones
Returns: Returns:
str: Selected menu option or "exit" if user pressed escape str: Selected menu option or "exit" if user pressed escape
""" """
# Get speech instance # Get speech instance
speech = Speech.get_instance() speech = Speech.get_instance()
# Start with Play option # Start with Play option
allOptions = ["play"] allOptions = ["play"]
# Add high scores option if scores exist # Add high scores option if scores exist
if Scoreboard.has_high_scores(): if Scoreboard.has_high_scores():
allOptions.append("high_scores") allOptions.append("high_scores")
# Add custom options (other menu items, etc.) # Add custom options (other menu items, etc.)
allOptions.extend(customOptions) allOptions.extend(customOptions)
# Add standard options in preferred order # Add standard options in preferred order
allOptions.append("learn_sounds") allOptions.append("learn_sounds")
# Check for instructions file # Check for instructions file
if os.path.isfile('files/instructions.txt'): if os.path.isfile('files/instructions.txt'):
allOptions.append("instructions") allOptions.append("instructions")
# Check for credits file # Check for credits file
if os.path.isfile('files/credits.txt'): if os.path.isfile('files/credits.txt'):
allOptions.append("credits") allOptions.append("credits")
# Final options # Final options
allOptions.extend(["donate", "exit_game"]) allOptions.extend(["donate", "exit_game"])
# Track if music was previously playing # Track if music was previously playing
musicWasPlaying = pygame.mixer.music.get_busy() musicWasPlaying = pygame.mixer.music.get_busy()
# Only start menu music if no music is currently playing # Only start menu music if no music is currently playing
if not musicWasPlaying: if not musicWasPlaying:
try: try:
@ -91,23 +91,23 @@ def game_menu(sounds, playCallback=None, *customOptions):
play_bgm("sounds/music_menu.ogg") play_bgm("sounds/music_menu.ogg")
except: except:
pass pass
loop = True loop = True
pygame.mixer.stop() pygame.mixer.stop()
currentIndex = 0 currentIndex = 0
lastSpoken = -1 # Track last spoken index lastSpoken = -1 # Track last spoken index
while loop: while loop:
if currentIndex != lastSpoken: if currentIndex != lastSpoken:
speech.speak(allOptions[currentIndex]) speech.speak(allOptions[currentIndex])
lastSpoken = currentIndex lastSpoken = currentIndex
event = pygame.event.wait() event = pygame.event.wait()
if event.type == pygame.KEYDOWN: if event.type == pygame.KEYDOWN:
# Check for Alt modifier # Check for Alt modifier
mods = pygame.key.get_mods() mods = pygame.key.get_mods()
altPressed = mods & pygame.KMOD_ALT altPressed = mods & pygame.KMOD_ALT
# Volume controls (require Alt) # Volume controls (require Alt)
if altPressed: if altPressed:
if event.key == pygame.K_PAGEUP: if event.key == pygame.K_PAGEUP:
@ -169,9 +169,9 @@ def game_menu(sounds, playCallback=None, *customOptions):
time.sleep(sounds['menu-select'].get_length()) time.sleep(sounds['menu-select'].get_length())
except: except:
pass pass
selectedOption = allOptions[currentIndex] selectedOption = allOptions[currentIndex]
# Special case for exit_game with fade # Special case for exit_game with fade
if selectedOption == "exit_game": if selectedOption == "exit_game":
exit_game(500 if pygame.mixer.music.get_busy() else 0) exit_game(500 if pygame.mixer.music.get_busy() else 0)
@ -202,7 +202,7 @@ def game_menu(sounds, playCallback=None, *customOptions):
pygame.mixer.music.pause() pygame.mixer.music.pause()
except: except:
pass pass
# Handle standard options # Handle standard options
if selectedOption == "instructions": if selectedOption == "instructions":
instructions() instructions()
@ -214,7 +214,7 @@ def game_menu(sounds, playCallback=None, *customOptions):
Scoreboard.display_high_scores() Scoreboard.display_high_scores()
elif selectedOption == "donate": elif selectedOption == "donate":
donate() donate()
# Unpause music after function returns # Unpause music after function returns
try: try:
# Check if music is actually paused before trying to unpause # Check if music is actually paused before trying to unpause
@ -248,105 +248,105 @@ def game_menu(sounds, playCallback=None, *customOptions):
except: except:
pass pass
return allOptions[currentIndex] return allOptions[currentIndex]
event = pygame.event.clear() event = pygame.event.clear()
time.sleep(0.001) time.sleep(0.001)
def learn_sounds(sounds): def learn_sounds(sounds):
"""Interactive menu for learning game sounds. """Interactive menu for learning game sounds.
Allows users to: Allows users to:
- Navigate through available sounds with up/down arrows - Navigate through available sounds with up/down arrows
- Navigate between sound categories (folders) using Page Up/Page Down or Left/Right arrows - Navigate between sound categories (folders) using Page Up/Page Down or Left/Right arrows
- Play selected sounds with Enter - Play selected sounds with Enter
- Return to menu with Escape - Return to menu with Escape
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 starting with 'game-intro', 'music_menu', or '_'
Args: Args:
sounds (dict): Dictionary of available sound objects sounds (dict): Dictionary of available sound objects
Returns: Returns:
str: "menu" if user exits with escape str: "menu" if user exits with escape
""" """
# Get speech instance # Get speech instance
speech = Speech.get_instance() speech = Speech.get_instance()
# Define exclusion criteria # Define exclusion criteria
excludedPrefixes = ["game-intro", "music_menu", "_"] excludedPrefixes = ["game-intro", "music_menu", "_"]
excludedDirs = ["ambience", "."] excludedDirs = ["ambience", "."]
# Organize sounds by directory # Organize sounds by directory
soundsByDir = {} soundsByDir = {}
# 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 # Skip if key has any excluded prefix
if any(soundKey.lower().startswith(prefix.lower()) for prefix in excludedPrefixes): if any(soundKey.lower().startswith(prefix.lower()) for prefix in excludedPrefixes):
continue continue
# Split key into path parts # Split key into path parts
parts = soundKey.split('/') parts = soundKey.split('/')
# Skip if any part of the path is an excluded directory # 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): if any(part.lower() == dirName.lower() or part.startswith('.') for part in parts for dirName in excludedDirs):
continue continue
# Determine the directory # Determine the directory
if '/' in soundKey: if '/' in soundKey:
directory = soundKey.split('/')[0] directory = soundKey.split('/')[0]
else: else:
directory = 'root' # Root directory sounds directory = 'root' # Root directory sounds
# Add to sounds by directory # Add to sounds by directory
if directory not in soundsByDir: if directory not in soundsByDir:
soundsByDir[directory] = [] soundsByDir[directory] = []
soundsByDir[directory].append(soundKey) soundsByDir[directory].append(soundKey)
# Sort each directory's sounds # Sort each directory's sounds
for directory in soundsByDir: for directory in soundsByDir:
soundsByDir[directory].sort() soundsByDir[directory].sort()
# If no sounds found, inform the user and return # If no sounds found, inform the user and return
if not soundsByDir: if not soundsByDir:
speech.speak("No sounds available to learn.") speech.speak("No sounds available to learn.")
return "menu" return "menu"
# Get list of directories in sorted order # Get list of directories in sorted order
directories = sorted(soundsByDir.keys()) directories = sorted(soundsByDir.keys())
# Start with first directory # Start with first directory
currentDirIndex = 0 currentDirIndex = 0
currentDir = directories[currentDirIndex] currentDir = directories[currentDirIndex]
currentSoundKeys = soundsByDir[currentDir] currentSoundKeys = soundsByDir[currentDir]
currentSoundIndex = 0 currentSoundIndex = 0
# Display appropriate message based on number of directories # Display appropriate message based on number of directories
if len(directories) > 1: 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.") 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 # Track last spoken to avoid repetition
lastSpoken = -1 lastSpoken = -1
directoryChanged = True # Flag to track if directory just changed directoryChanged = True # Flag to track if directory just changed
# Flag to track when to exit the loop # Flag to track when to exit the loop
returnToMenu = False returnToMenu = False
while not returnToMenu: while not returnToMenu:
# Announce current sound # Announce current sound
if currentSoundIndex != lastSpoken: if currentSoundIndex != lastSpoken:
totalSounds = len(currentSoundKeys) totalSounds = len(currentSoundKeys)
soundName = currentSoundKeys[currentSoundIndex] soundName = currentSoundKeys[currentSoundIndex]
# Remove directory prefix if present # Remove directory prefix if present
if '/' in soundName: if '/' in soundName:
displayName = '/'.join(soundName.split('/')[1:]) displayName = '/'.join(soundName.split('/')[1:])
else: else:
displayName = soundName displayName = soundName
# If directory just changed, include directory name in announcement # If directory just changed, include directory name in announcement
if directoryChanged: if directoryChanged:
dirDescription = "Root directory" if currentDir == 'root' else currentDir dirDescription = "Root directory" if currentDir == 'root' else currentDir
@ -354,24 +354,24 @@ def learn_sounds(sounds):
directoryChanged = False # Reset flag after announcement directoryChanged = False # Reset flag after announcement
else: else:
announcement = f"{displayName}, {currentSoundIndex + 1} of {totalSounds}" announcement = f"{displayName}, {currentSoundIndex + 1} of {totalSounds}"
speech.speak(announcement) speech.speak(announcement)
lastSpoken = currentSoundIndex lastSpoken = currentSoundIndex
event = pygame.event.wait() event = pygame.event.wait()
if event.type == pygame.KEYDOWN: if event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE: if event.key == pygame.K_ESCAPE:
returnToMenu = True returnToMenu = True
# Sound navigation # Sound navigation
elif event.key in [pygame.K_DOWN, pygame.K_s] and currentSoundIndex < len(currentSoundKeys) - 1: elif event.key in [pygame.K_DOWN, pygame.K_s] and currentSoundIndex < len(currentSoundKeys) - 1:
pygame.mixer.stop() pygame.mixer.stop()
currentSoundIndex += 1 currentSoundIndex += 1
elif event.key in [pygame.K_UP, pygame.K_w] and currentSoundIndex > 0: elif event.key in [pygame.K_UP, pygame.K_w] and currentSoundIndex > 0:
pygame.mixer.stop() pygame.mixer.stop()
currentSoundIndex -= 1 currentSoundIndex -= 1
# Directory navigation # Directory navigation
elif event.key in [pygame.K_PAGEDOWN, pygame.K_RIGHT] and currentDirIndex < len(directories) - 1: elif event.key in [pygame.K_PAGEDOWN, pygame.K_RIGHT] and currentDirIndex < len(directories) - 1:
pygame.mixer.stop() pygame.mixer.stop()
@ -381,7 +381,7 @@ def learn_sounds(sounds):
currentSoundIndex = 0 currentSoundIndex = 0
directoryChanged = True # Set flag on directory change directoryChanged = True # Set flag on directory change
lastSpoken = -1 # Force announcement lastSpoken = -1 # Force announcement
elif event.key in [pygame.K_PAGEUP, pygame.K_LEFT] and currentDirIndex > 0: elif event.key in [pygame.K_PAGEUP, pygame.K_LEFT] and currentDirIndex > 0:
pygame.mixer.stop() pygame.mixer.stop()
currentDirIndex -= 1 currentDirIndex -= 1
@ -390,7 +390,7 @@ def learn_sounds(sounds):
currentSoundIndex = 0 currentSoundIndex = 0
directoryChanged = True # Set flag on directory change directoryChanged = True # Set flag on directory change
lastSpoken = -1 # Force announcement lastSpoken = -1 # Force announcement
# Play sound # Play sound
elif event.key == pygame.K_RETURN: elif event.key == pygame.K_RETURN:
try: try:
@ -400,16 +400,16 @@ def learn_sounds(sounds):
except Exception as e: except Exception as e:
print(f"Error playing sound: {e}") print(f"Error playing sound: {e}")
speech.speak("Could not play sound.") speech.speak("Could not play sound.")
event = pygame.event.clear() event = pygame.event.clear()
pygame.event.pump() # Process pygame's internal events pygame.event.pump() # Process pygame's internal events
time.sleep(0.001) time.sleep(0.001)
return "menu" return "menu"
def instructions(): def instructions():
"""Display game instructions from file. """Display game instructions from file.
Reads and displays instructions from 'files/instructions.txt'. Reads and displays instructions from 'files/instructions.txt'.
If file is missing, displays an error message. If file is missing, displays an error message.
""" """
@ -441,7 +441,7 @@ def credits():
def donate(): def donate():
"""Open the donation webpage. """Open the donation webpage.
Opens the Ko-fi donation page. Opens the Ko-fi donation page.
""" """
webbrowser.open('https://ko-fi.com/stormux') webbrowser.open('https://ko-fi.com/stormux')
@ -449,20 +449,20 @@ def donate():
def exit_game(fade=0): def exit_game(fade=0):
"""Clean up and exit the game properly. """Clean up and exit the game properly.
Args: Args:
fade (int): Milliseconds to fade out music before exiting. fade (int): Milliseconds to fade out music before exiting.
0 means stop immediately (default) 0 means stop immediately (default)
""" """
# Force clear any pending events to prevent hanging # Force clear any pending events to prevent hanging
pygame.event.clear() pygame.event.clear()
# Stop all mixer channels first # Stop all mixer channels first
try: try:
pygame.mixer.stop() pygame.mixer.stop()
except Exception as e: except Exception as e:
print(f"Warning: Could not stop mixer channels: {e}") print(f"Warning: Could not stop mixer channels: {e}")
# Get speech instance and handle all providers # Get speech instance and handle all providers
try: try:
speech = Speech.get_instance() speech = Speech.get_instance()
@ -473,7 +473,7 @@ def exit_game(fade=0):
print(f"Warning: Could not close speech: {e}") print(f"Warning: Could not close speech: {e}")
except Exception as e: except Exception as e:
print(f"Warning: Could not get speech instance: {e}") print(f"Warning: Could not get speech instance: {e}")
# Handle music based on fade parameter # Handle music based on fade parameter
try: try:
if fade > 0 and pygame.mixer.music.get_busy(): if fade > 0 and pygame.mixer.music.get_busy():
@ -484,13 +484,13 @@ def exit_game(fade=0):
pygame.mixer.music.stop() pygame.mixer.music.stop()
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 # Clean up pygame
try: try:
pygame.quit() pygame.quit()
except Exception as e: except Exception as e:
print(f"Warning: Error during pygame.quit(): {e}") print(f"Warning: Error during pygame.quit(): {e}")
# Use os._exit for immediate termination # Use os._exit for immediate termination
import os import os
os._exit(0) os._exit(0)

View File

@ -18,10 +18,10 @@ from .config import localConfig, write_config, read_config
class Scoreboard: class Scoreboard:
"""Handles high score tracking with player names.""" """Handles high score tracking with player names."""
def __init__(self, score=0, configService=None, speech=None): def __init__(self, score=0, configService=None, speech=None):
"""Initialize scoreboard. """Initialize scoreboard.
Args: Args:
score (int): Initial score (default: 0) score (int): Initial score (default: 0)
configService (ConfigService): Config service (default: global instance) configService (ConfigService): Config service (default: global instance)
@ -29,15 +29,15 @@ class Scoreboard:
""" """
# Ensure services are properly initialized # Ensure services are properly initialized
self._ensure_services() self._ensure_services()
self.configService = configService or ConfigService.get_instance() self.configService = configService or ConfigService.get_instance()
self.speech = speech or Speech.get_instance() self.speech = speech or Speech.get_instance()
self.currentScore = score self.currentScore = score
self.highScores = [] self.highScores = []
# For backward compatibility # For backward compatibility
read_config() read_config()
try: try:
# Try to use configService # Try to use configService
self.configService.localConfig.add_section("scoreboard") self.configService.localConfig.add_section("scoreboard")
@ -47,7 +47,7 @@ class Scoreboard:
localConfig.add_section("scoreboard") localConfig.add_section("scoreboard")
except: except:
pass pass
# Load existing high scores # Load existing high scores
for i in range(1, 11): for i in range(1, 11):
try: try:
@ -72,15 +72,15 @@ class Scoreboard:
'name': "Player", 'name': "Player",
'score': 0 'score': 0
}) })
# Sort high scores by score value in descending order # Sort high scores by score value in descending order
self.highScores.sort(key=lambda x: x['score'], reverse=True) self.highScores.sort(key=lambda x: x['score'], reverse=True)
def _ensure_services(self): def _ensure_services(self):
"""Ensure PathService and ConfigService are properly initialized.""" """Ensure PathService and ConfigService are properly initialized."""
# Get PathService and make sure it has a game name # Get PathService and make sure it has a game name
pathService = PathService.get_instance() pathService = PathService.get_instance()
# If no game name yet, try to get from pygame window title # If no game name yet, try to get from pygame window title
if not pathService.gameName: if not pathService.gameName:
try: try:
@ -89,55 +89,55 @@ class Scoreboard:
pathService.gameName = pygame.display.get_caption()[0] pathService.gameName = pygame.display.get_caption()[0]
except: except:
pass pass
# Initialize path service if we have a game name but no paths set up # Initialize path service if we have a game name but no paths set up
if pathService.gameName and not pathService.gamePath: if pathService.gameName and not pathService.gamePath:
pathService.initialize(pathService.gameName) pathService.initialize(pathService.gameName)
# Get ConfigService and connect to PathService # Get ConfigService and connect to PathService
configService = ConfigService.get_instance() configService = ConfigService.get_instance()
if not hasattr(configService, 'pathService') or not configService.pathService: if not hasattr(configService, 'pathService') or not configService.pathService:
if pathService.gameName: if pathService.gameName:
configService.set_game_info(pathService.gameName, pathService) configService.set_game_info(pathService.gameName, pathService)
# Ensure the game directory exists # Ensure the game directory exists
if pathService.gamePath and not os.path.exists(pathService.gamePath): if pathService.gamePath and not os.path.exists(pathService.gamePath):
try: try:
os.makedirs(pathService.gamePath) os.makedirs(pathService.gamePath)
except Exception as e: except Exception as e:
print(f"Error creating game directory: {e}") print(f"Error creating game directory: {e}")
def get_score(self): def get_score(self):
"""Get current score.""" """Get current score."""
return self.currentScore return self.currentScore
def get_high_scores(self): def get_high_scores(self):
"""Get list of high scores.""" """Get list of high scores."""
return self.highScores return self.highScores
def decrease_score(self, points=1): def decrease_score(self, points=1):
"""Decrease the current score.""" """Decrease the current score."""
self.currentScore -= int(points) self.currentScore -= int(points)
return self return self
def increase_score(self, points=1): def increase_score(self, points=1):
"""Increase the current score.""" """Increase the current score."""
self.currentScore += int(points) self.currentScore += int(points)
return self return self
def set_score(self, score): def set_score(self, score):
"""Set the current score to a specific value.""" """Set the current score to a specific value."""
self.currentScore = int(score) self.currentScore = int(score)
return self return self
def reset_score(self): def reset_score(self):
"""Reset the current score to zero.""" """Reset the current score to zero."""
self.currentScore = 0 self.currentScore = 0
return self return self
def check_high_score(self): def check_high_score(self):
"""Check if current score qualifies as a high score. """Check if current score qualifies as a high score.
Returns: Returns:
int: Position (1-10) if high score, None if not int: Position (1-10) if high score, None if not
""" """
@ -145,23 +145,23 @@ class Scoreboard:
if self.currentScore > entry['score']: if self.currentScore > entry['score']:
return i + 1 return i + 1
return None return None
def add_high_score(self, name=None): def add_high_score(self, name=None):
"""Add current score to high scores if it qualifies. """Add current score to high scores if it qualifies.
Args: Args:
name (str): Player name (if None, will prompt user) name (str): Player name (if None, will prompt user)
Returns: Returns:
bool: True if score was added, False if not bool: True if score was added, False if not
""" """
# Ensure services are properly set up # Ensure services are properly set up
self._ensure_services() self._ensure_services()
position = self.check_high_score() position = self.check_high_score()
if position is None: if position is None:
return False return False
# Get player name # Get player name
if name is None: if name is None:
# Import get_input here to avoid circular imports # Import get_input here to avoid circular imports
@ -169,23 +169,23 @@ class Scoreboard:
name = get_input("New high score! Enter your name:", "Player") name = get_input("New high score! Enter your name:", "Player")
if name is None: # User cancelled if name is None: # User cancelled
name = "Player" name = "Player"
# Insert new score at correct position # Insert new score at correct position
self.highScores.insert(position - 1, { self.highScores.insert(position - 1, {
'name': name, 'name': name,
'score': self.currentScore 'score': self.currentScore
}) })
# Keep only top 10 # Keep only top 10
self.highScores = self.highScores[:10] self.highScores = self.highScores[:10]
# Save to config - try both methods for maximum compatibility # Save to config - try both methods for maximum compatibility
try: try:
# Try new method first # Try new method first
for i, entry in enumerate(self.highScores): 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"score_{i+1}", str(entry['score']))
self.configService.localConfig.set("scoreboard", f"name_{i+1}", entry['name']) self.configService.localConfig.set("scoreboard", f"name_{i+1}", entry['name'])
# Try to write with configService # Try to write with configService
try: try:
self.configService.write_local_config() 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"score_{i+1}", str(entry['score']))
localConfig.set("scoreboard", f"name_{i+1}", entry['name']) localConfig.set("scoreboard", f"name_{i+1}", entry['name'])
write_config() write_config()
except Exception as e: except Exception as e:
print(f"Error writing high scores: {e}") print(f"Error writing high scores: {e}")
# If all else fails, try direct old method # 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"score_{i+1}", str(entry['score']))
localConfig.set("scoreboard", f"name_{i+1}", entry['name']) localConfig.set("scoreboard", f"name_{i+1}", entry['name'])
write_config() write_config()
# Announce success # Announce success
try: try:
self.speech.messagebox(f"Congratulations {name}! You got position {position} on the scoreboard!") self.speech.messagebox(f"Congratulations {name}! You got position {position} on the scoreboard!")
@ -212,14 +212,14 @@ class Scoreboard:
# Fallback to global speak function # Fallback to global speak function
from .speech import speak from .speech import speak
speak(f"Congratulations {name}! You got position {position} on the scoreboard!") speak(f"Congratulations {name}! You got position {position} on the scoreboard!")
time.sleep(1) time.sleep(1)
return True return True
@staticmethod @staticmethod
def has_high_scores(): def has_high_scores():
"""Check if the current game has any high scores. """Check if the current game has any high scores.
Returns: Returns:
bool: True if at least one high score exists, False otherwise bool: True if at least one high score exists, False otherwise
""" """
@ -227,7 +227,7 @@ class Scoreboard:
# Get PathService to access game name # Get PathService to access game name
pathService = PathService.get_instance() pathService = PathService.get_instance()
gameName = pathService.gameName gameName = pathService.gameName
# If no game name, try to get from window title # If no game name, try to get from window title
if not gameName: if not gameName:
try: try:
@ -237,31 +237,31 @@ class Scoreboard:
pathService.gameName = gameName pathService.gameName = gameName
except: except:
pass pass
# Ensure path service is properly initialized # Ensure path service is properly initialized
if gameName and not pathService.gamePath: if gameName and not pathService.gamePath:
pathService.initialize(gameName) pathService.initialize(gameName)
# Get the config file path # Get the config file path
configPath = os.path.join(pathService.gamePath, "config.ini") configPath = os.path.join(pathService.gamePath, "config.ini")
# If config file doesn't exist, there are no scores # If config file doesn't exist, there are no scores
if not os.path.exists(configPath): if not os.path.exists(configPath):
return False return False
# Ensure config service is properly connected to path service # Ensure config service is properly connected to path service
configService = ConfigService.get_instance() configService = ConfigService.get_instance()
configService.set_game_info(gameName, pathService) configService.set_game_info(gameName, pathService)
# Create scoreboard using the properly initialized services # Create scoreboard using the properly initialized services
board = Scoreboard(0, configService) board = Scoreboard(0, configService)
# Force a read of local config to ensure fresh data # Force a read of local config to ensure fresh data
configService.read_local_config() configService.read_local_config()
# Get high scores # Get high scores
scores = board.get_high_scores() scores = board.get_high_scores()
# Check if any score is greater than zero # Check if any score is greater than zero
return any(score['score'] > 0 for score in scores) return any(score['score'] > 0 for score in scores)
except Exception as e: except Exception as e:
@ -271,7 +271,7 @@ class Scoreboard:
@staticmethod @staticmethod
def display_high_scores(): def display_high_scores():
"""Display high scores for the current game. """Display high scores for the current game.
Reads the high scores from Scoreboard class. Reads the high scores from Scoreboard class.
Shows the game name at the top followed by the available scores. Shows the game name at the top followed by the available scores.
""" """
@ -279,7 +279,7 @@ class Scoreboard:
# Get PathService to access game name # Get PathService to access game name
pathService = PathService.get_instance() pathService = PathService.get_instance()
gameName = pathService.gameName gameName = pathService.gameName
# If no game name, try to get from window title # If no game name, try to get from window title
if not gameName: if not gameName:
try: try:
@ -289,30 +289,30 @@ class Scoreboard:
pathService.gameName = gameName pathService.gameName = gameName
except: except:
pass pass
# Ensure path service is properly initialized # Ensure path service is properly initialized
if gameName and not pathService.gamePath: if gameName and not pathService.gamePath:
pathService.initialize(gameName) pathService.initialize(gameName)
# Ensure config service is properly connected to path service # Ensure config service is properly connected to path service
configService = ConfigService.get_instance() configService = ConfigService.get_instance()
configService.set_game_info(gameName, pathService) configService.set_game_info(gameName, pathService)
# Create scoreboard using the properly initialized services # Create scoreboard using the properly initialized services
board = Scoreboard(0, configService) board = Scoreboard(0, configService)
# Force a read of local config to ensure fresh data # Force a read of local config to ensure fresh data
configService.read_local_config() configService.read_local_config()
# Get high scores # Get high scores
scores = board.get_high_scores() scores = board.get_high_scores()
# Filter out scores with zero points # Filter out scores with zero points
validScores = [score for score in scores if score['score'] > 0] validScores = [score for score in scores if score['score'] > 0]
# Prepare the lines to display # Prepare the lines to display
lines = [f"High Scores for {gameName}:"] lines = [f"High Scores for {gameName}:"]
# Add scores to the display list # Add scores to the display list
if validScores: if validScores:
for i, entry in enumerate(validScores, 1): for i, entry in enumerate(validScores, 1):
@ -320,13 +320,13 @@ class Scoreboard:
lines.append(scoreStr) lines.append(scoreStr)
else: else:
lines.append("No high scores yet.") lines.append("No high scores yet.")
# Display the high scores # Display the high scores
display_text(lines) display_text(lines)
except Exception as e: except Exception as e:
print(f"Error displaying high scores: {e}") print(f"Error displaying high scores: {e}")
info = ["Could not display high scores."] info = ["Could not display high scores."]
display_text(info) display_text(info)
# For backward compatibility with older code that might call displayHigh_scores # For backward compatibility with older code that might call displayHigh_scores
displayHigh_scores = display_high_scores displayHigh_scores = display_high_scores

View File

@ -17,37 +17,37 @@ from .config import gamePath, globalPath, write_config, read_config
class ConfigService: class ConfigService:
"""Configuration management service.""" """Configuration management service."""
_instance = None _instance = None
@classmethod @classmethod
def get_instance(cls): def get_instance(cls):
"""Get or create the singleton instance.""" """Get or create the singleton instance."""
if cls._instance is None: if cls._instance is None:
cls._instance = ConfigService() cls._instance = ConfigService()
return cls._instance return cls._instance
def __init__(self): def __init__(self):
"""Initialize configuration parsers.""" """Initialize configuration parsers."""
self.localConfig = configparser.ConfigParser() self.localConfig = configparser.ConfigParser()
self.globalConfig = configparser.ConfigParser() self.globalConfig = configparser.ConfigParser()
self.gameTitle = None self.gameTitle = None
self.pathService = None self.pathService = None
def set_game_info(self, gameTitle, pathService): def set_game_info(self, gameTitle, pathService):
"""Set game information and initialize configs. """Set game information and initialize configs.
Args: Args:
gameTitle (str): Title of the game gameTitle (str): Title of the game
pathService (PathService): Path service instance pathService (PathService): Path service instance
""" """
self.gameTitle = gameTitle self.gameTitle = gameTitle
self.pathService = pathService self.pathService = pathService
# Load existing configurations # Load existing configurations
self.read_local_config() self.read_local_config()
self.read_global_config() self.read_global_config()
def read_local_config(self): def read_local_config(self):
"""Read local configuration from file.""" """Read local configuration from file."""
try: try:
@ -66,7 +66,7 @@ class ConfigService:
self.localConfig.read_dict(globals().get('localConfig', {})) self.localConfig.read_dict(globals().get('localConfig', {}))
except: except:
pass pass
def read_global_config(self): def read_global_config(self):
"""Read global configuration from file.""" """Read global configuration from file."""
try: try:
@ -85,7 +85,7 @@ class ConfigService:
self.globalConfig.read_dict(globals().get('globalConfig', {})) self.globalConfig.read_dict(globals().get('globalConfig', {}))
except: except:
pass pass
def write_local_config(self): def write_local_config(self):
"""Write local configuration to file.""" """Write local configuration to file."""
try: try:
@ -104,7 +104,7 @@ class ConfigService:
write_config(False) write_config(False)
except Exception as e: except Exception as e:
print(f"Warning: Failed to write local config: {e}") print(f"Warning: Failed to write local config: {e}")
def write_global_config(self): def write_global_config(self):
"""Write global configuration to file.""" """Write global configuration to file."""
try: try:
@ -127,37 +127,37 @@ class ConfigService:
class VolumeService: class VolumeService:
"""Volume management service.""" """Volume management service."""
_instance = None _instance = None
@classmethod @classmethod
def get_instance(cls): def get_instance(cls):
"""Get or create the singleton instance.""" """Get or create the singleton instance."""
if cls._instance is None: if cls._instance is None:
cls._instance = VolumeService() cls._instance = VolumeService()
return cls._instance return cls._instance
def __init__(self): def __init__(self):
"""Initialize volume settings.""" """Initialize volume settings."""
self.bgmVolume = 0.75 # Default background music volume self.bgmVolume = 0.75 # Default background music volume
self.sfxVolume = 1.0 # Default sound effects volume self.sfxVolume = 1.0 # Default sound effects volume
self.masterVolume = 1.0 # Default master volume self.masterVolume = 1.0 # Default master volume
def adjust_master_volume(self, change, pygameMixer=None): def adjust_master_volume(self, change, pygameMixer=None):
"""Adjust the master volume for all sounds. """Adjust the master volume for all sounds.
Args: Args:
change (float): Amount to change volume by (positive or negative) change (float): Amount to change volume by (positive or negative)
pygameMixer: Optional pygame.mixer module for real-time updates pygameMixer: Optional pygame.mixer module for real-time updates
""" """
self.masterVolume = max(0.0, min(1.0, self.masterVolume + change)) self.masterVolume = max(0.0, min(1.0, self.masterVolume + change))
# Update real-time audio if pygame mixer is provided # Update real-time audio if pygame mixer is provided
if pygameMixer: if pygameMixer:
# Update music volume # Update music volume
if pygameMixer.music.get_busy(): if pygameMixer.music.get_busy():
pygameMixer.music.set_volume(self.bgmVolume * self.masterVolume) pygameMixer.music.set_volume(self.bgmVolume * self.masterVolume)
# Update all sound channels # Update all sound channels
for i in range(pygameMixer.get_num_channels()): for i in range(pygameMixer.get_num_channels()):
channel = pygameMixer.Channel(i) channel = pygameMixer.Channel(i)
@ -170,29 +170,29 @@ class VolumeService:
# Stereo audio # Stereo audio
left, right = currentVolume left, right = currentVolume
channel.set_volume(left * self.masterVolume, right * self.masterVolume) channel.set_volume(left * self.masterVolume, right * self.masterVolume)
def adjust_bgm_volume(self, change, pygameMixer=None): def adjust_bgm_volume(self, change, pygameMixer=None):
"""Adjust only the background music volume. """Adjust only the background music volume.
Args: Args:
change (float): Amount to change volume by (positive or negative) change (float): Amount to change volume by (positive or negative)
pygameMixer: Optional pygame.mixer module for real-time updates pygameMixer: Optional pygame.mixer module for real-time updates
""" """
self.bgmVolume = max(0.0, min(1.0, self.bgmVolume + change)) self.bgmVolume = max(0.0, min(1.0, self.bgmVolume + change))
# Update real-time audio if pygame mixer is provided # Update real-time audio if pygame mixer is provided
if pygameMixer and pygameMixer.music.get_busy(): if pygameMixer and pygameMixer.music.get_busy():
pygameMixer.music.set_volume(self.bgmVolume * self.masterVolume) pygameMixer.music.set_volume(self.bgmVolume * self.masterVolume)
def adjust_sfx_volume(self, change, pygameMixer=None): def adjust_sfx_volume(self, change, pygameMixer=None):
"""Adjust volume for sound effects only. """Adjust volume for sound effects only.
Args: Args:
change (float): Amount to change volume by (positive or negative) change (float): Amount to change volume by (positive or negative)
pygameMixer: Optional pygame.mixer module for real-time updates pygameMixer: Optional pygame.mixer module for real-time updates
""" """
self.sfxVolume = max(0.0, min(1.0, self.sfxVolume + change)) self.sfxVolume = max(0.0, min(1.0, self.sfxVolume + change))
# Update real-time audio if pygame mixer is provided # Update real-time audio if pygame mixer is provided
if pygameMixer: if pygameMixer:
# Update all sound channels except reserved ones # Update all sound channels except reserved ones
@ -208,18 +208,18 @@ class VolumeService:
left, right = currentVolume left, right = currentVolume
channel.set_volume(left * self.sfxVolume * self.masterVolume, channel.set_volume(left * self.sfxVolume * self.masterVolume,
right * self.sfxVolume * self.masterVolume) right * self.sfxVolume * self.masterVolume)
def get_bgm_volume(self): def get_bgm_volume(self):
"""Get the current BGM volume with master adjustment. """Get the current BGM volume with master adjustment.
Returns: Returns:
float: Current adjusted BGM volume float: Current adjusted BGM volume
""" """
return self.bgmVolume * self.masterVolume return self.bgmVolume * self.masterVolume
def get_sfx_volume(self): def get_sfx_volume(self):
"""Get the current SFX volume with master adjustment. """Get the current SFX volume with master adjustment.
Returns: Returns:
float: Current adjusted SFX volume float: Current adjusted SFX volume
""" """
@ -228,32 +228,32 @@ class VolumeService:
class PathService: class PathService:
"""Path management service.""" """Path management service."""
_instance = None _instance = None
@classmethod @classmethod
def get_instance(cls): def get_instance(cls):
"""Get or create the singleton instance.""" """Get or create the singleton instance."""
if cls._instance is None: if cls._instance is None:
cls._instance = PathService() cls._instance = PathService()
return cls._instance return cls._instance
def __init__(self): def __init__(self):
"""Initialize path variables.""" """Initialize path variables."""
self.globalPath = None self.globalPath = None
self.gamePath = None self.gamePath = None
self.gameName = None self.gameName = None
# Try to initialize from global variables for backward compatibility # Try to initialize from global variables for backward compatibility
global gamePath, globalPath global gamePath, globalPath
if gamePath: if gamePath:
self.gamePath = gamePath self.gamePath = gamePath
if globalPath: if globalPath:
self.globalPath = globalPath self.globalPath = globalPath
def initialize(self, gameTitle): def initialize(self, gameTitle):
"""Initialize paths for a game. """Initialize paths for a game.
Args: Args:
gameTitle (str): Title of the game gameTitle (str): Title of the game
""" """
@ -261,14 +261,14 @@ class PathService:
self.globalPath = os.path.join(BaseDirectory.xdg_config_home, "storm-games") self.globalPath = os.path.join(BaseDirectory.xdg_config_home, "storm-games")
self.gamePath = os.path.join(self.globalPath, self.gamePath = os.path.join(self.globalPath,
str.lower(str.replace(gameTitle, " ", "-"))) str.lower(str.replace(gameTitle, " ", "-")))
# Create game directory if it doesn't exist # Create game directory if it doesn't exist
if not os.path.exists(self.gamePath): if not os.path.exists(self.gamePath):
os.makedirs(self.gamePath) os.makedirs(self.gamePath)
# Update global variables for backward compatibility # Update global variables for backward compatibility
global gamePath, globalPath global gamePath, globalPath
gamePath = self.gamePath gamePath = self.gamePath
globalPath = self.globalPath globalPath = self.globalPath
return self return self

1124
sound.py

File diff suppressed because it is too large Load Diff

View File

@ -15,26 +15,26 @@ from sys import exit
class Speech: class Speech:
"""Handles text-to-speech functionality.""" """Handles text-to-speech functionality."""
_instance = None _instance = None
@classmethod @classmethod
def get_instance(cls): def get_instance(cls):
"""Get or create the singleton instance.""" """Get or create the singleton instance."""
if cls._instance is None: if cls._instance is None:
cls._instance = Speech() cls._instance = Speech()
return cls._instance return cls._instance
def __init__(self): def __init__(self):
"""Initialize speech system with available provider.""" """Initialize speech system with available provider."""
# Handle speech delays so we don't get stuttering # Handle speech delays so we don't get stuttering
self.lastSpoken = {"text": None, "time": 0} self.lastSpoken = {"text": None, "time": 0}
self.speechDelay = 250 # ms self.speechDelay = 250 # ms
# Try to initialize a speech provider # Try to initialize a speech provider
self.provider = None self.provider = None
self.providerName = None self.providerName = None
# Try speechd first # Try speechd first
try: try:
import speechd import speechd
@ -44,7 +44,7 @@ class Speech:
return return
except ImportError: except ImportError:
pass pass
# Try accessible_output2 next # Try accessible_output2 next
try: try:
import accessible_output2.outputs.auto import accessible_output2.outputs.auto
@ -54,31 +54,31 @@ class Speech:
return return
except ImportError: except ImportError:
pass pass
# 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):
"""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)
""" """
if not self.provider: if not self.provider:
return return
currentTime = pygame.time.get_ticks() currentTime = pygame.time.get_ticks()
# Check if this is the same text within the delay window # 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): currentTime - self.lastSpoken["time"] < self.speechDelay):
return return
# Update last spoken tracking # Update last spoken tracking
self.lastSpoken["text"] = text self.lastSpoken["text"] = text
self.lastSpoken["time"] = currentTime self.lastSpoken["time"] = currentTime
# Proceed with speech # Proceed with speech
if self.providerName == "speechd": if self.providerName == "speechd":
if interrupt: if interrupt:
@ -86,12 +86,12 @@ class Speech:
self.spd.say(text) self.spd.say(text)
elif self.providerName == "accessible_output2": elif self.providerName == "accessible_output2":
self.ao2.speak(text, interrupt=interrupt) self.ao2.speak(text, interrupt=interrupt)
# Display the text on screen # Display the text on screen
screen = pygame.display.get_surface() screen = pygame.display.get_surface()
if not screen: if not screen:
return return
font = pygame.font.Font(None, 36) font = pygame.font.Font(None, 36)
# Wrap the text # Wrap the text
maxWidth = screen.get_width() - 40 # Leave a 20-pixel margin on each side maxWidth = screen.get_width() - 40 # Leave a 20-pixel margin on each side
@ -109,7 +109,7 @@ class Speech:
screen.blit(surface, textRect) screen.blit(surface, textRect)
currentY += surface.get_height() currentY += surface.get_height()
pygame.display.flip() pygame.display.flip()
def close(self): def close(self):
"""Clean up speech resources.""" """Clean up speech resources."""
if self.providerName == "speechd": if self.providerName == "speechd":
@ -120,7 +120,7 @@ _speechInstance = None
def speak(text, interrupt=True): def speak(text, interrupt=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)

146
utils.py
View File

@ -26,137 +26,137 @@ from .scoreboard import Scoreboard
class Game: class Game:
"""Central class to manage all game systems.""" """Central class to manage all game systems."""
def __init__(self, title): def __init__(self, title):
"""Initialize a new game. """Initialize a new game.
Args: Args:
title (str): Title of the game title (str): Title of the game
""" """
self.title = title self.title = title
# Initialize services # Initialize services
self.pathService = PathService.get_instance().initialize(title) self.pathService = PathService.get_instance().initialize(title)
self.configService = ConfigService.get_instance() self.configService = ConfigService.get_instance()
self.configService.set_game_info(title, self.pathService) self.configService.set_game_info(title, self.pathService)
self.volumeService = VolumeService.get_instance() self.volumeService = VolumeService.get_instance()
# Initialize game components (lazy loaded) # Initialize game components (lazy loaded)
self._speech = None self._speech = None
self._sound = None self._sound = None
self._scoreboard = None self._scoreboard = None
# Display text instructions flag # Display text instructions flag
self.displayTextUsageInstructions = False self.displayTextUsageInstructions = False
@property @property
def speech(self): def speech(self):
"""Get the speech system (lazy loaded). """Get the speech system (lazy loaded).
Returns: Returns:
Speech: Speech system instance Speech: Speech system instance
""" """
if not self._speech: if not self._speech:
self._speech = Speech.get_instance() self._speech = Speech.get_instance()
return self._speech return self._speech
@property @property
def sound(self): def sound(self):
"""Get the sound system (lazy loaded). """Get the sound system (lazy loaded).
Returns: Returns:
Sound: Sound system instance Sound: Sound system instance
""" """
if not self._sound: if not self._sound:
self._sound = Sound("sounds/", self.volumeService) self._sound = Sound("sounds/", self.volumeService)
return self._sound return self._sound
@property @property
def scoreboard(self): def scoreboard(self):
"""Get the scoreboard (lazy loaded). """Get the scoreboard (lazy loaded).
Returns: Returns:
Scoreboard: Scoreboard instance Scoreboard: Scoreboard instance
""" """
if not self._scoreboard: if not self._scoreboard:
self._scoreboard = Scoreboard(self.configService) self._scoreboard = Scoreboard(self.configService)
return self._scoreboard return self._scoreboard
def initialize(self): def initialize(self):
"""Initialize the game GUI and sound system. """Initialize the game GUI and sound system.
Returns: Returns:
Game: Self for method chaining Game: Self for method chaining
""" """
# Set process title # Set process title
setproctitle(str.lower(str.replace(self.title, " ", ""))) setproctitle(str.lower(str.replace(self.title, " ", "")))
# Seed the random generator # Seed the random generator
random.seed() random.seed()
# Initialize pygame # Initialize pygame
pygame.init() pygame.init()
pygame.display.set_mode((800, 600)) pygame.display.set_mode((800, 600))
pygame.display.set_caption(self.title) pygame.display.set_caption(self.title)
# Set up audio system # Set up audio system
pygame.mixer.pre_init(44100, -16, 2, 1024) pygame.mixer.pre_init(44100, -16, 2, 1024)
pygame.mixer.init() pygame.mixer.init()
pygame.mixer.set_num_channels(32) pygame.mixer.set_num_channels(32)
pygame.mixer.set_reserved(0) # Reserve channel for cut scenes pygame.mixer.set_reserved(0) # Reserve channel for cut scenes
# Enable key repeat for volume controls # Enable key repeat for volume controls
pygame.key.set_repeat(500, 100) pygame.key.set_repeat(500, 100)
# Load sound effects # Load sound effects
self.sound self.sound
# Play intro sound if available # Play intro sound if available
if 'game-intro' in self.sound.sounds: if 'game-intro' in self.sound.sounds:
self.sound.cut_scene('game-intro') self.sound.cut_scene('game-intro')
return self return self
def speak(self, text, interrupt=True): def speak(self, text, interrupt=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
Returns: Returns:
Game: Self for method chaining Game: Self for method chaining
""" """
self.speech.speak(text, interrupt) self.speech.speak(text, interrupt)
return self return self
def play_bgm(self, musicFile): def play_bgm(self, musicFile):
"""Play background music. """Play background music.
Args: Args:
musicFile (str): Path to music file musicFile (str): Path to music file
Returns: Returns:
Game: Self for method chaining Game: Self for method chaining
""" """
self.sound.play_bgm(musicFile) self.sound.play_bgm(musicFile)
return self return self
def display_text(self, textLines): def display_text(self, textLines):
"""Display text with navigation controls. """Display text with navigation controls.
Args: Args:
textLines (list): List of text lines textLines (list): List of text lines
Returns: Returns:
Game: Self for method chaining Game: Self for method chaining
""" """
# Store original text with blank lines for copying # Store original text with blank lines for copying
originalText = textLines.copy() originalText = textLines.copy()
# Create navigation text by filtering out blank lines # Create navigation text by filtering out blank lines
navText = [line for line in textLines if line.strip()] navText = [line for line in textLines if line.strip()]
# Add instructions at the start on the first display # Add instructions at the start on the first display
if not self.displayTextUsageInstructions: if not self.displayTextUsageInstructions:
instructions = ("Press space to read the whole text. Use up and down arrows to navigate " instructions = ("Press space to read the whole text. Use up and down arrows to navigate "
@ -164,20 +164,20 @@ class Game:
"or t to copy the entire text. Press enter or escape when you are done reading.") "or t to copy the entire text. Press enter or escape when you are done reading.")
navText.insert(0, instructions) navText.insert(0, instructions)
self.displayTextUsageInstructions = True self.displayTextUsageInstructions = True
# Add end marker # Add end marker
navText.append("End of text.") navText.append("End of text.")
currentIndex = 0 currentIndex = 0
self.speech.speak(navText[currentIndex]) self.speech.speak(navText[currentIndex])
while True: while True:
event = pygame.event.wait() event = pygame.event.wait()
if event.type == pygame.KEYDOWN: if event.type == pygame.KEYDOWN:
# Check for Alt modifier # Check for Alt modifier
mods = pygame.key.get_mods() mods = pygame.key.get_mods()
altPressed = mods & pygame.KMOD_ALT altPressed = mods & pygame.KMOD_ALT
# Volume controls (require Alt) # Volume controls (require Alt)
if altPressed: if altPressed:
if event.key == pygame.K_PAGEUP: if event.key == pygame.K_PAGEUP:
@ -195,19 +195,19 @@ class Game:
else: else:
if event.key in (pygame.K_ESCAPE, pygame.K_RETURN): if event.key in (pygame.K_ESCAPE, pygame.K_RETURN):
return self return self
if event.key in [pygame.K_DOWN, pygame.K_s] and currentIndex < len(navText) - 1: if event.key in [pygame.K_DOWN, pygame.K_s] and currentIndex < len(navText) - 1:
currentIndex += 1 currentIndex += 1
self.speech.speak(navText[currentIndex]) self.speech.speak(navText[currentIndex])
if event.key in [pygame.K_UP, pygame.K_w] and currentIndex > 0: if event.key in [pygame.K_UP, pygame.K_w] and currentIndex > 0:
currentIndex -= 1 currentIndex -= 1
self.speech.speak(navText[currentIndex]) self.speech.speak(navText[currentIndex])
if event.key == pygame.K_SPACE: if event.key == pygame.K_SPACE:
# Join with newlines to preserve spacing in speech # Join with newlines to preserve spacing in speech
self.speech.speak('\n'.join(originalText[1:-1])) self.speech.speak('\n'.join(originalText[1:-1]))
if event.key == pygame.K_c: if event.key == pygame.K_c:
try: try:
import pyperclip import pyperclip
@ -215,7 +215,7 @@ class Game:
self.speech.speak("Copied " + navText[currentIndex] + " to the clipboard.") self.speech.speak("Copied " + navText[currentIndex] + " to the clipboard.")
except: except:
self.speech.speak("Failed to copy the text to the clipboard.") self.speech.speak("Failed to copy the text to the clipboard.")
if event.key == pygame.K_t: if event.key == pygame.K_t:
try: try:
import pyperclip import pyperclip
@ -224,10 +224,10 @@ class Game:
self.speech.speak("Copied entire message to the clipboard.") self.speech.speak("Copied entire message to the clipboard.")
except: except:
self.speech.speak("Failed to copy the text to the clipboard.") self.speech.speak("Failed to copy the text to the clipboard.")
pygame.event.clear() pygame.event.clear()
time.sleep(0.001) time.sleep(0.001)
def exit(self): def exit(self):
"""Clean up and exit the game.""" """Clean up and exit the game."""
if self._speech and self.speech.providerName == "speechd": if self._speech and self.speech.providerName == "speechd":
@ -241,12 +241,12 @@ class Game:
def check_for_updates(currentVersion, gameName, url): def check_for_updates(currentVersion, gameName, url):
"""Check for game updates. """Check for game updates.
Args: Args:
currentVersion (str): Current version string (e.g. "1.0.0") currentVersion (str): Current version string (e.g. "1.0.0")
gameName (str): Name of the game gameName (str): Name of the game
url (str): URL to check for updates url (str): URL to check for updates
Returns: Returns:
dict: Update information or None if no update available dict: Update information or None if no update available
""" """
@ -266,10 +266,10 @@ def check_for_updates(currentVersion, gameName, url):
def get_version_tuple(versionStr): def get_version_tuple(versionStr):
"""Convert version string to comparable tuple. """Convert version string to comparable tuple.
Args: Args:
versionStr (str): Version string (e.g. "1.0.0") versionStr (str): Version string (e.g. "1.0.0")
Returns: Returns:
tuple: Version as tuple of integers tuple: Version as tuple of integers
""" """
@ -277,11 +277,11 @@ def get_version_tuple(versionStr):
def check_compatibility(requiredVersion, currentVersion): def check_compatibility(requiredVersion, currentVersion):
"""Check if current version meets minimum required version. """Check if current version meets minimum required version.
Args: Args:
requiredVersion (str): Minimum required version string requiredVersion (str): Minimum required version string
currentVersion (str): Current version string currentVersion (str): Current version string
Returns: Returns:
bool: True if compatible, False otherwise bool: True if compatible, False otherwise
""" """
@ -291,10 +291,10 @@ def check_compatibility(requiredVersion, currentVersion):
def sanitize_filename(filename): def sanitize_filename(filename):
"""Sanitize a filename to be safe for all operating systems. """Sanitize a filename to be safe for all operating systems.
Args: Args:
filename (str): Original filename filename (str): Original filename
Returns: Returns:
str: Sanitized filename str: Sanitized filename
""" """
@ -309,12 +309,12 @@ def sanitize_filename(filename):
def lerp(start, end, factor): def lerp(start, end, factor):
"""Linear interpolation between two values. """Linear interpolation between two values.
Args: Args:
start (float): Start value start (float): Start value
end (float): End value end (float): End value
factor (float): Interpolation factor (0.0-1.0) factor (float): Interpolation factor (0.0-1.0)
Returns: Returns:
float: Interpolated value float: Interpolated value
""" """
@ -322,12 +322,12 @@ def lerp(start, end, factor):
def smooth_step(edge0, edge1, x): def smooth_step(edge0, edge1, x):
"""Hermite interpolation between two values. """Hermite interpolation between two values.
Args: Args:
edge0 (float): Start edge edge0 (float): Start edge
edge1 (float): End edge edge1 (float): End edge
x (float): Value to interpolate x (float): Value to interpolate
Returns: Returns:
float: Interpolated value with smooth step float: Interpolated value with smooth step
""" """
@ -338,13 +338,13 @@ def smooth_step(edge0, edge1, x):
def distance_2d(x1, y1, x2, y2): def distance_2d(x1, y1, x2, y2):
"""Calculate Euclidean distance between two 2D points. """Calculate Euclidean distance between two 2D points.
Args: Args:
x1 (float): X coordinate of first point x1 (float): X coordinate of first point
y1 (float): Y coordinate of first point y1 (float): Y coordinate of first point
x2 (float): X coordinate of second point x2 (float): X coordinate of second point
y2 (float): Y coordinate of second point y2 (float): Y coordinate of second point
Returns: Returns:
float: Distance between points float: Distance between points
""" """
@ -352,17 +352,17 @@ def distance_2d(x1, y1, x2, y2):
def generate_tone(frequency, duration=0.1, sampleRate=44100, volume=0.2): def generate_tone(frequency, duration=0.1, sampleRate=44100, volume=0.2):
"""Generate a tone at the specified frequency. """Generate a tone at the specified frequency.
Args: Args:
frequency (float): Frequency in Hz frequency (float): Frequency in Hz
duration (float): Duration in seconds (default: 0.1) duration (float): Duration in seconds (default: 0.1)
sampleRate (int): Sample rate in Hz (default: 44100) sampleRate (int): Sample rate in Hz (default: 44100)
volume (float): Volume from 0.0 to 1.0 (default: 0.2) volume (float): Volume from 0.0 to 1.0 (default: 0.2)
Returns: Returns:
pygame.mixer.Sound: Sound object with the generated tone pygame.mixer.Sound: Sound object with the generated tone
""" """
t = np.linspace(0, duration, int(sampleRate * duration), False) t = np.linspace(0, duration, int(sampleRate * duration), False)
tone = np.sin(2 * np.pi * frequency * t) tone = np.sin(2 * np.pi * frequency * t)
stereoTone = np.vstack((tone, tone)).T # Create a 2D array for stereo stereoTone = np.vstack((tone, tone)).T # Create a 2D array for stereo
@ -372,17 +372,17 @@ def generate_tone(frequency, duration=0.1, sampleRate=44100, volume=0.2):
def x_powerbar(): def x_powerbar():
"""Sound based horizontal power bar """Sound based horizontal power bar
Returns: Returns:
int: Selected position between -50 and 50 int: Selected position between -50 and 50
""" """
clock = pygame.time.Clock() clock = pygame.time.Clock()
screen = pygame.display.get_surface() screen = pygame.display.get_surface()
position = -50 # Start from the leftmost position position = -50 # Start from the leftmost position
direction = 1 # Move right initially direction = 1 # Move right initially
barHeight = 20 barHeight = 20
while True: while True:
frequency = 440 # A4 note frequency = 440 # A4 note
leftVolume = (50 - position) / 100 leftVolume = (50 - position) / 100
@ -390,7 +390,7 @@ def x_powerbar():
tone = generate_tone(frequency) tone = generate_tone(frequency)
channel = tone.play() channel = tone.play()
channel.set_volume(leftVolume, rightVolume) channel.set_volume(leftVolume, rightVolume)
# Visual representation # Visual representation
screen.fill((0, 0, 0)) screen.fill((0, 0, 0))
barWidth = screen.get_width() - 40 # Leave 20px margin on each side barWidth = screen.get_width() - 40 # Leave 20px margin on each side
@ -398,13 +398,13 @@ def x_powerbar():
markerPos = int(20 + (position + 50) / 100 * barWidth) 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.draw.rect(screen, (255, 0, 0), (markerPos - 5, screen.get_height() // 2 - barHeight, 10, barHeight * 2))
pygame.display.flip() pygame.display.flip()
for event in pygame.event.get(): for event in pygame.event.get():
check_for_exit() check_for_exit()
if event.type == pygame.KEYDOWN and event.key == pygame.K_SPACE: if event.type == pygame.KEYDOWN and event.key == pygame.K_SPACE:
channel.stop() channel.stop()
return position # This will return a value between -50 and 50 return position # This will return a value between -50 and 50
position += direction position += direction
if position > 50: if position > 50:
position = 50 position = 50
@ -412,27 +412,27 @@ def x_powerbar():
elif position < -50: elif position < -50:
position = -50 position = -50
direction = 1 direction = 1
clock.tick(40) # Speed of bar clock.tick(40) # Speed of bar
def y_powerbar(): def y_powerbar():
"""Sound based vertical power bar """Sound based vertical power bar
Returns: Returns:
int: Selected power level between 0 and 100 int: Selected power level between 0 and 100
""" """
clock = pygame.time.Clock() clock = pygame.time.Clock()
screen = pygame.display.get_surface() screen = pygame.display.get_surface()
power = 0 power = 0
direction = 1 # 1 for increasing, -1 for decreasing direction = 1 # 1 for increasing, -1 for decreasing
barWidth = 20 barWidth = 20
while True: while True:
frequency = 220 + (power * 5) # Adjust these values to change the pitch range frequency = 220 + (power * 5) # Adjust these values to change the pitch range
tone = generate_tone(frequency) tone = generate_tone(frequency)
channel = tone.play() channel = tone.play()
# Visual representation # Visual representation
screen.fill((0, 0, 0)) screen.fill((0, 0, 0))
barHeight = screen.get_height() - 40 # Leave 20px margin on top and bottom barHeight = screen.get_height() - 40 # Leave 20px margin on top and bottom
@ -440,15 +440,15 @@ def y_powerbar():
markerPos = int(20 + (100 - power) / 100 * barHeight) 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.draw.rect(screen, (255, 0, 0), (screen.get_width() // 2 - barWidth, markerPos - 5, barWidth * 2, 10))
pygame.display.flip() pygame.display.flip()
for event in pygame.event.get(): for event in pygame.event.get():
check_for_exit() check_for_exit()
if event.type == pygame.KEYDOWN and event.key == pygame.K_SPACE: if event.type == pygame.KEYDOWN and event.key == pygame.K_SPACE:
channel.stop() channel.stop()
return power return power
power += direction power += direction
if power >= 100 or power <= 0: if power >= 100 or power <= 0:
direction *= -1 # Reverse direction at limits direction *= -1 # Reverse direction at limits
clock.tick(40) clock.tick(40)