source: nagslang/game_object.py@ 307:c2bbb1e70d6f

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

Rename animate to update and pass seconds, for future fun

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