source: nagslang/game_object.py @ 385:51deb78cae52

Last change on this file since 385:51deb78cae52 was 385:51deb78cae52, checked in by Stefano Rivera <stefano@…>, 7 years ago

Use a result object to get new drawables back to the area

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