tilemap-parser 4.2.7__tar.gz → 4.2.8__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {tilemap_parser-4.2.7 → tilemap_parser-4.2.8}/PKG-INFO +1 -1
- {tilemap_parser-4.2.7 → tilemap_parser-4.2.8}/pyproject.toml +1 -1
- {tilemap_parser-4.2.7 → tilemap_parser-4.2.8}/src/tilemap_parser/parser/map_parse.py +3 -0
- {tilemap_parser-4.2.7 → tilemap_parser-4.2.8}/src/tilemap_parser/runtime/__init__.py +1 -0
- {tilemap_parser-4.2.7 → tilemap_parser-4.2.8}/src/tilemap_parser/runtime/map_object.py +1 -1
- tilemap_parser-4.2.8/src/tilemap_parser/runtime/navigation/__init__.py +3 -0
- tilemap_parser-4.2.8/src/tilemap_parser/runtime/navigation/nav_grid.py +184 -0
- tilemap_parser-4.2.8/src/tilemap_parser/runtime/navigation/path_follower.py +76 -0
- tilemap_parser-4.2.8/src/tilemap_parser/runtime/navigation/pathfinder.py +75 -0
- {tilemap_parser-4.2.7 → tilemap_parser-4.2.8}/src/tilemap_parser.egg-info/PKG-INFO +1 -1
- {tilemap_parser-4.2.7 → tilemap_parser-4.2.8}/src/tilemap_parser.egg-info/SOURCES.txt +5 -0
- tilemap_parser-4.2.8/tests/test_navigation.py +317 -0
- {tilemap_parser-4.2.7 → tilemap_parser-4.2.8}/tests/test_object_surfaces.py +93 -6
- {tilemap_parser-4.2.7 → tilemap_parser-4.2.8}/LICENSE +0 -0
- {tilemap_parser-4.2.7 → tilemap_parser-4.2.8}/README.md +0 -0
- {tilemap_parser-4.2.7 → tilemap_parser-4.2.8}/setup.cfg +0 -0
- {tilemap_parser-4.2.7 → tilemap_parser-4.2.8}/src/tilemap_parser/__init__.py +0 -0
- {tilemap_parser-4.2.7 → tilemap_parser-4.2.8}/src/tilemap_parser/parser/__init__.py +0 -0
- {tilemap_parser-4.2.7 → tilemap_parser-4.2.8}/src/tilemap_parser/parser/animation.py +0 -0
- {tilemap_parser-4.2.7 → tilemap_parser-4.2.8}/src/tilemap_parser/parser/collision.py +0 -0
- {tilemap_parser-4.2.7 → tilemap_parser-4.2.8}/src/tilemap_parser/parser/collision_loader.py +0 -0
- {tilemap_parser-4.2.7 → tilemap_parser-4.2.8}/src/tilemap_parser/parser/node_parse.py +0 -0
- {tilemap_parser-4.2.7 → tilemap_parser-4.2.8}/src/tilemap_parser/parser/particle.py +0 -0
- {tilemap_parser-4.2.7 → tilemap_parser-4.2.8}/src/tilemap_parser/parser/tmx_converter.py +0 -0
- {tilemap_parser-4.2.7 → tilemap_parser-4.2.8}/src/tilemap_parser/runtime/animation_player.py +0 -0
- {tilemap_parser-4.2.7 → tilemap_parser-4.2.8}/src/tilemap_parser/runtime/area_node.py +0 -0
- {tilemap_parser-4.2.7 → tilemap_parser-4.2.8}/src/tilemap_parser/runtime/camera.py +0 -0
- {tilemap_parser-4.2.7 → tilemap_parser-4.2.8}/src/tilemap_parser/runtime/collision_cache.py +0 -0
- {tilemap_parser-4.2.7 → tilemap_parser-4.2.8}/src/tilemap_parser/runtime/map_loader.py +0 -0
- {tilemap_parser-4.2.7 → tilemap_parser-4.2.8}/src/tilemap_parser/runtime/object_collision.py +0 -0
- {tilemap_parser-4.2.7 → tilemap_parser-4.2.8}/src/tilemap_parser/runtime/particles.py +0 -0
- {tilemap_parser-4.2.7 → tilemap_parser-4.2.8}/src/tilemap_parser/runtime/protocols.py +0 -0
- {tilemap_parser-4.2.7 → tilemap_parser-4.2.8}/src/tilemap_parser/runtime/renderer.py +0 -0
- {tilemap_parser-4.2.7 → tilemap_parser-4.2.8}/src/tilemap_parser/runtime/tile_collision.py +0 -0
- {tilemap_parser-4.2.7 → tilemap_parser-4.2.8}/src/tilemap_parser/utils/__init__.py +0 -0
- {tilemap_parser-4.2.7 → tilemap_parser-4.2.8}/src/tilemap_parser/utils/geometry.py +0 -0
- {tilemap_parser-4.2.7 → tilemap_parser-4.2.8}/src/tilemap_parser.egg-info/dependency_links.txt +0 -0
- {tilemap_parser-4.2.7 → tilemap_parser-4.2.8}/src/tilemap_parser.egg-info/requires.txt +0 -0
- {tilemap_parser-4.2.7 → tilemap_parser-4.2.8}/src/tilemap_parser.egg-info/top_level.txt +0 -0
- {tilemap_parser-4.2.7 → tilemap_parser-4.2.8}/tests/test_camera.py +0 -0
- {tilemap_parser-4.2.7 → tilemap_parser-4.2.8}/tests/test_collision.py +0 -0
- {tilemap_parser-4.2.7 → tilemap_parser-4.2.8}/tests/test_geometry.py +0 -0
- {tilemap_parser-4.2.7 → tilemap_parser-4.2.8}/tests/test_gid_collision.py +0 -0
- {tilemap_parser-4.2.7 → tilemap_parser-4.2.8}/tests/test_integration_animation.py +0 -0
- {tilemap_parser-4.2.7 → tilemap_parser-4.2.8}/tests/test_integration_collision.py +0 -0
- {tilemap_parser-4.2.7 → tilemap_parser-4.2.8}/tests/test_integration_map_loader.py +0 -0
- {tilemap_parser-4.2.7 → tilemap_parser-4.2.8}/tests/test_map_loader.py +0 -0
- {tilemap_parser-4.2.7 → tilemap_parser-4.2.8}/tests/test_map_object.py +0 -0
- {tilemap_parser-4.2.7 → tilemap_parser-4.2.8}/tests/test_move_grounded.py +0 -0
- {tilemap_parser-4.2.7 → tilemap_parser-4.2.8}/tests/test_object_collision.py +0 -0
- {tilemap_parser-4.2.7 → tilemap_parser-4.2.8}/tests/test_particle_emitter.py +0 -0
- {tilemap_parser-4.2.7 → tilemap_parser-4.2.8}/tests/test_render_scale.py +0 -0
- {tilemap_parser-4.2.7 → tilemap_parser-4.2.8}/tests/test_tile_collision.py +0 -0
- {tilemap_parser-4.2.7 → tilemap_parser-4.2.8}/tests/test_tile_layer_renderer_y_sort.py +0 -0
- {tilemap_parser-4.2.7 → tilemap_parser-4.2.8}/tests/test_tmx_converter.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tilemap-parser
|
|
3
|
-
Version: 4.2.
|
|
3
|
+
Version: 4.2.8
|
|
4
4
|
Summary: Standalone parser/loader for tilemap-editor JSON maps, sprite animations, and collision detection runtime.
|
|
5
5
|
Author: tilemap parser contributors
|
|
6
6
|
License: GNU GENERAL PUBLIC LICENSE
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "tilemap-parser"
|
|
7
|
-
version = "4.2.
|
|
7
|
+
version = "4.2.8"
|
|
8
8
|
description = "Standalone parser/loader for tilemap-editor JSON maps, sprite animations, and collision detection runtime."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -165,6 +165,7 @@ class ParsedLayer:
|
|
|
165
165
|
tiles: Dict[Point, ParsedTile] = field(default_factory=dict)
|
|
166
166
|
objects: Dict[int, ParsedObject] = field(default_factory=dict)
|
|
167
167
|
next_object_id: Optional[int] = None
|
|
168
|
+
ttypes: set[int] = field(default_factory=set)
|
|
168
169
|
|
|
169
170
|
|
|
170
171
|
@dataclass
|
|
@@ -301,6 +302,8 @@ def _parse_layer(layer_obj: JsonDict, layer_id: int, ctx: str) -> ParsedLayer:
|
|
|
301
302
|
_require_dict(layer_obj.get("objects", {}), f"{ctx}.objects"),
|
|
302
303
|
f"{ctx}.objects",
|
|
303
304
|
)
|
|
305
|
+
for obj in layer.objects.values():
|
|
306
|
+
layer.ttypes.add(obj.ttype)
|
|
304
307
|
if "next_object_id" in layer_obj and layer_obj["next_object_id"] is not None:
|
|
305
308
|
layer.next_object_id = _coerce_int(
|
|
306
309
|
layer_obj["next_object_id"], f"{ctx}.next_object_id"
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
from typing import Dict, List, Optional, Tuple
|
|
5
|
+
|
|
6
|
+
from ...parser.collision import TilesetCollision
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class NavGrid:
|
|
10
|
+
"""Walkability grid derived from tile collision data.
|
|
11
|
+
|
|
12
|
+
The base grid is a pure representation of the world — no entity size
|
|
13
|
+
or clearance baked in. Entity-specific clearance is layered on via
|
|
14
|
+
``erode(margin)`` which returns a derived grid with walls inflated.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
__slots__ = (
|
|
18
|
+
"tile_map",
|
|
19
|
+
"tileset_collision",
|
|
20
|
+
"tile_size",
|
|
21
|
+
"_eff_tw",
|
|
22
|
+
"_eff_th",
|
|
23
|
+
"_width",
|
|
24
|
+
"_height",
|
|
25
|
+
"_walkable",
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
tile_map: Dict[Tuple[int, int], int],
|
|
31
|
+
tileset_collision: TilesetCollision,
|
|
32
|
+
tile_size: Tuple[int, int],
|
|
33
|
+
render_scale: float = 1.0,
|
|
34
|
+
map_size: Optional[Tuple[int, int]] = None,
|
|
35
|
+
) -> None:
|
|
36
|
+
self.tile_map = tile_map
|
|
37
|
+
self.tileset_collision = tileset_collision
|
|
38
|
+
self.tile_size = tile_size
|
|
39
|
+
self._eff_tw = tile_size[0] * render_scale
|
|
40
|
+
self._eff_th = tile_size[1] * render_scale
|
|
41
|
+
|
|
42
|
+
if map_size is not None:
|
|
43
|
+
self._width, self._height = map_size
|
|
44
|
+
else:
|
|
45
|
+
self._width = 0
|
|
46
|
+
self._height = 0
|
|
47
|
+
for tx, ty in tile_map:
|
|
48
|
+
if tx >= self._width:
|
|
49
|
+
self._width = tx + 1
|
|
50
|
+
if ty >= self._height:
|
|
51
|
+
self._height = ty + 1
|
|
52
|
+
|
|
53
|
+
self._walkable = [[self._is_tile_walkable(x, y) for x in range(self._width)] for y in range(self._height)]
|
|
54
|
+
|
|
55
|
+
def _is_tile_walkable(self, tx: int, ty: int) -> bool:
|
|
56
|
+
tile_id = self.tile_map.get((tx, ty))
|
|
57
|
+
if tile_id is None:
|
|
58
|
+
return True
|
|
59
|
+
tile_data = self.tileset_collision.tiles.get(tile_id)
|
|
60
|
+
if tile_data is None:
|
|
61
|
+
return True
|
|
62
|
+
for poly in tile_data.shapes:
|
|
63
|
+
if poly.is_valid() and not poly.one_way:
|
|
64
|
+
return False
|
|
65
|
+
return True
|
|
66
|
+
|
|
67
|
+
def _in_bounds(self, tx: int, ty: int) -> bool:
|
|
68
|
+
return 0 <= tx < self._width and 0 <= ty < self._height
|
|
69
|
+
|
|
70
|
+
def is_solid(self, tx: int, ty: int) -> bool:
|
|
71
|
+
if not self._in_bounds(tx, ty):
|
|
72
|
+
return True
|
|
73
|
+
return not self._walkable[ty][tx]
|
|
74
|
+
|
|
75
|
+
def is_walkable(self, tx: int, ty: int) -> bool:
|
|
76
|
+
if not self._in_bounds(tx, ty):
|
|
77
|
+
return False
|
|
78
|
+
return self._walkable[ty][tx]
|
|
79
|
+
|
|
80
|
+
def is_one_way(self, tx: int, ty: int) -> bool:
|
|
81
|
+
tile_id = self.tile_map.get((tx, ty))
|
|
82
|
+
if tile_id is None:
|
|
83
|
+
return False
|
|
84
|
+
tile_data = self.tileset_collision.tiles.get(tile_id)
|
|
85
|
+
if tile_data is None:
|
|
86
|
+
return False
|
|
87
|
+
has_one_way = False
|
|
88
|
+
for poly in tile_data.shapes:
|
|
89
|
+
if not poly.is_valid():
|
|
90
|
+
continue
|
|
91
|
+
if poly.one_way:
|
|
92
|
+
has_one_way = True
|
|
93
|
+
else:
|
|
94
|
+
return False
|
|
95
|
+
return has_one_way
|
|
96
|
+
|
|
97
|
+
def copy(self) -> NavGrid:
|
|
98
|
+
new = NavGrid.__new__(NavGrid)
|
|
99
|
+
new.tile_map = self.tile_map
|
|
100
|
+
new.tileset_collision = self.tileset_collision
|
|
101
|
+
new.tile_size = self.tile_size
|
|
102
|
+
new._eff_tw = self._eff_tw
|
|
103
|
+
new._eff_th = self._eff_th
|
|
104
|
+
new._width = self._width
|
|
105
|
+
new._height = self._height
|
|
106
|
+
new._walkable = [row[:] for row in self._walkable]
|
|
107
|
+
return new
|
|
108
|
+
|
|
109
|
+
def erode(self, margin: float) -> NavGrid:
|
|
110
|
+
new = self.copy()
|
|
111
|
+
new._erode_in_place(margin)
|
|
112
|
+
return new
|
|
113
|
+
|
|
114
|
+
def _erode_in_place(self, margin: float) -> None:
|
|
115
|
+
original = [row[:] for row in self._walkable]
|
|
116
|
+
r = int(math.ceil(margin + 0.5))
|
|
117
|
+
|
|
118
|
+
for sy in range(self._height):
|
|
119
|
+
for sx in range(self._width):
|
|
120
|
+
if not original[sy][sx]:
|
|
121
|
+
continue
|
|
122
|
+
min_tx = max(0, sx - r)
|
|
123
|
+
max_tx = min(self._width - 1, sx + r)
|
|
124
|
+
min_ty = max(0, sy - r)
|
|
125
|
+
max_ty = min(self._height - 1, sy + r)
|
|
126
|
+
for ty in range(min_ty, max_ty + 1):
|
|
127
|
+
for tx in range(min_tx, max_tx + 1):
|
|
128
|
+
if original[ty][tx]:
|
|
129
|
+
continue
|
|
130
|
+
dx = abs(tx - sx)
|
|
131
|
+
dy = abs(ty - sy)
|
|
132
|
+
dist_x = max(0.0, dx - 0.5)
|
|
133
|
+
dist_y = max(0.0, dy - 0.5)
|
|
134
|
+
if dist_x == 0 and dist_y == 0:
|
|
135
|
+
dist = 0.0
|
|
136
|
+
elif dist_x == 0:
|
|
137
|
+
dist = dist_y
|
|
138
|
+
elif dist_y == 0:
|
|
139
|
+
dist = dist_x
|
|
140
|
+
else:
|
|
141
|
+
dist = math.hypot(dist_x, dist_y)
|
|
142
|
+
if dist <= margin:
|
|
143
|
+
self._walkable[sy][sx] = False
|
|
144
|
+
break
|
|
145
|
+
if not self._walkable[sy][sx]:
|
|
146
|
+
break
|
|
147
|
+
|
|
148
|
+
@classmethod
|
|
149
|
+
def for_entity(
|
|
150
|
+
cls,
|
|
151
|
+
tile_map: Dict[Tuple[int, int], int],
|
|
152
|
+
tileset_collision: TilesetCollision,
|
|
153
|
+
tile_size: Tuple[int, int],
|
|
154
|
+
sprite_width: float,
|
|
155
|
+
sprite_height: Optional[float] = None,
|
|
156
|
+
render_scale: float = 1.0,
|
|
157
|
+
map_size: Optional[Tuple[int, int]] = None,
|
|
158
|
+
cache: Optional[Dict[float, NavGrid]] = None,
|
|
159
|
+
) -> NavGrid:
|
|
160
|
+
tw = tile_size[0] * render_scale
|
|
161
|
+
size = max(sprite_width, sprite_height if sprite_height is not None else sprite_width)
|
|
162
|
+
margin = (size / 2.0) / tw
|
|
163
|
+
if cache is not None and margin in cache:
|
|
164
|
+
return cache[margin]
|
|
165
|
+
nav = cls(tile_map, tileset_collision, tile_size, render_scale, map_size).erode(margin)
|
|
166
|
+
if cache is not None:
|
|
167
|
+
cache[margin] = nav
|
|
168
|
+
return nav
|
|
169
|
+
|
|
170
|
+
def get_neighbors(self, tx: int, ty: int, *, diagonals: bool = False) -> List[Tuple[int, int]]:
|
|
171
|
+
neighbors: List[Tuple[int, int]] = []
|
|
172
|
+
for dx, dy in [(0, -1), (0, 1), (-1, 0), (1, 0)]:
|
|
173
|
+
nx, ny = tx + dx, ty + dy
|
|
174
|
+
if self.is_walkable(nx, ny):
|
|
175
|
+
neighbors.append((nx, ny))
|
|
176
|
+
if diagonals:
|
|
177
|
+
for dx, dy in [(-1, -1), (1, -1), (-1, 1), (1, 1)]:
|
|
178
|
+
nx, ny = tx + dx, ty + dy
|
|
179
|
+
if not self.is_walkable(nx, ny):
|
|
180
|
+
continue
|
|
181
|
+
if not self.is_walkable(tx + dx, ty) or not self.is_walkable(tx, ty + dy):
|
|
182
|
+
continue
|
|
183
|
+
neighbors.append((nx, ny))
|
|
184
|
+
return neighbors
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
from typing import List, Optional, Tuple
|
|
5
|
+
|
|
6
|
+
from ...parser.collision import TilesetCollision
|
|
7
|
+
from ..protocols import ICollidable
|
|
8
|
+
from ..tile_collision import CollisionRunner
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PathFollower:
|
|
12
|
+
__slots__ = ("_eff_tw", "_eff_th", "arrival_distance")
|
|
13
|
+
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
effective_tile_size: Tuple[float, float],
|
|
17
|
+
*,
|
|
18
|
+
arrival_distance: Optional[float] = None,
|
|
19
|
+
) -> None:
|
|
20
|
+
self._eff_tw, self._eff_th = effective_tile_size
|
|
21
|
+
self.arrival_distance = (
|
|
22
|
+
arrival_distance
|
|
23
|
+
if arrival_distance is not None
|
|
24
|
+
else math.hypot(self._eff_tw, self._eff_th) * 0.2
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
def update_rpg(
|
|
28
|
+
self,
|
|
29
|
+
sprite: ICollidable,
|
|
30
|
+
path: List[Tuple[int, int]],
|
|
31
|
+
waypoint_index: int,
|
|
32
|
+
collision_runner: CollisionRunner,
|
|
33
|
+
tileset_collision: TilesetCollision,
|
|
34
|
+
tile_map: dict,
|
|
35
|
+
speed: float = 200.0,
|
|
36
|
+
dt: float = 0.016,
|
|
37
|
+
) -> Tuple[int, bool, bool, bool]:
|
|
38
|
+
if not path or waypoint_index >= len(path):
|
|
39
|
+
return waypoint_index, True, False, False
|
|
40
|
+
|
|
41
|
+
while waypoint_index < len(path):
|
|
42
|
+
wx, wy = path[waypoint_index]
|
|
43
|
+
tx = wx * self._eff_tw + self._eff_tw * 0.5
|
|
44
|
+
ty = wy * self._eff_th + self._eff_th * 0.5
|
|
45
|
+
if math.hypot(tx - sprite.x, ty - sprite.y) < self.arrival_distance:
|
|
46
|
+
waypoint_index += 1
|
|
47
|
+
else:
|
|
48
|
+
break
|
|
49
|
+
|
|
50
|
+
if waypoint_index >= len(path):
|
|
51
|
+
return waypoint_index, True, False, False
|
|
52
|
+
|
|
53
|
+
wx, wy = path[waypoint_index]
|
|
54
|
+
tx = wx * self._eff_tw + self._eff_tw * 0.5
|
|
55
|
+
ty = wy * self._eff_th + self._eff_th * 0.5
|
|
56
|
+
|
|
57
|
+
dx = tx - sprite.x
|
|
58
|
+
dy = ty - sprite.y
|
|
59
|
+
dist = math.hypot(dx, dy)
|
|
60
|
+
|
|
61
|
+
if dist < 0.01:
|
|
62
|
+
delta_x = 0.0
|
|
63
|
+
delta_y = 0.0
|
|
64
|
+
else:
|
|
65
|
+
step = min(speed * dt, dist)
|
|
66
|
+
delta_x = (dx / dist) * step
|
|
67
|
+
delta_y = (dy / dist) * step
|
|
68
|
+
|
|
69
|
+
result = collision_runner.move_rpg(
|
|
70
|
+
sprite, tileset_collision, tile_map, delta_x, delta_y
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
if math.hypot(tx - sprite.x, ty - sprite.y) < self.arrival_distance:
|
|
74
|
+
waypoint_index += 1
|
|
75
|
+
|
|
76
|
+
return waypoint_index, waypoint_index >= len(path), result.hit_wall_x, result.hit_wall_y
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import heapq
|
|
4
|
+
from typing import Dict, List, Optional, Set, Tuple
|
|
5
|
+
|
|
6
|
+
from .nav_grid import NavGrid
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Pathfinder:
|
|
10
|
+
__slots__ = ("nav_grid",)
|
|
11
|
+
|
|
12
|
+
def __init__(self, nav_grid: NavGrid) -> None:
|
|
13
|
+
self.nav_grid = nav_grid
|
|
14
|
+
|
|
15
|
+
def find_path(
|
|
16
|
+
self,
|
|
17
|
+
start: Tuple[int, int],
|
|
18
|
+
end: Tuple[int, int],
|
|
19
|
+
max_steps: int = 2000,
|
|
20
|
+
) -> Optional[List[Tuple[int, int]]]:
|
|
21
|
+
sx, sy = start
|
|
22
|
+
ex, ey = end
|
|
23
|
+
|
|
24
|
+
if not self.nav_grid.is_walkable(ex, ey):
|
|
25
|
+
return None
|
|
26
|
+
|
|
27
|
+
open_set: List[Tuple[float, int, int]] = [(0.0, sx, sy)]
|
|
28
|
+
came_from: Dict[Tuple[int, int], Tuple[int, int]] = {}
|
|
29
|
+
g_score: Dict[Tuple[int, int], float] = {start: 0.0}
|
|
30
|
+
f_score: Dict[Tuple[int, int], float] = {
|
|
31
|
+
start: _manhattan(sx, sy, ex, ey)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
closed: Set[Tuple[int, int]] = set()
|
|
35
|
+
steps = 0
|
|
36
|
+
while open_set and steps < max_steps:
|
|
37
|
+
_, cx, cy = heapq.heappop(open_set)
|
|
38
|
+
current = (cx, cy)
|
|
39
|
+
|
|
40
|
+
if current in closed:
|
|
41
|
+
continue
|
|
42
|
+
closed.add(current)
|
|
43
|
+
steps += 1
|
|
44
|
+
|
|
45
|
+
if current == end:
|
|
46
|
+
return _reconstruct_path(came_from, current)
|
|
47
|
+
|
|
48
|
+
for neighbor in self.nav_grid.get_neighbors(cx, cy):
|
|
49
|
+
tentative = g_score[current] + 1.0
|
|
50
|
+
if tentative < g_score.get(neighbor, float("inf")):
|
|
51
|
+
came_from[neighbor] = current
|
|
52
|
+
g_score[neighbor] = tentative
|
|
53
|
+
f = tentative + _manhattan(
|
|
54
|
+
neighbor[0], neighbor[1], ex, ey
|
|
55
|
+
)
|
|
56
|
+
f_score[neighbor] = f
|
|
57
|
+
heapq.heappush(open_set, (f, neighbor[0], neighbor[1]))
|
|
58
|
+
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _manhattan(ax: int, ay: int, bx: int, by: int) -> float:
|
|
63
|
+
return float(abs(ax - bx) + abs(ay - by))
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _reconstruct_path(
|
|
67
|
+
came_from: Dict[Tuple[int, int], Tuple[int, int]],
|
|
68
|
+
current: Tuple[int, int],
|
|
69
|
+
) -> List[Tuple[int, int]]:
|
|
70
|
+
path = [current]
|
|
71
|
+
while current in came_from:
|
|
72
|
+
current = came_from[current]
|
|
73
|
+
path.append(current)
|
|
74
|
+
path.reverse()
|
|
75
|
+
return path
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tilemap-parser
|
|
3
|
-
Version: 4.2.
|
|
3
|
+
Version: 4.2.8
|
|
4
4
|
Summary: Standalone parser/loader for tilemap-editor JSON maps, sprite animations, and collision detection runtime.
|
|
5
5
|
Author: tilemap parser contributors
|
|
6
6
|
License: GNU GENERAL PUBLIC LICENSE
|
|
@@ -27,6 +27,10 @@ src/tilemap_parser/runtime/particles.py
|
|
|
27
27
|
src/tilemap_parser/runtime/protocols.py
|
|
28
28
|
src/tilemap_parser/runtime/renderer.py
|
|
29
29
|
src/tilemap_parser/runtime/tile_collision.py
|
|
30
|
+
src/tilemap_parser/runtime/navigation/__init__.py
|
|
31
|
+
src/tilemap_parser/runtime/navigation/nav_grid.py
|
|
32
|
+
src/tilemap_parser/runtime/navigation/path_follower.py
|
|
33
|
+
src/tilemap_parser/runtime/navigation/pathfinder.py
|
|
30
34
|
src/tilemap_parser/utils/__init__.py
|
|
31
35
|
src/tilemap_parser/utils/geometry.py
|
|
32
36
|
tests/test_camera.py
|
|
@@ -39,6 +43,7 @@ tests/test_integration_map_loader.py
|
|
|
39
43
|
tests/test_map_loader.py
|
|
40
44
|
tests/test_map_object.py
|
|
41
45
|
tests/test_move_grounded.py
|
|
46
|
+
tests/test_navigation.py
|
|
42
47
|
tests/test_object_collision.py
|
|
43
48
|
tests/test_object_surfaces.py
|
|
44
49
|
tests/test_particle_emitter.py
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from tilemap_parser.parser.collision import (
|
|
11
|
+
CollisionPolygon,
|
|
12
|
+
RectangleShape,
|
|
13
|
+
TileCollisionData,
|
|
14
|
+
TilesetCollision,
|
|
15
|
+
)
|
|
16
|
+
from tilemap_parser.runtime.navigation import NavGrid, Pathfinder, PathFollower
|
|
17
|
+
from tilemap_parser.runtime.tile_collision import CollisionRunner
|
|
18
|
+
|
|
19
|
+
TILE_SIZE = 32
|
|
20
|
+
FULL_TILE = [(0.0, 0.0), (32.0, 0.0), (32.0, 32.0), (0.0, 32.0)]
|
|
21
|
+
|
|
22
|
+
MAP = [
|
|
23
|
+
[1, 1, 1, 1, 1, 1],
|
|
24
|
+
[1, 0, 0, 0, 0, 1],
|
|
25
|
+
[1, 0, 1, 0, 0, 1],
|
|
26
|
+
[1, 0, 0, 0, 0, 1],
|
|
27
|
+
[1, 0, 0, 0, 0, 1],
|
|
28
|
+
[1, 1, 1, 1, 1, 1],
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
ROWS = len(MAP)
|
|
32
|
+
COLS = len(MAP[0])
|
|
33
|
+
|
|
34
|
+
START_TILE = (1, 2)
|
|
35
|
+
GOAL_TILE = (4, 4)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class MockSprite:
|
|
39
|
+
def __init__(self, x: float, y: float) -> None:
|
|
40
|
+
self.x = x
|
|
41
|
+
self.y = y
|
|
42
|
+
self.collision_shape = RectangleShape(width=18, height=26, offset=(-9, -13))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def build_tile_map() -> dict:
|
|
46
|
+
tile_map = {}
|
|
47
|
+
for r in range(ROWS):
|
|
48
|
+
for c in range(COLS):
|
|
49
|
+
if MAP[r][c] == 1:
|
|
50
|
+
tile_map[(c, r)] = 0
|
|
51
|
+
return tile_map
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def build_tileset() -> TilesetCollision:
|
|
55
|
+
return TilesetCollision(
|
|
56
|
+
tileset_name="test",
|
|
57
|
+
tile_size=(TILE_SIZE, TILE_SIZE),
|
|
58
|
+
tiles={
|
|
59
|
+
0: TileCollisionData(
|
|
60
|
+
tile_id=0,
|
|
61
|
+
shapes=[CollisionPolygon(vertices=FULL_TILE)],
|
|
62
|
+
),
|
|
63
|
+
},
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class TestNavigation:
|
|
68
|
+
def setup_method(self):
|
|
69
|
+
self.tile_map = build_tile_map()
|
|
70
|
+
self.tileset = build_tileset()
|
|
71
|
+
self.runner = CollisionRunner.from_game_type("rpg", (TILE_SIZE, TILE_SIZE))
|
|
72
|
+
self.nav_grid = NavGrid(self.tile_map, self.tileset, (TILE_SIZE, TILE_SIZE))
|
|
73
|
+
self.pathfinder = Pathfinder(self.nav_grid)
|
|
74
|
+
self.follower = PathFollower((TILE_SIZE, TILE_SIZE))
|
|
75
|
+
|
|
76
|
+
def test_path_exists_around_wall(self):
|
|
77
|
+
path = self.pathfinder.find_path(START_TILE, GOAL_TILE)
|
|
78
|
+
assert path is not None, "A* should find a path"
|
|
79
|
+
assert path[0] == START_TILE
|
|
80
|
+
assert path[-1] == GOAL_TILE
|
|
81
|
+
assert len(path) >= 3
|
|
82
|
+
|
|
83
|
+
def test_no_path_into_solid_wall(self):
|
|
84
|
+
path = self.pathfinder.find_path(START_TILE, (2, 2))
|
|
85
|
+
assert path is None
|
|
86
|
+
|
|
87
|
+
def test_no_path_outside_map(self):
|
|
88
|
+
path = self.pathfinder.find_path(START_TILE, (-1, -1))
|
|
89
|
+
assert path is None
|
|
90
|
+
|
|
91
|
+
def test_path_avoids_wall_tiles(self):
|
|
92
|
+
path = self.pathfinder.find_path(START_TILE, GOAL_TILE)
|
|
93
|
+
for tx, ty in path[1:-1]:
|
|
94
|
+
assert MAP[ty][tx] == 0, f"path goes through wall at ({tx}, {ty})"
|
|
95
|
+
|
|
96
|
+
def test_path_same_start_and_goal(self):
|
|
97
|
+
path = self.pathfinder.find_path((1, 1), (1, 1))
|
|
98
|
+
assert path == [(1, 1)]
|
|
99
|
+
|
|
100
|
+
def test_follower_reaches_goal(self):
|
|
101
|
+
path = self.pathfinder.find_path(START_TILE, GOAL_TILE)
|
|
102
|
+
assert path is not None
|
|
103
|
+
|
|
104
|
+
cx = START_TILE[0] * TILE_SIZE + TILE_SIZE * 0.5
|
|
105
|
+
cy = START_TILE[1] * TILE_SIZE + TILE_SIZE * 0.5
|
|
106
|
+
sprite = MockSprite(x=cx, y=cy)
|
|
107
|
+
|
|
108
|
+
waypoint_idx = 0
|
|
109
|
+
max_steps = 300
|
|
110
|
+
arrived = False
|
|
111
|
+
|
|
112
|
+
for _ in range(max_steps):
|
|
113
|
+
waypoint_idx, arrived, _, _ = self.follower.update_rpg(
|
|
114
|
+
sprite, path, waypoint_idx, self.runner,
|
|
115
|
+
self.tileset, self.tile_map, speed=150.0, dt=0.016,
|
|
116
|
+
)
|
|
117
|
+
if arrived:
|
|
118
|
+
break
|
|
119
|
+
|
|
120
|
+
assert arrived, (
|
|
121
|
+
f"sprite stuck at ({sprite.x:.1f}, {sprite.y:.1f}) "
|
|
122
|
+
f"waypoint {waypoint_idx}/{len(path)}"
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
gx = GOAL_TILE[0] * TILE_SIZE + TILE_SIZE * 0.5
|
|
126
|
+
gy = GOAL_TILE[1] * TILE_SIZE + TILE_SIZE * 0.5
|
|
127
|
+
dx = gx - sprite.x
|
|
128
|
+
dy = gy - sprite.y
|
|
129
|
+
assert (dx * dx + dy * dy) ** 0.5 < TILE_SIZE * 0.5, "sprite should be near goal tile"
|
|
130
|
+
|
|
131
|
+
def test_follower_stops_at_end_of_path(self):
|
|
132
|
+
path = self.pathfinder.find_path(START_TILE, GOAL_TILE)
|
|
133
|
+
assert path is not None
|
|
134
|
+
|
|
135
|
+
cx = START_TILE[0] * TILE_SIZE + TILE_SIZE * 0.5
|
|
136
|
+
cy = START_TILE[1] * TILE_SIZE + TILE_SIZE * 0.5
|
|
137
|
+
sprite = MockSprite(x=cx, y=cy)
|
|
138
|
+
|
|
139
|
+
_, arrived, _, _ = self.follower.update_rpg(
|
|
140
|
+
sprite, [], 0, self.runner,
|
|
141
|
+
self.tileset, self.tile_map,
|
|
142
|
+
)
|
|
143
|
+
assert arrived is True
|
|
144
|
+
|
|
145
|
+
_, arrived, _, _ = self.follower.update_rpg(
|
|
146
|
+
sprite, path, len(path), self.runner,
|
|
147
|
+
self.tileset, self.tile_map,
|
|
148
|
+
)
|
|
149
|
+
assert arrived is True
|
|
150
|
+
|
|
151
|
+
def test_nav_grid_walkable_tiles(self):
|
|
152
|
+
assert self.nav_grid.is_walkable(1, 1) is True
|
|
153
|
+
assert self.nav_grid.is_walkable(0, 0) is False
|
|
154
|
+
assert self.nav_grid.is_walkable(2, 2) is False
|
|
155
|
+
assert self.nav_grid.is_walkable(4, 4) is True
|
|
156
|
+
|
|
157
|
+
def test_nav_grid_solid(self):
|
|
158
|
+
assert self.nav_grid.is_solid(0, 0) is True
|
|
159
|
+
assert self.nav_grid.is_solid(1, 1) is False
|
|
160
|
+
|
|
161
|
+
def test_out_of_bounds_not_walkable(self):
|
|
162
|
+
assert self.nav_grid.is_walkable(-1, 0) is False
|
|
163
|
+
assert self.nav_grid.is_walkable(100, 100) is False
|
|
164
|
+
|
|
165
|
+
def test_out_of_bounds_solid(self):
|
|
166
|
+
assert self.nav_grid.is_solid(-1, 0) is True
|
|
167
|
+
assert self.nav_grid.is_solid(100, 100) is True
|
|
168
|
+
|
|
169
|
+
def test_get_neighbors_no_diagonals(self):
|
|
170
|
+
neighbors = self.nav_grid.get_neighbors(3, 3)
|
|
171
|
+
expected = {(3, 2), (3, 4), (2, 3), (4, 3)}
|
|
172
|
+
assert set(neighbors) == expected
|
|
173
|
+
|
|
174
|
+
def test_get_neighbors_with_diagonals(self):
|
|
175
|
+
neighbors = self.nav_grid.get_neighbors(3, 3, diagonals=True)
|
|
176
|
+
for nx, ny in neighbors:
|
|
177
|
+
assert self.nav_grid.is_walkable(nx, ny), f"({nx}, {ny}) not walkable"
|
|
178
|
+
assert (2, 2) not in neighbors
|
|
179
|
+
assert (3, 2) in neighbors
|
|
180
|
+
assert (4, 3) in neighbors
|
|
181
|
+
|
|
182
|
+
def test_neighbor_near_wall_excludes_wall(self):
|
|
183
|
+
neighbors = self.nav_grid.get_neighbors(2, 1)
|
|
184
|
+
for nx, ny in neighbors:
|
|
185
|
+
assert self.nav_grid.is_walkable(nx, ny), f"neighbor ({nx}, {ny}) is wall"
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class TestNavGridErosion:
|
|
189
|
+
def setup_method(self):
|
|
190
|
+
self.tile_map = build_tile_map()
|
|
191
|
+
self.tileset = build_tileset()
|
|
192
|
+
self.base = NavGrid(self.tile_map, self.tileset, (TILE_SIZE, TILE_SIZE))
|
|
193
|
+
|
|
194
|
+
def test_erosion_blocks_adjacent(self):
|
|
195
|
+
eroded = self.base.erode(1.0)
|
|
196
|
+
assert eroded.is_solid(0, 0) is True
|
|
197
|
+
assert eroded.is_solid(1, 1) is True
|
|
198
|
+
assert eroded.is_solid(1, 2) is True
|
|
199
|
+
assert eroded.is_solid(2, 1) is True
|
|
200
|
+
|
|
201
|
+
def test_erosion_does_not_affect_base(self):
|
|
202
|
+
self.base.erode(1.0)
|
|
203
|
+
assert self.base.is_walkable(1, 1) is True
|
|
204
|
+
|
|
205
|
+
def test_erosion_margin_half(self):
|
|
206
|
+
eroded = self.base.erode(0.5)
|
|
207
|
+
assert eroded.is_walkable(1, 1) is False
|
|
208
|
+
assert eroded.is_walkable(3, 3) is True
|
|
209
|
+
|
|
210
|
+
def test_copy_is_independent(self):
|
|
211
|
+
copy = self.base.copy()
|
|
212
|
+
assert copy.is_walkable(1, 1) is True
|
|
213
|
+
eroded = copy.erode(1.0)
|
|
214
|
+
assert copy.is_walkable(1, 1) is True
|
|
215
|
+
assert eroded.is_walkable(1, 1) is False
|
|
216
|
+
|
|
217
|
+
def test_for_entity_small_vs_large(self):
|
|
218
|
+
small = NavGrid.for_entity(
|
|
219
|
+
self.tile_map, self.tileset, (TILE_SIZE, TILE_SIZE), sprite_width=16
|
|
220
|
+
)
|
|
221
|
+
large = NavGrid.for_entity(
|
|
222
|
+
self.tile_map, self.tileset, (TILE_SIZE, TILE_SIZE), sprite_width=64
|
|
223
|
+
)
|
|
224
|
+
assert small.is_walkable(3, 3) is True
|
|
225
|
+
assert large.is_walkable(3, 3) is False
|
|
226
|
+
|
|
227
|
+
def test_for_entity_caching(self):
|
|
228
|
+
cache: dict = {}
|
|
229
|
+
a = NavGrid.for_entity(
|
|
230
|
+
self.tile_map, self.tileset, (TILE_SIZE, TILE_SIZE),
|
|
231
|
+
sprite_width=32, cache=cache,
|
|
232
|
+
)
|
|
233
|
+
b = NavGrid.for_entity(
|
|
234
|
+
self.tile_map, self.tileset, (TILE_SIZE, TILE_SIZE),
|
|
235
|
+
sprite_width=32, cache=cache,
|
|
236
|
+
)
|
|
237
|
+
assert a is b
|
|
238
|
+
|
|
239
|
+
def test_erosion_blocks_narrow_gap_no_alternate(self):
|
|
240
|
+
w, h = 6, 6
|
|
241
|
+
tight_map: dict[tuple[int, int], int] = {}
|
|
242
|
+
for x in range(w):
|
|
243
|
+
for y in range(h):
|
|
244
|
+
if x == 0 or x == w - 1 or y == 0 or y == h - 1:
|
|
245
|
+
continue
|
|
246
|
+
if x == 3 and y != 3:
|
|
247
|
+
tight_map[(x, y)] = 0
|
|
248
|
+
ts = TilesetCollision(
|
|
249
|
+
tileset_name="test",
|
|
250
|
+
tile_size=(TILE_SIZE, TILE_SIZE),
|
|
251
|
+
tiles={
|
|
252
|
+
0: TileCollisionData(
|
|
253
|
+
tile_id=0,
|
|
254
|
+
shapes=[CollisionPolygon(vertices=FULL_TILE)],
|
|
255
|
+
),
|
|
256
|
+
},
|
|
257
|
+
)
|
|
258
|
+
base = NavGrid(tight_map, ts, (TILE_SIZE, TILE_SIZE), map_size=(w, h))
|
|
259
|
+
pf = Pathfinder(base)
|
|
260
|
+
path = pf.find_path((1, 1), (4, 4))
|
|
261
|
+
assert path is not None
|
|
262
|
+
assert (3, 3) in path
|
|
263
|
+
|
|
264
|
+
eroded = base.erode(1.0)
|
|
265
|
+
pf_eroded = Pathfinder(eroded)
|
|
266
|
+
path_eroded = pf_eroded.find_path((1, 1), (4, 4))
|
|
267
|
+
assert path_eroded is None
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
class TestPathfinderClosedSet:
|
|
271
|
+
def setup_method(self):
|
|
272
|
+
self.tile_map = build_tile_map()
|
|
273
|
+
self.tileset = build_tileset()
|
|
274
|
+
self.nav_grid = NavGrid(self.tile_map, self.tileset, (TILE_SIZE, TILE_SIZE))
|
|
275
|
+
self.pathfinder = Pathfinder(self.nav_grid)
|
|
276
|
+
|
|
277
|
+
def test_path_found_with_closed_set(self):
|
|
278
|
+
path = self.pathfinder.find_path(START_TILE, GOAL_TILE)
|
|
279
|
+
assert path is not None
|
|
280
|
+
assert path[0] == START_TILE
|
|
281
|
+
assert path[-1] == GOAL_TILE
|
|
282
|
+
|
|
283
|
+
def test_no_path_with_closed_set(self):
|
|
284
|
+
path = self.pathfinder.find_path(START_TILE, (2, 2))
|
|
285
|
+
assert path is None
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
class TestPathFollowerStalling:
|
|
289
|
+
def setup_method(self):
|
|
290
|
+
self.tile_map = build_tile_map()
|
|
291
|
+
self.tileset = build_tileset()
|
|
292
|
+
self.runner = CollisionRunner.from_game_type("rpg", (TILE_SIZE, TILE_SIZE))
|
|
293
|
+
self.follower = PathFollower((TILE_SIZE, TILE_SIZE))
|
|
294
|
+
|
|
295
|
+
def test_update_rpg_returns_stall_info(self):
|
|
296
|
+
path = [(2, 2)]
|
|
297
|
+
sprite = MockSprite(x=5 * TILE_SIZE, y=5 * TILE_SIZE)
|
|
298
|
+
idx, done, hwx, hwy = self.follower.update_rpg(
|
|
299
|
+
sprite, path, 0, self.runner,
|
|
300
|
+
self.tileset, self.tile_map, speed=150.0, dt=0.016,
|
|
301
|
+
)
|
|
302
|
+
assert isinstance(hwx, bool)
|
|
303
|
+
assert isinstance(hwy, bool)
|
|
304
|
+
|
|
305
|
+
def test_update_rpg_arrived_has_no_stall(self):
|
|
306
|
+
path = [(2, 2)]
|
|
307
|
+
sprite = MockSprite(
|
|
308
|
+
x=2 * TILE_SIZE + TILE_SIZE * 0.5,
|
|
309
|
+
y=2 * TILE_SIZE + TILE_SIZE * 0.5,
|
|
310
|
+
)
|
|
311
|
+
_, done, hwx, hwy = self.follower.update_rpg(
|
|
312
|
+
sprite, path, 0, self.runner,
|
|
313
|
+
self.tileset, self.tile_map, speed=150.0, dt=0.016,
|
|
314
|
+
)
|
|
315
|
+
assert done is True
|
|
316
|
+
assert hwx is False
|
|
317
|
+
assert hwy is False
|
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
import json
|
|
2
|
+
import sys
|
|
2
3
|
import tempfile
|
|
3
4
|
from pathlib import Path
|
|
4
5
|
|
|
5
6
|
import pygame
|
|
6
7
|
import pytest
|
|
7
8
|
|
|
8
|
-
import sys
|
|
9
9
|
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
|
|
10
|
-
|
|
11
10
|
from tilemap_parser.runtime.map_loader import TilemapData
|
|
12
|
-
from tilemap_parser.parser.map_parse import ParsedObject, ParsedObjectArea
|
|
13
|
-
|
|
11
|
+
from tilemap_parser.parser.map_parse import ParsedObject, ParsedObjectArea, parse_map_dict
|
|
14
12
|
|
|
15
13
|
MINIMAL_MAP_META = {
|
|
16
14
|
"tile_size": "16;16",
|
|
@@ -188,5 +186,94 @@ class TestObjectSurfaces:
|
|
|
188
186
|
tileset_type="object",
|
|
189
187
|
variant=0,
|
|
190
188
|
)
|
|
191
|
-
|
|
192
|
-
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class TestParsedLayerTtypes:
|
|
192
|
+
def test_object_layer_ttypes_after_parse(self):
|
|
193
|
+
payload = {
|
|
194
|
+
"meta": {"tile_size": "16;16", "map_size": "10;10", "version": "1.1"},
|
|
195
|
+
"resources": {"tilesets": [{"path": "a.png", "type": "tile"}, {"path": "b.png", "type": "tile"}]},
|
|
196
|
+
"project_state": {"rules": [], "groups": []},
|
|
197
|
+
"data": {
|
|
198
|
+
"layers": [
|
|
199
|
+
{
|
|
200
|
+
"name": "Objs",
|
|
201
|
+
"type": "object",
|
|
202
|
+
"visible": True,
|
|
203
|
+
"locked": False,
|
|
204
|
+
"opacity": 1.0,
|
|
205
|
+
"z_index": 0,
|
|
206
|
+
"properties": {},
|
|
207
|
+
"tiles": {},
|
|
208
|
+
"objects": {
|
|
209
|
+
"1": {
|
|
210
|
+
"area": {"x": 0, "y": 0, "w": 16, "h": 16},
|
|
211
|
+
"ttype": 0,
|
|
212
|
+
"tileset_type": "object",
|
|
213
|
+
"variant": 0,
|
|
214
|
+
"properties": {},
|
|
215
|
+
},
|
|
216
|
+
"2": {
|
|
217
|
+
"area": {"x": 0, "y": 0, "w": 16, "h": 16},
|
|
218
|
+
"ttype": 1,
|
|
219
|
+
"tileset_type": "object",
|
|
220
|
+
"variant": 0,
|
|
221
|
+
"properties": {},
|
|
222
|
+
},
|
|
223
|
+
"3": {
|
|
224
|
+
"area": {"x": 0, "y": 0, "w": 16, "h": 16},
|
|
225
|
+
"ttype": 0,
|
|
226
|
+
"tileset_type": "object",
|
|
227
|
+
"variant": 1,
|
|
228
|
+
"properties": {},
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
"name": "Tiles",
|
|
234
|
+
"type": "tile",
|
|
235
|
+
"visible": True,
|
|
236
|
+
"locked": False,
|
|
237
|
+
"opacity": 1.0,
|
|
238
|
+
"z_index": 1,
|
|
239
|
+
"tiles": {},
|
|
240
|
+
},
|
|
241
|
+
],
|
|
242
|
+
},
|
|
243
|
+
}
|
|
244
|
+
parsed = parse_map_dict(payload)
|
|
245
|
+
obj_layer = parsed.layers[0]
|
|
246
|
+
tile_layer = parsed.layers[1]
|
|
247
|
+
|
|
248
|
+
assert obj_layer.ttypes == {0, 1}
|
|
249
|
+
assert tile_layer.ttypes == set()
|
|
250
|
+
|
|
251
|
+
def test_object_layer_empty_ttypes(self):
|
|
252
|
+
payload = {
|
|
253
|
+
"meta": {"tile_size": "16;16", "map_size": "10;10", "version": "1.1"},
|
|
254
|
+
"resources": {"tilesets": []},
|
|
255
|
+
"project_state": {"rules": [], "groups": []},
|
|
256
|
+
"data": {
|
|
257
|
+
"layers": [
|
|
258
|
+
{
|
|
259
|
+
"name": "Empty",
|
|
260
|
+
"type": "object",
|
|
261
|
+
"visible": True,
|
|
262
|
+
"locked": False,
|
|
263
|
+
"opacity": 1.0,
|
|
264
|
+
"z_index": 0,
|
|
265
|
+
"properties": {},
|
|
266
|
+
"tiles": {},
|
|
267
|
+
"objects": {},
|
|
268
|
+
}
|
|
269
|
+
],
|
|
270
|
+
},
|
|
271
|
+
}
|
|
272
|
+
parsed = parse_map_dict(payload)
|
|
273
|
+
assert parsed.layers[0].ttypes == set()
|
|
274
|
+
|
|
275
|
+
def test_ttypes_preserved_through_tilemap_data(self, map_data):
|
|
276
|
+
td, *_ = map_data
|
|
277
|
+
layer = td.get_layer("Object Layer 1")
|
|
278
|
+
assert layer is not None
|
|
279
|
+
assert layer.ttypes == {0}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tilemap_parser-4.2.7 → tilemap_parser-4.2.8}/src/tilemap_parser/runtime/animation_player.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tilemap_parser-4.2.7 → tilemap_parser-4.2.8}/src/tilemap_parser/runtime/object_collision.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tilemap_parser-4.2.7 → tilemap_parser-4.2.8}/src/tilemap_parser.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|