diff --git a/levels/README.md b/levels/README.md index 141affa..83cf513 100644 --- a/levels/README.md +++ b/levels/README.md @@ -80,7 +80,7 @@ Zombie spawn chance is 0-100, higher means more zombies. Item is also optional, { "x_range": [20, 35], // patrol or hunting range "y": 0, - "enemy_type": "goblin", // goblin, witch, ghoul, boogie_man, ghost, revenant + "enemy_type": "goblin", "health": 4, "damage": 2, "attack_range": 1.5, @@ -93,6 +93,58 @@ Zombie spawn chance is 0-100, higher means more zombies. Item is also optional, Attacks can be "hunter" or "patrol". The "patrol" option does not use the "turn_threshold" option. The "turn_threshold" option is how quickly the hunting enemy will turn around to attack the player. Hunters will leave their area to pursue the player once he has entered the enemy's range. +#### Special Enemy Behaviors + +Enemies can have special behaviors regardless of their type. Here are some examples: + +##### incorporeal Goblin + +``` json +{ + "x_range": [400, 415], + "y": 0, + "enemy_type": "goblin", + "health": 30, + "damage": 2, + "attack_range": 1, + "has_vulnerability": true, + "is_vulnerable": false, + "vulnerability_duration": 1000, + "invulnerability_duration": 5000, + "speed_multiplier": 0.8, + "attack_cooldown": 1200, + "attack_pattern": { + "type": "hunter", + "turn_threshold": 2 + } +} +``` + +##### Spawning Other Enemies (like revenants) + +You can mix and match these behaviors. For an example of a witch who spawns black cats, see "Wicked Quest/13.json" + + +## Sound Requirements for Special Behaviors + +When adding special behaviors to enemies, you'll need corresponding sound files: + +For vulnerability system: +- enemy_is_vulnerable.ogg - Sound when enemy becomes vulnerable + +For spawning behavior: +- enemy_spawn.ogg (optional) - Sound when spawning new enemies + +## Tips for Custom Enemies + +- Balance special behaviors carefully +- Test enemy combinations thoroughly +- Consider providing audio cues for special behaviors +- Remember faster enemies should generally do less damage +- Vulnerability systems work best with higher health values +- Spawning enemies should have lower health to compensate + + ### Hazards #### Skull Storm diff --git a/levels/Wicked Quest/10.json b/levels/Wicked Quest/10.json index 8e0e2c8..82ada6e 100644 --- a/levels/Wicked Quest/10.json +++ b/levels/Wicked Quest/10.json @@ -257,16 +257,25 @@ "static": true }, { - "x_range": [400, 420], + "type": "spider_web", + "x": 395, + "y": 0 + }, + { + "x_range": [400, 415], "y": 0, "enemy_type": "revenant", "health": 40, "damage": 1, "attack_range": 1, - "zombie_spawn_cooldown": 2500, "attack_pattern": { "type": "patrol" - } + }, + "can_spawn": true, + "spawn_type": "zombie", + "spawn_cooldown": 2500, + "spawn_chance": 100, + "spawn_distance": 5 } ], "boundaries": { diff --git a/levels/Wicked Quest/13.json b/levels/Wicked Quest/13.json index ef126a1..cf41a47 100644 --- a/levels/Wicked Quest/13.json +++ b/levels/Wicked Quest/13.json @@ -334,7 +334,12 @@ "attack_range": 2, "attack_pattern": { "type": "patrol" - } + }, + "can_spawn": true, + "spawn_type": "black_cat", + "spawn_cooldown": 5000, + "spawn_chance": 75, + "spawn_distance": 4 }, { "x_range": [405, 495], diff --git a/levels/Wicked Quest/8.json b/levels/Wicked Quest/8.json index 3fefc57..d6e2498 100644 --- a/levels/Wicked Quest/8.json +++ b/levels/Wicked Quest/8.json @@ -327,14 +327,17 @@ "health": 30, "damage": 2, "attack_range": 1, + "has_vulnerability": true, + "is_vulnerable": false, "vulnerability_duration": 1000, "invulnerability_duration": 5000, "speed_multiplier": 0.8, "attack_cooldown": 1200, "attack_pattern": { - "type": "patrol" + "type": "hunter", + "turn_threshold": 2 } -} + } ], "boundaries": { "left": 0, diff --git a/src/enemy.py b/src/enemy.py index 8e74cc5..117fabf 100644 --- a/src/enemy.py +++ b/src/enemy.py @@ -33,39 +33,35 @@ class Enemy(Object): self.attackCooldown = 1000 # 1 second between attacks self._currentX = self.xRange[0] # Initialize current position + # Add spawn configuration + self.canSpawn = kwargs.get('can_spawn', False) + if self.canSpawn: + self.spawnCooldown = kwargs.get('spawn_cooldown', 2000) + self.spawnChance = kwargs.get('spawn_chance', 25) + self.spawnType = kwargs.get('spawn_type', 'zombie') # Default to zombie for backward compatibility + self.spawnDistance = kwargs.get('spawn_distance', 5) + self.lastSpawnTime = 0 + # Attack pattern configuration self.attackPattern = kwargs.get('attack_pattern', {'type': 'patrol'}) self.turnThreshold = self.attackPattern.get('turn_threshold', 5) + # Initialize vulnerability system + self.hasVulnerabilitySystem = kwargs.get('has_vulnerability', False) + if self.hasVulnerabilitySystem: + self.isVulnerable = kwargs.get('is_vulnerable', False) # For enemies with vulnerability, default to invulnerable + self.vulnerabilityTimer = 0 + self.vulnerabilityDuration = kwargs.get('vulnerability_duration', 1000) + self.invulnerabilityDuration = kwargs.get('invulnerability_duration', 5000) + else: + self.isVulnerable = True + # Enemy type specific adjustments if enemyType == "zombie": self.movementSpeed *= 0.6 # Zombies are slower self.damage = level.player.get_max_health() # Instant death self.health = 1 # Easy to kill self.attackCooldown = 1500 # Slower attack rate - elif enemyType == "ghost": - self.isVulnerable = False - self.vulnerabilityTimer = 0 - self.vulnerabilityDuration = kwargs.get('vulnerability_duration', 3000) # Default 3 seconds - self.invulnerabilityDuration = kwargs.get('invulnerability_duration', 5000) # Default 5 seconds - self.movementSpeed *= kwargs.get('speed_multiplier', 0.8) # Default 80% speed - self.health = kwargs.get('health', 3) # Default 3 HP - self.damage = kwargs.get('damage', 2) # Default 2 damage - self.attackRange = kwargs.get('attack_range', 1) # Use provided or default 1 - self.attackCooldown = kwargs.get('attack_cooldown', 1200) # Default 1.2 seconds - self.attackPattern = kwargs.get('attack_pattern', { - 'type': 'hunter', - 'turn_threshold': 2 - }) - elif enemyType == "revenant": - self.movementSpeed *= 0.7 # Slower than normal - self.damage = 1 - self.health = kwargs.get('health', 40) - self.attackCooldown = 1500 # Slower direct attacks - self.zombieSpawnCooldown = kwargs.get('zombie_spawn_cooldown', 2000) # 2 seconds between spawns - self.lastZombieSpawn = 0 - self.zombieSpawnDistance = 5 - self.attackPattern = kwargs.get('attack_pattern', {'type': 'patrol'}) elif enemyType == "spider": speedMultiplier = kwargs.get('speed_multiplier', 2.0) self.movementSpeed *= speedMultiplier # Spiders are faster @@ -99,16 +95,21 @@ class Enemy(Object): if not self.isActive or self.health <= 0: return - # Ghost vulnerability state management - if self.enemyType == "ghost": + # Set initial sound for enemies with vulnerability system + if self.hasVulnerabilitySystem and self.channel is None: + soundName = f"{self.enemyType}_is_vulnerable" if self.isVulnerable else self.enemyType + self.channel = obj_play(self.sounds, soundName, player.xPos, self.xPos) + + # Enemy vulnerability state management + if self.hasVulnerabilitySystem: if self.isVulnerable and (currentTime - self.vulnerabilityTimer > self.vulnerabilityDuration): # Switch to invulnerable self.isVulnerable = False self.vulnerabilityTimer = currentTime - # Change sound back to base ghost sound + # Change sound back to base enemy sound if self.channel: obj_stop(self.channel) - self.channel = obj_play(self.sounds, "ghost", player.xPos, self.xPos) + self.channel = obj_play(self.sounds, self.enemyType, player.xPos, self.xPos) elif not self.isVulnerable and (currentTime - self.vulnerabilityTimer > self.invulnerabilityDuration): # Switch to vulnerable self.isVulnerable = True @@ -116,7 +117,7 @@ class Enemy(Object): # Change to vulnerable sound if self.channel: obj_stop(self.channel) - self.channel = obj_play(self.sounds, "ghost_is_vulnerable", player.xPos, self.xPos) + self.channel = obj_play(self.sounds, f"{self.enemyType}_is_vulnerable", player.xPos, self.xPos) # Check if player has entered territory if not self.hunting: @@ -151,33 +152,40 @@ class Enemy(Object): if self.can_attack(currentTime, player): self.attack(currentTime, player) - if self.enemyType == "revenant" and self.hunting: # Only spawn when player enters territory - # Check if it's time to spawn a zombie - if currentTime - self.lastZombieSpawn >= self.zombieSpawnCooldown: - # Spawn zombies relative to player position, not revenant - spawnDirection = random.choice([-1, 1]) - spawnX = player.xPos + (spawnDirection * self.zombieSpawnDistance) - - # Ensure spawn point is within level boundaries - spawnX = max(self.level.leftBoundary, min(spawnX, self.level.rightBoundary)) - - # Create new zombie - zombie = Enemy( - [spawnX, spawnX], # Single point range for spawn - self.yPos, - "zombie", - self.sounds, - self.level - ) - - # Add to level's enemies - self.level.enemies.append(zombie) - self.lastZombieSpawn = currentTime - - # Play spawn sound and speak message - if 'revenant_spawn_zombie' in self.sounds: - self.sounds['revenant_spawn_zombie'].play() - speak("Zombie spawned") + if self.canSpawn: + if currentTime - self.lastSpawnTime >= self.spawnCooldown: + distanceToPlayer = abs(player.xPos - self.xPos) + if distanceToPlayer <= 12: # Within audible range + # Random chance to spawn + if random.randint(1, 100) <= self.spawnChance: + # Spawn relative to player position + spawnDirection = random.choice([-1, 1]) + spawnX = player.xPos + (spawnDirection * self.spawnDistance) + + # Ensure spawn point is within level boundaries + spawnX = max(self.level.leftBoundary, min(spawnX, self.level.rightBoundary)) + + # Create new enemy of specified type + spawned = Enemy( + [spawnX, spawnX], # Single point range for spawn + self.yPos, + self.spawnType, + self.sounds, + self.level, + health=4, # Default health for spawned enemies + damage=2, # Default damage for spawned enemies + attack_range=1 # Default range for spawned enemies + ) + + # Add to level's enemies + self.level.enemies.append(spawned) + self.lastSpawnTime = currentTime + + # Play spawn sound if available + spawnSound = f"{self.enemyType}_spawn_{self.spawnType}" + if spawnSound in self.sounds: + self.sounds[spawnSound].play() + speak(f"{self.spawnType} spawned") # Check for attack opportunity if self.can_attack(currentTime, player): @@ -218,8 +226,8 @@ class Enemy(Object): def take_damage(self, amount): """Handle enemy taking damage""" - # Ghost can only take damage when vulnerable - if self.enemyType == "ghost" and not self.isVulnerable: + # Enemy can only take damage when vulnerable + if not self.isVulnerable: return self.health -= amount diff --git a/src/level.py b/src/level.py index 06d411e..e8bad99 100644 --- a/src/level.py +++ b/src/level.py @@ -147,7 +147,12 @@ class Level: damage=obj.get("damage", 1), attack_range=obj.get("attack_range", 1), movement_range=obj.get("movement_range", 5), - attack_pattern=obj.get("attack_pattern", {'type': 'patrol'}) # Add this line + attack_pattern=obj.get("attack_pattern", {'type': 'patrol'}), + can_spawn=obj.get("can_spawn", False), + spawn_type=obj.get("spawn_type", "zombie"), + spawn_cooldown=obj.get("spawn_cooldown", 2000), + spawn_chance=obj.get("spawn_chance", 25), + spawn_distance=obj.get("spawn_distance", 5) ) self.enemies.append(enemy) else: @@ -215,11 +220,12 @@ class Level: continue enemy.update(currentTime, self.player) - - if enemy.channel is None or not enemy.channel.get_busy(): - enemy.channel = obj_play(self.sounds, enemy.enemyType, self.player.xPos, enemy.xPos) - if enemy.channel is not None: - enemy.channel = obj_update(enemy.channel, self.player.xPos, enemy.xPos) + + if not enemy.hasVulnerabilitySystem: + if enemy.channel is None or not enemy.channel.get_busy(): + enemy.channel = obj_play(self.sounds, enemy.enemyType, self.player.xPos, enemy.xPos) + if enemy.channel is not None: + enemy.channel = obj_update(enemy.channel, self.player.xPos, enemy.xPos) # Update catapults for obj in self.objects: