source: nagslang/game_object.py@ 374:150332d6c1fb

Last change on this file since 374:150332d6c1fb was 374:150332d6c1fb, checked in by Stefano Rivera <stefano@…>, 9 years ago

Move the inventory to world, to slightly reduce overall hackyness

File size: 19.9 KB
RevLine 
[81]1import pymunk
[93]2import pymunk.pygame_util
[81]3
[263]4import math
5
[281]6from nagslang import environment
[201]7from nagslang import puzzle
[207]8from nagslang import render
[364]9from nagslang.mutators import FLIP_H, ImageOverlay
[107]10from nagslang.constants import (
[318]11 COLLISION_TYPE_DOOR, COLLISION_TYPE_FURNITURE, COLLISION_TYPE_PROJECTILE,
12 COLLISION_TYPE_SWITCH, COLLISION_TYPE_WEREWOLF_ATTACK,
[362]13 SWITCH_PUSHERS, ZORDER_FLOOR, ZORDER_LOW, ZORDER_HIGH)
[155]14from nagslang.resources import resources
[180]15from nagslang.events import DoorEvent
[81]16
[82]17
[235]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
[59]26class Physicser(object):
[93]27 def __init__(self, space):
[123]28 self._space = space
29
30 def get_space(self):
31 return self._space
32
[276]33 def set_space(self, new_space):
34 self._space = new_space
35
[123]36 def set_game_object(self, game_object):
37 self.game_object = game_object
38
39 def get_shape(self):
40 raise NotImplementedError()
[93]41
42 def add_to_space(self):
[215]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)
[59]47
[93]48 def remove_from_space(self):
[215]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)
[59]53
[93]54 def get_render_position(self, surface):
[215]55 pos = self.get_shape().body.position
56 return pymunk.pygame_util.to_pygame(pos, surface)
[63]57
[93]58 def get_angle(self):
[215]59 return self.get_shape().body.angle
60
[217]61 def get_velocity(self):
62 return self.get_shape().body.velocity
63
[216]64 def _get_position(self):
[215]65 return self.get_shape().body.position
66
[216]67 def _set_position(self, position):
[215]68 self.get_shape().body.position = position
[93]69
[216]70 position = property(_get_position, _set_position)
71
[93]72 def apply_impulse(self, j, r=(0, 0)):
[215]73 return self.get_shape().body.apply_impulse(j, r)
[59]74
75
76class SingleShapePhysicser(Physicser):
[93]77 def __init__(self, space, shape):
78 super(SingleShapePhysicser, self).__init__(space)
[59]79 self._shape = shape
[186]80 shape.physicser = self
[59]81
[123]82 def get_shape(self):
83 return self._shape
84
[59]85
[133]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)
[145]95 body.position = tuple(position)
[133]96 if damping is not None:
97 body.damping = damping
98 body.velocity_func = damping_velocity_func
99 return body
100
101
[59]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
[162]108 zorder = ZORDER_LOW
[218]109 is_moving = False # `True` if a movement animation should play.
[162]110
[281]111 def __init__(self, physicser, renderer, puzzler=None, overlay=None,
112 interactible=None):
[333]113 self.lifetime = 0
[93]114 self.physicser = physicser
[123]115 physicser.set_game_object(self)
[93]116 self.physicser.add_to_space()
[59]117 self.renderer = renderer
[123]118 renderer.set_game_object(self)
[81]119 self.puzzler = puzzler
[123]120 if puzzler is not None:
121 puzzler.set_game_object(self)
[191]122 self.overlay = overlay
123 if overlay is not None:
124 self.overlay.set_game_object(self)
[281]125 self.interactible = interactible
126 if interactible is not None:
127 self.interactible.set_game_object(self)
[293]128 self.remove = False # If true, will be removed from drawables
[371]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
[59]142
[346]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
[123]153 def get_space(self):
154 return self.physicser.get_space()
155
156 def get_shape(self):
157 return self.physicser.get_shape()
158
[93]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()
[59]164
[229]165 def get_facing_direction(self):
166 """Used by rendererd that care what direction an object is facing.
167 """
168 return None
169
[59]170 def render(self, surface):
[123]171 return self.renderer.render(surface)
[81]172
[333]173 def update(self, dt):
174 self.lifetime += dt
[371]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)
[333]179 self.renderer.update(dt)
[143]180
[302]181 def hit(self, weapon):
182 '''Was hit with a weapon (such as a bullet)'''
183 pass
184
[256]185 def collide_with_protagonist(self, protagonist):
[186]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 """
[192]191 return True
[186]192
[319]193 def collide_with_furniture(self, furniture):
194 return True
195
[333]196 def collide_with_claw_attack(self, claw_attack):
197 return True
198
[235]199 @classmethod
200 def requires(cls):
201 """Hints for the level editor"""
202 return [("name", "string")]
203
[81]204
205class FloorSwitch(GameObject):
[162]206 zorder = ZORDER_FLOOR
207
[93]208 def __init__(self, space, position):
[145]209 body = make_body(None, None, position)
[81]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__(
[93]214 SingleShapePhysicser(space, self.shape),
[207]215 render.ImageStateRenderer({
[162]216 True: resources.get_image('objects', 'sensor_on.png'),
217 False: resources.get_image('objects', 'sensor_off.png'),
218 }),
[201]219 puzzle.CollidePuzzler(*SWITCH_PUSHERS),
[81]220 )
221
[235]222 @classmethod
223 def requires(cls):
224 return [("name", "string"), ("position", "coordinates")]
225
[106]226
[191]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),
[207]236 render.ImageRenderer(resources.get_image('objects', 'note.png')),
[201]237 puzzle.CollidePuzzler(),
[222]238 render.TextOverlay(message),
[191]239 )
240
[235]241 @classmethod
242 def requires(cls):
243 return [("name", "string"), ("position", "coordinates"),
244 ("message", "text")]
245
[191]246
[106]247class FloorLight(GameObject):
[162]248 zorder = ZORDER_FLOOR
249
[106]250 def __init__(self, space, position, state_source):
[145]251 body = make_body(None, None, position)
[106]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),
[207]257 render.ImageStateRenderer({
[162]258 True: resources.get_image('objects', 'light_on.png'),
259 False: resources.get_image('objects', 'light_off.png'),
260 }),
[201]261 puzzle.StateProxyPuzzler(state_source),
[106]262 )
[133]263
[235]264 @classmethod
265 def requires(cls):
266 return [("name", "string"), ("position", "coordinates"),
267 ("state_source", "puzzler")]
268
[133]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)])
[208]275 self.shape.friction = 0.5
[318]276 self.shape.collision_type = COLLISION_TYPE_FURNITURE
[133]277 super(Box, self).__init__(
278 SingleShapePhysicser(space, self.shape),
[207]279 render.ImageRenderer(resources.get_image('objects', 'crate.png')),
[133]280 )
[176]281
[235]282 @classmethod
283 def requires(cls):
284 return [("name", "string"), ("position", "coordinates"),
285 ("state_source", "puzzler")]
286
[176]287
[359]288class BaseDoor(GameObject):
[176]289 zorder = ZORDER_FLOOR
[359]290 is_open = True
[176]291
[263]292 def __init__(self, space, position, destination, dest_pos, angle,
[359]293 renderer, condition):
[176]294 body = make_body(pymunk.inf, pymunk.inf, position, damping=0.5)
[281]295 self.shape = pymunk.Circle(body, 30)
[176]296 self.shape.collision_type = COLLISION_TYPE_DOOR
[264]297 self.shape.body.angle = float(angle) / 180 * math.pi
[176]298 self.shape.sensor = True
299 self.destination = destination
300 self.dest_pos = tuple(dest_pos)
[359]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):
[176]315 super(Door, self).__init__(
[359]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,
[346]334 render.ImageStateRenderer({
335 True: resources.get_image('objects', 'door.png'),
[359]336 False: resources.get_image('objects', 'locked_door.png'),
[346]337 }),
[359]338 environment.FunctionCondition(lambda p: self.is_open),
[176]339 )
340
[346]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
[359]347 self._stored_state.setdefault('is_open', False)
348 return True
[346]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)
[359]354 super(PuzzleDoor, self).update(dt)
[346]355
[235]356 @classmethod
357 def requires(cls):
358 return [("name", "string"), ("position", "coordinates"),
359 ("destination", "level name"), ("dest_pos", "coordinate"),
[263]360 ("angle", "degrees"),
[359]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
[364]368 overlay = ImageOverlay(
369 resources.get_image('objects', '%s.png' % (key_item,)))
[359]370 super(KeyedDoor, self).__init__(
371 space, position, destination, dest_pos, angle,
[364]372 render.ImageRenderer(resources.get_image(
373 'objects', 'door.png', transforms=(overlay,))),
[359]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")]
[235]382
[224]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
[256]401 def collide_with_protagonist(self, protagonist):
[224]402 if self.puzzler.get_state():
403 # Reject the collision, we can walk through.
404 return False
405 return True
[235]406
[319]407 collide_with_furniture = collide_with_protagonist
408
[235]409 @classmethod
410 def requires(cls):
411 return [("name", "string"), ("end1", "coordinates"),
412 ("end2", "coordinates"), ("key_state", "puzzler")]
[261]413
414
[282]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),
[296]424 render.ImageStateRenderer({
425 True: resources.get_image('objects', 'lever.png'),
426 False: resources.get_image(
427 'objects', 'lever.png', transforms=(FLIP_H,)),
428 }),
[282]429 puzzle.ParentAttrPuzzler('toggle_on'),
430 interactible=environment.Interactible(
431 environment.Action(self._toggle)),
432 )
433
[346]434 @property
435 def toggle_on(self):
436 return self._stored_state['toggle_on']
437
[282]438 def _toggle(self, protagonist):
[346]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
[282]446
447 @classmethod
448 def requires(cls):
449 return [("name", "string"), ("position", "coordinates")]
450
451
[261]452class Bullet(GameObject):
[363]453 def __init__(self, space, position, impulse, damage, bullet_type,
[305]454 source_collision_type):
[261]455 body = make_body(1, pymunk.inf, position)
[363]456 body.angle = impulse.angle
[293]457 self.last_position = position
[286]458 self.shape = pymunk.Circle(body, 2)
[293]459 self.shape.sensor = True
[261]460 self.shape.collision_type = COLLISION_TYPE_PROJECTILE
[305]461 self.damage = damage
[363]462 self.type = bullet_type
[293]463 self.source_collision_type = source_collision_type
[261]464 super(Bullet, self).__init__(
465 SingleShapePhysicser(space, self.shape),
[363]466 render.ImageRenderer(resources.get_image(
467 'objects', '%s.png' % self.type)),
[261]468 )
469 self.physicser.apply_impulse(impulse)
[286]470
[333]471 def update(self, dt):
472 super(Bullet, self).update(dt)
[297]473 position = (self.physicser.position.x, self.physicser.position.y)
[293]474 r = self.get_space().segment_query(self.last_position, position)
475 self.last_position = position
476 for collision in r:
[302]477 shape = collision.shape
478 if (shape.collision_type == self.source_collision_type
479 or shape == self.physicser.get_shape()
480 or shape.sensor):
[293]481 continue
[302]482 if hasattr(shape, 'physicser'):
483 shape.physicser.game_object.hit(self)
[293]484 self.physicser.remove_from_space()
485 self.remove = True
486 break
487
[286]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):
[374]504 protagonist.add_item(self._name)
[286]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,
[332]516 render.ImageRenderer(resources.get_image('objects', 'gun.png')),
[286]517 )
[312]518
519
520class ClawAttack(GameObject):
[356]521 def __init__(self, space, pos, vector, damage):
522 body = make_body(1, pymunk.inf,
523 (pos[0] + (vector.length * math.cos(vector.angle)),
[362]524 pos[1] + (vector.length * math.sin(vector.angle))))
[333]525 body.angle = vector.angle
[312]526 self.shape = pymunk.Circle(body, 30)
527 self.shape.sensor = True
528 self.shape.collision_type = COLLISION_TYPE_WEREWOLF_ATTACK
[333]529 self.damage = damage
[312]530 super(ClawAttack, self).__init__(
531 SingleShapePhysicser(space, self.shape),
[333]532 render.ImageRenderer(resources.get_image(
533 'objects', 'werewolf_SW_claw_attack.png',
534 transforms=(FLIP_H,))),
[312]535 )
536
[333]537 def update(self, dt):
538 super(ClawAttack, self).update(dt)
[335]539 if self.lifetime > 0.1:
[312]540 self.physicser.remove_from_space()
541 self.remove = True
[351]542
543
544class HostileTerrain(GameObject):
545 zorder = ZORDER_FLOOR
546 damage = None
547 tile = None
[362]548 tile_alpha = 255
[351]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,
[362]564 resources.get_image('tiles', self.tile),
565 self.tile_alpha))
[351]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:
[357]573 self.apply_effect(protagonist)
[351]574 self._ticks += 1
575 if self._ticks > self.rate:
576 self._ticks = 0
577
[357]578 def apply_effect(self, protagonist):
579 protagonist.lose_health(self.damage)
580
[351]581 @classmethod
582 def requires(cls):
583 return [("name", "string"), ("position", "coordinates"),
[354]584 ("outline", "polygon (convex)")]
[351]585
586
587class AcidFloor(HostileTerrain):
588 damage = 1
589 tile = 'acid.png'
[357]590
591
592class ForceWolfFloor(HostileTerrain):
593 tile = 'moonlight.png'
594 rate = 0
[362]595 tile_alpha = 150
596 zorder = ZORDER_HIGH
[357]597
598 def apply_effect(self, protagonist):
599 protagonist.force_wolf_form()
[364]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.