source: nagslang/game_object.py@ 296:eb08426a58fe

Last change on this file since 296:eb08426a58fe was 296:eb08426a58fe, checked in by Jeremy Thurgood <firxen@…>, 9 years ago

Levers look like levers.

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