tilemap-parser 5.1.0__tar.gz → 5.2.0__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-5.1.0 → tilemap_parser-5.2.0}/PKG-INFO +1 -1
- {tilemap_parser-5.1.0 → tilemap_parser-5.2.0}/pyproject.toml +1 -1
- {tilemap_parser-5.1.0 → tilemap_parser-5.2.0}/src/tilemap_parser/__init__.py +2 -0
- {tilemap_parser-5.1.0 → tilemap_parser-5.2.0}/src/tilemap_parser/parser/__init__.py +2 -0
- {tilemap_parser-5.1.0 → tilemap_parser-5.2.0}/src/tilemap_parser/parser/map_parse.py +109 -4
- {tilemap_parser-5.1.0 → tilemap_parser-5.2.0}/src/tilemap_parser/runtime/__init__.py +2 -1
- {tilemap_parser-5.1.0 → tilemap_parser-5.2.0}/src/tilemap_parser/runtime/map_loader.py +154 -2
- {tilemap_parser-5.1.0 → tilemap_parser-5.2.0}/src/tilemap_parser.egg-info/PKG-INFO +1 -1
- {tilemap_parser-5.1.0 → tilemap_parser-5.2.0}/src/tilemap_parser.egg-info/SOURCES.txt +2 -0
- tilemap_parser-5.2.0/tests/test_background_layer.py +223 -0
- tilemap_parser-5.2.0/tests/test_object_animation.py +364 -0
- {tilemap_parser-5.1.0 → tilemap_parser-5.2.0}/LICENSE +0 -0
- {tilemap_parser-5.1.0 → tilemap_parser-5.2.0}/README.md +0 -0
- {tilemap_parser-5.1.0 → tilemap_parser-5.2.0}/pypi-readme.md +0 -0
- {tilemap_parser-5.1.0 → tilemap_parser-5.2.0}/setup.cfg +0 -0
- {tilemap_parser-5.1.0 → tilemap_parser-5.2.0}/src/tilemap_parser/parser/animation.py +0 -0
- {tilemap_parser-5.1.0 → tilemap_parser-5.2.0}/src/tilemap_parser/parser/collision.py +0 -0
- {tilemap_parser-5.1.0 → tilemap_parser-5.2.0}/src/tilemap_parser/parser/collision_loader.py +0 -0
- {tilemap_parser-5.1.0 → tilemap_parser-5.2.0}/src/tilemap_parser/parser/node_parse.py +0 -0
- {tilemap_parser-5.1.0 → tilemap_parser-5.2.0}/src/tilemap_parser/parser/particle.py +0 -0
- {tilemap_parser-5.1.0 → tilemap_parser-5.2.0}/src/tilemap_parser/parser/tmx_converter.py +0 -0
- {tilemap_parser-5.1.0 → tilemap_parser-5.2.0}/src/tilemap_parser/runtime/animation_player.py +0 -0
- {tilemap_parser-5.1.0 → tilemap_parser-5.2.0}/src/tilemap_parser/runtime/area_node.py +0 -0
- {tilemap_parser-5.1.0 → tilemap_parser-5.2.0}/src/tilemap_parser/runtime/body.py +0 -0
- {tilemap_parser-5.1.0 → tilemap_parser-5.2.0}/src/tilemap_parser/runtime/camera.py +0 -0
- {tilemap_parser-5.1.0 → tilemap_parser-5.2.0}/src/tilemap_parser/runtime/collision/__init__.py +0 -0
- {tilemap_parser-5.1.0 → tilemap_parser-5.2.0}/src/tilemap_parser/runtime/collision/hit.py +0 -0
- {tilemap_parser-5.1.0 → tilemap_parser-5.2.0}/src/tilemap_parser/runtime/collision/manager.py +0 -0
- {tilemap_parser-5.1.0 → tilemap_parser-5.2.0}/src/tilemap_parser/runtime/collision/shapes.py +0 -0
- {tilemap_parser-5.1.0 → tilemap_parser-5.2.0}/src/tilemap_parser/runtime/collision_cache.py +0 -0
- {tilemap_parser-5.1.0 → tilemap_parser-5.2.0}/src/tilemap_parser/runtime/map_object.py +0 -0
- {tilemap_parser-5.1.0 → tilemap_parser-5.2.0}/src/tilemap_parser/runtime/movement/__init__.py +0 -0
- {tilemap_parser-5.1.0 → tilemap_parser-5.2.0}/src/tilemap_parser/runtime/movement/grounded.py +0 -0
- {tilemap_parser-5.1.0 → tilemap_parser-5.2.0}/src/tilemap_parser/runtime/movement/platformer.py +0 -0
- {tilemap_parser-5.1.0 → tilemap_parser-5.2.0}/src/tilemap_parser/runtime/movement/queries.py +0 -0
- {tilemap_parser-5.1.0 → tilemap_parser-5.2.0}/src/tilemap_parser/runtime/movement/rpg.py +0 -0
- {tilemap_parser-5.1.0 → tilemap_parser-5.2.0}/src/tilemap_parser/runtime/movement/runner.py +0 -0
- {tilemap_parser-5.1.0 → tilemap_parser-5.2.0}/src/tilemap_parser/runtime/movement/slide.py +0 -0
- {tilemap_parser-5.1.0 → tilemap_parser-5.2.0}/src/tilemap_parser/runtime/movement/types.py +0 -0
- {tilemap_parser-5.1.0 → tilemap_parser-5.2.0}/src/tilemap_parser/runtime/navigation/__init__.py +0 -0
- {tilemap_parser-5.1.0 → tilemap_parser-5.2.0}/src/tilemap_parser/runtime/navigation/nav_grid.py +0 -0
- {tilemap_parser-5.1.0 → tilemap_parser-5.2.0}/src/tilemap_parser/runtime/navigation/path_follower.py +0 -0
- {tilemap_parser-5.1.0 → tilemap_parser-5.2.0}/src/tilemap_parser/runtime/navigation/pathfinder.py +0 -0
- {tilemap_parser-5.1.0 → tilemap_parser-5.2.0}/src/tilemap_parser/runtime/object_collision.py +0 -0
- {tilemap_parser-5.1.0 → tilemap_parser-5.2.0}/src/tilemap_parser/runtime/particles.py +0 -0
- {tilemap_parser-5.1.0 → tilemap_parser-5.2.0}/src/tilemap_parser/runtime/polygon_query.py +0 -0
- {tilemap_parser-5.1.0 → tilemap_parser-5.2.0}/src/tilemap_parser/runtime/protocols.py +0 -0
- {tilemap_parser-5.1.0 → tilemap_parser-5.2.0}/src/tilemap_parser/runtime/renderer.py +0 -0
- {tilemap_parser-5.1.0 → tilemap_parser-5.2.0}/src/tilemap_parser/runtime/tile_collision.py +0 -0
- {tilemap_parser-5.1.0 → tilemap_parser-5.2.0}/src/tilemap_parser/runtime/world.py +0 -0
- {tilemap_parser-5.1.0 → tilemap_parser-5.2.0}/src/tilemap_parser/utils/__init__.py +0 -0
- {tilemap_parser-5.1.0 → tilemap_parser-5.2.0}/src/tilemap_parser/utils/geometry.py +0 -0
- {tilemap_parser-5.1.0 → tilemap_parser-5.2.0}/src/tilemap_parser.egg-info/dependency_links.txt +0 -0
- {tilemap_parser-5.1.0 → tilemap_parser-5.2.0}/src/tilemap_parser.egg-info/requires.txt +0 -0
- {tilemap_parser-5.1.0 → tilemap_parser-5.2.0}/src/tilemap_parser.egg-info/top_level.txt +0 -0
- {tilemap_parser-5.1.0 → tilemap_parser-5.2.0}/tests/test_body.py +0 -0
- {tilemap_parser-5.1.0 → tilemap_parser-5.2.0}/tests/test_camera.py +0 -0
- {tilemap_parser-5.1.0 → tilemap_parser-5.2.0}/tests/test_collision.py +0 -0
- {tilemap_parser-5.1.0 → tilemap_parser-5.2.0}/tests/test_geometry.py +0 -0
- {tilemap_parser-5.1.0 → tilemap_parser-5.2.0}/tests/test_gid_collision.py +0 -0
- {tilemap_parser-5.1.0 → tilemap_parser-5.2.0}/tests/test_gid_routing.py +0 -0
- {tilemap_parser-5.1.0 → tilemap_parser-5.2.0}/tests/test_integration_animation.py +0 -0
- {tilemap_parser-5.1.0 → tilemap_parser-5.2.0}/tests/test_integration_collision.py +0 -0
- {tilemap_parser-5.1.0 → tilemap_parser-5.2.0}/tests/test_integration_map_loader.py +0 -0
- {tilemap_parser-5.1.0 → tilemap_parser-5.2.0}/tests/test_map_loader.py +0 -0
- {tilemap_parser-5.1.0 → tilemap_parser-5.2.0}/tests/test_map_object.py +0 -0
- {tilemap_parser-5.1.0 → tilemap_parser-5.2.0}/tests/test_move_grounded.py +0 -0
- {tilemap_parser-5.1.0 → tilemap_parser-5.2.0}/tests/test_navigation.py +0 -0
- {tilemap_parser-5.1.0 → tilemap_parser-5.2.0}/tests/test_object_collision.py +0 -0
- {tilemap_parser-5.1.0 → tilemap_parser-5.2.0}/tests/test_object_surfaces.py +0 -0
- {tilemap_parser-5.1.0 → tilemap_parser-5.2.0}/tests/test_particle_emitter.py +0 -0
- {tilemap_parser-5.1.0 → tilemap_parser-5.2.0}/tests/test_render_scale.py +0 -0
- {tilemap_parser-5.1.0 → tilemap_parser-5.2.0}/tests/test_tile_collision.py +0 -0
- {tilemap_parser-5.1.0 → tilemap_parser-5.2.0}/tests/test_tile_layer_renderer_y_sort.py +0 -0
- {tilemap_parser-5.1.0 → tilemap_parser-5.2.0}/tests/test_tmx_converter.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tilemap-parser
|
|
3
|
-
Version: 5.
|
|
3
|
+
Version: 5.2.0
|
|
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 = "5.
|
|
7
|
+
version = "5.2.0"
|
|
8
8
|
description = "Standalone parser/loader for tilemap-editor JSON maps, sprite animations, and collision detection runtime."
|
|
9
9
|
readme = "pypi-readme.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -10,6 +10,7 @@ __all__ = [
|
|
|
10
10
|
"AnimationParseError",
|
|
11
11
|
"AnimationPlayer",
|
|
12
12
|
"AreaNode",
|
|
13
|
+
"BackgroundLayer",
|
|
13
14
|
"CapsuleShape",
|
|
14
15
|
"CharacterCollision",
|
|
15
16
|
"CircleShape",
|
|
@@ -26,6 +27,7 @@ __all__ = [
|
|
|
26
27
|
"LayerRenderStats",
|
|
27
28
|
"MapParseError",
|
|
28
29
|
"MovementMode",
|
|
30
|
+
"ObjectAnimation",
|
|
29
31
|
"ObjectCollisionData",
|
|
30
32
|
"ObjectCollisionManager",
|
|
31
33
|
"ObjectCollisionRegionData",
|
|
@@ -30,6 +30,7 @@ from .collision_loader import (
|
|
|
30
30
|
)
|
|
31
31
|
from .map_parse import (
|
|
32
32
|
MapParseError,
|
|
33
|
+
ObjectAnimation,
|
|
33
34
|
ParsedAutotileGroup,
|
|
34
35
|
ParsedAutotileRule,
|
|
35
36
|
ParsedLayer,
|
|
@@ -72,6 +73,7 @@ __all__ = [
|
|
|
72
73
|
"CollisionParseError",
|
|
73
74
|
"CollisionPolygon",
|
|
74
75
|
"MapParseError",
|
|
76
|
+
"ObjectAnimation",
|
|
75
77
|
"ObjectCollisionData",
|
|
76
78
|
"ObjectCollisionRegionData",
|
|
77
79
|
"ParsedAutotileGroup",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
+
import math
|
|
4
5
|
import re
|
|
5
6
|
import string
|
|
6
7
|
from dataclasses import dataclass, field
|
|
@@ -121,6 +122,20 @@ class ParsedObjectArea:
|
|
|
121
122
|
h: int
|
|
122
123
|
|
|
123
124
|
|
|
125
|
+
@dataclass
|
|
126
|
+
class ObjectAnimation:
|
|
127
|
+
frame_count: int
|
|
128
|
+
frame_duration_ms: float
|
|
129
|
+
speed: float = 1.0
|
|
130
|
+
loop: bool = True
|
|
131
|
+
animation_mode: str = "default"
|
|
132
|
+
random_phase: bool = False
|
|
133
|
+
frames: List[int] = field(default_factory=list)
|
|
134
|
+
frame_stride: int = 1
|
|
135
|
+
frame_w: Optional[int] = None
|
|
136
|
+
frame_h: Optional[int] = None
|
|
137
|
+
|
|
138
|
+
|
|
124
139
|
@dataclass
|
|
125
140
|
class ParsedObject:
|
|
126
141
|
area: ParsedObjectArea
|
|
@@ -128,15 +143,18 @@ class ParsedObject:
|
|
|
128
143
|
tileset_type: str
|
|
129
144
|
variant: int
|
|
130
145
|
properties: Optional[JsonDict] = None
|
|
146
|
+
animation: Optional[ObjectAnimation] = None
|
|
131
147
|
|
|
132
148
|
|
|
133
149
|
@dataclass
|
|
134
150
|
class TilesetAnimation:
|
|
135
151
|
frame_count: int
|
|
136
152
|
frame_duration_ms: float
|
|
137
|
-
frame_stride: int
|
|
153
|
+
frame_stride: int = 1
|
|
138
154
|
loop: bool = True
|
|
139
155
|
animation_mode: str = "default"
|
|
156
|
+
frame_w: Optional[int] = None
|
|
157
|
+
frame_h: Optional[int] = None
|
|
140
158
|
|
|
141
159
|
|
|
142
160
|
@dataclass
|
|
@@ -166,6 +184,8 @@ class ParsedLayer:
|
|
|
166
184
|
objects: Dict[int, ParsedObject] = field(default_factory=dict)
|
|
167
185
|
next_object_id: Optional[int] = None
|
|
168
186
|
ttypes: set[int] = field(default_factory=set)
|
|
187
|
+
image_path: Optional[str] = None
|
|
188
|
+
image_rect: Optional[Tuple[int, int, int, int]] = None
|
|
169
189
|
|
|
170
190
|
|
|
171
191
|
@dataclass
|
|
@@ -256,6 +276,47 @@ def _parse_object_area(area_obj: JsonDict, ctx: str) -> ParsedObjectArea:
|
|
|
256
276
|
)
|
|
257
277
|
|
|
258
278
|
|
|
279
|
+
def _parse_object_animation(anim_obj: JsonDict, ctx: str) -> ObjectAnimation:
|
|
280
|
+
frame_count = _coerce_int(anim_obj.get("frame_count"), f"{ctx}.frame_count")
|
|
281
|
+
if frame_count < 1:
|
|
282
|
+
raise MapParseError(_ctx(f"{ctx}.frame_count", "must be >= 1"))
|
|
283
|
+
frame_duration_ms = _coerce_float(
|
|
284
|
+
anim_obj.get("frame_duration_ms"), f"{ctx}.frame_duration_ms"
|
|
285
|
+
)
|
|
286
|
+
if not math.isfinite(frame_duration_ms) or frame_duration_ms <= 0:
|
|
287
|
+
raise MapParseError(_ctx(f"{ctx}.frame_duration_ms", "must be > 0 and finite"))
|
|
288
|
+
frames_raw = anim_obj.get("frames")
|
|
289
|
+
frames: List[int] = []
|
|
290
|
+
if frames_raw is not None:
|
|
291
|
+
frames_list = _require_list(frames_raw, f"{ctx}.frames")
|
|
292
|
+
if len(frames_list) != frame_count:
|
|
293
|
+
raise MapParseError(
|
|
294
|
+
_ctx(
|
|
295
|
+
f"{ctx}.frames",
|
|
296
|
+
f"must contain exactly {frame_count} entries (got {len(frames_list)})",
|
|
297
|
+
)
|
|
298
|
+
)
|
|
299
|
+
frames = [_coerce_int(f, f"{ctx}.frames[{i}]") for i, f in enumerate(frames_list)]
|
|
300
|
+
for i, frame_idx in enumerate(frames):
|
|
301
|
+
if frame_idx < 0:
|
|
302
|
+
raise MapParseError(
|
|
303
|
+
_ctx(f"{ctx}.frames[{i}]", f"must be non-negative (got {frame_idx})")
|
|
304
|
+
)
|
|
305
|
+
return ObjectAnimation(
|
|
306
|
+
frame_count=frame_count,
|
|
307
|
+
frame_duration_ms=frame_duration_ms,
|
|
308
|
+
speed=_coerce_float(anim_obj.get("speed", 1.0), f"{ctx}.speed"),
|
|
309
|
+
loop=_coerce_bool(anim_obj.get("loop", True), f"{ctx}.loop"),
|
|
310
|
+
animation_mode=_require_str(
|
|
311
|
+
anim_obj.get("animation_mode", "default"), f"{ctx}.animation_mode"
|
|
312
|
+
),
|
|
313
|
+
random_phase=_coerce_bool(
|
|
314
|
+
anim_obj.get("random_phase", False), f"{ctx}.random_phase"
|
|
315
|
+
),
|
|
316
|
+
frames=frames,
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
|
|
259
320
|
def _parse_objects(objs_obj: JsonDict, ctx: str) -> Dict[int, ParsedObject]:
|
|
260
321
|
result: Dict[int, ParsedObject] = {}
|
|
261
322
|
for key, value in objs_obj.items():
|
|
@@ -269,12 +330,22 @@ def _parse_objects(objs_obj: JsonDict, ctx: str) -> Dict[int, ParsedObject]:
|
|
|
269
330
|
)
|
|
270
331
|
variant = _coerce_int(obj_dict.get("variant"), f"{ctx}.{key}.variant")
|
|
271
332
|
props = _optional_dict(obj_dict.get("properties"), f"{ctx}.{key}.properties")
|
|
333
|
+
animation_raw = obj_dict.get("animation")
|
|
334
|
+
animation = (
|
|
335
|
+
_parse_object_animation(
|
|
336
|
+
_require_dict(animation_raw, f"{ctx}.{key}.animation"),
|
|
337
|
+
f"{ctx}.{key}.animation",
|
|
338
|
+
)
|
|
339
|
+
if animation_raw is not None
|
|
340
|
+
else None
|
|
341
|
+
)
|
|
272
342
|
result[oid] = ParsedObject(
|
|
273
343
|
area=area,
|
|
274
344
|
ttype=ttype,
|
|
275
345
|
tileset_type=tileset_type,
|
|
276
346
|
variant=variant,
|
|
277
347
|
properties=props,
|
|
348
|
+
animation=animation,
|
|
278
349
|
)
|
|
279
350
|
return result
|
|
280
351
|
|
|
@@ -308,6 +379,24 @@ def _parse_layer(layer_obj: JsonDict, layer_id: int, ctx: str) -> ParsedLayer:
|
|
|
308
379
|
layer.next_object_id = _coerce_int(
|
|
309
380
|
layer_obj["next_object_id"], f"{ctx}.next_object_id"
|
|
310
381
|
)
|
|
382
|
+
|
|
383
|
+
image_path_raw = layer_obj.get("image_path")
|
|
384
|
+
if image_path_raw is not None:
|
|
385
|
+
layer.image_path = _require_str(image_path_raw, f"{ctx}.image_path")
|
|
386
|
+
# Normalize background/background_layer types with images to "image" type
|
|
387
|
+
if layer.layer_type in ("background", "background_layer"):
|
|
388
|
+
layer.layer_type = "image"
|
|
389
|
+
|
|
390
|
+
image_rect_raw = layer_obj.get("image_rect")
|
|
391
|
+
if image_rect_raw is not None:
|
|
392
|
+
rect_obj = _require_dict(image_rect_raw, f"{ctx}.image_rect")
|
|
393
|
+
layer.image_rect = (
|
|
394
|
+
_coerce_int(rect_obj.get("x"), f"{ctx}.image_rect.x"),
|
|
395
|
+
_coerce_int(rect_obj.get("y"), f"{ctx}.image_rect.y"),
|
|
396
|
+
_coerce_int(rect_obj.get("w"), f"{ctx}.image_rect.w"),
|
|
397
|
+
_coerce_int(rect_obj.get("h"), f"{ctx}.image_rect.h"),
|
|
398
|
+
)
|
|
399
|
+
|
|
311
400
|
return layer
|
|
312
401
|
|
|
313
402
|
|
|
@@ -380,12 +469,28 @@ def _parse_tilesets_list(tilesets_raw: List[Any], ctx: str) -> List[ParsedTilese
|
|
|
380
469
|
animation_raw = ts_obj.get("animation")
|
|
381
470
|
if animation_raw is not None:
|
|
382
471
|
anim_obj = _require_dict(animation_raw, f"{ctx}[{i}].animation")
|
|
472
|
+
frame_stride_raw = anim_obj.get("frame_stride")
|
|
473
|
+
frame_stride = (
|
|
474
|
+
_coerce_int(frame_stride_raw, f"{ctx}[{i}].animation.frame_stride")
|
|
475
|
+
if frame_stride_raw is not None
|
|
476
|
+
else 1
|
|
477
|
+
)
|
|
478
|
+
frame_w_raw = anim_obj.get("frame_w")
|
|
479
|
+
frame_h_raw = anim_obj.get("frame_h")
|
|
480
|
+
frame_count = _coerce_int(anim_obj.get("frame_count"), f"{ctx}[{i}].animation.frame_count")
|
|
481
|
+
frame_duration_ms = _coerce_float(anim_obj.get("frame_duration_ms"), f"{ctx}[{i}].animation.frame_duration_ms")
|
|
482
|
+
if frame_count < 1:
|
|
483
|
+
raise MapParseError(_ctx(f"{ctx}[{i}].animation.frame_count", "must be >= 1"))
|
|
484
|
+
if not math.isfinite(frame_duration_ms) or frame_duration_ms <= 0:
|
|
485
|
+
raise MapParseError(_ctx(f"{ctx}[{i}].animation.frame_duration_ms", "must be > 0 and finite"))
|
|
383
486
|
animation = TilesetAnimation(
|
|
384
|
-
frame_count=
|
|
385
|
-
frame_duration_ms=
|
|
386
|
-
frame_stride=
|
|
487
|
+
frame_count=frame_count,
|
|
488
|
+
frame_duration_ms=frame_duration_ms,
|
|
489
|
+
frame_stride=frame_stride,
|
|
387
490
|
loop=_coerce_bool(anim_obj.get("loop", True), f"{ctx}[{i}].animation.loop"),
|
|
388
491
|
animation_mode=_require_str(anim_obj.get("animation_mode", "default"), f"{ctx}[{i}].animation.animation_mode"),
|
|
492
|
+
frame_w=_coerce_int(frame_w_raw, f"{ctx}[{i}].animation.frame_w") if frame_w_raw is not None else None,
|
|
493
|
+
frame_h=_coerce_int(frame_h_raw, f"{ctx}[{i}].animation.frame_h") if frame_h_raw is not None else None,
|
|
389
494
|
)
|
|
390
495
|
out.append(
|
|
391
496
|
ParsedTileset(
|
|
@@ -16,7 +16,7 @@ from .map_object import MapObject, load_map_objects
|
|
|
16
16
|
from .movement import CollisionResult, CollisionRunner, MovementMode
|
|
17
17
|
from .polygon_query import rect_vs_tilemap
|
|
18
18
|
from .protocols import ICollidable, ICollidableObject, ICollidableSprite
|
|
19
|
-
from .map_loader import TilemapData, load_map
|
|
19
|
+
from .map_loader import BackgroundLayer, TilemapData, load_map
|
|
20
20
|
from .collision import (
|
|
21
21
|
CollisionHit,
|
|
22
22
|
ObjectCollisionManager,
|
|
@@ -43,6 +43,7 @@ from .particles import (
|
|
|
43
43
|
__all__ = [
|
|
44
44
|
"AnimationPlayer",
|
|
45
45
|
"AreaNode",
|
|
46
|
+
"BackgroundLayer",
|
|
46
47
|
"Body",
|
|
47
48
|
"Camera",
|
|
48
49
|
"CollisionCache",
|
|
@@ -4,12 +4,20 @@ import json
|
|
|
4
4
|
import math
|
|
5
5
|
from copy import deepcopy
|
|
6
6
|
from pathlib import Path
|
|
7
|
-
from typing import Dict, List, Optional, Tuple, Union
|
|
7
|
+
from typing import Dict, List, Literal, Optional, Tuple, TypedDict, Union
|
|
8
8
|
|
|
9
9
|
import pygame
|
|
10
10
|
from pygame import Rect, Surface
|
|
11
11
|
|
|
12
|
-
from ..parser.map_parse import
|
|
12
|
+
from ..parser.map_parse import (
|
|
13
|
+
MapParseError,
|
|
14
|
+
ObjectAnimation,
|
|
15
|
+
ParsedLayer,
|
|
16
|
+
ParsedMap,
|
|
17
|
+
ParsedObject,
|
|
18
|
+
ParsedTile,
|
|
19
|
+
parse_map_file,
|
|
20
|
+
)
|
|
13
21
|
from ..parser.node_parse import parse_nodes_dict
|
|
14
22
|
from .area_node import AreaNode
|
|
15
23
|
from .particles import ParticleEmitterNode
|
|
@@ -17,6 +25,31 @@ from .particles import ParticleEmitterNode
|
|
|
17
25
|
PathLike = Union[str, Path]
|
|
18
26
|
|
|
19
27
|
|
|
28
|
+
class BackgroundLayer:
|
|
29
|
+
__slots__ = ("image_path", "image_rect", "surface")
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
image_path: str,
|
|
34
|
+
image_rect: tuple[int, int, int, int] | None,
|
|
35
|
+
surface: Surface | None,
|
|
36
|
+
) -> None:
|
|
37
|
+
self.image_path = image_path
|
|
38
|
+
self.image_rect = image_rect
|
|
39
|
+
self.surface = surface
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# Public return for get_object_animation (exposed, not internal ObjectAnimation)
|
|
43
|
+
class AnimData(TypedDict, total=True):
|
|
44
|
+
frames: List[Surface]
|
|
45
|
+
properties: Dict[str, object]
|
|
46
|
+
frame_duration_ms: float
|
|
47
|
+
loop: bool
|
|
48
|
+
animation_mode: Literal["default", "random_start_times"]
|
|
49
|
+
frame_w: int
|
|
50
|
+
frame_h: int
|
|
51
|
+
|
|
52
|
+
|
|
20
53
|
class TilemapData:
|
|
21
54
|
def __init__(
|
|
22
55
|
self,
|
|
@@ -36,6 +69,7 @@ class TilemapData:
|
|
|
36
69
|
self.origin_offset = (0, 0)
|
|
37
70
|
self.area_nodes: List[AreaNode] = []
|
|
38
71
|
self.particle_emitters: List[ParticleEmitterNode] = []
|
|
72
|
+
self.background_layer: Optional[BackgroundLayer] = None
|
|
39
73
|
self._tw, self._th = parsed.meta.tile_size
|
|
40
74
|
self._build_path_index()
|
|
41
75
|
self._normalize_tile_ttypes()
|
|
@@ -117,6 +151,35 @@ class TilemapData:
|
|
|
117
151
|
result.particle_emitters = [
|
|
118
152
|
ParticleEmitterNode(n) for n in parsed.nodes if n.node_type == "particle_emitter"
|
|
119
153
|
]
|
|
154
|
+
|
|
155
|
+
for layer in parsed.layers:
|
|
156
|
+
if layer.image_path is not None and layer.layer_type == "image":
|
|
157
|
+
bg_path = _resolve_resource_path(layer.image_path, map_dir, extra_search_base)
|
|
158
|
+
bg_surface: Optional[Surface] = None
|
|
159
|
+
if bg_path.is_file():
|
|
160
|
+
try:
|
|
161
|
+
bg_surface = pygame.image.load(str(bg_path))
|
|
162
|
+
try:
|
|
163
|
+
bg_surface = bg_surface.convert_alpha()
|
|
164
|
+
except pygame.error:
|
|
165
|
+
pass
|
|
166
|
+
except pygame.error as e:
|
|
167
|
+
msg = f"Background layer image load failed: {e}"
|
|
168
|
+
warnings.append(msg)
|
|
169
|
+
if not skip_missing_images:
|
|
170
|
+
raise MapParseError(msg) from e
|
|
171
|
+
else:
|
|
172
|
+
msg = f"Background layer image not found: {layer.image_path!r} -> {bg_path}"
|
|
173
|
+
warnings.append(msg)
|
|
174
|
+
if not skip_missing_images:
|
|
175
|
+
raise MapParseError(msg)
|
|
176
|
+
result.background_layer = BackgroundLayer(
|
|
177
|
+
image_path=layer.image_path,
|
|
178
|
+
image_rect=layer.image_rect,
|
|
179
|
+
surface=bg_surface,
|
|
180
|
+
)
|
|
181
|
+
break
|
|
182
|
+
|
|
120
183
|
return result
|
|
121
184
|
|
|
122
185
|
def _build_path_index(self) -> None:
|
|
@@ -327,9 +390,98 @@ class TilemapData:
|
|
|
327
390
|
"frame_stride": anim.frame_stride,
|
|
328
391
|
"loop": anim.loop,
|
|
329
392
|
"animation_mode": anim.animation_mode,
|
|
393
|
+
"frame_w": anim.frame_w,
|
|
394
|
+
"frame_h": anim.frame_h,
|
|
330
395
|
}
|
|
331
396
|
return None
|
|
332
397
|
|
|
398
|
+
def get_object_animation(self, obj: ParsedObject, render_scale: float = 1.0) -> Optional[AnimData]:
|
|
399
|
+
"""Return effective object animation as ``AnimData`` or ``None``.
|
|
400
|
+
|
|
401
|
+
When per-object ``animation`` is ``None``, falls back to the object's
|
|
402
|
+
tileset ``ParsedTileset.animation`` (shared strip). ``render_scale``
|
|
403
|
+
scales both ``frames`` and ``frame_w/h`` in the returned dict
|
|
404
|
+
(default ``1.0`` leaves at source resolution). No ``Surface`` overload -
|
|
405
|
+
single dict with ``frames`` + ``properties`` + ``loop``/``duration``/``mode``/``w/h``.
|
|
406
|
+
"""
|
|
407
|
+
if not isinstance(render_scale, (int, float)) or not math.isfinite(render_scale) or render_scale <= 0:
|
|
408
|
+
raise ValueError(f"render_scale must be finite and > 0, got {render_scale!r}")
|
|
409
|
+
# effective animation (per-object or tileset fallback) - internal ObjectAnimationData
|
|
410
|
+
anim_data: Optional[ObjectAnimation] = obj.animation # internal, keep frame_count
|
|
411
|
+
if anim_data is None:
|
|
412
|
+
t_anim = self.get_tileset_animation(obj.ttype)
|
|
413
|
+
if t_anim is None:
|
|
414
|
+
return None
|
|
415
|
+
anim_data = ObjectAnimation(
|
|
416
|
+
frame_count=t_anim["frame_count"],
|
|
417
|
+
frame_duration_ms=t_anim["frame_duration_ms"],
|
|
418
|
+
loop=t_anim.get("loop", True),
|
|
419
|
+
animation_mode=t_anim.get("animation_mode", "default"),
|
|
420
|
+
frames=[],
|
|
421
|
+
frame_stride=t_anim.get("frame_stride", 1),
|
|
422
|
+
frame_w=t_anim.get("frame_w"),
|
|
423
|
+
frame_h=t_anim.get("frame_h"),
|
|
424
|
+
)
|
|
425
|
+
if obj.ttype < 0 or obj.ttype >= len(self.surfaces):
|
|
426
|
+
return None
|
|
427
|
+
source = self.surfaces[obj.ttype]
|
|
428
|
+
if source is None:
|
|
429
|
+
return None
|
|
430
|
+
# frame dimensions - prefer animation's w/h, else area, else tile_size, scaled by render_scale
|
|
431
|
+
fw = getattr(anim_data, "frame_w", None)
|
|
432
|
+
if fw is None:
|
|
433
|
+
fw = obj.area.w if obj.area.w > 0 else self._tw
|
|
434
|
+
fh = getattr(anim_data, "frame_h", None)
|
|
435
|
+
if fh is None:
|
|
436
|
+
fh = obj.area.h if obj.area.h > 0 else self._th
|
|
437
|
+
if fw <= 0 or fh <= 0:
|
|
438
|
+
return None
|
|
439
|
+
# apply render_scale to dimensions
|
|
440
|
+
if render_scale != 1.0:
|
|
441
|
+
fw = int(fw * render_scale)
|
|
442
|
+
fh = int(fh * render_scale)
|
|
443
|
+
# cols based on original fw before scale for correct slicing
|
|
444
|
+
orig_fw = getattr(anim_data, "frame_w", None)
|
|
445
|
+
if orig_fw is None:
|
|
446
|
+
orig_fw = obj.area.w if obj.area.w > 0 else self._tw
|
|
447
|
+
if orig_fw is None or orig_fw <= 0:
|
|
448
|
+
orig_fw = fw
|
|
449
|
+
cols = max(1, source.get_width() // orig_fw) if render_scale != 1.0 else max(1, source.get_width() // fw)
|
|
450
|
+
if anim_data.frames:
|
|
451
|
+
frame_indices = anim_data.frames
|
|
452
|
+
else:
|
|
453
|
+
stride = getattr(anim_data, "frame_stride", 1)
|
|
454
|
+
if stride is None or stride <= 0:
|
|
455
|
+
stride = 1
|
|
456
|
+
if stride == 1:
|
|
457
|
+
frame_indices = list(range(anim_data.frame_count))
|
|
458
|
+
else:
|
|
459
|
+
frame_indices = [i * stride for i in range(anim_data.frame_count)]
|
|
460
|
+
frames: List[Surface] = []
|
|
461
|
+
for fi in frame_indices:
|
|
462
|
+
col = fi % cols
|
|
463
|
+
row = fi // cols
|
|
464
|
+
src_fw = getattr(anim_data, "frame_w", None) or (obj.area.w if obj.area.w > 0 else self._tw)
|
|
465
|
+
src_fh = getattr(anim_data, "frame_h", None) or (obj.area.h if obj.area.h > 0 else self._th)
|
|
466
|
+
src = Rect(col * src_fw, row * src_fh, src_fw, src_fh)
|
|
467
|
+
if not source.get_rect().contains(src):
|
|
468
|
+
surf = Surface((src_fw, src_fh), pygame.SRCALPHA)
|
|
469
|
+
else:
|
|
470
|
+
cell = source.subsurface(src)
|
|
471
|
+
surf = cell.copy()
|
|
472
|
+
if render_scale != 1.0:
|
|
473
|
+
surf = pygame.transform.scale(surf, (fw, fh))
|
|
474
|
+
frames.append(surf)
|
|
475
|
+
return {
|
|
476
|
+
"frames": frames,
|
|
477
|
+
"properties": dict(obj.properties) if obj.properties else {},
|
|
478
|
+
"frame_duration_ms": float(anim_data.frame_duration_ms),
|
|
479
|
+
"loop": bool(anim_data.loop),
|
|
480
|
+
"animation_mode": str(anim_data.animation_mode),
|
|
481
|
+
"frame_w": fw,
|
|
482
|
+
"frame_h": fh,
|
|
483
|
+
}
|
|
484
|
+
|
|
333
485
|
|
|
334
486
|
def _variant_surface(
|
|
335
487
|
surf: Surface,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tilemap-parser
|
|
3
|
-
Version: 5.
|
|
3
|
+
Version: 5.2.0
|
|
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
|
|
@@ -49,6 +49,7 @@ src/tilemap_parser/runtime/navigation/path_follower.py
|
|
|
49
49
|
src/tilemap_parser/runtime/navigation/pathfinder.py
|
|
50
50
|
src/tilemap_parser/utils/__init__.py
|
|
51
51
|
src/tilemap_parser/utils/geometry.py
|
|
52
|
+
tests/test_background_layer.py
|
|
52
53
|
tests/test_body.py
|
|
53
54
|
tests/test_camera.py
|
|
54
55
|
tests/test_collision.py
|
|
@@ -62,6 +63,7 @@ tests/test_map_loader.py
|
|
|
62
63
|
tests/test_map_object.py
|
|
63
64
|
tests/test_move_grounded.py
|
|
64
65
|
tests/test_navigation.py
|
|
66
|
+
tests/test_object_animation.py
|
|
65
67
|
tests/test_object_collision.py
|
|
66
68
|
tests/test_object_surfaces.py
|
|
67
69
|
tests/test_particle_emitter.py
|