source: nagslang/game_object.py@ 437:d087dfb10896

Last change on this file since 437:d087dfb10896 was 437:d087dfb10896, checked in by Stefano Rivera <stefano@…>, 8 years ago

Replace menu and death screen with the starting level

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