source: nagslang/game_object.py@ 382:70f6917cad07

Last change on this file since 382:70f6917cad07 was 382:70f6917cad07, checked in by Jeremy Thurgood <firxen@…>, 8 years ago

Some notes.

File size: 18.4 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
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
26class Physicser(object):
27 def __init__(self, space):
28 self._space = space
29
30 def get_space(self):
31 return self._space
32
33 def set_space(self, new_space):
34 self._space = new_space
35
36 def set_game_object(self, game_object):
37 self.game_object = game_object
38
39 def get_shape(self):
40 raise NotImplementedError()
41
42 def add_to_space(self):
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)
47
48 def remove_from_space(self):
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)
53
54 def get_render_position(self, surface):
55 pos = self.get_shape().body.position
56 return pymunk.pygame_util.to_pygame(pos, surface)
57
58 def get_angle(self):
59 return self.get_shape().body.angle
60
61 def get_velocity(self):
62 return self.get_shape().body.velocity
63
64 def _get_position(self):
65 return self.get_shape().body.position
66
67 def _set_position(self, position):
68 self.get_shape().body.position = position
69
70 position = property(_get_position, _set_position)
71
72 def apply_impulse(self, j, r=(0, 0)):
73 return self.get_shape().body.apply_impulse(j, r)
74
75
76class SingleShapePhysicser(Physicser):
77 def __init__(self, space, shape):
78 super(SingleShapePhysicser, self).__init__(space)
79 self._shape = shape
80 shape.physicser = self
81
82 def get_shape(self):
83 return self._shape
84
85
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)
95 body.position = tuple(position)
96 if damping is not None:
97 body.damping = damping
98 body.velocity_func = damping_velocity_func
99 return body
100
101
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
108 zorder = ZORDER_LOW
109 is_moving = False # `True` if a movement animation should play.
110
111 def __init__(self, physicser, renderer, puzzler=None, overlay=None,
112 interactible=None):
113 self.lifetime = 0
114 self.physicser = physicser
115 physicser.set_game_object(self)
116 self.physicser.add_to_space()
117 self.renderer = renderer
118 renderer.set_game_object(self)
119 self.puzzler = puzzler
120 if puzzler is not None:
121 puzzler.set_game_object(self)
122 self.overlay = overlay
123 if overlay is not None:
124 self.overlay.set_game_object(self)
125 self.interactible = interactible
126 if interactible is not None:
127 self.interactible.set_game_object(self)
128 self.remove = False # If true, will be removed from drawables
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
142
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
153 def get_space(self):
154 return self.physicser.get_space()
155
156 def get_shape(self):
157 return self.physicser.get_shape()
158
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()
164
165 def get_facing_direction(self):
166 """Used by rendererd that care what direction an object is facing.
167 """
168 return None
169
170 def render(self, surface):
171 return self.renderer.render(surface)
172
173 def update(self, dt):
174 self.lifetime += dt
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)
179 self.renderer.update(dt)
180
181 def hit(self, weapon):
182 '''Was hit with a weapon (such as a bullet)'''
183 pass
184
185 def collide_with_protagonist(self, protagonist):
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 """
191 return True
192
193 def collide_with_furniture(self, furniture):
194 return True
195
196 def collide_with_claw_attack(self, claw_attack):
197 return True
198
199 @classmethod
200 def requires(cls):
201 """Hints for the level editor"""
202 return [("name", "string")]
203
204
205class FloorSwitch(GameObject):
206 zorder = ZORDER_FLOOR
207
208 def __init__(self, space, position):
209 body = make_body(None, None, position)
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__(
214 SingleShapePhysicser(space, self.shape),
215 render.ImageStateRenderer({
216 True: resources.get_image('objects', 'sensor_on.png'),
217 False: resources.get_image('objects', 'sensor_off.png'),
218 }),
219 puzzle.CollidePuzzler(*SWITCH_PUSHERS),
220 )
221
222 @classmethod
223 def requires(cls):
224 return [("name", "string"), ("position", "coordinates")]
225
226
227class Note(GameObject):
228 zorder = ZORDER_FLOOR
229
230 def __init__(self, space, position, message):
231 print repr(message)
232 body = make_body(None, None, position)
233 self.shape = pymunk.Circle(body, 30)
234 self.shape.sensor = True
235 super(Note, self).__init__(
236 SingleShapePhysicser(space, self.shape),
237 render.ImageRenderer(resources.get_image('objects', 'note.png')),
238 puzzle.CollidePuzzler(),
239 render.TextOverlay(message),
240 )
241
242 @classmethod
243 def requires(cls):
244 return [("name", "string"), ("position", "coordinates"),
245 ("message", "text")]
246
247
248class FloorLight(GameObject):
249 zorder = ZORDER_FLOOR
250
251 def __init__(self, space, position, state_source):
252 body = make_body(None, None, position)
253 self.shape = pymunk.Circle(body, 10)
254 self.shape.collision_type = COLLISION_TYPE_SWITCH
255 self.shape.sensor = True
256 super(FloorLight, self).__init__(
257 SingleShapePhysicser(space, self.shape),
258 render.ImageStateRenderer({
259 True: resources.get_image('objects', 'light_on.png'),
260 False: resources.get_image('objects', 'light_off.png'),
261 }),
262 puzzle.StateProxyPuzzler(state_source),
263 )
264
265 @classmethod
266 def requires(cls):
267 return [("name", "string"), ("position", "coordinates"),
268 ("state_source", "puzzler")]
269
270
271class Box(GameObject):
272 def __init__(self, space, position):
273 body = make_body(10, 10000, position, damping=0.5)
274 self.shape = pymunk.Poly(
275 body, [(-20, -20), (20, -20), (20, 20), (-20, 20)])
276 self.shape.friction = 0.5
277 self.shape.collision_type = COLLISION_TYPE_FURNITURE
278 super(Box, self).__init__(
279 SingleShapePhysicser(space, self.shape),
280 render.ImageRenderer(resources.get_image('objects', 'crate.png')),
281 )
282
283 @classmethod
284 def requires(cls):
285 return [("name", "string"), ("position", "coordinates"),
286 ("state_source", "puzzler")]
287
288
289class BaseDoor(GameObject):
290 zorder = ZORDER_FLOOR
291 is_open = True
292
293 def __init__(self, space, position, destination, dest_pos, angle,
294 renderer, condition):
295 body = make_body(pymunk.inf, pymunk.inf, position, damping=0.5)
296 self.shape = pymunk.Circle(body, 30)
297 self.shape.collision_type = COLLISION_TYPE_DOOR
298 self.shape.body.angle = float(angle) / 180 * math.pi
299 self.shape.sensor = True
300 self.destination = destination
301 self.dest_pos = tuple(dest_pos)
302 super(BaseDoor, self).__init__(
303 SingleShapePhysicser(space, self.shape),
304 renderer,
305 puzzle.ParentAttrPuzzler('is_open'),
306 interactible=environment.Interactible(
307 environment.Action(self._post_door_event, condition)),
308 )
309
310 def _post_door_event(self, protagonist):
311 DoorEvent.post(self.destination, self.dest_pos)
312
313
314class Door(BaseDoor):
315 def __init__(self, space, position, destination, dest_pos, angle):
316 super(Door, self).__init__(
317 space, position, destination, dest_pos, angle,
318 render.ImageRenderer(resources.get_image('objects', 'door.png')),
319 environment.YesCondition(),
320 )
321
322 @classmethod
323 def requires(cls):
324 return [("name", "string"), ("position", "coordinates"),
325 ("destination", "level name"), ("dest_pos", "coordinate"),
326 ("angle", "degrees")]
327
328
329class PuzzleDoor(BaseDoor):
330 def __init__(self, space, position, destination, dest_pos, angle,
331 key_state):
332 self._key_state = key_state
333 super(PuzzleDoor, self).__init__(
334 space, position, destination, dest_pos, angle,
335 render.ImageStateRenderer({
336 True: resources.get_image('objects', 'door.png'),
337 False: resources.get_image('objects', 'locked_door.png'),
338 }),
339 environment.FunctionCondition(lambda p: self.is_open),
340 )
341
342 @property
343 def is_open(self):
344 return self._stored_state['is_open']
345
346 def set_stored_state_dict(self, stored_state):
347 self._stored_state = stored_state
348 self._stored_state.setdefault('is_open', False)
349 return True
350
351 def update(self, dt):
352 if not self.is_open:
353 self._stored_state['is_open'] = self.puzzler.glue.get_state_of(
354 self._key_state)
355 super(PuzzleDoor, self).update(dt)
356
357 @classmethod
358 def requires(cls):
359 return [("name", "string"), ("position", "coordinates"),
360 ("destination", "level name"), ("dest_pos", "coordinate"),
361 ("angle", "degrees"),
362 ("key_state", "puzzler")]
363
364
365class KeyedDoor(BaseDoor):
366 def __init__(self, space, position, destination, dest_pos, angle,
367 key_item=None):
368 self._key_item = key_item
369 overlay = ImageOverlay(
370 resources.get_image('objects', '%s.png' % (key_item,)))
371 super(KeyedDoor, self).__init__(
372 space, position, destination, dest_pos, angle,
373 render.ImageRenderer(resources.get_image(
374 'objects', 'door.png', transforms=(overlay,))),
375 environment.ItemRequiredCondition(key_item),
376 )
377
378 @classmethod
379 def requires(cls):
380 return [("name", "string"), ("position", "coordinates"),
381 ("destination", "level name"), ("dest_pos", "coordinate"),
382 ("angle", "degrees"), ("key_item", "item name")]
383
384
385class Bulkhead(GameObject):
386 zorder = ZORDER_FLOOR
387
388 def __init__(self, space, end1, end2, key_state=None):
389 body = make_body(None, None, (0, 0))
390 self.shape = pymunk.Segment(body, tuple(end1), tuple(end2), 3)
391 self.shape.collision_type = COLLISION_TYPE_DOOR
392 if key_state is None:
393 puzzler = puzzle.YesPuzzler()
394 else:
395 puzzler = puzzle.StateProxyPuzzler(key_state)
396 super(Bulkhead, self).__init__(
397 SingleShapePhysicser(space, self.shape),
398 render.ShapeStateRenderer(),
399 puzzler,
400 )
401
402 def collide_with_protagonist(self, protagonist):
403 if self.puzzler.get_state():
404 # Reject the collision, we can walk through.
405 return False
406 return True
407
408 collide_with_furniture = collide_with_protagonist
409
410 @classmethod
411 def requires(cls):
412 return [("name", "string"), ("end1", "coordinates"),
413 ("end2", "coordinates"), ("key_state", "puzzler")]
414
415
416class ToggleSwitch(GameObject):
417 zorder = ZORDER_LOW
418
419 def __init__(self, space, position):
420 body = make_body(None, None, position)
421 self.shape = pymunk.Circle(body, 20)
422 self.shape.sensor = True
423 super(ToggleSwitch, self).__init__(
424 SingleShapePhysicser(space, self.shape),
425 render.ImageStateRenderer({
426 True: resources.get_image('objects', 'lever.png'),
427 False: resources.get_image(
428 'objects', 'lever.png', transforms=(FLIP_H,)),
429 }),
430 puzzle.ParentAttrPuzzler('toggle_on'),
431 interactible=environment.Interactible(
432 environment.Action(self._toggle)),
433 )
434
435 @property
436 def toggle_on(self):
437 return self._stored_state['toggle_on']
438
439 def _toggle(self, protagonist):
440 self._stored_state['toggle_on'] = not self.toggle_on
441
442 def set_stored_state_dict(self, stored_state):
443 self._stored_state = stored_state
444 # We start in the "off" position.
445 self._stored_state.setdefault('toggle_on', False)
446 return True
447
448 @classmethod
449 def requires(cls):
450 return [("name", "string"), ("position", "coordinates")]
451
452
453class Bullet(GameObject):
454 def __init__(self, space, position, impulse, damage, bullet_type,
455 source_collision_type):
456 body = make_body(1, pymunk.inf, position)
457 body.angle = impulse.angle
458 self.last_position = position
459 self.shape = pymunk.Circle(body, 2)
460 self.shape.sensor = True
461 self.shape.collision_type = COLLISION_TYPE_PROJECTILE
462 self.damage = damage
463 self.type = bullet_type
464 self.source_collision_type = source_collision_type
465 super(Bullet, self).__init__(
466 SingleShapePhysicser(space, self.shape),
467 render.ImageRenderer(resources.get_image(
468 'objects', '%s.png' % self.type)),
469 )
470 self.physicser.apply_impulse(impulse)
471
472 def update(self, dt):
473 super(Bullet, self).update(dt)
474 position = (self.physicser.position.x, self.physicser.position.y)
475 r = self.get_space().segment_query(self.last_position, position)
476 self.last_position = position
477 for collision in r:
478 shape = collision.shape
479 if (shape.collision_type == self.source_collision_type
480 or shape == self.physicser.get_shape()
481 or shape.sensor):
482 continue
483 if hasattr(shape, 'physicser'):
484 shape.physicser.game_object.hit(self)
485 self.physicser.remove_from_space()
486 self.remove = True
487 break
488
489
490class ClawAttack(GameObject):
491 def __init__(self, space, pos, vector, damage):
492 body = make_body(1, pymunk.inf,
493 (pos[0] + (vector.length * math.cos(vector.angle)),
494 pos[1] + (vector.length * math.sin(vector.angle))))
495 body.angle = vector.angle
496 self.shape = pymunk.Circle(body, 30)
497 self.shape.sensor = True
498 self.shape.collision_type = COLLISION_TYPE_WEREWOLF_ATTACK
499 self.damage = damage
500 super(ClawAttack, self).__init__(
501 SingleShapePhysicser(space, self.shape),
502 render.ImageRenderer(resources.get_image(
503 'objects', 'werewolf_SW_claw_attack.png',
504 transforms=(FLIP_H,))),
505 )
506
507 def update(self, dt):
508 super(ClawAttack, self).update(dt)
509 if self.lifetime > 0.1:
510 self.physicser.remove_from_space()
511 self.remove = True
512
513
514class HostileTerrain(GameObject):
515 zorder = ZORDER_FLOOR
516 damage = None
517 tile = None
518 tile_alpha = 255
519 # How often to hit the player
520 rate = 5
521
522 def __init__(self, space, position, outline):
523 body = make_body(10, pymunk.inf, position)
524 # Adjust shape relative to position
525 shape_outline = [(p[0] - position[0], p[1] - position[1]) for
526 p in outline]
527 self.shape = pymunk.Poly(body, shape_outline)
528 self._ticks = 0
529 self.shape.collision_type = COLLISION_TYPE_SWITCH
530 self.shape.sensor = True
531 super(HostileTerrain, self).__init__(
532 SingleShapePhysicser(space, self.shape),
533 render.TiledRenderer(outline,
534 resources.get_image('tiles', self.tile),
535 self.tile_alpha))
536
537 def collide_with_protagonist(self, protagonist):
538 # We're called every frame we're colliding, so
539 # There are timing issues with stepping on and
540 # off terrian, but as long as the rate is reasonably
541 # low, they shouldn't impact gameplay
542 if self._ticks == 0:
543 self.apply_effect(protagonist)
544 self._ticks += 1
545 if self._ticks > self.rate:
546 self._ticks = 0
547
548 def apply_effect(self, protagonist):
549 protagonist.lose_health(self.damage)
550
551 @classmethod
552 def requires(cls):
553 return [("name", "string"), ("position", "coordinates"),
554 ("outline", "polygon (convex)")]
555
556
557class AcidFloor(HostileTerrain):
558 damage = 1
559 tile = 'acid.png'
560 tile_alpha = 200
561
562
563class ForceWolfFloor(HostileTerrain):
564 tile = 'moonlight.png'
565 rate = 0
566 tile_alpha = 150
567 zorder = ZORDER_HIGH
568
569 def apply_effect(self, protagonist):
570 protagonist.force_wolf_form()
Note: See TracBrowser for help on using the repository browser.