view nagslang/enemies.py @ 393:8d961e05b7b6

Use Result to handle firing
author Stefano Rivera <stefano@rivera.za.net>
date Sat, 07 Sep 2013 01:04:01 +0200
parents 866cdc74b26a
children 7fcde01ea50e
line wrap: on
line source

import math
import random

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
from nagslang.game_object import (GameObject, SingleShapePhysicser, Result,
                                  Bullet, 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

    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:
            EnemyDeathEvent.post()
            self.physicser.remove_from_space()

    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,
                      result=None):
        if result is None:
            result = Result()

        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 result

        if not self.check_timer('reload_time'):
            self.start_timer('reload_time', reload_time)
            vec = Vec2d((target.x - pos.x, target.y - pos.y))
            if vec.length < range_:
                vec.length = speed
                result.add += (Bullet(self.get_space(), pos, vec, damage,
                                      type_, COLLISION_TYPE_ENEMY),)
        return result

    def greedy_move(self, target):
        """Simple greedy path finder"""
        def _calc_step(tp, pp):
            if (tp < pp):
                step = max(-1, tp - pp)
            elif (tp > pp):
                step = min(1, tp - pp)
            if abs(step) < 0.5:
                step = 0
            return step
        x_step = _calc_step(target[0], self.physicser.position[0])
        y_step = _calc_step(target[1], self.physicser.position[1])
        return (x_step, y_step)

    def random_move(self):
        """Random move"""
        x_step = random.choice([-1, 0, 1])
        y_step = random.choice([-1, 0, 1])
        return x_step, y_step

    def update(self, dt):
        super(Enemy, self).update(dt)
        result = Result()
        if self.health <= 0:
            result.remove += (self,)
            result.add += (DeadEnemy(self.get_space(), self.world,
                                     self.physicser.position,
                                     self.enemy_type),)
        return result


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, y_step = self.greedy_move(target)
        if abs(x_step) < 1 and abs(y_step) < 1:
            self._switch_direction()
        self.set_direction(x_step, y_step)
        result = self.ranged_attack(300, ACID_SPEED, ACID_DAMAGE, 'acid', 0.2)
        return result.merge(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 _calc_movement(self):
        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 0, 0
        self.is_moving = True
        dx, dy = self.greedy_move(target)
        return dx, dy

    def update(self, dt):
        # Calculate the step every frame
        # Distance to the protagonist
        result = self.ranged_attack(300, ACID_SPEED, ACID_DAMAGE, 'acid', 0.2)
        dx, dy = self._calc_movement()
        self.set_direction(dx, dy)
        return result.merge(super(ChargingAlien, self).update(dt))

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


class RunAndGunAlien(ChargingAlien):
    # Simplistic shooter
    # Move until we're in range, and then randomly
    impulse_factor = 90
    is_moving = True

    def __init__(self, space, world, position, attack_range=100):
        super(RunAndGunAlien, self).__init__(space, world, position,
                                             attack_range)
        self.count = 0
        self._old_move = (0, 0)

    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 _calc_movement(self):
        pos = self.physicser.position
        target = self.world.protagonist.get_shape().body.position
        if pos.get_distance(target) < self._range:
            if self.count > 10:
                self._old_move = self.random_move()
                self.count = 0
            return self._old_move
        else:
            return self.greedy_move(target)

    def update(self, dt):
        self.count += 1
        return super(RunAndGunAlien, self).update(dt)

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