Stats tracking updated. Work on skull storms. Enemy death sound added for goblins. Various updates and fixes.
This commit is contained in:
118
levels/2.json
Normal file
118
levels/2.json
Normal 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
|
||||
}
|
||||
}
|
||||
Submodule libstormgames updated: 658709ebce...80fe2caff3
BIN
sounds/falling_skull2.ogg
(Stored with Git LFS)
Normal file
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
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
BIN
sounds/player_takes_damage.ogg
(Stored with Git LFS)
Normal file
Binary file not shown.
@@ -3,8 +3,9 @@ from src.object import Object
|
||||
from src.powerup import PowerUp
|
||||
import random
|
||||
|
||||
|
||||
class CoffinObject(Object):
|
||||
def __init__(self, x, y, sounds):
|
||||
def __init__(self, x, y, sounds, level):
|
||||
super().__init__(
|
||||
x, y, "coffin",
|
||||
isStatic=True,
|
||||
@@ -12,6 +13,7 @@ class CoffinObject(Object):
|
||||
isHazard=False
|
||||
)
|
||||
self.sounds = sounds
|
||||
self.level = level # Store level reference
|
||||
self.is_broken = False
|
||||
self.dropped_item = None
|
||||
|
||||
@@ -20,6 +22,8 @@ class CoffinObject(Object):
|
||||
if not self.is_broken:
|
||||
self.is_broken = True
|
||||
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
|
||||
if self.channel:
|
||||
|
||||
17
src/enemy.py
17
src/enemy.py
@@ -4,18 +4,19 @@ import pygame
|
||||
|
||||
|
||||
class Enemy(Object):
|
||||
def __init__(self, xRange, y, enemyType, sounds, **kwargs):
|
||||
def __init__(self, xRange, y, enemyType, sounds, level, **kwargs):
|
||||
# Initialize base object properties
|
||||
super().__init__(
|
||||
xRange,
|
||||
y,
|
||||
f"{enemyType}", # Base sound for ambient noise
|
||||
f"{enemyType}", # Base sound
|
||||
isStatic=False,
|
||||
isHazard=True
|
||||
)
|
||||
|
||||
# Enemy specific properties
|
||||
self.enemyType = enemyType
|
||||
self.level = level
|
||||
self.health = kwargs.get('health', 5) # Default 5 HP
|
||||
self.damage = kwargs.get('damage', 1) # Default 1 damage
|
||||
self.attackRange = kwargs.get('attack_range', 1) # Default 1 tile range
|
||||
@@ -106,7 +107,7 @@ class Enemy(Object):
|
||||
self.sounds[attackSound].play()
|
||||
# Deal damage to player
|
||||
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):
|
||||
"""Handle enemy taking damage"""
|
||||
@@ -120,7 +121,11 @@ class Enemy(Object):
|
||||
if self.channel:
|
||||
obj_stop(self.channel)
|
||||
self.channel = None
|
||||
# Play death sound if available
|
||||
deathSound = f"{self.enemyType}_death"
|
||||
# Play death sound if available using positional audio
|
||||
deathSound = f"{self.enemyType}_dies"
|
||||
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)
|
||||
|
||||
15
src/level.py
15
src/level.py
@@ -72,7 +72,8 @@ class Level:
|
||||
coffin = CoffinObject(
|
||||
xPos[0],
|
||||
obj["y"],
|
||||
self.sounds
|
||||
self.sounds,
|
||||
self # Pass level reference
|
||||
)
|
||||
self.objects.append(coffin)
|
||||
# Check if this is an enemy
|
||||
@@ -82,6 +83,7 @@ class Level:
|
||||
obj["y"],
|
||||
obj["enemy_type"],
|
||||
self.sounds,
|
||||
self, # Pass level reference
|
||||
health=obj.get("health", 5),
|
||||
damage=obj.get("damage", 1),
|
||||
attack_range=obj.get("attack_range", 1),
|
||||
@@ -99,6 +101,10 @@ class Level:
|
||||
zombie_spawn_chance=obj.get("zombie_spawn_chance", 0)
|
||||
)
|
||||
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):
|
||||
"""Update all audio and entity state."""
|
||||
@@ -126,6 +132,7 @@ class Level:
|
||||
obj.yPos,
|
||||
"zombie",
|
||||
self.sounds,
|
||||
self, # Pass the level reference
|
||||
health=3,
|
||||
damage=10,
|
||||
attack_range=1
|
||||
@@ -235,13 +242,19 @@ class Level:
|
||||
self.sounds[f'get_{obj.soundName}'].play()
|
||||
obj.collect_at_position(currentPos)
|
||||
self.player.collectedItems.append(obj.soundName)
|
||||
self.player.stats.update_stat('Items collected', 1)
|
||||
if obj.soundName == "coin":
|
||||
self.player._coins += 1
|
||||
self.player.stats.update_stat('Bone dust', 1)
|
||||
elif obj.isHazard and not self.player.isJumping:
|
||||
if not self.player.isInvincible:
|
||||
self.sounds[obj.soundName].play()
|
||||
speak("You fell in an open grave!")
|
||||
self.player.set_health(0)
|
||||
return False
|
||||
else:
|
||||
# When invincible, treat it like a successful jump over the grave
|
||||
pass
|
||||
|
||||
# Handle boundaries
|
||||
if self.player.xPos < self.leftBoundary:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import pygame
|
||||
from libstormgames import *
|
||||
from src.stat_tracker import StatTracker
|
||||
from src.weapon import Weapon
|
||||
|
||||
|
||||
@@ -21,6 +22,7 @@ class Player:
|
||||
self._lives = 1
|
||||
self.distanceSinceLastStep = 0
|
||||
self.stepDistance = 0.5
|
||||
self.stats = StatTracker()
|
||||
|
||||
# Inventory system
|
||||
self.inventory = []
|
||||
|
||||
@@ -55,14 +55,14 @@ class SkullStorm(Object):
|
||||
fallProgress = timeElapsed / skull['fall_duration']
|
||||
currentY = self.yPos * (1 - fallProgress)
|
||||
|
||||
if skull['channel'] is None or not skull['channel'].get_busy():
|
||||
skull['channel'] = play_random_falling(
|
||||
self.sounds,
|
||||
'falling_skull',
|
||||
player.xPos,
|
||||
skull['x'],
|
||||
self.yPos,
|
||||
currentY
|
||||
currentY,
|
||||
existingChannel=skull['channel']
|
||||
)
|
||||
|
||||
# Check if we should spawn a new skull
|
||||
|
||||
44
src/stat_tracker.py
Normal file
44
src/stat_tracker.py
Normal 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)
|
||||
123
wicked_quest.py
123
wicked_quest.py
@@ -9,22 +9,42 @@ from src.player import Player
|
||||
|
||||
class WickedQuest:
|
||||
def __init__(self):
|
||||
"""Initialize game and load sounds."""
|
||||
self.sounds = initialize_gui("Wicked Quest")
|
||||
self.currentLevel = None
|
||||
self.lastThrowTime = 0
|
||||
self.throwDelay = 250
|
||||
self.player = None # Will be initialized when first level loads
|
||||
|
||||
def load_level(self, levelNumber):
|
||||
"""Load a level from its JSON file."""
|
||||
levelFile = f"levels/{levelNumber}.json"
|
||||
try:
|
||||
with open(levelFile, 'r') as 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:
|
||||
speak("Level not found")
|
||||
return False
|
||||
return True
|
||||
|
||||
def handle_input(self):
|
||||
"""Process keyboard input for player actions."""
|
||||
keys = pygame.key.get_pressed()
|
||||
player = self.currentLevel.player
|
||||
currentTime = pygame.time.get_ticks()
|
||||
@@ -45,17 +65,20 @@ class WickedQuest:
|
||||
player.xPos += currentSpeed
|
||||
player.facingRight = True
|
||||
|
||||
# Status queries
|
||||
if keys[pygame.K_c]:
|
||||
speak(f"{player.get_coins()} coins")
|
||||
|
||||
if keys[pygame.K_h]:
|
||||
speak(f"{player.get_health()} HP")
|
||||
|
||||
if keys[pygame.K_l]:
|
||||
speak(f"{player.get_lives()} lives")
|
||||
|
||||
if keys[pygame.K_f]: # Throw projectile
|
||||
if keys[pygame.K_j]: # Check jack o'lanterns
|
||||
speak(f"{player.get_jack_o_lanterns()} jack o'lanterns")
|
||||
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
|
||||
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
|
||||
player.distanceSinceLastStep = 0
|
||||
|
||||
def game_loop(self):
|
||||
clock = pygame.time.Clock()
|
||||
def display_level_stats(self, timeTaken):
|
||||
"""Display level completion statistics."""
|
||||
# Convert time from milliseconds to minutes:seconds
|
||||
minutes = timeTaken // 60000
|
||||
seconds = (timeTaken % 60000) // 1000
|
||||
|
||||
while self.currentLevel.player.get_health() > 0 and self.currentLevel.player.get_lives() > 0:
|
||||
# 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):
|
||||
"""Main game loop handling updates and state changes."""
|
||||
clock = pygame.time.Clock()
|
||||
startTime = pygame.time.get_ticks()
|
||||
currentLevelNum = 1
|
||||
|
||||
while True:
|
||||
currentTime = pygame.time.get_ticks()
|
||||
|
||||
if check_for_exit():
|
||||
return
|
||||
|
||||
# Update player state (including power-ups)
|
||||
# Update game state
|
||||
self.currentLevel.player.update(currentTime)
|
||||
|
||||
self.handle_input()
|
||||
|
||||
# Update audio positioning and handle collisions
|
||||
self.currentLevel.update_audio()
|
||||
self.currentLevel.handle_collisions()
|
||||
|
||||
# Handle combat interactions
|
||||
# Handle combat and projectiles
|
||||
self.currentLevel.handle_combat(currentTime)
|
||||
|
||||
# Update projectiles
|
||||
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
|
||||
|
||||
# Player died or ran out of lives
|
||||
speak("Game Over")
|
||||
|
||||
def run(self):
|
||||
"""Main game loop with menu system."""
|
||||
while True:
|
||||
choice = game_menu(self.sounds, "play", "instructions", "learn_sounds", "credits", "donate", "exit")
|
||||
|
||||
if choice == "exit":
|
||||
exit_game()
|
||||
elif choice == "play":
|
||||
self.player = None # Reset player for new game
|
||||
if self.load_level(1):
|
||||
self.game_loop()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
game = WickedQuest()
|
||||
game.run()
|
||||
|
||||
Reference in New Issue
Block a user