tilemap-parser 5.0.3__tar.gz → 5.1.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.0.3 → tilemap_parser-5.1.0}/PKG-INFO +1 -1
- {tilemap_parser-5.0.3 → tilemap_parser-5.1.0}/pyproject.toml +1 -1
- {tilemap_parser-5.0.3 → tilemap_parser-5.1.0}/src/tilemap_parser/parser/collision.py +53 -3
- {tilemap_parser-5.0.3 → tilemap_parser-5.1.0}/src/tilemap_parser/parser/collision_loader.py +4 -1
- {tilemap_parser-5.0.3 → tilemap_parser-5.1.0}/src/tilemap_parser/runtime/animation_player.py +49 -8
- {tilemap_parser-5.0.3 → tilemap_parser-5.1.0}/src/tilemap_parser/runtime/collision_cache.py +12 -6
- {tilemap_parser-5.0.3 → tilemap_parser-5.1.0}/src/tilemap_parser/runtime/movement/platformer.py +2 -3
- {tilemap_parser-5.0.3 → tilemap_parser-5.1.0}/src/tilemap_parser/runtime/movement/queries.py +24 -13
- {tilemap_parser-5.0.3 → tilemap_parser-5.1.0}/src/tilemap_parser/runtime/movement/runner.py +10 -5
- {tilemap_parser-5.0.3 → tilemap_parser-5.1.0}/src/tilemap_parser/runtime/navigation/nav_grid.py +45 -19
- {tilemap_parser-5.0.3 → tilemap_parser-5.1.0}/src/tilemap_parser/runtime/particles.py +18 -0
- {tilemap_parser-5.0.3 → tilemap_parser-5.1.0}/src/tilemap_parser/runtime/world.py +70 -5
- {tilemap_parser-5.0.3 → tilemap_parser-5.1.0}/src/tilemap_parser.egg-info/PKG-INFO +1 -1
- {tilemap_parser-5.0.3 → tilemap_parser-5.1.0}/src/tilemap_parser.egg-info/SOURCES.txt +1 -0
- {tilemap_parser-5.0.3 → tilemap_parser-5.1.0}/tests/test_collision.py +121 -1
- tilemap_parser-5.1.0/tests/test_gid_routing.py +293 -0
- {tilemap_parser-5.0.3 → tilemap_parser-5.1.0}/tests/test_integration_animation.py +125 -0
- {tilemap_parser-5.0.3 → tilemap_parser-5.1.0}/LICENSE +0 -0
- {tilemap_parser-5.0.3 → tilemap_parser-5.1.0}/README.md +0 -0
- {tilemap_parser-5.0.3 → tilemap_parser-5.1.0}/pypi-readme.md +0 -0
- {tilemap_parser-5.0.3 → tilemap_parser-5.1.0}/setup.cfg +0 -0
- {tilemap_parser-5.0.3 → tilemap_parser-5.1.0}/src/tilemap_parser/__init__.py +0 -0
- {tilemap_parser-5.0.3 → tilemap_parser-5.1.0}/src/tilemap_parser/parser/__init__.py +0 -0
- {tilemap_parser-5.0.3 → tilemap_parser-5.1.0}/src/tilemap_parser/parser/animation.py +0 -0
- {tilemap_parser-5.0.3 → tilemap_parser-5.1.0}/src/tilemap_parser/parser/map_parse.py +0 -0
- {tilemap_parser-5.0.3 → tilemap_parser-5.1.0}/src/tilemap_parser/parser/node_parse.py +0 -0
- {tilemap_parser-5.0.3 → tilemap_parser-5.1.0}/src/tilemap_parser/parser/particle.py +0 -0
- {tilemap_parser-5.0.3 → tilemap_parser-5.1.0}/src/tilemap_parser/parser/tmx_converter.py +0 -0
- {tilemap_parser-5.0.3 → tilemap_parser-5.1.0}/src/tilemap_parser/runtime/__init__.py +0 -0
- {tilemap_parser-5.0.3 → tilemap_parser-5.1.0}/src/tilemap_parser/runtime/area_node.py +0 -0
- {tilemap_parser-5.0.3 → tilemap_parser-5.1.0}/src/tilemap_parser/runtime/body.py +0 -0
- {tilemap_parser-5.0.3 → tilemap_parser-5.1.0}/src/tilemap_parser/runtime/camera.py +0 -0
- {tilemap_parser-5.0.3 → tilemap_parser-5.1.0}/src/tilemap_parser/runtime/collision/__init__.py +0 -0
- {tilemap_parser-5.0.3 → tilemap_parser-5.1.0}/src/tilemap_parser/runtime/collision/hit.py +0 -0
- {tilemap_parser-5.0.3 → tilemap_parser-5.1.0}/src/tilemap_parser/runtime/collision/manager.py +0 -0
- {tilemap_parser-5.0.3 → tilemap_parser-5.1.0}/src/tilemap_parser/runtime/collision/shapes.py +0 -0
- {tilemap_parser-5.0.3 → tilemap_parser-5.1.0}/src/tilemap_parser/runtime/map_loader.py +0 -0
- {tilemap_parser-5.0.3 → tilemap_parser-5.1.0}/src/tilemap_parser/runtime/map_object.py +0 -0
- {tilemap_parser-5.0.3 → tilemap_parser-5.1.0}/src/tilemap_parser/runtime/movement/__init__.py +0 -0
- {tilemap_parser-5.0.3 → tilemap_parser-5.1.0}/src/tilemap_parser/runtime/movement/grounded.py +0 -0
- {tilemap_parser-5.0.3 → tilemap_parser-5.1.0}/src/tilemap_parser/runtime/movement/rpg.py +0 -0
- {tilemap_parser-5.0.3 → tilemap_parser-5.1.0}/src/tilemap_parser/runtime/movement/slide.py +0 -0
- {tilemap_parser-5.0.3 → tilemap_parser-5.1.0}/src/tilemap_parser/runtime/movement/types.py +0 -0
- {tilemap_parser-5.0.3 → tilemap_parser-5.1.0}/src/tilemap_parser/runtime/navigation/__init__.py +0 -0
- {tilemap_parser-5.0.3 → tilemap_parser-5.1.0}/src/tilemap_parser/runtime/navigation/path_follower.py +0 -0
- {tilemap_parser-5.0.3 → tilemap_parser-5.1.0}/src/tilemap_parser/runtime/navigation/pathfinder.py +0 -0
- {tilemap_parser-5.0.3 → tilemap_parser-5.1.0}/src/tilemap_parser/runtime/object_collision.py +0 -0
- {tilemap_parser-5.0.3 → tilemap_parser-5.1.0}/src/tilemap_parser/runtime/polygon_query.py +0 -0
- {tilemap_parser-5.0.3 → tilemap_parser-5.1.0}/src/tilemap_parser/runtime/protocols.py +0 -0
- {tilemap_parser-5.0.3 → tilemap_parser-5.1.0}/src/tilemap_parser/runtime/renderer.py +0 -0
- {tilemap_parser-5.0.3 → tilemap_parser-5.1.0}/src/tilemap_parser/runtime/tile_collision.py +0 -0
- {tilemap_parser-5.0.3 → tilemap_parser-5.1.0}/src/tilemap_parser/utils/__init__.py +0 -0
- {tilemap_parser-5.0.3 → tilemap_parser-5.1.0}/src/tilemap_parser/utils/geometry.py +0 -0
- {tilemap_parser-5.0.3 → tilemap_parser-5.1.0}/src/tilemap_parser.egg-info/dependency_links.txt +0 -0
- {tilemap_parser-5.0.3 → tilemap_parser-5.1.0}/src/tilemap_parser.egg-info/requires.txt +0 -0
- {tilemap_parser-5.0.3 → tilemap_parser-5.1.0}/src/tilemap_parser.egg-info/top_level.txt +0 -0
- {tilemap_parser-5.0.3 → tilemap_parser-5.1.0}/tests/test_body.py +0 -0
- {tilemap_parser-5.0.3 → tilemap_parser-5.1.0}/tests/test_camera.py +0 -0
- {tilemap_parser-5.0.3 → tilemap_parser-5.1.0}/tests/test_geometry.py +0 -0
- {tilemap_parser-5.0.3 → tilemap_parser-5.1.0}/tests/test_gid_collision.py +0 -0
- {tilemap_parser-5.0.3 → tilemap_parser-5.1.0}/tests/test_integration_collision.py +0 -0
- {tilemap_parser-5.0.3 → tilemap_parser-5.1.0}/tests/test_integration_map_loader.py +0 -0
- {tilemap_parser-5.0.3 → tilemap_parser-5.1.0}/tests/test_map_loader.py +0 -0
- {tilemap_parser-5.0.3 → tilemap_parser-5.1.0}/tests/test_map_object.py +0 -0
- {tilemap_parser-5.0.3 → tilemap_parser-5.1.0}/tests/test_move_grounded.py +0 -0
- {tilemap_parser-5.0.3 → tilemap_parser-5.1.0}/tests/test_navigation.py +0 -0
- {tilemap_parser-5.0.3 → tilemap_parser-5.1.0}/tests/test_object_collision.py +0 -0
- {tilemap_parser-5.0.3 → tilemap_parser-5.1.0}/tests/test_object_surfaces.py +0 -0
- {tilemap_parser-5.0.3 → tilemap_parser-5.1.0}/tests/test_particle_emitter.py +0 -0
- {tilemap_parser-5.0.3 → tilemap_parser-5.1.0}/tests/test_render_scale.py +0 -0
- {tilemap_parser-5.0.3 → tilemap_parser-5.1.0}/tests/test_tile_collision.py +0 -0
- {tilemap_parser-5.0.3 → tilemap_parser-5.1.0}/tests/test_tile_layer_renderer_y_sort.py +0 -0
- {tilemap_parser-5.0.3 → tilemap_parser-5.1.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.0
|
|
3
|
+
Version: 5.1.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.0
|
|
7
|
+
version = "5.1.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"
|
|
@@ -9,6 +9,7 @@ Provides parsing of collision data from JSON into data classes:
|
|
|
9
9
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
|
+
import math
|
|
12
13
|
from dataclasses import dataclass, field
|
|
13
14
|
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
14
15
|
|
|
@@ -34,6 +35,13 @@ class CollisionPolygon:
|
|
|
34
35
|
world_vertices = [(tile_x + vx * scale, tile_y + vy * scale) for vx, vy in self.vertices]
|
|
35
36
|
return CollisionPolygon(vertices=world_vertices, one_way=self.one_way)
|
|
36
37
|
|
|
38
|
+
def scaled(self, scale: float) -> "CollisionPolygon":
|
|
39
|
+
"""Return a copy with all vertices multiplied by *scale*."""
|
|
40
|
+
return CollisionPolygon(
|
|
41
|
+
vertices=[(vx * scale, vy * scale) for vx, vy in self.vertices],
|
|
42
|
+
one_way=self.one_way,
|
|
43
|
+
)
|
|
44
|
+
|
|
37
45
|
def is_valid(self) -> bool:
|
|
38
46
|
"""Check if polygon has at least 3 vertices"""
|
|
39
47
|
return len(self.vertices) >= 3
|
|
@@ -130,6 +138,14 @@ class RectangleShape:
|
|
|
130
138
|
top = y + self.offset[1]
|
|
131
139
|
return (left, top, left + self.width, top + self.height)
|
|
132
140
|
|
|
141
|
+
def scaled(self, scale: float) -> "RectangleShape":
|
|
142
|
+
"""Return a copy with dimensions and offset multiplied by *scale*."""
|
|
143
|
+
return RectangleShape(
|
|
144
|
+
width=self.width * scale,
|
|
145
|
+
height=self.height * scale,
|
|
146
|
+
offset=(self.offset[0] * scale, self.offset[1] * scale),
|
|
147
|
+
)
|
|
148
|
+
|
|
133
149
|
|
|
134
150
|
@dataclass
|
|
135
151
|
class CircleShape:
|
|
@@ -142,6 +158,13 @@ class CircleShape:
|
|
|
142
158
|
"""Get center position in world space"""
|
|
143
159
|
return (x + self.offset[0], y + self.offset[1])
|
|
144
160
|
|
|
161
|
+
def scaled(self, scale: float) -> "CircleShape":
|
|
162
|
+
"""Return a copy with radius and offset multiplied by *scale*."""
|
|
163
|
+
return CircleShape(
|
|
164
|
+
radius=self.radius * scale,
|
|
165
|
+
offset=(self.offset[0] * scale, self.offset[1] * scale),
|
|
166
|
+
)
|
|
167
|
+
|
|
145
168
|
|
|
146
169
|
@dataclass
|
|
147
170
|
class CapsuleShape:
|
|
@@ -159,6 +182,14 @@ class CapsuleShape:
|
|
|
159
182
|
"""Get bottom circle center in world space"""
|
|
160
183
|
return (x + self.offset[0], y + self.offset[1] + self.height)
|
|
161
184
|
|
|
185
|
+
def scaled(self, scale: float) -> "CapsuleShape":
|
|
186
|
+
"""Return a copy with radius, height, and offset multiplied by *scale*."""
|
|
187
|
+
return CapsuleShape(
|
|
188
|
+
radius=self.radius * scale,
|
|
189
|
+
height=self.height * scale,
|
|
190
|
+
offset=(self.offset[0] * scale, self.offset[1] * scale),
|
|
191
|
+
)
|
|
192
|
+
|
|
162
193
|
|
|
163
194
|
CharacterShapeType = Union[RectangleShape, CircleShape, CapsuleShape, CollisionPolygon]
|
|
164
195
|
|
|
@@ -173,6 +204,16 @@ class CharacterCollision:
|
|
|
173
204
|
collision_layer: int = 1
|
|
174
205
|
collision_mask: int = 0xFFFFFFFF
|
|
175
206
|
|
|
207
|
+
def scaled(self, scale: float) -> "CharacterCollision":
|
|
208
|
+
"""Return a copy whose shape is scaled by *scale* (typically ``render_scale``)."""
|
|
209
|
+
return CharacterCollision(
|
|
210
|
+
name=self.name,
|
|
211
|
+
shape=self.shape.scaled(scale),
|
|
212
|
+
properties=dict(self.properties),
|
|
213
|
+
collision_layer=self.collision_layer,
|
|
214
|
+
collision_mask=self.collision_mask,
|
|
215
|
+
)
|
|
216
|
+
|
|
176
217
|
|
|
177
218
|
@dataclass
|
|
178
219
|
class ObjectCollisionRegionData:
|
|
@@ -255,19 +296,28 @@ def parse_tileset_collision(data: JsonDict) -> TilesetCollision:
|
|
|
255
296
|
raise CollisionParseError(f"Invalid tileset collision data: {e}") from e
|
|
256
297
|
|
|
257
298
|
|
|
258
|
-
def parse_character_collision(data: JsonDict) -> CharacterCollision:
|
|
299
|
+
def parse_character_collision(data: JsonDict, render_scale: float = 1.0) -> CharacterCollision:
|
|
259
300
|
"""
|
|
260
301
|
Parse character collision data from dictionary.
|
|
261
302
|
|
|
262
303
|
Args:
|
|
263
304
|
data: Dictionary loaded from .collision.json file
|
|
305
|
+
render_scale: Multiplier applied to shape dimensions and offsets
|
|
306
|
+
(no-op when 1.0).
|
|
264
307
|
|
|
265
308
|
Returns:
|
|
266
309
|
CharacterCollision object
|
|
267
310
|
|
|
268
311
|
Raises:
|
|
269
|
-
CollisionParseError: If data format is invalid
|
|
312
|
+
CollisionParseError: If data format is invalid or render_scale is not
|
|
313
|
+
finite and greater than zero.
|
|
270
314
|
"""
|
|
315
|
+
if (
|
|
316
|
+
not isinstance(render_scale, (int, float))
|
|
317
|
+
or not math.isfinite(render_scale)
|
|
318
|
+
or render_scale <= 0
|
|
319
|
+
):
|
|
320
|
+
raise CollisionParseError(f"render_scale must be finite and > 0, got {render_scale!r}")
|
|
271
321
|
try:
|
|
272
322
|
name = data["name"]
|
|
273
323
|
shape_data = data["shape"]
|
|
@@ -312,7 +362,7 @@ def parse_character_collision(data: JsonDict) -> CharacterCollision:
|
|
|
312
362
|
properties=properties,
|
|
313
363
|
collision_layer=collision_layer,
|
|
314
364
|
collision_mask=collision_mask,
|
|
315
|
-
)
|
|
365
|
+
).scaled(render_scale)
|
|
316
366
|
except (KeyError, ValueError, TypeError) as e:
|
|
317
367
|
raise CollisionParseError(f"Invalid character collision data: {e}") from e
|
|
318
368
|
|
|
@@ -55,6 +55,7 @@ def load_tileset_collision(
|
|
|
55
55
|
|
|
56
56
|
def load_character_collision(
|
|
57
57
|
collision_path: Union[str, Path],
|
|
58
|
+
render_scale: float = 1.0,
|
|
58
59
|
) -> CharacterCollision | None:
|
|
59
60
|
"""
|
|
60
61
|
Load character collision data from a collision JSON file.
|
|
@@ -64,6 +65,8 @@ def load_character_collision(
|
|
|
64
65
|
|
|
65
66
|
Args:
|
|
66
67
|
collision_path: Direct path to the .collision.json file.
|
|
68
|
+
render_scale: Multiplier applied to shape dimensions and offsets
|
|
69
|
+
(no-op when 1.0).
|
|
67
70
|
|
|
68
71
|
Returns:
|
|
69
72
|
CharacterCollision object, or None if the file does not exist.
|
|
@@ -79,7 +82,7 @@ def load_character_collision(
|
|
|
79
82
|
try:
|
|
80
83
|
with open(collision_path, "r", encoding="utf-8") as f:
|
|
81
84
|
data = json.load(f)
|
|
82
|
-
return parse_character_collision(data)
|
|
85
|
+
return parse_character_collision(data, render_scale=render_scale)
|
|
83
86
|
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as e:
|
|
84
87
|
raise CollisionParseError(f"Cannot load {collision_path}: {e}") from e
|
|
85
88
|
|
{tilemap_parser-5.0.3 → tilemap_parser-5.1.0}/src/tilemap_parser/runtime/animation_player.py
RENAMED
|
@@ -20,6 +20,9 @@ class SpriteAnimationSet:
|
|
|
20
20
|
json_path: Optional[Path] = None
|
|
21
21
|
grid_offset_x: int = 0
|
|
22
22
|
grid_offset_y: int = 0
|
|
23
|
+
render_scale: float = 1.0
|
|
24
|
+
grid_columns: int = 0
|
|
25
|
+
grid_rows: int = 0
|
|
23
26
|
|
|
24
27
|
@classmethod
|
|
25
28
|
def load(
|
|
@@ -28,6 +31,7 @@ class SpriteAnimationSet:
|
|
|
28
31
|
*,
|
|
29
32
|
spritesheet_path: Optional[PathLike] = None,
|
|
30
33
|
extra_search_base: Optional[Path] = None,
|
|
34
|
+
render_scale: float = 1.0,
|
|
31
35
|
) -> "SpriteAnimationSet":
|
|
32
36
|
path = Path(json_path)
|
|
33
37
|
library = parse_animation_file(path)
|
|
@@ -59,6 +63,29 @@ class SpriteAnimationSet:
|
|
|
59
63
|
except pygame.error as e:
|
|
60
64
|
raise AnimationParseError(f"Failed to load image {image_path}: {e}") from e
|
|
61
65
|
|
|
66
|
+
tw, th = library.tile_size
|
|
67
|
+
ox, oy = library.grid_offset
|
|
68
|
+
grid_columns = max(1, (surface.get_width() - ox) // tw)
|
|
69
|
+
grid_rows = max(1, (surface.get_height() - oy) // th)
|
|
70
|
+
|
|
71
|
+
if render_scale != 1.0:
|
|
72
|
+
if render_scale <= 0:
|
|
73
|
+
raise ValueError(f"render_scale must be > 0, got {render_scale}")
|
|
74
|
+
w, h = surface.get_size()
|
|
75
|
+
sw, sh = int(w * render_scale), int(h * render_scale)
|
|
76
|
+
stw, sth = int(tw * render_scale), int(th * render_scale)
|
|
77
|
+
if sw < 1 or sh < 1 or stw < 1 or sth < 1:
|
|
78
|
+
raise ValueError(f"render_scale {render_scale} produces zero-sized atlas cells")
|
|
79
|
+
if (
|
|
80
|
+
int(ox * render_scale) + grid_columns * stw > sw
|
|
81
|
+
or int(oy * render_scale) + grid_rows * sth > sh
|
|
82
|
+
):
|
|
83
|
+
raise ValueError(
|
|
84
|
+
f"render_scale {render_scale} cannot maintain atlas grid cell bounds"
|
|
85
|
+
)
|
|
86
|
+
surface = pygame.transform.scale(surface, (sw, sh))
|
|
87
|
+
library = _scaled_library(library, render_scale)
|
|
88
|
+
|
|
62
89
|
return cls(
|
|
63
90
|
library=library,
|
|
64
91
|
surface=surface,
|
|
@@ -66,6 +93,9 @@ class SpriteAnimationSet:
|
|
|
66
93
|
json_path=path,
|
|
67
94
|
grid_offset_x=library.grid_offset[0],
|
|
68
95
|
grid_offset_y=library.grid_offset[1],
|
|
96
|
+
render_scale=render_scale,
|
|
97
|
+
grid_columns=grid_columns,
|
|
98
|
+
grid_rows=grid_rows,
|
|
69
99
|
)
|
|
70
100
|
|
|
71
101
|
def get_content_bounds(self, clip_name: str) -> Optional[Rect]:
|
|
@@ -88,15 +118,10 @@ class SpriteAnimationSet:
|
|
|
88
118
|
|
|
89
119
|
def get_image(self, variant_id: int, *, copy_surface: bool = True) -> Optional[Surface]:
|
|
90
120
|
tw, th = self.library.tile_size
|
|
91
|
-
if tw <= 0 or th <= 0:
|
|
92
|
-
return None
|
|
93
|
-
available_w = self.surface.get_width() - self.grid_offset_x
|
|
94
|
-
available_h = self.surface.get_height() - self.grid_offset_y
|
|
95
|
-
if available_w < tw or available_h < th:
|
|
121
|
+
if tw <= 0 or th <= 0 or self.grid_columns <= 0 or self.grid_rows <= 0:
|
|
96
122
|
return None
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
row = variant_id // cols
|
|
123
|
+
col = variant_id % self.grid_columns
|
|
124
|
+
row = variant_id // self.grid_columns
|
|
100
125
|
src = Rect(self.grid_offset_x + col * tw, self.grid_offset_y + row * th, tw, th)
|
|
101
126
|
if not self.surface.get_rect().contains(src):
|
|
102
127
|
return None
|
|
@@ -161,3 +186,19 @@ class AnimationPlayer:
|
|
|
161
186
|
if variant not in self._frame_cache:
|
|
162
187
|
self._frame_cache[variant] = self.animation_set.get_image(variant, copy_surface=True)
|
|
163
188
|
return self._frame_cache[variant]
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _scaled_library(library: AnimationLibrary, scale: float) -> AnimationLibrary:
|
|
192
|
+
"""Return a copy of *library* whose grid geometry is scaled by *scale*.
|
|
193
|
+
|
|
194
|
+
Kept in lockstep with the scaled spritesheet surface so that
|
|
195
|
+
:meth:`SpriteAnimationSet.get_image` continues to address cells
|
|
196
|
+
correctly.
|
|
197
|
+
"""
|
|
198
|
+
return AnimationLibrary(
|
|
199
|
+
animations=library.animations,
|
|
200
|
+
spritesheet_path=library.spritesheet_path,
|
|
201
|
+
tile_size=(int(library.tile_size[0] * scale), int(library.tile_size[1] * scale)),
|
|
202
|
+
grid_offset=(int(library.grid_offset[0] * scale), int(library.grid_offset[1] * scale)),
|
|
203
|
+
trim_transparent=library.trim_transparent,
|
|
204
|
+
)
|
|
@@ -8,7 +8,7 @@ repeated file I/O during runtime.
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
10
|
from pathlib import Path
|
|
11
|
-
from typing import Dict, Optional, Union
|
|
11
|
+
from typing import Dict, Optional, Tuple, Union
|
|
12
12
|
|
|
13
13
|
from ..parser.collision import (
|
|
14
14
|
CharacterCollision,
|
|
@@ -39,7 +39,7 @@ class CollisionCache:
|
|
|
39
39
|
|
|
40
40
|
def __init__(self):
|
|
41
41
|
self._tileset_cache: Dict[str, Optional[TilesetCollision]] = {}
|
|
42
|
-
self._character_cache: Dict[str, Optional[CharacterCollision]] = {}
|
|
42
|
+
self._character_cache: Dict[Tuple[str, float], Optional[CharacterCollision]] = {}
|
|
43
43
|
self._object_cache: Dict[str, Optional[ObjectCollisionData]] = {}
|
|
44
44
|
|
|
45
45
|
def get_tileset_collision(
|
|
@@ -58,17 +58,21 @@ class CollisionCache:
|
|
|
58
58
|
return self._tileset_cache[key]
|
|
59
59
|
|
|
60
60
|
def get_character_collision(
|
|
61
|
-
self, collision_path: Union[str, Path]
|
|
61
|
+
self, collision_path: Union[str, Path], render_scale: float = 1.0
|
|
62
62
|
) -> Optional[CharacterCollision]:
|
|
63
63
|
"""Get character collision data (cached).
|
|
64
64
|
|
|
65
65
|
Args:
|
|
66
66
|
collision_path: Direct path to the .collision.json file.
|
|
67
|
+
render_scale: Multiplier applied to shape dimensions and offsets.
|
|
68
|
+
Cached per (path, render_scale) pair.
|
|
67
69
|
"""
|
|
68
|
-
key = str(Path(collision_path).resolve())
|
|
70
|
+
key = (str(Path(collision_path).resolve()), render_scale)
|
|
69
71
|
|
|
70
72
|
if key not in self._character_cache:
|
|
71
|
-
self._character_cache[key] = load_character_collision(
|
|
73
|
+
self._character_cache[key] = load_character_collision(
|
|
74
|
+
collision_path, render_scale=render_scale
|
|
75
|
+
)
|
|
72
76
|
|
|
73
77
|
return self._character_cache[key]
|
|
74
78
|
|
|
@@ -134,13 +138,15 @@ def get_cached_tileset_collision(
|
|
|
134
138
|
|
|
135
139
|
def get_cached_character_collision(
|
|
136
140
|
collision_path: Union[str, Path],
|
|
141
|
+
render_scale: float = 1.0,
|
|
137
142
|
) -> Optional[CharacterCollision]:
|
|
138
143
|
"""Get character collision using global cache.
|
|
139
144
|
|
|
140
145
|
Args:
|
|
141
146
|
collision_path: Direct path to the .collision.json file.
|
|
147
|
+
render_scale: Multiplier applied to shape dimensions and offsets.
|
|
142
148
|
"""
|
|
143
|
-
return _global_cache.get_character_collision(collision_path)
|
|
149
|
+
return _global_cache.get_character_collision(collision_path, render_scale=render_scale)
|
|
144
150
|
|
|
145
151
|
|
|
146
152
|
def get_cached_object_collision(
|
{tilemap_parser-5.0.3 → tilemap_parser-5.1.0}/src/tilemap_parser/runtime/movement/platformer.py
RENAMED
|
@@ -8,6 +8,7 @@ from ...parser.collision import TilesetCollision
|
|
|
8
8
|
from ..polygon_query import _check_sprite_polygon_offset, get_shape_bounds
|
|
9
9
|
from ..protocols import ICollidableSprite
|
|
10
10
|
from ..world import PhysicsWorld
|
|
11
|
+
from .queries import _resolve_tile_data
|
|
11
12
|
from .types import CollisionResult, Vector2
|
|
12
13
|
|
|
13
14
|
|
|
@@ -118,9 +119,7 @@ def move_platformer(
|
|
|
118
119
|
for tile_y in range(min_tile_y, max_tile_y + 1):
|
|
119
120
|
for tile_x in range(min_tile_x, max_tile_x + 1):
|
|
120
121
|
tile_id = tile_map.get((tile_x, tile_y))
|
|
121
|
-
|
|
122
|
-
continue
|
|
123
|
-
tile_data = tileset_collision.tiles.get(tile_id)
|
|
122
|
+
tile_data = _resolve_tile_data(world, tileset_collision, tile_id)
|
|
124
123
|
if tile_data is None:
|
|
125
124
|
continue
|
|
126
125
|
ox = tile_x * tw
|
{tilemap_parser-5.0.3 → tilemap_parser-5.1.0}/src/tilemap_parser/runtime/movement/queries.py
RENAMED
|
@@ -4,12 +4,31 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import math
|
|
6
6
|
|
|
7
|
-
from ...parser.collision import CollisionPolygon, TilesetCollision
|
|
7
|
+
from ...parser.collision import CollisionPolygon, TileCollisionData, TilesetCollision
|
|
8
8
|
from ..collision.hit import should_collide
|
|
9
9
|
from ..polygon_query import _check_sprite_polygon_offset, get_shape_bounds
|
|
10
10
|
from ..protocols import ICollidable, ICollidableSprite
|
|
11
11
|
|
|
12
12
|
|
|
13
|
+
def _resolve_tile_data(
|
|
14
|
+
world,
|
|
15
|
+
tileset_collision: TilesetCollision,
|
|
16
|
+
tile_id: int | None,
|
|
17
|
+
) -> TileCollisionData | None:
|
|
18
|
+
"""Fetch collision data for *tile_id*, GID-routed when a world is attached.
|
|
19
|
+
|
|
20
|
+
With an attached world built via ``from_map(..., use_gids=True)`` the id
|
|
21
|
+
is range-routed against the map's grid resources (decoration tilesets can
|
|
22
|
+
never alias into the collision owner's local keys). Without a world the
|
|
23
|
+
historic literal lookup applies.
|
|
24
|
+
"""
|
|
25
|
+
if tile_id is None:
|
|
26
|
+
return None
|
|
27
|
+
if world is not None:
|
|
28
|
+
return world.resolve_collision(tile_id)
|
|
29
|
+
return tileset_collision.tiles.get(tile_id)
|
|
30
|
+
|
|
31
|
+
|
|
13
32
|
def _collides_at(
|
|
14
33
|
self,
|
|
15
34
|
sprite: ICollidable,
|
|
@@ -35,9 +54,7 @@ def _collides_at(
|
|
|
35
54
|
for tile_y in range(min_tile_y, max_tile_y + 1):
|
|
36
55
|
for tile_x in range(min_tile_x, max_tile_x + 1):
|
|
37
56
|
tile_id = tile_map.get((tile_x, tile_y))
|
|
38
|
-
|
|
39
|
-
continue
|
|
40
|
-
tile_data = tileset_collision.tiles.get(tile_id)
|
|
57
|
+
tile_data = _resolve_tile_data(world, tileset_collision, tile_id)
|
|
41
58
|
if tile_data is None:
|
|
42
59
|
continue
|
|
43
60
|
ox = tile_x * tw
|
|
@@ -72,9 +89,7 @@ def _first_colliding_shape(
|
|
|
72
89
|
for tile_y in range(min_tile_y, max_tile_y + 1):
|
|
73
90
|
for tile_x in range(min_tile_x, max_tile_x + 1):
|
|
74
91
|
tile_id = tile_map.get((tile_x, tile_y))
|
|
75
|
-
|
|
76
|
-
continue
|
|
77
|
-
tile_data = tileset_collision.tiles.get(tile_id)
|
|
92
|
+
tile_data = _resolve_tile_data(world, tileset_collision, tile_id)
|
|
78
93
|
if tile_data is None:
|
|
79
94
|
continue
|
|
80
95
|
ox = tile_x * tw
|
|
@@ -113,9 +128,7 @@ def _collides_at_platformer(
|
|
|
113
128
|
for tile_y in range(min_tile_y, max_tile_y + 1):
|
|
114
129
|
for tile_x in range(min_tile_x, max_tile_x + 1):
|
|
115
130
|
tile_id = tile_map.get((tile_x, tile_y))
|
|
116
|
-
|
|
117
|
-
continue
|
|
118
|
-
tile_data = tileset_collision.tiles.get(tile_id)
|
|
131
|
+
tile_data = _resolve_tile_data(world, tileset_collision, tile_id)
|
|
119
132
|
if tile_data is None:
|
|
120
133
|
continue
|
|
121
134
|
ox = tile_x * tw
|
|
@@ -218,9 +231,7 @@ def _find_walkable_ground_y(
|
|
|
218
231
|
for tile_y in range(min_tile_y, max_tile_y + 1):
|
|
219
232
|
for tile_x in range(min_tile_x, max_tile_x + 1):
|
|
220
233
|
tile_id = tile_map.get((tile_x, tile_y))
|
|
221
|
-
|
|
222
|
-
continue
|
|
223
|
-
tile_data = tileset_collision.tiles.get(tile_id)
|
|
234
|
+
tile_data = _resolve_tile_data(world, tileset_collision, tile_id)
|
|
224
235
|
if tile_data is None:
|
|
225
236
|
continue
|
|
226
237
|
ox = tile_x * tw
|
|
@@ -151,13 +151,19 @@ class CollisionRunner:
|
|
|
151
151
|
tile_x, tile_y = self.get_tile_at(world_x, world_y)
|
|
152
152
|
tile_id = tile_map.get((tile_x, tile_y))
|
|
153
153
|
|
|
154
|
-
|
|
154
|
+
world = self._resolve_world(None)
|
|
155
|
+
tile_data = queries._resolve_tile_data(world, tileset_collision, tile_id)
|
|
156
|
+
if tile_data is None:
|
|
155
157
|
return []
|
|
156
158
|
|
|
157
159
|
tile_world_x = tile_x * self._eff_tw
|
|
158
160
|
tile_world_y = tile_y * self._eff_th
|
|
159
161
|
|
|
160
|
-
return
|
|
162
|
+
return [
|
|
163
|
+
shape.transform(tile_world_x, tile_world_y, self.render_scale)
|
|
164
|
+
for shape in tile_data.shapes
|
|
165
|
+
if shape.is_valid()
|
|
166
|
+
]
|
|
161
167
|
|
|
162
168
|
def get_nearby_tile_shapes(
|
|
163
169
|
self,
|
|
@@ -182,12 +188,11 @@ class CollisionRunner:
|
|
|
182
188
|
max_tile_y = int(bottom // th) + margin
|
|
183
189
|
|
|
184
190
|
shapes = []
|
|
191
|
+
world = self._resolve_world(None)
|
|
185
192
|
for tile_y in range(min_tile_y, max_tile_y + 1):
|
|
186
193
|
for tile_x in range(min_tile_x, max_tile_x + 1):
|
|
187
194
|
tile_id = tile_map.get((tile_x, tile_y))
|
|
188
|
-
|
|
189
|
-
continue
|
|
190
|
-
tile_data = tileset_collision.tiles.get(tile_id)
|
|
195
|
+
tile_data = queries._resolve_tile_data(world, tileset_collision, tile_id)
|
|
191
196
|
if tile_data is None:
|
|
192
197
|
continue
|
|
193
198
|
tile_world_x = tile_x * tw
|
{tilemap_parser-5.0.3 → tilemap_parser-5.1.0}/src/tilemap_parser/runtime/navigation/nav_grid.py
RENAMED
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import math
|
|
4
4
|
from typing import Dict, List, Optional, Tuple
|
|
5
5
|
|
|
6
|
-
from ...parser.collision import TilesetCollision
|
|
6
|
+
from ...parser.collision import TileCollisionData, TilesetCollision
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class NavGrid:
|
|
@@ -15,14 +15,15 @@ class NavGrid:
|
|
|
15
15
|
"""
|
|
16
16
|
|
|
17
17
|
__slots__ = (
|
|
18
|
-
"tile_map",
|
|
19
|
-
"tileset_collision",
|
|
20
|
-
"tile_size",
|
|
21
|
-
"_eff_tw",
|
|
22
18
|
"_eff_th",
|
|
23
|
-
"
|
|
19
|
+
"_eff_tw",
|
|
20
|
+
"_gid_resolver",
|
|
24
21
|
"_height",
|
|
25
22
|
"_walkable",
|
|
23
|
+
"_width",
|
|
24
|
+
"tile_map",
|
|
25
|
+
"tile_size",
|
|
26
|
+
"tileset_collision",
|
|
26
27
|
)
|
|
27
28
|
|
|
28
29
|
def __init__(
|
|
@@ -32,9 +33,14 @@ class NavGrid:
|
|
|
32
33
|
tile_size: Tuple[int, int],
|
|
33
34
|
render_scale: float = 1.0,
|
|
34
35
|
map_size: Optional[Tuple[int, int]] = None,
|
|
36
|
+
gid_resolver=None,
|
|
35
37
|
) -> None:
|
|
36
38
|
self.tile_map = tile_map
|
|
37
39
|
self.tileset_collision = tileset_collision
|
|
40
|
+
# Optional callable (int) -> TileCollisionData | None. Pass
|
|
41
|
+
# ``physics_world.resolve_collision`` for GID-routed maps so ids from
|
|
42
|
+
# non-collidable grid tilesets resolve to None instead of aliasing.
|
|
43
|
+
self._gid_resolver = gid_resolver
|
|
38
44
|
self.tile_size = tile_size
|
|
39
45
|
self._eff_tw = tile_size[0] * render_scale
|
|
40
46
|
self._eff_th = tile_size[1] * render_scale
|
|
@@ -52,11 +58,15 @@ class NavGrid:
|
|
|
52
58
|
|
|
53
59
|
self._walkable = [[self._is_tile_walkable(x, y) for x in range(self._width)] for y in range(self._height)]
|
|
54
60
|
|
|
55
|
-
def
|
|
56
|
-
tile_id = self.tile_map.get((tx, ty))
|
|
61
|
+
def _resolve(self, tile_id):
|
|
57
62
|
if tile_id is None:
|
|
58
|
-
return
|
|
59
|
-
|
|
63
|
+
return None
|
|
64
|
+
if self._gid_resolver is not None:
|
|
65
|
+
return self._gid_resolver(tile_id)
|
|
66
|
+
return self.tileset_collision.tiles.get(tile_id)
|
|
67
|
+
|
|
68
|
+
def _is_tile_walkable(self, tx: int, ty: int) -> bool:
|
|
69
|
+
tile_data = self._resolve(self.tile_map.get((tx, ty)))
|
|
60
70
|
if tile_data is None:
|
|
61
71
|
return True
|
|
62
72
|
for poly in tile_data.shapes:
|
|
@@ -78,10 +88,7 @@ class NavGrid:
|
|
|
78
88
|
return self._walkable[ty][tx]
|
|
79
89
|
|
|
80
90
|
def is_one_way(self, tx: int, ty: int) -> bool:
|
|
81
|
-
|
|
82
|
-
if tile_id is None:
|
|
83
|
-
return False
|
|
84
|
-
tile_data = self.tileset_collision.tiles.get(tile_id)
|
|
91
|
+
tile_data = self._resolve(self.tile_map.get((tx, ty)))
|
|
85
92
|
if tile_data is None:
|
|
86
93
|
return False
|
|
87
94
|
has_one_way = False
|
|
@@ -104,6 +111,10 @@ class NavGrid:
|
|
|
104
111
|
new._width = self._width
|
|
105
112
|
new._height = self._height
|
|
106
113
|
new._walkable = [row[:] for row in self._walkable]
|
|
114
|
+
# _gid_resolver lives in __slots__: dropping it here made every
|
|
115
|
+
# derived grid raise AttributeError once _resolve() ran (e.g. via
|
|
116
|
+
# is_one_way), and silently lost GID routing before that.
|
|
117
|
+
new._gid_resolver = self._gid_resolver
|
|
107
118
|
return new
|
|
108
119
|
|
|
109
120
|
def erode(self, margin: float) -> NavGrid:
|
|
@@ -155,16 +166,31 @@ class NavGrid:
|
|
|
155
166
|
sprite_height: Optional[float] = None,
|
|
156
167
|
render_scale: float = 1.0,
|
|
157
168
|
map_size: Optional[Tuple[int, int]] = None,
|
|
158
|
-
cache: Optional[
|
|
169
|
+
cache: Optional[dict[tuple[float, bool], NavGrid]] = None,
|
|
170
|
+
gid_resolver=None,
|
|
159
171
|
) -> NavGrid:
|
|
172
|
+
"""Build (or fetch from *cache*) an eroded grid for one entity size.
|
|
173
|
+
|
|
174
|
+
The cache is scoped to a single map/routing context and is keyed by
|
|
175
|
+
``(margin, resolver_is_literal)`` so literal and GID-routed grids can
|
|
176
|
+
never cross-contaminate through a shared cache dict.
|
|
177
|
+
"""
|
|
160
178
|
tw = tile_size[0] * render_scale
|
|
161
179
|
size = max(sprite_width, sprite_height if sprite_height is not None else sprite_width)
|
|
162
180
|
margin = (size / 2.0) / tw
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
181
|
+
key = (margin, gid_resolver is None)
|
|
182
|
+
if cache is not None and key in cache:
|
|
183
|
+
return cache[key]
|
|
184
|
+
nav = cls(
|
|
185
|
+
tile_map,
|
|
186
|
+
tileset_collision,
|
|
187
|
+
tile_size,
|
|
188
|
+
render_scale,
|
|
189
|
+
map_size,
|
|
190
|
+
gid_resolver=gid_resolver,
|
|
191
|
+
).erode(margin)
|
|
166
192
|
if cache is not None:
|
|
167
|
-
cache[
|
|
193
|
+
cache[key] = nav
|
|
168
194
|
return nav
|
|
169
195
|
|
|
170
196
|
def get_neighbors(self, tx: int, ty: int, *, diagonals: bool = False) -> List[Tuple[int, int]]:
|
|
@@ -177,6 +177,23 @@ def _make_heart_texture() -> Surface:
|
|
|
177
177
|
return s
|
|
178
178
|
|
|
179
179
|
|
|
180
|
+
def _make_line_texture() -> Surface:
|
|
181
|
+
# will be applied on next release not yet
|
|
182
|
+
"""Vertical streak spanning the full canvas with a thin core.
|
|
183
|
+
|
|
184
|
+
Kept axis-aligned (not rotated): the renderer only rotates particles
|
|
185
|
+
when ``rotation_speed`` is non-zero, so a vertical line reads as a
|
|
186
|
+
falling streak for rain and stream effects.
|
|
187
|
+
"""
|
|
188
|
+
s = Surface((PARTICLE_TEXTURE_SIZE, PARTICLE_TEXTURE_SIZE), pygame.SRCALPHA)
|
|
189
|
+
w = max(1, PARTICLE_TEXTURE_SIZE // 6)
|
|
190
|
+
x = (PARTICLE_TEXTURE_SIZE - w) // 2
|
|
191
|
+
pygame.draw.rect(s, (255, 255, 255, 255), Rect(x, 2, w, PARTICLE_TEXTURE_SIZE - 4))
|
|
192
|
+
pygame.draw.rect(s, (255, 255, 255, 140), Rect(x, 0, w, 2))
|
|
193
|
+
pygame.draw.rect(s, (255, 255, 255, 140), Rect(x, PARTICLE_TEXTURE_SIZE - 2, w, 2))
|
|
194
|
+
return s
|
|
195
|
+
|
|
196
|
+
|
|
180
197
|
def _get_base_texture(shape: str) -> Surface:
|
|
181
198
|
if shape not in _TEXTURE_CACHE:
|
|
182
199
|
makers = {
|
|
@@ -188,6 +205,7 @@ def _get_base_texture(shape: str) -> Surface:
|
|
|
188
205
|
"smoke": _make_smoke_texture,
|
|
189
206
|
"fog": _make_fog_texture,
|
|
190
207
|
"heart": _make_heart_texture,
|
|
208
|
+
"line": _make_line_texture,
|
|
191
209
|
}
|
|
192
210
|
maker = makers.get(shape, _make_circle_texture)
|
|
193
211
|
_TEXTURE_CACHE[shape] = maker()
|