initial commit, definitely not ready for use quite yet.
This commit is contained in:
commit
d7199f499f
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()
|
Loading…
x
Reference in New Issue
Block a user