view nagslang/enemies.py @ 366:9898fa231c4b

Move ranged attack code to Enemy. Don't shoot through solid objects
author Stefano Rivera <stefano@rivera.za.net>
date Fri, 06 Sep 2013 21:07:24 +0200
parents 3dd08e18580f
children 57895217bd74
line wrap: on
line source

import math

import pymunk
import pymunk.pygame_util
from pymunk.vec2d import Vec2d

from nagslang import render
from nagslang.constants import (COLLISION_TYPE_ENEMY, COLLISION_TYPE_FURNITURE,
                                ACID_SPEED, ACID_DAMAGE, ZORDER_MID)
from nagslang.events import EnemyDeathEvent, FireEvent
from nagslang.game_object import GameObject, SingleShapePhysicser, make_body
from nagslang.mutators import FLIP_H
from nagslang.resources import resources
from nagslang.utils import vec_with_length


def get_editable_enemies():
    classes = []
    for cls_name, cls in globals().iteritems():
        if isinstance(cls, type) and issubclass(cls, Enemy):
            if hasattr(cls, 'requires'):
                classes.append((cls_name, cls))
    return classes


def get_alien_image(enemy_type, suffix, *transforms):
    image_name = 'alien_%s_%s.png' % (enemy_type, suffix)
    return resources.get_image('creatures', image_name, transforms=transforms)


class Enemy(GameObject):
    """A base class for mobile enemies"""
    zorder = ZORDER_MID
    enemy_type = None  # Which set of images to use
    enemy_damage = None
    health = None
    impulse_factor = None

    def __init__(self, space, world, position):
        super(Enemy, self).__init__(
            self.make_physics(space, position), self.make_renderer())
        self.world = world
        self.angle = 0
        self._last_fired = 0

    def make_physics(self, space, position):
        raise NotImplementedError

    def make_renderer(self):
        return render.FacingSelectionRenderer({
            'left': render.TimedAnimatedRenderer(
                [get_alien_image(self.enemy_type, '1'),
                 get_alien_image(self.enemy_type, '2')], 3),
            'right': render.TimedAnimatedRenderer(
                [get_alien_image(self.enemy_type, '1', FLIP_H),
                 get_alien_image(self.enemy_type, '2', FLIP_H)], 3),
        })

    def attack(self):
        raise NotImplementedError

    @classmethod
    def requires(cls):
        return [("name", "string"), ("position", "coordinates")]

    def hit(self, weapon):
        self.lose_health(weapon.damage)

    def lose_health(self, amount):
        self.health -= amount
        if self.health < 0:
            self.physicser.remove_from_space()
            self.remove = True
            EnemyDeathEvent.post(self.physicser.position, self.enemy_type)

    def set_direction(self, dx, dy):
        vec = vec_with_length((dx, dy), self.impulse_factor)
        self.angle = vec.angle
        self.physicser.apply_impulse(vec)

    def collide_with_protagonist(self, protagonist):
        if self.enemy_damage is not None:
            protagonist.lose_health(self.enemy_damage)

    def collide_with_claw_attack(self, claw_attack):
        self.lose_health(claw_attack.damage)

    def ranged_attack(self, range_, speed, damage, type_, reload_time):
        pos = self.physicser.position
        target = self.world.protagonist.get_shape().body.position

        r = self.get_space().segment_query(pos, target)
        for collision in r:
            shape = collision.shape
            if (shape in (self.get_shape(), self.world.protagonist.get_shape())
                    or shape.sensor):
                continue
            return

        if self.lifetime - self._last_fired >= reload_time:
            vec = Vec2d((target.x - pos.x, target.y - pos.y))
            if vec.length < range_:
                vec.length = speed
                FireEvent.post(pos, vec, damage, type_,
                               COLLISION_TYPE_ENEMY)
                self._last_fired = self.lifetime


class DeadEnemy(GameObject):
    def __init__(self, space, world, position, enemy_type='A'):
        body = make_body(10, 10000, position, damping=0.5)
        self.shape = pymunk.Poly(
            body, [(-20, -20), (20, -20), (20, 20), (-20, 20)])
        self.shape.friction = 0.5
        self.shape.collision_type = COLLISION_TYPE_FURNITURE
        super(DeadEnemy, self).__init__(
            SingleShapePhysicser(space, self.shape),
            render.ImageRenderer(resources.get_image(
                'creatures', 'alien_%s_dead.png' % enemy_type)),
        )

    @classmethod
    def requires(cls):
        return [("name", "string"), ("position", "coordinates")]


class PatrollingAlien(Enemy):
    is_moving = True  # Always walking.
    enemy_type = 'A'
    health = 42
    enemy_damage = 15
    impulse_factor = 50

    def __init__(self, space, world, position, end_position):
        # An enemy that patrols between the two points
        super(PatrollingAlien, self).__init__(space, world, position)
        self._start_pos = position
        self._end_pos = end_position
        self._direction = 'away'

    def make_physics(self, space, position):
        body = make_body(10, pymunk.inf, position, 0.8)
        shape = pymunk.Circle(body, 30)
        shape.elasticity = 1.0
        shape.friction = 0.05
        shape.collision_type = COLLISION_TYPE_ENEMY
        return SingleShapePhysicser(space, shape)

    def get_render_angle(self):
        # No image rotation when rendering, please.
        return 0

    def get_facing_direction(self):
        # Enemies can face left or right.
        if - math.pi / 2 < self.angle <= math.pi / 2:
            return 'right'
        else:
            return 'left'

    def _switch_direction(self):
        if self._direction == 'away':
            self._direction = 'towards'
        else:
            self._direction = 'away'

    def update(self, dt):
        # Calculate the step every frame
        if self._direction == 'away':
            target = self._end_pos
        else:
            target = self._start_pos
        x_step = 0
        y_step = 0
        if (target[0] < self.physicser.position[0]):
            x_step = max(-1, target[0] - self.physicser.position[0])
        elif (target[0] > self.physicser.position[0]):
            x_step = min(1, target[0] - self.physicser.position[0])
        if abs(x_step) < 0.5:
            x_step = 0
        if (target[1] < self.physicser.position[1]):
            y_step = max(-1, target[1] - self.physicser.position[1])
        elif (target[1] > self.physicser.position[1]):
            y_step = min(1, target[1] - self.physicser.position[1])
        if abs(y_step) < 0.5:
            y_step = 0
        if abs(x_step) < 1 and abs(y_step) < 1:
            self._switch_direction()
        self.set_direction(x_step, y_step)
        self.ranged_attack(300, ACID_SPEED, ACID_DAMAGE, 'acid', 0.2)
        super(PatrollingAlien, self).update(dt)

    @classmethod
    def requires(cls):
        return [("name", "string"), ("position", "coordinates"),
                ("end_position", "coordinates")]


class ChargingAlien(Enemy):
    # Simplistic charging of the protagonist
    is_moving = False
    enemy_type = 'B'
    health = 42
    enemy_damage = 20
    impulse_factor = 300

    def __init__(self, space, world, position, attack_range=100):
        super(ChargingAlien, self).__init__(space, world, position)
        self._range = attack_range

    def make_physics(self, space, position):
        body = make_body(100, pymunk.inf, position, 0.8)
        shape = pymunk.Circle(body, 30)
        shape.elasticity = 1.0
        shape.friction = 0.05
        shape.collision_type = COLLISION_TYPE_ENEMY
        return SingleShapePhysicser(space, shape)

    def get_render_angle(self):
        # No image rotation when rendering, please.
        return 0

    def get_facing_direction(self):
        # Enemies can face left or right.
        if - math.pi / 2 < self.angle <= math.pi / 2:
            return 'right'
        else:
            return 'left'

    def update(self, dt):
        # Calculate the step every frame
        # Distance to the protagonist
        self.ranged_attack(300, ACID_SPEED, ACID_DAMAGE, 'acid', 0.2)
        pos = self.physicser.position
        target = self.world.protagonist.get_shape().body.position
        if pos.get_distance(target) > self._range:
            # stop
            self.is_moving = False
            return
        self.is_moving = True
        dx = target.x - pos.x
        dy = target.y - pos.y
        self.set_direction(dx, dy)
        super(ChargingAlien, self).update(dt)

    @classmethod
    def requires(cls):
        return [("name", "string"), ("position", "coordinates"),
                ("attack_range", "distance")]