source: nagslang/enemies.py @ 368:57895217bd74

Last change on this file since 368:57895217bd74 was 368:57895217bd74, checked in by Neil Muller <drnlmuller@…>, 7 years ago

Start refactoring alien movement

File size: 9.8 KB
Line 
1import math
2import random
3
4import pymunk
5import pymunk.pygame_util
6from pymunk.vec2d import Vec2d
7
8from nagslang import render
9from nagslang.constants import (COLLISION_TYPE_ENEMY, COLLISION_TYPE_FURNITURE,
10                                ACID_SPEED, ACID_DAMAGE, ZORDER_MID)
11from nagslang.events import EnemyDeathEvent, FireEvent
12from nagslang.game_object import GameObject, SingleShapePhysicser, make_body
13from nagslang.mutators import FLIP_H
14from nagslang.resources import resources
15from nagslang.utils import vec_with_length
16
17
18def get_editable_enemies():
19    classes = []
20    for cls_name, cls in globals().iteritems():
21        if isinstance(cls, type) and issubclass(cls, Enemy):
22            if hasattr(cls, 'requires'):
23                classes.append((cls_name, cls))
24    return classes
25
26
27def get_alien_image(enemy_type, suffix, *transforms):
28    image_name = 'alien_%s_%s.png' % (enemy_type, suffix)
29    return resources.get_image('creatures', image_name, transforms=transforms)
30
31
32class Enemy(GameObject):
33    """A base class for mobile enemies"""
34    zorder = ZORDER_MID
35    enemy_type = None  # Which set of images to use
36    enemy_damage = None
37    health = None
38    impulse_factor = None
39
40    def __init__(self, space, world, position):
41        super(Enemy, self).__init__(
42            self.make_physics(space, position), self.make_renderer())
43        self.world = world
44        self.angle = 0
45        self._last_fired = 0
46
47    def make_physics(self, space, position):
48        raise NotImplementedError
49
50    def make_renderer(self):
51        return render.FacingSelectionRenderer({
52            'left': render.TimedAnimatedRenderer(
53                [get_alien_image(self.enemy_type, '1'),
54                 get_alien_image(self.enemy_type, '2')], 3),
55            'right': render.TimedAnimatedRenderer(
56                [get_alien_image(self.enemy_type, '1', FLIP_H),
57                 get_alien_image(self.enemy_type, '2', FLIP_H)], 3),
58        })
59
60    def attack(self):
61        raise NotImplementedError
62
63    @classmethod
64    def requires(cls):
65        return [("name", "string"), ("position", "coordinates")]
66
67    def hit(self, weapon):
68        self.lose_health(weapon.damage)
69
70    def lose_health(self, amount):
71        self.health -= amount
72        if self.health < 0:
73            self.physicser.remove_from_space()
74            self.remove = True
75            EnemyDeathEvent.post(self.physicser.position, self.enemy_type)
76
77    def set_direction(self, dx, dy):
78        vec = vec_with_length((dx, dy), self.impulse_factor)
79        self.angle = vec.angle
80        self.physicser.apply_impulse(vec)
81
82    def collide_with_protagonist(self, protagonist):
83        if self.enemy_damage is not None:
84            protagonist.lose_health(self.enemy_damage)
85
86    def collide_with_claw_attack(self, claw_attack):
87        self.lose_health(claw_attack.damage)
88
89    def ranged_attack(self, range_, speed, damage, type_, reload_time):
90        pos = self.physicser.position
91        target = self.world.protagonist.get_shape().body.position
92
93        r = self.get_space().segment_query(pos, target)
94        for collision in r:
95            shape = collision.shape
96            if (shape in (self.get_shape(), self.world.protagonist.get_shape())
97                    or shape.sensor):
98                continue
99            return
100
101        if self.lifetime - self._last_fired >= reload_time:
102            vec = Vec2d((target.x - pos.x, target.y - pos.y))
103            if vec.length < range_:
104                vec.length = speed
105                FireEvent.post(pos, vec, damage, type_,
106                               COLLISION_TYPE_ENEMY)
107                self._last_fired = self.lifetime
108
109    def greedy_move(self, target):
110        """Simple greedy path finder"""
111        def _calc_step(tp, pp):
112            if (tp < pp):
113                step = max(-1, tp - pp)
114            elif (tp > pp):
115                step = min(1, tp - pp)
116            if abs(step) < 0.5:
117                step = 0
118            return step
119        x_step = _calc_step(target[0], self.physicser.position[0])
120        y_step = _calc_step(target[1], self.physicser.position[1])
121        return (x_step, y_step)
122
123    def random_move(self):
124        """Random move"""
125        x_step = random.choice([-1, 0, 1])
126        y_step = random.choice([-1, 0, 1])
127        return x_step, y_step
128
129
130class DeadEnemy(GameObject):
131    def __init__(self, space, world, position, enemy_type='A'):
132        body = make_body(10, 10000, position, damping=0.5)
133        self.shape = pymunk.Poly(
134            body, [(-20, -20), (20, -20), (20, 20), (-20, 20)])
135        self.shape.friction = 0.5
136        self.shape.collision_type = COLLISION_TYPE_FURNITURE
137        super(DeadEnemy, self).__init__(
138            SingleShapePhysicser(space, self.shape),
139            render.ImageRenderer(resources.get_image(
140                'creatures', 'alien_%s_dead.png' % enemy_type)),
141        )
142
143    @classmethod
144    def requires(cls):
145        return [("name", "string"), ("position", "coordinates")]
146
147
148class PatrollingAlien(Enemy):
149    is_moving = True  # Always walking.
150    enemy_type = 'A'
151    health = 42
152    enemy_damage = 15
153    impulse_factor = 50
154
155    def __init__(self, space, world, position, end_position):
156        # An enemy that patrols between the two points
157        super(PatrollingAlien, self).__init__(space, world, position)
158        self._start_pos = position
159        self._end_pos = end_position
160        self._direction = 'away'
161
162    def make_physics(self, space, position):
163        body = make_body(10, pymunk.inf, position, 0.8)
164        shape = pymunk.Circle(body, 30)
165        shape.elasticity = 1.0
166        shape.friction = 0.05
167        shape.collision_type = COLLISION_TYPE_ENEMY
168        return SingleShapePhysicser(space, shape)
169
170    def get_render_angle(self):
171        # No image rotation when rendering, please.
172        return 0
173
174    def get_facing_direction(self):
175        # Enemies can face left or right.
176        if - math.pi / 2 < self.angle <= math.pi / 2:
177            return 'right'
178        else:
179            return 'left'
180
181    def _switch_direction(self):
182        if self._direction == 'away':
183            self._direction = 'towards'
184        else:
185            self._direction = 'away'
186
187    def update(self, dt):
188        # Calculate the step every frame
189        if self._direction == 'away':
190            target = self._end_pos
191        else:
192            target = self._start_pos
193        x_step, y_step = self.greedy_move(target)
194        if abs(x_step) < 1 and abs(y_step) < 1:
195            self._switch_direction()
196        self.set_direction(x_step, y_step)
197        self.ranged_attack(300, ACID_SPEED, ACID_DAMAGE, 'acid', 0.2)
198        super(PatrollingAlien, self).update(dt)
199
200    @classmethod
201    def requires(cls):
202        return [("name", "string"), ("position", "coordinates"),
203                ("end_position", "coordinates")]
204
205
206class ChargingAlien(Enemy):
207    # Simplistic charging of the protagonist
208    is_moving = False
209    enemy_type = 'B'
210    health = 42
211    enemy_damage = 20
212    impulse_factor = 300
213
214    def __init__(self, space, world, position, attack_range=100):
215        super(ChargingAlien, self).__init__(space, world, position)
216        self._range = attack_range
217
218    def make_physics(self, space, position):
219        body = make_body(100, pymunk.inf, position, 0.8)
220        shape = pymunk.Circle(body, 30)
221        shape.elasticity = 1.0
222        shape.friction = 0.05
223        shape.collision_type = COLLISION_TYPE_ENEMY
224        return SingleShapePhysicser(space, shape)
225
226    def get_render_angle(self):
227        # No image rotation when rendering, please.
228        return 0
229
230    def get_facing_direction(self):
231        # Enemies can face left or right.
232        if - math.pi / 2 < self.angle <= math.pi / 2:
233            return 'right'
234        else:
235            return 'left'
236
237    def _calc_movement(self):
238        pos = self.physicser.position
239        target = self.world.protagonist.get_shape().body.position
240        if pos.get_distance(target) > self._range:
241            # stop
242            self.is_moving = False
243            return 0, 0
244        self.is_moving = True
245        dx, dy = self.greedy_move(target)
246        return dx, dy
247
248    def update(self, dt):
249        # Calculate the step every frame
250        # Distance to the protagonist
251        self.ranged_attack(300, ACID_SPEED, ACID_DAMAGE, 'acid', 0.2)
252        dx, dy = self._calc_movement()
253        self.set_direction(dx, dy)
254        super(ChargingAlien, self).update(dt)
255
256    @classmethod
257    def requires(cls):
258        return [("name", "string"), ("position", "coordinates"),
259                ("attack_range", "distance")]
260
261
262class RunAndGunAlien(ChargingAlien):
263    # Simplistic shooter
264    # Move until we're in range, and then randomly
265    impulse_factor = 90
266    is_moving = True
267
268    def __init__(self, space, world, position, attack_range=100):
269        super(RunAndGunAlien, self).__init__(space, world, position,
270                                             attack_range)
271        self.count = 0
272        self._old_move = (0, 0)
273
274    def make_physics(self, space, position):
275        body = make_body(10, pymunk.inf, position, 0.8)
276        shape = pymunk.Circle(body, 30)
277        shape.elasticity = 1.0
278        shape.friction = 0.05
279        shape.collision_type = COLLISION_TYPE_ENEMY
280        return SingleShapePhysicser(space, shape)
281
282    def _calc_movement(self):
283        pos = self.physicser.position
284        target = self.world.protagonist.get_shape().body.position
285        if pos.get_distance(target) < self._range:
286            if self.count > 10:
287                self._old_move = self.random_move()
288                self.count = 0
289            return self._old_move
290        else:
291            return self.greedy_move(target)
292
293    def update(self, dt):
294        super(RunAndGunAlien, self).update(dt)
295        self.count += 1
296
297    @classmethod
298    def requires(cls):
299        return [("name", "string"), ("position", "coordinates"),
300                ("attack_range", "distance")]
Note: See TracBrowser for help on using the repository browser.