Mercurial > nagslang
view nagslang/game_object.py @ 647:aeb366d97774
Show splash image on startup
author | Stefano Rivera <stefano@rivera.za.net> |
---|---|
date | Sun, 08 Sep 2013 02:02:09 +0200 |
parents | db960388b912 |
children | baacd0462d8e |
line wrap: on
line source
import pymunk import pymunk.pygame_util import math from nagslang import environment from nagslang import puzzle from nagslang import render from nagslang.mutators import FLIP_H, ImageOverlay, rotator, scaler from nagslang.constants import ( COLLISION_TYPE_DOOR, COLLISION_TYPE_FURNITURE, COLLISION_TYPE_PROJECTILE, COLLISION_TYPE_SWITCH, COLLISION_TYPE_SHEEP, COLLISION_TYPE_SHEEP_PEN, COLLISION_TYPE_WEREWOLF_ATTACK, SWITCH_PUSHERS, ZORDER_FLOOR, ZORDER_LOW, ZORDER_HIGH) from nagslang.resources import resources from nagslang.events import DoorEvent, QuitEvent from nagslang.sound import sound class Result(object): ''' Return from an update() function, to add new objects to the world, and/or remove old objects. ''' def __init__(self, add=(), remove=()): self.add = add self.remove = remove def merge(self, result): if result is not None: self.add += result.add self.remove += result.remove return self def get_editable_game_objects(): classes = [] for cls_name, cls in globals().iteritems(): if isinstance(cls, type) and hasattr(cls, 'requires'): classes.append((cls_name, cls)) return classes class Physicser(object): def __init__(self, space): self._space = space def get_space(self): return self._space def set_space(self, new_space): self._space = new_space def set_game_object(self, game_object): self.game_object = game_object def get_shape(self): raise NotImplementedError() def add_to_space(self): shape = self.get_shape() self.get_space().add(shape) if not shape.body.is_static: self.get_space().add(shape.body) def remove_from_space(self): shape = self.get_shape() self.get_space().remove(shape) if not shape.body.is_static: self.get_space().remove(shape.body) def get_render_position(self, surface): pos = self.get_shape().body.position return pymunk.pygame_util.to_pygame(pos, surface) def get_angle(self): return self.get_shape().body.angle def get_velocity(self): return self.get_shape().body.velocity def _get_position(self): return self.get_shape().body.position def _set_position(self, position): self.get_shape().body.position = position position = property(_get_position, _set_position) def apply_impulse(self, j, r=(0, 0)): return self.get_shape().body.apply_impulse(j, r) class SingleShapePhysicser(Physicser): def __init__(self, space, shape): super(SingleShapePhysicser, self).__init__(space) self._shape = shape shape.physicser = self def get_shape(self): return self._shape class MultiShapePhysicser(Physicser): def __init__(self, space, shape, *extra_shapes): super(MultiShapePhysicser, self).__init__(space) self._shape = shape self._extra_shapes = extra_shapes shape.physicser = self for es in extra_shapes: es.physicser = self def get_shape(self): return self._shape def add_to_space(self): shape = self.get_shape() self.get_space().add(shape) if not shape.body.is_static: self.get_space().add(shape.body) for s in self._extra_shapes: self.get_space().add(s) def remove_from_space(self): shape = self.get_shape() self.get_space().remove(shape) if not shape.body.is_static: self.get_space().remove(shape.body) for s in self._extra_shapes: self.get_space().remove(s) def damping_velocity_func(body, gravity, damping, dt): """Apply custom damping to this body's velocity. """ damping = getattr(body, 'damping', damping) return pymunk.Body.update_velocity(body, gravity, damping, dt) def make_body(mass, moment, position, damping=None): body = pymunk.Body(mass, moment) body.position = tuple(position) if damping is not None: body.damping = damping body.velocity_func = damping_velocity_func return body class GameObject(object): """A representation of a thing in the game world. This has a rendery thing, physicsy things and maybe some other things. """ zorder = ZORDER_LOW is_moving = False # `True` if a movement animation should play. def __init__(self, physicser, renderer, puzzler=None, overlay=None, interactible=None): self.lifetime = 0 self.physicser = physicser if physicser is not None: physicser.set_game_object(self) self.physicser.add_to_space() self.renderer = renderer renderer.set_game_object(self) self.puzzler = puzzler if puzzler is not None: puzzler.set_game_object(self) self.overlay = overlay if overlay is not None: self.overlay.set_game_object(self) self.interactible = interactible if interactible is not None: self.interactible.set_game_object(self) self._timers = {} self._active_timers = {} def add_timer(self, name, secs): self._timers[name] = secs def start_timer(self, name, secs=None): if secs is None: secs = self._timers[name] self._active_timers[name] = secs def check_timer(self, name): return name in self._active_timers def set_stored_state_dict(self, stored_state): """Override this to set up whatever state storage you want. The `stored_state` dict passed in contains whatever saved state we might have for this object. If the return value of this method evaluates to `True`, the contents of the `stored_state` dict will be saved, otherwise it will be discarded. """ pass def get_space(self): return self.physicser.get_space() def get_shape(self): return self.physicser.get_shape() def get_render_position(self, surface): return self.physicser.get_render_position(surface) def get_render_angle(self): return self.physicser.get_angle() def get_facing_direction(self): """Used by rendererd that care what direction an object is facing. """ return None def render(self, surface): return self.renderer.render(surface) def update(self, dt): self.lifetime += dt for timer in self._active_timers.keys(): self._active_timers[timer] -= dt if self._active_timers[timer] <= 0: self._active_timers.pop(timer) self.renderer.update(dt) def hit(self, weapon): '''Was hit with a weapon (such as a bullet)''' pass def collide_with_protagonist(self, protagonist): """Called as a `pre_solve` collision callback with the protagonist. You can return `False` to ignore the collision, anything else (including `None`) to process the collision as normal. """ return True def collide_with_furniture(self, furniture): return True def collide_with_claw_attack(self, claw_attack): return True def environmental_movement(self, vec): self.physicser.apply_impulse(vec) @classmethod def requires(cls): """Hints for the level editor""" return [("name", "string")] @classmethod def movable(cls): # Are we movable hints = cls.requires() for x in hints: if 'position' in x: return True return False class FloorSwitch(GameObject): zorder = ZORDER_FLOOR def __init__(self, space, position): body = make_body(None, None, position) self.shape = pymunk.Circle(body, 30) self.shape.collision_type = COLLISION_TYPE_SWITCH self.shape.sensor = True super(FloorSwitch, self).__init__( SingleShapePhysicser(space, self.shape), render.ImageStateRenderer({ True: resources.get_image('objects', 'sensor_on.png'), False: resources.get_image('objects', 'sensor_off.png'), }), puzzle.CollidePuzzler(*SWITCH_PUSHERS), ) @classmethod def requires(cls): return [("name", "string"), ("position", "coordinates")] class Note(GameObject): zorder = ZORDER_FLOOR def __init__(self, space, position, message): body = make_body(None, None, position) self.shape = pymunk.Circle(body, 30) self.shape.sensor = True super(Note, self).__init__( SingleShapePhysicser(space, self.shape), render.ImageRenderer(resources.get_image('objects', 'note.png')), puzzle.CollidePuzzler(), render.TextOverlay(message), ) @classmethod def requires(cls): return [("name", "string"), ("position", "coordinates"), ("message", "text")] class EphemeralNote(GameObject): def __init__(self, message, timeout, **kwargs): kwargs.setdefault('bg_colour', (255, 180, 180, 192)) super(EphemeralNote, self).__init__( None, render.NullRenderer(), puzzle.YesPuzzler(), render.TextOverlay(message, **kwargs), ) self.add_timer('timeout', timeout) self.start_timer('timeout') def update(self, dt): super(EphemeralNote, self).update(dt) if not self.check_timer('timeout'): return Result(remove=[self]) class SplashImage(GameObject): def __init__(self, image, timeout): super(SplashImage, self).__init__( None, render.NullRenderer(), puzzle.YesPuzzler(), render.ImageOverlay(image), ) self.add_timer('timeout', timeout) self.start_timer('timeout') def update(self, dt): super(SplashImage, self).update(dt) if not self.check_timer('timeout'): return Result(remove=[self]) class FloorLight(GameObject): zorder = ZORDER_FLOOR def __init__(self, space, position, state_source): body = make_body(None, None, position) self.shape = pymunk.Circle(body, 10) self.shape.collision_type = COLLISION_TYPE_SWITCH self.shape.sensor = True super(FloorLight, self).__init__( SingleShapePhysicser(space, self.shape), render.ImageStateRenderer({ True: resources.get_image('objects', 'light_on.png'), False: resources.get_image('objects', 'light_off.png'), }), puzzle.StateProxyPuzzler(state_source), ) @classmethod def requires(cls): return [("name", "string"), ("position", "coordinates"), ("state_source", "puzzler")] class Box(GameObject): def __init__(self, space, position): body = make_body(10, 10000, position, damping=0.5) self.shape = pymunk.Poly( body, [(-20, -20), (20, -20), (20, 20), (-20, 20)]) self.shape.friction = 0.5 self.shape.collision_type = COLLISION_TYPE_FURNITURE super(Box, self).__init__( SingleShapePhysicser(space, self.shape), render.ImageRenderer(resources.get_image('objects', 'crate.png')), ) @classmethod def requires(cls): return [("name", "string"), ("position", "coordinates"), ("state_source", "puzzler")] class SokoBox(GameObject): def __init__(self, space, position): body = make_body(5, pymunk.inf, position, 0.1) self.shape = pymunk.Poly( body, [(-40, -40), (40, -40), (40, 40), (-40, 40)]) self.shape.friction = 2.0 self.shape.collision_type = COLLISION_TYPE_FURNITURE super(SokoBox, self).__init__( SingleShapePhysicser(space, self.shape), render.ImageRenderer( resources.get_image('objects', 'sokobox.png')), ) @classmethod def requires(cls): return [("name", "string"), ("position", "coordinates"), ("state_source", "puzzler")] class BaseDoor(GameObject): zorder = ZORDER_FLOOR is_open = True def __init__(self, space, position, destination, dest_pos, angle, renderer, condition): body = make_body(pymunk.inf, pymunk.inf, position, damping=0.5) self.shape = pymunk.Circle(body, 30) self.shape.collision_type = COLLISION_TYPE_DOOR self.shape.body.angle = float(angle) / 180 * math.pi self.shape.sensor = True self.destination = destination self.dest_pos = tuple(dest_pos) super(BaseDoor, self).__init__( SingleShapePhysicser(space, self.shape), renderer, puzzle.ParentAttrPuzzler('is_open'), interactible=environment.Interactible( environment.Action(self._post_door_event, condition)), ) def _post_door_event(self, protagonist): self.door_opened() DoorEvent.post(self.destination, self.dest_pos) def door_opened(self): sound.play_sound('robotstep2.ogg') class Door(BaseDoor): image_name = "door.png" def __init__(self, space, position, destination, dest_pos, angle): super(Door, self).__init__( space, position, destination, dest_pos, angle, render.ImageRenderer( resources.get_image('objects', self.image_name)), environment.YesCondition(), ) @classmethod def requires(cls): return [("name", "string"), ("position", "coordinates"), ("destination", "level name"), ("dest_pos", "coordinate"), ("angle", "degrees")] class RestartGameDoor(Door): def _post_door_event(self, protagonist): protagonist.world.reset() super(RestartGameDoor, self)._post_door_event(protagonist) class ContinueGameDoor(Door): def _post_door_event(self, protagonist): world = protagonist.world if world.level[0]: DoorEvent.post(world.level[0], world.level[1]) else: # New game? super(ContinueGameDoor, self)._post_door_event(protagonist) class RocketConsole(Door): image_name = "rocket_console.png" def _post_door_event(self, protagonist): QuitEvent.post() def make_overlay_image(image_name, angle): transforms = () if angle != 0: transforms = (rotator(-angle),) return resources.get_image('objects', image_name, transforms=transforms) class PuzzleDoor(BaseDoor): def __init__(self, space, position, destination, dest_pos, angle, key_state): self._key_state = key_state overlay = ImageOverlay(make_overlay_image('lock.png', angle)) super(PuzzleDoor, self).__init__( space, position, destination, dest_pos, angle, render.ImageStateRenderer({ True: resources.get_image('objects', 'door.png'), False: resources.get_image( 'objects', 'door.png', transforms=(overlay,)), }), environment.FunctionCondition(lambda p: self.is_open), ) @property def is_open(self): if self._stored_state['is_open']: return True return self.puzzler.glue.get_state_of(self._key_state) def door_opened(self): self._stored_state['is_open'] = True super(PuzzleDoor, self).door_opened() def set_stored_state_dict(self, stored_state): self._stored_state = stored_state self._stored_state.setdefault('is_open', False) return True @classmethod def requires(cls): return [("name", "string"), ("position", "coordinates"), ("destination", "level name"), ("dest_pos", "coordinate"), ("angle", "degrees"), ("key_state", "puzzler")] class KeyedDoor(BaseDoor): def __init__(self, space, position, destination, dest_pos, angle, key_item=None): self._key_item = key_item overlay = ImageOverlay( make_overlay_image('%s.png' % (key_item,), angle)) super(KeyedDoor, self).__init__( space, position, destination, dest_pos, angle, render.ImageRenderer(resources.get_image( 'objects', 'door.png', transforms=(overlay,))), environment.ItemRequiredCondition(key_item), ) @classmethod def requires(cls): return [("name", "string"), ("position", "coordinates"), ("destination", "level name"), ("dest_pos", "coordinate"), ("angle", "degrees"), ("key_item", "item name")] class Hatch(GameObject): zorder = ZORDER_FLOOR def __init__(self, space, end1, end2, key_state=None): a = pymunk.Vec2d(end1) b = pymunk.Vec2d(end2) offset = b - a offset.length /= 2 mid = (a + offset).int_tuple body = make_body(None, None, mid) self.shape = pymunk.Segment( body, body.world_to_local(tuple(end1)), body.world_to_local(tuple(end2)), 7) self.shape.collision_type = COLLISION_TYPE_DOOR if key_state is None: puzzler = puzzle.YesPuzzler() else: puzzler = puzzle.StateProxyPuzzler(key_state) super(Hatch, self).__init__( SingleShapePhysicser(space, self.shape), render.HatchRenderer(), puzzler, ) def collide_with_protagonist(self, protagonist): if self.puzzler.get_state(): # Reject the collision, we can walk through. return False return True collide_with_furniture = collide_with_protagonist @classmethod def requires(cls): return [("name", "string"), ("end1", "coordinates"), ("end2", "coordinates"), ("key_state", "puzzler")] # The level knows that hatches are magical @classmethod def movable(cls): return True class KeyedHatch(GameObject): zorder = ZORDER_FLOOR def __init__(self, space, end1, end2, key_item): a = pymunk.Vec2d(end1) b = pymunk.Vec2d(end2) offset = b - a offset.length /= 2 mid = (a + offset).int_tuple body = make_body(None, None, mid) self.shape = pymunk.Segment( body, body.world_to_local(tuple(end1)), body.world_to_local(tuple(end2)), 7) self.shape.collision_type = COLLISION_TYPE_DOOR other_shape = pymunk.Circle(body, 30) other_shape.collision_type = COLLISION_TYPE_DOOR other_shape.sensor = True self._key_item = key_item super(KeyedHatch, self).__init__( MultiShapePhysicser(space, self.shape, other_shape), render.KeyedHatchRenderer( resources.get_image( 'objects', '%s.png' % (key_item,), transforms=(scaler((32, 32)),))), puzzle.ParentAttrPuzzler('is_open'), ) self.add_timer('door_open', 0.1) @property def is_open(self): return self.check_timer('door_open') def collide_with_protagonist(self, protagonist): if protagonist.has_item(self._key_item): self.start_timer('door_open') return False return True @classmethod def requires(cls): return [("name", "string"), ("end1", "coordinates"), ("end2", "coordinates"), ("key_item", "item name")] # The level knows that hatches are magical @classmethod def movable(cls): return True class ToggleSwitch(GameObject): zorder = ZORDER_LOW def __init__(self, space, position): body = make_body(None, None, position) self.shape = pymunk.Circle(body, 20) self.shape.sensor = True super(ToggleSwitch, self).__init__( SingleShapePhysicser(space, self.shape), render.ImageStateRenderer({ True: resources.get_image('objects', 'lever.png'), False: resources.get_image( 'objects', 'lever.png', transforms=(FLIP_H,)), }), puzzle.ParentAttrPuzzler('toggle_on'), interactible=environment.Interactible( environment.Action(self._toggle)), ) @property def toggle_on(self): return self._stored_state['toggle_on'] def _toggle(self, protagonist): self._stored_state['toggle_on'] = not self.toggle_on def set_stored_state_dict(self, stored_state): self._stored_state = stored_state # We start in the "off" position. self._stored_state.setdefault('toggle_on', False) return True @classmethod def requires(cls): return [("name", "string"), ("position", "coordinates")] class Bullet(GameObject): def __init__(self, space, position, impulse, damage, bullet_type, source_collision_type, bullet_sound=None): body = make_body(1, pymunk.inf, position) body.angle = impulse.angle self.last_position = position self.shape = pymunk.Circle(body, 2) self.shape.sensor = True self.shape.collision_type = COLLISION_TYPE_PROJECTILE self.damage = damage self.type = bullet_type self.source_collision_type = source_collision_type super(Bullet, self).__init__( SingleShapePhysicser(space, self.shape), render.ImageRenderer(resources.get_image( 'objects', '%s.png' % self.type)), ) self.physicser.apply_impulse(impulse) if bullet_sound is not None: sound.play_sound(bullet_sound, 0.3) def update(self, dt): super(Bullet, self).update(dt) position = (self.physicser.position.x, self.physicser.position.y) r = self.get_space().segment_query(self.last_position, position) self.last_position = position for collision in r: shape = collision.shape if (shape.collision_type == self.source_collision_type or shape == self.physicser.get_shape() or shape.sensor): continue if hasattr(shape, 'physicser'): shape.physicser.game_object.hit(self) self.physicser.remove_from_space() return Result(remove=[self]) class ClawAttack(GameObject): def __init__(self, space, pos, vector, damage): body = make_body(1, pymunk.inf, (pos[0] + (vector.length * math.cos(vector.angle)), pos[1] + (vector.length * math.sin(vector.angle)))) body.angle = vector.angle self.shape = pymunk.Circle(body, 30) self.shape.sensor = True self.shape.collision_type = COLLISION_TYPE_WEREWOLF_ATTACK self.damage = damage super(ClawAttack, self).__init__( SingleShapePhysicser(space, self.shape), render.ImageRenderer(resources.get_image( 'objects', 'werewolf_SW_claw_attack.png', transforms=(FLIP_H,))), ) def update(self, dt): super(ClawAttack, self).update(dt) if self.lifetime > 0.1: self.physicser.remove_from_space() return Result(remove=[self]) class HostileTerrain(GameObject): zorder = ZORDER_FLOOR damage = None tiles = [] tile_alpha = 255 tile_frame_ticks = 3 # How often to hit the player rate = 5 def __init__(self, space, position, outline): body = make_body(10, pymunk.inf, position) # Adjust shape relative to position shape_outline = [(p[0] - position[0], p[1] - position[1]) for p in outline] self.shape = pymunk.Poly(body, shape_outline) self._ticks = 0 self.shape.collision_type = COLLISION_TYPE_SWITCH self.shape.sensor = True renderer = self._fix_image(outline) self._collider = puzzle.CollidePuzzler() self._collider.set_game_object(self) self._protagonist = None super(HostileTerrain, self).__init__( SingleShapePhysicser(space, self.shape), renderer) def _fix_image(self, outline): if len(self.tiles) > 1: tile_images = [resources.get_image('tiles', x) for x in self.tiles] renderer = render.TimedTiledRenderer(outline, tile_images, self.tile_frame_ticks, self.tile_alpha) else: tile_image = resources.get_image('tiles', self.tiles[0]) renderer = render.TiledRenderer(outline, tile_image, self.tile_alpha) return renderer def update_image(self, new_outline): self.renderer = self._fix_image(new_outline) def update(self, seconds): if self._collider.get_state(): if self._ticks == 0: self.apply_effect(self._protagonist) self._ticks += 1 if self._ticks > self.rate: self._ticks = 0 def collide_with_protagonist(self, protagonist): self._protagonist = protagonist def apply_effect(self, protagonist): protagonist.lose_health(self.damage) @classmethod def requires(cls): return [("name", "string"), ("position", "coordinates"), ("outline", "polygon (convex)")] class AcidFloor(HostileTerrain): damage = 1 tiles = ['acid.png', 'acid2.png', 'acid3.png'] tile_alpha = 200 tile_frame_ticks = 10 class ForceWolfFloor(HostileTerrain): tiles = ['moonlight.png'] rate = 0 tile_alpha = 150 zorder = ZORDER_HIGH def apply_effect(self, protagonist): protagonist.force_wolf_form() class GravityWell(GameObject): zorder = ZORDER_FLOOR # How often to hit the player rate = 5 def __init__(self, space, position, radius, force): body = make_body(None, None, position) # Adjust shape relative to position self._radius = radius self.shape = pymunk.Circle(body, radius) self.centre = pymunk.Circle(body, 10) self.centre.friction = pymunk.inf self._ticks = 0 self.force = force self.shape.collision_type = COLLISION_TYPE_SWITCH self.shape.sensor = True super(GravityWell, self).__init__( MultiShapePhysicser(space, self.shape, self.centre), render.ImageRenderer(resources.get_image( 'objects', 'gravity_well.png')), ) def collide_with_protagonist(self, protagonist): # We're called every frame we're colliding, so # There are timing issues with stepping on and # off terrian, but as long as the rate is reasonably # low, they shouldn't impact gameplay self.apply_effect(protagonist) def collide_with_furniture(self, furniture): # We're called every frame we're colliding, so # There are timing issues with stepping on and # off terrian, but as long as the rate is reasonably # low, they shouldn't impact gameplay self.apply_effect(furniture) def apply_effect(self, object_to_move): movement = self.physicser.position - object_to_move.physicser.position local_force = self.force * math.sqrt( object_to_move.get_shape().body.mass) movement.length = local_force object_to_move.environmental_movement(movement) @classmethod def requires(cls): return [("name", "string"), ("position", "coordinates"), ("radius", "int"), ("force", "int")] class SheepPen(GameObject): zorder = ZORDER_FLOOR def __init__(self, space, position, outline, sheep_count): body = make_body(None, None, position) # Adjust shape relative to position shape_outline = [(p[0] - position[0], p[1] - position[1]) for p in outline] self.shape = pymunk.Poly(body, shape_outline) self.shape.collision_type = COLLISION_TYPE_SHEEP_PEN self.shape.sensor = True super(SheepPen, self).__init__( SingleShapePhysicser(space, self.shape), render.Renderer(), puzzle.MultiCollidePuzzler(sheep_count, COLLISION_TYPE_SHEEP), ) @classmethod def requires(cls): return [("name", "string"), ("position", "coordinates"), ("outline", "polygon (convex)"), ("sheep_count", "int")]