mirror of
https://github.com/OMGeeky/pytiled_parser.git
synced 2026-02-23 15:49:52 +01:00
added parsing of hitboxes
This commit is contained in:
@@ -11,30 +11,23 @@ from pathlib import Path
|
||||
|
||||
import xml.etree.ElementTree as etree
|
||||
|
||||
from typing import * # pylint: disable=W0401
|
||||
from typing import NamedTuple, Union, Optional, List, Dict
|
||||
|
||||
|
||||
class EncodingError(Exception):
|
||||
"""
|
||||
Tmx layer encoding is of an unknown type.
|
||||
"""
|
||||
"""Tmx layer encoding is of an unknown type."""
|
||||
|
||||
|
||||
class TileNotFoundError(Exception):
|
||||
"""
|
||||
Tile not found in tileset.
|
||||
"""
|
||||
"""Tile not found in tileset."""
|
||||
|
||||
|
||||
class ImageNotFoundError(Exception):
|
||||
"""
|
||||
Image not found.
|
||||
"""
|
||||
"""Image not found."""
|
||||
|
||||
|
||||
class Color(NamedTuple):
|
||||
"""
|
||||
Color object.
|
||||
"""Color object.
|
||||
|
||||
Attributes:
|
||||
:red (int): Red, between 1 and 255.
|
||||
@@ -49,17 +42,27 @@ class Color(NamedTuple):
|
||||
|
||||
|
||||
class OrderedPair(NamedTuple):
|
||||
"""
|
||||
OrderedPair NamedTuple.
|
||||
"""OrderedPair NamedTuple.
|
||||
|
||||
Attributes:
|
||||
:x (Union[int, float]): X coordinate.
|
||||
:y (Union[int, float]): Y coordinate.
|
||||
x (Union[int, float]): X coordinate.
|
||||
y (Union[int, float]): Y coordinate.
|
||||
"""
|
||||
x: Union[int, float]
|
||||
y: Union[int, float]
|
||||
|
||||
|
||||
class Size(NamedTuple):
|
||||
"""Size NamedTuple.
|
||||
|
||||
Attributes:
|
||||
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]
|
||||
|
||||
|
||||
class Template:
|
||||
"""
|
||||
FIXME TODO
|
||||
@@ -102,7 +105,7 @@ class Image(NamedTuple):
|
||||
:height (Optional[str]): The image height in pixels (optional).
|
||||
"""
|
||||
source: str
|
||||
size: OrderedPair
|
||||
size: Size
|
||||
trans: Optional[Color]
|
||||
|
||||
|
||||
@@ -222,7 +225,7 @@ Either a 2 dimensional array of integers representing the global tile IDs
|
||||
|
||||
@dataclasses.dataclass
|
||||
class _LayerBase:
|
||||
size: OrderedPair
|
||||
size: Size
|
||||
data: LayerData
|
||||
|
||||
|
||||
@@ -234,7 +237,7 @@ class Layer(LayerType, _LayerBase):
|
||||
See: https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#layer
|
||||
|
||||
Attributes:
|
||||
:size (OrderedPair): The width of the layer in tiles. Always the same
|
||||
:size (Size): The width of the layer in tiles. Always the same
|
||||
as the map width for not infitite maps.
|
||||
:data (LayerData): Either an 2 dimensional array of integers
|
||||
representing the global tile IDs for the map layer, or a list of
|
||||
@@ -250,7 +253,7 @@ class _ObjectBase:
|
||||
|
||||
@dataclasses.dataclass
|
||||
class _ObjectDefaults:
|
||||
size: OrderedPair = OrderedPair(0, 0)
|
||||
size: Size = Size(0, 0)
|
||||
rotation: int = 0
|
||||
opacity: int = 0xFF
|
||||
|
||||
@@ -266,15 +269,15 @@ class Object(_ObjectDefaults, _ObjectBase):
|
||||
"""
|
||||
ObjectGroup Object.
|
||||
|
||||
See: \
|
||||
https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#object
|
||||
See:
|
||||
https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#object
|
||||
|
||||
Args:
|
||||
: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
|
||||
gets the same ID.
|
||||
:location (OrderedPair): The location of the object in pixels.
|
||||
:size (OrderedPair): The width of the object in pixels
|
||||
:size (Size): The width of the object in pixels
|
||||
(default: (0, 0)).
|
||||
:rotation (int): The rotation of the object in degrees clockwise
|
||||
(default: 0).
|
||||
@@ -355,8 +358,8 @@ class PolylineObject(Object, _PointsObjectBase):
|
||||
"""
|
||||
Polyline defined by a set of connections between points.
|
||||
|
||||
See: \
|
||||
https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#polyline
|
||||
See:
|
||||
https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#polyline
|
||||
|
||||
Attributes:
|
||||
:points (List[Tuple[int, int]]): List of coordinates relative to \
|
||||
@@ -444,10 +447,12 @@ https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#objectgroup
|
||||
"""
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class _LayerGroupBase(_LayerTypeBase):
|
||||
layers: Optional[List[LayerType]]
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class LayerGroup(LayerType):
|
||||
"""
|
||||
Layer Group.
|
||||
@@ -464,6 +469,12 @@ class LayerGroup(LayerType):
|
||||
"""
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class Hitbox:
|
||||
"""Group of hitboxes for
|
||||
"""
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class Tile:
|
||||
"""
|
||||
@@ -482,7 +493,7 @@ class Tile:
|
||||
terrain: Optional[TileTerrain]
|
||||
animation: Optional[List[Frame]]
|
||||
image: Optional[Image]
|
||||
hit_box: Optional[List[Object]]
|
||||
hitboxes: Optional[List[Object]]
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
@@ -492,7 +503,7 @@ class TileSet:
|
||||
|
||||
Args:
|
||||
:name (str): The name of this tileset.
|
||||
:max_tile_size (OrderedPair): The maximum size of a tile in this
|
||||
:max_tile_size (Size): The maximum size of a tile in this
|
||||
tile set in pixels.
|
||||
:spacing (int): The spacing in pixels between the tiles in this
|
||||
tileset (applies to the tileset image).
|
||||
@@ -516,7 +527,7 @@ class TileSet:
|
||||
:tiles (Optional[Dict[int, Tile]]): Dict of Tile objects by Tile.id.
|
||||
"""
|
||||
name: str
|
||||
max_tile_size: OrderedPair
|
||||
max_tile_size: Size
|
||||
spacing: Optional[int]
|
||||
margin: Optional[int]
|
||||
tile_count: Optional[int]
|
||||
@@ -529,6 +540,9 @@ class TileSet:
|
||||
tiles: Optional[Dict[int, Tile]]
|
||||
|
||||
|
||||
TileSetDict = Dict[int, TileSet]
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class TileMap:
|
||||
"""
|
||||
@@ -548,8 +562,8 @@ class TileMap:
|
||||
rendered. Valid values are right-down, right-up, left-down and
|
||||
left-up. In all cases, the map is drawn row-by-row. (only
|
||||
supported for orthogonal maps at the moment)
|
||||
:map_size (OrderedPair): The map width in tiles.
|
||||
:tile_size (OrderedPair): The width of a tile.
|
||||
:map_size (Size): The map width in tiles.
|
||||
:tile_size (Size): The width of a tile.
|
||||
:infinite (bool): If the map is infinite or not.
|
||||
:hexsidelength (int): Only for hexagonal maps. Determines the width or
|
||||
height (depending on the staggered axis) of the tile’s edge, in
|
||||
@@ -563,8 +577,8 @@ class TileMap:
|
||||
:nextlayerid (int): Stores the next available ID for new layers.
|
||||
:nextobjectid (int): Stores the next available ID for new objects.
|
||||
:tile_sets (dict[str, TileSet]): Dict of tile sets used
|
||||
in this map. Key is the source for external tile sets or the name
|
||||
for embedded ones. The value is a TileSet object.
|
||||
in this map. Key is the first GID for the tile set. The value
|
||||
is a TileSet object.
|
||||
:layers List[LayerType]: List of layer objects by draw order.
|
||||
"""
|
||||
parent_dir: Path
|
||||
@@ -573,13 +587,13 @@ class TileMap:
|
||||
tiled_version: str
|
||||
orientation: str
|
||||
render_order: str
|
||||
map_size: OrderedPair
|
||||
tile_size: OrderedPair
|
||||
map_size: Size
|
||||
tile_size: Size
|
||||
infinite: bool
|
||||
next_layer_id: int
|
||||
next_object_id: int
|
||||
|
||||
tile_sets: Dict[int, TileSet]
|
||||
tile_sets: TileSetDict
|
||||
layers: List[LayerType]
|
||||
|
||||
hex_side_length: Optional[int] = None
|
||||
@@ -607,5 +621,3 @@ class TileMap:
|
||||
[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)
|
||||
'''
|
||||
|
||||
#buffer
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import functools
|
||||
import re
|
||||
import base64
|
||||
import gzip
|
||||
import re
|
||||
import zlib
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from typing import *
|
||||
from typing import Dict, List, Optional, Union
|
||||
|
||||
import pytiled_parser.objects as objects
|
||||
import pytiled_parser.utilities as utilities
|
||||
|
||||
import xml.etree.ElementTree as etree
|
||||
|
||||
|
||||
def _decode_base64_data(data_text, compression, layer_width):
|
||||
tile_grid: List[List[int]] = [[]]
|
||||
|
||||
@@ -47,8 +49,7 @@ def _decode_base64_data(data_text, compression, layer_width):
|
||||
|
||||
|
||||
def _decode_csv_layer(data_text):
|
||||
"""
|
||||
Decodes csv encoded layer data.
|
||||
"""Decodes csv encoded layer data.
|
||||
|
||||
Credit:
|
||||
"""
|
||||
@@ -69,8 +70,7 @@ def _decode_csv_layer(data_text):
|
||||
|
||||
def _decode_data(element: etree.Element, layer_width: int, encoding: str,
|
||||
compression: Optional[str]) -> List[List[int]]:
|
||||
"""
|
||||
Decodes data or chunk data.
|
||||
"""Decodes data or chunk data.
|
||||
|
||||
Args:
|
||||
:element (Element): Element to have text decoded.
|
||||
@@ -105,8 +105,7 @@ def _decode_data(element: etree.Element, layer_width: int, encoding: str,
|
||||
|
||||
def _parse_data(element: etree.Element,
|
||||
layer_width: int) -> objects.LayerData:
|
||||
"""
|
||||
Parses layer data.
|
||||
"""Parses layer data.
|
||||
|
||||
Will parse CSV, base64, gzip-base64, or zlip-base64 encoded data.
|
||||
|
||||
@@ -143,9 +142,7 @@ def _parse_data(element: etree.Element,
|
||||
|
||||
def _parse_layer(element: etree.Element,
|
||||
layer_type: objects.LayerType) -> objects.Layer:
|
||||
"""
|
||||
Parse layer element given.
|
||||
"""
|
||||
"""Parse layer element given."""
|
||||
width = int(element.attrib['width'])
|
||||
height = int(element.attrib['height'])
|
||||
size = objects.OrderedPair(width, height)
|
||||
@@ -159,9 +156,7 @@ def _parse_layer(element: etree.Element,
|
||||
|
||||
|
||||
def _parse_layer_type(layer_element: etree.Element) -> objects.LayerType:
|
||||
"""
|
||||
Parse layer type element given.
|
||||
"""
|
||||
"""Parse layer type element given."""
|
||||
id = int(layer_element.attrib['id'])
|
||||
|
||||
name = layer_element.attrib['name']
|
||||
@@ -198,13 +193,10 @@ def _parse_layer_type(layer_element: etree.Element) -> objects.LayerType:
|
||||
# return _parse_layer_group(layer_element, layer_type_object)
|
||||
|
||||
|
||||
def _parse_object_group(element: etree.Element,
|
||||
layer_type: objects.LayerType) -> objects.ObjectGroup:
|
||||
def _parse_objects(objects: List[etree.Element]) -> List[objects.Object]:
|
||||
"""
|
||||
Parse object group element given.
|
||||
"""
|
||||
object_elements = element.findall('./object')
|
||||
tile_objects: List[objects.Object] = []
|
||||
tiled_objects: List[objects.Object] = []
|
||||
|
||||
for object_element in object_elements:
|
||||
id = int(object_element.attrib['id'])
|
||||
@@ -246,9 +238,21 @@ def _parse_object_group(element: etree.Element,
|
||||
if properties_element is not None:
|
||||
object.properties = _parse_properties_element(properties_element)
|
||||
|
||||
tile_objects.append(object)
|
||||
tiled_objects.append(object)
|
||||
|
||||
object_group = objects.ObjectGroup(tile_objects, **layer_type.__dict__)
|
||||
return tiled_objects
|
||||
|
||||
|
||||
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'))
|
||||
|
||||
object_group = objects.ObjectGroup(tiled_objects, **layer_type.__dict__)
|
||||
try:
|
||||
color = utilities.parse_color(element.attrib['color'])
|
||||
except KeyError:
|
||||
@@ -265,8 +269,7 @@ 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:
|
||||
"""
|
||||
Parses an external tile set.
|
||||
"""Parses an external tile set.
|
||||
|
||||
Caches the results to speed up subsequent instances.
|
||||
"""
|
||||
@@ -276,17 +279,21 @@ def _parse_external_tile_set(parent_dir: Path, tile_set_element: etree.Element
|
||||
return _parse_tile_set(tile_set_tree)
|
||||
|
||||
|
||||
def _parse_hitboxes(element: etree.Element) -> List[objects.Object]:
|
||||
return _parse_objects(element.findall('./object'))
|
||||
|
||||
|
||||
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 is not optional
|
||||
id = int(tile_element.attrib['id'])
|
||||
|
||||
# optional attributes
|
||||
type = None
|
||||
#optional attributes
|
||||
tile_type = None
|
||||
try:
|
||||
type = tile_element.attrib['type']
|
||||
tile_type = tile_element.attrib['type']
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
@@ -296,15 +303,15 @@ def _parse_tiles(tile_element_list: List[etree.Element]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
# an attempt to explain how terrains are handled is below.
|
||||
# 'terrain' attribute is a comma seperated list of 4 values,
|
||||
# each is either an integer or blank
|
||||
#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
|
||||
|
||||
# convert to list of values
|
||||
#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 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 == '':
|
||||
terrain_list.append(None)
|
||||
@@ -312,7 +319,7 @@ def _parse_tiles(tile_element_list: List[etree.Element]
|
||||
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')
|
||||
if tile_animation_element:
|
||||
@@ -332,21 +339,19 @@ def _parse_tiles(tile_element_list: List[etree.Element]
|
||||
if tile_image_element is not None:
|
||||
tile_image = _parse_image_element(tile_image_element)
|
||||
|
||||
object_group = None
|
||||
tile_object_group_element = tile_element.find('./objectgroup')
|
||||
if tile_object_group_element:
|
||||
### FIXME: why did they do this :(
|
||||
pass
|
||||
hitboxes = None
|
||||
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, type, tile_terrain, animation,
|
||||
tile_image, object_group)
|
||||
tiles[id] = objects.Tile(id, tile_type, tile_terrain, animation,
|
||||
tile_image, hitboxes)
|
||||
|
||||
return tiles
|
||||
|
||||
|
||||
def _parse_image_element(image_element: etree.Element) -> objects.Image:
|
||||
"""
|
||||
Parse image element given.
|
||||
"""Parse image element given.
|
||||
|
||||
Returns:
|
||||
:Color: Color in Arcade's preffered format.
|
||||
@@ -361,15 +366,14 @@ def _parse_image_element(image_element: etree.Element) -> objects.Image:
|
||||
|
||||
width = int(image_element.attrib['width'])
|
||||
height = int(image_element.attrib['height'])
|
||||
size = objects.OrderedPair(width, height)
|
||||
size = objects.Size(width, height)
|
||||
|
||||
return objects.Image(source, size, trans)
|
||||
|
||||
|
||||
def _parse_properties_element(properties_element: etree.Element
|
||||
) -> objects.Properties:
|
||||
"""
|
||||
Adds Tiled property to Properties dict.
|
||||
"""Adds Tiled property to Properties dict.
|
||||
|
||||
Args:
|
||||
:name (str): Name of property.
|
||||
|
||||
@@ -9,7 +9,7 @@ def parse_color(color: str) -> objects.Color:
|
||||
:Color: Color object in the format that Arcade understands.
|
||||
"""
|
||||
# strip initial '#' character
|
||||
if not len(color) % 2 == 0: # pylint: disable=C2001
|
||||
if not len(color) % 2 == 0:
|
||||
color = color[1:]
|
||||
|
||||
if len(color) == 6:
|
||||
@@ -25,3 +25,21 @@ def parse_color(color: str) -> objects.Color:
|
||||
blue = int(color[6:8], 16)
|
||||
|
||||
return objects.Color(red, green, blue, alpha)
|
||||
|
||||
|
||||
def get_tile_by_gid(tile_sets: objects.TileSetDict, gid: int) -> objects.Tile:
|
||||
"""Gets Tile from a global tile ID.
|
||||
|
||||
Args:
|
||||
tile_sets (objects.TileSetDict): TileSetDict from TileMap.
|
||||
gid (int): Global tile ID of the tile to be returned.
|
||||
|
||||
Returns:
|
||||
objects.Tile: The Tile object reffered to by the global tile ID.
|
||||
"""
|
||||
for tileset_key, tileset in tile_sets.items():
|
||||
for tile_key, tile in tileset.tiles.items():
|
||||
tile_gid = tile.id + tileset_key
|
||||
if tile_gid == gid:
|
||||
return tile
|
||||
return None
|
||||
|
||||
40
tests/test_data/tile_set_image_hitboxes.tsx
Normal file
40
tests/test_data/tile_set_image_hitboxes.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<tileset version="1.2" tiledversion="1.2.3" name="tile_set_image" tilewidth="32" tileheight="32" spacing="1" margin="1" tilecount="48" columns="8">
|
||||
<image source="images/tmw_desert_spacing.png" width="265" height="199"/>
|
||||
<tile id="9">
|
||||
<objectgroup draworder="index">
|
||||
<object id="2" name="wall" type="rectangle type" x="1" y="1" width="32" height="32" rotation="1"/>
|
||||
</objectgroup>
|
||||
</tile>
|
||||
<tile id="19">
|
||||
<objectgroup draworder="index">
|
||||
<object id="1" name="wall corner" type="polygon type" x="32" y="1" rotation="1">
|
||||
<polygon points="0,0 -32,0 -32,32 -16,32.1818 -15.8182,16.9091 0.181818,17.0909"/>
|
||||
</object>
|
||||
</objectgroup>
|
||||
</tile>
|
||||
<tile id="20">
|
||||
<objectgroup draworder="index">
|
||||
<object id="1" name="polyline" type="polyline type" x="1.45455" y="1.45455" rotation="1">
|
||||
<polyline points="0,0 25.0909,21.2727 9.63636,28.3636"/>
|
||||
</object>
|
||||
</objectgroup>
|
||||
</tile>
|
||||
<tile id="31">
|
||||
<objectgroup draworder="index">
|
||||
<object id="1" name="rock 1" type="elipse type" x="5.09091" y="2.54545" width="19.6364" height="19.2727" rotation="1">
|
||||
<ellipse/>
|
||||
</object>
|
||||
<object id="2" name="rock 2" type="elipse type" x="16.1818" y="22" width="8.54545" height="8.36364" rotation="-1">
|
||||
<ellipse/>
|
||||
</object>
|
||||
</objectgroup>
|
||||
</tile>
|
||||
<tile id="45">
|
||||
<objectgroup draworder="index">
|
||||
<object id="1" name="sign" type="point type" x="14.7273" y="26.3636">
|
||||
<point/>
|
||||
</object>
|
||||
</objectgroup>
|
||||
</tile>
|
||||
</tileset>
|
||||
@@ -15,7 +15,8 @@ def test_map_simple():
|
||||
"""
|
||||
TMX with a very simple spritesheet tile set and some properties.
|
||||
"""
|
||||
map = pytiled_parser.parse_tile_map(Path("../test_data/test_map_simple.tmx"))
|
||||
map = pytiled_parser.parse_tile_map(
|
||||
Path("../test_data/test_map_simple.tmx"))
|
||||
|
||||
# map
|
||||
# unsure how to get paths to compare propperly
|
||||
|
||||
Reference in New Issue
Block a user