tilemap-parser 4.2.0__tar.gz → 4.2.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {tilemap_parser-4.2.0 → tilemap_parser-4.2.1}/PKG-INFO +1 -1
- {tilemap_parser-4.2.0 → tilemap_parser-4.2.1}/pyproject.toml +1 -1
- {tilemap_parser-4.2.0 → tilemap_parser-4.2.1}/src/tilemap_parser/runtime/__init__.py +3 -0
- {tilemap_parser-4.2.0 → tilemap_parser-4.2.1}/src/tilemap_parser/runtime/map_loader.py +16 -4
- tilemap_parser-4.2.1/src/tilemap_parser/runtime/map_object.py +181 -0
- {tilemap_parser-4.2.0 → tilemap_parser-4.2.1}/src/tilemap_parser/runtime/object_collision.py +124 -80
- {tilemap_parser-4.2.0 → tilemap_parser-4.2.1}/src/tilemap_parser.egg-info/PKG-INFO +1 -1
- {tilemap_parser-4.2.0 → tilemap_parser-4.2.1}/src/tilemap_parser.egg-info/SOURCES.txt +2 -0
- tilemap_parser-4.2.1/tests/test_map_object.py +707 -0
- {tilemap_parser-4.2.0 → tilemap_parser-4.2.1}/LICENSE +0 -0
- {tilemap_parser-4.2.0 → tilemap_parser-4.2.1}/README.md +0 -0
- {tilemap_parser-4.2.0 → tilemap_parser-4.2.1}/setup.cfg +0 -0
- {tilemap_parser-4.2.0 → tilemap_parser-4.2.1}/src/tilemap_parser/__init__.py +0 -0
- {tilemap_parser-4.2.0 → tilemap_parser-4.2.1}/src/tilemap_parser/parser/__init__.py +0 -0
- {tilemap_parser-4.2.0 → tilemap_parser-4.2.1}/src/tilemap_parser/parser/animation.py +0 -0
- {tilemap_parser-4.2.0 → tilemap_parser-4.2.1}/src/tilemap_parser/parser/collision.py +0 -0
- {tilemap_parser-4.2.0 → tilemap_parser-4.2.1}/src/tilemap_parser/parser/collision_loader.py +0 -0
- {tilemap_parser-4.2.0 → tilemap_parser-4.2.1}/src/tilemap_parser/parser/map_parse.py +0 -0
- {tilemap_parser-4.2.0 → tilemap_parser-4.2.1}/src/tilemap_parser/parser/node_parse.py +0 -0
- {tilemap_parser-4.2.0 → tilemap_parser-4.2.1}/src/tilemap_parser/parser/particle.py +0 -0
- {tilemap_parser-4.2.0 → tilemap_parser-4.2.1}/src/tilemap_parser/parser/tmx_converter.py +0 -0
- {tilemap_parser-4.2.0 → tilemap_parser-4.2.1}/src/tilemap_parser/runtime/animation_player.py +0 -0
- {tilemap_parser-4.2.0 → tilemap_parser-4.2.1}/src/tilemap_parser/runtime/area_node.py +0 -0
- {tilemap_parser-4.2.0 → tilemap_parser-4.2.1}/src/tilemap_parser/runtime/camera.py +0 -0
- {tilemap_parser-4.2.0 → tilemap_parser-4.2.1}/src/tilemap_parser/runtime/collision_cache.py +0 -0
- {tilemap_parser-4.2.0 → tilemap_parser-4.2.1}/src/tilemap_parser/runtime/particles.py +0 -0
- {tilemap_parser-4.2.0 → tilemap_parser-4.2.1}/src/tilemap_parser/runtime/renderer.py +0 -0
- {tilemap_parser-4.2.0 → tilemap_parser-4.2.1}/src/tilemap_parser/runtime/tile_collision.py +0 -0
- {tilemap_parser-4.2.0 → tilemap_parser-4.2.1}/src/tilemap_parser/utils/__init__.py +0 -0
- {tilemap_parser-4.2.0 → tilemap_parser-4.2.1}/src/tilemap_parser/utils/geometry.py +0 -0
- {tilemap_parser-4.2.0 → tilemap_parser-4.2.1}/src/tilemap_parser.egg-info/dependency_links.txt +0 -0
- {tilemap_parser-4.2.0 → tilemap_parser-4.2.1}/src/tilemap_parser.egg-info/requires.txt +0 -0
- {tilemap_parser-4.2.0 → tilemap_parser-4.2.1}/src/tilemap_parser.egg-info/top_level.txt +0 -0
- {tilemap_parser-4.2.0 → tilemap_parser-4.2.1}/tests/test_camera.py +0 -0
- {tilemap_parser-4.2.0 → tilemap_parser-4.2.1}/tests/test_collision.py +0 -0
- {tilemap_parser-4.2.0 → tilemap_parser-4.2.1}/tests/test_geometry.py +0 -0
- {tilemap_parser-4.2.0 → tilemap_parser-4.2.1}/tests/test_gid_collision.py +0 -0
- {tilemap_parser-4.2.0 → tilemap_parser-4.2.1}/tests/test_integration_animation.py +0 -0
- {tilemap_parser-4.2.0 → tilemap_parser-4.2.1}/tests/test_integration_collision.py +0 -0
- {tilemap_parser-4.2.0 → tilemap_parser-4.2.1}/tests/test_integration_map_loader.py +0 -0
- {tilemap_parser-4.2.0 → tilemap_parser-4.2.1}/tests/test_map_loader.py +0 -0
- {tilemap_parser-4.2.0 → tilemap_parser-4.2.1}/tests/test_move_grounded.py +0 -0
- {tilemap_parser-4.2.0 → tilemap_parser-4.2.1}/tests/test_object_collision.py +0 -0
- {tilemap_parser-4.2.0 → tilemap_parser-4.2.1}/tests/test_object_surfaces.py +0 -0
- {tilemap_parser-4.2.0 → tilemap_parser-4.2.1}/tests/test_particle_emitter.py +0 -0
- {tilemap_parser-4.2.0 → tilemap_parser-4.2.1}/tests/test_render_scale.py +0 -0
- {tilemap_parser-4.2.0 → tilemap_parser-4.2.1}/tests/test_tile_collision.py +0 -0
- {tilemap_parser-4.2.0 → tilemap_parser-4.2.1}/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.1
|
|
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.1"
|
|
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"
|
|
@@ -10,6 +10,7 @@ from .collision_cache import (
|
|
|
10
10
|
load_object_collision,
|
|
11
11
|
load_tileset_collision,
|
|
12
12
|
)
|
|
13
|
+
from .map_object import MapObject, load_map_objects
|
|
13
14
|
from .tile_collision import (
|
|
14
15
|
CollisionResult,
|
|
15
16
|
CollisionRunner,
|
|
@@ -47,6 +48,7 @@ __all__ = [
|
|
|
47
48
|
"ICollidableObject",
|
|
48
49
|
"ICollidableSprite",
|
|
49
50
|
"LayerRenderStats",
|
|
51
|
+
"MapObject",
|
|
50
52
|
"MovementMode",
|
|
51
53
|
"ObjectCollisionManager",
|
|
52
54
|
"SpriteAnimationSet",
|
|
@@ -59,6 +61,7 @@ __all__ = [
|
|
|
59
61
|
"get_cached_tileset_collision",
|
|
60
62
|
"load_character_collision",
|
|
61
63
|
"load_map",
|
|
64
|
+
"load_map_objects",
|
|
62
65
|
"load_object_collision",
|
|
63
66
|
"load_tileset_collision",
|
|
64
67
|
"Particle",
|
|
@@ -68,7 +68,12 @@ class TilemapData:
|
|
|
68
68
|
surfaces.append(None)
|
|
69
69
|
continue
|
|
70
70
|
try:
|
|
71
|
-
|
|
71
|
+
surf = pygame.image.load(str(resolved))
|
|
72
|
+
try:
|
|
73
|
+
surf = surf.convert_alpha()
|
|
74
|
+
except pygame.error:
|
|
75
|
+
pass
|
|
76
|
+
surfaces.append(surf)
|
|
72
77
|
except pygame.error as e:
|
|
73
78
|
msg = f"Tileset load failed ({i}) {resolved}: {e}"
|
|
74
79
|
warnings.append(msg)
|
|
@@ -244,6 +249,16 @@ class TilemapData:
|
|
|
244
249
|
return None
|
|
245
250
|
return _variant_surface(source, variant, self.tile_size, copy_surface=copy_surface)
|
|
246
251
|
|
|
252
|
+
def get_object_surface(self, obj: ParsedObject, *, copy_surface: bool = True) -> Optional[Surface]:
|
|
253
|
+
if obj.ttype < 0 or obj.ttype >= len(self.surfaces):
|
|
254
|
+
return None
|
|
255
|
+
source = self.surfaces[obj.ttype]
|
|
256
|
+
if source is None:
|
|
257
|
+
return None
|
|
258
|
+
if (obj.area.w, obj.area.h) == (source.get_width(), source.get_height()):
|
|
259
|
+
return source.copy() if copy_surface else source
|
|
260
|
+
return _variant_surface(source, obj.variant, self.tile_size, copy_surface=copy_surface)
|
|
261
|
+
|
|
247
262
|
def get_tile_surface(self, ttype: int, variant: int, *, copy_surface: bool = True) -> Optional[Surface]:
|
|
248
263
|
return self.get_image(variant=variant, ttype=ttype, copy_surface=copy_surface)
|
|
249
264
|
|
|
@@ -259,9 +274,6 @@ class TilemapData:
|
|
|
259
274
|
return None
|
|
260
275
|
return self.get_tile_surface(tile.ttype, tile.variant)
|
|
261
276
|
|
|
262
|
-
def get_object_surface(self, obj: ParsedObject, *, copy_surface: bool = True) -> Optional[Surface]:
|
|
263
|
-
return self.get_image(variant=obj.variant, ttype=obj.ttype, copy_surface=copy_surface)
|
|
264
|
-
|
|
265
277
|
def get_object_surface_by_id(
|
|
266
278
|
self, layer_id_or_name: Union[int, str], object_id: int, *, copy_surface: bool = True
|
|
267
279
|
) -> Optional[Tuple[Surface, int, int]]:
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Runtime map object loading and representation.
|
|
3
|
+
|
|
4
|
+
Bridges the gap between parsed map objects, collision data, and
|
|
5
|
+
the collision manager. Provides a ready-to-use :class:`MapObject`
|
|
6
|
+
that implements :class:`ICollidableObject` and carries a pygame
|
|
7
|
+
:class:`~pygame.Surface` for rendering.
|
|
8
|
+
|
|
9
|
+
Usage::
|
|
10
|
+
|
|
11
|
+
objects = load_map_objects(tilemap_data, MAPS_PATH / "collision")
|
|
12
|
+
for obj in objects:
|
|
13
|
+
collision_manager.add_object(obj)
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Dict, List, Optional, Union
|
|
20
|
+
|
|
21
|
+
from pygame import Surface
|
|
22
|
+
|
|
23
|
+
from ..parser.collision import CollisionPolygon, ObjectCollisionData
|
|
24
|
+
from ..parser.collision_loader import load_object_collision
|
|
25
|
+
from .collision_cache import CollisionCache
|
|
26
|
+
from .map_loader import TilemapData
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class MapObject:
|
|
30
|
+
"""
|
|
31
|
+
A game object loaded from a tilemap, carrying a surface for
|
|
32
|
+
rendering and collision data for physics.
|
|
33
|
+
|
|
34
|
+
Satisfies the :class:`ICollidableObject` protocol so it can be
|
|
35
|
+
added directly to an :class:`ObjectCollisionManager`.
|
|
36
|
+
|
|
37
|
+
When a region contains multiple disjoint polygons, *all* shapes
|
|
38
|
+
are stored in :attr:`collision_shapes` and the first shape is
|
|
39
|
+
used as :attr:`collision_shape` for protocol compatibility.
|
|
40
|
+
The collision system (:func:`check_collision`) iterates all
|
|
41
|
+
shape pairs when either object has multiple shapes.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
__slots__ = (
|
|
45
|
+
"x",
|
|
46
|
+
"y",
|
|
47
|
+
"surface",
|
|
48
|
+
"collision_shape",
|
|
49
|
+
"collision_shapes",
|
|
50
|
+
"collision_layer",
|
|
51
|
+
"collision_mask",
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
x: float,
|
|
57
|
+
y: float,
|
|
58
|
+
surface: Surface,
|
|
59
|
+
collision_shape: CollisionPolygon,
|
|
60
|
+
*,
|
|
61
|
+
collision_shapes: Optional[List[CollisionPolygon]] = None,
|
|
62
|
+
collision_layer: int = 1,
|
|
63
|
+
collision_mask: int = 0xFFFFFFFF,
|
|
64
|
+
) -> None:
|
|
65
|
+
self.x = x
|
|
66
|
+
self.y = y
|
|
67
|
+
self.surface = surface
|
|
68
|
+
self.collision_shapes = collision_shapes or [collision_shape]
|
|
69
|
+
self.collision_shape = self.collision_shapes[0]
|
|
70
|
+
self.collision_layer = collision_layer
|
|
71
|
+
self.collision_mask = collision_mask
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _resolve_object_collision_filename(tileset_path: Union[str, Path]) -> str:
|
|
75
|
+
"""Extract the object collision filename for a given tileset path.
|
|
76
|
+
|
|
77
|
+
Example: ``"building7.png"`` → ``"building7.object_collision.json"``
|
|
78
|
+
"""
|
|
79
|
+
return f"{Path(tileset_path).stem}.object_collision.json"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def load_map_objects(
|
|
83
|
+
tilemap_data: TilemapData,
|
|
84
|
+
collision_dir: Union[str, Path],
|
|
85
|
+
*,
|
|
86
|
+
cache: Optional[CollisionCache] = None,
|
|
87
|
+
) -> List[MapObject]:
|
|
88
|
+
"""Load all collidable objects from a tilemap.
|
|
89
|
+
|
|
90
|
+
Iterates every object layer in *tilemap_data*, resolves the
|
|
91
|
+
corresponding ``.object_collision.json`` from *collision_dir*, and
|
|
92
|
+
builds :class:`MapObject` instances with surfaces, owner-local
|
|
93
|
+
collision shapes (region offset only; consumers add x/y), and
|
|
94
|
+
layer/mask values from the collision regions.
|
|
95
|
+
|
|
96
|
+
Collision data is cached per tileset index so the same file is
|
|
97
|
+
never loaded twice.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
tilemap_data: Loaded tilemap data (surfaces, tilesets, layers).
|
|
101
|
+
collision_dir: Directory containing ``*.object_collision.json``
|
|
102
|
+
files (typically ``<map_path>/collision/``).
|
|
103
|
+
cache: Optional :class:`CollisionCache` for caching parsed
|
|
104
|
+
collision data across calls.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
List of :class:`MapObject` instances, one per map object that
|
|
108
|
+
has both a valid surface and a matching collision region.
|
|
109
|
+
When a region contains multiple collision polygons, all shapes
|
|
110
|
+
are preserved in :attr:`MapObject.collision_shapes` and the
|
|
111
|
+
collision system checks each shape pair during narrowphase.
|
|
112
|
+
"""
|
|
113
|
+
collision_dir = Path(collision_dir)
|
|
114
|
+
objects: List[MapObject] = []
|
|
115
|
+
object_layers = tilemap_data.get_layers(layer_type="object")
|
|
116
|
+
|
|
117
|
+
loaded_collision: Dict[int, Optional[ObjectCollisionData]] = {}
|
|
118
|
+
|
|
119
|
+
for layer in object_layers:
|
|
120
|
+
for obj_id, obj in layer.objects.items():
|
|
121
|
+
surf_x_y = tilemap_data.get_object_surface_by_id(layer.id, obj_id)
|
|
122
|
+
if surf_x_y is None:
|
|
123
|
+
continue
|
|
124
|
+
|
|
125
|
+
surf, x, y = surf_x_y
|
|
126
|
+
|
|
127
|
+
ttype = obj.ttype
|
|
128
|
+
if ttype not in loaded_collision:
|
|
129
|
+
collision_data = _load_collision_for_tileset(
|
|
130
|
+
tilemap_data, ttype, collision_dir, cache
|
|
131
|
+
)
|
|
132
|
+
loaded_collision[ttype] = collision_data
|
|
133
|
+
|
|
134
|
+
collision_data = loaded_collision[ttype]
|
|
135
|
+
if collision_data is None:
|
|
136
|
+
continue
|
|
137
|
+
|
|
138
|
+
world_shapes = []
|
|
139
|
+
region_layer = 1
|
|
140
|
+
region_mask = 0xFFFFFFFF
|
|
141
|
+
for region in collision_data.regions.values():
|
|
142
|
+
if not world_shapes:
|
|
143
|
+
region_layer = region.collision_layer
|
|
144
|
+
region_mask = region.collision_mask
|
|
145
|
+
ox = region.region_rect[0]
|
|
146
|
+
oy = region.region_rect[1]
|
|
147
|
+
world_shapes.extend(shape.transform(ox, oy) for shape in region.shapes)
|
|
148
|
+
if not world_shapes:
|
|
149
|
+
continue
|
|
150
|
+
|
|
151
|
+
game_obj = MapObject(
|
|
152
|
+
x=x,
|
|
153
|
+
y=y,
|
|
154
|
+
surface=surf,
|
|
155
|
+
collision_shape=world_shapes[0],
|
|
156
|
+
collision_shapes=world_shapes,
|
|
157
|
+
collision_layer=region_layer,
|
|
158
|
+
collision_mask=region_mask,
|
|
159
|
+
)
|
|
160
|
+
objects.append(game_obj)
|
|
161
|
+
|
|
162
|
+
return objects
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _load_collision_for_tileset(
|
|
166
|
+
tilemap_data: TilemapData,
|
|
167
|
+
ttype: int,
|
|
168
|
+
collision_dir: Path,
|
|
169
|
+
cache: Optional[CollisionCache],
|
|
170
|
+
) -> Optional[ObjectCollisionData]:
|
|
171
|
+
"""Load (or retrieve from cache) collision data for a tileset index."""
|
|
172
|
+
if ttype < 0 or ttype >= len(tilemap_data.parsed.tilesets):
|
|
173
|
+
return None
|
|
174
|
+
|
|
175
|
+
tileset_path = tilemap_data.parsed.tilesets[ttype].path
|
|
176
|
+
coll_filename = _resolve_object_collision_filename(tileset_path)
|
|
177
|
+
coll_path = collision_dir / coll_filename
|
|
178
|
+
|
|
179
|
+
if cache is not None:
|
|
180
|
+
return cache.get_object_collision(coll_path)
|
|
181
|
+
return load_object_collision(coll_path)
|
{tilemap_parser-4.2.0 → tilemap_parser-4.2.1}/src/tilemap_parser/runtime/object_collision.py
RENAMED
|
@@ -133,96 +133,89 @@ def should_collide(
|
|
|
133
133
|
_should_collide = should_collide
|
|
134
134
|
|
|
135
135
|
|
|
136
|
-
def
|
|
137
|
-
|
|
138
|
-
obj_b: ICollidableObject,
|
|
139
|
-
) -> Optional[CollisionHit]:
|
|
140
|
-
"""
|
|
141
|
-
Check if two objects collide.
|
|
136
|
+
def _get_shapes(obj: ICollidableObject) -> List:
|
|
137
|
+
"""Return all collision shapes for an object.
|
|
142
138
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
3. Narrowphase geometry dispatch
|
|
147
|
-
4. Return CollisionHit or None
|
|
148
|
-
|
|
149
|
-
All shape combinations are supported, including polygon-vs-anything.
|
|
139
|
+
Objects with a :attr:`collision_shapes` attribute (e.g.
|
|
140
|
+
:class:`MapObject`) may carry multiple polygons per region;
|
|
141
|
+
single-shape objects return ``[obj.collision_shape]``.
|
|
150
142
|
"""
|
|
151
|
-
|
|
152
|
-
if not
|
|
153
|
-
return
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
143
|
+
shapes = getattr(obj, "collision_shapes", None)
|
|
144
|
+
if shapes is not None and len(shapes) > 0:
|
|
145
|
+
return list(shapes)
|
|
146
|
+
return [obj.collision_shape]
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _combined_aabb(x: float, y: float, shapes: List) -> tuple[float, float, float, float]:
|
|
150
|
+
"""Union AABB across all shapes at position *(x, y)*."""
|
|
151
|
+
left = top = float("inf")
|
|
152
|
+
right = bottom = float("-inf")
|
|
153
|
+
for shape in shapes:
|
|
154
|
+
sx0, sy0, sx1, sy1 = get_shape_aabb(x, y, shape)
|
|
155
|
+
if sx0 < left:
|
|
156
|
+
left = sx0
|
|
157
|
+
if sy0 < top:
|
|
158
|
+
top = sy0
|
|
159
|
+
if sx1 > right:
|
|
160
|
+
right = sx1
|
|
161
|
+
if sy1 > bottom:
|
|
162
|
+
bottom = sy1
|
|
163
|
+
return (left, top, right, bottom)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _check_pair(
|
|
167
|
+
obj_a: ICollidableObject,
|
|
168
|
+
obj_b: ICollidableObject,
|
|
169
|
+
shape_a,
|
|
170
|
+
shape_b,
|
|
171
|
+
aabb_a: tuple[float, float, float, float],
|
|
172
|
+
aabb_b: tuple[float, float, float, float],
|
|
173
|
+
) -> Optional[CollisionInfo]:
|
|
174
|
+
"""Run narrowphase for a single shape pair. (Extracted from
|
|
175
|
+
:func:`check_collision` for reuse in the multi-shape loop.)"""
|
|
167
176
|
if isinstance(shape_a, CircleShape) and isinstance(shape_b, CircleShape):
|
|
168
177
|
ca = (obj_a.x + shape_a.offset[0], obj_a.y + shape_a.offset[1])
|
|
169
178
|
cb = (obj_b.x + shape_b.offset[0], obj_b.y + shape_b.offset[1])
|
|
170
|
-
|
|
179
|
+
return circle_vs_circle(ca, shape_a.radius, cb, shape_b.radius)
|
|
171
180
|
|
|
172
181
|
elif isinstance(shape_a, RectangleShape) and isinstance(shape_b, RectangleShape):
|
|
173
|
-
|
|
182
|
+
return rect_vs_rect(aabb_a, aabb_b)
|
|
174
183
|
|
|
175
184
|
elif isinstance(shape_a, RectangleShape) and isinstance(shape_b, CircleShape):
|
|
176
185
|
cb = (obj_b.x + shape_b.offset[0], obj_b.y + shape_b.offset[1])
|
|
177
|
-
|
|
186
|
+
return rect_vs_circle(aabb_a, cb, shape_b.radius)
|
|
178
187
|
|
|
179
188
|
elif isinstance(shape_a, CircleShape) and isinstance(shape_b, RectangleShape):
|
|
180
189
|
ca = (obj_a.x + shape_a.offset[0], obj_a.y + shape_a.offset[1])
|
|
181
|
-
|
|
182
|
-
if result is not None:
|
|
183
|
-
# Flip normal: was rect→circle, need circle→rect
|
|
184
|
-
info = CollisionInfo(
|
|
185
|
-
normal=(-result.normal[0], -result.normal[1]),
|
|
186
|
-
depth=result.depth,
|
|
187
|
-
)
|
|
190
|
+
return _flip_result(rect_vs_circle(aabb_b, ca, shape_a.radius))
|
|
188
191
|
|
|
189
192
|
# Polygon vs Polygon
|
|
190
193
|
elif isinstance(shape_a, CollisionPolygon) and isinstance(shape_b, CollisionPolygon):
|
|
191
194
|
verts_a = [(obj_a.x + v[0], obj_a.y + v[1]) for v in shape_a.vertices]
|
|
192
195
|
verts_b = [(obj_b.x + v[0], obj_b.y + v[1]) for v in shape_b.vertices]
|
|
193
|
-
|
|
196
|
+
return polygon_vs_polygon(verts_a, verts_b)
|
|
194
197
|
|
|
195
198
|
# Polygon vs Circle
|
|
196
199
|
elif isinstance(shape_a, CollisionPolygon) and isinstance(shape_b, CircleShape):
|
|
197
200
|
verts_a = [(obj_a.x + v[0], obj_a.y + v[1]) for v in shape_a.vertices]
|
|
198
201
|
center_b = (obj_b.x + shape_b.offset[0], obj_b.y + shape_b.offset[1])
|
|
199
|
-
|
|
202
|
+
return polygon_vs_circle(verts_a, center_b, shape_b.radius)
|
|
200
203
|
|
|
201
204
|
# Circle vs Polygon (flip normal)
|
|
202
205
|
elif isinstance(shape_a, CircleShape) and isinstance(shape_b, CollisionPolygon):
|
|
203
206
|
verts_b = [(obj_b.x + v[0], obj_b.y + v[1]) for v in shape_b.vertices]
|
|
204
207
|
center_a = (obj_a.x + shape_a.offset[0], obj_a.y + shape_a.offset[1])
|
|
205
|
-
|
|
206
|
-
if result is not None:
|
|
207
|
-
info = CollisionInfo(
|
|
208
|
-
normal=(-result.normal[0], -result.normal[1]),
|
|
209
|
-
depth=result.depth,
|
|
210
|
-
)
|
|
208
|
+
return _flip_result(polygon_vs_circle(verts_b, center_a, shape_a.radius))
|
|
211
209
|
|
|
212
210
|
# Polygon vs Rect
|
|
213
211
|
elif isinstance(shape_a, CollisionPolygon) and isinstance(shape_b, RectangleShape):
|
|
214
212
|
verts_a = [(obj_a.x + v[0], obj_a.y + v[1]) for v in shape_a.vertices]
|
|
215
|
-
|
|
213
|
+
return polygon_vs_rect(verts_a, aabb_b)
|
|
216
214
|
|
|
217
215
|
# Rect vs Polygon (flip normal)
|
|
218
216
|
elif isinstance(shape_a, RectangleShape) and isinstance(shape_b, CollisionPolygon):
|
|
219
217
|
verts_b = [(obj_b.x + v[0], obj_b.y + v[1]) for v in shape_b.vertices]
|
|
220
|
-
|
|
221
|
-
if result is not None:
|
|
222
|
-
info = CollisionInfo(
|
|
223
|
-
normal=(-result.normal[0], -result.normal[1]),
|
|
224
|
-
depth=result.depth,
|
|
225
|
-
)
|
|
218
|
+
return _flip_result(polygon_vs_rect(verts_b, aabb_a))
|
|
226
219
|
|
|
227
220
|
# Capsule pairs
|
|
228
221
|
elif isinstance(shape_a, CapsuleShape) and isinstance(shape_b, CapsuleShape):
|
|
@@ -230,73 +223,121 @@ def check_collision(
|
|
|
230
223
|
p2 = (p1[0], p1[1] + shape_a.height)
|
|
231
224
|
q1 = (obj_b.x + shape_b.offset[0], obj_b.y + shape_b.offset[1])
|
|
232
225
|
q2 = (q1[0], q1[1] + shape_b.height)
|
|
233
|
-
|
|
226
|
+
return capsule_vs_capsule(p1, p2, shape_a.radius, q1, q2, shape_b.radius)
|
|
234
227
|
|
|
235
228
|
elif isinstance(shape_a, CapsuleShape) and isinstance(shape_b, CircleShape):
|
|
236
229
|
p1 = (obj_a.x + shape_a.offset[0], obj_a.y + shape_a.offset[1])
|
|
237
230
|
p2 = (p1[0], p1[1] + shape_a.height)
|
|
238
231
|
cb = (obj_b.x + shape_b.offset[0], obj_b.y + shape_b.offset[1])
|
|
239
|
-
|
|
232
|
+
return capsule_vs_circle(p1, p2, shape_a.radius, cb, shape_b.radius)
|
|
240
233
|
|
|
241
234
|
elif isinstance(shape_a, CircleShape) and isinstance(shape_b, CapsuleShape):
|
|
242
235
|
ca = (obj_a.x + shape_a.offset[0], obj_a.y + shape_a.offset[1])
|
|
243
236
|
q1 = (obj_b.x + shape_b.offset[0], obj_b.y + shape_b.offset[1])
|
|
244
237
|
q2 = (q1[0], q1[1] + shape_b.height)
|
|
245
|
-
|
|
246
|
-
if result is not None:
|
|
247
|
-
info = CollisionInfo(
|
|
248
|
-
normal=(-result.normal[0], -result.normal[1]),
|
|
249
|
-
depth=result.depth,
|
|
250
|
-
)
|
|
238
|
+
return _flip_result(capsule_vs_circle(q1, q2, shape_b.radius, ca, shape_a.radius))
|
|
251
239
|
|
|
252
240
|
elif isinstance(shape_a, CapsuleShape) and isinstance(shape_b, RectangleShape):
|
|
253
241
|
p1 = (obj_a.x + shape_a.offset[0], obj_a.y + shape_a.offset[1])
|
|
254
242
|
p2 = (p1[0], p1[1] + shape_a.height)
|
|
255
|
-
|
|
243
|
+
return capsule_vs_rect(p1, p2, shape_a.radius, aabb_b)
|
|
256
244
|
|
|
257
245
|
elif isinstance(shape_a, RectangleShape) and isinstance(shape_b, CapsuleShape):
|
|
258
246
|
q1 = (obj_b.x + shape_b.offset[0], obj_b.y + shape_b.offset[1])
|
|
259
247
|
q2 = (q1[0], q1[1] + shape_b.height)
|
|
260
|
-
|
|
261
|
-
if result is not None:
|
|
262
|
-
info = CollisionInfo(
|
|
263
|
-
normal=(-result.normal[0], -result.normal[1]),
|
|
264
|
-
depth=result.depth,
|
|
265
|
-
)
|
|
248
|
+
return _flip_result(capsule_vs_rect(q1, q2, shape_b.radius, aabb_a))
|
|
266
249
|
|
|
267
250
|
elif isinstance(shape_a, CapsuleShape) and isinstance(shape_b, CollisionPolygon):
|
|
268
251
|
p1 = (obj_a.x + shape_a.offset[0], obj_a.y + shape_a.offset[1])
|
|
269
252
|
p2 = (p1[0], p1[1] + shape_a.height)
|
|
270
253
|
verts_b = [(obj_b.x + v[0], obj_b.y + v[1]) for v in shape_b.vertices]
|
|
271
|
-
|
|
254
|
+
return capsule_vs_polygon(p1, p2, shape_a.radius, verts_b)
|
|
272
255
|
|
|
273
256
|
elif isinstance(shape_a, CollisionPolygon) and isinstance(shape_b, CapsuleShape):
|
|
274
257
|
verts_a = [(obj_a.x + v[0], obj_a.y + v[1]) for v in shape_a.vertices]
|
|
275
258
|
q1 = (obj_b.x + shape_b.offset[0], obj_b.y + shape_b.offset[1])
|
|
276
259
|
q2 = (q1[0], q1[1] + shape_b.height)
|
|
277
|
-
|
|
278
|
-
if result is not None:
|
|
279
|
-
info = CollisionInfo(
|
|
280
|
-
normal=(-result.normal[0], -result.normal[1]),
|
|
281
|
-
depth=result.depth,
|
|
282
|
-
)
|
|
260
|
+
return _flip_result(capsule_vs_polygon(q1, q2, shape_b.radius, verts_a))
|
|
283
261
|
|
|
284
262
|
else:
|
|
285
263
|
warnings.warn(
|
|
286
264
|
f"Unhandled collision shape pair: {type(shape_a).__name__} vs {type(shape_b).__name__}",
|
|
287
265
|
UserWarning,
|
|
288
|
-
stacklevel=
|
|
266
|
+
stacklevel=3,
|
|
289
267
|
)
|
|
290
268
|
return None
|
|
291
269
|
|
|
270
|
+
|
|
271
|
+
def _flip_result(info: Optional[CollisionInfo]) -> Optional[CollisionInfo]:
|
|
272
|
+
"""Flip the normal of a :class:`CollisionInfo` in place."""
|
|
292
273
|
if info is None:
|
|
293
274
|
return None
|
|
275
|
+
return CollisionInfo(
|
|
276
|
+
normal=(-info.normal[0], -info.normal[1]),
|
|
277
|
+
depth=info.depth,
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def check_collision(
|
|
282
|
+
obj_a: ICollidableObject,
|
|
283
|
+
obj_b: ICollidableObject,
|
|
284
|
+
) -> Optional[CollisionHit]:
|
|
285
|
+
"""
|
|
286
|
+
Check if two objects collide.
|
|
287
|
+
|
|
288
|
+
Pipeline:
|
|
289
|
+
1. Layer filtering
|
|
290
|
+
2. Broadphase AABB rejection
|
|
291
|
+
3. Narrowphase geometry dispatch
|
|
292
|
+
4. Return CollisionHit or None
|
|
293
|
+
|
|
294
|
+
Supports multi-shape objects (those with a ``collision_shapes``
|
|
295
|
+
attribute). When both objects have a single shape the behaviour
|
|
296
|
+
is identical to previous versions.
|
|
297
|
+
"""
|
|
298
|
+
# 1. Layer filter
|
|
299
|
+
if not should_collide(obj_a, obj_b):
|
|
300
|
+
return None
|
|
301
|
+
|
|
302
|
+
# 2. Broadphase — use combined AABB when an object has multiple shapes
|
|
303
|
+
shapes_a = _get_shapes(obj_a)
|
|
304
|
+
shapes_b = _get_shapes(obj_b)
|
|
305
|
+
|
|
306
|
+
if len(shapes_a) == 1:
|
|
307
|
+
aabb_a = get_shape_aabb(obj_a.x, obj_a.y, shapes_a[0])
|
|
308
|
+
else:
|
|
309
|
+
aabb_a = _combined_aabb(obj_a.x, obj_a.y, shapes_a)
|
|
310
|
+
|
|
311
|
+
if len(shapes_b) == 1:
|
|
312
|
+
aabb_b = get_shape_aabb(obj_b.x, obj_b.y, shapes_b[0])
|
|
313
|
+
else:
|
|
314
|
+
aabb_b = _combined_aabb(obj_b.x, obj_b.y, shapes_b)
|
|
315
|
+
|
|
316
|
+
if not aabb_overlap(aabb_a, aabb_b):
|
|
317
|
+
return None
|
|
318
|
+
|
|
319
|
+
# 3. Narrowphase — iterate all shape pairs, keep the deepest
|
|
320
|
+
deepest: Optional[CollisionInfo] = None
|
|
321
|
+
|
|
322
|
+
for shape_a in shapes_a:
|
|
323
|
+
for shape_b in shapes_b:
|
|
324
|
+
pair_aabb_a = get_shape_aabb(obj_a.x, obj_a.y, shape_a)
|
|
325
|
+
pair_aabb_b = get_shape_aabb(obj_b.x, obj_b.y, shape_b)
|
|
326
|
+
if not aabb_overlap(pair_aabb_a, pair_aabb_b):
|
|
327
|
+
continue
|
|
328
|
+
|
|
329
|
+
info = _check_pair(obj_a, obj_b, shape_a, shape_b, pair_aabb_a, pair_aabb_b)
|
|
330
|
+
if info is not None and (deepest is None or info.depth > deepest.depth):
|
|
331
|
+
deepest = info
|
|
332
|
+
|
|
333
|
+
if deepest is None:
|
|
334
|
+
return None
|
|
294
335
|
|
|
295
336
|
return CollisionHit(
|
|
296
337
|
object_a=obj_a,
|
|
297
338
|
object_b=obj_b,
|
|
298
|
-
normal=
|
|
299
|
-
depth=
|
|
339
|
+
normal=deepest.normal,
|
|
340
|
+
depth=deepest.depth,
|
|
300
341
|
)
|
|
301
342
|
|
|
302
343
|
|
|
@@ -392,7 +433,10 @@ class ObjectCollisionManager:
|
|
|
392
433
|
self,
|
|
393
434
|
obj: ICollidableObject,
|
|
394
435
|
) -> tuple[float, float, float, float]:
|
|
395
|
-
|
|
436
|
+
shapes = _get_shapes(obj)
|
|
437
|
+
if len(shapes) == 1:
|
|
438
|
+
return get_shape_aabb(obj.x, obj.y, shapes[0])
|
|
439
|
+
return _combined_aabb(obj.x, obj.y, shapes)
|
|
396
440
|
|
|
397
441
|
def _build_spatial_index(
|
|
398
442
|
self,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tilemap-parser
|
|
3
|
-
Version: 4.2.
|
|
3
|
+
Version: 4.2.1
|
|
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
|
|
@@ -21,6 +21,7 @@ src/tilemap_parser/runtime/area_node.py
|
|
|
21
21
|
src/tilemap_parser/runtime/camera.py
|
|
22
22
|
src/tilemap_parser/runtime/collision_cache.py
|
|
23
23
|
src/tilemap_parser/runtime/map_loader.py
|
|
24
|
+
src/tilemap_parser/runtime/map_object.py
|
|
24
25
|
src/tilemap_parser/runtime/object_collision.py
|
|
25
26
|
src/tilemap_parser/runtime/particles.py
|
|
26
27
|
src/tilemap_parser/runtime/renderer.py
|
|
@@ -35,6 +36,7 @@ tests/test_integration_animation.py
|
|
|
35
36
|
tests/test_integration_collision.py
|
|
36
37
|
tests/test_integration_map_loader.py
|
|
37
38
|
tests/test_map_loader.py
|
|
39
|
+
tests/test_map_object.py
|
|
38
40
|
tests/test_move_grounded.py
|
|
39
41
|
tests/test_object_collision.py
|
|
40
42
|
tests/test_object_surfaces.py
|