From d7199f499f8b1940facdb09844e916a2a9303bde Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Sun, 23 Feb 2025 01:22:30 -0500 Subject: [PATCH] initial commit, definitely not ready for use quite yet. --- __init__.py | 97 +++++++ __pycache__/__init__.cpython-313.pyc | Bin 0 -> 3595 bytes __pycache__/config.cpython-313.pyc | Bin 0 -> 6980 bytes __pycache__/display.cpython-313.pyc | Bin 0 -> 7080 bytes __pycache__/menu.cpython-313.pyc | Bin 0 -> 5885 bytes __pycache__/scoreboard.cpython-313.pyc | Bin 0 -> 5320 bytes __pycache__/sound.cpython-313.pyc | Bin 0 -> 12205 bytes __pycache__/speech.cpython-313.pyc | Bin 0 -> 3657 bytes config.py | 158 +++++++++++ display.py | 140 ++++++++++ menu.py | 176 ++++++++++++ scoreboard.py | 126 +++++++++ sound.py | 366 +++++++++++++++++++++++++ speech.py | 84 ++++++ 14 files changed, 1147 insertions(+) create mode 100644 __init__.py create mode 100644 __pycache__/__init__.cpython-313.pyc create mode 100644 __pycache__/config.cpython-313.pyc create mode 100644 __pycache__/display.cpython-313.pyc create mode 100644 __pycache__/menu.cpython-313.pyc create mode 100644 __pycache__/scoreboard.cpython-313.pyc create mode 100644 __pycache__/sound.cpython-313.pyc create mode 100644 __pycache__/speech.cpython-313.pyc create mode 100644 config.py create mode 100644 display.py create mode 100644 menu.py create mode 100644 scoreboard.py create mode 100644 sound.py create mode 100644 speech.py diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..0d78b6c --- /dev/null +++ b/__init__.py @@ -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() diff --git a/__pycache__/__init__.cpython-313.pyc b/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ecd28dc7e49314c3b727bb2fa77cb2a3ead39ae2 GIT binary patch literal 3595 zcmb_f-EZT@5noD_L{YS)OZ+9-Zg?MQj?TsI0e30jbJ|`VC#{7u3^7Jir~pArluR5& zs!QswGWu{&MGXXS4_6~sv}pU7{0l|@hze)JE^7k?+LwEC?F4zv%u=L2d@azT3urmJ zGrK!8`#kVN|zYqM_^6z}%q6 zO|f0P3*A2X385T#JEn3puEr>@#*2g|R37@Int(o~CZQM96!d9Tpd$E8^v7DAhKoKy z#od=V*lXvV2k3wSldFzHYo@JxXt!m_%t*K6dPdV-sP--abs_QI@8VrhZ=ZaeM;HW6 z14yhr2w5l=|^6&NtSe**zDpx{kfE(>tnRo>^6V6o% zA+<>cG>gvLBxMHwP%8MAXNSi)D!*}g4pz&TXTx)3(ugVXod-V*A#5-}2A~>%>wVgA zmqZyFvOY3B0QgPUqso#T44itjQ2bzBx#FiIK|iIn^tNl%C_39uRv9QM2Cko~1-Qhp z2A0dPN71c*!bL>VB+{ZAVU^;I_?a?acFlPVm}{gGD2s~Txb9}}X#M7GCK-2%H@4!Ad zZ>oPzNcGQafQ<^jqyK=xTy9v-7L)@cW_!)#0CIw$)>44EnoMQHa$Y;9x=|~M1!3_C^ z7U4^$QK&c^fP+!~P2jt~hYo@;OQihX^4{`sZsK>7f0+Bt+)-h^TbMs8-02qXyvW@- ze&fck_+LxM`SDN1r{YomYBzuNAb%alnWvdx{KtjE{Oz+8$=`*T&QA{SbGuu({UUc8 z7i6Di5ArfZ>ZnY37RcnD<__~$&*HEdodXrCQB8sYh96pb@jdAFNd@|#m7~B6XCS{v z)u=x=01!@z?To9QPQ54>%a-G`7S_SdSQ`Ae-fH=A+O|LEnBQGgrRU;XC~#%i#UObP zrdM9)5Ys1L^!sP7@ zEIF4R4>AOg{8%?V_I3*3y71A?TMY0)mAWOfx*>1r)rW3NuR=w1da(eGOt4r8;GSR$ z7~1J|P=vk%-JnYmmHzawOZ(XWE=r@1W~B9z18ZKD{Qu*cN^~=?DhTE1b<;nze_jKT zI`pK{M5qNnCj%F!swmWM1x`i`)3>4Xd6d88?>CV(HSF&w?W_XR{T6iK z?$FQ0u24J>W+OjiojnldPI^Gk?dA@J0&}?{1Q(ByK(QLJ_XM3lY8*REfK1DEC4-L! z8#0}y`J$!;{|A7UH0`Ht-Rg02npQWd>sh94*bcB$npShFnnq`EE6jKl(LoVKl$kc6 zX|4x?{-krCL5*(Wd-?>?{?pK8J7;In{b8FoBf#v!IV zrk5`)2X6Qdg=Yf6wBzDMp2azi`&W$T_%9{G&76|iQ!;r@4 HI=0!@A?C{w literal 0 HcmV?d00001 diff --git a/__pycache__/config.cpython-313.pyc b/__pycache__/config.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..05be1beca0a3d82c4412b572a3342752096ad104 GIT binary patch literal 6980 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 lruRYymj(>;HuY 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 literal 0 HcmV?d00001 diff --git a/__pycache__/menu.cpython-313.pyc b/__pycache__/menu.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ba3baa49fde61d4bbc223d095bf21e0b793eff98 GIT binary patch literal 5885 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 GDfWLCS6Kf5 literal 0 HcmV?d00001 diff --git a/__pycache__/scoreboard.cpython-313.pyc b/__pycache__/scoreboard.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7764ef94be89021f49e19573688bf5d56001f1e0 GIT binary patch literal 5320 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 l_BMZbIJe! literal 0 HcmV?d00001 diff --git a/__pycache__/sound.cpython-313.pyc b/__pycache__/sound.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b8409cd9da100b12eb40097d91e814ed3e782bd9 GIT binary patch literal 12205 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 W75~8rhEJt-asJNhUHN|pZYQ1q literal 0 HcmV?d00001 diff --git a/__pycache__/speech.cpython-313.pyc b/__pycache__/speech.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e4e7f5b94ff2f4304507c2fa8b87cb3499a672a7 GIT binary patch literal 3657 zcma)9O;8-i74Dgx{Tl{$g~V#b-wpx`RuW4T3nh?v%M!?vB21hek&uw8sbP0mCK_fo zJ+ndJi%%(3WtVUqMfhYNrBi%P%Y0lHsz zzxTS|>-WBX?6tS^1ip9Q|5JIQjgWuhAow_+*n1m@`$Qs?NQ}hZWGLhIEM=h&-3Y7}kFG!$L4*ThxLUKXt)RaN6< z%~rKiF{z#7=Pb~n0-H)jPenS7ozmSE}}RMFsX+R(&RMQ7PV5jZ%)^ zK~rY4j1#)8t`$AIyw*38>|KQMeez2J?jh(J8j@JbNuh2+!%`ew@+*c$q%e%5QUuzV z6s7TUEXz9)&ow&&s8}Dq4j$6syuPj8$zE|=uE-oX21 z#KEX3(@%hdxyc&L98-uknVQ77Pk~Gj2ymq^3I{9;>3}M92UM#s^tI@Kl49ToUTTvB zsU3bgq=b~5h=a~Vz)@xf$O78peo>qT{{~Rc49270QsMW}iK_e&L3p*&2l9dNYQsHOp34ydYgt zbRA<#tayPHN2=5LYMicLpEZL3Y3hO$szR`eHw?>$ zjsCBI&pE^iU#4*6ogvs%+XU-&&O_rwimG9&^p^G<4w#KO@g>u+WdkzPCoM42VeUB0 zniEru;<8EQJ56fMiLX?3Thlc|b;7G!$zIMToi@iT{4$25w zW1dC13b;g;HHZaSb`p{cTDf2Xquu%|G#`*}heo~_sP$aj;J%Kf8@(CqPwhtdxXC;f zx*I*GKg)lb-|jiLG5z68qwfrKv->AVs`tUd#|!nYGqtWW4WXlv>T9IZjgDiRQlqQy zVXTqrZgh6-CP->{m&8*12KN#FeE&E}_Uw@eXx?SR?Vvw-sgb!*&rH-Z6ZK5KmdV#M zQ?<<0cIN6P_j{o+FuuwCq2u{C$NTpP1N`5lPCp)8*-jaclg4fcXI|{Kp}H3qrurx1 z<$m&IKYwK+^kw$ql??k;CUs>z{M9%IGez`X@F9S>P&ET_b)PH*^Q6$ETEuA*B(21Z zf}|0eBZUBRTWkPksCJH-oF_yP37P-Skz66<8ih9xv@~H2ru=UbfJA}VFLU$BBjwp) z;C!8KTm!w4KwGPq!tS}=f^~MppH=`gfg6MG&{x1zIP?Wx5ML+LenBe12vAmBre$lAhjjKhM=YCWC&gP> zl-Mq;f{p{?E~kcINUIfF99c9?9r%B?thxmSh_n`qRZ0QDd~D{me?h3b2t(sw#H@=b zP)5X?5)Q*teA9L?9<%K(8^s5e_F&VG4JH`iF@P*W6kv>Un_{$i&=t}FOkoykA&OT* z@h-Aqijn4Rjs9~4|7bEIpc6-6o`UznwlF8q7b4A~#08hx$1HcR^DtxWLh}LH?;z>k z2W!7u`{e!4Z`Dp_H-y~?X+KsMdTT=O)=z7~&_ixpINuP`^`3LJo^uc1sr9^B6W;uy zRKIkscIn!-aIGP9g3QoJJ(jM;(p#xoOl%B}9T>r?ki3__o8S8RcH5bMB+`xKi3h^R zLOnTHOAdaWe4_!&Gk0g|LVr!@-?E!z^s^nmw7L7H}NEP%C!MAGYYiZWX(%8G4M zZLw;rmMlLa?t#3-!>FLxxCIAh|8L3PxC_s?o-n^ZnF(KIo?T%=;er3~Y`EK#L+)&p zPNO8^3w;9`r`;=Ta;utAGFK`3--)3|SE*8QccAYeGv24WwG;cHXJnq 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') diff --git a/menu.py b/menu.py new file mode 100644 index 0000000..f4951ff --- /dev/null +++ b/menu.py @@ -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.") diff --git a/scoreboard.py b/scoreboard.py new file mode 100644 index 0000000..1c31189 --- /dev/null +++ b/scoreboard.py @@ -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 diff --git a/sound.py b/sound.py new file mode 100644 index 0000000..af47058 --- /dev/null +++ b/sound.py @@ -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() diff --git a/speech.py b/speech.py new file mode 100644 index 0000000..2b6221a --- /dev/null +++ b/speech.py @@ -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()