1 | import math
|
---|
2 | import random
|
---|
3 |
|
---|
4 | import pymunk
|
---|
5 | import pymunk.pygame_util
|
---|
6 |
|
---|
7 | from nagslang import render
|
---|
8 | from nagslang.constants import (
|
---|
9 | COLLISION_TYPE_ENEMY, COLLISION_TYPE_FURNITURE, COLLISION_TYPE_SHEEP,
|
---|
10 | ACID_SPEED, ACID_DAMAGE, ZORDER_MID)
|
---|
11 | from nagslang.game_object import (GameObject, SingleShapePhysicser, Result,
|
---|
12 | Bullet, make_body)
|
---|
13 | from nagslang.collectable import KeyCard
|
---|
14 | from nagslang.mutators import FLIP_H
|
---|
15 | from nagslang.resources import resources
|
---|
16 | from nagslang.utils import vec_with_length
|
---|
17 | from nagslang.sound import sound
|
---|
18 |
|
---|
19 |
|
---|
20 | def 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 |
|
---|
29 | def 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 |
|
---|
34 | class 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 |
|
---|
178 | class 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 |
|
---|
196 | class 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 |
|
---|
243 | class 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 |
|
---|
283 | class 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 |
|
---|
315 | class 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 |
|
---|
356 | class 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")]
|
---|