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)