From be6dfdf53a609e15669b300367169213b90d32c1 Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Fri, 14 Mar 2025 23:10:14 -0400 Subject: [PATCH] Attempt to fix traceback on game exit with some older games. --- __init__.py | 32 ++--- config.py | 64 ++++----- display.py | 20 +-- menu.py | 50 +++++-- scoreboard.py | 66 ++++----- services.py | 178 ++++++++++++------------- sound.py | 362 +++++++++++++++++++++++++------------------------- speech.py | 54 ++++---- utils.py | 114 ++++++++-------- 9 files changed, 483 insertions(+), 457 deletions(-) diff --git a/__init__.py b/__init__.py index 523d4c5..d127b39 100755 --- a/__init__.py +++ b/__init__.py @@ -126,42 +126,42 @@ __all__ = [ ] # Create global instances for backward compatibility -config_service = ConfigService.get_instance() -volume_service = VolumeService.get_instance() -path_service = PathService.get_instance() +configService = ConfigService.get_instance() +volumeService = VolumeService.get_instance() +pathService = PathService.get_instance() # Set up backward compatibility hooks for initialize_gui -_original_initialize_gui = initialize_gui +_originalInitializeGui = initialize_gui -def initialize_gui_with_services(game_title): +def initialize_gui_with_services(gameTitle): """Wrapper around initialize_gui that initializes services.""" # Initialize path service - path_service.initialize(game_title) + pathService.initialize(gameTitle) # Connect config service to path service - config_service.set_game_info(game_title, path_service) + configService.set_game_info(gameTitle, pathService) # Call original initialize_gui - return _original_initialize_gui(game_title) + return _originalInitializeGui(gameTitle) # Replace initialize_gui with the wrapped version initialize_gui = initialize_gui_with_services # Initialize global scoreboard constructor -_original_scoreboard_init = Scoreboard.__init__ +_originalScoreboardInit = Scoreboard.__init__ -def scoreboard_init_with_services(self, score=0, config_service=None, speech=None): +def scoreboard_init_with_services(self, score=0, configService=None, speech=None): """Wrapper around Scoreboard.__init__ that ensures services are initialized.""" # Use global services if not specified - if config_service is None: - config_service = ConfigService.get_instance() + if configService is None: + configService = ConfigService.get_instance() - # Ensure path_service is connected if using defaults - if not hasattr(config_service, 'path_service') and path_service.game_path is not None: - config_service.path_service = path_service + # 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 - _original_scoreboard_init(self, score, config_service, speech) + _originalScoreboardInit(self, score, configService, speech) # Replace Scoreboard.__init__ with the wrapped version Scoreboard.__init__ = scoreboard_init_with_services diff --git a/config.py b/config.py index b64be49..a5715bc 100644 --- a/config.py +++ b/config.py @@ -14,24 +14,24 @@ from xdg import BaseDirectory class Config: """Configuration management class for Storm Games.""" - def __init__(self, game_title): + def __init__(self, gameTitle): """Initialize configuration system for a game. Args: - game_title (str): Title of the game + gameTitle (str): Title of the game """ - self.game_title = game_title - self.global_path = os.path.join(BaseDirectory.xdg_config_home, "storm-games") - self.game_path = os.path.join(self.global_path, - str.lower(str.replace(game_title, " ", "-"))) + self.gameTitle = gameTitle + 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.game_path): - os.makedirs(self.game_path) + if not os.path.exists(self.gamePath): + os.makedirs(self.gamePath) # Initialize config parsers - self.local_config = configparser.ConfigParser() - self.global_config = configparser.ConfigParser() + self.localConfig = configparser.ConfigParser() + self.globalConfig = configparser.ConfigParser() # Load existing configurations self.read_local_config() @@ -40,28 +40,28 @@ class Config: def read_local_config(self): """Read local configuration from file.""" try: - with open(os.path.join(self.game_path, "config.ini"), 'r') as configfile: - self.local_config.read_file(configfile) + with open(os.path.join(self.gamePath, "config.ini"), 'r') as configFile: + self.localConfig.read_file(configFile) except: pass def read_global_config(self): """Read global configuration from file.""" try: - with open(os.path.join(self.global_path, "config.ini"), 'r') as configfile: - self.global_config.read_file(configfile) + with open(os.path.join(self.globalPath, "config.ini"), 'r') as configFile: + self.globalConfig.read_file(configFile) except: pass def write_local_config(self): """Write local configuration to file.""" - with open(os.path.join(self.game_path, "config.ini"), 'w') as configfile: - self.local_config.write(configfile) + 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.global_path, "config.ini"), 'w') as configfile: - self.global_config.write(configfile) + with open(os.path.join(self.globalPath, "config.ini"), 'w') as configFile: + self.globalConfig.write(configFile) # Global variables for backward compatibility localConfig = configparser.ConfigParser() @@ -69,34 +69,34 @@ globalConfig = configparser.ConfigParser() gamePath = "" globalPath = "" -def write_config(write_global=False): +def write_config(writeGlobal=False): """Write configuration to file. Args: - write_global (bool): If True, write to global config, otherwise local (default: False) + writeGlobal (bool): If True, write to global config, otherwise local (default: False) """ - if not write_global: - with open(gamePath + "/config.ini", 'w') as configfile: - localConfig.write(configfile) + if not writeGlobal: + with open(gamePath + "/config.ini", 'w') as configFile: + localConfig.write(configFile) else: - with open(globalPath + "/config.ini", 'w') as configfile: - globalConfig.write(configfile) + with open(globalPath + "/config.ini", 'w') as configFile: + globalConfig.write(configFile) -def read_config(read_global=False): +def read_config(readGlobal=False): """Read configuration from file. Args: - read_global (bool): If True, read global config, otherwise local (default: False) + readGlobal (bool): If True, read global config, otherwise local (default: False) """ - if not read_global: + if not readGlobal: try: - with open(gamePath + "/config.ini", 'r') as configfile: - localConfig.read_file(configfile) + with open(gamePath + "/config.ini", 'r') as configFile: + localConfig.read_file(configFile) except: pass else: try: - with open(globalPath + "/config.ini", 'r') as configfile: - globalConfig.read_file(configfile) + with open(globalPath + "/config.ini", 'r') as configFile: + globalConfig.read_file(configFile) except: pass diff --git a/display.py b/display.py index 3bef3a8..a5b3ad7 100644 --- a/display.py +++ b/display.py @@ -31,7 +31,7 @@ def initialize_gui(gameTitle): dict: Dictionary of loaded sound objects """ # Initialize path service with game title - path_service = PathService.get_instance().initialize(gameTitle) + pathService = PathService.get_instance().initialize(gameTitle) # Seed the random generator to the clock random.seed() @@ -95,7 +95,7 @@ def display_text(text): """ # Get service instances speech = Speech.get_instance() - volume_service = VolumeService.get_instance() + volumeService = VolumeService.get_instance() # Store original text with blank lines for copying originalText = text.copy() @@ -123,22 +123,22 @@ def display_text(text): if event.type == pygame.KEYDOWN: # Check for Alt modifier mods = pygame.key.get_mods() - alt_pressed = mods & pygame.KMOD_ALT + altPressed = mods & pygame.KMOD_ALT # Volume controls (require Alt) - if alt_pressed: + if altPressed: if event.key == pygame.K_PAGEUP: - volume_service.adjust_master_volume(0.1, pygame.mixer) + volumeService.adjust_master_volume(0.1, pygame.mixer) elif event.key == pygame.K_PAGEDOWN: - volume_service.adjust_master_volume(-0.1, pygame.mixer) + volumeService.adjust_master_volume(-0.1, pygame.mixer) elif event.key == pygame.K_HOME: - volume_service.adjust_bgm_volume(0.1, pygame.mixer) + volumeService.adjust_bgm_volume(0.1, pygame.mixer) elif event.key == pygame.K_END: - volume_service.adjust_bgm_volume(-0.1, pygame.mixer) + volumeService.adjust_bgm_volume(-0.1, pygame.mixer) elif event.key == pygame.K_INSERT: - volume_service.adjust_sfx_volume(0.1, pygame.mixer) + volumeService.adjust_sfx_volume(0.1, pygame.mixer) elif event.key == pygame.K_DELETE: - volume_service.adjust_sfx_volume(-0.1, pygame.mixer) + volumeService.adjust_sfx_volume(-0.1, pygame.mixer) else: if event.key in (pygame.K_ESCAPE, pygame.K_RETURN): return diff --git a/menu.py b/menu.py index 14b615e..e856f7e 100644 --- a/menu.py +++ b/menu.py @@ -69,10 +69,10 @@ def game_menu(sounds, *options): if event.type == pygame.KEYDOWN: # Check for Alt modifier mods = pygame.key.get_mods() - alt_pressed = mods & pygame.KMOD_ALT + altPressed = mods & pygame.KMOD_ALT # Volume controls (require Alt) - if alt_pressed: + if altPressed: if event.key == pygame.K_PAGEUP: adjust_master_volume(0.1) elif event.key == pygame.K_PAGEDOWN: @@ -88,7 +88,8 @@ def game_menu(sounds, *options): # Regular menu navigation (no Alt required) else: if event.key == pygame.K_ESCAPE: - exit_game() + # Exit with fade if music is playing + exit_game(500 if pygame.mixer.music.get_busy() else 0) elif event.key == pygame.K_HOME: if currentIndex != 0: currentIndex = 0 @@ -131,13 +132,17 @@ def game_menu(sounds, *options): time.sleep(sounds['menu-select'].get_length()) except: pass - eval(options[currentIndex] + "()") + + # Special case for exit_game with fade + if options[currentIndex] == "exit_game": + exit_game(500 if pygame.mixer.music.get_busy() else 0) + else: + eval(options[currentIndex] + "()") except: lastSpoken = -1 - pygame.mixer.music.fadeout(500) try: - pygame.mixer.music.fadeout(750) - time.sleep(1.0) + pygame.mixer.music.fadeout(500) + time.sleep(0.5) except: pass @@ -274,13 +279,34 @@ def donate(): pygame.mixer.music.pause() webbrowser.open('https://ko-fi.com/stormux') -def exit_game(): - """Clean up and exit the game.""" +def exit_game(fade=0): + """Clean up and exit the game. + + Args: + fade (int): Milliseconds to fade out music before exiting. + 0 means stop immediately (default) + """ # Get speech instance and check provider type speech = Speech.get_instance() - if speech.provider_name == "speechd": + if speech.providerName == "speechd": speech.close() - pygame.mixer.music.stop() - pygame.quit() + # Handle music based on fade parameter + try: + if fade > 0: + pygame.mixer.music.fadeout(fade) + # Brief pause to allow fade to start, but not complete + pygame.time.wait(min(fade // 4, 200)) # Wait up to 200ms maximum + else: + pygame.mixer.music.stop() + except Exception as e: + print(f"Warning: Could not handle music during exit: {e}") + + # Clean up pygame + try: + pygame.quit() + except Exception as e: + print(f"Warning: Error during pygame.quit(): {e}") + + # Exit the program exit() diff --git a/scoreboard.py b/scoreboard.py index 7848d1e..db9e343 100644 --- a/scoreboard.py +++ b/scoreboard.py @@ -17,25 +17,25 @@ from .config import localConfig, write_config, read_config class Scoreboard: """Handles high score tracking with player names.""" - def __init__(self, score=0, config_service=None, speech=None): + def __init__(self, score=0, configService=None, speech=None): """Initialize scoreboard. Args: score (int): Initial score (default: 0) - config_service (ConfigService): Config service (default: global instance) + configService (ConfigService): Config service (default: global instance) speech (Speech): Speech system (default: global instance) """ - self.config_service = config_service or ConfigService.get_instance() + self.configService = configService or ConfigService.get_instance() self.speech = speech or Speech.get_instance() - self.current_score = score - self.high_scores = [] + self.currentScore = score + self.highScores = [] # For backward compatibility read_config() try: - # Try to use config_service - self.config_service.local_config.add_section("scoreboard") + # Try to use configService + self.configService.local_config.add_section("scoreboard") except: # Fallback to old method try: @@ -46,10 +46,10 @@ class Scoreboard: # Load existing high scores for i in range(1, 11): try: - # Try to use config_service - score = self.config_service.local_config.getint("scoreboard", f"score_{i}") - name = self.config_service.local_config.get("scoreboard", f"name_{i}") - self.high_scores.append({ + # Try to use configService + score = self.configService.local_config.getint("scoreboard", f"score_{i}") + name = self.configService.local_config.get("scoreboard", f"name_{i}") + self.highScores.append({ 'name': name, 'score': score }) @@ -58,45 +58,45 @@ class Scoreboard: try: score = localConfig.getint("scoreboard", f"score_{i}") name = localConfig.get("scoreboard", f"name_{i}") - self.high_scores.append({ + self.highScores.append({ 'name': name, 'score': score }) except: - self.high_scores.append({ + self.highScores.append({ 'name': "Player", 'score': 0 }) # Sort high scores by score value in descending order - self.high_scores.sort(key=lambda x: x['score'], reverse=True) + self.highScores.sort(key=lambda x: x['score'], reverse=True) def get_score(self): """Get current score.""" - return self.current_score + return self.currentScore def get_high_scores(self): """Get list of high scores.""" - return self.high_scores + return self.highScores def decrease_score(self, points=1): """Decrease the current score.""" - self.current_score -= int(points) + self.currentScore -= int(points) return self def increase_score(self, points=1): """Increase the current score.""" - self.current_score += int(points) + self.currentScore += int(points) return self def set_score(self, score): """Set the current score to a specific value.""" - self.current_score = int(score) + self.currentScore = int(score) return self def reset_score(self): """Reset the current score to zero.""" - self.current_score = 0 + self.currentScore = 0 return self def check_high_score(self): @@ -105,8 +105,8 @@ class Scoreboard: Returns: int: Position (1-10) if high score, None if not """ - for i, entry in enumerate(self.high_scores): - if self.current_score > entry['score']: + for i, entry in enumerate(self.highScores): + if self.currentScore > entry['score']: return i + 1 return None @@ -132,34 +132,34 @@ class Scoreboard: name = "Player" # Insert new score at correct position - self.high_scores.insert(position - 1, { + self.highScores.insert(position - 1, { 'name': name, - 'score': self.current_score + 'score': self.currentScore }) # Keep only top 10 - self.high_scores = self.high_scores[: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.high_scores): - self.config_service.local_config.set("scoreboard", f"score_{i+1}", str(entry['score'])) - self.config_service.local_config.set("scoreboard", f"name_{i+1}", entry['name']) + for i, entry in enumerate(self.highScores): + self.configService.local_config.set("scoreboard", f"score_{i+1}", str(entry['score'])) + self.configService.local_config.set("scoreboard", f"name_{i+1}", entry['name']) - # Try to write with config_service + # Try to write with configService try: - self.config_service.write_local_config() + self.configService.write_local_config() except Exception as e: - # Fallback to old method if config_service fails - for i, entry in enumerate(self.high_scores): + # Fallback to old method if configService fails + for i, entry in enumerate(self.highScores): 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: # If all else fails, try direct old method - for i, entry in enumerate(self.high_scores): + for i, entry in enumerate(self.highScores): localConfig.set("scoreboard", f"score_{i+1}", str(entry['score'])) localConfig.set("scoreboard", f"name_{i+1}", entry['name']) write_config() diff --git a/services.py b/services.py index a0d3fb4..fb16e54 100644 --- a/services.py +++ b/services.py @@ -29,20 +29,20 @@ class ConfigService: def __init__(self): """Initialize configuration parsers.""" - self.local_config = configparser.ConfigParser() - self.global_config = configparser.ConfigParser() - self.game_title = None - self.path_service = None + self.localConfig = configparser.ConfigParser() + self.globalConfig = configparser.ConfigParser() + self.gameTitle = None + self.pathService = None - def set_game_info(self, game_title, path_service): + def set_game_info(self, gameTitle, pathService): """Set game information and initialize configs. Args: - game_title (str): Title of the game - path_service (PathService): Path service instance + gameTitle (str): Title of the game + pathService (PathService): Path service instance """ - self.game_title = game_title - self.path_service = path_service + self.gameTitle = gameTitle + self.pathService = pathService # Load existing configurations self.read_local_config() @@ -51,56 +51,56 @@ class ConfigService: def read_local_config(self): """Read local configuration from file.""" try: - # Try to use path_service if available - if self.path_service and self.path_service.game_path: - with open(os.path.join(self.path_service.game_path, "config.ini"), 'r') as configfile: - self.local_config.read_file(configfile) + # Try to use pathService if available + if self.pathService and self.pathService.gamePath: + with open(os.path.join(self.pathService.gamePath, "config.ini"), 'r') as configFile: + self.localConfig.read_file(configFile) # Fallback to global gamePath elif gamePath: - with open(os.path.join(gamePath, "config.ini"), 'r') as configfile: - self.local_config.read_file(configfile) + with open(os.path.join(gamePath, "config.ini"), 'r') as configFile: + self.localConfig.read_file(configFile) # Delegate to old function as last resort else: read_config(False) - self.local_config = configparser.ConfigParser() - self.local_config.read_dict(globals().get('localConfig', {})) + self.localConfig = configparser.ConfigParser() + self.localConfig.read_dict(globals().get('localConfig', {})) except: pass def read_global_config(self): """Read global configuration from file.""" try: - # Try to use path_service if available - if self.path_service and self.path_service.global_path: - with open(os.path.join(self.path_service.global_path, "config.ini"), 'r') as configfile: - self.global_config.read_file(configfile) + # Try to use pathService if available + if self.pathService and self.pathService.globalPath: + with open(os.path.join(self.pathService.globalPath, "config.ini"), 'r') as configFile: + self.globalConfig.read_file(configFile) # Fallback to global globalPath elif globalPath: - with open(os.path.join(globalPath, "config.ini"), 'r') as configfile: - self.global_config.read_file(configfile) + with open(os.path.join(globalPath, "config.ini"), 'r') as configFile: + self.globalConfig.read_file(configFile) # Delegate to old function as last resort else: read_config(True) - self.global_config = configparser.ConfigParser() - self.global_config.read_dict(globals().get('globalConfig', {})) + self.globalConfig = configparser.ConfigParser() + self.globalConfig.read_dict(globals().get('globalConfig', {})) except: pass def write_local_config(self): """Write local configuration to file.""" try: - # Try to use path_service if available - if self.path_service and self.path_service.game_path: - with open(os.path.join(self.path_service.game_path, "config.ini"), 'w') as configfile: - self.local_config.write(configfile) + # Try to use pathService if available + if self.pathService and self.pathService.gamePath: + with open(os.path.join(self.pathService.gamePath, "config.ini"), 'w') as configFile: + self.localConfig.write(configFile) # Fallback to global gamePath elif gamePath: - with open(os.path.join(gamePath, "config.ini"), 'w') as configfile: - self.local_config.write(configfile) + with open(os.path.join(gamePath, "config.ini"), 'w') as configFile: + self.localConfig.write(configFile) # Delegate to old function as last resort else: # Update old global config - globals()['localConfig'] = self.local_config + globals()['localConfig'] = self.localConfig write_config(False) except Exception as e: print(f"Warning: Failed to write local config: {e}") @@ -108,18 +108,18 @@ class ConfigService: def write_global_config(self): """Write global configuration to file.""" try: - # Try to use path_service if available - if self.path_service and self.path_service.global_path: - with open(os.path.join(self.path_service.global_path, "config.ini"), 'w') as configfile: - self.global_config.write(configfile) + # Try to use pathService if available + if self.pathService and self.pathService.globalPath: + with open(os.path.join(self.pathService.globalPath, "config.ini"), 'w') as configFile: + self.globalConfig.write(configFile) # Fallback to global globalPath elif globalPath: - with open(os.path.join(globalPath, "config.ini"), 'w') as configfile: - self.global_config.write(configfile) + with open(os.path.join(globalPath, "config.ini"), 'w') as configFile: + self.globalConfig.write(configFile) # Delegate to old function as last resort else: # Update old global config - globals()['globalConfig'] = self.global_config + globals()['globalConfig'] = self.globalConfig write_config(True) except Exception as e: print(f"Warning: Failed to write global config: {e}") @@ -139,75 +139,75 @@ class VolumeService: def __init__(self): """Initialize volume settings.""" - self.bgm_volume = 0.75 # Default background music volume - self.sfx_volume = 1.0 # Default sound effects volume - self.master_volume = 1.0 # Default master volume + 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, pygame_mixer=None): + 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) - pygame_mixer: Optional pygame.mixer module for real-time updates + pygameMixer: Optional pygame.mixer module for real-time updates """ - self.master_volume = max(0.0, min(1.0, self.master_volume + change)) + self.masterVolume = max(0.0, min(1.0, self.masterVolume + change)) # Update real-time audio if pygame mixer is provided - if pygame_mixer: + if pygameMixer: # Update music volume - if pygame_mixer.music.get_busy(): - pygame_mixer.music.set_volume(self.bgm_volume * self.master_volume) + if pygameMixer.music.get_busy(): + pygameMixer.music.set_volume(self.bgmVolume * self.masterVolume) # Update all sound channels - for i in range(pygame_mixer.get_num_channels()): - channel = pygame_mixer.Channel(i) + for i in range(pygameMixer.get_num_channels()): + channel = pygameMixer.Channel(i) if channel.get_busy(): - current_volume = channel.get_volume() - if isinstance(current_volume, (int, float)): + currentVolume = channel.get_volume() + if isinstance(currentVolume, (int, float)): # Mono audio - channel.set_volume(current_volume * self.master_volume) + channel.set_volume(currentVolume * self.masterVolume) else: # Stereo audio - left, right = current_volume - channel.set_volume(left * self.master_volume, right * self.master_volume) + left, right = currentVolume + channel.set_volume(left * self.masterVolume, right * self.masterVolume) - def adjust_bgm_volume(self, change, pygame_mixer=None): + 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) - pygame_mixer: Optional pygame.mixer module for real-time updates + pygameMixer: Optional pygame.mixer module for real-time updates """ - self.bgm_volume = max(0.0, min(1.0, self.bgm_volume + change)) + self.bgmVolume = max(0.0, min(1.0, self.bgmVolume + change)) # Update real-time audio if pygame mixer is provided - if pygame_mixer and pygame_mixer.music.get_busy(): - pygame_mixer.music.set_volume(self.bgm_volume * self.master_volume) + if pygameMixer and pygameMixer.music.get_busy(): + pygameMixer.music.set_volume(self.bgmVolume * self.masterVolume) - def adjust_sfx_volume(self, change, pygame_mixer=None): + 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) - pygame_mixer: Optional pygame.mixer module for real-time updates + pygameMixer: Optional pygame.mixer module for real-time updates """ - self.sfx_volume = max(0.0, min(1.0, self.sfx_volume + change)) + self.sfxVolume = max(0.0, min(1.0, self.sfxVolume + change)) # Update real-time audio if pygame mixer is provided - if pygame_mixer: + if pygameMixer: # Update all sound channels except reserved ones - for i in range(pygame_mixer.get_num_channels()): - channel = pygame_mixer.Channel(i) + for i in range(pygameMixer.get_num_channels()): + channel = pygameMixer.Channel(i) if channel.get_busy(): - current_volume = channel.get_volume() - if isinstance(current_volume, (int, float)): + currentVolume = channel.get_volume() + if isinstance(currentVolume, (int, float)): # Mono audio - channel.set_volume(current_volume * self.sfx_volume * self.master_volume) + channel.set_volume(currentVolume * self.sfxVolume * self.masterVolume) else: # Stereo audio - left, right = current_volume - channel.set_volume(left * self.sfx_volume * self.master_volume, - right * self.sfx_volume * self.master_volume) + 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. @@ -215,7 +215,7 @@ class VolumeService: Returns: float: Current adjusted BGM volume """ - return self.bgm_volume * self.master_volume + return self.bgmVolume * self.masterVolume def get_sfx_volume(self): """Get the current SFX volume with master adjustment. @@ -223,7 +223,7 @@ class VolumeService: Returns: float: Current adjusted SFX volume """ - return self.sfx_volume * self.master_volume + return self.sfxVolume * self.masterVolume class PathService: @@ -240,35 +240,35 @@ class PathService: def __init__(self): """Initialize path variables.""" - self.global_path = None - self.game_path = None - self.game_name = None + self.globalPath = None + self.gamePath = None + self.gameName = None # Try to initialize from global variables for backward compatibility global gamePath, globalPath if gamePath: - self.game_path = gamePath + self.gamePath = gamePath if globalPath: - self.global_path = globalPath + self.globalPath = globalPath - def initialize(self, game_title): + def initialize(self, gameTitle): """Initialize paths for a game. Args: - game_title (str): Title of the game + gameTitle (str): Title of the game """ - self.game_name = game_title - self.global_path = os.path.join(BaseDirectory.xdg_config_home, "storm-games") - self.game_path = os.path.join(self.global_path, - str.lower(str.replace(game_title, " ", "-"))) + self.gameName = gameTitle + 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.game_path): - os.makedirs(self.game_path) + if not os.path.exists(self.gamePath): + os.makedirs(self.gamePath) # Update global variables for backward compatibility global gamePath, globalPath - gamePath = self.game_path - globalPath = self.global_path + gamePath = self.gamePath + globalPath = self.globalPath return self diff --git a/sound.py b/sound.py index 1147bcc..f6ca7fb 100644 --- a/sound.py +++ b/sound.py @@ -17,21 +17,21 @@ from os.path import isfile, join from .services import VolumeService # Global instance for backward compatibility -volume_service = VolumeService.get_instance() +volumeService = VolumeService.get_instance() class Sound: """Handles sound loading and playback.""" - def __init__(self, sound_dir="sounds/", volume_service=None): + def __init__(self, soundDir="sounds/", volumeService=None): """Initialize sound system. Args: - sound_dir (str): Directory containing sound files (default: "sounds/") - volume_service (VolumeService): Volume service (default: global instance) + soundDir (str): Directory containing sound files (default: "sounds/") + volumeService (VolumeService): Volume service (default: global instance) """ - self.sound_dir = sound_dir + self.soundDir = soundDir self.sounds = {} - self.volume_service = volume_service or VolumeService.get_instance() + self.volumeService = volumeService or VolumeService.get_instance() # Initialize pygame mixer if not already done if not pygame.mixer.get_init(): @@ -46,13 +46,13 @@ class Sound: def load_sounds(self): """Load all sound files from the sound directory.""" try: - sound_files = [f for f in listdir(self.sound_dir) - if isfile(join(self.sound_dir, f)) + soundFiles = [f for f in listdir(self.soundDir) + if isfile(join(self.soundDir, f)) and (f.split('.')[1].lower() in ["ogg", "wav"])] # Create dictionary of sound objects - for f in sound_files: - self.sounds[f.split('.')[0]] = pygame.mixer.Sound(join(self.sound_dir, f)) + for f in soundFiles: + self.sounds[f.split('.')[0]] = pygame.mixer.Sound(join(self.soundDir, f)) except Exception as e: print(f"Error loading sounds: {e}") @@ -70,109 +70,109 @@ class Sound: """ return self.sounds - def play_bgm(self, music_file): + def play_bgm(self, musicFile): """Play background music with proper volume settings. Args: - music_file (str): Path to the music file to play + musicFile (str): Path to the music file to play """ try: pygame.mixer.music.stop() - pygame.mixer.music.load(music_file) - pygame.mixer.music.set_volume(self.volume_service.get_bgm_volume()) + pygame.mixer.music.load(musicFile) + pygame.mixer.music.set_volume(self.volumeService.get_bgm_volume()) pygame.mixer.music.play(-1) # Loop indefinitely except Exception as e: pass - def play_sound(self, sound_name, volume=1.0): + def play_sound(self, soundName, volume=1.0): """Play a sound with current volume settings applied. Args: - sound_name (str): Name of sound to play + soundName (str): Name of sound to play volume (float): Base volume for the sound (0.0-1.0, default: 1.0) Returns: pygame.mixer.Channel: The channel the sound is playing on """ - if sound_name not in self.sounds: + if soundName not in self.sounds: return None - sound = self.sounds[sound_name] + sound = self.sounds[soundName] channel = sound.play() if channel: - channel.set_volume(volume * self.volume_service.get_sfx_volume()) + channel.set_volume(volume * self.volumeService.get_sfx_volume()) return channel - def calculate_volume_and_pan(self, player_pos, obj_pos, max_distance=12): + def calculate_volume_and_pan(self, playerPos, objPos, maxDistance=12): """Calculate volume and stereo panning based on relative positions. Args: - player_pos (float): Player's position on x-axis - obj_pos (float): Object's position on x-axis - max_distance (float): Maximum audible distance (default: 12) + playerPos (float): Player's position on x-axis + objPos (float): Object's position on x-axis + maxDistance (float): Maximum audible distance (default: 12) Returns: tuple: (volume, left_vol, right_vol) values between 0 and 1 """ - distance = abs(player_pos - obj_pos) + distance = abs(playerPos - objPos) - if distance > max_distance: + if distance > maxDistance: return 0, 0, 0 # No sound if out of range # Calculate volume (non-linear scaling for more noticeable changes) # Apply masterVolume as the maximum possible volume - volume = (((max_distance - distance) / max_distance) ** 1.5) * self.volume_service.master_volume + volume = (((maxDistance - distance) / maxDistance) ** 1.5) * self.volumeService.masterVolume # Determine left/right based on relative position - if player_pos < obj_pos: + if playerPos < objPos: # Object is to the right - left = max(0, 1 - (obj_pos - player_pos) / max_distance) + left = max(0, 1 - (objPos - playerPos) / maxDistance) right = 1 - elif player_pos > obj_pos: + elif playerPos > objPos: # Object is to the left left = 1 - right = max(0, 1 - (player_pos - obj_pos) / max_distance) + right = max(0, 1 - (playerPos - objPos) / maxDistance) else: # Player is on the object left = right = 1 return volume, left, right - def obj_play(self, sound_name, player_pos, obj_pos, loop=True): + def obj_play(self, soundName, playerPos, objPos, loop=True): """Play a sound with positional audio. Args: - sound_name (str): Name of sound to play - player_pos (float): Player's position for audio panning - obj_pos (float): Object's position for audio panning + soundName (str): Name of sound to play + playerPos (float): Player's position for audio panning + objPos (float): Object's position for audio panning loop (bool): Whether to loop the sound (default: True) Returns: pygame.mixer.Channel: Sound channel object, or None if out of range """ - if sound_name not in self.sounds: + if soundName not in self.sounds: return None - volume, left, right = self.calculate_volume_and_pan(player_pos, obj_pos) + volume, left, right = self.calculate_volume_and_pan(playerPos, objPos) if volume == 0: return None # Don't play if out of range # Play the sound on a new channel - channel = self.sounds[sound_name].play(-1 if loop else 0) + channel = self.sounds[soundName].play(-1 if loop else 0) if channel: channel.set_volume( - volume * left * self.volume_service.sfx_volume, - volume * right * self.volume_service.sfx_volume + volume * left * self.volumeService.sfxVolume, + volume * right * self.volumeService.sfxVolume ) return channel - def obj_update(self, channel, player_pos, obj_pos): + def obj_update(self, channel, playerPos, objPos): """Update positional audio for a playing sound. Args: channel: Sound channel to update - player_pos (float): New player position - obj_pos (float): New object position + playerPos (float): New player position + objPos (float): New object position Returns: pygame.mixer.Channel: Updated channel, or None if sound should stop @@ -180,15 +180,15 @@ class Sound: if channel is None: return None - volume, left, right = self.calculate_volume_and_pan(player_pos, obj_pos) + volume, left, right = self.calculate_volume_and_pan(playerPos, objPos) if volume == 0: channel.stop() return None # Apply the volume and pan channel.set_volume( - volume * left * self.volume_service.sfx_volume, - volume * right * self.volume_service.sfx_volume + volume * left * self.volumeService.sfxVolume, + volume * right * self.volumeService.sfxVolume ) return channel @@ -207,19 +207,19 @@ class Sound: except: return channel - def play_ambiance(self, sound_names, probability, random_location=False): + def play_ambiance(self, soundNames, probability, randomLocation=False): """Play random ambient sounds with optional positional audio. Args: - sound_names (list): List of possible sound names to choose from + soundNames (list): List of possible sound names to choose from probability (int): Chance to play (1-100) - random_location (bool): Whether to randomize stereo position + randomLocation (bool): Whether to randomize stereo position Returns: pygame.mixer.Channel: Sound channel if played, None otherwise """ # Check if any of the sounds in the list is already playing - for sound_name in sound_names: + for soundName in soundNames: if pygame.mixer.find_channel(True) and pygame.mixer.find_channel(True).get_busy(): return None @@ -227,124 +227,124 @@ class Sound: return None # Choose a random sound from the list - ambiance_sound = random.choice(sound_names) - if ambiance_sound not in self.sounds: + ambianceSound = random.choice(soundNames) + if ambianceSound not in self.sounds: return None - channel = self.sounds[ambiance_sound].play() + channel = self.sounds[ambianceSound].play() - if random_location and channel: - left_volume = random.random() * self.volume_service.get_sfx_volume() - right_volume = random.random() * self.volume_service.get_sfx_volume() - channel.set_volume(left_volume, right_volume) + if randomLocation and channel: + leftVolume = random.random() * self.volumeService.get_sfx_volume() + rightVolume = random.random() * self.volumeService.get_sfx_volume() + channel.set_volume(leftVolume, rightVolume) return channel - def play_random(self, sound_prefix, pause=False, interrupt=False): + def play_random(self, soundPrefix, pause=False, interrupt=False): """Play a random variation of a sound. Args: - sound_prefix (str): Base name of sound (will match all starting with this) + soundPrefix (str): Base name of sound (will match all starting with this) pause (bool): Whether to pause execution until sound finishes interrupt (bool): Whether to interrupt other sounds """ keys = [] for i in self.sounds.keys(): - if re.match("^" + sound_prefix + ".*", i): + if re.match("^" + soundPrefix + ".*", i): keys.append(i) if not keys: # No matching sounds found return None - random_key = random.choice(keys) + randomKey = random.choice(keys) if interrupt: - self.cut_scene(random_key) + self.cut_scene(randomKey) return - channel = self.sounds[random_key].play() - sfx_volume = self.volume_service.get_sfx_volume() + channel = self.sounds[randomKey].play() + sfxVolume = self.volumeService.get_sfx_volume() if channel: - channel.set_volume(sfx_volume, sfx_volume) + channel.set_volume(sfxVolume, sfxVolume) if pause: - time.sleep(self.sounds[random_key].get_length()) + time.sleep(self.sounds[randomKey].get_length()) return channel - def play_random_positional(self, sound_prefix, player_x, object_x): + def play_random_positional(self, soundPrefix, playerX, objectX): """Play a random variation of a sound with positional audio. Args: - sound_prefix (str): Base name of sound to match - player_x (float): Player's x position - object_x (float): Object's x position + soundPrefix (str): Base name of sound to match + playerX (float): Player's x position + objectX (float): Object's x position Returns: pygame.mixer.Channel: Sound channel if played, None otherwise """ - keys = [k for k in self.sounds.keys() if k.startswith(sound_prefix)] + keys = [k for k in self.sounds.keys() if k.startswith(soundPrefix)] if not keys: return None - random_key = random.choice(keys) - volume, left, right = self.calculate_volume_and_pan(player_x, object_x) + randomKey = random.choice(keys) + volume, left, right = self.calculate_volume_and_pan(playerX, objectX) if volume == 0: return None - channel = self.sounds[random_key].play() + channel = self.sounds[randomKey].play() if channel: channel.set_volume( - volume * left * self.volume_service.sfx_volume, - volume * right * self.volume_service.sfx_volume + volume * left * self.volumeService.sfxVolume, + volume * right * self.volumeService.sfxVolume ) return channel - def play_directional_sound(self, sound_name, player_pos, obj_pos, center_distance=3, volume=1.0): + def play_directional_sound(self, soundName, playerPos, objPos, centerDistance=3, volume=1.0): """Play a sound with simplified directional audio. For sounds that need to be heard clearly regardless of distance, but still provide directional feedback. Sound plays at full volume but pans left/right based on relative position. Args: - sound_name (str): Name of sound to play - player_pos (float): Player's x position - obj_pos (float): Object's x position - center_distance (float): Distance within which sound plays center (default: 3) + soundName (str): Name of sound to play + playerPos (float): Player's x position + objPos (float): Object's x position + centerDistance (float): Distance within which sound plays center (default: 3) volume (float): Base volume multiplier (0.0-1.0, default: 1.0) Returns: pygame.mixer.Channel: The channel the sound is playing on """ - if sound_name not in self.sounds: + if soundName not in self.sounds: return None - channel = self.sounds[sound_name].play() + channel = self.sounds[soundName].play() if channel: # Apply volume settings - final_volume = volume * self.volume_service.get_sfx_volume() + finalVolume = volume * self.volumeService.get_sfx_volume() # If player is within centerDistance tiles of object, play in center - if abs(player_pos - obj_pos) <= center_distance: + if abs(playerPos - objPos) <= centerDistance: # Equal volume in both speakers (center) - channel.set_volume(final_volume, final_volume) - elif player_pos > obj_pos: + channel.set_volume(finalVolume, finalVolume) + elif playerPos > objPos: # Object is to the left of player - channel.set_volume(final_volume, (final_volume + 0.01) / 2) + channel.set_volume(finalVolume, (finalVolume + 0.01) / 2) else: # Object is to the right of player - channel.set_volume((final_volume + 0.01) / 2, final_volume) + channel.set_volume((finalVolume + 0.01) / 2, finalVolume) return channel - def cut_scene(self, sound_name): + def cut_scene(self, soundName): """Play a sound as a cut scene, stopping other sounds. Args: - sound_name (str): Name of sound to play + soundName (str): Name of sound to play """ - if sound_name not in self.sounds: + if soundName not in self.sounds: return pygame.event.clear() @@ -354,11 +354,11 @@ class Sound: channel = pygame.mixer.Channel(0) # Apply the appropriate volume settings - sfx_volume = self.volume_service.get_sfx_volume() - channel.set_volume(sfx_volume, sfx_volume) + sfxVolume = self.volumeService.get_sfx_volume() + channel.set_volume(sfxVolume, sfxVolume) # Play the sound - channel.play(self.sounds[sound_name]) + channel.play(self.sounds[soundName]) while pygame.mixer.get_busy(): for event in pygame.event.get(): @@ -367,74 +367,74 @@ class Sound: return pygame.time.delay(10) - def play_random_falling(self, sound_prefix, player_x, object_x, start_y, - current_y=0, max_y=20, existing_channel=None): + def play_random_falling(self, soundPrefix, playerX, objectX, startY, + currentY=0, maxY=20, existingChannel=None): """Play or update a falling sound with positional audio and volume based on height. Args: - sound_prefix (str): Base name of sound to match - player_x (float): Player's x position - object_x (float): Object's x position - start_y (float): Starting Y position (0-20, higher = quieter start) - current_y (float): Current Y position (0 = ground level) (default: 0) - max_y (float): Maximum Y value (default: 20) - existing_channel: Existing sound channel to update (default: None) + soundPrefix (str): Base name of sound to match + playerX (float): Player's x position + objectX (float): Object's x position + startY (float): Starting Y position (0-20, higher = quieter start) + currentY (float): Current Y position (0 = ground level) (default: 0) + maxY (float): Maximum Y value (default: 20) + existingChannel: Existing sound channel to update (default: None) Returns: pygame.mixer.Channel: Sound channel for updating position/volume, or None if sound should stop """ # Calculate horizontal positioning - volume, left, right = self.calculate_volume_and_pan(player_x, object_x) + volume, left, right = self.calculate_volume_and_pan(playerX, objectX) - # Calculate vertical fall volume multiplier (0 at max_y, 1 at y=0) - fall_multiplier = 1 - (current_y / max_y) + # Calculate vertical fall volume multiplier (0 at maxY, 1 at y=0) + fallMultiplier = 1 - (currentY / maxY) # Adjust final volumes - final_volume = volume * fall_multiplier - final_left = left * final_volume - final_right = right * final_volume + finalVolume = volume * fallMultiplier + finalLeft = left * finalVolume + finalRight = right * finalVolume - if existing_channel is not None: + if existingChannel is not None: if volume == 0: # Out of audible range - existing_channel.stop() + existingChannel.stop() return None - existing_channel.set_volume( - final_left * self.volume_service.sfx_volume, - final_right * self.volume_service.sfx_volume + existingChannel.set_volume( + finalLeft * self.volumeService.sfxVolume, + finalRight * self.volumeService.sfxVolume ) - return existing_channel + return existingChannel else: # Need to create new channel if volume == 0: # Don't start if out of range return None # Find matching sound files - keys = [k for k in self.sounds.keys() if k.startswith(sound_prefix)] + keys = [k for k in self.sounds.keys() if k.startswith(soundPrefix)] if not keys: return None - random_key = random.choice(keys) - channel = self.sounds[random_key].play() + randomKey = random.choice(keys) + channel = self.sounds[randomKey].play() if channel: channel.set_volume( - final_left * self.volume_service.sfx_volume, - final_right * self.volume_service.sfx_volume + finalLeft * self.volumeService.sfxVolume, + finalRight * self.volumeService.sfxVolume ) return channel # Global functions for backward compatibility -def play_bgm(music_file): +def play_bgm(musicFile): """Play background music with proper volume settings. Args: - music_file (str): Path to the music file to play + musicFile (str): Path to the music file to play """ try: pygame.mixer.music.stop() - pygame.mixer.music.load(music_file) - pygame.mixer.music.set_volume(volume_service.get_bgm_volume()) + pygame.mixer.music.load(musicFile) + pygame.mixer.music.set_volume(volumeService.get_bgm_volume()) pygame.mixer.music.play(-1) # Loop indefinitely except Exception as e: pass @@ -445,7 +445,7 @@ def adjust_master_volume(change): Args: change (float): Amount to change volume by (positive or negative) """ - volume_service.adjust_master_volume(change, pygame.mixer) + volumeService.adjust_master_volume(change, pygame.mixer) def adjust_bgm_volume(change): """Adjust only the background music volume. @@ -453,7 +453,7 @@ def adjust_bgm_volume(change): Args: change (float): Amount to change volume by (positive or negative) """ - volume_service.adjust_bgm_volume(change, pygame.mixer) + volumeService.adjust_bgm_volume(change, pygame.mixer) def adjust_sfx_volume(change): """Adjust volume for sound effects only. @@ -461,37 +461,37 @@ def adjust_sfx_volume(change): Args: change (float): Amount to change volume by (positive or negative) """ - volume_service.adjust_sfx_volume(change, pygame.mixer) + volumeService.adjust_sfx_volume(change, pygame.mixer) -def calculate_volume_and_pan(player_pos, obj_pos): +def calculate_volume_and_pan(playerPos, objPos): """Calculate volume and stereo panning based on relative positions. Args: - player_pos (float): Player's position on x-axis - obj_pos (float): Object's position on x-axis + playerPos (float): Player's position on x-axis + objPos (float): Object's position on x-axis Returns: tuple: (volume, left_vol, right_vol) values between 0 and 1 """ - distance = abs(player_pos - obj_pos) - max_distance = 12 # Maximum audible distance + distance = abs(playerPos - objPos) + maxDistance = 12 # Maximum audible distance - if distance > max_distance: + if distance > maxDistance: return 0, 0, 0 # No sound if out of range # Calculate volume (non-linear scaling for more noticeable changes) # Apply masterVolume as the maximum possible volume - volume = (((max_distance - distance) / max_distance) ** 1.5) * volume_service.master_volume + volume = (((maxDistance - distance) / maxDistance) ** 1.5) * volumeService.masterVolume # Determine left/right based on relative position - if player_pos < obj_pos: + if playerPos < objPos: # Object is to the right - left = max(0, 1 - (obj_pos - player_pos) / max_distance) + left = max(0, 1 - (objPos - playerPos) / maxDistance) right = 1 - elif player_pos > obj_pos: + elif playerPos > objPos: # Object is to the left left = 1 - right = max(0, 1 - (player_pos - obj_pos) / max_distance) + right = max(0, 1 - (playerPos - objPos) / maxDistance) else: # Player is on the object left = right = 1 @@ -510,40 +510,40 @@ def play_sound(sound, volume=1.0): """ channel = sound.play() if channel: - channel.set_volume(volume * volume_service.get_sfx_volume()) + channel.set_volume(volume * volumeService.get_sfx_volume()) return channel -def obj_play(sounds, soundName, player_pos, obj_pos, loop=True): +def obj_play(sounds, soundName, playerPos, objPos, loop=True): """Play a sound with positional audio. Args: sounds (dict): Dictionary of sound objects soundName (str): Name of sound to play - player_pos (float): Player's position for audio panning - obj_pos (float): Object's position for audio panning + playerPos (float): Player's position for audio panning + objPos (float): Object's position for audio panning loop (bool): Whether to loop the sound (default: True) Returns: pygame.mixer.Channel: Sound channel object, or None if out of range """ - volume, left, right = calculate_volume_and_pan(player_pos, obj_pos) + volume, left, right = calculate_volume_and_pan(playerPos, objPos) if volume == 0: return None # Don't play if out of range # Play the sound on a new channel channel = sounds[soundName].play(-1 if loop else 0) if channel: - channel.set_volume(volume * left * volume_service.sfx_volume, - volume * right * volume_service.sfx_volume) + channel.set_volume(volume * left * volumeService.sfxVolume, + volume * right * volumeService.sfxVolume) return channel -def obj_update(channel, player_pos, obj_pos): +def obj_update(channel, playerPos, objPos): """Update positional audio for a playing sound. Args: channel: Sound channel to update - player_pos (float): New player position - obj_pos (float): New object position + playerPos (float): New player position + objPos (float): New object position Returns: pygame.mixer.Channel: Updated channel, or None if sound should stop @@ -551,14 +551,14 @@ def obj_update(channel, player_pos, obj_pos): if channel is None: return None - volume, left, right = calculate_volume_and_pan(player_pos, obj_pos) + volume, left, right = calculate_volume_and_pan(playerPos, objPos) if volume == 0: channel.stop() return None # Apply the volume and pan - channel.set_volume(volume * left * volume_service.sfx_volume, - volume * right * volume_service.sfx_volume) + channel.set_volume(volume * left * volumeService.sfxVolume, + volume * right * volumeService.sfxVolume) return channel def obj_stop(channel): @@ -601,8 +601,8 @@ def play_ambiance(sounds, soundNames, probability, randomLocation=False): channel = sounds[ambianceSound].play() if randomLocation and channel: - leftVolume = random.random() * volume_service.get_sfx_volume() - rightVolume = random.random() * volume_service.get_sfx_volume() + leftVolume = random.random() * volumeService.get_sfx_volume() + rightVolume = random.random() * volumeService.get_sfx_volume() channel.set_volume(leftVolume, rightVolume) return channel @@ -632,20 +632,20 @@ def play_random(sounds, soundName, pause=False, interrupt=False): channel = sounds[randomKey].play() if channel: - sfx_volume = volume_service.get_sfx_volume() - channel.set_volume(sfx_volume, sfx_volume) + sfxVolume = volumeService.get_sfx_volume() + channel.set_volume(sfxVolume, sfxVolume) if pause: time.sleep(sounds[randomKey].get_length()) -def play_random_positional(sounds, soundName, player_x, object_x): +def play_random_positional(sounds, soundName, playerX, objectX): """Play a random variation of a sound with positional audio. Args: sounds (dict): Dictionary of sound objects soundName (str): Base name of sound to match - player_x (float): Player's x position - object_x (float): Object's x position + playerX (float): Player's x position + objectX (float): Object's x position Returns: pygame.mixer.Channel: Sound channel if played, None otherwise @@ -655,15 +655,15 @@ def play_random_positional(sounds, soundName, player_x, object_x): return None randomKey = random.choice(keys) - volume, left, right = calculate_volume_and_pan(player_x, object_x) + volume, left, right = calculate_volume_and_pan(playerX, objectX) if volume == 0: return None channel = sounds[randomKey].play() if channel: - channel.set_volume(volume * left * volume_service.sfx_volume, - volume * right * volume_service.sfx_volume) + channel.set_volume(volume * left * volumeService.sfxVolume, + volume * right * volumeService.sfxVolume) return channel def play_directional_sound(sounds, soundName, playerPos, objPos, centerDistance=3, volume=1.0): @@ -686,7 +686,7 @@ def play_directional_sound(sounds, soundName, playerPos, objPos, centerDistance= channel = sounds[soundName].play() if channel: # Apply volume settings - finalVolume = volume * volume_service.get_sfx_volume() + finalVolume = volume * volumeService.get_sfx_volume() # If player is within centerDistance tiles of object, play in center if abs(playerPos - objPos) <= centerDistance: @@ -714,8 +714,8 @@ def cut_scene(sounds, soundName): channel = pygame.mixer.Channel(0) # Apply the appropriate volume settings - sfx_volume = volume_service.get_sfx_volume() - channel.set_volume(sfx_volume, sfx_volume) + sfxVolume = volumeService.get_sfx_volume() + channel.set_volume(sfxVolume, sfxVolume) # Play the sound channel.play(sounds[soundName]) @@ -727,42 +727,42 @@ def cut_scene(sounds, soundName): return pygame.time.delay(10) -def play_random_falling(sounds, soundName, player_x, object_x, start_y, - currentY=0, max_y=20, existing_channel=None): +def play_random_falling(sounds, soundName, playerX, objectX, startY, + currentY=0, maxY=20, existingChannel=None): """Play or update a falling sound with positional audio and volume based on height. Args: sounds (dict): Dictionary of sound objects soundName (str): Base name of sound to match - player_x (float): Player's x position - object_x (float): Object's x position - start_y (float): Starting Y position (0-20, higher = quieter start) + playerX (float): Player's x position + objectX (float): Object's x position + startY (float): Starting Y position (0-20, higher = quieter start) currentY (float): Current Y position (0 = ground level) (default: 0) - max_y (float): Maximum Y value (default: 20) - existing_channel: Existing sound channel to update (default: None) + maxY (float): Maximum Y value (default: 20) + existingChannel: Existing sound channel to update (default: None) Returns: pygame.mixer.Channel: Sound channel for updating position/volume, or None if sound should stop """ # Calculate horizontal positioning - volume, left, right = calculate_volume_and_pan(player_x, object_x) + volume, left, right = calculate_volume_and_pan(playerX, objectX) - # Calculate vertical fall volume multiplier (0 at max_y, 1 at y=0) - fallMultiplier = 1 - (currentY / max_y) + # Calculate vertical fall volume multiplier (0 at maxY, 1 at y=0) + fallMultiplier = 1 - (currentY / maxY) # Adjust final volumes finalVolume = volume * fallMultiplier finalLeft = left * finalVolume finalRight = right * finalVolume - if existing_channel is not None: + if existingChannel is not None: if volume == 0: # Out of audible range - existing_channel.stop() + existingChannel.stop() return None - existing_channel.set_volume(finalLeft * volume_service.sfx_volume, - finalRight * volume_service.sfx_volume) - return existing_channel + existingChannel.set_volume(finalLeft * volumeService.sfxVolume, + finalRight * volumeService.sfxVolume) + return existingChannel else: # Need to create new channel if volume == 0: # Don't start if out of range return None @@ -775,6 +775,6 @@ def play_random_falling(sounds, soundName, player_x, object_x, start_y, randomKey = random.choice(keys) channel = sounds[randomKey].play() if channel: - channel.set_volume(finalLeft * volume_service.sfx_volume, - finalRight * volume_service.sfx_volume) + channel.set_volume(finalLeft * volumeService.sfxVolume, + finalRight * volumeService.sfxVolume) return channel diff --git a/speech.py b/speech.py index 907a66a..7e9a591 100644 --- a/speech.py +++ b/speech.py @@ -28,19 +28,19 @@ class Speech: def __init__(self): """Initialize speech system with available provider.""" # Handle speech delays so we don't get stuttering - self.last_spoken = {"text": None, "time": 0} - self.speech_delay = 250 # ms + self.lastSpoken = {"text": None, "time": 0} + self.speechDelay = 250 # ms # Try to initialize a speech provider self.provider = None - self.provider_name = None + self.providerName = None # Try speechd first try: import speechd self.spd = speechd.Client() self.provider = self.spd - self.provider_name = "speechd" + self.providerName = "speechd" return except ImportError: pass @@ -50,7 +50,7 @@ class Speech: import accessible_output2.outputs.auto self.ao2 = accessible_output2.outputs.auto.Auto() self.provider = self.ao2 - self.provider_name = "accessible_output2" + self.providerName = "accessible_output2" return except ImportError: pass @@ -68,23 +68,23 @@ class Speech: if not self.provider: return - current_time = pygame.time.get_ticks() + currentTime = pygame.time.get_ticks() # Check if this is the same text within the delay window - if (self.last_spoken["text"] == text and - current_time - self.last_spoken["time"] < self.speech_delay): + if (self.lastSpoken["text"] == text and + currentTime - self.lastSpoken["time"] < self.speechDelay): return # Update last spoken tracking - self.last_spoken["text"] = text - self.last_spoken["time"] = current_time + self.lastSpoken["text"] = text + self.lastSpoken["time"] = currentTime # Proceed with speech - if self.provider_name == "speechd": + if self.providerName == "speechd": if interrupt: self.spd.cancel() self.spd.say(text) - elif self.provider_name == "accessible_output2": + elif self.providerName == "accessible_output2": self.ao2.speak(text, interrupt=interrupt) # Display the text on screen @@ -94,29 +94,29 @@ class Speech: font = pygame.font.Font(None, 36) # Wrap the text - max_width = screen.get_width() - 40 # Leave a 20-pixel margin on each side - wrapped_text = textwrap.wrap(text, width=max_width // font.size('A')[0]) + maxWidth = screen.get_width() - 40 # Leave a 20-pixel margin on each side + wrappedText = textwrap.wrap(text, width=maxWidth // font.size('A')[0]) # Render each line - text_surfaces = [font.render(line, True, (255, 255, 255)) for line in wrapped_text] + textSurfaces = [font.render(line, True, (255, 255, 255)) for line in wrappedText] screen.fill((0, 0, 0)) # Clear screen with black # Calculate total height of text block - total_height = sum(surface.get_height() for surface in text_surfaces) + totalHeight = sum(surface.get_height() for surface in textSurfaces) # Start y-position (centered vertically) - current_y = (screen.get_height() - total_height) // 2 + currentY = (screen.get_height() - totalHeight) // 2 # Blit each line of text - for surface in text_surfaces: - text_rect = surface.get_rect(center=(screen.get_width() // 2, current_y + surface.get_height() // 2)) - screen.blit(surface, text_rect) - current_y += surface.get_height() + for surface in textSurfaces: + textRect = surface.get_rect(center=(screen.get_width() // 2, currentY + surface.get_height() // 2)) + screen.blit(surface, textRect) + currentY += surface.get_height() pygame.display.flip() def close(self): """Clean up speech resources.""" - if self.provider_name == "speechd": + if self.providerName == "speechd": self.spd.close() # Global instance for backward compatibility -_speech_instance = None +_speechInstance = None def speak(text, interrupt=True): """Speak text using the global speech instance. @@ -125,10 +125,10 @@ def speak(text, interrupt=True): text (str): Text to speak and display interrupt (bool): Whether to interrupt current speech (default: True) """ - global _speech_instance - if _speech_instance is None: - _speech_instance = Speech.get_instance() - _speech_instance.speak(text, interrupt) + global _speechInstance + if _speechInstance is None: + _speechInstance = Speech.get_instance() + _speechInstance.speak(text, interrupt) def messagebox(text): """Display a simple message box with text. diff --git a/utils.py b/utils.py index 0838d86..8197ae2 100644 --- a/utils.py +++ b/utils.py @@ -36,10 +36,10 @@ class Game: self.title = title # Initialize services - self.path_service = PathService.get_instance().initialize(title) - self.config_service = ConfigService.get_instance() - self.config_service.set_game_info(title, self.path_service) - self.volume_service = VolumeService.get_instance() + 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 @@ -47,7 +47,7 @@ class Game: self._scoreboard = None # Display text instructions flag - self.display_text_usage_instructions = False + self.displayTextUsageInstructions = False @property def speech(self): @@ -68,7 +68,7 @@ class Game: Sound: Sound system instance """ if not self._sound: - self._sound = Sound("sounds/", self.volume_service) + self._sound = Sound("sounds/", self.volumeService) return self._sound @property @@ -79,7 +79,7 @@ class Game: Scoreboard: Scoreboard instance """ if not self._scoreboard: - self._scoreboard = Scoreboard(self.config_service) + self._scoreboard = Scoreboard(self.configService) return self._scoreboard def initialize(self): @@ -130,89 +130,89 @@ class Game: self.speech.speak(text, interrupt) return self - def play_bgm(self, music_file): + def play_bgm(self, musicFile): """Play background music. Args: - music_file (str): Path to music file + musicFile (str): Path to music file Returns: Game: Self for method chaining """ - self.sound.play_bgm(music_file) + self.sound.play_bgm(musicFile) return self - def display_text(self, text_lines): + def display_text(self, textLines): """Display text with navigation controls. Args: - text_lines (list): List of text lines + textLines (list): List of text lines Returns: Game: Self for method chaining """ # Store original text with blank lines for copying - original_text = text_lines.copy() + originalText = textLines.copy() # Create navigation text by filtering out blank lines - nav_text = [line for line in text_lines if line.strip()] + navText = [line for line in textLines if line.strip()] # Add instructions at the start on the first display - if not self.display_text_usage_instructions: + if not self.displayTextUsageInstructions: instructions = ("Press space to read the whole text. Use up and down arrows to navigate " "the text line by line. Press c to copy the current line to the clipboard " "or t to copy the entire text. Press enter or escape when you are done reading.") - nav_text.insert(0, instructions) - self.display_text_usage_instructions = True + navText.insert(0, instructions) + self.displayTextUsageInstructions = True # Add end marker - nav_text.append("End of text.") + navText.append("End of text.") - current_index = 0 - self.speech.speak(nav_text[current_index]) + 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() - alt_pressed = mods & pygame.KMOD_ALT + altPressed = mods & pygame.KMOD_ALT # Volume controls (require Alt) - if alt_pressed: + if altPressed: if event.key == pygame.K_PAGEUP: - self.volume_service.adjust_master_volume(0.1, pygame.mixer) + self.volumeService.adjust_master_volume(0.1, pygame.mixer) elif event.key == pygame.K_PAGEDOWN: - self.volume_service.adjust_master_volume(-0.1, pygame.mixer) + self.volumeService.adjust_master_volume(-0.1, pygame.mixer) elif event.key == pygame.K_HOME: - self.volume_service.adjust_bgm_volume(0.1, pygame.mixer) + self.volumeService.adjust_bgm_volume(0.1, pygame.mixer) elif event.key == pygame.K_END: - self.volume_service.adjust_bgm_volume(-0.1, pygame.mixer) + self.volumeService.adjust_bgm_volume(-0.1, pygame.mixer) elif event.key == pygame.K_INSERT: - self.volume_service.adjust_sfx_volume(0.1, pygame.mixer) + self.volumeService.adjust_sfx_volume(0.1, pygame.mixer) elif event.key == pygame.K_DELETE: - self.volume_service.adjust_sfx_volume(-0.1, pygame.mixer) + self.volumeService.adjust_sfx_volume(-0.1, pygame.mixer) else: if event.key in (pygame.K_ESCAPE, pygame.K_RETURN): return self - if event.key in [pygame.K_DOWN, pygame.K_s] and current_index < len(nav_text) - 1: - current_index += 1 - self.speech.speak(nav_text[current_index]) + 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 current_index > 0: - current_index -= 1 - self.speech.speak(nav_text[current_index]) + 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(original_text[1:-1])) + self.speech.speak('\n'.join(originalText[1:-1])) if event.key == pygame.K_c: try: import pyperclip - pyperclip.copy(nav_text[current_index]) - self.speech.speak("Copied " + nav_text[current_index] + " to the clipboard.") + pyperclip.copy(navText[currentIndex]) + self.speech.speak("Copied " + navText[currentIndex] + " to the clipboard.") except: self.speech.speak("Failed to copy the text to the clipboard.") @@ -220,7 +220,7 @@ class Game: try: import pyperclip # Join with newlines to preserve blank lines in full text - pyperclip.copy(''.join(original_text[2:-1])) + pyperclip.copy(''.join(originalText[2:-1])) self.speech.speak("Copied entire message to the clipboard.") except: self.speech.speak("Failed to copy the text to the clipboard.") @@ -239,12 +239,12 @@ class Game: # Utility functions -def check_for_updates(current_version, game_name, url): +def check_for_updates(currentVersion, gameName, url): """Check for game updates. Args: - current_version (str): Current version string (e.g. "1.0.0") - game_name (str): Name of the game + currentVersion (str): Current version string (e.g. "1.0.0") + gameName (str): Name of the game url (str): URL to check for updates Returns: @@ -254,7 +254,7 @@ def check_for_updates(current_version, game_name, url): response = requests.get(url, timeout=5) if response.status_code == 200: data = response.json() - if 'version' in data and data['version'] > current_version: + if 'version' in data and data['version'] > currentVersion: return { 'version': data['version'], 'url': data.get('url', ''), @@ -264,29 +264,29 @@ def check_for_updates(current_version, game_name, url): print(f"Error checking for updates: {e}") return None -def get_version_tuple(version_str): +def get_version_tuple(versionStr): """Convert version string to comparable tuple. Args: - version_str (str): Version string (e.g. "1.0.0") + versionStr (str): Version string (e.g. "1.0.0") Returns: tuple: Version as tuple of integers """ - return tuple(map(int, version_str.split('.'))) + return tuple(map(int, versionStr.split('.'))) -def check_compatibility(required_version, current_version): +def check_compatibility(requiredVersion, currentVersion): """Check if current version meets minimum required version. Args: - required_version (str): Minimum required version string - current_version (str): Current version string + requiredVersion (str): Minimum required version string + currentVersion (str): Current version string Returns: bool: True if compatible, False otherwise """ - req = get_version_tuple(required_version) - cur = get_version_tuple(current_version) + req = get_version_tuple(requiredVersion) + cur = get_version_tuple(currentVersion) return cur >= req def sanitize_filename(filename): @@ -350,25 +350,25 @@ def distance_2d(x1, y1, x2, y2): """ return math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2) -def generate_tone(frequency, duration=0.1, sample_rate=44100, volume=0.2): +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) - sample_rate (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) Returns: pygame.mixer.Sound: Sound object with the generated tone """ - t = np.linspace(0, duration, int(sample_rate * duration), False) + t = np.linspace(0, duration, int(sampleRate * duration), False) tone = np.sin(2 * np.pi * frequency * t) - stereo_tone = np.vstack((tone, tone)).T # Create a 2D array for stereo - stereo_tone = (stereo_tone * 32767 * volume).astype(np.int16) # Apply volume - stereo_tone = np.ascontiguousarray(stereo_tone) # Ensure C-contiguous array - return pygame.sndarray.make_sound(stereo_tone) + stereoTone = np.vstack((tone, tone)).T # Create a 2D array for stereo + stereoTone = (stereoTone * 32767 * volume).astype(np.int16) # Apply volume + stereoTone = np.ascontiguousarray(stereoTone) # Ensure C-contiguous array + return pygame.sndarray.make_sound(stereoTone) def x_powerbar(): """Sound based horizontal power bar