source: nagslang/game_object.py@ 319:01e98732de46

Last change on this file since 319:01e98732de46 was 319:01e98732de46, checked in by Stefano Rivera <stefano@…>, 9 years ago

Open bulkheads are no longer opaque to furniture

File size: 14.5 KB
Line 
1import pymunk
2import pymunk.pygame_util
3
4import math
5import time
6
7from nagslang import environment
8from nagslang import puzzle
9from nagslang import render
10from nagslang.mutators import FLIP_H
11from nagslang.constants import (
12 COLLISION_TYPE_DOOR, COLLISION_TYPE_FURNITURE, COLLISION_TYPE_PROJECTILE,
13 COLLISION_TYPE_SWITCH, COLLISION_TYPE_WEREWOLF_ATTACK,
14 SWITCH_PUSHERS, ZORDER_FLOOR, ZORDER_LOW)
15from nagslang.resources import resources
16from nagslang.events import DoorEvent
17
18
19def get_editable_game_objects():
20 classes = []
21 for cls_name, cls in globals().iteritems():
22 if isinstance(cls, type) and hasattr(cls, 'requires'):
23 classes.append((cls_name, cls))
24 return classes
25
26
27class Physicser(object):
28 def __init__(self, space):
29 self._space = space
30
31 def get_space(self):
32 return self._space
33
34 def set_space(self, new_space):
35 self._space = new_space
36
37 def set_game_object(self, game_object):
38 self.game_object = game_object
39
40 def get_shape(self):
41 raise NotImplementedError()
42
43 def add_to_space(self):
44 shape = self.get_shape()
45 self.get_space().add(shape)
46 if not shape.body.is_static:
47 self.get_space().add(shape.body)
48
49 def remove_from_space(self):
50 shape = self.get_shape()
51 self.get_space().remove(shape)
52 if not shape.body.is_static:
53 self.get_space().remove(shape.body)
54
55 def get_render_position(self, surface):
56 pos = self.get_shape().body.position
57 return pymunk.pygame_util.to_pygame(pos, surface)
58
59 def get_angle(self):
60 return self.get_shape().body.angle
61
62 def get_velocity(self):
63 return self.get_shape().body.velocity
64
65 def _get_position(self):
66 return self.get_shape().body.position
67
68 def _set_position(self, position):
69 self.get_shape().body.position = position
70
71 position = property(_get_position, _set_position)
72
73 def apply_impulse(self, j, r=(0, 0)):
74 return self.get_shape().body.apply_impulse(j, r)
75
76
77class SingleShapePhysicser(Physicser):
78 def __init__(self, space, shape):
79 super(SingleShapePhysicser, self).__init__(space)
80 self._shape = shape
81 shape.physicser = self
82
83 def get_shape(self):
84 return self._shape
85
86
87def damping_velocity_func(body, gravity, damping, dt):
88 """Apply custom damping to this body's velocity.
89 """
90 damping = getattr(body, 'damping', damping)
91 return pymunk.Body.update_velocity(body, gravity, damping, dt)
92
93
94def make_body(mass, moment, position, damping=None):
95 body = pymunk.Body(mass, moment)
96 body.position = tuple(position)
97 if damping is not None:
98 body.damping = damping
99 body.velocity_func = damping_velocity_func
100 return body
101
102
103class GameObject(object):
104 """A representation of a thing in the game world.
105
106 This has a rendery thing, physicsy things and maybe some other things.
107 """
108
109 zorder = ZORDER_LOW
110 is_moving = False # `True` if a movement animation should play.
111
112 def __init__(self, physicser, renderer, puzzler=None, overlay=None,
113 interactible=None):
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
130 def get_space(self):
131 return self.physicser.get_space()
132
133 def get_shape(self):
134 return self.physicser.get_shape()
135
136 def get_render_position(self, surface):
137 return self.physicser.get_render_position(surface)
138
139 def get_render_angle(self):
140 return self.physicser.get_angle()
141
142 def get_facing_direction(self):
143 """Used by rendererd that care what direction an object is facing.
144 """
145 return None
146
147 def render(self, surface):
148 return self.renderer.render(surface)
149
150 def update(self, seconds):
151 self.renderer.update(seconds)
152
153 def hit(self, weapon):
154 '''Was hit with a weapon (such as a bullet)'''
155 pass
156
157 def collide_with_protagonist(self, protagonist):
158 """Called as a `pre_solve` collision callback with the protagonist.
159
160 You can return `False` to ignore the collision, anything else
161 (including `None`) to process the collision as normal.
162 """
163 return True
164
165 def collide_with_furniture(self, furniture):
166 return True
167
168 @classmethod
169 def requires(cls):
170 """Hints for the level editor"""
171 return [("name", "string")]
172
173
174class FloorSwitch(GameObject):
175 zorder = ZORDER_FLOOR
176
177 def __init__(self, space, position):
178 body = make_body(None, None, position)
179 self.shape = pymunk.Circle(body, 30)
180 self.shape.collision_type = COLLISION_TYPE_SWITCH
181 self.shape.sensor = True
182 super(FloorSwitch, self).__init__(
183 SingleShapePhysicser(space, self.shape),
184 render.ImageStateRenderer({
185 True: resources.get_image('objects', 'sensor_on.png'),
186 False: resources.get_image('objects', 'sensor_off.png'),
187 }),
188 puzzle.CollidePuzzler(*SWITCH_PUSHERS),
189 )
190
191 @classmethod
192 def requires(cls):
193 return [("name", "string"), ("position", "coordinates")]
194
195
196class Note(GameObject):
197 zorder = ZORDER_FLOOR
198
199 def __init__(self, space, position, message):
200 body = make_body(None, None, position)
201 self.shape = pymunk.Circle(body, 30)
202 self.shape.sensor = True
203 super(Note, self).__init__(
204 SingleShapePhysicser(space, self.shape),
205 render.ImageRenderer(resources.get_image('objects', 'note.png')),
206 puzzle.CollidePuzzler(),
207 render.TextOverlay(message),
208 )
209
210 @classmethod
211 def requires(cls):
212 return [("name", "string"), ("position", "coordinates"),
213 ("message", "text")]
214
215
216class FloorLight(GameObject):
217 zorder = ZORDER_FLOOR
218
219 def __init__(self, space, position, state_source):
220 body = make_body(None, None, position)
221 self.shape = pymunk.Circle(body, 10)
222 self.shape.collision_type = COLLISION_TYPE_SWITCH
223 self.shape.sensor = True
224 super(FloorLight, self).__init__(
225 SingleShapePhysicser(space, self.shape),
226 render.ImageStateRenderer({
227 True: resources.get_image('objects', 'light_on.png'),
228 False: resources.get_image('objects', 'light_off.png'),
229 }),
230 puzzle.StateProxyPuzzler(state_source),
231 )
232
233 @classmethod
234 def requires(cls):
235 return [("name", "string"), ("position", "coordinates"),
236 ("state_source", "puzzler")]
237
238
239class Box(GameObject):
240 def __init__(self, space, position):
241 body = make_body(10, 10000, position, damping=0.5)
242 self.shape = pymunk.Poly(
243 body, [(-20, -20), (20, -20), (20, 20), (-20, 20)])
244 self.shape.friction = 0.5
245 self.shape.collision_type = COLLISION_TYPE_FURNITURE
246 super(Box, self).__init__(
247 SingleShapePhysicser(space, self.shape),
248 render.ImageRenderer(resources.get_image('objects', 'crate.png')),
249 )
250
251 @classmethod
252 def requires(cls):
253 return [("name", "string"), ("position", "coordinates"),
254 ("state_source", "puzzler")]
255
256
257class Door(GameObject):
258 zorder = ZORDER_FLOOR
259
260 def __init__(self, space, position, destination, dest_pos, angle,
261 key_state=None):
262 body = make_body(pymunk.inf, pymunk.inf, position, damping=0.5)
263 self.shape = pymunk.Circle(body, 30)
264 self.shape.collision_type = COLLISION_TYPE_DOOR
265 self.shape.body.angle = float(angle) / 180 * math.pi
266 self.shape.sensor = True
267 self.destination = destination
268 self.dest_pos = tuple(dest_pos)
269 puzzler = None
270 action = environment.Action(self._post_door_event)
271 if key_state is not None:
272 puzzler = puzzle.StateProxyPuzzler(key_state)
273 action.condition = environment.PuzzleStateCondition(puzzler)
274 super(Door, self).__init__(
275 SingleShapePhysicser(space, self.shape),
276 render.ImageRenderer(resources.get_image('objects', 'door.png')),
277 puzzler,
278 interactible=environment.Interactible(action),
279 )
280
281 def _post_door_event(self, protagonist):
282 DoorEvent.post(self.destination, self.dest_pos)
283
284 @classmethod
285 def requires(cls):
286 return [("name", "string"), ("position", "coordinates"),
287 ("destination", "level name"), ("dest_pos", "coordinate"),
288 ("angle", "degrees"),
289 ("key_state", "puzzler (optional)")]
290
291
292class Bulkhead(GameObject):
293 zorder = ZORDER_FLOOR
294
295 def __init__(self, space, end1, end2, key_state=None):
296 body = make_body(None, None, (0, 0))
297 self.shape = pymunk.Segment(body, tuple(end1), tuple(end2), 3)
298 self.shape.collision_type = COLLISION_TYPE_DOOR
299 if key_state is None:
300 puzzler = puzzle.YesPuzzler()
301 else:
302 puzzler = puzzle.StateProxyPuzzler(key_state)
303 super(Bulkhead, self).__init__(
304 SingleShapePhysicser(space, self.shape),
305 render.ShapeStateRenderer(),
306 puzzler,
307 )
308
309 def collide_with_protagonist(self, protagonist):
310 if self.puzzler.get_state():
311 # Reject the collision, we can walk through.
312 return False
313 return True
314
315 collide_with_furniture = collide_with_protagonist
316
317 @classmethod
318 def requires(cls):
319 return [("name", "string"), ("end1", "coordinates"),
320 ("end2", "coordinates"), ("key_state", "puzzler")]
321
322
323class ToggleSwitch(GameObject):
324 zorder = ZORDER_LOW
325
326 def __init__(self, space, position):
327 body = make_body(None, None, position)
328 self.shape = pymunk.Circle(body, 20)
329 self.shape.sensor = True
330 self.toggle_on = False
331 super(ToggleSwitch, self).__init__(
332 SingleShapePhysicser(space, self.shape),
333 render.ImageStateRenderer({
334 True: resources.get_image('objects', 'lever.png'),
335 False: resources.get_image(
336 'objects', 'lever.png', transforms=(FLIP_H,)),
337 }),
338 puzzle.ParentAttrPuzzler('toggle_on'),
339 interactible=environment.Interactible(
340 environment.Action(self._toggle)),
341 )
342
343 def _toggle(self, protagonist):
344 self.toggle_on = not self.toggle_on
345
346 @classmethod
347 def requires(cls):
348 return [("name", "string"), ("position", "coordinates")]
349
350
351class Bullet(GameObject):
352 def __init__(self, space, position, impulse, damage,
353 source_collision_type):
354 body = make_body(1, pymunk.inf, position)
355 self.last_position = position
356 self.shape = pymunk.Circle(body, 2)
357 self.shape.sensor = True
358 self.shape.collision_type = COLLISION_TYPE_PROJECTILE
359 self.damage = damage
360 self.source_collision_type = source_collision_type
361 super(Bullet, self).__init__(
362 SingleShapePhysicser(space, self.shape),
363 render.ImageRenderer(resources.get_image('objects', 'bullet.png')),
364 )
365 self.physicser.apply_impulse(impulse)
366
367 def update(self, seconds):
368 super(Bullet, self).update(seconds)
369 position = (self.physicser.position.x, self.physicser.position.y)
370 r = self.get_space().segment_query(self.last_position, position)
371 self.last_position = position
372 for collision in r:
373 shape = collision.shape
374 if (shape.collision_type == self.source_collision_type
375 or shape == self.physicser.get_shape()
376 or shape.sensor):
377 continue
378 if hasattr(shape, 'physicser'):
379 shape.physicser.game_object.hit(self)
380 self.physicser.remove_from_space()
381 self.remove = True
382 break
383
384
385class CollectibleGameObject(GameObject):
386 zorder = ZORDER_LOW
387
388 def __init__(self, space, name, shape, renderer):
389 self._name = name
390 shape.sensor = True
391 super(CollectibleGameObject, self).__init__(
392 SingleShapePhysicser(space, shape),
393 renderer,
394 interactible=environment.Interactible(
395 environment.Action(
396 self._collect, environment.HumanFormCondition())),
397 )
398
399 def _collect(self, protagonist):
400 protagonist.inventory[self._name] = self
401 # TODO: Make this less hacky.
402 self.physicser.remove_from_space()
403 self.renderer = render.NullRenderer()
404
405
406class Gun(CollectibleGameObject):
407 def __init__(self, space, position):
408 body = make_body(None, None, position)
409 self.shape = pymunk.Circle(body, 20)
410 super(Gun, self).__init__(
411 space, 'gun', self.shape,
412 render.ImageRenderer(resources.get_image('objects', 'bullet.png')),
413 )
414
415
416class ClawAttack(GameObject):
417 def __init__(self, space, position, vector, source_collision_type):
418 body = make_body(1, pymunk.inf, position)
419 self.last_position = position
420 self.shape = pymunk.Circle(body, 30)
421 self.shape.sensor = True
422 self.shape.collision_type = COLLISION_TYPE_WEREWOLF_ATTACK
423 self.source_collision_type = source_collision_type
424 super(ClawAttack, self).__init__(
425 SingleShapePhysicser(space, self.shape),
426 render.ImageRenderer(resources.get_image
427 ('objects', 'werewolf_SW_claw_attack.png')),
428 )
429 self.time_created = time.time()
430
431 def animate(self):
432 super(ClawAttack, self).animate()
433 position = (self.physicser.position.x, self.physicser.position.y)
434 r = self.get_space().segment_query(self.last_position, position)
435 self.last_position = position
436 for collision in r:
437 shape = collision.shape
438 if (shape.collision_type == self.source_collision_type
439 or shape == self.physicser.get_shape()
440 or shape.sensor):
441 continue
442 if hasattr(shape, 'physicser'):
443 shape.physicser.game_object.hit(self)
444 if time.time() - self.time_created > 0.2:
445 self.physicser.remove_from_space()
446 self.remove = True
Note: See TracBrowser for help on using the repository browser.