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