diff --git a/levels/1.json b/levels/1.json index 317c676..27c8345 100644 --- a/levels/1.json +++ b/levels/1.json @@ -7,13 +7,6 @@ "y": 0 }, "objects": [ - { - "x": 2, - "y": 3, - "item": "guts", - "sound": "coffin", - "type": "coffin" - }, { "x_range": [5, 8], "y": 3, @@ -38,7 +31,7 @@ "x_range": [25, 30], "y": 0, "enemy_type": "goblin", - "health": 3, + "health": 1, "damage": 1, "attack_range": 1, "attack_pattern": { @@ -54,10 +47,10 @@ { "x": 45, "y": 0, - "hazard": true, + "type": "grave", "sound": "grave", "static": true, - "zombie_spawn_chance": 10 + "zombie_spawn_chance": 0 }, { "x_range": [55, 57], @@ -76,8 +69,8 @@ "x_range": [75, 80], "y": 0, "enemy_type": "goblin", - "health": 5, - "damage": 2, + "health": 1, + "damage": 1, "attack_range": 1, "attack_pattern": { "type": "patrol" @@ -86,10 +79,10 @@ { "x": 85, "y": 0, - "hazard": true, + "type": "grave", "sound": "grave", "static": true, - "zombie_spawn_chance": 15 + "zombie_spawn_chance": 0 }, { "x_range": [95, 100], @@ -125,11 +118,52 @@ "sound": "coin", "collectible": true, "static": true + }, + { + "x": 165, + "y": 0, + "type": "grave", + "sound": "grave", + "static": true, + "zombie_spawn_chance": 0 + }, + { + "x_range": [170, 172], + "y": 3, + "sound": "coin", + "collectible": true, + "static": true + }, + { + "x": 180, + "y": 3, + "sound": "coffin", + "type": "coffin" + }, + { + "x_range": [185, 190], + "y": 0, + "enemy_type": "goblin", + "health": 1, + "damage": 1, + "attack_range": 1, + "attack_pattern": { + "type": "patrol" + } + }, + { + "x": 195, + "y": 0, + "type": "grave", + "sound": "grave", + "static": true, + "zombie_spawn_chance": 0, + "item": "guts" } ], "boundaries": { "left": 0, - "right": 165 + "right": 200 }, "footstep_sound": "footstep_stone" } diff --git a/sounds/duck.ogg b/sounds/duck.ogg new file mode 100644 index 0000000..ab35cae --- /dev/null +++ b/sounds/duck.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6a8cd7ca9533d882e60f569912f09d492618e4866c750d3530d14fc82c1e4dd5 +size 6402 diff --git a/sounds/stand.ogg b/sounds/stand.ogg new file mode 100644 index 0000000..3def621 --- /dev/null +++ b/sounds/stand.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1c80a3a101357681d269b52242286a69ae015c9e17bb3728a2bad8031f7e90d1 +size 6496 diff --git a/src/coffin.py b/src/coffin.py index c6a53ed..5d89c09 100644 --- a/src/coffin.py +++ b/src/coffin.py @@ -15,14 +15,14 @@ class CoffinObject(Object): ) self.sounds = sounds self.level = level - self.is_broken = False + 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.is_broken: - self.is_broken = True + if not self.isBroken: + self.isBroken = True play_sound(self.sounds['coffin_shatter']) self.level.player.stats.update_stat('Coffins broken', 1) diff --git a/src/grave.py b/src/grave.py new file mode 100644 index 0000000..51d635c --- /dev/null +++ b/src/grave.py @@ -0,0 +1,34 @@ +from libstormgames import * +from src.object import Object +from src.powerup import PowerUp + + +class GraveObject(Object): + def __init__(self, x, y, sounds, item=None, zombieSpawnChance=0): + super().__init__( + x, y, "grave", + isStatic=True, + isCollectible=False, + isHazard=True, + zombieSpawnChance=zombieSpawnChance + ) + self.graveItem = item + self.isCollected = False # Renamed to match style of isHazard, isStatic etc + self.sounds = sounds + + 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 + """ + # If grave has no item or item was already collected, player dies + if not self.graveItem or self.isCollected: + return False + + # Collect the item if player is ducking + if player.isDucking: + self.isCollected = True # Mark as collected when collection succeeds + return True + + return False diff --git a/src/level.py b/src/level.py index 5c2d7ae..02b393e 100644 --- a/src/level.py +++ b/src/level.py @@ -4,6 +4,7 @@ from libstormgames import * from src.catapult import Catapult from src.coffin import CoffinObject from src.enemy import Enemy +from src.grave import GraveObject from src.object import Object from src.player import Player from src.projectile import Projectile @@ -19,7 +20,9 @@ class Level: self.bouncing_items = [] self.projectiles = [] # Track active projectiles self.player = player - self.edge_warning_channel = None + self.lastWarningTime = 0 + self.warningInterval = int(self.sounds['edge'].get_length() * 1000) # Convert seconds to milliseconds + self.weapon_hit_channel = None self.leftBoundary = levelData["boundaries"]["left"] self.rightBoundary = levelData["boundaries"]["right"] @@ -61,6 +64,16 @@ class Level: firingRange=obj.get("range", 20) ) self.objects.append(catapult) + # Check if this is a grave + elif obj.get("type") == "grave": + grave = GraveObject( + xPos[0], + obj["y"], + self.sounds, + item=obj.get("item", None), + zombieSpawnChance=obj.get("zombie_spawn_chance", 0) + ) + self.objects.append(grave) # Check if this is a skull storm elif obj.get("type") == "skull_storm": skullStorm = SkullStorm( @@ -105,11 +118,11 @@ class Level: isStatic=obj.get("static", True), isCollectible=obj.get("collectible", False), isHazard=obj.get("hazard", False), - zombie_spawn_chance=obj.get("zombie_spawn_chance", 0) + zombieSpawnChance=obj.get("zombieSpawnChance", 0) ) self.objects.append(gameObject) enemyCount = len(self.enemies) - coffinCount = sum(1 for obj in self.objects if hasattr(obj, 'is_broken')) + coffinCount = sum(1 for obj in self.objects if hasattr(obj, 'isBroken')) player.stats.update_stat('Enemies remaining', enemyCount) player.stats.update_stat('Coffins remaining', coffinCount) @@ -124,16 +137,16 @@ class Level: # Check for potential zombie spawn from graves if (obj.soundName == "grave" and - obj.zombie_spawn_chance > 0 and - not obj.has_spawned): + 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.has_spawned = True + obj.hasSpawned = True roll = random.randint(1, 100) - if roll <= obj.zombie_spawn_chance: + if roll <= obj.zombieSpawnChance: zombie = Enemy( [obj.xPos, obj.xPos], obj.yPos, @@ -209,8 +222,8 @@ class Level: # Check for coffin hits for obj in self.objects: - if hasattr(obj, 'is_broken'): # Check if it's a coffin without using isinstance - if (not obj.is_broken and + if hasattr(obj, 'isBroken'): # Check if it's a coffin without using isinstance + if (not obj.isBroken and obj.xPos >= attackRange[0] and obj.xPos <= attackRange[1] and self.player.isJumping): # Must be jumping to hit floating coffins @@ -229,16 +242,18 @@ class Level: for obj in self.objects: if not obj.isActive: continue - + # Handle grave edge warnings if obj.isHazard: distance = abs(self.player.xPos - obj.xPos) - if distance <= 2 and not self.player.isJumping and not self.player.isInvincible: - if self.edge_warning_channel is None or not self.edge_warning_channel.get_busy(): - self.edge_warning_channel = play_sound(self.sounds['edge']) - else: - if self.edge_warning_channel is not None and not self.edge_warning_channel.get_busy(): - self.edge_warning_channel = None + currentTime = pygame.time.get_ticks() + if (distance <= 2 and not self.player.isJumping and not self.player.isInvincible + and currentTime - self.lastWarningTime >= self.warningInterval): + if isinstance(obj, GraveObject) and obj.graveItem and not obj.isCollected: + play_sound(self.sounds['_edge']) + else: + play_sound(self.sounds['edge']) + self.lastWarningTime = currentTime if obj.is_in_range(self.player.xPos): if obj.isCollectible and self.player.isJumping: @@ -255,25 +270,41 @@ class Level: # Only heal if below max health if self.player.get_health() < self.player.get_max_health(): self.player.set_health(min( - self.player.get_health() + 1, - self.player.get_max_health() + self.player.get_health() + 1, + self.player.get_max_health() )) - if self.player._coins % 100 == 0: - # Extra life - self.player._coins = 0 - self.player._lives += 1 - play_sound(self.sounds['get_extra_life']) + if self.player._coins % 100 == 0: + # Extra life + self.player._coins = 0 + self.player._lives += 1 + play_sound(self.sounds['get_extra_life']) elif obj.isHazard and not self.player.isJumping: - if not self.player.isInvincible: - play_sound(self.sounds[obj.soundName]) - speak("You fell in an open grave! Now, it's yours!") - self.player.set_health(0) - return False - else: - # When invincible, treat it like a successful jump over the grave - pass + if isinstance(obj, GraveObject): + can_collect = obj.collect_grave_item(self.player) + + if can_collect: + # Successfully collected item while ducking + play_sound(self.sounds[f'get_{obj.graveItem}']) + self.player.stats.update_stat('Items collected', 1) + # Create PowerUp to handle the item effect + item = PowerUp(obj.xPos, obj.yPos, obj.graveItem, self.sounds, 1) + item.apply_effect(self.player) + # Stop grave's current audio channel + if obj.channel: + obj_stop(obj.channel) + # Remove the grave + obj.graveItem = None + obj.channel = None + obj.isActive = False # Mark the grave as inactive after collection + continue + elif not self.player.isInvincible: + # Kill player for normal graves or non-ducking collision + play_sound(self.sounds[obj.soundName]) + speak("You fell in an open grave! Now, it's yours!") + self.player.set_health(0) + return False # Handle boundaries if self.player.xPos < self.leftBoundary: diff --git a/src/object.py b/src/object.py index 4e3c54b..b1c42fd 100644 --- a/src/object.py +++ b/src/object.py @@ -1,7 +1,7 @@ from libstormgames import * class Object: - def __init__(self, x, yPos, soundName, isStatic=True, isCollectible=False, isHazard=False, zombie_spawn_chance=0): + def __init__(self, x, yPos, soundName, isStatic=True, isCollectible=False, isHazard=False, zombieSpawnChance=0): # x can be either a single position or a range [start, end] self.xRange = [x, x] if isinstance(x, (int, float)) else x self.yPos = yPos @@ -9,8 +9,8 @@ class Object: self.isStatic = isStatic self.isCollectible = isCollectible self.isHazard = isHazard - self.zombie_spawn_chance = zombie_spawn_chance - self.has_spawned = False # Track if this object has spawned a zombie + self.zombieSpawnChance = zombieSpawnChance + self.hasSpawned = False self.channel = None # For tracking the sound channel self.isActive = True # For collectibles in a range, track which positions have been collected diff --git a/src/player.py b/src/player.py index f8cac9f..3a6ad04 100644 --- a/src/player.py +++ b/src/player.py @@ -13,6 +13,7 @@ class Player: self.moveSpeed = 0.05 self.jumpDuration = 1000 # Jump duration in milliseconds self.jumpStartTime = 0 + self.isDucking = False self.isJumping = False self.isRunning = False self.runMultiplier = 1.5 # Same multiplier as jumping @@ -69,6 +70,20 @@ class Player: return (self.distanceSinceLastStep >= self.get_step_distance() and currentTime - self.lastStepTime >= self.get_step_interval()) + def duck(self): + """Start ducking""" + if not self.isDucking and not self.isJumping: # Can't duck while jumping + self.isDucking = True + play_sound(self.sounds['duck']) + return True + return False + + def stand(self): + """Stop ducking state and play sound""" + if self.isDucking: + self.isDucking = False + play_sound(self.sounds['stand']) + def update(self, currentTime): """Update player state""" # Check if invincibility has expired