Stats tracking updated. Work on skull storms. Enemy death sound added for goblins. Various updates and fixes.

This commit is contained in:
Storm Dragon
2025-02-04 00:28:50 -05:00
parent 1d033e067a
commit 4f7f5504d1
12 changed files with 323 additions and 47 deletions

118
levels/2.json Normal file
View File

@@ -0,0 +1,118 @@
{
"level_id": 2,
"name": "The Graveyard",
"description": "The mausoleum led to an ancient graveyard. Watch out for falling skulls!",
"player_start": {
"x": 0,
"y": 0
},
"objects": [
{
"x": 5,
"y": 3,
"sound": "coffin",
"type": "coffin"
},
{
"x_range": [15, 17],
"y": 3,
"sound": "coin",
"collectible": true,
"static": true
},
{
"x": 25,
"y": 0,
"enemy_type": "goblin",
"health": 4,
"damage": 2,
"attack_range": 1,
"movement_range": 5
},
{
"x": 35,
"y": 0,
"hazard": true,
"sound": "grave",
"static": true,
"zombie_spawn_chance": 15
},
{
"x_range": [45, 60],
"y": 12,
"type": "skull_storm",
"damage": 3,
"maximum_skulls": 2,
"frequency": {
"min": 3,
"max": 6
}
},
{
"x": 55,
"y": 3,
"sound": "coffin",
"type": "coffin"
},
{
"x_range": [65, 67],
"y": 3,
"sound": "coin",
"collectible": true,
"static": true
},
{
"x": 75,
"y": 0,
"enemy_type": "goblin",
"health": 5,
"damage": 2,
"attack_range": 1,
"movement_range": 6
},
{
"x": 85,
"y": 3,
"sound": "coffin",
"type": "coffin"
},
{
"x": 95,
"y": 0,
"hazard": true,
"sound": "grave",
"static": true,
"zombie_spawn_chance": 20
},
{
"x_range": [105, 107],
"y": 3,
"sound": "coin",
"collectible": true,
"static": true
},
{
"x": 120,
"y": 0,
"type": "catapult",
"direction": -1,
"fire_interval": 5000,
"range": 15
},
{
"x_range": [130, 160],
"y": 15,
"type": "skull_storm",
"damage": 4,
"maximum_skulls": 3,
"frequency": {
"min": 2,
"max": 5
}
}
],
"boundaries": {
"left": 0,
"right": 170
}
}

BIN
sounds/falling_skull2.ogg (Stored with Git LFS) Normal file

Binary file not shown.

BIN
sounds/goblin_dies.ogg (Stored with Git LFS) Normal file

Binary file not shown.

BIN
sounds/player_takes_damage.ogg (Stored with Git LFS) Normal file

Binary file not shown.

View File

@@ -3,8 +3,9 @@ from src.object import Object
from src.powerup import PowerUp from src.powerup import PowerUp
import random import random
class CoffinObject(Object): class CoffinObject(Object):
def __init__(self, x, y, sounds): def __init__(self, x, y, sounds, level):
super().__init__( super().__init__(
x, y, "coffin", x, y, "coffin",
isStatic=True, isStatic=True,
@@ -12,6 +13,7 @@ class CoffinObject(Object):
isHazard=False isHazard=False
) )
self.sounds = sounds self.sounds = sounds
self.level = level # Store level reference
self.is_broken = False self.is_broken = False
self.dropped_item = None self.dropped_item = None
@@ -20,6 +22,8 @@ class CoffinObject(Object):
if not self.is_broken: if not self.is_broken:
self.is_broken = True self.is_broken = True
self.sounds['coffin_shatter'].play() self.sounds['coffin_shatter'].play()
self.level.player.stats.update_stat('Coffins broken', 1)
self.level.player.stats.update_stat('Coffins remaining', -1)
# Stop the ongoing coffin sound # Stop the ongoing coffin sound
if self.channel: if self.channel:

View File

@@ -4,18 +4,19 @@ import pygame
class Enemy(Object): class Enemy(Object):
def __init__(self, xRange, y, enemyType, sounds, **kwargs): def __init__(self, xRange, y, enemyType, sounds, level, **kwargs):
# Initialize base object properties # Initialize base object properties
super().__init__( super().__init__(
xRange, xRange,
y, y,
f"{enemyType}", # Base sound for ambient noise f"{enemyType}", # Base sound
isStatic=False, isStatic=False,
isHazard=True isHazard=True
) )
# Enemy specific properties # Enemy specific properties
self.enemyType = enemyType self.enemyType = enemyType
self.level = level
self.health = kwargs.get('health', 5) # Default 5 HP self.health = kwargs.get('health', 5) # Default 5 HP
self.damage = kwargs.get('damage', 1) # Default 1 damage self.damage = kwargs.get('damage', 1) # Default 1 damage
self.attackRange = kwargs.get('attack_range', 1) # Default 1 tile range self.attackRange = kwargs.get('attack_range', 1) # Default 1 tile range
@@ -106,7 +107,7 @@ class Enemy(Object):
self.sounds[attackSound].play() self.sounds[attackSound].play()
# Deal damage to player # Deal damage to player
player.set_health(player.get_health() - self.damage) player.set_health(player.get_health() - self.damage)
speak(f"The {self.enemyType} hits you!") self.sounds['player_takes_damage'].play()
def take_damage(self, amount): def take_damage(self, amount):
"""Handle enemy taking damage""" """Handle enemy taking damage"""
@@ -120,7 +121,11 @@ class Enemy(Object):
if self.channel: if self.channel:
obj_stop(self.channel) obj_stop(self.channel)
self.channel = None self.channel = None
# Play death sound if available # Play death sound if available using positional audio
deathSound = f"{self.enemyType}_death" deathSound = f"{self.enemyType}_dies"
if deathSound in self.sounds: if deathSound in self.sounds:
self.sounds[deathSound].play() self.channel = obj_play(self.sounds, deathSound, self.level.player.xPos, self.xPos, loop=False)
# Update stats
self.level.player.stats.update_stat('Enemies killed', 1)
self.level.player.stats.update_stat('Enemies remaining', -1)

View File

@@ -72,7 +72,8 @@ class Level:
coffin = CoffinObject( coffin = CoffinObject(
xPos[0], xPos[0],
obj["y"], obj["y"],
self.sounds self.sounds,
self # Pass level reference
) )
self.objects.append(coffin) self.objects.append(coffin)
# Check if this is an enemy # Check if this is an enemy
@@ -82,6 +83,7 @@ class Level:
obj["y"], obj["y"],
obj["enemy_type"], obj["enemy_type"],
self.sounds, self.sounds,
self, # Pass level reference
health=obj.get("health", 5), health=obj.get("health", 5),
damage=obj.get("damage", 1), damage=obj.get("damage", 1),
attack_range=obj.get("attack_range", 1), attack_range=obj.get("attack_range", 1),
@@ -99,6 +101,10 @@ class Level:
zombie_spawn_chance=obj.get("zombie_spawn_chance", 0) zombie_spawn_chance=obj.get("zombie_spawn_chance", 0)
) )
self.objects.append(gameObject) self.objects.append(gameObject)
enemyCount = len(self.enemies)
coffinCount = sum(1 for obj in self.objects if hasattr(obj, 'is_broken'))
player.stats.update_stat('Enemies remaining', enemyCount)
player.stats.update_stat('Coffins remaining', coffinCount)
def update_audio(self): def update_audio(self):
"""Update all audio and entity state.""" """Update all audio and entity state."""
@@ -126,6 +132,7 @@ class Level:
obj.yPos, obj.yPos,
"zombie", "zombie",
self.sounds, self.sounds,
self, # Pass the level reference
health=3, health=3,
damage=10, damage=10,
attack_range=1 attack_range=1
@@ -235,13 +242,19 @@ class Level:
self.sounds[f'get_{obj.soundName}'].play() self.sounds[f'get_{obj.soundName}'].play()
obj.collect_at_position(currentPos) obj.collect_at_position(currentPos)
self.player.collectedItems.append(obj.soundName) self.player.collectedItems.append(obj.soundName)
self.player.stats.update_stat('Items collected', 1)
if obj.soundName == "coin": if obj.soundName == "coin":
self.player._coins += 1 self.player._coins += 1
self.player.stats.update_stat('Bone dust', 1)
elif obj.isHazard and not self.player.isJumping: elif obj.isHazard and not self.player.isJumping:
self.sounds[obj.soundName].play() if not self.player.isInvincible:
speak("You fell in an open grave!") self.sounds[obj.soundName].play()
self.player.set_health(0) speak("You fell in an open grave!")
return False self.player.set_health(0)
return False
else:
# When invincible, treat it like a successful jump over the grave
pass
# Handle boundaries # Handle boundaries
if self.player.xPos < self.leftBoundary: if self.player.xPos < self.leftBoundary:

View File

@@ -1,5 +1,6 @@
import pygame import pygame
from libstormgames import * from libstormgames import *
from src.stat_tracker import StatTracker
from src.weapon import Weapon from src.weapon import Weapon
@@ -21,6 +22,7 @@ class Player:
self._lives = 1 self._lives = 1
self.distanceSinceLastStep = 0 self.distanceSinceLastStep = 0
self.stepDistance = 0.5 self.stepDistance = 0.5
self.stats = StatTracker()
# Inventory system # Inventory system
self.inventory = [] self.inventory = []

View File

@@ -55,15 +55,15 @@ class SkullStorm(Object):
fallProgress = timeElapsed / skull['fall_duration'] fallProgress = timeElapsed / skull['fall_duration']
currentY = self.yPos * (1 - fallProgress) currentY = self.yPos * (1 - fallProgress)
if skull['channel'] is None or not skull['channel'].get_busy(): skull['channel'] = play_random_falling(
skull['channel'] = play_random_falling( self.sounds,
self.sounds, 'falling_skull',
'falling_skull', player.xPos,
player.xPos, skull['x'],
skull['x'], self.yPos,
self.yPos, currentY,
currentY existingChannel=skull['channel']
) )
# Check if we should spawn a new skull # Check if we should spawn a new skull
if (len(self.activeSkulls) < self.maxSkulls and if (len(self.activeSkulls) < self.maxSkulls and

44
src/stat_tracker.py Normal file
View File

@@ -0,0 +1,44 @@
class StatTracker:
def __init__(self):
# Base dictionary for tracking stats
self.total = {
'Bone dust': 0,
'Enemies killed': 0,
'Enemies remaining': 0,
'Coffins broken': 0,
'Coffins remaining': 0,
'Items collected': 0,
'Total time': 0
}
# Create level stats from total (shallow copy is fine here)
self.level = self.total.copy()
self.total['levelsCompleted'] = 0
def reset_level(self):
"""Reset level stats based on variable type"""
for key in self.level:
if isinstance(self.level[key], (int, float)):
self.level[key] = 0
elif isinstance(self.level[key], str):
self.level[key] = ""
elif isinstance(self.level[key], list):
self.level[key] = []
elif self.level[key] is None:
self.level[key] = None
def update_stat(self, statName, value=1, levelOnly=False):
"""Update a stat in both level and total (unless levelOnly is True)"""
if statName in self.level:
self.level[statName] += value
if not levelOnly and statName in self.total:
self.total[statName] += value
def get_level_stat(self, statName):
"""Get a level stat"""
return self.level.get(statName, 0)
def get_total_stat(self, statName):
"""Get a total stat"""
return self.total.get(statName, 0)

View File

@@ -9,22 +9,42 @@ from src.player import Player
class WickedQuest: class WickedQuest:
def __init__(self): def __init__(self):
"""Initialize game and load sounds."""
self.sounds = initialize_gui("Wicked Quest") self.sounds = initialize_gui("Wicked Quest")
self.currentLevel = None self.currentLevel = None
self.lastThrowTime = 0
self.throwDelay = 250
self.player = None # Will be initialized when first level loads
def load_level(self, levelNumber): def load_level(self, levelNumber):
"""Load a level from its JSON file."""
levelFile = f"levels/{levelNumber}.json" levelFile = f"levels/{levelNumber}.json"
try: try:
with open(levelFile, 'r') as f: with open(levelFile, 'r') as f:
levelData = json.load(f) levelData = json.load(f)
self.currentLevel = Level(levelData, self.sounds)
speak(f"Level {levelNumber} loaded") # Create player if this is the first level
if self.player is None:
self.player = Player(levelData["player_start"]["x"], levelData["player_start"]["y"], self.sounds)
else:
# Just update player position for new level
self.player.xPos = levelData["player_start"]["x"]
self.player.yPos = levelData["player_start"]["y"]
# Pass existing player to new level
self.currentLevel = Level(levelData, self.sounds, self.player)
# Announce level details
levelIntro = f"Level {levelData['level_id']}, {levelData['name']}. {levelData['description']}"
messagebox(levelIntro)
return True
except FileNotFoundError: except FileNotFoundError:
speak("Level not found") speak("Level not found")
return False return False
return True
def handle_input(self): def handle_input(self):
"""Process keyboard input for player actions."""
keys = pygame.key.get_pressed() keys = pygame.key.get_pressed()
player = self.currentLevel.player player = self.currentLevel.player
currentTime = pygame.time.get_ticks() currentTime = pygame.time.get_ticks()
@@ -45,17 +65,20 @@ class WickedQuest:
player.xPos += currentSpeed player.xPos += currentSpeed
player.facingRight = True player.facingRight = True
# Status queries
if keys[pygame.K_c]: if keys[pygame.K_c]:
speak(f"{player.get_coins()} coins") speak(f"{player.get_coins()} coins")
if keys[pygame.K_h]: if keys[pygame.K_h]:
speak(f"{player.get_health()} HP") speak(f"{player.get_health()} HP")
if keys[pygame.K_l]: if keys[pygame.K_l]:
speak(f"{player.get_lives()} lives") speak(f"{player.get_lives()} lives")
if keys[pygame.K_j]: # Check jack o'lanterns
if keys[pygame.K_f]: # Throw projectile speak(f"{player.get_jack_o_lanterns()} jack o'lanterns")
self.currentLevel.throw_projectile() if keys[pygame.K_f]:
currentTime = pygame.time.get_ticks()
if currentTime - self.lastThrowTime >= self.throwDelay:
self.currentLevel.throw_projectile()
self.lastThrowTime = currentTime
# Handle attack with either CTRL key # Handle attack with either CTRL key
if (keys[pygame.K_LCTRL] or keys[pygame.K_RCTRL]) and player.start_attack(currentTime): if (keys[pygame.K_LCTRL] or keys[pygame.K_RCTRL]) and player.start_attack(currentTime):
@@ -81,45 +104,103 @@ class WickedQuest:
# Reset step distance tracking after landing # Reset step distance tracking after landing
player.distanceSinceLastStep = 0 player.distanceSinceLastStep = 0
def display_level_stats(self, timeTaken):
"""Display level completion statistics."""
# Convert time from milliseconds to minutes:seconds
minutes = timeTaken // 60000
seconds = (timeTaken % 60000) // 1000
# Update time in stats
self.currentLevel.player.stats.update_stat('Total time', timeTaken, levelOnly=True)
report = [f"Level {self.currentLevel.levelId} Complete!"]
report.append(f"Time taken: {minutes} minutes and {seconds} seconds")
# Add all level stats
for key in self.currentLevel.player.stats.level:
if key != 'Total time': # Skip time since we already displayed it
report.append(f"{key}: {self.currentLevel.player.stats.get_level_stat(key)}")
pygame.mixer.stop()
display_text(report)
self.currentLevel.player.stats.reset_level()
def display_game_over(self, timeTaken):
"""Display game over screen with statistics."""
minutes = timeTaken // 60000
seconds = (timeTaken % 60000) // 1000
report = ["Game Over!"]
report.append(f"Time taken: {minutes} minutes and {seconds} seconds")
# Add all total stats
for key in self.currentLevel.player.stats.total:
if key not in ['Total time', 'levelsCompleted']: # Skip these
report.append(f"Total {key}: {self.currentLevel.player.stats.get_total_stat(key)}")
display_text(report)
def game_loop(self): def game_loop(self):
"""Main game loop handling updates and state changes."""
clock = pygame.time.Clock() clock = pygame.time.Clock()
startTime = pygame.time.get_ticks()
while self.currentLevel.player.get_health() > 0 and self.currentLevel.player.get_lives() > 0: currentLevelNum = 1
while True:
currentTime = pygame.time.get_ticks() currentTime = pygame.time.get_ticks()
if check_for_exit(): if check_for_exit():
return return
# Update player state (including power-ups) # Update game state
self.currentLevel.player.update(currentTime) self.currentLevel.player.update(currentTime)
self.handle_input() self.handle_input()
# Update audio positioning and handle collisions
self.currentLevel.update_audio() self.currentLevel.update_audio()
self.currentLevel.handle_collisions()
# Handle combat and projectiles
# Handle combat interactions
self.currentLevel.handle_combat(currentTime) self.currentLevel.handle_combat(currentTime)
# Update projectiles
self.currentLevel.handle_projectiles(currentTime) self.currentLevel.handle_projectiles(currentTime)
# Check for death first
if self.currentLevel.player.get_health() <= 0:
if self.currentLevel.player.get_lives() <= 0:
# Game over
pygame.mixer.stop()
self.display_game_over(pygame.time.get_ticks() - startTime)
return
# Handle collisions and check level completion
if self.currentLevel.handle_collisions(): # Changed from elif to if
# Level completed
self.display_level_stats(pygame.time.get_ticks() - startTime)
# Try to load next level
currentLevelNum += 1
if self.load_level(currentLevelNum):
# Reset timer for new level
startTime = pygame.time.get_ticks()
continue
else:
# No more levels - game complete!
messagebox("Congratulations! You've completed all available levels!")
self.display_game_over(pygame.time.get_ticks() - startTime)
return
clock.tick(60) # 60 FPS clock.tick(60) # 60 FPS
# Player died or ran out of lives
speak("Game Over")
def run(self): def run(self):
"""Main game loop with menu system."""
while True: while True:
choice = game_menu(self.sounds, "play", "instructions", "learn_sounds", "credits", "donate", "exit") choice = game_menu(self.sounds, "play", "instructions", "learn_sounds", "credits", "donate", "exit")
if choice == "exit": if choice == "exit":
exit_game() exit_game()
elif choice == "play": elif choice == "play":
self.player = None # Reset player for new game
if self.load_level(1): if self.load_level(1):
self.game_loop() self.game_loop()
if __name__ == "__main__": if __name__ == "__main__":
game = WickedQuest() game = WickedQuest()
game.run() game.run()