diff --git a/levels/README.md b/levels/README.md index 1d8ed29..727e599 100644 --- a/levels/README.md +++ b/levels/README.md @@ -279,11 +279,76 @@ The `maximum_skulls` setting controls how many skulls can be falling simultaneou { "type": "spider_web", "x": 15, - "y": 0 + "y": 3, + "sound_overrides": { + "base": "floating_trap", + "hit_sound": "trap_trigger", + "spawn_enemy": "elf" + } } ``` -Slows player movement and attack speed temporarily when walked over. Automatically spawns a spider enemy at the web location. +**Spider Web Mechanics:** +Spider webs create interactive traps that can slow player movement and attack speed temporarily when triggered. They automatically spawn an enemy at the web location. When triggered, the player bounces back 3 tiles and may receive a 15-second penalty (half movement speed, double attack time) depending on the slowdown setting. + +**Y-Position Based Behavior:** +- **`"y": 0` (Ground Level)** - Ground trap that triggers when walking/running. **Avoid by jumping over it.** +- **`"y": > 0` (Air Level)** - Air trap that triggers when walking or jumping. **Avoid by ducking under it.** + +**Sound Override Properties:** +- `"base"`: Override the ambient web sound (e.g., "floating_trap", "bear_trap") +- `"hit_sound"`: Override the trigger sound (e.g., "trap_trigger", "snap") +- `"spawn_enemy"`: Override spawned enemy type ("spider", "elf", "witch") +- `"enemy_stats"`: Override spawned enemy stats (health, damage, speed_multiplier, attack_range) +- `"slowdown"`: Override slowdown effect (true/false, defaults to true) + +**Level Design Guidelines:** +- **For traditional spider webs, always use `"y": > 0`** (air placement) unless you're specifically creating a themed ground trap +- **Ground webs** (`"y": 0`) work well for: bear traps, tripwires, pressure plates, snowdrifts, present piles +- **Air webs** (`"y": > 0`) work well for: hanging webs, floating pods, magical traps, dangling ornaments +- **Slowdown considerations**: Use `"slowdown": false` for traps where the slowdown effect doesn't make thematic sense (stepping on a snowdrift shouldn't entangle you like a web would) + +**Example Themed Variations:** +```json +// Christmas snowdrift (ground trap - jump to avoid) +{ + "type": "spider_web", + "x": 50, + "y": 0, + "sound_overrides": { + "base": "snow_drift", + "hit_sound": "snow_drift_hit", + "spawn_enemy": "abominable_snowman", + "enemy_stats": { + "health": 12, + "damage": 3, + "speed_multiplier": 0.8, + "attack_range": 1 + }, + "slowdown": false + } +} + +// Floating snow cloud (air trap - duck to avoid) +{ + "type": "spider_web", + "x": 75, + "y": 4, + "sound_overrides": { + "base": "floating_snow_cloud", + "hit_sound": "cloud_burst", + "spawn_enemy": "witch", + "slowdown": true + } +} + +// Traditional spider web (default behavior) +{ + "type": "spider_web", + "x": 100, + "y": 2 +} +``` #### Grasping Hands ```json @@ -315,17 +380,26 @@ While not directly supported, you can create the illusion of coffins that releas { "type": "spider_web", "x": 25, - "y": 3 + "y": 3, + "sound_overrides": { + "base": "coffin", + "hit_sound": "coffin_shatter", + "spawn_enemy": "elf" + } } ``` -**Setup in your level pack's sound directory:** +**Modern Approach (Recommended):** +Use sound overrides directly in your JSON for cleaner, more maintainable theming. The above creates a "coffin" that shatters and spawns an enemy when touched. + +**Legacy Approach:** +You can still replace sounds in your level pack's sound directory: - Replace `spiderweb.ogg` with `coffin.ogg` (ambient coffin sound) - Replace `hit_spiderweb.ogg` with `coffin_shatter.ogg` (break sound) - Replace `spider.ogg` with `mummy.ogg` (spawned enemy sound) - Replace `spider_dies.ogg` with `mummy_dies.ogg` (death sound) -**Result:** Players hear a coffin, when they touch it they hear it shatter, get slowed down (representing the enemy emerging), and a "mummy" automatically spawns from the coffin. The enemy is internally still a spider but sounds and feels like a mummy emerging from a sarcophagus. The slow down for the player can be because the skeleton is tangled in the mummy wrappings, or perhapse he's tangled in the shattered wood. It also does not float, so players would quickly learn to be war, maybe this coffin is one to just be passed under (low hanging). +**Result:** Players hear a coffin, when they touch it they hear it shatter, get slowed down (representing the enemy emerging), and a "mummy" automatically spawns from the coffin. Since this uses `"y": 3` (air level), players must duck to avoid triggering it - creating the "low hanging coffin" effect you described. ### Complex Enemy Combinations - **Spawner + Vulnerability:** Create a tough ghost that spawns minions while alternating between vulnerable states diff --git a/src/level.py b/src/level.py index a13feb2..23b67c2 100644 --- a/src/level.py +++ b/src/level.py @@ -213,6 +213,9 @@ class Level: isStatic=True, isCollectible=False, ) + # Apply sound overrides if specified + if "sound_overrides" in obj: + self._apply_object_sound_overrides(web, obj["sound_overrides"]) self.objects.append(web) # Check if this is an enemy elif "enemy_type" in obj: @@ -403,6 +406,40 @@ class Level: ) self.enemies.append(spider) + def spawn_web_enemy(self, xPos, yPos, enemyType="spider", enemyStats=None): + """Spawn an enemy from a web trigger (supports any enemy type)""" + # Use provided stats or defaults based on enemy type + if enemyStats: + health = enemyStats.get("health", 8) + damage = enemyStats.get("damage", 2) + speed_multiplier = enemyStats.get("speed_multiplier", 1.0) + attack_range = enemyStats.get("attack_range", 1) + else: + # Default stats for common enemy types (backward compatibility) + if enemyType == "spider": + health, damage, speed_multiplier = 8, 8, 2.0 + elif enemyType == "elf": + health, damage, speed_multiplier = 2, 1, 1.0 + elif enemyType == "witch": + health, damage, speed_multiplier = 6, 2, 1.0 + else: + # For any other enemy type, use reasonable defaults + health, damage, speed_multiplier = 6, 2, 1.0 + attack_range = 1 + + enemy = Enemy( + [xPos - 5, xPos + 5], # Give enemy a patrol range + yPos, + enemyType, + self.sounds, + self, + health=health, + damage=damage, + attack_range=attack_range, + speed_multiplier=speed_multiplier, + ) + self.enemies.append(enemy) + def handle_collisions(self): """Handle all collision checks and return True if level is complete.""" # Add a pump here so it gets called reasonably often. @@ -474,14 +511,28 @@ class Level: ) # Use survivor_bonus sound if available, fallback to bone_dust continue - # Handle spiderweb - this should trigger for both walking and jumping if not ducking - if obj.originalSoundName == "spiderweb" and not self.player.isDucking: + # Handle spiderweb - collision depends on Y position + # Y = 0 (ground): hit if walking (avoid by jumping) + # Y > 0 (air): hit if not ducking (avoid by ducking) + webTriggered = False + if obj.originalSoundName == "spiderweb": + if obj.yPos == 0: + # Ground web: triggers if player is walking (not jumping) + webTriggered = not self.player.isJumping + else: + # Air web: triggers if player is not ducking + webTriggered = not self.player.isDucking + + if webTriggered: # 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.webObject = obj # Pass web object for override information + # Use override hit sound if available, otherwise default + hitSound = getattr(obj, 'hitSoundOverride', 'hit_spiderweb') + play_sound(self.sounds[hitSound]) webEffect.apply_effect(self.player, self) # Deactivate web @@ -675,3 +726,45 @@ class Level: # Store the override for later use when coffin is broken if not hasattr(obj, 'itemSoundOverride'): obj.itemSoundOverride = soundOverrides["item"] + + # Handle skull storm sound overrides and message overrides + if hasattr(obj, 'sounds') and isinstance(obj.sounds, dict): + # Create a copy of the original sounds dict for this object + overriddenSounds = dict(obj.sounds) + # Apply any overrides that exist in both the override dict and the sound system + for soundKey, overrideKey in soundOverrides.items(): + if soundKey in overriddenSounds and overrideKey in self.sounds: + overriddenSounds[soundKey] = self.sounds[overrideKey] + # Handle numbered sound overrides (e.g., falling_skull1, falling_skull2) + elif soundKey.endswith('_skull') and overrideKey in self.sounds: + # Find all numbered variants and override them + for i in range(1, 10): # Support up to 9 numbered variants + numberedKey = f"{soundKey}{i}" + if numberedKey in overriddenSounds: + # Use falling_poop1, falling_poop2 etc. if they exist, otherwise use base sound + numberedOverride = f"{overrideKey}{i}" if f"{overrideKey}{i}" in self.sounds else overrideKey + if numberedOverride in self.sounds: + overriddenSounds[numberedKey] = self.sounds[numberedOverride] + # Replace the object's sounds with the overridden version + obj.sounds = overriddenSounds + + # Handle skull storm TTS message overrides + if hasattr(obj, 'endMessage') and "end_message" in soundOverrides: + obj.endMessage = soundOverrides["end_message"] + + # Handle spider web sound overrides + if hasattr(obj, 'originalSoundName') and obj.originalSoundName == "spiderweb": + # Store hit sound override for use in collision detection + if "hit_sound" in soundOverrides and soundOverrides["hit_sound"] in self.sounds: + obj.hitSoundOverride = soundOverrides["hit_sound"] + # Store enemy type override for spawning + if "spawn_enemy" in soundOverrides: + obj.spawnEnemyOverride = soundOverrides["spawn_enemy"] + # Store enemy stats override for spawning + if "enemy_stats" in soundOverrides: + obj.enemyStatsOverride = soundOverrides["enemy_stats"] + # Store slowdown override (defaults to True if not specified) + if "slowdown" in soundOverrides: + obj.slowdownOverride = soundOverrides["slowdown"] + else: + obj.slowdownOverride = True # Default behavior is to slow down diff --git a/src/powerup.py b/src/powerup.py index 1497e8b..efa9952 100644 --- a/src/powerup.py +++ b/src/powerup.py @@ -110,18 +110,32 @@ class PowerUp(Object): # Bounce player back (happens even if invincible) player.xPos -= 3 if player.xPos > self.xPos else -3 - # Only apply debuffs if not invincible + # Only apply debuffs if not invincible and slowdown is enabled 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 + # Check if slowdown is enabled (default is True) + shouldSlowDown = True + if hasattr(self, "webObject") and hasattr(self.webObject, "slowdownOverride"): + shouldSlowDown = self.webObject.slowdownOverride - # Tell level to spawn a spider + if shouldSlowDown: + # 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 an enemy (spider by default, or override type) if hasattr(self, "level"): - self.level.spawn_spider(self.xPos, self.yPos) + # Check for enemy spawn override from web object + enemyType = "spider" # Default + enemyStats = None + if hasattr(self, "webObject"): + if hasattr(self.webObject, "spawnEnemyOverride"): + enemyType = self.webObject.spawnEnemyOverride + if hasattr(self.webObject, "enemyStatsOverride"): + enemyStats = self.webObject.enemyStatsOverride + self.level.spawn_web_enemy(self.xPos, self.yPos, enemyType, enemyStats) # Stop movement sound when collected if self.channel: diff --git a/src/skull_storm.py b/src/skull_storm.py index 93b1db2..8a9e91a 100644 --- a/src/skull_storm.py +++ b/src/skull_storm.py @@ -24,6 +24,9 @@ class SkullStorm(Object): self.nextSkullDelay = random.randint(self.minFreq, self.maxFreq) self.playerInRange = False + # Default TTS message (can be overridden) + self.endMessage = "Skull storm ended." + def update(self, currentTime, player): """Update all active skulls and potentially spawn new ones.""" if not self.isActive: @@ -39,7 +42,7 @@ class SkullStorm(Object): # Player just left range self.playerInRange = False play_sound(self.sounds["skull_storm_ends"]) - speak("Skull storm ended.") + speak(self.endMessage) # Clear any active skulls when player leaves the range for skull in self.activeSkulls[:]: