diff --git a/src/catapult.py b/src/catapult.py index 69aea38..a68eb14 100644 --- a/src/catapult.py +++ b/src/catapult.py @@ -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) diff --git a/src/coffin.py b/src/coffin.py index d30dfa9..34f3375 100644 --- a/src/coffin.py +++ b/src/coffin.py @@ -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, diff --git a/src/enemy.py b/src/enemy.py index 17cd1e2..157b084 100644 --- a/src/enemy.py +++ b/src/enemy.py @@ -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, diff --git a/src/game_selection.py b/src/game_selection.py index e954489..44289c4 100644 --- a/src/game_selection.py +++ b/src/game_selection.py @@ -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 """ diff --git a/src/grasping_hands.py b/src/grasping_hands.py index 1c72dcd..b6b9798 100644 --- a/src/grasping_hands.py +++ b/src/grasping_hands.py @@ -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 diff --git a/src/grave.py b/src/grave.py index 544dad9..12c1847 100644 --- a/src/grave.py +++ b/src/grave.py @@ -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 diff --git a/src/item_types.py b/src/item_types.py index e27931e..5676f66 100644 --- a/src/item_types.py +++ b/src/item_types.py @@ -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""" diff --git a/src/level.py b/src/level.py index cbb43c2..fb23260 100644 --- a/src/level.py +++ b/src/level.py @@ -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'], diff --git a/src/object.py b/src/object.py index abd5153..b720d12 100644 --- a/src/object.py +++ b/src/object.py @@ -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) diff --git a/src/player.py b/src/player.py index 23fe659..8d750ee 100644 --- a/src/player.py +++ b/src/player.py @@ -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): diff --git a/src/powerup.py b/src/powerup.py index bbeaad0..dc7bea9 100644 --- a/src/powerup.py +++ b/src/powerup.py @@ -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() diff --git a/src/projectile.py b/src/projectile.py index 9e74e89..783e193 100644 --- a/src/projectile.py +++ b/src/projectile.py @@ -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) diff --git a/src/skull_storm.py b/src/skull_storm.py index 5fa812b..ca79c92 100644 --- a/src/skull_storm.py +++ b/src/skull_storm.py @@ -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]), diff --git a/src/stat_tracker.py b/src/stat_tracker.py index 92a8321..457f12b 100644 --- a/src/stat_tracker.py +++ b/src/stat_tracker.py @@ -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) diff --git a/src/weapon.py b/src/weapon.py index 9aee1b1..8c43f45 100644 --- a/src/weapon.py +++ b/src/weapon.py @@ -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 diff --git a/wicked_quest.py b/wicked_quest.py index 956221a..2b60a65 100755 --- a/wicked_quest.py +++ b/wicked_quest.py @@ -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":