tilemap-parser 4.2.3__tar.gz → 4.2.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {tilemap_parser-4.2.3 → tilemap_parser-4.2.4}/PKG-INFO +1 -1
- {tilemap_parser-4.2.3 → tilemap_parser-4.2.4}/pyproject.toml +1 -1
- {tilemap_parser-4.2.3 → tilemap_parser-4.2.4}/src/tilemap_parser/parser/map_parse.py +5 -0
- {tilemap_parser-4.2.3 → tilemap_parser-4.2.4}/src/tilemap_parser/parser/tmx_converter.py +1 -0
- {tilemap_parser-4.2.3 → tilemap_parser-4.2.4}/src/tilemap_parser/runtime/map_object.py +3 -0
- {tilemap_parser-4.2.3 → tilemap_parser-4.2.4}/src/tilemap_parser/runtime/protocols.py +11 -2
- {tilemap_parser-4.2.3 → tilemap_parser-4.2.4}/src/tilemap_parser/runtime/renderer.py +41 -28
- {tilemap_parser-4.2.3 → tilemap_parser-4.2.4}/src/tilemap_parser.egg-info/PKG-INFO +1 -1
- {tilemap_parser-4.2.3 → tilemap_parser-4.2.4}/src/tilemap_parser.egg-info/SOURCES.txt +1 -0
- tilemap_parser-4.2.4/tests/test_tile_layer_renderer_y_sort.py +348 -0
- {tilemap_parser-4.2.3 → tilemap_parser-4.2.4}/LICENSE +0 -0
- {tilemap_parser-4.2.3 → tilemap_parser-4.2.4}/README.md +0 -0
- {tilemap_parser-4.2.3 → tilemap_parser-4.2.4}/setup.cfg +0 -0
- {tilemap_parser-4.2.3 → tilemap_parser-4.2.4}/src/tilemap_parser/__init__.py +0 -0
- {tilemap_parser-4.2.3 → tilemap_parser-4.2.4}/src/tilemap_parser/parser/__init__.py +0 -0
- {tilemap_parser-4.2.3 → tilemap_parser-4.2.4}/src/tilemap_parser/parser/animation.py +0 -0
- {tilemap_parser-4.2.3 → tilemap_parser-4.2.4}/src/tilemap_parser/parser/collision.py +0 -0
- {tilemap_parser-4.2.3 → tilemap_parser-4.2.4}/src/tilemap_parser/parser/collision_loader.py +0 -0
- {tilemap_parser-4.2.3 → tilemap_parser-4.2.4}/src/tilemap_parser/parser/node_parse.py +0 -0
- {tilemap_parser-4.2.3 → tilemap_parser-4.2.4}/src/tilemap_parser/parser/particle.py +0 -0
- {tilemap_parser-4.2.3 → tilemap_parser-4.2.4}/src/tilemap_parser/runtime/__init__.py +0 -0
- {tilemap_parser-4.2.3 → tilemap_parser-4.2.4}/src/tilemap_parser/runtime/animation_player.py +0 -0
- {tilemap_parser-4.2.3 → tilemap_parser-4.2.4}/src/tilemap_parser/runtime/area_node.py +0 -0
- {tilemap_parser-4.2.3 → tilemap_parser-4.2.4}/src/tilemap_parser/runtime/camera.py +0 -0
- {tilemap_parser-4.2.3 → tilemap_parser-4.2.4}/src/tilemap_parser/runtime/collision_cache.py +0 -0
- {tilemap_parser-4.2.3 → tilemap_parser-4.2.4}/src/tilemap_parser/runtime/map_loader.py +0 -0
- {tilemap_parser-4.2.3 → tilemap_parser-4.2.4}/src/tilemap_parser/runtime/object_collision.py +0 -0
- {tilemap_parser-4.2.3 → tilemap_parser-4.2.4}/src/tilemap_parser/runtime/particles.py +0 -0
- {tilemap_parser-4.2.3 → tilemap_parser-4.2.4}/src/tilemap_parser/runtime/tile_collision.py +0 -0
- {tilemap_parser-4.2.3 → tilemap_parser-4.2.4}/src/tilemap_parser/utils/__init__.py +0 -0
- {tilemap_parser-4.2.3 → tilemap_parser-4.2.4}/src/tilemap_parser/utils/geometry.py +0 -0
- {tilemap_parser-4.2.3 → tilemap_parser-4.2.4}/src/tilemap_parser.egg-info/dependency_links.txt +0 -0
- {tilemap_parser-4.2.3 → tilemap_parser-4.2.4}/src/tilemap_parser.egg-info/requires.txt +0 -0
- {tilemap_parser-4.2.3 → tilemap_parser-4.2.4}/src/tilemap_parser.egg-info/top_level.txt +0 -0
- {tilemap_parser-4.2.3 → tilemap_parser-4.2.4}/tests/test_camera.py +0 -0
- {tilemap_parser-4.2.3 → tilemap_parser-4.2.4}/tests/test_collision.py +0 -0
- {tilemap_parser-4.2.3 → tilemap_parser-4.2.4}/tests/test_geometry.py +0 -0
- {tilemap_parser-4.2.3 → tilemap_parser-4.2.4}/tests/test_gid_collision.py +0 -0
- {tilemap_parser-4.2.3 → tilemap_parser-4.2.4}/tests/test_integration_animation.py +0 -0
- {tilemap_parser-4.2.3 → tilemap_parser-4.2.4}/tests/test_integration_collision.py +0 -0
- {tilemap_parser-4.2.3 → tilemap_parser-4.2.4}/tests/test_integration_map_loader.py +0 -0
- {tilemap_parser-4.2.3 → tilemap_parser-4.2.4}/tests/test_map_loader.py +0 -0
- {tilemap_parser-4.2.3 → tilemap_parser-4.2.4}/tests/test_map_object.py +0 -0
- {tilemap_parser-4.2.3 → tilemap_parser-4.2.4}/tests/test_move_grounded.py +0 -0
- {tilemap_parser-4.2.3 → tilemap_parser-4.2.4}/tests/test_object_collision.py +0 -0
- {tilemap_parser-4.2.3 → tilemap_parser-4.2.4}/tests/test_object_surfaces.py +0 -0
- {tilemap_parser-4.2.3 → tilemap_parser-4.2.4}/tests/test_particle_emitter.py +0 -0
- {tilemap_parser-4.2.3 → tilemap_parser-4.2.4}/tests/test_render_scale.py +0 -0
- {tilemap_parser-4.2.3 → tilemap_parser-4.2.4}/tests/test_tile_collision.py +0 -0
- {tilemap_parser-4.2.3 → tilemap_parser-4.2.4}/tests/test_tmx_converter.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tilemap-parser
|
|
3
|
-
Version: 4.2.
|
|
3
|
+
Version: 4.2.4
|
|
4
4
|
Summary: Standalone parser/loader for tilemap-editor JSON maps, sprite animations, and collision detection runtime.
|
|
5
5
|
Author: tilemap parser contributors
|
|
6
6
|
License: GNU GENERAL PUBLIC LICENSE
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "tilemap-parser"
|
|
7
|
-
version = "4.2.
|
|
7
|
+
version = "4.2.4"
|
|
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"
|
|
@@ -159,6 +159,8 @@ class ParsedLayer:
|
|
|
159
159
|
locked: bool
|
|
160
160
|
opacity: float
|
|
161
161
|
z_index: int
|
|
162
|
+
y_sort: bool = False
|
|
163
|
+
y_sort_origin: int = 0
|
|
162
164
|
properties: JsonDict = field(default_factory=dict)
|
|
163
165
|
tiles: Dict[Point, ParsedTile] = field(default_factory=dict)
|
|
164
166
|
objects: Dict[int, ParsedObject] = field(default_factory=dict)
|
|
@@ -285,6 +287,8 @@ def _parse_layer(layer_obj: JsonDict, layer_id: int, ctx: str) -> ParsedLayer:
|
|
|
285
287
|
locked=_coerce_bool(layer_obj.get("locked", False), f"{ctx}.locked"),
|
|
286
288
|
opacity=_coerce_float(layer_obj.get("opacity", 1.0), f"{ctx}.opacity"),
|
|
287
289
|
z_index=_coerce_int(layer_obj.get("z_index", layer_id), f"{ctx}.z_index"),
|
|
290
|
+
y_sort=_coerce_bool(layer_obj.get("y_sort", False), f"{ctx}.y_sort"),
|
|
291
|
+
y_sort_origin=layer_obj.get("y_sort_origin", 0),
|
|
288
292
|
properties=_optional_dict(layer_obj.get("properties"), f"{ctx}.properties")
|
|
289
293
|
or {},
|
|
290
294
|
)
|
|
@@ -409,6 +413,7 @@ def _expand_ongrid_to_layer(data_obj: JsonDict, ctx: str) -> List[ParsedLayer]:
|
|
|
409
413
|
locked=False,
|
|
410
414
|
opacity=1.0,
|
|
411
415
|
z_index=0,
|
|
416
|
+
y_sort=False, y_sort_origin=0,
|
|
412
417
|
properties={},
|
|
413
418
|
)
|
|
414
419
|
for loc_str, tile_data in raw_ongrid.items():
|
|
@@ -56,6 +56,7 @@ class MapObject:
|
|
|
56
56
|
"collision_shapes",
|
|
57
57
|
"collision_layer",
|
|
58
58
|
"collision_mask",
|
|
59
|
+
"y_sort_origin",
|
|
59
60
|
)
|
|
60
61
|
|
|
61
62
|
def __init__(
|
|
@@ -68,6 +69,7 @@ class MapObject:
|
|
|
68
69
|
collision_shapes: Optional[List[CollisionPolygon]] = None,
|
|
69
70
|
collision_layer: int = 1,
|
|
70
71
|
collision_mask: int = 0xFFFFFFFF,
|
|
72
|
+
y_sort_origin: Optional[int] = None,
|
|
71
73
|
) -> None:
|
|
72
74
|
self.x = x
|
|
73
75
|
self.y = y
|
|
@@ -76,6 +78,7 @@ class MapObject:
|
|
|
76
78
|
self.collision_shape = self.collision_shapes[0]
|
|
77
79
|
self.collision_layer = collision_layer
|
|
78
80
|
self.collision_mask = collision_mask
|
|
81
|
+
self.y_sort_origin = y_sort_origin
|
|
79
82
|
|
|
80
83
|
|
|
81
84
|
def _resolve_object_collision_filename(tileset_path: Union[str, Path]) -> str:
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from typing import Protocol
|
|
3
|
+
from typing import Protocol
|
|
4
|
+
|
|
5
|
+
import pygame
|
|
6
|
+
from pygame import Surface
|
|
4
7
|
|
|
5
8
|
from ..parser.collision import (
|
|
6
9
|
CapsuleShape,
|
|
@@ -10,6 +13,12 @@ from ..parser.collision import (
|
|
|
10
13
|
)
|
|
11
14
|
|
|
12
15
|
|
|
16
|
+
class ExtraObject(Protocol):
|
|
17
|
+
surface: Surface | None
|
|
18
|
+
x: float
|
|
19
|
+
y: float
|
|
20
|
+
|
|
21
|
+
|
|
13
22
|
class ICollidable(Protocol):
|
|
14
23
|
"""Base protocol for anything with a world position and collision shape.
|
|
15
24
|
|
|
@@ -25,4 +34,4 @@ class ICollidable(Protocol):
|
|
|
25
34
|
|
|
26
35
|
x: float
|
|
27
36
|
y: float
|
|
28
|
-
collision_shape:
|
|
37
|
+
collision_shape: RectangleShape | CircleShape | CapsuleShape | CollisionPolygon
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from collections.abc import Sequence
|
|
3
4
|
from dataclasses import dataclass
|
|
4
|
-
from typing import Dict, List, Optional, Tuple, Union
|
|
5
5
|
|
|
6
6
|
import pygame
|
|
7
7
|
from pygame import Rect, Surface, transform
|
|
8
8
|
|
|
9
9
|
from .map_loader import TilemapData
|
|
10
|
+
from .protocols import ExtraObject
|
|
10
11
|
|
|
11
12
|
CHUNK_SIZE = 32
|
|
12
13
|
|
|
@@ -19,19 +20,15 @@ class LayerRenderStats:
|
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
class TileLayerRenderer:
|
|
22
|
-
def __init__(
|
|
23
|
-
self, data: TilemapData, *, include_hidden_layers: bool = False
|
|
24
|
-
) -> None:
|
|
23
|
+
def __init__(self, data: TilemapData, *, include_hidden_layers: bool = False) -> None:
|
|
25
24
|
self.data = data
|
|
26
|
-
self.tile_layers = data.get_tile_layers_dict(
|
|
27
|
-
include_hidden=include_hidden_layers
|
|
28
|
-
)
|
|
25
|
+
self.tile_layers = data.get_tile_layers_dict(include_hidden=include_hidden_layers)
|
|
29
26
|
self._sorted_layer_ids = sorted(
|
|
30
27
|
self.tile_layers.keys(),
|
|
31
28
|
key=lambda lid: (self.tile_layers[lid].z_index, lid),
|
|
32
29
|
)
|
|
33
30
|
|
|
34
|
-
self._variant_cache:
|
|
31
|
+
self._variant_cache: dict[tuple[int, int], Surface | None] = {}
|
|
35
32
|
|
|
36
33
|
self._tile_w, self._tile_h = data.tile_size
|
|
37
34
|
self._rs = data.render_scale
|
|
@@ -45,7 +42,7 @@ class TileLayerRenderer:
|
|
|
45
42
|
f"got tile_size=({self._tile_w}, {self._tile_h}) render_scale={self._rs}"
|
|
46
43
|
)
|
|
47
44
|
|
|
48
|
-
self._tileset_animations:
|
|
45
|
+
self._tileset_animations: dict[int, dict] = {}
|
|
49
46
|
for ts_idx, ts in enumerate(data.parsed.tilesets):
|
|
50
47
|
if ts.animation is not None:
|
|
51
48
|
self._tileset_animations[ts_idx] = {
|
|
@@ -56,9 +53,9 @@ class TileLayerRenderer:
|
|
|
56
53
|
"animation_mode": ts.animation.animation_mode,
|
|
57
54
|
}
|
|
58
55
|
|
|
59
|
-
self._layer_chunks:
|
|
56
|
+
self._layer_chunks: dict[int, dict[tuple[int, int], list[tuple[int, int]]]] = {}
|
|
60
57
|
for layer_id, layer in self.tile_layers.items():
|
|
61
|
-
chunks:
|
|
58
|
+
chunks: dict[tuple[int, int], list[tuple[int, int]]] = {}
|
|
62
59
|
for (x, y), tile in layer.tiles.items():
|
|
63
60
|
if not isinstance(tile.ttype, int):
|
|
64
61
|
continue
|
|
@@ -69,10 +66,10 @@ class TileLayerRenderer:
|
|
|
69
66
|
chunks[chunk_key].append((x, y))
|
|
70
67
|
self._layer_chunks[layer_id] = chunks
|
|
71
68
|
|
|
72
|
-
def get_layer_dict(self) ->
|
|
69
|
+
def get_layer_dict(self) -> dict[int, object]:
|
|
73
70
|
return dict(self.tile_layers)
|
|
74
71
|
|
|
75
|
-
def _get_cached_variant(self, ttype: int, variant: int) ->
|
|
72
|
+
def _get_cached_variant(self, ttype: int, variant: int) -> Surface | None:
|
|
76
73
|
key = (ttype, variant)
|
|
77
74
|
if key not in self._variant_cache:
|
|
78
75
|
cell = self.data.get_tile_surface(ttype, variant, copy_surface=True)
|
|
@@ -121,17 +118,21 @@ class TileLayerRenderer:
|
|
|
121
118
|
def render(
|
|
122
119
|
self,
|
|
123
120
|
target: Surface,
|
|
124
|
-
camera_xy:
|
|
125
|
-
viewport_size:
|
|
121
|
+
camera_xy: tuple[float, float] | tuple[int, int] = (0, 0),
|
|
122
|
+
viewport_size: tuple[int, int] | None = None,
|
|
126
123
|
*,
|
|
127
|
-
|
|
124
|
+
extra_objects: Sequence[ExtraObject] | None = None,
|
|
125
|
+
current_time_ms: float | None = None,
|
|
128
126
|
) -> LayerRenderStats:
|
|
129
|
-
"""Render visible tile layers.
|
|
127
|
+
"""Render visible tile layers, optionally merged with extra objects.
|
|
128
|
+
|
|
129
|
+
When *extra_objects* is provided, each item must have ``surface``,
|
|
130
|
+
``x``, and ``y`` attributes (duck-typed). Extras are blitted after
|
|
131
|
+
all tile layers, preserving caller order.
|
|
130
132
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
the cost of a full scan.
|
|
133
|
+
Tiles are blitted per layer in natural chunk order. When
|
|
134
|
+
``layer.y_sort`` is enabled, tiles within each chunk are sorted by
|
|
135
|
+
their tile-space Y coordinate before blitting.
|
|
135
136
|
"""
|
|
136
137
|
cam_x, cam_y = float(camera_xy[0]), float(camera_xy[1])
|
|
137
138
|
if viewport_size is None:
|
|
@@ -171,11 +172,14 @@ class TileLayerRenderer:
|
|
|
171
172
|
if not chunk:
|
|
172
173
|
continue
|
|
173
174
|
|
|
174
|
-
|
|
175
|
+
tile_iter = chunk
|
|
176
|
+
if layer.y_sort:
|
|
177
|
+
origin = layer.y_sort_origin
|
|
178
|
+
tile_iter = sorted(chunk, key=lambda p: p[1] * self._eff_h + origin)
|
|
179
|
+
|
|
180
|
+
for x, y in tile_iter:
|
|
175
181
|
tile = layer.tiles[(x, y)]
|
|
176
|
-
display_variant = self._compute_display_variant(
|
|
177
|
-
tile.variant, tile.ttype, x, y, time_ms
|
|
178
|
-
)
|
|
182
|
+
display_variant = self._compute_display_variant(tile.variant, tile.ttype, x, y, time_ms)
|
|
179
183
|
cell = self._get_cached_variant(tile.ttype, display_variant)
|
|
180
184
|
if cell is None:
|
|
181
185
|
skipped += 1
|
|
@@ -187,6 +191,15 @@ class TileLayerRenderer:
|
|
|
187
191
|
)
|
|
188
192
|
drawn += 1
|
|
189
193
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
194
|
+
if extra_objects:
|
|
195
|
+
for obj in extra_objects:
|
|
196
|
+
surf = obj.surface
|
|
197
|
+
if surf is None:
|
|
198
|
+
continue
|
|
199
|
+
target.blit(surf, (obj.x - cam_x, obj.y - cam_y))
|
|
200
|
+
|
|
201
|
+
return LayerRenderStats(drawn_tiles=drawn, skipped_tiles=skipped, visible_layers=visible_layers)
|
|
202
|
+
|
|
203
|
+
@property
|
|
204
|
+
def sorted_layer_ids(self) -> list[int]:
|
|
205
|
+
return list(self._sorted_layer_ids)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tilemap-parser
|
|
3
|
-
Version: 4.2.
|
|
3
|
+
Version: 4.2.4
|
|
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
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for TileLayerRenderer y-sort and extra_objects.
|
|
3
|
+
|
|
4
|
+
Covers:
|
|
5
|
+
- y_sort read from parsed layer data
|
|
6
|
+
- y-major iteration when layer.y_sort=True
|
|
7
|
+
- extra_objects merged and sorted by (z_index, y)
|
|
8
|
+
- Duck-typed extra objects (surface, x, y only)
|
|
9
|
+
- Smoke test: render with extra_objects does not crash, stats correct
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
|
|
16
|
+
|
|
17
|
+
import pygame
|
|
18
|
+
import pytest
|
|
19
|
+
from pygame import Surface
|
|
20
|
+
|
|
21
|
+
from tilemap_parser.parser.map_parse import (
|
|
22
|
+
ParsedLayer,
|
|
23
|
+
ParsedMeta,
|
|
24
|
+
ParsedMap,
|
|
25
|
+
_parse_layer,
|
|
26
|
+
)
|
|
27
|
+
from tilemap_parser.runtime.map_loader import TilemapData
|
|
28
|
+
from tilemap_parser.runtime.renderer import TileLayerRenderer
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
# ParsedLayer y_sort field
|
|
33
|
+
# ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class TestParsedLayerYSort:
|
|
37
|
+
def test_default_is_false(self):
|
|
38
|
+
"""y_sort defaults to False when not present in data."""
|
|
39
|
+
d = {
|
|
40
|
+
"name": "test",
|
|
41
|
+
"type": "tile",
|
|
42
|
+
"visible": True,
|
|
43
|
+
"locked": False,
|
|
44
|
+
"opacity": 1.0,
|
|
45
|
+
"z_index": 0,
|
|
46
|
+
}
|
|
47
|
+
layer = _parse_layer(d, 0, "test")
|
|
48
|
+
assert layer.y_sort is False
|
|
49
|
+
|
|
50
|
+
def test_reads_true(self):
|
|
51
|
+
d = {
|
|
52
|
+
"name": "test",
|
|
53
|
+
"type": "tile",
|
|
54
|
+
"visible": True,
|
|
55
|
+
"locked": False,
|
|
56
|
+
"opacity": 1.0,
|
|
57
|
+
"z_index": 0,
|
|
58
|
+
"y_sort": True,
|
|
59
|
+
}
|
|
60
|
+
layer = _parse_layer(d, 0, "test")
|
|
61
|
+
assert layer.y_sort is True
|
|
62
|
+
|
|
63
|
+
def test_reads_false_explicit(self):
|
|
64
|
+
d = {
|
|
65
|
+
"name": "test",
|
|
66
|
+
"type": "tile",
|
|
67
|
+
"visible": True,
|
|
68
|
+
"locked": False,
|
|
69
|
+
"opacity": 1.0,
|
|
70
|
+
"z_index": 0,
|
|
71
|
+
"y_sort": False,
|
|
72
|
+
}
|
|
73
|
+
layer = _parse_layer(d, 0, "test")
|
|
74
|
+
assert layer.y_sort is False
|
|
75
|
+
|
|
76
|
+
def test_y_sort_origin_defaults_to_zero(self):
|
|
77
|
+
d = {"name": "test", "type": "tile", "visible": True, "locked": False, "opacity": 1.0, "z_index": 0}
|
|
78
|
+
layer = _parse_layer(d, 0, "test")
|
|
79
|
+
assert layer.y_sort_origin == 0
|
|
80
|
+
|
|
81
|
+
def test_y_sort_origin_reads_from_json(self):
|
|
82
|
+
d = {"name": "test", "type": "tile", "visible": True, "locked": False, "opacity": 1.0, "z_index": 0, "y_sort_origin": 16}
|
|
83
|
+
layer = _parse_layer(d, 0, "test")
|
|
84
|
+
assert layer.y_sort_origin == 16
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# ---------------------------------------------------------------------------
|
|
88
|
+
# Helpers — minimal map for renderer tests
|
|
89
|
+
# ---------------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _dummy_tileset_surface(w=16, h=16) -> Surface:
|
|
93
|
+
"""Create a solid-colour surface to serve as a tileset cell."""
|
|
94
|
+
surf = Surface((w, h))
|
|
95
|
+
surf.fill((200, 100, 50))
|
|
96
|
+
return surf
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class DummyVisual:
|
|
100
|
+
"""Minimal duck-typed visual — surface, x, y, optional z_index."""
|
|
101
|
+
|
|
102
|
+
__slots__ = ("surface", "x", "y", "z_index")
|
|
103
|
+
|
|
104
|
+
def __init__(self, surface, x, y, z_index=0):
|
|
105
|
+
self.surface = surface
|
|
106
|
+
self.x = x
|
|
107
|
+
self.y = y
|
|
108
|
+
self.z_index = z_index
|
|
109
|
+
|
|
110
|
+
def __repr__(self):
|
|
111
|
+
return f"DummyVisual(x={self.x}, y={self.y}, z={self.z_index})"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _make_tile(pos, ttype=0, variant=0):
|
|
115
|
+
from tilemap_parser.parser.map_parse import ParsedTile
|
|
116
|
+
|
|
117
|
+
return ParsedTile(pos=pos, ttype=ttype, variant=variant, gid=None, properties=None)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _make_map_data(layers, tile_size=(16, 16)) -> TilemapData:
|
|
121
|
+
"""Build a TilemapData from a list of ParsedLayer objects."""
|
|
122
|
+
from pathlib import Path
|
|
123
|
+
from tilemap_parser.parser.map_parse import ParsedTileset
|
|
124
|
+
|
|
125
|
+
meta = ParsedMeta(
|
|
126
|
+
tile_size=tile_size,
|
|
127
|
+
map_size=(10, 10),
|
|
128
|
+
initial_map_size=(10, 10),
|
|
129
|
+
zoom_level=1.0,
|
|
130
|
+
scroll=(0, 0),
|
|
131
|
+
version="1.1",
|
|
132
|
+
render_scale=1.0,
|
|
133
|
+
)
|
|
134
|
+
mock_map = object.__new__(ParsedMap)
|
|
135
|
+
mock_map.meta = meta
|
|
136
|
+
mock_map.layers = layers
|
|
137
|
+
mock_map.tilesets = [ParsedTileset(path="dummy.png", type="tile")]
|
|
138
|
+
tw, th = tile_size
|
|
139
|
+
resolved_paths = [Path("dummy.png")]
|
|
140
|
+
return TilemapData(mock_map, [_dummy_tileset_surface(tw, th)], resolved_paths, [])
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
# ---------------------------------------------------------------------------
|
|
144
|
+
# TileLayerRenderer — extra_objects
|
|
145
|
+
# ---------------------------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class TestExtraObjects:
|
|
149
|
+
def make_map_data(self) -> TilemapData:
|
|
150
|
+
"""Minimal 1-layer map with a single tile at (0,0)."""
|
|
151
|
+
layer = ParsedLayer(
|
|
152
|
+
id=0,
|
|
153
|
+
name="Ground",
|
|
154
|
+
layer_type="tile",
|
|
155
|
+
visible=True,
|
|
156
|
+
locked=False,
|
|
157
|
+
opacity=1.0,
|
|
158
|
+
z_index=0,
|
|
159
|
+
)
|
|
160
|
+
layer.tiles[(0, 0)] = _make_tile((0, 0))
|
|
161
|
+
return _make_map_data([layer])
|
|
162
|
+
|
|
163
|
+
def test_extra_objects_smoke(self):
|
|
164
|
+
"""Render with extra_objects does not crash."""
|
|
165
|
+
data = self.make_map_data()
|
|
166
|
+
renderer = TileLayerRenderer(data)
|
|
167
|
+
target = Surface((100, 100))
|
|
168
|
+
objs = [DummyVisual(_dummy_tileset_surface(), 10, 20)]
|
|
169
|
+
stats = renderer.render(target, (0, 0), extra_objects=objs)
|
|
170
|
+
assert stats.drawn_tiles == 1
|
|
171
|
+
assert stats.visible_layers == 1
|
|
172
|
+
|
|
173
|
+
def test_extra_objects_merged_into_drawn_count(self):
|
|
174
|
+
"""drawn_tiles only counts tiles, not extras (unchanged stat)."""
|
|
175
|
+
data = self.make_map_data()
|
|
176
|
+
renderer = TileLayerRenderer(data)
|
|
177
|
+
target = Surface((100, 100))
|
|
178
|
+
objs = [DummyVisual(_dummy_tileset_surface(), 10, 20)]
|
|
179
|
+
stats = renderer.render(target, (0, 0), extra_objects=objs)
|
|
180
|
+
assert stats.drawn_tiles == 1 # extras not counted here
|
|
181
|
+
|
|
182
|
+
def test_multiple_extra_objects(self):
|
|
183
|
+
"""Multiple extra objects all blit without error."""
|
|
184
|
+
data = self.make_map_data()
|
|
185
|
+
renderer = TileLayerRenderer(data)
|
|
186
|
+
target = Surface((200, 200))
|
|
187
|
+
objs = [
|
|
188
|
+
DummyVisual(_dummy_tileset_surface(), 10, 20, z_index=0),
|
|
189
|
+
DummyVisual(_dummy_tileset_surface(), 30, 40, z_index=1),
|
|
190
|
+
DummyVisual(_dummy_tileset_surface(), 50, 60, z_index=0),
|
|
191
|
+
]
|
|
192
|
+
renderer.render(target, (0, 0), extra_objects=objs)
|
|
193
|
+
# no crash is the assertion
|
|
194
|
+
|
|
195
|
+
def test_duck_typed_minimal(self):
|
|
196
|
+
"""Object with only surface/x/y works (no z_index)."""
|
|
197
|
+
data = self.make_map_data()
|
|
198
|
+
renderer = TileLayerRenderer(data)
|
|
199
|
+
target = Surface((100, 100))
|
|
200
|
+
|
|
201
|
+
class MinimalVisual:
|
|
202
|
+
def __init__(self):
|
|
203
|
+
self.surface = _dummy_tileset_surface()
|
|
204
|
+
self.x = 5
|
|
205
|
+
self.y = 15
|
|
206
|
+
|
|
207
|
+
renderer.render(target, (0, 0), extra_objects=[MinimalVisual()])
|
|
208
|
+
|
|
209
|
+
def test_extra_object_with_none_surface_skipped(self):
|
|
210
|
+
"""Objects with surface=None are silently skipped."""
|
|
211
|
+
data = self.make_map_data()
|
|
212
|
+
renderer = TileLayerRenderer(data)
|
|
213
|
+
target = Surface((100, 100))
|
|
214
|
+
|
|
215
|
+
class NoneSurfaceVisual:
|
|
216
|
+
def __init__(self):
|
|
217
|
+
self.surface = None
|
|
218
|
+
self.x = 0
|
|
219
|
+
self.y = 0
|
|
220
|
+
|
|
221
|
+
renderer.render(target, (0, 0), extra_objects=[NoneSurfaceVisual()])
|
|
222
|
+
|
|
223
|
+
def test_render_without_extra_objects_still_works(self):
|
|
224
|
+
"""Calling render() without extra_objects is unchanged."""
|
|
225
|
+
data = self.make_map_data()
|
|
226
|
+
renderer = TileLayerRenderer(data)
|
|
227
|
+
target = Surface((100, 100))
|
|
228
|
+
stats = renderer.render(target, (0, 0))
|
|
229
|
+
assert stats.drawn_tiles == 1
|
|
230
|
+
assert stats.visible_layers == 1
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
# ---------------------------------------------------------------------------
|
|
234
|
+
# TileLayerRenderer — y_sort iteration
|
|
235
|
+
# ---------------------------------------------------------------------------
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
class _SpySurface(pygame.Surface):
|
|
239
|
+
"""Records (dest_x, dest_y) for each blit call."""
|
|
240
|
+
|
|
241
|
+
def __init__(self, size):
|
|
242
|
+
super().__init__(size)
|
|
243
|
+
self.blit_calls: list[tuple[float, float]] = []
|
|
244
|
+
|
|
245
|
+
def blit(self, source, dest, *args, **kwargs):
|
|
246
|
+
if isinstance(dest, tuple):
|
|
247
|
+
self.blit_calls.append((float(dest[0]), float(dest[1])))
|
|
248
|
+
else:
|
|
249
|
+
self.blit_calls.append((float(dest.x), float(dest.y)))
|
|
250
|
+
return super().blit(source, dest, *args, **kwargs)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
class TestLayerYSort:
|
|
254
|
+
"""Verify that y_sort changes iteration order within a chunk."""
|
|
255
|
+
|
|
256
|
+
def make_map_data_two_tiles(self, y_sort: bool = True) -> TilemapData:
|
|
257
|
+
"""One layer with two tiles at different Y positions."""
|
|
258
|
+
layer = ParsedLayer(
|
|
259
|
+
id=0,
|
|
260
|
+
name="Bushes",
|
|
261
|
+
layer_type="tile",
|
|
262
|
+
visible=True,
|
|
263
|
+
locked=False,
|
|
264
|
+
opacity=1.0,
|
|
265
|
+
z_index=0,
|
|
266
|
+
y_sort=y_sort,
|
|
267
|
+
)
|
|
268
|
+
layer.tiles[(0, 0)] = _make_tile((0, 0))
|
|
269
|
+
layer.tiles[(0, 5)] = _make_tile((0, 5))
|
|
270
|
+
return _make_map_data([layer])
|
|
271
|
+
|
|
272
|
+
def test_y_sort_layer_does_not_crash(self):
|
|
273
|
+
"""Layer with y_sort=True renders without error."""
|
|
274
|
+
data = self.make_map_data_two_tiles()
|
|
275
|
+
renderer = TileLayerRenderer(data)
|
|
276
|
+
target = Surface((200, 200))
|
|
277
|
+
stats = renderer.render(target, (0, 0))
|
|
278
|
+
assert stats.drawn_tiles == 2
|
|
279
|
+
|
|
280
|
+
def test_y_major_order(self):
|
|
281
|
+
"""y-sort within a chunk produces y-major iteration."""
|
|
282
|
+
data = self.make_map_data_two_tiles(y_sort=True)
|
|
283
|
+
renderer = TileLayerRenderer(data)
|
|
284
|
+
target = _SpySurface((200, 200))
|
|
285
|
+
renderer.render(target, (0, 0))
|
|
286
|
+
ys = [y for _, y in target.blit_calls]
|
|
287
|
+
assert ys == sorted(ys)
|
|
288
|
+
|
|
289
|
+
def test_no_ysort_preserves_input_order(self):
|
|
290
|
+
"""Without y-sort the chunk iteration is insertion order."""
|
|
291
|
+
data = self.make_map_data_two_tiles(y_sort=False)
|
|
292
|
+
renderer = TileLayerRenderer(data)
|
|
293
|
+
target = _SpySurface((200, 200))
|
|
294
|
+
renderer.render(target, (0, 0))
|
|
295
|
+
ef = 16
|
|
296
|
+
assert target.blit_calls == [(0.0, 0.0), (0.0, 5.0 * ef)]
|
|
297
|
+
|
|
298
|
+
def test_y_sort_origin_shifts_sort_order(self):
|
|
299
|
+
"""y_sort_origin offsets the Y used for sorting."""
|
|
300
|
+
layer = ParsedLayer(
|
|
301
|
+
id=0, name="test", layer_type="tile", visible=True,
|
|
302
|
+
locked=False, opacity=1.0, z_index=0,
|
|
303
|
+
y_sort=True, y_sort_origin=32,
|
|
304
|
+
)
|
|
305
|
+
layer.tiles[(0, 0)] = _make_tile((0, 0))
|
|
306
|
+
layer.tiles[(0, 1)] = _make_tile((0, 1))
|
|
307
|
+
data = _make_map_data([layer])
|
|
308
|
+
renderer = TileLayerRenderer(data)
|
|
309
|
+
target = _SpySurface((200, 200))
|
|
310
|
+
renderer.render(target, (0, 0))
|
|
311
|
+
ef = 16
|
|
312
|
+
assert target.blit_calls == [(0.0, 0.0), (0.0, 1.0 * ef)]
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
# ---------------------------------------------------------------------------
|
|
316
|
+
# Integration — combined y_sort + extra_objects
|
|
317
|
+
# ---------------------------------------------------------------------------
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
class TestIntegration:
|
|
321
|
+
def test_y_sort_with_extra_objects(self):
|
|
322
|
+
"""y_sort layer renders tiles first, then extras in caller order."""
|
|
323
|
+
layer = ParsedLayer(
|
|
324
|
+
id=0,
|
|
325
|
+
name="Bushes",
|
|
326
|
+
layer_type="tile",
|
|
327
|
+
visible=True,
|
|
328
|
+
locked=False,
|
|
329
|
+
opacity=1.0,
|
|
330
|
+
z_index=0,
|
|
331
|
+
y_sort=True,
|
|
332
|
+
)
|
|
333
|
+
layer.tiles[(0, 0)] = _make_tile((0, 0))
|
|
334
|
+
layer.tiles[(0, 5)] = _make_tile((0, 5))
|
|
335
|
+
data = _make_map_data([layer])
|
|
336
|
+
renderer = TileLayerRenderer(data)
|
|
337
|
+
target = _SpySurface((200, 200))
|
|
338
|
+
ef = 16
|
|
339
|
+
objs = [
|
|
340
|
+
DummyVisual(_dummy_tileset_surface(), 5, 25),
|
|
341
|
+
DummyVisual(_dummy_tileset_surface(), 5, 3),
|
|
342
|
+
]
|
|
343
|
+
renderer.render(target, (0, 0), extra_objects=objs)
|
|
344
|
+
assert len(target.blit_calls) == 4
|
|
345
|
+
assert target.blit_calls[0] == (0.0, 0.0) # tile y=0
|
|
346
|
+
assert target.blit_calls[1] == (0.0, 5.0 * ef) # tile y=5
|
|
347
|
+
assert target.blit_calls[2] == (5.0, 25.0) # extra in caller order
|
|
348
|
+
assert target.blit_calls[3] == (5.0, 3.0) # extra in caller order
|
|
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
|
{tilemap_parser-4.2.3 → tilemap_parser-4.2.4}/src/tilemap_parser/runtime/animation_player.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tilemap_parser-4.2.3 → tilemap_parser-4.2.4}/src/tilemap_parser/runtime/object_collision.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tilemap_parser-4.2.3 → tilemap_parser-4.2.4}/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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|