source: nagslang/enemies.py @ 372:024304f6d068

Last change on this file since 372:024304f6d068 was 372:024304f6d068, checked in by Jeremy Thurgood <firxen@…>, 7 years ago

Use timers for enemy ballistics cooldown.

File size: 9.7 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
46    def make_physics(self, space, position):
47        raise NotImplementedError
48
49    def make_renderer(self):
50        return render.FacingSelectionRenderer({
51            'left': render.TimedAnimatedRenderer(
52                [get_alien_image(self.enemy_type, '1'),
53                 get_alien_image(self.enemy_type, '2')], 3),
54            'right': render.TimedAnimatedRenderer(
55                [get_alien_image(self.enemy_type, '1', FLIP_H),
56                 get_alien_image(self.enemy_type, '2', FLIP_H)], 3),
57        })
58
59    def attack(self):
60        raise NotImplementedError
61
62    @classmethod
63    def requires(cls):
64        return [("name", "string"), ("position", "coordinates")]
65
66    def hit(self, weapon):
67        self.lose_health(weapon.damage)
68
69    def lose_health(self, amount):
70        self.health -= amount
71        if self.health < 0:
72            self.physicser.remove_from_space()
73            self.remove = True
74            EnemyDeathEvent.post(self.physicser.position, self.enemy_type)
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        pos = self.physicser.position
90        target = self.world.protagonist.get_shape().body.position
91
92        r = self.get_space().segment_query(pos, target)
93        for collision in r:
94            shape = collision.shape
95            if (shape in (self.get_shape(), self.world.protagonist.get_shape())
96                    or shape.sensor):
97                continue
98            return
99
100        if not self.check_timer('reload_time'):
101            self.start_timer('reload_time', 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
108    def greedy_move(self, target):
109        """Simple greedy path finder"""
110        def _calc_step(tp, pp):
111            if (tp < pp):
112                step = max(-1, tp - pp)
113            elif (tp > pp):
114                step = min(1, tp - pp)
115            if abs(step) < 0.5:
116                step = 0
117            return step
118        x_step = _calc_step(target[0], self.physicser.position[0])
119        y_step = _calc_step(target[1], self.physicser.position[1])
120        return (x_step, y_step)
121
122    def random_move(self):
123        """Random move"""
124        x_step = random.choice([-1, 0, 1])
125        y_step = random.choice([-1, 0, 1])
126        return x_step, y_step
127
128
129class DeadEnemy(GameObject):
130    def __init__(self, space, world, position, enemy_type='A'):
131        body = make_body(10, 10000, position, damping=0.5)
132        self.shape = pymunk.Poly(
133            body, [(-20, -20), (20, -20), (20, 20), (-20, 20)])
134        self.shape.friction = 0.5
135        self.shape.collision_type = COLLISION_TYPE_FURNITURE
136        super(DeadEnemy, self).__init__(
137            SingleShapePhysicser(space, self.shape),
138            render.ImageRenderer(resources.get_image(
139                'creatures', 'alien_%s_dead.png' % enemy_type)),
140        )
141
142    @classmethod
143    def requires(cls):
144        return [("name", "string"), ("position", "coordinates")]
145
146
147class PatrollingAlien(Enemy):
148    is_moving = True  # Always walking.
149    enemy_type = 'A'
150    health = 42
151    enemy_damage = 15
152    impulse_factor = 50
153
154    def __init__(self, space, world, position, end_position):
155        # An enemy that patrols between the two points
156        super(PatrollingAlien, self).__init__(space, world, position)
157        self._start_pos = position
158        self._end_pos = end_position
159        self._direction = 'away'
160
161    def make_physics(self, space, position):
162        body = make_body(10, pymunk.inf, position, 0.8)
163        shape = pymunk.Circle(body, 30)
164        shape.elasticity = 1.0
165        shape.friction = 0.05
166        shape.collision_type = COLLISION_TYPE_ENEMY
167        return SingleShapePhysicser(space, shape)
168
169    def get_render_angle(self):
170        # No image rotation when rendering, please.
171        return 0
172
173    def get_facing_direction(self):
174        # Enemies can face left or right.
175        if - math.pi / 2 < self.angle <= math.pi / 2:
176            return 'right'
177        else:
178            return 'left'
179
180    def _switch_direction(self):
181        if self._direction == 'away':
182            self._direction = 'towards'
183        else:
184            self._direction = 'away'
185
186    def update(self, dt):
187        # Calculate the step every frame
188        if self._direction == 'away':
189            target = self._end_pos
190        else:
191            target = self._start_pos
192        x_step, y_step = self.greedy_move(target)
193        if abs(x_step) < 1 and abs(y_step) < 1:
194            self._switch_direction()
195        self.set_direction(x_step, y_step)
196        self.ranged_attack(300, ACID_SPEED, ACID_DAMAGE, 'acid', 0.2)
197        super(PatrollingAlien, self).update(dt)
198
199    @classmethod
200    def requires(cls):
201        return [("name", "string"), ("position", "coordinates"),
202                ("end_position", "coordinates")]
203
204
205class ChargingAlien(Enemy):
206    # Simplistic charging of the protagonist
207    is_moving = False
208    enemy_type = 'B'
209    health = 42
210    enemy_damage = 20
211    impulse_factor = 300
212
213    def __init__(self, space, world, position, attack_range=100):
214        super(ChargingAlien, self).__init__(space, world, position)
215        self._range = attack_range
216
217    def make_physics(self, space, position):
218        body = make_body(100, pymunk.inf, position, 0.8)
219        shape = pymunk.Circle(body, 30)
220        shape.elasticity = 1.0
221        shape.friction = 0.05
222        shape.collision_type = COLLISION_TYPE_ENEMY
223        return SingleShapePhysicser(space, shape)
224
225    def get_render_angle(self):
226        # No image rotation when rendering, please.
227        return 0
228
229    def get_facing_direction(self):
230        # Enemies can face left or right.
231        if - math.pi / 2 < self.angle <= math.pi / 2:
232            return 'right'
233        else:
234            return 'left'
235
236    def _calc_movement(self):
237        pos = self.physicser.position
238        target = self.world.protagonist.get_shape().body.position
239        if pos.get_distance(target) > self._range:
240            # stop
241            self.is_moving = False
242            return 0, 0
243        self.is_moving = True
244        dx, dy = self.greedy_move(target)
245        return dx, dy
246
247    def update(self, dt):
248        # Calculate the step every frame
249        # Distance to the protagonist
250        self.ranged_attack(300, ACID_SPEED, ACID_DAMAGE, 'acid', 0.2)
251        dx, dy = self._calc_movement()
252        self.set_direction(dx, dy)
253        super(ChargingAlien, self).update(dt)
254
255    @classmethod
256    def requires(cls):
257        return [("name", "string"), ("position", "coordinates"),
258                ("attack_range", "distance")]
259
260
261class RunAndGunAlien(ChargingAlien):
262    # Simplistic shooter
263    # Move until we're in range, and then randomly
264    impulse_factor = 90
265    is_moving = True
266
267    def __init__(self, space, world, position, attack_range=100):
268        super(RunAndGunAlien, self).__init__(space, world, position,
269                                             attack_range)
270        self.count = 0
271        self._old_move = (0, 0)
272
273    def make_physics(self, space, position):
274        body = make_body(10, pymunk.inf, position, 0.8)
275        shape = pymunk.Circle(body, 30)
276        shape.elasticity = 1.0
277        shape.friction = 0.05
278        shape.collision_type = COLLISION_TYPE_ENEMY
279        return SingleShapePhysicser(space, shape)
280
281    def _calc_movement(self):
282        pos = self.physicser.position
283        target = self.world.protagonist.get_shape().body.position
284        if pos.get_distance(target) < self._range:
285            if self.count > 10:
286                self._old_move = self.random_move()
287                self.count = 0
288            return self._old_move
289        else:
290            return self.greedy_move(target)
291
292    def update(self, dt):
293        super(RunAndGunAlien, self).update(dt)
294        self.count += 1
295
296    @classmethod
297    def requires(cls):
298        return [("name", "string"), ("position", "coordinates"),
299                ("attack_range", "distance")]
Note: See TracBrowser for help on using the repository browser.