source: nagslang/enemies.py @ 393:8d961e05b7b6

Last change on this file since 393:8d961e05b7b6 was 393:8d961e05b7b6, checked in by Stefano Rivera <stefano@…>, 7 years ago

Use Result to handle firing

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