lint and spacing fixes. Also fixed problem with levels not showing up in compiled version (hopefully).
This commit is contained in:
@@ -1,2 +0,0 @@
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
74
src/enemy.py
74
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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
32
src/grave.py
32
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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
150
src/level.py
150
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"])
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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}")
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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!")
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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'),
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user