source: nagslang/game_object.py@ 293:47226c661ae2

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

Bullets that mostly die when they hit things

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