merge changes made by pvcraven

This commit is contained in:
Benjamin Kirkbride
2019-06-29 17:38:45 -04:00
parent 5b62df0409
commit 73f5223d3d
4 changed files with 112 additions and 84 deletions

View File

@@ -2,7 +2,6 @@
pytiled_parser objects for Tiled maps. pytiled_parser objects for Tiled maps.
""" """
import functools import functools
import re import re
import xml.etree.ElementTree as etree import xml.etree.ElementTree as etree
@@ -41,6 +40,18 @@ class OrderedPair(NamedTuple):
y: Union[int, float] y: Union[int, float]
class Property(NamedTuple):
"""OrderedPair NamedTuple.
Attributes:
name str: Name of property
value str: Value of property
"""
name: str
value: str
class Size(NamedTuple): class Size(NamedTuple):
"""Size NamedTuple. """Size NamedTuple.
@@ -194,7 +205,7 @@ class Layer:
for more info. for more info.
""" """
id: int id_: int
name: str name: str
offset: Optional[OrderedPair] offset: Optional[OrderedPair]
@@ -238,9 +249,10 @@ class TiledObject:
https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#object https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#object
Args: Args:
:id (int): Unique ID of the object. Each object that is placed on a :id_ (int): Unique ID of the object. Each object that is placed on a
map gets a unique id. Even if an object was deleted, no object map gets a unique id. Even if an object was deleted, no object
gets the same ID. gets the same ID.
:gid (Optional[int]): Global tiled object ID
:location (OrderedPair): The location of the object in pixels. :location (OrderedPair): The location of the object in pixels.
:size (Size): The width of the object in pixels :size (Size): The width of the object in pixels
(default: (0, 0)). (default: (0, 0)).
@@ -250,13 +262,13 @@ class TiledObject:
:name (Optional[str]): The name of the object. :name (Optional[str]): The name of the object.
:type (Optional[str]): The type of the object. :type (Optional[str]): The type of the object.
:properties (Properties): The properties of the TiledObject. :properties (Properties): The properties of the TiledObject.
:template Optional[Template]: A reference to a Template object :template Optional[Template]: A reference to a Template object FIXME
FIXME
""" """
id: int id_: int
location: OrderedPair gid: Optional[int] = None
location: OrderedPair
size: Size = Size(0, 0) size: Size = Size(0, 0)
rotation: int = 0 rotation: int = 0
opacity: float = 1 opacity: float = 1
@@ -427,35 +439,6 @@ class LayerGroup(Layer):
layers: Optional[List[Union["LayerGroup", Layer, ObjectLayer]]] layers: Optional[List[Union["LayerGroup", Layer, ObjectLayer]]]
@attr.s(auto_attribs=True)
class Hitbox:
"""Group of hitboxes for FIXME
"""
@attr.s(auto_attribs=True)
class Tile:
"""
Individual tile object.
Args:
:id (int): The local tile ID within its tileset.
:type (str): The type of the tile. Refers to an object type and is
used by tile objects.
:terrain (int): Defines the terrain type of each corner of the tile.
:animation (List[Frame]): Each tile can have exactly one animation
associated with it.
"""
id: int
type: Optional[str] = None
terrain: Optional[TileTerrain] = None
animation: Optional[List[Frame]] = None
image: Optional[Image] = None
hitboxes: Optional[List[TiledObject]] = None
@attr.s(auto_attribs=True) @attr.s(auto_attribs=True)
class TileSet: class TileSet:
""" """
@@ -499,12 +482,35 @@ class TileSet:
properties: Optional[Properties] = None properties: Optional[Properties] = None
image: Optional[Image] = None image: Optional[Image] = None
terrain_types: Optional[List[Terrain]] = None terrain_types: Optional[List[Terrain]] = None
tiles: Optional[Dict[int, Tile]] = None tiles: Optional[Dict[int, "Tile"]] = None
TileSetDict = Dict[int, TileSet] TileSetDict = Dict[int, TileSet]
@attr.s(auto_attribs=True, kw_only=True)
class Tile:
"""
Individual tile object.
Args:
:id (int): The local tile ID within its tileset.
:type (str): The type of the tile. Refers to an object type and is
used by tile objects.
:terrain (int): Defines the terrain type of each corner of the tile.
:animation (List[Frame]): Each tile can have exactly one animation
associated with it.
"""
id_: int
type_: Optional[str] = None
terrain: Optional[TileTerrain] = None
animation: Optional[List[Frame]] = None
image: Optional[Image] = None
properties: Optional[List[Property]] = None
tileset: Optional[TileSet] = None
@attr.s(auto_attribs=True) @attr.s(auto_attribs=True)
class TileMap: class TileMap:
""" """

View File

@@ -54,7 +54,7 @@ def _decode_csv_data(data_text: str) -> List[List[int]]:
""" """
tile_grid = [] tile_grid = []
lines: List[str] = data_text.split("\n") lines: List[str] = data_text.split("\n")
# remove erronious empty lists due to a newline being on both ends of text # remove erroneous empty lists due to a newline being on both ends of text
lines = lines[1:-1] lines = lines[1:-1]
for line in lines: for line in lines:
line_list = line.split(",") line_list = line.split(",")
@@ -160,7 +160,7 @@ def _parse_layer(
Returns: Returns:
FIXME FIXME
""" """
id = int(layer_element.attrib["id"]) id_ = int(layer_element.attrib["id"])
name = layer_element.attrib["name"] name = layer_element.attrib["name"]
@@ -197,7 +197,7 @@ def _parse_layer(
else: else:
properties = None properties = None
return id, name, offset, opacity, properties return id_, name, offset, opacity, properties
def _parse_tile_layer(element: etree.Element,) -> objects.TileLayer: def _parse_tile_layer(element: etree.Element,) -> objects.TileLayer:
@@ -209,7 +209,7 @@ def _parse_tile_layer(element: etree.Element,) -> objects.TileLayer:
Returns: Returns:
TileLayer: The tile layer object. TileLayer: The tile layer object.
""" """
id, name, offset, opacity, properties = _parse_layer(element) id_, name, offset, opacity, properties = _parse_layer(element)
width = int(element.attrib["width"]) width = int(element.attrib["width"])
height = int(element.attrib["height"]) height = int(element.attrib["height"])
@@ -222,7 +222,7 @@ def _parse_tile_layer(element: etree.Element,) -> objects.TileLayer:
raise ValueError(f"{element} has no child data element.") raise ValueError(f"{element} has no child data element.")
return objects.TileLayer( return objects.TileLayer(
id=id, id_=id_,
name=name, name=name,
offset=offset, offset=offset,
opacity=opacity, opacity=opacity,
@@ -246,12 +246,17 @@ def _parse_objects(
tiled_objects: List[objects.TiledObject] = [] tiled_objects: List[objects.TiledObject] = []
for object_element in object_elements: for object_element in object_elements:
id = int(object_element.attrib["id"]) id_ = int(object_element.attrib["id"])
location_x = float(object_element.attrib["x"]) location_x = float(object_element.attrib["x"])
location_y = float(object_element.attrib["y"]) location_y = float(object_element.attrib["y"])
location = objects.OrderedPair(location_x, location_y) location = objects.OrderedPair(location_x, location_y)
tiled_object = objects.TiledObject(id=id, location=location) tiled_object = objects.TiledObject(id_=id_, location=location)
try:
tiled_object.gid = int(object_element.attrib["gid"])
except KeyError:
tiled_object.gid = None
try: try:
width = float(object_element.attrib["width"]) width = float(object_element.attrib["width"])
@@ -310,22 +315,24 @@ def _parse_object_layer(element: etree.Element,) -> objects.ObjectLayer:
Returns: Returns:
ObjectLayer: The object layer object. ObjectLayer: The object layer object.
""" """
id, name, offset, opacity, properties = _parse_layer(element) id_, name, offset, opacity, properties = _parse_layer(element)
tiled_objects = _parse_objects(element.findall("./object")) tiled_objects = _parse_objects(element.findall("./object"))
color = None
try: try:
color = element.attrib["color"] color = element.attrib["color"]
except KeyError: except KeyError:
pass pass
draw_order = None
try: try:
draw_order = element.attrib["draworder"] draw_order = element.attrib["draworder"]
except KeyError: except KeyError:
pass pass
return objects.ObjectLayer( return objects.ObjectLayer(
id=id, id_=id_,
name=name, name=name,
offset=offset, offset=offset,
opacity=opacity, opacity=opacity,
@@ -350,12 +357,12 @@ def _parse_layer_group(element: etree.Element,) -> objects.LayerGroup:
Returns: Returns:
LayerGroup: The layer group object. LayerGroup: The layer group object.
""" """
id, name, offset, opacity, properties = _parse_layer(element) id_, name, offset, opacity, properties = _parse_layer(element)
layers = _get_layers(element) layers = _get_layers(element)
return objects.LayerGroup( return objects.LayerGroup(
id=id, id_=id_,
name=name, name=name,
offset=offset, offset=offset,
opacity=opacity, opacity=opacity,
@@ -426,23 +433,18 @@ def _parse_external_tile_set(
return _parse_tile_set(tile_set_tree) return _parse_tile_set(tile_set_tree)
def _parse_hitboxes(element: etree.Element) -> List[objects.TiledObject]:
"""Parses all hitboxes for a given tile."""
return _parse_objects(element.findall("./object"))
def _parse_tiles( def _parse_tiles(
tile_element_list: List[etree.Element] tile_element_list: List[etree.Element]
) -> Dict[int, objects.Tile]: ) -> Dict[int, objects.Tile]:
tiles: Dict[int, objects.Tile] = {} tiles: Dict[int, objects.Tile] = {}
for tile_element in tile_element_list: for tile_element in tile_element_list:
# id is not optional # id is not optional
id = int(tile_element.attrib["id"]) id_ = int(tile_element.attrib["id"])
# optional attributes # optional attributes
tile_type = None _type = None
try: try:
tile_type = tile_element.attrib["type"] _type = tile_element.attrib["type"]
except KeyError: except KeyError:
pass pass
@@ -453,20 +455,32 @@ def _parse_tiles(
pass pass
else: 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, # '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 # convert to list of values
terrain_list_attrib = re.split(",", tile_terrain_attrib) terrain_list_attrib = re.split(",", tile_terrain_attrib)
# terrain_list is list of indexes of Tileset.terrain_types # terrain_list is list of indexes of Tileset.terrain_types
terrain_list: List[Optional[int]] = [] terrain_list: List[Optional[int]] = []
# each index in terrain_list_attrib reffers to a corner # each index in terrain_list_attrib refers to a corner
for corner in terrain_list_attrib: for corner in terrain_list_attrib:
if corner == "": if corner == "":
terrain_list.append(None) terrain_list.append(None)
else: else:
terrain_list.append(int(corner)) terrain_list.append(int(corner))
tile_terrain = objects.TileTerrain(*terrain_list) terrain = objects.TileTerrain(*terrain_list)
# tile element optional sub-elements
properties: Optional[List[objects.Property]] = None
tile_properties_element = tile_element.find("./properties")
if tile_properties_element:
properties = []
property_list = tile_properties_element.findall("./property")
for property_ in property_list:
name = property_.attrib["name"]
value = property_.attrib["value"]
obj = objects.Property(name, value)
properties.append(obj)
# tile element optional sub-elements # tile element optional sub-elements
animation: Optional[List[objects.Frame]] = None animation: Optional[List[objects.Frame]] = None
@@ -475,26 +489,27 @@ def _parse_tiles(
animation = [] animation = []
frames = tile_animation_element.findall("./frame") frames = tile_animation_element.findall("./frame")
for frame in frames: for frame in frames:
# tileid reffers to the Tile.id of the animation frame # tileid refers to the Tile.id of the animation frame
tile_id = int(frame.attrib["tileid"]) id_ = int(frame.attrib["tileid"])
# duration is in MS. Should perhaps be converted to seconds. # duration is in MS. Should perhaps be converted to seconds.
# FIXME: make decision # FIXME: make decision
duration = int(frame.attrib["duration"]) duration = int(frame.attrib["duration"])
animation.append(objects.Frame(tile_id, duration)) animation.append(objects.Frame(id_, duration))
# if this is None, then the Tile is part of a spritesheet # if this is None, then the Tile is part of a spritesheet
tile_image = None image = None
tile_image_element = tile_element.find("./image") image_element = tile_element.find("./image")
if tile_image_element is not None: if image_element is not None:
tile_image = _parse_image_element(tile_image_element) image = _parse_image_element(image_element)
hitboxes = None tiles[id_] = objects.Tile(
tile_hitboxes_element = tile_element.find("./objectgroup") id_=id_,
if tile_hitboxes_element is not None: type_=_type,
hitboxes = _parse_hitboxes(tile_hitboxes_element) terrain=terrain,
animation=animation,
tiles[id] = objects.Tile( image=image,
id, tile_type, tile_terrain, animation, tile_image, hitboxes properties=properties,
tileset=None,
) )
return tiles return tiles
@@ -504,7 +519,7 @@ def _parse_image_element(image_element: etree.Element) -> objects.Image:
"""Parse image element given. """Parse image element given.
Returns: Returns:
: Color in Arcade's preffered format. : Color in Arcade's preferred format.
""" """
image = objects.Image(image_element.attrib["source"]) image = objects.Image(image_element.attrib["source"])
@@ -641,7 +656,7 @@ def _parse_tile_set(tile_set_element: etree.Element) -> objects.TileSet:
tile_element_list = tile_set_element.findall("./tile") tile_element_list = tile_set_element.findall("./tile")
tiles = _parse_tiles(tile_element_list) tiles = _parse_tiles(tile_element_list)
return objects.TileSet( tileset = objects.TileSet(
name, name,
max_tile_size, max_tile_size,
spacing, spacing,
@@ -656,6 +671,13 @@ def _parse_tile_set(tile_set_element: etree.Element) -> objects.TileSet:
tiles, tiles,
) )
# Go back and create a circular link so tiles know what tileset they are
# part of. Needed for animation.
for my_id, my_tile in tiles.items():
my_tile.tileset = tileset
return tileset
def _get_tile_sets( def _get_tile_sets(
map_element: etree.Element, parent_dir: Path map_element: etree.Element, parent_dir: Path

View File

@@ -175,7 +175,7 @@ def create_tile_set(qty_of_tiles):
tiles = {} tiles = {}
for tile_id in range(qty_of_tiles): for tile_id in range(qty_of_tiles):
tiles[tile_id] = objects.Tile(tile_id) tiles[tile_id] = objects.Tile(id_=tile_id)
tile_set.tiles = tiles tile_set.tiles = tiles
@@ -184,14 +184,14 @@ def create_tile_set(qty_of_tiles):
tile_by_gid = [ tile_by_gid = [
(1, {1: create_tile_set(0)}, None), (1, {1: create_tile_set(0)}, None),
(1, {1: create_tile_set(1)}, objects.Tile(0)), (1, {1: create_tile_set(1)}, objects.Tile(id_=0)),
(1, {1: create_tile_set(2)}, objects.Tile(0)), (1, {1: create_tile_set(2)}, objects.Tile(id_=0)),
(2, {1: create_tile_set(1)}, None), (2, {1: create_tile_set(1)}, None),
(10, {1: create_tile_set(10)}, objects.Tile(9)), (10, {1: create_tile_set(10)}, objects.Tile(id_=9)),
(1, {1: create_tile_set(1), 2: create_tile_set(1)}, objects.Tile(0)), (1, {1: create_tile_set(1), 2: create_tile_set(1)}, objects.Tile(id_=0)),
(2, {1: create_tile_set(1), 2: create_tile_set(1)}, objects.Tile(0)), (2, {1: create_tile_set(1), 2: create_tile_set(1)}, objects.Tile(id_=0)),
(3, {1: create_tile_set(1), 2: create_tile_set(1)}, None), (3, {1: create_tile_set(1), 2: create_tile_set(1)}, None),
(15, {1: create_tile_set(5), 6: create_tile_set(10)}, objects.Tile(9)), (15, {1: create_tile_set(5), 6: create_tile_set(10)}, objects.Tile(id_=9)),
( (
20, 20,
{ {
@@ -199,7 +199,7 @@ tile_by_gid = [
6: create_tile_set(10), 6: create_tile_set(10),
16: create_tile_set(10), 16: create_tile_set(10),
}, },
objects.Tile(4), objects.Tile(id_=4),
), ),
] ]

View File

@@ -74,7 +74,7 @@ def test_map_simple():
[33, 34, 35, 36, 37, 38, 39, 40], [33, 34, 35, 36, 37, 38, 39, 40],
[41, 42, 43, 44, 45, 46, 47, 48], [41, 42, 43, 44, 45, 46, 47, 48],
] ]
assert map.layers[0].id == 1 assert map.layers[0].id_ == 1
assert map.layers[0].name == "Tile Layer 1" assert map.layers[0].name == "Tile Layer 1"
assert map.layers[0].offset == None assert map.layers[0].offset == None
assert map.layers[0].opacity == None assert map.layers[0].opacity == None