diff --git a/levels/Wicked Quest/4.json b/levels/Wicked Quest/4.json new file mode 100644 index 0000000..d6a49b2 --- /dev/null +++ b/levels/Wicked Quest/4.json @@ -0,0 +1,280 @@ +{ + "level_id": 4, + "name": "Spider's Domain", + "description": "The spiders have claimed this part of the graveyard. Be careful to not disturb them. You know how spiders think, if you can't eat it, kill it!", + "player_start": { + "x": 0, + "y": 0 + }, + "objects": [ + { + "x_range": [5, 8], + "y": 3, + "sound": "coin", + "collectible": true, + "static": true + }, + { + "type": "spider_web", + "x": 10, + "y": 0 + }, + { + "x": 15, + "y": 3, + "sound": "coffin", + "type": "coffin" + }, + { + "x_range": [20, 23], + "y": 3, + "sound": "coin", + "collectible": true, + "static": true + }, + { + "x_range": [25, 35], + "y": 0, + "enemy_type": "goblin", + "health": 4, + "damage": 2, + "attack_range": 1, + "attack_pattern": { + "type": "patrol" + } + }, + { + "x_range": [30, 45], + "y": 12, + "type": "skull_storm", + "damage": 3, + "maximum_skulls": 2, + "frequency": { + "min": 2, + "max": 4 + } + }, + { + "type": "spider_web", + "x": 40, + "y": 0 + }, + { + "x": 45, + "y": 0, + "type": "grave", + "sound": "grave", + "static": true, + "zombie_spawn_chance": 0 + }, + { + "x_range": [50, 54], + "y": 3, + "sound": "coin", + "collectible": true, + "static": true + }, + { + "x": 60, + "y": 3, + "sound": "coffin", + "type": "coffin" + }, + { + "x_range": [65, 85], + "y": 0, + "enemy_type": "witch", + "health": 8, + "damage": 2, + "attack_range": 1, + "attack_pattern": { + "type": "patrol" + } + }, + { + "type": "spider_web", + "x": 75, + "y": 0 + }, + { + "x_range": [80, 95], + "y": 15, + "type": "skull_storm", + "damage": 4, + "maximum_skulls": 3, + "frequency": { + "min": 1, + "max": 3 + } + }, + { + "x_range": [90, 93], + "y": 3, + "sound": "coin", + "collectible": true, + "static": true + }, + { + "x": 100, + "y": 0, + "type": "catapult", + "fire_interval": 4000, + "range": 20 + }, + { + "x": 110, + "y": 0, + "type": "grave", + "sound": "grave", + "static": true, + "zombie_spawn_chance": 50 + }, + { + "x_range": [115, 118], + "y": 3, + "sound": "coin", + "collectible": true, + "static": true + }, + { + "type": "spider_web", + "x": 120, + "y": 0 + }, + { + "x_range": [125, 145], + "y": 0, + "enemy_type": "ghoul", + "health": 12, + "damage": 3, + "attack_range": 2, + "attack_pattern": { + "type": "hunter", + "turn_threshold": 4 + } + }, + { + "x": 135, + "y": 3, + "sound": "coffin", + "type": "coffin" + }, + { + "x_range": [140, 160], + "y": 15, + "type": "skull_storm", + "damage": 4, + "maximum_skulls": 3, + "frequency": { + "min": 1, + "max": 3 + } + }, + { + "x_range": [150, 153], + "y": 3, + "sound": "coin", + "collectible": true, + "static": true + }, + { + "x": 165, + "y": 0, + "type": "grave", + "sound": "grave", + "static": true, + "item": "guts", + "zombie_spawn_chance": 0 + }, + { + "type": "spider_web", + "x": 175, + "y": 0 + }, + { + "x_range": [180, 200], + "y": 0, + "enemy_type": "witch", + "health": 8, + "damage": 2, + "attack_range": 1, + "attack_pattern": { + "type": "patrol" + } + }, + { + "x_range": [185, 188], + "y": 3, + "sound": "coin", + "collectible": true, + "static": true + }, + { + "x": 195, + "y": 3, + "item": "hand_of_glory", + "sound": "coffin", + "type": "coffin" + }, + { + "x_range": [205, 225], + "y": 12, + "type": "skull_storm", + "damage": 4, + "maximum_skulls": 3, + "frequency": { + "min": 1, + "max": 3 + } + }, + { + "x_range": [210, 213], + "y": 3, + "sound": "coin", + "collectible": true, + "static": true + }, + { + "type": "spider_web", + "x": 220, + "y": 0 + }, + { + "x": 225, + "y": 0, + "type": "catapult", + "fire_interval": 4000, + "range": 20 + }, + { + "x_range": [230, 245], + "y": 0, + "enemy_type": "ghoul", + "health": 12, + "damage": 3, + "attack_range": 2, + "attack_pattern": { + "type": "hunter", + "turn_threshold": 4 + } + }, + { + "x_range": [235, 238], + "y": 3, + "sound": "coin", + "collectible": true, + "static": true + }, + { + "x": 245, + "y": 3, + "sound": "coffin", + "type": "coffin" + } + ], + "boundaries": { + "left": 0, + "right": 250 + }, + "footstep_sound": "footstep_tall_grass" +} diff --git a/libstormgames b/libstormgames index b479811..7cbbc64 160000 --- a/libstormgames +++ b/libstormgames @@ -1 +1 @@ -Subproject commit b479811a98f25ff163ce0205765ffea930fef43c +Subproject commit 7cbbc64d27fe533e27332371a332ffb298a82d3c diff --git a/sounds/get_cauldron.ogg b/sounds/get_cauldron.ogg index 4accc87..e5852fc 100644 --- a/sounds/get_cauldron.ogg +++ b/sounds/get_cauldron.ogg @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d7766bb2439162bdd150bc5aa10c0eda706a4c0798a7d62d9cf45a8aa9b42c2d -size 27373 +oid sha256:3ca51ddaf4af11c43c60aa01d2577297d734ae2a78d23a9a351bc70167841941 +size 32860 diff --git a/sounds/get_guts.ogg b/sounds/get_guts.ogg index dd845a8..ebcdbb5 100644 --- a/sounds/get_guts.ogg +++ b/sounds/get_guts.ogg @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a61aa20920a924efefada3ad8b23e84720aca90ef0be42f2fc44c0d0534e0184 -size 43261 +oid sha256:89754168fdba52a771ffedd918e9decb2e3806fd788660c99b848aa98fbd44c6 +size 48030 diff --git a/sounds/hit_spiderweb.ogg b/sounds/hit_spiderweb.ogg new file mode 100644 index 0000000..daa1c22 --- /dev/null +++ b/sounds/hit_spiderweb.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:671b852968b9f1bc5fa920fad101724ea8e24d0c01e1c84de23455b72e717ed8 +size 8234 diff --git a/sounds/spider.ogg b/sounds/spider.ogg new file mode 100644 index 0000000..3debbe5 --- /dev/null +++ b/sounds/spider.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aea4c6e5618ef6a5248406208e78b9086a23840046c4cff1477aecad51e3df7e +size 10303 diff --git a/sounds/spider_dies.ogg b/sounds/spider_dies.ogg new file mode 100644 index 0000000..99dad50 --- /dev/null +++ b/sounds/spider_dies.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a37f156b329db4480671f1375b7e03dbea92299d7777cece512d241ae3813317 +size 15030 diff --git a/sounds/spiderweb.ogg b/sounds/spiderweb.ogg new file mode 100644 index 0000000..42e4072 --- /dev/null +++ b/sounds/spiderweb.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ecf8c005ed8be33bc124f45a6372e4d36928d48c962c8b56e5748652f7b2b0dd +size 41041 diff --git a/src/enemy.py b/src/enemy.py index 5b47aa8..e9e5d79 100644 --- a/src/enemy.py +++ b/src/enemy.py @@ -41,6 +41,11 @@ class Enemy(Object): self.damage = level.player.get_max_health() # Instant death self.health = 1 # Easy to kill self.attackCooldown = 1500 # Slower attack rate + elif enemyType == "spider": + speedMultiplier = kwargs.get('speed_multiplier', 2.0) + self.movementSpeed *= speedMultiplier # Spiders are faster + self.attackPattern = {'type': 'hunter'} # Spiders actively hunt the player + self.turnThreshold = 3 # Spiders turn around quickly to chase player @property def xPos(self): diff --git a/src/level.py b/src/level.py index 06f9fdc..8ef82bd 100644 --- a/src/level.py +++ b/src/level.py @@ -95,6 +95,27 @@ class Level: item=obj.get("item", "random") # Get item type or default to random ) self.objects.append(coffin) + # Check if this is a spider web + elif obj.get("type") == "spider_web": + # Check distance from graves + isValidPosition = True + for existingObj in self.objects: + if (existingObj.soundName == "grave" and + not hasattr(existingObj, 'graveItem')): + distance = abs(obj["x"] - existingObj.xPos) + if distance < 3: + isValidPosition = False + break + + if isValidPosition: + web = Object( + obj["x"], # Just pass the single x value + obj["y"], + "spiderweb", + isStatic=True, + isCollectible=False, + ) + self.objects.append(web) # Check if this is an enemy elif "enemy_type" in obj: enemy = Enemy( @@ -229,7 +250,21 @@ class Level: if obj.hit(self.player.xPos): self.bouncing_items.append(obj.dropped_item) - #speak(f"{obj.dropped_item.soundName} falls out!") + + def spawn_spider(self, xPos, yPos): + """Spawn a spider at the given position""" + spider = Enemy( + [xPos - 5, xPos + 5], # Give spider a patrol range + yPos, + "spider", + self.sounds, + self, + health=8, + damage=8, + attack_range=1, + speed_multiplier=2.0 + ) + self.enemies.append(spider) def handle_collisions(self): """Handle all collision checks and return True if level is complete.""" @@ -243,7 +278,7 @@ class Level: continue # Handle grave edge warnings - if obj.isHazard: + if obj.isHazard and obj.soundName != "spiderweb": # Explicitly exclude spiderwebs 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 @@ -254,56 +289,80 @@ class Level: play_sound(self.sounds['edge']) self.lastWarningTime = currentTime - if obj.is_in_range(self.player.xPos): - if obj.isCollectible and self.player.isJumping: - currentPos = round(self.player.xPos) - if currentPos not in obj.collectedPositions: - play_sound(self.sounds[f'get_{obj.soundName}']) - obj.collect_at_position(currentPos) - self.player.collectedItems.append(obj.soundName) + if not obj.is_in_range(self.player.xPos): + continue + + # Handle collectibles + if obj.isCollectible and self.player.isJumping: + currentPos = round(self.player.xPos) + if currentPos not in obj.collectedPositions: + play_sound(self.sounds[f'get_{obj.soundName}']) + obj.collect_at_position(currentPos) + self.player.collectedItems.append(obj.soundName) + self.player.stats.update_stat('Items collected', 1) + if obj.soundName == "coin": + self.player._coins += 1 + self.player.stats.update_stat('Bone dust', 1) + if self.player._coins % 5 == 0: + # 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() + )) + + if self.player._coins % 100 == 0: + # Extra life + self.player._coins = 0 + self.player._lives += 1 + play_sound(self.sounds['get_extra_life']) + continue + + # Handle spiderweb - this should trigger for both walking and jumping if not ducking + if obj.soundName == "spiderweb" and not self.player.isDucking: + # Create and apply web effect + webEffect = PowerUp( + obj.xPos, + obj.yPos, + 'spiderweb', + self.sounds, + 0 # No direction needed since it's just for effect + ) + webEffect.level = self # Pass level reference for spider spawning + play_sound(self.sounds['hit_spiderweb']) + webEffect.apply_effect(self.player) + + # Deactivate web + obj.isActive = False + obj.channel = obj_stop(obj.channel) + continue + + # Handle graves and other hazards + if obj.isHazard and not self.player.isJumping: + 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) - if obj.soundName == "coin": - self.player._coins += 1 - self.player.stats.update_stat('Bone dust', 1) - if self.player._coins % 5 == 0: - # 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() - )) - - 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 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 + # 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/player.py b/src/player.py index 3a6ad04..a4fd151 100644 --- a/src/player.py +++ b/src/player.py @@ -86,6 +86,13 @@ class Player: def update(self, currentTime): """Update player state""" + if hasattr(self, 'webPenaltyEndTime'): + if currentTime >= self.webPenaltyEndTime: + self.moveSpeed *= 2 # Restore speed + if self.currentWeapon: + self.currentWeapon.attackDuration *= 0.5 # Restore attack speed + del self.webPenaltyEndTime + # Check if invincibility has expired if self.isInvincible and currentTime - self.invincibilityStartTime >= self.invincibilityDuration: self.isInvincible = False diff --git a/src/powerup.py b/src/powerup.py index 56c3996..3cb2016 100644 --- a/src/powerup.py +++ b/src/powerup.py @@ -57,6 +57,22 @@ class PowerUp(Object): broomWeapon = Weapon.create_witch_broom() player.add_weapon(broomWeapon) player.equip_weapon(broomWeapon) + elif self.item_type == 'spiderweb': + # Bounce player back (happens even if invincible) + player.xPos -= 3 if player.xPos > self.xPos else -3 + + # Only apply debuffs if not invincible + if not player.isInvincible: + # Half speed and double attack time for 15 seconds + player.moveSpeed *= 0.5 + if player.currentWeapon: + player.currentWeapon.attackDuration *= 2 + # Set timer for penalty removal + player.webPenaltyEndTime = pygame.time.get_ticks() + 15000 + + # 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: