tilemap-parser 3.1.23__tar.gz → 5.0.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-3.1.23 → tilemap_parser-5.0.0}/PKG-INFO +31 -3
- {tilemap_parser-3.1.23 → tilemap_parser-5.0.0}/README.md +30 -2
- {tilemap_parser-3.1.23 → tilemap_parser-5.0.0}/pyproject.toml +1 -1
- {tilemap_parser-3.1.23 → tilemap_parser-5.0.0}/src/tilemap_parser/parser/__init__.py +4 -0
- {tilemap_parser-3.1.23 → tilemap_parser-5.0.0}/src/tilemap_parser/parser/map_parse.py +21 -3
- tilemap_parser-5.0.0/src/tilemap_parser/parser/tmx_converter.py +447 -0
- {tilemap_parser-3.1.23 → tilemap_parser-5.0.0}/src/tilemap_parser.egg-info/PKG-INFO +31 -3
- {tilemap_parser-3.1.23 → tilemap_parser-5.0.0}/src/tilemap_parser.egg-info/SOURCES.txt +3 -1
- tilemap_parser-5.0.0/tests/test_tmx_converter.py +442 -0
- {tilemap_parser-3.1.23 → tilemap_parser-5.0.0}/LICENSE +0 -0
- {tilemap_parser-3.1.23 → tilemap_parser-5.0.0}/setup.cfg +0 -0
- {tilemap_parser-3.1.23 → tilemap_parser-5.0.0}/src/tilemap_parser/__init__.py +0 -0
- {tilemap_parser-3.1.23 → tilemap_parser-5.0.0}/src/tilemap_parser/parser/animation.py +0 -0
- {tilemap_parser-3.1.23 → tilemap_parser-5.0.0}/src/tilemap_parser/parser/collision.py +0 -0
- {tilemap_parser-3.1.23 → tilemap_parser-5.0.0}/src/tilemap_parser/parser/collision_loader.py +0 -0
- {tilemap_parser-3.1.23 → tilemap_parser-5.0.0}/src/tilemap_parser/parser/node_parse.py +0 -0
- {tilemap_parser-3.1.23 → tilemap_parser-5.0.0}/src/tilemap_parser/parser/particle.py +0 -0
- {tilemap_parser-3.1.23 → tilemap_parser-5.0.0}/src/tilemap_parser/runtime/__init__.py +0 -0
- {tilemap_parser-3.1.23 → tilemap_parser-5.0.0}/src/tilemap_parser/runtime/animation_player.py +0 -0
- {tilemap_parser-3.1.23 → tilemap_parser-5.0.0}/src/tilemap_parser/runtime/area_node.py +0 -0
- {tilemap_parser-3.1.23 → tilemap_parser-5.0.0}/src/tilemap_parser/runtime/camera.py +0 -0
- {tilemap_parser-3.1.23 → tilemap_parser-5.0.0}/src/tilemap_parser/runtime/collision_cache.py +0 -0
- {tilemap_parser-3.1.23 → tilemap_parser-5.0.0}/src/tilemap_parser/runtime/map_loader.py +0 -0
- {tilemap_parser-3.1.23 → tilemap_parser-5.0.0}/src/tilemap_parser/runtime/object_collision.py +0 -0
- {tilemap_parser-3.1.23 → tilemap_parser-5.0.0}/src/tilemap_parser/runtime/particles.py +0 -0
- {tilemap_parser-3.1.23 → tilemap_parser-5.0.0}/src/tilemap_parser/runtime/renderer.py +0 -0
- {tilemap_parser-3.1.23 → tilemap_parser-5.0.0}/src/tilemap_parser/runtime/tile_collision.py +0 -0
- {tilemap_parser-3.1.23 → tilemap_parser-5.0.0}/src/tilemap_parser/utils/__init__.py +0 -0
- {tilemap_parser-3.1.23 → tilemap_parser-5.0.0}/src/tilemap_parser/utils/geometry.py +0 -0
- {tilemap_parser-3.1.23 → tilemap_parser-5.0.0}/src/tilemap_parser.egg-info/dependency_links.txt +0 -0
- {tilemap_parser-3.1.23 → tilemap_parser-5.0.0}/src/tilemap_parser.egg-info/requires.txt +0 -0
- {tilemap_parser-3.1.23 → tilemap_parser-5.0.0}/src/tilemap_parser.egg-info/top_level.txt +0 -0
- {tilemap_parser-3.1.23 → tilemap_parser-5.0.0}/tests/test_camera.py +0 -0
- {tilemap_parser-3.1.23 → tilemap_parser-5.0.0}/tests/test_collision.py +0 -0
- {tilemap_parser-3.1.23 → tilemap_parser-5.0.0}/tests/test_geometry.py +0 -0
- {tilemap_parser-3.1.23 → tilemap_parser-5.0.0}/tests/test_integration_animation.py +0 -0
- {tilemap_parser-3.1.23 → tilemap_parser-5.0.0}/tests/test_integration_collision.py +0 -0
- {tilemap_parser-3.1.23 → tilemap_parser-5.0.0}/tests/test_integration_map_loader.py +0 -0
- {tilemap_parser-3.1.23 → tilemap_parser-5.0.0}/tests/test_map_loader.py +0 -0
- {tilemap_parser-3.1.23 → tilemap_parser-5.0.0}/tests/test_move_grounded.py +0 -0
- {tilemap_parser-3.1.23 → tilemap_parser-5.0.0}/tests/test_object_collision.py +0 -0
- {tilemap_parser-3.1.23 → tilemap_parser-5.0.0}/tests/test_object_surfaces.py +0 -0
- {tilemap_parser-3.1.23 → tilemap_parser-5.0.0}/tests/test_particle_emitter.py +0 -0
- {tilemap_parser-3.1.23 → tilemap_parser-5.0.0}/tests/test_render_scale.py +0 -0
- {tilemap_parser-3.1.23 → tilemap_parser-5.0.0}/tests/test_tile_collision.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tilemap-parser
|
|
3
|
-
Version:
|
|
3
|
+
Version: 5.0.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
|
|
@@ -704,7 +704,10 @@ Standalone parser/loader for tilemap-editor JSON maps, sprite animations, and co
|
|
|
704
704
|
## Features
|
|
705
705
|
|
|
706
706
|
- **Map parsing**: Load and query tilemaps, layers, objects, and autotile data from JSON
|
|
707
|
+
- **Chunked tile rendering**: Efficient visibility culling and chunk-based rendering with `TileLayerRenderer`
|
|
708
|
+
- **Camera system**: Centered/deadzone follow, lerp smoothing, screen-shake, and bounds clamping with `Camera`
|
|
707
709
|
- **Animation**: Frame-based sprite animation with `AnimationPlayer`
|
|
710
|
+
- **Particle system**: Configurable particle emitters with shapes, color transitions, alpha fades, gravity, and batch rendering with `ParticleSystem` + `SpriteBatchRenderer`
|
|
708
711
|
- **Collision (tile-based)**: Polygon collision detection for tilemaps with slide, platformer, and RPG movement modes via `CollisionRunner`
|
|
709
712
|
- **Collision (object-to-object)**: Spatial-grid mixed-shape collision detection (rect, circle, capsule, polygon) with layer filtering via `ObjectCollisionManager`
|
|
710
713
|
- **Capsule support**: Full capsule collision against all shape types
|
|
@@ -713,16 +716,22 @@ Standalone parser/loader for tilemap-editor JSON maps, sprite animations, and co
|
|
|
713
716
|
## Quick Start
|
|
714
717
|
|
|
715
718
|
```python
|
|
716
|
-
from tilemap_parser import load_map, TileLayerRenderer
|
|
719
|
+
from tilemap_parser import load_map, TileLayerRenderer, Camera
|
|
717
720
|
|
|
721
|
+
# Load map and set up renderer
|
|
718
722
|
game_data = load_map("path/to/map.json")
|
|
719
723
|
renderer = TileLayerRenderer(game_data)
|
|
724
|
+
|
|
725
|
+
# Set up camera
|
|
726
|
+
camera = Camera(800, 600, mode="centered")
|
|
727
|
+
camera.follow(player)
|
|
720
728
|
```
|
|
721
729
|
|
|
722
730
|
```python
|
|
723
731
|
from tilemap_parser import (
|
|
724
732
|
CollisionRunner, CollisionCache,
|
|
725
733
|
ObjectCollisionManager, CircleShape, RectangleShape,
|
|
734
|
+
ParticleSystem, ParticleSystemConfig
|
|
726
735
|
)
|
|
727
736
|
|
|
728
737
|
# Tile-based collision
|
|
@@ -735,10 +744,29 @@ manager = ObjectCollisionManager()
|
|
|
735
744
|
manager.add_object(player)
|
|
736
745
|
for hit in manager.check_all_collisions():
|
|
737
746
|
hit.resolve() # separate both objects
|
|
747
|
+
|
|
748
|
+
# Particle system
|
|
749
|
+
particle_config = ParticleSystemConfig(
|
|
750
|
+
particle_shape="circle",
|
|
751
|
+
spawn_rate=10.0,
|
|
752
|
+
max_particles=100,
|
|
753
|
+
lifetime_min=0.5,
|
|
754
|
+
lifetime_max=2.0,
|
|
755
|
+
speed_min=50,
|
|
756
|
+
speed_max=100,
|
|
757
|
+
direction=270,
|
|
758
|
+
spread=45,
|
|
759
|
+
start_color_r=255, start_color_g=200, start_color_b=100, start_color_a=255,
|
|
760
|
+
end_color_r=255, end_color_g=100, end_color_b=50, end_color_a=0,
|
|
761
|
+
alpha_fade="fade_out",
|
|
762
|
+
gravity_x=0,
|
|
763
|
+
gravity_y=50,
|
|
764
|
+
)
|
|
765
|
+
particles = ParticleSystem(particle_config)
|
|
738
766
|
```
|
|
739
767
|
|
|
740
768
|
## Links
|
|
741
769
|
|
|
742
|
-
- **Docs**: https://tilemap-parser.
|
|
770
|
+
- **Docs**: [https://deepwiki.com/FluffyBrudy/tilemap-parser](https://deepwiki.com/FluffyBrudy/tilemap-parser)
|
|
743
771
|
- **Editor**: https://pypi.org/project/tilemap-editor/
|
|
744
772
|
- **Repository**: https://github.com/FluffyBrudy/tilemap-parser
|
|
@@ -5,7 +5,10 @@ Standalone parser/loader for tilemap-editor JSON maps, sprite animations, and co
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
7
|
- **Map parsing**: Load and query tilemaps, layers, objects, and autotile data from JSON
|
|
8
|
+
- **Chunked tile rendering**: Efficient visibility culling and chunk-based rendering with `TileLayerRenderer`
|
|
9
|
+
- **Camera system**: Centered/deadzone follow, lerp smoothing, screen-shake, and bounds clamping with `Camera`
|
|
8
10
|
- **Animation**: Frame-based sprite animation with `AnimationPlayer`
|
|
11
|
+
- **Particle system**: Configurable particle emitters with shapes, color transitions, alpha fades, gravity, and batch rendering with `ParticleSystem` + `SpriteBatchRenderer`
|
|
9
12
|
- **Collision (tile-based)**: Polygon collision detection for tilemaps with slide, platformer, and RPG movement modes via `CollisionRunner`
|
|
10
13
|
- **Collision (object-to-object)**: Spatial-grid mixed-shape collision detection (rect, circle, capsule, polygon) with layer filtering via `ObjectCollisionManager`
|
|
11
14
|
- **Capsule support**: Full capsule collision against all shape types
|
|
@@ -14,16 +17,22 @@ Standalone parser/loader for tilemap-editor JSON maps, sprite animations, and co
|
|
|
14
17
|
## Quick Start
|
|
15
18
|
|
|
16
19
|
```python
|
|
17
|
-
from tilemap_parser import load_map, TileLayerRenderer
|
|
20
|
+
from tilemap_parser import load_map, TileLayerRenderer, Camera
|
|
18
21
|
|
|
22
|
+
# Load map and set up renderer
|
|
19
23
|
game_data = load_map("path/to/map.json")
|
|
20
24
|
renderer = TileLayerRenderer(game_data)
|
|
25
|
+
|
|
26
|
+
# Set up camera
|
|
27
|
+
camera = Camera(800, 600, mode="centered")
|
|
28
|
+
camera.follow(player)
|
|
21
29
|
```
|
|
22
30
|
|
|
23
31
|
```python
|
|
24
32
|
from tilemap_parser import (
|
|
25
33
|
CollisionRunner, CollisionCache,
|
|
26
34
|
ObjectCollisionManager, CircleShape, RectangleShape,
|
|
35
|
+
ParticleSystem, ParticleSystemConfig
|
|
27
36
|
)
|
|
28
37
|
|
|
29
38
|
# Tile-based collision
|
|
@@ -36,10 +45,29 @@ manager = ObjectCollisionManager()
|
|
|
36
45
|
manager.add_object(player)
|
|
37
46
|
for hit in manager.check_all_collisions():
|
|
38
47
|
hit.resolve() # separate both objects
|
|
48
|
+
|
|
49
|
+
# Particle system
|
|
50
|
+
particle_config = ParticleSystemConfig(
|
|
51
|
+
particle_shape="circle",
|
|
52
|
+
spawn_rate=10.0,
|
|
53
|
+
max_particles=100,
|
|
54
|
+
lifetime_min=0.5,
|
|
55
|
+
lifetime_max=2.0,
|
|
56
|
+
speed_min=50,
|
|
57
|
+
speed_max=100,
|
|
58
|
+
direction=270,
|
|
59
|
+
spread=45,
|
|
60
|
+
start_color_r=255, start_color_g=200, start_color_b=100, start_color_a=255,
|
|
61
|
+
end_color_r=255, end_color_g=100, end_color_b=50, end_color_a=0,
|
|
62
|
+
alpha_fade="fade_out",
|
|
63
|
+
gravity_x=0,
|
|
64
|
+
gravity_y=50,
|
|
65
|
+
)
|
|
66
|
+
particles = ParticleSystem(particle_config)
|
|
39
67
|
```
|
|
40
68
|
|
|
41
69
|
## Links
|
|
42
70
|
|
|
43
|
-
- **Docs**: https://tilemap-parser.
|
|
71
|
+
- **Docs**: [https://deepwiki.com/FluffyBrudy/tilemap-parser](https://deepwiki.com/FluffyBrudy/tilemap-parser)
|
|
44
72
|
- **Editor**: https://pypi.org/project/tilemap-editor/
|
|
45
73
|
- **Repository**: https://github.com/FluffyBrudy/tilemap-parser
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "tilemap-parser"
|
|
7
|
-
version = "
|
|
7
|
+
version = "5.0.0"
|
|
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"
|
|
@@ -46,6 +46,7 @@ from .map_parse import (
|
|
|
46
46
|
parse_map_json,
|
|
47
47
|
)
|
|
48
48
|
from .node_parse import ParsedNode, parse_nodes_dict, parse_nodes_file
|
|
49
|
+
from .tmx_converter import TmxParseError, parse_tmx_file, parse_tsx_file
|
|
49
50
|
from .particle import (
|
|
50
51
|
ALPHA_FADE_MODES,
|
|
51
52
|
EMISSION_SHAPES,
|
|
@@ -84,6 +85,7 @@ __all__ = [
|
|
|
84
85
|
"TilesetAnimation",
|
|
85
86
|
"TileCollisionData",
|
|
86
87
|
"TilesetCollision",
|
|
88
|
+
"TmxParseError",
|
|
87
89
|
"parse_animation_dict",
|
|
88
90
|
"parse_animation_file",
|
|
89
91
|
"parse_animation_json",
|
|
@@ -96,6 +98,8 @@ __all__ = [
|
|
|
96
98
|
"parse_map_json",
|
|
97
99
|
"parse_nodes_dict",
|
|
98
100
|
"parse_nodes_file",
|
|
101
|
+
"parse_tmx_file",
|
|
102
|
+
"parse_tsx_file",
|
|
99
103
|
"parse_object_collision",
|
|
100
104
|
"parse_tileset_collision",
|
|
101
105
|
"ALPHA_FADE_MODES",
|
|
@@ -106,6 +106,10 @@ class ParsedTile:
|
|
|
106
106
|
ttype: TilesetRef
|
|
107
107
|
variant: int
|
|
108
108
|
properties: Optional[JsonDict] = None
|
|
109
|
+
flip_h: bool = False
|
|
110
|
+
flip_v: bool = False
|
|
111
|
+
flip_d: bool = False
|
|
112
|
+
rotated_hex120: bool = False
|
|
109
113
|
|
|
110
114
|
|
|
111
115
|
@dataclass
|
|
@@ -213,7 +217,16 @@ def _parse_tile(tile_data: JsonDict, ctx: str) -> ParsedTile:
|
|
|
213
217
|
else:
|
|
214
218
|
ttype = _coerce_int(ttype_raw, f"{ctx}.ttype")
|
|
215
219
|
props = _optional_dict(tile_data.get("properties"), f"{ctx}.properties")
|
|
216
|
-
return ParsedTile(
|
|
220
|
+
return ParsedTile(
|
|
221
|
+
pos=pos,
|
|
222
|
+
ttype=ttype,
|
|
223
|
+
variant=variant,
|
|
224
|
+
properties=props,
|
|
225
|
+
flip_h=tile_data.get("flip_h", False),
|
|
226
|
+
flip_v=tile_data.get("flip_v", False),
|
|
227
|
+
flip_d=tile_data.get("flip_d", False),
|
|
228
|
+
rotated_hex120=tile_data.get("rotated_hex120", False),
|
|
229
|
+
)
|
|
217
230
|
|
|
218
231
|
|
|
219
232
|
def _parse_tiles(tiles_obj: JsonDict, ctx: str) -> Dict[Point, ParsedTile]:
|
|
@@ -484,8 +497,13 @@ def parse_map_file(path: Union[str, Path]) -> ParsedMap:
|
|
|
484
497
|
p = Path(path)
|
|
485
498
|
if not p.is_file():
|
|
486
499
|
raise MapParseError(f"Not a file: {p}")
|
|
487
|
-
|
|
488
|
-
|
|
500
|
+
suffix = p.suffix.lower()
|
|
501
|
+
if suffix == ".tmx":
|
|
502
|
+
from .tmx_converter import parse_tmx_file
|
|
503
|
+
|
|
504
|
+
return parse_tmx_file(p)
|
|
505
|
+
if suffix != ".json":
|
|
506
|
+
raise MapParseError(f"Expected .json or .tmx map file, got {suffix!r}")
|
|
489
507
|
try:
|
|
490
508
|
text = p.read_text(encoding="utf-8")
|
|
491
509
|
except OSError as e:
|
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import gzip
|
|
5
|
+
import logging
|
|
6
|
+
import struct
|
|
7
|
+
import zlib
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
10
|
+
from xml.etree import ElementTree
|
|
11
|
+
|
|
12
|
+
from .map_parse import (
|
|
13
|
+
MapParseError,
|
|
14
|
+
ParsedLayer,
|
|
15
|
+
ParsedMap,
|
|
16
|
+
ParsedMeta,
|
|
17
|
+
ParsedProjectState,
|
|
18
|
+
ParsedTile,
|
|
19
|
+
ParsedTileset,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
log = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
TILE_FLIP_H = 0x80000000
|
|
25
|
+
TILE_FLIP_V = 0x40000000
|
|
26
|
+
TILE_FLIP_D = 0x20000000
|
|
27
|
+
TILE_FLIP_HEX = 0x10000000
|
|
28
|
+
TILE_FLIP_MASK = 0x0FFFFFFF
|
|
29
|
+
|
|
30
|
+
FLIP_BIT_NAMES = {
|
|
31
|
+
TILE_FLIP_H: "H",
|
|
32
|
+
TILE_FLIP_V: "V",
|
|
33
|
+
TILE_FLIP_D: "D",
|
|
34
|
+
TILE_FLIP_HEX: "HEX120",
|
|
35
|
+
}
|
|
36
|
+
FLIP_ALL = TILE_FLIP_H | TILE_FLIP_V | TILE_FLIP_D | TILE_FLIP_HEX
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class TmxParseError(MapParseError):
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _coerce_int(value: Any, ctx: str) -> int:
|
|
44
|
+
try:
|
|
45
|
+
return int(value)
|
|
46
|
+
except (ValueError, TypeError) as e:
|
|
47
|
+
raise TmxParseError(f"{ctx}: expected int, got {value!r}") from e
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _coerce_float(value: Any, ctx: str) -> float:
|
|
51
|
+
try:
|
|
52
|
+
return float(value)
|
|
53
|
+
except (ValueError, TypeError) as e:
|
|
54
|
+
raise TmxParseError(f"{ctx}: expected float, got {value!r}") from e
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _require_attr(elem: ElementTree.Element, name: str, ctx: str) -> str:
|
|
58
|
+
value = elem.get(name)
|
|
59
|
+
if value is None:
|
|
60
|
+
raise TmxParseError(f"{ctx}: missing required attribute {name!r}")
|
|
61
|
+
return value
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _decode_flip_flags(
|
|
65
|
+
gid: int,
|
|
66
|
+
) -> Tuple[int, bool, bool, bool, bool]:
|
|
67
|
+
flipped = gid & FLIP_ALL
|
|
68
|
+
stripped = gid & TILE_FLIP_MASK
|
|
69
|
+
if flipped:
|
|
70
|
+
bits = "+".join(name for mask, name in FLIP_BIT_NAMES.items() if flipped & mask)
|
|
71
|
+
log.debug("Tile GID 0x%x has flip flag(s) %s — decoded", gid, bits)
|
|
72
|
+
return (
|
|
73
|
+
stripped,
|
|
74
|
+
bool(gid & TILE_FLIP_H),
|
|
75
|
+
bool(gid & TILE_FLIP_V),
|
|
76
|
+
bool(gid & TILE_FLIP_D),
|
|
77
|
+
bool(gid & TILE_FLIP_HEX),
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _parse_tsx_root(root: ElementTree.Element, source_path: Path) -> dict:
|
|
82
|
+
if root.tag == "tileset":
|
|
83
|
+
tileset = root
|
|
84
|
+
else:
|
|
85
|
+
tileset = root.find("tileset")
|
|
86
|
+
if tileset is None:
|
|
87
|
+
raise TmxParseError(f"TSX {source_path}: missing <tileset> root element")
|
|
88
|
+
return _build_tsx_data(tileset, source_path)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _build_tsx_data(ts_elem: ElementTree.Element, source_path: Path) -> dict:
|
|
92
|
+
name = ts_elem.get("name", "")
|
|
93
|
+
tilewidth = _coerce_int(_require_attr(ts_elem, "tilewidth", name), f"{name}.tilewidth")
|
|
94
|
+
tileheight = _coerce_int(_require_attr(ts_elem, "tileheight", name), f"{name}.tileheight")
|
|
95
|
+
tilecount = _coerce_int(ts_elem.get("tilecount", 0), f"{name}.tilecount")
|
|
96
|
+
columns = _coerce_int(ts_elem.get("columns", 0), f"{name}.columns")
|
|
97
|
+
|
|
98
|
+
image_elem = ts_elem.find("image")
|
|
99
|
+
image_source = ""
|
|
100
|
+
image_width = 0
|
|
101
|
+
image_height = 0
|
|
102
|
+
if image_elem is not None:
|
|
103
|
+
image_source = image_elem.get("source", "")
|
|
104
|
+
image_width = _coerce_int(image_elem.get("width", 0), f"{name}.image.width")
|
|
105
|
+
image_height = _coerce_int(image_elem.get("height", 0), f"{name}.image.height")
|
|
106
|
+
|
|
107
|
+
data: dict = {
|
|
108
|
+
"name": name,
|
|
109
|
+
"tilewidth": tilewidth,
|
|
110
|
+
"tileheight": tileheight,
|
|
111
|
+
"tilecount": tilecount,
|
|
112
|
+
"columns": columns,
|
|
113
|
+
"image": image_source,
|
|
114
|
+
"imagewidth": image_width,
|
|
115
|
+
"imageheight": image_height,
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
props = _parse_properties(ts_elem)
|
|
119
|
+
if props:
|
|
120
|
+
data["properties"] = props
|
|
121
|
+
|
|
122
|
+
tile_properties: Dict[str, Dict[str, Any]] = {}
|
|
123
|
+
for tile_elem in ts_elem.findall("tile"):
|
|
124
|
+
tid = _coerce_int(_require_attr(tile_elem, "id", f"{name}.tile"), f"{name}.tile.id")
|
|
125
|
+
tile_props = _parse_properties(tile_elem)
|
|
126
|
+
if tile_props:
|
|
127
|
+
tile_properties[str(tid)] = tile_props
|
|
128
|
+
if tile_properties:
|
|
129
|
+
data["tile_properties"] = tile_properties
|
|
130
|
+
|
|
131
|
+
return data
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _coerce_property_value(value: str, prop_type: str) -> Any:
|
|
135
|
+
if prop_type == "int":
|
|
136
|
+
return int(value)
|
|
137
|
+
elif prop_type == "float":
|
|
138
|
+
return float(value)
|
|
139
|
+
elif prop_type == "bool":
|
|
140
|
+
return value.lower() in ("true", "1")
|
|
141
|
+
elif prop_type == "color":
|
|
142
|
+
return value
|
|
143
|
+
return value
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _parse_properties(elem: ElementTree.Element) -> Dict[str, Any]:
|
|
147
|
+
props: Dict[str, Any] = {}
|
|
148
|
+
props_elem = elem.find("properties")
|
|
149
|
+
if props_elem is None:
|
|
150
|
+
return props
|
|
151
|
+
for prop in props_elem.findall("property"):
|
|
152
|
+
prop_name = prop.get("name", "")
|
|
153
|
+
prop_type = prop.get("type", "string")
|
|
154
|
+
raw_value = prop.get("value")
|
|
155
|
+
if raw_value is None:
|
|
156
|
+
raw_value = (prop.text or "").strip()
|
|
157
|
+
props[prop_name] = _coerce_property_value(raw_value, prop_type)
|
|
158
|
+
return props
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _build_embedded_tileset_properties(data: dict) -> Dict[str, Any]:
|
|
162
|
+
props: Dict[str, Any] = {}
|
|
163
|
+
if "properties" in data:
|
|
164
|
+
props["properties"] = data["properties"]
|
|
165
|
+
return props
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _build_embedded_tile_properties(data: dict) -> Dict[str, Dict[str, Any]]:
|
|
169
|
+
return data.get("tile_properties", {})
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _tileset_data_to_parsed(data: dict) -> ParsedTileset:
|
|
173
|
+
image_path = data.get("image", "")
|
|
174
|
+
ts_type = "tile"
|
|
175
|
+
props = _build_embedded_tileset_properties(data)
|
|
176
|
+
tile_props = _build_embedded_tile_properties(data)
|
|
177
|
+
return ParsedTileset(
|
|
178
|
+
path=image_path,
|
|
179
|
+
type=ts_type,
|
|
180
|
+
properties=props,
|
|
181
|
+
tile_properties=tile_props,
|
|
182
|
+
animation=None,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def parse_tsx_file(path: Union[str, Path]) -> dict:
|
|
187
|
+
p = Path(path)
|
|
188
|
+
if not p.is_file():
|
|
189
|
+
raise TmxParseError(f"TSX file not found: {p}")
|
|
190
|
+
try:
|
|
191
|
+
tree = ElementTree.parse(p)
|
|
192
|
+
except ElementTree.ParseError as e:
|
|
193
|
+
raise TmxParseError(f"Invalid TSX XML in {p}: {e}") from e
|
|
194
|
+
return _parse_tsx_root(tree.getroot(), p)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _resolve_image_path(
|
|
198
|
+
raw_path: str, map_dir: Path, tsx_dir: Optional[Path] = None
|
|
199
|
+
) -> str:
|
|
200
|
+
if not raw_path:
|
|
201
|
+
return ""
|
|
202
|
+
p = Path(raw_path)
|
|
203
|
+
if p.is_absolute():
|
|
204
|
+
return raw_path
|
|
205
|
+
if tsx_dir:
|
|
206
|
+
candidate = tsx_dir / p
|
|
207
|
+
if candidate.exists():
|
|
208
|
+
return str(candidate)
|
|
209
|
+
candidate = map_dir / p
|
|
210
|
+
if candidate.exists():
|
|
211
|
+
return str(candidate)
|
|
212
|
+
return raw_path
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _decode_csv(text: str) -> List[int]:
|
|
216
|
+
gids: List[int] = []
|
|
217
|
+
for line in text.strip().splitlines():
|
|
218
|
+
line = line.strip()
|
|
219
|
+
if not line:
|
|
220
|
+
continue
|
|
221
|
+
for token in line.split(","):
|
|
222
|
+
token = token.strip()
|
|
223
|
+
if not token:
|
|
224
|
+
continue
|
|
225
|
+
base = 16 if token.lower().startswith("0x") else 10
|
|
226
|
+
gids.append(int(token, base))
|
|
227
|
+
return gids
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _decode_base64(text: str, compression: Optional[str]) -> List[int]:
|
|
231
|
+
try:
|
|
232
|
+
raw = base64.b64decode(text.strip())
|
|
233
|
+
if compression == "zlib":
|
|
234
|
+
raw = zlib.decompress(raw)
|
|
235
|
+
elif compression == "gzip":
|
|
236
|
+
raw = gzip.decompress(raw)
|
|
237
|
+
elif compression:
|
|
238
|
+
raise TmxParseError(
|
|
239
|
+
f"Unsupported tile data compression: {compression!r}"
|
|
240
|
+
)
|
|
241
|
+
except (ValueError, OSError, zlib.error) as e:
|
|
242
|
+
raise TmxParseError(f"Failed to decode tile data: {e}") from e
|
|
243
|
+
|
|
244
|
+
if len(raw) % 4 != 0:
|
|
245
|
+
raise TmxParseError(
|
|
246
|
+
f"Tile data payload has {len(raw)} bytes, expected multiple of 4"
|
|
247
|
+
)
|
|
248
|
+
count = len(raw) // 4
|
|
249
|
+
return list(struct.unpack(f"<{count}I", raw))
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _parse_tile_layer(
|
|
253
|
+
layer_elem: ElementTree.Element,
|
|
254
|
+
tilesets: List[Tuple[int, ParsedTileset]],
|
|
255
|
+
ctx: str,
|
|
256
|
+
) -> ParsedLayer:
|
|
257
|
+
name = layer_elem.get("name", "Unnamed")
|
|
258
|
+
width = _coerce_int(_require_attr(layer_elem, "width", ctx), f"{ctx}.width")
|
|
259
|
+
height = _coerce_int(_require_attr(layer_elem, "height", ctx), f"{ctx}.height")
|
|
260
|
+
visible = layer_elem.get("visible", "1") != "0"
|
|
261
|
+
opacity = _coerce_float(layer_elem.get("opacity", 1.0), f"{ctx}.opacity")
|
|
262
|
+
|
|
263
|
+
layer = ParsedLayer(
|
|
264
|
+
id=0,
|
|
265
|
+
name=name,
|
|
266
|
+
layer_type="tile",
|
|
267
|
+
visible=visible,
|
|
268
|
+
locked=False,
|
|
269
|
+
opacity=opacity,
|
|
270
|
+
z_index=0,
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
layer.properties = _parse_properties(layer_elem)
|
|
274
|
+
|
|
275
|
+
data_elem = layer_elem.find("data")
|
|
276
|
+
if data_elem is None:
|
|
277
|
+
return layer
|
|
278
|
+
|
|
279
|
+
encoding = data_elem.get("encoding", "xml")
|
|
280
|
+
compression = data_elem.get("compression")
|
|
281
|
+
text = data_elem.text or ""
|
|
282
|
+
|
|
283
|
+
if encoding == "csv":
|
|
284
|
+
gids = _decode_csv(text)
|
|
285
|
+
elif encoding == "base64":
|
|
286
|
+
gids = _decode_base64(text, compression)
|
|
287
|
+
else:
|
|
288
|
+
if encoding != "xml":
|
|
289
|
+
log.warning("%s: unsupported encoding %r, treating as xml", ctx, encoding)
|
|
290
|
+
gids = _decode_xml_tiles(data_elem, ctx)
|
|
291
|
+
|
|
292
|
+
expected = width * height
|
|
293
|
+
if len(gids) != expected:
|
|
294
|
+
raise TmxParseError(
|
|
295
|
+
f"{ctx}: expected {expected} tile GIDs, got {len(gids)}"
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
for idx, raw_gid in enumerate(gids):
|
|
299
|
+
if raw_gid == 0:
|
|
300
|
+
continue
|
|
301
|
+
tx = idx % width
|
|
302
|
+
ty = idx // width
|
|
303
|
+
stripped_gid, flip_h, flip_v, flip_d, flip_hex = _decode_flip_flags(raw_gid)
|
|
304
|
+
ttype, variant = _map_gid(stripped_gid, tilesets, f"{ctx}.tile[{ty},{tx}]")
|
|
305
|
+
if ttype is not None:
|
|
306
|
+
pos = (tx, ty)
|
|
307
|
+
layer.tiles[pos] = ParsedTile(
|
|
308
|
+
pos=pos,
|
|
309
|
+
ttype=ttype,
|
|
310
|
+
variant=variant,
|
|
311
|
+
flip_h=flip_h,
|
|
312
|
+
flip_v=flip_v,
|
|
313
|
+
flip_d=flip_d,
|
|
314
|
+
rotated_hex120=flip_hex,
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
return layer
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _decode_xml_tiles(data_elem: ElementTree.Element, ctx: str) -> List[int]:
|
|
321
|
+
gids: List[int] = []
|
|
322
|
+
for tile_elem in data_elem.findall("tile"):
|
|
323
|
+
gid_str = tile_elem.get("gid", "0")
|
|
324
|
+
gids.append(int(gid_str))
|
|
325
|
+
return gids
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _map_gid(
|
|
329
|
+
gid: int,
|
|
330
|
+
tilesets: List[Tuple[int, ParsedTileset]],
|
|
331
|
+
ctx: str,
|
|
332
|
+
) -> Tuple[Optional[Union[int, str]], int]:
|
|
333
|
+
for i, (firstgid, ts) in enumerate(tilesets):
|
|
334
|
+
tilecount = int(getattr(ts, "_tilecount", 0))
|
|
335
|
+
if ts.animation is not None:
|
|
336
|
+
tilecount = max(tilecount, ts.animation.frame_count)
|
|
337
|
+
if tilecount > 0:
|
|
338
|
+
if firstgid <= gid < firstgid + tilecount:
|
|
339
|
+
return i, gid - firstgid
|
|
340
|
+
else:
|
|
341
|
+
next_firstgid = tilesets[i + 1][0] if i + 1 < len(tilesets) else float("inf")
|
|
342
|
+
if firstgid <= gid < next_firstgid:
|
|
343
|
+
return i, gid - firstgid
|
|
344
|
+
log.debug("%s: GID %d does not fall into any known tileset range", ctx, gid)
|
|
345
|
+
return None, 0
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _parse_tmx_root(root: ElementTree.Element, map_dir: Path) -> ParsedMap:
|
|
349
|
+
map_elem = root
|
|
350
|
+
|
|
351
|
+
tilewidth = _coerce_int(
|
|
352
|
+
_require_attr(map_elem, "tilewidth", "<map>"), "<map>.tilewidth"
|
|
353
|
+
)
|
|
354
|
+
tileheight = _coerce_int(
|
|
355
|
+
_require_attr(map_elem, "tileheight", "<map>"), "<map>.tileheight"
|
|
356
|
+
)
|
|
357
|
+
map_width = _coerce_int(
|
|
358
|
+
_require_attr(map_elem, "width", "<map>"), "<map>.width"
|
|
359
|
+
)
|
|
360
|
+
map_height = _coerce_int(
|
|
361
|
+
_require_attr(map_elem, "height", "<map>"), "<map>.height"
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
meta = ParsedMeta(
|
|
365
|
+
tile_size=(tilewidth, tileheight),
|
|
366
|
+
map_size=(map_width, map_height),
|
|
367
|
+
initial_map_size=(map_width, map_height),
|
|
368
|
+
zoom_level=1.0,
|
|
369
|
+
scroll=(0, 0),
|
|
370
|
+
version="1.1",
|
|
371
|
+
render_scale=1.0,
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
parsed_tilesets: List[ParsedTileset] = []
|
|
375
|
+
tileset_ranges: List[Tuple[int, ParsedTileset]] = []
|
|
376
|
+
|
|
377
|
+
for ts_elem in map_elem.findall("tileset"):
|
|
378
|
+
firstgid = _coerce_int(
|
|
379
|
+
_require_attr(ts_elem, "firstgid", "<tileset>"), "<tileset>.firstgid"
|
|
380
|
+
)
|
|
381
|
+
source = ts_elem.get("source", "")
|
|
382
|
+
if source:
|
|
383
|
+
tsx_path = (map_dir / source).resolve()
|
|
384
|
+
if not tsx_path.is_file():
|
|
385
|
+
log.warning("Referenced TSX not found: %s", tsx_path)
|
|
386
|
+
ts = ParsedTileset(path="", type="tile")
|
|
387
|
+
ts_data: dict = {"tilecount": 0}
|
|
388
|
+
else:
|
|
389
|
+
ts_data = parse_tsx_file(tsx_path)
|
|
390
|
+
ts_data["image"] = _resolve_image_path(
|
|
391
|
+
ts_data.get("image", ""), map_dir, tsx_path.parent
|
|
392
|
+
)
|
|
393
|
+
ts = _tileset_data_to_parsed(ts_data)
|
|
394
|
+
else:
|
|
395
|
+
ts_data = _build_tsx_data(ts_elem, map_dir)
|
|
396
|
+
ts_data["image"] = _resolve_image_path(
|
|
397
|
+
ts_data.get("image", ""), map_dir, None
|
|
398
|
+
)
|
|
399
|
+
ts = _tileset_data_to_parsed(ts_data)
|
|
400
|
+
|
|
401
|
+
tilecount = ts_data.get("tilecount", 0)
|
|
402
|
+
if tilecount == 0 and ts_data.get("imagewidth") and ts_data.get("tilewidth"):
|
|
403
|
+
cols = ts_data["imagewidth"] // ts_data["tilewidth"]
|
|
404
|
+
rows = ts_data["imageheight"] // ts_data["tileheight"]
|
|
405
|
+
tilecount = cols * rows
|
|
406
|
+
if tilecount:
|
|
407
|
+
ts._tilecount = tilecount
|
|
408
|
+
parsed_tilesets.append(ts)
|
|
409
|
+
tileset_ranges.append((firstgid, parsed_tilesets[-1]))
|
|
410
|
+
if tilecount == 0:
|
|
411
|
+
log.warning(
|
|
412
|
+
"Tileset %r (firstgid=%d) has tilecount=0, GID lookup may be imprecise",
|
|
413
|
+
ts_data.get("name", ""),
|
|
414
|
+
firstgid,
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
layers: List[ParsedLayer] = []
|
|
418
|
+
for i, layer_elem in enumerate(map_elem.findall("layer")):
|
|
419
|
+
layer = _parse_tile_layer(
|
|
420
|
+
layer_elem, tileset_ranges, f"layer[{i}]"
|
|
421
|
+
)
|
|
422
|
+
layer.id = i
|
|
423
|
+
layer.z_index = i
|
|
424
|
+
layers.append(layer)
|
|
425
|
+
|
|
426
|
+
project_state = ParsedProjectState(rules=[], groups=[], automap_rules=None)
|
|
427
|
+
|
|
428
|
+
return ParsedMap(
|
|
429
|
+
meta=meta,
|
|
430
|
+
layers=layers,
|
|
431
|
+
tilesets=parsed_tilesets,
|
|
432
|
+
project_state=project_state,
|
|
433
|
+
raw={},
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def parse_tmx_file(path: Union[str, Path]) -> ParsedMap:
|
|
438
|
+
p = Path(path)
|
|
439
|
+
if not p.is_file():
|
|
440
|
+
raise TmxParseError(f"TMX file not found: {p}")
|
|
441
|
+
if p.suffix.lower() not in (".tmx", ".xml"):
|
|
442
|
+
raise TmxParseError(f"Expected .tmx file, got {p.suffix!r}")
|
|
443
|
+
try:
|
|
444
|
+
tree = ElementTree.parse(p)
|
|
445
|
+
except ElementTree.ParseError as e:
|
|
446
|
+
raise TmxParseError(f"Invalid TMX XML in {p}: {e}") from e
|
|
447
|
+
return _parse_tmx_root(tree.getroot(), p.parent)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tilemap-parser
|
|
3
|
-
Version:
|
|
3
|
+
Version: 5.0.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
|
|
@@ -704,7 +704,10 @@ Standalone parser/loader for tilemap-editor JSON maps, sprite animations, and co
|
|
|
704
704
|
## Features
|
|
705
705
|
|
|
706
706
|
- **Map parsing**: Load and query tilemaps, layers, objects, and autotile data from JSON
|
|
707
|
+
- **Chunked tile rendering**: Efficient visibility culling and chunk-based rendering with `TileLayerRenderer`
|
|
708
|
+
- **Camera system**: Centered/deadzone follow, lerp smoothing, screen-shake, and bounds clamping with `Camera`
|
|
707
709
|
- **Animation**: Frame-based sprite animation with `AnimationPlayer`
|
|
710
|
+
- **Particle system**: Configurable particle emitters with shapes, color transitions, alpha fades, gravity, and batch rendering with `ParticleSystem` + `SpriteBatchRenderer`
|
|
708
711
|
- **Collision (tile-based)**: Polygon collision detection for tilemaps with slide, platformer, and RPG movement modes via `CollisionRunner`
|
|
709
712
|
- **Collision (object-to-object)**: Spatial-grid mixed-shape collision detection (rect, circle, capsule, polygon) with layer filtering via `ObjectCollisionManager`
|
|
710
713
|
- **Capsule support**: Full capsule collision against all shape types
|
|
@@ -713,16 +716,22 @@ Standalone parser/loader for tilemap-editor JSON maps, sprite animations, and co
|
|
|
713
716
|
## Quick Start
|
|
714
717
|
|
|
715
718
|
```python
|
|
716
|
-
from tilemap_parser import load_map, TileLayerRenderer
|
|
719
|
+
from tilemap_parser import load_map, TileLayerRenderer, Camera
|
|
717
720
|
|
|
721
|
+
# Load map and set up renderer
|
|
718
722
|
game_data = load_map("path/to/map.json")
|
|
719
723
|
renderer = TileLayerRenderer(game_data)
|
|
724
|
+
|
|
725
|
+
# Set up camera
|
|
726
|
+
camera = Camera(800, 600, mode="centered")
|
|
727
|
+
camera.follow(player)
|
|
720
728
|
```
|
|
721
729
|
|
|
722
730
|
```python
|
|
723
731
|
from tilemap_parser import (
|
|
724
732
|
CollisionRunner, CollisionCache,
|
|
725
733
|
ObjectCollisionManager, CircleShape, RectangleShape,
|
|
734
|
+
ParticleSystem, ParticleSystemConfig
|
|
726
735
|
)
|
|
727
736
|
|
|
728
737
|
# Tile-based collision
|
|
@@ -735,10 +744,29 @@ manager = ObjectCollisionManager()
|
|
|
735
744
|
manager.add_object(player)
|
|
736
745
|
for hit in manager.check_all_collisions():
|
|
737
746
|
hit.resolve() # separate both objects
|
|
747
|
+
|
|
748
|
+
# Particle system
|
|
749
|
+
particle_config = ParticleSystemConfig(
|
|
750
|
+
particle_shape="circle",
|
|
751
|
+
spawn_rate=10.0,
|
|
752
|
+
max_particles=100,
|
|
753
|
+
lifetime_min=0.5,
|
|
754
|
+
lifetime_max=2.0,
|
|
755
|
+
speed_min=50,
|
|
756
|
+
speed_max=100,
|
|
757
|
+
direction=270,
|
|
758
|
+
spread=45,
|
|
759
|
+
start_color_r=255, start_color_g=200, start_color_b=100, start_color_a=255,
|
|
760
|
+
end_color_r=255, end_color_g=100, end_color_b=50, end_color_a=0,
|
|
761
|
+
alpha_fade="fade_out",
|
|
762
|
+
gravity_x=0,
|
|
763
|
+
gravity_y=50,
|
|
764
|
+
)
|
|
765
|
+
particles = ParticleSystem(particle_config)
|
|
738
766
|
```
|
|
739
767
|
|
|
740
768
|
## Links
|
|
741
769
|
|
|
742
|
-
- **Docs**: https://tilemap-parser.
|
|
770
|
+
- **Docs**: [https://deepwiki.com/FluffyBrudy/tilemap-parser](https://deepwiki.com/FluffyBrudy/tilemap-parser)
|
|
743
771
|
- **Editor**: https://pypi.org/project/tilemap-editor/
|
|
744
772
|
- **Repository**: https://github.com/FluffyBrudy/tilemap-parser
|
|
@@ -14,6 +14,7 @@ src/tilemap_parser/parser/collision_loader.py
|
|
|
14
14
|
src/tilemap_parser/parser/map_parse.py
|
|
15
15
|
src/tilemap_parser/parser/node_parse.py
|
|
16
16
|
src/tilemap_parser/parser/particle.py
|
|
17
|
+
src/tilemap_parser/parser/tmx_converter.py
|
|
17
18
|
src/tilemap_parser/runtime/__init__.py
|
|
18
19
|
src/tilemap_parser/runtime/animation_player.py
|
|
19
20
|
src/tilemap_parser/runtime/area_node.py
|
|
@@ -38,4 +39,5 @@ tests/test_object_collision.py
|
|
|
38
39
|
tests/test_object_surfaces.py
|
|
39
40
|
tests/test_particle_emitter.py
|
|
40
41
|
tests/test_render_scale.py
|
|
41
|
-
tests/test_tile_collision.py
|
|
42
|
+
tests/test_tile_collision.py
|
|
43
|
+
tests/test_tmx_converter.py
|
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
FIXTURES = Path(__file__).parent / "fixtures"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _resolve(name: str) -> Path:
|
|
9
|
+
p = FIXTURES / name
|
|
10
|
+
assert p.is_file(), f"Fixture not found: {p}"
|
|
11
|
+
return p
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TestParseTsx:
|
|
15
|
+
def test_basic_tsx(self):
|
|
16
|
+
from tilemap_parser.parser.tmx_converter import parse_tsx_file
|
|
17
|
+
|
|
18
|
+
data = parse_tsx_file(_resolve("test_tileset.tsx"))
|
|
19
|
+
assert data["name"] == "test_tileset"
|
|
20
|
+
assert data["tilewidth"] == 32
|
|
21
|
+
assert data["tileheight"] == 32
|
|
22
|
+
assert data["tilecount"] == 4
|
|
23
|
+
assert data["columns"] == 2
|
|
24
|
+
assert data["image"] == "tileset.png"
|
|
25
|
+
assert "properties" not in data
|
|
26
|
+
assert "tile_properties" in data
|
|
27
|
+
tile0 = data["tile_properties"]["0"]
|
|
28
|
+
assert tile0["grass"] is True
|
|
29
|
+
tile1 = data["tile_properties"]["1"]
|
|
30
|
+
assert tile1["hp"] == 10
|
|
31
|
+
|
|
32
|
+
def test_tsx_file_not_found(self):
|
|
33
|
+
from tilemap_parser.parser.tmx_converter import TmxParseError, parse_tsx_file
|
|
34
|
+
|
|
35
|
+
with pytest.raises(TmxParseError, match="not found"):
|
|
36
|
+
parse_tsx_file("/nonexistent/nope.tsx")
|
|
37
|
+
|
|
38
|
+
def test_tsx_invalid_xml(self, tmp_path):
|
|
39
|
+
from tilemap_parser.parser.tmx_converter import TmxParseError, parse_tsx_file
|
|
40
|
+
|
|
41
|
+
bad = tmp_path / "bad.tsx"
|
|
42
|
+
bad.write_text("not xml")
|
|
43
|
+
with pytest.raises(TmxParseError, match="Invalid TSX"):
|
|
44
|
+
parse_tsx_file(bad)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class TestParseTmxCsv:
|
|
48
|
+
def test_load_via_parse_map_file(self):
|
|
49
|
+
from tilemap_parser.parser.map_parse import parse_map_file
|
|
50
|
+
|
|
51
|
+
parsed = parse_map_file(_resolve("test_map_csv.tmx"))
|
|
52
|
+
|
|
53
|
+
assert parsed.meta.tile_size == (32, 32)
|
|
54
|
+
assert parsed.meta.map_size == (3, 3)
|
|
55
|
+
|
|
56
|
+
assert len(parsed.tilesets) == 1
|
|
57
|
+
assert parsed.tilesets[0].path.endswith("tileset.png")
|
|
58
|
+
|
|
59
|
+
assert len(parsed.layers) == 1
|
|
60
|
+
layer = parsed.layers[0]
|
|
61
|
+
assert layer.name == "Tile Layer 1"
|
|
62
|
+
assert layer.layer_type == "tile"
|
|
63
|
+
assert layer.visible is True
|
|
64
|
+
assert layer.opacity == 1.0
|
|
65
|
+
|
|
66
|
+
assert len(layer.tiles) == 4 # only GIDs 1-4 valid (tilecount=4)
|
|
67
|
+
|
|
68
|
+
assert (0, 0) in layer.tiles
|
|
69
|
+
assert layer.tiles[(0, 0)].ttype == 0
|
|
70
|
+
assert layer.tiles[(0, 0)].variant == 0
|
|
71
|
+
assert layer.tiles[(1, 0)].variant == 1
|
|
72
|
+
assert layer.tiles[(2, 0)].variant == 2
|
|
73
|
+
assert layer.tiles[(0, 1)].variant == 3
|
|
74
|
+
|
|
75
|
+
assert (2, 2) not in layer.tiles # GID 0 → empty
|
|
76
|
+
|
|
77
|
+
def test_embedded_tileset(self):
|
|
78
|
+
from tilemap_parser.parser.map_parse import parse_map_file
|
|
79
|
+
|
|
80
|
+
parsed = parse_map_file(_resolve("test_map_embedded_tileset.tmx"))
|
|
81
|
+
|
|
82
|
+
assert len(parsed.tilesets) == 1
|
|
83
|
+
ts = parsed.tilesets[0]
|
|
84
|
+
assert ts.path.endswith("builtin.png")
|
|
85
|
+
assert ts.type == "tile"
|
|
86
|
+
assert ts._tilecount == 4
|
|
87
|
+
|
|
88
|
+
layer = parsed.layers[0]
|
|
89
|
+
assert len(layer.tiles) == 4
|
|
90
|
+
assert layer.tiles[(0, 0)].ttype == 0
|
|
91
|
+
assert layer.tiles[(0, 0)].variant == 0
|
|
92
|
+
assert layer.tiles[(1, 1)].variant == 3
|
|
93
|
+
|
|
94
|
+
def test_empty_map(self):
|
|
95
|
+
from tilemap_parser.parser.map_parse import parse_map_file
|
|
96
|
+
|
|
97
|
+
parsed = parse_map_file(_resolve("test_map_empty.tmx"))
|
|
98
|
+
|
|
99
|
+
assert parsed.meta.map_size == (10, 10)
|
|
100
|
+
assert len(parsed.tilesets) == 0
|
|
101
|
+
assert len(parsed.layers) == 1
|
|
102
|
+
assert len(parsed.layers[0].tiles) == 0
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class TestParseTmxBase64:
|
|
106
|
+
def test_base64_zlib(self):
|
|
107
|
+
from tilemap_parser.parser.map_parse import parse_map_file
|
|
108
|
+
|
|
109
|
+
parsed = parse_map_file(_resolve("test_map_base64.tmx"))
|
|
110
|
+
|
|
111
|
+
assert len(parsed.layers) == 1
|
|
112
|
+
layer = parsed.layers[0]
|
|
113
|
+
assert len(layer.tiles) == 4
|
|
114
|
+
|
|
115
|
+
assert layer.tiles[(0, 0)].ttype == 0
|
|
116
|
+
assert layer.tiles[(0, 0)].variant == 0
|
|
117
|
+
assert layer.tiles[(1, 0)].variant == 1
|
|
118
|
+
assert (2, 2) not in layer.tiles # GID 0 → empty
|
|
119
|
+
|
|
120
|
+
def test_base64_raw_uncompressed(self):
|
|
121
|
+
from tilemap_parser.parser.map_parse import parse_map_file
|
|
122
|
+
|
|
123
|
+
parsed = parse_map_file(_resolve("test_map_base64_raw.tmx"))
|
|
124
|
+
|
|
125
|
+
assert len(parsed.layers) == 1
|
|
126
|
+
layer = parsed.layers[0]
|
|
127
|
+
assert len(layer.tiles) == 4
|
|
128
|
+
assert layer.tiles[(0, 0)].variant == 0
|
|
129
|
+
assert layer.tiles[(2, 0)].variant == 2
|
|
130
|
+
assert (1, 1) not in layer.tiles # GID 5 is out of range
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class TestFlipFlags:
|
|
134
|
+
def test_flip_flags_preserved_on_parsed_tile(self):
|
|
135
|
+
from tilemap_parser.parser.map_parse import parse_map_file
|
|
136
|
+
|
|
137
|
+
parsed = parse_map_file(_resolve("test_map_flip.tmx"))
|
|
138
|
+
|
|
139
|
+
layer = parsed.layers[0]
|
|
140
|
+
assert len(layer.tiles) == 2
|
|
141
|
+
|
|
142
|
+
tile_with_flag = layer.tiles[(0, 0)]
|
|
143
|
+
assert tile_with_flag.variant == 0
|
|
144
|
+
assert tile_with_flag.flip_h is True
|
|
145
|
+
assert tile_with_flag.flip_v is False
|
|
146
|
+
assert tile_with_flag.flip_d is False
|
|
147
|
+
assert tile_with_flag.rotated_hex120 is False
|
|
148
|
+
|
|
149
|
+
tile_no_flag = layer.tiles[(1, 0)]
|
|
150
|
+
assert tile_no_flag.variant == 0
|
|
151
|
+
assert tile_no_flag.flip_h is False
|
|
152
|
+
assert tile_no_flag.flip_v is False
|
|
153
|
+
assert tile_no_flag.flip_d is False
|
|
154
|
+
assert tile_no_flag.rotated_hex120 is False
|
|
155
|
+
|
|
156
|
+
def test_strip_flip_bits(self):
|
|
157
|
+
from tilemap_parser.parser.tmx_converter import (
|
|
158
|
+
TILE_FLIP_H,
|
|
159
|
+
TILE_FLIP_V,
|
|
160
|
+
TILE_FLIP_D,
|
|
161
|
+
TILE_FLIP_HEX,
|
|
162
|
+
TILE_FLIP_MASK,
|
|
163
|
+
_decode_flip_flags,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
assert _decode_flip_flags(1) == (1, False, False, False, False)
|
|
167
|
+
assert _decode_flip_flags(TILE_FLIP_H | 5) == (5, True, False, False, False)
|
|
168
|
+
assert _decode_flip_flags(TILE_FLIP_V | 3) == (3, False, True, False, False)
|
|
169
|
+
assert _decode_flip_flags(TILE_FLIP_D | 7) == (7, False, False, True, False)
|
|
170
|
+
assert _decode_flip_flags(TILE_FLIP_HEX | 11) == (11, False, False, False, True)
|
|
171
|
+
assert _decode_flip_flags(TILE_FLIP_H | TILE_FLIP_V | 42) == (42, True, True, False, False)
|
|
172
|
+
assert _decode_flip_flags(TILE_FLIP_H | TILE_FLIP_V | TILE_FLIP_D | 99) == (99, True, True, True, False)
|
|
173
|
+
assert _decode_flip_flags(0) == (0, False, False, False, False)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class TestDecodeHelpers:
|
|
177
|
+
def test_decode_csv(self):
|
|
178
|
+
from tilemap_parser.parser.tmx_converter import _decode_csv
|
|
179
|
+
|
|
180
|
+
result = _decode_csv("1,2,3\n4,5,6\n")
|
|
181
|
+
assert result == [1, 2, 3, 4, 5, 6]
|
|
182
|
+
|
|
183
|
+
def test_decode_csv_empty(self):
|
|
184
|
+
from tilemap_parser.parser.tmx_converter import _decode_csv
|
|
185
|
+
|
|
186
|
+
assert _decode_csv("") == []
|
|
187
|
+
|
|
188
|
+
def test_decode_base64_zlib(self):
|
|
189
|
+
import base64
|
|
190
|
+
import struct
|
|
191
|
+
import zlib
|
|
192
|
+
|
|
193
|
+
from tilemap_parser.parser.tmx_converter import _decode_base64
|
|
194
|
+
|
|
195
|
+
data = struct.pack("<4I", 10, 20, 30, 40)
|
|
196
|
+
b64 = base64.b64encode(zlib.compress(data)).decode()
|
|
197
|
+
result = _decode_base64(b64, "zlib")
|
|
198
|
+
assert result == [10, 20, 30, 40]
|
|
199
|
+
|
|
200
|
+
def test_decode_base64_uncompressed(self):
|
|
201
|
+
import base64
|
|
202
|
+
import struct
|
|
203
|
+
|
|
204
|
+
from tilemap_parser.parser.tmx_converter import _decode_base64
|
|
205
|
+
|
|
206
|
+
data = struct.pack("<3I", 7, 8, 9)
|
|
207
|
+
b64 = base64.b64encode(data).decode()
|
|
208
|
+
result = _decode_base64(b64, None)
|
|
209
|
+
assert result == [7, 8, 9]
|
|
210
|
+
|
|
211
|
+
def test_decode_base64_unsupported_compression(self):
|
|
212
|
+
from tilemap_parser.parser.tmx_converter import TmxParseError, _decode_base64
|
|
213
|
+
|
|
214
|
+
with pytest.raises(TmxParseError, match="Unsupported"):
|
|
215
|
+
_decode_base64("AAAA", "lzma")
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
class TestProjectState:
|
|
219
|
+
def test_project_state_empty(self):
|
|
220
|
+
from tilemap_parser.parser.map_parse import parse_map_file
|
|
221
|
+
|
|
222
|
+
parsed = parse_map_file(_resolve("test_map_csv.tmx"))
|
|
223
|
+
|
|
224
|
+
assert len(parsed.project_state.rules) == 0
|
|
225
|
+
assert len(parsed.project_state.groups) == 0
|
|
226
|
+
assert parsed.project_state.automap_rules is None
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class TestXmlEncoding:
|
|
230
|
+
def test_xml_tile_encoding(self, tmp_path):
|
|
231
|
+
from tilemap_parser.parser.tmx_converter import parse_tmx_file
|
|
232
|
+
|
|
233
|
+
tmx = tmp_path / "xml_encoded.tmx"
|
|
234
|
+
tmx.write_text("""<?xml version="1.0" encoding="UTF-8"?>
|
|
235
|
+
<map version="1.10" orientation="orthogonal" renderorder="right-down" width="2" height="2" tilewidth="16" tileheight="16" infinite="0">
|
|
236
|
+
<tileset firstgid="1" name="a" tilewidth="16" tileheight="16" tilecount="4" columns="2">
|
|
237
|
+
<image source="a.png" width="32" height="32"/>
|
|
238
|
+
</tileset>
|
|
239
|
+
<layer id="1" name="Tile Layer 1" width="2" height="2">
|
|
240
|
+
<data>
|
|
241
|
+
<tile gid="1"/>
|
|
242
|
+
<tile gid="2"/>
|
|
243
|
+
<tile gid="3"/>
|
|
244
|
+
<tile gid="4"/>
|
|
245
|
+
</data>
|
|
246
|
+
</layer>
|
|
247
|
+
</map>""")
|
|
248
|
+
parsed = parse_tmx_file(tmx)
|
|
249
|
+
assert len(parsed.layers[0].tiles) == 4
|
|
250
|
+
assert parsed.layers[0].tiles[(0, 0)].variant == 0
|
|
251
|
+
assert parsed.layers[0].tiles[(1, 1)].variant == 3
|
|
252
|
+
|
|
253
|
+
def test_xml_with_empty_tiles(self, tmp_path):
|
|
254
|
+
from tilemap_parser.parser.tmx_converter import parse_tmx_file
|
|
255
|
+
|
|
256
|
+
tmx = tmp_path / "xml_empty.tmx"
|
|
257
|
+
tmx.write_text("""<?xml version="1.0" encoding="UTF-8"?>
|
|
258
|
+
<map version="1.10" orientation="orthogonal" renderorder="right-down" width="2" height="2" tilewidth="16" tileheight="16" infinite="0">
|
|
259
|
+
<tileset firstgid="1" name="a" tilewidth="16" tileheight="16" tilecount="4" columns="2">
|
|
260
|
+
<image source="a.png" width="32" height="32"/>
|
|
261
|
+
</tileset>
|
|
262
|
+
<layer id="1" name="Empty" width="2" height="2">
|
|
263
|
+
<data>
|
|
264
|
+
<tile gid="0"/>
|
|
265
|
+
<tile gid="0"/>
|
|
266
|
+
<tile gid="0"/>
|
|
267
|
+
<tile gid="1"/>
|
|
268
|
+
</data>
|
|
269
|
+
</layer>
|
|
270
|
+
</map>""")
|
|
271
|
+
parsed = parse_tmx_file(tmx)
|
|
272
|
+
assert len(parsed.layers[0].tiles) == 1
|
|
273
|
+
assert (1, 1) in parsed.layers[0].tiles
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
class TestLayerProperties:
|
|
277
|
+
def test_layer_visibility_and_opacity(self, tmp_path):
|
|
278
|
+
from tilemap_parser.parser.tmx_converter import parse_tmx_file
|
|
279
|
+
|
|
280
|
+
tmx = tmp_path / "props.tmx"
|
|
281
|
+
tmx.write_text("""<?xml version="1.0" encoding="UTF-8"?>
|
|
282
|
+
<map version="1.10" orientation="orthogonal" renderorder="right-down" width="1" height="1" tilewidth="16" tileheight="16" infinite="0">
|
|
283
|
+
<layer id="1" name="Hidden" width="1" height="1" visible="0" opacity="0.5">
|
|
284
|
+
<data encoding="csv">0</data>
|
|
285
|
+
</layer>
|
|
286
|
+
</map>""")
|
|
287
|
+
parsed = parse_tmx_file(tmx)
|
|
288
|
+
layer = parsed.layers[0]
|
|
289
|
+
assert layer.visible is False
|
|
290
|
+
assert layer.opacity == 0.5
|
|
291
|
+
assert layer.name == "Hidden"
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
class TestMultipleLayers:
|
|
295
|
+
def test_two_layers(self, tmp_path):
|
|
296
|
+
from tilemap_parser.parser.tmx_converter import parse_tmx_file
|
|
297
|
+
|
|
298
|
+
tmx = tmp_path / "two_layers.tmx"
|
|
299
|
+
tmx.write_text("""<?xml version="1.0" encoding="UTF-8"?>
|
|
300
|
+
<map version="1.10" orientation="orthogonal" renderorder="right-down" width="2" height="2" tilewidth="16" tileheight="16" infinite="0">
|
|
301
|
+
<tileset firstgid="1" name="a" tilewidth="16" tileheight="16" tilecount="4" columns="2">
|
|
302
|
+
<image source="a.png" width="32" height="32"/>
|
|
303
|
+
</tileset>
|
|
304
|
+
<layer id="1" name="Ground" width="2" height="2">
|
|
305
|
+
<data encoding="csv">1,1,1,1</data>
|
|
306
|
+
</layer>
|
|
307
|
+
<layer id="2" name="Overlay" width="2" height="2">
|
|
308
|
+
<data encoding="csv">0,2,0,0</data>
|
|
309
|
+
</layer>
|
|
310
|
+
</map>""")
|
|
311
|
+
parsed = parse_tmx_file(tmx)
|
|
312
|
+
assert len(parsed.layers) == 2
|
|
313
|
+
assert parsed.layers[0].name == "Ground"
|
|
314
|
+
assert parsed.layers[0].z_index == 0
|
|
315
|
+
assert len(parsed.layers[0].tiles) == 4
|
|
316
|
+
assert parsed.layers[1].name == "Overlay"
|
|
317
|
+
assert parsed.layers[1].z_index == 1
|
|
318
|
+
assert len(parsed.layers[1].tiles) == 1
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
class TestRuntimeIntegration:
|
|
322
|
+
def test_tmx_loads_via_tilemap_data(self, tmp_path):
|
|
323
|
+
import pygame
|
|
324
|
+
|
|
325
|
+
from tilemap_parser.runtime.map_loader import TilemapData
|
|
326
|
+
|
|
327
|
+
data_dir = tmp_path / "data"
|
|
328
|
+
data_dir.mkdir()
|
|
329
|
+
assets_dir = tmp_path / "assets"
|
|
330
|
+
assets_dir.mkdir()
|
|
331
|
+
|
|
332
|
+
png = assets_dir / "tileset.png"
|
|
333
|
+
surf = pygame.Surface((64, 64))
|
|
334
|
+
surf.fill((255, 0, 255))
|
|
335
|
+
pygame.image.save(surf, str(png))
|
|
336
|
+
|
|
337
|
+
tmx = data_dir / "test.tmx"
|
|
338
|
+
tmx.write_text("""<?xml version="1.0" encoding="UTF-8"?>
|
|
339
|
+
<map version="1.10" orientation="orthogonal" renderorder="right-down" width="2" height="2" tilewidth="32" tileheight="32" infinite="0">
|
|
340
|
+
<tileset firstgid="1" name="test" tilewidth="32" tileheight="32" tilecount="4" columns="2">
|
|
341
|
+
<image source="../assets/tileset.png" width="64" height="64"/>
|
|
342
|
+
</tileset>
|
|
343
|
+
<layer id="1" name="Ground" width="2" height="2">
|
|
344
|
+
<data encoding="csv">1,2,3,4</data>
|
|
345
|
+
</layer>
|
|
346
|
+
</map>""")
|
|
347
|
+
|
|
348
|
+
td = TilemapData.load(tmx, skip_missing_images=False)
|
|
349
|
+
assert len(td.warnings) == 0
|
|
350
|
+
assert len(td.surfaces) == 1
|
|
351
|
+
assert td.surfaces[0] is not None
|
|
352
|
+
assert len(td.parsed.layers) == 1
|
|
353
|
+
layer_data = td.get_layer("Ground")
|
|
354
|
+
assert layer_data is not None
|
|
355
|
+
assert len(layer_data.tiles) == 4
|
|
356
|
+
|
|
357
|
+
def test_tmx_missing_tileset_warns(self, tmp_path):
|
|
358
|
+
import pygame
|
|
359
|
+
|
|
360
|
+
from tilemap_parser.runtime.map_loader import TilemapData
|
|
361
|
+
|
|
362
|
+
tmx = tmp_path / "missing.tmx"
|
|
363
|
+
tmx.write_text("""<?xml version="1.0" encoding="UTF-8"?>
|
|
364
|
+
<map version="1.10" orientation="orthogonal" renderorder="right-down" width="2" height="2" tilewidth="32" tileheight="32" infinite="0">
|
|
365
|
+
<tileset firstgid="1" source="nonexistent.tsx"/>
|
|
366
|
+
<layer id="1" name="Ground" width="2" height="2">
|
|
367
|
+
<data encoding="csv">0,0,0,0</data>
|
|
368
|
+
</layer>
|
|
369
|
+
</map>""")
|
|
370
|
+
|
|
371
|
+
td = TilemapData.load(tmx, extra_search_base=tmp_path)
|
|
372
|
+
assert len(td.warnings) >= 1
|
|
373
|
+
|
|
374
|
+
def test_real_sewers_map_from_tiled_examples(self):
|
|
375
|
+
import pygame
|
|
376
|
+
|
|
377
|
+
from tilemap_parser.runtime.map_loader import TilemapData
|
|
378
|
+
from tilemap_parser.parser.map_parse import parse_map_file
|
|
379
|
+
|
|
380
|
+
tmx_path = _resolve("sewers.tmx")
|
|
381
|
+
png_path = _resolve("sewer_tileset.png")
|
|
382
|
+
assert png_path.is_file()
|
|
383
|
+
|
|
384
|
+
parsed = parse_map_file(tmx_path)
|
|
385
|
+
|
|
386
|
+
assert parsed.meta.tile_size == (24, 24)
|
|
387
|
+
assert parsed.meta.map_size == (50, 50)
|
|
388
|
+
|
|
389
|
+
assert len(parsed.tilesets) == 1
|
|
390
|
+
ts = parsed.tilesets[0]
|
|
391
|
+
assert ts.path.endswith("sewer_tileset.png")
|
|
392
|
+
assert ts.type == "tile"
|
|
393
|
+
|
|
394
|
+
assert len(parsed.layers) == 2
|
|
395
|
+
bottom = parsed.layers[0]
|
|
396
|
+
top = parsed.layers[1]
|
|
397
|
+
|
|
398
|
+
assert bottom.name == "Bottom"
|
|
399
|
+
assert bottom.layer_type == "tile"
|
|
400
|
+
assert bottom.opacity == 1.0
|
|
401
|
+
assert len(bottom.tiles) > 0
|
|
402
|
+
|
|
403
|
+
assert top.name == "Top"
|
|
404
|
+
assert top.layer_type == "tile"
|
|
405
|
+
assert top.opacity == 0.49
|
|
406
|
+
assert len(top.tiles) > 0
|
|
407
|
+
|
|
408
|
+
all_ttypes = {t.ttype for t in bottom.tiles.values()}
|
|
409
|
+
assert all_ttypes == {0}
|
|
410
|
+
|
|
411
|
+
all_variants = {t.variant for t in bottom.tiles.values()}
|
|
412
|
+
max_variant = max(all_variants)
|
|
413
|
+
min_variant = min(all_variants)
|
|
414
|
+
assert min_variant >= 0
|
|
415
|
+
assert max_variant < 72 # 8 cols * 9 rows
|
|
416
|
+
|
|
417
|
+
td = TilemapData.load(tmx_path, skip_missing_images=False)
|
|
418
|
+
assert len(td.warnings) == 0
|
|
419
|
+
assert len(td.surfaces) == 1
|
|
420
|
+
assert td.surfaces[0] is not None
|
|
421
|
+
assert td.surfaces[0].get_size() == (192, 217)
|
|
422
|
+
|
|
423
|
+
assert len(td.parsed.layers) == 2
|
|
424
|
+
|
|
425
|
+
assert td.get_layer("Bottom") is not None
|
|
426
|
+
assert td.get_layer("Top") is not None
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
class TestErrorCases:
|
|
430
|
+
def test_nonexistent_file(self):
|
|
431
|
+
from tilemap_parser.parser.map_parse import parse_map_file
|
|
432
|
+
|
|
433
|
+
with pytest.raises(Exception, match="Not a file"):
|
|
434
|
+
parse_map_file("/nonexistent/map.tmx")
|
|
435
|
+
|
|
436
|
+
def test_wrong_extension_raises_error(self, tmp_path):
|
|
437
|
+
from tilemap_parser.parser.map_parse import parse_map_file
|
|
438
|
+
|
|
439
|
+
f = tmp_path / "test.py"
|
|
440
|
+
f.write_text("not a map")
|
|
441
|
+
with pytest.raises(Exception, match=r"\.json.*\.tmx"):
|
|
442
|
+
parse_map_file(f)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tilemap_parser-3.1.23 → tilemap_parser-5.0.0}/src/tilemap_parser/parser/collision_loader.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tilemap_parser-3.1.23 → tilemap_parser-5.0.0}/src/tilemap_parser/runtime/animation_player.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tilemap_parser-3.1.23 → tilemap_parser-5.0.0}/src/tilemap_parser/runtime/collision_cache.py
RENAMED
|
File without changes
|
|
File without changes
|
{tilemap_parser-3.1.23 → tilemap_parser-5.0.0}/src/tilemap_parser/runtime/object_collision.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tilemap_parser-3.1.23 → tilemap_parser-5.0.0}/src/tilemap_parser.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|