Compare commits
10 Commits
6bddf282a7
...
master
Author | SHA1 | Date | |
---|---|---|---|
8264db4a2f | |||
4aa3475ac4 | |||
0d9764abd9 | |||
e68f6798d7 | |||
a20da7df8c | |||
e35c826b05 | |||
ea0dcd9ce9 | |||
23b3e5792e | |||
8e274731c5 | |||
4d59a610a0 |
643
README.md
Normal file
643
README.md
Normal file
@ -0,0 +1,643 @@
|
||||
# PygStormGames Library
|
||||
|
||||
A Python framework for creating accessible audio games with Pyglet.
|
||||
|
||||
## Overview
|
||||
|
||||
PygStormGames is a specialized game framework built on top of Pyglet, designed specifically for creating audio-focused games with accessibility in mind. The library provides a complete set of tools for building games with rich audio feedback, text-to-speech integration, menu systems, and more.
|
||||
|
||||
## Core Features
|
||||
|
||||
- **Accessible Interface**: Built-in text-to-speech support and screen reader compatibility
|
||||
- **Audio Management**: Spatial audio, sound effects, and background music
|
||||
- **Menu System**: Fully accessible menu with navigation sounds
|
||||
- **Configuration**: Local and global settings management
|
||||
- **Scoreboard**: High score tracking and persistence
|
||||
- **Text Display**: Accessible text display with navigation
|
||||
|
||||
## Installation
|
||||
|
||||
PygStormGames requires Python 3.6+ and depends on the following libraries:
|
||||
- pyglet
|
||||
- pyperclip
|
||||
- wx (wxPython)
|
||||
- xdg
|
||||
- speechd or accessible_output2 (for text-to-speech)
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Basic Game Structure
|
||||
|
||||
```python
|
||||
from pygstormgames import pygstormgames
|
||||
|
||||
class MyGame:
|
||||
def __init__(self):
|
||||
# Initialize the game with a title
|
||||
self.game = pygstormgames("My Audio Game")
|
||||
|
||||
# Set up game-specific variables and components
|
||||
self.score = 0
|
||||
self.gameActive = False
|
||||
|
||||
# Set up the main game loop
|
||||
pyglet.clock.schedule_interval(self.update, 1/60.0)
|
||||
|
||||
def update(self, dt):
|
||||
# Main game update loop
|
||||
if self.gameActive:
|
||||
# Update game state
|
||||
pass
|
||||
|
||||
def start(self):
|
||||
# Show main menu
|
||||
selection = self.game.menu.game_menu()
|
||||
|
||||
if selection == "play":
|
||||
# Start the game
|
||||
self.gameActive = True
|
||||
self.game.run()
|
||||
|
||||
if __name__ == "__main__":
|
||||
game = MyGame()
|
||||
game.start()
|
||||
```
|
||||
|
||||
## Main Components
|
||||
|
||||
### pygstormgames Class
|
||||
|
||||
The main class that coordinates all game systems. Initialize with the game title.
|
||||
|
||||
```python
|
||||
game = pygstormgames("My Game Title")
|
||||
```
|
||||
|
||||
#### Methods
|
||||
|
||||
- `run()`: Start the game loop
|
||||
- `wait(validKeys=None, timeout=None)`: Wait for key press(es) with optional timeout
|
||||
- `wait_for_completion(condition, validKeys=None, timeout=None)`: Wait for a condition to be met, keys to be pressed, or timeout
|
||||
- `pause_game()`: Pause all game systems
|
||||
- `exit_game()`: Clean up and exit the game
|
||||
|
||||
### Config
|
||||
|
||||
Handles loading and saving of both local and global game configurations.
|
||||
|
||||
```python
|
||||
# Get a config value (section, key, default value, global or local)
|
||||
volume = game.config.get_float("audio", "master_volume", 0.8)
|
||||
|
||||
# Save a config value
|
||||
game.config.set_value("audio", "master_volume", 0.9)
|
||||
```
|
||||
|
||||
#### Methods
|
||||
|
||||
- `get_value(section, key, default=None, globalConfig=False)`: Get string value
|
||||
- `set_value(section, key, value, globalConfig=False)`: Set value
|
||||
- `get_int(section, key, default=0, globalConfig=False)`: Get integer value
|
||||
- `get_float(section, key, default=0.0, globalConfig=False)`: Get float value
|
||||
- `get_bool(section, key, default=False, globalConfig=False)`: Get boolean value
|
||||
|
||||
### Display
|
||||
|
||||
Handles text display, navigation, and information presentation.
|
||||
|
||||
```python
|
||||
# Display text with speech output
|
||||
game.display.display_text(textLines, game.speech)
|
||||
|
||||
# Show a message box
|
||||
game.display.messagebox("Game over! Your score: 1000")
|
||||
|
||||
# Get text input from the user
|
||||
name = game.display.get_input("Enter your name:", "Player")
|
||||
```
|
||||
|
||||
#### Methods
|
||||
|
||||
- `display_text(text, speech)`: Display and navigate text with speech output
|
||||
- `instructions(speech)`: Display game instructions from file
|
||||
- `credits(speech)`: Display game credits from file
|
||||
- `messagebox(text)`: Display a simple message box with text
|
||||
- `get_input(prompt="Enter text:", defaultText="")`: Display a dialog box for text input
|
||||
- `donate(speech)`: Open the donation webpage
|
||||
|
||||
### Menu
|
||||
|
||||
Handles main menu and submenu functionality for games.
|
||||
|
||||
```python
|
||||
# Show a menu and get selection
|
||||
options = ["start", "options", "exit"]
|
||||
selection = game.menu.show_menu(options, "Main Menu")
|
||||
|
||||
# Show the default game menu
|
||||
selection = game.menu.game_menu()
|
||||
```
|
||||
|
||||
#### Methods
|
||||
|
||||
- `show_menu(options, title=None, with_music=False)`: Display a menu and return selected option
|
||||
- `game_menu()`: Show main game menu
|
||||
- `learn_sounds()`: Interactive menu for learning game sounds
|
||||
|
||||
### Sound
|
||||
|
||||
Handles all audio functionality including background music, sound effects, and volume control.
|
||||
|
||||
```python
|
||||
# Play a sound effect
|
||||
game.sound.play_sound("explosion")
|
||||
|
||||
# Play background music
|
||||
game.sound.play_bgm("sounds/music_level1.ogg")
|
||||
|
||||
# Adjust volume
|
||||
game.sound.adjust_master_volume(0.1) # Increase by 10%
|
||||
```
|
||||
|
||||
#### Methods
|
||||
|
||||
- `play_sound(soundName, volume=1.0)`: Play a sound effect
|
||||
- `play_bgm(music_file)`: Play background music
|
||||
- `pause_bgm()`: Pause background music
|
||||
- `resume_bgm()`: Resume background music
|
||||
- `play_random(base_name, pause=False, interrupt=False)`: Play random variation of a sound
|
||||
- `play_positional(soundName, source_pos, listener_pos, mode='2d')`: Play sound with positional audio
|
||||
- `update_positional(player, source_pos, listener_pos, mode='2d')`: Update position of a playing sound
|
||||
- `cut_scene(soundName)`: Play a sound as a cut scene
|
||||
- `adjust_master_volume(change)`: Adjust master volume
|
||||
- `adjust_bgm_volume(change)`: Adjust background music volume
|
||||
- `adjust_sfx_volume(change)`: Adjust sound effects volume
|
||||
- `get_volumes()`: Get current volume levels
|
||||
- `pause()`: Pause all audio
|
||||
- `resume()`: Resume all audio
|
||||
- `stop_all_sounds()`: Stop all playing sounds
|
||||
|
||||
### Speech
|
||||
|
||||
Provides text-to-speech functionality with screen text display support.
|
||||
|
||||
```python
|
||||
# Speak text
|
||||
game.speech.speak("Welcome to my game!")
|
||||
|
||||
# Speak with no interruption
|
||||
game.speech.speak("This will wait for previous speech to finish", interrupt=False)
|
||||
```
|
||||
|
||||
#### Methods
|
||||
|
||||
- `speak(text, interrupt=True)`: Speak text and display it on screen
|
||||
|
||||
### Scoreboard
|
||||
|
||||
Handles high score tracking with player names and score management.
|
||||
|
||||
```python
|
||||
# Increase the score
|
||||
game.scoreboard.increase_score(100)
|
||||
|
||||
# Check and add high score if qualified
|
||||
if game.scoreboard.check_high_score():
|
||||
game.scoreboard.add_high_score()
|
||||
|
||||
# Get high scores
|
||||
highScores = game.scoreboard.get_high_scores()
|
||||
```
|
||||
|
||||
#### Methods
|
||||
|
||||
- `get_score()`: Get current score
|
||||
- `get_high_scores()`: Get list of high scores
|
||||
- `decrease_score(points=1)`: Decrease the current score
|
||||
- `increase_score(points=1)`: Increase the current score
|
||||
- `check_high_score()`: Check if current score qualifies as a high score
|
||||
- `add_high_score()`: Add current score to high scores if it qualifies
|
||||
|
||||
## Key Event Handling
|
||||
|
||||
PygStormGames provides a simple way to handle keyboard input:
|
||||
|
||||
```python
|
||||
# Wait for any key
|
||||
key, modifiers = game.wait()
|
||||
|
||||
# Wait for specific keys
|
||||
validKeys = [pyglet.window.key.SPACE, pyglet.window.key.RETURN]
|
||||
key, modifiers = game.wait(validKeys)
|
||||
|
||||
# Wait with timeout (5 seconds)
|
||||
key, modifiers = game.wait(timeout=5.0)
|
||||
|
||||
# Check for specific key
|
||||
if key == pyglet.window.key.SPACE:
|
||||
# Space was pressed
|
||||
pass
|
||||
```
|
||||
|
||||
## Directory Structure
|
||||
|
||||
PygStormGames expects the following directory structure:
|
||||
|
||||
```
|
||||
your_game/
|
||||
â __init__.py
|
||||
â main.py
|
||||
â sounds/ # Sound effects and music
|
||||
â â music_menu.ogg
|
||||
â â game-intro.ogg
|
||||
â â ...
|
||||
â files/ # Game text files
|
||||
â instructions.txt
|
||||
â credits.txt
|
||||
```
|
||||
|
||||
### Required Sound Files
|
||||
|
||||
PygStormGames looks for these sound files, though they're optional:
|
||||
- `game-intro.ogg`: Played when the game starts
|
||||
- `music_menu.ogg`: Played during menu navigation
|
||||
- `menu-move.ogg`: Played when navigating menu items
|
||||
- `menu-select.ogg`: Played when selecting a menu item
|
||||
|
||||
## Advanced Usage Examples
|
||||
|
||||
### Creating a Custom Menu
|
||||
|
||||
```python
|
||||
def settings_menu(self):
|
||||
options = ["sound volume", "music volume", "speech rate", "back"]
|
||||
|
||||
while True:
|
||||
selection = self.game.menu.show_menu(options, "Settings")
|
||||
|
||||
if selection == "sound volume":
|
||||
self.adjust_sound_volume()
|
||||
elif selection == "music volume":
|
||||
self.adjust_music_volume()
|
||||
elif selection == "speech rate":
|
||||
self.adjust_speech_rate()
|
||||
elif selection == "back":
|
||||
return
|
||||
```
|
||||
|
||||
### Playing Positional Audio
|
||||
|
||||
```python
|
||||
# For 2D games (x-position only)
|
||||
def update_enemy_sounds(self):
|
||||
for enemy in self.enemies:
|
||||
# Player at x=0, enemy at some x position
|
||||
self.game.sound.play_positional(
|
||||
"enemy-sound",
|
||||
enemy.x, # Source position
|
||||
self.player.x, # Listener position
|
||||
mode='2d'
|
||||
)
|
||||
|
||||
# For 3D games
|
||||
def update_enemy_sounds_3d(self):
|
||||
for enemy in self.enemies:
|
||||
# 3D positions (x, y, z)
|
||||
source_pos = (enemy.x, enemy.y, enemy.z)
|
||||
listener_pos = (self.player.x, self.player.y, self.player.z)
|
||||
|
||||
self.game.sound.play_positional(
|
||||
"enemy-sound",
|
||||
source_pos,
|
||||
listener_pos,
|
||||
mode='3d'
|
||||
)
|
||||
```
|
||||
|
||||
### Creating a Countdown Timer
|
||||
|
||||
```python
|
||||
def start_countdown(self):
|
||||
for i in range(3, 0, -1):
|
||||
self.game.speech.speak(str(i))
|
||||
self.game.sound.play_sound(f"countdown-{i}")
|
||||
# Wait 1 second
|
||||
self.game.wait(timeout=1.0)
|
||||
|
||||
self.game.speech.speak("Go!")
|
||||
self.game.sound.play_sound("start")
|
||||
self.gameActive = True
|
||||
```
|
||||
|
||||
### Saving and Loading Game Progress
|
||||
|
||||
```python
|
||||
def save_game(self):
|
||||
# Save player progress
|
||||
self.game.config.set_value("save", "level", self.currentLevel)
|
||||
self.game.config.set_value("save", "score", self.score)
|
||||
self.game.config.set_value("save", "health", self.playerHealth)
|
||||
self.game.speech.speak("Game saved.")
|
||||
|
||||
def load_game(self):
|
||||
# Load player progress
|
||||
self.currentLevel = self.game.config.get_int("save", "level", 1)
|
||||
self.score = self.game.config.get_int("save", "score", 0)
|
||||
self.playerHealth = self.game.config.get_int("save", "health", 100)
|
||||
self.game.speech.speak(f"Game loaded. Level {self.currentLevel}")
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Game Loop with Pause Support
|
||||
|
||||
```python
|
||||
def update(self, dt):
|
||||
# Skip if game is paused
|
||||
if self.game._paused:
|
||||
return
|
||||
|
||||
# Update game state
|
||||
self.update_player(dt)
|
||||
self.update_enemies(dt)
|
||||
self.check_collisions()
|
||||
self.update_score()
|
||||
```
|
||||
|
||||
### Handling Game Over
|
||||
|
||||
```python
|
||||
def game_over(self):
|
||||
# Stop gameplay sounds
|
||||
self.game.sound.stop_all_sounds()
|
||||
|
||||
# Play game over sound
|
||||
self.game.sound.play_sound("game-over")
|
||||
|
||||
# Display final score
|
||||
finalScore = self.game.scoreboard.get_score()
|
||||
self.game.speech.speak(f"Game over! Your score: {finalScore}")
|
||||
|
||||
# Check for high score
|
||||
if self.game.scoreboard.check_high_score():
|
||||
self.game.scoreboard.add_high_score()
|
||||
|
||||
# Wait for key press
|
||||
self.game.display.messagebox("Game over!")
|
||||
|
||||
# Return to main menu
|
||||
self.game.menu.game_menu()
|
||||
```
|
||||
|
||||
### Creating a Timed Event System
|
||||
|
||||
```python
|
||||
class TimedEvent:
|
||||
def __init__(self, time, callback):
|
||||
self.triggerTime = time
|
||||
self.callback = callback
|
||||
self.triggered = False
|
||||
|
||||
class EventSystem:
|
||||
def __init__(self, game):
|
||||
self.game = game
|
||||
self.events = []
|
||||
self.gameTime = 0
|
||||
|
||||
def add_event(self, delay, callback):
|
||||
triggerTime = self.gameTime + delay
|
||||
self.events.append(TimedEvent(triggerTime, callback))
|
||||
|
||||
def update(self, dt):
|
||||
self.gameTime += dt
|
||||
|
||||
for event in self.events:
|
||||
if not event.triggered and self.gameTime >= event.triggerTime:
|
||||
event.callback()
|
||||
event.triggered = True
|
||||
|
||||
# Remove triggered events
|
||||
self.events = [e for e in self.events if not e.triggered]
|
||||
|
||||
# Usage
|
||||
events = EventSystem(game)
|
||||
events.add_event(5.0, lambda: game.speech.speak("Warning! Enemy approaching!"))
|
||||
```
|
||||
|
||||
## Tips and Best Practices
|
||||
|
||||
1. **Audio Cues**: Always provide clear audio feedback for important actions and events.
|
||||
|
||||
2. **Speech Rate**: Keep speech concise and prioritize critical information.
|
||||
|
||||
3. **Sound Balance**: Maintain a good balance between speech, sound effects, and music.
|
||||
|
||||
4. **Consistent Controls**: Keep controls consistent and provide easy ways to learn them.
|
||||
|
||||
5. **Performance**: Be mindful of audio channel usage, especially with spatial audio.
|
||||
|
||||
6. **Documentation**: Include clear instructions for controls and gameplay.
|
||||
|
||||
7. **File Structure**: Follow the expected file structure for sounds and text files.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **No speech output**
|
||||
- Ensure speechd or accessible_output2 is installed
|
||||
|
||||
2. **Sound files not playing**
|
||||
- Verify file paths and formats (ogg or wav)
|
||||
- Check if sound files exist in the correct directory
|
||||
- Check volume settings in the game
|
||||
|
||||
3. **Menus not responding**
|
||||
- Ensure pyglet event loop is running
|
||||
- Check if key handlers are properly registered
|
||||
|
||||
### Debugging Tips
|
||||
|
||||
- Use `print` statements to track game state
|
||||
- Enable more verbose logging in the speech module
|
||||
- Test with minimal sound files to isolate issues
|
||||
|
||||
## Complete Example Game
|
||||
|
||||
Here's a simple example of a complete game that demonstrates the key features of PygStormGames:
|
||||
|
||||
```python
|
||||
import pyglet
|
||||
from pygstormgames import pygstormgames
|
||||
import random
|
||||
import math
|
||||
|
||||
class SimpleGame:
|
||||
def __init__(self):
|
||||
# Initialize game
|
||||
self.game = pygstormgames("Sound Hunter")
|
||||
|
||||
# Game variables
|
||||
self.playerX = 0
|
||||
self.playerY = 0
|
||||
self.targetX = 0
|
||||
self.targetY = 0
|
||||
self.score = 0
|
||||
self.timeLeft = 60
|
||||
self.gameActive = False
|
||||
|
||||
# Set up game loop
|
||||
pyglet.clock.schedule_interval(self.update, 1/60.0)
|
||||
|
||||
def start_game(self):
|
||||
# Reset game state
|
||||
self.score = 0
|
||||
self.timeLeft = 60
|
||||
self.gameActive = True
|
||||
self.game.scoreboard.currentScore = 0
|
||||
|
||||
# Place target
|
||||
self.place_new_target()
|
||||
|
||||
# Start background music
|
||||
self.game.sound.play_bgm("sounds/game_music.ogg")
|
||||
|
||||
# Announce game start
|
||||
self.game.speech.speak("Game started. Use arrow keys to find the target.")
|
||||
|
||||
def place_new_target(self):
|
||||
# Place target randomly within range
|
||||
distance = random.uniform(5, 15)
|
||||
angle = random.uniform(0, 2 * math.pi)
|
||||
|
||||
self.targetX = math.cos(angle) * distance
|
||||
self.targetY = math.sin(angle) * distance
|
||||
|
||||
# Play new target sound
|
||||
self.game.sound.play_sound("new-target")
|
||||
|
||||
def update(self, dt):
|
||||
if not self.gameActive or self.game._paused:
|
||||
return
|
||||
|
||||
# Update timer
|
||||
self.timeLeft -= dt
|
||||
if self.timeLeft <= 0:
|
||||
self.game_over()
|
||||
return
|
||||
|
||||
# Play positional target sound every second
|
||||
if int(self.timeLeft) != int(self.timeLeft - dt):
|
||||
self.game.sound.play_positional(
|
||||
"target-ping",
|
||||
self.targetX,
|
||||
self.playerX,
|
||||
mode='2d'
|
||||
)
|
||||
|
||||
# Calculate distance to target
|
||||
distance = math.sqrt((self.playerX - self.targetX)**2 +
|
||||
(self.playerY - self.targetY)**2)
|
||||
|
||||
# Check for target collection
|
||||
if distance < 1.0:
|
||||
self.collect_target()
|
||||
|
||||
def on_key_press(self, symbol, modifiers):
|
||||
if not self.gameActive:
|
||||
return
|
||||
|
||||
# Move player
|
||||
if symbol == pyglet.window.key.UP:
|
||||
self.playerY += 1
|
||||
elif symbol == pyglet.window.key.DOWN:
|
||||
self.playerY -= 1
|
||||
elif symbol == pyglet.window.key.LEFT:
|
||||
self.playerX -= 1
|
||||
elif symbol == pyglet.window.key.RIGHT:
|
||||
self.playerX += 1
|
||||
|
||||
# Announce position occasionally
|
||||
if random.random() < 0.1:
|
||||
self.announce_position()
|
||||
|
||||
def announce_position(self):
|
||||
# Calculate distance and direction to target
|
||||
dx = self.targetX - self.playerX
|
||||
dy = self.targetY - self.playerY
|
||||
distance = math.sqrt(dx*dx + dy*dy)
|
||||
|
||||
# Create directional hint
|
||||
if abs(dx) > abs(dy):
|
||||
direction = "east" if dx > 0 else "west"
|
||||
else:
|
||||
direction = "north" if dy > 0 else "south"
|
||||
|
||||
# Announce distance category
|
||||
if distance < 2.0:
|
||||
proximity = "very close"
|
||||
elif distance < 5.0:
|
||||
proximity = "close"
|
||||
elif distance < 10.0:
|
||||
proximity = "medium distance"
|
||||
else:
|
||||
proximity = "far away"
|
||||
|
||||
self.game.speech.speak(f"Target is {proximity} to the {direction}")
|
||||
|
||||
def collect_target(self):
|
||||
# Play collection sound
|
||||
self.game.sound.play_sound("target-collect")
|
||||
|
||||
# Update score
|
||||
self.score += 1
|
||||
self.game.scoreboard.increase_score(100)
|
||||
|
||||
# Announce collection
|
||||
self.game.speech.speak(f"Target collected! Score: {self.score}")
|
||||
|
||||
# Place new target
|
||||
self.place_new_target()
|
||||
|
||||
# Add time bonus
|
||||
self.timeLeft += 5
|
||||
|
||||
def game_over(self):
|
||||
self.gameActive = False
|
||||
self.game.sound.stop_all_sounds()
|
||||
self.game.sound.play_sound("game-over")
|
||||
|
||||
finalScore = self.game.scoreboard.get_score()
|
||||
self.game.speech.speak(f"Game over! Final score: {finalScore}")
|
||||
|
||||
# Check for high score
|
||||
self.game.scoreboard.add_high_score()
|
||||
|
||||
# Return to menu after delay
|
||||
self.game.wait(timeout=3.0)
|
||||
self.start()
|
||||
|
||||
def start(self):
|
||||
# Register key handlers
|
||||
self.game.display.window.push_handlers(self.on_key_press)
|
||||
|
||||
# Show main menu
|
||||
while True:
|
||||
selection = self.game.menu.game_menu()
|
||||
|
||||
if selection == "play":
|
||||
self.start_game()
|
||||
break
|
||||
elif selection == "exit":
|
||||
self.game.exit_game()
|
||||
break
|
||||
|
||||
# Start game loop
|
||||
self.game.run()
|
||||
|
||||
if __name__ == "__main__":
|
||||
game = SimpleGame()
|
||||
game.start()
|
||||
```
|
||||
|
||||
This example demonstrates using menus, speech, positional audio, and scoreboards in a simple audio-based game where the player needs to find targets using audio cues.
|
29
__init__.py
29
__init__.py
@ -89,17 +89,20 @@ class pygstormgames:
|
||||
self.speech.cleanup()
|
||||
pyglet.app.exit()
|
||||
|
||||
def wait(self, validKeys=None):
|
||||
"""Wait for key press(es).
|
||||
def wait(self, validKeys=None, timeout=None):
|
||||
"""Wait for key press(es) with optional timeout.
|
||||
|
||||
Args:
|
||||
validKeys (list, optional): List of pyglet.window.key values to wait for.
|
||||
If None, accepts any key press.
|
||||
timeout (float, optional): Time in seconds to wait for input.
|
||||
If None, waits indefinitely.
|
||||
|
||||
Returns:
|
||||
tuple: (key, modifiers) that were pressed
|
||||
tuple: (key, modifiers) that were pressed. Returns (None, None) on timeout.
|
||||
"""
|
||||
keyResult = [None, None] # Use list to allow modification in closure
|
||||
start_time = time.time()
|
||||
|
||||
def on_key_press(symbol, modifiers):
|
||||
if validKeys is None or symbol in validKeys:
|
||||
@ -110,25 +113,31 @@ class pygstormgames:
|
||||
# Register temporary handler
|
||||
self.display.window.push_handlers(on_key_press=on_key_press)
|
||||
|
||||
# Wait for valid key press
|
||||
# Wait for valid key press or timeout
|
||||
while keyResult[0] is None:
|
||||
self.display.window.dispatch_events()
|
||||
pyglet.clock.tick()
|
||||
|
||||
# Check for timeout
|
||||
if timeout is not None and time.time() - start_time > timeout:
|
||||
break
|
||||
|
||||
# Clean up
|
||||
self.display.window.remove_handlers()
|
||||
return tuple(keyResult)
|
||||
|
||||
def wait_for_completion(self, condition, validKeys=None):
|
||||
"""Wait for either a condition to be met or valid keys to be pressed.
|
||||
def wait_for_completion(self, condition, validKeys=None, timeout=None):
|
||||
"""Wait for either a condition to be met, valid keys to be pressed, or timeout.
|
||||
|
||||
Args:
|
||||
condition: Function that returns True when waiting should end
|
||||
validKeys (list, optional): List of pyglet.window.key values that can interrupt
|
||||
If None, uses ESCAPE, RETURN, and SPACE
|
||||
timeout (float, optional): Time in seconds to wait before timing out
|
||||
If None, waits indefinitely
|
||||
|
||||
Returns:
|
||||
bool: True if interrupted by key press, False if condition was met
|
||||
bool: True if interrupted by key press or timeout, False if condition was met
|
||||
"""
|
||||
if validKeys is None:
|
||||
validKeys = [pyglet.window.key.ESCAPE,
|
||||
@ -136,6 +145,7 @@ class pygstormgames:
|
||||
pyglet.window.key.SPACE]
|
||||
|
||||
interrupted = [False] # Use list to allow modification in closure
|
||||
start_time = time.time()
|
||||
|
||||
def on_key_press(symbol, modifiers):
|
||||
if symbol in validKeys:
|
||||
@ -148,6 +158,11 @@ class pygstormgames:
|
||||
self.display.window.dispatch_events()
|
||||
pyglet.clock.tick()
|
||||
|
||||
# Check for timeout
|
||||
if timeout is not None and time.time() - start_time > timeout:
|
||||
interrupted[0] = True
|
||||
break
|
||||
|
||||
self.display.window.remove_handlers()
|
||||
return interrupted[0]
|
||||
|
||||
|
11
display.py
11
display.py
@ -37,6 +37,8 @@ class Display:
|
||||
text (list): List of text lines to display
|
||||
speech (Speech): Speech system for audio output
|
||||
"""
|
||||
# Stop bgm if present.
|
||||
self.game.sound.pause_bgm()
|
||||
# Store original text with blank lines for copying
|
||||
self.originalText = text.copy()
|
||||
|
||||
@ -163,11 +165,20 @@ class Display:
|
||||
app = wx.App(False)
|
||||
dialog = wx.TextEntryDialog(None, prompt, "Input", defaultText)
|
||||
dialog.SetValue(defaultText)
|
||||
|
||||
# Bring dialog to front and give it focus
|
||||
dialog.Raise()
|
||||
dialog.SetFocus()
|
||||
|
||||
if dialog.ShowModal() == wx.ID_OK:
|
||||
userInput = dialog.GetValue()
|
||||
else:
|
||||
userInput = None
|
||||
dialog.Destroy()
|
||||
|
||||
# Return focus to game window
|
||||
self.window.activate()
|
||||
|
||||
return userInput
|
||||
|
||||
def donate(self, speech):
|
||||
|
38
menu.py
38
menu.py
@ -26,7 +26,7 @@ class Menu:
|
||||
try:
|
||||
if self.game.sound.currentBgm:
|
||||
self.game.sound.currentBgm.pause()
|
||||
self.game.sound.play_bgm("sounds/music_menu.ogg")
|
||||
self.game.sound.play_bgm("music_menu")
|
||||
except:
|
||||
pass
|
||||
|
||||
@ -171,19 +171,18 @@ class Menu:
|
||||
except:
|
||||
pass
|
||||
|
||||
self.currentIndex = 0
|
||||
# Get list of available sounds, excluding music and ambiance directories
|
||||
soundList = self.game.sound.get_sound_list()
|
||||
|
||||
# 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("_"))]
|
||||
|
||||
if not soundFiles:
|
||||
if not soundList:
|
||||
self.game.speech.speak("No sounds available to learn.")
|
||||
return "menu"
|
||||
|
||||
# Sort sounds by name
|
||||
soundList.sort()
|
||||
|
||||
self.currentIndex = 0
|
||||
|
||||
validKeys = [
|
||||
pyglet.window.key.ESCAPE,
|
||||
pyglet.window.key.RETURN,
|
||||
@ -193,8 +192,13 @@ class Menu:
|
||||
pyglet.window.key.S
|
||||
]
|
||||
|
||||
# Speak initial instructions
|
||||
self.game.speech.speak("Learn game sounds. Use up and down arrow keys or W/S to navigate, Enter to play sound, Escape to exit.")
|
||||
|
||||
# Speak initial sound name
|
||||
self.game.speech.speak(soundFiles[self.currentIndex][:-4])
|
||||
soundName = soundList[self.currentIndex]
|
||||
displayName = soundName.replace("/", " in folder ")
|
||||
self.game.speech.speak(f"{displayName}: 1 of {len(soundList)}")
|
||||
|
||||
while True:
|
||||
key, _ = self.game.wait(validKeys)
|
||||
@ -207,18 +211,22 @@ class Menu:
|
||||
return "menu"
|
||||
|
||||
if key in [pyglet.window.key.DOWN, pyglet.window.key.S]:
|
||||
if self.currentIndex < len(soundFiles) - 1:
|
||||
if self.currentIndex < len(soundList) - 1:
|
||||
self.game.sound.stop_all_sounds()
|
||||
self.currentIndex += 1
|
||||
self.game.speech.speak(soundFiles[self.currentIndex][:-4])
|
||||
soundName = soundList[self.currentIndex]
|
||||
displayName = soundName.replace("/", " in folder ")
|
||||
self.game.speech.speak(f"{displayName}: {self.currentIndex + 1} of {len(soundList)}")
|
||||
|
||||
if key in [pyglet.window.key.UP, pyglet.window.key.W]:
|
||||
if self.currentIndex > 0:
|
||||
self.game.sound.stop_all_sounds()
|
||||
self.currentIndex -= 1
|
||||
self.game.speech.speak(soundFiles[self.currentIndex][:-4])
|
||||
soundName = soundList[self.currentIndex]
|
||||
displayName = soundName.replace("/", " in folder ")
|
||||
self.game.speech.speak(f"{displayName}: {self.currentIndex + 1} of {len(soundList)}")
|
||||
|
||||
if key == pyglet.window.key.RETURN:
|
||||
soundName = soundFiles[self.currentIndex][:-4]
|
||||
soundName = soundList[self.currentIndex]
|
||||
self.game.sound.stop_all_sounds()
|
||||
self.game.sound.play_sound(soundName)
|
||||
|
@ -3,6 +3,7 @@
|
||||
Handles high score tracking with player names and score management.
|
||||
"""
|
||||
|
||||
import pyglet
|
||||
import time
|
||||
|
||||
class Scoreboard:
|
||||
@ -121,6 +122,12 @@ class Scoreboard:
|
||||
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!")
|
||||
# Force window refresh after dialog
|
||||
self.game.display.window.dispatch_events()
|
||||
|
||||
self.game.display.messagebox(f"Congratulations {name}! You got position {position} on the scoreboard!")
|
||||
time.sleep(1)
|
||||
|
||||
# Make sure window is still responsive
|
||||
self.game.display.window.dispatch_events()
|
||||
return True
|
||||
|
227
sound.py
227
sound.py
@ -5,6 +5,7 @@ Handles all audio functionality including:
|
||||
- Sound effects with 2D/3D positional audio
|
||||
- Volume control for master, BGM, and SFX
|
||||
- Audio loading and resource management
|
||||
- Support for organizing sounds in subdirectories
|
||||
"""
|
||||
|
||||
import os
|
||||
@ -12,12 +13,15 @@ import pyglet
|
||||
import random
|
||||
import re
|
||||
import time
|
||||
from os.path import isfile, join
|
||||
from os.path import isfile, join, isdir
|
||||
from pyglet.window import key
|
||||
|
||||
class Sound:
|
||||
"""Handles audio playback and management."""
|
||||
|
||||
# Directories to exclude from the learn sounds menu
|
||||
excludedLearnDirs = ['music', 'ambiance']
|
||||
|
||||
def __init__(self, game):
|
||||
"""Initialize sound system.
|
||||
|
||||
@ -36,50 +40,139 @@ class Sound:
|
||||
self.currentBgm = None
|
||||
|
||||
# Load sound resources
|
||||
self.sounds = self._load_sounds()
|
||||
self.sounds = {} # Dictionary of loaded sound objects
|
||||
self.soundPaths = {} # Dictionary to track original paths
|
||||
self.soundDirectories = {} # Track which directory each sound belongs to
|
||||
self._load_sounds()
|
||||
|
||||
self.activeSounds = [] # Track playing sounds
|
||||
|
||||
def _load_sounds(self):
|
||||
"""Load all sound files from sounds directory.
|
||||
"""Load all sound files from sounds directory and subdirectories.
|
||||
|
||||
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:
|
||||
if not os.path.exists("sounds/"):
|
||||
print("No sounds directory found")
|
||||
return {}
|
||||
return
|
||||
|
||||
try:
|
||||
# Walk through all subdirectories
|
||||
for root, dirs, files in os.walk("sounds/"):
|
||||
# Process sound files in this directory
|
||||
for file in files:
|
||||
if file.lower().endswith(('.wav', '.ogg', '.opus')):
|
||||
# Get relative path from sounds directory
|
||||
rel_path = os.path.relpath(os.path.join(root, file), "sounds/")
|
||||
|
||||
# Extract name without extension
|
||||
basename = os.path.splitext(file)[0]
|
||||
|
||||
# Get directory relative to sounds folder
|
||||
subdir = os.path.dirname(rel_path)
|
||||
|
||||
# Create a sound key that maintains subdirectory structure if needed
|
||||
if subdir:
|
||||
sound_key = f"{subdir}/{basename}"
|
||||
directory = subdir.split('/')[0] # Get top-level directory
|
||||
else:
|
||||
sound_key = basename
|
||||
directory = ""
|
||||
|
||||
# Full path to the sound file
|
||||
fullPath = f"sounds/{rel_path}"
|
||||
|
||||
# Load the sound
|
||||
try:
|
||||
self.sounds[sound_key] = pyglet.media.load(fullPath, streaming=False)
|
||||
self.soundPaths[sound_key] = fullPath
|
||||
self.soundDirectories[sound_key] = directory
|
||||
except Exception as e:
|
||||
print(f"Error loading sound {fullPath}: {e}")
|
||||
except Exception as e:
|
||||
print(f"Error loading sounds: {e}")
|
||||
|
||||
return sounds
|
||||
|
||||
def play_bgm(self, music_file):
|
||||
"""Play background music with proper volume.
|
||||
def get_sound_list(self, excludeDirs=None, includeSpecial=False):
|
||||
"""Get a list of available sounds, optionally excluding certain directories and special sounds.
|
||||
|
||||
Args:
|
||||
music_file (str): Path to music file
|
||||
excludeDirs (list): List of directory names to exclude
|
||||
includeSpecial (bool): Whether to include special sounds (logo, music_menu, etc.)
|
||||
|
||||
Returns:
|
||||
list: List of sound keys
|
||||
"""
|
||||
if excludeDirs is None:
|
||||
excludeDirs = self.excludedLearnDirs
|
||||
|
||||
# Special sound names to exclude unless specifically requested
|
||||
specialSounds = ["music_menu", "logo", "game-intro"]
|
||||
|
||||
# Filter sounds based on their directories and special names
|
||||
filtered_sounds = []
|
||||
for key, directory in self.soundDirectories.items():
|
||||
# Skip sounds in excluded directories
|
||||
if directory in excludeDirs:
|
||||
continue
|
||||
|
||||
# Skip special sounds unless specifically requested
|
||||
basename = key.split('/')[-1] # Get the filename without directory
|
||||
if not includeSpecial and (basename in specialSounds or basename.startswith('_')):
|
||||
continue
|
||||
|
||||
filtered_sounds.append(key)
|
||||
|
||||
return filtered_sounds
|
||||
|
||||
def play_bgm(self, music_name):
|
||||
"""Play background music with proper volume and looping.
|
||||
|
||||
Args:
|
||||
music_name (str): Name of the music file in the sounds directory
|
||||
Can be just the name (e.g., "music_menu")
|
||||
or include subdirectory (e.g., "music/title")
|
||||
"""
|
||||
try:
|
||||
# Clean up old player
|
||||
if self.currentBgm:
|
||||
self.currentBgm.pause()
|
||||
self.currentBgm = None
|
||||
|
||||
# Load and play new music
|
||||
music = pyglet.media.load(music_file, streaming=True)
|
||||
# Check if the music is in the loaded sounds library
|
||||
if music_name in self.sounds:
|
||||
# Use the already loaded sound
|
||||
music = self.sounds[music_name]
|
||||
music_file = None
|
||||
else:
|
||||
# Try to load with extensions if not found
|
||||
for ext in ['.ogg', '.wav', '.opus']:
|
||||
if music_name.endswith(ext):
|
||||
music_file = f"sounds/{music_name}"
|
||||
break
|
||||
elif os.path.exists(f"sounds/{music_name}{ext}"):
|
||||
music_file = f"sounds/{music_name}{ext}"
|
||||
break
|
||||
|
||||
# If we didn't find a file, try the direct path
|
||||
if not music_file:
|
||||
music_file = f"sounds/{music_name}"
|
||||
if not os.path.exists(music_file):
|
||||
music_file += ".ogg" # Default to .ogg if no extension
|
||||
|
||||
# Load the music file
|
||||
music = pyglet.media.load(music_file, streaming=True)
|
||||
|
||||
# Create and configure player
|
||||
player = pyglet.media.Player()
|
||||
player.queue(music)
|
||||
player.loop = True
|
||||
player.volume = self.bgmVolume * self.masterVolume
|
||||
player.queue(music)
|
||||
player.play()
|
||||
player.on_eos = lambda: (player.queue(music), player.play())
|
||||
|
||||
# Store reference
|
||||
self.currentBgm = player
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error playing background music: {e}")
|
||||
|
||||
@ -97,13 +190,33 @@ class Sound:
|
||||
"""Play a sound effect with volume settings.
|
||||
|
||||
Args:
|
||||
soundName (str): Name of sound to play
|
||||
sound_name (str): Name of sound to play, can include subdirectory path
|
||||
e.g. "explosion" or "monsters/growl"
|
||||
volume (float): Base volume for sound (0.0-1.0)
|
||||
|
||||
Returns:
|
||||
pyglet.media.Player: Sound player object
|
||||
"""
|
||||
if soundName not in self.sounds:
|
||||
# Try adding .ogg extension for direct file paths
|
||||
if not soundName.endswith(('.wav', '.ogg', '.opus')):
|
||||
# Try to find the sound with various extensions
|
||||
for ext in ['.ogg', '.wav', '.opus']:
|
||||
testName = f"{soundName}{ext}"
|
||||
if os.path.exists(f"sounds/{testName}"):
|
||||
try:
|
||||
sound = pyglet.media.load(f"sounds/{testName}", streaming=False)
|
||||
player = pyglet.media.Player()
|
||||
player.queue(sound)
|
||||
player.volume = volume * self.sfxVolume * self.masterVolume
|
||||
player.play()
|
||||
|
||||
self.activeSounds.append(player)
|
||||
return player
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
print(f"Sound not found: {soundName}")
|
||||
return None
|
||||
|
||||
player = pyglet.media.Player()
|
||||
@ -114,16 +227,24 @@ class Sound:
|
||||
self.activeSounds.append(player)
|
||||
return player
|
||||
|
||||
def play_random(self, base_name, pause=False, interrupt=False):
|
||||
def play_random(self, baseName, pause=False, interrupt=False):
|
||||
"""Play random variation of a sound.
|
||||
|
||||
Args:
|
||||
base_name (str): Base name of sound
|
||||
baseName (str): Base name of sound, can include subdirectory
|
||||
pause (bool): Wait for sound to finish
|
||||
interrupt (bool): Stop other sounds
|
||||
"""
|
||||
# Check if baseName includes a directory
|
||||
if '/' in baseName:
|
||||
dirPart = os.path.dirname(baseName)
|
||||
namePart = os.path.basename(baseName)
|
||||
pattern = f"^{dirPart}/.*{namePart}.*"
|
||||
else:
|
||||
pattern = f"^{baseName}.*"
|
||||
|
||||
matches = [name for name in self.sounds.keys()
|
||||
if re.match(f"^{base_name}.*", name)]
|
||||
if re.match(pattern, name)]
|
||||
|
||||
if not matches:
|
||||
return None
|
||||
@ -169,25 +290,26 @@ class Sound:
|
||||
|
||||
return (x, y, z)
|
||||
|
||||
def play_positional(self, soundName, source_pos, listener_pos, mode='2d',
|
||||
direction=None, cone_angles=None):
|
||||
def play_positional(self, soundName, sourcePos, listenerPos, mode='2d',
|
||||
direction=None, coneAngles=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)
|
||||
soundName (str): Name of sound to play, can include subdirectory
|
||||
sourcePos: Position of sound source (float for 2D, tuple for 3D)
|
||||
listenerPos: 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
|
||||
coneAngles: Optional tuple (inner, outer) angles for sound cone
|
||||
|
||||
Returns:
|
||||
pyglet.media.Player: Sound player object
|
||||
"""
|
||||
if soundName not in self.sounds:
|
||||
print(f"Sound not found for positional audio: {soundName}")
|
||||
return None
|
||||
|
||||
position = self.calculate_positional_audio(source_pos, listener_pos, mode)
|
||||
position = self.calculate_positional_audio(sourcePos, listenerPos, mode)
|
||||
if position is None: # Too far to hear
|
||||
return None
|
||||
|
||||
@ -199,29 +321,29 @@ class Sound:
|
||||
# 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
|
||||
if coneAngles:
|
||||
player.cone_inner_angle, player.cone_outer_angle = coneAngles
|
||||
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',
|
||||
def update_positional(self, player, sourcePos, listenerPos, 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
|
||||
sourcePos: New source position
|
||||
listenerPos: 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)
|
||||
position = self.calculate_positional_audio(sourcePos, listenerPos, mode)
|
||||
if position is None:
|
||||
player.pause()
|
||||
return
|
||||
@ -231,18 +353,37 @@ class Sound:
|
||||
player.cone_orientation = direction
|
||||
|
||||
def cut_scene(self, soundName):
|
||||
"""Play a sound as a cut scene, stopping other sounds and waiting for completion."""
|
||||
"""Play a sound as a cut scene, stopping other sounds and waiting for completion.
|
||||
|
||||
Args:
|
||||
soundName (str): Name of sound to play, can include subdirectory
|
||||
"""
|
||||
# Stop all current sounds
|
||||
self.stop_all_sounds()
|
||||
if self.currentBgm:
|
||||
self.currentBgm.pause()
|
||||
|
||||
if soundName not in self.sounds:
|
||||
# Find all matching sound variations
|
||||
if '/' in soundName:
|
||||
dirPart = os.path.dirname(soundName)
|
||||
namePart = os.path.basename(soundName)
|
||||
pattern = f"^{dirPart}/.*{namePart}.*"
|
||||
else:
|
||||
pattern = f"^{soundName}.*"
|
||||
|
||||
matches = [name for name in self.sounds.keys()
|
||||
if re.match(pattern, name)]
|
||||
|
||||
if not matches:
|
||||
print(f"No matching sounds found for cut scene: {soundName}")
|
||||
return
|
||||
|
||||
# Pick a random variation
|
||||
selectedSound = random.choice(matches)
|
||||
|
||||
# Create and configure the player
|
||||
player = pyglet.media.Player()
|
||||
player.queue(self.sounds[soundName])
|
||||
player.queue(self.sounds[selectedSound])
|
||||
player.volume = self.sfxVolume * self.masterVolume
|
||||
|
||||
# Start playback
|
||||
@ -250,7 +391,7 @@ class Sound:
|
||||
|
||||
# Make sure to give pyglet enough cycles to start playing
|
||||
startTime = time.time()
|
||||
duration = self.sounds[soundName].duration
|
||||
duration = self.sounds[selectedSound].duration
|
||||
pyglet.clock.tick()
|
||||
|
||||
# Wait for completion or skip
|
||||
|
Reference in New Issue
Block a user