source: nagslang/enemies.py@ 391:866cdc74b26a

Last change on this file since 391:866cdc74b26a was 391:866cdc74b26a, checked in by Stefano Rivera <stefano@…>, 7 years ago

Use Result to handle enemy death (but keep the event for accounting purposes)

File size: 10.0 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 FireEvent, EnemyDeathEvent
12from nagslang.game_object import (GameObject, SingleShapePhysicser, Result,
13 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 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 def update(self, dt):
129 super(Enemy, self).update(dt)
130 if self.health <= 0:
131 return Result(
132 add=[DeadEnemy(self.get_space(), self.world,
133 self.physicser.position, self.enemy_type)],
134 remove=[self])
135
136
137class DeadEnemy(GameObject):
138 def __init__(self, space, world, position, enemy_type='A'):
139 body = make_body(10, 10000, position, damping=0.5)
140 self.shape = pymunk.Poly(
141 body, [(-20, -20), (20, -20), (20, 20), (-20, 20)])
142 self.shape.friction = 0.5
143 self.shape.collision_type = COLLISION_TYPE_FURNITURE
144 super(DeadEnemy, self).__init__(
145 SingleShapePhysicser(space, self.shape),
146 render.ImageRenderer(resources.get_image(
147 'creatures', 'alien_%s_dead.png' % enemy_type)),
148 )
149
150 @classmethod
151 def requires(cls):
152 return [("name", "string"), ("position", "coordinates")]
153
154
155class PatrollingAlien(Enemy):
156 is_moving = True # Always walking.
157 enemy_type = 'A'
158 health = 42
159 enemy_damage = 15
160 impulse_factor = 50
161
162 def __init__(self, space, world, position, end_position):
163 # An enemy that patrols between the two points
164 super(PatrollingAlien, self).__init__(space, world, position)
165 self._start_pos = position
166 self._end_pos = end_position
167 self._direction = 'away'
168
169 def make_physics(self, space, position):
170 body = make_body(10, pymunk.inf, position, 0.8)
171 shape = pymunk.Circle(body, 30)
172 shape.elasticity = 1.0
173 shape.friction = 0.05
174 shape.collision_type = COLLISION_TYPE_ENEMY
175 return SingleShapePhysicser(space, shape)
176
177 def get_render_angle(self):
178 # No image rotation when rendering, please.
179 return 0
180
181 def get_facing_direction(self):
182 # Enemies can face left or right.
183 if - math.pi / 2 < self.angle <= math.pi / 2:
184 return 'right'
185 else:
186 return 'left'
187
188 def _switch_direction(self):
189 if self._direction == 'away':
190 self._direction = 'towards'
191 else:
192 self._direction = 'away'
193
194 def update(self, dt):
195 # Calculate the step every frame
196 if self._direction == 'away':
197 target = self._end_pos
198 else:
199 target = self._start_pos
200 x_step, y_step = self.greedy_move(target)
201 if abs(x_step) < 1 and abs(y_step) < 1:
202 self._switch_direction()
203 self.set_direction(x_step, y_step)
204 self.ranged_attack(300, ACID_SPEED, ACID_DAMAGE, 'acid', 0.2)
205 return super(PatrollingAlien, self).update(dt)
206
207 @classmethod
208 def requires(cls):
209 return [("name", "string"), ("position", "coordinates"),
210 ("end_position", "coordinates")]
211
212
213class ChargingAlien(Enemy):
214 # Simplistic charging of the protagonist
215 is_moving = False
216 enemy_type = 'B'
217 health = 42
218 enemy_damage = 20
219 impulse_factor = 300
220
221 def __init__(self, space, world, position, attack_range=100):
222 super(ChargingAlien, self).__init__(space, world, position)
223 self._range = attack_range
224
225 def make_physics(self, space, position):
226 body = make_body(100, pymunk.inf, position, 0.8)
227 shape = pymunk.Circle(body, 30)
228 shape.elasticity = 1.0
229 shape.friction = 0.05
230 shape.collision_type = COLLISION_TYPE_ENEMY
231 return SingleShapePhysicser(space, shape)
232
233 def get_render_angle(self):
234 # No image rotation when rendering, please.
235 return 0
236
237 def get_facing_direction(self):
238 # Enemies can face left or right.
239 if - math.pi / 2 < self.angle <= math.pi / 2:
240 return 'right'
241 else:
242 return 'left'
243
244 def _calc_movement(self):
245 pos = self.physicser.position
246 target = self.world.protagonist.get_shape().body.position
247 if pos.get_distance(target) > self._range:
248 # stop
249 self.is_moving = False
250 return 0, 0
251 self.is_moving = True
252 dx, dy = self.greedy_move(target)
253 return dx, dy
254
255 def update(self, dt):
256 # Calculate the step every frame
257 # Distance to the protagonist
258 self.ranged_attack(300, ACID_SPEED, ACID_DAMAGE, 'acid', 0.2)
259 dx, dy = self._calc_movement()
260 self.set_direction(dx, dy)
261 return super(ChargingAlien, self).update(dt)
262
263 @classmethod
264 def requires(cls):
265 return [("name", "string"), ("position", "coordinates"),
266 ("attack_range", "distance")]
267
268
269class RunAndGunAlien(ChargingAlien):
270 # Simplistic shooter
271 # Move until we're in range, and then randomly
272 impulse_factor = 90
273 is_moving = True
274
275 def __init__(self, space, world, position, attack_range=100):
276 super(RunAndGunAlien, self).__init__(space, world, position,
277 attack_range)
278 self.count = 0
279 self._old_move = (0, 0)
280
281 def make_physics(self, space, position):
282 body = make_body(10, pymunk.inf, position, 0.8)
283 shape = pymunk.Circle(body, 30)
284 shape.elasticity = 1.0
285 shape.friction = 0.05
286 shape.collision_type = COLLISION_TYPE_ENEMY
287 return SingleShapePhysicser(space, shape)
288
289 def _calc_movement(self):
290 pos = self.physicser.position
291 target = self.world.protagonist.get_shape().body.position
292 if pos.get_distance(target) < self._range:
293 if self.count > 10:
294 self._old_move = self.random_move()
295 self.count = 0
296 return self._old_move
297 else:
298 return self.greedy_move(target)
299
300 def update(self, dt):
301 self.count += 1
302 return super(RunAndGunAlien, self).update(dt)
303
304 @classmethod
305 def requires(cls):
306 return [("name", "string"), ("position", "coordinates"),
307 ("attack_range", "distance")]
Note: See TracBrowser for help on using the repository browser.