tilemap-parser 3.1.19__tar.gz → 3.1.21__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {tilemap_parser-3.1.19 → tilemap_parser-3.1.21}/PKG-INFO +2 -2
- {tilemap_parser-3.1.19 → tilemap_parser-3.1.21}/README.md +1 -1
- {tilemap_parser-3.1.19 → tilemap_parser-3.1.21}/pyproject.toml +1 -1
- {tilemap_parser-3.1.19 → tilemap_parser-3.1.21}/src/tilemap_parser/runtime/object_collision.py +120 -14
- {tilemap_parser-3.1.19 → tilemap_parser-3.1.21}/src/tilemap_parser.egg-info/PKG-INFO +2 -2
- {tilemap_parser-3.1.19 → tilemap_parser-3.1.21}/tests/test_object_collision.py +193 -17
- {tilemap_parser-3.1.19 → tilemap_parser-3.1.21}/tests/test_tile_collision.py +2 -2
- {tilemap_parser-3.1.19 → tilemap_parser-3.1.21}/LICENSE +0 -0
- {tilemap_parser-3.1.19 → tilemap_parser-3.1.21}/setup.cfg +0 -0
- {tilemap_parser-3.1.19 → tilemap_parser-3.1.21}/src/tilemap_parser/__init__.py +0 -0
- {tilemap_parser-3.1.19 → tilemap_parser-3.1.21}/src/tilemap_parser/parser/__init__.py +0 -0
- {tilemap_parser-3.1.19 → tilemap_parser-3.1.21}/src/tilemap_parser/parser/animation.py +0 -0
- {tilemap_parser-3.1.19 → tilemap_parser-3.1.21}/src/tilemap_parser/parser/collision.py +0 -0
- {tilemap_parser-3.1.19 → tilemap_parser-3.1.21}/src/tilemap_parser/parser/collision_loader.py +0 -0
- {tilemap_parser-3.1.19 → tilemap_parser-3.1.21}/src/tilemap_parser/parser/map_parse.py +0 -0
- {tilemap_parser-3.1.19 → tilemap_parser-3.1.21}/src/tilemap_parser/parser/node_parse.py +0 -0
- {tilemap_parser-3.1.19 → tilemap_parser-3.1.21}/src/tilemap_parser/parser/particle.py +0 -0
- {tilemap_parser-3.1.19 → tilemap_parser-3.1.21}/src/tilemap_parser/runtime/__init__.py +0 -0
- {tilemap_parser-3.1.19 → tilemap_parser-3.1.21}/src/tilemap_parser/runtime/animation_player.py +0 -0
- {tilemap_parser-3.1.19 → tilemap_parser-3.1.21}/src/tilemap_parser/runtime/area_node.py +0 -0
- {tilemap_parser-3.1.19 → tilemap_parser-3.1.21}/src/tilemap_parser/runtime/collision_cache.py +0 -0
- {tilemap_parser-3.1.19 → tilemap_parser-3.1.21}/src/tilemap_parser/runtime/map_loader.py +0 -0
- {tilemap_parser-3.1.19 → tilemap_parser-3.1.21}/src/tilemap_parser/runtime/particles.py +0 -0
- {tilemap_parser-3.1.19 → tilemap_parser-3.1.21}/src/tilemap_parser/runtime/renderer.py +0 -0
- {tilemap_parser-3.1.19 → tilemap_parser-3.1.21}/src/tilemap_parser/runtime/tile_collision.py +0 -0
- {tilemap_parser-3.1.19 → tilemap_parser-3.1.21}/src/tilemap_parser/utils/__init__.py +0 -0
- {tilemap_parser-3.1.19 → tilemap_parser-3.1.21}/src/tilemap_parser/utils/geometry.py +0 -0
- {tilemap_parser-3.1.19 → tilemap_parser-3.1.21}/src/tilemap_parser.egg-info/SOURCES.txt +0 -0
- {tilemap_parser-3.1.19 → tilemap_parser-3.1.21}/src/tilemap_parser.egg-info/dependency_links.txt +0 -0
- {tilemap_parser-3.1.19 → tilemap_parser-3.1.21}/src/tilemap_parser.egg-info/requires.txt +0 -0
- {tilemap_parser-3.1.19 → tilemap_parser-3.1.21}/src/tilemap_parser.egg-info/top_level.txt +0 -0
- {tilemap_parser-3.1.19 → tilemap_parser-3.1.21}/tests/test_collision.py +0 -0
- {tilemap_parser-3.1.19 → tilemap_parser-3.1.21}/tests/test_geometry.py +0 -0
- {tilemap_parser-3.1.19 → tilemap_parser-3.1.21}/tests/test_map_loader.py +0 -0
- {tilemap_parser-3.1.19 → tilemap_parser-3.1.21}/tests/test_object_surfaces.py +0 -0
- {tilemap_parser-3.1.19 → tilemap_parser-3.1.21}/tests/test_render_scale.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tilemap-parser
|
|
3
|
-
Version: 3.1.
|
|
3
|
+
Version: 3.1.21
|
|
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
|
|
@@ -706,7 +706,7 @@ Standalone parser/loader for tilemap-editor JSON maps, sprite animations, and co
|
|
|
706
706
|
- **Map parsing**: Load and query tilemaps, layers, objects, and autotile data from JSON
|
|
707
707
|
- **Animation**: Frame-based sprite animation with `AnimationPlayer`
|
|
708
708
|
- **Collision (tile-based)**: Polygon collision detection for tilemaps with slide, platformer, and RPG movement modes via `CollisionRunner`
|
|
709
|
-
- **Collision (object-to-object)**:
|
|
709
|
+
- **Collision (object-to-object)**: Spatial-grid mixed-shape collision detection (rect, circle, capsule, polygon) with layer filtering via `ObjectCollisionManager`
|
|
710
710
|
- **Capsule support**: Full capsule collision against all shape types
|
|
711
711
|
- **Hit helpers**: `CollisionHit.resolve()`, `involves()`, `other()` for ergonomic separation
|
|
712
712
|
|
|
@@ -7,7 +7,7 @@ Standalone parser/loader for tilemap-editor JSON maps, sprite animations, and co
|
|
|
7
7
|
- **Map parsing**: Load and query tilemaps, layers, objects, and autotile data from JSON
|
|
8
8
|
- **Animation**: Frame-based sprite animation with `AnimationPlayer`
|
|
9
9
|
- **Collision (tile-based)**: Polygon collision detection for tilemaps with slide, platformer, and RPG movement modes via `CollisionRunner`
|
|
10
|
-
- **Collision (object-to-object)**:
|
|
10
|
+
- **Collision (object-to-object)**: Spatial-grid mixed-shape collision detection (rect, circle, capsule, polygon) with layer filtering via `ObjectCollisionManager`
|
|
11
11
|
- **Capsule support**: Full capsule collision against all shape types
|
|
12
12
|
- **Hit helpers**: `CollisionHit.resolve()`, `involves()`, `other()` for ergonomic separation
|
|
13
13
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "tilemap-parser"
|
|
7
|
-
version = "3.1.
|
|
7
|
+
version = "3.1.21"
|
|
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"
|
{tilemap_parser-3.1.19 → tilemap_parser-3.1.21}/src/tilemap_parser/runtime/object_collision.py
RENAMED
|
@@ -9,7 +9,8 @@ from __future__ import annotations
|
|
|
9
9
|
|
|
10
10
|
import warnings
|
|
11
11
|
from dataclasses import dataclass
|
|
12
|
-
from
|
|
12
|
+
from math import floor, isfinite
|
|
13
|
+
from typing import Dict, Iterable, Iterator, List, Optional, Protocol, Set, Tuple, Union
|
|
13
14
|
|
|
14
15
|
from ..parser.collision import (
|
|
15
16
|
CapsuleShape,
|
|
@@ -310,16 +311,47 @@ class ObjectCollisionManager:
|
|
|
310
311
|
- All-vs-all and one-vs-all queries
|
|
311
312
|
- Layer filtering
|
|
312
313
|
|
|
313
|
-
|
|
314
|
-
|
|
314
|
+
Uses a uniform-grid spatial broadphase and exact shape narrowphase.
|
|
315
|
+
The grid is rebuilt for each query so moved objects are always indexed
|
|
316
|
+
at their current world positions.
|
|
315
317
|
"""
|
|
316
318
|
|
|
317
|
-
def __init__(
|
|
319
|
+
def __init__(
|
|
320
|
+
self,
|
|
321
|
+
objects: Optional[Iterable[ICollidableObject]] = None,
|
|
322
|
+
*,
|
|
323
|
+
cell_size: float = 128.0,
|
|
324
|
+
) -> None:
|
|
325
|
+
if not isfinite(cell_size) or cell_size <= 0:
|
|
326
|
+
raise ValueError("cell_size must be a finite positive number")
|
|
327
|
+
|
|
318
328
|
self.objects: List[ICollidableObject] = []
|
|
329
|
+
self.cell_size = float(cell_size)
|
|
330
|
+
if objects is not None:
|
|
331
|
+
for obj in objects:
|
|
332
|
+
self.add_object(obj)
|
|
333
|
+
|
|
334
|
+
def __len__(self) -> int:
|
|
335
|
+
"""Return the number of objects currently managed."""
|
|
336
|
+
return len(self.objects)
|
|
337
|
+
|
|
338
|
+
def __iter__(self) -> Iterator[ICollidableObject]:
|
|
339
|
+
"""Iterate over managed objects in insertion order."""
|
|
340
|
+
return iter(self.objects)
|
|
341
|
+
|
|
342
|
+
def __contains__(self, obj: object) -> bool:
|
|
343
|
+
"""Return True if the exact object instance is managed."""
|
|
344
|
+
return any(existing is obj for existing in self.objects)
|
|
345
|
+
|
|
346
|
+
def _find_object_index(self, obj: ICollidableObject) -> int:
|
|
347
|
+
for index, existing in enumerate(self.objects):
|
|
348
|
+
if existing is obj:
|
|
349
|
+
return index
|
|
350
|
+
return -1
|
|
319
351
|
|
|
320
352
|
def add_object(self, obj: ICollidableObject) -> None:
|
|
321
353
|
"""Add an object to the collision system."""
|
|
322
|
-
if obj
|
|
354
|
+
if self._find_object_index(obj) != -1:
|
|
323
355
|
warnings.warn(
|
|
324
356
|
f"Object {obj} is already in the collision manager, skipping.",
|
|
325
357
|
UserWarning,
|
|
@@ -330,27 +362,78 @@ class ObjectCollisionManager:
|
|
|
330
362
|
|
|
331
363
|
def remove_object(self, obj: ICollidableObject) -> None:
|
|
332
364
|
"""Remove an object from the collision system."""
|
|
333
|
-
|
|
365
|
+
index = self._find_object_index(obj)
|
|
366
|
+
if index == -1:
|
|
334
367
|
warnings.warn(
|
|
335
368
|
f"Object {obj} is not in the collision manager, skipping.",
|
|
336
369
|
UserWarning,
|
|
337
370
|
stacklevel=2,
|
|
338
371
|
)
|
|
339
372
|
return
|
|
340
|
-
self.objects
|
|
373
|
+
del self.objects[index]
|
|
374
|
+
|
|
375
|
+
def clear(self) -> None:
|
|
376
|
+
"""Remove all objects from the collision system."""
|
|
377
|
+
self.objects.clear()
|
|
378
|
+
|
|
379
|
+
def _cells_for_aabb(
|
|
380
|
+
self,
|
|
381
|
+
aabb: tuple[float, float, float, float],
|
|
382
|
+
) -> Iterator[Tuple[int, int]]:
|
|
383
|
+
left, top, right, bottom = aabb
|
|
384
|
+
min_cell_x = floor(left / self.cell_size)
|
|
385
|
+
max_cell_x = floor(right / self.cell_size)
|
|
386
|
+
min_cell_y = floor(top / self.cell_size)
|
|
387
|
+
max_cell_y = floor(bottom / self.cell_size)
|
|
388
|
+
|
|
389
|
+
for cell_y in range(min_cell_y, max_cell_y + 1):
|
|
390
|
+
for cell_x in range(min_cell_x, max_cell_x + 1):
|
|
391
|
+
yield (cell_x, cell_y)
|
|
392
|
+
|
|
393
|
+
def _object_aabb(
|
|
394
|
+
self,
|
|
395
|
+
obj: ICollidableObject,
|
|
396
|
+
) -> tuple[float, float, float, float]:
|
|
397
|
+
return get_shape_aabb(obj.x, obj.y, obj.collision_shape)
|
|
398
|
+
|
|
399
|
+
def _build_spatial_index(
|
|
400
|
+
self,
|
|
401
|
+
) -> tuple[Tuple[ICollidableObject, ...], Dict[Tuple[int, int], List[int]]]:
|
|
402
|
+
objects = tuple(self.objects)
|
|
403
|
+
grid: Dict[Tuple[int, int], List[int]] = {}
|
|
404
|
+
|
|
405
|
+
for index, obj in enumerate(objects):
|
|
406
|
+
for cell in self._cells_for_aabb(self._object_aabb(obj)):
|
|
407
|
+
grid.setdefault(cell, []).append(index)
|
|
408
|
+
|
|
409
|
+
return objects, grid
|
|
410
|
+
|
|
411
|
+
def _candidate_indices(
|
|
412
|
+
self,
|
|
413
|
+
obj: ICollidableObject,
|
|
414
|
+
grid: Dict[Tuple[int, int], List[int]],
|
|
415
|
+
) -> Set[int]:
|
|
416
|
+
candidates: Set[int] = set()
|
|
417
|
+
for cell in self._cells_for_aabb(self._object_aabb(obj)):
|
|
418
|
+
candidates.update(grid.get(cell, ()))
|
|
419
|
+
return candidates
|
|
341
420
|
|
|
342
421
|
def check_all_collisions(self) -> List[CollisionHit]:
|
|
343
422
|
"""
|
|
344
|
-
Check every
|
|
423
|
+
Check every potentially colliding pair.
|
|
345
424
|
|
|
346
425
|
Returns a list of CollisionHit for all colliding pairs.
|
|
347
426
|
Each pair appears at most once (i, j) with j > i.
|
|
348
427
|
"""
|
|
428
|
+
objects, grid = self._build_spatial_index()
|
|
349
429
|
hits: List[CollisionHit] = []
|
|
350
|
-
|
|
351
|
-
for i in
|
|
352
|
-
|
|
353
|
-
|
|
430
|
+
|
|
431
|
+
for i, obj in enumerate(objects):
|
|
432
|
+
candidate_indices = self._candidate_indices(obj, grid)
|
|
433
|
+
for j in sorted(candidate_indices):
|
|
434
|
+
if j <= i:
|
|
435
|
+
continue
|
|
436
|
+
hit = check_collision(objects[i], objects[j])
|
|
354
437
|
if hit is not None:
|
|
355
438
|
hits.append(hit)
|
|
356
439
|
return hits
|
|
@@ -359,13 +442,36 @@ class ObjectCollisionManager:
|
|
|
359
442
|
"""
|
|
360
443
|
Check one object against all others.
|
|
361
444
|
|
|
362
|
-
|
|
445
|
+
The queried object does not need to be managed. If it is managed,
|
|
446
|
+
comparison with itself is skipped by identity.
|
|
363
447
|
"""
|
|
448
|
+
objects, grid = self._build_spatial_index()
|
|
364
449
|
hits: List[CollisionHit] = []
|
|
365
|
-
for
|
|
450
|
+
for index in sorted(self._candidate_indices(obj, grid)):
|
|
451
|
+
other = objects[index]
|
|
366
452
|
if other is obj:
|
|
367
453
|
continue
|
|
368
454
|
hit = check_collision(obj, other)
|
|
369
455
|
if hit is not None:
|
|
370
456
|
hits.append(hit)
|
|
371
457
|
return hits
|
|
458
|
+
|
|
459
|
+
def check_object_first(self, obj: ICollidableObject) -> Optional[CollisionHit]:
|
|
460
|
+
"""
|
|
461
|
+
Check one object against all others and return the first collision hit.
|
|
462
|
+
|
|
463
|
+
The queried object does not need to be managed. If it is managed,
|
|
464
|
+
comparison with itself is skipped by identity.
|
|
465
|
+
|
|
466
|
+
Candidate iteration follows insertion order among the spatially relevant
|
|
467
|
+
managed objects.
|
|
468
|
+
"""
|
|
469
|
+
objects, grid = self._build_spatial_index()
|
|
470
|
+
for index in sorted(self._candidate_indices(obj, grid)):
|
|
471
|
+
other = objects[index]
|
|
472
|
+
if other is obj:
|
|
473
|
+
continue
|
|
474
|
+
hit = check_collision(obj, other)
|
|
475
|
+
if hit is not None:
|
|
476
|
+
return hit
|
|
477
|
+
return None
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tilemap-parser
|
|
3
|
-
Version: 3.1.
|
|
3
|
+
Version: 3.1.21
|
|
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
|
|
@@ -706,7 +706,7 @@ Standalone parser/loader for tilemap-editor JSON maps, sprite animations, and co
|
|
|
706
706
|
- **Map parsing**: Load and query tilemaps, layers, objects, and autotile data from JSON
|
|
707
707
|
- **Animation**: Frame-based sprite animation with `AnimationPlayer`
|
|
708
708
|
- **Collision (tile-based)**: Polygon collision detection for tilemaps with slide, platformer, and RPG movement modes via `CollisionRunner`
|
|
709
|
-
- **Collision (object-to-object)**:
|
|
709
|
+
- **Collision (object-to-object)**: Spatial-grid mixed-shape collision detection (rect, circle, capsule, polygon) with layer filtering via `ObjectCollisionManager`
|
|
710
710
|
- **Capsule support**: Full capsule collision against all shape types
|
|
711
711
|
- **Hit helpers**: `CollisionHit.resolve()`, `involves()`, `other()` for ergonomic separation
|
|
712
712
|
|
|
@@ -8,13 +8,15 @@ Covers:
|
|
|
8
8
|
- ObjectCollisionManager (add/remove, queries, duplicates, warnings)
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
|
+
import sys
|
|
11
12
|
import warnings
|
|
12
|
-
import pytest
|
|
13
13
|
from pathlib import Path
|
|
14
|
-
|
|
14
|
+
|
|
15
|
+
import pytest
|
|
15
16
|
|
|
16
17
|
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
|
|
17
18
|
|
|
19
|
+
import tilemap_parser.runtime.object_collision as object_collision_runtime
|
|
18
20
|
from tilemap_parser.parser.collision import (
|
|
19
21
|
CapsuleShape,
|
|
20
22
|
CircleShape,
|
|
@@ -24,15 +26,15 @@ from tilemap_parser.parser.collision import (
|
|
|
24
26
|
from tilemap_parser.runtime.object_collision import (
|
|
25
27
|
CollisionHit,
|
|
26
28
|
ObjectCollisionManager,
|
|
27
|
-
should_collide,
|
|
28
29
|
check_collision,
|
|
30
|
+
should_collide,
|
|
29
31
|
)
|
|
30
32
|
|
|
31
|
-
|
|
32
33
|
# ---------------------------------------------------------------------------
|
|
33
34
|
# Helpers — concrete objects implementing ICollidableObject
|
|
34
35
|
# ---------------------------------------------------------------------------
|
|
35
36
|
|
|
37
|
+
|
|
36
38
|
class _DummyObject:
|
|
37
39
|
"""Minimal collidable object for tests."""
|
|
38
40
|
|
|
@@ -67,6 +69,7 @@ def _make_poly(x, y, vertices, collision_layer=1, collision_mask=0xFFFFFFFF):
|
|
|
67
69
|
# should_collide
|
|
68
70
|
# ---------------------------------------------------------------------------
|
|
69
71
|
|
|
72
|
+
|
|
70
73
|
class TestShouldCollide:
|
|
71
74
|
def test_default_layers_collide(self):
|
|
72
75
|
a = _make_rect(0, 0, 10, 10)
|
|
@@ -93,6 +96,7 @@ class TestShouldCollide:
|
|
|
93
96
|
x = 0.0
|
|
94
97
|
y = 0.0
|
|
95
98
|
collision_shape = RectangleShape(width=10, height=10)
|
|
99
|
+
|
|
96
100
|
assert should_collide(BareObject(), BareObject()) is True
|
|
97
101
|
|
|
98
102
|
|
|
@@ -100,6 +104,7 @@ class TestShouldCollide:
|
|
|
100
104
|
# check_collision
|
|
101
105
|
# ---------------------------------------------------------------------------
|
|
102
106
|
|
|
107
|
+
|
|
103
108
|
class TestCheckCollisionCircleCircle:
|
|
104
109
|
def test_colliding(self):
|
|
105
110
|
a = _make_circle(0, 0, 10)
|
|
@@ -236,6 +241,7 @@ class TestCheckCollisionPolygon:
|
|
|
236
241
|
# ObjectCollisionManager
|
|
237
242
|
# ---------------------------------------------------------------------------
|
|
238
243
|
|
|
244
|
+
|
|
239
245
|
class TestManagerAddRemove:
|
|
240
246
|
def test_add_and_remove(self):
|
|
241
247
|
mgr = ObjectCollisionManager()
|
|
@@ -257,6 +263,22 @@ class TestManagerAddRemove:
|
|
|
257
263
|
assert "already" in str(w[0].message).lower()
|
|
258
264
|
assert len(mgr.objects) == 1
|
|
259
265
|
|
|
266
|
+
def test_add_value_equal_distinct_objects(self):
|
|
267
|
+
class EqualObject(_DummyObject):
|
|
268
|
+
def __eq__(self, other):
|
|
269
|
+
return isinstance(other, EqualObject)
|
|
270
|
+
|
|
271
|
+
mgr = ObjectCollisionManager()
|
|
272
|
+
a = EqualObject(0, 0)
|
|
273
|
+
b = EqualObject(20, 0)
|
|
274
|
+
|
|
275
|
+
mgr.add_object(a)
|
|
276
|
+
mgr.add_object(b)
|
|
277
|
+
|
|
278
|
+
assert len(mgr.objects) == 2
|
|
279
|
+
assert mgr.objects[0] is a
|
|
280
|
+
assert mgr.objects[1] is b
|
|
281
|
+
|
|
260
282
|
def test_remove_nonexistent_warns(self):
|
|
261
283
|
mgr = ObjectCollisionManager()
|
|
262
284
|
obj = _make_rect(0, 0, 10, 10)
|
|
@@ -267,8 +289,49 @@ class TestManagerAddRemove:
|
|
|
267
289
|
assert issubclass(w[0].category, UserWarning)
|
|
268
290
|
assert "not" in str(w[0].message).lower()
|
|
269
291
|
|
|
292
|
+
def test_remove_uses_identity_not_equality(self):
|
|
293
|
+
class EqualObject(_DummyObject):
|
|
294
|
+
def __eq__(self, other):
|
|
295
|
+
return isinstance(other, EqualObject)
|
|
296
|
+
|
|
297
|
+
mgr = ObjectCollisionManager()
|
|
298
|
+
a = EqualObject(0, 0)
|
|
299
|
+
b = EqualObject(20, 0)
|
|
300
|
+
mgr.add_object(a)
|
|
301
|
+
mgr.add_object(b)
|
|
302
|
+
|
|
303
|
+
mgr.remove_object(b)
|
|
304
|
+
|
|
305
|
+
assert len(mgr.objects) == 1
|
|
306
|
+
assert mgr.objects[0] is a
|
|
307
|
+
|
|
308
|
+
def test_construct_with_objects(self):
|
|
309
|
+
a = _make_rect(0, 0, 10, 10)
|
|
310
|
+
b = _make_rect(20, 0, 10, 10)
|
|
311
|
+
|
|
312
|
+
mgr = ObjectCollisionManager([a, b])
|
|
313
|
+
|
|
314
|
+
assert len(mgr) == 2
|
|
315
|
+
objects = list(mgr)
|
|
316
|
+
assert objects[0] is a
|
|
317
|
+
assert objects[1] is b
|
|
318
|
+
assert a in mgr
|
|
319
|
+
assert b in mgr
|
|
320
|
+
|
|
321
|
+
def test_clear(self):
|
|
322
|
+
mgr = ObjectCollisionManager([_make_rect(0, 0, 10, 10)])
|
|
323
|
+
|
|
324
|
+
mgr.clear()
|
|
325
|
+
|
|
326
|
+
assert len(mgr) == 0
|
|
327
|
+
assert mgr.objects == []
|
|
328
|
+
|
|
270
329
|
|
|
271
330
|
class TestManagerCheckAll:
|
|
331
|
+
def test_invalid_cell_size_raises(self):
|
|
332
|
+
with pytest.raises(ValueError, match="cell_size"):
|
|
333
|
+
ObjectCollisionManager(cell_size=0)
|
|
334
|
+
|
|
272
335
|
def test_no_duplicates(self):
|
|
273
336
|
"""Three overlapping circles — should produce exactly 3 pairs."""
|
|
274
337
|
mgr = ObjectCollisionManager()
|
|
@@ -286,6 +349,56 @@ class TestManagerCheckAll:
|
|
|
286
349
|
mgr.add_object(_make_rect(0, 0, 10, 10))
|
|
287
350
|
assert mgr.check_all_collisions() == []
|
|
288
351
|
|
|
352
|
+
def test_spatial_broadphase_skips_distant_pairs(self, monkeypatch):
|
|
353
|
+
mgr = ObjectCollisionManager(cell_size=16)
|
|
354
|
+
a = _make_rect(0, 0, 10, 10)
|
|
355
|
+
b = _make_rect(100, 0, 10, 10)
|
|
356
|
+
mgr.add_object(a)
|
|
357
|
+
mgr.add_object(b)
|
|
358
|
+
calls = []
|
|
359
|
+
|
|
360
|
+
def fake_check_collision(obj_a, obj_b):
|
|
361
|
+
calls.append((obj_a, obj_b))
|
|
362
|
+
return None
|
|
363
|
+
|
|
364
|
+
monkeypatch.setattr(
|
|
365
|
+
object_collision_runtime,
|
|
366
|
+
"check_collision",
|
|
367
|
+
fake_check_collision,
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
assert mgr.check_all_collisions() == []
|
|
371
|
+
assert calls == []
|
|
372
|
+
|
|
373
|
+
def test_large_object_spanning_cells_collides(self):
|
|
374
|
+
mgr = ObjectCollisionManager(cell_size=16)
|
|
375
|
+
large = _make_rect(0, 0, 64, 16)
|
|
376
|
+
small = _make_rect(48, 0, 8, 8)
|
|
377
|
+
mgr.add_object(large)
|
|
378
|
+
mgr.add_object(small)
|
|
379
|
+
|
|
380
|
+
hits = mgr.check_all_collisions()
|
|
381
|
+
|
|
382
|
+
assert len(hits) == 1
|
|
383
|
+
assert hits[0].object_a is large
|
|
384
|
+
assert hits[0].object_b is small
|
|
385
|
+
|
|
386
|
+
def test_moved_objects_are_reindexed_per_query(self):
|
|
387
|
+
mgr = ObjectCollisionManager(cell_size=16)
|
|
388
|
+
a = _make_rect(0, 0, 10, 10)
|
|
389
|
+
b = _make_rect(100, 0, 10, 10)
|
|
390
|
+
mgr.add_object(a)
|
|
391
|
+
mgr.add_object(b)
|
|
392
|
+
|
|
393
|
+
assert mgr.check_all_collisions() == []
|
|
394
|
+
|
|
395
|
+
b.x = 8
|
|
396
|
+
hits = mgr.check_all_collisions()
|
|
397
|
+
|
|
398
|
+
assert len(hits) == 1
|
|
399
|
+
assert hits[0].involves(a)
|
|
400
|
+
assert hits[0].involves(b)
|
|
401
|
+
|
|
289
402
|
|
|
290
403
|
class TestManagerCheckObject:
|
|
291
404
|
def test_returns_hits(self):
|
|
@@ -316,11 +429,59 @@ class TestManagerCheckObject:
|
|
|
316
429
|
hits = mgr.check_object(a)
|
|
317
430
|
assert hits == []
|
|
318
431
|
|
|
432
|
+
def test_unmanaged_query_object(self):
|
|
433
|
+
mgr = ObjectCollisionManager(cell_size=16)
|
|
434
|
+
query = _make_rect(0, 0, 10, 10)
|
|
435
|
+
managed = _make_rect(8, 0, 10, 10)
|
|
436
|
+
far = _make_rect(100, 0, 10, 10)
|
|
437
|
+
mgr.add_object(managed)
|
|
438
|
+
mgr.add_object(far)
|
|
439
|
+
|
|
440
|
+
hits = mgr.check_object(query)
|
|
441
|
+
|
|
442
|
+
assert len(hits) == 1
|
|
443
|
+
assert hits[0].object_a is query
|
|
444
|
+
assert hits[0].object_b is managed
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
class TestManagerCheckObjectFirst:
|
|
448
|
+
def test_returns_first_hit(self):
|
|
449
|
+
mgr = ObjectCollisionManager()
|
|
450
|
+
query = _make_circle(0, 0, 10)
|
|
451
|
+
first = _make_circle(15, 0, 10)
|
|
452
|
+
second = _make_circle(18, 0, 10)
|
|
453
|
+
far = _make_circle(100, 0, 10)
|
|
454
|
+
mgr.add_object(first)
|
|
455
|
+
mgr.add_object(second)
|
|
456
|
+
mgr.add_object(far)
|
|
457
|
+
|
|
458
|
+
hit = mgr.check_object_first(query)
|
|
459
|
+
|
|
460
|
+
assert hit is not None
|
|
461
|
+
assert hit.object_a is query
|
|
462
|
+
assert hit.object_b is first
|
|
463
|
+
|
|
464
|
+
def test_returns_none_when_no_hit(self):
|
|
465
|
+
mgr = ObjectCollisionManager()
|
|
466
|
+
query = _make_rect(0, 0, 10, 10)
|
|
467
|
+
far = _make_rect(100, 0, 10, 10)
|
|
468
|
+
mgr.add_object(far)
|
|
469
|
+
|
|
470
|
+
assert mgr.check_object_first(query) is None
|
|
471
|
+
|
|
472
|
+
def test_skips_self(self):
|
|
473
|
+
mgr = ObjectCollisionManager()
|
|
474
|
+
obj = _make_rect(0, 0, 10, 10)
|
|
475
|
+
mgr.add_object(obj)
|
|
476
|
+
|
|
477
|
+
assert mgr.check_object_first(obj) is None
|
|
478
|
+
|
|
319
479
|
|
|
320
480
|
# ---------------------------------------------------------------------------
|
|
321
481
|
# CollisionHit helpers
|
|
322
482
|
# ---------------------------------------------------------------------------
|
|
323
483
|
|
|
484
|
+
|
|
324
485
|
class TestCollisionHitHelpers:
|
|
325
486
|
def test_resolve_separates(self):
|
|
326
487
|
a = _make_circle(0, 0, 10)
|
|
@@ -364,8 +525,10 @@ class TestCollisionHitHelpers:
|
|
|
364
525
|
def test_slide_head_on(self):
|
|
365
526
|
"""Directly into surface → zero tangential motion."""
|
|
366
527
|
hit = CollisionHit(
|
|
367
|
-
object_a=None,
|
|
368
|
-
|
|
528
|
+
object_a=None,
|
|
529
|
+
object_b=None,
|
|
530
|
+
normal=(1.0, 0.0),
|
|
531
|
+
depth=5.0,
|
|
369
532
|
)
|
|
370
533
|
sx, sy = hit.slide_velocity(10.0, 0.0)
|
|
371
534
|
assert sx == 0.0
|
|
@@ -374,8 +537,10 @@ class TestCollisionHitHelpers:
|
|
|
374
537
|
def test_slide_angled(self):
|
|
375
538
|
"""Diagonal into surface → only perpendicular component removed."""
|
|
376
539
|
hit = CollisionHit(
|
|
377
|
-
object_a=None,
|
|
378
|
-
|
|
540
|
+
object_a=None,
|
|
541
|
+
object_b=None,
|
|
542
|
+
normal=(1.0, 0.0),
|
|
543
|
+
depth=5.0,
|
|
379
544
|
)
|
|
380
545
|
sx, sy = hit.slide_velocity(10.0, 5.0)
|
|
381
546
|
assert sx == 0.0 # x component fully removed
|
|
@@ -384,8 +549,10 @@ class TestCollisionHitHelpers:
|
|
|
384
549
|
def test_slide_parallel(self):
|
|
385
550
|
"""Velocity along the surface → unchanged."""
|
|
386
551
|
hit = CollisionHit(
|
|
387
|
-
object_a=None,
|
|
388
|
-
|
|
552
|
+
object_a=None,
|
|
553
|
+
object_b=None,
|
|
554
|
+
normal=(0.0, 1.0),
|
|
555
|
+
depth=5.0,
|
|
389
556
|
)
|
|
390
557
|
sx, sy = hit.slide_velocity(10.0, 0.0)
|
|
391
558
|
assert sx == 10.0
|
|
@@ -394,8 +561,10 @@ class TestCollisionHitHelpers:
|
|
|
394
561
|
def test_slide_away(self):
|
|
395
562
|
"""Velocity pointing away from surface → unchanged."""
|
|
396
563
|
hit = CollisionHit(
|
|
397
|
-
object_a=None,
|
|
398
|
-
|
|
564
|
+
object_a=None,
|
|
565
|
+
object_b=None,
|
|
566
|
+
normal=(1.0, 0.0),
|
|
567
|
+
depth=5.0,
|
|
399
568
|
)
|
|
400
569
|
sx, sy = hit.slide_velocity(-10.0, 0.0)
|
|
401
570
|
assert sx == -10.0
|
|
@@ -404,8 +573,10 @@ class TestCollisionHitHelpers:
|
|
|
404
573
|
def test_slide_zero(self):
|
|
405
574
|
"""Zero velocity → zero."""
|
|
406
575
|
hit = CollisionHit(
|
|
407
|
-
object_a=None,
|
|
408
|
-
|
|
576
|
+
object_a=None,
|
|
577
|
+
object_b=None,
|
|
578
|
+
normal=(1.0, 0.0),
|
|
579
|
+
depth=5.0,
|
|
409
580
|
)
|
|
410
581
|
sx, sy = hit.slide_velocity(0.0, 0.0)
|
|
411
582
|
assert sx == 0.0
|
|
@@ -414,10 +585,13 @@ class TestCollisionHitHelpers:
|
|
|
414
585
|
def test_slide_diagonal_normal(self):
|
|
415
586
|
"""Normal at 45°, velocity into surface → slide along surface."""
|
|
416
587
|
import math
|
|
588
|
+
|
|
417
589
|
n = (1.0 / math.sqrt(2), 1.0 / math.sqrt(2))
|
|
418
590
|
hit = CollisionHit(
|
|
419
|
-
object_a=None,
|
|
420
|
-
|
|
591
|
+
object_a=None,
|
|
592
|
+
object_b=None,
|
|
593
|
+
normal=n,
|
|
594
|
+
depth=5.0,
|
|
421
595
|
)
|
|
422
596
|
sx, sy = hit.slide_velocity(1.0, 0.0)
|
|
423
597
|
# Dot = 1/sqrt(2) ≈ 0.707
|
|
@@ -430,6 +604,7 @@ class TestCollisionHitHelpers:
|
|
|
430
604
|
# Capsule collision
|
|
431
605
|
# ---------------------------------------------------------------------------
|
|
432
606
|
|
|
607
|
+
|
|
433
608
|
def _make_capsule(
|
|
434
609
|
x: float = 0.0,
|
|
435
610
|
y: float = 0.0,
|
|
@@ -441,7 +616,8 @@ def _make_capsule(
|
|
|
441
616
|
collision_mask: int = 0xFFFFFFFF,
|
|
442
617
|
) -> _DummyObject:
|
|
443
618
|
return _DummyObject(
|
|
444
|
-
x=x,
|
|
619
|
+
x=x,
|
|
620
|
+
y=y,
|
|
445
621
|
shape=CapsuleShape(radius=radius, height=height, offset=(ox, oy)),
|
|
446
622
|
collision_layer=collision_layer,
|
|
447
623
|
collision_mask=collision_mask,
|
|
@@ -142,9 +142,9 @@ class TestShapeBounds:
|
|
|
142
142
|
sprite = MockSprite(x=100, y=200, shape=CapsuleShape(radius=8, height=48, offset=(0, 0)))
|
|
143
143
|
left, top, right, bottom = get_shape_bounds(sprite)
|
|
144
144
|
assert left == 92
|
|
145
|
-
assert top ==
|
|
145
|
+
assert top == 192
|
|
146
146
|
assert right == 108
|
|
147
|
-
assert bottom ==
|
|
147
|
+
assert bottom == 256
|
|
148
148
|
|
|
149
149
|
|
|
150
150
|
# ===========================================================================
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tilemap_parser-3.1.19 → tilemap_parser-3.1.21}/src/tilemap_parser/parser/collision_loader.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tilemap_parser-3.1.19 → tilemap_parser-3.1.21}/src/tilemap_parser/runtime/animation_player.py
RENAMED
|
File without changes
|
|
File without changes
|
{tilemap_parser-3.1.19 → tilemap_parser-3.1.21}/src/tilemap_parser/runtime/collision_cache.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tilemap_parser-3.1.19 → tilemap_parser-3.1.21}/src/tilemap_parser/runtime/tile_collision.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tilemap_parser-3.1.19 → tilemap_parser-3.1.21}/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
|