Attempt to fix traceback on game exit with some older games.

This commit is contained in:
Storm Dragon 2025-03-14 23:10:14 -04:00
parent 2ad22ff1ae
commit be6dfdf53a
9 changed files with 483 additions and 457 deletions

View File

@ -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

View File

@ -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

View File

@ -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

44
menu.py
View File

@ -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
# 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()
# 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()

View File

@ -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()

View File

@ -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

362
sound.py
View File

@ -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

View File

@ -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.

114
utils.py
View File

@ -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