352 lines
13 KiB
Python
352 lines
13 KiB
Python
"""
|
|
Functions and classes for managing a map created in the "Tiled Map Editor"
|
|
"""
|
|
|
|
import xml.etree.ElementTree as ElementTree
|
|
import base64
|
|
import zlib
|
|
import gzip
|
|
|
|
from pathlib import Path
|
|
|
|
from arcade.isometric import isometric_grid_to_screen
|
|
from arcade import Sprite
|
|
from arcade import SpriteList
|
|
|
|
|
|
class TiledMap:
|
|
""" This class holds a tiled map, and tile set from the map. """
|
|
def __init__(self):
|
|
self.global_tile_set = {}
|
|
self.layers_int_data = {}
|
|
self.layers = {}
|
|
self.version = None
|
|
self.orientation = None
|
|
self.renderorder = None
|
|
self.width = None
|
|
self.height = None
|
|
self.tilewidth = None
|
|
self.tileheight = None
|
|
self.backgroundcolor = None
|
|
self.nextobjectid = None
|
|
|
|
|
|
class Tile:
|
|
""" This class represents an individual tile from a tileset. """
|
|
def __init__(self):
|
|
self.local_id = 0
|
|
self.width = 0
|
|
self.height = 0
|
|
self.source = None
|
|
self.points = None
|
|
|
|
|
|
class GridLocation:
|
|
""" This represents a location on the grid. Contains the x/y of the
|
|
grid location, and the tile that is on it. """
|
|
def __init__(self):
|
|
self.tile = None
|
|
self.center_x = 0
|
|
self.center_y = 0
|
|
|
|
|
|
def _process_csv_encoding(data_text):
|
|
layer_grid_ints = []
|
|
lines = data_text.split("\n")
|
|
for line in lines:
|
|
line_list = line.split(",")
|
|
while '' in line_list:
|
|
line_list.remove('')
|
|
line_list_int = [int(item) for item in line_list]
|
|
layer_grid_ints.append(line_list_int)
|
|
|
|
return layer_grid_ints
|
|
|
|
|
|
def _process_base64_encoding(data_text, compression, layer_width):
|
|
layer_grid_ints = [[]]
|
|
|
|
unencoded_data = base64.b64decode(data_text)
|
|
if compression == "zlib":
|
|
unzipped_data = zlib.decompress(unencoded_data)
|
|
elif compression == "gzip":
|
|
unzipped_data = gzip.decompress(unencoded_data)
|
|
elif compression is None:
|
|
unzipped_data = unencoded_data
|
|
else:
|
|
raise ValueError(f"Unsupported compression type '{compression}'.")
|
|
|
|
# Turn bytes into 4-byte integers
|
|
byte_count = 0
|
|
int_count = 0
|
|
int_value = 0
|
|
row_count = 0
|
|
for byte in unzipped_data:
|
|
int_value += byte << (byte_count * 8)
|
|
byte_count += 1
|
|
if byte_count % 4 == 0:
|
|
byte_count = 0
|
|
int_count += 1
|
|
layer_grid_ints[row_count].append(int_value)
|
|
int_value = 0
|
|
if int_count % layer_width == 0:
|
|
row_count += 1
|
|
layer_grid_ints.append([])
|
|
|
|
layer_grid_ints.pop()
|
|
return layer_grid_ints
|
|
|
|
|
|
def _parse_points(point_text: str):
|
|
result = []
|
|
point_list = point_text.split(" ")
|
|
for point in point_list:
|
|
z = point.split(",")
|
|
result.append([round(float(z[0])), round(float(z[1]))])
|
|
|
|
return result
|
|
|
|
|
|
def read_tiled_map(tmx_file: str, scaling: float = 1, tsx_file: str = None) -> TiledMap:
|
|
"""
|
|
read_tiled_map has been deprecated. Use arcade.tilemap.read_tmx instead.
|
|
|
|
Given a tmx_file, this will read in a tiled map, and return
|
|
a TiledMap object.
|
|
|
|
Given a tsx_file, the map will use it as the tileset.
|
|
If tsx_file is not specified, it will use the tileset specified
|
|
within the tmx_file.
|
|
|
|
Important: Tiles must be a "collection" of images.
|
|
|
|
Hitboxes can be drawn around tiles in the tileset editor,
|
|
but only polygons are supported.
|
|
(This is a great area for PR's to improve things.)
|
|
|
|
:param str tmx_file: String with name of our TMX file
|
|
:param float scaling: Scaling factor. 0.5 will half all widths and heights
|
|
:param str tsx_file: Tileset to use (can be specified in TMX file)
|
|
|
|
:returns: Map
|
|
:rtype: TiledMap
|
|
"""
|
|
from warnings import warn
|
|
warn('read_tiled_map has been deprecated. Use arcade.tilemap.read_tmx instead.', DeprecationWarning)
|
|
|
|
# Create a map object to store this stuff in
|
|
my_map = TiledMap()
|
|
|
|
# Read in and parse the file
|
|
tree = ElementTree.parse(tmx_file)
|
|
|
|
# Root node should be 'map'
|
|
map_tag = tree.getroot()
|
|
|
|
# Pull attributes that should be in the file for the map
|
|
my_map.version = map_tag.attrib["version"]
|
|
my_map.orientation = map_tag.attrib["orientation"]
|
|
my_map.renderorder = map_tag.attrib["renderorder"]
|
|
my_map.width = int(map_tag.attrib["width"])
|
|
my_map.height = int(map_tag.attrib["height"])
|
|
my_map.tilewidth = int(map_tag.attrib["tilewidth"])
|
|
my_map.tileheight = int(map_tag.attrib["tileheight"])
|
|
|
|
# Background color is optional, and may or may not be in there
|
|
if "backgroundcolor" in map_tag.attrib:
|
|
# Decode the background color string
|
|
background_color_string = map_tag.attrib["backgroundcolor"]
|
|
red_hex = "0x" + background_color_string[1:3]
|
|
green_hex = "0x" + background_color_string[3:5]
|
|
blue_hex = "0x" + background_color_string[5:7]
|
|
red = int(red_hex, 16)
|
|
green = int(green_hex, 16)
|
|
blue = int(blue_hex, 16)
|
|
my_map.backgroundcolor = (red, green, blue)
|
|
|
|
my_map.nextobjectid = map_tag.attrib["nextobjectid"]
|
|
|
|
# Grab all the tilesets
|
|
tileset_tag_list = map_tag.findall('./tileset')
|
|
|
|
# --- Tileset Data ---
|
|
|
|
# Loop through each tileset
|
|
for tileset_tag in tileset_tag_list:
|
|
firstgid = int(tileset_tag.attrib["firstgid"])
|
|
if tsx_file is not None or "source" in tileset_tag.attrib:
|
|
if tsx_file is not None:
|
|
tileset_tree = ElementTree.parse(tsx_file)
|
|
else:
|
|
source = tileset_tag.attrib["source"]
|
|
try:
|
|
tileset_tree = ElementTree.parse(source)
|
|
except FileNotFoundError:
|
|
source = Path(tmx_file).parent / Path(source)
|
|
tileset_tree = ElementTree.parse(source)
|
|
# Root node should be 'map'
|
|
tileset_root = tileset_tree.getroot()
|
|
tile_tag_list = tileset_root.findall("tile")
|
|
else:
|
|
# Grab each tile
|
|
tile_tag_list = tileset_tag.findall("tile")
|
|
|
|
# Loop through each tile
|
|
for tile_tag in tile_tag_list:
|
|
# Make a tile object
|
|
my_tile = Tile()
|
|
image = tile_tag.find("image")
|
|
my_tile.local_id = tile_tag.attrib["id"]
|
|
my_tile.width = int(image.attrib["width"])
|
|
my_tile.height = int(image.attrib["height"])
|
|
my_tile.source = image.attrib["source"]
|
|
key = str(int(my_tile.local_id) + 1)
|
|
my_map.global_tile_set[key] = my_tile
|
|
firstgid += 1
|
|
|
|
objectgroup = tile_tag.find("objectgroup")
|
|
if objectgroup:
|
|
my_object = objectgroup.find("object")
|
|
if my_object:
|
|
offset_x = round(float(my_object.attrib['x']))
|
|
offset_y = round(float(my_object.attrib['y']))
|
|
|
|
polygon = my_object.find("polygon")
|
|
if polygon is not None:
|
|
point_list = _parse_points(polygon.attrib['points'])
|
|
for point in point_list:
|
|
point[0] += offset_x
|
|
point[1] += offset_y
|
|
point[1] = my_tile.height - point[1]
|
|
point[0] -= my_tile.width // 2
|
|
point[1] -= my_tile.height // 2
|
|
point[0] *= scaling
|
|
point[1] *= scaling
|
|
point[0] = int(point[0])
|
|
point[1] = int(point[1])
|
|
|
|
my_tile.points = point_list
|
|
|
|
polygon = my_object.find("polyline")
|
|
if polygon is not None:
|
|
point_list = _parse_points(polygon.attrib['points'])
|
|
for point in point_list:
|
|
point[0] += offset_x
|
|
point[1] += offset_y
|
|
point[1] = my_tile.height - point[1]
|
|
point[0] -= my_tile.width // 2
|
|
point[1] -= my_tile.height // 2
|
|
point[0] *= scaling
|
|
point[1] *= scaling
|
|
point[0] = int(point[0])
|
|
point[1] = int(point[1])
|
|
|
|
if point_list[0][0] != point_list[-1][0] or point_list[0][1] != point_list[-1][1]:
|
|
point_list.append([point_list[0][0], point_list[0][1]])
|
|
|
|
my_tile.points = point_list
|
|
|
|
# --- Map Data ---
|
|
|
|
# Grab each layer
|
|
layer_tag_list = map_tag.findall('./layer')
|
|
for layer_tag in layer_tag_list:
|
|
layer_width = int(layer_tag.attrib['width'])
|
|
|
|
# Unzip and unencode each layer
|
|
data = layer_tag.find("data")
|
|
data_text = data.text.strip()
|
|
encoding = data.attrib['encoding']
|
|
if 'compression' in data.attrib:
|
|
compression = data.attrib['compression']
|
|
else:
|
|
compression = None
|
|
|
|
if encoding == "csv":
|
|
layer_grid_ints = _process_csv_encoding(data_text)
|
|
elif encoding == "base64":
|
|
layer_grid_ints = _process_base64_encoding(data_text, compression, layer_width)
|
|
else:
|
|
print(f"Error, unexpected encoding: {encoding}.")
|
|
break
|
|
|
|
# Great, we have a grid of ints. Save that according to the layer name
|
|
my_map.layers_int_data[layer_tag.attrib["name"]] = layer_grid_ints
|
|
|
|
# Now create grid objects for each tile
|
|
layer_grid_objs = []
|
|
for row_index, row in enumerate(layer_grid_ints):
|
|
layer_grid_objs.append([])
|
|
for column_index, column in enumerate(row):
|
|
grid_loc = GridLocation()
|
|
if layer_grid_ints[row_index][column_index] != 0:
|
|
key = str(layer_grid_ints[row_index][column_index])
|
|
|
|
if key not in my_map.global_tile_set:
|
|
print(f"Warning, tried to load '{key}' and it is not in the tileset.")
|
|
else:
|
|
grid_loc.tile = my_map.global_tile_set[key]
|
|
|
|
if my_map.renderorder == "right-down":
|
|
adjusted_row_index = my_map.height - row_index - 1
|
|
else:
|
|
adjusted_row_index = row_index
|
|
|
|
if my_map.orientation == "orthogonal":
|
|
grid_loc.center_x = column_index * my_map.tilewidth + my_map.tilewidth // 2
|
|
grid_loc.center_y = adjusted_row_index * my_map.tileheight + my_map.tilewidth // 2
|
|
else:
|
|
grid_loc.center_x, grid_loc.center_y = isometric_grid_to_screen(column_index,
|
|
row_index,
|
|
my_map.width,
|
|
my_map.height,
|
|
my_map.tilewidth,
|
|
my_map.tileheight)
|
|
|
|
layer_grid_objs[row_index].append(grid_loc)
|
|
|
|
my_map.layers[layer_tag.attrib["name"]] = layer_grid_objs
|
|
|
|
return my_map
|
|
|
|
|
|
def generate_sprites(map_object: TiledMap, layer_name: str, scaling: float, base_directory="") -> SpriteList:
|
|
"""
|
|
generate_sprites has been deprecated. Use arcade.tilemap.process_layer instead.
|
|
Generate the sprites for a layer in a map.
|
|
|
|
:param TiledMap map_object: Map previously read in from read_tiled_map function
|
|
:param layer_name: Name of the layer we want to generate sprites from. Case sensitive.
|
|
:param scaling: Scaling factor.
|
|
:param base_directory: Directory to read images from. Defaults to current directory.
|
|
:return: List of sprites
|
|
:rtype: SpriteList
|
|
"""
|
|
from warnings import warn
|
|
sprite_list = SpriteList()
|
|
|
|
if layer_name not in map_object.layers_int_data:
|
|
print(f"Warning, no layer named '{layer_name}'.")
|
|
return sprite_list
|
|
|
|
map_array = map_object.layers_int_data[layer_name]
|
|
|
|
# Loop through the layer and add in the wall list
|
|
for row_index, row in enumerate(map_array):
|
|
for column_index, item in enumerate(row):
|
|
if str(item) in map_object.global_tile_set:
|
|
tile_info = map_object.global_tile_set[str(item)]
|
|
tmx_file = base_directory + tile_info.source
|
|
|
|
my_sprite = Sprite(tmx_file, scaling)
|
|
my_sprite.right = column_index * (map_object.tilewidth * scaling)
|
|
my_sprite.top = (map_object.height - row_index) * (map_object.tileheight * scaling)
|
|
|
|
if tile_info.points is not None:
|
|
my_sprite.set_points(tile_info.points)
|
|
sprite_list.append(my_sprite)
|
|
elif item != 0:
|
|
print(f"Warning, could not find {item} image to load.")
|
|
|
|
return sprite_list
|