class GameWindow(gui.Widget): """The main game object.""" def __init__(self, resource_dirs=None, **params): params['width'] = 450 params['height'] = 300 params['focusable'] = False super(GameWindow, self).__init__(**params) self.surface = pygame.Surface((450, 300)) self.surface.fill((0, 0, 0)) self.grid = False self.shadows = pygame.sprite.RenderUpdates() self.hilights = pygame.sprite.RenderUpdates() self.sprites = SortedUpdates() self.overlays = pygame.sprite.RenderUpdates() self.overlays_sprites = {} self.animated_background = pygame.sprite.RenderUpdates() self.animated_background_sprites = {} self._tileset = "tileset" self._sprite_cache = TileCache(32, 32, resource_dirs=resource_dirs) self.map_cache = TileCache(MAP_TILE_WIDTH, MAP_TILE_HEIGHT, resource_dirs=resource_dirs) # Separate cache for shadows, as convert_alpha() ruins their # transparency. self._shadow_cache = TileCache(32, 32, resource_dirs=resource_dirs, convert_alpha=False) self._time_sprite = None self._clones = {} self._gates = {} self._crates = {} self.active_animation = False self._gevent_seq = [] self._gevent_queue = Queue.Queue() self.level = None self._event_handler = { # play 'move-up': functools.partial(self._move, Direction.NORTH), 'move-down': functools.partial(self._move, Direction.SOUTH), 'move-left': functools.partial(self._move, Direction.WEST), 'move-right': functools.partial(self._move, Direction.EAST), 'add-player-clone': self._player_clone, 'remove-player-clone': self._player_clone, 'field-activated': self._field_state_change, 'field-deactivated': self._field_state_change, 'time-paradox': self._time_paradox, 'time-jump': self._time_jump, 'game-complete': self._game_complete, 'end-of-turn': self._end_of_turn, 'goal-obtained': lambda *x: self.goal.kill(), 'goal-lost': lambda *x: self.animated_background.add(self.goal), 'jump-moveable': self._jump_moveable, # edit 'new-map': self._new_map, 'replace-tile': self._replace_tile, 'remove-special-field': self._remove_special_field, 'add-crate': self._add_remove_crate, 'remove-crate': self._add_remove_crate, } @property def pending_animation(self): return self.active_animation or not self._gevent_queue.empty() @property def tileset(self): return self._tileset @tileset.setter def tileset(self, ntile): self._make_background(tileset=ntile) self._tileset = ntile self.repaint() def use_level(self, level, grid=None): """Set the level as the current one.""" if grid is not None: self.grid = grid self.level = level self._gevent_seq = [] self._gevent_queue = Queue.Queue() level.add_event_listener(self._new_event) self._new_map() def _new_event(self, e): self._gevent_seq.append(e) if e.event_type == "end-of-event-sequence": # flush self._gevent_queue.put(self._gevent_seq) self._gevent_seq = [] def _make_background(self, tileset=None): self.overlays = pygame.sprite.RenderUpdates() # Render the level map if tileset is None: tileset = self._tileset background, overlays = make_background(self.level, map_cache=self.map_cache, tileset=tileset, grid=self.grid) self.surface.fill((0, 0, 0)) self.surface.blit(background, (0,0)) self._add_overlay(overlays) def _new_map(self, *args): self.shadows = pygame.sprite.RenderUpdates() self.sprites = SortedUpdates() self.animated_background = pygame.sprite.RenderUpdates() self.overlays_sprites = {} self.animated_background_sprites = {} self._clones = {} self._gates = {} self._crates = {} level = self.level # Render the level map self._make_background() for field in level.iter_fields(): # Crates looks best in 32x32, gates and buttons in 24x16 # - if its "on top of" a field 32x32 usually looks best. # - if it is (like) a field, 24x16 is usually better # - use sprite_cache and map_cache accordingly. crate = level.get_crate_at(field.position) if crate: self._add_crate(crate) self._init_field(field) try: iter_clones = level.iter_clones except AttributeError: # Not a playable map, no clones to place iter_clones = None if iter_clones: for clone in iter_clones(): self._new_clone(clone) self.repaint() def _add_crate(self, crate): c_sprite = MoveableSprite(crate.position, self._sprite_cache['crate'], c_depth=1) self._crates[crate] = c_sprite self.sprites.add(c_sprite) def _add_remove_crate(self, evt): if evt.event_type == 'add-crate': self._add_crate(evt.source) else: _kill_sprite(self._crates, evt.source) def _init_field(self, field): ani_bg = None if field.symbol == '-' or field.symbol == '_': ani_bg = Sprite(field.position, self.map_cache['campfiregate']) self._gates[field.position] = ani_bg if field.symbol == '-': ani_bg.state = 1 if field.symbol == 'b': ani_bg = Sprite(field.position, self.map_cache['stonebutton']) if field.symbol == 'o': ani_bg = Sprite(field.position, self.map_cache['onetimebutton']) if field.symbol == 'p': ani_bg = Sprite(field.position, self.map_cache['onetimepassage']) if field.symbol == 'P': ani_bg = Sprite(field.position, self.map_cache['pallet']) if field.symbol == 'S': ani_bg = Sprite(field.position, self.map_cache['timemachine']) if self._time_sprite is None: self._time_sprite = TimeSprite(field.position, self._sprite_cache["clock"], c_depth =1) else: self._time_sprite.pos = field.position if field.symbol == 'G': ani_bg = Sprite(field.position, self.map_cache['coingoal']) self.goal = ani_bg if ani_bg: self.animated_background.add(ani_bg) self.animated_background_sprites[field.position] = ani_bg return def _add_overlay(self, overlays): # Add the overlays for the level map for (x, y), image in overlays.iteritems(): overlay = pygame.sprite.Sprite(self.overlays) overlay.image = image.subsurface(0, 0, MAP_TILE_WIDTH, MAP_TILE_HEIGHT/2) overlay.rect = image.get_rect().move(Position(x*MAP_TILE_WIDTH, y * MAP_TILE_HEIGHT - MAP_TILE_HEIGHT/2)) self.overlays_sprites[Position(x,y)] = overlay def _remove_special_field(self, evt): f = evt.source _kill_sprite(self.animated_background_sprites, f.position) def _replace_tile(self, evt): f = evt.source # Kill the old animations on this field (if any) _kill_sprite(self._gates, f.position) _kill_sprite(self.animated_background_sprites, f.position) _kill_sprite(self.overlays_sprites, f.position) # Kill "nearby" overlays - they should only be "south", but mah for d in (Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST): dpos = f.position.dir_pos(d) _kill_sprite(self.overlays_sprites, dpos) # FIXME: remove old overlay overlays = update_background(self.map_cache[self._tileset], self.surface, self.level, f, fixup=True, grid=self.grid) self._add_overlay(overlays) ani_bg = None self._init_field(f) self.repaint() def _move(self, d, event): """Start walking in specified direction.""" actor = None if event.source in self._crates: actor = self._crates[event.source] else: actor = self._clones[event.source][0] if d == Direction.NO_ACT or not event.success: actor.animation = actor.do_nothing_animation() return pos = actor.pos target = pos.dir_pos(d) actor.direction = d actor.animation = actor.walk_animation() self.repaint() def _field_state_change(self, event): src_pos = event.source.position # determine the new state from the event name. While it may be tempting # to use the state of the source, the problem is that we may be processing # an "old" event from our queue. Thus the source may have already changed # state again. So to make sure the animation looks correct, use the state # at the time of the event and not the source's current state. nstate = 0 if event.event_type == "field-activated": nstate = 1 if src_pos in self._gates: self._gates[src_pos].state = nstate if event.source.symbol == "b" or event.source.symbol == "o" or event.source.symbol == "p": b = self.animated_background_sprites[src_pos] if b: b.state = nstate def _new_clone(self, clone): sprite = PlayerSprite(clone, self._sprite_cache['player']) # Ensure the sprite is created at the start location. # - The player may have enqueued a lot of actions, so the model puts # the clone at its current position. However, the animation just # reached the time jump, so the player should see the clone at # the start location. As the animation catches up, the clone will # eventually reach its intented location. sprite.pos = self.level.start_location.position shadow = Shadow(sprite, self._shadow_cache["shadow"][0][0]) self._clones[clone] = (sprite, shadow) self.sprites.add(sprite) self.shadows.add(shadow) def _player_clone(self, event): if event.event_type == "add-player-clone": self._new_clone(event.source) elif event.source in self._clones: csprite, shadow = self._clones[event.source] del self._clones[event.source] csprite.kill() shadow.kill() self.repaint() def _time_jump(self, *args): self._time_sprite.animation = self._time_sprite.time_jump_animation() self.sprites.add(self._time_sprite) def process_game_events(self): if self.active_animation or self._gevent_queue.empty(): # if the game event queue is empty just skip the code below. return try: seq = self._gevent_queue.get_nowait() print "Event seq [%s]" % (", ".join(x.event_type for x in seq)) for e in seq: if e.event_type not in self._event_handler: continue self._event_handler[e.event_type](e) self.active_animation = True except Queue.Empty: pass # expected def _time_paradox(self, e): self.repaint() def _jump_moveable(self, event): actor = None if event.source in self._crates: actor = self._crates[event.source] elif event.source in self._clones: actor = self._clones[event.source][0] actor.pos = event.source.position self.repaint() def _end_of_turn(self, _): self.repaint() def _game_complete(self, _): self.repaint() def make_hilight(self, lpos, color="yellow"): hilight = pygame.Surface((MAP_TILE_WIDTH, MAP_TILE_HEIGHT)) hilight.fill(pygame.Color(color)) hilight.set_alpha(0x80) s = Sprite(lpos, ((hilight,),)) self.hilights.add(s) return s def paint(self, s): # Draw the whole screen initially s.blit(self.surface, (0, 0)) if self.level: self.update(s) def update(self, s): if not self.level: return # Don't clear shadows and overlays, only sprites. self.sprites.clear(s, self.surface) self.animated_background.clear(s, self.surface) self.hilights.clear(s, self.surface) self.sprites.update() self.animated_background.update() has_animation = lambda x: self._clones[x][0].animation is not None active_animation = any(itertools.ifilter(has_animation, self._clones)) if self._time_sprite and self._time_sprite.animation is not None: active_animation = True if not active_animation: self.active_animation = False self.shadows.update() # Draw the "animated" background (gates). These may be dirty even # if no actor approcated it (eg. via buttons) dirty = self.animated_background.draw(s) # Don't add shadows to dirty rectangles, as they already fit inside # sprite rectangles. But draw them after the animated background # (or the background would hide them) self.shadows.draw(s) # Draw hilights on top of backgrounds... dirty.extend(self.hilights.draw(s)) # Draw actors (and crates) on top of everything dirty.extend(self.sprites.draw(s)) # Don't add ovelays to dirty rectangles, only the places where # sprites are need to be updated, and those are already dirty. self.overlays.draw(s) return dirty