source: nagslang/enemies.py@ 689:45d2a6aef3a4

Last change on this file since 689:45d2a6aef3a4 was 689:45d2a6aef3a4, checked in by Neil Muller <drnlmuller@…>, 2 years ago

Some flake8 fixes

File size: 12.7 KB
RevLine 
[229]1import math
[368]2import random
[229]3
[168]4import pymunk
5import pymunk.pygame_util
6
[207]7from nagslang import render
[410]8from nagslang.constants import (
9 COLLISION_TYPE_ENEMY, COLLISION_TYPE_FURNITURE, COLLISION_TYPE_SHEEP,
10 ACID_SPEED, ACID_DAMAGE, ZORDER_MID)
[385]11from nagslang.game_object import (GameObject, SingleShapePhysicser, Result,
[393]12 Bullet, make_body)
[602]13from nagslang.collectable import KeyCard
[168]14from nagslang.mutators import FLIP_H
15from nagslang.resources import resources
[334]16from nagslang.utils import vec_with_length
[620]17from nagslang.sound import sound
[168]18
19
[235]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
[334]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
[168]34class Enemy(GameObject):
35 """A base class for mobile enemies"""
[334]36 zorder = ZORDER_MID
37 enemy_type = None # Which set of images to use
[333]38 enemy_damage = None
[334]39 health = None
40 impulse_factor = None
[410]41 random_move_time = 0.3
[168]42
[277]43 def __init__(self, space, world, position):
[334]44 super(Enemy, self).__init__(
45 self.make_physics(space, position), self.make_renderer())
46 self.world = world
47 self.angle = 0
[410]48 self.add_timer('random_move', self.random_move_time)
49 self._last_random_direction = (0, 0)
[168]50
[334]51 def make_physics(self, space, position):
[168]52 raise NotImplementedError
53
[334]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 })
[168]63
[410]64 def get_render_angle(self):
65 # No image rotation when rendering, please.
66 return 0
[168]67
[410]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'
[235]74
[310]75 def hit(self, weapon):
76 self.lose_health(weapon.damage)
77
[305]78 def lose_health(self, amount):
79 self.health -= amount
[385]80 if self.health <= 0:
[406]81 self.world.kills += 1
[305]82 self.physicser.remove_from_space()
[308]83
[446]84 def set_direction(self, dx, dy, force_factor=1):
85 vec = vec_with_length((dx, dy), self.impulse_factor * force_factor)
[334]86 self.angle = vec.angle
87 self.physicser.apply_impulse(vec)
88
[333]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
[409]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
[410]114 def ranged_attack(self, range_, speed, damage, type_, reload_time, result):
[409]115 vec = self.range_to_visible_protagonist()
116 if vec is None:
[410]117 return
[366]118
[372]119 if not self.check_timer('reload_time'):
120 self.start_timer('reload_time', reload_time)
[366]121 if vec.length < range_:
122 vec.length = speed
[409]123 result.add += (Bullet(
124 self.get_space(), self.physicser.position, vec, damage,
[612]125 type_, COLLISION_TYPE_ENEMY,
126 "mouth_pop_2a.ogg"),)
[366]127
[368]128 def greedy_move(self, target):
129 """Simple greedy path finder"""
130 def _calc_step(tp, pp):
[453]131 step = 0
[368]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"""
[410]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
[368]156
[385]157 def update(self, dt):
158 super(Enemy, self).update(dt)
[393]159 result = Result()
[385]160 if self.health <= 0:
[393]161 result.remove += (self,)
162 result.add += (DeadEnemy(self.get_space(), self.world,
163 self.physicser.position,
164 self.enemy_type),)
[620]165 self.play_death_sound()
[410]166 self.move(result)
167 self.attack(result)
[393]168 return result
[385]169
[620]170 def play_death_sound(self):
171 sound.play_sound("synth_detuned1.ogg")
172
[410]173 @classmethod
174 def requires(cls):
175 return [("name", "string"), ("position", "coordinates")]
176
[308]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
[318]184 self.shape.collision_type = COLLISION_TYPE_FURNITURE
[308]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")]
[305]194
[168]195
196class PatrollingAlien(Enemy):
[218]197 is_moving = True # Always walking.
[308]198 enemy_type = 'A'
[334]199 health = 42
[341]200 enemy_damage = 15
[334]201 impulse_factor = 50
[168]202
[277]203 def __init__(self, space, world, position, end_position):
[168]204 # An enemy that patrols between the two points
[277]205 super(PatrollingAlien, self).__init__(space, world, position)
[168]206 self._start_pos = position
207 self._end_pos = end_position
208 self._direction = 'away'
209
[334]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)
[168]217
218 def _switch_direction(self):
219 if self._direction == 'away':
220 self._direction = 'towards'
221 else:
222 self._direction = 'away'
223
[410]224 def move(self, result):
[168]225 if self._direction == 'away':
226 target = self._end_pos
227 else:
228 target = self._start_pos
[368]229 x_step, y_step = self.greedy_move(target)
[168]230 if abs(x_step) < 1 and abs(y_step) < 1:
231 self._switch_direction()
232 self.set_direction(x_step, y_step)
[410]233
234 def attack(self, result):
235 self.ranged_attack(300, ACID_SPEED, ACID_DAMAGE, 'acid', 0.2, result)
[258]236
[235]237 @classmethod
238 def requires(cls):
239 return [("name", "string"), ("position", "coordinates"),
240 ("end_position", "coordinates")]
[278]241
242
243class ChargingAlien(Enemy):
244 # Simplistic charging of the protagonist
245 is_moving = False
[308]246 enemy_type = 'B'
[334]247 health = 42
[341]248 enemy_damage = 20
[334]249 impulse_factor = 300
[278]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
[334]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)
[278]262
[410]263 def move(self, result):
[334]264 pos = self.physicser.position
[278]265 target = self.world.protagonist.get_shape().body.position
266 if pos.get_distance(target) > self._range:
267 # stop
268 self.is_moving = False
[368]269 return 0, 0
[278]270 self.is_moving = True
[368]271 dx, dy = self.greedy_move(target)
[410]272 self.set_direction(dx, dy)
[368]273
[410]274 def attack(self, result):
275 self.ranged_attack(300, ACID_SPEED, ACID_DAMAGE, 'acid', 0.2, result)
[278]276
277 @classmethod
278 def requires(cls):
279 return [("name", "string"), ("position", "coordinates"),
280 ("attack_range", "distance")]
[368]281
282
283class RunAndGunAlien(ChargingAlien):
284 # Simplistic shooter
285 # Move until we're in range, and then randomly
[500]286 enemy_type = "C"
[368]287 impulse_factor = 90
[500]288 health = 42
289 enemy_damage = 25
[368]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
[410]300 def move(self, result):
[368]301 pos = self.physicser.position
302 target = self.world.protagonist.get_shape().body.position
303 if pos.get_distance(target) < self._range:
[410]304 step = self.random_move()
[368]305 else:
[410]306 step = self.greedy_move(target)
307 self.set_direction(*step)
[368]308
309 @classmethod
310 def requires(cls):
311 return [("name", "string"), ("position", "coordinates"),
312 ("attack_range", "distance")]
[554]313
[553]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
[629]324 spawn_class_args = (400,)
[554]325
[553]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)
[554]329
[553]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):
[554]334 result.add += (self.spawn_class(self.get_space(),
[689]335 self.world, self.physicser.position,
336 *self.spawn_class_args),)
[554]337
[553]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)
[623]344 if (self.health <= 0
345 and not self.world.protagonist.has_item('keycard_cyan')):
[602]346 result.add += (KeyCard(self.get_space(),
[623]347 self.physicser.position, 'keycard_cyan'),)
[553]348 return result
349
350 @classmethod
351 def requires(cls):
352 return [("name", "string"), ("position", "coordinates"),
353 ("attack_range", "distance")]
[410]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
[415]362 human_vision_range = 100
363 wolf_vision_range = 200
[410]364
365 def make_physics(self, space, position):
366 body = make_body(10, pymunk.inf, position, 0.8)
[600]367 shape = pymunk.Circle(body, 20)
[410]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()
[415]375 prot = self.world.protagonist
376 if vec is not None:
377 if prot.in_human_form() and vec.length < self.human_vision_range:
[446]378 self.set_direction(vec.x, vec.y, 1.5)
379 return
[415]380 if prot.in_wolf_form() and vec.length < self.wolf_vision_range:
[446]381 vec = -vec
382 self.set_direction(vec.x, vec.y, 3)
383 return
384 self.set_direction(*self.random_move())
[410]385
[620]386 def play_death_sound(self):
387 sound.play_sound("eviltoy1.ogg")
388
[410]389 @classmethod
390 def requires(cls):
391 return [("name", "string"), ("position", "coordinates")]
Note: See TracBrowser for help on using the repository browser.