from libstormgames import * from src.object import Object from src.powerup import PowerUp import pygame class Enemy(Object): def __init__(self, xRange, y, enemyType, sounds, level, **kwargs): # Track when critters should start hunting self.hunting = False # Initialize base object properties super().__init__( xRange, y, f"{enemyType}", # Base sound isStatic=False, isHazard=True ) # Enemy specific properties self.enemyType = enemyType self.level = level self.health = kwargs.get('health', 5) # Default 5 HP self.damage = kwargs.get('damage', 1) # Default 1 damage self.attackRange = kwargs.get('attack_range', 1) # Default 1 tile range self.sounds = sounds # Store reference to game sounds # Movement and behavior properties self.movingRight = True # Initial direction self.movementSpeed = 0.03 # Base speed self.patrolStart = self.xRange[0] # Use xRange directly for patrol boundaries self.patrolEnd = self.xRange[1] self.lastAttackTime = 0 self.attackCooldown = 1000 # 1 second between attacks self._currentX = self.xRange[0] # Initialize current position # Attack pattern configuration self.attackPattern = kwargs.get('attack_pattern', {'type': 'patrol'}) self.turnThreshold = self.attackPattern.get('turn_threshold', 5) # 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 self.attackPattern = {'type': 'hunter'} # Spiders actively hunt the player self.turnThreshold = 3 # Spiders turn around quickly to chase player @property def xPos(self): """Current x position""" return self._currentX @xPos.setter def xPos(self, value): """Set current x position""" self._currentX = value def patrol_movement(self): """Standard back-and-forth patrol movement""" if self.movingRight: self.xPos += self.movementSpeed if self.xPos >= self.patrolEnd: self.movingRight = False else: self.xPos -= self.movementSpeed if self.xPos <= self.patrolStart: self.movingRight = True def update(self, currentTime, player): """Update enemy position and handle attacks""" if not self.isActive or self.health <= 0: return # Ghost vulnerability state management if self.enemyType == "ghost": 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 if self.channel: obj_stop(self.channel) self.channel = obj_play(self.sounds, "ghost", player.xPos, self.xPos) elif not self.isVulnerable and (currentTime - self.vulnerabilityTimer > self.invulnerabilityDuration): # Switch to vulnerable self.isVulnerable = True self.vulnerabilityTimer = currentTime # Change to vulnerable sound if self.channel: obj_stop(self.channel) self.channel = obj_play(self.sounds, "ghost_is_vulnerable", player.xPos, self.xPos) # Check if player has entered territory if not self.hunting: if self.patrolStart <= player.xPos <= self.patrolEnd: self.hunting = True # Handle movement based on enemy type and pattern if (self.enemyType == "zombie" or (self.attackPattern['type'] == 'hunter' and self.hunting)): distanceToPlayer = player.xPos - self.xPos # If we've moved past the player by more than the turn threshold, turn around if abs(distanceToPlayer) >= self.turnThreshold: self.movingRight = distanceToPlayer > 0 # Otherwise keep moving in current direction self.xPos += self.movementSpeed if self.movingRight else -self.movementSpeed # Enforce level boundaries if self.xPos < self.level.leftBoundary: self.xPos = self.level.leftBoundary self.movingRight = True elif self.xPos > self.level.rightBoundary: self.xPos = self.level.rightBoundary self.movingRight = False else: # Non-hunting enemies use standard patrol self.patrol_movement() # Check for attack opportunity 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") # Check for attack opportunity if self.can_attack(currentTime, player): self.attack(currentTime, player) def can_attack(self, currentTime, player): """Check if enemy can attack player""" # Must have cooled down from last attack if currentTime - self.lastAttackTime < self.attackCooldown: return False # Don't attack if player is jumping if player.isJumping: return False # Check if player is in range and on same side we're facing distance = abs(player.xPos - self.xPos) tolerance = 0.5 # Same tolerance as we used for the grave if distance <= (self.attackRange + tolerance): # Only attack if we're facing the right way playerOnRight = player.xPos > self.xPos return playerOnRight == self.movingRight return False def attack(self, currentTime, player): """Perform attack on player""" if player.isInvincible: return self.lastAttackTime = currentTime # Play attack sound attackSound = f"{self.enemyType}_attack" if attackSound in self.sounds: self.sounds[attackSound].play() # Deal damage to player player.set_health(player.get_health() - self.damage) self.sounds['player_takes_damage'].play() def take_damage(self, amount): """Handle enemy taking damage""" # Ghost can only take damage when vulnerable if self.enemyType == "ghost" and not self.isVulnerable: return self.health -= amount if self.health <= 0: self.die() def die(self): """Handle enemy death""" self.isActive = False if self.channel: obj_stop(self.channel) self.channel = None # Calculate and award points based on enemy stats basePoints = self.health * 500 damageModifier = self.damage * 750 rangeModifier = self.attackRange * 250 speedModifier = int(self.movementSpeed * 1000) totalPoints = max(basePoints + damageModifier + rangeModifier + speedModifier, 1000) # Award points self.level.levelScore += totalPoints # Play death sound if available using positional audio deathSound = f"{self.enemyType}_dies" if deathSound in self.sounds: self.channel = obj_play(self.sounds, deathSound, self.level.player.xPos, self.xPos, loop=False) # Handle witch-specific drops if self.enemyType == "witch": # Determine which item to drop hasBroom = any(weapon.name == "witch_broom" for weapon in self.level.player.weapons) hasNunchucks = any(weapon.name == "nunchucks" for weapon in self.level.player.weapons) # Drop witch_broom only if player has neither broom nor nunchucks itemType = "witch_broom" if not (hasBroom or hasNunchucks) else "cauldron" # Create drop 1-2 tiles away in random direction direction = random.choice([-1, 1]) dropDistance = random.randint(1, 2) dropX = self.xPos + (direction * dropDistance) droppedItem = PowerUp( dropX, self.yPos, itemType, self.sounds, direction, self.level.leftBoundary, self.level.rightBoundary ) self.level.bouncing_items.append(droppedItem) # Update stats self.level.player.stats.update_stat('Enemies killed', 1)