Compare commits
87 Commits
devel
...
619cb5508a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
619cb5508a | ||
|
|
2c34f31a82 | ||
|
|
a9c2c4332d | ||
|
|
fedb09be94 | ||
|
|
27765e62bc | ||
|
|
23aea6badf | ||
|
|
af38d5af76 | ||
|
|
4d0436c5a9 | ||
|
|
f51bd6dee4 | ||
|
|
3b2bcd928d | ||
|
|
91f39aad88 | ||
|
|
1dc0ac2a7f | ||
|
|
3f8385599b | ||
|
|
468c663cc1 | ||
|
|
fe772cbb1e | ||
|
|
2c101d1778 | ||
|
|
8f81323668 | ||
|
|
be6dfdf53a | ||
|
|
2ad22ff1ae | ||
|
|
aba87e87ad | ||
|
|
df7945e3b6 | ||
|
|
7902dfacd1 | ||
|
|
10b46fa168 | ||
|
|
e7d5b03e55 | ||
|
|
173220d167 | ||
|
|
2f791da5b7 | ||
|
|
9997b684ca | ||
|
|
e7caff3d0f | ||
|
|
db6c34f714 | ||
|
|
3862b36d56 | ||
|
|
8bfe968b4b | ||
|
|
e2c69e3af7 | ||
|
|
da17b71c28 | ||
|
|
1cb57391d8 | ||
|
|
7f62e6ccca | ||
|
|
943e2acf53 | ||
|
|
c242fc6832 | ||
|
|
6d2c6e04d8 | ||
|
|
97431c0c74 | ||
|
|
4a15f951f0 | ||
|
|
5a791510ea | ||
|
|
7cbbc64d27 | ||
|
|
b479811a98 | ||
|
|
d5d737d0c0 | ||
|
|
dd246db5be | ||
|
|
21216f361a | ||
|
|
68e72f5d81 | ||
|
|
80fe2caff3 | ||
|
|
2df86c9c76 | ||
|
|
5fa90f9e84 | ||
|
|
658709ebce | ||
|
|
d5c79c0770 | ||
|
|
24f9a126d4 | ||
|
|
c316d4e570 | ||
|
|
e66655d75f | ||
|
|
c5406d5089 | ||
|
|
b5b472eebe | ||
|
|
9f03de15b8 | ||
|
|
428a48678d | ||
|
|
9a6d6374f9 | ||
|
|
df386cbbd9 | ||
|
|
38522aee78 | ||
|
|
0e9c52f5e1 | ||
|
|
fabf48ff42 | ||
|
|
0c73e98876 | ||
|
|
0ef11785ec | ||
|
|
58ab5aa854 | ||
|
|
155ed6ec39 | ||
|
|
68ad08be46 | ||
|
|
536659338e | ||
|
|
37aa764d68 | ||
|
|
84a722bb8e | ||
|
|
b897abf0a3 | ||
|
|
678af54346 | ||
|
|
d456b8b3b3 | ||
|
|
c5c32943e2 | ||
|
|
34d89ca54b | ||
|
|
e8bf4f9565 | ||
|
|
dd350c0285 | ||
|
|
ae93e02e69 | ||
|
|
21c0795ea9 | ||
|
|
67d2315cef | ||
|
|
b6afb5450e | ||
|
|
42266d4b6c | ||
|
|
54842bac29 | ||
|
|
08f06699c8 | ||
|
|
7ef11be54c |
831
README.md
831
README.md
@@ -1,8 +1,829 @@
|
|||||||
# libstormgames
|
# libstormgames
|
||||||
|
|
||||||
Library to make writing audiogames easier.
|
A Python library to make creating audio games easier.
|
||||||
|
|
||||||
## Requirements
|
|
||||||
configparser pyxdg pygame pyperclip requests setproctitle
|
## Overview
|
||||||
### For mac os X and windows
|
|
||||||
accessible_output2
|
`libstormgames` provides a comprehensive set of tools for developing accessible games with audio-first design. It handles common game development tasks including:
|
||||||
|
|
||||||
|
- 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.
|
||||||
|
|||||||
228
__init__.py
Normal file → Executable file
228
__init__.py
Normal file → Executable file
@@ -0,0 +1,228 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Standard initializations and functions shared by all Storm Games.
|
||||||
|
|
||||||
|
This module provides core functionality for Storm Games including:
|
||||||
|
- Sound and speech handling
|
||||||
|
- Volume controls
|
||||||
|
- Configuration management
|
||||||
|
- Score tracking
|
||||||
|
- GUI initialization
|
||||||
|
- Game menu systems
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Import service classes
|
||||||
|
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
|
||||||
|
|
||||||
|
def randint(a, b):
|
||||||
|
"""Return a random integer N such that a <= N <= b."""
|
||||||
|
return random.randint(a, b)
|
||||||
|
|
||||||
|
def choice(seq):
|
||||||
|
"""Return a random element from the non-empty sequence seq."""
|
||||||
|
return random.choice(seq)
|
||||||
|
|
||||||
|
def uniform(a, b):
|
||||||
|
"""Return a random floating point number N such that a <= N <= b."""
|
||||||
|
return random.uniform(a, b)
|
||||||
|
|
||||||
|
def seed(a=None):
|
||||||
|
"""Initialize the random number generator."""
|
||||||
|
return random.seed(a)
|
||||||
|
|||||||
102
config.py
Normal file
102
config.py
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
#!/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
|
||||||
174
display.py
Normal file
174
display.py
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
#!/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
|
||||||
|
try:
|
||||||
|
from os import listdir
|
||||||
|
from os.path import isfile, join
|
||||||
|
soundFiles = [f for f in listdir("sounds/")
|
||||||
|
if isfile(join("sounds/", f))
|
||||||
|
and (f.split('.')[1].lower() in ["ogg", "wav"])]
|
||||||
|
except Exception as e:
|
||||||
|
print("No sounds found.")
|
||||||
|
Speech.get_instance().speak("No sounds found.", False)
|
||||||
|
soundFiles = []
|
||||||
|
|
||||||
|
# Create dictionary of sound objects
|
||||||
|
soundData = {}
|
||||||
|
for f in soundFiles:
|
||||||
|
soundData[f.split('.')[0]] = pygame.mixer.Sound("sounds/" + f)
|
||||||
|
|
||||||
|
# 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
Normal file
77
input.py
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
#!/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()
|
||||||
273
libstormgames.py
273
libstormgames.py
@@ -1,273 +0,0 @@
|
|||||||
#!/bin/python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""Standard initializations and functions shared by all games."""
|
|
||||||
|
|
||||||
from sys import exit
|
|
||||||
import configparser
|
|
||||||
import os
|
|
||||||
from os import listdir
|
|
||||||
from os.path import isfile, join
|
|
||||||
from inspect import isfunction
|
|
||||||
from xdg import BaseDirectory
|
|
||||||
from setproctitle import setproctitle
|
|
||||||
import pygame
|
|
||||||
import pyperclip
|
|
||||||
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 time
|
|
||||||
|
|
||||||
localConfig = configparser.ConfigParser()
|
|
||||||
globalConfig = configparser.ConfigParser()
|
|
||||||
|
|
||||||
class scoreboard():
|
|
||||||
'Handles scores and top 10'
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.oldScores = []
|
|
||||||
for i in range(9):
|
|
||||||
try:
|
|
||||||
self.oldScores[i] = read_config("scoreboard", i)
|
|
||||||
except:
|
|
||||||
self.oldScores[i] = 0
|
|
||||||
|
|
||||||
|
|
||||||
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(section, value, readGlobal = False):
|
|
||||||
if readGlobal == False:
|
|
||||||
with open(gamePath + "config.ini", 'r') as configfile:
|
|
||||||
return localConfig.read(section, value)
|
|
||||||
else:
|
|
||||||
with open(globalPath + "config.ini", 'r') as configfile:
|
|
||||||
return globalConfig.read(section, value)
|
|
||||||
|
|
||||||
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()
|
|
||||||
pygame.mixer.music.stop()
|
|
||||||
pygame.quit()
|
|
||||||
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, " ", "")))
|
|
||||||
# start pygame
|
|
||||||
pygame.init()
|
|
||||||
# start the display (required by the event loop)
|
|
||||||
pygame.display.set_mode((320, 200))
|
|
||||||
pygame.display.set_caption(gameTitle)
|
|
||||||
# Set 32 channels for sound by default
|
|
||||||
pygame.mixer.init()
|
|
||||||
pygame.mixer.set_num_channels(32)
|
|
||||||
# Reserve the cut scene channel
|
|
||||||
pygame.mixer.set_reserved(0)
|
|
||||||
# Load sounds from the sound directory and creates a list like that {'bottle': 'bottle.ogg'}
|
|
||||||
soundFiles = [f for f in listdir("sounds/") if isfile(join("sounds/", f)) and (f.split('.')[1].lower() in ["ogg","wav"])]
|
|
||||||
#lets make a dict with pygame.mixer.Sound() objects {'bottle':<soundobject>}
|
|
||||||
soundData = {}
|
|
||||||
for f in soundFiles:
|
|
||||||
soundData[f.split('.')[0]] = pygame.mixer.Sound("sounds/" + f)
|
|
||||||
soundData['game-intro'].play()
|
|
||||||
time.sleep(soundData['game-intro'].get_length())
|
|
||||||
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 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
|
|
||||||
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_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://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=stormdragon2976@gmail.com&lc=US&item_name=Donation+to+Storm+Games&no_note=0&cn=¤cy_code=USD&bn=PP-DonationsBF:btn_donateCC_LG.gif:NonHosted')
|
|
||||||
412
menu.py
Normal file
412
menu.py
Normal file
@@ -0,0 +1,412 @@
|
|||||||
|
#!/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
|
||||||
|
- Play selected sounds
|
||||||
|
- Return to menu with escape key
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sounds (dict): Dictionary of available sound objects
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: "menu" if user exits with escape
|
||||||
|
"""
|
||||||
|
# Get speech instance
|
||||||
|
speech = Speech.get_instance()
|
||||||
|
|
||||||
|
currentIndex = 0
|
||||||
|
|
||||||
|
# Get list of available sounds, excluding special sounds
|
||||||
|
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"])
|
||||||
|
and (not f.lower().startswith("_"))]
|
||||||
|
|
||||||
|
# Sort the sound files alphabetically
|
||||||
|
soundFiles.sort()
|
||||||
|
|
||||||
|
# Total number of sound files
|
||||||
|
totalSounds = len(soundFiles)
|
||||||
|
|
||||||
|
# Track last spoken index to avoid repetition
|
||||||
|
lastSpoken = -1
|
||||||
|
|
||||||
|
# Flag to track when to exit the loop
|
||||||
|
returnToMenu = False
|
||||||
|
|
||||||
|
while not returnToMenu:
|
||||||
|
if currentIndex != lastSpoken:
|
||||||
|
# Speak the sound name followed by its position in the list
|
||||||
|
speech.speak(f"{soundFiles[currentIndex][:-4]}, {currentIndex + 1} of {totalSounds}")
|
||||||
|
lastSpoken = currentIndex
|
||||||
|
|
||||||
|
event = pygame.event.wait()
|
||||||
|
if event.type == pygame.KEYDOWN:
|
||||||
|
if event.key == pygame.K_ESCAPE:
|
||||||
|
returnToMenu = True
|
||||||
|
|
||||||
|
if event.key in [pygame.K_DOWN, pygame.K_s] and currentIndex < len(soundFiles) - 1:
|
||||||
|
pygame.mixer.stop()
|
||||||
|
currentIndex += 1
|
||||||
|
|
||||||
|
if event.key in [pygame.K_UP, pygame.K_w] and currentIndex > 0:
|
||||||
|
pygame.mixer.stop()
|
||||||
|
currentIndex -= 1
|
||||||
|
|
||||||
|
if event.key == pygame.K_RETURN:
|
||||||
|
try:
|
||||||
|
soundName = soundFiles[currentIndex][:-4]
|
||||||
|
pygame.mixer.stop()
|
||||||
|
sounds[soundName].play()
|
||||||
|
except:
|
||||||
|
lastSpoken = -1
|
||||||
|
speech.speak("Could not play sound.")
|
||||||
|
|
||||||
|
event = pygame.event.clear()
|
||||||
|
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)
|
||||||
8
requirements.txt
Normal file
8
requirements.txt
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
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
|
||||||
2
requirements_linux.txt
Normal file
2
requirements_linux.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
-r requirements.txt
|
||||||
|
python-speechd>=0.11.1
|
||||||
332
scoreboard.py
Normal file
332
scoreboard.py
Normal file
@@ -0,0 +1,332 @@
|
|||||||
|
#!/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
Normal file
274
services.py
Normal file
@@ -0,0 +1,274 @@
|
|||||||
|
#!/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
|
||||||
780
sound.py
Normal file
780
sound.py
Normal file
@@ -0,0 +1,780 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Sound handling for Storm Games.
|
||||||
|
|
||||||
|
Provides functionality for:
|
||||||
|
- Playing background music and sound effects
|
||||||
|
- Positional audio
|
||||||
|
- Volume controls
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pygame
|
||||||
|
import random
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
from os import listdir
|
||||||
|
from os.path import isfile, join
|
||||||
|
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.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
soundDir (str): Directory containing sound files (default: "sounds/")
|
||||||
|
volumeService (VolumeService): Volume service (default: global instance)
|
||||||
|
"""
|
||||||
|
self.soundDir = soundDir
|
||||||
|
self.sounds = {}
|
||||||
|
self.volumeService = volumeService or VolumeService.get_instance()
|
||||||
|
|
||||||
|
# Initialize pygame mixer if not already done
|
||||||
|
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) # Reserve channel for cut scenes
|
||||||
|
|
||||||
|
# Load sounds
|
||||||
|
self.load_sounds()
|
||||||
|
|
||||||
|
def load_sounds(self):
|
||||||
|
"""Load all sound files from the sound directory."""
|
||||||
|
try:
|
||||||
|
soundFiles = [f for f in listdir(self.soundDir)
|
||||||
|
if isfile(join(self.soundDir, f))
|
||||||
|
and (f.split('.')[1].lower() in ["ogg", "wav"])]
|
||||||
|
|
||||||
|
# Create dictionary of sound objects
|
||||||
|
for f in soundFiles:
|
||||||
|
self.sounds[f.split('.')[0]] = pygame.mixer.Sound(join(self.soundDir, f))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading sounds: {e}")
|
||||||
|
|
||||||
|
def play_intro(self):
|
||||||
|
"""Play the game intro sound if available."""
|
||||||
|
if 'game-intro' in self.sounds:
|
||||||
|
self.cut_scene('game-intro')
|
||||||
|
|
||||||
|
def get_sounds(self):
|
||||||
|
"""Get the dictionary of loaded sound objects.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Dictionary of loaded sound objects
|
||||||
|
"""
|
||||||
|
return self.sounds
|
||||||
|
|
||||||
|
def play_bgm(self, musicFile):
|
||||||
|
"""Play background music with proper volume settings.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
musicFile (str): Path to the music file to play
|
||||||
|
"""
|
||||||
|
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) # Loop indefinitely
|
||||||
|
except Exception as e:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def play_sound(self, soundName, volume=1.0):
|
||||||
|
"""Play a sound with current volume settings applied.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
soundName (str): Name of sound to play
|
||||||
|
volume (float): Base volume for the sound (0.0-1.0, default: 1.0)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
pygame.mixer.Channel: The channel the sound is playing on
|
||||||
|
"""
|
||||||
|
if soundName not in self.sounds:
|
||||||
|
return None
|
||||||
|
|
||||||
|
sound = self.sounds[soundName]
|
||||||
|
channel = sound.play()
|
||||||
|
if channel:
|
||||||
|
channel.set_volume(volume * self.volumeService.get_sfx_volume())
|
||||||
|
return channel
|
||||||
|
|
||||||
|
def calculate_volume_and_pan(self, playerPos, objPos, maxDistance=12):
|
||||||
|
"""Calculate volume and stereo panning based on relative positions.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
playerPos (float): Player's position on x-axis
|
||||||
|
objPos (float): Object's position on x-axis
|
||||||
|
maxDistance (float): Maximum audible distance (default: 12)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple: (volume, left_vol, right_vol) values between 0 and 1
|
||||||
|
"""
|
||||||
|
distance = abs(playerPos - objPos)
|
||||||
|
|
||||||
|
if distance > maxDistance:
|
||||||
|
return 0, 0, 0 # No sound if out of range
|
||||||
|
|
||||||
|
# Calculate volume (non-linear scaling for more noticeable changes)
|
||||||
|
# Apply masterVolume as the maximum possible volume
|
||||||
|
volume = (((maxDistance - distance) / maxDistance) ** 1.5) * self.volumeService.masterVolume
|
||||||
|
|
||||||
|
# Determine left/right based on relative position
|
||||||
|
if playerPos < objPos:
|
||||||
|
# Object is to the right
|
||||||
|
left = max(0, 1 - (objPos - playerPos) / maxDistance)
|
||||||
|
right = 1
|
||||||
|
elif playerPos > objPos:
|
||||||
|
# Object is to the left
|
||||||
|
left = 1
|
||||||
|
right = max(0, 1 - (playerPos - objPos) / maxDistance)
|
||||||
|
else:
|
||||||
|
# Player is on the object
|
||||||
|
left = right = 1
|
||||||
|
|
||||||
|
return volume, left, right
|
||||||
|
|
||||||
|
def obj_play(self, soundName, playerPos, objPos, loop=True):
|
||||||
|
"""Play a sound with positional audio.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
soundName (str): Name of sound to play
|
||||||
|
playerPos (float): Player's position for audio panning
|
||||||
|
objPos (float): Object's position for audio panning
|
||||||
|
loop (bool): Whether to loop the sound (default: True)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
pygame.mixer.Channel: Sound channel object, or None if out of range
|
||||||
|
"""
|
||||||
|
if soundName not in self.sounds:
|
||||||
|
return None
|
||||||
|
|
||||||
|
volume, left, right = self.calculate_volume_and_pan(playerPos, objPos)
|
||||||
|
if volume == 0:
|
||||||
|
return None # Don't play if out of range
|
||||||
|
|
||||||
|
# Play the sound on a new channel
|
||||||
|
channel = self.sounds[soundName].play(-1 if loop else 0)
|
||||||
|
if channel:
|
||||||
|
channel.set_volume(
|
||||||
|
volume * left * self.volumeService.sfxVolume,
|
||||||
|
volume * right * self.volumeService.sfxVolume
|
||||||
|
)
|
||||||
|
return channel
|
||||||
|
|
||||||
|
def obj_update(self, channel, playerPos, objPos):
|
||||||
|
"""Update positional audio for a playing sound.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
channel: Sound channel to update
|
||||||
|
playerPos (float): New player position
|
||||||
|
objPos (float): New object position
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
pygame.mixer.Channel: Updated channel, or None if sound should stop
|
||||||
|
"""
|
||||||
|
if channel is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
volume, left, right = self.calculate_volume_and_pan(playerPos, objPos)
|
||||||
|
if volume == 0:
|
||||||
|
channel.stop()
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Apply the volume and pan
|
||||||
|
channel.set_volume(
|
||||||
|
volume * left * self.volumeService.sfxVolume,
|
||||||
|
volume * right * self.volumeService.sfxVolume
|
||||||
|
)
|
||||||
|
return channel
|
||||||
|
|
||||||
|
def obj_stop(self, channel):
|
||||||
|
"""Stop a playing sound channel.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
channel: Sound channel to stop
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None if stopped successfully, otherwise returns original channel
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
channel.stop()
|
||||||
|
return None
|
||||||
|
except:
|
||||||
|
return channel
|
||||||
|
|
||||||
|
def play_ambiance(self, soundNames, probability, randomLocation=False):
|
||||||
|
"""Play random ambient sounds with optional positional audio.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
soundNames (list): List of possible sound names to choose from
|
||||||
|
probability (int): Chance to play (1-100)
|
||||||
|
randomLocation (bool): Whether to randomize stereo position
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
pygame.mixer.Channel: Sound channel if played, None otherwise
|
||||||
|
"""
|
||||||
|
# Check if any of the sounds in the list is already playing
|
||||||
|
for soundName in soundNames:
|
||||||
|
if pygame.mixer.find_channel(True) and pygame.mixer.find_channel(True).get_busy():
|
||||||
|
return None
|
||||||
|
|
||||||
|
if random.randint(1, 100) > probability:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Choose a random sound from the list
|
||||||
|
ambianceSound = random.choice(soundNames)
|
||||||
|
if ambianceSound not in self.sounds:
|
||||||
|
return None
|
||||||
|
|
||||||
|
channel = self.sounds[ambianceSound].play()
|
||||||
|
|
||||||
|
if randomLocation and channel:
|
||||||
|
leftVolume = random.random() * self.volumeService.get_sfx_volume()
|
||||||
|
rightVolume = random.random() * self.volumeService.get_sfx_volume()
|
||||||
|
channel.set_volume(leftVolume, rightVolume)
|
||||||
|
|
||||||
|
return channel
|
||||||
|
|
||||||
|
def play_random(self, soundPrefix, pause=False, interrupt=False):
|
||||||
|
"""Play a random variation of a sound.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
soundPrefix (str): Base name of sound (will match all starting with this)
|
||||||
|
pause (bool): Whether to pause execution until sound finishes
|
||||||
|
interrupt (bool): Whether to interrupt other sounds
|
||||||
|
"""
|
||||||
|
keys = []
|
||||||
|
for i in self.sounds.keys():
|
||||||
|
if re.match("^" + soundPrefix + ".*", i):
|
||||||
|
keys.append(i)
|
||||||
|
|
||||||
|
if not keys: # No matching sounds found
|
||||||
|
return None
|
||||||
|
|
||||||
|
randomKey = random.choice(keys)
|
||||||
|
|
||||||
|
if interrupt:
|
||||||
|
self.cut_scene(randomKey)
|
||||||
|
return
|
||||||
|
|
||||||
|
channel = self.sounds[randomKey].play()
|
||||||
|
sfxVolume = self.volumeService.get_sfx_volume()
|
||||||
|
|
||||||
|
if channel:
|
||||||
|
channel.set_volume(sfxVolume, sfxVolume)
|
||||||
|
|
||||||
|
if pause:
|
||||||
|
time.sleep(self.sounds[randomKey].get_length())
|
||||||
|
|
||||||
|
return channel
|
||||||
|
|
||||||
|
def play_random_positional(self, soundPrefix, playerX, objectX):
|
||||||
|
"""Play a random variation of a sound with positional audio.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
soundPrefix (str): Base name of sound to match
|
||||||
|
playerX (float): Player's x position
|
||||||
|
objectX (float): Object's x position
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
pygame.mixer.Channel: Sound channel if played, None otherwise
|
||||||
|
"""
|
||||||
|
keys = [k for k in self.sounds.keys() if k.startswith(soundPrefix)]
|
||||||
|
if not keys:
|
||||||
|
return None
|
||||||
|
|
||||||
|
randomKey = random.choice(keys)
|
||||||
|
volume, left, right = self.calculate_volume_and_pan(playerX, objectX)
|
||||||
|
|
||||||
|
if volume == 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
channel = self.sounds[randomKey].play()
|
||||||
|
if channel:
|
||||||
|
channel.set_volume(
|
||||||
|
volume * left * self.volumeService.sfxVolume,
|
||||||
|
volume * right * self.volumeService.sfxVolume
|
||||||
|
)
|
||||||
|
return channel
|
||||||
|
|
||||||
|
def play_directional_sound(self, soundName, playerPos, objPos, centerDistance=3, volume=1.0):
|
||||||
|
"""Play a sound with simplified directional audio.
|
||||||
|
|
||||||
|
For sounds that need to be heard clearly regardless of distance, but still provide
|
||||||
|
directional feedback. Sound plays at full volume but pans left/right based on relative position.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
soundName (str): Name of sound to play
|
||||||
|
playerPos (float): Player's x position
|
||||||
|
objPos (float): Object's x position
|
||||||
|
centerDistance (float): Distance within which sound plays center (default: 3)
|
||||||
|
volume (float): Base volume multiplier (0.0-1.0, default: 1.0)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
pygame.mixer.Channel: The channel the sound is playing on
|
||||||
|
"""
|
||||||
|
if soundName not in self.sounds:
|
||||||
|
return None
|
||||||
|
|
||||||
|
channel = self.sounds[soundName].play()
|
||||||
|
if channel:
|
||||||
|
# Apply volume settings
|
||||||
|
finalVolume = volume * self.volumeService.get_sfx_volume()
|
||||||
|
|
||||||
|
# If player is within centerDistance tiles of object, play in center
|
||||||
|
if abs(playerPos - objPos) <= centerDistance:
|
||||||
|
# Equal volume in both speakers (center)
|
||||||
|
channel.set_volume(finalVolume, finalVolume)
|
||||||
|
elif playerPos > objPos:
|
||||||
|
# Object is to the left of player
|
||||||
|
channel.set_volume(finalVolume, (finalVolume + 0.01) / 2)
|
||||||
|
else:
|
||||||
|
# Object is to the right of player
|
||||||
|
channel.set_volume((finalVolume + 0.01) / 2, finalVolume)
|
||||||
|
return channel
|
||||||
|
|
||||||
|
def cut_scene(self, soundName):
|
||||||
|
"""Play a sound as a cut scene, stopping other sounds.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
soundName (str): Name of sound to play
|
||||||
|
"""
|
||||||
|
if soundName not in self.sounds:
|
||||||
|
return
|
||||||
|
|
||||||
|
pygame.event.clear()
|
||||||
|
pygame.mixer.stop()
|
||||||
|
|
||||||
|
# Get the reserved channel (0) for cut scenes
|
||||||
|
channel = pygame.mixer.Channel(0)
|
||||||
|
|
||||||
|
# Apply the appropriate volume settings
|
||||||
|
sfxVolume = self.volumeService.get_sfx_volume()
|
||||||
|
channel.set_volume(sfxVolume, sfxVolume)
|
||||||
|
|
||||||
|
# Play the sound
|
||||||
|
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
|
||||||
|
pygame.time.delay(10)
|
||||||
|
|
||||||
|
def play_random_falling(self, soundPrefix, playerX, objectX, startY,
|
||||||
|
currentY=0, maxY=20, existingChannel=None):
|
||||||
|
"""Play or update a falling sound with positional audio and volume based on height.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
soundPrefix (str): Base name of sound to match
|
||||||
|
playerX (float): Player's x position
|
||||||
|
objectX (float): Object's x position
|
||||||
|
startY (float): Starting Y position (0-20, higher = quieter start)
|
||||||
|
currentY (float): Current Y position (0 = ground level) (default: 0)
|
||||||
|
maxY (float): Maximum Y value (default: 20)
|
||||||
|
existingChannel: Existing sound channel to update (default: None)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
pygame.mixer.Channel: Sound channel for updating position/volume,
|
||||||
|
or None if sound should stop
|
||||||
|
"""
|
||||||
|
# Calculate horizontal positioning
|
||||||
|
volume, left, right = self.calculate_volume_and_pan(playerX, objectX)
|
||||||
|
|
||||||
|
# Calculate vertical fall volume multiplier (0 at maxY, 1 at y=0)
|
||||||
|
fallMultiplier = 1 - (currentY / maxY)
|
||||||
|
|
||||||
|
# Adjust final volumes
|
||||||
|
finalVolume = volume * fallMultiplier
|
||||||
|
finalLeft = left * finalVolume
|
||||||
|
finalRight = right * finalVolume
|
||||||
|
|
||||||
|
if existingChannel is not None:
|
||||||
|
if volume == 0: # Out of audible range
|
||||||
|
existingChannel.stop()
|
||||||
|
return None
|
||||||
|
existingChannel.set_volume(
|
||||||
|
finalLeft * self.volumeService.sfxVolume,
|
||||||
|
finalRight * self.volumeService.sfxVolume
|
||||||
|
)
|
||||||
|
return existingChannel
|
||||||
|
else: # Need to create new channel
|
||||||
|
if volume == 0: # Don't start if out of range
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Find matching sound files
|
||||||
|
keys = [k for k in self.sounds.keys() if k.startswith(soundPrefix)]
|
||||||
|
if not keys:
|
||||||
|
return None
|
||||||
|
|
||||||
|
randomKey = random.choice(keys)
|
||||||
|
channel = self.sounds[randomKey].play()
|
||||||
|
if channel:
|
||||||
|
channel.set_volume(
|
||||||
|
finalLeft * self.volumeService.sfxVolume,
|
||||||
|
finalRight * self.volumeService.sfxVolume
|
||||||
|
)
|
||||||
|
return channel
|
||||||
|
|
||||||
|
|
||||||
|
# Global functions for backward compatibility
|
||||||
|
|
||||||
|
def play_bgm(musicFile):
|
||||||
|
"""Play background music with proper volume settings.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
musicFile (str): Path to the music file to play
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
pygame.mixer.music.stop()
|
||||||
|
pygame.mixer.music.load(musicFile)
|
||||||
|
pygame.mixer.music.set_volume(volumeService.get_bgm_volume())
|
||||||
|
pygame.mixer.music.play(-1) # Loop indefinitely
|
||||||
|
except Exception as e:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def adjust_master_volume(change):
|
||||||
|
"""Adjust the master volume for all sounds.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
change (float): Amount to change volume by (positive or negative)
|
||||||
|
"""
|
||||||
|
volumeService.adjust_master_volume(change, pygame.mixer)
|
||||||
|
|
||||||
|
def adjust_bgm_volume(change):
|
||||||
|
"""Adjust only the background music volume.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
change (float): Amount to change volume by (positive or negative)
|
||||||
|
"""
|
||||||
|
volumeService.adjust_bgm_volume(change, pygame.mixer)
|
||||||
|
|
||||||
|
def adjust_sfx_volume(change):
|
||||||
|
"""Adjust volume for sound effects only.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
change (float): Amount to change volume by (positive or negative)
|
||||||
|
"""
|
||||||
|
volumeService.adjust_sfx_volume(change, pygame.mixer)
|
||||||
|
|
||||||
|
def calculate_volume_and_pan(playerPos, objPos):
|
||||||
|
"""Calculate volume and stereo panning based on relative positions.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
playerPos (float): Player's position on x-axis
|
||||||
|
objPos (float): Object's position on x-axis
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple: (volume, left_vol, right_vol) values between 0 and 1
|
||||||
|
"""
|
||||||
|
distance = abs(playerPos - objPos)
|
||||||
|
maxDistance = 12 # Maximum audible distance
|
||||||
|
|
||||||
|
if distance > maxDistance:
|
||||||
|
return 0, 0, 0 # No sound if out of range
|
||||||
|
|
||||||
|
# Calculate volume (non-linear scaling for more noticeable changes)
|
||||||
|
# Apply masterVolume as the maximum possible volume
|
||||||
|
volume = (((maxDistance - distance) / maxDistance) ** 1.5) * volumeService.masterVolume
|
||||||
|
|
||||||
|
# Determine left/right based on relative position
|
||||||
|
if playerPos < objPos:
|
||||||
|
# Object is to the right
|
||||||
|
left = max(0, 1 - (objPos - playerPos) / maxDistance)
|
||||||
|
right = 1
|
||||||
|
elif playerPos > objPos:
|
||||||
|
# Object is to the left
|
||||||
|
left = 1
|
||||||
|
right = max(0, 1 - (playerPos - objPos) / maxDistance)
|
||||||
|
else:
|
||||||
|
# Player is on the object
|
||||||
|
left = right = 1
|
||||||
|
|
||||||
|
return volume, left, right
|
||||||
|
|
||||||
|
def play_sound(sound, volume=1.0):
|
||||||
|
"""Play a sound with current volume settings applied.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sound: pygame Sound object to play
|
||||||
|
volume: base volume for the sound (0.0-1.0, default: 1.0)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
pygame.mixer.Channel: The channel the sound is playing on
|
||||||
|
"""
|
||||||
|
channel = sound.play()
|
||||||
|
if channel:
|
||||||
|
channel.set_volume(volume * volumeService.get_sfx_volume())
|
||||||
|
return channel
|
||||||
|
|
||||||
|
def obj_play(sounds, soundName, playerPos, objPos, loop=True):
|
||||||
|
"""Play a sound with positional audio.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sounds (dict): Dictionary of sound objects
|
||||||
|
soundName (str): Name of sound to play
|
||||||
|
playerPos (float): Player's position for audio panning
|
||||||
|
objPos (float): Object's position for audio panning
|
||||||
|
loop (bool): Whether to loop the sound (default: True)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
pygame.mixer.Channel: Sound channel object, or None if out of range
|
||||||
|
"""
|
||||||
|
volume, left, right = calculate_volume_and_pan(playerPos, objPos)
|
||||||
|
if volume == 0:
|
||||||
|
return None # Don't play if out of range
|
||||||
|
|
||||||
|
# Play the sound on a new channel
|
||||||
|
channel = sounds[soundName].play(-1 if loop else 0)
|
||||||
|
if channel:
|
||||||
|
channel.set_volume(volume * left * volumeService.sfxVolume,
|
||||||
|
volume * right * volumeService.sfxVolume)
|
||||||
|
return channel
|
||||||
|
|
||||||
|
def obj_update(channel, playerPos, objPos):
|
||||||
|
"""Update positional audio for a playing sound.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
channel: Sound channel to update
|
||||||
|
playerPos (float): New player position
|
||||||
|
objPos (float): New object position
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
pygame.mixer.Channel: Updated channel, or None if sound should stop
|
||||||
|
"""
|
||||||
|
if channel is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
volume, left, right = calculate_volume_and_pan(playerPos, objPos)
|
||||||
|
if volume == 0:
|
||||||
|
channel.stop()
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Apply the volume and pan
|
||||||
|
channel.set_volume(volume * left * volumeService.sfxVolume,
|
||||||
|
volume * right * volumeService.sfxVolume)
|
||||||
|
return channel
|
||||||
|
|
||||||
|
def obj_stop(channel):
|
||||||
|
"""Stop a playing sound channel.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
channel: Sound channel to stop
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None if stopped successfully, otherwise returns original channel
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
channel.stop()
|
||||||
|
return None
|
||||||
|
except:
|
||||||
|
return channel
|
||||||
|
|
||||||
|
def play_ambiance(sounds, soundNames, probability, randomLocation=False):
|
||||||
|
"""Play random ambient sounds with optional positional audio.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sounds (dict): Dictionary of sound objects
|
||||||
|
soundNames (list): List of possible sound names to choose from
|
||||||
|
probability (int): Chance to play (1-100)
|
||||||
|
randomLocation (bool): Whether to randomize stereo position
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
pygame.mixer.Channel: Sound channel if played, None otherwise
|
||||||
|
"""
|
||||||
|
# Check if any of the sounds in the list is already playing
|
||||||
|
for soundName in soundNames:
|
||||||
|
if pygame.mixer.find_channel(True) and pygame.mixer.find_channel(True).get_busy():
|
||||||
|
return None
|
||||||
|
|
||||||
|
if random.randint(1, 100) > probability:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Choose a random sound from the list
|
||||||
|
ambianceSound = random.choice(soundNames)
|
||||||
|
channel = sounds[ambianceSound].play()
|
||||||
|
|
||||||
|
if randomLocation and channel:
|
||||||
|
leftVolume = random.random() * volumeService.get_sfx_volume()
|
||||||
|
rightVolume = random.random() * volumeService.get_sfx_volume()
|
||||||
|
channel.set_volume(leftVolume, rightVolume)
|
||||||
|
|
||||||
|
return channel
|
||||||
|
|
||||||
|
def play_random(sounds, soundName, pause=False, interrupt=False):
|
||||||
|
"""Play a random variation of a sound.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sounds (dict): Dictionary of sound objects
|
||||||
|
soundName (str): Base name of sound (will match all starting with this)
|
||||||
|
pause (bool): Whether to pause execution until sound finishes
|
||||||
|
interrupt (bool): Whether to interrupt other sounds
|
||||||
|
"""
|
||||||
|
key = []
|
||||||
|
for i in sounds.keys():
|
||||||
|
if re.match("^" + soundName + ".*", i):
|
||||||
|
key.append(i)
|
||||||
|
|
||||||
|
if not key: # No matching sounds found
|
||||||
|
return
|
||||||
|
|
||||||
|
randomKey = random.choice(key)
|
||||||
|
|
||||||
|
if interrupt:
|
||||||
|
cut_scene(sounds, randomKey)
|
||||||
|
return
|
||||||
|
|
||||||
|
channel = sounds[randomKey].play()
|
||||||
|
if channel:
|
||||||
|
sfxVolume = volumeService.get_sfx_volume()
|
||||||
|
channel.set_volume(sfxVolume, sfxVolume)
|
||||||
|
|
||||||
|
if pause:
|
||||||
|
time.sleep(sounds[randomKey].get_length())
|
||||||
|
|
||||||
|
def play_random_positional(sounds, soundName, playerX, objectX):
|
||||||
|
"""Play a random variation of a sound with positional audio.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sounds (dict): Dictionary of sound objects
|
||||||
|
soundName (str): Base name of sound to match
|
||||||
|
playerX (float): Player's x position
|
||||||
|
objectX (float): Object's x position
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
pygame.mixer.Channel: Sound channel if played, None otherwise
|
||||||
|
"""
|
||||||
|
keys = [k for k in sounds.keys() if k.startswith(soundName)]
|
||||||
|
if not keys:
|
||||||
|
return None
|
||||||
|
|
||||||
|
randomKey = random.choice(keys)
|
||||||
|
volume, left, right = calculate_volume_and_pan(playerX, objectX)
|
||||||
|
|
||||||
|
if volume == 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
channel = sounds[randomKey].play()
|
||||||
|
if channel:
|
||||||
|
channel.set_volume(volume * left * volumeService.sfxVolume,
|
||||||
|
volume * right * volumeService.sfxVolume)
|
||||||
|
return channel
|
||||||
|
|
||||||
|
def play_directional_sound(sounds, soundName, playerPos, objPos, centerDistance=3, volume=1.0):
|
||||||
|
"""Play a sound with simplified directional audio.
|
||||||
|
|
||||||
|
For sounds that need to be heard clearly regardless of distance, but still provide
|
||||||
|
directional feedback. Sound plays at full volume but pans left/right based on relative position.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sounds (dict): Dictionary of sound objects
|
||||||
|
soundName (str): Name of sound to play
|
||||||
|
playerPos (float): Player's x position
|
||||||
|
objPos (float): Object's x position
|
||||||
|
centerDistance (float): Distance within which sound plays center (default: 3)
|
||||||
|
volume (float): Base volume multiplier (0.0-1.0, default: 1.0)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
pygame.mixer.Channel: The channel the sound is playing on
|
||||||
|
"""
|
||||||
|
channel = sounds[soundName].play()
|
||||||
|
if channel:
|
||||||
|
# Apply volume settings
|
||||||
|
finalVolume = volume * volumeService.get_sfx_volume()
|
||||||
|
|
||||||
|
# If player is within centerDistance tiles of object, play in center
|
||||||
|
if abs(playerPos - objPos) <= centerDistance:
|
||||||
|
# Equal volume in both speakers (center)
|
||||||
|
channel.set_volume(finalVolume, finalVolume)
|
||||||
|
elif playerPos > objPos:
|
||||||
|
# Object is to the left of player
|
||||||
|
channel.set_volume(finalVolume, (finalVolume + 0.01) / 2)
|
||||||
|
else:
|
||||||
|
# Object is to the right of player
|
||||||
|
channel.set_volume((finalVolume + 0.01) / 2, finalVolume)
|
||||||
|
return channel
|
||||||
|
|
||||||
|
def cut_scene(sounds, soundName):
|
||||||
|
"""Play a sound as a cut scene, stopping other sounds.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sounds (dict): Dictionary of sound objects
|
||||||
|
soundName (str): Name of sound to play
|
||||||
|
"""
|
||||||
|
pygame.event.clear()
|
||||||
|
pygame.mixer.stop()
|
||||||
|
|
||||||
|
# Get the reserved channel (0) for cut scenes
|
||||||
|
channel = pygame.mixer.Channel(0)
|
||||||
|
|
||||||
|
# Apply the appropriate volume settings
|
||||||
|
sfxVolume = volumeService.get_sfx_volume()
|
||||||
|
channel.set_volume(sfxVolume, sfxVolume)
|
||||||
|
|
||||||
|
# Play the sound
|
||||||
|
channel.play(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
|
||||||
|
pygame.time.delay(10)
|
||||||
|
|
||||||
|
def play_random_falling(sounds, soundName, playerX, objectX, startY,
|
||||||
|
currentY=0, maxY=20, existingChannel=None):
|
||||||
|
"""Play or update a falling sound with positional audio and volume based on height.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sounds (dict): Dictionary of sound objects
|
||||||
|
soundName (str): Base name of sound to match
|
||||||
|
playerX (float): Player's x position
|
||||||
|
objectX (float): Object's x position
|
||||||
|
startY (float): Starting Y position (0-20, higher = quieter start)
|
||||||
|
currentY (float): Current Y position (0 = ground level) (default: 0)
|
||||||
|
maxY (float): Maximum Y value (default: 20)
|
||||||
|
existingChannel: Existing sound channel to update (default: None)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
pygame.mixer.Channel: Sound channel for updating position/volume,
|
||||||
|
or None if sound should stop
|
||||||
|
"""
|
||||||
|
# Calculate horizontal positioning
|
||||||
|
volume, left, right = calculate_volume_and_pan(playerX, objectX)
|
||||||
|
|
||||||
|
# Calculate vertical fall volume multiplier (0 at maxY, 1 at y=0)
|
||||||
|
fallMultiplier = 1 - (currentY / maxY)
|
||||||
|
|
||||||
|
# Adjust final volumes
|
||||||
|
finalVolume = volume * fallMultiplier
|
||||||
|
finalLeft = left * finalVolume
|
||||||
|
finalRight = right * finalVolume
|
||||||
|
|
||||||
|
if existingChannel is not None:
|
||||||
|
if volume == 0: # Out of audible range
|
||||||
|
existingChannel.stop()
|
||||||
|
return None
|
||||||
|
existingChannel.set_volume(finalLeft * volumeService.sfxVolume,
|
||||||
|
finalRight * volumeService.sfxVolume)
|
||||||
|
return existingChannel
|
||||||
|
else: # Need to create new channel
|
||||||
|
if volume == 0: # Don't start if out of range
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Find matching sound files
|
||||||
|
keys = [k for k in sounds.keys() if k.startswith(soundName)]
|
||||||
|
if not keys:
|
||||||
|
return None
|
||||||
|
|
||||||
|
randomKey = random.choice(keys)
|
||||||
|
channel = sounds[randomKey].play()
|
||||||
|
if channel:
|
||||||
|
channel.set_volume(finalLeft * volumeService.sfxVolume,
|
||||||
|
finalRight * volumeService.sfxVolume)
|
||||||
|
return channel
|
||||||
149
speech.py
Normal file
149
speech.py
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
#!/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
Normal file
454
utils.py
Normal file
@@ -0,0 +1,454 @@
|
|||||||
|
#!/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