source: nagslang/game_object.py@ 281:9b56e954c674

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

Protagonist actions, now required for operating doors.

File size: 10.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.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
126 def get_space(self):
127 return self.physicser.get_space()
128
129 def get_shape(self):
130 return self.physicser.get_shape()
131
132 def get_render_position(self, surface):
133 return self.physicser.get_render_position(surface)
134
135 def get_render_angle(self):
136 return self.physicser.get_angle()
137
138 def get_facing_direction(self):
139 """Used by rendererd that care what direction an object is facing.
140 """
141 return None
142
143 def render(self, surface):
144 return self.renderer.render(surface)
145
146 def animate(self):
147 self.renderer.animate()
148
149 def collide_with_protagonist(self, protagonist):
150 """Called as a `pre_solve` collision callback with the protagonist.
151
152 You can return `False` to ignore the collision, anything else
153 (including `None`) to process the collision as normal.
154 """
155 return True
156
157 @classmethod
158 def requires(cls):
159 """Hints for the level editor"""
160 return [("name", "string")]
161
162
163class FloorSwitch(GameObject):
164 zorder = ZORDER_FLOOR
165
166 def __init__(self, space, position):
167 body = make_body(None, None, position)
168 self.shape = pymunk.Circle(body, 30)
169 self.shape.collision_type = COLLISION_TYPE_SWITCH
170 self.shape.sensor = True
171 super(FloorSwitch, self).__init__(
172 SingleShapePhysicser(space, self.shape),
173 render.ImageStateRenderer({
174 True: resources.get_image('objects', 'sensor_on.png'),
175 False: resources.get_image('objects', 'sensor_off.png'),
176 }),
177 puzzle.CollidePuzzler(*SWITCH_PUSHERS),
178 )
179
180 @classmethod
181 def requires(cls):
182 return [("name", "string"), ("position", "coordinates")]
183
184
185class Note(GameObject):
186 zorder = ZORDER_FLOOR
187
188 def __init__(self, space, position, message):
189 body = make_body(None, None, position)
190 self.shape = pymunk.Circle(body, 30)
191 self.shape.sensor = True
192 super(Note, self).__init__(
193 SingleShapePhysicser(space, self.shape),
194 render.ImageRenderer(resources.get_image('objects', 'note.png')),
195 puzzle.CollidePuzzler(),
196 render.TextOverlay(message),
197 )
198
199 @classmethod
200 def requires(cls):
201 return [("name", "string"), ("position", "coordinates"),
202 ("message", "text")]
203
204
205class FloorLight(GameObject):
206 zorder = ZORDER_FLOOR
207
208 def __init__(self, space, position, state_source):
209 body = make_body(None, None, position)
210 self.shape = pymunk.Circle(body, 10)
211 self.shape.collision_type = COLLISION_TYPE_SWITCH
212 self.shape.sensor = True
213 super(FloorLight, self).__init__(
214 SingleShapePhysicser(space, self.shape),
215 render.ImageStateRenderer({
216 True: resources.get_image('objects', 'light_on.png'),
217 False: resources.get_image('objects', 'light_off.png'),
218 }),
219 puzzle.StateProxyPuzzler(state_source),
220 )
221
222 @classmethod
223 def requires(cls):
224 return [("name", "string"), ("position", "coordinates"),
225 ("state_source", "puzzler")]
226
227
228class Box(GameObject):
229 def __init__(self, space, position):
230 body = make_body(10, 10000, position, damping=0.5)
231 self.shape = pymunk.Poly(
232 body, [(-20, -20), (20, -20), (20, 20), (-20, 20)])
233 self.shape.friction = 0.5
234 self.shape.collision_type = COLLISION_TYPE_BOX
235 super(Box, self).__init__(
236 SingleShapePhysicser(space, self.shape),
237 render.ImageRenderer(resources.get_image('objects', 'crate.png')),
238 )
239
240 @classmethod
241 def requires(cls):
242 return [("name", "string"), ("position", "coordinates"),
243 ("state_source", "puzzler")]
244
245
246class Door(GameObject):
247 zorder = ZORDER_FLOOR
248
249 def __init__(self, space, position, destination, dest_pos, angle,
250 key_state=None):
251 body = make_body(pymunk.inf, pymunk.inf, position, damping=0.5)
252 self.shape = pymunk.Circle(body, 30)
253 self.shape.collision_type = COLLISION_TYPE_DOOR
254 self.shape.body.angle = float(angle) / 180 * math.pi
255 self.shape.sensor = True
256 self.destination = destination
257 self.dest_pos = tuple(dest_pos)
258 puzzler = None
259 action = environment.Action(self._post_door_event)
260 if key_state is not None:
261 puzzler = puzzle.StateProxyPuzzler(key_state)
262 action.condition = environment.PuzzleStateCondition(puzzler)
263 super(Door, self).__init__(
264 SingleShapePhysicser(space, self.shape),
265 render.ImageRenderer(resources.get_image('objects', 'door.png')),
266 puzzler,
267 interactible=environment.Interactible(action),
268 )
269
270 def _post_door_event(self, protagonist):
271 DoorEvent.post(self.destination, self.dest_pos)
272
273 @classmethod
274 def requires(cls):
275 return [("name", "string"), ("position", "coordinates"),
276 ("destination", "level name"), ("dest_pos", "coordinate"),
277 ("angle", "degrees"),
278 ("key_state", "puzzler (optional)")]
279
280
281class Bulkhead(GameObject):
282 zorder = ZORDER_FLOOR
283
284 def __init__(self, space, end1, end2, key_state=None):
285 body = make_body(None, None, (0, 0))
286 self.shape = pymunk.Segment(body, tuple(end1), tuple(end2), 3)
287 self.shape.collision_type = COLLISION_TYPE_DOOR
288 if key_state is None:
289 puzzler = puzzle.YesPuzzler()
290 else:
291 puzzler = puzzle.StateProxyPuzzler(key_state)
292 super(Bulkhead, self).__init__(
293 SingleShapePhysicser(space, self.shape),
294 render.ShapeStateRenderer(),
295 puzzler,
296 )
297
298 def collide_with_protagonist(self, protagonist):
299 if self.puzzler.get_state():
300 # Reject the collision, we can walk through.
301 return False
302 return True
303
304 @classmethod
305 def requires(cls):
306 return [("name", "string"), ("end1", "coordinates"),
307 ("end2", "coordinates"), ("key_state", "puzzler")]
308
309
310class Bullet(GameObject):
311 def __init__(self, space, position, impulse):
312 body = make_body(1, pymunk.inf, position)
313 self.shape = pymunk.Circle(body, 1)
314 self.shape.collision_type = COLLISION_TYPE_PROJECTILE
315 super(Bullet, self).__init__(
316 SingleShapePhysicser(space, self.shape),
317 render.ImageRenderer(resources.get_image('objects', 'bullet.png')),
318 )
319 self.physicser.apply_impulse(impulse)
Note: See TracBrowser for help on using the repository browser.