From 1d033e067ae4ae9a536b6fab3ec4f49a98e064b8 Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Mon, 3 Feb 2025 02:17:53 -0500 Subject: [PATCH] Updated libstormgames submodule. Added skull storms. Fixed no jack-o-lanterns reported when there actually were. Fixed player being recreated on each new level thus resetting all stats. --- libstormgames | 2 +- sounds/falling_skull1.ogg | 3 + sounds/skull_lands.ogg | 3 + sounds/skull_storm.ogg | 3 + sounds/throw_jack_o_lantern.ogg | 3 + src/level.py | 63 ++++++++++++------ src/player.py | 35 ++++------ src/powerup.py | 12 ++-- src/skull_storm.py | 112 ++++++++++++++++++++++++++++++++ 9 files changed, 186 insertions(+), 50 deletions(-) create mode 100644 sounds/falling_skull1.ogg create mode 100644 sounds/skull_lands.ogg create mode 100644 sounds/skull_storm.ogg create mode 100644 sounds/throw_jack_o_lantern.ogg create mode 100644 src/skull_storm.py diff --git a/libstormgames b/libstormgames index d5c79c0..658709e 160000 --- a/libstormgames +++ b/libstormgames @@ -1 +1 @@ -Subproject commit d5c79c0770867c21418d10d3dff7c60b595b6547 +Subproject commit 658709ebcec4a66745cfe8b32047ea20d0ef185d diff --git a/sounds/falling_skull1.ogg b/sounds/falling_skull1.ogg new file mode 100644 index 0000000..615fd0f --- /dev/null +++ b/sounds/falling_skull1.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:58c8752f5381ce8d4a79345948752337daf3f4c66433bff9298de4c37aa13617 +size 24208 diff --git a/sounds/skull_lands.ogg b/sounds/skull_lands.ogg new file mode 100644 index 0000000..a0bfe34 --- /dev/null +++ b/sounds/skull_lands.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fecf0bb9637f23a8cdd26393e2b54d20d05f62a51e9ed37c326684e34e3177b2 +size 8803 diff --git a/sounds/skull_storm.ogg b/sounds/skull_storm.ogg new file mode 100644 index 0000000..51b0534 --- /dev/null +++ b/sounds/skull_storm.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3652a77e86dd1a0a3dfcfcb414bbc1f71f5ee498a1080c93b43607483c5b0727 +size 20962 diff --git a/sounds/throw_jack_o_lantern.ogg b/sounds/throw_jack_o_lantern.ogg new file mode 100644 index 0000000..c85f744 --- /dev/null +++ b/sounds/throw_jack_o_lantern.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:61d311edc322bc3c2931d7306169201cd31c6ef3e7f3d3749e355d0ea851793d +size 11849 diff --git a/src/level.py b/src/level.py index ba94c43..375ca14 100644 --- a/src/level.py +++ b/src/level.py @@ -8,15 +8,17 @@ from src.object import Object from src.player import Player from src.projectile import Projectile from src.powerup import PowerUp +from src.skull_storm import SkullStorm + class Level: - def __init__(self, levelData, sounds): + def __init__(self, levelData, sounds, player): self.sounds = sounds self.objects = [] self.enemies = [] self.bouncing_items = [] self.projectiles = [] # Track active projectiles - self.player = Player(levelData["player_start"]["x"], levelData["player_start"]["y"], sounds) + self.player = player self.edge_warning_channel = None self.weapon_hit_channel = None self.leftBoundary = levelData["boundaries"]["left"] @@ -53,6 +55,18 @@ class Level: firingRange=obj.get("range", 20) ) self.objects.append(catapult) + # Check if this is a skull storm + elif obj.get("type") == "skull_storm": + skullStorm = SkullStorm( + xPos, + obj["y"], + self.sounds, + obj.get("damage", 5), + obj.get("maximum_skulls", 3), + obj.get("frequency", {}).get("min", 2), + obj.get("frequency", {}).get("max", 5) + ) + self.objects.append(skullStorm) # Check if this is a coffin elif obj.get("type") == "coffin": coffin = CoffinObject( @@ -120,15 +134,13 @@ class Level: speak("A zombie emerges from the grave!") # Handle object audio - if not obj.isStatic: - if obj.channel is None or not obj.channel.get_busy(): - obj.channel = obj_play(self.sounds, obj.soundName, self.player.xPos, obj.xPos) - else: - if obj.channel is None: - obj.channel = obj_play(self.sounds, obj.soundName, self.player.xPos, obj.xPos) - if obj.channel is not None: obj.channel = obj_update(obj.channel, self.player.xPos, obj.xPos) + elif obj.soundName: # Only try to play sound if soundName is not empty + if not obj.isStatic: + 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: @@ -147,6 +159,11 @@ class Level: if isinstance(obj, Catapult): obj.update(currentTime, self.player) + # Update skull storms + for obj in self.objects: + if isinstance(obj, SkullStorm): + obj.update(currentTime, self.player) + # Update bouncing items for item in self.bouncing_items[:]: # Copy list to allow removal if not item.update(currentTime): @@ -250,23 +267,31 @@ 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: proj.hit_enemy(enemy) self.projectiles.remove(proj) + # Calculate volume and pan for splat sound based on final position + volume, left, right = calculate_volume_and_pan(self.player.xPos, proj.x) + if volume > 0: # Only play if within audible range + channel = self.sounds["pumpkin_splat"].play() + if channel: + channel.set_volume(volume * left, volume * right) break def throw_projectile(self): """Have player throw a projectile""" proj_info = self.player.throw_projectile() - if proj_info: - self.projectiles.append(Projectile( - proj_info['type'], - proj_info['start_x'], - proj_info['direction'] - )) - # Play throw sound - if f"{proj_info['type']}_throw" in self.sounds: - self.sounds[f"{proj_info['type']}_throw"].play() + if proj_info is None: + speak("No jack o'lanterns to throw!") + return + + self.projectiles.append(Projectile( + proj_info['type'], + proj_info['start_x'], + proj_info['direction'] + )) + # Play throw sound + self.sounds['throw_jack_o_lantern'].play() diff --git a/src/player.py b/src/player.py index 32ce21b..b647120 100644 --- a/src/player.py +++ b/src/player.py @@ -26,6 +26,7 @@ class Player: self.inventory = [] self.collectedItems = [] self._coins = 0 + self._jack_o_lantern_count = 0 # Combat related attributes self.weapons = [] @@ -38,9 +39,6 @@ class Player: self.invincibilityStartTime = 0 self.invincibilityDuration = 5000 # 5 seconds of invincibility - # Projectiles - self.projectiles = [] # List of type and quantity tuples - # Initialize starting weapon (rusty shovel) self.add_weapon(Weapon( name="rusty_shovel", @@ -63,33 +61,22 @@ class Player: self.isInvincible = True self.invincibilityStartTime = pygame.time.get_ticks() - def add_projectile(self, projectile_type): - """Add a projectile to inventory""" - # Find if we already have this type - for proj in self.projectiles: - if proj[0] == projectile_type: - proj[1] += 1 # Increase quantity - speak(f"Now have {proj[1]} {projectile_type}s") - return + def get_jack_o_lanterns(self): + """Get number of jack o'lanterns""" + return self._jack_o_lantern_count - # If not found, add new type with quantity 1 - self.projectiles.append([projectile_type, 1]) + def add_jack_o_lantern(self): + """Add a jack o'lantern""" + self._jack_o_lantern_count += 1 def throw_projectile(self): - """Throw the first available projectile""" - if not self.projectiles: - speak("No projectiles to throw!") + """Throw a jack o'lantern if we have any""" + if self.get_jack_o_lanterns() <= 0: return None - - # Get the first projectile type - projectile = self.projectiles[0] - projectile[1] -= 1 # Decrease quantity - if projectile[1] <= 0: - self.projectiles.pop(0) # Remove if none left - + self._jack_o_lantern_count -= 1 return { - 'type': projectile[0], + 'type': 'jack_o_lantern', 'start_x': self.xPos, 'direction': 1 if self.facingRight else -1 } diff --git a/src/powerup.py b/src/powerup.py index 04d392b..d9bfe2f 100644 --- a/src/powerup.py +++ b/src/powerup.py @@ -20,16 +20,16 @@ class PowerUp(Object): """Update item position""" if not self.isActive: return False - + # Update position self._currentX += self.direction * self.speed - + # Update positional audio if self.channel is None or not self.channel.get_busy(): self.channel = obj_play(self.sounds, "item_bounce", self.xPos, self._currentX) else: self.channel = obj_update(self.channel, self.xPos, self._currentX) - + # Check if item has gone too far (20 tiles) if abs(self._currentX - self.xRange[0]) > 20: self.isActive = False @@ -37,7 +37,7 @@ class PowerUp(Object): self.channel.stop() self.channel = None return False - + return True def apply_effect(self, player): @@ -46,8 +46,8 @@ class PowerUp(Object): player.start_invincibility() speak("Hand of Glory makes you invincible!") elif self.item_type == 'jack_o_lantern': - player.add_projectile('jack_o_lantern') - speak("Gained a Jack-o'-lantern projectile!") + player.add_jack_o_lantern() + speak("Gained a Jack-o'-lantern!") # Stop movement sound when collected if self.channel: diff --git a/src/skull_storm.py b/src/skull_storm.py new file mode 100644 index 0000000..99b2090 --- /dev/null +++ b/src/skull_storm.py @@ -0,0 +1,112 @@ +from libstormgames import * +from src.object import Object + +class SkullStorm(Object): + """Handles falling skulls within a specified range.""" + + def __init__(self, xRange, y, sounds, damage, maxSkulls=3, minFreq=2, maxFreq=5): + super().__init__( + xRange, + y, + "", # No ambient sound for the skull storm + isStatic=True, + isCollectible=False, + isHazard=False + ) + self.sounds = sounds + self.damage = damage + 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) + self.playerInRange = False + + def update(self, currentTime, player): + """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: + # Player just entered range - play the warning sound + self.sounds['skull_storm'].play() + self.playerInRange = True + elif not inRange and self.playerInRange: # Only speak when actually leaving range + # Player just left range + self.playerInRange = False + speak("Skull storm ended.") + + if not inRange: + return + + # Update existing skulls + for skull in self.activeSkulls[:]: # Copy list to allow removal + if currentTime >= skull['land_time']: + # Skull has landed + self.handle_landing(skull, player) + self.activeSkulls.remove(skull) + else: + # Update falling sound + timeElapsed = currentTime - skull['start_time'] + fallProgress = timeElapsed / skull['fall_duration'] + currentY = self.yPos * (1 - fallProgress) + + if skull['channel'] is None or not skull['channel'].get_busy(): + skull['channel'] = play_random_falling( + self.sounds, + 'falling_skull', + player.xPos, + skull['x'], + self.yPos, + currentY + ) + + # 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]), + 'start_time': currentTime, + 'fall_duration': fallDuration, + 'land_time': currentTime + fallDuration, + 'channel': None + } + + self.activeSkulls.append(skull) + + def handle_landing(self, skull, player): + """Handle a skull landing.""" + # Stop falling sound + if skull['channel']: + obj_stop(skull['channel']) + + # Play landing sound with positional audio once + channel = pygame.mixer.find_channel(True) # Find an available channel + if channel: + soundObj = self.sounds['skull_lands'] + channel.play(soundObj, 0) # Play once (0 = no loops) + # Apply positional audio + volume, left, right = calculate_volume_and_pan(player.xPos, skull['x']) + channel.set_volume(volume * left, volume * right) + + # Check if player was hit + if abs(player.xPos - skull['x']) < 1: # Within 1 tile + if not player.isJumping: # Only hit if not jumping + player.set_health(player.get_health() - self.damage) + speak("Hit by falling skull!")