lint and spacing fixes. Also fixed problem with levels not showing up in compiled version (hopefully).

This commit is contained in:
Storm Dragon
2025-09-09 22:02:33 -04:00
parent 949c12f193
commit 21d3cd7788
21 changed files with 540 additions and 584 deletions

View File

@@ -1,2 +0,0 @@

View File

@@ -14,7 +14,7 @@ class Pumpkin:
self.isActive = True self.isActive = True
self.damage = playerMaxHealth // 2 # Half of player's max health self.damage = playerMaxHealth // 2 # Half of player's max health
self.soundChannel = None self.soundChannel = None
self.soundName = 'pumpkin_low' if isHigh else 'pumpkin_high' # Inverted mapping self.soundName = "pumpkin_low" if isHigh else "pumpkin_high" # Inverted mapping
def update(self, sounds, playerX): def update(self, sounds, playerX):
"""Update pumpkin position and sound""" """Update pumpkin position and sound"""
@@ -39,7 +39,7 @@ class Pumpkin:
# Calculate volume and pan for splat sound based on final position # Calculate volume and pan for splat sound based on final position
volume, left, right = calculate_volume_and_pan(playerX, self.x) volume, left, right = calculate_volume_and_pan(playerX, self.x)
if volume > 0: # Only play if within audible range if volume > 0: # Only play if within audible range
obj_play(sounds, 'pumpkin_splat', playerX, self.x, loop=False) obj_play(sounds, "pumpkin_splat", playerX, self.x, loop=False)
def check_collision(self, player): def check_collision(self, player):
"""Check if pumpkin hits player""" """Check if pumpkin hits player"""
@@ -58,7 +58,9 @@ class Pumpkin:
class Catapult(Object): class Catapult(Object):
def __init__(self, x, y, sounds, direction=1, fireInterval=5000, firingRange=20): def __init__(self, x, y, sounds, direction=1, fireInterval=5000, firingRange=20):
super().__init__( super().__init__(
x, y, "catapult", x,
y,
"catapult",
isStatic=True, isStatic=True,
isCollectible=False, isCollectible=False,
) )
@@ -77,18 +79,14 @@ class Catapult(Object):
self.lastFireTime = currentTime self.lastFireTime = currentTime
# Play launch sound using directional audio # Play launch sound using directional audio
play_directional_sound(self.sounds, 'catapult_launch', player.xPos, self.xPos) play_directional_sound(self.sounds, "catapult_launch", player.xPos, self.xPos)
# Set up pending pumpkin # Set up pending pumpkin
isHigh = random.choice([True, False]) isHigh = random.choice([True, False])
fireDirection = 1 if player.xPos > self.xPos else -1 fireDirection = 1 if player.xPos > self.xPos else -1
# Store pumpkin data for later creation # Store pumpkin data for later creation
self.pendingPumpkin = { self.pendingPumpkin = {"isHigh": isHigh, "direction": fireDirection, "playerMaxHealth": player.get_max_health()}
'isHigh': isHigh,
'direction': fireDirection,
'playerMaxHealth': player.get_max_health()
}
# Set when to actually launch the pumpkin # Set when to actually launch the pumpkin
self.pumpkinLaunchTime = currentTime + self.launchDelay self.pumpkinLaunchTime = currentTime + self.launchDelay
@@ -116,9 +114,9 @@ class Catapult(Object):
# Create and fire the pending pumpkin # Create and fire the pending pumpkin
pumpkin = Pumpkin( pumpkin = Pumpkin(
self.xPos, self.xPos,
self.pendingPumpkin['isHigh'], self.pendingPumpkin["isHigh"],
self.pendingPumpkin['direction'], self.pendingPumpkin["direction"],
self.pendingPumpkin['playerMaxHealth'] self.pendingPumpkin["playerMaxHealth"],
) )
self.activePumpkins.append(pumpkin) self.activePumpkins.append(pumpkin)
self.pendingPumpkin = None self.pendingPumpkin = None
@@ -140,6 +138,4 @@ class Catapult(Object):
pumpkin.isActive = False pumpkin.isActive = False
self.activePumpkins.remove(pumpkin) self.activePumpkins.remove(pumpkin)
if not player.isInvincible: if not player.isInvincible:
self.sounds['player_takes_damage'].play() self.sounds["player_takes_damage"].play()

View File

@@ -9,12 +9,7 @@ from src.powerup import PowerUp
class CoffinObject(Object): class CoffinObject(Object):
def __init__(self, x, y, sounds, level, item="random"): def __init__(self, x, y, sounds, level, item="random"):
super().__init__( super().__init__(x, y, "coffin", isStatic=True, isCollectible=False, isHazard=False)
x, y, "coffin",
isStatic=True,
isCollectible=False,
isHazard=False
)
self.sounds = sounds self.sounds = sounds
self.level = level self.level = level
self.isBroken = False self.isBroken = False
@@ -25,9 +20,9 @@ class CoffinObject(Object):
"""Handle being hit by the player's weapon""" """Handle being hit by the player's weapon"""
if not self.isBroken: if not self.isBroken:
self.isBroken = True self.isBroken = True
play_sound(self.sounds['coffin_shatter']) play_sound(self.sounds["coffin_shatter"])
self.level.levelScore += 500 self.level.levelScore += 500
self.level.player.stats.update_stat('Coffins broken', 1) self.level.player.stats.update_stat("Coffins broken", 1)
# Stop the ongoing coffin sound # Stop the ongoing coffin sound
if self.channel: if self.channel:
@@ -54,16 +49,8 @@ class CoffinObject(Object):
drop_x = self.xPos + (direction * drop_distance) drop_x = self.xPos + (direction * drop_distance)
self.dropped_item = PowerUp( self.dropped_item = PowerUp(
drop_x, drop_x, self.yPos, item_type, self.sounds, direction, self.level.leftBoundary, self.level.rightBoundary
self.yPos,
item_type,
self.sounds,
direction,
self.level.leftBoundary,
self.level.rightBoundary
) )
return True return True
return False return False

View File

@@ -11,31 +11,25 @@ import pygame
from libstormgames import * from libstormgames import *
from src.object import Object from src.object import Object
class DeathSound(Object): class DeathSound(Object):
"""Special object that plays enemy death sounds at fixed positions and self-removes.""" """Special object that plays enemy death sounds at fixed positions and self-removes."""
def __init__(self, x, y, enemyType, sounds): def __init__(self, x, y, enemyType, sounds):
# Initialize as a static object with the death sound name # Initialize as a static object with the death sound name
deathSoundName = f"{enemyType}_dies" deathSoundName = f"{enemyType}_dies"
super().__init__( super().__init__(x, y, deathSoundName, isStatic=True, isCollectible=False, isHazard=False)
x,
y,
deathSoundName,
isStatic=True,
isCollectible=False,
isHazard=False
)
self.sounds = sounds self.sounds = sounds
self.enemyType = enemyType self.enemyType = enemyType
self.startTime = pygame.time.get_ticks() self.startTime = pygame.time.get_ticks()
# Get the duration of the death sound if it exists # Get the duration of the death sound if it exists
if deathSoundName in sounds: if deathSoundName in sounds:
self.soundDuration = sounds[deathSoundName].get_length() * 1000 # Convert to milliseconds self.soundDuration = sounds[deathSoundName].get_length() * 1000 # Convert to milliseconds
else: else:
self.soundDuration = 1000 # Default 1 second if sound doesn't exist self.soundDuration = 1000 # Default 1 second if sound doesn't exist
def update(self, currentTime): def update(self, currentTime):
"""Check if sound has finished playing and mark for removal.""" """Check if sound has finished playing and mark for removal."""
if currentTime - self.startTime >= self.soundDuration: if currentTime - self.startTime >= self.soundDuration:

View File

@@ -6,25 +6,20 @@ from libstormgames import *
from src.object import Object from src.object import Object
from src.powerup import PowerUp from src.powerup import PowerUp
class Enemy(Object): class Enemy(Object):
def __init__(self, xRange, y, enemyType, sounds, level, **kwargs): def __init__(self, xRange, y, enemyType, sounds, level, **kwargs):
# Track when critters should start hunting # Track when critters should start hunting
self.hunting = False self.hunting = False
# Initialize base object properties # Initialize base object properties
super().__init__( super().__init__(xRange, y, f"{enemyType}", isStatic=False, isHazard=True) # Base sound
xRange,
y,
f"{enemyType}", # Base sound
isStatic=False,
isHazard=True
)
# Enemy specific properties # Enemy specific properties
self.enemyType = enemyType self.enemyType = enemyType
self.level = level 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
self.sounds = sounds # Store reference to game sounds self.sounds = sounds # Store reference to game sounds
# Movement and behavior properties # Movement and behavior properties
@@ -37,25 +32,25 @@ class Enemy(Object):
self._currentX = self.xRange[0] # Initialize current position self._currentX = self.xRange[0] # Initialize current position
# Add spawn configuration # Add spawn configuration
self.canSpawn = kwargs.get('can_spawn', False) self.canSpawn = kwargs.get("can_spawn", False)
if self.canSpawn: if self.canSpawn:
self.spawnCooldown = kwargs.get('spawn_cooldown', 2000) self.spawnCooldown = kwargs.get("spawn_cooldown", 2000)
self.spawnChance = kwargs.get('spawn_chance', 25) self.spawnChance = kwargs.get("spawn_chance", 25)
self.spawnType = kwargs.get('spawn_type', 'zombie') # Default to zombie for backward compatibility self.spawnType = kwargs.get("spawn_type", "zombie") # Default to zombie for backward compatibility
self.spawnDistance = kwargs.get('spawn_distance', 5) self.spawnDistance = kwargs.get("spawn_distance", 5)
self.lastSpawnTime = 0 self.lastSpawnTime = 0
# Attack pattern configuration # Attack pattern configuration
self.attackPattern = kwargs.get('attack_pattern', {'type': 'patrol'}) self.attackPattern = kwargs.get("attack_pattern", {"type": "patrol"})
self.turnThreshold = self.attackPattern.get('turn_threshold', 5) self.turnThreshold = self.attackPattern.get("turn_threshold", 5)
# Initialize vulnerability system # Initialize vulnerability system
self.hasVulnerabilitySystem = kwargs.get('has_vulnerability', False) self.hasVulnerabilitySystem = kwargs.get("has_vulnerability", False)
if self.hasVulnerabilitySystem: if self.hasVulnerabilitySystem:
self.isVulnerable = False # Start invulnerable self.isVulnerable = False # Start invulnerable
self.vulnerabilityTimer = pygame.time.get_ticks() self.vulnerabilityTimer = pygame.time.get_ticks()
self.vulnerabilityDuration = kwargs.get('vulnerability_duration', 2000) self.vulnerabilityDuration = kwargs.get("vulnerability_duration", 2000)
self.invulnerabilityDuration = kwargs.get('invulnerability_duration', 5000) self.invulnerabilityDuration = kwargs.get("invulnerability_duration", 5000)
soundName = f"{self.enemyType}_is_vulnerable" if self.isVulnerable else self.enemyType soundName = f"{self.enemyType}_is_vulnerable" if self.isVulnerable else self.enemyType
self.channel = obj_play(self.sounds, soundName, self.level.player.xPos, self.xPos) self.channel = obj_play(self.sounds, soundName, self.level.player.xPos, self.xPos)
else: else:
@@ -68,19 +63,17 @@ class Enemy(Object):
self.health = 1 # Easy to kill self.health = 1 # Easy to kill
self.attackCooldown = 1500 # Slower attack rate self.attackCooldown = 1500 # Slower attack rate
elif enemyType == "spider": elif enemyType == "spider":
speedMultiplier = kwargs.get('speed_multiplier', 2.0) speedMultiplier = kwargs.get("speed_multiplier", 2.0)
self.movementSpeed *= speedMultiplier # Spiders are faster self.movementSpeed *= speedMultiplier # Spiders are faster
self.attackPattern = {'type': 'hunter'} # Spiders actively hunt the player self.attackPattern = {"type": "hunter"} # Spiders actively hunt the player
self.turnThreshold = 3 # Spiders turn around quickly to chase player self.turnThreshold = 3 # Spiders turn around quickly to chase player
@property @property
def xPos(self): def xPos(self):
"""Current x position""" """Current x position"""
return self._currentX return self._currentX
@xPos.setter @xPos.setter
def xPos(self, value): def xPos(self, value):
"""Set current x position""" """Set current x position"""
self._currentX = value self._currentX = value
@@ -111,7 +104,9 @@ class Enemy(Object):
self.channel = obj_update(self.channel, player.xPos, self.xPos) self.channel = obj_update(self.channel, player.xPos, self.xPos)
# Check for vulnerability state change # Check for vulnerability state change
if currentTime - self.vulnerabilityTimer >= (self.vulnerabilityDuration if self.isVulnerable else self.invulnerabilityDuration): if currentTime - self.vulnerabilityTimer >= (
self.vulnerabilityDuration if self.isVulnerable else self.invulnerabilityDuration
):
self.isVulnerable = not self.isVulnerable self.isVulnerable = not self.isVulnerable
self.vulnerabilityTimer = currentTime self.vulnerabilityTimer = currentTime
@@ -126,8 +121,7 @@ class Enemy(Object):
self.hunting = True self.hunting = True
# Handle movement based on enemy type and pattern # Handle movement based on enemy type and pattern
if (self.enemyType == "zombie" or if self.enemyType == "zombie" or (self.attackPattern["type"] == "hunter" and self.hunting):
(self.attackPattern['type'] == 'hunter' and self.hunting)):
distanceToPlayer = player.xPos - self.xPos distanceToPlayer = player.xPos - self.xPos
@@ -167,9 +161,9 @@ class Enemy(Object):
spawnX = max(self.level.leftBoundary, min(spawnX, self.level.rightBoundary)) spawnX = max(self.level.leftBoundary, min(spawnX, self.level.rightBoundary))
# Set behavior based on game mode # Set behavior based on game mode
behavior = 'hunter' if self.level.levelId == 999 else 'patrol' behavior = "hunter" if self.level.levelId == 999 else "patrol"
turn_rate = 2 if self.level.levelId == 999 else 8 # Faster turn rate for survival turn_rate = 2 if self.level.levelId == 999 else 8 # Faster turn rate for survival
# Create new enemy of specified type # Create new enemy of specified type
spawned = Enemy( spawned = Enemy(
[spawnX, spawnX], # Single point range for spawn [spawnX, spawnX], # Single point range for spawn
@@ -180,8 +174,8 @@ class Enemy(Object):
health=4, # Default health for spawned enemies health=4, # Default health for spawned enemies
damage=2, # Default damage for spawned enemies damage=2, # Default damage for spawned enemies
attack_range=1, # Default range for spawned enemies attack_range=1, # Default range for spawned enemies
attack_pattern={'type': behavior}, attack_pattern={"type": behavior},
turn_rate=turn_rate turn_rate=turn_rate,
) )
# Add to level's enemies # Add to level's enemies
@@ -221,7 +215,8 @@ class Enemy(Object):
def attack(self, currentTime, player): def attack(self, currentTime, player):
"""Perform attack on player""" """Perform attack on player"""
if player.isInvincible: return if player.isInvincible:
return
self.lastAttackTime = currentTime self.lastAttackTime = currentTime
# Play attack sound # Play attack sound
attackSound = f"{self.enemyType}_attack" attackSound = f"{self.enemyType}_attack"
@@ -229,7 +224,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)
self.sounds['player_takes_damage'].play() self.sounds["player_takes_damage"].play()
def take_damage(self, amount): def take_damage(self, amount):
"""Handle enemy taking damage""" """Handle enemy taking damage"""
@@ -259,6 +254,7 @@ class Enemy(Object):
# Create a DeathSound object to play death sound at fixed position # Create a DeathSound object to play death sound at fixed position
from src.die_monster_die import DeathSound from src.die_monster_die import DeathSound
deathSoundObj = DeathSound(self.xPos, self.yPos, self.enemyType, self.sounds) deathSoundObj = DeathSound(self.xPos, self.yPos, self.enemyType, self.sounds)
self.level.objects.append(deathSoundObj) self.level.objects.append(deathSoundObj)
@@ -276,17 +272,9 @@ class Enemy(Object):
dropX = self.xPos + (direction * dropDistance) dropX = self.xPos + (direction * dropDistance)
droppedItem = PowerUp( droppedItem = PowerUp(
dropX, dropX, self.yPos, itemType, self.sounds, direction, self.level.leftBoundary, self.level.rightBoundary
self.yPos,
itemType,
self.sounds,
direction,
self.level.leftBoundary,
self.level.rightBoundary
) )
self.level.bouncing_items.append(droppedItem) self.level.bouncing_items.append(droppedItem)
# Update stats # Update stats
self.level.player.stats.update_stat('Enemies killed', 1) self.level.player.stats.update_stat("Enemies killed", 1)

View File

@@ -2,11 +2,11 @@
import os import os
import sys import sys
import time
import pygame
from os.path import isdir, join from os.path import isdir, join
from libstormgames import speak, instruction_menu from libstormgames import speak, instruction_menu
def get_available_games(): def get_available_games():
"""Get list of available game directories in levels folder. """Get list of available game directories in levels folder.
@@ -15,18 +15,19 @@ def get_available_games():
""" """
try: try:
# Handle PyInstaller path issues # Handle PyInstaller path issues
if hasattr(sys, '_MEIPASS'): if hasattr(sys, "_MEIPASS"):
# Running as PyInstaller executable # Running as PyInstaller executable
base_path = sys._MEIPASS base_path = sys._MEIPASS
else: else:
# Running as script # Running as script
base_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) base_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
levels_path = os.path.join(base_path, "levels") levels_path = os.path.join(base_path, "levels")
return [d for d in os.listdir(levels_path) if isdir(join(levels_path, d))] return [d for d in os.listdir(levels_path) if isdir(join(levels_path, d)) and not d.endswith(".md")]
except FileNotFoundError: except FileNotFoundError:
return [] return []
def selection_menu(sounds, *options): def selection_menu(sounds, *options):
"""Display level selection menu using instruction_menu. """Display level selection menu using instruction_menu.
@@ -39,6 +40,7 @@ def selection_menu(sounds, *options):
""" """
return instruction_menu(sounds, "Select an adventure", *options) return instruction_menu(sounds, "Select an adventure", *options)
def select_game(sounds): def select_game(sounds):
"""Display game selection menu and return chosen game. """Display game selection menu and return chosen game.
@@ -48,49 +50,48 @@ def select_game(sounds):
Returns: Returns:
str: Selected game directory name or None if cancelled str: Selected game directory name or None if cancelled
""" """
availableGames = get_available_games() available_games = get_available_games()
if not availableGames: if not available_games:
speak("No games found in levels directory!") speak("No games found in levels directory!")
return None return None
# Convert directory names to display names (replace underscores with spaces) # Convert directory names to display names (replace underscores with spaces)
menuOptions = [game.replace("_", " ") for game in availableGames] menu_options = [game.replace("_", " ") for game in available_games]
choice = selection_menu(sounds, *menuOptions) choice = selection_menu(sounds, *menu_options)
if choice is None: if choice is None:
return None return None
# Convert display name back to directory name if needed # Convert display name back to directory name if needed
gameDir = choice.replace(" ", "_") game_dir = choice.replace(" ", "_")
if gameDir not in availableGames: if game_dir not in available_games:
gameDir = choice # Use original if conversion doesn't match game_dir = choice # Use original if conversion doesn't match
return gameDir return game_dir
def get_level_path(gameDir, levelNum):
def get_level_path(game_dir, level_num):
"""Get full path to level JSON file. """Get full path to level JSON file.
Args: Args:
gameDir (str): Game directory name game_dir (str): Game directory name
levelNum (int): Level number level_num (int): Level number
Returns: Returns:
str: Full path to level JSON file str: Full path to level JSON file
""" """
if gameDir is None: if game_dir is None:
raise ValueError("gameDir cannot be None") raise ValueError("game_dir cannot be None")
# Handle PyInstaller path issues # Handle PyInstaller path issues
if hasattr(sys, '_MEIPASS'): if hasattr(sys, "_MEIPASS"):
# Running as PyInstaller executable # Running as PyInstaller executable
base_path = sys._MEIPASS base_path = sys._MEIPASS
else: else:
# Running as script # Running as script
base_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) base_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
level_path = os.path.join(base_path, "levels", gameDir, f"{levelNum}.json") level_path = os.path.join(base_path, "levels", game_dir, f"{level_num}.json")
return level_path return level_path

View File

@@ -4,6 +4,7 @@ import pygame
from libstormgames import * from libstormgames import *
from src.object import Object from src.object import Object
class GraspingHands(Object): class GraspingHands(Object):
"""A hazard where the ground crumbles beneath the player as undead hands reach up.""" """A hazard where the ground crumbles beneath the player as undead hands reach up."""
@@ -14,7 +15,7 @@ class GraspingHands(Object):
"", # Empty string so no regular object sound plays "", # Empty string so no regular object sound plays
isStatic=True, isStatic=True,
isCollectible=False, isCollectible=False,
isHazard=True isHazard=True,
) )
self.sounds = sounds self.sounds = sounds
self.delay = delay # Delay in milliseconds before ground starts crumbling self.delay = delay # Delay in milliseconds before ground starts crumbling
@@ -51,7 +52,7 @@ class GraspingHands(Object):
self.isReset = False self.isReset = False
# Play initial warning sound # Play initial warning sound
play_sound(self.sounds['grasping_hands_start']) play_sound(self.sounds["grasping_hands_start"])
speak("The ground crumbles as the dead reach for you.") speak("The ground crumbles as the dead reach for you.")
def reset(self): def reset(self):
@@ -67,7 +68,7 @@ class GraspingHands(Object):
self.crumbleChannel = None self.crumbleChannel = None
# Play the end sound # Play the end sound
play_sound(self.sounds['grasping_hands_end']) play_sound(self.sounds["grasping_hands_end"])
def update(self, currentTime, player): def update(self, currentTime, player):
"""Update the grasping hands trap state""" """Update the grasping hands trap state"""
@@ -92,7 +93,7 @@ class GraspingHands(Object):
# Manage the looping positional audio for the crumbling ground # Manage the looping positional audio for the crumbling ground
if self.crumbleChannel is None or not self.crumbleChannel.get_busy(): if self.crumbleChannel is None or not self.crumbleChannel.get_busy():
# Start the sound if it's not playing # Start the sound if it's not playing
self.crumbleChannel = obj_play(self.sounds, 'grasping_hands', player.xPos, self.crumblePosition) self.crumbleChannel = obj_play(self.sounds, "grasping_hands", player.xPos, self.crumblePosition)
else: else:
# Update the sound position # Update the sound position
self.crumbleChannel = obj_update(self.crumbleChannel, player.xPos, self.crumblePosition) self.crumbleChannel = obj_update(self.crumbleChannel, player.xPos, self.crumblePosition)
@@ -100,8 +101,9 @@ class GraspingHands(Object):
# Check if player is caught by crumbling # Check if player is caught by crumbling
playerCaught = False playerCaught = False
if not player.isJumping: if not player.isJumping:
if (self.crumbleDirection > 0 and player.xPos <= self.crumblePosition) or \ if (self.crumbleDirection > 0 and player.xPos <= self.crumblePosition) or (
(self.crumbleDirection < 0 and player.xPos >= self.crumblePosition): self.crumbleDirection < 0 and player.xPos >= self.crumblePosition
):
playerCaught = True playerCaught = True
if playerCaught: if playerCaught:
@@ -122,8 +124,6 @@ class GraspingHands(Object):
def __del__(self): def __del__(self):
"""Cleanup when object is destroyed""" """Cleanup when object is destroyed"""
# Ensure sound is stopped when object is destroyed # Ensure sound is stopped when object is destroyed
if hasattr(self, 'crumbleChannel') and self.crumbleChannel: if hasattr(self, "crumbleChannel") and self.crumbleChannel:
obj_stop(self.crumbleChannel) obj_stop(self.crumbleChannel)
self.crumbleChannel = None self.crumbleChannel = None

View File

@@ -8,13 +8,9 @@ from src.powerup import PowerUp
class GraveObject(Object): class GraveObject(Object):
def __init__(self, x, y, sounds, item=None, zombieSpawnChance=0): def __init__(self, x, y, sounds, item=None, zombieSpawnChance=0):
super().__init__( super().__init__(
x, y, "grave", x, y, "grave", isStatic=True, isCollectible=False, isHazard=True, zombieSpawnChance=zombieSpawnChance
isStatic=True,
isCollectible=False,
isHazard=True,
zombieSpawnChance=zombieSpawnChance
) )
self.graveItem = item self.graveItem = item
self.isCollected = False # Renamed to match style of isHazard, isStatic etc self.isCollected = False # Renamed to match style of isHazard, isStatic etc
self.isFilled = False # Track if grave has been filled with shovel self.isFilled = False # Track if grave has been filled with shovel
self.sounds = sounds self.sounds = sounds
@@ -30,8 +26,12 @@ class GraveObject(Object):
return False return False
# Collect the item if player is ducking, walking (not running), and wielding shovel # Collect the item if player is ducking, walking (not running), and wielding shovel
if (player.isDucking and not player.isRunning and if (
player.currentWeapon and player.currentWeapon.name == "rusty_shovel"): player.isDucking
and not player.isRunning
and player.currentWeapon
and player.currentWeapon.name == "rusty_shovel"
):
self.isCollected = True # Mark as collected when collection succeeds self.isCollected = True # Mark as collected when collection succeeds
return True return True
@@ -39,21 +39,25 @@ class GraveObject(Object):
def can_fill_grave(self, player): def can_fill_grave(self, player):
"""Check if grave can be filled with shovel. """Check if grave can be filled with shovel.
Returns: Returns:
bool: True if grave can be filled, False otherwise bool: True if grave can be filled, False otherwise
""" """
# Can only fill empty graves (no item) that haven't been filled yet # Can only fill empty graves (no item) that haven't been filled yet
if self.graveItem or self.isFilled: if self.graveItem or self.isFilled:
return False return False
# Must be ducking, walking (not running), and wielding shovel # Must be ducking, walking (not running), and wielding shovel
return (player.isDucking and not player.isRunning and return (
player.currentWeapon and player.currentWeapon.name == "rusty_shovel") player.isDucking
and not player.isRunning
and player.currentWeapon
and player.currentWeapon.name == "rusty_shovel"
)
def fill_grave(self, player): def fill_grave(self, player):
"""Fill the grave with dirt using shovel. """Fill the grave with dirt using shovel.
Returns: Returns:
bool: True if grave was filled successfully bool: True if grave was filled successfully
""" """
@@ -62,5 +66,3 @@ class GraveObject(Object):
self.isHazard = False # No longer a hazard once filled self.isHazard = False # No longer a hazard once filled
return True return True
return False return False

View File

@@ -6,6 +6,7 @@ from enum import Enum, auto
class ItemType(Enum): class ItemType(Enum):
"""Defines available item types and their properties""" """Defines available item types and their properties"""
GUTS = auto() GUTS = auto()
HAND_OF_GLORY = auto() HAND_OF_GLORY = auto()
JACK_O_LANTERN = auto() JACK_O_LANTERN = auto()
@@ -13,14 +14,12 @@ class ItemType(Enum):
CAULDRON = auto() CAULDRON = auto()
WITCH_BROOM = auto() WITCH_BROOM = auto()
class ItemProperties: class ItemProperties:
"""Manages item properties and availability""" """Manages item properties and availability"""
# Items that can appear in random drops # Items that can appear in random drops
RANDOM_ELIGIBLE = { RANDOM_ELIGIBLE = {ItemType.HAND_OF_GLORY: "hand_of_glory", ItemType.JACK_O_LANTERN: "jack_o_lantern"}
ItemType.HAND_OF_GLORY: "hand_of_glory",
ItemType.JACK_O_LANTERN: "jack_o_lantern"
}
# All possible items (including special ones) # All possible items (including special ones)
ALL_ITEMS = { ALL_ITEMS = {
@@ -29,7 +28,7 @@ class ItemProperties:
ItemType.JACK_O_LANTERN: "jack_o_lantern", ItemType.JACK_O_LANTERN: "jack_o_lantern",
ItemType.EXTRA_LIFE: "extra_life", ItemType.EXTRA_LIFE: "extra_life",
ItemType.CAULDRON: "cauldron", ItemType.CAULDRON: "cauldron",
ItemType.WITCH_BROOM: "witch_broom" ItemType.WITCH_BROOM: "witch_broom",
} }
@staticmethod @staticmethod
@@ -55,5 +54,3 @@ class ItemProperties:
if name == item_name: if name == item_name:
return item_type return item_type
return None return None

View File

@@ -25,7 +25,7 @@ class Level:
self.projectiles = [] # Track active projectiles self.projectiles = [] # Track active projectiles
self.player = player self.player = player
self.lastWarningTime = 0 self.lastWarningTime = 0
self.warningInterval = int(self.sounds['edge'].get_length() * 1000) # Convert seconds to milliseconds self.warningInterval = int(self.sounds["edge"].get_length() * 1000) # Convert seconds to milliseconds
self.weapon_hit_channel = None self.weapon_hit_channel = None
self.leftBoundary = levelData["boundaries"]["left"] self.leftBoundary = levelData["boundaries"]["left"]
@@ -42,34 +42,34 @@ class Level:
self.player.set_footstep_sound(self.footstepSound) self.player.set_footstep_sound(self.footstepSound)
# Level intro message (skip for survival mode) # Level intro message (skip for survival mode)
if levelData['level_id'] != 999: # 999 is survival mode if levelData["level_id"] != 999: # 999 is survival mode
levelIntro = f"Level {levelData['level_id']}, {levelData['name']}. " levelIntro = f"Level {levelData['level_id']}, {levelData['name']}. "
if self.isLocked: if self.isLocked:
levelIntro += "This is a boss level. You must defeat all enemies before you can advance. " levelIntro += "This is a boss level. You must defeat all enemies before you can advance. "
levelIntro += levelData['description'] levelIntro += levelData["description"]
messagebox(levelIntro) messagebox(levelIntro)
# Handle level music # Handle level music
try: try:
pygame.mixer.music.stop() pygame.mixer.music.stop()
if "ambience" in levelData: if "ambience" in levelData:
ambientFile = levelData['ambience'] ambientFile = levelData["ambience"]
# Build list of paths to try (pack-specific first, then generic) # Build list of paths to try (pack-specific first, then generic)
ambiencePaths = [] ambiencePaths = []
if self.levelPackName: if self.levelPackName:
ambiencePaths.append(f"sounds/{self.levelPackName}/ambience/{ambientFile}") ambiencePaths.append(f"sounds/{self.levelPackName}/ambience/{ambientFile}")
ambiencePaths.append(f"sounds/ambience/{ambientFile}") ambiencePaths.append(f"sounds/ambience/{ambientFile}")
# Try each path until one works # Try each path until one works
for ambiencePath in ambiencePaths: for ambiencePath in ambiencePaths:
try: try:
pygame.mixer.music.load(ambiencePath) pygame.mixer.music.load(ambiencePath)
pygame.mixer.music.play(-1) # Loop indefinitely pygame.mixer.music.play(-1) # Loop indefinitely
break break
except: except Exception:
continue continue
except: except Exception:
pass pass
# Create end of level object at right boundary # Create end of level object at right boundary
@@ -79,7 +79,7 @@ class Level:
"end_of_level", "end_of_level",
isStatic=True, isStatic=True,
isCollectible=False, isCollectible=False,
isHazard=False isHazard=False,
) )
self.objects.append(endLevel) self.objects.append(endLevel)
@@ -98,7 +98,7 @@ class Level:
obj["y"], obj["y"],
self.sounds, self.sounds,
fireInterval=obj.get("fireInterval", 5000), fireInterval=obj.get("fireInterval", 5000),
firingRange=obj.get("range", 20) firingRange=obj.get("range", 20),
) )
self.objects.append(catapult) self.objects.append(catapult)
# Check if this is grasping hands # Check if this is grasping hands
@@ -108,7 +108,7 @@ class Level:
obj["y"], obj["y"],
self.sounds, self.sounds,
delay=obj.get("delay", 1000), delay=obj.get("delay", 1000),
crumble_speed=obj.get("crumble_speed", 0.03) crumble_speed=obj.get("crumble_speed", 0.03),
) )
self.objects.append(graspingHands) self.objects.append(graspingHands)
# Check if this is a grave # Check if this is a grave
@@ -118,7 +118,7 @@ class Level:
obj["y"], obj["y"],
self.sounds, self.sounds,
item=obj.get("item", None), item=obj.get("item", None),
zombieSpawnChance=obj.get("zombie_spawn_chance", 0) zombieSpawnChance=obj.get("zombie_spawn_chance", 0),
) )
self.objects.append(grave) self.objects.append(grave)
# Check if this is a skull storm # Check if this is a skull storm
@@ -130,7 +130,7 @@ class Level:
obj.get("damage", 5), obj.get("damage", 5),
obj.get("maximum_skulls", 3), obj.get("maximum_skulls", 3),
obj.get("frequency", {}).get("min", 2), obj.get("frequency", {}).get("min", 2),
obj.get("frequency", {}).get("max", 5) obj.get("frequency", {}).get("max", 5),
) )
self.objects.append(skullStorm) self.objects.append(skullStorm)
# Check if this is a coffin # Check if this is a coffin
@@ -140,7 +140,7 @@ class Level:
obj["y"], obj["y"],
self.sounds, self.sounds,
self, # Pass level reference self, # Pass level reference
item=obj.get("item", "random") # Get item type or default to random item=obj.get("item", "random"), # Get item type or default to random
) )
self.objects.append(coffin) self.objects.append(coffin)
# Check if this is a spider web # Check if this is a spider web
@@ -148,8 +148,7 @@ class Level:
# Check distance from graves # Check distance from graves
isValidPosition = True isValidPosition = True
for existingObj in self.objects: for existingObj in self.objects:
if (existingObj.soundName == "grave" and if existingObj.soundName == "grave" and not hasattr(existingObj, "graveItem"):
not hasattr(existingObj, 'graveItem')):
distance = abs(obj["x"] - existingObj.xPos) distance = abs(obj["x"] - existingObj.xPos)
if distance < 3: if distance < 3:
isValidPosition = False isValidPosition = False
@@ -176,7 +175,7 @@ class Level:
damage=obj.get("damage", 1), damage=obj.get("damage", 1),
attack_range=obj.get("attack_range", 1), attack_range=obj.get("attack_range", 1),
movement_range=obj.get("movement_range", 5), movement_range=obj.get("movement_range", 5),
attack_pattern=obj.get("attack_pattern", {'type': 'patrol'}), attack_pattern=obj.get("attack_pattern", {"type": "patrol"}),
can_spawn=obj.get("can_spawn", False), can_spawn=obj.get("can_spawn", False),
spawn_type=obj.get("spawn_type", "zombie"), spawn_type=obj.get("spawn_type", "zombie"),
spawn_cooldown=obj.get("spawn_cooldown", 2000), spawn_cooldown=obj.get("spawn_cooldown", 2000),
@@ -185,7 +184,7 @@ class Level:
has_vulnerability=obj.get("has_vulnerability", False), has_vulnerability=obj.get("has_vulnerability", False),
is_vulnerable=obj.get("is_vulnerable", False), is_vulnerable=obj.get("is_vulnerable", False),
vulnerability_duration=obj.get("vulnerability_duration", 1000), vulnerability_duration=obj.get("vulnerability_duration", 1000),
invulnerability_duration=obj.get("invulnerability_duration", 5000) invulnerability_duration=obj.get("invulnerability_duration", 5000),
) )
self.enemies.append(enemy) self.enemies.append(enemy)
else: else:
@@ -196,13 +195,13 @@ class Level:
isStatic=obj.get("static", True), isStatic=obj.get("static", True),
isCollectible=obj.get("collectible", False), isCollectible=obj.get("collectible", False),
isHazard=obj.get("hazard", False), isHazard=obj.get("hazard", False),
zombieSpawnChance=obj.get("zombieSpawnChance", 0) zombieSpawnChance=obj.get("zombieSpawnChance", 0),
) )
self.objects.append(gameObject) self.objects.append(gameObject)
enemyCount = len(self.enemies) enemyCount = len(self.enemies)
coffinCount = sum(1 for obj in self.objects if hasattr(obj, 'isBroken')) coffinCount = sum(1 for obj in self.objects if hasattr(obj, "isBroken"))
player.stats.update_stat('Enemies remaining', enemyCount) player.stats.update_stat("Enemies remaining", enemyCount)
player.stats.update_stat('Coffins remaining', coffinCount) 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."""
@@ -214,9 +213,7 @@ class Level:
continue continue
# Check for potential zombie spawn from graves # Check for potential zombie spawn from graves
if (obj.soundName == "grave" and if obj.soundName == "grave" and obj.zombieSpawnChance > 0 and not obj.hasSpawned:
obj.zombieSpawnChance > 0 and
not obj.hasSpawned):
distance = abs(self.player.xPos - obj.xPos) distance = abs(self.player.xPos - obj.xPos)
if distance < 6: # Within 6 tiles if distance < 6: # Within 6 tiles
@@ -226,9 +223,9 @@ class Level:
roll = random.randint(1, 100) roll = random.randint(1, 100)
if roll <= obj.zombieSpawnChance: if roll <= obj.zombieSpawnChance:
# Set behavior based on game mode # Set behavior based on game mode
behavior = 'hunter' if self.levelId == 999 else 'patrol' behavior = "hunter" if self.levelId == 999 else "patrol"
turn_rate = 2 if self.levelId == 999 else 8 # Faster turn rate for survival turn_rate = 2 if self.levelId == 999 else 8 # Faster turn rate for survival
zombie = Enemy( zombie = Enemy(
[obj.xPos, obj.xPos], [obj.xPos, obj.xPos],
obj.yPos, obj.yPos,
@@ -238,8 +235,8 @@ class Level:
health=3, health=3,
damage=10, damage=10,
attack_range=1, attack_range=1,
attack_pattern={'type': behavior}, attack_pattern={"type": behavior},
turn_rate=turn_rate turn_rate=turn_rate,
) )
self.enemies.append(zombie) self.enemies.append(zombie)
speak("A zombie emerges from the grave!") speak("A zombie emerges from the grave!")
@@ -283,13 +280,14 @@ class Level:
caught = obj.update(currentTime, self.player) caught = obj.update(currentTime, self.player)
if caught: if caught:
return # Stop if player is dead return # Stop if player is dead
# Update death sound objects # Update death sound objects
from src.die_monster_die import DeathSound from src.die_monster_die import DeathSound
for obj in self.objects: for obj in self.objects:
if isinstance(obj, DeathSound): if isinstance(obj, DeathSound):
obj.update(currentTime) obj.update(currentTime)
# Clean up inactive objects (including finished death sounds) # Clean up inactive objects (including finished death sounds)
self.objects = [obj for obj in self.objects if obj.isActive] self.objects = [obj for obj in self.objects if obj.isActive]
@@ -303,7 +301,7 @@ class Level:
# Check for item collection # Check for item collection
if abs(item._currentX - self.player.xPos) < 1 and self.player.isJumping: if abs(item._currentX - self.player.xPos) < 1 and self.player.isJumping:
play_sound(self.sounds[f'get_{item.soundName}']) play_sound(self.sounds[f"get_{item.soundName}"])
item.apply_effect(self.player, self) item.apply_effect(self.player, self)
self.levelScore += 1000 # All items collected points awarded self.levelScore += 1000 # All items collected points awarded
item.isActive = False item.isActive = False
@@ -325,11 +323,13 @@ class Level:
# Check for coffin hits # Check for coffin hits
for obj in self.objects: for obj in self.objects:
if hasattr(obj, 'isBroken'): # Check if it's a coffin without using isinstance if hasattr(obj, "isBroken"): # Check if it's a coffin without using isinstance
if (not obj.isBroken and if (
obj.xPos >= attackRange[0] and not obj.isBroken
obj.xPos <= attackRange[1] and and obj.xPos >= attackRange[0]
self.player.isJumping): # Must be jumping to hit floating coffins and obj.xPos <= attackRange[1]
and self.player.isJumping
): # Must be jumping to hit floating coffins
if obj.hit(self.player.xPos): if obj.hit(self.player.xPos):
self.bouncing_items.append(obj.dropped_item) self.bouncing_items.append(obj.dropped_item)
@@ -345,7 +345,7 @@ class Level:
health=8, health=8,
damage=8, damage=8,
attack_range=1, attack_range=1,
speed_multiplier=2.0 speed_multiplier=2.0,
) )
self.enemies.append(spider) self.enemies.append(spider)
@@ -364,15 +364,21 @@ class Level:
continue continue
# Handle grave edge warnings # Handle grave edge warnings
if obj.isHazard and obj.soundName != "spiderweb" and not isinstance(obj, GraspingHands): # Exclude spiderwebs and grasping hands if (
obj.isHazard and obj.soundName != "spiderweb" and not isinstance(obj, GraspingHands)
): # Exclude spiderwebs and grasping hands
distance = abs(self.player.xPos - obj.xPos) distance = abs(self.player.xPos - obj.xPos)
currentTime = pygame.time.get_ticks() currentTime = pygame.time.get_ticks()
if (distance <= 2 and not self.player.isJumping and not self.player.isInvincible if (
and currentTime - self.lastWarningTime >= self.warningInterval): distance <= 2
and not self.player.isJumping
and not self.player.isInvincible
and currentTime - self.lastWarningTime >= self.warningInterval
):
if isinstance(obj, GraveObject) and obj.graveItem and not obj.isCollected: if isinstance(obj, GraveObject) and obj.graveItem and not obj.isCollected:
play_sound(self.sounds['_edge']) play_sound(self.sounds["_edge"])
else: else:
play_sound(self.sounds['edge']) play_sound(self.sounds["edge"])
self.lastWarningTime = currentTime self.lastWarningTime = currentTime
if not obj.is_in_range(self.player.xPos): if not obj.is_in_range(self.player.xPos):
@@ -382,22 +388,19 @@ class Level:
if obj.isCollectible and self.player.isJumping and not self.player.diedThisFrame: if obj.isCollectible and self.player.isJumping and not self.player.diedThisFrame:
currentPos = round(self.player.xPos) currentPos = round(self.player.xPos)
if currentPos not in obj.collectedPositions: if currentPos not in obj.collectedPositions:
play_sound(self.sounds[f'get_{obj.soundName}']) play_sound(self.sounds[f"get_{obj.soundName}"])
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) self.player.stats.update_stat("Items collected", 1)
if obj.soundName == "bone_dust": if obj.soundName == "bone_dust":
self.player._coins += 1 self.player._coins += 1
self.player.add_save_bone_dust(1) # Add to save bone dust counter too self.player.add_save_bone_dust(1) # Add to save bone dust counter too
self.levelScore += 100 self.levelScore += 100
self.player.stats.update_stat('Bone dust', 1) self.player.stats.update_stat("Bone dust", 1)
if self.player._coins % 5 == 0: if self.player._coins % 5 == 0:
# Only heal if below max health # Only heal if below max health
if self.player.get_health() < self.player.get_max_health(): if self.player.get_health() < self.player.get_max_health():
self.player.set_health(min( self.player.set_health(min(self.player.get_health() + 1, self.player.get_max_health()))
self.player.get_health() + 1,
self.player.get_max_health()
))
if self.player._coins % 100 == 0: if self.player._coins % 100 == 0:
# Only give extra lives in story mode, not survival mode (level_id 999) # Only give extra lives in story mode, not survival mode (level_id 999)
@@ -406,27 +409,25 @@ class Level:
self.player._coins = 0 self.player._coins = 0
self.player._lives += 1 self.player._lives += 1
self.levelScore += 1000 self.levelScore += 1000
play_sound(self.sounds['get_extra_life']) play_sound(self.sounds["get_extra_life"])
else: else:
# In survival mode, reset coin counter but give bonus score instead # In survival mode, reset coin counter but give bonus score instead
self.player._coins = 0 self.player._coins = 0
self.levelScore += 2000 # Double score bonus instead of extra life self.levelScore += 2000 # Double score bonus instead of extra life
speak("100 bone dust collected! Bonus score!") speak("100 bone dust collected! Bonus score!")
play_sound(self.sounds.get('survivor_bonus', 'bone_dust')) # Use survivor_bonus sound if available, fallback to bone_dust play_sound(
self.sounds.get("survivor_bonus", "bone_dust")
) # Use survivor_bonus sound if available, fallback to bone_dust
continue continue
# Handle spiderweb - this should trigger for both walking and jumping if not ducking # Handle spiderweb - this should trigger for both walking and jumping if not ducking
if obj.soundName == "spiderweb" and not self.player.isDucking: if obj.soundName == "spiderweb" and not self.player.isDucking:
# Create and apply web effect # Create and apply web effect
webEffect = PowerUp( webEffect = PowerUp(
obj.xPos, obj.xPos, obj.yPos, "spiderweb", self.sounds, 0 # No direction needed since it's just for effect
obj.yPos,
'spiderweb',
self.sounds,
0 # No direction needed since it's just for effect
) )
webEffect.level = self # Pass level reference for spider spawning webEffect.level = self # Pass level reference for spider spawning
play_sound(self.sounds['hit_spiderweb']) play_sound(self.sounds["hit_spiderweb"])
webEffect.apply_effect(self.player, self) webEffect.apply_effect(self.player, self)
# Deactivate web # Deactivate web
@@ -442,12 +443,13 @@ class Level:
if can_collect: if can_collect:
# Successfully collected item while ducking with shovel # Successfully collected item while ducking with shovel
play_sound(self.sounds[f'get_{obj.graveItem}']) play_sound(self.sounds[f"get_{obj.graveItem}"])
play_sound(self.sounds.get('fill_in_grave', 'shovel_dig')) # Also play fill sound play_sound(self.sounds.get("fill_in_grave", "shovel_dig")) # Also play fill sound
self.player.stats.update_stat('Items collected', 1) self.player.stats.update_stat("Items collected", 1)
# Create PowerUp to handle the item effect # Create PowerUp to handle the item effect
item = PowerUp(obj.xPos, obj.yPos, obj.graveItem, self.sounds, 1, item = PowerUp(
self.leftBoundary, self.rightBoundary) obj.xPos, obj.yPos, obj.graveItem, self.sounds, 1, self.leftBoundary, self.rightBoundary
)
item.apply_effect(self.player, self) item.apply_effect(self.player, self)
# Stop grave's current audio channel # Stop grave's current audio channel
if obj.channel: if obj.channel:
@@ -459,8 +461,10 @@ class Level:
continue continue
elif can_fill and obj.fill_grave(self.player): elif can_fill and obj.fill_grave(self.player):
# Successfully filled empty grave with shovel # Successfully filled empty grave with shovel
play_sound(self.sounds.get('fill_in_grave', 'shovel_dig')) # Use fill_in_grave sound if available, fallback to shovel_dig play_sound(
self.player.stats.update_stat('Graves filled', 1) self.sounds.get("fill_in_grave", "shovel_dig")
) # Use fill_in_grave sound if available, fallback to shovel_dig
self.player.stats.update_stat("Graves filled", 1)
# Stop grave's current audio channel # Stop grave's current audio channel
if obj.channel: if obj.channel:
obj_stop(obj.channel) obj_stop(obj.channel)
@@ -488,14 +492,14 @@ class Level:
# If level is locked, check for remaining enemies # If level is locked, check for remaining enemies
if self.isLocked and any(enemy.isActive for enemy in self.enemies): if self.isLocked and any(enemy.isActive for enemy in self.enemies):
speak("You must defeat all enemies before proceeding!") speak("You must defeat all enemies before proceeding!")
play_sound(self.sounds['locked']) play_sound(self.sounds["locked"])
# Push player back a bit # Push player back a bit
self.player.xPos -= 5 self.player.xPos -= 5
return False return False
# Level complete # Level complete
pygame.mixer.stop() pygame.mixer.stop()
play_sound(self.sounds['end_of_level']) play_sound(self.sounds["end_of_level"])
self.levelScore += 10000 self.levelScore += 10000
# Actually update the scoreboard with level completion # Actually update the scoreboard with level completion
self.player.scoreboard.increase_score(self.levelScore) self.player.scoreboard.increase_score(self.levelScore)
@@ -518,7 +522,7 @@ class Level:
# Calculate volume and pan for splat sound based on final position # Calculate volume and pan for splat sound based on final position
volume, left, right = calculate_volume_and_pan(self.player.xPos, proj.x) volume, left, right = calculate_volume_and_pan(self.player.xPos, proj.x)
if volume > 0: # Only play if within audible range if volume > 0: # Only play if within audible range
obj_play(self.sounds, 'pumpkin_splat', self.player.xPos, proj.x, loop=False) obj_play(self.sounds, "pumpkin_splat", self.player.xPos, proj.x, loop=False)
break break
def throw_projectile(self): def throw_projectile(self):
@@ -528,12 +532,6 @@ class Level:
speak("No jack o'lanterns to throw!") speak("No jack o'lanterns to throw!")
return return
self.projectiles.append(Projectile( self.projectiles.append(Projectile(proj_info["type"], proj_info["start_x"], proj_info["direction"]))
proj_info['type'],
proj_info['start_x'],
proj_info['direction']
))
# Play throw sound # Play throw sound
play_sound(self.sounds['throw_jack_o_lantern']) play_sound(self.sounds["throw_jack_o_lantern"])

View File

@@ -2,6 +2,7 @@
from libstormgames import * from libstormgames import *
class Object: class Object:
def __init__(self, x, yPos, soundName, isStatic=True, isCollectible=False, isHazard=False, zombieSpawnChance=0): def __init__(self, x, yPos, soundName, isStatic=True, isCollectible=False, isHazard=False, zombieSpawnChance=0):
# x can be either a single position or a range [start, end] # x can be either a single position or a range [start, end]
@@ -37,5 +38,3 @@ class Object:
if self.channel: if self.channel:
obj_stop(self.channel) obj_stop(self.channel)
self.channel = None self.channel = None

View File

@@ -13,10 +13,10 @@ from libstormgames.sound import Sound
class PackSoundSystem(dict): class PackSoundSystem(dict):
"""Sound system with hierarchical pack-specific loading.""" """Sound system with hierarchical pack-specific loading."""
def __init__(self, originalSounds, soundDir="sounds/", levelPackName=None): def __init__(self, originalSounds, soundDir="sounds/", levelPackName=None):
"""Initialize pack-specific sound system. """Initialize pack-specific sound system.
Args: Args:
originalSounds (dict): Original sound dictionary from initialize_gui originalSounds (dict): Original sound dictionary from initialize_gui
soundDir (str): Base sound directory soundDir (str): Base sound directory
@@ -24,34 +24,33 @@ class PackSoundSystem(dict):
""" """
# Initialize dict with original sounds # Initialize dict with original sounds
super().__init__(originalSounds) super().__init__(originalSounds)
self.soundDir = soundDir self.soundDir = soundDir
self.levelPackName = levelPackName self.levelPackName = levelPackName
# Load pack-specific sounds if pack name provided # Load pack-specific sounds if pack name provided
if levelPackName: if levelPackName:
self._load_pack_sounds() self._load_pack_sounds()
def _load_pack_sounds(self): def _load_pack_sounds(self):
"""Load pack-specific sounds from sounds/[pack_name]/ directory.""" """Load pack-specific sounds from sounds/[pack_name]/ directory."""
packSoundDir = os.path.join(self.soundDir, self.levelPackName) packSoundDir = os.path.join(self.soundDir, self.levelPackName)
if not os.path.exists(packSoundDir): if not os.path.exists(packSoundDir):
return return
try: try:
for dirPath, _, fileNames in os.walk(packSoundDir): for dirPath, _, fileNames in os.walk(packSoundDir):
relPath = os.path.relpath(dirPath, packSoundDir) relPath = os.path.relpath(dirPath, packSoundDir)
for fileName in fileNames: for fileName in fileNames:
if fileName.lower().endswith(('.ogg', '.wav')): if fileName.lower().endswith((".ogg", ".wav")):
fullPath = os.path.join(dirPath, fileName) fullPath = os.path.join(dirPath, fileName)
baseName = os.path.splitext(fileName)[0] baseName = os.path.splitext(fileName)[0]
# Create sound key same as base system # Create sound key same as base system
soundKey = baseName if relPath == '.' else os.path.join(relPath, baseName).replace('\\', '/') soundKey = baseName if relPath == "." else os.path.join(relPath, baseName).replace("\\", "/")
# Add/override sound in the main dictionary # Add/override sound in the main dictionary
self[soundKey] = pygame.mixer.Sound(fullPath) self[soundKey] = pygame.mixer.Sound(fullPath)
except Exception as e: except Exception as e:
print(f"Error loading pack sounds: {e}") print(f"Error loading pack sounds: {e}")

View File

@@ -58,32 +58,36 @@ class Player:
self.isInvincible = False self.isInvincible = False
self.invincibilityStartTime = 0 self.invincibilityStartTime = 0
self.invincibilityDuration = 10000 # 10 seconds of invincibility self.invincibilityDuration = 10000 # 10 seconds of invincibility
# Death state tracking (to prevent revival after death in same frame) # Death state tracking (to prevent revival after death in same frame)
self.diedThisFrame = False self.diedThisFrame = False
# Initialize starting weapon (rusty shovel) # Initialize starting weapon (rusty shovel)
self.add_weapon(Weapon( self.add_weapon(
name="rusty_shovel", Weapon(
damage=2, name="rusty_shovel",
range=2, damage=2,
attackSound="player_shovel_attack", range=2,
hitSound="player_shovel_hit", attackSound="player_shovel_attack",
attackDuration=200 # 200ms attack duration hitSound="player_shovel_hit",
)) attackDuration=200, # 200ms attack duration
)
)
self.scoreboard = Scoreboard() self.scoreboard = Scoreboard()
def should_play_footstep(self, currentTime): def should_play_footstep(self, currentTime):
"""Check if it's time to play a footstep sound""" """Check if it's time to play a footstep sound"""
return (self.distanceSinceLastStep >= self.get_step_distance() and return (
currentTime - self.lastStepTime >= self.get_step_interval()) self.distanceSinceLastStep >= self.get_step_distance()
and currentTime - self.lastStepTime >= self.get_step_interval()
)
def duck(self): def duck(self):
"""Start ducking""" """Start ducking"""
if not self.isDucking and not self.isJumping: # Can't duck while jumping if not self.isDucking and not self.isJumping: # Can't duck while jumping
self.isDucking = True self.isDucking = True
play_sound(self.sounds['duck']) play_sound(self.sounds["duck"])
return True return True
return False return False
@@ -91,14 +95,14 @@ class Player:
"""Stop ducking state and play sound""" """Stop ducking state and play sound"""
if self.isDucking: if self.isDucking:
self.isDucking = False self.isDucking = False
play_sound(self.sounds['stand']) play_sound(self.sounds["stand"])
def update(self, currentTime): def update(self, currentTime):
"""Update player state""" """Update player state"""
# Reset death flag at start of each frame # Reset death flag at start of each frame
self.diedThisFrame = False self.diedThisFrame = False
if hasattr(self, 'webPenaltyEndTime'): if hasattr(self, "webPenaltyEndTime"):
if currentTime >= self.webPenaltyEndTime: if currentTime >= self.webPenaltyEndTime:
self.moveSpeed *= 2 # Restore speed self.moveSpeed *= 2 # Restore speed
if self.currentWeapon: if self.currentWeapon:
@@ -107,15 +111,17 @@ class Player:
# Check invincibility status # Check invincibility status
if self.isInvincible: if self.isInvincible:
remaining_time = (self.invincibilityStartTime + self.invincibilityDuration - currentTime) / 1000 # Convert to seconds remaining_time = (
self.invincibilityStartTime + self.invincibilityDuration - currentTime
) / 1000 # Convert to seconds
# Handle countdown sounds # Handle countdown sounds
if not hasattr(self, '_last_countdown'): if not hasattr(self, "_last_countdown"):
self._last_countdown = 4 # Start counting from 4 to catch 3,2,1 self._last_countdown = 4 # Start counting from 4 to catch 3,2,1
current_second = int(remaining_time) current_second = int(remaining_time)
if current_second < self._last_countdown and current_second <= 3 and current_second > 0: if current_second < self._last_countdown and current_second <= 3 and current_second > 0:
play_sound(self.sounds['end_of_invincibility_warning']) play_sound(self.sounds["end_of_invincibility_warning"])
self._last_countdown = current_second self._last_countdown = current_second
# Check if invincibility has expired # Check if invincibility has expired
@@ -128,7 +134,7 @@ class Player:
"""Activate invincibility from Hand of Glory""" """Activate invincibility from Hand of Glory"""
self.isInvincible = True self.isInvincible = True
self.invincibilityStartTime = pygame.time.get_ticks() self.invincibilityStartTime = pygame.time.get_ticks()
if hasattr(self, '_last_countdown'): if hasattr(self, "_last_countdown"):
del self._last_countdown # Reset countdown if it exists del self._last_countdown # Reset countdown if it exists
def extra_life(self): def extra_life(self):
@@ -156,30 +162,26 @@ class Player:
return None return None
self._jack_o_lantern_count -= 1 self._jack_o_lantern_count -= 1
return { return {"type": "jack_o_lantern", "start_x": self.xPos, "direction": 1 if self.facingRight else -1}
'type': 'jack_o_lantern',
'start_x': self.xPos,
'direction': 1 if self.facingRight else -1
}
def get_step_distance(self): def get_step_distance(self):
"""Get step distance based on current speed""" """Get step distance based on current speed"""
weaponBonus = self.currentWeapon.speedBonus if self.currentWeapon else 1.0 weaponBonus = self.currentWeapon.speedBonus if self.currentWeapon else 1.0
totalMultiplier = weaponBonus totalMultiplier = weaponBonus
if self.isRunning or self.isJumping: if self.isRunning or self.isJumping:
totalMultiplier *= self.runMultiplier totalMultiplier *= self.runMultiplier
return self.baseStepDistance / totalMultiplier return self.baseStepDistance / totalMultiplier
def get_step_interval(self): def get_step_interval(self):
"""Get minimum time between steps based on current speed""" """Get minimum time between steps based on current speed"""
weaponBonus = self.currentWeapon.speedBonus if self.currentWeapon else 1.0 weaponBonus = self.currentWeapon.speedBonus if self.currentWeapon else 1.0
totalMultiplier = weaponBonus totalMultiplier = weaponBonus
if self.isRunning or self.isJumping: if self.isRunning or self.isJumping:
totalMultiplier *= self.runMultiplier totalMultiplier *= self.runMultiplier
return self.baseStepInterval / totalMultiplier return self.baseStepInterval / totalMultiplier
def get_health(self): def get_health(self):
@@ -198,8 +200,8 @@ class Player:
"""Calculate current speed based on state and weapon""" """Calculate current speed based on state and weapon"""
baseSpeed = self.moveSpeed baseSpeed = self.moveSpeed
weaponBonus = self.currentWeapon.speedBonus if self.currentWeapon else 1.0 weaponBonus = self.currentWeapon.speedBonus if self.currentWeapon else 1.0
if self.isJumping or self.isRunning: if self.isJumping or self.isRunning:
return baseSpeed * self.runMultiplier * weaponBonus return baseSpeed * self.runMultiplier * weaponBonus
return baseSpeed * weaponBonus return baseSpeed * weaponBonus
@@ -218,7 +220,7 @@ class Player:
# Oops, allow healing while invincible. # Oops, allow healing while invincible.
if self.isInvincible and value < old_health: if self.isInvincible and value < old_health:
return return
self._health = max(0, value) # Health can't go below 0 self._health = max(0, value) # Health can't go below 0
@@ -230,10 +232,10 @@ class Player:
pygame.mixer.stop() pygame.mixer.stop()
try: try:
pygame.mixer.music.stop() pygame.mixer.music.stop()
except: except Exception:
pass pass
cut_scene(self.sounds, 'lose_a_life') cut_scene(self.sounds, "lose_a_life")
def set_max_health(self, value): def set_max_health(self, value):
"""Set max health""" """Set max health"""
@@ -279,23 +281,19 @@ class Player:
def switch_to_weapon(self, weaponIndex): def switch_to_weapon(self, weaponIndex):
"""Switch to weapon by index (1=shovel, 2=broom, 3=nunchucks)""" """Switch to weapon by index (1=shovel, 2=broom, 3=nunchucks)"""
weaponMap = { weaponMap = {1: "rusty_shovel", 2: "witch_broom", 3: "nunchucks"}
1: "rusty_shovel",
2: "witch_broom",
3: "nunchucks"
}
targetWeaponName = weaponMap.get(weaponIndex) targetWeaponName = weaponMap.get(weaponIndex)
if not targetWeaponName: if not targetWeaponName:
return False return False
# Find the weapon in player's inventory # Find the weapon in player's inventory
for weapon in self.weapons: for weapon in self.weapons:
if weapon.name == targetWeaponName: if weapon.name == targetWeaponName:
self.equip_weapon(weapon) self.equip_weapon(weapon)
speak(weapon.name.replace('_', ' ')) speak(weapon.name.replace("_", " "))
return True return True
# Weapon not found in inventory # Weapon not found in inventory
return False return False
@@ -317,5 +315,3 @@ class Player:
if not self.currentWeapon or not self.currentWeapon.is_attack_active(currentTime): if not self.currentWeapon or not self.currentWeapon.is_attack_active(currentTime):
return None return None
return self.currentWeapon.get_attack_range(self.xPos, self.facingRight) return self.currentWeapon.get_attack_range(self.xPos, self.facingRight)

View File

@@ -5,14 +5,10 @@ from libstormgames import *
from src.object import Object from src.object import Object
from src.weapon import Weapon from src.weapon import Weapon
class PowerUp(Object): class PowerUp(Object):
def __init__(self, x, y, item_type, sounds, direction, left_boundary=1, right_boundary=100): def __init__(self, x, y, item_type, sounds, direction, left_boundary=1, right_boundary=100):
super().__init__( super().__init__(x, y, item_type, isStatic=False, isCollectible=True, isHazard=False)
x, y, item_type,
isStatic=False,
isCollectible=True,
isHazard=False
)
self.sounds = sounds self.sounds = sounds
self.direction = direction self.direction = direction
self.speed = 0.049 # Base movement speed self.speed = 0.049 # Base movement speed
@@ -58,35 +54,32 @@ class PowerUp(Object):
def apply_effect(self, player, level=None): def apply_effect(self, player, level=None):
"""Apply the item's effect when collected""" """Apply the item's effect when collected"""
if self.item_type == 'hand_of_glory': if self.item_type == "hand_of_glory":
player.start_invincibility() player.start_invincibility()
elif self.item_type == 'cauldron': elif self.item_type == "cauldron":
player.restore_health() player.restore_health()
elif self.item_type == 'guts': elif self.item_type == "guts":
player.add_guts() player.add_guts()
player.collectedItems.append('guts') player.collectedItems.append("guts")
self.check_for_nunchucks(player) self.check_for_nunchucks(player)
elif self.item_type == 'jack_o_lantern': elif self.item_type == "jack_o_lantern":
player.add_jack_o_lantern() player.add_jack_o_lantern()
elif self.item_type == 'extra_life': elif self.item_type == "extra_life":
# Don't give extra lives in survival mode # Don't give extra lives in survival mode
if level and level.levelId == 999: if level and level.levelId == 999:
# In survival mode, give bonus score instead # In survival mode, give bonus score instead
level.levelScore += 2000 level.levelScore += 2000
speak("Extra life found! Bonus score in survival mode!") speak("Extra life found! Bonus score in survival mode!")
play_sound(self.sounds.get('survivor_bonus', 'get_extra_life')) # Use survivor_bonus sound if available play_sound(self.sounds.get("survivor_bonus", "get_extra_life")) # Use survivor_bonus sound if available
else: else:
player.extra_life() player.extra_life()
elif self.item_type == 'shin_bone': # Add shin bone handling elif self.item_type == "shin_bone": # Add shin bone handling
player.shinBoneCount += 1 player.shinBoneCount += 1
player._coins += 5 player._coins += 5
player.add_save_bone_dust(5) # Add to save bone dust counter too player.add_save_bone_dust(5) # Add to save bone dust counter too
if player.get_health() < player.get_max_health(): if player.get_health() < player.get_max_health():
player.set_health(min( player.set_health(min(player.get_health() + 1, player.get_max_health()))
player.get_health() + 1,
player.get_max_health()
))
# Check for 100 coin bonus after adding shin bone coins # Check for 100 coin bonus after adding shin bone coins
if player._coins >= 100: if player._coins >= 100:
# Only give extra lives in story mode, not survival mode (level_id 999) # Only give extra lives in story mode, not survival mode (level_id 999)
@@ -95,20 +88,22 @@ class PowerUp(Object):
player._coins = 0 player._coins = 0
player._lives += 1 player._lives += 1
level.levelScore += 1000 level.levelScore += 1000
play_sound(self.sounds['get_extra_life']) play_sound(self.sounds["get_extra_life"])
else: else:
# In survival mode, reset coin counter but give bonus score instead # In survival mode, reset coin counter but give bonus score instead
player._coins = 0 player._coins = 0
level.levelScore += 2000 # Double score bonus instead of extra life level.levelScore += 2000 # Double score bonus instead of extra life
speak("100 bone dust collected! Bonus score!") speak("100 bone dust collected! Bonus score!")
play_sound(self.sounds.get('survivor_bonus', 'bone_dust')) # Use survivor_bonus sound if available, fallback to bone_dust play_sound(
self.sounds.get("survivor_bonus", "bone_dust")
) # Use survivor_bonus sound if available, fallback to bone_dust
self.check_for_nunchucks(player) self.check_for_nunchucks(player)
elif self.item_type == 'witch_broom': elif self.item_type == "witch_broom":
broomWeapon = Weapon.create_witch_broom() broomWeapon = Weapon.create_witch_broom()
player.add_weapon(broomWeapon) player.add_weapon(broomWeapon)
player.equip_weapon(broomWeapon) player.equip_weapon(broomWeapon)
elif self.item_type == 'spiderweb': elif self.item_type == "spiderweb":
# Bounce player back (happens even if invincible) # Bounce player back (happens even if invincible)
player.xPos -= 3 if player.xPos > self.xPos else -3 player.xPos -= 3 if player.xPos > self.xPos else -3
@@ -122,7 +117,7 @@ class PowerUp(Object):
player.webPenaltyEndTime = pygame.time.get_ticks() + 15000 player.webPenaltyEndTime = pygame.time.get_ticks() + 15000
# Tell level to spawn a spider # Tell level to spawn a spider
if hasattr(self, 'level'): if hasattr(self, "level"):
self.level.spawn_spider(self.xPos, self.yPos) self.level.spawn_spider(self.xPos, self.yPos)
# Stop movement sound when collected # Stop movement sound when collected
@@ -131,20 +126,20 @@ class PowerUp(Object):
self.channel = None self.channel = None
# Item tracking # Item tracking
player.stats.update_stat('Items collected', 1) player.stats.update_stat("Items collected", 1)
def check_for_nunchucks(self, player): def check_for_nunchucks(self, player):
"""Check if player has materials for nunchucks and create if conditions are met""" """Check if player has materials for nunchucks and create if conditions are met"""
if (player.shinBoneCount >= 2 and if (
'guts' in player.collectedItems and player.shinBoneCount >= 2
not any(weapon.name == "nunchucks" for weapon in player.weapons)): and "guts" in player.collectedItems
and not any(weapon.name == "nunchucks" for weapon in player.weapons)
):
nunchucksWeapon = Weapon.create_nunchucks() nunchucksWeapon = Weapon.create_nunchucks()
player.add_weapon(nunchucksWeapon) player.add_weapon(nunchucksWeapon)
player.equip_weapon(nunchucksWeapon) player.equip_weapon(nunchucksWeapon)
basePoints = nunchucksWeapon.damage * 1000 basePoints = nunchucksWeapon.damage * 1000
rangeModifier = nunchucksWeapon.range * 500 rangeModifier = nunchucksWeapon.range * 500
player.scoreboard.increase_score(basePoints + rangeModifier) player.scoreboard.increase_score(basePoints + rangeModifier)
play_sound(self.sounds['get_nunchucks']) play_sound(self.sounds["get_nunchucks"])
player.stats.update_stat('Items collected', 1) player.stats.update_stat("Items collected", 1)

View File

@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
class Projectile: class Projectile:
def __init__(self, projectile_type, start_x, direction): def __init__(self, projectile_type, start_x, direction):
self.type = projectile_type self.type = projectile_type
@@ -29,5 +30,3 @@ class Projectile:
"""Handle hitting an enemy""" """Handle hitting an enemy"""
enemy.take_damage(self.damage) enemy.take_damage(self.damage)
self.isActive = False # Projectile is destroyed on hit self.isActive = False # Projectile is destroyed on hit

View File

@@ -11,8 +11,8 @@ class SaveManager:
def __init__(self): def __init__(self):
"""Initialize save manager with XDG-compliant save directory""" """Initialize save manager with XDG-compliant save directory"""
# Use XDG_CONFIG_HOME or default to ~/.config # Use XDG_CONFIG_HOME or default to ~/.config
config_home = os.environ.get('XDG_CONFIG_HOME', os.path.expanduser('~/.config')) config_home = os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config"))
self.save_dir = Path(config_home) / 'storm-games' / 'wicked-quest' self.save_dir = Path(config_home) / "storm-games" / "wicked-quest"
self.save_dir.mkdir(parents=True, exist_ok=True) self.save_dir.mkdir(parents=True, exist_ok=True)
self.max_saves = 10 self.max_saves = 10
@@ -24,7 +24,7 @@ class SaveManager:
# Validate required parameters # Validate required parameters
if current_game is None: if current_game is None:
return False, "No game selected to save" return False, "No game selected to save"
if current_level is None: if current_level is None:
return False, "No current level to save" return False, "No current level to save"
@@ -34,30 +34,30 @@ class SaveManager:
# Create save data # Create save data
save_data = { save_data = {
'player_state': { "player_state": {
'xPos': player.xPos, "xPos": player.xPos,
'yPos': player.yPos, "yPos": player.yPos,
'health': player._health, "health": player._health,
'maxHealth': player._maxHealth, "maxHealth": player._maxHealth,
'lives': player._lives, "lives": player._lives,
'coins': player._coins, "coins": player._coins,
'saveBoneDust': player._saveBoneDust, "saveBoneDust": player._saveBoneDust,
'jackOLanternCount': player._jack_o_lantern_count, "jackOLanternCount": player._jack_o_lantern_count,
'shinBoneCount': player.shinBoneCount, "shinBoneCount": player.shinBoneCount,
'inventory': player.inventory, "inventory": player.inventory,
'collectedItems': player.collectedItems, "collectedItems": player.collectedItems,
'weapons': self._serialize_weapons(player.weapons), "weapons": self._serialize_weapons(player.weapons),
'currentWeaponName': player.currentWeapon.name if player.currentWeapon else None, "currentWeaponName": player.currentWeapon.name if player.currentWeapon else None,
'stats': self._serialize_stats(player.stats), "stats": self._serialize_stats(player.stats),
'scoreboard': self._serialize_scoreboard(player.scoreboard) "scoreboard": self._serialize_scoreboard(player.scoreboard),
}, },
'game_state': { "game_state": {
'currentLevel': current_level, "currentLevel": current_level,
'currentGame': current_game, "currentGame": current_game,
'gameStartTime': game_start_time, "gameStartTime": game_start_time,
'saveTime': datetime.now() "saveTime": datetime.now(),
}, },
'version': '1.0' "version": "1.0",
} }
# Generate filename with timestamp # Generate filename with timestamp
@@ -67,26 +67,26 @@ class SaveManager:
try: try:
# Write to temporary file first, then rename for atomic operation # Write to temporary file first, then rename for atomic operation
temp_filepath = filepath.with_suffix('.tmp') temp_filepath = filepath.with_suffix(".tmp")
with open(temp_filepath, 'wb') as f: with open(temp_filepath, "wb") as f:
pickle.dump(save_data, f) pickle.dump(save_data, f)
f.flush() # Ensure data is written to disk f.flush() # Ensure data is written to disk
os.fsync(f.fileno()) # Force write to disk os.fsync(f.fileno()) # Force write to disk
# Atomic rename (replaces old file if it exists) # Atomic rename (replaces old file if it exists)
temp_filepath.rename(filepath) temp_filepath.rename(filepath)
# Clean up old saves if we exceed max_saves # Clean up old saves if we exceed max_saves
self._cleanup_old_saves() self._cleanup_old_saves()
return True, f"Game saved to {filename}" return True, f"Game saved to {filename}"
except Exception as e: except Exception as e:
# Clean up temp file if it exists # Clean up temp file if it exists
if temp_filepath.exists(): if temp_filepath.exists():
try: try:
temp_filepath.unlink() temp_filepath.unlink()
except: except Exception:
pass pass
return False, f"Failed to save game: {str(e)}" return False, f"Failed to save game: {str(e)}"
@@ -94,132 +94,136 @@ class SaveManager:
"""Serialize weapons for saving""" """Serialize weapons for saving"""
serialized = [] serialized = []
for weapon in weapons: for weapon in weapons:
serialized.append({ serialized.append(
'name': weapon.name, {
'damage': weapon.damage, "name": weapon.name,
'range': weapon.range, "damage": weapon.damage,
'attackSound': weapon.attackSound, "range": weapon.range,
'hitSound': weapon.hitSound, "attackSound": weapon.attackSound,
'attackDuration': weapon.attackDuration, "hitSound": weapon.hitSound,
'speedBonus': getattr(weapon, 'speedBonus', 1.0), "attackDuration": weapon.attackDuration,
'jumpDurationBonus': getattr(weapon, 'jumpDurationBonus', 1.0) "speedBonus": getattr(weapon, "speedBonus", 1.0),
}) "jumpDurationBonus": getattr(weapon, "jumpDurationBonus", 1.0),
}
)
return serialized return serialized
def _deserialize_weapons(self, weapon_data): def _deserialize_weapons(self, weapon_data):
"""Deserialize weapons from save data""" """Deserialize weapons from save data"""
from src.weapon import Weapon from src.weapon import Weapon
weapons = [] weapons = []
for data in weapon_data: for data in weapon_data:
# Handle backward compatibility for old saves # Handle backward compatibility for old saves
speedBonus = data.get('speedBonus', 1.0) speedBonus = data.get("speedBonus", 1.0)
jumpDurationBonus = data.get('jumpDurationBonus', 1.0) jumpDurationBonus = data.get("jumpDurationBonus", 1.0)
# For old saves, restore proper bonuses for specific weapons # For old saves, restore proper bonuses for specific weapons
if data['name'] == 'witch_broom' and speedBonus == 1.0: if data["name"] == "witch_broom" and speedBonus == 1.0:
speedBonus = 1.17 speedBonus = 1.17
jumpDurationBonus = 1.25 jumpDurationBonus = 1.25
weapon = Weapon( weapon = Weapon(
name=data['name'], name=data["name"],
damage=data['damage'], damage=data["damage"],
range=data['range'], range=data["range"],
attackSound=data['attackSound'], attackSound=data["attackSound"],
hitSound=data['hitSound'], hitSound=data["hitSound"],
attackDuration=data['attackDuration'], attackDuration=data["attackDuration"],
speedBonus=speedBonus, speedBonus=speedBonus,
jumpDurationBonus=jumpDurationBonus jumpDurationBonus=jumpDurationBonus,
) )
weapons.append(weapon) weapons.append(weapon)
return weapons return weapons
def _serialize_stats(self, stats): def _serialize_stats(self, stats):
"""Serialize stats for saving""" """Serialize stats for saving"""
return { return {"total": stats.total.copy(), "level": stats.level.copy()}
'total': stats.total.copy(),
'level': stats.level.copy()
}
def _deserialize_stats(self, stats_data): def _deserialize_stats(self, stats_data):
"""Deserialize stats from save data""" """Deserialize stats from save data"""
from src.stat_tracker import StatTracker from src.stat_tracker import StatTracker
stats = StatTracker() stats = StatTracker()
if 'total' in stats_data: if "total" in stats_data:
stats.total.update(stats_data['total']) stats.total.update(stats_data["total"])
if 'level' in stats_data: if "level" in stats_data:
stats.level.update(stats_data['level']) stats.level.update(stats_data["level"])
return stats return stats
def _serialize_scoreboard(self, scoreboard): def _serialize_scoreboard(self, scoreboard):
"""Serialize scoreboard for saving""" """Serialize scoreboard for saving"""
return { return {
'currentScore': getattr(scoreboard, 'currentScore', 0), "currentScore": getattr(scoreboard, "currentScore", 0),
'highScores': getattr(scoreboard, 'highScores', []) "highScores": getattr(scoreboard, "highScores", []),
} }
def _deserialize_scoreboard(self, scoreboard_data): def _deserialize_scoreboard(self, scoreboard_data):
"""Deserialize scoreboard from save data""" """Deserialize scoreboard from save data"""
from libstormgames import Scoreboard from libstormgames import Scoreboard
scoreboard = Scoreboard() scoreboard = Scoreboard()
if 'currentScore' in scoreboard_data: if "currentScore" in scoreboard_data:
scoreboard.currentScore = scoreboard_data['currentScore'] scoreboard.currentScore = scoreboard_data["currentScore"]
if 'highScores' in scoreboard_data: if "highScores" in scoreboard_data:
scoreboard.highScores = scoreboard_data['highScores'] scoreboard.highScores = scoreboard_data["highScores"]
return scoreboard return scoreboard
def get_save_files(self): def get_save_files(self):
"""Get list of save files with metadata""" """Get list of save files with metadata"""
save_files = [] save_files = []
pattern = str(self.save_dir / "save_*.pickle") pattern = str(self.save_dir / "save_*.pickle")
for filepath in glob.glob(pattern): for filepath in glob.glob(pattern):
try: try:
with open(filepath, 'rb') as f: with open(filepath, "rb") as f:
save_data = pickle.load(f) save_data = pickle.load(f)
# Validate save data structure # Validate save data structure
if not self._validate_save_data(save_data): if not self._validate_save_data(save_data):
print(f"Invalid save file structure: {filepath}") print(f"Invalid save file structure: {filepath}")
continue continue
# Extract save info # Extract save info
save_time = save_data['game_state']['saveTime'] save_time = save_data["game_state"]["saveTime"]
level = save_data['game_state']['currentLevel'] level = save_data["game_state"]["currentLevel"]
game_name = save_data['game_state']['currentGame'] game_name = save_data["game_state"]["currentGame"]
# Format display name # Format display name
formatted_time = save_time.strftime("%B %d %I:%M%p") formatted_time = save_time.strftime("%B %d %I:%M%p")
display_name = f"{formatted_time} {game_name} Level {level}" display_name = f"{formatted_time} {game_name} Level {level}"
save_files.append({ save_files.append(
'filepath': filepath, {
'display_name': display_name, "filepath": filepath,
'save_time': save_time, "display_name": display_name,
'level': level, "save_time": save_time,
'game_name': game_name, "level": level,
'save_data': save_data "game_name": game_name,
}) "save_data": save_data,
}
)
except (pickle.PickleError, EOFError, OSError) as e: except (pickle.PickleError, EOFError, OSError) as e:
print(f"Corrupted save file {filepath}: {e}") print(f"Corrupted save file {filepath}: {e}")
# Try to remove corrupted save file # Try to remove corrupted save file
try: try:
os.remove(filepath) os.remove(filepath)
print(f"Removed corrupted save file: {filepath}") print(f"Removed corrupted save file: {filepath}")
except: except Exception:
pass pass
continue continue
except Exception as e: except Exception as e:
print(f"Error reading save file {filepath}: {e}") print(f"Error reading save file {filepath}: {e}")
continue continue
# Sort by save time (newest first) # Sort by save time (newest first)
save_files.sort(key=lambda x: x['save_time'], reverse=True) save_files.sort(key=lambda x: x["save_time"], reverse=True)
return save_files return save_files
def load_save(self, filepath): def load_save(self, filepath):
"""Load game state from save file""" """Load game state from save file"""
try: try:
with open(filepath, 'rb') as f: with open(filepath, "rb") as f:
save_data = pickle.load(f) save_data = pickle.load(f)
return True, save_data return True, save_data
except Exception as e: except Exception as e:
@@ -227,55 +231,57 @@ class SaveManager:
def restore_player_state(self, player, save_data): def restore_player_state(self, player, save_data):
"""Restore player state from save data""" """Restore player state from save data"""
player_state = save_data['player_state'] player_state = save_data["player_state"]
# Restore basic attributes # Restore basic attributes
player.xPos = player_state['xPos'] player.xPos = player_state["xPos"]
player.yPos = player_state['yPos'] player.yPos = player_state["yPos"]
player._health = player_state['health'] player._health = player_state["health"]
player._maxHealth = player_state['maxHealth'] player._maxHealth = player_state["maxHealth"]
player._lives = player_state['lives'] player._lives = player_state["lives"]
player._coins = player_state['coins'] player._coins = player_state["coins"]
player._saveBoneDust = player_state['saveBoneDust'] player._saveBoneDust = player_state["saveBoneDust"]
player._jack_o_lantern_count = player_state['jackOLanternCount'] player._jack_o_lantern_count = player_state["jackOLanternCount"]
player.shinBoneCount = player_state['shinBoneCount'] player.shinBoneCount = player_state["shinBoneCount"]
player.inventory = player_state['inventory'] player.inventory = player_state["inventory"]
player.collectedItems = player_state['collectedItems'] player.collectedItems = player_state["collectedItems"]
# Restore weapons # Restore weapons
player.weapons = self._deserialize_weapons(player_state['weapons']) player.weapons = self._deserialize_weapons(player_state["weapons"])
# Restore current weapon # Restore current weapon
current_weapon_name = player_state.get('currentWeaponName') current_weapon_name = player_state.get("currentWeaponName")
if current_weapon_name: if current_weapon_name:
for weapon in player.weapons: for weapon in player.weapons:
if weapon.name == current_weapon_name: if weapon.name == current_weapon_name:
player.currentWeapon = weapon player.currentWeapon = weapon
break break
# Restore stats # Restore stats
if 'stats' in player_state: if "stats" in player_state:
player.stats = self._deserialize_stats(player_state['stats']) player.stats = self._deserialize_stats(player_state["stats"])
else: else:
from src.stat_tracker import StatTracker from src.stat_tracker import StatTracker
player.stats = StatTracker() player.stats = StatTracker()
# Restore scoreboard # Restore scoreboard
if 'scoreboard' in player_state: if "scoreboard" in player_state:
player.scoreboard = self._deserialize_scoreboard(player_state['scoreboard']) player.scoreboard = self._deserialize_scoreboard(player_state["scoreboard"])
else: else:
from libstormgames import Scoreboard from libstormgames import Scoreboard
player.scoreboard = Scoreboard() player.scoreboard = Scoreboard()
def _cleanup_old_saves(self): def _cleanup_old_saves(self):
"""Remove old save files if we exceed max_saves""" """Remove old save files if we exceed max_saves"""
save_files = self.get_save_files() save_files = self.get_save_files()
if len(save_files) > self.max_saves: if len(save_files) > self.max_saves:
# Remove oldest saves # Remove oldest saves
for save_file in save_files[self.max_saves:]: for save_file in save_files[self.max_saves:]:
try: try:
os.remove(save_file['filepath']) os.remove(save_file["filepath"])
except Exception as e: except Exception as e:
print(f"Error removing old save {save_file['filepath']}: {e}") print(f"Error removing old save {save_file['filepath']}: {e}")
@@ -283,25 +289,24 @@ class SaveManager:
"""Validate that save data has required structure""" """Validate that save data has required structure"""
try: try:
# Check for required top-level keys # Check for required top-level keys
required_keys = ['player_state', 'game_state', 'version'] required_keys = ["player_state", "game_state", "version"]
if not all(key in save_data for key in required_keys): if not all(key in save_data for key in required_keys):
return False return False
# Check player_state structure # Check player_state structure
player_required = ['xPos', 'yPos', 'health', 'maxHealth', 'lives', 'coins', 'saveBoneDust'] player_required = ["xPos", "yPos", "health", "maxHealth", "lives", "coins", "saveBoneDust"]
if not all(key in save_data['player_state'] for key in player_required): if not all(key in save_data["player_state"] for key in player_required):
return False return False
# Check game_state structure # Check game_state structure
game_required = ['currentLevel', 'currentGame', 'gameStartTime', 'saveTime'] game_required = ["currentLevel", "currentGame", "gameStartTime", "saveTime"]
if not all(key in save_data['game_state'] for key in game_required): if not all(key in save_data["game_state"] for key in game_required):
return False return False
return True return True
except: except Exception:
return False return False
def has_saves(self): def has_saves(self):
"""Check if any save files exist""" """Check if any save files exist"""
return len(self.get_save_files()) > 0 return len(self.get_save_files()) > 0

View File

@@ -5,17 +5,13 @@ import random
from libstormgames import * from libstormgames import *
from src.object import Object from src.object import Object
class SkullStorm(Object): class SkullStorm(Object):
"""Handles falling skulls within a specified range.""" """Handles falling skulls within a specified range."""
def __init__(self, xRange, y, sounds, damage, maxSkulls=3, minFreq=2, maxFreq=5): def __init__(self, xRange, y, sounds, damage, maxSkulls=3, minFreq=2, maxFreq=5):
super().__init__( super().__init__(
xRange, xRange, y, "", isStatic=True, isCollectible=False, isHazard=False # No ambient sound for the skull storm
y,
"", # No ambient sound for the skull storm
isStatic=True,
isCollectible=False,
isHazard=False
) )
self.sounds = sounds self.sounds = sounds
self.damage = damage self.damage = damage
@@ -37,7 +33,7 @@ class SkullStorm(Object):
inRange = self.xRange[0] <= player.xPos <= self.xRange[1] inRange = self.xRange[0] <= player.xPos <= self.xRange[1]
if inRange and not self.playerInRange: if inRange and not self.playerInRange:
# Player just entered range - play the warning sound # Player just entered range - play the warning sound
play_sound(self.sounds['skull_storm']) play_sound(self.sounds["skull_storm"])
self.playerInRange = True self.playerInRange = True
elif not inRange and self.playerInRange: # Only speak when actually leaving range elif not inRange and self.playerInRange: # Only speak when actually leaving range
# Player just left range # Player just left range
@@ -46,8 +42,8 @@ class SkullStorm(Object):
# Clear any active skulls when player leaves the range # Clear any active skulls when player leaves the range
for skull in self.activeSkulls[:]: for skull in self.activeSkulls[:]:
if skull['channel']: if skull["channel"]:
obj_stop(skull['channel']) obj_stop(skull["channel"])
self.activeSkulls = [] # Reset the list of active skulls self.activeSkulls = [] # Reset the list of active skulls
if not inRange: if not inRange:
@@ -55,29 +51,28 @@ class SkullStorm(Object):
# Update existing skulls # Update existing skulls
for skull in self.activeSkulls[:]: # Copy list to allow removal for skull in self.activeSkulls[:]: # Copy list to allow removal
if currentTime >= skull['land_time']: if currentTime >= skull["land_time"]:
# Skull has landed # Skull has landed
self.handle_landing(skull, player) self.handle_landing(skull, player)
self.activeSkulls.remove(skull) self.activeSkulls.remove(skull)
else: else:
# Update falling sound # Update falling sound
timeElapsed = currentTime - skull['start_time'] timeElapsed = currentTime - skull["start_time"]
fallProgress = timeElapsed / skull['fall_duration'] fallProgress = timeElapsed / skull["fall_duration"]
currentY = self.yPos * (1 - fallProgress) currentY = self.yPos * (1 - fallProgress)
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'] 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 currentTime - self.lastSkullTime >= self.nextSkullDelay:
currentTime - self.lastSkullTime >= self.nextSkullDelay):
self.spawn_skull(currentTime) self.spawn_skull(currentTime)
def spawn_skull(self, currentTime): def spawn_skull(self, currentTime):
@@ -91,11 +86,11 @@ class SkullStorm(Object):
# Create new skull # Create new skull
skull = { skull = {
'x': random.uniform(self.xRange[0], self.xRange[1]), "x": random.uniform(self.xRange[0], self.xRange[1]),
'start_time': currentTime, "start_time": currentTime,
'fall_duration': fallDuration, "fall_duration": fallDuration,
'land_time': currentTime + fallDuration, "land_time": currentTime + fallDuration,
'channel': None "channel": None,
} }
self.activeSkulls.append(skull) self.activeSkulls.append(skull)
@@ -103,20 +98,18 @@ class SkullStorm(Object):
def handle_landing(self, skull, player): def handle_landing(self, skull, player):
"""Handle a skull landing.""" """Handle a skull landing."""
# Stop falling sound # Stop falling sound
if skull['channel']: if skull["channel"]:
obj_stop(skull['channel']) obj_stop(skull["channel"])
# Play landing sound with positional audio once # Play landing sound with positional audio once
channel = pygame.mixer.find_channel(True) # Find an available channel channel = pygame.mixer.find_channel(True) # Find an available channel
if channel: if channel:
soundObj = self.sounds['skull_lands'] soundObj = self.sounds["skull_lands"]
obj_play(self.sounds, 'skull_lands', player.xPos, skull['x'], loop=False) obj_play(self.sounds, "skull_lands", player.xPos, skull["x"], loop=False)
# Check if player was hit # Check if player was hit
if abs(player.xPos - skull['x']) < 1: # Within 1 tile if abs(player.xPos - skull["x"]) < 1: # Within 1 tile
if not player.isInvincible: if not player.isInvincible:
player.set_health(player.get_health() - self.damage) player.set_health(player.get_health() - self.damage)
self.sounds['player_takes_damage'].play() self.sounds["player_takes_damage"].play()
speak("Hit by falling skull!") speak("Hit by falling skull!")

View File

@@ -1,20 +1,15 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
class StatTracker: class StatTracker:
def __init__(self): def __init__(self):
# Base dictionary for tracking stats # Base dictionary for tracking stats
self.total = { self.total = {"Bone dust": 0, "Enemies killed": 0, "Coffins broken": 0, "Items collected": 0, "Total time": 0}
'Bone dust': 0,
'Enemies killed': 0,
'Coffins broken': 0,
'Items collected': 0,
'Total time': 0
}
# Create level stats from total (shallow copy is fine here) # Create level stats from total (shallow copy is fine here)
self.level = self.total.copy() self.level = self.total.copy()
self.total['levelsCompleted'] = 0 self.total["levelsCompleted"] = 0
def reset_level(self): def reset_level(self):
"""Reset level stats based on variable type""" """Reset level stats based on variable type"""
@@ -42,5 +37,3 @@ class StatTracker:
def get_total_stat(self, statName): def get_total_stat(self, statName):
"""Get a total stat""" """Get a total stat"""
return self.total.get(statName, 0) return self.total.get(statName, 0)

View File

@@ -10,7 +10,7 @@ from src.game_selection import get_level_path
class SurvivalGenerator: class SurvivalGenerator:
def __init__(self, gamePack): def __init__(self, gamePack):
"""Initialize the survival generator for a specific game pack. """Initialize the survival generator for a specific game pack.
Args: Args:
gamePack (str): Name of the game pack directory gamePack (str): Name of the game pack directory
""" """
@@ -24,57 +24,57 @@ class SurvivalGenerator:
self.footstepSounds = [] self.footstepSounds = []
self.loadLevelData() self.loadLevelData()
self.parseTemplates() self.parseTemplates()
def loadLevelData(self): def loadLevelData(self):
"""Load all level JSON files from the game pack.""" """Load all level JSON files from the game pack."""
levelFiles = [] levelFiles = []
packPath = os.path.join("levels", self.gamePack) packPath = os.path.join("levels", self.gamePack)
if not os.path.exists(packPath): if not os.path.exists(packPath):
raise FileNotFoundError(f"Game pack '{self.gamePack}' not found") raise FileNotFoundError(f"Game pack '{self.gamePack}' not found")
# Get all JSON files in the pack directory # Get all JSON files in the pack directory
for file in os.listdir(packPath): for file in os.listdir(packPath):
if file.endswith('.json') and file[0].isdigit(): if file.endswith(".json") and file[0].isdigit():
levelFiles.append(file) levelFiles.append(file)
# Load each level file # Load each level file
for levelFile in levelFiles: for levelFile in levelFiles:
levelPath = os.path.join(packPath, levelFile) levelPath = os.path.join(packPath, levelFile)
with open(levelPath, 'r') as f: with open(levelPath, "r") as f:
levelNum = int(levelFile.split('.')[0]) levelNum = int(levelFile.split(".")[0])
self.levelData[levelNum] = json.load(f) self.levelData[levelNum] = json.load(f)
def parseTemplates(self): def parseTemplates(self):
"""Parse all level data to extract object templates by type.""" """Parse all level data to extract object templates by type."""
for levelNum, data in self.levelData.items(): for levelNum, data in self.levelData.items():
# Store ambience and footstep sounds (remove duplicates) # Store ambience and footstep sounds (remove duplicates)
if 'ambience' in data and data['ambience'] not in self.ambientSounds: if "ambience" in data and data["ambience"] not in self.ambientSounds:
self.ambientSounds.append(data['ambience']) self.ambientSounds.append(data["ambience"])
if 'footstep_sound' in data and data['footstep_sound'] not in self.footstepSounds: if "footstep_sound" in data and data["footstep_sound"] not in self.footstepSounds:
self.footstepSounds.append(data['footstep_sound']) self.footstepSounds.append(data["footstep_sound"])
# Parse objects # Parse objects
for obj in data.get('objects', []): for obj in data.get("objects", []):
objCopy = copy.deepcopy(obj) objCopy = copy.deepcopy(obj)
# Categorize objects # Categorize objects
if 'enemy_type' in obj: if "enemy_type" in obj:
self.enemyTemplates.append(objCopy) self.enemyTemplates.append(objCopy)
elif obj.get('collectible', False) or obj.get('sound') == 'bone_dust': elif obj.get("collectible", False) or obj.get("sound") == "bone_dust":
self.collectibleTemplates.append(objCopy) self.collectibleTemplates.append(objCopy)
elif obj.get('type') in ['skull_storm', 'catapult', 'grasping_hands']: elif obj.get("type") in ["skull_storm", "catapult", "grasping_hands"]:
self.hazardTemplates.append(objCopy) self.hazardTemplates.append(objCopy)
else: else:
self.objectTemplates.append(objCopy) self.objectTemplates.append(objCopy)
def generate_survival_level(self, difficultyLevel=1, segmentLength=100): def generate_survival_level(self, difficultyLevel=1, segmentLength=100):
"""Generate an endless survival level segment. """Generate an endless survival level segment.
Args: Args:
difficultyLevel (int): Current difficulty level (increases over time) difficultyLevel (int): Current difficulty level (increases over time)
segmentLength (int): Length of this level segment segmentLength (int): Length of this level segment
Returns: Returns:
dict: Generated level data dict: Generated level data
""" """
@@ -88,30 +88,30 @@ class SurvivalGenerator:
"boundaries": {"left": 0, "right": segmentLength}, "boundaries": {"left": 0, "right": segmentLength},
"locked": True, # Enable lock system for survival mode "locked": True, # Enable lock system for survival mode
"ambience": "Escaping the Grave.ogg", # Will be overridden below "ambience": "Escaping the Grave.ogg", # Will be overridden below
"footstep_sound": "footstep_stone" # Will be overridden below "footstep_sound": "footstep_stone", # Will be overridden below
} }
# Choose random music and footstep from collected unique tracks # Choose random music and footstep from collected unique tracks
if self.ambientSounds: if self.ambientSounds:
levelData["ambience"] = random.choice(self.ambientSounds) levelData["ambience"] = random.choice(self.ambientSounds)
if self.footstepSounds: if self.footstepSounds:
levelData["footstep_sound"] = random.choice(self.footstepSounds) levelData["footstep_sound"] = random.choice(self.footstepSounds)
# Calculate spawn rates based on difficulty # Calculate spawn rates based on difficulty
collectibleDensity = max(0.1, 0.3 - (difficultyLevel * 0.02)) # Fewer collectibles over time collectibleDensity = max(0.1, 0.3 - (difficultyLevel * 0.02)) # Fewer collectibles over time
enemyDensity = min(0.8, 0.2 + (difficultyLevel * 0.05)) # More enemies over time enemyDensity = min(0.8, 0.2 + (difficultyLevel * 0.05)) # More enemies over time
hazardDensity = min(0.4, 0.1 + (difficultyLevel * 0.03)) # More hazards over time hazardDensity = min(0.4, 0.1 + (difficultyLevel * 0.03)) # More hazards over time
objectDensity = max(0.1, 0.2 - (difficultyLevel * 0.01)) # Fewer misc objects over time objectDensity = max(0.1, 0.2 - (difficultyLevel * 0.01)) # Fewer misc objects over time
# Generate objects across the segment with buffer zones # Generate objects across the segment with buffer zones
startBufferZone = 25 # Safe zone at start of each wave startBufferZone = 25 # Safe zone at start of each wave
endBufferZone = 30 # Safe zone at end of each wave endBufferZone = 30 # Safe zone at end of each wave
currentX = startBufferZone # Start placing objects after start buffer zone currentX = startBufferZone # Start placing objects after start buffer zone
while currentX < segmentLength - endBufferZone: while currentX < segmentLength - endBufferZone:
# Determine what to place based on probability # Determine what to place based on probability
rand = random.random() rand = random.random()
if rand < collectibleDensity and self.collectibleTemplates: if rand < collectibleDensity and self.collectibleTemplates:
obj = self.place_collectible(currentX, difficultyLevel) obj = self.place_collectible(currentX, difficultyLevel)
currentX += random.randint(8, 15) currentX += random.randint(8, 15)
@@ -127,121 +127,118 @@ class SurvivalGenerator:
else: else:
currentX += random.randint(5, 15) currentX += random.randint(5, 15)
continue continue
if obj: if obj:
levelData["objects"].append(obj) levelData["objects"].append(obj)
# Add end-of-level marker at the end, within the end buffer zone # Add end-of-level marker at the end, within the end buffer zone
endMarker = { endMarker = {
"x": segmentLength - (endBufferZone // 2), # Place marker in middle of end buffer "x": segmentLength - (endBufferZone // 2), # Place marker in middle of end buffer
"y": 0, "y": 0,
"sound": "end_of_level" "sound": "end_of_level",
} }
levelData["objects"].append(endMarker) levelData["objects"].append(endMarker)
return levelData return levelData
def place_collectible(self, xPos, difficultyLevel): def place_collectible(self, xPos, difficultyLevel):
"""Place a collectible at the given position.""" """Place a collectible at the given position."""
template = random.choice(self.collectibleTemplates) template = random.choice(self.collectibleTemplates)
obj = copy.deepcopy(template) obj = copy.deepcopy(template)
# Handle x_range vs single x # Handle x_range vs single x
if 'x_range' in obj: if "x_range" in obj:
rangeSize = obj['x_range'][1] - obj['x_range'][0] rangeSize = obj["x_range"][1] - obj["x_range"][0]
obj['x_range'] = [xPos, xPos + rangeSize] obj["x_range"] = [xPos, xPos + rangeSize]
else: else:
obj['x'] = xPos obj["x"] = xPos
return obj return obj
def place_enemy(self, xPos, difficultyLevel): def place_enemy(self, xPos, difficultyLevel):
"""Place an enemy at the given position with scaled difficulty.""" """Place an enemy at the given position with scaled difficulty."""
# Filter out boss enemies for early waves # Filter out boss enemies for early waves
bossEnemies = ['witch', 'boogie_man', 'revenant', 'ghost', 'headless_horseman'] bossEnemies = ["witch", "boogie_man", "revenant", "ghost", "headless_horseman"]
if difficultyLevel < 3: # Waves 1-2: no bosses if difficultyLevel < 3: # Waves 1-2: no bosses
availableEnemies = [e for e in self.enemyTemplates availableEnemies = [e for e in self.enemyTemplates if e.get("enemy_type") not in bossEnemies]
if e.get('enemy_type') not in bossEnemies]
elif difficultyLevel < 5: # Waves 3-4: exclude the hardest bosses elif difficultyLevel < 5: # Waves 3-4: exclude the hardest bosses
hardestBosses = ['revenant', 'ghost', 'headless_horseman'] hardestBosses = ["revenant", "ghost", "headless_horseman"]
availableEnemies = [e for e in self.enemyTemplates availableEnemies = [e for e in self.enemyTemplates if e.get("enemy_type") not in hardestBosses]
if e.get('enemy_type') not in hardestBosses]
else: # Wave 5+: all enemies allowed else: # Wave 5+: all enemies allowed
availableEnemies = self.enemyTemplates availableEnemies = self.enemyTemplates
# Fallback to all enemies if filtering removed everything # Fallback to all enemies if filtering removed everything
if not availableEnemies: if not availableEnemies:
availableEnemies = self.enemyTemplates availableEnemies = self.enemyTemplates
template = random.choice(availableEnemies) template = random.choice(availableEnemies)
obj = copy.deepcopy(template) obj = copy.deepcopy(template)
# Dynamic health scaling: random between wave/2 and wave # Dynamic health scaling: random between wave/2 and wave
minHealth = max(1, difficultyLevel // 2) minHealth = max(1, difficultyLevel // 2)
maxHealth = max(1, difficultyLevel) maxHealth = max(1, difficultyLevel)
obj['health'] = random.randint(minHealth, maxHealth) obj["health"] = random.randint(minHealth, maxHealth)
# Damage scaling (keep existing logic) # Damage scaling (keep existing logic)
damageMultiplier = 1 + (difficultyLevel * 0.1) damageMultiplier = 1 + (difficultyLevel * 0.1)
obj['damage'] = max(1, int(obj.get('damage', 1) * damageMultiplier)) obj["damage"] = max(1, int(obj.get("damage", 1) * damageMultiplier))
# Set all enemies to hunter mode for survival # Set all enemies to hunter mode for survival
obj['behavior'] = 'hunter' obj["behavior"] = "hunter"
# Progressive turn rate reduction: start at 6, decrease to 1 # Progressive turn rate reduction: start at 6, decrease to 1
obj['turn_rate'] = max(1, 7 - difficultyLevel) obj["turn_rate"] = max(1, 7 - difficultyLevel)
# Handle x_range vs single x # Handle x_range vs single x
if 'x_range' in obj: if "x_range" in obj:
rangeSize = obj['x_range'][1] - obj['x_range'][0] rangeSize = obj["x_range"][1] - obj["x_range"][0]
obj['x_range'] = [xPos, xPos + rangeSize] obj["x_range"] = [xPos, xPos + rangeSize]
else: else:
obj['x'] = xPos obj["x"] = xPos
return obj return obj
def place_hazard(self, xPos, difficultyLevel): def place_hazard(self, xPos, difficultyLevel):
"""Place a hazard at the given position with scaled difficulty.""" """Place a hazard at the given position with scaled difficulty."""
template = random.choice(self.hazardTemplates) template = random.choice(self.hazardTemplates)
obj = copy.deepcopy(template) obj = copy.deepcopy(template)
# Scale hazard difficulty # Scale hazard difficulty
if obj.get('type') == 'skull_storm': if obj.get("type") == "skull_storm":
obj['damage'] = max(1, int(obj.get('damage', 1) * (1 + difficultyLevel * 0.1))) obj["damage"] = max(1, int(obj.get("damage", 1) * (1 + difficultyLevel * 0.1)))
obj['maximum_skulls'] = min(6, obj.get('maximum_skulls', 2) + (difficultyLevel // 3)) obj["maximum_skulls"] = min(6, obj.get("maximum_skulls", 2) + (difficultyLevel // 3))
elif obj.get('type') == 'catapult': elif obj.get("type") == "catapult":
obj['fire_interval'] = max(1000, obj.get('fire_interval', 4000) - (difficultyLevel * 100)) obj["fire_interval"] = max(1000, obj.get("fire_interval", 4000) - (difficultyLevel * 100))
# Handle x_range vs single x # Handle x_range vs single x
if 'x_range' in obj: if "x_range" in obj:
rangeSize = obj['x_range'][1] - obj['x_range'][0] rangeSize = obj["x_range"][1] - obj["x_range"][0]
obj['x_range'] = [xPos, xPos + rangeSize] obj["x_range"] = [xPos, xPos + rangeSize]
else: else:
obj['x'] = xPos obj["x"] = xPos
return obj return obj
def place_object(self, xPos, difficultyLevel): def place_object(self, xPos, difficultyLevel):
"""Place a misc object at the given position.""" """Place a misc object at the given position."""
template = random.choice(self.objectTemplates) template = random.choice(self.objectTemplates)
obj = copy.deepcopy(template) obj = copy.deepcopy(template)
# Handle graves - increase zombie spawn chance with difficulty
if obj.get('type') == 'grave':
baseChance = obj.get('zombie_spawn_chance', 0)
obj['zombie_spawn_chance'] = min(50, baseChance + (difficultyLevel * 2))
# Handle coffins - make all items random in survival mode
if obj.get('type') == 'coffin':
obj['item'] = 'random' # Override any specified item
# Handle x_range vs single x
if 'x_range' in obj:
rangeSize = obj['x_range'][1] - obj['x_range'][0]
obj['x_range'] = [xPos, xPos + rangeSize]
else:
obj['x'] = xPos
return obj
# Handle graves - increase zombie spawn chance with difficulty
if obj.get("type") == "grave":
baseChance = obj.get("zombie_spawn_chance", 0)
obj["zombie_spawn_chance"] = min(50, baseChance + (difficultyLevel * 2))
# Handle coffins - make all items random in survival mode
if obj.get("type") == "coffin":
obj["item"] = "random" # Override any specified item
# Handle x_range vs single x
if "x_range" in obj:
rangeSize = obj["x_range"][1] - obj["x_range"][0]
obj["x_range"] = [xPos, xPos + rangeSize]
else:
obj["x"] = xPos
return obj

View File

@@ -1,7 +1,19 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
class Weapon: class Weapon:
def __init__(self, name, damage, range, attackSound, hitSound, cooldown=500, attackDuration=200, speedBonus=1.0, jumpDurationBonus=1.0): def __init__(
self,
name,
damage,
range,
attackSound,
hitSound,
cooldown=500,
attackDuration=200,
speedBonus=1.0,
jumpDurationBonus=1.0,
):
self.name = name self.name = name
self.damage = damage self.damage = damage
self.range = range # Range in tiles self.range = range # Range in tiles
@@ -24,7 +36,7 @@ class Weapon:
attackSound="player_nunchuck_attack", attackSound="player_nunchuck_attack",
hitSound="player_nunchuck_hit", hitSound="player_nunchuck_hit",
cooldown=250, cooldown=250,
attackDuration=100 attackDuration=100,
) )
@classmethod @classmethod
@@ -38,8 +50,8 @@ class Weapon:
hitSound="player_broom_hit", hitSound="player_broom_hit",
cooldown=500, cooldown=500,
attackDuration=200, attackDuration=200,
speedBonus=1.17, # 17% speed bonus when wielding the broom speedBonus=1.17, # 17% speed bonus when wielding the broom
jumpDurationBonus=1.25 # 25% longer jump duration for better traversal jumpDurationBonus=1.25, # 25% longer jump duration for better traversal
) )
def can_attack(self, currentTime): def can_attack(self, currentTime):
@@ -73,5 +85,3 @@ class Weapon:
self.hitEnemies.add(enemy) self.hitEnemies.add(enemy)
return True return True
return False return False

View File

@@ -1,12 +1,21 @@
# -*- mode: python ; coding: utf-8 -*- # -*- mode: python ; coding: utf-8 -*-
import os
# Collect level directories dynamically
level_dirs = []
levels_path = 'levels'
if os.path.exists(levels_path):
for item in os.listdir(levels_path):
item_path = os.path.join(levels_path, item)
if os.path.isdir(item_path):
level_dirs.append((item_path, item_path))
a = Analysis( a = Analysis(
['wicked_quest.py'], ['wicked_quest.py'],
pathex=[], pathex=[],
binaries=[], binaries=[],
datas=[ datas=level_dirs + [
('levels', 'levels'),
('sounds', 'sounds'), ('sounds', 'sounds'),
('libstormgames', 'libstormgames'), ('libstormgames', 'libstormgames'),
], ],