1 | import math
|
---|
2 |
|
---|
3 | import pymunk
|
---|
4 | import pymunk.pygame_util
|
---|
5 |
|
---|
6 | from nagslang import render
|
---|
7 | from nagslang.constants import (COLLISION_TYPE_ENEMY, COLLISION_TYPE_FURNITURE,
|
---|
8 | ZORDER_MID)
|
---|
9 | from nagslang.events import EnemyDeathEvent
|
---|
10 | from nagslang.game_object import GameObject, SingleShapePhysicser, make_body
|
---|
11 | from nagslang.mutators import FLIP_H
|
---|
12 | from nagslang.resources import resources
|
---|
13 |
|
---|
14 |
|
---|
15 | def get_editable_enemies():
|
---|
16 | classes = []
|
---|
17 | for cls_name, cls in globals().iteritems():
|
---|
18 | if isinstance(cls, type) and issubclass(cls, Enemy):
|
---|
19 | if hasattr(cls, 'requires'):
|
---|
20 | classes.append((cls_name, cls))
|
---|
21 | return classes
|
---|
22 |
|
---|
23 |
|
---|
24 | class Enemy(GameObject):
|
---|
25 | """A base class for mobile enemies"""
|
---|
26 | enemy_type = None # Image to use for dead Enemy
|
---|
27 | enemy_damage = None
|
---|
28 |
|
---|
29 | def __init__(self, space, world, position):
|
---|
30 | self._setup_physics(space, position)
|
---|
31 | self._setup_renderer()
|
---|
32 | self.health = 42
|
---|
33 |
|
---|
34 | super(Enemy, self).__init__(
|
---|
35 | self._physicser, self.renderer)
|
---|
36 | self.zorder = ZORDER_MID
|
---|
37 | self.world = world
|
---|
38 |
|
---|
39 | def _get_image(self, name, *transforms):
|
---|
40 | return resources.get_image('creatures', name, transforms=transforms)
|
---|
41 |
|
---|
42 | def _setup_physics(self, space, position):
|
---|
43 | raise NotImplementedError
|
---|
44 |
|
---|
45 | def _setup_renderer(self):
|
---|
46 | raise NotImplementedError
|
---|
47 |
|
---|
48 | def attack(self):
|
---|
49 | raise NotImplementedError
|
---|
50 |
|
---|
51 | @classmethod
|
---|
52 | def requires(cls):
|
---|
53 | return [("name", "string"), ("position", "coordinates")]
|
---|
54 |
|
---|
55 | def hit(self, weapon):
|
---|
56 | self.lose_health(weapon.damage)
|
---|
57 |
|
---|
58 | def lose_health(self, amount):
|
---|
59 | self.health -= amount
|
---|
60 | if self.health < 0:
|
---|
61 | self.physicser.remove_from_space()
|
---|
62 | self.remove = True
|
---|
63 | EnemyDeathEvent.post(self.physicser.position, self.enemy_type)
|
---|
64 |
|
---|
65 | def collide_with_protagonist(self, protagonist):
|
---|
66 | if self.enemy_damage is not None:
|
---|
67 | protagonist.lose_health(self.enemy_damage)
|
---|
68 |
|
---|
69 | def collide_with_claw_attack(self, claw_attack):
|
---|
70 | self.lose_health(claw_attack.damage)
|
---|
71 |
|
---|
72 |
|
---|
73 | class DeadEnemy(GameObject):
|
---|
74 | def __init__(self, space, world, position, enemy_type='A'):
|
---|
75 | body = make_body(10, 10000, position, damping=0.5)
|
---|
76 | self.shape = pymunk.Poly(
|
---|
77 | body, [(-20, -20), (20, -20), (20, 20), (-20, 20)])
|
---|
78 | self.shape.friction = 0.5
|
---|
79 | self.shape.collision_type = COLLISION_TYPE_FURNITURE
|
---|
80 | super(DeadEnemy, self).__init__(
|
---|
81 | SingleShapePhysicser(space, self.shape),
|
---|
82 | render.ImageRenderer(resources.get_image(
|
---|
83 | 'creatures', 'alien_%s_dead.png' % enemy_type)),
|
---|
84 | )
|
---|
85 |
|
---|
86 | @classmethod
|
---|
87 | def requires(cls):
|
---|
88 | return [("name", "string"), ("position", "coordinates")]
|
---|
89 |
|
---|
90 |
|
---|
91 | class PatrollingAlien(Enemy):
|
---|
92 | is_moving = True # Always walking.
|
---|
93 | enemy_type = 'A'
|
---|
94 |
|
---|
95 | def __init__(self, space, world, position, end_position):
|
---|
96 | # An enemy that patrols between the two points
|
---|
97 | super(PatrollingAlien, self).__init__(space, world, position)
|
---|
98 | self._start_pos = position
|
---|
99 | self._end_pos = end_position
|
---|
100 | self._direction = 'away'
|
---|
101 |
|
---|
102 | def _setup_physics(self, space, position):
|
---|
103 | self._body = make_body(10, pymunk.inf, position, 0.8)
|
---|
104 |
|
---|
105 | self._shape = pymunk.Circle(self._body, 30)
|
---|
106 |
|
---|
107 | self._shape.elasticity = 1.0
|
---|
108 | self._shape.friction = 0.05
|
---|
109 | self._shape.collision_type = COLLISION_TYPE_ENEMY
|
---|
110 | self._physicser = SingleShapePhysicser(space, self._shape)
|
---|
111 | self.impulse_factor = 50
|
---|
112 | self.angle = 0
|
---|
113 |
|
---|
114 | def _setup_renderer(self):
|
---|
115 | self.renderer = render.FacingSelectionRenderer({
|
---|
116 | 'left': render.TimedAnimatedRenderer(
|
---|
117 | [self._get_image('alien_A_1.png'),
|
---|
118 | self._get_image('alien_A_2.png')], 3),
|
---|
119 | 'right': render.TimedAnimatedRenderer(
|
---|
120 | [self._get_image('alien_A_1.png', FLIP_H),
|
---|
121 | self._get_image('alien_A_2.png', FLIP_H)], 3),
|
---|
122 | })
|
---|
123 |
|
---|
124 | def get_render_angle(self):
|
---|
125 | # No image rotation when rendering, please.
|
---|
126 | return 0
|
---|
127 |
|
---|
128 | def get_facing_direction(self):
|
---|
129 | # Enemies can face left or right.
|
---|
130 | if - math.pi / 2 < self.angle <= math.pi / 2:
|
---|
131 | return 'right'
|
---|
132 | else:
|
---|
133 | return 'left'
|
---|
134 |
|
---|
135 | def _switch_direction(self):
|
---|
136 | if self._direction == 'away':
|
---|
137 | self._direction = 'towards'
|
---|
138 | else:
|
---|
139 | self._direction = 'away'
|
---|
140 |
|
---|
141 | def set_direction(self, dx, dy):
|
---|
142 | self.angle = pymunk.Vec2d((dx, dy)).angle
|
---|
143 | self._body.apply_impulse(
|
---|
144 | (dx * self.impulse_factor, dy * self.impulse_factor))
|
---|
145 |
|
---|
146 | def update(self, dt):
|
---|
147 | # Calculate the step every frame
|
---|
148 | if self._direction == 'away':
|
---|
149 | target = self._end_pos
|
---|
150 | else:
|
---|
151 | target = self._start_pos
|
---|
152 | x_step = 0
|
---|
153 | y_step = 0
|
---|
154 | if (target[0] < self._body.position[0]):
|
---|
155 | x_step = max(-1, target[0] - self._body.position[0])
|
---|
156 | elif (target[0] > self._body.position[0]):
|
---|
157 | x_step = min(1, target[0] - self._body.position[0])
|
---|
158 | if abs(x_step) < 0.5:
|
---|
159 | x_step = 0
|
---|
160 | if (target[1] < self._body.position[1]):
|
---|
161 | y_step = max(-1, target[1] - self._body.position[1])
|
---|
162 | elif (target[1] > self._body.position[1]):
|
---|
163 | y_step = min(1, target[1] - self._body.position[1])
|
---|
164 | if abs(y_step) < 0.5:
|
---|
165 | y_step = 0
|
---|
166 | if abs(x_step) < 1 and abs(y_step) < 1:
|
---|
167 | self._switch_direction()
|
---|
168 | self.set_direction(x_step, y_step)
|
---|
169 | super(PatrollingAlien, self).update(dt)
|
---|
170 |
|
---|
171 | @classmethod
|
---|
172 | def requires(cls):
|
---|
173 | return [("name", "string"), ("position", "coordinates"),
|
---|
174 | ("end_position", "coordinates")]
|
---|
175 |
|
---|
176 |
|
---|
177 | class ChargingAlien(Enemy):
|
---|
178 | # Simplistic charging of the protagonist
|
---|
179 | is_moving = False
|
---|
180 | enemy_type = 'B'
|
---|
181 |
|
---|
182 | def __init__(self, space, world, position, attack_range=100):
|
---|
183 | super(ChargingAlien, self).__init__(space, world, position)
|
---|
184 | self._range = attack_range
|
---|
185 |
|
---|
186 | def _setup_physics(self, space, position):
|
---|
187 | self._body = make_body(100, pymunk.inf, position, 0.8)
|
---|
188 |
|
---|
189 | self._shape = pymunk.Circle(self._body, 30)
|
---|
190 |
|
---|
191 | self._shape.elasticity = 1.0
|
---|
192 | self._shape.friction = 0.05
|
---|
193 | self._shape.collision_type = COLLISION_TYPE_ENEMY
|
---|
194 | self._physicser = SingleShapePhysicser(space, self._shape)
|
---|
195 | self.impulse_factor = 300
|
---|
196 | self.angle = 0
|
---|
197 |
|
---|
198 | def _setup_renderer(self):
|
---|
199 | self.renderer = render.FacingSelectionRenderer({
|
---|
200 | 'left': render.TimedAnimatedRenderer(
|
---|
201 | [self._get_image('alien_B_1.png'),
|
---|
202 | self._get_image('alien_B_2.png')], 3),
|
---|
203 | 'right': render.TimedAnimatedRenderer(
|
---|
204 | [self._get_image('alien_B_1.png', FLIP_H),
|
---|
205 | self._get_image('alien_B_2.png', FLIP_H)], 3),
|
---|
206 | })
|
---|
207 |
|
---|
208 | def get_render_angle(self):
|
---|
209 | # No image rotation when rendering, please.
|
---|
210 | return 0
|
---|
211 |
|
---|
212 | def get_facing_direction(self):
|
---|
213 | # Enemies can face left or right.
|
---|
214 | if - math.pi / 2 < self.angle <= math.pi / 2:
|
---|
215 | return 'right'
|
---|
216 | else:
|
---|
217 | return 'left'
|
---|
218 |
|
---|
219 | def set_direction(self, dx, dy):
|
---|
220 | self.angle = pymunk.Vec2d((dx, dy)).angle
|
---|
221 | self._body.apply_impulse(
|
---|
222 | (dx * self.impulse_factor, dy * self.impulse_factor))
|
---|
223 |
|
---|
224 | def update(self, dt):
|
---|
225 | # Calculate the step every frame
|
---|
226 | # Distance to the protagonist
|
---|
227 | pos = self._body.position
|
---|
228 | target = self.world.protagonist.get_shape().body.position
|
---|
229 | if pos.get_distance(target) > self._range:
|
---|
230 | # stop
|
---|
231 | self.is_moving = False
|
---|
232 | return
|
---|
233 | self.is_moving = True
|
---|
234 | x_step = 0
|
---|
235 | y_step = 0
|
---|
236 | if (target[0] < pos[0]):
|
---|
237 | x_step = max(-1, target[0] - pos[0])
|
---|
238 | elif (target[0] > pos[0]):
|
---|
239 | x_step = min(1, target[0] - pos[0])
|
---|
240 | if abs(x_step) < 0.5:
|
---|
241 | x_step = 0
|
---|
242 | if (target[1] < pos[1]):
|
---|
243 | y_step = max(-1, target[1] - pos[1])
|
---|
244 | elif (target[1] > pos[1]):
|
---|
245 | y_step = min(1, target[1] - pos[1])
|
---|
246 | if abs(y_step) < 0.5:
|
---|
247 | y_step = 0
|
---|
248 | self.set_direction(x_step, y_step)
|
---|
249 | super(ChargingAlien, self).update(dt)
|
---|
250 |
|
---|
251 | @classmethod
|
---|
252 | def requires(cls):
|
---|
253 | return [("name", "string"), ("position", "coordinates"),
|
---|
254 | ("attack_range", "distance")]
|
---|