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.
Files changed (55) hide show
  1. {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/PKG-INFO +1 -1
  2. {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/pyproject.toml +1 -1
  3. {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/src/tilemap_parser/parser/map_parse.py +3 -0
  4. {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/src/tilemap_parser/runtime/__init__.py +1 -0
  5. {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/src/tilemap_parser/runtime/map_object.py +32 -23
  6. tilemap_parser-4.2.8/src/tilemap_parser/runtime/navigation/__init__.py +3 -0
  7. tilemap_parser-4.2.8/src/tilemap_parser/runtime/navigation/nav_grid.py +184 -0
  8. tilemap_parser-4.2.8/src/tilemap_parser/runtime/navigation/path_follower.py +76 -0
  9. tilemap_parser-4.2.8/src/tilemap_parser/runtime/navigation/pathfinder.py +75 -0
  10. {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/src/tilemap_parser.egg-info/PKG-INFO +1 -1
  11. {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/src/tilemap_parser.egg-info/SOURCES.txt +5 -0
  12. {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/tests/test_map_object.py +90 -35
  13. tilemap_parser-4.2.8/tests/test_navigation.py +317 -0
  14. {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/tests/test_object_surfaces.py +93 -6
  15. {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/LICENSE +0 -0
  16. {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/README.md +0 -0
  17. {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/setup.cfg +0 -0
  18. {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/src/tilemap_parser/__init__.py +0 -0
  19. {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/src/tilemap_parser/parser/__init__.py +0 -0
  20. {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/src/tilemap_parser/parser/animation.py +0 -0
  21. {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/src/tilemap_parser/parser/collision.py +0 -0
  22. {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/src/tilemap_parser/parser/collision_loader.py +0 -0
  23. {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/src/tilemap_parser/parser/node_parse.py +0 -0
  24. {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/src/tilemap_parser/parser/particle.py +0 -0
  25. {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/src/tilemap_parser/parser/tmx_converter.py +0 -0
  26. {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/src/tilemap_parser/runtime/animation_player.py +0 -0
  27. {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/src/tilemap_parser/runtime/area_node.py +0 -0
  28. {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/src/tilemap_parser/runtime/camera.py +0 -0
  29. {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/src/tilemap_parser/runtime/collision_cache.py +0 -0
  30. {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/src/tilemap_parser/runtime/map_loader.py +0 -0
  31. {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/src/tilemap_parser/runtime/object_collision.py +0 -0
  32. {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/src/tilemap_parser/runtime/particles.py +0 -0
  33. {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/src/tilemap_parser/runtime/protocols.py +0 -0
  34. {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/src/tilemap_parser/runtime/renderer.py +0 -0
  35. {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/src/tilemap_parser/runtime/tile_collision.py +0 -0
  36. {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/src/tilemap_parser/utils/__init__.py +0 -0
  37. {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/src/tilemap_parser/utils/geometry.py +0 -0
  38. {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/src/tilemap_parser.egg-info/dependency_links.txt +0 -0
  39. {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/src/tilemap_parser.egg-info/requires.txt +0 -0
  40. {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/src/tilemap_parser.egg-info/top_level.txt +0 -0
  41. {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/tests/test_camera.py +0 -0
  42. {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/tests/test_collision.py +0 -0
  43. {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/tests/test_geometry.py +0 -0
  44. {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/tests/test_gid_collision.py +0 -0
  45. {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/tests/test_integration_animation.py +0 -0
  46. {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/tests/test_integration_collision.py +0 -0
  47. {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/tests/test_integration_map_loader.py +0 -0
  48. {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/tests/test_map_loader.py +0 -0
  49. {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/tests/test_move_grounded.py +0 -0
  50. {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/tests/test_object_collision.py +0 -0
  51. {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/tests/test_particle_emitter.py +0 -0
  52. {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/tests/test_render_scale.py +0 -0
  53. {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/tests/test_tile_collision.py +0 -0
  54. {tilemap_parser-4.2.6 → tilemap_parser-4.2.8}/tests/test_tile_layer_renderer_y_sort.py +0 -0
  55. {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.6
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.6"
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"
@@ -28,6 +28,7 @@ from .object_collision import (
28
28
  )
29
29
  from .renderer import LayerRenderStats, TileLayerRenderer
30
30
  from .area_node import AreaNode
31
+ from . import navigation
31
32
  from .particles import (
32
33
  Particle,
33
34
  ParticleEmitter,
@@ -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
- "x",
53
- "y",
54
- "surface",
55
- "collision_shape",
56
- "collision_shapes",
57
- "collision_layer",
58
- "collision_mask",
59
- "y_sort_origin",
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: Optional[Surface] = None,
67
- collision_shape: Optional[CollisionPolygon] = None,
72
+ surface: Surface | None = None,
73
+ collision_shape: CollisionPolygon | None = None,
68
74
  *,
69
- collision_shapes: Optional[List[CollisionPolygon]] = None,
75
+ collision_shapes: list[CollisionPolygon] | None = None,
70
76
  collision_layer: int = 1,
71
77
  collision_mask: int = 0xFFFFFFFF,
72
- y_sort_origin: Optional[int] = None,
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: Union[str, Path]) -> str:
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: Union[str, Path],
113
+ collision_dir: str | Path,
106
114
  *,
107
- cache: Optional[CollisionCache] = None,
115
+ cache: CollisionCache | None = None,
108
116
  require_collision: bool = True,
109
- ) -> List[MapObject]:
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: List[MapObject] = []
160
+ objects: list[MapObject] = []
153
161
  object_layers = tilemap_data.get_layers(layer_type="object")
154
162
 
155
- loaded_collision: Dict[int, Optional[ObjectCollisionData]] = {}
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,3 @@
1
+ from .nav_grid import NavGrid
2
+ from .pathfinder import Pathfinder
3
+ from .path_follower import PathFollower
@@ -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.6
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