tilemap-parser 4.2.6__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.6 → tilemap_parser-4.2.8}/PKG-INFO +1 -1
- {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/pyproject.toml +1 -1
- {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/src/tilemap_parser/parser/map_parse.py +3 -0
- {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/src/tilemap_parser/runtime/__init__.py +1 -0
- {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/src/tilemap_parser/runtime/map_object.py +32 -23
- 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.6 → tilemap_parser-4.2.8}/src/tilemap_parser.egg-info/PKG-INFO +1 -1
- {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/src/tilemap_parser.egg-info/SOURCES.txt +5 -0
- {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/tests/test_map_object.py +90 -35
- tilemap_parser-4.2.8/tests/test_navigation.py +317 -0
- {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/tests/test_object_surfaces.py +93 -6
- {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/LICENSE +0 -0
- {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/README.md +0 -0
- {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/setup.cfg +0 -0
- {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/src/tilemap_parser/__init__.py +0 -0
- {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/src/tilemap_parser/parser/__init__.py +0 -0
- {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/src/tilemap_parser/parser/animation.py +0 -0
- {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/src/tilemap_parser/parser/collision.py +0 -0
- {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/src/tilemap_parser/parser/collision_loader.py +0 -0
- {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/src/tilemap_parser/parser/node_parse.py +0 -0
- {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/src/tilemap_parser/parser/particle.py +0 -0
- {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/src/tilemap_parser/parser/tmx_converter.py +0 -0
- {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/src/tilemap_parser/runtime/animation_player.py +0 -0
- {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/src/tilemap_parser/runtime/area_node.py +0 -0
- {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/src/tilemap_parser/runtime/camera.py +0 -0
- {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/src/tilemap_parser/runtime/collision_cache.py +0 -0
- {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/src/tilemap_parser/runtime/map_loader.py +0 -0
- {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/src/tilemap_parser/runtime/object_collision.py +0 -0
- {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/src/tilemap_parser/runtime/particles.py +0 -0
- {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/src/tilemap_parser/runtime/protocols.py +0 -0
- {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/src/tilemap_parser/runtime/renderer.py +0 -0
- {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/src/tilemap_parser/runtime/tile_collision.py +0 -0
- {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/src/tilemap_parser/utils/__init__.py +0 -0
- {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/src/tilemap_parser/utils/geometry.py +0 -0
- {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/src/tilemap_parser.egg-info/dependency_links.txt +0 -0
- {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/src/tilemap_parser.egg-info/requires.txt +0 -0
- {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/src/tilemap_parser.egg-info/top_level.txt +0 -0
- {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/tests/test_camera.py +0 -0
- {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/tests/test_collision.py +0 -0
- {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/tests/test_geometry.py +0 -0
- {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/tests/test_gid_collision.py +0 -0
- {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/tests/test_integration_animation.py +0 -0
- {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/tests/test_integration_collision.py +0 -0
- {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/tests/test_integration_map_loader.py +0 -0
- {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/tests/test_map_loader.py +0 -0
- {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/tests/test_move_grounded.py +0 -0
- {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/tests/test_object_collision.py +0 -0
- {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/tests/test_particle_emitter.py +0 -0
- {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/tests/test_render_scale.py +0 -0
- {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/tests/test_tile_collision.py +0 -0
- {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/tests/test_tile_layer_renderer_y_sort.py +0 -0
- {tilemap_parser-4.2.6 → 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"
|
|
@@ -16,7 +16,7 @@ Usage::
|
|
|
16
16
|
from __future__ import annotations
|
|
17
17
|
|
|
18
18
|
from pathlib import Path
|
|
19
|
-
from typing import Dict, List, Optional, Union
|
|
19
|
+
from typing import Dict, List, Optional, Union, final
|
|
20
20
|
|
|
21
21
|
import pygame
|
|
22
22
|
from pygame import Surface
|
|
@@ -27,6 +27,7 @@ from .collision_cache import CollisionCache
|
|
|
27
27
|
from .map_loader import TilemapData
|
|
28
28
|
|
|
29
29
|
|
|
30
|
+
|
|
30
31
|
class MapObject:
|
|
31
32
|
"""
|
|
32
33
|
A game object loaded from a tilemap, carrying a surface for
|
|
@@ -48,28 +49,34 @@ class MapObject:
|
|
|
48
49
|
shape pairs when either object has multiple shapes.
|
|
49
50
|
"""
|
|
50
51
|
|
|
51
|
-
__slots__ = (
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
52
|
+
__slots__ = tuple(
|
|
53
|
+
sorted(
|
|
54
|
+
(
|
|
55
|
+
"x",
|
|
56
|
+
"y",
|
|
57
|
+
"surface",
|
|
58
|
+
"collision_shape",
|
|
59
|
+
"collision_shapes",
|
|
60
|
+
"collision_layer",
|
|
61
|
+
"collision_mask",
|
|
62
|
+
"y_sort_origin",
|
|
63
|
+
"ttype",
|
|
64
|
+
)
|
|
65
|
+
)
|
|
60
66
|
)
|
|
61
67
|
|
|
62
68
|
def __init__(
|
|
63
69
|
self,
|
|
64
70
|
x: float,
|
|
65
71
|
y: float,
|
|
66
|
-
surface:
|
|
67
|
-
collision_shape:
|
|
72
|
+
surface: Surface | None = None,
|
|
73
|
+
collision_shape: CollisionPolygon | None = None,
|
|
68
74
|
*,
|
|
69
|
-
collision_shapes:
|
|
75
|
+
collision_shapes: list[CollisionPolygon] | None = None,
|
|
70
76
|
collision_layer: int = 1,
|
|
71
77
|
collision_mask: int = 0xFFFFFFFF,
|
|
72
|
-
y_sort_origin:
|
|
78
|
+
y_sort_origin: int | None = None,
|
|
79
|
+
ttype: int = -1,
|
|
73
80
|
) -> None:
|
|
74
81
|
self.x = x
|
|
75
82
|
self.y = y
|
|
@@ -86,13 +93,14 @@ class MapObject:
|
|
|
86
93
|
self.collision_layer = collision_layer
|
|
87
94
|
self.collision_mask = collision_mask
|
|
88
95
|
self.y_sort_origin = y_sort_origin
|
|
96
|
+
self.ttype = ttype
|
|
89
97
|
|
|
90
98
|
@property
|
|
91
99
|
def has_collision(self) -> bool:
|
|
92
100
|
return len(self.collision_shapes) > 0
|
|
93
101
|
|
|
94
102
|
|
|
95
|
-
def _resolve_object_collision_filename(tileset_path:
|
|
103
|
+
def _resolve_object_collision_filename(tileset_path: str | Path) -> str:
|
|
96
104
|
"""Extract the object collision filename for a given tileset path.
|
|
97
105
|
|
|
98
106
|
Example: ``"building7.png"`` → ``"building7.object_collision.json"``
|
|
@@ -102,11 +110,11 @@ def _resolve_object_collision_filename(tileset_path: Union[str, Path]) -> str:
|
|
|
102
110
|
|
|
103
111
|
def load_map_objects(
|
|
104
112
|
tilemap_data: TilemapData,
|
|
105
|
-
collision_dir:
|
|
113
|
+
collision_dir: str | Path,
|
|
106
114
|
*,
|
|
107
|
-
cache:
|
|
115
|
+
cache: CollisionCache | None = None,
|
|
108
116
|
require_collision: bool = True,
|
|
109
|
-
) ->
|
|
117
|
+
) -> list[MapObject]:
|
|
110
118
|
"""Load objects from a tilemap.
|
|
111
119
|
|
|
112
120
|
Iterates every object layer in *tilemap_data*, resolves the
|
|
@@ -149,10 +157,10 @@ def load_map_objects(
|
|
|
149
157
|
List of :class:`MapObject` instances.
|
|
150
158
|
"""
|
|
151
159
|
collision_dir = Path(collision_dir)
|
|
152
|
-
objects:
|
|
160
|
+
objects: list[MapObject] = []
|
|
153
161
|
object_layers = tilemap_data.get_layers(layer_type="object")
|
|
154
162
|
|
|
155
|
-
loaded_collision:
|
|
163
|
+
loaded_collision: dict[int, ObjectCollisionData | None] = {}
|
|
156
164
|
|
|
157
165
|
for layer in object_layers:
|
|
158
166
|
for obj_id, obj in layer.objects.items():
|
|
@@ -171,9 +179,7 @@ def load_map_objects(
|
|
|
171
179
|
|
|
172
180
|
ttype = obj.ttype
|
|
173
181
|
if ttype not in loaded_collision:
|
|
174
|
-
collision_data = _load_collision_for_tileset(
|
|
175
|
-
tilemap_data, ttype, collision_dir, cache
|
|
176
|
-
)
|
|
182
|
+
collision_data = _load_collision_for_tileset(tilemap_data, ttype, collision_dir, cache)
|
|
177
183
|
loaded_collision[ttype] = collision_data
|
|
178
184
|
|
|
179
185
|
collision_data = loaded_collision[ttype]
|
|
@@ -186,6 +192,7 @@ def load_map_objects(
|
|
|
186
192
|
surface=surf,
|
|
187
193
|
collision_shape=None,
|
|
188
194
|
collision_shapes=[],
|
|
195
|
+
ttype=obj.ttype,
|
|
189
196
|
)
|
|
190
197
|
objects.append(game_obj)
|
|
191
198
|
continue
|
|
@@ -209,6 +216,7 @@ def load_map_objects(
|
|
|
209
216
|
surface=surf,
|
|
210
217
|
collision_shape=None,
|
|
211
218
|
collision_shapes=[],
|
|
219
|
+
ttype=obj.ttype,
|
|
212
220
|
)
|
|
213
221
|
objects.append(game_obj)
|
|
214
222
|
continue
|
|
@@ -221,6 +229,7 @@ def load_map_objects(
|
|
|
221
229
|
collision_shapes=world_shapes,
|
|
222
230
|
collision_layer=region_layer,
|
|
223
231
|
collision_mask=region_mask,
|
|
232
|
+
ttype=obj.ttype,
|
|
224
233
|
)
|
|
225
234
|
objects.append(game_obj)
|
|
226
235
|
|
|
@@ -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
|