diff --git a/sounds/grasping_hands.ogg b/sounds/grasping_hands.ogg new file mode 100644 index 0000000..27d6f8d --- /dev/null +++ b/sounds/grasping_hands.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f558b2bcfb09f1b758fa5b130baaaa63ba7c748aaf581ceb2e29e65bb271e9ae +size 20074 diff --git a/sounds/grasping_hands_end.ogg b/sounds/grasping_hands_end.ogg new file mode 100644 index 0000000..30ea081 --- /dev/null +++ b/sounds/grasping_hands_end.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5d1d0e52574dae0bd9d271bfae6898ba8ff3e5646d0631084978fc491cd838e9 +size 13641 diff --git a/sounds/grasping_hands_start.ogg b/sounds/grasping_hands_start.ogg new file mode 100644 index 0000000..ce5df1d --- /dev/null +++ b/sounds/grasping_hands_start.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a734c92c05c44b2f035d72879f55a3897365c0086930210179a47668d6fe8b4b +size 50058 diff --git a/src/grasping_hands.py b/src/grasping_hands.py new file mode 100644 index 0000000..1c72dcd --- /dev/null +++ b/src/grasping_hands.py @@ -0,0 +1,127 @@ +# -*- coding: utf-8 -*- + +import pygame +from libstormgames import * +from src.object import Object + +class GraspingHands(Object): + """A hazard where the ground crumbles beneath the player as undead hands reach up.""" + + def __init__(self, xRange, y, sounds, delay=1000, crumble_speed=0.065): + super().__init__( + xRange, + y, + "", # Empty string so no regular object sound plays + isStatic=True, + isCollectible=False, + isHazard=True + ) + 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? + self.crumblePosition = 0 # The position of the crumbling ground + self.isActive = True # Is this hazard active? + self.isReset = True # Has this hazard been reset? + 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 + self.entryFromRight = True + self.crumblePosition = self.xRange[1] # Start crumbling from right boundary + self.crumbleDirection = -1 # Crumble moves left + else: + # Player entered from left side (or middle) + 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 + self.crumbleChannel = obj_play(self.sounds, 'grasping_hands', player.xPos, self.crumblePosition) + else: + # Update the sound position + self.crumbleChannel = obj_update(self.crumbleChannel, player.xPos, self.crumblePosition) + + # 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 + # Stop the crumbling sound before death + 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 + if hasattr(self, 'crumbleChannel') and self.crumbleChannel: + obj_stop(self.crumbleChannel) + self.crumbleChannel = None diff --git a/src/level.py b/src/level.py index 1799e04..cbb43c2 100644 --- a/src/level.py +++ b/src/level.py @@ -6,6 +6,7 @@ from libstormgames import * from src.catapult import Catapult from src.coffin import CoffinObject from src.enemy import Enemy +from src.grasping_hands import GraspingHands from src.grave import GraveObject from src.object import Object from src.player import Player @@ -87,6 +88,16 @@ class Level: firingRange=obj.get("range", 20) ) self.objects.append(catapult) + # Check if this is grasping hands + elif obj.get("type") == "grasping_hands": + graspingHands = GraspingHands( + xPos, + obj["y"], + self.sounds, + delay=obj.get("delay", 1000), + crumble_speed=obj.get("crumble_speed", 0.03) + ) + self.objects.append(graspingHands) # Check if this is a grave elif obj.get("type") == "grave": grave = GraveObject( @@ -247,6 +258,13 @@ class Level: if isinstance(obj, SkullStorm): obj.update(currentTime, self.player) + # Update grasping hands + for obj in self.objects: + if isinstance(obj, GraspingHands): + caught = obj.update(currentTime, self.player) + if caught: + return # Stop if player is dead + # Update bouncing items for item in self.bouncing_items[:]: # Copy list to allow removal if not item.update(currentTime, self.player.xPos): @@ -318,7 +336,7 @@ class Level: continue # Handle grave edge warnings - if obj.isHazard and obj.soundName != "spiderweb": # Explicitly exclude spiderwebs + if obj.isHazard and obj.soundName != "spiderweb" and not isinstance(obj, GraspingHands): # Exclude spiderwebs and grasping hands distance = abs(self.player.xPos - obj.xPos) currentTime = pygame.time.get_ticks() if (distance <= 2 and not self.player.isJumping and not self.player.isInvincible