diff --git a/src/__init__.py b/src/__init__.py index 139597f..e69de29 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -1,2 +0,0 @@ - - diff --git a/src/catapult.py b/src/catapult.py index 8966db1..ab25efb 100644 --- a/src/catapult.py +++ b/src/catapult.py @@ -14,7 +14,7 @@ class Pumpkin: self.isActive = True self.damage = playerMaxHealth // 2 # Half of player's max health 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): """Update pumpkin position and sound""" @@ -39,7 +39,7 @@ class Pumpkin: # Calculate volume and pan for splat sound based on final position volume, left, right = calculate_volume_and_pan(playerX, self.x) 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): """Check if pumpkin hits player""" @@ -58,7 +58,9 @@ class Pumpkin: class Catapult(Object): def __init__(self, x, y, sounds, direction=1, fireInterval=5000, firingRange=20): super().__init__( - x, y, "catapult", + x, + y, + "catapult", isStatic=True, isCollectible=False, ) @@ -77,18 +79,14 @@ class Catapult(Object): self.lastFireTime = currentTime # 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 isHigh = random.choice([True, False]) fireDirection = 1 if player.xPos > self.xPos else -1 # Store pumpkin data for later creation - self.pendingPumpkin = { - 'isHigh': isHigh, - 'direction': fireDirection, - 'playerMaxHealth': player.get_max_health() - } + self.pendingPumpkin = {"isHigh": isHigh, "direction": fireDirection, "playerMaxHealth": player.get_max_health()} # Set when to actually launch the pumpkin self.pumpkinLaunchTime = currentTime + self.launchDelay @@ -116,9 +114,9 @@ class Catapult(Object): # Create and fire the pending pumpkin pumpkin = Pumpkin( self.xPos, - self.pendingPumpkin['isHigh'], - self.pendingPumpkin['direction'], - self.pendingPumpkin['playerMaxHealth'] + self.pendingPumpkin["isHigh"], + self.pendingPumpkin["direction"], + self.pendingPumpkin["playerMaxHealth"], ) self.activePumpkins.append(pumpkin) self.pendingPumpkin = None @@ -140,6 +138,4 @@ class Catapult(Object): pumpkin.isActive = False self.activePumpkins.remove(pumpkin) if not player.isInvincible: - self.sounds['player_takes_damage'].play() - - + self.sounds["player_takes_damage"].play() diff --git a/src/coffin.py b/src/coffin.py index 08ebe74..a7061df 100644 --- a/src/coffin.py +++ b/src/coffin.py @@ -9,12 +9,7 @@ from src.powerup import PowerUp class CoffinObject(Object): def __init__(self, x, y, sounds, level, item="random"): - super().__init__( - x, y, "coffin", - isStatic=True, - isCollectible=False, - isHazard=False - ) + super().__init__(x, y, "coffin", isStatic=True, isCollectible=False, isHazard=False) self.sounds = sounds self.level = level self.isBroken = False @@ -25,9 +20,9 @@ class CoffinObject(Object): """Handle being hit by the player's weapon""" if not self.isBroken: self.isBroken = True - play_sound(self.sounds['coffin_shatter']) + play_sound(self.sounds["coffin_shatter"]) 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 if self.channel: @@ -54,16 +49,8 @@ class CoffinObject(Object): drop_x = self.xPos + (direction * drop_distance) self.dropped_item = PowerUp( - drop_x, - self.yPos, - item_type, - self.sounds, - direction, - self.level.leftBoundary, - self.level.rightBoundary + drop_x, self.yPos, item_type, self.sounds, direction, self.level.leftBoundary, self.level.rightBoundary ) return True return False - - diff --git a/src/die_monster_die.py b/src/die_monster_die.py index 0d4dea4..9fe54de 100644 --- a/src/die_monster_die.py +++ b/src/die_monster_die.py @@ -11,31 +11,25 @@ import pygame from libstormgames import * from src.object import Object + class DeathSound(Object): """Special object that plays enemy death sounds at fixed positions and self-removes.""" - + def __init__(self, x, y, enemyType, sounds): # Initialize as a static object with the death sound name deathSoundName = f"{enemyType}_dies" - super().__init__( - x, - y, - deathSoundName, - isStatic=True, - isCollectible=False, - isHazard=False - ) - + super().__init__(x, y, deathSoundName, isStatic=True, isCollectible=False, isHazard=False) + self.sounds = sounds self.enemyType = enemyType self.startTime = pygame.time.get_ticks() - + # Get the duration of the death sound if it exists if deathSoundName in sounds: self.soundDuration = sounds[deathSoundName].get_length() * 1000 # Convert to milliseconds else: self.soundDuration = 1000 # Default 1 second if sound doesn't exist - + def update(self, currentTime): """Check if sound has finished playing and mark for removal.""" if currentTime - self.startTime >= self.soundDuration: diff --git a/src/enemy.py b/src/enemy.py index b129b91..0093d88 100644 --- a/src/enemy.py +++ b/src/enemy.py @@ -6,25 +6,20 @@ from libstormgames import * from src.object import Object from src.powerup import PowerUp + class Enemy(Object): def __init__(self, xRange, y, enemyType, sounds, level, **kwargs): # Track when critters should start hunting self.hunting = False # Initialize base object properties - super().__init__( - xRange, - y, - f"{enemyType}", # Base sound - isStatic=False, - isHazard=True - ) + super().__init__(xRange, y, f"{enemyType}", isStatic=False, isHazard=True) # Base sound # Enemy specific properties self.enemyType = enemyType self.level = level - self.health = kwargs.get('health', 5) # Default 5 HP - self.damage = kwargs.get('damage', 1) # Default 1 damage - self.attackRange = kwargs.get('attack_range', 1) # Default 1 tile range + self.health = kwargs.get("health", 5) # Default 5 HP + self.damage = kwargs.get("damage", 1) # Default 1 damage + self.attackRange = kwargs.get("attack_range", 1) # Default 1 tile range self.sounds = sounds # Store reference to game sounds # Movement and behavior properties @@ -37,25 +32,25 @@ class Enemy(Object): self._currentX = self.xRange[0] # Initialize current position # Add spawn configuration - self.canSpawn = kwargs.get('can_spawn', False) + self.canSpawn = kwargs.get("can_spawn", False) if self.canSpawn: - self.spawnCooldown = kwargs.get('spawn_cooldown', 2000) - self.spawnChance = kwargs.get('spawn_chance', 25) - self.spawnType = kwargs.get('spawn_type', 'zombie') # Default to zombie for backward compatibility - self.spawnDistance = kwargs.get('spawn_distance', 5) + self.spawnCooldown = kwargs.get("spawn_cooldown", 2000) + self.spawnChance = kwargs.get("spawn_chance", 25) + self.spawnType = kwargs.get("spawn_type", "zombie") # Default to zombie for backward compatibility + self.spawnDistance = kwargs.get("spawn_distance", 5) self.lastSpawnTime = 0 # Attack pattern configuration - self.attackPattern = kwargs.get('attack_pattern', {'type': 'patrol'}) - self.turnThreshold = self.attackPattern.get('turn_threshold', 5) + self.attackPattern = kwargs.get("attack_pattern", {"type": "patrol"}) + self.turnThreshold = self.attackPattern.get("turn_threshold", 5) # Initialize vulnerability system - self.hasVulnerabilitySystem = kwargs.get('has_vulnerability', False) + self.hasVulnerabilitySystem = kwargs.get("has_vulnerability", False) if self.hasVulnerabilitySystem: self.isVulnerable = False # Start invulnerable self.vulnerabilityTimer = pygame.time.get_ticks() - self.vulnerabilityDuration = kwargs.get('vulnerability_duration', 2000) - self.invulnerabilityDuration = kwargs.get('invulnerability_duration', 5000) + self.vulnerabilityDuration = kwargs.get("vulnerability_duration", 2000) + self.invulnerabilityDuration = kwargs.get("invulnerability_duration", 5000) 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) else: @@ -68,19 +63,17 @@ class Enemy(Object): self.health = 1 # Easy to kill self.attackCooldown = 1500 # Slower attack rate elif enemyType == "spider": - speedMultiplier = kwargs.get('speed_multiplier', 2.0) + speedMultiplier = kwargs.get("speed_multiplier", 2.0) 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 - @property def xPos(self): """Current x position""" return self._currentX @xPos.setter - def xPos(self, value): """Set current x position""" self._currentX = value @@ -111,7 +104,9 @@ class Enemy(Object): self.channel = obj_update(self.channel, player.xPos, self.xPos) # 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.vulnerabilityTimer = currentTime @@ -126,8 +121,7 @@ class Enemy(Object): self.hunting = True # Handle movement based on enemy type and pattern - if (self.enemyType == "zombie" or - (self.attackPattern['type'] == 'hunter' and self.hunting)): + if self.enemyType == "zombie" or (self.attackPattern["type"] == "hunter" and self.hunting): distanceToPlayer = player.xPos - self.xPos @@ -167,9 +161,9 @@ class Enemy(Object): spawnX = max(self.level.leftBoundary, min(spawnX, self.level.rightBoundary)) # 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 - + # Create new enemy of specified type spawned = Enemy( [spawnX, spawnX], # Single point range for spawn @@ -180,8 +174,8 @@ class Enemy(Object): health=4, # Default health for spawned enemies damage=2, # Default damage for spawned enemies attack_range=1, # Default range for spawned enemies - attack_pattern={'type': behavior}, - turn_rate=turn_rate + attack_pattern={"type": behavior}, + turn_rate=turn_rate, ) # Add to level's enemies @@ -221,7 +215,8 @@ class Enemy(Object): def attack(self, currentTime, player): """Perform attack on player""" - if player.isInvincible: return + if player.isInvincible: + return self.lastAttackTime = currentTime # Play attack sound attackSound = f"{self.enemyType}_attack" @@ -229,7 +224,7 @@ class Enemy(Object): self.sounds[attackSound].play() # Deal damage to player 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): """Handle enemy taking damage""" @@ -259,6 +254,7 @@ class Enemy(Object): # Create a DeathSound object to play death sound at fixed position from src.die_monster_die import DeathSound + deathSoundObj = DeathSound(self.xPos, self.yPos, self.enemyType, self.sounds) self.level.objects.append(deathSoundObj) @@ -276,17 +272,9 @@ class Enemy(Object): dropX = self.xPos + (direction * dropDistance) droppedItem = PowerUp( - dropX, - self.yPos, - itemType, - self.sounds, - direction, - self.level.leftBoundary, - self.level.rightBoundary + dropX, self.yPos, itemType, self.sounds, direction, self.level.leftBoundary, self.level.rightBoundary ) self.level.bouncing_items.append(droppedItem) # Update stats - self.level.player.stats.update_stat('Enemies killed', 1) - - + self.level.player.stats.update_stat("Enemies killed", 1) diff --git a/src/game_selection.py b/src/game_selection.py index 5fa829d..99f9bd2 100644 --- a/src/game_selection.py +++ b/src/game_selection.py @@ -2,11 +2,11 @@ import os import sys -import time -import pygame from os.path import isdir, join + from libstormgames import speak, instruction_menu + def get_available_games(): """Get list of available game directories in levels folder. @@ -15,18 +15,19 @@ def get_available_games(): """ try: # Handle PyInstaller path issues - if hasattr(sys, '_MEIPASS'): + if hasattr(sys, "_MEIPASS"): # Running as PyInstaller executable base_path = sys._MEIPASS else: # Running as script base_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - + 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: return [] + def selection_menu(sounds, *options): """Display level selection menu using instruction_menu. @@ -39,6 +40,7 @@ def selection_menu(sounds, *options): """ return instruction_menu(sounds, "Select an adventure", *options) + def select_game(sounds): """Display game selection menu and return chosen game. @@ -48,49 +50,48 @@ def select_game(sounds): Returns: 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!") return None # 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: return None # Convert display name back to directory name if needed - gameDir = choice.replace(" ", "_") - if gameDir not in availableGames: - gameDir = choice # Use original if conversion doesn't match + game_dir = choice.replace(" ", "_") + if game_dir not in available_games: + 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. Args: - gameDir (str): Game directory name - levelNum (int): Level number + game_dir (str): Game directory name + level_num (int): Level number Returns: str: Full path to level JSON file """ - if gameDir is None: - raise ValueError("gameDir cannot be None") - + if game_dir is None: + raise ValueError("game_dir cannot be None") + # Handle PyInstaller path issues - if hasattr(sys, '_MEIPASS'): + if hasattr(sys, "_MEIPASS"): # Running as PyInstaller executable base_path = sys._MEIPASS else: # Running as script 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 - - diff --git a/src/grasping_hands.py b/src/grasping_hands.py index 3b8a753..f36d9ae 100644 --- a/src/grasping_hands.py +++ b/src/grasping_hands.py @@ -4,6 +4,7 @@ import pygame from libstormgames import * from src.object import Object + class GraspingHands(Object): """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 isStatic=True, isCollectible=False, - isHazard=True + isHazard=True, ) self.sounds = sounds self.delay = delay # Delay in milliseconds before ground starts crumbling @@ -51,7 +52,7 @@ class GraspingHands(Object): self.isReset = False # 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.") def reset(self): @@ -67,7 +68,7 @@ class GraspingHands(Object): self.crumbleChannel = None # Play the end sound - play_sound(self.sounds['grasping_hands_end']) + play_sound(self.sounds["grasping_hands_end"]) def update(self, currentTime, player): """Update the grasping hands trap state""" @@ -92,7 +93,7 @@ class GraspingHands(Object): # Manage the looping positional audio for the crumbling ground if self.crumbleChannel is None or not self.crumbleChannel.get_busy(): # 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: # Update the sound position self.crumbleChannel = obj_update(self.crumbleChannel, player.xPos, self.crumblePosition) @@ -100,8 +101,9 @@ class GraspingHands(Object): # Check if player is caught by crumbling playerCaught = False if not player.isJumping: - if (self.crumbleDirection > 0 and player.xPos <= self.crumblePosition) or \ - (self.crumbleDirection < 0 and player.xPos >= self.crumblePosition): + if (self.crumbleDirection > 0 and player.xPos <= self.crumblePosition) or ( + self.crumbleDirection < 0 and player.xPos >= self.crumblePosition + ): playerCaught = True if playerCaught: @@ -122,8 +124,6 @@ class GraspingHands(Object): def __del__(self): """Cleanup 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) self.crumbleChannel = None - - diff --git a/src/grave.py b/src/grave.py index 105bd66..07270b8 100644 --- a/src/grave.py +++ b/src/grave.py @@ -8,13 +8,9 @@ from src.powerup import PowerUp class GraveObject(Object): def __init__(self, x, y, sounds, item=None, zombieSpawnChance=0): super().__init__( - x, y, "grave", - isStatic=True, - isCollectible=False, - isHazard=True, - zombieSpawnChance=zombieSpawnChance + x, y, "grave", 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.isFilled = False # Track if grave has been filled with shovel self.sounds = sounds @@ -30,8 +26,12 @@ class GraveObject(Object): return False # Collect the item if player is ducking, walking (not running), and wielding shovel - if (player.isDucking and not player.isRunning and - player.currentWeapon and player.currentWeapon.name == "rusty_shovel"): + if ( + player.isDucking + and not player.isRunning + and player.currentWeapon + and player.currentWeapon.name == "rusty_shovel" + ): self.isCollected = True # Mark as collected when collection succeeds return True @@ -39,21 +39,25 @@ class GraveObject(Object): def can_fill_grave(self, player): """Check if grave can be filled with shovel. - + Returns: bool: True if grave can be filled, False otherwise """ # Can only fill empty graves (no item) that haven't been filled yet if self.graveItem or self.isFilled: return False - + # Must be ducking, walking (not running), and wielding shovel - return (player.isDucking and not player.isRunning and - player.currentWeapon and player.currentWeapon.name == "rusty_shovel") + return ( + player.isDucking + and not player.isRunning + and player.currentWeapon + and player.currentWeapon.name == "rusty_shovel" + ) def fill_grave(self, player): """Fill the grave with dirt using shovel. - + Returns: bool: True if grave was filled successfully """ @@ -62,5 +66,3 @@ class GraveObject(Object): self.isHazard = False # No longer a hazard once filled return True return False - - diff --git a/src/item_types.py b/src/item_types.py index ca7fa0e..1e3bc8a 100644 --- a/src/item_types.py +++ b/src/item_types.py @@ -6,6 +6,7 @@ from enum import Enum, auto class ItemType(Enum): """Defines available item types and their properties""" + GUTS = auto() HAND_OF_GLORY = auto() JACK_O_LANTERN = auto() @@ -13,14 +14,12 @@ class ItemType(Enum): CAULDRON = auto() WITCH_BROOM = auto() + class ItemProperties: """Manages item properties and availability""" # Items that can appear in random drops - RANDOM_ELIGIBLE = { - ItemType.HAND_OF_GLORY: "hand_of_glory", - ItemType.JACK_O_LANTERN: "jack_o_lantern" - } + RANDOM_ELIGIBLE = {ItemType.HAND_OF_GLORY: "hand_of_glory", ItemType.JACK_O_LANTERN: "jack_o_lantern"} # All possible items (including special ones) ALL_ITEMS = { @@ -29,7 +28,7 @@ class ItemProperties: ItemType.JACK_O_LANTERN: "jack_o_lantern", ItemType.EXTRA_LIFE: "extra_life", ItemType.CAULDRON: "cauldron", - ItemType.WITCH_BROOM: "witch_broom" + ItemType.WITCH_BROOM: "witch_broom", } @staticmethod @@ -55,5 +54,3 @@ class ItemProperties: if name == item_name: return item_type return None - - diff --git a/src/level.py b/src/level.py index dd3f96f..e179ce1 100644 --- a/src/level.py +++ b/src/level.py @@ -25,7 +25,7 @@ class Level: self.projectiles = [] # Track active projectiles self.player = player 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.leftBoundary = levelData["boundaries"]["left"] @@ -42,34 +42,34 @@ class Level: self.player.set_footstep_sound(self.footstepSound) # 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']}. " if self.isLocked: levelIntro += "This is a boss level. You must defeat all enemies before you can advance. " - levelIntro += levelData['description'] + levelIntro += levelData["description"] messagebox(levelIntro) # Handle level music try: pygame.mixer.music.stop() if "ambience" in levelData: - ambientFile = levelData['ambience'] - + ambientFile = levelData["ambience"] + # Build list of paths to try (pack-specific first, then generic) ambiencePaths = [] if self.levelPackName: ambiencePaths.append(f"sounds/{self.levelPackName}/ambience/{ambientFile}") ambiencePaths.append(f"sounds/ambience/{ambientFile}") - + # Try each path until one works for ambiencePath in ambiencePaths: try: pygame.mixer.music.load(ambiencePath) pygame.mixer.music.play(-1) # Loop indefinitely break - except: + except Exception: continue - except: + except Exception: pass # Create end of level object at right boundary @@ -79,7 +79,7 @@ class Level: "end_of_level", isStatic=True, isCollectible=False, - isHazard=False + isHazard=False, ) self.objects.append(endLevel) @@ -98,7 +98,7 @@ class Level: obj["y"], self.sounds, fireInterval=obj.get("fireInterval", 5000), - firingRange=obj.get("range", 20) + firingRange=obj.get("range", 20), ) self.objects.append(catapult) # Check if this is grasping hands @@ -108,7 +108,7 @@ class Level: obj["y"], self.sounds, 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) # Check if this is a grave @@ -118,7 +118,7 @@ class Level: obj["y"], self.sounds, item=obj.get("item", None), - zombieSpawnChance=obj.get("zombie_spawn_chance", 0) + zombieSpawnChance=obj.get("zombie_spawn_chance", 0), ) self.objects.append(grave) # Check if this is a skull storm @@ -130,7 +130,7 @@ class Level: obj.get("damage", 5), obj.get("maximum_skulls", 3), obj.get("frequency", {}).get("min", 2), - obj.get("frequency", {}).get("max", 5) + obj.get("frequency", {}).get("max", 5), ) self.objects.append(skullStorm) # Check if this is a coffin @@ -140,7 +140,7 @@ class Level: obj["y"], self.sounds, 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) # Check if this is a spider web @@ -148,8 +148,7 @@ class Level: # Check distance from graves isValidPosition = True for existingObj in self.objects: - if (existingObj.soundName == "grave" and - not hasattr(existingObj, 'graveItem')): + if existingObj.soundName == "grave" and not hasattr(existingObj, "graveItem"): distance = abs(obj["x"] - existingObj.xPos) if distance < 3: isValidPosition = False @@ -176,7 +175,7 @@ class Level: damage=obj.get("damage", 1), attack_range=obj.get("attack_range", 1), 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), spawn_type=obj.get("spawn_type", "zombie"), spawn_cooldown=obj.get("spawn_cooldown", 2000), @@ -185,7 +184,7 @@ class Level: has_vulnerability=obj.get("has_vulnerability", False), is_vulnerable=obj.get("is_vulnerable", False), 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) else: @@ -196,13 +195,13 @@ class Level: isStatic=obj.get("static", True), isCollectible=obj.get("collectible", False), isHazard=obj.get("hazard", False), - zombieSpawnChance=obj.get("zombieSpawnChance", 0) + zombieSpawnChance=obj.get("zombieSpawnChance", 0), ) self.objects.append(gameObject) enemyCount = len(self.enemies) - coffinCount = sum(1 for obj in self.objects if hasattr(obj, 'isBroken')) - player.stats.update_stat('Enemies remaining', enemyCount) - player.stats.update_stat('Coffins remaining', coffinCount) + coffinCount = sum(1 for obj in self.objects if hasattr(obj, "isBroken")) + player.stats.update_stat("Enemies remaining", enemyCount) + player.stats.update_stat("Coffins remaining", coffinCount) def update_audio(self): """Update all audio and entity state.""" @@ -214,9 +213,7 @@ class Level: continue # Check for potential zombie spawn from graves - if (obj.soundName == "grave" and - obj.zombieSpawnChance > 0 and - not obj.hasSpawned): + if obj.soundName == "grave" and obj.zombieSpawnChance > 0 and not obj.hasSpawned: distance = abs(self.player.xPos - obj.xPos) if distance < 6: # Within 6 tiles @@ -226,9 +223,9 @@ class Level: roll = random.randint(1, 100) if roll <= obj.zombieSpawnChance: # 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 - + zombie = Enemy( [obj.xPos, obj.xPos], obj.yPos, @@ -238,8 +235,8 @@ class Level: health=3, damage=10, attack_range=1, - attack_pattern={'type': behavior}, - turn_rate=turn_rate + attack_pattern={"type": behavior}, + turn_rate=turn_rate, ) self.enemies.append(zombie) speak("A zombie emerges from the grave!") @@ -283,13 +280,14 @@ class Level: caught = obj.update(currentTime, self.player) if caught: return # Stop if player is dead - + # Update death sound objects from src.die_monster_die import DeathSound + for obj in self.objects: if isinstance(obj, DeathSound): obj.update(currentTime) - + # Clean up inactive objects (including finished death sounds) self.objects = [obj for obj in self.objects if obj.isActive] @@ -303,7 +301,7 @@ class Level: # Check for item collection 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) self.levelScore += 1000 # All items collected points awarded item.isActive = False @@ -325,11 +323,13 @@ class Level: # Check for coffin hits for obj in self.objects: - if hasattr(obj, 'isBroken'): # Check if it's a coffin without using isinstance - if (not obj.isBroken and - obj.xPos >= attackRange[0] and - obj.xPos <= attackRange[1] and - self.player.isJumping): # Must be jumping to hit floating coffins + if hasattr(obj, "isBroken"): # Check if it's a coffin without using isinstance + if ( + not obj.isBroken + and obj.xPos >= attackRange[0] + and obj.xPos <= attackRange[1] + and self.player.isJumping + ): # Must be jumping to hit floating coffins if obj.hit(self.player.xPos): self.bouncing_items.append(obj.dropped_item) @@ -345,7 +345,7 @@ class Level: health=8, damage=8, attack_range=1, - speed_multiplier=2.0 + speed_multiplier=2.0, ) self.enemies.append(spider) @@ -364,15 +364,21 @@ class Level: continue # 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) currentTime = pygame.time.get_ticks() - if (distance <= 2 and not self.player.isJumping and not self.player.isInvincible - and currentTime - self.lastWarningTime >= self.warningInterval): + if ( + 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: - play_sound(self.sounds['_edge']) + play_sound(self.sounds["_edge"]) else: - play_sound(self.sounds['edge']) + play_sound(self.sounds["edge"]) self.lastWarningTime = currentTime 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: currentPos = round(self.player.xPos) 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) 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": self.player._coins += 1 self.player.add_save_bone_dust(1) # Add to save bone dust counter too 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: # Only heal if below max health if self.player.get_health() < self.player.get_max_health(): - self.player.set_health(min( - self.player.get_health() + 1, - self.player.get_max_health() - )) + self.player.set_health(min(self.player.get_health() + 1, self.player.get_max_health())) if self.player._coins % 100 == 0: # 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._lives += 1 self.levelScore += 1000 - play_sound(self.sounds['get_extra_life']) + play_sound(self.sounds["get_extra_life"]) else: # In survival mode, reset coin counter but give bonus score instead self.player._coins = 0 self.levelScore += 2000 # Double score bonus instead of extra life 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 # Handle spiderweb - this should trigger for both walking and jumping if not ducking if obj.soundName == "spiderweb" and not self.player.isDucking: # Create and apply web effect webEffect = PowerUp( - obj.xPos, - obj.yPos, - 'spiderweb', - self.sounds, - 0 # No direction needed since it's just for effect + obj.xPos, obj.yPos, "spiderweb", self.sounds, 0 # No direction needed since it's just for effect ) 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) # Deactivate web @@ -442,12 +443,13 @@ class Level: if can_collect: # Successfully collected item while ducking with shovel - play_sound(self.sounds[f'get_{obj.graveItem}']) - play_sound(self.sounds.get('fill_in_grave', 'shovel_dig')) # Also play fill sound - self.player.stats.update_stat('Items collected', 1) + play_sound(self.sounds[f"get_{obj.graveItem}"]) + play_sound(self.sounds.get("fill_in_grave", "shovel_dig")) # Also play fill sound + self.player.stats.update_stat("Items collected", 1) # Create PowerUp to handle the item effect - item = PowerUp(obj.xPos, obj.yPos, obj.graveItem, self.sounds, 1, - self.leftBoundary, self.rightBoundary) + item = PowerUp( + obj.xPos, obj.yPos, obj.graveItem, self.sounds, 1, self.leftBoundary, self.rightBoundary + ) item.apply_effect(self.player, self) # Stop grave's current audio channel if obj.channel: @@ -459,8 +461,10 @@ class Level: continue elif can_fill and obj.fill_grave(self.player): # 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 - self.player.stats.update_stat('Graves filled', 1) + play_sound( + 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 if obj.channel: obj_stop(obj.channel) @@ -488,14 +492,14 @@ class Level: # If level is locked, check for remaining enemies if self.isLocked and any(enemy.isActive for enemy in self.enemies): speak("You must defeat all enemies before proceeding!") - play_sound(self.sounds['locked']) + play_sound(self.sounds["locked"]) # Push player back a bit self.player.xPos -= 5 return False # Level complete pygame.mixer.stop() - play_sound(self.sounds['end_of_level']) + play_sound(self.sounds["end_of_level"]) self.levelScore += 10000 # Actually update the scoreboard with level completion self.player.scoreboard.increase_score(self.levelScore) @@ -518,7 +522,7 @@ class Level: # Calculate volume and pan for splat sound based on final position volume, left, right = calculate_volume_and_pan(self.player.xPos, proj.x) 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 def throw_projectile(self): @@ -528,12 +532,6 @@ class Level: speak("No jack o'lanterns to throw!") return - self.projectiles.append(Projectile( - proj_info['type'], - proj_info['start_x'], - proj_info['direction'] - )) + self.projectiles.append(Projectile(proj_info["type"], proj_info["start_x"], proj_info["direction"])) # Play throw sound - play_sound(self.sounds['throw_jack_o_lantern']) - - + play_sound(self.sounds["throw_jack_o_lantern"]) diff --git a/src/object.py b/src/object.py index b99e992..531bf00 100644 --- a/src/object.py +++ b/src/object.py @@ -2,6 +2,7 @@ from libstormgames import * + class Object: 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] @@ -37,5 +38,3 @@ class Object: if self.channel: obj_stop(self.channel) self.channel = None - - diff --git a/src/pack_sound_system.py b/src/pack_sound_system.py index 0df1ea3..c150bb4 100644 --- a/src/pack_sound_system.py +++ b/src/pack_sound_system.py @@ -13,10 +13,10 @@ from libstormgames.sound import Sound class PackSoundSystem(dict): """Sound system with hierarchical pack-specific loading.""" - + def __init__(self, originalSounds, soundDir="sounds/", levelPackName=None): """Initialize pack-specific sound system. - + Args: originalSounds (dict): Original sound dictionary from initialize_gui soundDir (str): Base sound directory @@ -24,34 +24,33 @@ class PackSoundSystem(dict): """ # Initialize dict with original sounds super().__init__(originalSounds) - + self.soundDir = soundDir self.levelPackName = levelPackName - + # Load pack-specific sounds if pack name provided if levelPackName: self._load_pack_sounds() - + def _load_pack_sounds(self): """Load pack-specific sounds from sounds/[pack_name]/ directory.""" packSoundDir = os.path.join(self.soundDir, self.levelPackName) if not os.path.exists(packSoundDir): return - + try: for dirPath, _, fileNames in os.walk(packSoundDir): relPath = os.path.relpath(dirPath, packSoundDir) - + for fileName in fileNames: - if fileName.lower().endswith(('.ogg', '.wav')): + if fileName.lower().endswith((".ogg", ".wav")): fullPath = os.path.join(dirPath, fileName) baseName = os.path.splitext(fileName)[0] - + # 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 self[soundKey] = pygame.mixer.Sound(fullPath) - + except Exception as e: print(f"Error loading pack sounds: {e}") - diff --git a/src/player.py b/src/player.py index 61b9fe5..02237d9 100644 --- a/src/player.py +++ b/src/player.py @@ -58,32 +58,36 @@ class Player: self.isInvincible = False self.invincibilityStartTime = 0 self.invincibilityDuration = 10000 # 10 seconds of invincibility - + # Death state tracking (to prevent revival after death in same frame) self.diedThisFrame = False # Initialize starting weapon (rusty shovel) - self.add_weapon(Weapon( - name="rusty_shovel", - damage=2, - range=2, - attackSound="player_shovel_attack", - hitSound="player_shovel_hit", - attackDuration=200 # 200ms attack duration - )) + self.add_weapon( + Weapon( + name="rusty_shovel", + damage=2, + range=2, + attackSound="player_shovel_attack", + hitSound="player_shovel_hit", + attackDuration=200, # 200ms attack duration + ) + ) self.scoreboard = Scoreboard() def should_play_footstep(self, currentTime): """Check if it's time to play a footstep sound""" - return (self.distanceSinceLastStep >= self.get_step_distance() and - currentTime - self.lastStepTime >= self.get_step_interval()) + return ( + self.distanceSinceLastStep >= self.get_step_distance() + and currentTime - self.lastStepTime >= self.get_step_interval() + ) def duck(self): """Start ducking""" if not self.isDucking and not self.isJumping: # Can't duck while jumping self.isDucking = True - play_sound(self.sounds['duck']) + play_sound(self.sounds["duck"]) return True return False @@ -91,14 +95,14 @@ class Player: """Stop ducking state and play sound""" if self.isDucking: self.isDucking = False - play_sound(self.sounds['stand']) + play_sound(self.sounds["stand"]) def update(self, currentTime): """Update player state""" # Reset death flag at start of each frame self.diedThisFrame = False - - if hasattr(self, 'webPenaltyEndTime'): + + if hasattr(self, "webPenaltyEndTime"): if currentTime >= self.webPenaltyEndTime: self.moveSpeed *= 2 # Restore speed if self.currentWeapon: @@ -107,15 +111,17 @@ class Player: # Check invincibility status 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 - 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 current_second = int(remaining_time) 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 # Check if invincibility has expired @@ -128,7 +134,7 @@ class Player: """Activate invincibility from Hand of Glory""" self.isInvincible = True 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 def extra_life(self): @@ -156,30 +162,26 @@ class Player: return None self._jack_o_lantern_count -= 1 - return { - 'type': 'jack_o_lantern', - 'start_x': self.xPos, - 'direction': 1 if self.facingRight else -1 - } + return {"type": "jack_o_lantern", "start_x": self.xPos, "direction": 1 if self.facingRight else -1} def get_step_distance(self): """Get step distance based on current speed""" weaponBonus = self.currentWeapon.speedBonus if self.currentWeapon else 1.0 totalMultiplier = weaponBonus - + if self.isRunning or self.isJumping: totalMultiplier *= self.runMultiplier - + return self.baseStepDistance / totalMultiplier def get_step_interval(self): """Get minimum time between steps based on current speed""" weaponBonus = self.currentWeapon.speedBonus if self.currentWeapon else 1.0 totalMultiplier = weaponBonus - + if self.isRunning or self.isJumping: totalMultiplier *= self.runMultiplier - + return self.baseStepInterval / totalMultiplier def get_health(self): @@ -198,8 +200,8 @@ class Player: """Calculate current speed based on state and weapon""" baseSpeed = self.moveSpeed 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 * weaponBonus @@ -218,7 +220,7 @@ class Player: # Oops, allow healing while invincible. if self.isInvincible and value < old_health: - return + return self._health = max(0, value) # Health can't go below 0 @@ -230,10 +232,10 @@ class Player: pygame.mixer.stop() try: pygame.mixer.music.stop() - except: + except Exception: pass - cut_scene(self.sounds, 'lose_a_life') + cut_scene(self.sounds, "lose_a_life") def set_max_health(self, value): """Set max health""" @@ -279,23 +281,19 @@ class Player: def switch_to_weapon(self, weaponIndex): """Switch to weapon by index (1=shovel, 2=broom, 3=nunchucks)""" - weaponMap = { - 1: "rusty_shovel", - 2: "witch_broom", - 3: "nunchucks" - } - + weaponMap = {1: "rusty_shovel", 2: "witch_broom", 3: "nunchucks"} + targetWeaponName = weaponMap.get(weaponIndex) if not targetWeaponName: return False - + # Find the weapon in player's inventory for weapon in self.weapons: if weapon.name == targetWeaponName: self.equip_weapon(weapon) - speak(weapon.name.replace('_', ' ')) + speak(weapon.name.replace("_", " ")) return True - + # Weapon not found in inventory return False @@ -317,5 +315,3 @@ class Player: if not self.currentWeapon or not self.currentWeapon.is_attack_active(currentTime): return None return self.currentWeapon.get_attack_range(self.xPos, self.facingRight) - - diff --git a/src/powerup.py b/src/powerup.py index 9a5a893..d16168e 100644 --- a/src/powerup.py +++ b/src/powerup.py @@ -5,14 +5,10 @@ from libstormgames import * from src.object import Object from src.weapon import Weapon + class PowerUp(Object): def __init__(self, x, y, item_type, sounds, direction, left_boundary=1, right_boundary=100): - super().__init__( - x, y, item_type, - isStatic=False, - isCollectible=True, - isHazard=False - ) + super().__init__(x, y, item_type, isStatic=False, isCollectible=True, isHazard=False) self.sounds = sounds self.direction = direction self.speed = 0.049 # Base movement speed @@ -58,35 +54,32 @@ class PowerUp(Object): def apply_effect(self, player, level=None): """Apply the item's effect when collected""" - if self.item_type == 'hand_of_glory': + if self.item_type == "hand_of_glory": player.start_invincibility() - elif self.item_type == 'cauldron': + elif self.item_type == "cauldron": player.restore_health() - elif self.item_type == 'guts': + elif self.item_type == "guts": player.add_guts() - player.collectedItems.append('guts') + player.collectedItems.append("guts") self.check_for_nunchucks(player) - elif self.item_type == 'jack_o_lantern': + elif self.item_type == "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 if level and level.levelId == 999: # In survival mode, give bonus score instead level.levelScore += 2000 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: 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._coins += 5 player.add_save_bone_dust(5) # Add to save bone dust counter too if player.get_health() < player.get_max_health(): - player.set_health(min( - player.get_health() + 1, - player.get_max_health() - )) - + player.set_health(min(player.get_health() + 1, player.get_max_health())) + # Check for 100 coin bonus after adding shin bone coins if player._coins >= 100: # Only give extra lives in story mode, not survival mode (level_id 999) @@ -95,20 +88,22 @@ class PowerUp(Object): player._coins = 0 player._lives += 1 level.levelScore += 1000 - play_sound(self.sounds['get_extra_life']) + play_sound(self.sounds["get_extra_life"]) else: # In survival mode, reset coin counter but give bonus score instead player._coins = 0 level.levelScore += 2000 # Double score bonus instead of extra life 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) - elif self.item_type == 'witch_broom': + elif self.item_type == "witch_broom": broomWeapon = Weapon.create_witch_broom() player.add_weapon(broomWeapon) player.equip_weapon(broomWeapon) - elif self.item_type == 'spiderweb': + elif self.item_type == "spiderweb": # Bounce player back (happens even if invincible) player.xPos -= 3 if player.xPos > self.xPos else -3 @@ -122,7 +117,7 @@ class PowerUp(Object): player.webPenaltyEndTime = pygame.time.get_ticks() + 15000 # Tell level to spawn a spider - if hasattr(self, 'level'): + if hasattr(self, "level"): self.level.spawn_spider(self.xPos, self.yPos) # Stop movement sound when collected @@ -131,20 +126,20 @@ class PowerUp(Object): self.channel = None # Item tracking - player.stats.update_stat('Items collected', 1) + player.stats.update_stat("Items collected", 1) def check_for_nunchucks(self, player): """Check if player has materials for nunchucks and create if conditions are met""" - if (player.shinBoneCount >= 2 and - 'guts' in player.collectedItems and - not any(weapon.name == "nunchucks" for weapon in player.weapons)): + if ( + player.shinBoneCount >= 2 + and "guts" in player.collectedItems + and not any(weapon.name == "nunchucks" for weapon in player.weapons) + ): nunchucksWeapon = Weapon.create_nunchucks() player.add_weapon(nunchucksWeapon) player.equip_weapon(nunchucksWeapon) basePoints = nunchucksWeapon.damage * 1000 rangeModifier = nunchucksWeapon.range * 500 player.scoreboard.increase_score(basePoints + rangeModifier) - play_sound(self.sounds['get_nunchucks']) - player.stats.update_stat('Items collected', 1) - - + play_sound(self.sounds["get_nunchucks"]) + player.stats.update_stat("Items collected", 1) diff --git a/src/projectile.py b/src/projectile.py index e4abfd8..7907a2a 100644 --- a/src/projectile.py +++ b/src/projectile.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- + class Projectile: def __init__(self, projectile_type, start_x, direction): self.type = projectile_type @@ -29,5 +30,3 @@ class Projectile: """Handle hitting an enemy""" enemy.take_damage(self.damage) self.isActive = False # Projectile is destroyed on hit - - diff --git a/src/save_manager.py b/src/save_manager.py index 3110e53..8735d57 100644 --- a/src/save_manager.py +++ b/src/save_manager.py @@ -11,8 +11,8 @@ class SaveManager: def __init__(self): """Initialize save manager with XDG-compliant save directory""" # Use XDG_CONFIG_HOME or default to ~/.config - config_home = os.environ.get('XDG_CONFIG_HOME', os.path.expanduser('~/.config')) - self.save_dir = Path(config_home) / 'storm-games' / 'wicked-quest' + 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.mkdir(parents=True, exist_ok=True) self.max_saves = 10 @@ -24,7 +24,7 @@ class SaveManager: # Validate required parameters if current_game is None: return False, "No game selected to save" - + if current_level is None: return False, "No current level to save" @@ -34,30 +34,30 @@ class SaveManager: # Create save data save_data = { - 'player_state': { - 'xPos': player.xPos, - 'yPos': player.yPos, - 'health': player._health, - 'maxHealth': player._maxHealth, - 'lives': player._lives, - 'coins': player._coins, - 'saveBoneDust': player._saveBoneDust, - 'jackOLanternCount': player._jack_o_lantern_count, - 'shinBoneCount': player.shinBoneCount, - 'inventory': player.inventory, - 'collectedItems': player.collectedItems, - 'weapons': self._serialize_weapons(player.weapons), - 'currentWeaponName': player.currentWeapon.name if player.currentWeapon else None, - 'stats': self._serialize_stats(player.stats), - 'scoreboard': self._serialize_scoreboard(player.scoreboard) + "player_state": { + "xPos": player.xPos, + "yPos": player.yPos, + "health": player._health, + "maxHealth": player._maxHealth, + "lives": player._lives, + "coins": player._coins, + "saveBoneDust": player._saveBoneDust, + "jackOLanternCount": player._jack_o_lantern_count, + "shinBoneCount": player.shinBoneCount, + "inventory": player.inventory, + "collectedItems": player.collectedItems, + "weapons": self._serialize_weapons(player.weapons), + "currentWeaponName": player.currentWeapon.name if player.currentWeapon else None, + "stats": self._serialize_stats(player.stats), + "scoreboard": self._serialize_scoreboard(player.scoreboard), }, - 'game_state': { - 'currentLevel': current_level, - 'currentGame': current_game, - 'gameStartTime': game_start_time, - 'saveTime': datetime.now() + "game_state": { + "currentLevel": current_level, + "currentGame": current_game, + "gameStartTime": game_start_time, + "saveTime": datetime.now(), }, - 'version': '1.0' + "version": "1.0", } # Generate filename with timestamp @@ -67,26 +67,26 @@ class SaveManager: try: # Write to temporary file first, then rename for atomic operation - temp_filepath = filepath.with_suffix('.tmp') - - with open(temp_filepath, 'wb') as f: + temp_filepath = filepath.with_suffix(".tmp") + + with open(temp_filepath, "wb") as f: pickle.dump(save_data, f) f.flush() # Ensure data is written to disk os.fsync(f.fileno()) # Force write to disk - + # Atomic rename (replaces old file if it exists) temp_filepath.rename(filepath) - + # Clean up old saves if we exceed max_saves self._cleanup_old_saves() - + return True, f"Game saved to {filename}" except Exception as e: # Clean up temp file if it exists if temp_filepath.exists(): try: temp_filepath.unlink() - except: + except Exception: pass return False, f"Failed to save game: {str(e)}" @@ -94,132 +94,136 @@ class SaveManager: """Serialize weapons for saving""" serialized = [] for weapon in weapons: - serialized.append({ - 'name': weapon.name, - 'damage': weapon.damage, - 'range': weapon.range, - 'attackSound': weapon.attackSound, - 'hitSound': weapon.hitSound, - 'attackDuration': weapon.attackDuration, - 'speedBonus': getattr(weapon, 'speedBonus', 1.0), - 'jumpDurationBonus': getattr(weapon, 'jumpDurationBonus', 1.0) - }) + serialized.append( + { + "name": weapon.name, + "damage": weapon.damage, + "range": weapon.range, + "attackSound": weapon.attackSound, + "hitSound": weapon.hitSound, + "attackDuration": weapon.attackDuration, + "speedBonus": getattr(weapon, "speedBonus", 1.0), + "jumpDurationBonus": getattr(weapon, "jumpDurationBonus", 1.0), + } + ) return serialized def _deserialize_weapons(self, weapon_data): """Deserialize weapons from save data""" from src.weapon import Weapon + weapons = [] for data in weapon_data: # Handle backward compatibility for old saves - speedBonus = data.get('speedBonus', 1.0) - jumpDurationBonus = data.get('jumpDurationBonus', 1.0) - + speedBonus = data.get("speedBonus", 1.0) + jumpDurationBonus = data.get("jumpDurationBonus", 1.0) + # 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 jumpDurationBonus = 1.25 - + weapon = Weapon( - name=data['name'], - damage=data['damage'], - range=data['range'], - attackSound=data['attackSound'], - hitSound=data['hitSound'], - attackDuration=data['attackDuration'], + name=data["name"], + damage=data["damage"], + range=data["range"], + attackSound=data["attackSound"], + hitSound=data["hitSound"], + attackDuration=data["attackDuration"], speedBonus=speedBonus, - jumpDurationBonus=jumpDurationBonus + jumpDurationBonus=jumpDurationBonus, ) weapons.append(weapon) return weapons def _serialize_stats(self, stats): """Serialize stats for saving""" - return { - 'total': stats.total.copy(), - 'level': stats.level.copy() - } + return {"total": stats.total.copy(), "level": stats.level.copy()} def _deserialize_stats(self, stats_data): """Deserialize stats from save data""" from src.stat_tracker import StatTracker + stats = StatTracker() - if 'total' in stats_data: - stats.total.update(stats_data['total']) - if 'level' in stats_data: - stats.level.update(stats_data['level']) + if "total" in stats_data: + stats.total.update(stats_data["total"]) + if "level" in stats_data: + stats.level.update(stats_data["level"]) return stats def _serialize_scoreboard(self, scoreboard): """Serialize scoreboard for saving""" return { - 'currentScore': getattr(scoreboard, 'currentScore', 0), - 'highScores': getattr(scoreboard, 'highScores', []) + "currentScore": getattr(scoreboard, "currentScore", 0), + "highScores": getattr(scoreboard, "highScores", []), } def _deserialize_scoreboard(self, scoreboard_data): """Deserialize scoreboard from save data""" from libstormgames import Scoreboard + scoreboard = Scoreboard() - if 'currentScore' in scoreboard_data: - scoreboard.currentScore = scoreboard_data['currentScore'] - if 'highScores' in scoreboard_data: - scoreboard.highScores = scoreboard_data['highScores'] + if "currentScore" in scoreboard_data: + scoreboard.currentScore = scoreboard_data["currentScore"] + if "highScores" in scoreboard_data: + scoreboard.highScores = scoreboard_data["highScores"] return scoreboard def get_save_files(self): """Get list of save files with metadata""" save_files = [] pattern = str(self.save_dir / "save_*.pickle") - + for filepath in glob.glob(pattern): try: - with open(filepath, 'rb') as f: + with open(filepath, "rb") as f: save_data = pickle.load(f) - + # Validate save data structure if not self._validate_save_data(save_data): print(f"Invalid save file structure: {filepath}") continue - + # Extract save info - save_time = save_data['game_state']['saveTime'] - level = save_data['game_state']['currentLevel'] - game_name = save_data['game_state']['currentGame'] - + save_time = save_data["game_state"]["saveTime"] + level = save_data["game_state"]["currentLevel"] + game_name = save_data["game_state"]["currentGame"] + # Format display name formatted_time = save_time.strftime("%B %d %I:%M%p") display_name = f"{formatted_time} {game_name} Level {level}" - - save_files.append({ - 'filepath': filepath, - 'display_name': display_name, - 'save_time': save_time, - 'level': level, - 'game_name': game_name, - 'save_data': save_data - }) + + save_files.append( + { + "filepath": filepath, + "display_name": display_name, + "save_time": save_time, + "level": level, + "game_name": game_name, + "save_data": save_data, + } + ) except (pickle.PickleError, EOFError, OSError) as e: print(f"Corrupted save file {filepath}: {e}") # Try to remove corrupted save file try: os.remove(filepath) print(f"Removed corrupted save file: {filepath}") - except: + except Exception: pass continue except Exception as e: print(f"Error reading save file {filepath}: {e}") continue - + # 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 def load_save(self, filepath): """Load game state from save file""" try: - with open(filepath, 'rb') as f: + with open(filepath, "rb") as f: save_data = pickle.load(f) return True, save_data except Exception as e: @@ -227,55 +231,57 @@ class SaveManager: def restore_player_state(self, player, save_data): """Restore player state from save data""" - player_state = save_data['player_state'] - + player_state = save_data["player_state"] + # Restore basic attributes - player.xPos = player_state['xPos'] - player.yPos = player_state['yPos'] - player._health = player_state['health'] - player._maxHealth = player_state['maxHealth'] - player._lives = player_state['lives'] - player._coins = player_state['coins'] - player._saveBoneDust = player_state['saveBoneDust'] - player._jack_o_lantern_count = player_state['jackOLanternCount'] - player.shinBoneCount = player_state['shinBoneCount'] - player.inventory = player_state['inventory'] - player.collectedItems = player_state['collectedItems'] - + player.xPos = player_state["xPos"] + player.yPos = player_state["yPos"] + player._health = player_state["health"] + player._maxHealth = player_state["maxHealth"] + player._lives = player_state["lives"] + player._coins = player_state["coins"] + player._saveBoneDust = player_state["saveBoneDust"] + player._jack_o_lantern_count = player_state["jackOLanternCount"] + player.shinBoneCount = player_state["shinBoneCount"] + player.inventory = player_state["inventory"] + player.collectedItems = player_state["collectedItems"] + # Restore weapons - player.weapons = self._deserialize_weapons(player_state['weapons']) - + player.weapons = self._deserialize_weapons(player_state["weapons"]) + # Restore current weapon - current_weapon_name = player_state.get('currentWeaponName') + current_weapon_name = player_state.get("currentWeaponName") if current_weapon_name: for weapon in player.weapons: if weapon.name == current_weapon_name: player.currentWeapon = weapon break - + # Restore stats - if 'stats' in player_state: - player.stats = self._deserialize_stats(player_state['stats']) + if "stats" in player_state: + player.stats = self._deserialize_stats(player_state["stats"]) else: from src.stat_tracker import StatTracker + player.stats = StatTracker() - + # Restore scoreboard - if 'scoreboard' in player_state: - player.scoreboard = self._deserialize_scoreboard(player_state['scoreboard']) + if "scoreboard" in player_state: + player.scoreboard = self._deserialize_scoreboard(player_state["scoreboard"]) else: from libstormgames import Scoreboard + player.scoreboard = Scoreboard() def _cleanup_old_saves(self): """Remove old save files if we exceed max_saves""" save_files = self.get_save_files() - + if len(save_files) > self.max_saves: # Remove oldest saves for save_file in save_files[self.max_saves:]: try: - os.remove(save_file['filepath']) + os.remove(save_file["filepath"]) except Exception as e: print(f"Error removing old save {save_file['filepath']}: {e}") @@ -283,25 +289,24 @@ class SaveManager: """Validate that save data has required structure""" try: # 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): return False - + # Check player_state structure - player_required = ['xPos', 'yPos', 'health', 'maxHealth', 'lives', 'coins', 'saveBoneDust'] - if not all(key in save_data['player_state'] for key in player_required): + player_required = ["xPos", "yPos", "health", "maxHealth", "lives", "coins", "saveBoneDust"] + if not all(key in save_data["player_state"] for key in player_required): return False - + # Check game_state structure - game_required = ['currentLevel', 'currentGame', 'gameStartTime', 'saveTime'] - if not all(key in save_data['game_state'] for key in game_required): + game_required = ["currentLevel", "currentGame", "gameStartTime", "saveTime"] + if not all(key in save_data["game_state"] for key in game_required): return False - + return True - except: + except Exception: return False def has_saves(self): """Check if any save files exist""" return len(self.get_save_files()) > 0 - diff --git a/src/skull_storm.py b/src/skull_storm.py index 0d0dcd1..034d32f 100644 --- a/src/skull_storm.py +++ b/src/skull_storm.py @@ -5,17 +5,13 @@ import random from libstormgames import * from src.object import Object + class SkullStorm(Object): """Handles falling skulls within a specified range.""" def __init__(self, xRange, y, sounds, damage, maxSkulls=3, minFreq=2, maxFreq=5): super().__init__( - xRange, - y, - "", # No ambient sound for the skull storm - isStatic=True, - isCollectible=False, - isHazard=False + xRange, y, "", isStatic=True, isCollectible=False, isHazard=False # No ambient sound for the skull storm ) self.sounds = sounds self.damage = damage @@ -37,7 +33,7 @@ class SkullStorm(Object): inRange = self.xRange[0] <= player.xPos <= self.xRange[1] if inRange and not self.playerInRange: # Player just entered range - play the warning sound - play_sound(self.sounds['skull_storm']) + play_sound(self.sounds["skull_storm"]) self.playerInRange = True elif not inRange and self.playerInRange: # Only speak when actually leaving range # Player just left range @@ -46,8 +42,8 @@ class SkullStorm(Object): # Clear any active skulls when player leaves the range for skull in self.activeSkulls[:]: - if skull['channel']: - obj_stop(skull['channel']) + if skull["channel"]: + obj_stop(skull["channel"]) self.activeSkulls = [] # Reset the list of active skulls if not inRange: @@ -55,29 +51,28 @@ class SkullStorm(Object): # Update existing skulls for skull in self.activeSkulls[:]: # Copy list to allow removal - if currentTime >= skull['land_time']: + if currentTime >= skull["land_time"]: # Skull has landed self.handle_landing(skull, player) self.activeSkulls.remove(skull) else: # Update falling sound - timeElapsed = currentTime - skull['start_time'] - fallProgress = timeElapsed / skull['fall_duration'] + timeElapsed = currentTime - skull["start_time"] + fallProgress = timeElapsed / skull["fall_duration"] currentY = self.yPos * (1 - fallProgress) - skull['channel'] = play_random_falling( + skull["channel"] = play_random_falling( self.sounds, - 'falling_skull', + "falling_skull", player.xPos, - skull['x'], + skull["x"], self.yPos, currentY, - existingChannel=skull['channel'] + existingChannel=skull["channel"], ) # Check if we should spawn a new skull - if (len(self.activeSkulls) < self.maxSkulls and - currentTime - self.lastSkullTime >= self.nextSkullDelay): + if len(self.activeSkulls) < self.maxSkulls and currentTime - self.lastSkullTime >= self.nextSkullDelay: self.spawn_skull(currentTime) def spawn_skull(self, currentTime): @@ -91,11 +86,11 @@ class SkullStorm(Object): # Create new skull skull = { - 'x': random.uniform(self.xRange[0], self.xRange[1]), - 'start_time': currentTime, - 'fall_duration': fallDuration, - 'land_time': currentTime + fallDuration, - 'channel': None + "x": random.uniform(self.xRange[0], self.xRange[1]), + "start_time": currentTime, + "fall_duration": fallDuration, + "land_time": currentTime + fallDuration, + "channel": None, } self.activeSkulls.append(skull) @@ -103,20 +98,18 @@ class SkullStorm(Object): def handle_landing(self, skull, player): """Handle a skull landing.""" # Stop falling sound - if skull['channel']: - obj_stop(skull['channel']) + if skull["channel"]: + obj_stop(skull["channel"]) # Play landing sound with positional audio once channel = pygame.mixer.find_channel(True) # Find an available channel if channel: - soundObj = self.sounds['skull_lands'] - obj_play(self.sounds, 'skull_lands', player.xPos, skull['x'], loop=False) + soundObj = self.sounds["skull_lands"] + obj_play(self.sounds, "skull_lands", player.xPos, skull["x"], loop=False) # 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: 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!") - - diff --git a/src/stat_tracker.py b/src/stat_tracker.py index 2b07e62..e7e6dec 100644 --- a/src/stat_tracker.py +++ b/src/stat_tracker.py @@ -1,20 +1,15 @@ # -*- coding: utf-8 -*- + class StatTracker: def __init__(self): # Base dictionary for tracking stats - self.total = { - 'Bone dust': 0, - 'Enemies killed': 0, - 'Coffins broken': 0, - 'Items collected': 0, - 'Total time': 0 - } + self.total = {"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) self.level = self.total.copy() - self.total['levelsCompleted'] = 0 + self.total["levelsCompleted"] = 0 def reset_level(self): """Reset level stats based on variable type""" @@ -42,5 +37,3 @@ class StatTracker: def get_total_stat(self, statName): """Get a total stat""" return self.total.get(statName, 0) - - diff --git a/src/survival_generator.py b/src/survival_generator.py index f166869..08f9249 100644 --- a/src/survival_generator.py +++ b/src/survival_generator.py @@ -10,7 +10,7 @@ from src.game_selection import get_level_path class SurvivalGenerator: def __init__(self, gamePack): """Initialize the survival generator for a specific game pack. - + Args: gamePack (str): Name of the game pack directory """ @@ -24,57 +24,57 @@ class SurvivalGenerator: self.footstepSounds = [] self.loadLevelData() self.parseTemplates() - + def loadLevelData(self): """Load all level JSON files from the game pack.""" levelFiles = [] packPath = os.path.join("levels", self.gamePack) - + if not os.path.exists(packPath): raise FileNotFoundError(f"Game pack '{self.gamePack}' not found") - + # Get all JSON files in the pack directory 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) - + # Load each level file for levelFile in levelFiles: levelPath = os.path.join(packPath, levelFile) - with open(levelPath, 'r') as f: - levelNum = int(levelFile.split('.')[0]) + with open(levelPath, "r") as f: + levelNum = int(levelFile.split(".")[0]) self.levelData[levelNum] = json.load(f) - + def parseTemplates(self): """Parse all level data to extract object templates by type.""" for levelNum, data in self.levelData.items(): # Store ambience and footstep sounds (remove duplicates) - if 'ambience' in data and data['ambience'] not in self.ambientSounds: - self.ambientSounds.append(data['ambience']) - if 'footstep_sound' in data and data['footstep_sound'] not in self.footstepSounds: - self.footstepSounds.append(data['footstep_sound']) - + if "ambience" in data and data["ambience"] not in self.ambientSounds: + self.ambientSounds.append(data["ambience"]) + if "footstep_sound" in data and data["footstep_sound"] not in self.footstepSounds: + self.footstepSounds.append(data["footstep_sound"]) + # Parse objects - for obj in data.get('objects', []): + for obj in data.get("objects", []): objCopy = copy.deepcopy(obj) - + # Categorize objects - if 'enemy_type' in obj: + if "enemy_type" in obj: 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) - elif obj.get('type') in ['skull_storm', 'catapult', 'grasping_hands']: + elif obj.get("type") in ["skull_storm", "catapult", "grasping_hands"]: self.hazardTemplates.append(objCopy) else: self.objectTemplates.append(objCopy) - + def generate_survival_level(self, difficultyLevel=1, segmentLength=100): """Generate an endless survival level segment. - + Args: difficultyLevel (int): Current difficulty level (increases over time) segmentLength (int): Length of this level segment - + Returns: dict: Generated level data """ @@ -88,30 +88,30 @@ class SurvivalGenerator: "boundaries": {"left": 0, "right": segmentLength}, "locked": True, # Enable lock system for survival mode "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 if self.ambientSounds: levelData["ambience"] = random.choice(self.ambientSounds) if self.footstepSounds: levelData["footstep_sound"] = random.choice(self.footstepSounds) - + # Calculate spawn rates based on difficulty 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 objectDensity = max(0.1, 0.2 - (difficultyLevel * 0.01)) # Fewer misc objects over time - + # Generate objects across the segment with buffer zones 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 - + while currentX < segmentLength - endBufferZone: # Determine what to place based on probability rand = random.random() - + if rand < collectibleDensity and self.collectibleTemplates: obj = self.place_collectible(currentX, difficultyLevel) currentX += random.randint(8, 15) @@ -127,121 +127,118 @@ class SurvivalGenerator: else: currentX += random.randint(5, 15) continue - + if obj: levelData["objects"].append(obj) - + # Add end-of-level marker at the end, within the end buffer zone endMarker = { "x": segmentLength - (endBufferZone // 2), # Place marker in middle of end buffer "y": 0, - "sound": "end_of_level" + "sound": "end_of_level", } levelData["objects"].append(endMarker) - + return levelData - + def place_collectible(self, xPos, difficultyLevel): """Place a collectible at the given position.""" template = random.choice(self.collectibleTemplates) obj = copy.deepcopy(template) - + # 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] + if "x_range" in obj: + rangeSize = obj["x_range"][1] - obj["x_range"][0] + obj["x_range"] = [xPos, xPos + rangeSize] else: - obj['x'] = xPos - + obj["x"] = xPos + return obj - + def place_enemy(self, xPos, difficultyLevel): """Place an enemy at the given position with scaled difficulty.""" # 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 - availableEnemies = [e for e in self.enemyTemplates - if e.get('enemy_type') not in bossEnemies] + availableEnemies = [e for e in self.enemyTemplates if e.get("enemy_type") not in bossEnemies] elif difficultyLevel < 5: # Waves 3-4: exclude the hardest bosses - hardestBosses = ['revenant', 'ghost', 'headless_horseman'] - availableEnemies = [e for e in self.enemyTemplates - if e.get('enemy_type') not in hardestBosses] + hardestBosses = ["revenant", "ghost", "headless_horseman"] + availableEnemies = [e for e in self.enemyTemplates if e.get("enemy_type") not in hardestBosses] else: # Wave 5+: all enemies allowed availableEnemies = self.enemyTemplates - + # Fallback to all enemies if filtering removed everything if not availableEnemies: availableEnemies = self.enemyTemplates - + template = random.choice(availableEnemies) obj = copy.deepcopy(template) - + # Dynamic health scaling: random between wave/2 and wave minHealth = max(1, difficultyLevel // 2) maxHealth = max(1, difficultyLevel) - obj['health'] = random.randint(minHealth, maxHealth) - + obj["health"] = random.randint(minHealth, maxHealth) + # Damage scaling (keep existing logic) 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 - obj['behavior'] = 'hunter' - + obj["behavior"] = "hunter" + # 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 - if 'x_range' in obj: - rangeSize = obj['x_range'][1] - obj['x_range'][0] - obj['x_range'] = [xPos, xPos + rangeSize] + if "x_range" in obj: + rangeSize = obj["x_range"][1] - obj["x_range"][0] + obj["x_range"] = [xPos, xPos + rangeSize] else: - obj['x'] = xPos - + obj["x"] = xPos + return obj - + def place_hazard(self, xPos, difficultyLevel): """Place a hazard at the given position with scaled difficulty.""" template = random.choice(self.hazardTemplates) obj = copy.deepcopy(template) - + # Scale hazard difficulty - if obj.get('type') == 'skull_storm': - 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)) - elif obj.get('type') == 'catapult': - obj['fire_interval'] = max(1000, obj.get('fire_interval', 4000) - (difficultyLevel * 100)) - - # 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] + if obj.get("type") == "skull_storm": + 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)) + elif obj.get("type") == "catapult": + obj["fire_interval"] = max(1000, obj.get("fire_interval", 4000) - (difficultyLevel * 100)) + + # 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 - + obj["x"] = xPos + return obj - + def place_object(self, xPos, difficultyLevel): """Place a misc object at the given position.""" template = random.choice(self.objectTemplates) 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 diff --git a/src/weapon.py b/src/weapon.py index 8f347d8..199d795 100644 --- a/src/weapon.py +++ b/src/weapon.py @@ -1,7 +1,19 @@ # -*- coding: utf-8 -*- + 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.damage = damage self.range = range # Range in tiles @@ -24,7 +36,7 @@ class Weapon: attackSound="player_nunchuck_attack", hitSound="player_nunchuck_hit", cooldown=250, - attackDuration=100 + attackDuration=100, ) @classmethod @@ -38,8 +50,8 @@ class Weapon: hitSound="player_broom_hit", cooldown=500, attackDuration=200, - speedBonus=1.17, # 17% speed bonus when wielding the broom - jumpDurationBonus=1.25 # 25% longer jump duration for better traversal + speedBonus=1.17, # 17% speed bonus when wielding the broom + jumpDurationBonus=1.25, # 25% longer jump duration for better traversal ) def can_attack(self, currentTime): @@ -73,5 +85,3 @@ class Weapon: self.hitEnemies.add(enemy) return True return False - - diff --git a/wicked_quest.spec b/wicked_quest.spec index 1e1ff7f..0a57a19 100644 --- a/wicked_quest.spec +++ b/wicked_quest.spec @@ -1,12 +1,21 @@ # -*- 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( ['wicked_quest.py'], pathex=[], binaries=[], - datas=[ - ('levels', 'levels'), + datas=level_dirs + [ ('sounds', 'sounds'), ('libstormgames', 'libstormgames'), ],