tilemap-parser 1.0.1__tar.gz → 1.1.1__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-1.0.1/src/tilemap_parser.egg-info → tilemap_parser-1.1.1}/PKG-INFO +1 -1
- {tilemap_parser-1.0.1 → tilemap_parser-1.1.1}/pyproject.toml +1 -1
- {tilemap_parser-1.0.1 → tilemap_parser-1.1.1}/src/tilemap_parser/__init__.py +44 -0
- tilemap_parser-1.1.1/src/tilemap_parser/collision.py +344 -0
- tilemap_parser-1.1.1/src/tilemap_parser/collision_runner.py +918 -0
- {tilemap_parser-1.0.1 → tilemap_parser-1.1.1/src/tilemap_parser.egg-info}/PKG-INFO +1 -1
- {tilemap_parser-1.0.1 → tilemap_parser-1.1.1}/src/tilemap_parser.egg-info/SOURCES.txt +2 -0
- {tilemap_parser-1.0.1 → tilemap_parser-1.1.1}/LICENSE +0 -0
- {tilemap_parser-1.0.1 → tilemap_parser-1.1.1}/README.md +0 -0
- {tilemap_parser-1.0.1 → tilemap_parser-1.1.1}/setup.cfg +0 -0
- {tilemap_parser-1.0.1 → tilemap_parser-1.1.1}/src/tilemap_parser/animation.py +0 -0
- {tilemap_parser-1.0.1 → tilemap_parser-1.1.1}/src/tilemap_parser/map_loader.py +0 -0
- {tilemap_parser-1.0.1 → tilemap_parser-1.1.1}/src/tilemap_parser/map_parse.py +0 -0
- {tilemap_parser-1.0.1 → tilemap_parser-1.1.1}/src/tilemap_parser/renderer.py +0 -0
- {tilemap_parser-1.0.1 → tilemap_parser-1.1.1}/src/tilemap_parser.egg-info/dependency_links.txt +0 -0
- {tilemap_parser-1.0.1 → tilemap_parser-1.1.1}/src/tilemap_parser.egg-info/requires.txt +0 -0
- {tilemap_parser-1.0.1 → tilemap_parser-1.1.1}/src/tilemap_parser.egg-info/top_level.txt +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "tilemap-parser"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.1.1"
|
|
8
8
|
description = "Standalone parser/loader for tilemap-editor JSON maps and sprite animation JSON."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -10,6 +10,30 @@ from .animation import (
|
|
|
10
10
|
parse_animation_file,
|
|
11
11
|
parse_animation_json,
|
|
12
12
|
)
|
|
13
|
+
from .collision import (
|
|
14
|
+
CapsuleShape,
|
|
15
|
+
CharacterCollision,
|
|
16
|
+
CircleShape,
|
|
17
|
+
CollisionCache,
|
|
18
|
+
CollisionParseError,
|
|
19
|
+
CollisionPolygon,
|
|
20
|
+
RectangleShape,
|
|
21
|
+
TileCollisionData,
|
|
22
|
+
TilesetCollision,
|
|
23
|
+
clear_collision_cache,
|
|
24
|
+
get_cached_character_collision,
|
|
25
|
+
get_cached_tileset_collision,
|
|
26
|
+
load_character_collision,
|
|
27
|
+
load_tileset_collision,
|
|
28
|
+
parse_character_collision,
|
|
29
|
+
parse_tileset_collision,
|
|
30
|
+
)
|
|
31
|
+
from .collision_runner import (
|
|
32
|
+
CollisionResult,
|
|
33
|
+
CollisionRunner,
|
|
34
|
+
ICollidableSprite,
|
|
35
|
+
MovementMode,
|
|
36
|
+
)
|
|
13
37
|
from .map_loader import TilemapData, load_map
|
|
14
38
|
from .map_parse import (
|
|
15
39
|
MapParseError,
|
|
@@ -36,8 +60,18 @@ __all__ = [
|
|
|
36
60
|
"AnimationMarker",
|
|
37
61
|
"AnimationParseError",
|
|
38
62
|
"AnimationPlayer",
|
|
63
|
+
"CapsuleShape",
|
|
64
|
+
"CharacterCollision",
|
|
65
|
+
"CircleShape",
|
|
66
|
+
"CollisionCache",
|
|
67
|
+
"CollisionParseError",
|
|
68
|
+
"CollisionPolygon",
|
|
69
|
+
"CollisionResult",
|
|
70
|
+
"CollisionRunner",
|
|
71
|
+
"ICollidableSprite",
|
|
39
72
|
"LayerRenderStats",
|
|
40
73
|
"MapParseError",
|
|
74
|
+
"MovementMode",
|
|
41
75
|
"ParsedAutotileGroup",
|
|
42
76
|
"ParsedAutotileRule",
|
|
43
77
|
"ParsedLayer",
|
|
@@ -48,14 +82,24 @@ __all__ = [
|
|
|
48
82
|
"ParsedProjectState",
|
|
49
83
|
"ParsedTile",
|
|
50
84
|
"ParsedTileset",
|
|
85
|
+
"RectangleShape",
|
|
51
86
|
"SpriteAnimationSet",
|
|
87
|
+
"TileCollisionData",
|
|
52
88
|
"TileLayerRenderer",
|
|
89
|
+
"TilesetCollision",
|
|
53
90
|
"TilemapData",
|
|
91
|
+
"clear_collision_cache",
|
|
92
|
+
"get_cached_character_collision",
|
|
93
|
+
"get_cached_tileset_collision",
|
|
94
|
+
"load_character_collision",
|
|
54
95
|
"load_map",
|
|
96
|
+
"load_tileset_collision",
|
|
55
97
|
"parse_animation_dict",
|
|
56
98
|
"parse_animation_file",
|
|
57
99
|
"parse_animation_json",
|
|
100
|
+
"parse_character_collision",
|
|
58
101
|
"parse_map_dict",
|
|
59
102
|
"parse_map_file",
|
|
60
103
|
"parse_map_json",
|
|
104
|
+
"parse_tileset_collision",
|
|
61
105
|
]
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Collision data parser and runtime utilities for tilemap collision systems.
|
|
3
|
+
|
|
4
|
+
This module provides parsers for both tileset collision (polygon-based) and
|
|
5
|
+
character collision (geometric shapes) data generated by the tilemap editor.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
14
|
+
|
|
15
|
+
JsonDict = Dict[str, Any]
|
|
16
|
+
Point = Tuple[float, float]
|
|
17
|
+
IntPoint = Tuple[int, int]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CollisionParseError(ValueError):
|
|
21
|
+
"""Raised when collision data cannot be parsed"""
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class CollisionPolygon:
|
|
27
|
+
"""Polygon collision shape for a tile"""
|
|
28
|
+
|
|
29
|
+
vertices: List[Point]
|
|
30
|
+
one_way: bool = False
|
|
31
|
+
|
|
32
|
+
def transform(self, tile_x: float, tile_y: float) -> "CollisionPolygon":
|
|
33
|
+
"""Transform polygon to world space coordinates"""
|
|
34
|
+
world_vertices = [(tile_x + vx, tile_y + vy) for vx, vy in self.vertices]
|
|
35
|
+
return CollisionPolygon(vertices=world_vertices, one_way=self.one_way)
|
|
36
|
+
|
|
37
|
+
def is_valid(self) -> bool:
|
|
38
|
+
"""Check if polygon has at least 3 vertices"""
|
|
39
|
+
return len(self.vertices) >= 3
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class TileCollisionData:
|
|
44
|
+
"""Collision data for a single tile"""
|
|
45
|
+
|
|
46
|
+
tile_id: int
|
|
47
|
+
shapes: List[CollisionPolygon] = field(default_factory=list)
|
|
48
|
+
|
|
49
|
+
def has_collision(self) -> bool:
|
|
50
|
+
"""Check if tile has any valid collision shapes"""
|
|
51
|
+
return any(shape.is_valid() for shape in self.shapes)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class TilesetCollision:
|
|
56
|
+
"""Complete collision data for a tileset"""
|
|
57
|
+
|
|
58
|
+
tileset_name: str
|
|
59
|
+
tile_size: IntPoint
|
|
60
|
+
tiles: Dict[int, TileCollisionData] = field(default_factory=dict)
|
|
61
|
+
|
|
62
|
+
def get_tile_collision(self, tile_id: int) -> Optional[TileCollisionData]:
|
|
63
|
+
"""Get collision data for a specific tile"""
|
|
64
|
+
return self.tiles.get(tile_id)
|
|
65
|
+
|
|
66
|
+
def has_collision(self, tile_id: int) -> bool:
|
|
67
|
+
"""Check if tile has collision data"""
|
|
68
|
+
tile_data = self.get_tile_collision(tile_id)
|
|
69
|
+
return tile_data is not None and tile_data.has_collision()
|
|
70
|
+
|
|
71
|
+
def get_world_shapes(
|
|
72
|
+
self, tile_id: int, tile_x: float, tile_y: float
|
|
73
|
+
) -> List[CollisionPolygon]:
|
|
74
|
+
"""Get collision shapes transformed to world space"""
|
|
75
|
+
tile_data = self.get_tile_collision(tile_id)
|
|
76
|
+
if not tile_data:
|
|
77
|
+
return []
|
|
78
|
+
return [shape.transform(tile_x, tile_y) for shape in tile_data.shapes]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass
|
|
82
|
+
class RectangleShape:
|
|
83
|
+
"""Rectangle collision shape"""
|
|
84
|
+
|
|
85
|
+
width: float
|
|
86
|
+
height: float
|
|
87
|
+
offset: Point = (0.0, 0.0)
|
|
88
|
+
|
|
89
|
+
def get_bounds(self, x: float, y: float) -> Tuple[float, float, float, float]:
|
|
90
|
+
"""Get AABB bounds in world space (left, top, right, bottom)"""
|
|
91
|
+
left = x + self.offset[0]
|
|
92
|
+
top = y + self.offset[1]
|
|
93
|
+
return (left, top, left + self.width, top + self.height)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@dataclass
|
|
97
|
+
class CircleShape:
|
|
98
|
+
"""Circle collision shape"""
|
|
99
|
+
|
|
100
|
+
radius: float
|
|
101
|
+
offset: Point = (0.0, 0.0)
|
|
102
|
+
|
|
103
|
+
def get_center(self, x: float, y: float) -> Point:
|
|
104
|
+
"""Get center position in world space"""
|
|
105
|
+
return (x + self.offset[0], y + self.offset[1])
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@dataclass
|
|
109
|
+
class CapsuleShape:
|
|
110
|
+
"""Capsule collision shape (vertical orientation)"""
|
|
111
|
+
|
|
112
|
+
radius: float
|
|
113
|
+
height: float
|
|
114
|
+
offset: Point = (0.0, 0.0)
|
|
115
|
+
|
|
116
|
+
def get_top_center(self, x: float, y: float) -> Point:
|
|
117
|
+
"""Get top circle center in world space"""
|
|
118
|
+
return (x + self.offset[0], y + self.offset[1])
|
|
119
|
+
|
|
120
|
+
def get_bottom_center(self, x: float, y: float) -> Point:
|
|
121
|
+
"""Get bottom circle center in world space"""
|
|
122
|
+
return (x + self.offset[0], y + self.offset[1] + self.height)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
CharacterShapeType = Union[RectangleShape, CircleShape, CapsuleShape]
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@dataclass
|
|
129
|
+
class CharacterCollision:
|
|
130
|
+
"""Complete collision data for a character sprite"""
|
|
131
|
+
|
|
132
|
+
name: str
|
|
133
|
+
shape: CharacterShapeType
|
|
134
|
+
properties: Dict[str, Any] = field(default_factory=dict)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def parse_tileset_collision(data: JsonDict) -> TilesetCollision:
|
|
138
|
+
"""
|
|
139
|
+
Parse tileset collision data from dictionary.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
data: Dictionary loaded from .collision.json file
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
TilesetCollision object
|
|
146
|
+
|
|
147
|
+
Raises:
|
|
148
|
+
CollisionParseError: If data format is invalid
|
|
149
|
+
"""
|
|
150
|
+
try:
|
|
151
|
+
tileset_name = data["tileset_name"]
|
|
152
|
+
tile_size_raw = data["tile_size"]
|
|
153
|
+
tile_size = (int(tile_size_raw[0]), int(tile_size_raw[1]))
|
|
154
|
+
|
|
155
|
+
tiles: Dict[int, TileCollisionData] = {}
|
|
156
|
+
tiles_data = data.get("tiles", {})
|
|
157
|
+
|
|
158
|
+
for tile_id_str, tile_data in tiles_data.items():
|
|
159
|
+
tile_id = int(tile_id_str)
|
|
160
|
+
shapes: List[CollisionPolygon] = []
|
|
161
|
+
|
|
162
|
+
for shape_data in tile_data.get("shapes", []):
|
|
163
|
+
vertices = [tuple(v) for v in shape_data["vertices"]]
|
|
164
|
+
one_way = shape_data.get("one_way", False)
|
|
165
|
+
shapes.append(CollisionPolygon(vertices=vertices, one_way=one_way))
|
|
166
|
+
|
|
167
|
+
tiles[tile_id] = TileCollisionData(tile_id=tile_id, shapes=shapes)
|
|
168
|
+
|
|
169
|
+
return TilesetCollision(
|
|
170
|
+
tileset_name=tileset_name, tile_size=tile_size, tiles=tiles
|
|
171
|
+
)
|
|
172
|
+
except (KeyError, ValueError, TypeError) as e:
|
|
173
|
+
raise CollisionParseError(f"Invalid tileset collision data: {e}") from e
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def parse_character_collision(data: JsonDict) -> CharacterCollision:
|
|
177
|
+
"""
|
|
178
|
+
Parse character collision data from dictionary.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
data: Dictionary loaded from .collision.json file
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
CharacterCollision object
|
|
185
|
+
|
|
186
|
+
Raises:
|
|
187
|
+
CollisionParseError: If data format is invalid
|
|
188
|
+
"""
|
|
189
|
+
try:
|
|
190
|
+
name = data["name"]
|
|
191
|
+
shape_data = data["shape"]
|
|
192
|
+
shape_type = shape_data["type"]
|
|
193
|
+
offset = tuple(shape_data.get("offset", (0.0, 0.0)))
|
|
194
|
+
|
|
195
|
+
if shape_type == "rectangle":
|
|
196
|
+
shape = RectangleShape(
|
|
197
|
+
width=float(shape_data["width"]),
|
|
198
|
+
height=float(shape_data["height"]),
|
|
199
|
+
offset=offset,
|
|
200
|
+
)
|
|
201
|
+
elif shape_type == "circle":
|
|
202
|
+
shape = CircleShape(radius=float(shape_data["radius"]), offset=offset)
|
|
203
|
+
elif shape_type == "capsule":
|
|
204
|
+
shape = CapsuleShape(
|
|
205
|
+
radius=float(shape_data["radius"]),
|
|
206
|
+
height=float(shape_data["height"]),
|
|
207
|
+
offset=offset,
|
|
208
|
+
)
|
|
209
|
+
else:
|
|
210
|
+
raise CollisionParseError(f"Unknown shape type: {shape_type}")
|
|
211
|
+
|
|
212
|
+
properties = data.get("properties", {})
|
|
213
|
+
|
|
214
|
+
return CharacterCollision(name=name, shape=shape, properties=properties)
|
|
215
|
+
except (KeyError, ValueError, TypeError) as e:
|
|
216
|
+
raise CollisionParseError(f"Invalid character collision data: {e}") from e
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def load_tileset_collision(
|
|
220
|
+
tileset_path: Union[str, Path],
|
|
221
|
+
) -> Optional[TilesetCollision]:
|
|
222
|
+
"""
|
|
223
|
+
Load tileset collision data from file.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
tileset_path: Path to tileset image file
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
TilesetCollision object or None if collision file doesn't exist
|
|
230
|
+
|
|
231
|
+
Raises:
|
|
232
|
+
CollisionParseError: If collision file exists but is invalid
|
|
233
|
+
"""
|
|
234
|
+
tileset_path = Path(tileset_path)
|
|
235
|
+
collision_path = tileset_path.with_suffix(".collision.json")
|
|
236
|
+
|
|
237
|
+
if not collision_path.exists():
|
|
238
|
+
return None
|
|
239
|
+
|
|
240
|
+
try:
|
|
241
|
+
with open(collision_path, "r", encoding="utf-8") as f:
|
|
242
|
+
data = json.load(f)
|
|
243
|
+
return parse_tileset_collision(data)
|
|
244
|
+
except (OSError, json.JSONDecodeError) as e:
|
|
245
|
+
raise CollisionParseError(f"Cannot load {collision_path}: {e}") from e
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def load_character_collision(
|
|
249
|
+
sprite_path: Union[str, Path],
|
|
250
|
+
) -> Optional[CharacterCollision]:
|
|
251
|
+
"""
|
|
252
|
+
Load character collision data from file.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
sprite_path: Path to character sprite image file
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
CharacterCollision object or None if collision file doesn't exist
|
|
259
|
+
|
|
260
|
+
Raises:
|
|
261
|
+
CollisionParseError: If collision file exists but is invalid
|
|
262
|
+
"""
|
|
263
|
+
sprite_path = Path(sprite_path)
|
|
264
|
+
collision_path = sprite_path.with_suffix(".collision.json")
|
|
265
|
+
|
|
266
|
+
if not collision_path.exists():
|
|
267
|
+
return None
|
|
268
|
+
|
|
269
|
+
try:
|
|
270
|
+
with open(collision_path, "r", encoding="utf-8") as f:
|
|
271
|
+
data = json.load(f)
|
|
272
|
+
return parse_character_collision(data)
|
|
273
|
+
except (OSError, json.JSONDecodeError) as e:
|
|
274
|
+
raise CollisionParseError(f"Cannot load {collision_path}: {e}") from e
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
class CollisionCache:
|
|
278
|
+
"""
|
|
279
|
+
Optimized collision data cache with fast lookups.
|
|
280
|
+
|
|
281
|
+
Caches parsed collision data to avoid repeated file I/O and parsing.
|
|
282
|
+
Useful for runtime performance in game engines.
|
|
283
|
+
"""
|
|
284
|
+
|
|
285
|
+
def __init__(self):
|
|
286
|
+
self._tileset_cache: Dict[str, Optional[TilesetCollision]] = {}
|
|
287
|
+
self._character_cache: Dict[str, Optional[CharacterCollision]] = {}
|
|
288
|
+
|
|
289
|
+
def get_tileset_collision(
|
|
290
|
+
self, tileset_path: Union[str, Path]
|
|
291
|
+
) -> Optional[TilesetCollision]:
|
|
292
|
+
"""Get tileset collision data (cached)"""
|
|
293
|
+
key = str(Path(tileset_path).resolve())
|
|
294
|
+
|
|
295
|
+
if key not in self._tileset_cache:
|
|
296
|
+
self._tileset_cache[key] = load_tileset_collision(tileset_path)
|
|
297
|
+
|
|
298
|
+
return self._tileset_cache[key]
|
|
299
|
+
|
|
300
|
+
def get_character_collision(
|
|
301
|
+
self, sprite_path: Union[str, Path]
|
|
302
|
+
) -> Optional[CharacterCollision]:
|
|
303
|
+
"""Get character collision data (cached)"""
|
|
304
|
+
key = str(Path(sprite_path).resolve())
|
|
305
|
+
|
|
306
|
+
if key not in self._character_cache:
|
|
307
|
+
self._character_cache[key] = load_character_collision(sprite_path)
|
|
308
|
+
|
|
309
|
+
return self._character_cache[key]
|
|
310
|
+
|
|
311
|
+
def clear(self):
|
|
312
|
+
"""Clear all cached collision data"""
|
|
313
|
+
self._tileset_cache.clear()
|
|
314
|
+
self._character_cache.clear()
|
|
315
|
+
|
|
316
|
+
def preload_tileset(self, tileset_path: Union[str, Path]):
|
|
317
|
+
"""Preload tileset collision data into cache"""
|
|
318
|
+
self.get_tileset_collision(tileset_path)
|
|
319
|
+
|
|
320
|
+
def preload_character(self, sprite_path: Union[str, Path]):
|
|
321
|
+
"""Preload character collision data into cache"""
|
|
322
|
+
self.get_character_collision(sprite_path)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
_global_cache = CollisionCache()
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def get_cached_tileset_collision(
|
|
329
|
+
tileset_path: Union[str, Path],
|
|
330
|
+
) -> Optional[TilesetCollision]:
|
|
331
|
+
"""Get tileset collision using global cache"""
|
|
332
|
+
return _global_cache.get_tileset_collision(tileset_path)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def get_cached_character_collision(
|
|
336
|
+
sprite_path: Union[str, Path],
|
|
337
|
+
) -> Optional[CharacterCollision]:
|
|
338
|
+
"""Get character collision using global cache"""
|
|
339
|
+
return _global_cache.get_character_collision(sprite_path)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def clear_collision_cache():
|
|
343
|
+
"""Clear global collision cache"""
|
|
344
|
+
_global_cache.clear()
|