source: nagslang/enemies.py@ 366:9898fa231c4b

Last change on this file since 366:9898fa231c4b was 366:9898fa231c4b, checked in by Stefano Rivera <stefano@…>, 9 years ago

Move ranged attack code to Enemy. Don't shoot through solid objects

File size: 8.3 KB
Line 
1import math
2
3import pymunk
4import pymunk.pygame_util
5from pymunk.vec2d import Vec2d
6
7from nagslang import render
8from nagslang.constants import (COLLISION_TYPE_ENEMY, COLLISION_TYPE_FURNITURE,
9 ACID_SPEED, ACID_DAMAGE, ZORDER_MID)
10from nagslang.events import EnemyDeathEvent, FireEvent
11from nagslang.game_object import GameObject, SingleShapePhysicser, 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 self._last_fired = 0
45
46 def make_physics(self, space, position):
47 raise NotImplementedError
48
49 def make_renderer(self):
50 return render.FacingSelectionRenderer({
51 'left': render.TimedAnimatedRenderer(
52 [get_alien_image(self.enemy_type, '1'),
53 get_alien_image(self.enemy_type, '2')], 3),
54 'right': render.TimedAnimatedRenderer(
55 [get_alien_image(self.enemy_type, '1', FLIP_H),
56 get_alien_image(self.enemy_type, '2', FLIP_H)], 3),
57 })
58
59 def attack(self):
60 raise NotImplementedError
61
62 @classmethod
63 def requires(cls):
64 return [("name", "string"), ("position", "coordinates")]
65
66 def hit(self, weapon):
67 self.lose_health(weapon.damage)
68
69 def lose_health(self, amount):
70 self.health -= amount
71 if self.health < 0:
72 self.physicser.remove_from_space()
73 self.remove = True
74 EnemyDeathEvent.post(self.physicser.position, self.enemy_type)
75
76 def set_direction(self, dx, dy):
77 vec = vec_with_length((dx, dy), self.impulse_factor)
78 self.angle = vec.angle
79 self.physicser.apply_impulse(vec)
80
81 def collide_with_protagonist(self, protagonist):
82 if self.enemy_damage is not None:
83 protagonist.lose_health(self.enemy_damage)
84
85 def collide_with_claw_attack(self, claw_attack):
86 self.lose_health(claw_attack.damage)
87
88 def ranged_attack(self, range_, speed, damage, type_, reload_time):
89 pos = self.physicser.position
90 target = self.world.protagonist.get_shape().body.position
91
92 r = self.get_space().segment_query(pos, target)
93 for collision in r:
94 shape = collision.shape
95 if (shape in (self.get_shape(), self.world.protagonist.get_shape())
96 or shape.sensor):
97 continue
98 return
99
100 if self.lifetime - self._last_fired >= reload_time:
101 vec = Vec2d((target.x - pos.x, target.y - pos.y))
102 if vec.length < range_:
103 vec.length = speed
104 FireEvent.post(pos, vec, damage, type_,
105 COLLISION_TYPE_ENEMY)
106 self._last_fired = self.lifetime
107
108
109class DeadEnemy(GameObject):
110 def __init__(self, space, world, position, enemy_type='A'):
111 body = make_body(10, 10000, position, damping=0.5)
112 self.shape = pymunk.Poly(
113 body, [(-20, -20), (20, -20), (20, 20), (-20, 20)])
114 self.shape.friction = 0.5
115 self.shape.collision_type = COLLISION_TYPE_FURNITURE
116 super(DeadEnemy, self).__init__(
117 SingleShapePhysicser(space, self.shape),
118 render.ImageRenderer(resources.get_image(
119 'creatures', 'alien_%s_dead.png' % enemy_type)),
120 )
121
122 @classmethod
123 def requires(cls):
124 return [("name", "string"), ("position", "coordinates")]
125
126
127class PatrollingAlien(Enemy):
128 is_moving = True # Always walking.
129 enemy_type = 'A'
130 health = 42
131 enemy_damage = 15
132 impulse_factor = 50
133
134 def __init__(self, space, world, position, end_position):
135 # An enemy that patrols between the two points
136 super(PatrollingAlien, self).__init__(space, world, position)
137 self._start_pos = position
138 self._end_pos = end_position
139 self._direction = 'away'
140
141 def make_physics(self, space, position):
142 body = make_body(10, pymunk.inf, position, 0.8)
143 shape = pymunk.Circle(body, 30)
144 shape.elasticity = 1.0
145 shape.friction = 0.05
146 shape.collision_type = COLLISION_TYPE_ENEMY
147 return SingleShapePhysicser(space, shape)
148
149 def get_render_angle(self):
150 # No image rotation when rendering, please.
151 return 0
152
153 def get_facing_direction(self):
154 # Enemies can face left or right.
155 if - math.pi / 2 < self.angle <= math.pi / 2:
156 return 'right'
157 else:
158 return 'left'
159
160 def _switch_direction(self):
161 if self._direction == 'away':
162 self._direction = 'towards'
163 else:
164 self._direction = 'away'
165
166 def update(self, dt):
167 # Calculate the step every frame
168 if self._direction == 'away':
169 target = self._end_pos
170 else:
171 target = self._start_pos
172 x_step = 0
173 y_step = 0
174 if (target[0] < self.physicser.position[0]):
175 x_step = max(-1, target[0] - self.physicser.position[0])
176 elif (target[0] > self.physicser.position[0]):
177 x_step = min(1, target[0] - self.physicser.position[0])
178 if abs(x_step) < 0.5:
179 x_step = 0
180 if (target[1] < self.physicser.position[1]):
181 y_step = max(-1, target[1] - self.physicser.position[1])
182 elif (target[1] > self.physicser.position[1]):
183 y_step = min(1, target[1] - self.physicser.position[1])
184 if abs(y_step) < 0.5:
185 y_step = 0
186 if abs(x_step) < 1 and abs(y_step) < 1:
187 self._switch_direction()
188 self.set_direction(x_step, y_step)
189 self.ranged_attack(300, ACID_SPEED, ACID_DAMAGE, 'acid', 0.2)
190 super(PatrollingAlien, self).update(dt)
191
192 @classmethod
193 def requires(cls):
194 return [("name", "string"), ("position", "coordinates"),
195 ("end_position", "coordinates")]
196
197
198class ChargingAlien(Enemy):
199 # Simplistic charging of the protagonist
200 is_moving = False
201 enemy_type = 'B'
202 health = 42
203 enemy_damage = 20
204 impulse_factor = 300
205
206 def __init__(self, space, world, position, attack_range=100):
207 super(ChargingAlien, self).__init__(space, world, position)
208 self._range = attack_range
209
210 def make_physics(self, space, position):
211 body = make_body(100, 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 get_render_angle(self):
219 # No image rotation when rendering, please.
220 return 0
221
222 def get_facing_direction(self):
223 # Enemies can face left or right.
224 if - math.pi / 2 < self.angle <= math.pi / 2:
225 return 'right'
226 else:
227 return 'left'
228
229 def update(self, dt):
230 # Calculate the step every frame
231 # Distance to the protagonist
232 self.ranged_attack(300, ACID_SPEED, ACID_DAMAGE, 'acid', 0.2)
233 pos = self.physicser.position
234 target = self.world.protagonist.get_shape().body.position
235 if pos.get_distance(target) > self._range:
236 # stop
237 self.is_moving = False
238 return
239 self.is_moving = True
240 dx = target.x - pos.x
241 dy = target.y - pos.y
242 self.set_direction(dx, dy)
243 super(ChargingAlien, self).update(dt)
244
245 @classmethod
246 def requires(cls):
247 return [("name", "string"), ("position", "coordinates"),
248 ("attack_range", "distance")]
Note: See TracBrowser for help on using the repository browser.