1115 lines
32 KiB
Markdown
1115 lines
32 KiB
Markdown
# libstormgames
|
|
|
|
A Python library to make creating audio games easier.
|
|
|
|
|
|
## Overview
|
|
|
|
`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
|
|
- **NEW**: Statistics tracking with level/total separation
|
|
- **NEW**: Save/load management with atomic operations
|
|
- **NEW**: Combat systems with weapons and projectiles
|
|
|
|
|
|
## 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
|
|
- **stat_tracker**: Statistics tracking system
|
|
- **save_manager**: Save/load management
|
|
- **combat**: Weapon and projectile systems
|
|
|
|
|
|
## 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`.
|
|
|
|
|
|
### StatTracker
|
|
|
|
Flexible statistics tracking with separate level and total counters:
|
|
|
|
```python
|
|
# Initialize with default stats
|
|
stats = sg.StatTracker({
|
|
"kills": 0,
|
|
"deaths": 0,
|
|
"score": 0,
|
|
"time_played": 0.0
|
|
})
|
|
|
|
# Update stats during gameplay
|
|
stats.update_stat("kills", 1) # Increment kills
|
|
stats.update_stat("time_played", 1.5) # Add playtime
|
|
|
|
# Access current values
|
|
level_kills = stats.get_stat("kills") # Current level kills
|
|
total_kills = stats.get_stat("kills", from_total=True) # All-time kills
|
|
|
|
# Reset level stats for new level (keeps totals)
|
|
stats.reset_level()
|
|
|
|
# Add new stats dynamically
|
|
stats.add_stat("boss_kills", 0)
|
|
|
|
# Serialization for save/load
|
|
stats_data = stats.to_dict()
|
|
restored_stats = sg.StatTracker.from_dict(stats_data)
|
|
```
|
|
|
|
|
|
### SaveManager
|
|
|
|
Atomic save/load operations with corruption detection:
|
|
|
|
```python
|
|
# Initialize save manager
|
|
save_manager = sg.SaveManager("my-rpg-game")
|
|
|
|
# Create a comprehensive save
|
|
game_state = {
|
|
"player": {
|
|
"level": 5,
|
|
"hp": 80,
|
|
"inventory": ["sword", "health_potion"]
|
|
},
|
|
"world": {
|
|
"current_area": "enchanted_forest",
|
|
"completed_quests": ["tutorial", "first_boss"]
|
|
},
|
|
"stats": stats.to_dict() # Include StatTracker data
|
|
}
|
|
|
|
# Add metadata for save selection screen
|
|
metadata = {
|
|
"display_name": "Enchanted Forest - Level 5",
|
|
"level": 5,
|
|
"playtime": "2.5 hours"
|
|
}
|
|
|
|
# Create the save (atomic operation)
|
|
save_path = save_manager.create_save(game_state, metadata)
|
|
|
|
# List and load saves
|
|
save_files = save_manager.get_save_files() # Newest first
|
|
if save_files:
|
|
loaded_data, loaded_metadata = save_manager.load_save(save_files[0])
|
|
restored_stats = sg.StatTracker.from_dict(loaded_data["stats"])
|
|
```
|
|
|
|
|
|
### Combat Systems
|
|
|
|
#### Weapons
|
|
|
|
```python
|
|
# Create weapons using factory methods
|
|
sword = sg.Weapon.create_sword("Iron Sword", damage=15)
|
|
dagger = sg.Weapon.create_dagger("Steel Dagger", damage=12)
|
|
|
|
# Custom weapon with stat bonuses
|
|
bow = sg.Weapon(
|
|
name="Elvish Bow", damage=18, range_value=8,
|
|
cooldown=600, stat_bonuses={"speed": 1.15}
|
|
)
|
|
|
|
# Combat usage
|
|
if sword.can_attack(): # Check cooldown
|
|
damage = sword.attack() # Start attack
|
|
|
|
if sword.can_hit_target(enemy_pos, player_pos, facing_right, "enemy1"):
|
|
actual_damage = sword.hit_target("enemy1")
|
|
sg.speak(f"Hit for {actual_damage} damage!")
|
|
```
|
|
|
|
#### Projectiles
|
|
|
|
```python
|
|
# Create projectiles
|
|
arrow = sg.Projectile.create_arrow(
|
|
start_pos=(player_x, player_y),
|
|
direction=(1, 0) # Moving right
|
|
)
|
|
|
|
# Game loop integration
|
|
active_projectiles = []
|
|
|
|
def update_projectiles():
|
|
for projectile in active_projectiles[:]:
|
|
if not projectile.update(): # Move and check range
|
|
active_projectiles.remove(projectile)
|
|
continue
|
|
|
|
# Check enemy collisions
|
|
for enemy in enemies:
|
|
if projectile.check_collision(enemy.position, enemy.size):
|
|
damage = projectile.hit()
|
|
enemy.take_damage(damage)
|
|
active_projectiles.remove(projectile)
|
|
break
|
|
```
|
|
|
|
|
|
## 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 New Systems
|
|
|
|
```python
|
|
import libstormgames as sg
|
|
import pygame
|
|
import random
|
|
|
|
class ModernRPGGame:
|
|
def __init__(self):
|
|
# Create a Game instance that manages all subsystems
|
|
self.game = sg.Game("Modern RPG Demo").initialize()
|
|
|
|
# Initialize new game systems
|
|
self.player_stats = sg.StatTracker({
|
|
"level": 1, "exp": 0, "hp": 100, "mp": 50,
|
|
"kills": 0, "deaths": 0, "playtime": 0.0,
|
|
"items_found": 0, "gold": 0
|
|
})
|
|
self.save_manager = sg.SaveManager("modern-rpg-demo")
|
|
|
|
# Combat system
|
|
self.player_weapon = sg.Weapon.create_sword("Starting Sword", damage=10)
|
|
self.projectiles = []
|
|
self.enemies = []
|
|
|
|
# Game state
|
|
self.player_x = 5
|
|
self.player_y = 5
|
|
self.current_area = "village"
|
|
self.difficulty = "normal"
|
|
|
|
# Load settings
|
|
try:
|
|
self.difficulty = self.game.configService.localConfig.get("settings", "difficulty")
|
|
except:
|
|
self.game.configService.localConfig.add_section("settings")
|
|
self.game.configService.localConfig.set("settings", "difficulty", "normal")
|
|
self.game.configService.write_local_config()
|
|
|
|
def play_game(self):
|
|
"""Main game loop demonstrating new systems."""
|
|
self.game.speak(f"Starting game on {self.difficulty} difficulty")
|
|
self.game.play_bgm("sounds/game_music.ogg")
|
|
|
|
start_time = pygame.time.get_ticks()
|
|
|
|
# Game loop
|
|
running = True
|
|
while running:
|
|
current_time = pygame.time.get_ticks()
|
|
|
|
# Update playtime stats
|
|
playtime_hours = (current_time - start_time) / 3600000.0 # Convert to hours
|
|
self.player_stats.set_stat("playtime", playtime_hours, level_only=True)
|
|
|
|
# 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:
|
|
# Combat example
|
|
self.player_attack()
|
|
elif event.key == pygame.K_s:
|
|
# Quick save
|
|
self.quick_save()
|
|
elif event.key == pygame.K_l:
|
|
# Quick load
|
|
self.quick_load()
|
|
|
|
# Update game systems
|
|
self.update_combat()
|
|
self.update_player_stats()
|
|
|
|
# Random events
|
|
if random.random() < 0.01:
|
|
self.random_encounter()
|
|
|
|
pygame.time.delay(50)
|
|
|
|
# Game over - update total stats
|
|
total_playtime = self.player_stats.get_stat("playtime", from_total=True)
|
|
self.game.speak(f"Session ended. Total playtime: {total_playtime:.1f} hours")
|
|
|
|
return "menu"
|
|
|
|
def player_attack(self):
|
|
"""Handle player combat."""
|
|
if self.player_weapon.can_attack():
|
|
damage = self.player_weapon.attack()
|
|
self.game.speak("Attack!")
|
|
|
|
# Simulate hitting enemies
|
|
if self.enemies and self.player_weapon.is_attack_active():
|
|
enemy = self.enemies[0] # Attack first enemy
|
|
if self.player_weapon.can_hit_target(
|
|
enemy.position, (self.player_x, self.player_y),
|
|
facing_right=True, target_id=enemy.id
|
|
):
|
|
actual_damage = self.player_weapon.hit_target(enemy.id)
|
|
self.player_stats.update_stat("damage_dealt", actual_damage)
|
|
|
|
# Remove enemy if defeated
|
|
if enemy.take_damage(actual_damage):
|
|
self.enemies.remove(enemy)
|
|
self.player_stats.update_stat("kills", 1)
|
|
self.player_stats.update_stat("exp", 25)
|
|
self.game.speak("Enemy defeated!")
|
|
|
|
def update_combat(self):
|
|
"""Update combat systems."""
|
|
# Update projectiles
|
|
for projectile in self.projectiles[:]:
|
|
if not projectile.update():
|
|
self.projectiles.remove(projectile)
|
|
continue
|
|
|
|
# Check enemy collisions
|
|
for enemy in self.enemies[:]:
|
|
if projectile.check_collision(enemy.position, enemy.size):
|
|
damage = projectile.hit()
|
|
if enemy.take_damage(damage):
|
|
self.enemies.remove(enemy)
|
|
self.player_stats.update_stat("kills", 1)
|
|
self.projectiles.remove(projectile)
|
|
break
|
|
|
|
def update_player_stats(self):
|
|
"""Handle player progression."""
|
|
exp = self.player_stats.get_stat("exp")
|
|
level = self.player_stats.get_stat("level")
|
|
|
|
# Level up check
|
|
exp_needed = level * 100
|
|
if exp >= exp_needed:
|
|
self.player_stats.set_stat("level", level + 1)
|
|
self.player_stats.set_stat("exp", exp - exp_needed)
|
|
self.player_stats.set_stat("hp", 100) # Full heal on level up
|
|
self.game.speak(f"Level up! Now level {level + 1}!")
|
|
|
|
def random_encounter(self):
|
|
"""Create random encounters."""
|
|
self.game.speak("An enemy appears!")
|
|
# Add enemy logic here
|
|
self.player_stats.update_stat("encounters", 1)
|
|
|
|
def quick_save(self):
|
|
"""Create a quick save."""
|
|
try:
|
|
save_name = f"Quick Save - Level {self.player_stats.get_stat('level')}"
|
|
self.create_complete_save(save_name)
|
|
self.game.speak("Game saved!")
|
|
except Exception as e:
|
|
self.game.speak(f"Save failed: {e}")
|
|
|
|
def quick_load(self):
|
|
"""Load the most recent save."""
|
|
try:
|
|
saves = self.save_manager.get_save_files()
|
|
if saves:
|
|
self.load_complete_save(saves[0])
|
|
self.game.speak("Game loaded!")
|
|
else:
|
|
self.game.speak("No saves found!")
|
|
except Exception as e:
|
|
self.game.speak(f"Load failed: {e}")
|
|
|
|
def create_complete_save(self, save_name):
|
|
"""Create comprehensive save with all systems."""
|
|
complete_state = {
|
|
"player_stats": self.player_stats.to_dict(),
|
|
"weapon": self.player_weapon.to_dict(),
|
|
"player_position": (self.player_x, self.player_y),
|
|
"current_area": self.current_area,
|
|
"difficulty": self.difficulty,
|
|
"enemies": [enemy.to_dict() for enemy in self.enemies],
|
|
"projectiles": [proj.to_dict() for proj in self.projectiles]
|
|
}
|
|
|
|
metadata = {
|
|
"display_name": save_name,
|
|
"level": self.player_stats.get_stat("level"),
|
|
"location": self.current_area,
|
|
"playtime": f"{self.player_stats.get_stat('playtime', from_total=True):.1f}h"
|
|
}
|
|
|
|
return self.save_manager.create_save(complete_state, metadata)
|
|
|
|
def load_complete_save(self, save_path):
|
|
"""Load comprehensive save restoring all systems."""
|
|
data, metadata = self.save_manager.load_save(save_path)
|
|
|
|
# Restore all systems
|
|
self.player_stats = sg.StatTracker.from_dict(data["player_stats"])
|
|
self.player_weapon = sg.Weapon.from_dict(data["weapon"])
|
|
self.player_x, self.player_y = data["player_position"]
|
|
self.current_area = data["current_area"]
|
|
self.difficulty = data["difficulty"]
|
|
|
|
# Restore dynamic objects (implementation depends on your enemy/projectile classes)
|
|
# self.enemies = [Enemy.from_dict(e) for e in data["enemies"]]
|
|
# self.projectiles = [sg.Projectile.from_dict(p) for p in data["projectiles"]]
|
|
|
|
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
|
|
|
|
6. **New game systems**:
|
|
- Use StatTracker for comprehensive statistics with level/total separation
|
|
- Implement SaveManager for reliable save/load with metadata
|
|
- Leverage Combat systems for professional weapon and projectile mechanics
|
|
- Combine all systems for rich, full-featured games
|
|
|
|
7. **Performance considerations**:
|
|
- Reset level stats regularly to prevent memory bloat
|
|
- Clean up old saves periodically using SaveManager methods
|
|
- Remove inactive projectiles from update loops
|
|
- Use weapon cooldowns to prevent spam attacks
|
|
|
|
|
|
## 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.
|