source: nagslang/game_object.py@ 297:b00ed05f7364

Last change on this file since 297:b00ed05f7364 was 297:b00ed05f7364, checked in by Stefano Rivera <stefano@…>, 8 years ago

Create a tuple of bullet position, so we don't just hold a reference to a mutable object

File size: 12.8 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 animate(self):
149 self.renderer.animate()
150
151 def collide_with_protagonist(self, protagonist):
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 """
157 return True
158
159 @classmethod
160 def requires(cls):
161 """Hints for the level editor"""
162 return [("name", "string")]
163
164
165class FloorSwitch(GameObject):
166 zorder = ZORDER_FLOOR
167
168 def __init__(self, space, position):
169 body = make_body(None, None, position)
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__(
174 SingleShapePhysicser(space, self.shape),
175 render.ImageStateRenderer({
176 True: resources.get_image('objects', 'sensor_on.png'),
177 False: resources.get_image('objects', 'sensor_off.png'),
178 }),
179 puzzle.CollidePuzzler(*SWITCH_PUSHERS),
180 )
181
182 @classmethod
183 def requires(cls):
184 return [("name", "string"), ("position", "coordinates")]
185
186
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),
196 render.ImageRenderer(resources.get_image('objects', 'note.png')),
197 puzzle.CollidePuzzler(),
198 render.TextOverlay(message),
199 )
200
201 @classmethod
202 def requires(cls):
203 return [("name", "string"), ("position", "coordinates"),
204 ("message", "text")]
205
206
207class FloorLight(GameObject):
208 zorder = ZORDER_FLOOR
209
210 def __init__(self, space, position, state_source):
211 body = make_body(None, None, position)
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),
217 render.ImageStateRenderer({
218 True: resources.get_image('objects', 'light_on.png'),
219 False: resources.get_image('objects', 'light_off.png'),
220 }),
221 puzzle.StateProxyPuzzler(state_source),
222 )
223
224 @classmethod
225 def requires(cls):
226 return [("name", "string"), ("position", "coordinates"),
227 ("state_source", "puzzler")]
228
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)])
235 self.shape.friction = 0.5
236 self.shape.collision_type = COLLISION_TYPE_BOX
237 super(Box, self).__init__(
238 SingleShapePhysicser(space, self.shape),
239 render.ImageRenderer(resources.get_image('objects', 'crate.png')),
240 )
241
242 @classmethod
243 def requires(cls):
244 return [("name", "string"), ("position", "coordinates"),
245 ("state_source", "puzzler")]
246
247
248class Door(GameObject):
249 zorder = ZORDER_FLOOR
250
251 def __init__(self, space, position, destination, dest_pos, angle,
252 key_state=None):
253 body = make_body(pymunk.inf, pymunk.inf, position, damping=0.5)
254 self.shape = pymunk.Circle(body, 30)
255 self.shape.collision_type = COLLISION_TYPE_DOOR
256 self.shape.body.angle = float(angle) / 180 * math.pi
257 self.shape.sensor = True
258 self.destination = destination
259 self.dest_pos = tuple(dest_pos)
260 puzzler = None
261 action = environment.Action(self._post_door_event)
262 if key_state is not None:
263 puzzler = puzzle.StateProxyPuzzler(key_state)
264 action.condition = environment.PuzzleStateCondition(puzzler)
265 super(Door, self).__init__(
266 SingleShapePhysicser(space, self.shape),
267 render.ImageRenderer(resources.get_image('objects', 'door.png')),
268 puzzler,
269 interactible=environment.Interactible(action),
270 )
271
272 def _post_door_event(self, protagonist):
273 DoorEvent.post(self.destination, self.dest_pos)
274
275 @classmethod
276 def requires(cls):
277 return [("name", "string"), ("position", "coordinates"),
278 ("destination", "level name"), ("dest_pos", "coordinate"),
279 ("angle", "degrees"),
280 ("key_state", "puzzler (optional)")]
281
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
300 def collide_with_protagonist(self, protagonist):
301 if self.puzzler.get_state():
302 # Reject the collision, we can walk through.
303 return False
304 return True
305
306 @classmethod
307 def requires(cls):
308 return [("name", "string"), ("end1", "coordinates"),
309 ("end2", "coordinates"), ("key_state", "puzzler")]
310
311
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),
322 render.ImageStateRenderer({
323 True: resources.get_image('objects', 'lever.png'),
324 False: resources.get_image(
325 'objects', 'lever.png', transforms=(FLIP_H,)),
326 }),
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
340class Bullet(GameObject):
341 def __init__(self, space, position, impulse, source_collision_type):
342 body = make_body(1, pymunk.inf, position)
343 self.last_position = position
344 self.shape = pymunk.Circle(body, 2)
345 self.shape.sensor = True
346 self.shape.collision_type = COLLISION_TYPE_PROJECTILE
347 self.source_collision_type = source_collision_type
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)
353
354 def animate(self):
355 super(Bullet, self).animate()
356 position = (self.physicser.position.x, self.physicser.position.y)
357 r = self.get_space().segment_query(self.last_position, position)
358 self.last_position = position
359 for collision in r:
360 if (collision.shape.collision_type == self.source_collision_type
361 or collision.shape == self.physicser.get_shape()
362 or collision.shape.sensor):
363 continue
364 print "Hit", collision.shape.collision_type
365 self.physicser.remove_from_space()
366 self.remove = True
367 break
368
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.