source: nagslang/protagonist.py @ 363:3dd08e18580f

Last change on this file since 363:3dd08e18580f was 363:3dd08e18580f, checked in by Stefano Rivera <stefano@…>, 7 years ago

Acid attacks shoot things that look like acid

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