source: nagslang/enemies.py@ 602:df5c2041e07f

Last change on this file since 602:df5c2041e07f was 602:df5c2041e07f, checked in by Adrianna Pińska <adrianna.pinska@…>, 8 years ago

queen coughs up magenta keycard upon expiring

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