source: nagslang/game_object.py@ 417:01f48d8dc56a

Last change on this file since 417:01f48d8dc56a was 417:01f48d8dc56a, checked in by davidsharpe@…, 8 years ago

Well, gravity push.

File size: 21.2 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 def environmental_movement(self, dx, dy):
215 if (dx, dy) == (0, 0):
216 return
217 self.physicser.apply_impulse((dx, dy))
218
219 @classmethod
220 def requires(cls):
221 """Hints for the level editor"""
222 return [("name", "string")]
223
224
225class FloorSwitch(GameObject):
226 zorder = ZORDER_FLOOR
227
228 def __init__(self, space, position):
229 body = make_body(None, None, position)
230 self.shape = pymunk.Circle(body, 30)
231 self.shape.collision_type = COLLISION_TYPE_SWITCH
232 self.shape.sensor = True
233 super(FloorSwitch, self).__init__(
234 SingleShapePhysicser(space, self.shape),
235 render.ImageStateRenderer({
236 True: resources.get_image('objects', 'sensor_on.png'),
237 False: resources.get_image('objects', 'sensor_off.png'),
238 }),
239 puzzle.CollidePuzzler(*SWITCH_PUSHERS),
240 )
241
242 @classmethod
243 def requires(cls):
244 return [("name", "string"), ("position", "coordinates")]
245
246
247class Note(GameObject):
248 zorder = ZORDER_FLOOR
249
250 def __init__(self, space, position, message):
251 body = make_body(None, None, position)
252 self.shape = pymunk.Circle(body, 30)
253 self.shape.sensor = True
254 super(Note, self).__init__(
255 SingleShapePhysicser(space, self.shape),
256 render.ImageRenderer(resources.get_image('objects', 'note.png')),
257 puzzle.CollidePuzzler(),
258 render.TextOverlay(message),
259 )
260
261 @classmethod
262 def requires(cls):
263 return [("name", "string"), ("position", "coordinates"),
264 ("message", "text")]
265
266
267class FloorLight(GameObject):
268 zorder = ZORDER_FLOOR
269
270 def __init__(self, space, position, state_source):
271 body = make_body(None, None, position)
272 self.shape = pymunk.Circle(body, 10)
273 self.shape.collision_type = COLLISION_TYPE_SWITCH
274 self.shape.sensor = True
275 super(FloorLight, self).__init__(
276 SingleShapePhysicser(space, self.shape),
277 render.ImageStateRenderer({
278 True: resources.get_image('objects', 'light_on.png'),
279 False: resources.get_image('objects', 'light_off.png'),
280 }),
281 puzzle.StateProxyPuzzler(state_source),
282 )
283
284 @classmethod
285 def requires(cls):
286 return [("name", "string"), ("position", "coordinates"),
287 ("state_source", "puzzler")]
288
289
290class Box(GameObject):
291 def __init__(self, space, position):
292 body = make_body(10, 10000, position, damping=0.5)
293 self.shape = pymunk.Poly(
294 body, [(-20, -20), (20, -20), (20, 20), (-20, 20)])
295 self.shape.friction = 0.5
296 self.shape.collision_type = COLLISION_TYPE_FURNITURE
297 super(Box, self).__init__(
298 SingleShapePhysicser(space, self.shape),
299 render.ImageRenderer(resources.get_image('objects', 'crate.png')),
300 )
301
302 @classmethod
303 def requires(cls):
304 return [("name", "string"), ("position", "coordinates"),
305 ("state_source", "puzzler")]
306
307
308class BaseDoor(GameObject):
309 zorder = ZORDER_FLOOR
310 is_open = True
311
312 def __init__(self, space, position, destination, dest_pos, angle,
313 renderer, condition):
314 body = make_body(pymunk.inf, pymunk.inf, position, damping=0.5)
315 self.shape = pymunk.Circle(body, 30)
316 self.shape.collision_type = COLLISION_TYPE_DOOR
317 self.shape.body.angle = float(angle) / 180 * math.pi
318 self.shape.sensor = True
319 self.destination = destination
320 self.dest_pos = tuple(dest_pos)
321 super(BaseDoor, self).__init__(
322 SingleShapePhysicser(space, self.shape),
323 renderer,
324 puzzle.ParentAttrPuzzler('is_open'),
325 interactible=environment.Interactible(
326 environment.Action(self._post_door_event, condition)),
327 )
328
329 def _post_door_event(self, protagonist):
330 DoorEvent.post(self.destination, self.dest_pos)
331
332
333class Door(BaseDoor):
334 def __init__(self, space, position, destination, dest_pos, angle):
335 super(Door, self).__init__(
336 space, position, destination, dest_pos, angle,
337 render.ImageRenderer(resources.get_image('objects', 'door.png')),
338 environment.YesCondition(),
339 )
340
341 @classmethod
342 def requires(cls):
343 return [("name", "string"), ("position", "coordinates"),
344 ("destination", "level name"), ("dest_pos", "coordinate"),
345 ("angle", "degrees")]
346
347
348class PuzzleDoor(BaseDoor):
349 def __init__(self, space, position, destination, dest_pos, angle,
350 key_state):
351 self._key_state = key_state
352 overlay = ImageOverlay(resources.get_image('objects', 'lock.png'))
353 super(PuzzleDoor, self).__init__(
354 space, position, destination, dest_pos, angle,
355 render.ImageStateRenderer({
356 True: resources.get_image('objects', 'door.png'),
357 False: resources.get_image(
358 'objects', 'door.png', transforms=(overlay,)),
359 }),
360 environment.FunctionCondition(lambda p: self.is_open),
361 )
362
363 @property
364 def is_open(self):
365 return self._stored_state['is_open']
366
367 def set_stored_state_dict(self, stored_state):
368 self._stored_state = stored_state
369 self._stored_state.setdefault('is_open', False)
370 return True
371
372 def update(self, dt):
373 if not self.is_open:
374 self._stored_state['is_open'] = self.puzzler.glue.get_state_of(
375 self._key_state)
376 super(PuzzleDoor, self).update(dt)
377
378 @classmethod
379 def requires(cls):
380 return [("name", "string"), ("position", "coordinates"),
381 ("destination", "level name"), ("dest_pos", "coordinate"),
382 ("angle", "degrees"),
383 ("key_state", "puzzler")]
384
385
386class KeyedDoor(BaseDoor):
387 def __init__(self, space, position, destination, dest_pos, angle,
388 key_item=None):
389 self._key_item = key_item
390 overlay = ImageOverlay(
391 resources.get_image('objects', '%s.png' % (key_item,)))
392 super(KeyedDoor, self).__init__(
393 space, position, destination, dest_pos, angle,
394 render.ImageRenderer(resources.get_image(
395 'objects', 'door.png', transforms=(overlay,))),
396 environment.ItemRequiredCondition(key_item),
397 )
398
399 @classmethod
400 def requires(cls):
401 return [("name", "string"), ("position", "coordinates"),
402 ("destination", "level name"), ("dest_pos", "coordinate"),
403 ("angle", "degrees"), ("key_item", "item name")]
404
405
406class Bulkhead(GameObject):
407 zorder = ZORDER_FLOOR
408
409 def __init__(self, space, end1, end2, key_state=None):
410 body = make_body(None, None, (0, 0))
411 self.shape = pymunk.Segment(body, tuple(end1), tuple(end2), 3)
412 self.shape.collision_type = COLLISION_TYPE_DOOR
413 if key_state is None:
414 puzzler = puzzle.YesPuzzler()
415 else:
416 puzzler = puzzle.StateProxyPuzzler(key_state)
417 super(Bulkhead, self).__init__(
418 SingleShapePhysicser(space, self.shape),
419 render.ShapeStateRenderer(),
420 puzzler,
421 )
422
423 def collide_with_protagonist(self, protagonist):
424 if self.puzzler.get_state():
425 # Reject the collision, we can walk through.
426 return False
427 return True
428
429 collide_with_furniture = collide_with_protagonist
430
431 @classmethod
432 def requires(cls):
433 return [("name", "string"), ("end1", "coordinates"),
434 ("end2", "coordinates"), ("key_state", "puzzler")]
435
436
437class ToggleSwitch(GameObject):
438 zorder = ZORDER_LOW
439
440 def __init__(self, space, position):
441 body = make_body(None, None, position)
442 self.shape = pymunk.Circle(body, 20)
443 self.shape.sensor = True
444 super(ToggleSwitch, self).__init__(
445 SingleShapePhysicser(space, self.shape),
446 render.ImageStateRenderer({
447 True: resources.get_image('objects', 'lever.png'),
448 False: resources.get_image(
449 'objects', 'lever.png', transforms=(FLIP_H,)),
450 }),
451 puzzle.ParentAttrPuzzler('toggle_on'),
452 interactible=environment.Interactible(
453 environment.Action(self._toggle)),
454 )
455
456 @property
457 def toggle_on(self):
458 return self._stored_state['toggle_on']
459
460 def _toggle(self, protagonist):
461 self._stored_state['toggle_on'] = not self.toggle_on
462
463 def set_stored_state_dict(self, stored_state):
464 self._stored_state = stored_state
465 # We start in the "off" position.
466 self._stored_state.setdefault('toggle_on', False)
467 return True
468
469 @classmethod
470 def requires(cls):
471 return [("name", "string"), ("position", "coordinates")]
472
473
474class Bullet(GameObject):
475 def __init__(self, space, position, impulse, damage, bullet_type,
476 source_collision_type):
477 body = make_body(1, pymunk.inf, position)
478 body.angle = impulse.angle
479 self.last_position = position
480 self.shape = pymunk.Circle(body, 2)
481 self.shape.sensor = True
482 self.shape.collision_type = COLLISION_TYPE_PROJECTILE
483 self.damage = damage
484 self.type = bullet_type
485 self.source_collision_type = source_collision_type
486 super(Bullet, self).__init__(
487 SingleShapePhysicser(space, self.shape),
488 render.ImageRenderer(resources.get_image(
489 'objects', '%s.png' % self.type)),
490 )
491 self.physicser.apply_impulse(impulse)
492
493 def update(self, dt):
494 super(Bullet, self).update(dt)
495 position = (self.physicser.position.x, self.physicser.position.y)
496 r = self.get_space().segment_query(self.last_position, position)
497 self.last_position = position
498 for collision in r:
499 shape = collision.shape
500 if (shape.collision_type == self.source_collision_type
501 or shape == self.physicser.get_shape()
502 or shape.sensor):
503 continue
504 if hasattr(shape, 'physicser'):
505 shape.physicser.game_object.hit(self)
506 self.physicser.remove_from_space()
507 return Result(remove=[self])
508
509
510class ClawAttack(GameObject):
511 def __init__(self, space, pos, vector, damage):
512 body = make_body(1, pymunk.inf,
513 (pos[0] + (vector.length * math.cos(vector.angle)),
514 pos[1] + (vector.length * math.sin(vector.angle))))
515 body.angle = vector.angle
516 self.shape = pymunk.Circle(body, 30)
517 self.shape.sensor = True
518 self.shape.collision_type = COLLISION_TYPE_WEREWOLF_ATTACK
519 self.damage = damage
520 super(ClawAttack, self).__init__(
521 SingleShapePhysicser(space, self.shape),
522 render.ImageRenderer(resources.get_image(
523 'objects', 'werewolf_SW_claw_attack.png',
524 transforms=(FLIP_H,))),
525 )
526
527 def update(self, dt):
528 super(ClawAttack, self).update(dt)
529 if self.lifetime > 0.1:
530 self.physicser.remove_from_space()
531 return Result(remove=[self])
532
533
534class HostileTerrain(GameObject):
535 zorder = ZORDER_FLOOR
536 damage = None
537 tiles = []
538 tile_alpha = 255
539 tile_frame_ticks = 3
540 # How often to hit the player
541 rate = 5
542
543 def __init__(self, space, position, outline):
544 body = make_body(10, pymunk.inf, position)
545 # Adjust shape relative to position
546 shape_outline = [(p[0] - position[0], p[1] - position[1]) for
547 p in outline]
548 self.shape = pymunk.Poly(body, shape_outline)
549 self._ticks = 0
550 self.shape.collision_type = COLLISION_TYPE_SWITCH
551 self.shape.sensor = True
552 if len(self.tiles) > 1:
553 tile_images = [resources.get_image('tiles', x)
554 for x in self.tiles]
555 renderer = render.TimedTiledRenderer(outline, tile_images,
556 self.tile_frame_ticks,
557 self.tile_alpha)
558 else:
559 tile_image = resources.get_image('tiles', self.tiles[0])
560 renderer = render.TiledRenderer(outline, tile_image,
561 self.tile_alpha)
562 super(HostileTerrain, self).__init__(
563 SingleShapePhysicser(space, self.shape),
564 renderer)
565
566 def collide_with_protagonist(self, protagonist):
567 # We're called every frame we're colliding, so
568 # There are timing issues with stepping on and
569 # off terrian, but as long as the rate is reasonably
570 # low, they shouldn't impact gameplay
571 if self._ticks == 0:
572 self.apply_effect(protagonist)
573 self._ticks += 1
574 if self._ticks > self.rate:
575 self._ticks = 0
576
577 def apply_effect(self, protagonist):
578 protagonist.lose_health(self.damage)
579
580 @classmethod
581 def requires(cls):
582 return [("name", "string"), ("position", "coordinates"),
583 ("outline", "polygon (convex)")]
584
585
586class AcidFloor(HostileTerrain):
587 damage = 1
588 tiles = ['acid.png', 'acid2.png', 'acid3.png']
589 tile_alpha = 200
590 tile_frame_ticks = 10
591
592
593class ForceWolfFloor(HostileTerrain):
594 tiles = ['moonlight.png']
595 rate = 0
596 tile_alpha = 150
597 zorder = ZORDER_HIGH
598
599 def apply_effect(self, protagonist):
600 protagonist.force_wolf_form()
601
602class GravityWell(GameObject):
603 zorder = ZORDER_FLOOR
604 # How often to hit the player
605 rate = 5
606
607 def __init__(self, space, position, size, force):
608 body = make_body(10, pymunk.inf, position)
609 # Adjust shape relative to position
610 self.shape = pymunk.Circle(body, size)
611 self._ticks = 0
612 self.force = force
613 self.shape.collision_type = COLLISION_TYPE_SWITCH
614 self.shape.sensor = True
615 super(GravityWell, self).__init__(
616 SingleShapePhysicser(space, self.shape),
617 render.ImageRenderer(resources.get_image(
618 'objects', 'gravity_well.png')),
619 )
620
621 def collide_with_protagonist(self, protagonist):
622 # We're called every frame we're colliding, so
623 # There are timing issues with stepping on and
624 # off terrian, but as long as the rate is reasonably
625 # low, they shouldn't impact gameplay
626 if self._ticks == 0:
627 self.apply_effect(protagonist)
628 self._ticks += 1
629 if self._ticks > self.rate:
630 self._ticks = 0
631
632 def collide_with_furniture(self, furniture):
633 # We're called every frame we're colliding, so
634 # There are timing issues with stepping on and
635 # off terrian, but as long as the rate is reasonably
636 # low, they shouldn't impact gameplay
637 if self._ticks == 0:
638 self.apply_effect(furniture)
639 self._ticks += 1
640 if self._ticks > self.rate:
641 self._ticks = 0
642
643 def apply_effect(self, object_to_move):
644 object_to_move.environmental_movement(self.force, self.force)
645
646 @classmethod
647 def requires(cls):
648 return [("name", "string"), ("position", "coordinates"),
649 ("outline", "polygon (convex)")]
Note: See TracBrowser for help on using the repository browser.