source: nagslang/game_object.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: 18.7 KB
Line 
1import pymunk
2import pymunk.pygame_util
3
4import math
5
6from nagslang import environment
7from nagslang import puzzle
8from nagslang import render
9from nagslang.mutators import FLIP_H
10from nagslang.constants import (
11    COLLISION_TYPE_DOOR, COLLISION_TYPE_FURNITURE, COLLISION_TYPE_PROJECTILE,
12    COLLISION_TYPE_SWITCH, COLLISION_TYPE_WEREWOLF_ATTACK,
13    SWITCH_PUSHERS, ZORDER_FLOOR, ZORDER_LOW, ZORDER_HIGH)
14from nagslang.resources import resources
15from nagslang.events import DoorEvent
16
17
18def get_editable_game_objects():
19    classes = []
20    for cls_name, cls in globals().iteritems():
21        if isinstance(cls, type) and hasattr(cls, 'requires'):
22            classes.append((cls_name, cls))
23    return classes
24
25
26class Physicser(object):
27    def __init__(self, space):
28        self._space = space
29
30    def get_space(self):
31        return self._space
32
33    def set_space(self, new_space):
34        self._space = new_space
35
36    def set_game_object(self, game_object):
37        self.game_object = game_object
38
39    def get_shape(self):
40        raise NotImplementedError()
41
42    def add_to_space(self):
43        shape = self.get_shape()
44        self.get_space().add(shape)
45        if not shape.body.is_static:
46            self.get_space().add(shape.body)
47
48    def remove_from_space(self):
49        shape = self.get_shape()
50        self.get_space().remove(shape)
51        if not shape.body.is_static:
52            self.get_space().remove(shape.body)
53
54    def get_render_position(self, surface):
55        pos = self.get_shape().body.position
56        return pymunk.pygame_util.to_pygame(pos, surface)
57
58    def get_angle(self):
59        return self.get_shape().body.angle
60
61    def get_velocity(self):
62        return self.get_shape().body.velocity
63
64    def _get_position(self):
65        return self.get_shape().body.position
66
67    def _set_position(self, position):
68        self.get_shape().body.position = position
69
70    position = property(_get_position, _set_position)
71
72    def apply_impulse(self, j, r=(0, 0)):
73        return self.get_shape().body.apply_impulse(j, r)
74
75
76class SingleShapePhysicser(Physicser):
77    def __init__(self, space, shape):
78        super(SingleShapePhysicser, self).__init__(space)
79        self._shape = shape
80        shape.physicser = self
81
82    def get_shape(self):
83        return self._shape
84
85
86def damping_velocity_func(body, gravity, damping, dt):
87    """Apply custom damping to this body's velocity.
88    """
89    damping = getattr(body, 'damping', damping)
90    return pymunk.Body.update_velocity(body, gravity, damping, dt)
91
92
93def make_body(mass, moment, position, damping=None):
94    body = pymunk.Body(mass, moment)
95    body.position = tuple(position)
96    if damping is not None:
97        body.damping = damping
98        body.velocity_func = damping_velocity_func
99    return body
100
101
102class GameObject(object):
103    """A representation of a thing in the game world.
104
105    This has a rendery thing, physicsy things and maybe some other things.
106    """
107
108    zorder = ZORDER_LOW
109    is_moving = False  # `True` if a movement animation should play.
110
111    def __init__(self, physicser, renderer, puzzler=None, overlay=None,
112                 interactible=None):
113        self.lifetime = 0
114        self.physicser = physicser
115        physicser.set_game_object(self)
116        self.physicser.add_to_space()
117        self.renderer = renderer
118        renderer.set_game_object(self)
119        self.puzzler = puzzler
120        if puzzler is not None:
121            puzzler.set_game_object(self)
122        self.overlay = overlay
123        if overlay is not None:
124            self.overlay.set_game_object(self)
125        self.interactible = interactible
126        if interactible is not None:
127            self.interactible.set_game_object(self)
128        self.remove = False  # If true, will be removed from drawables
129
130    def set_stored_state_dict(self, stored_state):
131        """Override this to set up whatever state storage you want.
132
133        The `stored_state` dict passed in contains whatever saved state we
134        might have for this object. If the return value of this method
135        evaluates to `True`, the contents of the `stored_state` dict will be
136        saved, otherwise it will be discarded.
137        """
138        pass
139
140    def get_space(self):
141        return self.physicser.get_space()
142
143    def get_shape(self):
144        return self.physicser.get_shape()
145
146    def get_render_position(self, surface):
147        return self.physicser.get_render_position(surface)
148
149    def get_render_angle(self):
150        return self.physicser.get_angle()
151
152    def get_facing_direction(self):
153        """Used by rendererd that care what direction an object is facing.
154        """
155        return None
156
157    def render(self, surface):
158        return self.renderer.render(surface)
159
160    def update(self, dt):
161        self.lifetime += dt
162        self.renderer.update(dt)
163
164    def hit(self, weapon):
165        '''Was hit with a weapon (such as a bullet)'''
166        pass
167
168    def collide_with_protagonist(self, protagonist):
169        """Called as a `pre_solve` collision callback with the protagonist.
170
171        You can return `False` to ignore the collision, anything else
172        (including `None`) to process the collision as normal.
173        """
174        return True
175
176    def collide_with_furniture(self, furniture):
177        return True
178
179    def collide_with_claw_attack(self, claw_attack):
180        return True
181
182    @classmethod
183    def requires(cls):
184        """Hints for the level editor"""
185        return [("name", "string")]
186
187
188class FloorSwitch(GameObject):
189    zorder = ZORDER_FLOOR
190
191    def __init__(self, space, position):
192        body = make_body(None, None, position)
193        self.shape = pymunk.Circle(body, 30)
194        self.shape.collision_type = COLLISION_TYPE_SWITCH
195        self.shape.sensor = True
196        super(FloorSwitch, self).__init__(
197            SingleShapePhysicser(space, self.shape),
198            render.ImageStateRenderer({
199                True: resources.get_image('objects', 'sensor_on.png'),
200                False: resources.get_image('objects', 'sensor_off.png'),
201            }),
202            puzzle.CollidePuzzler(*SWITCH_PUSHERS),
203        )
204
205    @classmethod
206    def requires(cls):
207        return [("name", "string"), ("position", "coordinates")]
208
209
210class Note(GameObject):
211    zorder = ZORDER_FLOOR
212
213    def __init__(self, space, position, message):
214        body = make_body(None, None, position)
215        self.shape = pymunk.Circle(body, 30)
216        self.shape.sensor = True
217        super(Note, self).__init__(
218            SingleShapePhysicser(space, self.shape),
219            render.ImageRenderer(resources.get_image('objects', 'note.png')),
220            puzzle.CollidePuzzler(),
221            render.TextOverlay(message),
222        )
223
224    @classmethod
225    def requires(cls):
226        return [("name", "string"), ("position", "coordinates"),
227                ("message", "text")]
228
229
230class FloorLight(GameObject):
231    zorder = ZORDER_FLOOR
232
233    def __init__(self, space, position, state_source):
234        body = make_body(None, None, position)
235        self.shape = pymunk.Circle(body, 10)
236        self.shape.collision_type = COLLISION_TYPE_SWITCH
237        self.shape.sensor = True
238        super(FloorLight, self).__init__(
239            SingleShapePhysicser(space, self.shape),
240            render.ImageStateRenderer({
241                True: resources.get_image('objects', 'light_on.png'),
242                False: resources.get_image('objects', 'light_off.png'),
243            }),
244            puzzle.StateProxyPuzzler(state_source),
245        )
246
247    @classmethod
248    def requires(cls):
249        return [("name", "string"), ("position", "coordinates"),
250                ("state_source", "puzzler")]
251
252
253class Box(GameObject):
254    def __init__(self, space, position):
255        body = make_body(10, 10000, position, damping=0.5)
256        self.shape = pymunk.Poly(
257            body, [(-20, -20), (20, -20), (20, 20), (-20, 20)])
258        self.shape.friction = 0.5
259        self.shape.collision_type = COLLISION_TYPE_FURNITURE
260        super(Box, self).__init__(
261            SingleShapePhysicser(space, self.shape),
262            render.ImageRenderer(resources.get_image('objects', 'crate.png')),
263        )
264
265    @classmethod
266    def requires(cls):
267        return [("name", "string"), ("position", "coordinates"),
268                ("state_source", "puzzler")]
269
270
271class BaseDoor(GameObject):
272    zorder = ZORDER_FLOOR
273    is_open = True
274
275    def __init__(self, space, position, destination, dest_pos, angle,
276                 renderer, condition):
277        body = make_body(pymunk.inf, pymunk.inf, position, damping=0.5)
278        self.shape = pymunk.Circle(body, 30)
279        self.shape.collision_type = COLLISION_TYPE_DOOR
280        self.shape.body.angle = float(angle) / 180 * math.pi
281        self.shape.sensor = True
282        self.destination = destination
283        self.dest_pos = tuple(dest_pos)
284        super(BaseDoor, self).__init__(
285            SingleShapePhysicser(space, self.shape),
286            renderer,
287            puzzle.ParentAttrPuzzler('is_open'),
288            interactible=environment.Interactible(
289                environment.Action(self._post_door_event, condition)),
290        )
291
292    def _post_door_event(self, protagonist):
293        DoorEvent.post(self.destination, self.dest_pos)
294
295
296class Door(BaseDoor):
297    def __init__(self, space, position, destination, dest_pos, angle):
298        super(Door, self).__init__(
299            space, position, destination, dest_pos, angle,
300            render.ImageRenderer(resources.get_image('objects', 'door.png')),
301            environment.YesCondition(),
302        )
303
304    @classmethod
305    def requires(cls):
306        return [("name", "string"), ("position", "coordinates"),
307                ("destination", "level name"), ("dest_pos", "coordinate"),
308                ("angle", "degrees")]
309
310
311class PuzzleDoor(BaseDoor):
312    def __init__(self, space, position, destination, dest_pos, angle,
313                 key_state):
314        self._key_state = key_state
315        super(PuzzleDoor, self).__init__(
316            space, position, destination, dest_pos, angle,
317            render.ImageStateRenderer({
318                True: resources.get_image('objects', 'door.png'),
319                False: resources.get_image('objects', 'locked_door.png'),
320            }),
321            environment.FunctionCondition(lambda p: self.is_open),
322        )
323
324    @property
325    def is_open(self):
326        return self._stored_state['is_open']
327
328    def set_stored_state_dict(self, stored_state):
329        self._stored_state = stored_state
330        self._stored_state.setdefault('is_open', False)
331        return True
332
333    def update(self, dt):
334        if not self.is_open:
335            self._stored_state['is_open'] = self.puzzler.glue.get_state_of(
336                self._key_state)
337        super(PuzzleDoor, self).update(dt)
338
339    @classmethod
340    def requires(cls):
341        return [("name", "string"), ("position", "coordinates"),
342                ("destination", "level name"), ("dest_pos", "coordinate"),
343                ("angle", "degrees"),
344                ("key_state", "puzzler")]
345
346
347class KeyedDoor(BaseDoor):
348    def __init__(self, space, position, destination, dest_pos, angle,
349                 key_item=None):
350        self._key_item = key_item
351        super(KeyedDoor, self).__init__(
352            space, position, destination, dest_pos, angle,
353            render.ImageRenderer(resources.get_image('objects', 'door.png')),
354            environment.ItemRequiredCondition(key_item),
355        )
356
357    @classmethod
358    def requires(cls):
359        return [("name", "string"), ("position", "coordinates"),
360                ("destination", "level name"), ("dest_pos", "coordinate"),
361                ("angle", "degrees"), ("key_item", "item name")]
362
363
364class Bulkhead(GameObject):
365    zorder = ZORDER_FLOOR
366
367    def __init__(self, space, end1, end2, key_state=None):
368        body = make_body(None, None, (0, 0))
369        self.shape = pymunk.Segment(body, tuple(end1), tuple(end2), 3)
370        self.shape.collision_type = COLLISION_TYPE_DOOR
371        if key_state is None:
372            puzzler = puzzle.YesPuzzler()
373        else:
374            puzzler = puzzle.StateProxyPuzzler(key_state)
375        super(Bulkhead, self).__init__(
376            SingleShapePhysicser(space, self.shape),
377            render.ShapeStateRenderer(),
378            puzzler,
379        )
380
381    def collide_with_protagonist(self, protagonist):
382        if self.puzzler.get_state():
383            # Reject the collision, we can walk through.
384            return False
385        return True
386
387    collide_with_furniture = collide_with_protagonist
388
389    @classmethod
390    def requires(cls):
391        return [("name", "string"), ("end1", "coordinates"),
392                ("end2", "coordinates"), ("key_state", "puzzler")]
393
394
395class ToggleSwitch(GameObject):
396    zorder = ZORDER_LOW
397
398    def __init__(self, space, position):
399        body = make_body(None, None, position)
400        self.shape = pymunk.Circle(body, 20)
401        self.shape.sensor = True
402        super(ToggleSwitch, self).__init__(
403            SingleShapePhysicser(space, self.shape),
404            render.ImageStateRenderer({
405                True: resources.get_image('objects', 'lever.png'),
406                False: resources.get_image(
407                    'objects', 'lever.png', transforms=(FLIP_H,)),
408            }),
409            puzzle.ParentAttrPuzzler('toggle_on'),
410            interactible=environment.Interactible(
411                environment.Action(self._toggle)),
412        )
413
414    @property
415    def toggle_on(self):
416        return self._stored_state['toggle_on']
417
418    def _toggle(self, protagonist):
419        self._stored_state['toggle_on'] = not self.toggle_on
420
421    def set_stored_state_dict(self, stored_state):
422        self._stored_state = stored_state
423        # We start in the "off" position.
424        self._stored_state.setdefault('toggle_on', False)
425        return True
426
427    @classmethod
428    def requires(cls):
429        return [("name", "string"), ("position", "coordinates")]
430
431
432class Bullet(GameObject):
433    def __init__(self, space, position, impulse, damage, bullet_type,
434                 source_collision_type):
435        body = make_body(1, pymunk.inf, position)
436        body.angle = impulse.angle
437        self.last_position = position
438        self.shape = pymunk.Circle(body, 2)
439        self.shape.sensor = True
440        self.shape.collision_type = COLLISION_TYPE_PROJECTILE
441        self.damage = damage
442        self.type = bullet_type
443        self.source_collision_type = source_collision_type
444        super(Bullet, self).__init__(
445            SingleShapePhysicser(space, self.shape),
446            render.ImageRenderer(resources.get_image(
447                'objects', '%s.png' % self.type)),
448        )
449        self.physicser.apply_impulse(impulse)
450
451    def update(self, dt):
452        super(Bullet, self).update(dt)
453        position = (self.physicser.position.x, self.physicser.position.y)
454        r = self.get_space().segment_query(self.last_position, position)
455        self.last_position = position
456        for collision in r:
457            shape = collision.shape
458            if (shape.collision_type == self.source_collision_type
459                    or shape == self.physicser.get_shape()
460                    or shape.sensor):
461                continue
462            if hasattr(shape, 'physicser'):
463                shape.physicser.game_object.hit(self)
464            self.physicser.remove_from_space()
465            self.remove = True
466            break
467
468
469class CollectibleGameObject(GameObject):
470    zorder = ZORDER_LOW
471
472    def __init__(self, space, name, shape, renderer):
473        self._name = name
474        shape.sensor = True
475        super(CollectibleGameObject, self).__init__(
476            SingleShapePhysicser(space, shape),
477            renderer,
478            interactible=environment.Interactible(
479                environment.Action(
480                    self._collect, environment.HumanFormCondition())),
481        )
482
483    def _collect(self, protagonist):
484        protagonist.inventory[self._name] = self
485        # TODO: Make this less hacky.
486        self.physicser.remove_from_space()
487        self.renderer = render.NullRenderer()
488
489
490class Gun(CollectibleGameObject):
491    def __init__(self, space, position):
492        body = make_body(None, None, position)
493        self.shape = pymunk.Circle(body, 20)
494        super(Gun, self).__init__(
495            space, 'gun', self.shape,
496            render.ImageRenderer(resources.get_image('objects', 'gun.png')),
497        )
498
499
500class ClawAttack(GameObject):
501    def __init__(self, space, pos, vector, damage):
502        body = make_body(1, pymunk.inf,
503                         (pos[0] + (vector.length * math.cos(vector.angle)),
504                          pos[1] + (vector.length * math.sin(vector.angle))))
505        body.angle = vector.angle
506        self.shape = pymunk.Circle(body, 30)
507        self.shape.sensor = True
508        self.shape.collision_type = COLLISION_TYPE_WEREWOLF_ATTACK
509        self.damage = damage
510        super(ClawAttack, self).__init__(
511            SingleShapePhysicser(space, self.shape),
512            render.ImageRenderer(resources.get_image(
513                'objects', 'werewolf_SW_claw_attack.png',
514                transforms=(FLIP_H,))),
515        )
516
517    def update(self, dt):
518        super(ClawAttack, self).update(dt)
519        if self.lifetime > 0.1:
520            self.physicser.remove_from_space()
521            self.remove = True
522
523
524class HostileTerrain(GameObject):
525    zorder = ZORDER_FLOOR
526    damage = None
527    tile = None
528    tile_alpha = 255
529    # How often to hit the player
530    rate = 5
531
532    def __init__(self, space, position, outline):
533        body = make_body(10, pymunk.inf, position)
534        # Adjust shape relative to position
535        shape_outline = [(p[0] - position[0], p[1] - position[1]) for
536                         p in outline]
537        self.shape = pymunk.Poly(body, shape_outline)
538        self._ticks = 0
539        self.shape.collision_type = COLLISION_TYPE_SWITCH
540        self.shape.sensor = True
541        super(HostileTerrain, self).__init__(
542            SingleShapePhysicser(space, self.shape),
543            render.TiledRenderer(outline,
544                                 resources.get_image('tiles', self.tile),
545                                 self.tile_alpha))
546
547    def collide_with_protagonist(self, protagonist):
548        # We're called every frame we're colliding, so
549        # There are timing issues with stepping on and
550        # off terrian, but as long as the rate is reasonably
551        # low, they shouldn't impact gameplay
552        if self._ticks == 0:
553            self.apply_effect(protagonist)
554        self._ticks += 1
555        if self._ticks > self.rate:
556            self._ticks = 0
557
558    def apply_effect(self, protagonist):
559        protagonist.lose_health(self.damage)
560
561    @classmethod
562    def requires(cls):
563        return [("name", "string"), ("position", "coordinates"),
564                ("outline", "polygon (convex)")]
565
566
567class AcidFloor(HostileTerrain):
568    damage = 1
569    tile = 'acid.png'
570
571
572class ForceWolfFloor(HostileTerrain):
573    tile = 'moonlight.png'
574    rate = 0
575    tile_alpha = 150
576    zorder = ZORDER_HIGH
577
578    def apply_effect(self, protagonist):
579        protagonist.force_wolf_form()
Note: See TracBrowser for help on using the repository browser.