282 lines
12 KiB
Python
282 lines
12 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
|
|
|
|
# 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 = False # Start invulnerable
|
|
self.vulnerabilityTimer = pygame.time.get_ticks()
|
|
self.vulnerabilityDuration = kwargs.get('vulnerability_duration', 2000)
|
|
self.invulnerabilityDuration = kwargs.get('invulnerability_duration', 5000)
|
|
soundName = f"{self.enemyType}_is_vulnerable" if self.isVulnerable else self.enemyType
|
|
self.channel = obj_play(self.sounds, soundName, self.level.player.xPos, self.xPos)
|
|
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 == "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
|
|
|
|
# Initialize sound for enemies with vulnerability system immediately upon creation
|
|
if self.hasVulnerabilitySystem:
|
|
if 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)
|
|
# Update existing channel position
|
|
else:
|
|
self.channel = obj_update(self.channel, player.xPos, self.xPos)
|
|
|
|
# Check for vulnerability state change
|
|
if currentTime - self.vulnerabilityTimer >= (self.vulnerabilityDuration if self.isVulnerable else self.invulnerabilityDuration):
|
|
self.isVulnerable = not self.isVulnerable
|
|
self.vulnerabilityTimer = currentTime
|
|
|
|
if self.channel:
|
|
obj_stop(self.channel)
|
|
soundName = f"{self.enemyType}_is_vulnerable" if self.isVulnerable else self.enemyType
|
|
self.channel = obj_play(self.sounds, soundName, 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.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):
|
|
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"""
|
|
if self.hasVulnerabilitySystem 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)
|