source: nagslang/protagonist.py @ 393:8d961e05b7b6

Last change on this file since 393:8d961e05b7b6 was 393:8d961e05b7b6, checked in by Stefano Rivera <stefano@…>, 7 years ago

Use Result to handle firing

File size: 13.3 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 ClawEvent
13from nagslang.game_object import (
14    GameObject, Physicser, Result, Bullet, make_body)
15from nagslang.mutators import FLIP_H
16from nagslang.resources import resources
17from nagslang.events import ScreenChange
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 or self.check_timer('attack_cooldown'):
193            return
194        self.start_timer('attack_cooldown')
195        self.world.attacks += 1
196        return self.attack()
197
198    def handle_keypress(self, key_command):
199        if self.changing_sequence:
200            return
201        if key_command == CMD_TOGGLE_FORM:
202            self.world.transformations += 1
203            self.toggle_form()
204        if key_command == CMD_ACTION:
205            self.perform_action()
206
207    def get_render_angle(self):
208        # No image rotation when rendering, please.
209        return 0
210
211    def get_facing_direction(self):
212        # Our angle is quantised to 45 degree intervals, so possible values for
213        # x and y in a unit vector are +/-(0, sqrt(2)/2, 1) with some floating
214        # point imprecision. Rounding will normalise these to (-1.0, 0.0, 1.0)
215        # which we can safely turn into integers and use as dict keys.
216        vec = vec_from_angle(self.angle)
217        x = int(round(vec.x))
218        y = int(round(vec.y))
219
220        return {
221            (0, 1): 'N',
222            (0, -1): 'S',
223            (-1, 0): 'W',
224            (1, 0): 'E',
225            (1, 1): 'NE',
226            (1, -1): 'SE',
227            (-1, 1): 'NW',
228            (-1, -1): 'SW',
229        }[(x, y)]
230
231    def go_werewolf(self):
232        self.physicser.switch_form(self.form, self.WOLF_FORM)
233        self.form = self.WOLF_FORM
234        self.impulse_factor = 4000
235
236    def go_human(self):
237        self.physicser.switch_form(self.form, self.HUMAN_FORM)
238        self.form = self.HUMAN_FORM
239        self.impulse_factor = 500
240
241    def set_direction(self, dx, dy):
242        if (dx, dy) == (0, 0) or self.changing_sequence:
243            self.is_moving = False
244            return
245        self.is_moving = True
246        vec = vec_with_length((dx, dy), self.impulse_factor)
247        self.angle = vec.angle
248        self.physicser.apply_impulse(vec)
249
250    def set_position(self, position):
251        self.physicser.position = position
252
253    def copy_state(self, old_protagonist):
254        self.physicser.position = old_protagonist.physicser.position
255        self.physicser.switch_form(self.form, old_protagonist.form)
256        self.impulse_factor = old_protagonist.impulse_factor
257        self.form = old_protagonist.form
258        self.angle = old_protagonist.angle
259
260    def toggle_form(self):
261        if self.check_timer('change_delay'):
262            return
263        self.changing_sequence.extend(self.CHANGING_SEQUENCE[self.form])
264
265    def _go_to_next_form(self):
266        if self.changing_sequence.pop(0) == self.WOLF_FORM:
267            self.go_werewolf()
268        else:
269            self.go_human()
270
271    def get_current_interactible(self):
272        for shape in self.get_space().shape_query(self.get_shape()):
273            if shape.collision_type in NON_GAME_OBJECT_COLLIDERS:
274                # No game object here.
275                continue
276            interactible = shape.physicser.game_object.interactible
277            if interactible is not None:
278                return interactible
279        return None
280
281    def perform_action(self):
282        """Perform an action on the target.
283        """
284        interactible = self.get_current_interactible()
285        if interactible is None:
286            # Nothing to interact with.
287            return
288        action = interactible.select_action(self)
289        if action is None:
290            # Nothing to do with it.
291            return
292        return action.perform(self)
293
294    def attack(self):
295        """Attempt to hurt something.
296        """
297        if self.in_wolf_form():
298            return self.claw()
299        else:
300            return self.shoot()
301
302    def shoot(self):
303        if not self.has_item('gun'):
304            return
305        vec = vec_from_angle(self.angle, BULLET_SPEED)
306        return Result(add=(Bullet(self.get_space(), self.physicser.position,
307                                  vec, BULLET_DAMAGE, 'bullet',
308                                  COLLISION_TYPE_PLAYER),))
309
310    def claw(self):
311        claw_range = (math.sqrt(math.pow(self.physicser.get_velocity()[0], 2) +
312                                math.pow(self.physicser.get_velocity()[1], 2))
313                      / 20) + 30
314        vec = vec_from_angle(self.angle, claw_range)
315        ClawEvent.post(self.physicser.position, vec, CLAW_DAMAGE)
316
317    def in_wolf_form(self):
318        return self.form == self.WOLF_FORM
319
320    def in_human_form(self):
321        return self.form == self.HUMAN_FORM
322
323    def has_item(self, item):
324        return item in self.world.inventory
325
326    def add_item(self, item):
327        self.world.inventory.add(item)
328
329    def environmental_movement(self, dx, dy):
330        if (dx, dy) == (0, 0):
331            return
332        self.physicser.apply_impulse((dx, dy))
333
334    def get_health_level(self):
335        """Return current health level
336        """
337        return self.health_level
338
339    def hit(self, weapon):
340        '''Recieve an attack'''
341        self.lose_health(weapon.damage)
342
343    def die(self):
344        # Handle player death - may be called due to other reasons
345        # than zero health
346        self.world.deaths += 1
347        ScreenChange.post('dead')
348
349    def lose_health(self, amount):
350        if self.in_human_form():
351            self.health_level -= amount
352        else:
353            self.health_level -= amount / WEREWOLF_SOAK_FACTOR
354        if self.health_level <= PROTAGONIST_HEALTH_MIN_LEVEL:
355            self.die()
356
357    def gain_health(self, amount):
358        self.health_level += amount
359        if self.health_level > PROTAGONIST_HEALTH_MAX_LEVEL:
360            self.health_level = PROTAGONIST_HEALTH_MAX_LEVEL
361
362    def _decrement_timer(self, timer, dt):
363        if self._timers[timer] > 0:
364            self._timers[timer] -= dt
365        if self._timers[timer] < 0:
366            self._timers[timer] = 0
367
368    def update(self, dt):
369        if self.changing_sequence:
370            self._go_to_next_form()
371        if int(self.lifetime + dt) > int(self.lifetime):
372            if self.in_wolf_form():
373                self.gain_health(1)
374        super(Protagonist, self).update(dt)
375
376    def force_wolf_form(self):
377        if self.in_human_form() and not self.changing_sequence:
378            self.toggle_form()
379        self.start_timer('change_delay')
Note: See TracBrowser for help on using the repository browser.