source: nagslang/enemies.py@ 629:6b4c755c133e

Last change on this file since 629:6b4c755c133e was 629:6b4c755c133e, checked in by Neil Muller <drnlmuller@…>, 8 years ago

More attack range on spawned aliens

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