source: nagslang/game_object.py @ 401:f7ee43c0e5c9

Last change on this file since 401:f7ee43c0e5c9 was 401:f7ee43c0e5c9, checked in by Neil Muller <drnlmuller@…>, 7 years ago

Animated acid

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