1 | import pymunk |
---|
2 | import pymunk.pygame_util |
---|
3 | |
---|
4 | import math |
---|
5 | |
---|
6 | from nagslang import environment |
---|
7 | from nagslang import puzzle |
---|
8 | from nagslang import render |
---|
9 | from nagslang.mutators import FLIP_H, ImageOverlay |
---|
10 | from 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, ZORDER_HIGH) |
---|
14 | from nagslang.resources import resources |
---|
15 | from nagslang.events import DoorEvent |
---|
16 | |
---|
17 | |
---|
18 | class Result(object): |
---|
19 | ''' |
---|
20 | Return from an update() function, to add new objects to the world, and/or |
---|
21 | remove old objects. |
---|
22 | ''' |
---|
23 | def __init__(self, add=(), remove=()): |
---|
24 | self.add = add |
---|
25 | self.remove = remove |
---|
26 | |
---|
27 | def merge(self, result): |
---|
28 | if result is not None: |
---|
29 | self.add += result.add |
---|
30 | self.remove += result.remove |
---|
31 | return self |
---|
32 | |
---|
33 | |
---|
34 | def get_editable_game_objects(): |
---|
35 | classes = [] |
---|
36 | for cls_name, cls in globals().iteritems(): |
---|
37 | if isinstance(cls, type) and hasattr(cls, 'requires'): |
---|
38 | classes.append((cls_name, cls)) |
---|
39 | return classes |
---|
40 | |
---|
41 | |
---|
42 | class Physicser(object): |
---|
43 | def __init__(self, space): |
---|
44 | self._space = space |
---|
45 | |
---|
46 | def get_space(self): |
---|
47 | return self._space |
---|
48 | |
---|
49 | def set_space(self, new_space): |
---|
50 | self._space = new_space |
---|
51 | |
---|
52 | def set_game_object(self, game_object): |
---|
53 | self.game_object = game_object |
---|
54 | |
---|
55 | def get_shape(self): |
---|
56 | raise NotImplementedError() |
---|
57 | |
---|
58 | def add_to_space(self): |
---|
59 | shape = self.get_shape() |
---|
60 | self.get_space().add(shape) |
---|
61 | if not shape.body.is_static: |
---|
62 | self.get_space().add(shape.body) |
---|
63 | |
---|
64 | def remove_from_space(self): |
---|
65 | shape = self.get_shape() |
---|
66 | self.get_space().remove(shape) |
---|
67 | if not shape.body.is_static: |
---|
68 | self.get_space().remove(shape.body) |
---|
69 | |
---|
70 | def get_render_position(self, surface): |
---|
71 | pos = self.get_shape().body.position |
---|
72 | return pymunk.pygame_util.to_pygame(pos, surface) |
---|
73 | |
---|
74 | def get_angle(self): |
---|
75 | return self.get_shape().body.angle |
---|
76 | |
---|
77 | def get_velocity(self): |
---|
78 | return self.get_shape().body.velocity |
---|
79 | |
---|
80 | def _get_position(self): |
---|
81 | return self.get_shape().body.position |
---|
82 | |
---|
83 | def _set_position(self, position): |
---|
84 | self.get_shape().body.position = position |
---|
85 | |
---|
86 | position = property(_get_position, _set_position) |
---|
87 | |
---|
88 | def apply_impulse(self, j, r=(0, 0)): |
---|
89 | return self.get_shape().body.apply_impulse(j, r) |
---|
90 | |
---|
91 | |
---|
92 | class SingleShapePhysicser(Physicser): |
---|
93 | def __init__(self, space, shape): |
---|
94 | super(SingleShapePhysicser, self).__init__(space) |
---|
95 | self._shape = shape |
---|
96 | shape.physicser = self |
---|
97 | |
---|
98 | def get_shape(self): |
---|
99 | return self._shape |
---|
100 | |
---|
101 | |
---|
102 | def damping_velocity_func(body, gravity, damping, dt): |
---|
103 | """Apply custom damping to this body's velocity. |
---|
104 | """ |
---|
105 | damping = getattr(body, 'damping', damping) |
---|
106 | return pymunk.Body.update_velocity(body, gravity, damping, dt) |
---|
107 | |
---|
108 | |
---|
109 | def make_body(mass, moment, position, damping=None): |
---|
110 | body = pymunk.Body(mass, moment) |
---|
111 | body.position = tuple(position) |
---|
112 | if damping is not None: |
---|
113 | body.damping = damping |
---|
114 | body.velocity_func = damping_velocity_func |
---|
115 | return body |
---|
116 | |
---|
117 | |
---|
118 | class GameObject(object): |
---|
119 | """A representation of a thing in the game world. |
---|
120 | |
---|
121 | This has a rendery thing, physicsy things and maybe some other things. |
---|
122 | """ |
---|
123 | |
---|
124 | zorder = ZORDER_LOW |
---|
125 | is_moving = False # `True` if a movement animation should play. |
---|
126 | |
---|
127 | def __init__(self, physicser, renderer, puzzler=None, overlay=None, |
---|
128 | interactible=None): |
---|
129 | self.lifetime = 0 |
---|
130 | self.physicser = physicser |
---|
131 | physicser.set_game_object(self) |
---|
132 | self.physicser.add_to_space() |
---|
133 | self.renderer = renderer |
---|
134 | renderer.set_game_object(self) |
---|
135 | self.puzzler = puzzler |
---|
136 | if puzzler is not None: |
---|
137 | puzzler.set_game_object(self) |
---|
138 | self.overlay = overlay |
---|
139 | if overlay is not None: |
---|
140 | self.overlay.set_game_object(self) |
---|
141 | self.interactible = interactible |
---|
142 | if interactible is not None: |
---|
143 | self.interactible.set_game_object(self) |
---|
144 | self._timers = {} |
---|
145 | self._active_timers = {} |
---|
146 | |
---|
147 | def add_timer(self, name, secs): |
---|
148 | self._timers[name] = secs |
---|
149 | |
---|
150 | def start_timer(self, name, secs=None): |
---|
151 | if secs is None: |
---|
152 | secs = self._timers[name] |
---|
153 | self._active_timers[name] = secs |
---|
154 | |
---|
155 | def check_timer(self, name): |
---|
156 | return name in self._active_timers |
---|
157 | |
---|
158 | def set_stored_state_dict(self, stored_state): |
---|
159 | """Override this to set up whatever state storage you want. |
---|
160 | |
---|
161 | The `stored_state` dict passed in contains whatever saved state we |
---|
162 | might have for this object. If the return value of this method |
---|
163 | evaluates to `True`, the contents of the `stored_state` dict will be |
---|
164 | saved, otherwise it will be discarded. |
---|
165 | """ |
---|
166 | pass |
---|
167 | |
---|
168 | def get_space(self): |
---|
169 | return self.physicser.get_space() |
---|
170 | |
---|
171 | def get_shape(self): |
---|
172 | return self.physicser.get_shape() |
---|
173 | |
---|
174 | def get_render_position(self, surface): |
---|
175 | return self.physicser.get_render_position(surface) |
---|
176 | |
---|
177 | def get_render_angle(self): |
---|
178 | return self.physicser.get_angle() |
---|
179 | |
---|
180 | def get_facing_direction(self): |
---|
181 | """Used by rendererd that care what direction an object is facing. |
---|
182 | """ |
---|
183 | return None |
---|
184 | |
---|
185 | def render(self, surface): |
---|
186 | return self.renderer.render(surface) |
---|
187 | |
---|
188 | def update(self, dt): |
---|
189 | self.lifetime += dt |
---|
190 | for timer in self._active_timers.keys(): |
---|
191 | self._active_timers[timer] -= dt |
---|
192 | if self._active_timers[timer] <= 0: |
---|
193 | self._active_timers.pop(timer) |
---|
194 | self.renderer.update(dt) |
---|
195 | |
---|
196 | def hit(self, weapon): |
---|
197 | '''Was hit with a weapon (such as a bullet)''' |
---|
198 | pass |
---|
199 | |
---|
200 | def collide_with_protagonist(self, protagonist): |
---|
201 | """Called as a `pre_solve` collision callback with the protagonist. |
---|
202 | |
---|
203 | You can return `False` to ignore the collision, anything else |
---|
204 | (including `None`) to process the collision as normal. |
---|
205 | """ |
---|
206 | return True |
---|
207 | |
---|
208 | def collide_with_furniture(self, furniture): |
---|
209 | return True |
---|
210 | |
---|
211 | def collide_with_claw_attack(self, claw_attack): |
---|
212 | return True |
---|
213 | |
---|
214 | @classmethod |
---|
215 | def requires(cls): |
---|
216 | """Hints for the level editor""" |
---|
217 | return [("name", "string")] |
---|
218 | |
---|
219 | |
---|
220 | class FloorSwitch(GameObject): |
---|
221 | zorder = ZORDER_FLOOR |
---|
222 | |
---|
223 | def __init__(self, space, position): |
---|
224 | body = make_body(None, None, position) |
---|
225 | self.shape = pymunk.Circle(body, 30) |
---|
226 | self.shape.collision_type = COLLISION_TYPE_SWITCH |
---|
227 | self.shape.sensor = True |
---|
228 | super(FloorSwitch, self).__init__( |
---|
229 | SingleShapePhysicser(space, self.shape), |
---|
230 | render.ImageStateRenderer({ |
---|
231 | True: resources.get_image('objects', 'sensor_on.png'), |
---|
232 | False: resources.get_image('objects', 'sensor_off.png'), |
---|
233 | }), |
---|
234 | puzzle.CollidePuzzler(*SWITCH_PUSHERS), |
---|
235 | ) |
---|
236 | |
---|
237 | @classmethod |
---|
238 | def requires(cls): |
---|
239 | return [("name", "string"), ("position", "coordinates")] |
---|
240 | |
---|
241 | |
---|
242 | class Note(GameObject): |
---|
243 | zorder = ZORDER_FLOOR |
---|
244 | |
---|
245 | def __init__(self, space, position, message): |
---|
246 | body = make_body(None, None, position) |
---|
247 | self.shape = pymunk.Circle(body, 30) |
---|
248 | self.shape.sensor = True |
---|
249 | super(Note, self).__init__( |
---|
250 | SingleShapePhysicser(space, self.shape), |
---|
251 | render.ImageRenderer(resources.get_image('objects', 'note.png')), |
---|
252 | puzzle.CollidePuzzler(), |
---|
253 | render.TextOverlay(message), |
---|
254 | ) |
---|
255 | |
---|
256 | @classmethod |
---|
257 | def requires(cls): |
---|
258 | return [("name", "string"), ("position", "coordinates"), |
---|
259 | ("message", "text")] |
---|
260 | |
---|
261 | |
---|
262 | class FloorLight(GameObject): |
---|
263 | zorder = ZORDER_FLOOR |
---|
264 | |
---|
265 | def __init__(self, space, position, state_source): |
---|
266 | body = make_body(None, None, position) |
---|
267 | self.shape = pymunk.Circle(body, 10) |
---|
268 | self.shape.collision_type = COLLISION_TYPE_SWITCH |
---|
269 | self.shape.sensor = True |
---|
270 | super(FloorLight, self).__init__( |
---|
271 | SingleShapePhysicser(space, self.shape), |
---|
272 | render.ImageStateRenderer({ |
---|
273 | True: resources.get_image('objects', 'light_on.png'), |
---|
274 | False: resources.get_image('objects', 'light_off.png'), |
---|
275 | }), |
---|
276 | puzzle.StateProxyPuzzler(state_source), |
---|
277 | ) |
---|
278 | |
---|
279 | @classmethod |
---|
280 | def requires(cls): |
---|
281 | return [("name", "string"), ("position", "coordinates"), |
---|
282 | ("state_source", "puzzler")] |
---|
283 | |
---|
284 | |
---|
285 | class Box(GameObject): |
---|
286 | def __init__(self, space, position): |
---|
287 | body = make_body(10, 10000, position, damping=0.5) |
---|
288 | self.shape = pymunk.Poly( |
---|
289 | body, [(-20, -20), (20, -20), (20, 20), (-20, 20)]) |
---|
290 | self.shape.friction = 0.5 |
---|
291 | self.shape.collision_type = COLLISION_TYPE_FURNITURE |
---|
292 | super(Box, self).__init__( |
---|
293 | SingleShapePhysicser(space, self.shape), |
---|
294 | render.ImageRenderer(resources.get_image('objects', 'crate.png')), |
---|
295 | ) |
---|
296 | |
---|
297 | @classmethod |
---|
298 | def requires(cls): |
---|
299 | return [("name", "string"), ("position", "coordinates"), |
---|
300 | ("state_source", "puzzler")] |
---|
301 | |
---|
302 | |
---|
303 | class BaseDoor(GameObject): |
---|
304 | zorder = ZORDER_FLOOR |
---|
305 | is_open = True |
---|
306 | |
---|
307 | def __init__(self, space, position, destination, dest_pos, angle, |
---|
308 | renderer, condition): |
---|
309 | body = make_body(pymunk.inf, pymunk.inf, position, damping=0.5) |
---|
310 | self.shape = pymunk.Circle(body, 30) |
---|
311 | self.shape.collision_type = COLLISION_TYPE_DOOR |
---|
312 | self.shape.body.angle = float(angle) / 180 * math.pi |
---|
313 | self.shape.sensor = True |
---|
314 | self.destination = destination |
---|
315 | self.dest_pos = tuple(dest_pos) |
---|
316 | super(BaseDoor, self).__init__( |
---|
317 | SingleShapePhysicser(space, self.shape), |
---|
318 | renderer, |
---|
319 | puzzle.ParentAttrPuzzler('is_open'), |
---|
320 | interactible=environment.Interactible( |
---|
321 | environment.Action(self._post_door_event, condition)), |
---|
322 | ) |
---|
323 | |
---|
324 | def _post_door_event(self, protagonist): |
---|
325 | DoorEvent.post(self.destination, self.dest_pos) |
---|
326 | |
---|
327 | |
---|
328 | class Door(BaseDoor): |
---|
329 | def __init__(self, space, position, destination, dest_pos, angle): |
---|
330 | super(Door, self).__init__( |
---|
331 | space, position, destination, dest_pos, angle, |
---|
332 | render.ImageRenderer(resources.get_image('objects', 'door.png')), |
---|
333 | environment.YesCondition(), |
---|
334 | ) |
---|
335 | |
---|
336 | @classmethod |
---|
337 | def requires(cls): |
---|
338 | return [("name", "string"), ("position", "coordinates"), |
---|
339 | ("destination", "level name"), ("dest_pos", "coordinate"), |
---|
340 | ("angle", "degrees")] |
---|
341 | |
---|
342 | |
---|
343 | class PuzzleDoor(BaseDoor): |
---|
344 | def __init__(self, space, position, destination, dest_pos, angle, |
---|
345 | key_state): |
---|
346 | self._key_state = key_state |
---|
347 | overlay = ImageOverlay(resources.get_image('objects', 'lock.png')) |
---|
348 | super(PuzzleDoor, self).__init__( |
---|
349 | space, position, destination, dest_pos, angle, |
---|
350 | render.ImageStateRenderer({ |
---|
351 | True: resources.get_image('objects', 'door.png'), |
---|
352 | False: resources.get_image( |
---|
353 | 'objects', 'door.png', transforms=(overlay,)), |
---|
354 | }), |
---|
355 | environment.FunctionCondition(lambda p: self.is_open), |
---|
356 | ) |
---|
357 | |
---|
358 | @property |
---|
359 | def is_open(self): |
---|
360 | return self._stored_state['is_open'] |
---|
361 | |
---|
362 | def set_stored_state_dict(self, stored_state): |
---|
363 | self._stored_state = stored_state |
---|
364 | self._stored_state.setdefault('is_open', False) |
---|
365 | return True |
---|
366 | |
---|
367 | def update(self, dt): |
---|
368 | if not self.is_open: |
---|
369 | self._stored_state['is_open'] = self.puzzler.glue.get_state_of( |
---|
370 | self._key_state) |
---|
371 | super(PuzzleDoor, self).update(dt) |
---|
372 | |
---|
373 | @classmethod |
---|
374 | def requires(cls): |
---|
375 | return [("name", "string"), ("position", "coordinates"), |
---|
376 | ("destination", "level name"), ("dest_pos", "coordinate"), |
---|
377 | ("angle", "degrees"), |
---|
378 | ("key_state", "puzzler")] |
---|
379 | |
---|
380 | |
---|
381 | class KeyedDoor(BaseDoor): |
---|
382 | def __init__(self, space, position, destination, dest_pos, angle, |
---|
383 | key_item=None): |
---|
384 | self._key_item = key_item |
---|
385 | overlay = ImageOverlay( |
---|
386 | resources.get_image('objects', '%s.png' % (key_item,))) |
---|
387 | super(KeyedDoor, self).__init__( |
---|
388 | space, position, destination, dest_pos, angle, |
---|
389 | render.ImageRenderer(resources.get_image( |
---|
390 | 'objects', 'door.png', transforms=(overlay,))), |
---|
391 | environment.ItemRequiredCondition(key_item), |
---|
392 | ) |
---|
393 | |
---|
394 | @classmethod |
---|
395 | def requires(cls): |
---|
396 | return [("name", "string"), ("position", "coordinates"), |
---|
397 | ("destination", "level name"), ("dest_pos", "coordinate"), |
---|
398 | ("angle", "degrees"), ("key_item", "item name")] |
---|
399 | |
---|
400 | |
---|
401 | class Bulkhead(GameObject): |
---|
402 | zorder = ZORDER_FLOOR |
---|
403 | |
---|
404 | def __init__(self, space, end1, end2, key_state=None): |
---|
405 | body = make_body(None, None, (0, 0)) |
---|
406 | self.shape = pymunk.Segment(body, tuple(end1), tuple(end2), 3) |
---|
407 | self.shape.collision_type = COLLISION_TYPE_DOOR |
---|
408 | if key_state is None: |
---|
409 | puzzler = puzzle.YesPuzzler() |
---|
410 | else: |
---|
411 | puzzler = puzzle.StateProxyPuzzler(key_state) |
---|
412 | super(Bulkhead, self).__init__( |
---|
413 | SingleShapePhysicser(space, self.shape), |
---|
414 | render.ShapeStateRenderer(), |
---|
415 | puzzler, |
---|
416 | ) |
---|
417 | |
---|
418 | def collide_with_protagonist(self, protagonist): |
---|
419 | if self.puzzler.get_state(): |
---|
420 | # Reject the collision, we can walk through. |
---|
421 | return False |
---|
422 | return True |
---|
423 | |
---|
424 | collide_with_furniture = collide_with_protagonist |
---|
425 | |
---|
426 | @classmethod |
---|
427 | def requires(cls): |
---|
428 | return [("name", "string"), ("end1", "coordinates"), |
---|
429 | ("end2", "coordinates"), ("key_state", "puzzler")] |
---|
430 | |
---|
431 | |
---|
432 | class ToggleSwitch(GameObject): |
---|
433 | zorder = ZORDER_LOW |
---|
434 | |
---|
435 | def __init__(self, space, position): |
---|
436 | body = make_body(None, None, position) |
---|
437 | self.shape = pymunk.Circle(body, 20) |
---|
438 | self.shape.sensor = True |
---|
439 | super(ToggleSwitch, self).__init__( |
---|
440 | SingleShapePhysicser(space, self.shape), |
---|
441 | render.ImageStateRenderer({ |
---|
442 | True: resources.get_image('objects', 'lever.png'), |
---|
443 | False: resources.get_image( |
---|
444 | 'objects', 'lever.png', transforms=(FLIP_H,)), |
---|
445 | }), |
---|
446 | puzzle.ParentAttrPuzzler('toggle_on'), |
---|
447 | interactible=environment.Interactible( |
---|
448 | environment.Action(self._toggle)), |
---|
449 | ) |
---|
450 | |
---|
451 | @property |
---|
452 | def toggle_on(self): |
---|
453 | return self._stored_state['toggle_on'] |
---|
454 | |
---|
455 | def _toggle(self, protagonist): |
---|
456 | self._stored_state['toggle_on'] = not self.toggle_on |
---|
457 | |
---|
458 | def set_stored_state_dict(self, stored_state): |
---|
459 | self._stored_state = stored_state |
---|
460 | # We start in the "off" position. |
---|
461 | self._stored_state.setdefault('toggle_on', False) |
---|
462 | return True |
---|
463 | |
---|
464 | @classmethod |
---|
465 | def requires(cls): |
---|
466 | return [("name", "string"), ("position", "coordinates")] |
---|
467 | |
---|
468 | |
---|
469 | class Bullet(GameObject): |
---|
470 | def __init__(self, space, position, impulse, damage, bullet_type, |
---|
471 | source_collision_type): |
---|
472 | body = make_body(1, pymunk.inf, position) |
---|
473 | body.angle = impulse.angle |
---|
474 | self.last_position = position |
---|
475 | self.shape = pymunk.Circle(body, 2) |
---|
476 | self.shape.sensor = True |
---|
477 | self.shape.collision_type = COLLISION_TYPE_PROJECTILE |
---|
478 | self.damage = damage |
---|
479 | self.type = bullet_type |
---|
480 | self.source_collision_type = source_collision_type |
---|
481 | super(Bullet, self).__init__( |
---|
482 | SingleShapePhysicser(space, self.shape), |
---|
483 | render.ImageRenderer(resources.get_image( |
---|
484 | 'objects', '%s.png' % self.type)), |
---|
485 | ) |
---|
486 | self.physicser.apply_impulse(impulse) |
---|
487 | |
---|
488 | def update(self, dt): |
---|
489 | super(Bullet, self).update(dt) |
---|
490 | position = (self.physicser.position.x, self.physicser.position.y) |
---|
491 | r = self.get_space().segment_query(self.last_position, position) |
---|
492 | self.last_position = position |
---|
493 | for collision in r: |
---|
494 | shape = collision.shape |
---|
495 | if (shape.collision_type == self.source_collision_type |
---|
496 | or shape == self.physicser.get_shape() |
---|
497 | or shape.sensor): |
---|
498 | continue |
---|
499 | if hasattr(shape, 'physicser'): |
---|
500 | shape.physicser.game_object.hit(self) |
---|
501 | self.physicser.remove_from_space() |
---|
502 | return Result(remove=[self]) |
---|
503 | |
---|
504 | |
---|
505 | class ClawAttack(GameObject): |
---|
506 | def __init__(self, space, pos, vector, damage): |
---|
507 | body = make_body(1, pymunk.inf, |
---|
508 | (pos[0] + (vector.length * math.cos(vector.angle)), |
---|
509 | pos[1] + (vector.length * math.sin(vector.angle)))) |
---|
510 | body.angle = vector.angle |
---|
511 | self.shape = pymunk.Circle(body, 30) |
---|
512 | self.shape.sensor = True |
---|
513 | self.shape.collision_type = COLLISION_TYPE_WEREWOLF_ATTACK |
---|
514 | self.damage = damage |
---|
515 | super(ClawAttack, self).__init__( |
---|
516 | SingleShapePhysicser(space, self.shape), |
---|
517 | render.ImageRenderer(resources.get_image( |
---|
518 | 'objects', 'werewolf_SW_claw_attack.png', |
---|
519 | transforms=(FLIP_H,))), |
---|
520 | ) |
---|
521 | |
---|
522 | def update(self, dt): |
---|
523 | super(ClawAttack, self).update(dt) |
---|
524 | if self.lifetime > 0.1: |
---|
525 | self.physicser.remove_from_space() |
---|
526 | return Result(remove=[self]) |
---|
527 | |
---|
528 | |
---|
529 | class HostileTerrain(GameObject): |
---|
530 | zorder = ZORDER_FLOOR |
---|
531 | damage = None |
---|
532 | tile = None |
---|
533 | tile_alpha = 255 |
---|
534 | # How often to hit the player |
---|
535 | rate = 5 |
---|
536 | |
---|
537 | def __init__(self, space, position, outline): |
---|
538 | body = make_body(10, pymunk.inf, position) |
---|
539 | # Adjust shape relative to position |
---|
540 | shape_outline = [(p[0] - position[0], p[1] - position[1]) for |
---|
541 | p in outline] |
---|
542 | self.shape = pymunk.Poly(body, shape_outline) |
---|
543 | self._ticks = 0 |
---|
544 | self.shape.collision_type = COLLISION_TYPE_SWITCH |
---|
545 | self.shape.sensor = True |
---|
546 | super(HostileTerrain, self).__init__( |
---|
547 | SingleShapePhysicser(space, self.shape), |
---|
548 | render.TiledRenderer(outline, |
---|
549 | resources.get_image('tiles', self.tile), |
---|
550 | self.tile_alpha)) |
---|
551 | |
---|
552 | def collide_with_protagonist(self, protagonist): |
---|
553 | # We're called every frame we're colliding, so |
---|
554 | # There are timing issues with stepping on and |
---|
555 | # off terrian, but as long as the rate is reasonably |
---|
556 | # low, they shouldn't impact gameplay |
---|
557 | if self._ticks == 0: |
---|
558 | self.apply_effect(protagonist) |
---|
559 | self._ticks += 1 |
---|
560 | if self._ticks > self.rate: |
---|
561 | self._ticks = 0 |
---|
562 | |
---|
563 | def apply_effect(self, protagonist): |
---|
564 | protagonist.lose_health(self.damage) |
---|
565 | |
---|
566 | @classmethod |
---|
567 | def requires(cls): |
---|
568 | return [("name", "string"), ("position", "coordinates"), |
---|
569 | ("outline", "polygon (convex)")] |
---|
570 | |
---|
571 | |
---|
572 | class AcidFloor(HostileTerrain): |
---|
573 | damage = 1 |
---|
574 | tile = 'acid.png' |
---|
575 | tile_alpha = 200 |
---|
576 | |
---|
577 | |
---|
578 | class ForceWolfFloor(HostileTerrain): |
---|
579 | tile = 'moonlight.png' |
---|
580 | rate = 0 |
---|
581 | tile_alpha = 150 |
---|
582 | zorder = ZORDER_HIGH |
---|
583 | |
---|
584 | def apply_effect(self, protagonist): |
---|
585 | protagonist.force_wolf_form() |
---|