Compare commits
1 Commits
master
...
639198e8de
Author | SHA1 | Date | |
---|---|---|---|
639198e8de |
831
README.md
831
README.md
@ -1,829 +1,8 @@
|
|||||||
# libstormgames
|
# libstormgames
|
||||||
|
|
||||||
A Python library to make creating audio games easier.
|
Library to make writing audiogames easier.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
## Overview
|
configparser pyxdg pygame pyperclip requests setproctitle
|
||||||
|
### For mac os X and windows
|
||||||
`libstormgames` provides a comprehensive set of tools for developing accessible games with audio-first design. It handles common game development tasks including:
|
accessible_output2
|
||||||
|
|
||||||
- Sound and music playback with positional audio
|
|
||||||
- Text-to-speech integration
|
|
||||||
- Configuration management
|
|
||||||
- Score tracking and high score tables
|
|
||||||
- Input handling and keyboard controls
|
|
||||||
- Menu systems and text display
|
|
||||||
- GUI initialization
|
|
||||||
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
### Requirements
|
|
||||||
|
|
||||||
- pygame>=2.0.0
|
|
||||||
- pyperclip>=1.8.0
|
|
||||||
- requests>=2.25.0
|
|
||||||
- pyxdg>=0.27
|
|
||||||
- setproctitle>=1.2.0
|
|
||||||
- numpy>=1.19.0
|
|
||||||
- wxpython
|
|
||||||
|
|
||||||
#### Speech Providers (one required)
|
|
||||||
|
|
||||||
- Linux/Unix: `python-speechd` or `accessible-output2>=0.14`
|
|
||||||
- Windows/macOS: `accessible-output2>=0.14`
|
|
||||||
|
|
||||||
|
|
||||||
### Install from source
|
|
||||||
|
|
||||||
If you are on Linux, check your package manager first to see if the packages in requirements.txt are available.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://git.stormux.org/storm/libstormgames
|
|
||||||
cd libstormgames
|
|
||||||
pip install -e .
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## Getting Started
|
|
||||||
|
|
||||||
You can use libstormgames in two ways: the traditional function-based approach or the new class-based approach.
|
|
||||||
|
|
||||||
|
|
||||||
### Traditional Function-Based Approach
|
|
||||||
|
|
||||||
```python
|
|
||||||
#!/usr/bin/env python3
|
|
||||||
import libstormgames as sg
|
|
||||||
|
|
||||||
def main():
|
|
||||||
# Initialize the game
|
|
||||||
sounds = sg.initialize_gui("My First Audio Game")
|
|
||||||
|
|
||||||
# Welcome message
|
|
||||||
sg.speak("Welcome to My First Audio Game!")
|
|
||||||
|
|
||||||
# Create a scoreboard
|
|
||||||
scoreboard = sg.Scoreboard()
|
|
||||||
|
|
||||||
# Main game loop
|
|
||||||
def play_game():
|
|
||||||
sg.speak("Game started!")
|
|
||||||
scoreboard.increase_score(10)
|
|
||||||
sg.speak(f"Your score is {scoreboard.get_score()}")
|
|
||||||
scoreboard.add_high_score()
|
|
||||||
return "menu" # Return to menu after game ends
|
|
||||||
|
|
||||||
# Define menu options
|
|
||||||
while True:
|
|
||||||
choice = sg.game_menu(sounds)
|
|
||||||
if choice == "play":
|
|
||||||
play_game()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
```
|
|
||||||
|
|
||||||
### Modern Class-Based Approach
|
|
||||||
|
|
||||||
```python
|
|
||||||
#!/usr/bin/env python3
|
|
||||||
import libstormgames as sg
|
|
||||||
|
|
||||||
def main():
|
|
||||||
# Create and initialize a game
|
|
||||||
game = sg.Game("My First Audio Game").initialize()
|
|
||||||
|
|
||||||
# Welcome message
|
|
||||||
game.speak("Welcome to My First Audio Game!")
|
|
||||||
|
|
||||||
# Main game loop
|
|
||||||
def play_game():
|
|
||||||
game.speak("Game started!")
|
|
||||||
game.scoreboard.increase_score(10)
|
|
||||||
game.speak(f"Your score is {game.scoreboard.get_score()}")
|
|
||||||
game.scoreboard.add_high_score()
|
|
||||||
return "menu"
|
|
||||||
|
|
||||||
# Define menu options
|
|
||||||
while True:
|
|
||||||
choice = sg.game_menu(game.sound.get_sounds())
|
|
||||||
if choice == "play":
|
|
||||||
play_game()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## Library Structure
|
|
||||||
|
|
||||||
The library is organized into modules, each with a specific focus:
|
|
||||||
|
|
||||||
- **config**: Configuration management
|
|
||||||
- **services**: Core services that replace global variables
|
|
||||||
- **sound**: Sound and music playback
|
|
||||||
- **speech**: Text-to-speech functionality
|
|
||||||
- **scoreboard**: High score tracking
|
|
||||||
- **input**: Input handling and dialogs
|
|
||||||
- **display**: Text display and GUI functionality
|
|
||||||
- **menu**: Menu systems
|
|
||||||
- **utils**: Utility functions and Game class
|
|
||||||
|
|
||||||
|
|
||||||
## Core Classes
|
|
||||||
|
|
||||||
### Game
|
|
||||||
|
|
||||||
The Game class provides a central way to manage all game systems:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Create and initialize a game
|
|
||||||
game = sg.Game("My Game").initialize()
|
|
||||||
|
|
||||||
# Use fluent API for chaining commands
|
|
||||||
game.speak("Hello").play_bgm("music/theme")
|
|
||||||
|
|
||||||
# Access components directly
|
|
||||||
game.scoreboard.increase_score(10)
|
|
||||||
game.sound.play_random("explosion")
|
|
||||||
|
|
||||||
# Display text
|
|
||||||
|
|
||||||
game.display_text(["Line 1", "Line 2"])
|
|
||||||
|
|
||||||
# Clean exit
|
|
||||||
|
|
||||||
game.exit()
|
|
||||||
```
|
|
||||||
|
|
||||||
The initialization process sets up proper configuration paths:
|
|
||||||
- Creates game-specific directories if they don't exist
|
|
||||||
- Ensures all services have access to correct paths
|
|
||||||
- Connects all components for seamless operation
|
|
||||||
|
|
||||||
|
|
||||||
### Services
|
|
||||||
|
|
||||||
The library includes several service classes that replace global variables:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Volume service manages all volume settings
|
|
||||||
volume = sg.VolumeService.get_instance()
|
|
||||||
volume.adjust_master_volume(0.1)
|
|
||||||
|
|
||||||
# Path service manages file paths
|
|
||||||
paths = sg.PathService.get_instance()
|
|
||||||
print(paths.game_path)
|
|
||||||
|
|
||||||
# Config service manages configuration
|
|
||||||
config = sg.ConfigService.get_instance()
|
|
||||||
```
|
|
||||||
|
|
||||||
### Config
|
|
||||||
|
|
||||||
Handles configuration file management with local and global settings.
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Create a configuration manager
|
|
||||||
config = sg.Config("My Game")
|
|
||||||
|
|
||||||
# Access the local configuration
|
|
||||||
config.local_config.add_section("settings")
|
|
||||||
config.local_config.set("settings", "difficulty", "easy")
|
|
||||||
config.write_local_config()
|
|
||||||
|
|
||||||
# Read settings
|
|
||||||
config.read_local_config()
|
|
||||||
difficulty = config.local_config.get("settings", "difficulty")
|
|
||||||
```
|
|
||||||
|
|
||||||
Configuration files are automatically stored in standard system locations:
|
|
||||||
- Linux/Unix: `~/.config/storm-games/`
|
|
||||||
- Windows: `%APPDATA%\storm-games\`
|
|
||||||
- macOS: `~/Library/Application Support/storm-games/`
|
|
||||||
|
|
||||||
Each game has its own subdirectory based on the game name (lowercase with hyphens), for example, Wicked Quest becomes wicked-quest.
|
|
||||||
|
|
||||||
|
|
||||||
### Sound
|
|
||||||
|
|
||||||
Manages sound loading and playback with positional audio support.
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Create a sound manager
|
|
||||||
sound_system = sg.Sound("sounds/")
|
|
||||||
|
|
||||||
# Get the dictionary of loaded sounds
|
|
||||||
sounds = sound_system.get_sounds()
|
|
||||||
|
|
||||||
# Play a sound
|
|
||||||
sg.play_sound(sounds["explosion"])
|
|
||||||
|
|
||||||
# Play a sound with positional audio (player at x=5, object at x=10)
|
|
||||||
channel = sg.obj_play(sounds, "footsteps", 5, 10)
|
|
||||||
|
|
||||||
# Update sound position as player or object moves
|
|
||||||
channel = sg.obj_update(channel, 6, 10) # Player moved to x=6
|
|
||||||
|
|
||||||
# Stop the sound
|
|
||||||
channel = sg.obj_stop(channel)
|
|
||||||
|
|
||||||
# Play background music
|
|
||||||
sg.play_bgm("sounds/background.ogg")
|
|
||||||
|
|
||||||
# Adjust volume
|
|
||||||
sg.adjust_master_volume(0.1) # Increase master volume
|
|
||||||
sg.adjust_bgm_volume(-0.1) # Decrease background music volume
|
|
||||||
sg.adjust_sfx_volume(0.1) # Increase sound effects volume
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
### Speech
|
|
||||||
|
|
||||||
Provides text-to-speech functionality using available speech providers.
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Create a speech manager (usually you'll use the global instance)
|
|
||||||
speech = sg.Speech()
|
|
||||||
|
|
||||||
# Speak text
|
|
||||||
speech.speak("Hello, world!")
|
|
||||||
|
|
||||||
# Or use the global function for convenience
|
|
||||||
sg.speak("Hello, world!")
|
|
||||||
|
|
||||||
# Speak without interrupting previous speech
|
|
||||||
sg.speak("This won't interrupt", interrupt=False)
|
|
||||||
|
|
||||||
# Clean up when done
|
|
||||||
exit_game() or as the class, game.exit_game()
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
### Scoreboard
|
|
||||||
|
|
||||||
Tracks scores and manages high score tables.
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Create a scoreboard
|
|
||||||
scoreboard = sg.Scoreboard()
|
|
||||||
|
|
||||||
# Manipulate score
|
|
||||||
scoreboard.increase_score(10)
|
|
||||||
scoreboard.decrease_score(5)
|
|
||||||
current_score = scoreboard.get_score()
|
|
||||||
|
|
||||||
# Check for high score
|
|
||||||
position = scoreboard.check_high_score()
|
|
||||||
if position:
|
|
||||||
print(f"You're in position {position}!")
|
|
||||||
|
|
||||||
# Add high score (prompts for player name)
|
|
||||||
scoreboard.add_high_score()
|
|
||||||
|
|
||||||
# Get all high scores
|
|
||||||
high_scores = scoreboard.get_high_scores()
|
|
||||||
for entry in high_scores:
|
|
||||||
print(f"{entry['name']}: {entry['score']}")
|
|
||||||
```
|
|
||||||
|
|
||||||
The Scoreboard system automatically manages high score data in the game's configuration directory:
|
|
||||||
- Linux/Unix: `~/.config/storm-games/game-name/config.ini`
|
|
||||||
- Windows: `%APPDATA%\storm-games\game-name\config.ini`
|
|
||||||
- macOS: `~/Library/Application Support/storm-games/game-name/config.ini`
|
|
||||||
|
|
||||||
Where `game-name` is the lowercase, hyphenated version of your game title. For example,
|
|
||||||
"My Awesome Game" would use the directory `my-awesome-game`.
|
|
||||||
|
|
||||||
|
|
||||||
## Key Functions
|
|
||||||
|
|
||||||
### Game Initialization and Control
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Initialize game systems (traditional approach)
|
|
||||||
sounds = sg.initialize_gui("Game Title")
|
|
||||||
|
|
||||||
# Or use the Game class (modern approach)
|
|
||||||
game = sg.Game("Game Title").initialize()
|
|
||||||
|
|
||||||
# Pause the game (freezes until user presses backspace)
|
|
||||||
sg.pause_game()
|
|
||||||
|
|
||||||
# Check if user wants to exit
|
|
||||||
if sg.check_for_exit():
|
|
||||||
sg.exit_game()
|
|
||||||
|
|
||||||
# Exit the game properly
|
|
||||||
sg.exit_game()
|
|
||||||
```
|
|
||||||
|
|
||||||
### Menu System
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Basic menu example (returns option string)
|
|
||||||
choice = sg.game_menu(sounds)
|
|
||||||
if choice == "play":
|
|
||||||
# Start the game
|
|
||||||
play_game()
|
|
||||||
elif choice == "instructions":
|
|
||||||
sg.instructions()
|
|
||||||
# etc...
|
|
||||||
|
|
||||||
# Menu with play callback (callback is executed directly)
|
|
||||||
def play_callback():
|
|
||||||
sg.speak("Game started with callback!")
|
|
||||||
# Game code goes here
|
|
||||||
|
|
||||||
sg.game_menu(sounds, play_callback)
|
|
||||||
|
|
||||||
# Menu with custom options
|
|
||||||
choice = sg.game_menu(sounds, None, "practice_mode")
|
|
||||||
if choice == "practice_mode":
|
|
||||||
# Handle practice mode
|
|
||||||
practice_game()
|
|
||||||
elif choice == "difficulty":
|
|
||||||
# Handle difficulty settings
|
|
||||||
set_difficulty()
|
|
||||||
elif choice == "options":
|
|
||||||
# Handle options screen
|
|
||||||
show_options()
|
|
||||||
# etc...
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
### Complete Menu Example
|
|
||||||
|
|
||||||
```python
|
|
||||||
import libstormgames as sg
|
|
||||||
import pygame
|
|
||||||
|
|
||||||
def main():
|
|
||||||
# Initialize the game
|
|
||||||
sounds = sg.initialize_gui("Menu Example Game")
|
|
||||||
|
|
||||||
# Play menu music
|
|
||||||
sg.play_bgm("sounds/music_menu.ogg")
|
|
||||||
|
|
||||||
# Define play function with callback
|
|
||||||
def play_game():
|
|
||||||
sg.speak("Starting game!")
|
|
||||||
# Game code here
|
|
||||||
sg.speak("Game over!")
|
|
||||||
return "menu" # Return to menu after game ends
|
|
||||||
|
|
||||||
# Define custom menu option handlers
|
|
||||||
def practice_mode():
|
|
||||||
sg.speak("Starting practice mode!")
|
|
||||||
# Practice mode code here
|
|
||||||
return "menu"
|
|
||||||
|
|
||||||
def difficulty_settings():
|
|
||||||
# Show difficulty options
|
|
||||||
options = ["easy", "normal", "hard", "back"]
|
|
||||||
current = 0
|
|
||||||
|
|
||||||
while True:
|
|
||||||
sg.speak(options[current])
|
|
||||||
|
|
||||||
event = pygame.event.wait()
|
|
||||||
if event.type == pygame.KEYDOWN:
|
|
||||||
if event.key == pygame.K_UP and current > 0:
|
|
||||||
current -= 1
|
|
||||||
elif event.key == pygame.K_DOWN and current < len(options) - 1:
|
|
||||||
current += 1
|
|
||||||
elif event.key == pygame.K_RETURN:
|
|
||||||
if options[current] == "back":
|
|
||||||
return "menu"
|
|
||||||
sg.speak(f"Difficulty set to {options[current]}")
|
|
||||||
return "menu"
|
|
||||||
elif event.key == pygame.K_ESCAPE:
|
|
||||||
return "menu"
|
|
||||||
|
|
||||||
# Main menu loop
|
|
||||||
while True:
|
|
||||||
# Display menu with play callback and custom options
|
|
||||||
choice = sg.game_menu(sounds, play_game, "practice_mode", "difficulty")
|
|
||||||
|
|
||||||
# Handle menu choices
|
|
||||||
if choice == "practice_mode":
|
|
||||||
practice_mode()
|
|
||||||
elif choice == "difficulty":
|
|
||||||
difficulty_settings()
|
|
||||||
# Other options are handled automatically by game_menu
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
### Class-Based Menu Example
|
|
||||||
|
|
||||||
```python
|
|
||||||
import libstormgames as sg
|
|
||||||
import pygame
|
|
||||||
|
|
||||||
class MyGame:
|
|
||||||
def __init__(self):
|
|
||||||
# Create and initialize game
|
|
||||||
self.game = sg.Game("Class-Based Menu Example").initialize()
|
|
||||||
self.sounds = self.game.sound.get_sounds()
|
|
||||||
|
|
||||||
def play_game(self):
|
|
||||||
self.game.speak("Starting game!")
|
|
||||||
# Game code here
|
|
||||||
self.game.speak("Game over!")
|
|
||||||
return "menu"
|
|
||||||
|
|
||||||
def practice_mode(self):
|
|
||||||
self.game.speak("Starting practice mode!")
|
|
||||||
# Practice mode code here
|
|
||||||
return "menu"
|
|
||||||
|
|
||||||
def settings(self):
|
|
||||||
# A nested menu example
|
|
||||||
submenu_options = ["graphics", "audio", "controls", "back"]
|
|
||||||
current = 0
|
|
||||||
|
|
||||||
while True:
|
|
||||||
self.game.speak(submenu_options[current])
|
|
||||||
|
|
||||||
event = pygame.event.wait()
|
|
||||||
if event.type == pygame.KEYDOWN:
|
|
||||||
if event.key == pygame.K_UP and current > 0:
|
|
||||||
current -= 1
|
|
||||||
elif event.key == pygame.K_DOWN and current < len(submenu_options) - 1:
|
|
||||||
current += 1
|
|
||||||
elif event.key == pygame.K_RETURN:
|
|
||||||
if submenu_options[current] == "back":
|
|
||||||
return "menu"
|
|
||||||
self.game.speak(f"Selected {submenu_options[current]} settings")
|
|
||||||
return "menu"
|
|
||||||
elif event.key == pygame.K_ESCAPE:
|
|
||||||
return "menu"
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
# Main menu loop
|
|
||||||
while True:
|
|
||||||
# Use playCallback parameter to directly call play_game when "play" is selected
|
|
||||||
choice = sg.game_menu(self.sounds, None, "practice_mode",
|
|
||||||
"settings")
|
|
||||||
|
|
||||||
# Handle other menu options
|
|
||||||
if choice == "practice_mode":
|
|
||||||
self.practice_mode()
|
|
||||||
elif choice == "settings":
|
|
||||||
self.settings()
|
|
||||||
|
|
||||||
# Run the game
|
|
||||||
if __name__ == "__main__":
|
|
||||||
game = MyGame()
|
|
||||||
game.run()
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
### Text Display
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Display text with navigation
|
|
||||||
sg.display_text([
|
|
||||||
"Line 1 of instructions",
|
|
||||||
"Line 2 of instructions",
|
|
||||||
"Line 3 of instructions"
|
|
||||||
])
|
|
||||||
|
|
||||||
# Display a simple message box
|
|
||||||
sg.messagebox("Game Over! You scored 100 points.")
|
|
||||||
|
|
||||||
# Get text input from user
|
|
||||||
name = sg.get_input("Enter your name:", "Player")
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
### Sound Effects
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Play a random variation of a sound
|
|
||||||
sg.play_random(sounds, "explosion")
|
|
||||||
|
|
||||||
# Play positional sound with distance-based volume
|
|
||||||
sg.play_random_positional(sounds, "footsteps", player_x=5, object_x=10)
|
|
||||||
|
|
||||||
# Play directional sound (simplified left/right positioning)
|
|
||||||
sg.play_directional_sound(sounds, "voice", player_x=5, object_x=10)
|
|
||||||
|
|
||||||
# Play a sound as a cutscene (interrupts other sounds, waits until complete)
|
|
||||||
sg.cut_scene(sounds, "intro_speech")
|
|
||||||
|
|
||||||
# Play or update a falling sound
|
|
||||||
channel = sg.play_random_falling(sounds, "rock", player_x=5, object_x=8, start_y=10, currentY=5)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Utility Functions
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Check for game updates
|
|
||||||
update_info = sg.check_for_updates("1.0.0", "My Game", "https://example.com/version.json")
|
|
||||||
|
|
||||||
# Check compatibility with library version
|
|
||||||
is_compatible = sg.check_compatibility("1.0.0", "1.2.3")
|
|
||||||
|
|
||||||
# Sanitize a filename for any OS
|
|
||||||
safe_name = sg.sanitize_filename("User's File.txt")
|
|
||||||
|
|
||||||
# Calculate distance between points
|
|
||||||
distance = sg.distance_2d(x1=5, y1=10, x2=8, y2=15)
|
|
||||||
|
|
||||||
# Interpolation functions
|
|
||||||
mid_value = sg.lerp(start=0, end=10, factor=0.5) # Returns 5.0
|
|
||||||
smooth_value = sg.smooth_step(edge0=0, edge1=10, x=5) # Smooth transition
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## Advanced Examples
|
|
||||||
|
|
||||||
### Using the Game Class (Modern Approach)
|
|
||||||
|
|
||||||
```python
|
|
||||||
import libstormgames as sg
|
|
||||||
import pygame
|
|
||||||
import random
|
|
||||||
|
|
||||||
def main():
|
|
||||||
# Create and initialize the game
|
|
||||||
game = sg.Game("My Advanced Game").initialize()
|
|
||||||
|
|
||||||
# Set up game environment
|
|
||||||
game.play_bgm("sounds/background.ogg")
|
|
||||||
|
|
||||||
# Main game loop
|
|
||||||
running = True
|
|
||||||
player_x = 5
|
|
||||||
|
|
||||||
while running:
|
|
||||||
# Process events
|
|
||||||
for event in pygame.event.get():
|
|
||||||
if event.type == pygame.KEYDOWN:
|
|
||||||
if event.key == pygame.K_ESCAPE:
|
|
||||||
running = False
|
|
||||||
elif event.key == pygame.K_SPACE:
|
|
||||||
# Score points
|
|
||||||
game.scoreboard.increase_score(5)
|
|
||||||
game.speak(f"Score: {game.scoreboard.get_score()}")
|
|
||||||
|
|
||||||
# Update game state
|
|
||||||
player_x += random.uniform(-0.2, 0.2)
|
|
||||||
|
|
||||||
# Add random ambient sounds
|
|
||||||
if random.random() < 0.05:
|
|
||||||
sounds = game.sound.get_sounds()
|
|
||||||
if "ambient" in sounds:
|
|
||||||
sg.play_random_positional(sounds, "ambient", player_x,
|
|
||||||
player_x + random.uniform(-5, 5))
|
|
||||||
|
|
||||||
pygame.time.delay(50)
|
|
||||||
|
|
||||||
# Game over and cleanup
|
|
||||||
game.speak("Game over!")
|
|
||||||
game.exit()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
### Complex Sound Environment
|
|
||||||
|
|
||||||
```python
|
|
||||||
import libstormgames as sg
|
|
||||||
import time
|
|
||||||
import random
|
|
||||||
|
|
||||||
def create_sound_environment(player_x, player_y):
|
|
||||||
sounds = sg.initialize_gui("Sound Environment Demo")
|
|
||||||
|
|
||||||
# Place sound sources
|
|
||||||
water_x, water_y = 10, 5
|
|
||||||
fire_x, fire_y = 15, 8
|
|
||||||
wind_x, wind_y = 3, 12
|
|
||||||
|
|
||||||
# Play ambient sounds
|
|
||||||
water_channel = sg.obj_play(sounds, "water", player_x, water_x)
|
|
||||||
fire_channel = sg.obj_play(sounds, "fire", player_x, fire_x)
|
|
||||||
wind_channel = sg.obj_play(sounds, "wind", player_x, wind_x)
|
|
||||||
|
|
||||||
# Main loop
|
|
||||||
running = True
|
|
||||||
while running:
|
|
||||||
# Simulate player movement
|
|
||||||
player_x += random.uniform(-0.5, 0.5)
|
|
||||||
player_y += random.uniform(-0.5, 0.5)
|
|
||||||
|
|
||||||
# Update sound positions
|
|
||||||
water_channel = sg.obj_update(water_channel, player_x, water_x)
|
|
||||||
fire_channel = sg.obj_update(fire_channel, player_x, fire_x)
|
|
||||||
wind_channel = sg.obj_update(wind_channel, player_x, wind_x)
|
|
||||||
|
|
||||||
# Occasionally play random sound
|
|
||||||
if random.random() < 0.1:
|
|
||||||
sg.play_random_positional(sounds, "creature", player_x,
|
|
||||||
player_x + random.uniform(-5, 5))
|
|
||||||
|
|
||||||
# Check for exit
|
|
||||||
if sg.check_for_exit():
|
|
||||||
running = False
|
|
||||||
|
|
||||||
time.sleep(0.1)
|
|
||||||
|
|
||||||
# Clean up
|
|
||||||
sg.obj_stop(water_channel)
|
|
||||||
sg.obj_stop(fire_channel)
|
|
||||||
sg.obj_stop(wind_channel)
|
|
||||||
sg.exit_game()
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
### Complete Game Structure with Class-Based Architecture
|
|
||||||
|
|
||||||
```python
|
|
||||||
import libstormgames as sg
|
|
||||||
import pygame
|
|
||||||
import random
|
|
||||||
|
|
||||||
class MyGame:
|
|
||||||
def __init__(self):
|
|
||||||
# Create a Game instance that manages all subsystems
|
|
||||||
self.game = sg.Game("My Advanced Game").initialize()
|
|
||||||
|
|
||||||
# Game state
|
|
||||||
self.player_x = 5
|
|
||||||
self.player_y = 5
|
|
||||||
self.difficulty = "normal"
|
|
||||||
|
|
||||||
# Load settings
|
|
||||||
try:
|
|
||||||
self.difficulty = self.game.config_service.local_config.get("settings", "difficulty")
|
|
||||||
except:
|
|
||||||
self.game.config_service.local_config.add_section("settings")
|
|
||||||
self.game.config_service.local_config.set("settings", "difficulty", "normal")
|
|
||||||
self.game.config_service.write_local_config()
|
|
||||||
|
|
||||||
def play_game(self):
|
|
||||||
self.game.speak(f"Starting game on {self.difficulty} difficulty")
|
|
||||||
self.game.play_bgm("sounds/game_music.ogg")
|
|
||||||
|
|
||||||
# Game loop
|
|
||||||
running = True
|
|
||||||
while running:
|
|
||||||
# Update game state
|
|
||||||
self.player_x += random.uniform(-0.2, 0.2)
|
|
||||||
|
|
||||||
# Handle input
|
|
||||||
for event in pygame.event.get():
|
|
||||||
if event.type == pygame.KEYDOWN:
|
|
||||||
if event.key == pygame.K_ESCAPE:
|
|
||||||
running = False
|
|
||||||
elif event.key == pygame.K_SPACE:
|
|
||||||
self.game.scoreboard.increase_score()
|
|
||||||
self.game.speak(f"Score: {self.game.scoreboard.get_score()}")
|
|
||||||
|
|
||||||
# Add some random sounds
|
|
||||||
if random.random() < 0.05:
|
|
||||||
sounds = self.game.sound.get_sounds()
|
|
||||||
if "ambient" in sounds:
|
|
||||||
sg.play_random_positional(sounds, "ambient",
|
|
||||||
self.player_x, self.player_x + random.uniform(-10, 10))
|
|
||||||
|
|
||||||
pygame.time.delay(50)
|
|
||||||
|
|
||||||
# Game over
|
|
||||||
position = self.game.scoreboard.check_high_score()
|
|
||||||
if position:
|
|
||||||
self.game.speak(f"New high score! Position {position}")
|
|
||||||
self.game.scoreboard.add_high_score()
|
|
||||||
|
|
||||||
return "menu"
|
|
||||||
|
|
||||||
def settings(self):
|
|
||||||
options = ["easy", "normal", "hard", "back"]
|
|
||||||
current = options.index(self.difficulty) if self.difficulty in options else 1
|
|
||||||
|
|
||||||
while True:
|
|
||||||
self.game.speak(f"Current difficulty: {options[current]}")
|
|
||||||
|
|
||||||
# Wait for input
|
|
||||||
event = pygame.event.wait()
|
|
||||||
if event.type == pygame.KEYDOWN:
|
|
||||||
if event.key == pygame.K_UP and current > 0:
|
|
||||||
current -= 1
|
|
||||||
elif event.key == pygame.K_DOWN and current < len(options) - 1:
|
|
||||||
current += 1
|
|
||||||
elif event.key == pygame.K_RETURN:
|
|
||||||
if options[current] == "back":
|
|
||||||
return
|
|
||||||
self.difficulty = options[current]
|
|
||||||
self.game.config_service.local_config.set("settings", "difficulty", self.difficulty)
|
|
||||||
self.game.config_service.write_local_config()
|
|
||||||
self.game.speak(f"Difficulty set to {self.difficulty}")
|
|
||||||
return
|
|
||||||
elif event.key == pygame.K_ESCAPE:
|
|
||||||
return
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
# Main menu loop
|
|
||||||
while True:
|
|
||||||
sounds = self.game.sound.get_sounds()
|
|
||||||
choice = sg.game_menu(sounds, self.play_game, "settings",
|
|
||||||
"instructions", "credits", "donate", "exit_game")
|
|
||||||
|
|
||||||
if choice == "settings":
|
|
||||||
self.settings()
|
|
||||||
elif choice == "instructions":
|
|
||||||
sg.instructions()
|
|
||||||
elif choice == "credits":
|
|
||||||
sg.credits()
|
|
||||||
elif choice == "donate":
|
|
||||||
sg.donate()
|
|
||||||
elif choice == "exit_game":
|
|
||||||
self.game.exit()
|
|
||||||
|
|
||||||
# Run the game
|
|
||||||
if __name__ == "__main__":
|
|
||||||
game = MyGame()
|
|
||||||
game.run()
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## Best Practices
|
|
||||||
|
|
||||||
1. **Modern vs Traditional Approach**:
|
|
||||||
- New projects: Use the Game class for better organization
|
|
||||||
- Existing projects: Continue with global functions for compatibility
|
|
||||||
- Both approaches are fully supported
|
|
||||||
|
|
||||||
2. **Always clean up resources**:
|
|
||||||
- Use `exit_game()` or `game.exit()` when exiting to ensure proper cleanup
|
|
||||||
- Stop sounds that are no longer needed
|
|
||||||
|
|
||||||
3. **Volume control**:
|
|
||||||
- Implement the Alt+key volume controls in your game
|
|
||||||
- Use volume services for better control
|
|
||||||
|
|
||||||
4. **Configuration**:
|
|
||||||
- Save user preferences using the Config class
|
|
||||||
- Load settings at startup
|
|
||||||
|
|
||||||
5. **Path initialization**:
|
|
||||||
- Always initialize the framework with a proper game title
|
|
||||||
- Game title is used to determine configuration directory paths
|
|
||||||
- Services are interconnected, so proper initialization ensures correct operation
|
|
||||||
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### No Sound
|
|
||||||
|
|
||||||
- Ensure pygame mixer is properly initialized
|
|
||||||
- Check if sound files exist in the correct directory
|
|
||||||
- Verify file formats (OGG and WAV are supported)
|
|
||||||
|
|
||||||
### No Speech
|
|
||||||
|
|
||||||
- Make sure at least one speech provider is installed
|
|
||||||
- Linux/Unix: `python-speechd` or `accessible-output2`
|
|
||||||
- Windows/macOS: `accessible-output2`
|
|
||||||
- Check if pygame display is initialized properly
|
|
||||||
|
|
||||||
### Input Issues
|
|
||||||
|
|
||||||
- Ensure pygame is properly handling events
|
|
||||||
- Check event loop for proper event handling
|
|
||||||
|
|
||||||
### Scoreboard/Configuration Issues
|
|
||||||
|
|
||||||
- Check if path services are properly initialized with a game title
|
|
||||||
- Verify write permissions in the configuration directory
|
|
||||||
- For debugging, use the following code to view path configuration:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from libstormgames.services import PathService, ConfigService
|
|
||||||
|
|
||||||
# Print path information
|
|
||||||
path_service = PathService.get_instance()
|
|
||||||
print(f"Game Name: {path_service.gameName}")
|
|
||||||
print(f"Game Path: {path_service.gamePath}")
|
|
||||||
|
|
||||||
# Check config service connection
|
|
||||||
config_service = ConfigService.get_instance()
|
|
||||||
print(f"Config connected to path: {hasattr(config_service, 'pathService')}")
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## Contributing
|
|
||||||
|
|
||||||
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
This project is licensed under the GPL v3 License - see the LICENSE file for details.
|
|
||||||
|
594
__init__.py
594
__init__.py
@ -1,228 +1,380 @@
|
|||||||
#!/usr/bin/env python3
|
#!/bin/python
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""Standard initializations and functions shared by all Storm Games.
|
"""Standard initializations and functions shared by all games."""
|
||||||
|
|
||||||
This module provides core functionality for Storm Games including:
|
from sys import exit
|
||||||
- Sound and speech handling
|
import configparser
|
||||||
- Volume controls
|
import os
|
||||||
- Configuration management
|
from os import listdir
|
||||||
- Score tracking
|
from os.path import isfile, join
|
||||||
- GUI initialization
|
from inspect import isfunction
|
||||||
- Game menu systems
|
from xdg import BaseDirectory
|
||||||
"""
|
from setproctitle import setproctitle
|
||||||
|
import pyglet
|
||||||
# Import service classes
|
import pyperclip
|
||||||
from .services import (
|
|
||||||
ConfigService,
|
|
||||||
VolumeService,
|
|
||||||
PathService
|
|
||||||
)
|
|
||||||
|
|
||||||
# Import Sound class and functions
|
|
||||||
from .sound import (
|
|
||||||
Sound,
|
|
||||||
play_bgm,
|
|
||||||
play_sound,
|
|
||||||
adjust_bgm_volume,
|
|
||||||
adjust_sfx_volume,
|
|
||||||
adjust_master_volume,
|
|
||||||
play_ambiance,
|
|
||||||
play_random,
|
|
||||||
play_random_positional,
|
|
||||||
play_directional_sound,
|
|
||||||
obj_play,
|
|
||||||
obj_update,
|
|
||||||
obj_stop,
|
|
||||||
cut_scene,
|
|
||||||
play_random_falling,
|
|
||||||
calculate_volume_and_pan
|
|
||||||
)
|
|
||||||
|
|
||||||
# Import Speech class and functions
|
|
||||||
from .speech import messagebox, speak, Speech
|
|
||||||
|
|
||||||
# Import Scoreboard
|
|
||||||
from .scoreboard import Scoreboard
|
|
||||||
|
|
||||||
# Import input functions
|
|
||||||
from .input import get_input, check_for_exit, pause_game
|
|
||||||
|
|
||||||
# Import display functions
|
|
||||||
from .display import display_text, initialize_gui
|
|
||||||
|
|
||||||
# Import menu functions
|
|
||||||
from .menu import game_menu, learn_sounds, instructions, credits, donate, exit_game
|
|
||||||
|
|
||||||
# Update imports to reference Scoreboard methods
|
|
||||||
high_scores = Scoreboard.display_high_scores
|
|
||||||
has_high_scores = Scoreboard.has_high_scores
|
|
||||||
|
|
||||||
# Import utility functions and Game class
|
|
||||||
from .utils import (
|
|
||||||
Game,
|
|
||||||
check_for_updates,
|
|
||||||
get_version_tuple,
|
|
||||||
check_compatibility,
|
|
||||||
sanitize_filename,
|
|
||||||
lerp,
|
|
||||||
smooth_step,
|
|
||||||
distance_2d,
|
|
||||||
x_powerbar,
|
|
||||||
y_powerbar,
|
|
||||||
generate_tone
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
__version__ = '2.0.0'
|
|
||||||
|
|
||||||
# Make all symbols available at the package level
|
|
||||||
__all__ = [
|
|
||||||
# Services
|
|
||||||
'ConfigService', 'VolumeService', 'PathService',
|
|
||||||
|
|
||||||
# Sound
|
|
||||||
'Sound',
|
|
||||||
'play_bgm',
|
|
||||||
'play_sound',
|
|
||||||
'adjust_bgm_volume',
|
|
||||||
'adjust_sfx_volume',
|
|
||||||
'adjust_master_volume',
|
|
||||||
'play_ambiance',
|
|
||||||
'play_random',
|
|
||||||
'play_random_positional',
|
|
||||||
'play_directional_sound',
|
|
||||||
'obj_play',
|
|
||||||
'obj_update',
|
|
||||||
'obj_stop',
|
|
||||||
'cut_scene',
|
|
||||||
'play_random_falling',
|
|
||||||
'calculate_volume_and_pan',
|
|
||||||
|
|
||||||
# Speech
|
|
||||||
'messagebox',
|
|
||||||
'speak',
|
|
||||||
'Speech',
|
|
||||||
|
|
||||||
# Scoreboard
|
|
||||||
'Scoreboard',
|
|
||||||
|
|
||||||
# Input
|
|
||||||
'get_input', 'check_for_exit', 'pause_game',
|
|
||||||
|
|
||||||
# Display
|
|
||||||
'display_text', 'initialize_gui',
|
|
||||||
|
|
||||||
# Menu
|
|
||||||
'game_menu', 'learn_sounds', 'instructions', 'credits', 'donate', 'exit_game', 'high_scores', 'has_high_scores',
|
|
||||||
|
|
||||||
# Game class
|
|
||||||
'Game',
|
|
||||||
|
|
||||||
# Utils
|
|
||||||
'check_for_updates', 'get_version_tuple', 'check_compatibility',
|
|
||||||
'sanitize_filename', 'lerp', 'smooth_step', 'distance_2d',
|
|
||||||
'x_powerbar', 'y_powerbar', 'generate_tone',
|
|
||||||
|
|
||||||
# Re-exported functions from pygame, math, random
|
|
||||||
'get_ticks', 'delay', 'wait',
|
|
||||||
'sin', 'cos', 'sqrt', 'floor', 'ceil',
|
|
||||||
'randint', 'choice', 'uniform', 'seed'
|
|
||||||
]
|
|
||||||
|
|
||||||
# Create global instances for backward compatibility
|
|
||||||
configService = ConfigService.get_instance()
|
|
||||||
volumeService = VolumeService.get_instance()
|
|
||||||
pathService = PathService.get_instance()
|
|
||||||
|
|
||||||
# Set up backward compatibility hooks for initialize_gui
|
|
||||||
_originalInitializeGui = initialize_gui
|
|
||||||
|
|
||||||
def initialize_gui_with_services(gameTitle):
|
|
||||||
"""Wrapper around initialize_gui that initializes services."""
|
|
||||||
# Initialize path service
|
|
||||||
pathService.initialize(gameTitle)
|
|
||||||
|
|
||||||
# Connect config service to path service
|
|
||||||
configService.set_game_info(gameTitle, pathService)
|
|
||||||
|
|
||||||
# Call original initialize_gui
|
|
||||||
return _originalInitializeGui(gameTitle)
|
|
||||||
|
|
||||||
# Replace initialize_gui with the wrapped version
|
|
||||||
initialize_gui = initialize_gui_with_services
|
|
||||||
|
|
||||||
# Initialize global scoreboard constructor
|
|
||||||
_originalScoreboardInit = Scoreboard.__init__
|
|
||||||
|
|
||||||
def scoreboard_init_with_services(self, score=0, configService=None, speech=None):
|
|
||||||
"""Wrapper around Scoreboard.__init__ that ensures services are initialized."""
|
|
||||||
# Use global services if not specified
|
|
||||||
if configService is None:
|
|
||||||
configService = ConfigService.get_instance()
|
|
||||||
|
|
||||||
# Ensure pathService is connected if using defaults
|
|
||||||
if not hasattr(configService, 'pathService') and pathService.game_path is not None:
|
|
||||||
configService.pathService = pathService
|
|
||||||
|
|
||||||
# Call original init with services
|
|
||||||
_originalScoreboardInit(self, score, configService, speech)
|
|
||||||
|
|
||||||
# Replace Scoreboard.__init__ with the wrapped version
|
|
||||||
Scoreboard.__init__ = scoreboard_init_with_services
|
|
||||||
|
|
||||||
# Re-export pygame time functions for backward compatibility
|
|
||||||
import pygame.time
|
|
||||||
|
|
||||||
def get_ticks():
|
|
||||||
"""Get the number of milliseconds since pygame.init() was called."""
|
|
||||||
return pygame.time.get_ticks()
|
|
||||||
|
|
||||||
def delay(milliseconds):
|
|
||||||
"""Pause the program for a given number of milliseconds."""
|
|
||||||
return pygame.time.delay(milliseconds)
|
|
||||||
|
|
||||||
def wait(milliseconds):
|
|
||||||
"""Pause the program for a given number of milliseconds."""
|
|
||||||
return pygame.time.wait(milliseconds)
|
|
||||||
|
|
||||||
# Re-export math functions that might be used
|
|
||||||
import math
|
|
||||||
|
|
||||||
def sin(x):
|
|
||||||
"""Return the sine of x radians."""
|
|
||||||
return math.sin(x)
|
|
||||||
|
|
||||||
def cos(x):
|
|
||||||
"""Return the cosine of x radians."""
|
|
||||||
return math.cos(x)
|
|
||||||
|
|
||||||
def sqrt(x):
|
|
||||||
"""Return the square root of x."""
|
|
||||||
return math.sqrt(x)
|
|
||||||
|
|
||||||
def floor(x):
|
|
||||||
"""Return the floor of x."""
|
|
||||||
return math.floor(x)
|
|
||||||
|
|
||||||
def ceil(x):
|
|
||||||
"""Return the ceiling of x."""
|
|
||||||
return math.ceil(x)
|
|
||||||
|
|
||||||
# Re-export random functions that might be used
|
|
||||||
import random
|
import random
|
||||||
|
import re
|
||||||
|
import requests
|
||||||
|
import webbrowser
|
||||||
|
# Global variable for speech provider
|
||||||
|
try:
|
||||||
|
import speechd
|
||||||
|
spd = speechd.Client()
|
||||||
|
speechProvider = "speechd"
|
||||||
|
except ImportError:
|
||||||
|
import accessible_output2.outputs.auto
|
||||||
|
s = accessible_output2.outputs.auto.Auto()
|
||||||
|
speechProvider = "accessible_output2"
|
||||||
|
except ImportError:
|
||||||
|
print("No other speech providers found.")
|
||||||
|
exit()
|
||||||
|
import math
|
||||||
|
import time
|
||||||
|
|
||||||
def randint(a, b):
|
localConfig = configparser.ConfigParser()
|
||||||
"""Return a random integer N such that a <= N <= b."""
|
globalConfig = configparser.ConfigParser()
|
||||||
return random.randint(a, b)
|
|
||||||
|
|
||||||
def choice(seq):
|
class scoreboard():
|
||||||
"""Return a random element from the non-empty sequence seq."""
|
'Handles scores and top 10'
|
||||||
return random.choice(seq)
|
|
||||||
|
|
||||||
def uniform(a, b):
|
def __init__(self, startingScore = 0):
|
||||||
"""Return a random floating point number N such that a <= N <= b."""
|
read_config()
|
||||||
return random.uniform(a, b)
|
try:
|
||||||
|
localConfig.add_section("scoreboard")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
self.score = startingScore
|
||||||
|
self.oldScores = []
|
||||||
|
for i in range(1, 11):
|
||||||
|
try:
|
||||||
|
self.oldScores.insert(i - 1, localConfig.getint("scoreboard", str(i)))
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
self.oldScores.insert(i - 1, 0)
|
||||||
|
for i in range(1, 11):
|
||||||
|
if self.oldScores[i - 1] == None:
|
||||||
|
self.oldScores[i - 1] = 0
|
||||||
|
|
||||||
def seed(a=None):
|
def __del__(self):
|
||||||
"""Initialize the random number generator."""
|
self.Update_Scores()
|
||||||
return random.seed(a)
|
try:
|
||||||
|
write_config()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def Decrease_Score(self, points = 1):
|
||||||
|
self.score -= points
|
||||||
|
|
||||||
|
def Get_High_Score(self, position = 1):
|
||||||
|
return self.oldScores[position - 1]
|
||||||
|
|
||||||
|
def Get_Score(self):
|
||||||
|
return self.score
|
||||||
|
|
||||||
|
def Increase_Score(self, points = 1):
|
||||||
|
self.score += points
|
||||||
|
|
||||||
|
def New_High_Score(self):
|
||||||
|
for i, j in enumerate(self.oldScores):
|
||||||
|
if self.score > j: return i + 1
|
||||||
|
return None
|
||||||
|
|
||||||
|
def Update_Scores(self):
|
||||||
|
# Update the scores
|
||||||
|
for i, j in enumerate(self.oldScores):
|
||||||
|
if self.score > j:
|
||||||
|
self.oldScores.insert(i, self.score)
|
||||||
|
break
|
||||||
|
# Only keep the top 10 scores.
|
||||||
|
self.oldScores = self.oldScores[:10]
|
||||||
|
# Update the scoreboard section of the games config file.
|
||||||
|
for i, j in enumerate(self.oldScores):
|
||||||
|
localConfig.set("scoreboard", str(i + 1), str(j))
|
||||||
|
|
||||||
|
|
||||||
|
def write_config(writeGlobal = False):
|
||||||
|
if writeGlobal == False:
|
||||||
|
with open(gamePath + "/config.ini", 'w') as configfile:
|
||||||
|
localConfig.write(configfile)
|
||||||
|
else:
|
||||||
|
with open(globalPath + "/config.ini", 'w') as configfile:
|
||||||
|
globalConfig.write(configfile)
|
||||||
|
|
||||||
|
def read_config(readGlobal = False):
|
||||||
|
if readGlobal == False:
|
||||||
|
try:
|
||||||
|
with open(gamePath + "/config.ini", 'r') as configfile:
|
||||||
|
localConfig.read_file(configfile)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
with open(globalPath + "/config.ini", 'r') as configfile:
|
||||||
|
globalConfig.read_file(configfile)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def speak(text, interupt = True):
|
||||||
|
if speechProvider == "speechd":
|
||||||
|
if interupt == True: spd.cancel()
|
||||||
|
spd.say(text)
|
||||||
|
else:
|
||||||
|
if speechProvider == "accessible_output2":
|
||||||
|
s.speak(text, interrupt=True)
|
||||||
|
|
||||||
|
|
||||||
|
def exit_game():
|
||||||
|
if speechProvider == "speechd": spd.close()
|
||||||
|
# Close the pyglet window
|
||||||
|
pyglet.app.exit()
|
||||||
|
|
||||||
|
def initialize_gui(gameTitle):
|
||||||
|
# Check for, and possibly create, storm-games path
|
||||||
|
global globalPath
|
||||||
|
global gamePath
|
||||||
|
globalPath = BaseDirectory.xdg_config_home + "/storm-games"
|
||||||
|
gamePath = globalPath + "/" + str.lower(str.replace(gameTitle, " ", "-"))
|
||||||
|
if not os.path.exists(gamePath): os.makedirs(gamePath)
|
||||||
|
# Seed the random generator to the clock
|
||||||
|
random.seed()
|
||||||
|
# Set game's name
|
||||||
|
global gameName
|
||||||
|
gameName = gameTitle
|
||||||
|
setproctitle(str.lower(str.replace(gameTitle, " ", "")))
|
||||||
|
# init pyglet window
|
||||||
|
window = pyglet.window.Window(500, 300, gameTitle)
|
||||||
|
# Load sounds from the sound directory and creates a list like {'bottle': 'bottle.ogg'}
|
||||||
|
soundFiles = [f for f in listdir("sounds/") if isfile(join("sounds/", f)) and (f.split('.')[1].lower() in ["ogg","wav"])]
|
||||||
|
# make a dict with pyglet media {'bottle':<soundobject>}
|
||||||
|
soundData = {}
|
||||||
|
for f in soundFiles:
|
||||||
|
soundData[f.split('.')[0]] = pyglet.media.load("sounds/" + f, streaming = False)
|
||||||
|
soundData['game-intro'].play()
|
||||||
|
time.sleep(soundData['game-intro'].duration)
|
||||||
|
return soundData
|
||||||
|
|
||||||
|
def cut_scene(sounds, soundName):
|
||||||
|
pygame.event.clear()
|
||||||
|
pygame.mixer.stop()
|
||||||
|
c = pygame.mixer.Channel(0)
|
||||||
|
c.play(sounds[soundName])
|
||||||
|
while pygame.mixer.get_busy():
|
||||||
|
event = pygame.event.poll()
|
||||||
|
if event.type == pygame.KEYDOWN and event.key in [pygame.K_ESCAPE, pygame.K_RETURN, pygame.K_SPACE]:
|
||||||
|
pygame.mixer.stop()
|
||||||
|
pygame.event.pump()
|
||||||
|
|
||||||
|
def obj_play(sounds, soundName, playerPos, objPos):
|
||||||
|
distance = playerPos - objPos
|
||||||
|
if distance > 9 or distance < -9:
|
||||||
|
# The item is out of range, so play it at 0
|
||||||
|
left = 0
|
||||||
|
right = 0
|
||||||
|
elif distance == 0:
|
||||||
|
left = 0.9
|
||||||
|
right = 0.9
|
||||||
|
else:
|
||||||
|
angle = math.radians(distance * 5)
|
||||||
|
left = math.sqrt(2)/2.0 * (math.cos(angle) + math.sin(angle))
|
||||||
|
right = math.sqrt(2)/2.0 * (math.cos(angle) - math.sin(angle))
|
||||||
|
if left < 0: left *= -1
|
||||||
|
if right < 0: right *= -1
|
||||||
|
# x is the channel for the sound
|
||||||
|
x = sounds[soundName].play(-1)
|
||||||
|
# Apply the position information to the channel
|
||||||
|
x.set_volume(left, right)
|
||||||
|
# return the channel so that it can be used in the update and stop functions.
|
||||||
|
return x
|
||||||
|
|
||||||
|
def obj_update(x, playerPos, objPos):
|
||||||
|
distance = playerPos - objPos
|
||||||
|
if distance > 9 or distance < -9:
|
||||||
|
left = 0
|
||||||
|
right = 0
|
||||||
|
elif distance == 0:
|
||||||
|
left = 0.9
|
||||||
|
right = 0.9
|
||||||
|
else:
|
||||||
|
angle = math.radians(distance * 5)
|
||||||
|
left = math.sqrt(2)/2.0 * (math.cos(angle) + math.sin(angle))
|
||||||
|
right = math.sqrt(2)/2.0 * (math.cos(angle) - math.sin(angle))
|
||||||
|
if left < 0: left *= -1
|
||||||
|
if right < 0: right *= -1
|
||||||
|
# Apply the position information to the channel
|
||||||
|
x.set_volume(left, right)
|
||||||
|
# return the channel
|
||||||
|
return x
|
||||||
|
|
||||||
|
def obj_stop(x):
|
||||||
|
# Tries to stop a playing object channel
|
||||||
|
try:
|
||||||
|
x.stop()
|
||||||
|
return None
|
||||||
|
except:
|
||||||
|
return x
|
||||||
|
|
||||||
|
def play_random(sounds, soundName, pause = False, interrupt = False):
|
||||||
|
key = []
|
||||||
|
for i in sounds.keys():
|
||||||
|
if re.match("^" + soundName + ".*", i):
|
||||||
|
key.append(i)
|
||||||
|
randomKey = random.choice(key)
|
||||||
|
if interrupt == False:
|
||||||
|
sounds[randomKey].play()
|
||||||
|
else:
|
||||||
|
cut_scene(sounds, randomKey)
|
||||||
|
# Cut scenes override the pause option
|
||||||
|
return
|
||||||
|
if pause == True:
|
||||||
|
time.sleep(sounds[randomKey].get_length())
|
||||||
|
|
||||||
|
def instructions():
|
||||||
|
# Read in the instructions file
|
||||||
|
try:
|
||||||
|
with open('files/instructions.txt', 'r') as f:
|
||||||
|
info = f.readlines()
|
||||||
|
except:
|
||||||
|
info = ["Instructions file is missing."]
|
||||||
|
display_text(info)
|
||||||
|
|
||||||
|
def credits():
|
||||||
|
# Read in the credits file.
|
||||||
|
try:
|
||||||
|
with open('files/credits.txt', 'r') as f:
|
||||||
|
info = f.readlines()
|
||||||
|
# Add the header
|
||||||
|
info.insert(0, gameName + ": brought to you by Storm Dragon")
|
||||||
|
except:
|
||||||
|
info = ["Credits file is missing."]
|
||||||
|
display_text(info)
|
||||||
|
|
||||||
|
def display_text(text):
|
||||||
|
i = 0
|
||||||
|
text.insert(0, "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.")
|
||||||
|
text.append("End of text.")
|
||||||
|
speak(text[i])
|
||||||
|
while True:
|
||||||
|
event = pygame.event.wait()
|
||||||
|
if event.type == pygame.KEYDOWN:
|
||||||
|
if event.key == pygame.K_ESCAPE or event.key == pygame.K_RETURN: return
|
||||||
|
if event.key == pygame.K_DOWN and i < len(text) - 1: i = i + 1
|
||||||
|
if event.key == pygame.K_UP and i > 0: i = i - 1
|
||||||
|
if event.key == pygame.K_SPACE:
|
||||||
|
speak(' '.join(text[1:]))
|
||||||
|
else:
|
||||||
|
speak(text[i])
|
||||||
|
if event.key == pygame.K_c:
|
||||||
|
try:
|
||||||
|
pyperclip.copy(text[i])
|
||||||
|
speak("Copied " + text[i] + " to the clipboard.")
|
||||||
|
except:
|
||||||
|
speak("Failed to copy the text to the clipboard.")
|
||||||
|
if event.key == pygame.K_t:
|
||||||
|
try:
|
||||||
|
pyperclip.copy(' '.join(text[1:-1]))
|
||||||
|
speak("Copied entire message to the clipboard.")
|
||||||
|
except:
|
||||||
|
speak("Failed to copy the text to the clipboard.")
|
||||||
|
event = pygame.event.clear()
|
||||||
|
time.sleep(0.001)
|
||||||
|
|
||||||
|
def learn_sounds(sounds):
|
||||||
|
loop = True
|
||||||
|
pygame.mixer.music.pause()
|
||||||
|
i = 0
|
||||||
|
soundFiles = [f for f in 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"])]
|
||||||
|
# j keeps track of last spoken index so it isn't voiced on key up.
|
||||||
|
j = -1
|
||||||
|
while loop == True:
|
||||||
|
if i != j:
|
||||||
|
speak(soundFiles[i][:-4])
|
||||||
|
j = i
|
||||||
|
event = pygame.event.wait()
|
||||||
|
if event.type == pygame.KEYDOWN:
|
||||||
|
if event.key == pygame.K_ESCAPE: return "menu"
|
||||||
|
if event.key == pygame.K_DOWN and i < len(soundFiles) - 1:
|
||||||
|
pygame.mixer.stop()
|
||||||
|
i = i + 1
|
||||||
|
if event.key == pygame.K_UP and i > 0:
|
||||||
|
pygame.mixer.stop()
|
||||||
|
i = i - 1
|
||||||
|
if event.key == pygame.K_RETURN:
|
||||||
|
try:
|
||||||
|
soundName = soundFiles[i][:-4]
|
||||||
|
pygame.mixer.stop()
|
||||||
|
sounds[soundName].play()
|
||||||
|
continue
|
||||||
|
except:
|
||||||
|
j = -1
|
||||||
|
speak("Could not play sound.")
|
||||||
|
continue
|
||||||
|
event = pygame.event.clear()
|
||||||
|
time.sleep(0.001)
|
||||||
|
|
||||||
|
def game_menu(sounds, *options):
|
||||||
|
loop = True
|
||||||
|
pygame.mixer.stop()
|
||||||
|
if pygame.mixer.music.get_busy():
|
||||||
|
pygame.mixer.music.unpause()
|
||||||
|
else:
|
||||||
|
pygame.mixer.music.load("sounds/music_menu.ogg")
|
||||||
|
pygame.mixer.music.set_volume(0.75)
|
||||||
|
pygame.mixer.music.play(-1)
|
||||||
|
i = 0
|
||||||
|
# j keeps track of last spoken index so it isn't voiced on key up.
|
||||||
|
j = -1
|
||||||
|
while loop == True:
|
||||||
|
if i != j:
|
||||||
|
speak(options[i])
|
||||||
|
j = i
|
||||||
|
event = pygame.event.wait()
|
||||||
|
if event.type == pygame.KEYDOWN:
|
||||||
|
if event.key == pygame.K_ESCAPE: exit_game()
|
||||||
|
if event.key == pygame.K_DOWN and i < len(options) - 1:
|
||||||
|
i = i + 1
|
||||||
|
try:
|
||||||
|
sounds['menu-move'].play()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
if options[i] != "donate": pygame.mixer.music.unpause()
|
||||||
|
if event.key == pygame.K_UP and i > 0:
|
||||||
|
i = i - 1
|
||||||
|
try:
|
||||||
|
sounds['menu-move'].play()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
if options[i] != "donate": pygame.mixer.music.unpause()
|
||||||
|
if event.key == pygame.K_HOME and i != 0:
|
||||||
|
i = 0
|
||||||
|
try:
|
||||||
|
sounds['menu-move'].play()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
if options[i] != "donate": pygame.mixer.music.unpause()
|
||||||
|
if event.key == pygame.K_END and i != len(options) - 1:
|
||||||
|
i = len(options) -1
|
||||||
|
try:
|
||||||
|
sounds['menu-move'].play()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
if options[i] != "donate": pygame.mixer.music.unpause()
|
||||||
|
if event.key == pygame.K_RETURN:
|
||||||
|
try:
|
||||||
|
j = -1
|
||||||
|
try:
|
||||||
|
sounds['menu-select'].play()
|
||||||
|
time.sleep(sounds['menu-select'].get_length())
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
eval(options[i] + "()")
|
||||||
|
continue
|
||||||
|
except:
|
||||||
|
j = -1
|
||||||
|
return options[i]
|
||||||
|
continue
|
||||||
|
event = pygame.event.clear()
|
||||||
|
time.sleep(0.001)
|
||||||
|
|
||||||
|
def donate():
|
||||||
|
pygame.mixer.music.pause()
|
||||||
|
webbrowser.open('https://ko-fi.com/stormux')
|
||||||
|
102
config.py
102
config.py
@ -1,102 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""Configuration management for Storm Games.
|
|
||||||
|
|
||||||
Provides functionality for:
|
|
||||||
- Reading and writing configuration files
|
|
||||||
- Global and local configuration handling
|
|
||||||
"""
|
|
||||||
|
|
||||||
import configparser
|
|
||||||
import os
|
|
||||||
from xdg import BaseDirectory
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
"""Configuration management class for Storm Games."""
|
|
||||||
|
|
||||||
def __init__(self, gameTitle):
|
|
||||||
"""Initialize configuration system for a game.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
gameTitle (str): Title of the game
|
|
||||||
"""
|
|
||||||
self.gameTitle = gameTitle
|
|
||||||
self.globalPath = os.path.join(BaseDirectory.xdg_config_home, "storm-games")
|
|
||||||
self.gamePath = os.path.join(self.globalPath,
|
|
||||||
str.lower(str.replace(gameTitle, " ", "-")))
|
|
||||||
|
|
||||||
# Create game directory if it doesn't exist
|
|
||||||
if not os.path.exists(self.gamePath):
|
|
||||||
os.makedirs(self.gamePath)
|
|
||||||
|
|
||||||
# Initialize config parsers
|
|
||||||
self.localConfig = configparser.ConfigParser()
|
|
||||||
self.globalConfig = configparser.ConfigParser()
|
|
||||||
|
|
||||||
# Load existing configurations
|
|
||||||
self.read_local_config()
|
|
||||||
self.read_global_config()
|
|
||||||
|
|
||||||
def read_local_config(self):
|
|
||||||
"""Read local configuration from file."""
|
|
||||||
try:
|
|
||||||
with open(os.path.join(self.gamePath, "config.ini"), 'r') as configFile:
|
|
||||||
self.localConfig.read_file(configFile)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def read_global_config(self):
|
|
||||||
"""Read global configuration from file."""
|
|
||||||
try:
|
|
||||||
with open(os.path.join(self.globalPath, "config.ini"), 'r') as configFile:
|
|
||||||
self.globalConfig.read_file(configFile)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def write_local_config(self):
|
|
||||||
"""Write local configuration to file."""
|
|
||||||
with open(os.path.join(self.gamePath, "config.ini"), 'w') as configFile:
|
|
||||||
self.localConfig.write(configFile)
|
|
||||||
|
|
||||||
def write_global_config(self):
|
|
||||||
"""Write global configuration to file."""
|
|
||||||
with open(os.path.join(self.globalPath, "config.ini"), 'w') as configFile:
|
|
||||||
self.globalConfig.write(configFile)
|
|
||||||
|
|
||||||
# Global variables for backward compatibility
|
|
||||||
localConfig = configparser.ConfigParser()
|
|
||||||
globalConfig = configparser.ConfigParser()
|
|
||||||
gamePath = ""
|
|
||||||
globalPath = ""
|
|
||||||
|
|
||||||
def write_config(writeGlobal=False):
|
|
||||||
"""Write configuration to file.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
writeGlobal (bool): If True, write to global config, otherwise local (default: False)
|
|
||||||
"""
|
|
||||||
if not writeGlobal:
|
|
||||||
with open(gamePath + "/config.ini", 'w') as configFile:
|
|
||||||
localConfig.write(configFile)
|
|
||||||
else:
|
|
||||||
with open(globalPath + "/config.ini", 'w') as configFile:
|
|
||||||
globalConfig.write(configFile)
|
|
||||||
|
|
||||||
def read_config(readGlobal=False):
|
|
||||||
"""Read configuration from file.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
readGlobal (bool): If True, read global config, otherwise local (default: False)
|
|
||||||
"""
|
|
||||||
if not readGlobal:
|
|
||||||
try:
|
|
||||||
with open(gamePath + "/config.ini", 'r') as configFile:
|
|
||||||
localConfig.read_file(configFile)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
with open(globalPath + "/config.ini", 'r') as configFile:
|
|
||||||
globalConfig.read_file(configFile)
|
|
||||||
except:
|
|
||||||
pass
|
|
192
display.py
192
display.py
@ -1,192 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""Display functionality for Storm Games.
|
|
||||||
|
|
||||||
Provides functionality for:
|
|
||||||
- GUI initialization
|
|
||||||
- Text display with navigation
|
|
||||||
- Message boxes
|
|
||||||
"""
|
|
||||||
|
|
||||||
import pygame
|
|
||||||
import time
|
|
||||||
import os
|
|
||||||
import pyperclip
|
|
||||||
import random
|
|
||||||
from xdg import BaseDirectory
|
|
||||||
from setproctitle import setproctitle
|
|
||||||
from .speech import Speech
|
|
||||||
from .services import PathService, VolumeService
|
|
||||||
|
|
||||||
# Keep track of the instructions for navigating display_text has been shown
|
|
||||||
displayTextUsageInstructions = False
|
|
||||||
|
|
||||||
def initialize_gui(gameTitle):
|
|
||||||
"""Initialize the game GUI and sound system.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
gameTitle (str): Title of the game
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: Dictionary of loaded sound objects
|
|
||||||
"""
|
|
||||||
# Initialize path service with game title
|
|
||||||
pathService = PathService.get_instance().initialize(gameTitle)
|
|
||||||
|
|
||||||
# Seed the random generator to the clock
|
|
||||||
random.seed()
|
|
||||||
|
|
||||||
# Set game's name
|
|
||||||
setproctitle(str.lower(str.replace(gameTitle, " ", "-")))
|
|
||||||
|
|
||||||
# Initialize pygame
|
|
||||||
pygame.init()
|
|
||||||
pygame.display.set_mode((800, 600))
|
|
||||||
pygame.display.set_caption(gameTitle)
|
|
||||||
|
|
||||||
# Set up audio system
|
|
||||||
pygame.mixer.pre_init(44100, -16, 2, 1024)
|
|
||||||
pygame.mixer.init()
|
|
||||||
pygame.mixer.set_num_channels(32)
|
|
||||||
pygame.mixer.set_reserved(0) # Reserve channel for cut scenes
|
|
||||||
|
|
||||||
# Enable key repeat for volume controls
|
|
||||||
pygame.key.set_repeat(500, 100)
|
|
||||||
|
|
||||||
# Load sound files recursively including subdirectories
|
|
||||||
soundData = {}
|
|
||||||
try:
|
|
||||||
import os
|
|
||||||
|
|
||||||
soundDir = "sounds/"
|
|
||||||
# Walk through directory tree
|
|
||||||
for dirPath, dirNames, fileNames in os.walk(soundDir):
|
|
||||||
# Get relative path from soundDir
|
|
||||||
relPath = os.path.relpath(dirPath, soundDir)
|
|
||||||
|
|
||||||
# Process each file
|
|
||||||
for fileName in fileNames:
|
|
||||||
# Check if file is a valid sound file
|
|
||||||
if fileName.lower().endswith(('.ogg', '.wav')):
|
|
||||||
# Full path to the sound file
|
|
||||||
fullPath = os.path.join(dirPath, fileName)
|
|
||||||
|
|
||||||
# Create sound key (remove extension)
|
|
||||||
baseName = os.path.splitext(fileName)[0]
|
|
||||||
|
|
||||||
# If in root sounds dir, just use basename
|
|
||||||
if relPath == '.':
|
|
||||||
soundKey = baseName
|
|
||||||
else:
|
|
||||||
# Otherwise use relative path + basename, normalized with forward slashes
|
|
||||||
soundKey = os.path.join(relPath, baseName).replace('\\', '/')
|
|
||||||
|
|
||||||
# Load the sound
|
|
||||||
soundData[soundKey] = pygame.mixer.Sound(fullPath)
|
|
||||||
except Exception as e:
|
|
||||||
print("Error loading sounds:", e)
|
|
||||||
Speech.get_instance().speak("Error loading sounds.", False)
|
|
||||||
soundData = {}
|
|
||||||
|
|
||||||
# Play intro sound if available
|
|
||||||
from .sound import cut_scene
|
|
||||||
if 'game-intro' in soundData:
|
|
||||||
cut_scene(soundData, 'game-intro')
|
|
||||||
|
|
||||||
return soundData
|
|
||||||
|
|
||||||
def display_text(text):
|
|
||||||
"""Display and speak text with navigation controls.
|
|
||||||
|
|
||||||
Allows users to:
|
|
||||||
- Navigate text line by line with arrow keys (skipping blank lines)
|
|
||||||
- Listen to full text with space
|
|
||||||
- Copy current line or full text (preserving blank lines)
|
|
||||||
- Exit with enter/escape
|
|
||||||
- Volume controls (with Alt modifier):
|
|
||||||
- Alt+PageUp/PageDown: Master volume up/down
|
|
||||||
- Alt+Home/End: Background music volume up/down
|
|
||||||
- Alt+Insert/Delete: Sound effects volume up/down
|
|
||||||
|
|
||||||
Args:
|
|
||||||
text (list): List of text lines to display
|
|
||||||
"""
|
|
||||||
# Get service instances
|
|
||||||
speech = Speech.get_instance()
|
|
||||||
volumeService = VolumeService.get_instance()
|
|
||||||
|
|
||||||
# Store original text with blank lines for copying
|
|
||||||
originalText = text.copy()
|
|
||||||
|
|
||||||
# Create navigation text by filtering out blank lines
|
|
||||||
navText = [line for line in text if line.strip()]
|
|
||||||
|
|
||||||
# Add instructions at the start on the first display
|
|
||||||
global displayTextUsageInstructions
|
|
||||||
if not displayTextUsageInstructions:
|
|
||||||
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.")
|
|
||||||
navText.insert(0, instructions)
|
|
||||||
displayTextUsageInstructions = True
|
|
||||||
|
|
||||||
# Add end marker
|
|
||||||
navText.append("End of text.")
|
|
||||||
|
|
||||||
currentIndex = 0
|
|
||||||
speech.speak(navText[currentIndex])
|
|
||||||
|
|
||||||
while True:
|
|
||||||
event = pygame.event.wait()
|
|
||||||
if event.type == pygame.KEYDOWN:
|
|
||||||
# Check for Alt modifier
|
|
||||||
mods = pygame.key.get_mods()
|
|
||||||
altPressed = mods & pygame.KMOD_ALT
|
|
||||||
|
|
||||||
# Volume controls (require Alt)
|
|
||||||
if altPressed:
|
|
||||||
if event.key == pygame.K_PAGEUP:
|
|
||||||
volumeService.adjust_master_volume(0.1, pygame.mixer)
|
|
||||||
elif event.key == pygame.K_PAGEDOWN:
|
|
||||||
volumeService.adjust_master_volume(-0.1, pygame.mixer)
|
|
||||||
elif event.key == pygame.K_HOME:
|
|
||||||
volumeService.adjust_bgm_volume(0.1, pygame.mixer)
|
|
||||||
elif event.key == pygame.K_END:
|
|
||||||
volumeService.adjust_bgm_volume(-0.1, pygame.mixer)
|
|
||||||
elif event.key == pygame.K_INSERT:
|
|
||||||
volumeService.adjust_sfx_volume(0.1, pygame.mixer)
|
|
||||||
elif event.key == pygame.K_DELETE:
|
|
||||||
volumeService.adjust_sfx_volume(-0.1, pygame.mixer)
|
|
||||||
else:
|
|
||||||
if event.key in (pygame.K_ESCAPE, pygame.K_RETURN):
|
|
||||||
return
|
|
||||||
|
|
||||||
if event.key in [pygame.K_DOWN, pygame.K_s] and currentIndex < len(navText) - 1:
|
|
||||||
currentIndex += 1
|
|
||||||
speech.speak(navText[currentIndex])
|
|
||||||
|
|
||||||
if event.key in [pygame.K_UP, pygame.K_w] and currentIndex > 0:
|
|
||||||
currentIndex -= 1
|
|
||||||
speech.speak(navText[currentIndex])
|
|
||||||
|
|
||||||
if event.key == pygame.K_SPACE:
|
|
||||||
# Join with newlines to preserve spacing in speech
|
|
||||||
speech.speak('\n'.join(originalText[1:-1]))
|
|
||||||
|
|
||||||
if event.key == pygame.K_c:
|
|
||||||
try:
|
|
||||||
pyperclip.copy(navText[currentIndex])
|
|
||||||
speech.speak("Copied " + navText[currentIndex] + " to the clipboard.")
|
|
||||||
except:
|
|
||||||
speech.speak("Failed to copy the text to the clipboard.")
|
|
||||||
|
|
||||||
if event.key == pygame.K_t:
|
|
||||||
try:
|
|
||||||
# Join with newlines to preserve blank lines in full text
|
|
||||||
pyperclip.copy(''.join(originalText[2:-1]))
|
|
||||||
speech.speak("Copied entire message to the clipboard.")
|
|
||||||
except:
|
|
||||||
speech.speak("Failed to copy the text to the clipboard.")
|
|
||||||
|
|
||||||
event = pygame.event.clear()
|
|
||||||
time.sleep(0.001)
|
|
77
input.py
77
input.py
@ -1,77 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""Input handling for Storm Games.
|
|
||||||
|
|
||||||
Provides functionality for:
|
|
||||||
- Text input dialogs
|
|
||||||
- Game pause functionality
|
|
||||||
- Exit handling
|
|
||||||
"""
|
|
||||||
|
|
||||||
import pygame
|
|
||||||
import time
|
|
||||||
import wx
|
|
||||||
from .speech import speak
|
|
||||||
|
|
||||||
def get_input(prompt="Enter text:", text=""):
|
|
||||||
"""Display a dialog box for text input.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
prompt (str): Prompt text to display (default: "Enter text:")
|
|
||||||
text (str): Initial text in input box (default: "")
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: User input text, or None if cancelled
|
|
||||||
"""
|
|
||||||
app = wx.App(False)
|
|
||||||
dialog = wx.TextEntryDialog(None, prompt, "Input", text)
|
|
||||||
dialog.SetValue(text)
|
|
||||||
if dialog.ShowModal() == wx.ID_OK:
|
|
||||||
userInput = dialog.GetValue()
|
|
||||||
else:
|
|
||||||
userInput = None
|
|
||||||
dialog.Destroy()
|
|
||||||
return userInput
|
|
||||||
|
|
||||||
def pause_game():
|
|
||||||
"""Pauses the game until user presses backspace."""
|
|
||||||
speak("Game paused, press backspace to resume.")
|
|
||||||
pygame.event.clear()
|
|
||||||
try:
|
|
||||||
pygame.mixer.pause()
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
try:
|
|
||||||
pygame.mixer.music.pause()
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
while True:
|
|
||||||
event = pygame.event.wait()
|
|
||||||
if event.type == pygame.KEYDOWN and event.key == pygame.K_BACKSPACE:
|
|
||||||
break
|
|
||||||
|
|
||||||
try:
|
|
||||||
pygame.mixer.unpause()
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
try:
|
|
||||||
pygame.mixer.music.unpause()
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
pygame.event.pump()
|
|
||||||
|
|
||||||
def check_for_exit():
|
|
||||||
"""Check if user has pressed escape key.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if escape was pressed, False otherwise
|
|
||||||
"""
|
|
||||||
for event in pygame.event.get():
|
|
||||||
if event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
pygame.event.pump()
|
|
496
menu.py
496
menu.py
@ -1,496 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""Menu systems for Storm Games.
|
|
||||||
|
|
||||||
Provides functionality for:
|
|
||||||
- Game menu navigation
|
|
||||||
- Instructions display
|
|
||||||
- Credits display
|
|
||||||
- Sound learning interface
|
|
||||||
- Game exit handling
|
|
||||||
"""
|
|
||||||
|
|
||||||
import pygame
|
|
||||||
import time
|
|
||||||
import webbrowser
|
|
||||||
import os
|
|
||||||
from sys import exit
|
|
||||||
from os.path import isfile
|
|
||||||
from os import listdir
|
|
||||||
from os.path import join
|
|
||||||
from inspect import isfunction
|
|
||||||
from .speech import messagebox, Speech
|
|
||||||
from .sound import adjust_master_volume, adjust_bgm_volume, adjust_sfx_volume, play_bgm
|
|
||||||
from .display import display_text
|
|
||||||
from .scoreboard import Scoreboard
|
|
||||||
from .services import PathService, ConfigService
|
|
||||||
|
|
||||||
def game_menu(sounds, playCallback=None, *customOptions):
|
|
||||||
"""Display and handle the main game menu with standard and custom options.
|
|
||||||
|
|
||||||
Standard menu structure:
|
|
||||||
1. Play (always first)
|
|
||||||
2. High Scores
|
|
||||||
3. Custom options (if provided)
|
|
||||||
4. Learn Sounds
|
|
||||||
5. Instructions (if available)
|
|
||||||
6. Credits (if available)
|
|
||||||
7. Donate
|
|
||||||
8. Exit
|
|
||||||
|
|
||||||
Handles navigation with:
|
|
||||||
- Up/Down arrows for selection
|
|
||||||
- Home/End for first/last option
|
|
||||||
- Enter to select
|
|
||||||
- Escape to exit
|
|
||||||
- Volume controls (with Alt modifier)
|
|
||||||
|
|
||||||
Args:
|
|
||||||
sounds (dict): Dictionary of sound objects
|
|
||||||
playCallback (function, optional): Callback function for the "play" option.
|
|
||||||
If None, "play" is returned as a string like other options.
|
|
||||||
*customOptions: Additional custom options to include after play but before standard ones
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Selected menu option or "exit" if user pressed escape
|
|
||||||
"""
|
|
||||||
# Get speech instance
|
|
||||||
speech = Speech.get_instance()
|
|
||||||
|
|
||||||
# Start with Play option
|
|
||||||
allOptions = ["play"]
|
|
||||||
|
|
||||||
# Add high scores option if scores exist
|
|
||||||
if Scoreboard.has_high_scores():
|
|
||||||
allOptions.append("high_scores")
|
|
||||||
|
|
||||||
# Add custom options (other menu items, etc.)
|
|
||||||
allOptions.extend(customOptions)
|
|
||||||
|
|
||||||
# Add standard options in preferred order
|
|
||||||
allOptions.append("learn_sounds")
|
|
||||||
|
|
||||||
# Check for instructions file
|
|
||||||
if os.path.isfile('files/instructions.txt'):
|
|
||||||
allOptions.append("instructions")
|
|
||||||
|
|
||||||
# Check for credits file
|
|
||||||
if os.path.isfile('files/credits.txt'):
|
|
||||||
allOptions.append("credits")
|
|
||||||
|
|
||||||
# Final options
|
|
||||||
allOptions.extend(["donate", "exit_game"])
|
|
||||||
|
|
||||||
# Track if music was previously playing
|
|
||||||
musicWasPlaying = pygame.mixer.music.get_busy()
|
|
||||||
|
|
||||||
# Only start menu music if no music is currently playing
|
|
||||||
if not musicWasPlaying:
|
|
||||||
try:
|
|
||||||
from .sound import play_bgm
|
|
||||||
play_bgm("sounds/music_menu.ogg")
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
loop = True
|
|
||||||
pygame.mixer.stop()
|
|
||||||
currentIndex = 0
|
|
||||||
lastSpoken = -1 # Track last spoken index
|
|
||||||
|
|
||||||
while loop:
|
|
||||||
if currentIndex != lastSpoken:
|
|
||||||
speech.speak(allOptions[currentIndex])
|
|
||||||
lastSpoken = currentIndex
|
|
||||||
|
|
||||||
event = pygame.event.wait()
|
|
||||||
if event.type == pygame.KEYDOWN:
|
|
||||||
# Check for Alt modifier
|
|
||||||
mods = pygame.key.get_mods()
|
|
||||||
altPressed = mods & pygame.KMOD_ALT
|
|
||||||
|
|
||||||
# Volume controls (require Alt)
|
|
||||||
if altPressed:
|
|
||||||
if event.key == pygame.K_PAGEUP:
|
|
||||||
adjust_master_volume(0.1)
|
|
||||||
elif event.key == pygame.K_PAGEDOWN:
|
|
||||||
adjust_master_volume(-0.1)
|
|
||||||
elif event.key == pygame.K_HOME:
|
|
||||||
adjust_bgm_volume(0.1)
|
|
||||||
elif event.key == pygame.K_END:
|
|
||||||
adjust_bgm_volume(-0.1)
|
|
||||||
elif event.key == pygame.K_INSERT:
|
|
||||||
adjust_sfx_volume(0.1)
|
|
||||||
elif event.key == pygame.K_DELETE:
|
|
||||||
adjust_sfx_volume(-0.1)
|
|
||||||
# Regular menu navigation (no Alt required)
|
|
||||||
else:
|
|
||||||
if event.key == pygame.K_ESCAPE:
|
|
||||||
# Exit with fade if music is playing
|
|
||||||
exit_game(500 if pygame.mixer.music.get_busy() else 0)
|
|
||||||
elif event.key == pygame.K_HOME:
|
|
||||||
if currentIndex != 0:
|
|
||||||
currentIndex = 0
|
|
||||||
try:
|
|
||||||
sounds['menu-move'].play()
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
if allOptions[currentIndex] != "donate":
|
|
||||||
pygame.mixer.music.unpause()
|
|
||||||
elif event.key == pygame.K_END:
|
|
||||||
if currentIndex != len(allOptions) - 1:
|
|
||||||
currentIndex = len(allOptions) - 1
|
|
||||||
try:
|
|
||||||
sounds['menu-move'].play()
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
if allOptions[currentIndex] != "donate":
|
|
||||||
pygame.mixer.music.unpause()
|
|
||||||
elif event.key in [pygame.K_DOWN, pygame.K_s] and currentIndex < len(allOptions) - 1:
|
|
||||||
currentIndex += 1
|
|
||||||
try:
|
|
||||||
sounds['menu-move'].play()
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
if allOptions[currentIndex] != "donate":
|
|
||||||
pygame.mixer.music.unpause()
|
|
||||||
elif event.key in [pygame.K_UP, pygame.K_w] and currentIndex > 0:
|
|
||||||
currentIndex -= 1
|
|
||||||
try:
|
|
||||||
sounds['menu-move'].play()
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
if allOptions[currentIndex] != "donate":
|
|
||||||
pygame.mixer.music.unpause()
|
|
||||||
elif event.key == pygame.K_RETURN:
|
|
||||||
try:
|
|
||||||
lastSpoken = -1
|
|
||||||
try:
|
|
||||||
sounds['menu-select'].play()
|
|
||||||
time.sleep(sounds['menu-select'].get_length())
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
selectedOption = allOptions[currentIndex]
|
|
||||||
|
|
||||||
# Special case for exit_game with fade
|
|
||||||
if selectedOption == "exit_game":
|
|
||||||
exit_game(500 if pygame.mixer.music.get_busy() else 0)
|
|
||||||
# Special case for play option
|
|
||||||
elif selectedOption == "play":
|
|
||||||
if playCallback:
|
|
||||||
# If a play callback is provided, call it directly
|
|
||||||
try:
|
|
||||||
pygame.mixer.music.fadeout(500)
|
|
||||||
time.sleep(0.5)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Could not fade music: {e}")
|
|
||||||
pass
|
|
||||||
playCallback()
|
|
||||||
else:
|
|
||||||
# Otherwise return "play" to the caller
|
|
||||||
try:
|
|
||||||
pygame.mixer.music.fadeout(500)
|
|
||||||
time.sleep(0.5)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Could not fade music: {e}")
|
|
||||||
pass
|
|
||||||
return "play"
|
|
||||||
# Handle standard options directly
|
|
||||||
elif selectedOption in ["instructions", "credits", "learn_sounds", "high_scores", "donate"]:
|
|
||||||
# Pause music before calling the selected function
|
|
||||||
try:
|
|
||||||
pygame.mixer.music.pause()
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Handle standard options
|
|
||||||
if selectedOption == "instructions":
|
|
||||||
instructions()
|
|
||||||
elif selectedOption == "credits":
|
|
||||||
credits()
|
|
||||||
elif selectedOption == "learn_sounds":
|
|
||||||
learn_sounds(sounds)
|
|
||||||
elif selectedOption == "high_scores":
|
|
||||||
Scoreboard.display_high_scores()
|
|
||||||
elif selectedOption == "donate":
|
|
||||||
donate()
|
|
||||||
|
|
||||||
# Unpause music after function returns
|
|
||||||
try:
|
|
||||||
# Check if music is actually paused before trying to unpause
|
|
||||||
if not pygame.mixer.music.get_busy():
|
|
||||||
pygame.mixer.music.unpause()
|
|
||||||
# If music is already playing, don't try to restart it
|
|
||||||
except:
|
|
||||||
# Only start fresh music if no music is playing at all
|
|
||||||
if not pygame.mixer.music.get_busy():
|
|
||||||
try:
|
|
||||||
from .sound import play_bgm
|
|
||||||
play_bgm("sounds/music_menu.ogg")
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
# Return custom options to the calling function
|
|
||||||
else:
|
|
||||||
lastSpoken = -1
|
|
||||||
try:
|
|
||||||
pygame.mixer.music.fadeout(500)
|
|
||||||
time.sleep(0.5)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Could not fade music: {e}")
|
|
||||||
pass
|
|
||||||
return selectedOption
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error handling menu selection: {e}")
|
|
||||||
lastSpoken = -1
|
|
||||||
try:
|
|
||||||
pygame.mixer.music.fadeout(500)
|
|
||||||
time.sleep(0.5)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
return allOptions[currentIndex]
|
|
||||||
|
|
||||||
event = pygame.event.clear()
|
|
||||||
time.sleep(0.001)
|
|
||||||
|
|
||||||
def learn_sounds(sounds):
|
|
||||||
"""Interactive menu for learning game sounds.
|
|
||||||
|
|
||||||
Allows users to:
|
|
||||||
- Navigate through available sounds with up/down arrows
|
|
||||||
- Navigate between sound categories (folders) using Page Up/Page Down or Left/Right arrows
|
|
||||||
- Play selected sounds with Enter
|
|
||||||
- Return to menu with Escape
|
|
||||||
|
|
||||||
Excluded sounds:
|
|
||||||
- Files in folders named 'ambience' (at any level)
|
|
||||||
- Files in any directory starting with '.'
|
|
||||||
- Files starting with 'game-intro', 'music_menu', or '_'
|
|
||||||
|
|
||||||
Args:
|
|
||||||
sounds (dict): Dictionary of available sound objects
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: "menu" if user exits with escape
|
|
||||||
"""
|
|
||||||
# Get speech instance
|
|
||||||
speech = Speech.get_instance()
|
|
||||||
|
|
||||||
# Define exclusion criteria
|
|
||||||
excludedPrefixes = ["game-intro", "music_menu", "_"]
|
|
||||||
excludedDirs = ["ambience", "."]
|
|
||||||
|
|
||||||
# Organize sounds by directory
|
|
||||||
soundsByDir = {}
|
|
||||||
|
|
||||||
# Process each sound key in the dictionary
|
|
||||||
for soundKey in sounds.keys():
|
|
||||||
# Skip if key has any excluded prefix
|
|
||||||
if any(soundKey.lower().startswith(prefix.lower()) for prefix in excludedPrefixes):
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Split key into path parts
|
|
||||||
parts = soundKey.split('/')
|
|
||||||
|
|
||||||
# Skip if any part of the path is an excluded directory
|
|
||||||
if any(part.lower() == dirName.lower() or part.startswith('.') for part in parts for dirName in excludedDirs):
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Determine the directory
|
|
||||||
if '/' in soundKey:
|
|
||||||
directory = soundKey.split('/')[0]
|
|
||||||
else:
|
|
||||||
directory = 'root' # Root directory sounds
|
|
||||||
|
|
||||||
# Add to sounds by directory
|
|
||||||
if directory not in soundsByDir:
|
|
||||||
soundsByDir[directory] = []
|
|
||||||
soundsByDir[directory].append(soundKey)
|
|
||||||
|
|
||||||
# Sort each directory's sounds
|
|
||||||
for directory in soundsByDir:
|
|
||||||
soundsByDir[directory].sort()
|
|
||||||
|
|
||||||
# If no sounds found, inform the user and return
|
|
||||||
if not soundsByDir:
|
|
||||||
speech.speak("No sounds available to learn.")
|
|
||||||
return "menu"
|
|
||||||
|
|
||||||
# Get list of directories in sorted order
|
|
||||||
directories = sorted(soundsByDir.keys())
|
|
||||||
|
|
||||||
# Start with first directory
|
|
||||||
currentDirIndex = 0
|
|
||||||
currentDir = directories[currentDirIndex]
|
|
||||||
currentSoundKeys = soundsByDir[currentDir]
|
|
||||||
currentSoundIndex = 0
|
|
||||||
|
|
||||||
# Display appropriate message based on number of directories
|
|
||||||
if len(directories) > 1:
|
|
||||||
messagebox(f"Starting with {currentDir if currentDir != 'root' else 'root directory'} sounds. Use left and right arrows or page up and page down to navigate categories.")
|
|
||||||
|
|
||||||
# Track last spoken to avoid repetition
|
|
||||||
lastSpoken = -1
|
|
||||||
directoryChanged = True # Flag to track if directory just changed
|
|
||||||
|
|
||||||
# Flag to track when to exit the loop
|
|
||||||
returnToMenu = False
|
|
||||||
|
|
||||||
while not returnToMenu:
|
|
||||||
# Announce current sound
|
|
||||||
if currentSoundIndex != lastSpoken:
|
|
||||||
totalSounds = len(currentSoundKeys)
|
|
||||||
soundName = currentSoundKeys[currentSoundIndex]
|
|
||||||
|
|
||||||
# Remove directory prefix if present
|
|
||||||
if '/' in soundName:
|
|
||||||
displayName = '/'.join(soundName.split('/')[1:])
|
|
||||||
else:
|
|
||||||
displayName = soundName
|
|
||||||
|
|
||||||
# If directory just changed, include directory name in announcement
|
|
||||||
if directoryChanged:
|
|
||||||
dirDescription = "Root directory" if currentDir == 'root' else currentDir
|
|
||||||
announcement = f"{dirDescription}: {displayName}, {currentSoundIndex + 1} of {totalSounds}"
|
|
||||||
directoryChanged = False # Reset flag after announcement
|
|
||||||
else:
|
|
||||||
announcement = f"{displayName}, {currentSoundIndex + 1} of {totalSounds}"
|
|
||||||
|
|
||||||
speech.speak(announcement)
|
|
||||||
lastSpoken = currentSoundIndex
|
|
||||||
|
|
||||||
event = pygame.event.wait()
|
|
||||||
if event.type == pygame.KEYDOWN:
|
|
||||||
if event.key == pygame.K_ESCAPE:
|
|
||||||
returnToMenu = True
|
|
||||||
|
|
||||||
# Sound navigation
|
|
||||||
elif event.key in [pygame.K_DOWN, pygame.K_s] and currentSoundIndex < len(currentSoundKeys) - 1:
|
|
||||||
pygame.mixer.stop()
|
|
||||||
currentSoundIndex += 1
|
|
||||||
|
|
||||||
elif event.key in [pygame.K_UP, pygame.K_w] and currentSoundIndex > 0:
|
|
||||||
pygame.mixer.stop()
|
|
||||||
currentSoundIndex -= 1
|
|
||||||
|
|
||||||
# Directory navigation
|
|
||||||
elif event.key in [pygame.K_PAGEDOWN, pygame.K_RIGHT] and currentDirIndex < len(directories) - 1:
|
|
||||||
pygame.mixer.stop()
|
|
||||||
currentDirIndex += 1
|
|
||||||
currentDir = directories[currentDirIndex]
|
|
||||||
currentSoundKeys = soundsByDir[currentDir]
|
|
||||||
currentSoundIndex = 0
|
|
||||||
directoryChanged = True # Set flag on directory change
|
|
||||||
lastSpoken = -1 # Force announcement
|
|
||||||
|
|
||||||
elif event.key in [pygame.K_PAGEUP, pygame.K_LEFT] and currentDirIndex > 0:
|
|
||||||
pygame.mixer.stop()
|
|
||||||
currentDirIndex -= 1
|
|
||||||
currentDir = directories[currentDirIndex]
|
|
||||||
currentSoundKeys = soundsByDir[currentDir]
|
|
||||||
currentSoundIndex = 0
|
|
||||||
directoryChanged = True # Set flag on directory change
|
|
||||||
lastSpoken = -1 # Force announcement
|
|
||||||
|
|
||||||
# Play sound
|
|
||||||
elif event.key == pygame.K_RETURN:
|
|
||||||
try:
|
|
||||||
soundName = currentSoundKeys[currentSoundIndex]
|
|
||||||
pygame.mixer.stop()
|
|
||||||
sounds[soundName].play()
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error playing sound: {e}")
|
|
||||||
speech.speak("Could not play sound.")
|
|
||||||
|
|
||||||
event = pygame.event.clear()
|
|
||||||
pygame.event.pump() # Process pygame's internal events
|
|
||||||
time.sleep(0.001)
|
|
||||||
|
|
||||||
return "menu"
|
|
||||||
|
|
||||||
def instructions():
|
|
||||||
"""Display game instructions from file.
|
|
||||||
|
|
||||||
Reads and displays instructions from 'files/instructions.txt'.
|
|
||||||
If file is missing, displays an error message.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
with open('files/instructions.txt', 'r') as f:
|
|
||||||
info = f.readlines()
|
|
||||||
except:
|
|
||||||
info = ["Instructions file is missing."]
|
|
||||||
display_text(info)
|
|
||||||
|
|
||||||
def credits():
|
|
||||||
"""Display game credits from file.
|
|
||||||
|
|
||||||
Reads and displays credits from 'files/credits.txt'.
|
|
||||||
Adds game name header before displaying.
|
|
||||||
If file is missing, displays an error message.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
with open('files/credits.txt', 'r') as f:
|
|
||||||
info = f.readlines()
|
|
||||||
|
|
||||||
pathService = PathService.get_instance()
|
|
||||||
info.insert(0, pathService.gameName + "\n")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error in credits: {e}")
|
|
||||||
info = ["Credits file is missing."]
|
|
||||||
|
|
||||||
display_text(info)
|
|
||||||
|
|
||||||
def donate():
|
|
||||||
"""Open the donation webpage.
|
|
||||||
|
|
||||||
Opens the Ko-fi donation page.
|
|
||||||
"""
|
|
||||||
webbrowser.open('https://ko-fi.com/stormux')
|
|
||||||
messagebox("The donation page has been opened in your browser.")
|
|
||||||
|
|
||||||
def exit_game(fade=0):
|
|
||||||
"""Clean up and exit the game properly.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
fade (int): Milliseconds to fade out music before exiting.
|
|
||||||
0 means stop immediately (default)
|
|
||||||
"""
|
|
||||||
# Force clear any pending events to prevent hanging
|
|
||||||
pygame.event.clear()
|
|
||||||
|
|
||||||
# Stop all mixer channels first
|
|
||||||
try:
|
|
||||||
pygame.mixer.stop()
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Warning: Could not stop mixer channels: {e}")
|
|
||||||
|
|
||||||
# Get speech instance and handle all providers
|
|
||||||
try:
|
|
||||||
speech = Speech.get_instance()
|
|
||||||
# Try to close speech regardless of provider type
|
|
||||||
try:
|
|
||||||
speech.close()
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Warning: Could not close speech: {e}")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Warning: Could not get speech instance: {e}")
|
|
||||||
|
|
||||||
# Handle music based on fade parameter
|
|
||||||
try:
|
|
||||||
if fade > 0 and pygame.mixer.music.get_busy():
|
|
||||||
pygame.mixer.music.fadeout(fade)
|
|
||||||
# Wait for fade to start but don't wait for full completion
|
|
||||||
pygame.time.wait(min(250, fade))
|
|
||||||
else:
|
|
||||||
pygame.mixer.music.stop()
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Warning: Could not handle music during exit: {e}")
|
|
||||||
|
|
||||||
# Clean up pygame
|
|
||||||
try:
|
|
||||||
pygame.quit()
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Warning: Error during pygame.quit(): {e}")
|
|
||||||
|
|
||||||
# Use os._exit for immediate termination
|
|
||||||
import os
|
|
||||||
os._exit(0)
|
|
@ -1,8 +0,0 @@
|
|||||||
pygame>=2.0.0
|
|
||||||
pyperclip>=1.8.0
|
|
||||||
requests>=2.25.0
|
|
||||||
pyxdg>=0.27
|
|
||||||
setproctitle>=1.2.0
|
|
||||||
numpy>=1.19.0
|
|
||||||
accessible-output2>=0.14
|
|
||||||
wxpython
|
|
@ -1,2 +0,0 @@
|
|||||||
-r requirements.txt
|
|
||||||
python-speechd>=0.11.1
|
|
332
scoreboard.py
332
scoreboard.py
@ -1,332 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
Modified Scoreboard class with integrated fixes.
|
|
||||||
|
|
||||||
This code should replace the existing Scoreboard class in scoreboard.py.
|
|
||||||
The modifications ensure proper path handling and config operations.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import time
|
|
||||||
import os
|
|
||||||
from .services import ConfigService, PathService
|
|
||||||
from .speech import Speech
|
|
||||||
from .display import display_text
|
|
||||||
|
|
||||||
# For backward compatibility
|
|
||||||
from .config import localConfig, write_config, read_config
|
|
||||||
|
|
||||||
class Scoreboard:
|
|
||||||
"""Handles high score tracking with player names."""
|
|
||||||
|
|
||||||
def __init__(self, score=0, configService=None, speech=None):
|
|
||||||
"""Initialize scoreboard.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
score (int): Initial score (default: 0)
|
|
||||||
configService (ConfigService): Config service (default: global instance)
|
|
||||||
speech (Speech): Speech system (default: global instance)
|
|
||||||
"""
|
|
||||||
# Ensure services are properly initialized
|
|
||||||
self._ensure_services()
|
|
||||||
|
|
||||||
self.configService = configService or ConfigService.get_instance()
|
|
||||||
self.speech = speech or Speech.get_instance()
|
|
||||||
self.currentScore = score
|
|
||||||
self.highScores = []
|
|
||||||
|
|
||||||
# For backward compatibility
|
|
||||||
read_config()
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Try to use configService
|
|
||||||
self.configService.localConfig.add_section("scoreboard")
|
|
||||||
except:
|
|
||||||
# Fallback to old method
|
|
||||||
try:
|
|
||||||
localConfig.add_section("scoreboard")
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Load existing high scores
|
|
||||||
for i in range(1, 11):
|
|
||||||
try:
|
|
||||||
# Try to use configService
|
|
||||||
score = self.configService.localConfig.getint("scoreboard", f"score_{i}")
|
|
||||||
name = self.configService.localConfig.get("scoreboard", f"name_{i}")
|
|
||||||
self.highScores.append({
|
|
||||||
'name': name,
|
|
||||||
'score': score
|
|
||||||
})
|
|
||||||
except:
|
|
||||||
# Fallback to old method
|
|
||||||
try:
|
|
||||||
score = localConfig.getint("scoreboard", f"score_{i}")
|
|
||||||
name = localConfig.get("scoreboard", f"name_{i}")
|
|
||||||
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 _ensure_services(self):
|
|
||||||
"""Ensure PathService and ConfigService are properly initialized."""
|
|
||||||
# Get PathService and make sure it has a game name
|
|
||||||
pathService = PathService.get_instance()
|
|
||||||
|
|
||||||
# If no game name yet, try to get from pygame window title
|
|
||||||
if not pathService.gameName:
|
|
||||||
try:
|
|
||||||
import pygame
|
|
||||||
if pygame.display.get_caption()[0]:
|
|
||||||
pathService.gameName = pygame.display.get_caption()[0]
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Initialize path service if we have a game name but no paths set up
|
|
||||||
if pathService.gameName and not pathService.gamePath:
|
|
||||||
pathService.initialize(pathService.gameName)
|
|
||||||
|
|
||||||
# Get ConfigService and connect to PathService
|
|
||||||
configService = ConfigService.get_instance()
|
|
||||||
if not hasattr(configService, 'pathService') or not configService.pathService:
|
|
||||||
if pathService.gameName:
|
|
||||||
configService.set_game_info(pathService.gameName, pathService)
|
|
||||||
|
|
||||||
# Ensure the game directory exists
|
|
||||||
if pathService.gamePath and not os.path.exists(pathService.gamePath):
|
|
||||||
try:
|
|
||||||
os.makedirs(pathService.gamePath)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error creating game directory: {e}")
|
|
||||||
|
|
||||||
def get_score(self):
|
|
||||||
"""Get current score."""
|
|
||||||
return self.currentScore
|
|
||||||
|
|
||||||
def get_high_scores(self):
|
|
||||||
"""Get list of high scores."""
|
|
||||||
return self.highScores
|
|
||||||
|
|
||||||
def decrease_score(self, points=1):
|
|
||||||
"""Decrease the current score."""
|
|
||||||
self.currentScore -= int(points)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def increase_score(self, points=1):
|
|
||||||
"""Increase the current score."""
|
|
||||||
self.currentScore += int(points)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def set_score(self, score):
|
|
||||||
"""Set the current score to a specific value."""
|
|
||||||
self.currentScore = int(score)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def reset_score(self):
|
|
||||||
"""Reset the current score to zero."""
|
|
||||||
self.currentScore = 0
|
|
||||||
return self
|
|
||||||
|
|
||||||
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, name=None):
|
|
||||||
"""Add current score to high scores if it qualifies.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
name (str): Player name (if None, will prompt user)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if score was added, False if not
|
|
||||||
"""
|
|
||||||
# Ensure services are properly set up
|
|
||||||
self._ensure_services()
|
|
||||||
|
|
||||||
position = self.check_high_score()
|
|
||||||
if position is None:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Get player name
|
|
||||||
if name is None:
|
|
||||||
# Import get_input here to avoid circular imports
|
|
||||||
from .input import get_input
|
|
||||||
name = 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 - try both methods for maximum compatibility
|
|
||||||
try:
|
|
||||||
# Try new method first
|
|
||||||
for i, entry in enumerate(self.highScores):
|
|
||||||
self.configService.localConfig.set("scoreboard", f"score_{i+1}", str(entry['score']))
|
|
||||||
self.configService.localConfig.set("scoreboard", f"name_{i+1}", entry['name'])
|
|
||||||
|
|
||||||
# Try to write with configService
|
|
||||||
try:
|
|
||||||
self.configService.write_local_config()
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error writing config with configService: {e}")
|
|
||||||
# Fallback to old method if configService fails
|
|
||||||
for i, entry in enumerate(self.highScores):
|
|
||||||
localConfig.set("scoreboard", f"score_{i+1}", str(entry['score']))
|
|
||||||
localConfig.set("scoreboard", f"name_{i+1}", entry['name'])
|
|
||||||
write_config()
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error writing high scores: {e}")
|
|
||||||
# If all else fails, try direct old method
|
|
||||||
for i, entry in enumerate(self.highScores):
|
|
||||||
localConfig.set("scoreboard", f"score_{i+1}", str(entry['score']))
|
|
||||||
localConfig.set("scoreboard", f"name_{i+1}", entry['name'])
|
|
||||||
write_config()
|
|
||||||
|
|
||||||
# Announce success
|
|
||||||
try:
|
|
||||||
self.speech.messagebox(f"Congratulations {name}! You got position {position} on the scoreboard!")
|
|
||||||
except:
|
|
||||||
# Fallback to global speak function
|
|
||||||
from .speech import speak
|
|
||||||
speak(f"Congratulations {name}! You got position {position} on the scoreboard!")
|
|
||||||
|
|
||||||
time.sleep(1)
|
|
||||||
return True
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def has_high_scores():
|
|
||||||
"""Check if the current game has any high scores.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if at least one high score exists, False otherwise
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# Get PathService to access game name
|
|
||||||
pathService = PathService.get_instance()
|
|
||||||
gameName = pathService.gameName
|
|
||||||
|
|
||||||
# If no game name, try to get from window title
|
|
||||||
if not gameName:
|
|
||||||
try:
|
|
||||||
import pygame
|
|
||||||
if pygame.display.get_caption()[0]:
|
|
||||||
gameName = pygame.display.get_caption()[0]
|
|
||||||
pathService.gameName = gameName
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Ensure path service is properly initialized
|
|
||||||
if gameName and not pathService.gamePath:
|
|
||||||
pathService.initialize(gameName)
|
|
||||||
|
|
||||||
# Get the config file path
|
|
||||||
configPath = os.path.join(pathService.gamePath, "config.ini")
|
|
||||||
|
|
||||||
# If config file doesn't exist, there are no scores
|
|
||||||
if not os.path.exists(configPath):
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Ensure config service is properly connected to path service
|
|
||||||
configService = ConfigService.get_instance()
|
|
||||||
configService.set_game_info(gameName, pathService)
|
|
||||||
|
|
||||||
# Create scoreboard using the properly initialized services
|
|
||||||
board = Scoreboard(0, configService)
|
|
||||||
|
|
||||||
# Force a read of local config to ensure fresh data
|
|
||||||
configService.read_local_config()
|
|
||||||
|
|
||||||
# Get high scores
|
|
||||||
scores = board.get_high_scores()
|
|
||||||
|
|
||||||
# Check if any score is greater than zero
|
|
||||||
return any(score['score'] > 0 for score in scores)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error checking high scores: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def display_high_scores():
|
|
||||||
"""Display high scores for the current game.
|
|
||||||
|
|
||||||
Reads the high scores from Scoreboard class.
|
|
||||||
Shows the game name at the top followed by the available scores.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# Get PathService to access game name
|
|
||||||
pathService = PathService.get_instance()
|
|
||||||
gameName = pathService.gameName
|
|
||||||
|
|
||||||
# If no game name, try to get from window title
|
|
||||||
if not gameName:
|
|
||||||
try:
|
|
||||||
import pygame
|
|
||||||
if pygame.display.get_caption()[0]:
|
|
||||||
gameName = pygame.display.get_caption()[0]
|
|
||||||
pathService.gameName = gameName
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Ensure path service is properly initialized
|
|
||||||
if gameName and not pathService.gamePath:
|
|
||||||
pathService.initialize(gameName)
|
|
||||||
|
|
||||||
# Ensure config service is properly connected to path service
|
|
||||||
configService = ConfigService.get_instance()
|
|
||||||
configService.set_game_info(gameName, pathService)
|
|
||||||
|
|
||||||
# Create scoreboard using the properly initialized services
|
|
||||||
board = Scoreboard(0, configService)
|
|
||||||
|
|
||||||
# Force a read of local config to ensure fresh data
|
|
||||||
configService.read_local_config()
|
|
||||||
|
|
||||||
# Get high scores
|
|
||||||
scores = board.get_high_scores()
|
|
||||||
|
|
||||||
# Filter out scores with zero points
|
|
||||||
validScores = [score for score in scores if score['score'] > 0]
|
|
||||||
|
|
||||||
# Prepare the lines to display
|
|
||||||
lines = [f"High Scores for {gameName}:"]
|
|
||||||
|
|
||||||
# Add scores to the display list
|
|
||||||
if validScores:
|
|
||||||
for i, entry in enumerate(validScores, 1):
|
|
||||||
scoreStr = f"{i}. {entry['name']}: {entry['score']}"
|
|
||||||
lines.append(scoreStr)
|
|
||||||
else:
|
|
||||||
lines.append("No high scores yet.")
|
|
||||||
|
|
||||||
# Display the high scores
|
|
||||||
display_text(lines)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error displaying high scores: {e}")
|
|
||||||
info = ["Could not display high scores."]
|
|
||||||
display_text(info)
|
|
||||||
|
|
||||||
# For backward compatibility with older code that might call displayHigh_scores
|
|
||||||
displayHigh_scores = display_high_scores
|
|
274
services.py
274
services.py
@ -1,274 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""Service classes for Storm Games.
|
|
||||||
|
|
||||||
Provides centralized services to replace global variables:
|
|
||||||
- ConfigService: Manages game configuration
|
|
||||||
- VolumeService: Handles volume settings
|
|
||||||
- PathService: Manages file paths
|
|
||||||
"""
|
|
||||||
|
|
||||||
import configparser
|
|
||||||
import os
|
|
||||||
from xdg import BaseDirectory
|
|
||||||
|
|
||||||
# For backward compatibility
|
|
||||||
from .config import gamePath, globalPath, write_config, read_config
|
|
||||||
|
|
||||||
class ConfigService:
|
|
||||||
"""Configuration management service."""
|
|
||||||
|
|
||||||
_instance = None
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_instance(cls):
|
|
||||||
"""Get or create the singleton instance."""
|
|
||||||
if cls._instance is None:
|
|
||||||
cls._instance = ConfigService()
|
|
||||||
return cls._instance
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
"""Initialize configuration parsers."""
|
|
||||||
self.localConfig = configparser.ConfigParser()
|
|
||||||
self.globalConfig = configparser.ConfigParser()
|
|
||||||
self.gameTitle = None
|
|
||||||
self.pathService = None
|
|
||||||
|
|
||||||
def set_game_info(self, gameTitle, pathService):
|
|
||||||
"""Set game information and initialize configs.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
gameTitle (str): Title of the game
|
|
||||||
pathService (PathService): Path service instance
|
|
||||||
"""
|
|
||||||
self.gameTitle = gameTitle
|
|
||||||
self.pathService = pathService
|
|
||||||
|
|
||||||
# Load existing configurations
|
|
||||||
self.read_local_config()
|
|
||||||
self.read_global_config()
|
|
||||||
|
|
||||||
def read_local_config(self):
|
|
||||||
"""Read local configuration from file."""
|
|
||||||
try:
|
|
||||||
# Try to use pathService if available
|
|
||||||
if self.pathService and self.pathService.gamePath:
|
|
||||||
with open(os.path.join(self.pathService.gamePath, "config.ini"), 'r') as configFile:
|
|
||||||
self.localConfig.read_file(configFile)
|
|
||||||
# Fallback to global gamePath
|
|
||||||
elif gamePath:
|
|
||||||
with open(os.path.join(gamePath, "config.ini"), 'r') as configFile:
|
|
||||||
self.localConfig.read_file(configFile)
|
|
||||||
# Delegate to old function as last resort
|
|
||||||
else:
|
|
||||||
read_config(False)
|
|
||||||
self.localConfig = configparser.ConfigParser()
|
|
||||||
self.localConfig.read_dict(globals().get('localConfig', {}))
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def read_global_config(self):
|
|
||||||
"""Read global configuration from file."""
|
|
||||||
try:
|
|
||||||
# Try to use pathService if available
|
|
||||||
if self.pathService and self.pathService.globalPath:
|
|
||||||
with open(os.path.join(self.pathService.globalPath, "config.ini"), 'r') as configFile:
|
|
||||||
self.globalConfig.read_file(configFile)
|
|
||||||
# Fallback to global globalPath
|
|
||||||
elif globalPath:
|
|
||||||
with open(os.path.join(globalPath, "config.ini"), 'r') as configFile:
|
|
||||||
self.globalConfig.read_file(configFile)
|
|
||||||
# Delegate to old function as last resort
|
|
||||||
else:
|
|
||||||
read_config(True)
|
|
||||||
self.globalConfig = configparser.ConfigParser()
|
|
||||||
self.globalConfig.read_dict(globals().get('globalConfig', {}))
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def write_local_config(self):
|
|
||||||
"""Write local configuration to file."""
|
|
||||||
try:
|
|
||||||
# Try to use pathService if available
|
|
||||||
if self.pathService and self.pathService.gamePath:
|
|
||||||
with open(os.path.join(self.pathService.gamePath, "config.ini"), 'w') as configFile:
|
|
||||||
self.localConfig.write(configFile)
|
|
||||||
# Fallback to global gamePath
|
|
||||||
elif gamePath:
|
|
||||||
with open(os.path.join(gamePath, "config.ini"), 'w') as configFile:
|
|
||||||
self.localConfig.write(configFile)
|
|
||||||
# Delegate to old function as last resort
|
|
||||||
else:
|
|
||||||
# Update old global config
|
|
||||||
globals()['localConfig'] = self.localConfig
|
|
||||||
write_config(False)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Warning: Failed to write local config: {e}")
|
|
||||||
|
|
||||||
def write_global_config(self):
|
|
||||||
"""Write global configuration to file."""
|
|
||||||
try:
|
|
||||||
# Try to use pathService if available
|
|
||||||
if self.pathService and self.pathService.globalPath:
|
|
||||||
with open(os.path.join(self.pathService.globalPath, "config.ini"), 'w') as configFile:
|
|
||||||
self.globalConfig.write(configFile)
|
|
||||||
# Fallback to global globalPath
|
|
||||||
elif globalPath:
|
|
||||||
with open(os.path.join(globalPath, "config.ini"), 'w') as configFile:
|
|
||||||
self.globalConfig.write(configFile)
|
|
||||||
# Delegate to old function as last resort
|
|
||||||
else:
|
|
||||||
# Update old global config
|
|
||||||
globals()['globalConfig'] = self.globalConfig
|
|
||||||
write_config(True)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Warning: Failed to write global config: {e}")
|
|
||||||
|
|
||||||
|
|
||||||
class VolumeService:
|
|
||||||
"""Volume management service."""
|
|
||||||
|
|
||||||
_instance = None
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_instance(cls):
|
|
||||||
"""Get or create the singleton instance."""
|
|
||||||
if cls._instance is None:
|
|
||||||
cls._instance = VolumeService()
|
|
||||||
return cls._instance
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
"""Initialize volume settings."""
|
|
||||||
self.bgmVolume = 0.75 # Default background music volume
|
|
||||||
self.sfxVolume = 1.0 # Default sound effects volume
|
|
||||||
self.masterVolume = 1.0 # Default master volume
|
|
||||||
|
|
||||||
def adjust_master_volume(self, change, pygameMixer=None):
|
|
||||||
"""Adjust the master volume for all sounds.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
change (float): Amount to change volume by (positive or negative)
|
|
||||||
pygameMixer: Optional pygame.mixer module for real-time updates
|
|
||||||
"""
|
|
||||||
self.masterVolume = max(0.0, min(1.0, self.masterVolume + change))
|
|
||||||
|
|
||||||
# Update real-time audio if pygame mixer is provided
|
|
||||||
if pygameMixer:
|
|
||||||
# Update music volume
|
|
||||||
if pygameMixer.music.get_busy():
|
|
||||||
pygameMixer.music.set_volume(self.bgmVolume * self.masterVolume)
|
|
||||||
|
|
||||||
# Update all sound channels
|
|
||||||
for i in range(pygameMixer.get_num_channels()):
|
|
||||||
channel = pygameMixer.Channel(i)
|
|
||||||
if channel.get_busy():
|
|
||||||
currentVolume = channel.get_volume()
|
|
||||||
if isinstance(currentVolume, (int, float)):
|
|
||||||
# Mono audio
|
|
||||||
channel.set_volume(currentVolume * self.masterVolume)
|
|
||||||
else:
|
|
||||||
# Stereo audio
|
|
||||||
left, right = currentVolume
|
|
||||||
channel.set_volume(left * self.masterVolume, right * self.masterVolume)
|
|
||||||
|
|
||||||
def adjust_bgm_volume(self, change, pygameMixer=None):
|
|
||||||
"""Adjust only the background music volume.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
change (float): Amount to change volume by (positive or negative)
|
|
||||||
pygameMixer: Optional pygame.mixer module for real-time updates
|
|
||||||
"""
|
|
||||||
self.bgmVolume = max(0.0, min(1.0, self.bgmVolume + change))
|
|
||||||
|
|
||||||
# Update real-time audio if pygame mixer is provided
|
|
||||||
if pygameMixer and pygameMixer.music.get_busy():
|
|
||||||
pygameMixer.music.set_volume(self.bgmVolume * self.masterVolume)
|
|
||||||
|
|
||||||
def adjust_sfx_volume(self, change, pygameMixer=None):
|
|
||||||
"""Adjust volume for sound effects only.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
change (float): Amount to change volume by (positive or negative)
|
|
||||||
pygameMixer: Optional pygame.mixer module for real-time updates
|
|
||||||
"""
|
|
||||||
self.sfxVolume = max(0.0, min(1.0, self.sfxVolume + change))
|
|
||||||
|
|
||||||
# Update real-time audio if pygame mixer is provided
|
|
||||||
if pygameMixer:
|
|
||||||
# Update all sound channels except reserved ones
|
|
||||||
for i in range(pygameMixer.get_num_channels()):
|
|
||||||
channel = pygameMixer.Channel(i)
|
|
||||||
if channel.get_busy():
|
|
||||||
currentVolume = channel.get_volume()
|
|
||||||
if isinstance(currentVolume, (int, float)):
|
|
||||||
# Mono audio
|
|
||||||
channel.set_volume(currentVolume * self.sfxVolume * self.masterVolume)
|
|
||||||
else:
|
|
||||||
# Stereo audio
|
|
||||||
left, right = currentVolume
|
|
||||||
channel.set_volume(left * self.sfxVolume * self.masterVolume,
|
|
||||||
right * self.sfxVolume * self.masterVolume)
|
|
||||||
|
|
||||||
def get_bgm_volume(self):
|
|
||||||
"""Get the current BGM volume with master adjustment.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
float: Current adjusted BGM volume
|
|
||||||
"""
|
|
||||||
return self.bgmVolume * self.masterVolume
|
|
||||||
|
|
||||||
def get_sfx_volume(self):
|
|
||||||
"""Get the current SFX volume with master adjustment.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
float: Current adjusted SFX volume
|
|
||||||
"""
|
|
||||||
return self.sfxVolume * self.masterVolume
|
|
||||||
|
|
||||||
|
|
||||||
class PathService:
|
|
||||||
"""Path management service."""
|
|
||||||
|
|
||||||
_instance = None
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_instance(cls):
|
|
||||||
"""Get or create the singleton instance."""
|
|
||||||
if cls._instance is None:
|
|
||||||
cls._instance = PathService()
|
|
||||||
return cls._instance
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
"""Initialize path variables."""
|
|
||||||
self.globalPath = None
|
|
||||||
self.gamePath = None
|
|
||||||
self.gameName = None
|
|
||||||
|
|
||||||
# Try to initialize from global variables for backward compatibility
|
|
||||||
global gamePath, globalPath
|
|
||||||
if gamePath:
|
|
||||||
self.gamePath = gamePath
|
|
||||||
if globalPath:
|
|
||||||
self.globalPath = globalPath
|
|
||||||
|
|
||||||
def initialize(self, gameTitle):
|
|
||||||
"""Initialize paths for a game.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
gameTitle (str): Title of the game
|
|
||||||
"""
|
|
||||||
self.gameName = gameTitle
|
|
||||||
self.globalPath = os.path.join(BaseDirectory.xdg_config_home, "storm-games")
|
|
||||||
self.gamePath = os.path.join(self.globalPath,
|
|
||||||
str.lower(str.replace(gameTitle, " ", "-")))
|
|
||||||
|
|
||||||
# Create game directory if it doesn't exist
|
|
||||||
if not os.path.exists(self.gamePath):
|
|
||||||
os.makedirs(self.gamePath)
|
|
||||||
|
|
||||||
# Update global variables for backward compatibility
|
|
||||||
global gamePath, globalPath
|
|
||||||
gamePath = self.gamePath
|
|
||||||
globalPath = self.globalPath
|
|
||||||
|
|
||||||
return self
|
|
528
sound.py
528
sound.py
@ -1,528 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""Sound handling for Storm Games.
|
|
||||||
|
|
||||||
Provides functionality for:
|
|
||||||
- Playing background music and sound effects
|
|
||||||
- 2D positional audio (x,y)
|
|
||||||
- Volume controls
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import pygame
|
|
||||||
import random
|
|
||||||
import re
|
|
||||||
import time
|
|
||||||
import math
|
|
||||||
from .services import VolumeService
|
|
||||||
|
|
||||||
# Global instance for backward compatibility
|
|
||||||
volumeService = VolumeService.get_instance()
|
|
||||||
|
|
||||||
class Sound:
|
|
||||||
"""Handles sound loading and playback."""
|
|
||||||
|
|
||||||
def __init__(self, soundDir="sounds/", volumeService=None):
|
|
||||||
"""Initialize sound system."""
|
|
||||||
self.soundDir = soundDir
|
|
||||||
self.sounds = {}
|
|
||||||
self.volumeService = volumeService or VolumeService.get_instance()
|
|
||||||
|
|
||||||
if not pygame.mixer.get_init():
|
|
||||||
pygame.mixer.pre_init(44100, -16, 2, 1024)
|
|
||||||
pygame.mixer.init()
|
|
||||||
pygame.mixer.set_num_channels(32)
|
|
||||||
pygame.mixer.set_reserved(0)
|
|
||||||
|
|
||||||
self.load_sounds()
|
|
||||||
|
|
||||||
def load_sounds(self):
|
|
||||||
"""Load all sound files from the sound directory and its subdirectories."""
|
|
||||||
try:
|
|
||||||
for dirPath, _, fileNames in os.walk(self.soundDir):
|
|
||||||
relPath = os.path.relpath(dirPath, self.soundDir)
|
|
||||||
|
|
||||||
for fileName in fileNames:
|
|
||||||
if fileName.lower().endswith(('.ogg', '.wav')):
|
|
||||||
fullPath = os.path.join(dirPath, fileName)
|
|
||||||
baseName = os.path.splitext(fileName)[0]
|
|
||||||
|
|
||||||
soundKey = baseName if relPath == '.' else os.path.join(relPath, baseName).replace('\\', '/')
|
|
||||||
self.sounds[soundKey] = pygame.mixer.Sound(fullPath)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error loading sounds: {e}")
|
|
||||||
|
|
||||||
def _find_matching_sound(self, pattern):
|
|
||||||
"""Find a random sound matching the pattern."""
|
|
||||||
keys = [k for k in self.sounds.keys() if re.match("^" + pattern + ".*", k)]
|
|
||||||
return random.choice(keys) if keys else None
|
|
||||||
|
|
||||||
def _handle_cutscene(self, soundName):
|
|
||||||
"""Play a sound as a cut scene."""
|
|
||||||
pygame.event.clear()
|
|
||||||
pygame.mixer.stop()
|
|
||||||
|
|
||||||
channel = pygame.mixer.Channel(0)
|
|
||||||
sfxVolume = self.volumeService.get_sfx_volume()
|
|
||||||
channel.set_volume(sfxVolume, sfxVolume)
|
|
||||||
|
|
||||||
channel.play(self.sounds[soundName])
|
|
||||||
|
|
||||||
while pygame.mixer.get_busy():
|
|
||||||
for event in pygame.event.get():
|
|
||||||
if event.type == pygame.KEYDOWN and event.key in [pygame.K_ESCAPE, pygame.K_RETURN, pygame.K_SPACE]:
|
|
||||||
pygame.mixer.stop()
|
|
||||||
return None
|
|
||||||
pygame.time.delay(10)
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _get_stereo_panning(self, playerPos, objPos, centerDistance=None):
|
|
||||||
"""Calculate stereo panning based on positions."""
|
|
||||||
# Extract x-positions
|
|
||||||
playerX = playerPos[0] if isinstance(playerPos, (tuple, list)) else playerPos
|
|
||||||
objX = objPos[0] if isinstance(objPos, (tuple, list)) else objPos
|
|
||||||
|
|
||||||
# For directional sound with fixed distance
|
|
||||||
if centerDistance is not None:
|
|
||||||
if abs(playerX - objX) <= centerDistance:
|
|
||||||
return (1, 1) # Center
|
|
||||||
elif playerX > objX:
|
|
||||||
return (1, 0.505) # Left
|
|
||||||
else:
|
|
||||||
return (0.505, 1) # Right
|
|
||||||
|
|
||||||
# Calculate regular panning
|
|
||||||
volume, left, right = self.calculate_volume_and_pan(playerPos, objPos)
|
|
||||||
return (volume * left, volume * right) if volume > 0 else (0, 0)
|
|
||||||
|
|
||||||
def play_sound(self, soundName, volume=1.0, loop=False, playerPos=None, objPos=None,
|
|
||||||
centerDistance=None, pattern=False, interrupt=False, pause=False, cutScene=False):
|
|
||||||
"""Unified method to play sounds with various options."""
|
|
||||||
# Resolve sound name if pattern matching is requested
|
|
||||||
if pattern:
|
|
||||||
soundName = self._find_matching_sound(soundName)
|
|
||||||
if not soundName:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Check if sound exists
|
|
||||||
if soundName not in self.sounds:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Handle cut scene mode
|
|
||||||
if cutScene:
|
|
||||||
return self._handle_cutscene(soundName)
|
|
||||||
|
|
||||||
# Handle interrupt (stop other sounds)
|
|
||||||
if interrupt:
|
|
||||||
pygame.event.clear()
|
|
||||||
pygame.mixer.stop()
|
|
||||||
|
|
||||||
# Play the sound
|
|
||||||
channel = self.sounds[soundName].play(-1 if loop else 0)
|
|
||||||
if not channel:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Apply appropriate volume settings
|
|
||||||
sfx_volume = self.volumeService.get_sfx_volume()
|
|
||||||
|
|
||||||
# Handle positional audio if positions are provided
|
|
||||||
if playerPos is not None and objPos is not None:
|
|
||||||
# Calculate stereo panning
|
|
||||||
left_vol, right_vol = self._get_stereo_panning(playerPos, objPos, centerDistance)
|
|
||||||
|
|
||||||
# Don't play if out of range
|
|
||||||
if left_vol == 0 and right_vol == 0:
|
|
||||||
channel.stop()
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Apply positional volume adjustments
|
|
||||||
channel.set_volume(volume * left_vol * sfx_volume, volume * right_vol * sfx_volume)
|
|
||||||
else:
|
|
||||||
# Non-positional sound
|
|
||||||
channel.set_volume(volume * sfx_volume)
|
|
||||||
|
|
||||||
# Pause execution if requested
|
|
||||||
if pause:
|
|
||||||
time.sleep(self.sounds[soundName].get_length())
|
|
||||||
|
|
||||||
return channel
|
|
||||||
|
|
||||||
def calculate_volume_and_pan(self, playerPos, objPos, maxDistance=12):
|
|
||||||
"""Calculate volume and stereo panning based on relative positions."""
|
|
||||||
# Determine if we're using 2D or 1D positioning
|
|
||||||
if isinstance(playerPos, (tuple, list)) and isinstance(objPos, (tuple, list)):
|
|
||||||
# 2D distance calculation
|
|
||||||
distance = math.sqrt((playerPos[0] - objPos[0])**2 + (playerPos[1] - objPos[1])**2)
|
|
||||||
playerX, objX = playerPos[0], objPos[0]
|
|
||||||
else:
|
|
||||||
# 1D calculation (backward compatible)
|
|
||||||
distance = abs(playerPos - objPos)
|
|
||||||
playerX, objX = playerPos, objPos
|
|
||||||
|
|
||||||
if distance > maxDistance:
|
|
||||||
return 0, 0, 0 # No sound if out of range
|
|
||||||
|
|
||||||
# Calculate volume (non-linear scaling for more noticeable changes)
|
|
||||||
volume = (((maxDistance - distance) / maxDistance) ** 1.5) * self.volumeService.masterVolume
|
|
||||||
|
|
||||||
# Determine left/right based on relative position
|
|
||||||
if playerX < objX: # Object is to the right
|
|
||||||
left = max(0, 1 - (objX - playerX) / maxDistance)
|
|
||||||
right = 1
|
|
||||||
elif playerX > objX: # Object is to the left
|
|
||||||
left = 1
|
|
||||||
right = max(0, 1 - (playerX - objX) / maxDistance)
|
|
||||||
else: # Player is on the object
|
|
||||||
left = right = 1
|
|
||||||
|
|
||||||
return volume, left, right
|
|
||||||
|
|
||||||
def update_sound_position(self, channel, playerPos, objPos):
|
|
||||||
"""Update positional audio for a playing sound."""
|
|
||||||
if not channel:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Calculate new stereo panning
|
|
||||||
left_vol, right_vol = self._get_stereo_panning(playerPos, objPos)
|
|
||||||
|
|
||||||
# Stop if out of range
|
|
||||||
if left_vol == 0 and right_vol == 0:
|
|
||||||
channel.stop()
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Apply the volume and pan
|
|
||||||
channel.set_volume(left_vol * self.volumeService.sfxVolume, right_vol * self.volumeService.sfxVolume)
|
|
||||||
return channel
|
|
||||||
|
|
||||||
def stop_sound(self, channel):
|
|
||||||
"""Stop a playing sound channel."""
|
|
||||||
if channel:
|
|
||||||
try:
|
|
||||||
channel.stop()
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
return None
|
|
||||||
|
|
||||||
def play_falling_sound(self, soundPrefix, playerPos, objPos, startY, currentY=0, maxY=20, existingChannel=None):
|
|
||||||
"""Play or update a sound with positional audio that changes with height."""
|
|
||||||
# Extract positions
|
|
||||||
playerX = playerPos[0] if isinstance(playerPos, (tuple, list)) else playerPos
|
|
||||||
objX = objPos[0] if isinstance(objPos, (tuple, list)) else objPos
|
|
||||||
|
|
||||||
# Calculate volumes
|
|
||||||
volume, left, right = self.calculate_volume_and_pan(playerX, objX)
|
|
||||||
|
|
||||||
# Apply vertical fall multiplier (0 at maxY, 1 at y=0)
|
|
||||||
fallMultiplier = 1 - (currentY / maxY)
|
|
||||||
finalVolume = volume * fallMultiplier
|
|
||||||
finalLeft = left * finalVolume
|
|
||||||
finalRight = right * finalVolume
|
|
||||||
|
|
||||||
# Update existing channel or create new one
|
|
||||||
if existingChannel:
|
|
||||||
if volume == 0:
|
|
||||||
existingChannel.stop()
|
|
||||||
return None
|
|
||||||
existingChannel.set_volume(
|
|
||||||
finalLeft * self.volumeService.sfxVolume,
|
|
||||||
finalRight * self.volumeService.sfxVolume
|
|
||||||
)
|
|
||||||
return existingChannel
|
|
||||||
else:
|
|
||||||
if volume == 0:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Find a matching sound
|
|
||||||
soundName = self._find_matching_sound(soundPrefix)
|
|
||||||
if not soundName:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Play the sound
|
|
||||||
channel = self.sounds[soundName].play()
|
|
||||||
if channel:
|
|
||||||
channel.set_volume(
|
|
||||||
finalLeft * self.volumeService.sfxVolume,
|
|
||||||
finalRight * self.volumeService.sfxVolume
|
|
||||||
)
|
|
||||||
return channel
|
|
||||||
|
|
||||||
def play_bgm(self, musicFile):
|
|
||||||
"""Play background music with proper volume settings."""
|
|
||||||
try:
|
|
||||||
pygame.mixer.music.stop()
|
|
||||||
pygame.mixer.music.load(musicFile)
|
|
||||||
pygame.mixer.music.set_volume(self.volumeService.get_bgm_volume())
|
|
||||||
pygame.mixer.music.play(-1)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error playing background music: {e}")
|
|
||||||
|
|
||||||
def adjust_master_volume(self, change):
|
|
||||||
"""Adjust the master volume for all sounds."""
|
|
||||||
self.volumeService.adjust_master_volume(change, pygame.mixer)
|
|
||||||
|
|
||||||
def adjust_bgm_volume(self, change):
|
|
||||||
"""Adjust only the background music volume."""
|
|
||||||
self.volumeService.adjust_bgm_volume(change, pygame.mixer)
|
|
||||||
|
|
||||||
def adjust_sfx_volume(self, change):
|
|
||||||
"""Adjust volume for sound effects only."""
|
|
||||||
self.volumeService.adjust_sfx_volume(change, pygame.mixer)
|
|
||||||
|
|
||||||
|
|
||||||
# Optimized helper functions for global use
|
|
||||||
def _get_stereo_panning(playerPos, objPos, centerDistance=None, maxDistance=12):
|
|
||||||
"""Simplified panning calculation."""
|
|
||||||
# Extract x-positions
|
|
||||||
playerX = playerPos[0] if isinstance(playerPos, (tuple, list)) else playerPos
|
|
||||||
objX = objPos[0] if isinstance(objPos, (tuple, list)) else objPos
|
|
||||||
|
|
||||||
# For directional sound with fixed distance
|
|
||||||
if centerDistance is not None:
|
|
||||||
if abs(playerX - objX) <= centerDistance:
|
|
||||||
return (1, 1) # Center
|
|
||||||
elif playerX > objX:
|
|
||||||
return (1, 0.505) # Left
|
|
||||||
else:
|
|
||||||
return (0.505, 1) # Right
|
|
||||||
|
|
||||||
# Calculate distance
|
|
||||||
if isinstance(playerPos, (tuple, list)) and isinstance(objPos, (tuple, list)):
|
|
||||||
distance = math.sqrt((playerPos[0] - objPos[0])**2 + (playerPos[1] - objPos[1])**2)
|
|
||||||
else:
|
|
||||||
distance = abs(playerPos - objPos)
|
|
||||||
|
|
||||||
if distance > maxDistance:
|
|
||||||
return (0, 0) # No sound if out of range
|
|
||||||
|
|
||||||
# Calculate volume (non-linear scaling for more noticeable changes)
|
|
||||||
volume = (((maxDistance - distance) / maxDistance) ** 1.5) * volumeService.masterVolume
|
|
||||||
|
|
||||||
# Determine left/right based on relative position
|
|
||||||
if playerX < objX: # Object is to the right
|
|
||||||
left = max(0, 1 - (objX - playerX) / maxDistance)
|
|
||||||
right = 1
|
|
||||||
elif playerX > objX: # Object is to the left
|
|
||||||
left = 1
|
|
||||||
right = max(0, 1 - (playerX - objX) / maxDistance)
|
|
||||||
else: # Player is on the object
|
|
||||||
left = right = 1
|
|
||||||
|
|
||||||
return (volume * left, volume * right)
|
|
||||||
|
|
||||||
def _play_cutscene(sound, sounds=None):
|
|
||||||
"""Play a sound as a cut scene."""
|
|
||||||
pygame.event.clear()
|
|
||||||
pygame.mixer.stop()
|
|
||||||
|
|
||||||
channel = pygame.mixer.Channel(0)
|
|
||||||
sfxVolume = volumeService.get_sfx_volume()
|
|
||||||
channel.set_volume(sfxVolume, sfxVolume)
|
|
||||||
|
|
||||||
# Determine which sound to play
|
|
||||||
if isinstance(sound, pygame.mixer.Sound):
|
|
||||||
channel.play(sound)
|
|
||||||
elif isinstance(sounds, dict) and sound in sounds:
|
|
||||||
channel.play(sounds[sound])
|
|
||||||
elif isinstance(sounds, Sound) and sound in sounds.sounds:
|
|
||||||
channel.play(sounds.sounds[sound])
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Wait for completion or key press
|
|
||||||
while pygame.mixer.get_busy():
|
|
||||||
for event in pygame.event.get():
|
|
||||||
if event.type == pygame.KEYDOWN and event.key in [pygame.K_ESCAPE, pygame.K_RETURN, pygame.K_SPACE]:
|
|
||||||
pygame.mixer.stop()
|
|
||||||
return None
|
|
||||||
pygame.time.delay(10)
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _find_matching_sound(soundPattern, sounds):
|
|
||||||
"""Find sounds matching a pattern in a dictionary."""
|
|
||||||
if isinstance(sounds, Sound):
|
|
||||||
keys = [k for k in sounds.sounds.keys() if re.match("^" + soundPattern + ".*", k)]
|
|
||||||
else:
|
|
||||||
keys = [k for k in sounds.keys() if re.match("^" + soundPattern + ".*", k)]
|
|
||||||
return random.choice(keys) if keys else None
|
|
||||||
|
|
||||||
# Global functions for backward compatibility
|
|
||||||
def play_bgm(musicFile):
|
|
||||||
"""Play background music with proper volume settings."""
|
|
||||||
try:
|
|
||||||
pygame.mixer.music.stop()
|
|
||||||
pygame.mixer.music.load(musicFile)
|
|
||||||
pygame.mixer.music.set_volume(volumeService.get_bgm_volume())
|
|
||||||
pygame.mixer.music.play(-1)
|
|
||||||
except: pass
|
|
||||||
|
|
||||||
def adjust_master_volume(change):
|
|
||||||
"""Adjust the master volume."""
|
|
||||||
volumeService.adjust_master_volume(change, pygame.mixer)
|
|
||||||
|
|
||||||
def adjust_bgm_volume(change):
|
|
||||||
"""Adjust background music volume."""
|
|
||||||
volumeService.adjust_bgm_volume(change, pygame.mixer)
|
|
||||||
|
|
||||||
def adjust_sfx_volume(change):
|
|
||||||
"""Adjust sound effects volume."""
|
|
||||||
volumeService.adjust_sfx_volume(change, pygame.mixer)
|
|
||||||
|
|
||||||
def calculate_volume_and_pan(playerPos, objPos, maxDistance=12):
|
|
||||||
"""Calculate volume and stereo panning."""
|
|
||||||
left_vol, right_vol = _get_stereo_panning(playerPos, objPos, None, maxDistance)
|
|
||||||
# Convert to old format (volume, left, right)
|
|
||||||
if left_vol == 0 and right_vol == 0:
|
|
||||||
return 0, 0, 0
|
|
||||||
elif left_vol >= right_vol:
|
|
||||||
volume = left_vol
|
|
||||||
return volume, 1, right_vol/left_vol
|
|
||||||
else:
|
|
||||||
volume = right_vol
|
|
||||||
return volume, left_vol/right_vol, 1
|
|
||||||
|
|
||||||
def play_sound(sound_or_name, volume=1.0, loop=False, playerPos=None, objPos=None,
|
|
||||||
centerDistance=None, pattern=False, interrupt=False, pause=False,
|
|
||||||
cutScene=False, sounds=None):
|
|
||||||
"""Unified sound playing function with backward compatibility."""
|
|
||||||
# Handle cut scene mode
|
|
||||||
if cutScene:
|
|
||||||
return _play_cutscene(sound_or_name, sounds)
|
|
||||||
|
|
||||||
# Handle pattern matching
|
|
||||||
if pattern and isinstance(sound_or_name, str) and sounds:
|
|
||||||
matched_sound = _find_matching_sound(sound_or_name, sounds)
|
|
||||||
if not matched_sound:
|
|
||||||
return None
|
|
||||||
sound_or_name = matched_sound
|
|
||||||
|
|
||||||
# Handle interrupt
|
|
||||||
if interrupt:
|
|
||||||
pygame.event.clear()
|
|
||||||
pygame.mixer.stop()
|
|
||||||
|
|
||||||
# Case 1: Sound instance provided
|
|
||||||
if isinstance(sound_or_name, Sound):
|
|
||||||
return sound_or_name.play_sound(sound_or_name, volume, loop, playerPos, objPos,
|
|
||||||
centerDistance, False, False, pause, False)
|
|
||||||
|
|
||||||
# Case 2: Sound name with Sound instance
|
|
||||||
elif isinstance(sounds, Sound) and isinstance(sound_or_name, str):
|
|
||||||
return sounds.play_sound(sound_or_name, volume, loop, playerPos, objPos,
|
|
||||||
centerDistance, False, False, pause, False)
|
|
||||||
|
|
||||||
# Case 3: Direct pygame.Sound
|
|
||||||
elif isinstance(sound_or_name, pygame.mixer.Sound):
|
|
||||||
channel = sound_or_name.play(-1 if loop else 0)
|
|
||||||
if channel:
|
|
||||||
channel.set_volume(volume * volumeService.get_sfx_volume())
|
|
||||||
return channel
|
|
||||||
|
|
||||||
# Case 4: Sound name with dictionary
|
|
||||||
elif isinstance(sounds, dict) and isinstance(sound_or_name, str) and sound_or_name in sounds:
|
|
||||||
# Play the sound
|
|
||||||
channel = sounds[sound_or_name].play(-1 if loop else 0)
|
|
||||||
if not channel:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Apply volume settings
|
|
||||||
sfx_vol = volumeService.get_sfx_volume()
|
|
||||||
|
|
||||||
# Handle positional audio
|
|
||||||
if playerPos is not None and objPos is not None:
|
|
||||||
left_vol, right_vol = _get_stereo_panning(playerPos, objPos, centerDistance)
|
|
||||||
if left_vol == 0 and right_vol == 0:
|
|
||||||
channel.stop()
|
|
||||||
return None
|
|
||||||
channel.set_volume(volume * left_vol * sfx_vol, volume * right_vol * sfx_vol)
|
|
||||||
else:
|
|
||||||
channel.set_volume(volume * sfx_vol)
|
|
||||||
|
|
||||||
# Pause if requested
|
|
||||||
if pause:
|
|
||||||
time.sleep(sounds[sound_or_name].get_length())
|
|
||||||
|
|
||||||
return channel
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
def obj_update(channel, playerPos, objPos):
|
|
||||||
"""Update positional audio for a playing sound."""
|
|
||||||
if not channel:
|
|
||||||
return None
|
|
||||||
|
|
||||||
left_vol, right_vol = _get_stereo_panning(playerPos, objPos)
|
|
||||||
if left_vol == 0 and right_vol == 0:
|
|
||||||
channel.stop()
|
|
||||||
return None
|
|
||||||
|
|
||||||
channel.set_volume(left_vol * volumeService.sfxVolume, right_vol * volumeService.sfxVolume)
|
|
||||||
return channel
|
|
||||||
|
|
||||||
def obj_stop(channel):
|
|
||||||
"""Stop a sound channel."""
|
|
||||||
if channel:
|
|
||||||
try: channel.stop()
|
|
||||||
except: pass
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Extremely concise lambda definitions for legacy functions
|
|
||||||
obj_play = lambda sounds, soundName, playerPos, objPos, loop=True: play_sound(
|
|
||||||
soundName, 1.0, loop, playerPos, objPos, None, False, False, False, False, sounds)
|
|
||||||
|
|
||||||
play_ambiance = lambda sounds, soundNames, probability, randomLocation=False: play_sound(
|
|
||||||
random.choice(soundNames) if random.randint(1, 100) <= probability and not any(
|
|
||||||
pygame.mixer.find_channel(True) and pygame.mixer.find_channel(True).get_busy()
|
|
||||||
for _ in ([soundNames] if isinstance(soundNames, str) else soundNames)) else None,
|
|
||||||
1.0, False, None, None, None, False, False, False, False,
|
|
||||||
sounds if not isinstance(sounds, Sound) else None)
|
|
||||||
|
|
||||||
play_random = lambda sounds, soundName, pause=False, interrupt=False: play_sound(
|
|
||||||
soundName, 1.0, False, None, None, None, True, interrupt, pause, False, sounds)
|
|
||||||
|
|
||||||
play_random_positional = lambda sounds, soundName, playerX, objectX: play_sound(
|
|
||||||
soundName, 1.0, False, playerX, objectX, None, True, False, False, False, sounds)
|
|
||||||
|
|
||||||
play_directional_sound = lambda sounds, soundName, playerPos, objPos, centerDistance=3, volume=1.0: play_sound(
|
|
||||||
soundName, volume, False, playerPos, objPos, centerDistance, False, False, False, False, sounds)
|
|
||||||
|
|
||||||
cut_scene = lambda sounds, soundName: _play_cutscene(soundName, sounds)
|
|
||||||
|
|
||||||
def play_random_falling(sounds, soundName, playerX, objectX, startY, currentY=0, maxY=20, existingChannel=None):
|
|
||||||
"""Handle falling sound."""
|
|
||||||
if isinstance(sounds, Sound):
|
|
||||||
return sounds.play_falling_sound(soundName, playerX, objectX, startY, currentY, maxY, existingChannel)
|
|
||||||
|
|
||||||
# Legacy implementation
|
|
||||||
left_vol, right_vol = _get_stereo_panning(playerX, objectX)
|
|
||||||
if left_vol == 0 and right_vol == 0:
|
|
||||||
if existingChannel:
|
|
||||||
existingChannel.stop()
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Calculate fall multiplier
|
|
||||||
fallMultiplier = 1 - (currentY / maxY)
|
|
||||||
finalLeft = left_vol * fallMultiplier
|
|
||||||
finalRight = right_vol * fallMultiplier
|
|
||||||
|
|
||||||
if existingChannel:
|
|
||||||
existingChannel.set_volume(
|
|
||||||
finalLeft * volumeService.sfxVolume,
|
|
||||||
finalRight * volumeService.sfxVolume
|
|
||||||
)
|
|
||||||
return existingChannel
|
|
||||||
|
|
||||||
# Find matching sound
|
|
||||||
matched_sound = _find_matching_sound(soundName, sounds)
|
|
||||||
if not matched_sound:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Play the sound
|
|
||||||
channel = sounds[matched_sound].play()
|
|
||||||
if channel:
|
|
||||||
channel.set_volume(
|
|
||||||
finalLeft * volumeService.sfxVolume,
|
|
||||||
finalRight * volumeService.sfxVolume
|
|
||||||
)
|
|
||||||
return channel
|
|
149
speech.py
149
speech.py
@ -1,149 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""Speech handling for Storm Games.
|
|
||||||
|
|
||||||
Provides functionality for:
|
|
||||||
- Text-to-speech using different speech providers
|
|
||||||
- Speech delay control to prevent stuttering
|
|
||||||
- On-screen text display
|
|
||||||
"""
|
|
||||||
|
|
||||||
import pygame
|
|
||||||
import textwrap
|
|
||||||
import time
|
|
||||||
from sys import exit
|
|
||||||
|
|
||||||
class Speech:
|
|
||||||
"""Handles text-to-speech functionality."""
|
|
||||||
|
|
||||||
_instance = None
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_instance(cls):
|
|
||||||
"""Get or create the singleton instance."""
|
|
||||||
if cls._instance is None:
|
|
||||||
cls._instance = Speech()
|
|
||||||
return cls._instance
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
"""Initialize speech system with available provider."""
|
|
||||||
# Handle speech delays so we don't get stuttering
|
|
||||||
self.lastSpoken = {"text": None, "time": 0}
|
|
||||||
self.speechDelay = 250 # ms
|
|
||||||
|
|
||||||
# Try to initialize a speech provider
|
|
||||||
self.provider = None
|
|
||||||
self.providerName = None
|
|
||||||
|
|
||||||
# Try speechd first
|
|
||||||
try:
|
|
||||||
import speechd
|
|
||||||
self.spd = speechd.Client()
|
|
||||||
self.provider = self.spd
|
|
||||||
self.providerName = "speechd"
|
|
||||||
return
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Try accessible_output2 next
|
|
||||||
try:
|
|
||||||
import accessible_output2.outputs.auto
|
|
||||||
self.ao2 = accessible_output2.outputs.auto.Auto()
|
|
||||||
self.provider = self.ao2
|
|
||||||
self.providerName = "accessible_output2"
|
|
||||||
return
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# No speech providers found
|
|
||||||
print("No speech providers found.")
|
|
||||||
|
|
||||||
def speak(self, text, interrupt=True):
|
|
||||||
"""Speak text using the configured speech provider and display on screen.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
text (str): Text to speak and display
|
|
||||||
interrupt (bool): Whether to interrupt current speech (default: True)
|
|
||||||
"""
|
|
||||||
if not self.provider:
|
|
||||||
return
|
|
||||||
|
|
||||||
currentTime = pygame.time.get_ticks()
|
|
||||||
|
|
||||||
# Check if this is the same text within the delay window
|
|
||||||
if (self.lastSpoken["text"] == text and
|
|
||||||
currentTime - self.lastSpoken["time"] < self.speechDelay):
|
|
||||||
return
|
|
||||||
|
|
||||||
# Update last spoken tracking
|
|
||||||
self.lastSpoken["text"] = text
|
|
||||||
self.lastSpoken["time"] = currentTime
|
|
||||||
|
|
||||||
# Proceed with speech
|
|
||||||
if self.providerName == "speechd":
|
|
||||||
if interrupt:
|
|
||||||
self.spd.cancel()
|
|
||||||
self.spd.say(text)
|
|
||||||
elif self.providerName == "accessible_output2":
|
|
||||||
self.ao2.speak(text, interrupt=interrupt)
|
|
||||||
|
|
||||||
# Display the text on screen
|
|
||||||
screen = pygame.display.get_surface()
|
|
||||||
if not screen:
|
|
||||||
return
|
|
||||||
|
|
||||||
font = pygame.font.Font(None, 36)
|
|
||||||
# Wrap the text
|
|
||||||
maxWidth = screen.get_width() - 40 # Leave a 20-pixel margin on each side
|
|
||||||
wrappedText = textwrap.wrap(text, width=maxWidth // font.size('A')[0])
|
|
||||||
# Render each line
|
|
||||||
textSurfaces = [font.render(line, True, (255, 255, 255)) for line in wrappedText]
|
|
||||||
screen.fill((0, 0, 0)) # Clear screen with black
|
|
||||||
# Calculate total height of text block
|
|
||||||
totalHeight = sum(surface.get_height() for surface in textSurfaces)
|
|
||||||
# Start y-position (centered vertically)
|
|
||||||
currentY = (screen.get_height() - totalHeight) // 2
|
|
||||||
# Blit each line of text
|
|
||||||
for surface in textSurfaces:
|
|
||||||
textRect = surface.get_rect(center=(screen.get_width() // 2, currentY + surface.get_height() // 2))
|
|
||||||
screen.blit(surface, textRect)
|
|
||||||
currentY += surface.get_height()
|
|
||||||
pygame.display.flip()
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
"""Clean up speech resources."""
|
|
||||||
if self.providerName == "speechd":
|
|
||||||
self.spd.close()
|
|
||||||
|
|
||||||
# Global instance for backward compatibility
|
|
||||||
_speechInstance = None
|
|
||||||
|
|
||||||
def speak(text, interrupt=True):
|
|
||||||
"""Speak text using the global speech instance.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
text (str): Text to speak and display
|
|
||||||
interrupt (bool): Whether to interrupt current speech (default: True)
|
|
||||||
"""
|
|
||||||
global _speechInstance
|
|
||||||
if _speechInstance is None:
|
|
||||||
_speechInstance = Speech.get_instance()
|
|
||||||
_speechInstance.speak(text, interrupt)
|
|
||||||
|
|
||||||
def messagebox(text):
|
|
||||||
"""Display a simple message box with text.
|
|
||||||
|
|
||||||
Shows a message that can be repeated until the user chooses to continue.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
text (str): Message to display
|
|
||||||
"""
|
|
||||||
speech = Speech.get_instance()
|
|
||||||
speech.speak(text + "\nPress any key to repeat or enter to continue.")
|
|
||||||
while True:
|
|
||||||
event = pygame.event.wait()
|
|
||||||
if event.type == pygame.KEYDOWN:
|
|
||||||
if event.key in (pygame.K_ESCAPE, pygame.K_RETURN):
|
|
||||||
speech.speak(" ")
|
|
||||||
return
|
|
||||||
speech.speak(text + "\nPress any key to repeat or enter to continue.")
|
|
454
utils.py
454
utils.py
@ -1,454 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""Utility functions and Game class for Storm Games.
|
|
||||||
|
|
||||||
Provides:
|
|
||||||
- Game class for centralized management
|
|
||||||
- Miscellaneous helper functions
|
|
||||||
- Version checking utilities
|
|
||||||
"""
|
|
||||||
|
|
||||||
import pygame
|
|
||||||
import random
|
|
||||||
import math
|
|
||||||
import numpy as np
|
|
||||||
import time
|
|
||||||
import re
|
|
||||||
import requests
|
|
||||||
import os
|
|
||||||
from .input import check_for_exit
|
|
||||||
from setproctitle import setproctitle
|
|
||||||
|
|
||||||
from .services import PathService, ConfigService, VolumeService
|
|
||||||
from .sound import Sound
|
|
||||||
from .speech import Speech
|
|
||||||
from .scoreboard import Scoreboard
|
|
||||||
|
|
||||||
class Game:
|
|
||||||
"""Central class to manage all game systems."""
|
|
||||||
|
|
||||||
def __init__(self, title):
|
|
||||||
"""Initialize a new game.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
title (str): Title of the game
|
|
||||||
"""
|
|
||||||
self.title = title
|
|
||||||
|
|
||||||
# Initialize services
|
|
||||||
self.pathService = PathService.get_instance().initialize(title)
|
|
||||||
self.configService = ConfigService.get_instance()
|
|
||||||
self.configService.set_game_info(title, self.pathService)
|
|
||||||
self.volumeService = VolumeService.get_instance()
|
|
||||||
|
|
||||||
# Initialize game components (lazy loaded)
|
|
||||||
self._speech = None
|
|
||||||
self._sound = None
|
|
||||||
self._scoreboard = None
|
|
||||||
|
|
||||||
# Display text instructions flag
|
|
||||||
self.displayTextUsageInstructions = False
|
|
||||||
|
|
||||||
@property
|
|
||||||
def speech(self):
|
|
||||||
"""Get the speech system (lazy loaded).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Speech: Speech system instance
|
|
||||||
"""
|
|
||||||
if not self._speech:
|
|
||||||
self._speech = Speech.get_instance()
|
|
||||||
return self._speech
|
|
||||||
|
|
||||||
@property
|
|
||||||
def sound(self):
|
|
||||||
"""Get the sound system (lazy loaded).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Sound: Sound system instance
|
|
||||||
"""
|
|
||||||
if not self._sound:
|
|
||||||
self._sound = Sound("sounds/", self.volumeService)
|
|
||||||
return self._sound
|
|
||||||
|
|
||||||
@property
|
|
||||||
def scoreboard(self):
|
|
||||||
"""Get the scoreboard (lazy loaded).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Scoreboard: Scoreboard instance
|
|
||||||
"""
|
|
||||||
if not self._scoreboard:
|
|
||||||
self._scoreboard = Scoreboard(self.configService)
|
|
||||||
return self._scoreboard
|
|
||||||
|
|
||||||
def initialize(self):
|
|
||||||
"""Initialize the game GUI and sound system.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Game: Self for method chaining
|
|
||||||
"""
|
|
||||||
# Set process title
|
|
||||||
setproctitle(str.lower(str.replace(self.title, " ", "")))
|
|
||||||
|
|
||||||
# Seed the random generator
|
|
||||||
random.seed()
|
|
||||||
|
|
||||||
# Initialize pygame
|
|
||||||
pygame.init()
|
|
||||||
pygame.display.set_mode((800, 600))
|
|
||||||
pygame.display.set_caption(self.title)
|
|
||||||
|
|
||||||
# Set up audio system
|
|
||||||
pygame.mixer.pre_init(44100, -16, 2, 1024)
|
|
||||||
pygame.mixer.init()
|
|
||||||
pygame.mixer.set_num_channels(32)
|
|
||||||
pygame.mixer.set_reserved(0) # Reserve channel for cut scenes
|
|
||||||
|
|
||||||
# Enable key repeat for volume controls
|
|
||||||
pygame.key.set_repeat(500, 100)
|
|
||||||
|
|
||||||
# Load sound effects
|
|
||||||
self.sound
|
|
||||||
|
|
||||||
# Play intro sound if available
|
|
||||||
if 'game-intro' in self.sound.sounds:
|
|
||||||
self.sound.cut_scene('game-intro')
|
|
||||||
|
|
||||||
return self
|
|
||||||
|
|
||||||
def speak(self, text, interrupt=True):
|
|
||||||
"""Speak text using the speech system.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
text (str): Text to speak
|
|
||||||
interrupt (bool): Whether to interrupt current speech
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Game: Self for method chaining
|
|
||||||
"""
|
|
||||||
self.speech.speak(text, interrupt)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def play_bgm(self, musicFile):
|
|
||||||
"""Play background music.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
musicFile (str): Path to music file
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Game: Self for method chaining
|
|
||||||
"""
|
|
||||||
self.sound.play_bgm(musicFile)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def display_text(self, textLines):
|
|
||||||
"""Display text with navigation controls.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
textLines (list): List of text lines
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Game: Self for method chaining
|
|
||||||
"""
|
|
||||||
# Store original text with blank lines for copying
|
|
||||||
originalText = textLines.copy()
|
|
||||||
|
|
||||||
# Create navigation text by filtering out blank lines
|
|
||||||
navText = [line for line in textLines if line.strip()]
|
|
||||||
|
|
||||||
# Add instructions at the start on the first display
|
|
||||||
if not self.displayTextUsageInstructions:
|
|
||||||
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.")
|
|
||||||
navText.insert(0, instructions)
|
|
||||||
self.displayTextUsageInstructions = True
|
|
||||||
|
|
||||||
# Add end marker
|
|
||||||
navText.append("End of text.")
|
|
||||||
|
|
||||||
currentIndex = 0
|
|
||||||
self.speech.speak(navText[currentIndex])
|
|
||||||
|
|
||||||
while True:
|
|
||||||
event = pygame.event.wait()
|
|
||||||
if event.type == pygame.KEYDOWN:
|
|
||||||
# Check for Alt modifier
|
|
||||||
mods = pygame.key.get_mods()
|
|
||||||
altPressed = mods & pygame.KMOD_ALT
|
|
||||||
|
|
||||||
# Volume controls (require Alt)
|
|
||||||
if altPressed:
|
|
||||||
if event.key == pygame.K_PAGEUP:
|
|
||||||
self.volumeService.adjust_master_volume(0.1, pygame.mixer)
|
|
||||||
elif event.key == pygame.K_PAGEDOWN:
|
|
||||||
self.volumeService.adjust_master_volume(-0.1, pygame.mixer)
|
|
||||||
elif event.key == pygame.K_HOME:
|
|
||||||
self.volumeService.adjust_bgm_volume(0.1, pygame.mixer)
|
|
||||||
elif event.key == pygame.K_END:
|
|
||||||
self.volumeService.adjust_bgm_volume(-0.1, pygame.mixer)
|
|
||||||
elif event.key == pygame.K_INSERT:
|
|
||||||
self.volumeService.adjust_sfx_volume(0.1, pygame.mixer)
|
|
||||||
elif event.key == pygame.K_DELETE:
|
|
||||||
self.volumeService.adjust_sfx_volume(-0.1, pygame.mixer)
|
|
||||||
else:
|
|
||||||
if event.key in (pygame.K_ESCAPE, pygame.K_RETURN):
|
|
||||||
return self
|
|
||||||
|
|
||||||
if event.key in [pygame.K_DOWN, pygame.K_s] and currentIndex < len(navText) - 1:
|
|
||||||
currentIndex += 1
|
|
||||||
self.speech.speak(navText[currentIndex])
|
|
||||||
|
|
||||||
if event.key in [pygame.K_UP, pygame.K_w] and currentIndex > 0:
|
|
||||||
currentIndex -= 1
|
|
||||||
self.speech.speak(navText[currentIndex])
|
|
||||||
|
|
||||||
if event.key == pygame.K_SPACE:
|
|
||||||
# Join with newlines to preserve spacing in speech
|
|
||||||
self.speech.speak('\n'.join(originalText[1:-1]))
|
|
||||||
|
|
||||||
if event.key == pygame.K_c:
|
|
||||||
try:
|
|
||||||
import pyperclip
|
|
||||||
pyperclip.copy(navText[currentIndex])
|
|
||||||
self.speech.speak("Copied " + navText[currentIndex] + " to the clipboard.")
|
|
||||||
except:
|
|
||||||
self.speech.speak("Failed to copy the text to the clipboard.")
|
|
||||||
|
|
||||||
if event.key == pygame.K_t:
|
|
||||||
try:
|
|
||||||
import pyperclip
|
|
||||||
# Join with newlines to preserve blank lines in full text
|
|
||||||
pyperclip.copy(''.join(originalText[2:-1]))
|
|
||||||
self.speech.speak("Copied entire message to the clipboard.")
|
|
||||||
except:
|
|
||||||
self.speech.speak("Failed to copy the text to the clipboard.")
|
|
||||||
|
|
||||||
pygame.event.clear()
|
|
||||||
time.sleep(0.001)
|
|
||||||
|
|
||||||
def exit(self):
|
|
||||||
"""Clean up and exit the game."""
|
|
||||||
if self._speech and self.speech.providerName == "speechd":
|
|
||||||
self.speech.close()
|
|
||||||
pygame.mixer.music.stop()
|
|
||||||
pygame.quit()
|
|
||||||
import sys
|
|
||||||
sys.exit()
|
|
||||||
|
|
||||||
# Utility functions
|
|
||||||
|
|
||||||
def check_for_updates(currentVersion, gameName, url):
|
|
||||||
"""Check for game updates.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
currentVersion (str): Current version string (e.g. "1.0.0")
|
|
||||||
gameName (str): Name of the game
|
|
||||||
url (str): URL to check for updates
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: Update information or None if no update available
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
response = requests.get(url, timeout=5)
|
|
||||||
if response.status_code == 200:
|
|
||||||
data = response.json()
|
|
||||||
if 'version' in data and data['version'] > currentVersion:
|
|
||||||
return {
|
|
||||||
'version': data['version'],
|
|
||||||
'url': data.get('url', ''),
|
|
||||||
'notes': data.get('notes', '')
|
|
||||||
}
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error checking for updates: {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_version_tuple(versionStr):
|
|
||||||
"""Convert version string to comparable tuple.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
versionStr (str): Version string (e.g. "1.0.0")
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
tuple: Version as tuple of integers
|
|
||||||
"""
|
|
||||||
return tuple(map(int, versionStr.split('.')))
|
|
||||||
|
|
||||||
def check_compatibility(requiredVersion, currentVersion):
|
|
||||||
"""Check if current version meets minimum required version.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
requiredVersion (str): Minimum required version string
|
|
||||||
currentVersion (str): Current version string
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if compatible, False otherwise
|
|
||||||
"""
|
|
||||||
req = get_version_tuple(requiredVersion)
|
|
||||||
cur = get_version_tuple(currentVersion)
|
|
||||||
return cur >= req
|
|
||||||
|
|
||||||
def sanitize_filename(filename):
|
|
||||||
"""Sanitize a filename to be safe for all operating systems.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
filename (str): Original filename
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Sanitized filename
|
|
||||||
"""
|
|
||||||
# Remove invalid characters
|
|
||||||
filename = re.sub(r'[\\/*?:"<>|]', "", filename)
|
|
||||||
# Replace spaces with underscores
|
|
||||||
filename = filename.replace(" ", "_")
|
|
||||||
# Limit length
|
|
||||||
if len(filename) > 255:
|
|
||||||
filename = filename[:255]
|
|
||||||
return filename
|
|
||||||
|
|
||||||
def lerp(start, end, factor):
|
|
||||||
"""Linear interpolation between two values.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
start (float): Start value
|
|
||||||
end (float): End value
|
|
||||||
factor (float): Interpolation factor (0.0-1.0)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
float: Interpolated value
|
|
||||||
"""
|
|
||||||
return start + (end - start) * factor
|
|
||||||
|
|
||||||
def smooth_step(edge0, edge1, x):
|
|
||||||
"""Hermite interpolation between two values.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
edge0 (float): Start edge
|
|
||||||
edge1 (float): End edge
|
|
||||||
x (float): Value to interpolate
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
float: Interpolated value with smooth step
|
|
||||||
"""
|
|
||||||
# Scale, bias and saturate x to 0..1 range
|
|
||||||
x = max(0.0, min(1.0, (x - edge0) / (edge1 - edge0)))
|
|
||||||
# Evaluate polynomial
|
|
||||||
return x * x * (3 - 2 * x)
|
|
||||||
|
|
||||||
def distance_2d(x1, y1, x2, y2):
|
|
||||||
"""Calculate Euclidean distance between two 2D points.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
x1 (float): X coordinate of first point
|
|
||||||
y1 (float): Y coordinate of first point
|
|
||||||
x2 (float): X coordinate of second point
|
|
||||||
y2 (float): Y coordinate of second point
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
float: Distance between points
|
|
||||||
"""
|
|
||||||
return math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)
|
|
||||||
|
|
||||||
def generate_tone(frequency, duration=0.1, sampleRate=44100, volume=0.2):
|
|
||||||
"""Generate a tone at the specified frequency.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
frequency (float): Frequency in Hz
|
|
||||||
duration (float): Duration in seconds (default: 0.1)
|
|
||||||
sampleRate (int): Sample rate in Hz (default: 44100)
|
|
||||||
volume (float): Volume from 0.0 to 1.0 (default: 0.2)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
pygame.mixer.Sound: Sound object with the generated tone
|
|
||||||
"""
|
|
||||||
|
|
||||||
t = np.linspace(0, duration, int(sampleRate * duration), False)
|
|
||||||
tone = np.sin(2 * np.pi * frequency * t)
|
|
||||||
stereoTone = np.vstack((tone, tone)).T # Create a 2D array for stereo
|
|
||||||
stereoTone = (stereoTone * 32767 * volume).astype(np.int16) # Apply volume
|
|
||||||
stereoTone = np.ascontiguousarray(stereoTone) # Ensure C-contiguous array
|
|
||||||
return pygame.sndarray.make_sound(stereoTone)
|
|
||||||
|
|
||||||
def x_powerbar():
|
|
||||||
"""Sound based horizontal power bar
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
int: Selected position between -50 and 50
|
|
||||||
"""
|
|
||||||
|
|
||||||
clock = pygame.time.Clock()
|
|
||||||
screen = pygame.display.get_surface()
|
|
||||||
position = -50 # Start from the leftmost position
|
|
||||||
direction = 1 # Move right initially
|
|
||||||
barHeight = 20
|
|
||||||
|
|
||||||
while True:
|
|
||||||
frequency = 440 # A4 note
|
|
||||||
leftVolume = (50 - position) / 100
|
|
||||||
rightVolume = (position + 50) / 100
|
|
||||||
tone = generate_tone(frequency)
|
|
||||||
channel = tone.play()
|
|
||||||
channel.set_volume(leftVolume, rightVolume)
|
|
||||||
|
|
||||||
# Visual representation
|
|
||||||
screen.fill((0, 0, 0))
|
|
||||||
barWidth = screen.get_width() - 40 # Leave 20px margin on each side
|
|
||||||
pygame.draw.rect(screen, (100, 100, 100), (20, screen.get_height() // 2 - barHeight // 2, barWidth, barHeight))
|
|
||||||
markerPos = int(20 + (position + 50) / 100 * barWidth)
|
|
||||||
pygame.draw.rect(screen, (255, 0, 0), (markerPos - 5, screen.get_height() // 2 - barHeight, 10, barHeight * 2))
|
|
||||||
pygame.display.flip()
|
|
||||||
|
|
||||||
for event in pygame.event.get():
|
|
||||||
check_for_exit()
|
|
||||||
if event.type == pygame.KEYDOWN and event.key == pygame.K_SPACE:
|
|
||||||
channel.stop()
|
|
||||||
return position # This will return a value between -50 and 50
|
|
||||||
|
|
||||||
position += direction
|
|
||||||
if position > 50:
|
|
||||||
position = 50
|
|
||||||
direction = -1
|
|
||||||
elif position < -50:
|
|
||||||
position = -50
|
|
||||||
direction = 1
|
|
||||||
|
|
||||||
clock.tick(40) # Speed of bar
|
|
||||||
|
|
||||||
def y_powerbar():
|
|
||||||
"""Sound based vertical power bar
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
int: Selected power level between 0 and 100
|
|
||||||
"""
|
|
||||||
|
|
||||||
clock = pygame.time.Clock()
|
|
||||||
screen = pygame.display.get_surface()
|
|
||||||
power = 0
|
|
||||||
direction = 1 # 1 for increasing, -1 for decreasing
|
|
||||||
barWidth = 20
|
|
||||||
|
|
||||||
while True:
|
|
||||||
frequency = 220 + (power * 5) # Adjust these values to change the pitch range
|
|
||||||
tone = generate_tone(frequency)
|
|
||||||
channel = tone.play()
|
|
||||||
|
|
||||||
# Visual representation
|
|
||||||
screen.fill((0, 0, 0))
|
|
||||||
barHeight = screen.get_height() - 40 # Leave 20px margin on top and bottom
|
|
||||||
pygame.draw.rect(screen, (100, 100, 100), (screen.get_width() // 2 - barWidth // 2, 20, barWidth, barHeight))
|
|
||||||
markerPos = int(20 + (100 - power) / 100 * barHeight)
|
|
||||||
pygame.draw.rect(screen, (255, 0, 0), (screen.get_width() // 2 - barWidth, markerPos - 5, barWidth * 2, 10))
|
|
||||||
pygame.display.flip()
|
|
||||||
|
|
||||||
for event in pygame.event.get():
|
|
||||||
check_for_exit()
|
|
||||||
if event.type == pygame.KEYDOWN and event.key == pygame.K_SPACE:
|
|
||||||
channel.stop()
|
|
||||||
return power
|
|
||||||
|
|
||||||
power += direction
|
|
||||||
if power >= 100 or power <= 0:
|
|
||||||
direction *= -1 # Reverse direction at limits
|
|
||||||
|
|
||||||
clock.tick(40)
|
|
Reference in New Issue
Block a user