source: nagslang/game_object.py@ 333:3dd32686dbc3

Last change on this file since 333:3dd32686dbc3 was 333:3dd32686dbc3, checked in by Jeremy Thurgood <firxen@…>, 8 years ago

Better wolf claw attack.

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