initial commit, definitely not ready for use quite yet.

This commit is contained in:
Storm Dragon 2025-02-23 01:22:30 -05:00
commit d7199f499f
14 changed files with 1147 additions and 0 deletions

97
__init__.py Normal file
View 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()

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

158
config.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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()