Files

270 lines
7.6 KiB
Python

import math
from .. import constants
from ..constants import tileSize
from .entity import Actor
from . import physics
from .stateMachine import State, StateMachine
from .projectile import Projectile
class Enemy(Actor):
stompable = True
hasGravity = True
contactDamage = constants.contactDamage
kind = "enemy"
def __init__(self, x, y, w, h):
super().__init__(x, y, w, h)
self.hp = 1
self.direction = -1
self.machine = StateMachine(self)
self.spawnX = float(x)
self.spawnY = float(y)
def initialState(self):
return None
def stepGround(self, world, speed):
self.vx = speed * self.direction
self.vy += constants.gravity
if self.vy > constants.maxFallSpeed:
self.vy = constants.maxFallSpeed
physics.moveHorizontal(self, world.level, world.platforms)
if (self.direction > 0 and self.onWallRight) or (self.direction < 0 and self.onWallLeft):
self.direction *= -1
self.vx = 0.0
wasOnGround = self.onGround
physics.moveVertical(self, world.level, world.platforms)
physics.resolveSlope(self, world.level, wasOnGround)
self.facing = self.direction
if self.onGround:
aheadX = self.x + (self.w + 1 if self.direction > 0 else -1)
col = int(aheadX // tileSize)
row = int((self.y + self.h + 2) // tileSize)
if not world.level.solidAt(col, row) and not world.level.oneWayAt(col, row) and not world.level.slopeAt(col, row):
self.direction *= -1
def playerDistance(self, world):
return abs(world.player.centerX - self.centerX)
def playerVerticalDistance(self, world):
return abs(world.player.centerY - self.centerY)
def facePlayer(self, world):
self.direction = 1 if world.player.centerX >= self.centerX else -1
def update(self, world):
if self.machine.current is None:
self.machine.setInitial(self.initialState(), world)
self.machine.update(world)
def onStomped(self, world):
self.hp -= 1
if self.hp <= 0:
self.alive = False
world.spawnBurst("stomp", self.centerX, self.centerY, 6)
class PatrolState(State):
name = "patrol"
def update(self, owner, world):
owner.stepGround(world, constants.enemySpeed)
return None
class Goomba(Enemy):
kind = "goomba"
def initialState(self):
return "patrol"
def __init__(self, x, y):
super().__init__(x, y, 16, 16)
self.machine.addState(PatrolState())
class ChaserPatrol(State):
name = "patrol"
detectRange = 90.0
detectHeight = 40.0
def update(self, owner, world):
owner.stepGround(world, constants.enemySpeed)
if owner.playerDistance(world) < self.detectRange and owner.playerVerticalDistance(world) < self.detectHeight:
return "chase"
return None
class ChaseState(State):
name = "chase"
loseRange = 160.0
def onEnter(self, owner, world):
world.spawnBurst("alert", owner.centerX, owner.top - 4, 3)
def update(self, owner, world):
owner.facePlayer(world)
owner.stepGround(world, constants.chaserSpeed)
if owner.playerDistance(world) > self.loseRange:
return "patrol"
return None
class Chaser(Enemy):
kind = "chaser"
def __init__(self, x, y):
super().__init__(x, y, 16, 16)
self.hp = 2
self.machine.addState(ChaserPatrol()).addState(ChaseState())
def initialState(self):
return "patrol"
class FlyerHover(State):
name = "hover"
detectRange = 120.0
def update(self, owner, world):
owner.hoverPhase += 0.08
owner.x += owner.direction * constants.flyerSpeed
owner.y = owner.baseY + math.sin(owner.hoverPhase) * 14.0
box = owner.aabb
blockedRight = owner.direction > 0 and physics.touchingSolidSide(owner, world.level, world.platforms, 1)
blockedLeft = owner.direction < 0 and physics.touchingSolidSide(owner, world.level, world.platforms, -1)
if blockedRight or blockedLeft or owner.x < owner.patrolMin or owner.x > owner.patrolMax:
owner.direction *= -1
owner.x = min(max(owner.x, owner.patrolMin), owner.patrolMax)
owner.facing = owner.direction
if owner.playerDistance(world) < self.detectRange:
return "pursue"
return None
class FlyerPursue(State):
name = "pursue"
loseRange = 190.0
def update(self, owner, world):
target = world.player
dx = target.centerX - owner.centerX
dy = target.centerY - owner.centerY
distance = math.hypot(dx, dy) or 1.0
owner.x += (dx / distance) * constants.flyerSpeed * 1.4
owner.y += (dy / distance) * constants.flyerSpeed * 1.4
owner.baseY = owner.y
owner.facing = 1 if dx >= 0 else -1
owner.direction = owner.facing
if owner.playerDistance(world) > self.loseRange:
return "hover"
return None
class Flyer(Enemy):
kind = "flyer"
hasGravity = False
stompable = True
def __init__(self, x, y):
super().__init__(x, y, 16, 14)
self.baseY = float(y)
self.hoverPhase = 0.0
self.patrolMin = float(x) - 48.0
self.patrolMax = float(x) + 48.0
self.machine.addState(FlyerHover()).addState(FlyerPursue())
def initialState(self):
return "hover"
class ShooterIdle(State):
name = "idle"
detectRange = 150.0
def update(self, owner, world):
if owner.cooldown > 0:
owner.cooldown -= 1
if owner.playerDistance(world) < self.detectRange and owner.cooldown == 0:
owner.facePlayer(world)
return "aim"
return None
class ShooterAim(State):
name = "aim"
windup = 18
def update(self, owner, world):
owner.facePlayer(world)
if self.timeGuard(owner):
return "fire"
return None
def timeGuard(self, owner):
return owner.machine.timeInState >= self.windup
class ShooterFire(State):
name = "fire"
def onEnter(self, owner, world):
target = world.player
dx = target.centerX - owner.centerX
dy = target.centerY - owner.centerY
distance = math.hypot(dx, dy) or 1.0
speed = constants.projectileSpeed
projectile = Projectile(
owner.centerX - 3,
owner.centerY - 3,
(dx / distance) * speed,
(dy / distance) * speed,
owner="enemy",
damage=1,
)
world.spawnProjectile(projectile)
world.playSound("shoot", owner.centerX)
owner.cooldown = constants.shooterCooldown
def update(self, owner, world):
return "idle"
class Shooter(Enemy):
kind = "shooter"
hasGravity = True
stompable = True
def __init__(self, x, y):
super().__init__(x, y, 16, 16)
self.hp = 2
self.cooldown = 0
self.machine.addState(ShooterIdle()).addState(ShooterAim()).addState(ShooterFire())
def initialState(self):
return "idle"
def update(self, world):
if self.hasGravity:
self.vx = 0.0
self.vy += constants.gravity
if self.vy > constants.maxFallSpeed:
self.vy = constants.maxFallSpeed
physics.moveVertical(self, world.level, world.platforms)
super().update(world)
enemyFactory = {
"goomba": Goomba,
"chaser": Chaser,
"flyer": Flyer,
"shooter": Shooter,
}
def spawnEnemy(spawn):
factory = enemyFactory.get(spawn.kind, Goomba)
return factory(spawn.x, spawn.y)