New functionality added inspired by Wicked Quest game.
This commit is contained in:
329
README.md
329
README.md
@@ -14,6 +14,9 @@ A Python library to make creating audio games easier.
|
||||
- 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
|
||||
@@ -129,6 +132,9 @@ The library is organized into modules, each with a specific focus:
|
||||
- **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
|
||||
@@ -297,6 +303,131 @@ Where `game-name` is the lowercase, hyphenated version of your game title. For e
|
||||
"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
|
||||
@@ -644,40 +775,60 @@ def create_sound_environment(player_x, player_y):
|
||||
```
|
||||
|
||||
|
||||
### Complete Game Structure with Class-Based Architecture
|
||||
### Complete Game Structure with New Systems
|
||||
|
||||
```python
|
||||
import libstormgames as sg
|
||||
import pygame
|
||||
import random
|
||||
|
||||
class MyGame:
|
||||
class ModernRPGGame:
|
||||
def __init__(self):
|
||||
# Create a Game instance that manages all subsystems
|
||||
self.game = sg.Game("My Advanced Game").initialize()
|
||||
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.config_service.local_config.get("settings", "difficulty")
|
||||
self.difficulty = self.game.configService.localConfig.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()
|
||||
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:
|
||||
# Update game state
|
||||
self.player_x += random.uniform(-0.2, 0.2)
|
||||
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():
|
||||
@@ -685,26 +836,148 @@ class MyGame:
|
||||
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()}")
|
||||
# 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()
|
||||
|
||||
# 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))
|
||||
# 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
|
||||
position = self.game.scoreboard.check_high_score()
|
||||
if position:
|
||||
self.game.speak(f"New high score! Position {position}")
|
||||
self.game.scoreboard.add_high_score()
|
||||
# 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
|
||||
@@ -779,6 +1052,18 @@ if __name__ == "__main__":
|
||||
- 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
|
||||
|
||||
|
Reference in New Issue
Block a user