source: nagslang/game_object.py @ 371:21c1c329f8e3

Last change on this file since 371:21c1c329f8e3 was 371:21c1c329f8e3, checked in by Jeremy Thurgood <firxen@…>, 7 years ago

Automatic weapons.

File size: 19.9 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
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        self._timers = {}
130        self._active_timers = {}
131
132    def add_timer(self, name, secs):
133        self._timers[name] = secs
134
135    def start_timer(self, name, secs=None):
136        if secs is None:
137            secs = self._timers[name]
138        self._active_timers[name] = secs
139
140    def check_timer(self, name):
141        return name in self._active_timers
142
143    def set_stored_state_dict(self, stored_state):
144        """Override this to set up whatever state storage you want.
145
146        The `stored_state` dict passed in contains whatever saved state we
147        might have for this object. If the return value of this method
148        evaluates to `True`, the contents of the `stored_state` dict will be
149        saved, otherwise it will be discarded.
150        """
151        pass
152
153    def get_space(self):
154        return self.physicser.get_space()
155
156    def get_shape(self):
157        return self.physicser.get_shape()
158
159    def get_render_position(self, surface):
160        return self.physicser.get_render_position(surface)
161
162    def get_render_angle(self):
163        return self.physicser.get_angle()
164
165    def get_facing_direction(self):
166        """Used by rendererd that care what direction an object is facing.
167        """
168        return None
169
170    def render(self, surface):
171        return self.renderer.render(surface)
172
173    def update(self, dt):
174        self.lifetime += dt
175        for timer in self._active_timers.keys():
176            self._active_timers[timer] -= dt
177            if self._active_timers[timer] <= 0:
178                self._active_timers.pop(timer)
179        self.renderer.update(dt)
180
181    def hit(self, weapon):
182        '''Was hit with a weapon (such as a bullet)'''
183        pass
184
185    def collide_with_protagonist(self, protagonist):
186        """Called as a `pre_solve` collision callback with the protagonist.
187
188        You can return `False` to ignore the collision, anything else
189        (including `None`) to process the collision as normal.
190        """
191        return True
192
193    def collide_with_furniture(self, furniture):
194        return True
195
196    def collide_with_claw_attack(self, claw_attack):
197        return True
198
199    @classmethod
200    def requires(cls):
201        """Hints for the level editor"""
202        return [("name", "string")]
203
204
205class FloorSwitch(GameObject):
206    zorder = ZORDER_FLOOR
207
208    def __init__(self, space, position):
209        body = make_body(None, None, position)
210        self.shape = pymunk.Circle(body, 30)
211        self.shape.collision_type = COLLISION_TYPE_SWITCH
212        self.shape.sensor = True
213        super(FloorSwitch, self).__init__(
214            SingleShapePhysicser(space, self.shape),
215            render.ImageStateRenderer({
216                True: resources.get_image('objects', 'sensor_on.png'),
217                False: resources.get_image('objects', 'sensor_off.png'),
218            }),
219            puzzle.CollidePuzzler(*SWITCH_PUSHERS),
220        )
221
222    @classmethod
223    def requires(cls):
224        return [("name", "string"), ("position", "coordinates")]
225
226
227class Note(GameObject):
228    zorder = ZORDER_FLOOR
229
230    def __init__(self, space, position, message):
231        body = make_body(None, None, position)
232        self.shape = pymunk.Circle(body, 30)
233        self.shape.sensor = True
234        super(Note, self).__init__(
235            SingleShapePhysicser(space, self.shape),
236            render.ImageRenderer(resources.get_image('objects', 'note.png')),
237            puzzle.CollidePuzzler(),
238            render.TextOverlay(message),
239        )
240
241    @classmethod
242    def requires(cls):
243        return [("name", "string"), ("position", "coordinates"),
244                ("message", "text")]
245
246
247class FloorLight(GameObject):
248    zorder = ZORDER_FLOOR
249
250    def __init__(self, space, position, state_source):
251        body = make_body(None, None, position)
252        self.shape = pymunk.Circle(body, 10)
253        self.shape.collision_type = COLLISION_TYPE_SWITCH
254        self.shape.sensor = True
255        super(FloorLight, self).__init__(
256            SingleShapePhysicser(space, self.shape),
257            render.ImageStateRenderer({
258                True: resources.get_image('objects', 'light_on.png'),
259                False: resources.get_image('objects', 'light_off.png'),
260            }),
261            puzzle.StateProxyPuzzler(state_source),
262        )
263
264    @classmethod
265    def requires(cls):
266        return [("name", "string"), ("position", "coordinates"),
267                ("state_source", "puzzler")]
268
269
270class Box(GameObject):
271    def __init__(self, space, position):
272        body = make_body(10, 10000, position, damping=0.5)
273        self.shape = pymunk.Poly(
274            body, [(-20, -20), (20, -20), (20, 20), (-20, 20)])
275        self.shape.friction = 0.5
276        self.shape.collision_type = COLLISION_TYPE_FURNITURE
277        super(Box, self).__init__(
278            SingleShapePhysicser(space, self.shape),
279            render.ImageRenderer(resources.get_image('objects', 'crate.png')),
280        )
281
282    @classmethod
283    def requires(cls):
284        return [("name", "string"), ("position", "coordinates"),
285                ("state_source", "puzzler")]
286
287
288class BaseDoor(GameObject):
289    zorder = ZORDER_FLOOR
290    is_open = True
291
292    def __init__(self, space, position, destination, dest_pos, angle,
293                 renderer, condition):
294        body = make_body(pymunk.inf, pymunk.inf, position, damping=0.5)
295        self.shape = pymunk.Circle(body, 30)
296        self.shape.collision_type = COLLISION_TYPE_DOOR
297        self.shape.body.angle = float(angle) / 180 * math.pi
298        self.shape.sensor = True
299        self.destination = destination
300        self.dest_pos = tuple(dest_pos)
301        super(BaseDoor, self).__init__(
302            SingleShapePhysicser(space, self.shape),
303            renderer,
304            puzzle.ParentAttrPuzzler('is_open'),
305            interactible=environment.Interactible(
306                environment.Action(self._post_door_event, condition)),
307        )
308
309    def _post_door_event(self, protagonist):
310        DoorEvent.post(self.destination, self.dest_pos)
311
312
313class Door(BaseDoor):
314    def __init__(self, space, position, destination, dest_pos, angle):
315        super(Door, self).__init__(
316            space, position, destination, dest_pos, angle,
317            render.ImageRenderer(resources.get_image('objects', 'door.png')),
318            environment.YesCondition(),
319        )
320
321    @classmethod
322    def requires(cls):
323        return [("name", "string"), ("position", "coordinates"),
324                ("destination", "level name"), ("dest_pos", "coordinate"),
325                ("angle", "degrees")]
326
327
328class PuzzleDoor(BaseDoor):
329    def __init__(self, space, position, destination, dest_pos, angle,
330                 key_state):
331        self._key_state = key_state
332        super(PuzzleDoor, self).__init__(
333            space, position, destination, dest_pos, angle,
334            render.ImageStateRenderer({
335                True: resources.get_image('objects', 'door.png'),
336                False: resources.get_image('objects', 'locked_door.png'),
337            }),
338            environment.FunctionCondition(lambda p: self.is_open),
339        )
340
341    @property
342    def is_open(self):
343        return self._stored_state['is_open']
344
345    def set_stored_state_dict(self, stored_state):
346        self._stored_state = stored_state
347        self._stored_state.setdefault('is_open', False)
348        return True
349
350    def update(self, dt):
351        if not self.is_open:
352            self._stored_state['is_open'] = self.puzzler.glue.get_state_of(
353                self._key_state)
354        super(PuzzleDoor, self).update(dt)
355
356    @classmethod
357    def requires(cls):
358        return [("name", "string"), ("position", "coordinates"),
359                ("destination", "level name"), ("dest_pos", "coordinate"),
360                ("angle", "degrees"),
361                ("key_state", "puzzler")]
362
363
364class KeyedDoor(BaseDoor):
365    def __init__(self, space, position, destination, dest_pos, angle,
366                 key_item=None):
367        self._key_item = key_item
368        overlay = ImageOverlay(
369            resources.get_image('objects', '%s.png' % (key_item,)))
370        super(KeyedDoor, self).__init__(
371            space, position, destination, dest_pos, angle,
372            render.ImageRenderer(resources.get_image(
373                'objects', 'door.png', transforms=(overlay,))),
374            environment.ItemRequiredCondition(key_item),
375        )
376
377    @classmethod
378    def requires(cls):
379        return [("name", "string"), ("position", "coordinates"),
380                ("destination", "level name"), ("dest_pos", "coordinate"),
381                ("angle", "degrees"), ("key_item", "item name")]
382
383
384class Bulkhead(GameObject):
385    zorder = ZORDER_FLOOR
386
387    def __init__(self, space, end1, end2, key_state=None):
388        body = make_body(None, None, (0, 0))
389        self.shape = pymunk.Segment(body, tuple(end1), tuple(end2), 3)
390        self.shape.collision_type = COLLISION_TYPE_DOOR
391        if key_state is None:
392            puzzler = puzzle.YesPuzzler()
393        else:
394            puzzler = puzzle.StateProxyPuzzler(key_state)
395        super(Bulkhead, self).__init__(
396            SingleShapePhysicser(space, self.shape),
397            render.ShapeStateRenderer(),
398            puzzler,
399        )
400
401    def collide_with_protagonist(self, protagonist):
402        if self.puzzler.get_state():
403            # Reject the collision, we can walk through.
404            return False
405        return True
406
407    collide_with_furniture = collide_with_protagonist
408
409    @classmethod
410    def requires(cls):
411        return [("name", "string"), ("end1", "coordinates"),
412                ("end2", "coordinates"), ("key_state", "puzzler")]
413
414
415class ToggleSwitch(GameObject):
416    zorder = ZORDER_LOW
417
418    def __init__(self, space, position):
419        body = make_body(None, None, position)
420        self.shape = pymunk.Circle(body, 20)
421        self.shape.sensor = True
422        super(ToggleSwitch, self).__init__(
423            SingleShapePhysicser(space, self.shape),
424            render.ImageStateRenderer({
425                True: resources.get_image('objects', 'lever.png'),
426                False: resources.get_image(
427                    'objects', 'lever.png', transforms=(FLIP_H,)),
428            }),
429            puzzle.ParentAttrPuzzler('toggle_on'),
430            interactible=environment.Interactible(
431                environment.Action(self._toggle)),
432        )
433
434    @property
435    def toggle_on(self):
436        return self._stored_state['toggle_on']
437
438    def _toggle(self, protagonist):
439        self._stored_state['toggle_on'] = not self.toggle_on
440
441    def set_stored_state_dict(self, stored_state):
442        self._stored_state = stored_state
443        # We start in the "off" position.
444        self._stored_state.setdefault('toggle_on', False)
445        return True
446
447    @classmethod
448    def requires(cls):
449        return [("name", "string"), ("position", "coordinates")]
450
451
452class Bullet(GameObject):
453    def __init__(self, space, position, impulse, damage, bullet_type,
454                 source_collision_type):
455        body = make_body(1, pymunk.inf, position)
456        body.angle = impulse.angle
457        self.last_position = position
458        self.shape = pymunk.Circle(body, 2)
459        self.shape.sensor = True
460        self.shape.collision_type = COLLISION_TYPE_PROJECTILE
461        self.damage = damage
462        self.type = bullet_type
463        self.source_collision_type = source_collision_type
464        super(Bullet, self).__init__(
465            SingleShapePhysicser(space, self.shape),
466            render.ImageRenderer(resources.get_image(
467                'objects', '%s.png' % self.type)),
468        )
469        self.physicser.apply_impulse(impulse)
470
471    def update(self, dt):
472        super(Bullet, self).update(dt)
473        position = (self.physicser.position.x, self.physicser.position.y)
474        r = self.get_space().segment_query(self.last_position, position)
475        self.last_position = position
476        for collision in r:
477            shape = collision.shape
478            if (shape.collision_type == self.source_collision_type
479                    or shape == self.physicser.get_shape()
480                    or shape.sensor):
481                continue
482            if hasattr(shape, 'physicser'):
483                shape.physicser.game_object.hit(self)
484            self.physicser.remove_from_space()
485            self.remove = True
486            break
487
488
489class CollectibleGameObject(GameObject):
490    zorder = ZORDER_LOW
491
492    def __init__(self, space, name, shape, renderer):
493        self._name = name
494        shape.sensor = True
495        super(CollectibleGameObject, self).__init__(
496            SingleShapePhysicser(space, shape),
497            renderer,
498            interactible=environment.Interactible(
499                environment.Action(
500                    self._collect, environment.HumanFormCondition())),
501        )
502
503    def _collect(self, protagonist):
504        protagonist.inventory.add(self._name)
505        # TODO: Make this less hacky.
506        self.physicser.remove_from_space()
507        self.renderer = render.NullRenderer()
508
509
510class Gun(CollectibleGameObject):
511    def __init__(self, space, position):
512        body = make_body(None, None, position)
513        self.shape = pymunk.Circle(body, 20)
514        super(Gun, self).__init__(
515            space, 'gun', self.shape,
516            render.ImageRenderer(resources.get_image('objects', 'gun.png')),
517        )
518
519
520class ClawAttack(GameObject):
521    def __init__(self, space, pos, vector, damage):
522        body = make_body(1, pymunk.inf,
523                         (pos[0] + (vector.length * math.cos(vector.angle)),
524                          pos[1] + (vector.length * math.sin(vector.angle))))
525        body.angle = vector.angle
526        self.shape = pymunk.Circle(body, 30)
527        self.shape.sensor = True
528        self.shape.collision_type = COLLISION_TYPE_WEREWOLF_ATTACK
529        self.damage = damage
530        super(ClawAttack, self).__init__(
531            SingleShapePhysicser(space, self.shape),
532            render.ImageRenderer(resources.get_image(
533                'objects', 'werewolf_SW_claw_attack.png',
534                transforms=(FLIP_H,))),
535        )
536
537    def update(self, dt):
538        super(ClawAttack, self).update(dt)
539        if self.lifetime > 0.1:
540            self.physicser.remove_from_space()
541            self.remove = True
542
543
544class HostileTerrain(GameObject):
545    zorder = ZORDER_FLOOR
546    damage = None
547    tile = None
548    tile_alpha = 255
549    # How often to hit the player
550    rate = 5
551
552    def __init__(self, space, position, outline):
553        body = make_body(10, pymunk.inf, position)
554        # Adjust shape relative to position
555        shape_outline = [(p[0] - position[0], p[1] - position[1]) for
556                         p in outline]
557        self.shape = pymunk.Poly(body, shape_outline)
558        self._ticks = 0
559        self.shape.collision_type = COLLISION_TYPE_SWITCH
560        self.shape.sensor = True
561        super(HostileTerrain, self).__init__(
562            SingleShapePhysicser(space, self.shape),
563            render.TiledRenderer(outline,
564                                 resources.get_image('tiles', self.tile),
565                                 self.tile_alpha))
566
567    def collide_with_protagonist(self, protagonist):
568        # We're called every frame we're colliding, so
569        # There are timing issues with stepping on and
570        # off terrian, but as long as the rate is reasonably
571        # low, they shouldn't impact gameplay
572        if self._ticks == 0:
573            self.apply_effect(protagonist)
574        self._ticks += 1
575        if self._ticks > self.rate:
576            self._ticks = 0
577
578    def apply_effect(self, protagonist):
579        protagonist.lose_health(self.damage)
580
581    @classmethod
582    def requires(cls):
583        return [("name", "string"), ("position", "coordinates"),
584                ("outline", "polygon (convex)")]
585
586
587class AcidFloor(HostileTerrain):
588    damage = 1
589    tile = 'acid.png'
590
591
592class ForceWolfFloor(HostileTerrain):
593    tile = 'moonlight.png'
594    rate = 0
595    tile_alpha = 150
596    zorder = ZORDER_HIGH
597
598    def apply_effect(self, protagonist):
599        protagonist.force_wolf_form()
600
601
602class KeyCard(CollectibleGameObject):
603    def __init__(self, space, position, name):
604        body = make_body(None, None, position)
605        self.shape = pymunk.Circle(body, 20)
606        super(KeyCard, self).__init__(
607            space, name, self.shape,
608            render.ImageRenderer(
609                resources.get_image('objects', '%s.png' % (name,))),
610        )
611
612    @classmethod
613    def requires(cls):
614        return [("name", "string"), ("position", "coordinates"),
615                ("item_name", "string")]
Note: See TracBrowser for help on using the repository browser.