source: nagslang/enemies.py @ 453:12c71124bbca

Last change on this file since 453:12c71124bbca was 453:12c71124bbca, checked in by Neil Muller <drnlmuller@…>, 7 years ago

Zero is allowed

File size: 10.9 KB
Line 
1import math
2import random
3
4import pymunk
5import pymunk.pygame_util
6
7from nagslang import render
8from nagslang.constants import (
9    COLLISION_TYPE_ENEMY, COLLISION_TYPE_FURNITURE, COLLISION_TYPE_SHEEP,
10    ACID_SPEED, ACID_DAMAGE, ZORDER_MID)
11from nagslang.game_object import (GameObject, SingleShapePhysicser, Result,
12                                  Bullet, 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    random_move_time = 0.3
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        self.add_timer('random_move', self.random_move_time)
47        self._last_random_direction = (0, 0)
48
49    def make_physics(self, space, position):
50        raise NotImplementedError
51
52    def make_renderer(self):
53        return render.FacingSelectionRenderer({
54            'left': render.TimedAnimatedRenderer(
55                [get_alien_image(self.enemy_type, '1'),
56                 get_alien_image(self.enemy_type, '2')], 3),
57            'right': render.TimedAnimatedRenderer(
58                [get_alien_image(self.enemy_type, '1', FLIP_H),
59                 get_alien_image(self.enemy_type, '2', FLIP_H)], 3),
60        })
61
62    def get_render_angle(self):
63        # No image rotation when rendering, please.
64        return 0
65
66    def get_facing_direction(self):
67        # Enemies can face left or right.
68        if - math.pi / 2 < self.angle <= math.pi / 2:
69            return 'right'
70        else:
71            return 'left'
72
73    def hit(self, weapon):
74        self.lose_health(weapon.damage)
75
76    def lose_health(self, amount):
77        self.health -= amount
78        if self.health <= 0:
79            self.world.kills += 1
80            self.physicser.remove_from_space()
81
82    def set_direction(self, dx, dy, force_factor=1):
83        vec = vec_with_length((dx, dy), self.impulse_factor * force_factor)
84        self.angle = vec.angle
85        self.physicser.apply_impulse(vec)
86
87    def collide_with_protagonist(self, protagonist):
88        if self.enemy_damage is not None:
89            protagonist.lose_health(self.enemy_damage)
90
91    def collide_with_claw_attack(self, claw_attack):
92        self.lose_health(claw_attack.damage)
93
94    def range_to_visible_protagonist(self):
95        """Get a vector to the protagonist if there are no barriers in between.
96
97        Returns a vector to the protagonist if she is within line of sight,
98        otherwise `None`
99        """
100
101        pos = self.physicser.position
102        target = self.world.protagonist.get_shape().body.position
103
104        for collision in self.get_space().segment_query(pos, target):
105            shape = collision.shape
106            if (shape in (self.get_shape(), self.world.protagonist.get_shape())
107                    or shape.sensor):
108                continue
109            return None
110        return target - pos
111
112    def ranged_attack(self, range_, speed, damage, type_, reload_time, result):
113        vec = self.range_to_visible_protagonist()
114        if vec is None:
115            return
116
117        if not self.check_timer('reload_time'):
118            self.start_timer('reload_time', reload_time)
119            if vec.length < range_:
120                vec.length = speed
121                result.add += (Bullet(
122                    self.get_space(), self.physicser.position, vec, damage,
123                    type_, COLLISION_TYPE_ENEMY),)
124
125    def greedy_move(self, target):
126        """Simple greedy path finder"""
127        def _calc_step(tp, pp):
128            step = 0
129            if (tp < pp):
130                step = max(-1, tp - pp)
131            elif (tp > pp):
132                step = min(1, tp - pp)
133            if abs(step) < 0.5:
134                step = 0
135            return step
136        x_step = _calc_step(target[0], self.physicser.position[0])
137        y_step = _calc_step(target[1], self.physicser.position[1])
138        return (x_step, y_step)
139
140    def random_move(self):
141        """Random move"""
142        if not self.check_timer('random_move'):
143            self.start_timer('random_move')
144            self._last_random_direction = (
145                random.choice([-1, 0, 1]), random.choice([-1, 0, 1]))
146        return self._last_random_direction
147
148    def attack(self, result):
149        pass
150
151    def move(self, result):
152        pass
153
154    def update(self, dt):
155        super(Enemy, self).update(dt)
156        result = Result()
157        if self.health <= 0:
158            result.remove += (self,)
159            result.add += (DeadEnemy(self.get_space(), self.world,
160                                     self.physicser.position,
161                                     self.enemy_type),)
162        self.move(result)
163        self.attack(result)
164        return result
165
166    @classmethod
167    def requires(cls):
168        return [("name", "string"), ("position", "coordinates")]
169
170
171class DeadEnemy(GameObject):
172    def __init__(self, space, world, position, enemy_type='A'):
173        body = make_body(10, 10000, position, damping=0.5)
174        self.shape = pymunk.Poly(
175            body, [(-20, -20), (20, -20), (20, 20), (-20, 20)])
176        self.shape.friction = 0.5
177        self.shape.collision_type = COLLISION_TYPE_FURNITURE
178        super(DeadEnemy, self).__init__(
179            SingleShapePhysicser(space, self.shape),
180            render.ImageRenderer(resources.get_image(
181                'creatures', 'alien_%s_dead.png' % enemy_type)),
182        )
183
184    @classmethod
185    def requires(cls):
186        return [("name", "string"), ("position", "coordinates")]
187
188
189class PatrollingAlien(Enemy):
190    is_moving = True  # Always walking.
191    enemy_type = 'A'
192    health = 42
193    enemy_damage = 15
194    impulse_factor = 50
195
196    def __init__(self, space, world, position, end_position):
197        # An enemy that patrols between the two points
198        super(PatrollingAlien, self).__init__(space, world, position)
199        self._start_pos = position
200        self._end_pos = end_position
201        self._direction = 'away'
202
203    def make_physics(self, space, position):
204        body = make_body(10, pymunk.inf, position, 0.8)
205        shape = pymunk.Circle(body, 30)
206        shape.elasticity = 1.0
207        shape.friction = 0.05
208        shape.collision_type = COLLISION_TYPE_ENEMY
209        return SingleShapePhysicser(space, shape)
210
211    def _switch_direction(self):
212        if self._direction == 'away':
213            self._direction = 'towards'
214        else:
215            self._direction = 'away'
216
217    def move(self, result):
218        if self._direction == 'away':
219            target = self._end_pos
220        else:
221            target = self._start_pos
222        x_step, y_step = self.greedy_move(target)
223        if abs(x_step) < 1 and abs(y_step) < 1:
224            self._switch_direction()
225        self.set_direction(x_step, y_step)
226
227    def attack(self, result):
228        self.ranged_attack(300, ACID_SPEED, ACID_DAMAGE, 'acid', 0.2, result)
229
230    @classmethod
231    def requires(cls):
232        return [("name", "string"), ("position", "coordinates"),
233                ("end_position", "coordinates")]
234
235
236class ChargingAlien(Enemy):
237    # Simplistic charging of the protagonist
238    is_moving = False
239    enemy_type = 'B'
240    health = 42
241    enemy_damage = 20
242    impulse_factor = 300
243
244    def __init__(self, space, world, position, attack_range=100):
245        super(ChargingAlien, self).__init__(space, world, position)
246        self._range = attack_range
247
248    def make_physics(self, space, position):
249        body = make_body(100, pymunk.inf, position, 0.8)
250        shape = pymunk.Circle(body, 30)
251        shape.elasticity = 1.0
252        shape.friction = 0.05
253        shape.collision_type = COLLISION_TYPE_ENEMY
254        return SingleShapePhysicser(space, shape)
255
256    def move(self, result):
257        pos = self.physicser.position
258        target = self.world.protagonist.get_shape().body.position
259        if pos.get_distance(target) > self._range:
260            # stop
261            self.is_moving = False
262            return 0, 0
263        self.is_moving = True
264        dx, dy = self.greedy_move(target)
265        self.set_direction(dx, dy)
266
267    def attack(self, result):
268        self.ranged_attack(300, ACID_SPEED, ACID_DAMAGE, 'acid', 0.2, result)
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 make_physics(self, space, position):
283        body = make_body(10, pymunk.inf, position, 0.8)
284        shape = pymunk.Circle(body, 30)
285        shape.elasticity = 1.0
286        shape.friction = 0.05
287        shape.collision_type = COLLISION_TYPE_ENEMY
288        return SingleShapePhysicser(space, shape)
289
290    def move(self, result):
291        pos = self.physicser.position
292        target = self.world.protagonist.get_shape().body.position
293        if pos.get_distance(target) < self._range:
294            step = self.random_move()
295        else:
296            step = self.greedy_move(target)
297        self.set_direction(*step)
298
299    @classmethod
300    def requires(cls):
301        return [("name", "string"), ("position", "coordinates"),
302                ("attack_range", "distance")]
303
304
305class Sheep(Enemy):  # Only because we don't have a DeliciousCreature class.
306    is_moving = True  # Always walking.
307    enemy_type = 'sheep'
308    health = 10
309    impulse_factor = 50
310
311    human_vision_range = 100
312    wolf_vision_range = 200
313
314    def make_physics(self, space, position):
315        body = make_body(10, pymunk.inf, position, 0.8)
316        shape = pymunk.Circle(body, 30)
317        shape.elasticity = 1.0
318        shape.friction = 0.05
319        shape.collision_type = COLLISION_TYPE_SHEEP
320        return SingleShapePhysicser(space, shape)
321
322    def move(self, result):
323        vec = self.range_to_visible_protagonist()
324        prot = self.world.protagonist
325        if vec is not None:
326            if prot.in_human_form() and vec.length < self.human_vision_range:
327                self.set_direction(vec.x, vec.y, 1.5)
328                return
329            if prot.in_wolf_form() and vec.length < self.wolf_vision_range:
330                vec = -vec
331                self.set_direction(vec.x, vec.y, 3)
332                return
333        self.set_direction(*self.random_move())
334
335    @classmethod
336    def requires(cls):
337        return [("name", "string"), ("position", "coordinates")]
Note: See TracBrowser for help on using the repository browser.