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

View File

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

View File

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

View File

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

View File

@@ -6,7 +6,7 @@ from src.object import Object
class GraspingHands(Object):
"""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):
super().__init__(
xRange,
@@ -19,7 +19,7 @@ class GraspingHands(Object):
self.sounds = sounds
self.delay = delay # Delay in milliseconds before ground starts crumbling
self.crumble_speed = crumble_speed # How fast the crumbling catches up (tiles per frame)
# State tracking
self.isTriggered = False # Has the player entered 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.entryFromRight = False # Which side did player enter from
self.crumbleDirection = 1 # Direction the crumbling moves (1=right, -1=left)
def trigger(self, currentTime, playerX):
"""Trigger the grasping hands when player enters range"""
if not self.isTriggered:
self.isTriggered = True
self.triggerTime = currentTime
# Determine which side player entered from
if playerX > (self.xRange[0] + self.xRange[1]) / 2:
# Player entered from right side
@@ -47,48 +47,48 @@ class GraspingHands(Object):
self.entryFromRight = False
self.crumblePosition = self.xRange[0] # Start crumbling from left boundary
self.crumbleDirection = 1 # Crumble moves right
self.isReset = False
# Play initial warning sound
play_sound(self.sounds['grasping_hands_start'])
speak("The ground crumbles as the dead reach for you.")
def reset(self):
"""Reset the trap when player leaves the range"""
if not self.isReset:
self.isTriggered = False
self.crumblePosition = 0
self.isReset = True
# Stop the looping crumble sound if it's playing
if self.crumbleChannel:
obj_stop(self.crumbleChannel)
self.crumbleChannel = None
# Play the end sound
play_sound(self.sounds['grasping_hands_end'])
def update(self, currentTime, player):
"""Update the grasping hands trap state"""
if not self.isActive:
return False
# Check if player is in range
isInRange = self.xRange[0] <= player.xPos <= self.xRange[1]
# Handle player entering/exiting range
if isInRange and not self.isTriggered:
self.trigger(currentTime, player.xPos)
elif not isInRange and self.isTriggered:
self.reset()
return False
# If triggered and delay has passed, start crumbling
if self.isTriggered and currentTime - self.triggerTime >= self.delay:
# Update crumble position based on direction
self.crumblePosition += self.crumble_speed * self.crumbleDirection
# 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
@@ -96,14 +96,14 @@ class GraspingHands(Object):
else:
# Update the sound position
self.crumbleChannel = obj_update(self.crumbleChannel, player.xPos, self.crumblePosition)
# 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):
playerCaught = True
if playerCaught:
if not player.isInvincible:
# Player is caught - instant death
@@ -111,14 +111,14 @@ class GraspingHands(Object):
if self.crumbleChannel:
obj_stop(self.crumbleChannel)
self.crumbleChannel = None
speak("The hands of the dead drag you down!")
player.set_health(0)
return True
# Player is invincible - no warning needed
return False
def __del__(self):
"""Cleanup 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):
"""Handle collection of items from graves via ducking.
Returns:
bool: True if item was collected, False if player should die
"""
@@ -32,5 +32,5 @@ class GraveObject(Object):
if player.isDucking:
self.isCollected = True # Mark as collected when collection succeeds
return True
return False

View File

@@ -15,13 +15,13 @@ class ItemType(Enum):
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"
}
# All possible items (including special ones)
ALL_ITEMS = {
ItemType.GUTS: "guts",
@@ -31,23 +31,23 @@ class ItemProperties:
ItemType.CAULDRON: "cauldron",
ItemType.WITCH_BROOM: "witch_broom"
}
@staticmethod
def get_sound_name(item_type):
"""Convert enum to sound/asset name"""
return ItemProperties.ALL_ITEMS.get(item_type)
@staticmethod
def get_random_item():
"""Get a random item from eligible items"""
item_type = random.choice(list(ItemProperties.RANDOM_ELIGIBLE.keys()))
return ItemProperties.get_sound_name(item_type)
@staticmethod
def is_valid_item(item_name):
"""Check if an item name is valid"""
return item_name in [v for v in ItemProperties.ALL_ITEMS.values()]
@staticmethod
def get_item_type(item_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
self.footstepSound = levelData.get("footstep_sound", "footstep")
# Pass footstep sound to player
self.player.set_footstep_sound(self.footstepSound)
@@ -77,7 +77,7 @@ class Level:
xPos = obj["x_range"]
else:
xPos = [obj["x"], obj["x"]] # Single position as range
# Check if this is a catapult
if obj.get("type") == "catapult":
catapult = Catapult(
@@ -194,22 +194,22 @@ class Level:
def update_audio(self):
"""Update all audio and entity state."""
currentTime = pygame.time.get_ticks()
# Update regular objects and check for zombie spawning
for obj in self.objects:
if not obj.isActive:
continue
# Check for potential zombie spawn from graves
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
# Mark as checked before doing anything else to prevent multiple checks
obj.hasSpawned = True
roll = random.randint(1, 100)
if roll <= obj.zombieSpawnChance:
zombie = Enemy(
@@ -224,7 +224,7 @@ class Level:
)
self.enemies.append(zombie)
speak("A zombie emerges from the grave!")
# Handle object audio
if obj.channel is not None:
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)
else:
obj.channel = obj_play(self.sounds, obj.soundName, self.player.xPos, obj.xPos)
# Update enemies
for enemy in self.enemies:
if not enemy.isActive:
continue
enemy.update(currentTime, self.player)
# 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)
if enemy.channel is not None:
enemy.channel = obj_update(enemy.channel, self.player.xPos, enemy.xPos)
# Update catapults
for obj in self.objects:
if isinstance(obj, Catapult):
@@ -272,7 +272,7 @@ class Level:
if not item.isActive:
speak(f"{item.soundName} got away!")
continue
# Check for item collection
if abs(item._currentX - self.player.xPos) < 1 and self.player.isJumping:
play_sound(self.sounds[f'get_{item.soundName}'])
@@ -302,7 +302,7 @@ class Level:
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)
@@ -443,7 +443,7 @@ class Level:
# Push player back a bit
self.player.xPos -= 5
return False
# Level complete
pygame.mixer.stop()
play_sound(self.sounds['end_of_level'])
@@ -460,7 +460,7 @@ class Level:
if not proj.update():
self.projectiles.remove(proj)
continue
# Check for enemy hits
for enemy in self.enemies:
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
obj_play(self.sounds, 'pumpkin_splat', self.player.xPos, proj.x, loop=False)
break
def throw_projectile(self):
"""Have player throw a projectile"""
proj_info = self.player.throw_projectile()
if proj_info is None:
speak("No jack o'lanterns to throw!")
return
self.projectiles.append(Projectile(
proj_info['type'],
proj_info['start_x'],

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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