source: nagslang/protagonist.py

Last change on this file was 634:45eff33c3dad, checked in by Jeremy Thurgood <firxen@…>, 8 years ago

Increased health.

File size: 13.6 KB
Line 
1import math
2
3import pymunk
4import pymunk.pygame_util
5
6from nagslang import render
7from nagslang.constants import (
8 COLLISION_TYPE_PLAYER, ZORDER_MID, WEREWOLF_SOAK_FACTOR,
9 PROTAGONIST_HEALTH_MIN_LEVEL, PROTAGONIST_HEALTH_MAX_LEVEL,
10 NON_GAME_OBJECT_COLLIDERS, BULLET_DAMAGE, BULLET_SPEED, CLAW_DAMAGE,
11 CMD_TOGGLE_FORM, CMD_ACTION)
12from nagslang.game_object import (
13 GameObject, Physicser, Result, Bullet, ClawAttack, EphemeralNote,
14 make_body)
15from nagslang.mutators import FLIP_H
16from nagslang.resources import resources
17from nagslang.events import AddDrawableEvent, DeathEvent
18from nagslang.utils import vec_from_angle, vec_with_length
19
20
21class ProtagonistPhysicser(Physicser):
22 def __init__(self, space, form_shapes):
23 self._space = space
24 self._form_shapes = form_shapes
25 for shape in form_shapes.values():
26 shape.physicser = self
27
28 def switch_form(self, old_form, new_form):
29 self._space.remove(self._form_shapes[old_form])
30 shape = self._form_shapes[new_form]
31 self._space.add(shape)
32 for attr, value in shape.protagonist_body_props.iteritems():
33 setattr(shape.body, attr, value)
34
35 def get_shape(self):
36 return self._form_shapes[self.game_object.form]
37
38
39class ProtagonistFormSelectionRenderer(render.RendererSelectionRenderer):
40 def select_renderer(self):
41 return self.game_object.form
42
43
44def _make_change_sequence(old, new):
45 return (
46 new, new, old, old, old, old,
47 new, new, old, old, old,
48 new, new, old, old,
49 new, new, new, old, old,
50 new, new, new, new, old, old,
51 new)
52
53
54class Protagonist(GameObject):
55 """Representation of our fearless protagonist.
56
57 TODO: Factor out a bunch of this stuff when we need it for other objects.
58 """
59
60 HUMAN_FORM = 'human'
61 WOLF_FORM = 'wolf'
62
63 CHANGING_SEQUENCE = {
64 # The key is the form we're changing *from*.
65 HUMAN_FORM: _make_change_sequence(HUMAN_FORM, WOLF_FORM),
66 WOLF_FORM: _make_change_sequence(WOLF_FORM, HUMAN_FORM),
67 }
68
69 zorder = ZORDER_MID
70
71 def __init__(self, space, world, position):
72 self.form = self.HUMAN_FORM
73 super(Protagonist, self).__init__(
74 self._make_physics(space, position), self._make_renderer())
75 self.world = world
76 self.health_level = PROTAGONIST_HEALTH_MAX_LEVEL
77
78 self.angle = 0
79 self.is_moving = False
80 self.changing_sequence = []
81 self.add_timer('attack_cooldown', 0.2)
82 self.add_timer('change_delay', 0.1)
83
84 self.go_human()
85
86 def _make_physics(self, space, position):
87 body = make_body(10, pymunk.inf, position, 0.8)
88 body.velocity_limit = 1000
89
90 human = pymunk.Poly(body, [(-15, -30), (15, -30), (15, 30), (-15, 30)])
91 human.elasticity = 1.0
92 human.collision_type = COLLISION_TYPE_PLAYER
93 human.protagonist_body_props = {
94 'mass': 10,
95 'damping': 0.8,
96 }
97
98 wolf = pymunk.Circle(body, 30)
99 wolf.elasticity = 1.0
100 wolf.collision_type = COLLISION_TYPE_PLAYER
101 wolf.protagonist_body_props = {
102 'mass': 100,
103 'damping': 0.9,
104 }
105
106 return ProtagonistPhysicser(space, {
107 self.HUMAN_FORM: human,
108 self.WOLF_FORM: wolf,
109 })
110
111 def _get_image(self, name, *transforms):
112 return resources.get_image('creatures', name, transforms=transforms)
113
114 def change_space(self, new_space):
115 self.physicser.remove_from_space()
116 self.physicser.set_space(new_space)
117 self.physicser.add_to_space()
118
119 def reset(self):
120 self.health_level = PROTAGONIST_HEALTH_MAX_LEVEL
121 self.is_moving = False
122
123 self.go_human()
124
125 def _make_renderer(self):
126 return ProtagonistFormSelectionRenderer({
127 self.HUMAN_FORM: render.FacingSelectionRenderer(
128 {
129 'N': render.MovementAnimatedRenderer(
130 [self._get_image('human_N_1.png'),
131 self._get_image('human_N_2.png')], 3),
132 'S': render.MovementAnimatedRenderer(
133 [self._get_image('human_S_1.png'),
134 self._get_image('human_S_2.png')], 3),
135 'W': render.MovementAnimatedRenderer(
136 [self._get_image('human_W_1.png'),
137 self._get_image('human_W_2.png')], 3),
138 'E': render.MovementAnimatedRenderer(
139 [self._get_image('human_W_1.png', FLIP_H),
140 self._get_image('human_W_2.png', FLIP_H)], 3),
141 'NW': render.MovementAnimatedRenderer(
142 [self._get_image('human_NW_1.png'),
143 self._get_image('human_NW_2.png')], 3),
144 'NE': render.MovementAnimatedRenderer(
145 [self._get_image('human_NW_1.png', FLIP_H),
146 self._get_image('human_NW_2.png', FLIP_H)], 3),
147 'SW': render.MovementAnimatedRenderer(
148 [self._get_image('human_SW_1.png'),
149 self._get_image('human_SW_2.png')], 3),
150 'SE': render.MovementAnimatedRenderer(
151 [self._get_image('human_SW_1.png', FLIP_H),
152 self._get_image('human_SW_2.png', FLIP_H)], 3),
153 }),
154 self.WOLF_FORM: render.FacingSelectionRenderer(
155 {
156 'N': render.MovementAnimatedRenderer(
157 [self._get_image('werewolf_N_1.png'),
158 self._get_image('werewolf_N_2.png')], 3),
159 'S': render.MovementAnimatedRenderer(
160 [self._get_image('werewolf_S_1.png'),
161 self._get_image('werewolf_S_2.png')], 3),
162 'W': render.MovementAnimatedRenderer(
163 [self._get_image('werewolf_W_1.png'),
164 self._get_image('werewolf_W_2.png')], 3),
165 'E': render.MovementAnimatedRenderer(
166 [self._get_image('werewolf_W_1.png', FLIP_H),
167 self._get_image('werewolf_W_2.png', FLIP_H)], 3),
168 'NW': render.MovementAnimatedRenderer(
169 [self._get_image('werewolf_NW_1.png'),
170 self._get_image('werewolf_NW_2.png')], 3),
171 'NE': render.MovementAnimatedRenderer(
172 [self._get_image('werewolf_NW_1.png', FLIP_H),
173 self._get_image('werewolf_NW_2.png', FLIP_H)], 3),
174 'SW': render.MovementAnimatedRenderer(
175 [self._get_image('werewolf_SW_1.png'),
176 self._get_image('werewolf_SW_2.png')], 3),
177 'SE': render.MovementAnimatedRenderer(
178 [self._get_image('werewolf_SW_1.png', FLIP_H),
179 self._get_image('werewolf_SW_2.png', FLIP_H)], 3),
180 }),
181 })
182
183 @classmethod
184 def from_saved_state(cls, saved_state):
185 """Create an instance from the provided serialised state.
186 """
187 obj = cls()
188 # TODO: Update from saved state.
189 return obj
190
191 def handle_attack_key_down(self):
192 if self.changing_sequence:
193 self.fire_note(
194 "You can't do anything until your form has stabilised.")
195 return
196 if self.check_timer('attack_cooldown'):
197 return
198 self.start_timer('attack_cooldown')
199 self.world.attacks += 1
200 return self.attack()
201
202 def handle_keypress(self, key_command):
203 if self.changing_sequence:
204 self.fire_note(
205 "You can't do anything until your form has stabilised.")
206 if key_command == CMD_TOGGLE_FORM:
207 self.world.transformations += 1
208 self.toggle_form()
209 if key_command == CMD_ACTION:
210 self.perform_action()
211
212 def get_render_angle(self):
213 # No image rotation when rendering, please.
214 return 0
215
216 def get_facing_direction(self):
217 # Our angle is quantised to 45 degree intervals, so possible values for
218 # x and y in a unit vector are +/-(0, sqrt(2)/2, 1) with some floating
219 # point imprecision. Rounding will normalise these to (-1.0, 0.0, 1.0)
220 # which we can safely turn into integers and use as dict keys.
221 vec = vec_from_angle(self.angle)
222 x = int(round(vec.x))
223 y = int(round(vec.y))
224
225 return {
226 (0, 1): 'N',
227 (0, -1): 'S',
228 (-1, 0): 'W',
229 (1, 0): 'E',
230 (1, 1): 'NE',
231 (1, -1): 'SE',
232 (-1, 1): 'NW',
233 (-1, -1): 'SW',
234 }[(x, y)]
235
236 def go_werewolf(self):
237 self.physicser.switch_form(self.form, self.WOLF_FORM)
238 self.form = self.WOLF_FORM
239 self.impulse_factor = 4000
240
241 def go_human(self):
242 self.physicser.switch_form(self.form, self.HUMAN_FORM)
243 self.form = self.HUMAN_FORM
244 self.impulse_factor = 500
245
246 def set_direction(self, dx, dy):
247 if (dx, dy) == (0, 0) or self.changing_sequence:
248 self.is_moving = False
249 return
250 self.is_moving = True
251 vec = vec_with_length((dx, dy), self.impulse_factor)
252 self.angle = vec.angle
253 self.physicser.apply_impulse(vec)
254
255 def set_position(self, position):
256 self.physicser.position = position
257
258 def copy_state(self, old_protagonist):
259 self.physicser.position = old_protagonist.physicser.position
260 self.physicser.switch_form(self.form, old_protagonist.form)
261 self.impulse_factor = old_protagonist.impulse_factor
262 self.form = old_protagonist.form
263 self.angle = old_protagonist.angle
264
265 def toggle_form(self):
266 if self.check_timer('change_delay'):
267 return
268 self.changing_sequence.extend(self.CHANGING_SEQUENCE[self.form])
269
270 def _go_to_next_form(self):
271 if self.changing_sequence.pop(0) == self.WOLF_FORM:
272 self.go_werewolf()
273 else:
274 self.go_human()
275
276 def get_current_interactible(self):
277 for shape in self.get_space().shape_query(self.get_shape()):
278 if shape.collision_type in NON_GAME_OBJECT_COLLIDERS:
279 # No game object here.
280 continue
281 interactible = shape.physicser.game_object.interactible
282 if interactible is not None:
283 return interactible
284 return None
285
286 def perform_action(self):
287 """Perform an action on the target.
288 """
289 interactible = self.get_current_interactible()
290 if interactible is None:
291 # Nothing to interact with.
292 return
293 action = interactible.select_action(self)
294 if action is None:
295 # Nothing to do with it.
296 return
297 return action.perform(self)
298
299 def attack(self):
300 """Attempt to hurt something.
301 """
302 if self.in_wolf_form():
303 return self.claw()
304 else:
305 return self.shoot()
306
307 def shoot(self):
308 if not self.has_item('gun'):
309 self.fire_note('You are not armed.')
310 return
311 vec = vec_from_angle(self.angle, BULLET_SPEED)
312 return Result(add=(Bullet(self.get_space(), self.physicser.position,
313 vec, BULLET_DAMAGE, 'bullet',
314 COLLISION_TYPE_PLAYER,
315 "vocoder2_short.ogg"),))
316
317 def claw(self):
318 claw_range = (math.sqrt(math.pow(self.physicser.get_velocity()[0], 2) +
319 math.pow(self.physicser.get_velocity()[1], 2))
320 / 20) + 30
321 vec = vec_from_angle(self.angle, claw_range)
322 return Result(add=(ClawAttack(
323 self.get_space(), self.physicser.position, vec, CLAW_DAMAGE),))
324
325 def in_wolf_form(self):
326 return self.form == self.WOLF_FORM
327
328 def in_human_form(self):
329 return self.form == self.HUMAN_FORM
330
331 def has_item(self, item):
332 return item in self.world.inventory
333
334 def add_item(self, item):
335 self.world.inventory.add(item)
336
337 def get_health_level(self):
338 """Return current health level
339 """
340 return self.health_level
341
342 def hit(self, weapon):
343 '''Recieve an attack'''
344 self.lose_health(weapon.damage)
345
346 def die(self):
347 # Handle player death - may be called due to other reasons
348 # than zero health
349 self.reset()
350 DeathEvent.post()
351
352 def lose_health(self, amount):
353 if self.in_human_form():
354 self.health_level -= amount
355 else:
356 self.health_level -= amount / WEREWOLF_SOAK_FACTOR
357 if self.health_level <= PROTAGONIST_HEALTH_MIN_LEVEL:
358 self.die()
359
360 def gain_health(self, amount):
361 self.health_level += amount
362 if self.health_level > PROTAGONIST_HEALTH_MAX_LEVEL:
363 self.health_level = PROTAGONIST_HEALTH_MAX_LEVEL
364
365 def _decrement_timer(self, timer, dt):
366 if self._timers[timer] > 0:
367 self._timers[timer] -= dt
368 if self._timers[timer] < 0:
369 self._timers[timer] = 0
370
371 def update(self, dt):
372 if self.changing_sequence:
373 self._go_to_next_form()
374 if int(self.lifetime + dt) > int(self.lifetime):
375 if self.in_wolf_form():
376 self.gain_health(2)
377 super(Protagonist, self).update(dt)
378
379 def force_wolf_form(self):
380 if self.in_human_form() and not self.changing_sequence:
381 self.toggle_form()
382 self.start_timer('change_delay')
383
384 def fire_note(self, msg, secs=1):
385 AddDrawableEvent.post(EphemeralNote(msg, secs))
Note: See TracBrowser for help on using the repository browser.