source: nagslang/game_object.py@ 401:f7ee43c0e5c9

Last change on this file since 401:f7ee43c0e5c9 was 401:f7ee43c0e5c9, checked in by Neil Muller <drnlmuller@…>, 8 years ago

Animated acid

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