source: nagslang/game_object.py@ 313:768e1d06155f

Last change on this file since 313:768e1d06155f was 313:768e1d06155f, checked in by David Sharpe, 9 years ago

PEP8

File size: 14.4 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 SWITCH_PUSHERS, COLLISION_TYPE_SWITCH, COLLISION_TYPE_BOX, ZORDER_LOW,
13 ZORDER_FLOOR, COLLISION_TYPE_DOOR, COLLISION_TYPE_PROJECTILE,
14 COLLISION_TYPE_WEREWOLF_ATTACK)
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 @classmethod
166 def requires(cls):
167 """Hints for the level editor"""
168 return [("name", "string")]
169
170
171class FloorSwitch(GameObject):
172 zorder = ZORDER_FLOOR
173
174 def __init__(self, space, position):
175 body = make_body(None, None, position)
176 self.shape = pymunk.Circle(body, 30)
177 self.shape.collision_type = COLLISION_TYPE_SWITCH
178 self.shape.sensor = True
179 super(FloorSwitch, self).__init__(
180 SingleShapePhysicser(space, self.shape),
181 render.ImageStateRenderer({
182 True: resources.get_image('objects', 'sensor_on.png'),
183 False: resources.get_image('objects', 'sensor_off.png'),
184 }),
185 puzzle.CollidePuzzler(*SWITCH_PUSHERS),
186 )
187
188 @classmethod
189 def requires(cls):
190 return [("name", "string"), ("position", "coordinates")]
191
192
193class Note(GameObject):
194 zorder = ZORDER_FLOOR
195
196 def __init__(self, space, position, message):
197 body = make_body(None, None, position)
198 self.shape = pymunk.Circle(body, 30)
199 self.shape.sensor = True
200 super(Note, self).__init__(
201 SingleShapePhysicser(space, self.shape),
202 render.ImageRenderer(resources.get_image('objects', 'note.png')),
203 puzzle.CollidePuzzler(),
204 render.TextOverlay(message),
205 )
206
207 @classmethod
208 def requires(cls):
209 return [("name", "string"), ("position", "coordinates"),
210 ("message", "text")]
211
212
213class FloorLight(GameObject):
214 zorder = ZORDER_FLOOR
215
216 def __init__(self, space, position, state_source):
217 body = make_body(None, None, position)
218 self.shape = pymunk.Circle(body, 10)
219 self.shape.collision_type = COLLISION_TYPE_SWITCH
220 self.shape.sensor = True
221 super(FloorLight, self).__init__(
222 SingleShapePhysicser(space, self.shape),
223 render.ImageStateRenderer({
224 True: resources.get_image('objects', 'light_on.png'),
225 False: resources.get_image('objects', 'light_off.png'),
226 }),
227 puzzle.StateProxyPuzzler(state_source),
228 )
229
230 @classmethod
231 def requires(cls):
232 return [("name", "string"), ("position", "coordinates"),
233 ("state_source", "puzzler")]
234
235
236class Box(GameObject):
237 def __init__(self, space, position):
238 body = make_body(10, 10000, position, damping=0.5)
239 self.shape = pymunk.Poly(
240 body, [(-20, -20), (20, -20), (20, 20), (-20, 20)])
241 self.shape.friction = 0.5
242 self.shape.collision_type = COLLISION_TYPE_BOX
243 super(Box, self).__init__(
244 SingleShapePhysicser(space, self.shape),
245 render.ImageRenderer(resources.get_image('objects', 'crate.png')),
246 )
247
248 @classmethod
249 def requires(cls):
250 return [("name", "string"), ("position", "coordinates"),
251 ("state_source", "puzzler")]
252
253
254class Door(GameObject):
255 zorder = ZORDER_FLOOR
256
257 def __init__(self, space, position, destination, dest_pos, angle,
258 key_state=None):
259 body = make_body(pymunk.inf, pymunk.inf, position, damping=0.5)
260 self.shape = pymunk.Circle(body, 30)
261 self.shape.collision_type = COLLISION_TYPE_DOOR
262 self.shape.body.angle = float(angle) / 180 * math.pi
263 self.shape.sensor = True
264 self.destination = destination
265 self.dest_pos = tuple(dest_pos)
266 puzzler = None
267 action = environment.Action(self._post_door_event)
268 if key_state is not None:
269 puzzler = puzzle.StateProxyPuzzler(key_state)
270 action.condition = environment.PuzzleStateCondition(puzzler)
271 super(Door, self).__init__(
272 SingleShapePhysicser(space, self.shape),
273 render.ImageRenderer(resources.get_image('objects', 'door.png')),
274 puzzler,
275 interactible=environment.Interactible(action),
276 )
277
278 def _post_door_event(self, protagonist):
279 DoorEvent.post(self.destination, self.dest_pos)
280
281 @classmethod
282 def requires(cls):
283 return [("name", "string"), ("position", "coordinates"),
284 ("destination", "level name"), ("dest_pos", "coordinate"),
285 ("angle", "degrees"),
286 ("key_state", "puzzler (optional)")]
287
288
289class Bulkhead(GameObject):
290 zorder = ZORDER_FLOOR
291
292 def __init__(self, space, end1, end2, key_state=None):
293 body = make_body(None, None, (0, 0))
294 self.shape = pymunk.Segment(body, tuple(end1), tuple(end2), 3)
295 self.shape.collision_type = COLLISION_TYPE_DOOR
296 if key_state is None:
297 puzzler = puzzle.YesPuzzler()
298 else:
299 puzzler = puzzle.StateProxyPuzzler(key_state)
300 super(Bulkhead, self).__init__(
301 SingleShapePhysicser(space, self.shape),
302 render.ShapeStateRenderer(),
303 puzzler,
304 )
305
306 def collide_with_protagonist(self, protagonist):
307 if self.puzzler.get_state():
308 # Reject the collision, we can walk through.
309 return False
310 return True
311
312 @classmethod
313 def requires(cls):
314 return [("name", "string"), ("end1", "coordinates"),
315 ("end2", "coordinates"), ("key_state", "puzzler")]
316
317
318class ToggleSwitch(GameObject):
319 zorder = ZORDER_LOW
320
321 def __init__(self, space, position):
322 body = make_body(None, None, position)
323 self.shape = pymunk.Circle(body, 20)
324 self.shape.sensor = True
325 self.toggle_on = False
326 super(ToggleSwitch, self).__init__(
327 SingleShapePhysicser(space, self.shape),
328 render.ImageStateRenderer({
329 True: resources.get_image('objects', 'lever.png'),
330 False: resources.get_image(
331 'objects', 'lever.png', transforms=(FLIP_H,)),
332 }),
333 puzzle.ParentAttrPuzzler('toggle_on'),
334 interactible=environment.Interactible(
335 environment.Action(self._toggle)),
336 )
337
338 def _toggle(self, protagonist):
339 self.toggle_on = not self.toggle_on
340
341 @classmethod
342 def requires(cls):
343 return [("name", "string"), ("position", "coordinates")]
344
345
346class Bullet(GameObject):
347 def __init__(self, space, position, impulse, damage,
348 source_collision_type):
349 body = make_body(1, pymunk.inf, position)
350 self.last_position = position
351 self.shape = pymunk.Circle(body, 2)
352 self.shape.sensor = True
353 self.shape.collision_type = COLLISION_TYPE_PROJECTILE
354 self.damage = damage
355 self.source_collision_type = source_collision_type
356 super(Bullet, self).__init__(
357 SingleShapePhysicser(space, self.shape),
358 render.ImageRenderer(resources.get_image('objects', 'bullet.png')),
359 )
360 self.physicser.apply_impulse(impulse)
361
362 def update(self, seconds):
363 super(Bullet, self).update(seconds)
364 position = (self.physicser.position.x, self.physicser.position.y)
365 r = self.get_space().segment_query(self.last_position, position)
366 self.last_position = position
367 for collision in r:
368 shape = collision.shape
369 if (shape.collision_type == self.source_collision_type
370 or shape == self.physicser.get_shape()
371 or shape.sensor):
372 continue
373 if hasattr(shape, 'physicser'):
374 shape.physicser.game_object.hit(self)
375 self.physicser.remove_from_space()
376 self.remove = True
377 break
378
379
380class CollectibleGameObject(GameObject):
381 zorder = ZORDER_LOW
382
383 def __init__(self, space, name, shape, renderer):
384 self._name = name
385 shape.sensor = True
386 super(CollectibleGameObject, self).__init__(
387 SingleShapePhysicser(space, shape),
388 renderer,
389 interactible=environment.Interactible(
390 environment.Action(
391 self._collect, environment.HumanFormCondition())),
392 )
393
394 def _collect(self, protagonist):
395 protagonist.inventory[self._name] = self
396 # TODO: Make this less hacky.
397 self.physicser.remove_from_space()
398 self.renderer = render.NullRenderer()
399
400
401class Gun(CollectibleGameObject):
402 def __init__(self, space, position):
403 body = make_body(None, None, position)
404 self.shape = pymunk.Circle(body, 20)
405 super(Gun, self).__init__(
406 space, 'gun', self.shape,
407 render.ImageRenderer(resources.get_image('objects', 'bullet.png')),
408 )
409
410
411class ClawAttack(GameObject):
412 def __init__(self, space, position, vector, source_collision_type):
413 body = make_body(1, pymunk.inf, position)
414 self.last_position = position
415 self.shape = pymunk.Circle(body, 30)
416 self.shape.sensor = True
417 self.shape.collision_type = COLLISION_TYPE_WEREWOLF_ATTACK
418 self.source_collision_type = source_collision_type
419 super(ClawAttack, self).__init__(
420 SingleShapePhysicser(space, self.shape),
421 render.ImageRenderer(resources.get_image
422 ('objects', 'werewolf_SW_claw_attack.png')),
423 )
424 self.time_created = time.time()
425
426 def animate(self):
427 super(ClawAttack, self).animate()
428 position = (self.physicser.position.x, self.physicser.position.y)
429 r = self.get_space().segment_query(self.last_position, position)
430 self.last_position = position
431 for collision in r:
432 shape = collision.shape
433 if (shape.collision_type == self.source_collision_type
434 or shape == self.physicser.get_shape()
435 or shape.sensor):
436 continue
437 if hasattr(shape, 'physicser'):
438 shape.physicser.game_object.hit(self)
439 if time.time() - self.time_created > 0.2:
440 self.physicser.remove_from_space()
441 self.remove = True
Note: See TracBrowser for help on using the repository browser.