source: nagslang/enemies.py@ 415:9d2a8dfba670

Last change on this file since 415:9d2a8dfba670 was 415:9d2a8dfba670, checked in by Jeremy Thurgood <firxen@…>, 8 years ago

Sheep behaviour.

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