source: nagslang/protagonist.py @ 374:150332d6c1fb

Last change on this file since 374:150332d6c1fb was 374:150332d6c1fb, checked in by Stefano Rivera <stefano@…>, 7 years ago

Move the inventory to world, to slightly reduce overall hackyness

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