Files
wicked-quest/src/enemy.py
2025-02-14 21:39:32 -05:00

271 lines
11 KiB
Python

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)):
# Direct chase behavior for zombies and activated hunters
if player.xPos > self.xPos:
self.movingRight = True
self.xPos += self.movementSpeed
else:
self.movingRight = False
self.xPos -= self.movementSpeed
# Only enforce level boundaries, not patrol 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:
# Other enemies and non-activated hunters use patrol pattern
self.patrol_movement()
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)