def animate_capture_monster(self, is_captured, num_shakes, monster): """ Animation for capturing monsters. :param is_captured: boolean representing success of capture :param num_shakes: number of shakes before animation ends :param monster: the monster :return: """ monster_sprite = self._monster_sprite_map.get(monster, None) capdev = self.load_sprite('gfx/items/capture_device.png') animate = partial(self.animate, capdev.rect, transition='in_quad', duration=1.0) scale_sprite(capdev, .4) capdev.rect.center = scale(0), scale(0) animate(x=monster_sprite.rect.centerx) animate(y=monster_sprite.rect.centery) self.task(partial(toggle_visible, monster_sprite), 1.0) # make the monster go away temporarily def kill(): self._monster_sprite_map[monster].kill() self.hud[monster].kill() del self._monster_sprite_map[monster] del self.hud[monster] # TODO: cache this sprite from the first time it's used. # also, should loading animated sprites be more convenient? images = list() for fn in ["capture%02d.png" % i for i in range(1, 10)]: fn = 'animations/technique/' + fn image = tools.load_and_scale(fn) images.append((image, .07)) tech = PygAnimation(images, False) sprite = Sprite() sprite.image = tech sprite.rect = tech.get_rect() self.task(tech.play, 1.0) self.task(partial(self.sprites.add, sprite), 1.0) sprite.rect.midbottom = monster_sprite.rect.midbottom def shake_ball(initial_delay): animate = partial(self.animate, duration=0.1, transition='linear', delay=initial_delay) animate(capdev.rect, y=scale(3), relative=True) animate = partial(self.animate, duration=0.2, transition='linear', delay=initial_delay + 0.1) animate(capdev.rect, y=-scale(6), relative=True) animate = partial(self.animate, duration=0.1, transition='linear', delay=initial_delay + 0.3) animate(capdev.rect, y=scale(3), relative=True) for i in range(0, num_shakes): shake_ball(1.8 + i * 1.0) # leave a 0.6s wait between each shake if is_captured: self.task(kill, 2 + num_shakes) else: self.task(partial(toggle_visible, monster_sprite), 1.8 + num_shakes * 1.0) # make the monster appear again! self.task(tech.play, 1.8 + num_shakes * 1.0) self.task(capdev.kill, 1.8 + num_shakes * 1.0)
def animate_monster_release_bottom(self, feet, monster): """ :type feet: sequence :type monster: core.components.monster.Monster :return: """ capdev = self.load_sprite('gfx/items/capture_device.png') scale_sprite(capdev, .4) capdev.rect.center = feet[0], feet[1] - scale(60) # animate the capdev falling fall_time = .7 animate = partial(self.animate, duration=fall_time, transition='out_quad') animate(capdev.rect, bottom=feet[1], transition='in_back') animate(capdev, rotation=720, initial=0) # animate the capdev fading away delay = fall_time + .6 fade_duration = .9 h = capdev.rect.height animate = partial(self.animate, duration=fade_duration, delay=delay) animate(capdev, width=1, height=h * 1.5) animate(capdev.rect, y=-scale(14), relative=True) # convert the capdev sprite so we can fade it easily def func(): capdev.image = tools.convert_alpha_to_colorkey(capdev.image) self.animate(capdev.image, set_alpha=0, initial=255, duration=fade_duration) self.task(func, delay) self.task(capdev.kill, fall_time + delay + fade_duration) # load monster and set in final position monster_sprite = self.load_sprite(monster.back_battle_sprite, midbottom=feet) self._monster_sprite_map[monster] = monster_sprite # position monster_sprite off screen and set animation to move it back to final spot monster_sprite.rect.top = self.game.screen.get_height() self.animate(monster_sprite.rect, bottom=feet[1], transition='out_back', duration=.9, delay=fall_time + .5) # capdev opening animation images = list() for fn in ["capture%02d.png" % i for i in range(1, 10)]: fn = 'animations/technique/' + fn image = tools.load_and_scale(fn) images.append((image, .07)) delay = 1.3 tech = PygAnimation(images, False) sprite = Sprite() sprite.image = tech sprite.rect = tech.get_rect() sprite.rect.midbottom = feet self.task(tech.play, delay) self.task(partial(self.sprites.add, sprite), delay)
def load_technique_animation(technique): """ TODO: move to some generic animation loading thingy :param technique: :rtype: core.components.sprite.Sprite """ frame_time = .09 images = list() for fn in technique.images: image = tools.load_and_scale(fn) images.append((image, frame_time)) tech = PygAnimation(images, False) sprite = Sprite() sprite.image = tech sprite.rect = tech.get_rect() return sprite
def load_technique_animation(technique): """ TODO: move to some generic animation loading thingy :param technique: :rtype: core.components.sprite.Sprite """ frame_time = 0.09 images = list() for fn in technique.images: image = tools.load_and_scale(fn) images.append((image, frame_time)) tech = PygAnimation(images, False) sprite = Sprite() sprite.image = tech sprite.rect = tech.get_rect() return sprite
def loadfile(self, tile_size): """Loads the tile and collision data from the map file and returns a list of tiles with their position and pygame surface, a set of collision tile coordinates, and the size of the map itself. The list of tile surfaces is used to draw the map in the main game. The list of collision tile coordinates is used for collision detection. :param tile_size: An [x, y] size of each tile in pixels AFTER scaling. This is used for scaling and positioning. :type tile_size: List :rtype: List :returns: A multi-dimensional list of tiles in dictionary format; a set of collision coordinates; the map size. **Examples:** The list of tiles is structured in a way where you can access an individual tile by index number. For example, to get a tile located at (2, 1), you can access the tile's details using: >>> x = 2 >>> y = 1 >>> layer = 0 >>> tiles[x][y][layer] Here is an example of what the the tiles list data structure actually looks like: >>> tiles, collisions, mapsize = map.loadfile([24, 24]) >>> tiles [ [ [], [], [], [], [], [], [], [], [], [], [] ], [ [], [{'layer': 1, 'name': '6,0', 'position': (80, 80), 'surface': <Surface(16x16x32 SW)>, 'tile_pos': [1, 1], 'tileset': 'resources/gfx/tileset.png'}], [{'layer': 1, 'name': '7,0', 'position': (80, 160), 'surface': <Surface(16x16x32 SW)>, 'tile_pos': [1, 2], 'tileset': 'resources/gfx/tileset.png'}], [{'layer': 1, 'name': '8,0', 'position': (80, 240), 'surface': <Surface(16x16x32 SW)>, 'tile_pos': [1, 3], 'tileset': 'resources/gfx/tileset.png'}], [{'layer': 1, 'name': '9,0', 'position': (80, 320), 'surface': <Surface(16x16x32 SW)>, 'tile_pos': [1, 4], 'tileset': 'resources/gfx/tileset.png'}], [{'layer': 1, 'name': '9,0', 'position': (80, 400), 'surface': <Surface(16x16x32 SW)>, 'tile_pos': [1, 5], 'tileset': 'resources/gfx/tileset.png'}, {'layer': 3, 'name': '10,0', 'position': (80, 400), 'surface': <Surface(16x16x32 SW)>, 'tile_pos': [1, 5], 'tileset': 'resources/gfx/tileset.png'}], ... The collision map is a set of (x,y) coordinates that the player cannot walk through. This set is generated based on collision regions defined in the map file. Here is an example of what the collision set looks like: >>> tiles, collisions, mapsize = map.loadfile([24, 24]) >>> collisions set([(0, 2), (0, 3), (0, 4), (0, 5), (0, 6)]) """ # Create a list of all of the tiles in the map tiles = [] # Loop through all tiles in our map file and get the pygame surface associated with it. for x in range(0, int(self.data.width)): # Create a list of tile for the y-axis y_list = [] for y in range(0, int(self.data.height)): layer_list = [] # Get the number of tile layers. num_of_layers = 0 # PyTMX recently changed some of their attribute names. # This ensures we get the number of layers regardless of # the version of PyTMX. try: for layer in self.data.layers: if hasattr(layer, 'data'): num_of_layers += 1 except AttributeError: for layer in self.data.tilelayers: num_of_layers += 1 # Get all the map tiles for each layer for layer in range(0, num_of_layers): # PyTMX recently changed their method names. This # ensures the map will load regardless of the PyTMX # version. try: surface = self.data.getTileImage(x, y, layer) except AttributeError: surface = self.data.get_tile_image(x, y, layer) # Check to see if this tile has an animation tile_properties = self.data.get_tile_properties(x, y, layer) if tile_properties and "frames" in tile_properties: images_and_durations = [] for frame in tile_properties["frames"]: # bitcraft/PyTMX 3.20.14+ support if hasattr(frame, 'gid'): anim_surface = self.data.get_tile_image_by_gid(frame.gid) images_and_durations.append((anim_surface, float(frame.duration) / 1000)) # ShadowApex/PyTMX 3.20.13 fork support elif "gid" in frame: anim_surface = self.data.get_tile_image_by_gid(frame["gid"]) images_and_durations.append((anim_surface, float(frame["duration"]) / 1000)) # bitcraft/PyTMX 3.20.13 support elif "gid" not in frame: images_and_durations = [(surface, 1)] break surface = PygAnimation(images_and_durations) surface.play() # Create a tile based on the image if surface: tile = {'tile_pos': (x, y), 'position': (x * tile_size[0], y * tile_size[1]), 'layer': layer + 1, 'name': str(x) + "," + str(y), 'surface': surface } layer_list.append(tile) y_list.append(layer_list) tiles.append(y_list) # Get the dimensions of the map mapsize = self.size # Create a list of all tile monsters_in_play that we cannot walk through collision_map = {} # Create a dictionary of coordinates that have conditional collisions cond_collision_map = {} # Create a list of all pairs of adjacent tiles that are impassable (aka walls) # example: ((5,4),(5,3), both) collision_lines_map = set() # Right now our collisions are defined in our tmx file as large regions that the player # can't pass through. We need to convert these areas into individual tile coordinates # that the player can't pass through. # Loop through all of the collision objects in our tmx file. for collision_region in self.collisions: # >>> collision_region.__dict__ #{'gid': 0, # 'height': 16, # 'name': None, # 'parent': <TiledMap: "resources/maps/pallet_town-room.tmx">, # 'rotation': 0, # 'type': 'collision', # 'visible': 1, # 'width': 16, # 'x': 176, # 'y': 64} # Get the collision area's tile location and dimension in tiles using the tileset's # tile size. x = self.round_to_divisible(collision_region.x, self.tile_size[0]) / self.tile_size[0] y = self.round_to_divisible(collision_region.y, self.tile_size[1]) / self.tile_size[1] width = self.round_to_divisible(collision_region.width, self.tile_size[0]) / self.tile_size[0] height = self.round_to_divisible(collision_region.height, self.tile_size[1]) / self.tile_size[1] # Loop through properties and create list of directions for each property if collision_region.properties: enters = [] exits = [] for key in collision_region.properties: if "enter" in key: for direction in collision_region.properties[key].split(): enters.append(direction) elif "exit" in key: for direction in collision_region.properties[key].split(): exits.append(direction) # Loop through the area of this region and create all the tile coordinates that are # inside this region. for a in range(0, int(width)): for b in range(0, int(height)): collision_tile = (a + x, b + y) collision_map[collision_tile] = "None" # Check if collision region has properties, and is therefore a conditional zone # then add the location and conditions to semi_collision_map if collision_region.properties: tile_conditions = {} for key in collision_region.properties.keys(): if "enter" in key: tile_conditions['enter'] = enters if "exit" in key: tile_conditions['exit'] = exits collision_map[collision_tile] = tile_conditions # Similar to collisions, except we need to identify the tiles # on either side of the poly-line and prevent moving between # them for collision_line in self.collision_lines: # >>> collision_wall.__dict__ # {'name': None, # 'parent': <TiledMap: "resources/maps/test_pathfinding.tmx">, # 'visible': 1, # 'height': 160.0, # 'width': 80.0, ' # gid': 0, # 'closed': False, # 'y': 80.0, 'x': 80.0, # 'rotation': 0, # 'type': 'collision-wall', # 'points': ((80.0, 80.0), (80.0, 128.0), (160.0, 128.0), (160.0, 240.0)) # Another example: # 'points': ((192.0, 80.0), (192.0, 192.0)) # For each pair of points, get the tiles on either side of the line. # Assumption: A pair of points will only be vertical or horizontal (no diagonal lines) if len(collision_line.points) < 2: raise Exception("Error: map has polyline with only one point") # get two points, and round them point1 = (self.round_to_divisible(collision_line.points[0][0], self.tile_size[0]), self.round_to_divisible(collision_line.points[0][1], self.tile_size[1])) point2 = (self.round_to_divisible(collision_line.points[1][0], self.tile_size[0]), self.round_to_divisible(collision_line.points[1][1], self.tile_size[1])) # check to see if horizontal or vertical line_type = None if point1[0] == point2[0] and point1[1] != point2[1]: # x's are same, must be vertical line_type = 'vertical' elif point1[0] != point2[0] and point1[1] == point2[1]: # y's are same, must be horizontal line_type = 'horizontal' else: raise Exception("Error: Points on polyline are not strictly horizontal or vertical....") if line_type is 'vertical': # get all tile coordinates on either side x = point1[0] / self.tile_size[0] # same as point2[0] b/c vertical line_start = point1[1] line_end = point2[1] num_tiles_in_line = abs(line_start - line_end) / self.tile_size[1] # [1] b/c vertical curr_y = line_start / self.tile_size[1] for i in range(int(num_tiles_in_line)): if line_start > line_end: # slightly different # behavior depending on # direction left_side_tile = (x-1,curr_y-1) right_side_tile = (x,curr_y-1) curr_y -= 1 else: left_side_tile = (x-1,curr_y) right_side_tile = (x,curr_y) curr_y += 1 # TODO - if we want to enable single-direction # walls (i.e. for jumping) then ask map-designer # to include a special property for the direction # to block, and then here we only block in one # direction, not both. collision_lines_map.add((left_side_tile, "right")) collision_lines_map.add((right_side_tile, "left")) elif line_type is 'horizontal': # get all tile coordinates on either side y = point1[1] / self.tile_size[1] # same as point2[1] b/c horizontal line_start = point1[0] line_end = point2[0] num_tiles_in_line = abs(line_start - line_end) / self.tile_size[0] # [0] b/c horizontal curr_x = line_start / self.tile_size[0] for i in range(int(num_tiles_in_line)): if line_start > line_end: # slightly different # behavior depending on # direction top_side_tile = (curr_x-1,y-1) bottom_side_tile = (curr_x-1,y) curr_x -= 1 else: top_side_tile = (curr_x, y-1) bottom_side_tile = (curr_x, y) curr_x += 1 # TODO - if we want to enable single-direction # walls (i.e. for jumping) then ask map-designer # to include a special property for the direction # to block, and then here we only block in one # direction, not both. collision_lines_map.add((top_side_tile, "down")) collision_lines_map.add((bottom_side_tile, "up")) return tiles, collision_map, collision_lines_map, mapsize
def loadfile(self, tile_size): """Loads the tile and collision data from the map file and returns a list of tiles with their position and pygame surface, a set of collision tile coordinates, and the size of the map itself. The list of tile surfaces is used to draw the map in the main game. The list of collision tile coordinates is used for collision detection. :param tile_size: An [x, y] size of each tile in pixels AFTER scaling. This is used for scaling and positioning. :type tile_size: List :rtype: List :returns: A multi-dimensional list of tiles in dictionary format; a set of collision coordinates; the map size. **Examples:** The list of tiles is structured in a way where you can access an individual tile by index number. For example, to get a tile located at (2, 1), you can access the tile's details using: >>> x = 2 >>> y = 1 >>> layer = 0 >>> tiles[x][y][layer] Here is an example of what the the tiles list data structure actually looks like: >>> tiles, collisions, mapsize = map.loadfile([24, 24]) >>> tiles [ [ [], [], [], [], [], [], [], [], [], [], [] ], [ [], [{'layer': 1, 'name': '6,0', 'position': (80, 80), 'surface': <Surface(16x16x32 SW)>, 'tile_pos': [1, 1], 'tileset': 'resources/gfx/tileset.png'}], [{'layer': 1, 'name': '7,0', 'position': (80, 160), 'surface': <Surface(16x16x32 SW)>, 'tile_pos': [1, 2], 'tileset': 'resources/gfx/tileset.png'}], [{'layer': 1, 'name': '8,0', 'position': (80, 240), 'surface': <Surface(16x16x32 SW)>, 'tile_pos': [1, 3], 'tileset': 'resources/gfx/tileset.png'}], [{'layer': 1, 'name': '9,0', 'position': (80, 320), 'surface': <Surface(16x16x32 SW)>, 'tile_pos': [1, 4], 'tileset': 'resources/gfx/tileset.png'}], [{'layer': 1, 'name': '9,0', 'position': (80, 400), 'surface': <Surface(16x16x32 SW)>, 'tile_pos': [1, 5], 'tileset': 'resources/gfx/tileset.png'}, {'layer': 3, 'name': '10,0', 'position': (80, 400), 'surface': <Surface(16x16x32 SW)>, 'tile_pos': [1, 5], 'tileset': 'resources/gfx/tileset.png'}], ... The collision map is a set of (x,y) coordinates that the player cannot walk through. This set is generated based on collision regions defined in the map file. Here is an example of what the collision set looks like: >>> tiles, collisions, mapsize = map.loadfile([24, 24]) >>> collisions set([(0, 2), (0, 3), (0, 4), (0, 5), (0, 6)]) """ # Create a list of all of the tiles in the map tiles = [] # Loop through all tiles in our map file and get the pygame surface associated with it. for x in range(0, int(self.data.width)): # Create a list of tile for the y-axis y_list = [] for y in range(0, int(self.data.height)): layer_list = [] # Get the number of tile layers. num_of_layers = 0 # PyTMX recently changed some of their attribute names. # This ensures we get the number of layers regardless of # the version of PyTMX. try: for layer in self.data.layers: if hasattr(layer, "data"): num_of_layers += 1 except AttributeError: for layer in self.data.tilelayers: num_of_layers += 1 # Get all the map tiles for each layer for layer in range(0, num_of_layers): # PyTMX recently changed their method names. This # ensures the map will load regardless of the PyTMX # version. try: surface = self.data.getTileImage(x, y, layer) except AttributeError: surface = self.data.get_tile_image(x, y, layer) # Check to see if this tile has an animation tile_properties = self.data.get_tile_properties(x, y, layer) if tile_properties and "frames" in tile_properties: images_and_durations = [] for frame in tile_properties["frames"]: # bitcraft/PyTMX 3.20.14+ support if hasattr(frame, "gid"): anim_surface = self.data.get_tile_image_by_gid(frame.gid) images_and_durations.append((anim_surface, float(frame.duration) / 1000)) # ShadowApex/PyTMX 3.20.13 fork support elif "gid" in frame: anim_surface = self.data.get_tile_image_by_gid(frame["gid"]) images_and_durations.append((anim_surface, float(frame["duration"]) / 1000)) # bitcraft/PyTMX 3.20.13 support elif "gid" not in frame: images_and_durations = [(surface, 1)] break surface = PygAnimation(images_and_durations) surface.play() # Create a tile based on the image if surface: tile = { "tile_pos": (x, y), "position": (x * tile_size[0], y * tile_size[1]), "layer": layer + 1, "name": str(x) + "," + str(y), "surface": surface, } layer_list.append(tile) y_list.append(layer_list) tiles.append(y_list) # Get the dimensions of the map mapsize = self.size # Create a list of all tile monsters_in_play that we cannot walk through collision_map = {} # Create a dictionary of coordinates that have conditional collisions cond_collision_map = {} # Create a list of all pairs of adjacent tiles that are impassable (aka walls) # example: ((5,4),(5,3), both) collision_lines_map = set() # Right now our collisions are defined in our tmx file as large regions that the player # can't pass through. We need to convert these areas into individual tile coordinates # that the player can't pass through. # Loop through all of the collision objects in our tmx file. for collision_region in self.collisions: # >>> collision_region.__dict__ # {'gid': 0, # 'height': 16, # 'name': None, # 'parent': <TiledMap: "resources/maps/pallet_town-room.tmx">, # 'rotation': 0, # 'type': 'collision', # 'visible': 1, # 'width': 16, # 'x': 176, # 'y': 64} # Get the collision area's tile location and dimension in tiles using the tileset's # tile size. x = self.round_to_divisible(collision_region.x, self.tile_size[0]) / self.tile_size[0] y = self.round_to_divisible(collision_region.y, self.tile_size[1]) / self.tile_size[1] width = self.round_to_divisible(collision_region.width, self.tile_size[0]) / self.tile_size[0] height = self.round_to_divisible(collision_region.height, self.tile_size[1]) / self.tile_size[1] # Loop through properties and create list of directions for each property if collision_region.properties: enters = [] exits = [] for key in collision_region.properties: if "enter" in key: for direction in collision_region.properties[key].split(): enters.append(direction) elif "exit" in key: for direction in collision_region.properties[key].split(): exits.append(direction) # Loop through the area of this region and create all the tile coordinates that are # inside this region. for a in range(0, int(width)): for b in range(0, int(height)): collision_tile = (a + x, b + y) collision_map[collision_tile] = "None" # Check if collision region has properties, and is therefore a conditional zone # then add the location and conditions to semi_collision_map if collision_region.properties: tile_conditions = {} for key in collision_region.properties.keys(): if "enter" in key: tile_conditions["enter"] = enters if "exit" in key: tile_conditions["exit"] = exits collision_map[collision_tile] = tile_conditions # Similar to collisions, except we need to identify the tiles # on either side of the poly-line and prevent moving between # them for collision_line in self.collision_lines: # >>> collision_wall.__dict__ # {'name': None, # 'parent': <TiledMap: "resources/maps/test_pathfinding.tmx">, # 'visible': 1, # 'height': 160.0, # 'width': 80.0, ' # gid': 0, # 'closed': False, # 'y': 80.0, 'x': 80.0, # 'rotation': 0, # 'type': 'collision-wall', # 'points': ((80.0, 80.0), (80.0, 128.0), (160.0, 128.0), (160.0, 240.0)) # Another example: # 'points': ((192.0, 80.0), (192.0, 192.0)) # For each pair of points, get the tiles on either side of the line. # Assumption: A pair of points will only be vertical or horizontal (no diagonal lines) if len(collision_line.points) < 2: raise Exception("Error: map has polyline with only one point") # get two points, and round them point1 = ( self.round_to_divisible(collision_line.points[0][0], self.tile_size[0]), self.round_to_divisible(collision_line.points[0][1], self.tile_size[1]), ) point2 = ( self.round_to_divisible(collision_line.points[1][0], self.tile_size[0]), self.round_to_divisible(collision_line.points[1][1], self.tile_size[1]), ) # check to see if horizontal or vertical line_type = None if point1[0] == point2[0] and point1[1] != point2[1]: # x's are same, must be vertical line_type = "vertical" elif point1[0] != point2[0] and point1[1] == point2[1]: # y's are same, must be horizontal line_type = "horizontal" else: raise Exception("Error: Points on polyline are not strictly horizontal or vertical....") if line_type is "vertical": # get all tile coordinates on either side x = point1[0] / self.tile_size[0] # same as point2[0] b/c vertical line_start = point1[1] line_end = point2[1] num_tiles_in_line = abs(line_start - line_end) / self.tile_size[1] # [1] b/c vertical curr_y = line_start / self.tile_size[1] for i in range(int(num_tiles_in_line)): if line_start > line_end: # slightly different # behavior depending on # direction left_side_tile = (x - 1, curr_y - 1) right_side_tile = (x, curr_y - 1) curr_y -= 1 else: left_side_tile = (x - 1, curr_y) right_side_tile = (x, curr_y) curr_y += 1 # TODO - if we want to enable single-direction # walls (i.e. for jumping) then ask map-designer # to include a special property for the direction # to block, and then here we only block in one # direction, not both. collision_lines_map.add((left_side_tile, "right")) collision_lines_map.add((right_side_tile, "left")) elif line_type is "horizontal": # get all tile coordinates on either side y = point1[1] / self.tile_size[1] # same as point2[1] b/c horizontal line_start = point1[0] line_end = point2[0] num_tiles_in_line = abs(line_start - line_end) / self.tile_size[0] # [0] b/c horizontal curr_x = line_start / self.tile_size[0] for i in range(int(num_tiles_in_line)): if line_start > line_end: # slightly different # behavior depending on # direction top_side_tile = (curr_x - 1, y - 1) bottom_side_tile = (curr_x - 1, y) curr_x -= 1 else: top_side_tile = (curr_x, y - 1) bottom_side_tile = (curr_x, y) curr_x += 1 # TODO - if we want to enable single-direction # walls (i.e. for jumping) then ask map-designer # to include a special property for the direction # to block, and then here we only block in one # direction, not both. collision_lines_map.add((top_side_tile, "down")) collision_lines_map.add((bottom_side_tile, "up")) return tiles, collision_map, collision_lines_map, mapsize