source: nagslang/game_object.py@ 363:3dd08e18580f

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

Acid attacks shoot things that look like acid

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