source: nagslang/protagonist.py@ 336:1d487646a4d4

Last change on this file since 336:1d487646a4d4 was 336:1d487646a4d4, checked in by Jeremy Thurgood <firxen@…>, 8 years ago

Better key handling, form change delay.

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