Some minor cleanup.

This commit is contained in:
Storm Dragon
2025-03-21 20:54:13 -04:00
parent cb69189c8e
commit 87d0764156
16 changed files with 193 additions and 193 deletions

View File

@@ -20,15 +20,15 @@ class Pumpkin:
"""Update pumpkin position and sound""" """Update pumpkin position and sound"""
if not self.isActive: if not self.isActive:
return False return False
self.x += self.direction * self.speed self.x += self.direction * self.speed
# Update or start positional audio # Update or start positional audio
if self.soundChannel is None or not self.soundChannel.get_busy(): if self.soundChannel is None or not self.soundChannel.get_busy():
self.soundChannel = obj_play(sounds, self.soundName, playerX, self.x) self.soundChannel = obj_play(sounds, self.soundName, playerX, self.x)
else: else:
self.soundChannel = obj_update(self.soundChannel, playerX, self.x) self.soundChannel = obj_update(self.soundChannel, playerX, self.x)
return True return True
def stop_sound(self, sounds, playerX): def stop_sound(self, sounds, playerX):
@@ -45,7 +45,7 @@ class Pumpkin:
"""Check if pumpkin hits player""" """Check if pumpkin hits player"""
if not self.isActive: if not self.isActive:
return False return False
distance = abs(player.xPos - self.x) distance = abs(player.xPos - self.x)
if distance < 1: # Within 1 tile if distance < 1: # Within 1 tile
if self.isHigh and not player.isJumping: if self.isHigh and not player.isJumping:
@@ -75,21 +75,21 @@ class Catapult(Object):
def fire(self, currentTime, player): def fire(self, currentTime, player):
"""Start the firing sequence""" """Start the firing sequence"""
self.lastFireTime = currentTime self.lastFireTime = currentTime
# Play launch sound using directional audio # Play launch sound using directional audio
play_directional_sound(self.sounds, 'catapult_launch', player.xPos, self.xPos) play_directional_sound(self.sounds, 'catapult_launch', player.xPos, self.xPos)
# Set up pending pumpkin # Set up pending pumpkin
isHigh = random.choice([True, False]) isHigh = random.choice([True, False])
fireDirection = 1 if player.xPos > self.xPos else -1 fireDirection = 1 if player.xPos > self.xPos else -1
# Store pumpkin data for later creation # Store pumpkin data for later creation
self.pendingPumpkin = { self.pendingPumpkin = {
'isHigh': isHigh, 'isHigh': isHigh,
'direction': fireDirection, 'direction': fireDirection,
'playerMaxHealth': player.get_max_health() 'playerMaxHealth': player.get_max_health()
} }
# Set when to actually launch the pumpkin # Set when to actually launch the pumpkin
self.pumpkinLaunchTime = currentTime + self.launchDelay self.pumpkinLaunchTime = currentTime + self.launchDelay
@@ -97,11 +97,11 @@ class Catapult(Object):
"""Update catapult and its pumpkins""" """Update catapult and its pumpkins"""
if not self.isActive: if not self.isActive:
return return
# Check if player is in range # Check if player is in range
distance = abs(player.xPos - self.xPos) distance = abs(player.xPos - self.xPos)
inRange = distance <= self.firingRange inRange = distance <= self.firingRange
# Handle entering/leaving range # Handle entering/leaving range
if inRange and not self.isFiring: if inRange and not self.isFiring:
self.isFiring = True self.isFiring = True
@@ -110,7 +110,7 @@ class Catapult(Object):
elif not inRange and self.isFiring: elif not inRange and self.isFiring:
self.isFiring = False self.isFiring = False
speak("Out of pumpkin catapult range.") speak("Out of pumpkin catapult range.")
# Check for pending pumpkin launch # Check for pending pumpkin launch
if self.pendingPumpkin and currentTime >= self.pumpkinLaunchTime: if self.pendingPumpkin and currentTime >= self.pumpkinLaunchTime:
# Create and fire the pending pumpkin # Create and fire the pending pumpkin
@@ -122,18 +122,18 @@ class Catapult(Object):
) )
self.activePumpkins.append(pumpkin) self.activePumpkins.append(pumpkin)
self.pendingPumpkin = None self.pendingPumpkin = None
# Only start new fire sequence if in range and enough time has passed # Only start new fire sequence if in range and enough time has passed
if self.isFiring and currentTime - self.lastFireTime >= self.fireInterval: if self.isFiring and currentTime - self.lastFireTime >= self.fireInterval:
self.fire(currentTime, player) self.fire(currentTime, player)
# Always update existing pumpkins # Always update existing pumpkins
for pumpkin in self.activePumpkins[:]: # Copy list to allow removal for pumpkin in self.activePumpkins[:]: # Copy list to allow removal
if not pumpkin.update(self.sounds, player.xPos): if not pumpkin.update(self.sounds, player.xPos):
pumpkin.stop_sound(self.sounds, player.xPos) pumpkin.stop_sound(self.sounds, player.xPos)
self.activePumpkins.remove(pumpkin) self.activePumpkins.remove(pumpkin)
continue continue
if pumpkin.check_collision(player): if pumpkin.check_collision(player):
player.set_health(player.get_health() - pumpkin.damage) player.set_health(player.get_health() - pumpkin.damage)
pumpkin.stop_sound(self.sounds, player.xPos) pumpkin.stop_sound(self.sounds, player.xPos)

View File

@@ -20,7 +20,7 @@ class CoffinObject(Object):
self.isBroken = False self.isBroken = False
self.dropped_item = None self.dropped_item = None
self.specified_item = item self.specified_item = item
def hit(self, player_pos): def hit(self, player_pos):
"""Handle being hit by the player's weapon""" """Handle being hit by the player's weapon"""
if not self.isBroken: if not self.isBroken:
@@ -28,15 +28,15 @@ class CoffinObject(Object):
play_sound(self.sounds['coffin_shatter']) play_sound(self.sounds['coffin_shatter'])
self.level.levelScore += 500 self.level.levelScore += 500
self.level.player.stats.update_stat('Coffins broken', 1) self.level.player.stats.update_stat('Coffins broken', 1)
# Stop the ongoing coffin sound # Stop the ongoing coffin sound
if self.channel: if self.channel:
obj_stop(self.channel) obj_stop(self.channel)
self.channel = None self.channel = None
# Mark coffin as inactive since it's broken # Mark coffin as inactive since it's broken
self.isActive = False self.isActive = False
# Determine item to drop # Determine item to drop
if self.specified_item == "random": if self.specified_item == "random":
item_type = ItemProperties.get_random_item() item_type = ItemProperties.get_random_item()
@@ -47,12 +47,12 @@ class CoffinObject(Object):
else: else:
# Fall back to random if invalid item specified # Fall back to random if invalid item specified
item_type = ItemProperties.get_random_item() item_type = ItemProperties.get_random_item()
# Create item 1-2 tiles away in random direction # Create item 1-2 tiles away in random direction
direction = random.choice([-1, 1]) direction = random.choice([-1, 1])
drop_distance = random.randint(1, 2) drop_distance = random.randint(1, 2)
drop_x = self.xPos + (direction * drop_distance) drop_x = self.xPos + (direction * drop_distance)
self.dropped_item = PowerUp( self.dropped_item = PowerUp(
drop_x, drop_x,
self.yPos, self.yPos,

View File

@@ -18,7 +18,7 @@ class Enemy(Object):
isStatic=False, isStatic=False,
isHazard=True isHazard=True
) )
# Enemy specific properties # Enemy specific properties
self.enemyType = enemyType self.enemyType = enemyType
self.level = level self.level = level
@@ -26,7 +26,7 @@ class Enemy(Object):
self.damage = kwargs.get('damage', 1) # Default 1 damage self.damage = kwargs.get('damage', 1) # Default 1 damage
self.attackRange = kwargs.get('attack_range', 1) # Default 1 tile range self.attackRange = kwargs.get('attack_range', 1) # Default 1 tile range
self.sounds = sounds # Store reference to game sounds self.sounds = sounds # Store reference to game sounds
# Movement and behavior properties # Movement and behavior properties
self.movingRight = True # Initial direction self.movingRight = True # Initial direction
self.movementSpeed = 0.03 # Base speed self.movementSpeed = 0.03 # Base speed
@@ -35,7 +35,7 @@ class Enemy(Object):
self.lastAttackTime = 0 self.lastAttackTime = 0
self.attackCooldown = 1000 # 1 second between attacks self.attackCooldown = 1000 # 1 second between attacks
self._currentX = self.xRange[0] # Initialize current position self._currentX = self.xRange[0] # Initialize current position
# Add spawn configuration # Add spawn configuration
self.canSpawn = kwargs.get('can_spawn', False) self.canSpawn = kwargs.get('can_spawn', False)
if self.canSpawn: if self.canSpawn:
@@ -48,7 +48,7 @@ class Enemy(Object):
# Attack pattern configuration # Attack pattern configuration
self.attackPattern = kwargs.get('attack_pattern', {'type': 'patrol'}) self.attackPattern = kwargs.get('attack_pattern', {'type': 'patrol'})
self.turnThreshold = self.attackPattern.get('turn_threshold', 5) self.turnThreshold = self.attackPattern.get('turn_threshold', 5)
# Initialize vulnerability system # Initialize vulnerability system
self.hasVulnerabilitySystem = kwargs.get('has_vulnerability', False) self.hasVulnerabilitySystem = kwargs.get('has_vulnerability', False)
if self.hasVulnerabilitySystem: if self.hasVulnerabilitySystem:
@@ -73,18 +73,18 @@ class Enemy(Object):
self.attackPattern = {'type': 'hunter'} # Spiders actively hunt the player self.attackPattern = {'type': 'hunter'} # Spiders actively hunt the player
self.turnThreshold = 3 # Spiders turn around quickly to chase player self.turnThreshold = 3 # Spiders turn around quickly to chase player
@property @property
def xPos(self): def xPos(self):
"""Current x position""" """Current x position"""
return self._currentX return self._currentX
@xPos.setter @xPos.setter
def xPos(self, value): def xPos(self, value):
"""Set current x position""" """Set current x position"""
self._currentX = value self._currentX = value
def patrol_movement(self): def patrol_movement(self):
"""Standard back-and-forth patrol movement""" """Standard back-and-forth patrol movement"""
if self.movingRight: if self.movingRight:
@@ -100,7 +100,7 @@ class Enemy(Object):
"""Update enemy position and handle attacks""" """Update enemy position and handle attacks"""
if not self.isActive or self.health <= 0: if not self.isActive or self.health <= 0:
return return
# Initialize sound for enemies with vulnerability system immediately upon creation # Initialize sound for enemies with vulnerability system immediately upon creation
if self.hasVulnerabilitySystem: if self.hasVulnerabilitySystem:
if self.channel is None: if self.channel is None:
@@ -109,12 +109,12 @@ class Enemy(Object):
# Update existing channel position # Update existing channel position
else: else:
self.channel = obj_update(self.channel, player.xPos, self.xPos) self.channel = obj_update(self.channel, player.xPos, self.xPos)
# Check for vulnerability state change # Check for vulnerability state change
if currentTime - self.vulnerabilityTimer >= (self.vulnerabilityDuration if self.isVulnerable else self.invulnerabilityDuration): if currentTime - self.vulnerabilityTimer >= (self.vulnerabilityDuration if self.isVulnerable else self.invulnerabilityDuration):
self.isVulnerable = not self.isVulnerable self.isVulnerable = not self.isVulnerable
self.vulnerabilityTimer = currentTime self.vulnerabilityTimer = currentTime
if self.channel: if self.channel:
obj_stop(self.channel) obj_stop(self.channel)
soundName = f"{self.enemyType}_is_vulnerable" if self.isVulnerable else self.enemyType soundName = f"{self.enemyType}_is_vulnerable" if self.isVulnerable else self.enemyType
@@ -124,17 +124,17 @@ class Enemy(Object):
if not self.hunting: if not self.hunting:
if self.patrolStart <= player.xPos <= self.patrolEnd: if self.patrolStart <= player.xPos <= self.patrolEnd:
self.hunting = True self.hunting = True
# Handle movement based on enemy type and pattern # Handle movement based on enemy type and pattern
if (self.enemyType == "zombie" or if (self.enemyType == "zombie" or
(self.attackPattern['type'] == 'hunter' and self.hunting)): (self.attackPattern['type'] == 'hunter' and self.hunting)):
distanceToPlayer = player.xPos - self.xPos distanceToPlayer = player.xPos - self.xPos
# If we've moved past the player by more than the turn threshold, turn around # If we've moved past the player by more than the turn threshold, turn around
if abs(distanceToPlayer) >= self.turnThreshold: if abs(distanceToPlayer) >= self.turnThreshold:
self.movingRight = distanceToPlayer > 0 self.movingRight = distanceToPlayer > 0
# Otherwise keep moving in current direction # Otherwise keep moving in current direction
self.xPos += self.movementSpeed if self.movingRight else -self.movementSpeed self.xPos += self.movementSpeed if self.movingRight else -self.movementSpeed
@@ -152,7 +152,7 @@ class Enemy(Object):
# Check for attack opportunity # Check for attack opportunity
if self.can_attack(currentTime, player): if self.can_attack(currentTime, player):
self.attack(currentTime, player) self.attack(currentTime, player)
if self.canSpawn: if self.canSpawn:
if currentTime - self.lastSpawnTime >= self.spawnCooldown: if currentTime - self.lastSpawnTime >= self.spawnCooldown:
distanceToPlayer = abs(player.xPos - self.xPos) distanceToPlayer = abs(player.xPos - self.xPos)
@@ -197,22 +197,22 @@ class Enemy(Object):
# Must have cooled down from last attack # Must have cooled down from last attack
if currentTime - self.lastAttackTime < self.attackCooldown: if currentTime - self.lastAttackTime < self.attackCooldown:
return False return False
# Don't attack if player is jumping # Don't attack if player is jumping
if player.isJumping: if player.isJumping:
return False return False
# Check if player is in range and on same side we're facing # Check if player is in range and on same side we're facing
distance = abs(player.xPos - self.xPos) distance = abs(player.xPos - self.xPos)
tolerance = 0.5 # Same tolerance as we used for the grave tolerance = 0.5 # Same tolerance as we used for the grave
if distance <= (self.attackRange + tolerance): if distance <= (self.attackRange + tolerance):
# Only attack if we're facing the right way # Only attack if we're facing the right way
playerOnRight = player.xPos > self.xPos playerOnRight = player.xPos > self.xPos
return playerOnRight == self.movingRight return playerOnRight == self.movingRight
return False return False
def attack(self, currentTime, player): def attack(self, currentTime, player):
"""Perform attack on player""" """Perform attack on player"""
if player.isInvincible: return if player.isInvincible: return
@@ -224,7 +224,7 @@ class Enemy(Object):
# Deal damage to player # Deal damage to player
player.set_health(player.get_health() - self.damage) player.set_health(player.get_health() - self.damage)
self.sounds['player_takes_damage'].play() self.sounds['player_takes_damage'].play()
def take_damage(self, amount): def take_damage(self, amount):
"""Handle enemy taking damage""" """Handle enemy taking damage"""
if self.hasVulnerabilitySystem and not self.isVulnerable: if self.hasVulnerabilitySystem and not self.isVulnerable:
@@ -233,7 +233,7 @@ class Enemy(Object):
self.health -= amount self.health -= amount
if self.health <= 0: if self.health <= 0:
self.die() self.die()
def die(self): def die(self):
"""Handle enemy death""" """Handle enemy death"""
self.isActive = False self.isActive = False
@@ -247,7 +247,7 @@ class Enemy(Object):
rangeModifier = self.attackRange * 250 rangeModifier = self.attackRange * 250
speedModifier = int(self.movementSpeed * 1000) speedModifier = int(self.movementSpeed * 1000)
totalPoints = max(basePoints + damageModifier + rangeModifier + speedModifier, 1000) totalPoints = max(basePoints + damageModifier + rangeModifier + speedModifier, 1000)
# Award points # Award points
self.level.levelScore += totalPoints self.level.levelScore += totalPoints
@@ -263,12 +263,12 @@ class Enemy(Object):
hasNunchucks = any(weapon.name == "nunchucks" for weapon in self.level.player.weapons) hasNunchucks = any(weapon.name == "nunchucks" for weapon in self.level.player.weapons)
# Drop witch_broom only if player has neither broom nor nunchucks # Drop witch_broom only if player has neither broom nor nunchucks
itemType = "witch_broom" if not (hasBroom or hasNunchucks) else "cauldron" itemType = "witch_broom" if not (hasBroom or hasNunchucks) else "cauldron"
# Create drop 1-2 tiles away in random direction # Create drop 1-2 tiles away in random direction
direction = random.choice([-1, 1]) direction = random.choice([-1, 1])
dropDistance = random.randint(1, 2) dropDistance = random.randint(1, 2)
dropX = self.xPos + (direction * dropDistance) dropX = self.xPos + (direction * dropDistance)
droppedItem = PowerUp( droppedItem = PowerUp(
dropX, dropX,
self.yPos, self.yPos,

View File

@@ -8,7 +8,7 @@ from libstormgames import speak
def get_available_games(): def get_available_games():
"""Get list of available game directories in levels folder. """Get list of available game directories in levels folder.
Returns: Returns:
list: List of game directory names list: List of game directory names
""" """
@@ -19,11 +19,11 @@ def get_available_games():
def selection_menu(sounds, *options): def selection_menu(sounds, *options):
"""Display level selection menu. """Display level selection menu.
Args: Args:
sounds (dict): Dictionary of loaded sound effects sounds (dict): Dictionary of loaded sound effects
*options: Variable number of menu options *options: Variable number of menu options
Returns: Returns:
str: Selected option or None if cancelled str: Selected option or None if cancelled
""" """
@@ -31,53 +31,53 @@ def selection_menu(sounds, *options):
pygame.mixer.stop() pygame.mixer.stop()
i = 0 i = 0
j = -1 j = -1
# Clear any pending events # Clear any pending events
pygame.event.clear() pygame.event.clear()
speak("Select an adventure") speak("Select an adventure")
time.sleep(1.0) time.sleep(1.0)
while loop: while loop:
if i != j: if i != j:
speak(options[i]) speak(options[i])
j = i j = i
pygame.event.pump() pygame.event.pump()
event = pygame.event.wait() event = pygame.event.wait()
if event.type == pygame.KEYDOWN: if event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE: if event.key == pygame.K_ESCAPE:
return None return None
if event.key == pygame.K_DOWN and i < len(options) - 1: if event.key == pygame.K_DOWN and i < len(options) - 1:
i = i + 1 i = i + 1
try: try:
sounds['menu-move'].play() sounds['menu-move'].play()
except: except:
pass pass
if event.key == pygame.K_UP and i > 0: if event.key == pygame.K_UP and i > 0:
i = i - 1 i = i - 1
try: try:
sounds['menu-move'].play() sounds['menu-move'].play()
except: except:
pass pass
if event.key == pygame.K_HOME and i != 0: if event.key == pygame.K_HOME and i != 0:
i = 0 i = 0
try: try:
sounds['menu-move'].play() sounds['menu-move'].play()
except: except:
pass pass
if event.key == pygame.K_END and i != len(options) - 1: if event.key == pygame.K_END and i != len(options) - 1:
i = len(options) - 1 i = len(options) - 1
try: try:
sounds['menu-move'].play() sounds['menu-move'].play()
except: except:
pass pass
if event.key == pygame.K_RETURN: if event.key == pygame.K_RETURN:
try: try:
sounds['menu-select'].play() sounds['menu-select'].play()
@@ -87,48 +87,48 @@ def selection_menu(sounds, *options):
return options[i] return options[i]
elif event.type == pygame.QUIT: elif event.type == pygame.QUIT:
return None return None
pygame.event.pump() pygame.event.pump()
event = pygame.event.clear() event = pygame.event.clear()
time.sleep(0.001) time.sleep(0.001)
def select_game(sounds): def select_game(sounds):
"""Display game selection menu and return chosen game. """Display game selection menu and return chosen game.
Args: Args:
sounds (dict): Dictionary of loaded sound effects sounds (dict): Dictionary of loaded sound effects
Returns: Returns:
str: Selected game directory name or None if cancelled str: Selected game directory name or None if cancelled
""" """
availableGames = get_available_games() availableGames = get_available_games()
if not availableGames: if not availableGames:
speak("No games found in levels directory!") speak("No games found in levels directory!")
return None return None
# Convert directory names to display names (replace underscores with spaces) # Convert directory names to display names (replace underscores with spaces)
menuOptions = [game.replace("_", " ") for game in availableGames] menuOptions = [game.replace("_", " ") for game in availableGames]
choice = selection_menu(sounds, *menuOptions) choice = selection_menu(sounds, *menuOptions)
if choice is None: if choice is None:
return None return None
# Convert display name back to directory name if needed # Convert display name back to directory name if needed
gameDir = choice.replace(" ", "_") gameDir = choice.replace(" ", "_")
if gameDir not in availableGames: if gameDir not in availableGames:
gameDir = choice # Use original if conversion doesn't match gameDir = choice # Use original if conversion doesn't match
return gameDir return gameDir
def get_level_path(gameDir, levelNum): def get_level_path(gameDir, levelNum):
"""Get full path to level JSON file. """Get full path to level JSON file.
Args: Args:
gameDir (str): Game directory name gameDir (str): Game directory name
levelNum (int): Level number levelNum (int): Level number
Returns: Returns:
str: Full path to level JSON file str: Full path to level JSON file
""" """

View File

@@ -6,7 +6,7 @@ from src.object import Object
class GraspingHands(Object): class GraspingHands(Object):
"""A hazard where the ground crumbles beneath the player as undead hands reach up.""" """A hazard where the ground crumbles beneath the player as undead hands reach up."""
def __init__(self, xRange, y, sounds, delay=1000, crumble_speed=0.065): def __init__(self, xRange, y, sounds, delay=1000, crumble_speed=0.065):
super().__init__( super().__init__(
xRange, xRange,
@@ -19,7 +19,7 @@ class GraspingHands(Object):
self.sounds = sounds self.sounds = sounds
self.delay = delay # Delay in milliseconds before ground starts crumbling self.delay = delay # Delay in milliseconds before ground starts crumbling
self.crumble_speed = crumble_speed # How fast the crumbling catches up (tiles per frame) self.crumble_speed = crumble_speed # How fast the crumbling catches up (tiles per frame)
# State tracking # State tracking
self.isTriggered = False # Has the player entered the zone? self.isTriggered = False # Has the player entered the zone?
self.triggerTime = 0 # When did the player enter the zone? self.triggerTime = 0 # When did the player enter the zone?
@@ -29,13 +29,13 @@ class GraspingHands(Object):
self.crumbleChannel = None # Channel for the looping crumble sound self.crumbleChannel = None # Channel for the looping crumble sound
self.entryFromRight = False # Which side did player enter from self.entryFromRight = False # Which side did player enter from
self.crumbleDirection = 1 # Direction the crumbling moves (1=right, -1=left) self.crumbleDirection = 1 # Direction the crumbling moves (1=right, -1=left)
def trigger(self, currentTime, playerX): def trigger(self, currentTime, playerX):
"""Trigger the grasping hands when player enters range""" """Trigger the grasping hands when player enters range"""
if not self.isTriggered: if not self.isTriggered:
self.isTriggered = True self.isTriggered = True
self.triggerTime = currentTime self.triggerTime = currentTime
# Determine which side player entered from # Determine which side player entered from
if playerX > (self.xRange[0] + self.xRange[1]) / 2: if playerX > (self.xRange[0] + self.xRange[1]) / 2:
# Player entered from right side # Player entered from right side
@@ -47,48 +47,48 @@ class GraspingHands(Object):
self.entryFromRight = False self.entryFromRight = False
self.crumblePosition = self.xRange[0] # Start crumbling from left boundary self.crumblePosition = self.xRange[0] # Start crumbling from left boundary
self.crumbleDirection = 1 # Crumble moves right self.crumbleDirection = 1 # Crumble moves right
self.isReset = False self.isReset = False
# Play initial warning sound # Play initial warning sound
play_sound(self.sounds['grasping_hands_start']) play_sound(self.sounds['grasping_hands_start'])
speak("The ground crumbles as the dead reach for you.") speak("The ground crumbles as the dead reach for you.")
def reset(self): def reset(self):
"""Reset the trap when player leaves the range""" """Reset the trap when player leaves the range"""
if not self.isReset: if not self.isReset:
self.isTriggered = False self.isTriggered = False
self.crumblePosition = 0 self.crumblePosition = 0
self.isReset = True self.isReset = True
# Stop the looping crumble sound if it's playing # Stop the looping crumble sound if it's playing
if self.crumbleChannel: if self.crumbleChannel:
obj_stop(self.crumbleChannel) obj_stop(self.crumbleChannel)
self.crumbleChannel = None self.crumbleChannel = None
# Play the end sound # Play the end sound
play_sound(self.sounds['grasping_hands_end']) play_sound(self.sounds['grasping_hands_end'])
def update(self, currentTime, player): def update(self, currentTime, player):
"""Update the grasping hands trap state""" """Update the grasping hands trap state"""
if not self.isActive: if not self.isActive:
return False return False
# Check if player is in range # Check if player is in range
isInRange = self.xRange[0] <= player.xPos <= self.xRange[1] isInRange = self.xRange[0] <= player.xPos <= self.xRange[1]
# Handle player entering/exiting range # Handle player entering/exiting range
if isInRange and not self.isTriggered: if isInRange and not self.isTriggered:
self.trigger(currentTime, player.xPos) self.trigger(currentTime, player.xPos)
elif not isInRange and self.isTriggered: elif not isInRange and self.isTriggered:
self.reset() self.reset()
return False return False
# If triggered and delay has passed, start crumbling # If triggered and delay has passed, start crumbling
if self.isTriggered and currentTime - self.triggerTime >= self.delay: if self.isTriggered and currentTime - self.triggerTime >= self.delay:
# Update crumble position based on direction # Update crumble position based on direction
self.crumblePosition += self.crumble_speed * self.crumbleDirection self.crumblePosition += self.crumble_speed * self.crumbleDirection
# Manage the looping positional audio for the crumbling ground # Manage the looping positional audio for the crumbling ground
if self.crumbleChannel is None or not self.crumbleChannel.get_busy(): if self.crumbleChannel is None or not self.crumbleChannel.get_busy():
# Start the sound if it's not playing # Start the sound if it's not playing
@@ -96,14 +96,14 @@ class GraspingHands(Object):
else: else:
# Update the sound position # Update the sound position
self.crumbleChannel = obj_update(self.crumbleChannel, player.xPos, self.crumblePosition) self.crumbleChannel = obj_update(self.crumbleChannel, player.xPos, self.crumblePosition)
# Check if player is caught by crumbling # Check if player is caught by crumbling
playerCaught = False playerCaught = False
if not player.isJumping: if not player.isJumping:
if (self.crumbleDirection > 0 and player.xPos <= self.crumblePosition) or \ if (self.crumbleDirection > 0 and player.xPos <= self.crumblePosition) or \
(self.crumbleDirection < 0 and player.xPos >= self.crumblePosition): (self.crumbleDirection < 0 and player.xPos >= self.crumblePosition):
playerCaught = True playerCaught = True
if playerCaught: if playerCaught:
if not player.isInvincible: if not player.isInvincible:
# Player is caught - instant death # Player is caught - instant death
@@ -111,14 +111,14 @@ class GraspingHands(Object):
if self.crumbleChannel: if self.crumbleChannel:
obj_stop(self.crumbleChannel) obj_stop(self.crumbleChannel)
self.crumbleChannel = None self.crumbleChannel = None
speak("The hands of the dead drag you down!") speak("The hands of the dead drag you down!")
player.set_health(0) player.set_health(0)
return True return True
# Player is invincible - no warning needed # Player is invincible - no warning needed
return False return False
def __del__(self): def __del__(self):
"""Cleanup when object is destroyed""" """Cleanup when object is destroyed"""
# Ensure sound is stopped when object is destroyed # Ensure sound is stopped when object is destroyed

View File

@@ -20,7 +20,7 @@ class GraveObject(Object):
def collect_grave_item(self, player): def collect_grave_item(self, player):
"""Handle collection of items from graves via ducking. """Handle collection of items from graves via ducking.
Returns: Returns:
bool: True if item was collected, False if player should die bool: True if item was collected, False if player should die
""" """
@@ -32,5 +32,5 @@ class GraveObject(Object):
if player.isDucking: if player.isDucking:
self.isCollected = True # Mark as collected when collection succeeds self.isCollected = True # Mark as collected when collection succeeds
return True return True
return False return False

View File

@@ -15,13 +15,13 @@ class ItemType(Enum):
class ItemProperties: class ItemProperties:
"""Manages item properties and availability""" """Manages item properties and availability"""
# Items that can appear in random drops # Items that can appear in random drops
RANDOM_ELIGIBLE = { RANDOM_ELIGIBLE = {
ItemType.HAND_OF_GLORY: "hand_of_glory", ItemType.HAND_OF_GLORY: "hand_of_glory",
ItemType.JACK_O_LANTERN: "jack_o_lantern" ItemType.JACK_O_LANTERN: "jack_o_lantern"
} }
# All possible items (including special ones) # All possible items (including special ones)
ALL_ITEMS = { ALL_ITEMS = {
ItemType.GUTS: "guts", ItemType.GUTS: "guts",
@@ -31,23 +31,23 @@ class ItemProperties:
ItemType.CAULDRON: "cauldron", ItemType.CAULDRON: "cauldron",
ItemType.WITCH_BROOM: "witch_broom" ItemType.WITCH_BROOM: "witch_broom"
} }
@staticmethod @staticmethod
def get_sound_name(item_type): def get_sound_name(item_type):
"""Convert enum to sound/asset name""" """Convert enum to sound/asset name"""
return ItemProperties.ALL_ITEMS.get(item_type) return ItemProperties.ALL_ITEMS.get(item_type)
@staticmethod @staticmethod
def get_random_item(): def get_random_item():
"""Get a random item from eligible items""" """Get a random item from eligible items"""
item_type = random.choice(list(ItemProperties.RANDOM_ELIGIBLE.keys())) item_type = random.choice(list(ItemProperties.RANDOM_ELIGIBLE.keys()))
return ItemProperties.get_sound_name(item_type) return ItemProperties.get_sound_name(item_type)
@staticmethod @staticmethod
def is_valid_item(item_name): def is_valid_item(item_name):
"""Check if an item name is valid""" """Check if an item name is valid"""
return item_name in [v for v in ItemProperties.ALL_ITEMS.values()] return item_name in [v for v in ItemProperties.ALL_ITEMS.values()]
@staticmethod @staticmethod
def get_item_type(item_name): def get_item_type(item_name):
"""Get ItemType enum from string name""" """Get ItemType enum from string name"""

View File

@@ -36,7 +36,7 @@ class Level:
# Get footstep sound for this level, default to 'footstep' if not specified # Get footstep sound for this level, default to 'footstep' if not specified
self.footstepSound = levelData.get("footstep_sound", "footstep") self.footstepSound = levelData.get("footstep_sound", "footstep")
# Pass footstep sound to player # Pass footstep sound to player
self.player.set_footstep_sound(self.footstepSound) self.player.set_footstep_sound(self.footstepSound)
@@ -77,7 +77,7 @@ class Level:
xPos = obj["x_range"] xPos = obj["x_range"]
else: else:
xPos = [obj["x"], obj["x"]] # Single position as range xPos = [obj["x"], obj["x"]] # Single position as range
# Check if this is a catapult # Check if this is a catapult
if obj.get("type") == "catapult": if obj.get("type") == "catapult":
catapult = Catapult( catapult = Catapult(
@@ -194,22 +194,22 @@ class Level:
def update_audio(self): def update_audio(self):
"""Update all audio and entity state.""" """Update all audio and entity state."""
currentTime = pygame.time.get_ticks() currentTime = pygame.time.get_ticks()
# Update regular objects and check for zombie spawning # Update regular objects and check for zombie spawning
for obj in self.objects: for obj in self.objects:
if not obj.isActive: if not obj.isActive:
continue continue
# Check for potential zombie spawn from graves # Check for potential zombie spawn from graves
if (obj.soundName == "grave" and if (obj.soundName == "grave" and
obj.zombieSpawnChance > 0 and obj.zombieSpawnChance > 0 and
not obj.hasSpawned): not obj.hasSpawned):
distance = abs(self.player.xPos - obj.xPos) distance = abs(self.player.xPos - obj.xPos)
if distance < 6: # Within 6 tiles if distance < 6: # Within 6 tiles
# Mark as checked before doing anything else to prevent multiple checks # Mark as checked before doing anything else to prevent multiple checks
obj.hasSpawned = True obj.hasSpawned = True
roll = random.randint(1, 100) roll = random.randint(1, 100)
if roll <= obj.zombieSpawnChance: if roll <= obj.zombieSpawnChance:
zombie = Enemy( zombie = Enemy(
@@ -224,7 +224,7 @@ class Level:
) )
self.enemies.append(zombie) self.enemies.append(zombie)
speak("A zombie emerges from the grave!") speak("A zombie emerges from the grave!")
# Handle object audio # Handle object audio
if obj.channel is not None: if obj.channel is not None:
obj.channel = obj_update(obj.channel, self.player.xPos, obj.xPos) obj.channel = obj_update(obj.channel, self.player.xPos, obj.xPos)
@@ -233,12 +233,12 @@ class Level:
obj.channel = obj_play(self.sounds, obj.soundName, self.player.xPos, obj.xPos) obj.channel = obj_play(self.sounds, obj.soundName, self.player.xPos, obj.xPos)
else: else:
obj.channel = obj_play(self.sounds, obj.soundName, self.player.xPos, obj.xPos) obj.channel = obj_play(self.sounds, obj.soundName, self.player.xPos, obj.xPos)
# Update enemies # Update enemies
for enemy in self.enemies: for enemy in self.enemies:
if not enemy.isActive: if not enemy.isActive:
continue continue
enemy.update(currentTime, self.player) enemy.update(currentTime, self.player)
# Only handle audio for non-vulnerability enemies # Only handle audio for non-vulnerability enemies
@@ -247,7 +247,7 @@ class Level:
enemy.channel = obj_play(self.sounds, enemy.enemyType, self.player.xPos, enemy.xPos) enemy.channel = obj_play(self.sounds, enemy.enemyType, self.player.xPos, enemy.xPos)
if enemy.channel is not None: if enemy.channel is not None:
enemy.channel = obj_update(enemy.channel, self.player.xPos, enemy.xPos) enemy.channel = obj_update(enemy.channel, self.player.xPos, enemy.xPos)
# Update catapults # Update catapults
for obj in self.objects: for obj in self.objects:
if isinstance(obj, Catapult): if isinstance(obj, Catapult):
@@ -272,7 +272,7 @@ class Level:
if not item.isActive: if not item.isActive:
speak(f"{item.soundName} got away!") speak(f"{item.soundName} got away!")
continue continue
# Check for item collection # Check for item collection
if abs(item._currentX - self.player.xPos) < 1 and self.player.isJumping: if abs(item._currentX - self.player.xPos) < 1 and self.player.isJumping:
play_sound(self.sounds[f'get_{item.soundName}']) play_sound(self.sounds[f'get_{item.soundName}'])
@@ -302,7 +302,7 @@ class Level:
obj.xPos >= attackRange[0] and obj.xPos >= attackRange[0] and
obj.xPos <= attackRange[1] and obj.xPos <= attackRange[1] and
self.player.isJumping): # Must be jumping to hit floating coffins self.player.isJumping): # Must be jumping to hit floating coffins
if obj.hit(self.player.xPos): if obj.hit(self.player.xPos):
self.bouncing_items.append(obj.dropped_item) self.bouncing_items.append(obj.dropped_item)
@@ -443,7 +443,7 @@ class Level:
# Push player back a bit # Push player back a bit
self.player.xPos -= 5 self.player.xPos -= 5
return False return False
# Level complete # Level complete
pygame.mixer.stop() pygame.mixer.stop()
play_sound(self.sounds['end_of_level']) play_sound(self.sounds['end_of_level'])
@@ -460,7 +460,7 @@ class Level:
if not proj.update(): if not proj.update():
self.projectiles.remove(proj) self.projectiles.remove(proj)
continue continue
# Check for enemy hits # Check for enemy hits
for enemy in self.enemies: for enemy in self.enemies:
if enemy.isActive and abs(proj.x - enemy.xPos) < 1: if enemy.isActive and abs(proj.x - enemy.xPos) < 1:
@@ -471,14 +471,14 @@ class Level:
if volume > 0: # Only play if within audible range if volume > 0: # Only play if within audible range
obj_play(self.sounds, 'pumpkin_splat', self.player.xPos, proj.x, loop=False) obj_play(self.sounds, 'pumpkin_splat', self.player.xPos, proj.x, loop=False)
break break
def throw_projectile(self): def throw_projectile(self):
"""Have player throw a projectile""" """Have player throw a projectile"""
proj_info = self.player.throw_projectile() proj_info = self.player.throw_projectile()
if proj_info is None: if proj_info is None:
speak("No jack o'lanterns to throw!") speak("No jack o'lanterns to throw!")
return return
self.projectiles.append(Projectile( self.projectiles.append(Projectile(
proj_info['type'], proj_info['type'],
proj_info['start_x'], proj_info['start_x'],

View File

@@ -17,17 +17,17 @@ class Object:
self.isActive = True self.isActive = True
# For collectibles in a range, track which positions have been collected # For collectibles in a range, track which positions have been collected
self.collectedPositions = set() self.collectedPositions = set()
@property @property
def xPos(self): def xPos(self):
"""Return center of range for audio positioning""" """Return center of range for audio positioning"""
return (self.xRange[0] + self.xRange[1]) / 2 return (self.xRange[0] + self.xRange[1]) / 2
def is_in_range(self, x): def is_in_range(self, x):
"""Check if a given x position is within this object's range""" """Check if a given x position is within this object's range"""
tolerance = 0.5 # Half a unit tolerance tolerance = 0.5 # Half a unit tolerance
return (self.xRange[0] - tolerance) <= x <= (self.xRange[1] + tolerance) return (self.xRange[0] - tolerance) <= x <= (self.xRange[1] + tolerance)
def collect_at_position(self, x): def collect_at_position(self, x):
"""Mark a specific position in the range as collected""" """Mark a specific position in the range as collected"""
self.collectedPositions.add(x) self.collectedPositions.add(x)

View File

@@ -20,7 +20,7 @@ class Player:
self.isRunning = False self.isRunning = False
self.runMultiplier = 1.5 # Same multiplier as jumping self.runMultiplier = 1.5 # Same multiplier as jumping
self.facingRight = True self.facingRight = True
# Stats and tracking # Stats and tracking
self._health = 10 self._health = 10
self._maxHealth = 10 self._maxHealth = 10
@@ -39,25 +39,25 @@ class Player:
self.lastStepTime = 0 self.lastStepTime = 0
self.isRunning = False self.isRunning = False
self.runMultiplier = 1.5 self.runMultiplier = 1.5
# Inventory system # Inventory system
self.inventory = [] self.inventory = []
self.collectedItems = [] self.collectedItems = []
self._coins = 0 self._coins = 0
self._jack_o_lantern_count = 0 self._jack_o_lantern_count = 0
self.shinBoneCount = 0 self.shinBoneCount = 0
# Combat related attributes # Combat related attributes
self.weapons = [] self.weapons = []
self.currentWeapon = None self.currentWeapon = None
self.isAttacking = False self.isAttacking = False
self.lastAttackTime = 0 self.lastAttackTime = 0
# Power-up states # Power-up states
self.isInvincible = False self.isInvincible = False
self.invincibilityStartTime = 0 self.invincibilityStartTime = 0
self.invincibilityDuration = 10000 # 10 seconds of invincibility self.invincibilityDuration = 10000 # 10 seconds of invincibility
# Initialize starting weapon (rusty shovel) # Initialize starting weapon (rusty shovel)
self.add_weapon(Weapon( self.add_weapon(Weapon(
name="rusty_shovel", name="rusty_shovel",
@@ -97,64 +97,64 @@ class Player:
if self.currentWeapon: if self.currentWeapon:
self.currentWeapon.attackDuration *= 0.5 # Restore attack speed self.currentWeapon.attackDuration *= 0.5 # Restore attack speed
del self.webPenaltyEndTime del self.webPenaltyEndTime
# Check invincibility status # Check invincibility status
if self.isInvincible: if self.isInvincible:
remaining_time = (self.invincibilityStartTime + self.invincibilityDuration - currentTime) / 1000 # Convert to seconds remaining_time = (self.invincibilityStartTime + self.invincibilityDuration - currentTime) / 1000 # Convert to seconds
# Handle countdown sounds # Handle countdown sounds
if not hasattr(self, '_last_countdown'): if not hasattr(self, '_last_countdown'):
self._last_countdown = 4 # Start counting from 4 to catch 3,2,1 self._last_countdown = 4 # Start counting from 4 to catch 3,2,1
current_second = int(remaining_time) current_second = int(remaining_time)
if current_second < self._last_countdown and current_second <= 3 and current_second > 0: if current_second < self._last_countdown and current_second <= 3 and current_second > 0:
play_sound(self.sounds['end_of_invincibility_warning']) play_sound(self.sounds['end_of_invincibility_warning'])
self._last_countdown = current_second self._last_countdown = current_second
# Check if invincibility has expired # Check if invincibility has expired
if currentTime - self.invincibilityStartTime >= self.invincibilityDuration: if currentTime - self.invincibilityStartTime >= self.invincibilityDuration:
self.isInvincible = False self.isInvincible = False
speak("Invincibility wore off!") speak("Invincibility wore off!")
del self._last_countdown # Clean up countdown tracker del self._last_countdown # Clean up countdown tracker
def start_invincibility(self): def start_invincibility(self):
"""Activate invincibility from Hand of Glory""" """Activate invincibility from Hand of Glory"""
self.isInvincible = True self.isInvincible = True
self.invincibilityStartTime = pygame.time.get_ticks() self.invincibilityStartTime = pygame.time.get_ticks()
if hasattr(self, '_last_countdown'): if hasattr(self, '_last_countdown'):
del self._last_countdown # Reset countdown if it exists del self._last_countdown # Reset countdown if it exists
def extra_life(self): def extra_life(self):
"""Increment lives by 1""" """Increment lives by 1"""
self._lives += 1 self._lives += 1
def get_jack_o_lanterns(self): def get_jack_o_lanterns(self):
"""Get number of jack o'lanterns""" """Get number of jack o'lanterns"""
return self._jack_o_lantern_count return self._jack_o_lantern_count
def add_jack_o_lantern(self): def add_jack_o_lantern(self):
"""Add a jack o'lantern""" """Add a jack o'lantern"""
self._jack_o_lantern_count += 1 self._jack_o_lantern_count += 1
def add_guts(self): def add_guts(self):
"""Apply guts, increase max_health by 2 if less than 20 else restore health""" """Apply guts, increase max_health by 2 if less than 20 else restore health"""
if self._maxHealth < 20: if self._maxHealth < 20:
self._maxHealth += 2 self._maxHealth += 2
else: else:
self._health = self._maxHealth self._health = self._maxHealth
def throw_projectile(self): def throw_projectile(self):
"""Throw a jack o'lantern if we have any""" """Throw a jack o'lantern if we have any"""
if self.get_jack_o_lanterns() <= 0: if self.get_jack_o_lanterns() <= 0:
return None return None
self._jack_o_lantern_count -= 1 self._jack_o_lantern_count -= 1
return { return {
'type': 'jack_o_lantern', 'type': 'jack_o_lantern',
'start_x': self.xPos, 'start_x': self.xPos,
'direction': 1 if self.facingRight else -1 'direction': 1 if self.facingRight else -1
} }
def get_step_distance(self): def get_step_distance(self):
"""Get step distance based on current speed""" """Get step distance based on current speed"""
if self.isRunning or self.isJumping: if self.isRunning or self.isJumping:
@@ -170,11 +170,11 @@ class Player:
def get_health(self): def get_health(self):
"""Get current health""" """Get current health"""
return self._health return self._health
def get_max_health(self): def get_max_health(self):
"""Get current max health""" """Get current max health"""
return self._maxHealth return self._maxHealth
def restore_health(self): def restore_health(self):
"""Restore health to maximum""" """Restore health to maximum"""
self._health = self._maxHealth self._health = self._maxHealth
@@ -192,13 +192,13 @@ class Player:
def set_health(self, value): def set_health(self, value):
"""Set health and handle death if needed.""" """Set health and handle death if needed."""
old_health = self._health old_health = self._health
# Oops, allow healing while invincible. # Oops, allow healing while invincible.
if self.isInvincible and value < old_health: if self.isInvincible and value < old_health:
return return
self._health = max(0, value) # Health can't go below 0 self._health = max(0, value) # Health can't go below 0
if self._health == 0 and old_health > 0: if self._health == 0 and old_health > 0:
self._lives -= 1 self._lives -= 1
# Stop all current sounds before playing death sound # Stop all current sounds before playing death sound
@@ -209,7 +209,7 @@ class Player:
pass pass
cut_scene(self.sounds, 'lose_a_life') cut_scene(self.sounds, 'lose_a_life')
def set_max_health(self, value): def set_max_health(self, value):
"""Set max health""" """Set max health"""
self._maxHealth = value self._maxHealth = value
@@ -217,22 +217,22 @@ class Player:
def get_coins(self): def get_coins(self):
"""Get remaining coins""" """Get remaining coins"""
return self._coins return self._coins
def get_lives(self): def get_lives(self):
"""Get remaining lives""" """Get remaining lives"""
return self._lives return self._lives
def add_weapon(self, weapon): def add_weapon(self, weapon):
"""Add a new weapon to inventory and equip if first weapon""" """Add a new weapon to inventory and equip if first weapon"""
self.weapons.append(weapon) self.weapons.append(weapon)
if len(self.weapons) == 1: # If this is our first weapon, equip it if len(self.weapons) == 1: # If this is our first weapon, equip it
self.equip_weapon(weapon) self.equip_weapon(weapon)
def equip_weapon(self, weapon): def equip_weapon(self, weapon):
"""Equip a specific weapon""" """Equip a specific weapon"""
if weapon in self.weapons: if weapon in self.weapons:
self.currentWeapon = weapon self.currentWeapon = weapon
def add_item(self, item): def add_item(self, item):
"""Add an item to inventory""" """Add an item to inventory"""
self.inventory.append(item) self.inventory.append(item)
@@ -245,7 +245,7 @@ class Player:
self.lastAttackTime = currentTime self.lastAttackTime = currentTime
return True return True
return False return False
def get_attack_range(self, currentTime): def get_attack_range(self, currentTime):
"""Get the current attack's range based on position and facing direction""" """Get the current attack's range based on position and facing direction"""
if not self.currentWeapon or not self.currentWeapon.is_attack_active(currentTime): if not self.currentWeapon or not self.currentWeapon.is_attack_active(currentTime):

View File

@@ -29,7 +29,7 @@ class PowerUp(Object):
# Update position # Update position
new_x = self._currentX + self.direction * self.speed new_x = self._currentX + self.direction * self.speed
# Check boundaries and bounce if needed # Check boundaries and bounce if needed
if new_x < self.left_boundary: if new_x < self.left_boundary:
self._currentX = self.left_boundary self._currentX = self.left_boundary
@@ -55,7 +55,7 @@ class PowerUp(Object):
return False return False
return True return True
def apply_effect(self, player): def apply_effect(self, player):
"""Apply the item's effect when collected""" """Apply the item's effect when collected"""
if self.item_type == 'hand_of_glory': if self.item_type == 'hand_of_glory':
@@ -99,7 +99,7 @@ class PowerUp(Object):
# Tell level to spawn a spider # Tell level to spawn a spider
if hasattr(self, 'level'): if hasattr(self, 'level'):
self.level.spawn_spider(self.xPos, self.yPos) self.level.spawn_spider(self.xPos, self.yPos)
# Stop movement sound when collected # Stop movement sound when collected
if self.channel: if self.channel:
self.channel.stop() self.channel.stop()

View File

@@ -10,21 +10,21 @@ class Projectile:
self.damage = 5 # All projectiles do same damage for now self.damage = 5 # All projectiles do same damage for now
self.range = 12 # Maximum travel distance in tiles self.range = 12 # Maximum travel distance in tiles
self.start_x = start_x self.start_x = start_x
def update(self): def update(self):
"""Update projectile position and check if it should still exist""" """Update projectile position and check if it should still exist"""
if not self.isActive: if not self.isActive:
return False return False
self.x += self.direction * self.speed self.x += self.direction * self.speed
# Check if projectile has gone too far # Check if projectile has gone too far
if abs(self.x - self.start_x) > self.range: if abs(self.x - self.start_x) > self.range:
self.isActive = False self.isActive = False
return False return False
return True return True
def hit_enemy(self, enemy): def hit_enemy(self, enemy):
"""Handle hitting an enemy""" """Handle hitting an enemy"""
enemy.take_damage(self.damage) enemy.take_damage(self.damage)

View File

@@ -7,7 +7,7 @@ from src.object import Object
class SkullStorm(Object): class SkullStorm(Object):
"""Handles falling skulls within a specified range.""" """Handles falling skulls within a specified range."""
def __init__(self, xRange, y, sounds, damage, maxSkulls=3, minFreq=2, maxFreq=5): def __init__(self, xRange, y, sounds, damage, maxSkulls=3, minFreq=2, maxFreq=5):
super().__init__( super().__init__(
xRange, xRange,
@@ -22,7 +22,7 @@ class SkullStorm(Object):
self.maxSkulls = maxSkulls self.maxSkulls = maxSkulls
self.minFreq = minFreq * 1000 # Convert to milliseconds self.minFreq = minFreq * 1000 # Convert to milliseconds
self.maxFreq = maxFreq * 1000 self.maxFreq = maxFreq * 1000
self.activeSkulls = [] # List of currently falling skulls self.activeSkulls = [] # List of currently falling skulls
self.lastSkullTime = 0 self.lastSkullTime = 0
self.nextSkullDelay = random.randint(self.minFreq, self.maxFreq) self.nextSkullDelay = random.randint(self.minFreq, self.maxFreq)
@@ -32,7 +32,7 @@ class SkullStorm(Object):
"""Update all active skulls and potentially spawn new ones.""" """Update all active skulls and potentially spawn new ones."""
if not self.isActive: if not self.isActive:
return return
# Check if player has entered range # Check if player has entered range
inRange = self.xRange[0] <= player.xPos <= self.xRange[1] inRange = self.xRange[0] <= player.xPos <= self.xRange[1]
if inRange and not self.playerInRange: if inRange and not self.playerInRange:
@@ -43,16 +43,16 @@ class SkullStorm(Object):
# Player just left range # Player just left range
self.playerInRange = False self.playerInRange = False
speak("Skull storm ended.") speak("Skull storm ended.")
# Clear any active skulls when player leaves the range # Clear any active skulls when player leaves the range
for skull in self.activeSkulls[:]: for skull in self.activeSkulls[:]:
if skull['channel']: if skull['channel']:
obj_stop(skull['channel']) obj_stop(skull['channel'])
self.activeSkulls = [] # Reset the list of active skulls self.activeSkulls = [] # Reset the list of active skulls
if not inRange: if not inRange:
return return
# Update existing skulls # Update existing skulls
for skull in self.activeSkulls[:]: # Copy list to allow removal for skull in self.activeSkulls[:]: # Copy list to allow removal
if currentTime >= skull['land_time']: if currentTime >= skull['land_time']:
@@ -64,7 +64,7 @@ class SkullStorm(Object):
timeElapsed = currentTime - skull['start_time'] timeElapsed = currentTime - skull['start_time']
fallProgress = timeElapsed / skull['fall_duration'] fallProgress = timeElapsed / skull['fall_duration']
currentY = self.yPos * (1 - fallProgress) currentY = self.yPos * (1 - fallProgress)
skull['channel'] = play_random_falling( skull['channel'] = play_random_falling(
self.sounds, self.sounds,
'falling_skull', 'falling_skull',
@@ -74,21 +74,21 @@ class SkullStorm(Object):
currentY, currentY,
existingChannel=skull['channel'] existingChannel=skull['channel']
) )
# Check if we should spawn a new skull # Check if we should spawn a new skull
if (len(self.activeSkulls) < self.maxSkulls and if (len(self.activeSkulls) < self.maxSkulls and
currentTime - self.lastSkullTime >= self.nextSkullDelay): currentTime - self.lastSkullTime >= self.nextSkullDelay):
self.spawn_skull(currentTime) self.spawn_skull(currentTime)
def spawn_skull(self, currentTime): def spawn_skull(self, currentTime):
"""Spawn a new falling skull at a random position within range.""" """Spawn a new falling skull at a random position within range."""
# Reset timing # Reset timing
self.lastSkullTime = currentTime self.lastSkullTime = currentTime
self.nextSkullDelay = random.randint(self.minFreq, self.maxFreq) self.nextSkullDelay = random.randint(self.minFreq, self.maxFreq)
# Calculate fall duration based on height (higher = longer fall) # Calculate fall duration based on height (higher = longer fall)
fallDuration = self.yPos * 100 # 100ms per unit of height fallDuration = self.yPos * 100 # 100ms per unit of height
# Create new skull # Create new skull
skull = { skull = {
'x': random.uniform(self.xRange[0], self.xRange[1]), 'x': random.uniform(self.xRange[0], self.xRange[1]),

View File

@@ -10,10 +10,10 @@ class StatTracker:
'Items collected': 0, 'Items collected': 0,
'Total time': 0 'Total time': 0
} }
# Create level stats from total (shallow copy is fine here) # Create level stats from total (shallow copy is fine here)
self.level = self.total.copy() self.level = self.total.copy()
self.total['levelsCompleted'] = 0 self.total['levelsCompleted'] = 0
def reset_level(self): def reset_level(self):
@@ -27,18 +27,18 @@ class StatTracker:
self.level[key] = [] self.level[key] = []
elif self.level[key] is None: elif self.level[key] is None:
self.level[key] = None self.level[key] = None
def update_stat(self, statName, value=1, levelOnly=False): def update_stat(self, statName, value=1, levelOnly=False):
"""Update a stat in both level and total (unless levelOnly is True)""" """Update a stat in both level and total (unless levelOnly is True)"""
if statName in self.level: if statName in self.level:
self.level[statName] += value self.level[statName] += value
if not levelOnly and statName in self.total: if not levelOnly and statName in self.total:
self.total[statName] += value self.total[statName] += value
def get_level_stat(self, statName): def get_level_stat(self, statName):
"""Get a level stat""" """Get a level stat"""
return self.level.get(statName, 0) return self.level.get(statName, 0)
def get_total_stat(self, statName): def get_total_stat(self, statName):
"""Get a total stat""" """Get a total stat"""
return self.total.get(statName, 0) return self.total.get(statName, 0)

View File

@@ -11,7 +11,7 @@ class Weapon:
self.attackDuration = attackDuration # Milliseconds the attack is active self.attackDuration = attackDuration # Milliseconds the attack is active
self.lastAttackTime = 0 self.lastAttackTime = 0
self.hitEnemies = set() self.hitEnemies = set()
@classmethod @classmethod
def create_nunchucks(cls): def create_nunchucks(cls):
"""Create the nunchucks weapon""" """Create the nunchucks weapon"""
@@ -41,7 +41,7 @@ class Weapon:
def can_attack(self, currentTime): def can_attack(self, currentTime):
"""Check if enough time has passed since last attack""" """Check if enough time has passed since last attack"""
return currentTime - self.lastAttackTime >= self.cooldown return currentTime - self.lastAttackTime >= self.cooldown
def get_attack_range(self, playerPos, facingRight): def get_attack_range(self, playerPos, facingRight):
"""Calculate the area that this attack would hit""" """Calculate the area that this attack would hit"""
if facingRight: if facingRight:
@@ -56,7 +56,7 @@ class Weapon:
self.hitEnemies.clear() # Clear hit enemies for new attack self.hitEnemies.clear() # Clear hit enemies for new attack
return True return True
return False return False
def is_attack_active(self, currentTime): def is_attack_active(self, currentTime):
"""Check if the attack is still in its active frames""" """Check if the attack is still in its active frames"""
timeSinceAttack = currentTime - self.lastAttackTime timeSinceAttack = currentTime - self.lastAttackTime

View File

@@ -22,14 +22,14 @@ class WickedQuest:
self.player = None self.player = None
self.currentGame = None self.currentGame = None
self.runLock = False # Toggle behavior of the run keys self.runLock = False # Toggle behavior of the run keys
def load_level(self, levelNumber): def load_level(self, levelNumber):
"""Load a level from its JSON file.""" """Load a level from its JSON file."""
levelFile = get_level_path(self.currentGame, levelNumber) levelFile = get_level_path(self.currentGame, levelNumber)
try: try:
with open(levelFile, 'r') as f: with open(levelFile, 'r') as f:
levelData = json.load(f) levelData = json.load(f)
# Create player if this is the first level # Create player if this is the first level
if self.player is None: if self.player is None:
self.player = Player(levelData["player_start"]["x"], self.player = Player(levelData["player_start"]["x"],
@@ -51,11 +51,11 @@ class WickedQuest:
self.player.moveSpeed *= 2 # Restore normal speed self.player.moveSpeed *= 2 # Restore normal speed
if self.player.currentWeapon: if self.player.currentWeapon:
self.player.currentWeapon.attackDuration *= 0.5 # Restore normal attack speed self.player.currentWeapon.attackDuration *= 0.5 # Restore normal attack speed
# Pass existing player to new level # Pass existing player to new level
pygame.event.clear() pygame.event.clear()
self.currentLevel = Level(levelData, self.sounds, self.player) self.currentLevel = Level(levelData, self.sounds, self.player)
return True return True
except FileNotFoundError: except FileNotFoundError:
return False return False
@@ -63,14 +63,14 @@ class WickedQuest:
def validate_levels(self): def validate_levels(self):
"""Check if level files have valid JSON.""" """Check if level files have valid JSON."""
errors = [] errors = []
# Check levels from 1 until no more files are found # Check levels from 1 until no more files are found
levelNumber = 1 levelNumber = 1
while True: while True:
levelPath = get_level_path(self.currentGame, levelNumber) levelPath = get_level_path(self.currentGame, levelNumber)
if not os.path.exists(levelPath): if not os.path.exists(levelPath):
break break
try: try:
with open(levelPath, 'r') as f: with open(levelPath, 'r') as f:
# This will raise an exception if JSON is invalid # This will raise an exception if JSON is invalid
@@ -79,9 +79,9 @@ class WickedQuest:
errors.append(f"Level {levelNumber}: Invalid JSON format - {str(e)}") errors.append(f"Level {levelNumber}: Invalid JSON format - {str(e)}")
except Exception as e: except Exception as e:
errors.append(f"Level {levelNumber}: Error reading file - {str(e)}") errors.append(f"Level {levelNumber}: Error reading file - {str(e)}")
levelNumber += 1 levelNumber += 1
return errors return errors
def handle_input(self): def handle_input(self):
@@ -89,7 +89,7 @@ class WickedQuest:
keys = pygame.key.get_pressed() keys = pygame.key.get_pressed()
player = self.currentLevel.player player = self.currentLevel.player
currentTime = pygame.time.get_ticks() currentTime = pygame.time.get_ticks()
# Update running and ducking states # Update running and ducking states
if (keys[pygame.K_s] or keys[pygame.K_DOWN]) and not player.isDucking: if (keys[pygame.K_s] or keys[pygame.K_DOWN]) and not player.isDucking:
player.duck() player.duck()
@@ -106,7 +106,7 @@ class WickedQuest:
# Track movement distance for this frame # Track movement distance for this frame
movementDistance = 0 movementDistance = 0
# Horizontal movement # Horizontal movement
if keys[pygame.K_a] or keys[pygame.K_LEFT]: # Left if keys[pygame.K_a] or keys[pygame.K_LEFT]: # Left
movementDistance = currentSpeed movementDistance = currentSpeed
@@ -116,7 +116,7 @@ class WickedQuest:
movementDistance = currentSpeed movementDistance = currentSpeed
player.xPos += currentSpeed player.xPos += currentSpeed
player.facingRight = True player.facingRight = True
# Handle footsteps # Handle footsteps
if movementDistance > 0 and not player.isJumping: if movementDistance > 0 and not player.isJumping:
player.distanceSinceLastStep += movementDistance player.distanceSinceLastStep += movementDistance
@@ -147,13 +147,13 @@ class WickedQuest:
# Handle attack with either CTRL key # Handle attack with either CTRL key
if (keys[pygame.K_LCTRL] or keys[pygame.K_RCTRL]) and player.start_attack(currentTime): if (keys[pygame.K_LCTRL] or keys[pygame.K_RCTRL]) and player.start_attack(currentTime):
play_sound(self.sounds[player.currentWeapon.attackSound]) play_sound(self.sounds[player.currentWeapon.attackSound])
# Handle jumping # Handle jumping
if (keys[pygame.K_w] or keys[pygame.K_UP]) and not player.isJumping: if (keys[pygame.K_w] or keys[pygame.K_UP]) and not player.isJumping:
player.isJumping = True player.isJumping = True
player.jumpStartTime = currentTime player.jumpStartTime = currentTime
play_sound(self.sounds['jump']) play_sound(self.sounds['jump'])
# Check if jump should end # Check if jump should end
if player.isJumping and currentTime - player.jumpStartTime >= player.jumpDuration: if player.isJumping and currentTime - player.jumpStartTime >= player.jumpDuration:
player.isJumping = False player.isJumping = False
@@ -167,12 +167,12 @@ class WickedQuest:
# Convert time from milliseconds to minutes:seconds # Convert time from milliseconds to minutes:seconds
minutes = timeTaken // 60000 minutes = timeTaken // 60000
seconds = (timeTaken % 60000) // 1000 seconds = (timeTaken % 60000) // 1000
# Update time in stats # Update time in stats
self.currentLevel.player.stats.update_stat('Total time', timeTaken, levelOnly=True) self.currentLevel.player.stats.update_stat('Total time', timeTaken, levelOnly=True)
report = [f"Time taken: {minutes} minutes and {seconds} seconds"] report = [f"Time taken: {minutes} minutes and {seconds} seconds"]
# Add all level stats # Add all level stats
for key in self.currentLevel.player.stats.level: for key in self.currentLevel.player.stats.level:
if key != 'Total time': # Skip time since we already displayed it if key != 'Total time': # Skip time since we already displayed it
@@ -195,10 +195,10 @@ class WickedQuest:
"""Display game over screen with statistics.""" """Display game over screen with statistics."""
minutes = timeTaken // 60000 minutes = timeTaken // 60000
seconds = (timeTaken % 60000) // 1000 seconds = (timeTaken % 60000) // 1000
report = ["Game Over!"] report = ["Game Over!"]
report.append(f"Time taken: {minutes} minutes and {seconds} seconds") report.append(f"Time taken: {minutes} minutes and {seconds} seconds")
# Add all total stats # Add all total stats
for key in self.currentLevel.player.stats.total: for key in self.currentLevel.player.stats.total:
if key not in ['Total time', 'levelsCompleted']: # Skip these if key not in ['Total time', 'levelsCompleted']: # Skip these
@@ -222,14 +222,14 @@ class WickedQuest:
while True: while True:
currentTime = pygame.time.get_ticks() currentTime = pygame.time.get_ticks()
pygame.event.pump() pygame.event.pump()
# Game volume controls # Game volume controls
for event in pygame.event.get(): for event in pygame.event.get():
if event.type == pygame.KEYDOWN: if event.type == pygame.KEYDOWN:
# Check for Alt modifier # Check for Alt modifier
mods = pygame.key.get_mods() mods = pygame.key.get_mods()
altPressed = mods & pygame.KMOD_ALT altPressed = mods & pygame.KMOD_ALT
if event.key == pygame.K_ESCAPE: if event.key == pygame.K_ESCAPE:
try: try:
pygame.mixer.music.stop() pygame.mixer.music.stop()
@@ -260,11 +260,11 @@ class WickedQuest:
self.currentLevel.player.update(currentTime) self.currentLevel.player.update(currentTime)
self.handle_input() self.handle_input()
self.currentLevel.update_audio() self.currentLevel.update_audio()
# Handle combat and projectiles # Handle combat and projectiles
self.currentLevel.handle_combat(currentTime) self.currentLevel.handle_combat(currentTime)
self.currentLevel.handle_projectiles(currentTime) self.currentLevel.handle_projectiles(currentTime)
# Check for death first # Check for death first
if self.currentLevel.player.get_health() <= 0: if self.currentLevel.player.get_health() <= 0:
if self.currentLevel.player.get_lives() <= 0: if self.currentLevel.player.get_lives() <= 0:
@@ -307,7 +307,7 @@ class WickedQuest:
self.display_game_over(totalTime) self.display_game_over(totalTime)
return return
clock.tick(60) # 60 FPS clock.tick(60) # 60 FPS
def run(self): def run(self):
@@ -345,7 +345,7 @@ class WickedQuest:
for i, entry in enumerate(scores, 1): for i, entry in enumerate(scores, 1):
scoreStr = f"{i}. {entry['name']}: {entry['score']}" scoreStr = f"{i}. {entry['name']}: {entry['score']}"
lines.append(scoreStr) lines.append(scoreStr)
pygame.event.clear() pygame.event.clear()
display_text(lines) display_text(lines)
elif choice == "learn_sounds": elif choice == "learn_sounds":