diff --git a/pytiled_parser/objects.py b/pytiled_parser/objects.py index 6272a37..f53c617 100644 --- a/pytiled_parser/objects.py +++ b/pytiled_parser/objects.py @@ -35,6 +35,7 @@ class Color(NamedTuple): :blue (int): Blue, between 1 and 255. :alpha (int): Alpha, between 1 and 255. """ + red: int green: int blue: int @@ -48,6 +49,7 @@ class OrderedPair(NamedTuple): x (Union[int, float]): X coordinate. y (Union[int, float]): Y coordinate. """ + x: Union[int, float] y: Union[int, float] @@ -59,6 +61,7 @@ class Size(NamedTuple): width (Union[int, float]): The width of the object. size (Union[int, float]): The height of the object. """ + width: Union[int, float] height: Union[int, float] @@ -83,13 +86,15 @@ class Chunk: :layer_data (List[List(int)]): The global tile IDs in chunky according to row. """ + location: OrderedPair width: int height: int chunk_data: List[List[int]] -class Image(NamedTuple): +@dataclasses.dataclass +class Image: """ Image object. @@ -104,9 +109,10 @@ class Image(NamedTuple): (optional, used for tile index correction when the image changes). :height (Optional[str]): The image height in pixels (optional). """ + source: str - size: Size - trans: Optional[Color] + size: Optional[Size] = None + trans: Optional[Color] = None Properties = Dict[str, Union[int, float, Color, Path, str]] @@ -120,6 +126,7 @@ class Grid(NamedTuple): determines how tile overlays for terrain and collision information are rendered. """ + orientation: str width: int height: int @@ -134,6 +141,7 @@ class Terrain(NamedTuple): :tile (int): The local tile-id of the tile that represents the terrain visually. """ + name: str tile: int @@ -150,6 +158,7 @@ class Frame(NamedTuple): :duration (int): How long in milliseconds this frame should be displayed before advancing to the next frame. """ + tile_id: int duration: int @@ -168,6 +177,7 @@ class TileTerrain: :bottom_left (Optional[int]): Bottom left terrain type. :bottom_right (Optional[int]): Bottom right terrain type. """ + top_left: Optional[int] = None top_right: Optional[int] = None bottom_left: Optional[int] = None @@ -246,13 +256,13 @@ class Layer(LayerType, _LayerBase): @dataclasses.dataclass -class _ObjectBase: +class _TiledObjectBase: id: int location: OrderedPair @dataclasses.dataclass -class _ObjectDefaults: +class _TiledObjectDefaults: size: Size = Size(0, 0) rotation: int = 0 opacity: int = 0xFF @@ -265,9 +275,9 @@ class _ObjectDefaults: @dataclasses.dataclass -class Object(_ObjectDefaults, _ObjectBase): +class TiledObject(_TiledObjectDefaults, _TiledObjectBase): """ - ObjectGroup Object. + TiledObjectGroup object. See: https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#object @@ -284,14 +294,14 @@ class Object(_ObjectDefaults, _ObjectBase): :opacity (int): The opacity of the object. (default: 255) :name (Optional[str]): The name of the object. :type (Optional[str]): The type of the object. - :properties (Properties): The properties of the Object. + :properties (Properties): The properties of the TiledObject. :template Optional[Template]: A reference to a Template object FIXME """ @dataclasses.dataclass -class RectangleObject(Object): +class RectangleObject(TiledObject): """ Rectangle shape defined by a point, width, and height. @@ -302,7 +312,7 @@ class RectangleObject(Object): @dataclasses.dataclass -class ElipseObject(Object): +class ElipseObject(TiledObject): """ Elipse shape defined by a point, width, and height. @@ -311,7 +321,7 @@ class ElipseObject(Object): @dataclasses.dataclass -class PointObject(Object): +class PointObject(TiledObject): """ Point defined by a point (x,y). @@ -320,12 +330,12 @@ class PointObject(Object): @dataclasses.dataclass -class _TileObjectBase(_ObjectBase): +class _TileImageObjectBase(_TiledObjectBase): gid: int @dataclasses.dataclass -class TileObject(Object, _TileObjectBase): +class TileImageObject(TiledObject, _TileImageObjectBase): """ Polygon shape defined by a set of connections between points. @@ -337,12 +347,12 @@ class TileObject(Object, _TileObjectBase): @dataclasses.dataclass -class _PointsObjectBase(_ObjectBase): +class _PointsObjectBase(_TiledObjectBase): points: List[OrderedPair] @dataclasses.dataclass -class PolygonObject(Object, _PointsObjectBase): +class PolygonObject(TiledObject, _PointsObjectBase): """ Polygon shape defined by a set of connections between points. @@ -354,7 +364,7 @@ class PolygonObject(Object, _PointsObjectBase): @dataclasses.dataclass -class PolylineObject(Object, _PointsObjectBase): +class PolylineObject(TiledObject, _PointsObjectBase): """ Polyline defined by a set of connections between points. @@ -368,13 +378,13 @@ class PolylineObject(Object, _PointsObjectBase): @dataclasses.dataclass -class _TextObjectBase(_ObjectBase): +class _TextObjectBase(_TiledObjectBase): text: str @dataclasses.dataclass -class _TextObjectDefaults(_ObjectDefaults): - font_family: str = 'sans-serif' +class _TextObjectDefaults(_TiledObjectDefaults): + font_family: str = "sans-serif" font_size: int = 16 wrap: bool = False color: Color = Color(0xFF, 0, 0, 0) @@ -383,12 +393,12 @@ class _TextObjectDefaults(_ObjectDefaults): underline: bool = False strike_out: bool = False kerning: bool = False - horizontal_align: str = 'left' - vertical_align: str = 'top' + horizontal_align: str = "left" + vertical_align: str = "top" @dataclasses.dataclass -class TextObject(Object, _TextObjectDefaults, _TextObjectBase): +class TextObject(TiledObject, _TextObjectDefaults, _TextObjectBase): """ Text object with associated settings. @@ -414,19 +424,19 @@ class TextObject(Object, _TextObjectDefaults, _TextObjectBase): @dataclasses.dataclass class _ObjectGroupBase(_LayerTypeBase): - objects: List[Object] + objects: List[TiledObject] @dataclasses.dataclass class _ObjectGroupDefaults(_LayerTypeDefaults): color: Optional[Color] = None - draw_order: Optional[str] = 'topdown' + draw_order: Optional[str] = "topdown" @dataclasses.dataclass class ObjectGroup(LayerType, _ObjectGroupDefaults, _ObjectGroupBase): """ - Object Group Object. + TiledObject Group Object. The object group is in fact a map layer, and is hence called \ “object layer” in Tiled. @@ -443,7 +453,8 @@ https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#objectgroup to 'topdown'. See: https://doc.mapeditor.org/en/stable/manual/objects/#changing-stacking-order for more info. - :objects (Dict[int, Object]): Dict Object objects by Object.id. + :objects (Dict[int, TiledObject]): Dict TiledObject objects by + TiledObject.id. """ @@ -488,12 +499,13 @@ class Tile: :animation (List[Frame]): Each tile can have exactly one animation associated with it. """ + id: int type: Optional[str] terrain: Optional[TileTerrain] animation: Optional[List[Frame]] image: Optional[Image] - hitboxes: Optional[List[Object]] + hitboxes: Optional[List[TiledObject]] @dataclasses.dataclass @@ -526,6 +538,7 @@ class TileSet: file. :tiles (Optional[Dict[int, Tile]]): Dict of Tile objects by Tile.id. """ + name: str max_tile_size: Size spacing: Optional[int] @@ -581,6 +594,7 @@ class TileMap: is a TileSet object. :layers List[LayerType]: List of layer objects by draw order. """ + parent_dir: Path version: str @@ -604,7 +618,7 @@ class TileMap: properties: Optional[Properties] = None -''' +""" [22:16] <__m4ch1n3__> i would "[i for i in int_list if i < littler_then_value]" [22:16] <__m4ch1n3__> it returns a list of integers below "littler_then_value" [22:17] <__m4ch1n3__> !py3 [i for i in [1,2,3,4,1,2,3,4] if i < 3] @@ -620,4 +634,4 @@ class TileMap: [22:23] __m4ch1n3__: 100 [22:23] == markb1 [~mbiggers@45.36.35.206] has quit [Ping timeout: 245 seconds] [22:23] <__m4ch1n3__> !py3 max(i for i in [1, 10, 100] if i < 242) -''' +""" diff --git a/pytiled_parser/parser.py b/pytiled_parser/parser.py index 79ecf11..356eef4 100644 --- a/pytiled_parser/parser.py +++ b/pytiled_parser/parser.py @@ -60,16 +60,20 @@ def _decode_csv_layer(data_text): lines = lines[:-1] for line in lines: line_list = line.split(",") - while '' in line_list: - line_list.remove('') + while "" in line_list: + line_list.remove("") line_list_int = [int(item) for item in line_list] tile_grid.append(line_list_int) return tile_grid -def _decode_data(element: etree.Element, layer_width: int, encoding: str, - compression: Optional[str]) -> List[List[int]]: +def _decode_data( + element: etree.Element, + layer_width: int, + encoding: str, + compression: Optional[str], +) -> List[List[int]]: """Decodes data or chunk data. Args: @@ -81,30 +85,29 @@ def _decode_data(element: etree.Element, layer_width: int, encoding: str, :compression (str): Compression format of the layer data. """ # etree.Element.text comes with an appended and a prepended '\n' - supported_encodings = ['base64', 'csv'] + supported_encodings = ["base64", "csv"] if encoding not in supported_encodings: - raise ValueError('{encoding} is not a valid encoding') + raise ValueError("{encoding} is not a valid encoding") - supported_compression = [None, 'gzip', 'zlib'] + supported_compression = [None, "gzip", "zlib"] if compression is not None: - if encoding != 'base64': - raise ValueError('{encoding} does not support compression') + if encoding != "base64": + raise ValueError("{encoding} does not support compression") if compression not in supported_compression: - raise ValueError('{compression} is not a valid compression type') + raise ValueError("{compression} is not a valid compression type") try: data_text = element.text # type: ignore except AttributeError: - raise AttributeError('{element} lacks layer data.') + raise AttributeError("{element} lacks layer data.") - if encoding == 'csv': + if encoding == "csv": return _decode_csv_layer(data_text) return _decode_base64_data(data_text, compression, layer_width) -def _parse_data(element: etree.Element, - layer_width: int) -> objects.LayerData: +def _parse_data(element: etree.Element, layer_width: int) -> objects.LayerData: """Parses layer data. Will parse CSV, base64, gzip-base64, or zlip-base64 encoded data. @@ -116,103 +119,109 @@ def _parse_data(element: etree.Element, Returns: :LayerData: Data object containing layer data or chunks of data. """ - encoding = element.attrib['encoding'] + encoding = element.attrib["encoding"] compression = None try: - compression = element.attrib['compression'] + compression = element.attrib["compression"] except KeyError: pass - chunk_elements = element.findall('./chunk') + chunk_elements = element.findall("./chunk") if chunk_elements: chunks: List[objects.Chunk] = [] for chunk_element in chunk_elements: - x = int(chunk_element.attrib['x']) - y = int(chunk_element.attrib['y']) + x = int(chunk_element.attrib["x"]) + y = int(chunk_element.attrib["y"]) location = objects.OrderedPair(x, y) - width = int(chunk_element.attrib['width']) - height = int(chunk_element.attrib['height']) - layer_data = _decode_data(chunk_element, layer_width, encoding, - compression) + width = int(chunk_element.attrib["width"]) + height = int(chunk_element.attrib["height"]) + layer_data = _decode_data( + chunk_element, layer_width, encoding, compression + ) chunks.append(objects.Chunk(location, width, height, layer_data)) return chunks return _decode_data(element, layer_width, encoding, compression) -def _parse_layer(element: etree.Element, - layer_type: objects.LayerType) -> objects.Layer: +def _parse_layer( + element: etree.Element, layer_type: objects.LayerType +) -> objects.Layer: """Parse layer element given.""" - width = int(element.attrib['width']) - height = int(element.attrib['height']) + width = int(element.attrib["width"]) + height = int(element.attrib["height"]) size = objects.OrderedPair(width, height) - data_element = element.find('./data') + data_element = element.find("./data") if data_element is not None: data: objects.LayerData = _parse_data(data_element, width) else: - raise ValueError('{element} has no child data element.') + raise ValueError("{element} has no child data element.") return objects.Layer(size, data, **layer_type.__dict__) def _parse_layer_type(layer_element: etree.Element) -> objects.LayerType: """Parse layer type element given.""" - id = int(layer_element.attrib['id']) + id = int(layer_element.attrib["id"]) - name = layer_element.attrib['name'] + name = layer_element.attrib["name"] layer_type_object = objects.LayerType(id, name) try: - offset_x = float(layer_element.attrib['offsetx']) + offset_x = float(layer_element.attrib["offsetx"]) except KeyError: offset_x = 0 try: - offset_y = float(layer_element.attrib['offsety']) + offset_y = float(layer_element.attrib["offsety"]) except KeyError: offset_y = 0 offset = objects.OrderedPair(offset_x, offset_y) try: layer_type_object.opacity = round( - float(layer_element.attrib['opacity']) * 255) + float(layer_element.attrib["opacity"]) * 255 + ) except KeyError: pass - properties_element = layer_element.find('./properties') + properties_element = layer_element.find("./properties") if properties_element is not None: layer_type_object.properties = _parse_properties_element( - properties_element) + properties_element + ) - if layer_element.tag == 'layer': + if layer_element.tag == "layer": return _parse_layer(layer_element, layer_type_object) - elif layer_element.tag == 'objectgroup': + elif layer_element.tag == "objectgroup": return _parse_object_group(layer_element, layer_type_object) # else: # return _parse_layer_group(layer_element, layer_type_object) -def _parse_objects(object_elements: List[etree.Element]) -> List[objects.Object]: +def _parse_objects( + object_elements: List[etree.Element] +) -> List[objects.TiledObject]: """ """ - tiled_objects: List[objects.Object] = [] + tiled_objects: List[objects.TiledObject] = [] for object_element in object_elements: - id = int(object_element.attrib['id']) - location_x = float(object_element.attrib['x']) - location_y = float(object_element.attrib['y']) + id = int(object_element.attrib["id"]) + location_x = float(object_element.attrib["x"]) + location_y = float(object_element.attrib["y"]) location = objects.OrderedPair(location_x, location_y) - object = objects.Object(id, location) + object = objects.TiledObject(id, location) try: - width = float(object_element.attrib['width']) + width = float(object_element.attrib["width"]) except KeyError: width = 0 try: - height = float(object_element.attrib['height']) + height = float(object_element.attrib["height"]) except KeyError: height = 0 @@ -220,21 +229,22 @@ def _parse_objects(object_elements: List[etree.Element]) -> List[objects.Object] try: object.opacity = round( - float(object_element.attrib['opacity']) * 255) + float(object_element.attrib["opacity"]) * 255 + ) except KeyError: pass try: - object.rotation = int(object_element.attrib['rotation']) + object.rotation = int(object_element.attrib["rotation"]) except KeyError: pass try: - object.name = object_element.attrib['name'] + object.name = object_element.attrib["name"] except KeyError: pass - properties_element = object_element.find('./properties') + properties_element = object_element.find("./properties") if properties_element is not None: object.properties = _parse_properties_element(properties_element) @@ -243,23 +253,24 @@ def _parse_objects(object_elements: List[etree.Element]) -> List[objects.Object] return tiled_objects -def _parse_object_group(element: etree.Element, - layer_type: objects.LayerType) -> objects.ObjectGroup: +def _parse_object_group( + element: etree.Element, layer_type: objects.LayerType +) -> objects.ObjectGroup: """Parse the objectgroup element given. Args: layer_type (objects.LayerType): """ - tiled_objects = _parse_objects(element.findall('./object')) + tiled_objects = _parse_objects(element.findall("./object")) object_group = objects.ObjectGroup(tiled_objects, **layer_type.__dict__) try: - color = utilities.parse_color(element.attrib['color']) + color = utilities.parse_color(element.attrib["color"]) except KeyError: pass try: - draw_order = element.attrib['draworder'] + draw_order = element.attrib["draworder"] except KeyError: pass @@ -267,85 +278,88 @@ def _parse_object_group(element: etree.Element, @functools.lru_cache() -def _parse_external_tile_set(parent_dir: Path, tile_set_element: etree.Element - ) -> objects.TileSet: +def _parse_external_tile_set( + parent_dir: Path, tile_set_element: etree.Element +) -> objects.TileSet: """Parses an external tile set. Caches the results to speed up subsequent instances. """ - source = Path(tile_set_element.attrib['source']) + source = Path(tile_set_element.attrib["source"]) tile_set_tree = etree.parse(str(parent_dir / Path(source))).getroot() return _parse_tile_set(tile_set_tree) -def _parse_hitboxes(element: etree.Element) -> List[objects.Object]: - return _parse_objects(element.findall('./object')) +def _parse_hitboxes(element: etree.Element) -> List[objects.TiledObject]: + return _parse_objects(element.findall("./object")) -def _parse_tiles(tile_element_list: List[etree.Element] - ) -> Dict[int, objects.Tile]: +def _parse_tiles( + tile_element_list: List[etree.Element] +) -> Dict[int, objects.Tile]: tiles: Dict[int, objects.Tile] = {} for tile_element in tile_element_list: - #id is not optional - id = int(tile_element.attrib['id']) + # id is not optional + id = int(tile_element.attrib["id"]) - #optional attributes + # optional attributes tile_type = None try: - tile_type = tile_element.attrib['type'] + tile_type = tile_element.attrib["type"] except KeyError: pass tile_terrain = None try: - tile_terrain_attrib = tile_element.attrib['terrain'] + tile_terrain_attrib = tile_element.attrib["terrain"] except KeyError: pass else: - #below is an attempt to explain how terrains are handled. + # below is an attempt to explain how terrains are handled. #'terrain' attribute is a comma seperated list of 4 values, - #each is either an integer or blank + # each is either an integer or blank - #convert to list of values - terrain_list_attrib = re.split(',', tile_terrain_attrib) - #terrain_list is list of indexes of Tileset.terrain_types + # convert to list of values + terrain_list_attrib = re.split(",", tile_terrain_attrib) + # terrain_list is list of indexes of Tileset.terrain_types terrain_list: List[Optional[int]] = [] - #each index in terrain_list_attrib reffers to a corner + # each index in terrain_list_attrib reffers to a corner for corner in terrain_list_attrib: - if corner == '': + if corner == "": terrain_list.append(None) else: terrain_list.append(int(corner)) tile_terrain = objects.TileTerrain(*terrain_list) - #tile element optional sub-elements + # tile element optional sub-elements animation: Optional[List[objects.Frame]] = None - tile_animation_element = tile_element.find('./animation') + tile_animation_element = tile_element.find("./animation") if tile_animation_element: animation = [] - frames = tile_animation_element.findall('./frame') + frames = tile_animation_element.findall("./frame") for frame in frames: # tileid reffers to the Tile.id of the animation frame - tile_id = int(frame.attrib['tileid']) + tile_id = int(frame.attrib["tileid"]) # duration is in MS. Should perhaps be converted to seconds. # FIXME: make decision - duration = int(frame.attrib['duration']) + duration = int(frame.attrib["duration"]) animation.append(objects.Frame(tile_id, duration)) # if this is None, then the Tile is part of a spritesheet tile_image = None - tile_image_element = tile_element.find('./image') + tile_image_element = tile_element.find("./image") if tile_image_element is not None: tile_image = _parse_image_element(tile_image_element) hitboxes = None - tile_hitboxes_element = tile_element.find('./objectgroup') + tile_hitboxes_element = tile_element.find("./objectgroup") if tile_hitboxes_element is not None: hitboxes = _parse_hitboxes(tile_hitboxes_element) - tiles[id] = objects.Tile(id, tile_type, tile_terrain, animation, - tile_image, hitboxes) + tiles[id] = objects.Tile( + id, tile_type, tile_terrain, animation, tile_image, hitboxes + ) return tiles @@ -356,23 +370,25 @@ def _parse_image_element(image_element: etree.Element) -> objects.Image: Returns: :Color: Color in Arcade's preffered format. """ - source = image_element.attrib['source'] + image = objects.Image(image_element.attrib["source"]) + + width_attrib = image_element.attrib.get("width") + height_attrib = image_element.attrib.get("height") + + if width_attrib and height_attrib: + image.size = objects.Size(int(width_attrib), int(height_attrib)) - trans = None try: - trans = utilities.parse_color(image_element.attrib['trans']) + image.trans = utilities.parse_color(image_element.attrib["trans"]) except KeyError: pass - width = int(image_element.attrib['width']) - height = int(image_element.attrib['height']) - size = objects.Size(width, height) - - return objects.Image(source, size, trans) + return image -def _parse_properties_element(properties_element: etree.Element - ) -> objects.Properties: +def _parse_properties_element( + properties_element: etree.Element +) -> objects.Properties: """Adds Tiled property to Properties dict. Args: @@ -385,29 +401,30 @@ def _parse_properties_element(properties_element: etree.Element :Properties: Properties Dict object. """ properties: objects.Properties = {} - for property_element in properties_element.findall('./property'): - name = property_element.attrib['name'] + for property_element in properties_element.findall("./property"): + name = property_element.attrib["name"] try: - property_type = property_element.attrib['type'] + property_type = property_element.attrib["type"] except KeyError: # strings do not have an attribute in property elements - property_type = 'string' - value = property_element.attrib['value'] + property_type = "string" + value = property_element.attrib["value"] - property_types = ['string', 'int', 'float', 'bool', 'color', 'file'] - assert property_type in property_types, ( - f"Invalid type for property {name}") + property_types = ["string", "int", "float", "bool", "color", "file"] + assert ( + property_type in property_types + ), f"Invalid type for property {name}" - if property_type == 'int': + if property_type == "int": properties[name] = int(value) - elif property_type == 'float': + elif property_type == "float": properties[name] = float(value) - elif property_type == 'color': + elif property_type == "color": properties[name] = utilities.parse_color(value) - elif property_type == 'file': + elif property_type == "file": properties[name] = Path(value) - elif property_type == 'bool': - if value == 'true': + elif property_type == "bool": + if value == "true": properties[name] = True else: properties[name] = False @@ -422,75 +439,86 @@ def _parse_tile_set(tile_set_element: etree.Element) -> objects.TileSet: Parses a tile set that is embedded into a TMX. """ # get all basic attributes - name = tile_set_element.attrib['name'] - max_tile_width = int(tile_set_element.attrib['tilewidth']) - max_tile_height = int(tile_set_element.attrib['tileheight']) + name = tile_set_element.attrib["name"] + max_tile_width = int(tile_set_element.attrib["tilewidth"]) + max_tile_height = int(tile_set_element.attrib["tileheight"]) max_tile_size = objects.OrderedPair(max_tile_width, max_tile_height) spacing = None try: - spacing = int(tile_set_element.attrib['spacing']) + spacing = int(tile_set_element.attrib["spacing"]) except KeyError: pass margin = None try: - margin = int(tile_set_element.attrib['margin']) + margin = int(tile_set_element.attrib["margin"]) except KeyError: pass tile_count = None try: - tile_count = int(tile_set_element.attrib['tilecount']) + tile_count = int(tile_set_element.attrib["tilecount"]) except KeyError: pass columns = None try: - columns = int(tile_set_element.attrib['columns']) + columns = int(tile_set_element.attrib["columns"]) except KeyError: pass tile_offset = None - tileoffset_element = tile_set_element.find('./tileoffset') + tileoffset_element = tile_set_element.find("./tileoffset") if tileoffset_element is not None: - tile_offset_x = int(tileoffset_element.attrib['x']) - tile_offset_y = int(tileoffset_element.attrib['y']) + tile_offset_x = int(tileoffset_element.attrib["x"]) + tile_offset_y = int(tileoffset_element.attrib["y"]) tile_offset = objects.OrderedPair(tile_offset_x, tile_offset_y) grid = None - grid_element = tile_set_element.find('./grid') + grid_element = tile_set_element.find("./grid") if grid_element is not None: - grid_orientation = grid_element.attrib['orientation'] - grid_width = int(grid_element.attrib['width']) - grid_height = int(grid_element.attrib['height']) + grid_orientation = grid_element.attrib["orientation"] + grid_width = int(grid_element.attrib["width"]) + grid_height = int(grid_element.attrib["height"]) grid = objects.Grid(grid_orientation, grid_width, grid_height) properties = None - properties_element = tile_set_element.find('./properties') + properties_element = tile_set_element.find("./properties") if properties_element is not None: properties = _parse_properties_element(properties_element) terrain_types: Optional[List[objects.Terrain]] = None - terrain_types_element = tile_set_element.find('./terraintypes') + terrain_types_element = tile_set_element.find("./terraintypes") if terrain_types_element is not None: terrain_types = [] - for terrain in terrain_types_element.findall('./terrain'): - name = terrain.attrib['name'] - terrain_tile = int(terrain.attrib['tile']) + for terrain in terrain_types_element.findall("./terrain"): + name = terrain.attrib["name"] + terrain_tile = int(terrain.attrib["tile"]) terrain_types.append(objects.Terrain(name, terrain_tile)) image = None - image_element = tile_set_element.find('./image') + image_element = tile_set_element.find("./image") if image_element is not None: image = _parse_image_element(image_element) - tile_element_list = tile_set_element.findall('./tile') + tile_element_list = tile_set_element.findall("./tile") tiles = _parse_tiles(tile_element_list) - return objects.TileSet(name, max_tile_size, spacing, margin, tile_count, - columns, tile_offset, grid, properties, image, - terrain_types, tiles) + return objects.TileSet( + name, + max_tile_size, + spacing, + margin, + tile_count, + columns, + tile_offset, + grid, + properties, + image, + terrain_types, + tiles, + ) def parse_tile_map(tmx_file: Union[str, Path]): @@ -501,26 +529,26 @@ def parse_tile_map(tmx_file: Union[str, Path]): # positional arguments for TileMap parent_dir = Path(tmx_file).parent - version = map_element.attrib['version'] - tiled_version = map_element.attrib['tiledversion'] - orientation = map_element.attrib['orientation'] - render_order = map_element.attrib['renderorder'] - map_width = int(map_element.attrib['width']) - map_height = int(map_element.attrib['height']) + version = map_element.attrib["version"] + tiled_version = map_element.attrib["tiledversion"] + orientation = map_element.attrib["orientation"] + render_order = map_element.attrib["renderorder"] + map_width = int(map_element.attrib["width"]) + map_height = int(map_element.attrib["height"]) map_size = objects.OrderedPair(map_width, map_height) - tile_width = int(map_element.attrib['tilewidth']) - tile_height = int(map_element.attrib['tileheight']) + tile_width = int(map_element.attrib["tilewidth"]) + tile_height = int(map_element.attrib["tileheight"]) tile_size = objects.OrderedPair(tile_width, tile_height) - infinite_attribute = map_element.attrib['infinite'] - infinite = True if infinite_attribute == 'true' else False + infinite_attribute = map_element.attrib["infinite"] + infinite = True if infinite_attribute == "true" else False - next_layer_id = int(map_element.attrib['nextlayerid']) - next_object_id = int(map_element.attrib['nextobjectid']) + next_layer_id = int(map_element.attrib["nextlayerid"]) + next_object_id = int(map_element.attrib["nextobjectid"]) # parse all tilesets tile_sets: Dict[int, objects.TileSet] = {} - tile_set_element_list = map_element.findall('./tileset') + tile_set_element_list = map_element.findall("./tileset") for tile_set_element in tile_set_element_list: # tiled docs are ambiguous about the 'firstgid' attribute # current understanding is for the purposes of mapping the layer @@ -529,56 +557,67 @@ def parse_tile_map(tmx_file: Union[str, Path]): # tile set as they pertain to the map, not tile set specific as # the tiled docs can make it seem # 'firstgid' is saved beside each TileMap - firstgid = int(tile_set_element.attrib['firstgid']) + firstgid = int(tile_set_element.attrib["firstgid"]) try: # check if is an external TSX - source = tile_set_element.attrib['source'] + source = tile_set_element.attrib["source"] except KeyError: # the tile set in embedded - name = tile_set_element.attrib['name'] + name = tile_set_element.attrib["name"] tile_sets[firstgid] = _parse_tile_set(tile_set_element) else: # tile set is external tile_sets[firstgid] = _parse_external_tile_set( - parent_dir, tile_set_element) + parent_dir, tile_set_element + ) # parse all layers layers: List[objects.LayerType] = [] - layer_tags = ['layer', 'objectgroup', 'group'] - for element in map_element.findall('./'): + layer_tags = ["layer", "objectgroup", "group"] + for element in map_element.findall("./"): if element.tag not in layer_tags: # only layer_tags are layer elements continue layers.append(_parse_layer_type(element)) - tile_map = objects.TileMap(parent_dir, version, tiled_version, - orientation, render_order, map_size, tile_size, - infinite, next_layer_id, next_object_id, - tile_sets, layers) + tile_map = objects.TileMap( + parent_dir, + version, + tiled_version, + orientation, + render_order, + map_size, + tile_size, + infinite, + next_layer_id, + next_object_id, + tile_sets, + layers, + ) try: - tile_map.hex_side_length = int(map_element.attrib['hexsidelength']) + tile_map.hex_side_length = int(map_element.attrib["hexsidelength"]) except KeyError: pass try: - tile_map.stagger_axis = int(map_element.attrib['staggeraxis']) + tile_map.stagger_axis = int(map_element.attrib["staggeraxis"]) except KeyError: pass try: - tile_map.stagger_index = int(map_element.attrib['staggerindex']) + tile_map.stagger_index = int(map_element.attrib["staggerindex"]) except KeyError: pass try: - backgroundcolor = map_element.attrib['backgroundcolor'] + backgroundcolor = map_element.attrib["backgroundcolor"] except KeyError: pass else: tile_map.background_color = utilities.parse_color(backgroundcolor) - properties_element = map_tree.find('./properties') + properties_element = map_tree.find("./properties") if properties_element is not None: tile_map.properties = _parse_properties_element(properties_element) diff --git a/test/output.py b/test/output.py index 2b5305f..4faf5ab 100644 --- a/test/output.py +++ b/test/output.py @@ -1,25 +1,177 @@ -{ 'background_color': None, - 'height': 6, - 'hex_side_length': None, - 'infinite': False, - 'layers': [ Layer(width=8, height=6, data=[[1, 2, 3, 4, 5, 6, 7, 8], [9, 10, 11, 12, 13, 14, 15, 16], [17, 18, 19, 20, 21, 22, 23, 24], [25, 26, 27, 28, 29, 30, 31, 32], [33, 34, 35, 36, 37, 38, 39, 40], [41, 42, 43, 44, 45, 46, 47, 48]], id=1, name='Tile Layer 1', offset=OrderedPair(x=0, y=0), opacity=255, properties=None)], - 'next_layer_id': 2, - 'next_object_id': 1, - 'orientation': 'orthogonal', - 'parent_dir': PosixPath('/home/ben/Projects/pytiled_parser/pytiled_parser-venv/pytiled_parser/tests/test_data'), - 'properties': { 'bool property - false': False, - 'bool property - true': True, - 'color property': Color(red=73, green=252, blue=255, alpha=255), - 'file property': PosixPath('/var/log/syslog'), - 'float property': 1.23456789, - 'int property': 13, - 'string property': 'Hello, World!!'}, - 'render_order': 'right-down', - 'stagger_axis': None, - 'stagger_index': None, - 'tile_height': 32, - 'tile_sets': { 1: TileSet(name='tile_set_image', max_tile_size=OrderedPair(x=32, y=32), spacing=1, margin=1, tile_count=48, columns=8, tile_offset=None, grid=None, properties=None, image=Image(source='images/tmw_desert_spacing.png', trans=None, width=265, height=199), terrain_types=None, tiles={})}, - 'tile_width': 32, - 'tiled_version': '1.2.3', - 'version': '1.2', - 'width': 8} +{ + "background_color": None, + "hex_side_length": None, + "infinite": False, + "layers": [ + Layer( + size=OrderedPair(x=8, y=6), + data=[ + [1, 2, 3, 4, 5, 6, 7, 8], + [9, 10, 11, 12, 13, 14, 15, 16], + [17, 18, 19, 20, 21, 22, 23, 24], + [25, 26, 27, 28, 29, 30, 31, 32], + [33, 34, 35, 36, 37, 38, 39, 40], + [41, 42, 43, 44, 45, 46, 47, 48], + ], + id=1, + name="Tile Layer 1", + offset=OrderedPair(x=0, y=0), + opacity=255, + properties=None, + ) + ], + "map_size": OrderedPair(x=8, y=6), + "next_layer_id": 2, + "next_object_id": 1, + "orientation": "orthogonal", + "parent_dir": PosixPath( + "/home/benk/Projects/pytiled_parser/venv/pytiled_parser/tests/test_data" + ), + "properties": { + "bool property - false": False, + "bool property - true": True, + "color property": Color(red=73, green=252, blue=255, alpha=255), + "file property": PosixPath("../../../../../../../../var/log/syslog"), + "float property": 1.23456789, + "int property": 13, + "string property": "Hello, World!!", + }, + "render_order": "right-down", + "stagger_axis": None, + "stagger_index": None, + "tile_sets": { + 1: TileSet( + name="tile_set_image", + max_tile_size=OrderedPair(x=32, y=32), + spacing=1, + margin=1, + tile_count=48, + columns=8, + tile_offset=None, + grid=None, + properties=None, + image=Image( + source="images/tmw_desert_spacing.png", + size=Size(width=265, height=199), + trans=None, + ), + terrain_types=None, + tiles={ + 9: Tile( + id=9, + type=None, + terrain=None, + animation=None, + image=None, + hitboxes=[ + Object( + id=2, + location=OrderedPair(x=1.0, y=1.0), + size=Size(width=32.0, height=32.0), + rotation=1, + opacity=255, + name="wall", + type=None, + properties=None, + template=None, + ) + ], + ), + 19: Tile( + id=19, + type=None, + terrain=None, + animation=None, + image=None, + hitboxes=[ + Object( + id=1, + location=OrderedPair(x=32.0, y=1.0), + size=Size(width=0, height=0), + rotation=1, + opacity=255, + name="wall corner", + type=None, + properties=None, + template=None, + ) + ], + ), + 20: Tile( + id=20, + type=None, + terrain=None, + animation=None, + image=None, + hitboxes=[ + Object( + id=1, + location=OrderedPair(x=1.45455, y=1.45455), + size=Size(width=0, height=0), + rotation=1, + opacity=255, + name="polyline", + type=None, + properties=None, + template=None, + ) + ], + ), + 31: Tile( + id=31, + type=None, + terrain=None, + animation=None, + image=None, + hitboxes=[ + Object( + id=1, + location=OrderedPair(x=5.09091, y=2.54545), + size=Size(width=19.6364, height=19.2727), + rotation=1, + opacity=255, + name="rock 1", + type=None, + properties=None, + template=None, + ), + Object( + id=2, + location=OrderedPair(x=16.1818, y=22.0), + size=Size(width=8.54545, height=8.36364), + rotation=-1, + opacity=255, + name="rock 2", + type=None, + properties=None, + template=None, + ), + ], + ), + 45: Tile( + id=45, + type=None, + terrain=None, + animation=None, + image=None, + hitboxes=[ + Object( + id=1, + location=OrderedPair(x=14.7273, y=26.3636), + size=Size(width=0, height=0), + rotation=0, + opacity=255, + name="sign", + type=None, + properties=None, + template=None, + ) + ], + ), + }, + ) + }, + "tile_size": OrderedPair(x=32, y=32), + "tiled_version": "1.2.3", + "version": "1.2", +} diff --git a/test/test_tiled.py b/test/test_tiled.py index 431e072..3a6dde2 100644 --- a/test/test_tiled.py +++ b/test/test_tiled.py @@ -10,7 +10,7 @@ pp = pprint.PrettyPrinter(indent=4, compact=True, width=100) pp = pp.pprint -MAP_NAME = '/home/ben/Projects/pytiled_parser/pytiled_parser-venv/pytiled_parser/tests/test_data/test_map_simple.tmx' +MAP_NAME = '/home/benk/Projects/pytiled_parser/venv/pytiled_parser/tests/test_data/test_map_simple_hitboxes.tmx' map = pytiled_parser.parse_tile_map(MAP_NAME) diff --git a/tests/test_data/test_map_simple_hitboxes.tmx b/tests/test_data/test_map_simple_hitboxes.tmx new file mode 100644 index 0000000..f197ed6 --- /dev/null +++ b/tests/test_data/test_map_simple_hitboxes.tmx @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + +1,2,3,4,5,6,7,8, +9,10,11,12,13,14,15,16, +17,18,19,20,21,22,23,24, +25,26,27,28,29,30,31,32, +33,34,35,36,37,38,39,40, +41,42,43,44,45,46,47,48 + + +