initial commit, definitely not ready for use quite yet.
This commit is contained in:
		
							
								
								
									
										97
									
								
								__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,97 @@ | |||||||
|  | """ | ||||||
|  | Core initialization module for PygStormGames framework. | ||||||
|  |  | ||||||
|  | Provides the main PygStormGames class that serves as the central hub for game functionality. | ||||||
|  | """ | ||||||
|  |  | ||||||
|  | from .config import Config | ||||||
|  | from .display import Display | ||||||
|  | from .menu import Menu | ||||||
|  | from .scoreboard import Scoreboard | ||||||
|  | from .sound import Sound | ||||||
|  | from .speech import Speech | ||||||
|  | import pyglet | ||||||
|  |  | ||||||
|  | class pygstormgames: | ||||||
|  |     """Main class that coordinates all game systems.""" | ||||||
|  |      | ||||||
|  |     def __init__(self, gameTitle): | ||||||
|  |         """Initialize the game framework. | ||||||
|  |          | ||||||
|  |         Args: | ||||||
|  |             gameTitle (str): Title of the game | ||||||
|  |         """ | ||||||
|  |         self.gameTitle = gameTitle | ||||||
|  |         self._paused = False | ||||||
|  |          | ||||||
|  |         # Initialize core systems | ||||||
|  |         self.config = Config(gameTitle) | ||||||
|  |         self.display = Display(gameTitle) | ||||||
|  |         self.speech = Speech() | ||||||
|  |         self.sound = Sound(self) | ||||||
|  |         self.scoreboard = Scoreboard(self) | ||||||
|  |         self.menu = Menu(self) | ||||||
|  |          | ||||||
|  |         # Play intro sound if available | ||||||
|  |         try: | ||||||
|  |             player = self.sound.play_sound('game-intro') | ||||||
|  |             if player: | ||||||
|  |                 # Wait for completion or skip input | ||||||
|  |                 @self.display.window.event | ||||||
|  |                 def on_key_press(symbol, modifiers): | ||||||
|  |                     if symbol in (pyglet.window.key.ESCAPE,  | ||||||
|  |                                 pyglet.window.key.RETURN,  | ||||||
|  |                                 pyglet.window.key.SPACE): | ||||||
|  |                         player.pause() | ||||||
|  |                         # Remove the temporary event handler | ||||||
|  |                         self.display.window.remove_handler('on_key_press', on_key_press) | ||||||
|  |                         return True | ||||||
|  |                      | ||||||
|  |                 # Wait for sound to finish or user to skip | ||||||
|  |                 while player.playing: | ||||||
|  |                     self.display.window.dispatch_events() | ||||||
|  |                  | ||||||
|  |                 # Remove the temporary event handler if not already removed | ||||||
|  |                 self.display.window.remove_handler('on_key_press', on_key_press) | ||||||
|  |         except: | ||||||
|  |             pass | ||||||
|  |  | ||||||
|  |         # Set up window event handlers | ||||||
|  |         self.display.window.push_handlers(self.on_key_press) | ||||||
|  |          | ||||||
|  |     def on_key_press(self, symbol, modifiers): | ||||||
|  |         """Handle global keyboard events. | ||||||
|  |          | ||||||
|  |         Args: | ||||||
|  |             symbol: Pyglet key symbol | ||||||
|  |             modifiers: Key modifiers | ||||||
|  |         """ | ||||||
|  |         if self._paused: | ||||||
|  |             if symbol == pyglet.window.key.BACKSPACE: | ||||||
|  |                 self._paused = False | ||||||
|  |                 self.sound.resume() | ||||||
|  |                 self.speech.speak("Game resumed") | ||||||
|  |         else: | ||||||
|  |             # Global exit handler | ||||||
|  |             if symbol == pyglet.window.key.ESCAPE: | ||||||
|  |                 self.exit_game() | ||||||
|  |                  | ||||||
|  |             # Global pause handler  | ||||||
|  |             if symbol == pyglet.window.key.BACKSPACE: | ||||||
|  |                 self.pause_game() | ||||||
|  |              | ||||||
|  |     def run(self): | ||||||
|  |         """Start the game loop.""" | ||||||
|  |         pyglet.app.run() | ||||||
|  |          | ||||||
|  |     def pause_game(self): | ||||||
|  |         """Pause all game systems and wait for resume.""" | ||||||
|  |         self._paused = True | ||||||
|  |         self.sound.pause() | ||||||
|  |         self.speech.speak("Game paused, press backspace to resume.") | ||||||
|  |          | ||||||
|  |     def exit_game(self): | ||||||
|  |         """Clean up and exit the game.""" | ||||||
|  |         self.sound.cleanup() | ||||||
|  |         self.speech.cleanup() | ||||||
|  |         pyglet.app.exit() | ||||||
							
								
								
									
										
											BIN
										
									
								
								__pycache__/__init__.cpython-313.pyc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								__pycache__/__init__.cpython-313.pyc
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								__pycache__/config.cpython-313.pyc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								__pycache__/config.cpython-313.pyc
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								__pycache__/display.cpython-313.pyc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								__pycache__/display.cpython-313.pyc
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								__pycache__/menu.cpython-313.pyc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								__pycache__/menu.cpython-313.pyc
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								__pycache__/scoreboard.cpython-313.pyc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								__pycache__/scoreboard.cpython-313.pyc
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								__pycache__/sound.cpython-313.pyc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								__pycache__/sound.cpython-313.pyc
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								__pycache__/speech.cpython-313.pyc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								__pycache__/speech.cpython-313.pyc
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										158
									
								
								config.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										158
									
								
								config.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,158 @@ | |||||||
|  | """Configuration management module for PygStormGames. | ||||||
|  |  | ||||||
|  | Handles loading and saving of both local and global game configurations. | ||||||
|  | """ | ||||||
|  |  | ||||||
|  | import os | ||||||
|  | import configparser | ||||||
|  | from xdg import BaseDirectory | ||||||
|  |  | ||||||
|  | class Config: | ||||||
|  |     """Handles configuration file management.""" | ||||||
|  |      | ||||||
|  |     def __init__(self, gameTitle): | ||||||
|  |         """Initialize configuration system. | ||||||
|  |          | ||||||
|  |         Args: | ||||||
|  |             gameTitle (str): Title of the game | ||||||
|  |         """ | ||||||
|  |         # Set up config parsers | ||||||
|  |         self.localConfig = configparser.ConfigParser() | ||||||
|  |         self.globalConfig = configparser.ConfigParser() | ||||||
|  |          | ||||||
|  |         # Set up paths | ||||||
|  |         self.globalPath = os.path.join(BaseDirectory.xdg_config_home, "storm-games") | ||||||
|  |         gameDir = str.lower(str.replace(gameTitle, " ", "-")) | ||||||
|  |         self.gamePath = os.path.join(self.globalPath, gameDir) | ||||||
|  |          | ||||||
|  |         # Create directories if needed | ||||||
|  |         if not os.path.exists(self.gamePath): | ||||||
|  |             os.makedirs(self.gamePath) | ||||||
|  |              | ||||||
|  |         # Full paths to config files | ||||||
|  |         self.localConfigPath = os.path.join(self.gamePath, "config.ini") | ||||||
|  |         self.globalConfigPath = os.path.join(self.globalPath, "config.ini") | ||||||
|  |          | ||||||
|  |         # Load initial configurations | ||||||
|  |         self.read_config() | ||||||
|  |         self.read_config(globalConfig=True) | ||||||
|  |          | ||||||
|  |     def read_config(self, globalConfig=False): | ||||||
|  |         """Read configuration from file. | ||||||
|  |          | ||||||
|  |         Args: | ||||||
|  |             globalConfig (bool): If True, read global config, otherwise local | ||||||
|  |         """ | ||||||
|  |         config = self.globalConfig if globalConfig else self.localConfig | ||||||
|  |         path = self.globalConfigPath if globalConfig else self.localConfigPath | ||||||
|  |          | ||||||
|  |         try: | ||||||
|  |             with open(path, 'r') as configfile: | ||||||
|  |                 config.read_file(configfile) | ||||||
|  |         except FileNotFoundError: | ||||||
|  |             # It's okay if the file doesn't exist yet | ||||||
|  |             pass | ||||||
|  |         except Exception as e: | ||||||
|  |             print(f"Error reading {'global' if globalConfig else 'local'} config: {e}") | ||||||
|  |              | ||||||
|  |     def write_config(self, globalConfig=False): | ||||||
|  |         """Write configuration to file. | ||||||
|  |          | ||||||
|  |         Args: | ||||||
|  |             globalConfig (bool): If True, write to global config, otherwise local | ||||||
|  |         """ | ||||||
|  |         config = self.globalConfig if globalConfig else self.localConfig | ||||||
|  |         path = self.globalConfigPath if globalConfig else self.localConfigPath | ||||||
|  |          | ||||||
|  |         try: | ||||||
|  |             with open(path, 'w') as configfile: | ||||||
|  |                 config.write(configfile) | ||||||
|  |         except Exception as e: | ||||||
|  |             print(f"Error writing {'global' if globalConfig else 'local'} config: {e}") | ||||||
|  |              | ||||||
|  |     def get_value(self, section, key, default=None, globalConfig=False): | ||||||
|  |         """Get value from configuration. | ||||||
|  |          | ||||||
|  |         Args: | ||||||
|  |             section (str): Configuration section | ||||||
|  |             key (str): Configuration key | ||||||
|  |             default: Default value if not found | ||||||
|  |             globalConfig (bool): If True, read from global config | ||||||
|  |              | ||||||
|  |         Returns: | ||||||
|  |             Value from config or default if not found | ||||||
|  |         """ | ||||||
|  |         config = self.globalConfig if globalConfig else self.localConfig | ||||||
|  |         try: | ||||||
|  |             return config.get(section, key) | ||||||
|  |         except: | ||||||
|  |             return default | ||||||
|  |              | ||||||
|  |     def set_value(self, section, key, value, globalConfig=False): | ||||||
|  |         """Set value in configuration. | ||||||
|  |          | ||||||
|  |         Args: | ||||||
|  |             section (str): Configuration section | ||||||
|  |             key (str): Configuration key | ||||||
|  |             value: Value to set | ||||||
|  |             globalConfig (bool): If True, write to global config | ||||||
|  |         """ | ||||||
|  |         config = self.globalConfig if globalConfig else self.localConfig | ||||||
|  |          | ||||||
|  |         # Create section if it doesn't exist | ||||||
|  |         if not config.has_section(section): | ||||||
|  |             config.add_section(section) | ||||||
|  |              | ||||||
|  |         config.set(section, key, str(value)) | ||||||
|  |         self.write_config(globalConfig) | ||||||
|  |          | ||||||
|  |     def get_int(self, section, key, default=0, globalConfig=False): | ||||||
|  |         """Get integer value from configuration. | ||||||
|  |          | ||||||
|  |         Args: | ||||||
|  |             section (str): Configuration section | ||||||
|  |             key (str): Configuration key | ||||||
|  |             default (int): Default value if not found | ||||||
|  |             globalConfig (bool): If True, read from global config | ||||||
|  |              | ||||||
|  |         Returns: | ||||||
|  |             int: Value from config or default if not found | ||||||
|  |         """ | ||||||
|  |         try: | ||||||
|  |             return int(self.get_value(section, key, default, globalConfig)) | ||||||
|  |         except: | ||||||
|  |             return default | ||||||
|  |              | ||||||
|  |     def get_float(self, section, key, default=0.0, globalConfig=False): | ||||||
|  |         """Get float value from configuration. | ||||||
|  |          | ||||||
|  |         Args: | ||||||
|  |             section (str): Configuration section | ||||||
|  |             key (str): Configuration key | ||||||
|  |             default (float): Default value if not found | ||||||
|  |             globalConfig (bool): If True, read from global config | ||||||
|  |              | ||||||
|  |         Returns: | ||||||
|  |             float: Value from config or default if not found | ||||||
|  |         """ | ||||||
|  |         try: | ||||||
|  |             return float(self.get_value(section, key, default, globalConfig)) | ||||||
|  |         except: | ||||||
|  |             return default | ||||||
|  |              | ||||||
|  |     def get_bool(self, section, key, default=False, globalConfig=False): | ||||||
|  |         """Get boolean value from configuration. | ||||||
|  |          | ||||||
|  |         Args: | ||||||
|  |             section (str): Configuration section | ||||||
|  |             key (str): Configuration key | ||||||
|  |             default (bool): Default value if not found | ||||||
|  |             globalConfig (bool): If True, read from global config | ||||||
|  |              | ||||||
|  |         Returns: | ||||||
|  |             bool: Value from config or default if not found | ||||||
|  |         """ | ||||||
|  |         try: | ||||||
|  |             return self.get_value(section, key, default, globalConfig).lower() in ['true', '1', 'yes', 'on'] | ||||||
|  |         except: | ||||||
|  |             return default | ||||||
							
								
								
									
										140
									
								
								display.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										140
									
								
								display.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,140 @@ | |||||||
|  | """Display management module for PygStormGames. | ||||||
|  |  | ||||||
|  | Handles text display, navigation, and information presentation including: | ||||||
|  | - Text display with navigation | ||||||
|  | - Instructions display | ||||||
|  | - Credits display | ||||||
|  | - Donation link handling | ||||||
|  | """ | ||||||
|  |  | ||||||
|  | import os | ||||||
|  | import webbrowser | ||||||
|  | import pyglet | ||||||
|  | from pyglet.window import key | ||||||
|  | import pyperclip | ||||||
|  | import wx | ||||||
|  |  | ||||||
|  | class Display: | ||||||
|  |     """Handles display and text navigation systems.""" | ||||||
|  |      | ||||||
|  |     def __init__(self, gameTitle): | ||||||
|  |         """Initialize display system. | ||||||
|  |          | ||||||
|  |         Args: | ||||||
|  |             gameTitle (str): Title of the game | ||||||
|  |         """ | ||||||
|  |         self.window = pyglet.window.Window(800, 600, caption=gameTitle) | ||||||
|  |         self.currentText = [] | ||||||
|  |         self.currentIndex = 0 | ||||||
|  |         self.gameTitle = gameTitle | ||||||
|  |          | ||||||
|  |     def display_text(self, text, speech): | ||||||
|  |         """Display and navigate text with speech output. | ||||||
|  |          | ||||||
|  |         Args: | ||||||
|  |             text (list): List of text lines to display | ||||||
|  |             speech (Speech): Speech system for audio output | ||||||
|  |         """ | ||||||
|  |         # Store original text with blank lines for copying | ||||||
|  |         self.originalText = text.copy() | ||||||
|  |          | ||||||
|  |         # Create navigation text by filtering out blank lines | ||||||
|  |         self.navText = [line for line in text if line.strip()] | ||||||
|  |          | ||||||
|  |         # Add instructions at start | ||||||
|  |         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.") | ||||||
|  |         self.navText.insert(0, instructions) | ||||||
|  |          | ||||||
|  |         # Add end marker | ||||||
|  |         self.navText.append("End of text.") | ||||||
|  |          | ||||||
|  |         self.currentIndex = 0 | ||||||
|  |         speech.speak(self.navText[self.currentIndex]) | ||||||
|  |          | ||||||
|  |         @self.window.event | ||||||
|  |         def on_key_press(symbol, modifiers): | ||||||
|  |             if symbol in (key.ESCAPE, key.RETURN): | ||||||
|  |                 self.window.remove_handler('on_key_press', on_key_press) | ||||||
|  |                 return | ||||||
|  |                  | ||||||
|  |             if symbol in (key.DOWN, key.S) and self.currentIndex < len(self.navText) - 1: | ||||||
|  |                 self.currentIndex += 1 | ||||||
|  |                 speech.speak(self.navText[self.currentIndex]) | ||||||
|  |                  | ||||||
|  |             if symbol in (key.UP, key.W) and self.currentIndex > 0: | ||||||
|  |                 self.currentIndex -= 1 | ||||||
|  |                 speech.speak(self.navText[self.currentIndex]) | ||||||
|  |                  | ||||||
|  |             if symbol == key.SPACE: | ||||||
|  |                 speech.speak('\n'.join(self.originalText[1:-1])) | ||||||
|  |                  | ||||||
|  |             if symbol == key.C: | ||||||
|  |                 try: | ||||||
|  |                     pyperclip.copy(self.navText[self.currentIndex]) | ||||||
|  |                     speech.speak("Copied " + self.navText[self.currentIndex] + " to the clipboard.") | ||||||
|  |                 except: | ||||||
|  |                     speech.speak("Failed to copy the text to the clipboard.") | ||||||
|  |                      | ||||||
|  |             if symbol == key.T: | ||||||
|  |                 try: | ||||||
|  |                     pyperclip.copy(''.join(self.originalText[2:-1])) | ||||||
|  |                     speech.speak("Copied entire message to the clipboard.") | ||||||
|  |                 except: | ||||||
|  |                     speech.speak("Failed to copy the text to the clipboard.") | ||||||
|  |  | ||||||
|  |     def instructions(self, speech): | ||||||
|  |         """Display game instructions from file. | ||||||
|  |          | ||||||
|  |         Args: | ||||||
|  |             speech (Speech): Speech system for audio output | ||||||
|  |         """ | ||||||
|  |         try: | ||||||
|  |             with open('files/instructions.txt', 'r') as f: | ||||||
|  |                 info = f.readlines() | ||||||
|  |         except: | ||||||
|  |             info = ["Instructions file is missing."] | ||||||
|  |              | ||||||
|  |         self.display_text(info, speech) | ||||||
|  |  | ||||||
|  |     def credits(self, speech): | ||||||
|  |         """Display game credits from file. | ||||||
|  |          | ||||||
|  |         Args: | ||||||
|  |             speech (Speech): Speech system for audio output | ||||||
|  |         """ | ||||||
|  |         try: | ||||||
|  |             with open('files/credits.txt', 'r') as f: | ||||||
|  |                 info = f.readlines() | ||||||
|  |             # Add the header | ||||||
|  |             info.insert(0, f"{self.gameTitle}: brought to you by Storm Dragon") | ||||||
|  |         except: | ||||||
|  |             info = ["Credits file is missing."] | ||||||
|  |              | ||||||
|  |         self.display_text(info, speech) | ||||||
|  |  | ||||||
|  |     def get_input(self, prompt="Enter text:", default_text=""): | ||||||
|  |         """Display a dialog box for text input. | ||||||
|  |      | ||||||
|  |         Args: | ||||||
|  |             prompt (str): Prompt text to display | ||||||
|  |             default_text (str): Initial text in input box | ||||||
|  |          | ||||||
|  |         Returns: | ||||||
|  |             str: User input text, or None if cancelled | ||||||
|  |         """ | ||||||
|  |         app = wx.App(False) | ||||||
|  |         dialog = wx.TextEntryDialog(None, prompt, "Input", default_text) | ||||||
|  |         dialog.SetValue(default_text) | ||||||
|  |         if dialog.ShowModal() == wx.ID_OK: | ||||||
|  |             userInput = dialog.GetValue() | ||||||
|  |         else: | ||||||
|  |             userInput = None | ||||||
|  |         dialog.Destroy() | ||||||
|  |         return userInput | ||||||
|  |  | ||||||
|  |     def donate(self, speech): | ||||||
|  |         """Open the donation webpage.""" | ||||||
|  |         speech.speak("Opening donation page.") | ||||||
|  |         webbrowser.open('https://ko-fi.com/stormux') | ||||||
							
								
								
									
										176
									
								
								menu.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										176
									
								
								menu.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,176 @@ | |||||||
|  | """Menu system module for PygStormGames. | ||||||
|  |  | ||||||
|  | Handles main menu and submenu functionality for games. | ||||||
|  | """ | ||||||
|  |  | ||||||
|  | import os | ||||||
|  | import pyglet | ||||||
|  | from os.path import isfile, join | ||||||
|  | from pyglet.window import key | ||||||
|  |  | ||||||
|  | class Menu: | ||||||
|  |     """Handles menu systems.""" | ||||||
|  |      | ||||||
|  |     def __init__(self, game): | ||||||
|  |         """Initialize menu system. | ||||||
|  |          | ||||||
|  |         Args: | ||||||
|  |             game (PygStormGames): Reference to main game object | ||||||
|  |         """ | ||||||
|  |         self.game = game | ||||||
|  |         self.currentIndex = 0 | ||||||
|  |          | ||||||
|  |     def show_menu(self, options, title=None, with_music=False): | ||||||
|  |         """Display a menu and return selected option.""" | ||||||
|  |         if with_music: | ||||||
|  |             try: | ||||||
|  |                 if self.game.sound.currentBgm: | ||||||
|  |                     self.game.sound.currentBgm.pause() | ||||||
|  |                 self.game.sound.play_bgm("sounds/music_menu.ogg") | ||||||
|  |             except: | ||||||
|  |                 pass | ||||||
|  |              | ||||||
|  |         self.currentIndex = 0 | ||||||
|  |         lastSpoken = -1 | ||||||
|  |         selection = None  # Add this to store the selection | ||||||
|  |      | ||||||
|  |         if title: | ||||||
|  |             self.game.speech.speak(title) | ||||||
|  |          | ||||||
|  |         def key_handler(symbol, modifiers):  # Define handler outside event | ||||||
|  |             nonlocal selection, lastSpoken | ||||||
|  |             # Handle Alt+volume controls | ||||||
|  |             if modifiers & key.MOD_ALT: | ||||||
|  |                 if symbol == key.PAGEUP: | ||||||
|  |                     self.game.sound.adjust_master_volume(0.1) | ||||||
|  |                 elif symbol == key.PAGEDOWN: | ||||||
|  |                     self.game.sound.adjust_master_volume(-0.1) | ||||||
|  |                 elif symbol == key.HOME: | ||||||
|  |                     self.game.sound.adjust_bgm_volume(0.1) | ||||||
|  |                 elif symbol == key.END: | ||||||
|  |                     self.game.sound.adjust_bgm_volume(-0.1) | ||||||
|  |                 elif symbol == key.INSERT: | ||||||
|  |                     self.game.sound.adjust_sfx_volume(0.1) | ||||||
|  |                 elif symbol == key.DELETE: | ||||||
|  |                     self.game.sound.adjust_sfx_volume(-0.1) | ||||||
|  |                 return | ||||||
|  |              | ||||||
|  |             if symbol == key.ESCAPE: | ||||||
|  |                 selection = "exit" | ||||||
|  |                 return pyglet.event.EVENT_HANDLED | ||||||
|  |              | ||||||
|  |             if symbol == key.HOME and self.currentIndex != 0: | ||||||
|  |                 self.currentIndex = 0 | ||||||
|  |                 self.game.sound.play_sound('menu-move') | ||||||
|  |                 lastSpoken = -1  # Force speech | ||||||
|  |              | ||||||
|  |             elif symbol == key.END and self.currentIndex != len(options) - 1: | ||||||
|  |                 self.currentIndex = len(options) - 1 | ||||||
|  |                 self.game.sound.play_sound('menu-move') | ||||||
|  |                 lastSpoken = -1  # Force speech | ||||||
|  |              | ||||||
|  |             elif symbol in (key.DOWN, key.S) and self.currentIndex < len(options) - 1: | ||||||
|  |                 self.currentIndex += 1 | ||||||
|  |                 self.game.sound.play_sound('menu-move') | ||||||
|  |                 lastSpoken = -1  # Force speech | ||||||
|  |              | ||||||
|  |             elif symbol in (key.UP, key.W) and self.currentIndex > 0: | ||||||
|  |                 self.currentIndex -= 1 | ||||||
|  |                 self.game.sound.play_sound('menu-move') | ||||||
|  |                 lastSpoken = -1  # Force speech | ||||||
|  |              | ||||||
|  |             elif symbol == key.RETURN: | ||||||
|  |                 self.game.sound.play_sound('menu-select') | ||||||
|  |                 selection = options[self.currentIndex] | ||||||
|  |                 return pyglet.event.EVENT_HANDLED | ||||||
|  |              | ||||||
|  |             return pyglet.event.EVENT_HANDLED | ||||||
|  |  | ||||||
|  |         # Register the handler | ||||||
|  |         self.game.display.window.push_handlers(on_key_press=key_handler) | ||||||
|  |      | ||||||
|  |         # Main menu loop | ||||||
|  |         while selection is None: | ||||||
|  |             if self.currentIndex != lastSpoken: | ||||||
|  |                 self.game.speech.speak(options[self.currentIndex]) | ||||||
|  |                 lastSpoken = self.currentIndex | ||||||
|  |             self.game.display.window.dispatch_events() | ||||||
|  |              | ||||||
|  |         # Clean up | ||||||
|  |         self.game.display.window.remove_handlers() | ||||||
|  |         return selection | ||||||
|  |  | ||||||
|  |     def game_menu(self): | ||||||
|  |         """Show main game menu.""" | ||||||
|  |         options = [ | ||||||
|  |             "play", | ||||||
|  |             "instructions", | ||||||
|  |             "learn_sounds", | ||||||
|  |             "credits", | ||||||
|  |             "donate", | ||||||
|  |             "exit" | ||||||
|  |         ] | ||||||
|  |      | ||||||
|  |         return self.show_menu(options, with_music=True) | ||||||
|  |  | ||||||
|  |     def learn_sounds(self): | ||||||
|  |         """Interactive menu for learning game sounds. | ||||||
|  |      | ||||||
|  |         Allows users to: | ||||||
|  |         - Navigate through available sounds | ||||||
|  |         - Play selected sounds | ||||||
|  |         - Return to menu with escape key | ||||||
|  |      | ||||||
|  |         Returns: | ||||||
|  |             str: "menu" if user exits with escape | ||||||
|  |         """ | ||||||
|  |         try: | ||||||
|  |             self.game.sound.currentBgm.pause() | ||||||
|  |         except: | ||||||
|  |             pass | ||||||
|  |  | ||||||
|  |         self.currentIndex = 0 | ||||||
|  |      | ||||||
|  |         # Get list of available sounds, excluding special sounds | ||||||
|  |         soundFiles = [f for f in os.listdir("sounds/") | ||||||
|  |                     if isfile(join("sounds/", f)) | ||||||
|  |                     and (f.split('.')[1].lower() in ["ogg", "wav"]) | ||||||
|  |                     and (f.split('.')[0].lower() not in ["game-intro", "music_menu"]) | ||||||
|  |                     and (not f.lower().startswith("_"))] | ||||||
|  |      | ||||||
|  |         # Track last spoken index to avoid repetition | ||||||
|  |         lastSpoken = -1 | ||||||
|  |      | ||||||
|  |         while True: | ||||||
|  |             if self.currentIndex != lastSpoken: | ||||||
|  |                 self.game.speech.speak(soundFiles[self.currentIndex][:-4]) | ||||||
|  |                 lastSpoken = self.currentIndex | ||||||
|  |              | ||||||
|  |             event = self.game.display.window.dispatch_events() | ||||||
|  |          | ||||||
|  |         @self.game.display.window.event | ||||||
|  |         def on_key_press(symbol, modifiers): | ||||||
|  |             if symbol == key.ESCAPE: | ||||||
|  |                 try: | ||||||
|  |                     self.game.sound.currentBgm.unpause() | ||||||
|  |                 except: | ||||||
|  |                     pass | ||||||
|  |                 self.game.display.window.remove_handler('on_key_press', on_key_press) | ||||||
|  |                 return "menu" | ||||||
|  |                  | ||||||
|  |             if symbol in [key.DOWN, key.S] and self.currentIndex < len(soundFiles) - 1: | ||||||
|  |                 self.game.sound.stop_all_sounds() | ||||||
|  |                 self.currentIndex += 1 | ||||||
|  |                  | ||||||
|  |             if symbol in [key.UP, key.W] and self.currentIndex > 0: | ||||||
|  |                 self.game.sound.stop_all_sounds() | ||||||
|  |                 self.currentIndex -= 1 | ||||||
|  |                  | ||||||
|  |             if symbol == key.RETURN: | ||||||
|  |                 try: | ||||||
|  |                     soundName = soundFiles[self.currentIndex][:-4] | ||||||
|  |                     self.game.sound.stop_all_sounds() | ||||||
|  |                     self.game.sound.play_sound(soundName) | ||||||
|  |                 except: | ||||||
|  |                     lastSpoken = -1 | ||||||
|  |                     self.game.speech.speak("Could not play sound.") | ||||||
							
								
								
									
										126
									
								
								scoreboard.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										126
									
								
								scoreboard.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,126 @@ | |||||||
|  | """Scoreboard management module for PygStormGames. | ||||||
|  |  | ||||||
|  | Handles high score tracking with player names and score management. | ||||||
|  | """ | ||||||
|  |  | ||||||
|  | import time | ||||||
|  |  | ||||||
|  | class Scoreboard: | ||||||
|  |     """Handles score tracking and high score management.""" | ||||||
|  |      | ||||||
|  |     def __init__(self, game): | ||||||
|  |         """Initialize scoreboard system. | ||||||
|  |          | ||||||
|  |         Args: | ||||||
|  |             game (PygStormGames): Reference to main game object | ||||||
|  |         """ | ||||||
|  |         self.game = game | ||||||
|  |         self.currentScore = 0 | ||||||
|  |         self.highScores = [] | ||||||
|  |          | ||||||
|  |         # Initialize high scores section in config | ||||||
|  |         try: | ||||||
|  |             self.game.config.localConfig.add_section("scoreboard") | ||||||
|  |         except: | ||||||
|  |             pass | ||||||
|  |              | ||||||
|  |         # Load existing high scores | ||||||
|  |         self._loadHighScores() | ||||||
|  |          | ||||||
|  |     def _loadHighScores(self): | ||||||
|  |         """Load high scores from config file.""" | ||||||
|  |         self.highScores = [] | ||||||
|  |          | ||||||
|  |         for i in range(1, 11): | ||||||
|  |             try: | ||||||
|  |                 score = self.game.config.get_int("scoreboard", f"score_{i}") | ||||||
|  |                 name = self.game.config.get_value("scoreboard", f"name_{i}", "Player") | ||||||
|  |                 self.highScores.append({ | ||||||
|  |                     'name': name, | ||||||
|  |                     'score': score | ||||||
|  |                 }) | ||||||
|  |             except: | ||||||
|  |                 self.highScores.append({ | ||||||
|  |                     'name': "Player", | ||||||
|  |                     'score': 0 | ||||||
|  |                 }) | ||||||
|  |                  | ||||||
|  |         # Sort high scores by score value in descending order | ||||||
|  |         self.highScores.sort(key=lambda x: x['score'], reverse=True) | ||||||
|  |          | ||||||
|  |     def get_score(self): | ||||||
|  |         """Get current score. | ||||||
|  |          | ||||||
|  |         Returns: | ||||||
|  |             int: Current score | ||||||
|  |         """ | ||||||
|  |         return self.currentScore | ||||||
|  |          | ||||||
|  |     def get_high_scores(self): | ||||||
|  |         """Get list of high scores. | ||||||
|  |          | ||||||
|  |         Returns: | ||||||
|  |             list: List of high score dictionaries | ||||||
|  |         """ | ||||||
|  |         return self.highScores | ||||||
|  |          | ||||||
|  |     def decrease_score(self, points=1): | ||||||
|  |         """Decrease the current score. | ||||||
|  |          | ||||||
|  |         Args: | ||||||
|  |             points (int): Points to decrease by | ||||||
|  |         """ | ||||||
|  |         self.currentScore -= int(points) | ||||||
|  |          | ||||||
|  |     def increase_score(self, points=1): | ||||||
|  |         """Increase the current score. | ||||||
|  |          | ||||||
|  |         Args: | ||||||
|  |             points (int): Points to increase by | ||||||
|  |         """ | ||||||
|  |         self.currentScore += int(points) | ||||||
|  |          | ||||||
|  |     def check_high_score(self): | ||||||
|  |         """Check if current score qualifies as a high score. | ||||||
|  |          | ||||||
|  |         Returns: | ||||||
|  |             int: Position (1-10) if high score, None if not | ||||||
|  |         """ | ||||||
|  |         for i, entry in enumerate(self.highScores): | ||||||
|  |             if self.currentScore > entry['score']: | ||||||
|  |                 return i + 1 | ||||||
|  |         return None | ||||||
|  |          | ||||||
|  |     def add_high_score(self): | ||||||
|  |         """Add current score to high scores if it qualifies. | ||||||
|  |          | ||||||
|  |         Returns: | ||||||
|  |             bool: True if score was added, False if not | ||||||
|  |         """ | ||||||
|  |         position = self.check_high_score() | ||||||
|  |         if position is None: | ||||||
|  |             return False | ||||||
|  |              | ||||||
|  |         # Get player name | ||||||
|  |         self.game.speech.speak("New high score! Enter your name:") | ||||||
|  |         name = self.game.display.get_input("New high score! Enter your name:", "Player") | ||||||
|  |         if name is None:  # User cancelled | ||||||
|  |             name = "Player" | ||||||
|  |              | ||||||
|  |         # Insert new score at correct position | ||||||
|  |         self.highScores.insert(position - 1, { | ||||||
|  |             'name': name, | ||||||
|  |             'score': self.currentScore | ||||||
|  |         }) | ||||||
|  |          | ||||||
|  |         # Keep only top 10 | ||||||
|  |         self.highScores = self.highScores[:10] | ||||||
|  |          | ||||||
|  |         # Save to config | ||||||
|  |         for i, entry in enumerate(self.highScores): | ||||||
|  |             self.game.config.set_value("scoreboard", f"score_{i+1}", str(entry['score'])) | ||||||
|  |             self.game.config.set_value("scoreboard", f"name_{i+1}", entry['name']) | ||||||
|  |              | ||||||
|  |         self.game.speech.speak(f"Congratulations {name}! You got position {position} on the scoreboard!") | ||||||
|  |         time.sleep(1) | ||||||
|  |         return True | ||||||
							
								
								
									
										366
									
								
								sound.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										366
									
								
								sound.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,366 @@ | |||||||
|  | """Sound management module for PygStormGames. | ||||||
|  |  | ||||||
|  | Handles all audio functionality including: | ||||||
|  | - Background music playback | ||||||
|  | - Sound effects with 2D/3D positional audio | ||||||
|  | - Volume control for master, BGM, and SFX | ||||||
|  | - Audio loading and resource management | ||||||
|  | """ | ||||||
|  |  | ||||||
|  | import os | ||||||
|  | import random | ||||||
|  | import re | ||||||
|  | import pyglet | ||||||
|  | from os.path import isfile, join | ||||||
|  | from pyglet.window import key | ||||||
|  |  | ||||||
|  | class Sound: | ||||||
|  |     """Handles audio playback and management.""" | ||||||
|  |      | ||||||
|  |     def __init__(self, game): | ||||||
|  |         """Initialize sound system. | ||||||
|  |          | ||||||
|  |         Args: | ||||||
|  |             game (PygStormGames): Reference to main game object | ||||||
|  |         """ | ||||||
|  |         # Game reference for component access | ||||||
|  |         self.game = game | ||||||
|  |          | ||||||
|  |         # Volume control (0.0 - 1.0) | ||||||
|  |         self.bgmVolume = 0.75  # Background music | ||||||
|  |         self.sfxVolume = 1.0   # Sound effects | ||||||
|  |         self.masterVolume = 1.0  # Master volume | ||||||
|  |          | ||||||
|  |         # Current background music | ||||||
|  |         self.currentBgm = None | ||||||
|  |          | ||||||
|  |         # Load sound resources | ||||||
|  |         self.sounds = self._load_sounds() | ||||||
|  |         self.activeSounds = []  # Track playing sounds | ||||||
|  |          | ||||||
|  |     def _load_sounds(self): | ||||||
|  |         """Load all sound files from sounds directory. | ||||||
|  |      | ||||||
|  |         Returns: | ||||||
|  |             dict: Dictionary of loaded sound objects | ||||||
|  |         """ | ||||||
|  |         sounds = {} | ||||||
|  |         try: | ||||||
|  |             soundFiles = [f for f in os.listdir("sounds/")  | ||||||
|  |                         if isfile(join("sounds/", f))  | ||||||
|  |                         and f.lower().endswith(('.wav', '.ogg'))] | ||||||
|  |             for f in soundFiles: | ||||||
|  |                 name = os.path.splitext(f)[0] | ||||||
|  |                 sounds[name] = pyglet.media.load(f"sounds/{f}", streaming=False) | ||||||
|  |         except FileNotFoundError: | ||||||
|  |             print("No sounds directory found") | ||||||
|  |             return {} | ||||||
|  |         except Exception as e: | ||||||
|  |             print(f"Error loading sounds: {e}") | ||||||
|  |          | ||||||
|  |         return sounds | ||||||
|  |          | ||||||
|  |     def play_bgm(self, music_file): | ||||||
|  |         """Play background music with proper volume. | ||||||
|  |          | ||||||
|  |         Args: | ||||||
|  |             music_file (str): Path to music file | ||||||
|  |         """ | ||||||
|  |         try: | ||||||
|  |             if self.currentBgm: | ||||||
|  |                 self.currentBgm.pause() | ||||||
|  |                  | ||||||
|  |             # Load and play new music | ||||||
|  |             music = pyglet.media.load(music_file, streaming=True) | ||||||
|  |             player = pyglet.media.Player() | ||||||
|  |             player.queue(music) | ||||||
|  |             player.loop = True | ||||||
|  |             player.volume = self.bgmVolume * self.masterVolume | ||||||
|  |             player.play() | ||||||
|  |              | ||||||
|  |             self.currentBgm = player | ||||||
|  |         except Exception as e: | ||||||
|  |             print(f"Error playing background music: {e}") | ||||||
|  |              | ||||||
|  |     def play_sound(self, soundName, volume=1.0): | ||||||
|  |         """Play a sound effect with volume settings. | ||||||
|  |          | ||||||
|  |         Args: | ||||||
|  |             soundName (str): Name of sound to play | ||||||
|  |             volume (float): Base volume for sound (0.0-1.0) | ||||||
|  |              | ||||||
|  |         Returns: | ||||||
|  |             pyglet.media.Player: Sound player object | ||||||
|  |         """ | ||||||
|  |         if soundName not in self.sounds: | ||||||
|  |             return None | ||||||
|  |              | ||||||
|  |         player = pyglet.media.Player() | ||||||
|  |         player.queue(self.sounds[soundName]) | ||||||
|  |         player.volume = volume * self.sfxVolume * self.masterVolume | ||||||
|  |         player.play() | ||||||
|  |          | ||||||
|  |         self.activeSounds.append(player) | ||||||
|  |         return player | ||||||
|  |          | ||||||
|  |     def play_random(self, base_name, pause=False, interrupt=False): | ||||||
|  |         """Play random variation of a sound. | ||||||
|  |          | ||||||
|  |         Args: | ||||||
|  |             base_name (str): Base name of sound | ||||||
|  |             pause (bool): Wait for sound to finish | ||||||
|  |             interrupt (bool): Stop other sounds | ||||||
|  |         """ | ||||||
|  |         matches = [name for name in self.sounds.keys()  | ||||||
|  |                   if re.match(f"^{base_name}.*", name)] | ||||||
|  |                    | ||||||
|  |         if not matches: | ||||||
|  |             return None | ||||||
|  |              | ||||||
|  |         if interrupt: | ||||||
|  |             self.stop_all_sounds() | ||||||
|  |              | ||||||
|  |         soundName = random.choice(matches) | ||||||
|  |         player = self.play_sound(soundName) | ||||||
|  |          | ||||||
|  |         if pause and player: | ||||||
|  |             player.on_player_eos = lambda: None  # Wait for completion | ||||||
|  |              | ||||||
|  |     def calculate_positional_audio(self, source_pos, listener_pos, mode='2d'): | ||||||
|  |         """Calculate position for 3D audio. | ||||||
|  |          | ||||||
|  |         Args: | ||||||
|  |             source_pos: Either float (2D x-position) or tuple (3D x,y,z position) | ||||||
|  |             listener_pos: Either float (2D x-position) or tuple (3D x,y,z position) | ||||||
|  |             mode: '2d' or '3d' to specify positioning mode | ||||||
|  |              | ||||||
|  |         Returns: | ||||||
|  |             tuple: (x, y, z) position for sound source, or None if out of range | ||||||
|  |         """ | ||||||
|  |         if mode == '2d': | ||||||
|  |             distance = abs(source_pos - listener_pos) | ||||||
|  |             max_distance = 12 | ||||||
|  |              | ||||||
|  |             if distance > max_distance: | ||||||
|  |                 return None | ||||||
|  |                  | ||||||
|  |             return (source_pos - listener_pos, 0, -1) | ||||||
|  |         else: | ||||||
|  |             x = source_pos[0] - listener_pos[0] | ||||||
|  |             y = source_pos[1] - listener_pos[1] | ||||||
|  |             z = source_pos[2] - listener_pos[2] | ||||||
|  |              | ||||||
|  |             distance = (x*x + y*y + z*z) ** 0.5 | ||||||
|  |             max_distance = 20  # Larger for 3D space | ||||||
|  |              | ||||||
|  |             if distance > max_distance: | ||||||
|  |                 return None | ||||||
|  |                  | ||||||
|  |             return (x, y, z) | ||||||
|  |              | ||||||
|  |     def play_positional(self, soundName, source_pos, listener_pos, mode='2d',  | ||||||
|  |                        direction=None, cone_angles=None): | ||||||
|  |         """Play sound with positional audio. | ||||||
|  |          | ||||||
|  |         Args: | ||||||
|  |             soundName (str): Name of sound to play | ||||||
|  |             source_pos: Position of sound source (float for 2D, tuple for 3D) | ||||||
|  |             listener_pos: Position of listener (float for 2D, tuple for 3D) | ||||||
|  |             mode: '2d' or '3d' to specify positioning mode | ||||||
|  |             direction: Optional tuple (x,y,z) for directional sound | ||||||
|  |             cone_angles: Optional tuple (inner, outer) angles for sound cone | ||||||
|  |              | ||||||
|  |         Returns: | ||||||
|  |             pyglet.media.Player: Sound player object | ||||||
|  |         """ | ||||||
|  |         if soundName not in self.sounds: | ||||||
|  |             return None | ||||||
|  |              | ||||||
|  |         position = self.calculate_positional_audio(source_pos, listener_pos, mode) | ||||||
|  |         if position is None:  # Too far to hear | ||||||
|  |             return None | ||||||
|  |              | ||||||
|  |         player = pyglet.media.Player() | ||||||
|  |         player.queue(self.sounds[soundName]) | ||||||
|  |         player.position = position | ||||||
|  |         player.volume = self.sfxVolume * self.masterVolume | ||||||
|  |          | ||||||
|  |         # Set up directional audio if specified | ||||||
|  |         if direction and mode == '3d': | ||||||
|  |             player.cone_orientation = direction | ||||||
|  |             if cone_angles: | ||||||
|  |                 player.cone_inner_angle, player.cone_outer_angle = cone_angles | ||||||
|  |                 player.cone_outer_gain = 0.5  # Reduced volume outside cone | ||||||
|  |                  | ||||||
|  |         player.play() | ||||||
|  |         self.activeSounds.append(player) | ||||||
|  |         return player | ||||||
|  |          | ||||||
|  |     def update_positional(self, player, source_pos, listener_pos, mode='2d', | ||||||
|  |                          direction=None): | ||||||
|  |         """Update position of a playing sound. | ||||||
|  |          | ||||||
|  |         Args: | ||||||
|  |             player: Sound player to update | ||||||
|  |             source_pos: New source position | ||||||
|  |             listener_pos: New listener position | ||||||
|  |             mode: '2d' or '3d' positioning mode | ||||||
|  |             direction: Optional new direction for directional sound | ||||||
|  |         """ | ||||||
|  |         if not player or not player.playing: | ||||||
|  |             return | ||||||
|  |              | ||||||
|  |         position = self.calculate_positional_audio(source_pos, listener_pos, mode) | ||||||
|  |         if position is None: | ||||||
|  |             player.pause() | ||||||
|  |             return | ||||||
|  |              | ||||||
|  |         player.position = position | ||||||
|  |         if direction and mode == '3d': | ||||||
|  |             player.cone_orientation = direction | ||||||
|  |              | ||||||
|  |     def cut_scene(self, soundName): | ||||||
|  |         """Play a sound as a cut scene, stopping other sounds and waiting for completion. | ||||||
|  |          | ||||||
|  |         Args: | ||||||
|  |             soundName (str): Name of sound to play | ||||||
|  |              | ||||||
|  |         The method will block until either: | ||||||
|  |         - The sound finishes playing | ||||||
|  |         - The user presses ESC/RETURN/SPACE (if window is provided) | ||||||
|  |         """ | ||||||
|  |         # Stop all current sounds | ||||||
|  |         self.stop_all_sounds() | ||||||
|  |         if self.currentBgm: | ||||||
|  |             self.currentBgm.pause() | ||||||
|  |              | ||||||
|  |         if soundName not in self.sounds: | ||||||
|  |             return | ||||||
|  |              | ||||||
|  |         # Create and configure the player | ||||||
|  |         player = pyglet.media.Player() | ||||||
|  |         player.queue(self.sounds[soundName]) | ||||||
|  |         player.volume = self.sfxVolume * self.masterVolume | ||||||
|  |          | ||||||
|  |         # Flag to track if we should continue waiting | ||||||
|  |         shouldContinue = True | ||||||
|  |          | ||||||
|  |         def on_player_eos(): | ||||||
|  |             nonlocal shouldContinue | ||||||
|  |             shouldContinue = False | ||||||
|  |              | ||||||
|  |         # Set up completion callback | ||||||
|  |         player.push_handlers(on_eos=on_player_eos) | ||||||
|  |          | ||||||
|  |         # Get window from game display | ||||||
|  |         window = self.game.display.window | ||||||
|  |          | ||||||
|  |         # If we have a window, set up key handler for skipping | ||||||
|  |         if window: | ||||||
|  |             skipKeys = [key.ESCAPE, key.RETURN, key.SPACE] | ||||||
|  |              | ||||||
|  |             @window.event | ||||||
|  |             def on_key_press(symbol, modifiers): | ||||||
|  |                 nonlocal shouldContinue | ||||||
|  |                 if symbol in skipKeys: | ||||||
|  |                     shouldContinue = False | ||||||
|  |                     return True | ||||||
|  |                      | ||||||
|  |         # Start playback | ||||||
|  |         player.play() | ||||||
|  |          | ||||||
|  |         # Wait for completion or skip | ||||||
|  |         while shouldContinue and player.playing: | ||||||
|  |             if window: | ||||||
|  |                 window.dispatch_events() | ||||||
|  |             pyglet.clock.tick() | ||||||
|  |              | ||||||
|  |         # Ensure cleanup | ||||||
|  |         player.pause() | ||||||
|  |         player.delete() | ||||||
|  |          | ||||||
|  |         # Resume background music if it was playing | ||||||
|  |         if self.currentBgm: | ||||||
|  |             self.currentBgm.play() | ||||||
|  |  | ||||||
|  |     def adjust_master_volume(self, change): | ||||||
|  |         """Adjust master volume. | ||||||
|  |          | ||||||
|  |         Args: | ||||||
|  |             change (float): Volume change (-1.0 to 1.0) | ||||||
|  |         """ | ||||||
|  |         if not -1.0 <= change <= 1.0: | ||||||
|  |             return | ||||||
|  |  | ||||||
|  |         self.masterVolume = max(0.0, min(1.0, self.masterVolume + change)) | ||||||
|  |          | ||||||
|  |         # Update BGM | ||||||
|  |         if self.currentBgm: | ||||||
|  |             self.currentBgm.volume = self.bgmVolume * self.masterVolume | ||||||
|  |              | ||||||
|  |         # Update active sounds | ||||||
|  |         for sound in self.activeSounds: | ||||||
|  |             if sound.playing: | ||||||
|  |                 sound.volume *= self.masterVolume | ||||||
|  |                  | ||||||
|  |     def adjust_bgm_volume(self, change): | ||||||
|  |         """Adjust background music volume. | ||||||
|  |          | ||||||
|  |         Args: | ||||||
|  |             change (float): Volume change (-1.0 to 1.0) | ||||||
|  |         """ | ||||||
|  |         if not -1.0 <= change <= 1.0: | ||||||
|  |             return | ||||||
|  |  | ||||||
|  |         self.bgmVolume = max(0.0, min(1.0, self.bgmVolume + change)) | ||||||
|  |         if self.currentBgm: | ||||||
|  |             self.currentBgm.volume = self.bgmVolume * self.masterVolume | ||||||
|  |              | ||||||
|  |     def adjust_sfx_volume(self, change): | ||||||
|  |         """Adjust sound effects volume. | ||||||
|  |          | ||||||
|  |         Args: | ||||||
|  |             change (float): Volume change (-1.0 to 1.0) | ||||||
|  |         """ | ||||||
|  |         if not -1.0 <= change <= 1.0: | ||||||
|  |             return | ||||||
|  |  | ||||||
|  |         self.sfxVolume = max(0.0, min(1.0, self.sfxVolume + change)) | ||||||
|  |         for sound in self.activeSounds: | ||||||
|  |             if sound.playing: | ||||||
|  |                 sound.volume *= self.sfxVolume | ||||||
|  |                  | ||||||
|  |     def get_volumes(self): | ||||||
|  |         """Get current volume levels. | ||||||
|  |  | ||||||
|  |         Returns: | ||||||
|  |             tuple: (masterVolume, bgmVolume, sfxVolume) | ||||||
|  |         """ | ||||||
|  |         return (self.masterVolume, self.bgmVolume, self.sfxVolume) | ||||||
|  |  | ||||||
|  |     def pause(self): | ||||||
|  |         """Pause all audio.""" | ||||||
|  |         if self.currentBgm: | ||||||
|  |             self.currentBgm.pause() | ||||||
|  |         for sound in self.activeSounds: | ||||||
|  |             if sound.playing: | ||||||
|  |                 sound.pause() | ||||||
|  |                  | ||||||
|  |     def resume(self): | ||||||
|  |         """Resume all audio.""" | ||||||
|  |         if self.currentBgm: | ||||||
|  |             self.currentBgm.play() | ||||||
|  |         for sound in self.activeSounds: | ||||||
|  |             sound.play() | ||||||
|  |              | ||||||
|  |     def stop_all_sounds(self): | ||||||
|  |         """Stop all playing sounds.""" | ||||||
|  |         for sound in self.activeSounds: | ||||||
|  |             sound.pause() | ||||||
|  |         self.activeSounds.clear() | ||||||
|  |          | ||||||
|  |     def cleanup(self): | ||||||
|  |         """Clean up sound resources.""" | ||||||
|  |         if self.currentBgm: | ||||||
|  |             self.currentBgm.pause() | ||||||
|  |         self.stop_all_sounds() | ||||||
							
								
								
									
										84
									
								
								speech.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								speech.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,84 @@ | |||||||
|  | """Speech and text display module for PygStormGames. | ||||||
|  |  | ||||||
|  | Provides text-to-speech functionality with screen text display support. | ||||||
|  | Uses either speechd or accessible_output2 as the speech backend. | ||||||
|  | """ | ||||||
|  |  | ||||||
|  | import time | ||||||
|  | import pyglet | ||||||
|  | from pyglet.window import key | ||||||
|  | import textwrap | ||||||
|  |  | ||||||
|  | class Speech: | ||||||
|  |     """Handles speech output and text display.""" | ||||||
|  |      | ||||||
|  |     def __init__(self): | ||||||
|  |         """Initialize speech system with fallback providers.""" | ||||||
|  |         self._lastSpoken = {"text": None, "time": 0} | ||||||
|  |         self._speechDelay = 250  # ms delay between identical messages | ||||||
|  |          | ||||||
|  |         # Try to initialize speech providers in order of preference | ||||||
|  |         try: | ||||||
|  |             import speechd | ||||||
|  |             self._speech = speechd.Client() | ||||||
|  |             self._provider = "speechd" | ||||||
|  |         except ImportError: | ||||||
|  |             try: | ||||||
|  |                 import accessible_output2.outputs.auto | ||||||
|  |                 self._speech = accessible_output2.outputs.auto.Auto() | ||||||
|  |                 self._provider = "accessible_output2" | ||||||
|  |             except ImportError: | ||||||
|  |                 raise RuntimeError("No speech providers found. Install either speechd or accessible_output2.") | ||||||
|  |                  | ||||||
|  |         # Display settings | ||||||
|  |         self._font = pyglet.text.Label( | ||||||
|  |             '', | ||||||
|  |             font_name='Arial', | ||||||
|  |             font_size=36, | ||||||
|  |             x=400, y=300,  # Will be centered later | ||||||
|  |             anchor_x='center', anchor_y='center', | ||||||
|  |             multiline=True, | ||||||
|  |             width=760  # Allow 20px margin on each side | ||||||
|  |         ) | ||||||
|  |          | ||||||
|  |     def speak(self, text, interrupt=True): | ||||||
|  |         """Speak text and display it on screen. | ||||||
|  |          | ||||||
|  |         Args: | ||||||
|  |             text (str): Text to speak and display | ||||||
|  |             interrupt (bool): Whether to interrupt current speech | ||||||
|  |         """ | ||||||
|  |         current_time = time.time() * 1000 | ||||||
|  |          | ||||||
|  |         # Prevent rapid repeated messages | ||||||
|  |         if (self._lastSpoken["text"] == text and  | ||||||
|  |             current_time - self._lastSpoken["time"] < self._speechDelay): | ||||||
|  |             return | ||||||
|  |              | ||||||
|  |         # Update last spoken tracking | ||||||
|  |         self._lastSpoken["text"] = text | ||||||
|  |         self._lastSpoken["time"] = current_time | ||||||
|  |          | ||||||
|  |         # Handle speech output based on provider | ||||||
|  |         if self._provider == "speechd": | ||||||
|  |             if interrupt: | ||||||
|  |                 self._speech.cancel() | ||||||
|  |             self._speech.speak(text) | ||||||
|  |         else: | ||||||
|  |             self._speech.speak(text, interrupt=interrupt) | ||||||
|  |              | ||||||
|  |         # Update display text | ||||||
|  |         self._font.text = text | ||||||
|  |          | ||||||
|  |         # Center text vertically based on line count | ||||||
|  |         lineCount = len(text.split('\n')) | ||||||
|  |         self._font.y = 300 + (lineCount * self._font.font_size // 4) | ||||||
|  |          | ||||||
|  |     def cleanup(self): | ||||||
|  |         """Clean up speech system resources.""" | ||||||
|  |         if self._provider == "speechd": | ||||||
|  |             self._speech.close() | ||||||
|  |              | ||||||
|  |     def draw(self): | ||||||
|  |         """Draw the current text on screen.""" | ||||||
|  |         self._font.draw() | ||||||
		Reference in New Issue
	
	Block a user