source: nagslang/enemies.py @ 308:3dee86b6c216

Last change on this file since 308:3dee86b6c216 was 308:3dee86b6c216, checked in by Stefano Rivera <stefano@…>, 7 years ago

Enemies leave corpses

File size: 8.2 KB
Line 
1import math
2
3import pymunk
4import pymunk.pygame_util
5
6from nagslang import render
7from nagslang.constants import (COLLISION_TYPE_ENEMY, COLLISION_TYPE_BOX,
8                                ZORDER_MID)
9from nagslang.events import EnemyDeathEvent
10from nagslang.game_object import GameObject, SingleShapePhysicser, make_body
11from nagslang.mutators import FLIP_H
12from nagslang.resources import resources
13
14
15def get_editable_enemies():
16    classes = []
17    for cls_name, cls in globals().iteritems():
18        if isinstance(cls, type) and issubclass(cls, Enemy):
19            if hasattr(cls, 'requires'):
20                classes.append((cls_name, cls))
21    return classes
22
23
24class Enemy(GameObject):
25    """A base class for mobile enemies"""
26    enemy_type = None  # Image to use for dead Enemy
27
28    def __init__(self, space, world, position):
29        self._setup_physics(space, position)
30        self._setup_renderer()
31        self.health = 42
32
33        super(Enemy, self).__init__(
34            self._physicser, self.renderer)
35        self.zorder = ZORDER_MID
36        self.world = world
37
38    def _get_image(self, name, *transforms):
39        return resources.get_image('creatures', name, transforms=transforms)
40
41    def _setup_physics(self, space, position):
42        raise NotImplementedError
43
44    def _setup_renderer(self):
45        raise NotImplementedError
46
47    def attack(self):
48        raise NotImplementedError
49
50    @classmethod
51    def requires(cls):
52        return [("name", "string"), ("position", "coordinates")]
53
54    def lose_health(self, amount):
55        self.health -= amount
56        if self.health < 0:
57            self.physicser.remove_from_space()
58            self.remove = True
59            EnemyDeathEvent.post(self.physicser.position, 'A')
60
61
62class DeadEnemy(GameObject):
63    def __init__(self, space, world, position, enemy_type='A'):
64        body = make_body(10, 10000, position, damping=0.5)
65        self.shape = pymunk.Poly(
66            body, [(-20, -20), (20, -20), (20, 20), (-20, 20)])
67        self.shape.friction = 0.5
68        self.shape.collision_type = COLLISION_TYPE_BOX
69        super(DeadEnemy, self).__init__(
70            SingleShapePhysicser(space, self.shape),
71            render.ImageRenderer(resources.get_image(
72                'creatures', 'alien_%s_dead.png' % enemy_type)),
73        )
74
75    @classmethod
76    def requires(cls):
77        return [("name", "string"), ("position", "coordinates")]
78
79
80class PatrollingAlien(Enemy):
81    is_moving = True  # Always walking.
82    enemy_type = 'A'
83
84    def __init__(self, space, world, position, end_position):
85        # An enemy that patrols between the two points
86        super(PatrollingAlien, self).__init__(space, world, position)
87        self._start_pos = position
88        self._end_pos = end_position
89        self._direction = 'away'
90
91    def _setup_physics(self, space, position):
92        self._body = make_body(10, pymunk.inf, position, 0.8)
93
94        self._shape = pymunk.Circle(self._body, 30)
95
96        self._shape.elasticity = 1.0
97        self._shape.friction = 0.05
98        self._shape.collision_type = COLLISION_TYPE_ENEMY
99        self._physicser = SingleShapePhysicser(space, self._shape)
100        self.impulse_factor = 50
101        self.angle = 0
102
103    def _setup_renderer(self):
104        self.renderer = render.FacingSelectionRenderer({
105            'left': render.TimedAnimatedRenderer(
106                [self._get_image('alien_A_1.png'),
107                 self._get_image('alien_A_2.png')], 3),
108            'right': render.TimedAnimatedRenderer(
109                [self._get_image('alien_A_1.png', FLIP_H),
110                 self._get_image('alien_A_2.png', FLIP_H)], 3),
111        })
112
113    def get_render_angle(self):
114        # No image rotation when rendering, please.
115        return 0
116
117    def get_facing_direction(self):
118        # Enemies can face left or right.
119        if - math.pi / 2 < self.angle <= math.pi / 2:
120            return 'right'
121        else:
122            return 'left'
123
124    def _switch_direction(self):
125        if self._direction == 'away':
126            self._direction = 'towards'
127        else:
128            self._direction = 'away'
129
130    def set_direction(self, dx, dy):
131        self.angle = pymunk.Vec2d((dx, dy)).angle
132        self._body.apply_impulse(
133            (dx * self.impulse_factor, dy * self.impulse_factor))
134
135    def update(self, seconds):
136        # Calculate the step every frame
137        if self._direction == 'away':
138            target = self._end_pos
139        else:
140            target = self._start_pos
141        x_step = 0
142        y_step = 0
143        if (target[0] < self._body.position[0]):
144            x_step = max(-1, target[0] - self._body.position[0])
145        elif (target[0] > self._body.position[0]):
146            x_step = min(1, target[0] - self._body.position[0])
147        if abs(x_step) < 0.5:
148            x_step = 0
149        if (target[1] < self._body.position[1]):
150            y_step = max(-1, target[1] - self._body.position[1])
151        elif (target[1] > self._body.position[1]):
152            y_step = min(1, target[1] - self._body.position[1])
153        if abs(y_step) < 0.5:
154            y_step = 0
155        if abs(x_step) < 1 and abs(y_step) < 1:
156            self._switch_direction()
157        self.set_direction(x_step, y_step)
158        super(PatrollingAlien, self).update(seconds)
159
160    def collide_with_protagonist(self, protagonist):
161        protagonist.lose_health(15)
162
163    def hit(self, weapon):
164        self.lose_health(weapon.damage)
165
166    @classmethod
167    def requires(cls):
168        return [("name", "string"), ("position", "coordinates"),
169                ("end_position", "coordinates")]
170
171
172class ChargingAlien(Enemy):
173    # Simplistic charging of the protagonist
174    is_moving = False
175    enemy_type = 'B'
176
177    def __init__(self, space, world, position, attack_range=100):
178        super(ChargingAlien, self).__init__(space, world, position)
179        self._range = attack_range
180
181    def _setup_physics(self, space, position):
182        self._body = make_body(100, pymunk.inf, position, 0.8)
183
184        self._shape = pymunk.Circle(self._body, 30)
185
186        self._shape.elasticity = 1.0
187        self._shape.friction = 0.05
188        self._shape.collision_type = COLLISION_TYPE_ENEMY
189        self._physicser = SingleShapePhysicser(space, self._shape)
190        self.impulse_factor = 300
191        self.angle = 0
192
193    def _setup_renderer(self):
194        self.renderer = render.FacingSelectionRenderer({
195            'left': render.TimedAnimatedRenderer(
196                [self._get_image('alien_B_1.png'),
197                 self._get_image('alien_B_2.png')], 3),
198            'right': render.TimedAnimatedRenderer(
199                [self._get_image('alien_B_1.png', FLIP_H),
200                 self._get_image('alien_B_2.png', FLIP_H)], 3),
201        })
202
203    def get_render_angle(self):
204        # No image rotation when rendering, please.
205        return 0
206
207    def get_facing_direction(self):
208        # Enemies can face left or right.
209        if - math.pi / 2 < self.angle <= math.pi / 2:
210            return 'right'
211        else:
212            return 'left'
213
214    def set_direction(self, dx, dy):
215        self.angle = pymunk.Vec2d((dx, dy)).angle
216        self._body.apply_impulse(
217            (dx * self.impulse_factor, dy * self.impulse_factor))
218
219    def update(self, seconds):
220        # Calculate the step every frame
221        # Distance to the protagonist
222        pos = self._body.position
223        target = self.world.protagonist.get_shape().body.position
224        if pos.get_distance(target) > self._range:
225            # stop
226            self.is_moving = False
227            return
228        self.is_moving = True
229        x_step = 0
230        y_step = 0
231        if (target[0] < pos[0]):
232            x_step = max(-1, target[0] - pos[0])
233        elif (target[0] > pos[0]):
234            x_step = min(1, target[0] - pos[0])
235        if abs(x_step) < 0.5:
236            x_step = 0
237        if (target[1] < pos[1]):
238            y_step = max(-1, target[1] - pos[1])
239        elif (target[1] > pos[1]):
240            y_step = min(1, target[1] - pos[1])
241        if abs(y_step) < 0.5:
242            y_step = 0
243        self.set_direction(x_step, y_step)
244        super(ChargingAlien, self).update(seconds)
245
246    def collide_with_protagonist(self, protagonist):
247        protagonist.lose_health(20)
248
249    @classmethod
250    def requires(cls):
251        return [("name", "string"), ("position", "coordinates"),
252                ("attack_range", "distance")]
Note: See TracBrowser for help on using the repository browser.