source: nagslang/enemies.py@ 409:180c27514619

Last change on this file since 409:180c27514619 was 409:180c27514619, checked in by Jeremy Thurgood <firxen@…>, 8 years ago

Better protagonist finding for enemies.

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