tilemap-parser 4.2.1__tar.gz → 4.2.3__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.1 → tilemap_parser-4.2.3}/PKG-INFO +1 -1
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.3}/pyproject.toml +1 -1
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.3}/src/tilemap_parser/__init__.py +1 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.3}/src/tilemap_parser/runtime/__init__.py +2 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.3}/src/tilemap_parser/runtime/map_object.py +37 -6
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.3}/src/tilemap_parser/runtime/object_collision.py +3 -10
- tilemap_parser-4.2.3/src/tilemap_parser/runtime/protocols.py +28 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.3}/src/tilemap_parser/runtime/tile_collision.py +12 -11
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.3}/src/tilemap_parser.egg-info/PKG-INFO +1 -1
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.3}/src/tilemap_parser.egg-info/SOURCES.txt +1 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.3}/tests/test_map_object.py +595 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.3}/LICENSE +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.3}/README.md +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.3}/setup.cfg +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.3}/src/tilemap_parser/parser/__init__.py +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.3}/src/tilemap_parser/parser/animation.py +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.3}/src/tilemap_parser/parser/collision.py +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.3}/src/tilemap_parser/parser/collision_loader.py +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.3}/src/tilemap_parser/parser/map_parse.py +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.3}/src/tilemap_parser/parser/node_parse.py +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.3}/src/tilemap_parser/parser/particle.py +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.3}/src/tilemap_parser/parser/tmx_converter.py +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.3}/src/tilemap_parser/runtime/animation_player.py +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.3}/src/tilemap_parser/runtime/area_node.py +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.3}/src/tilemap_parser/runtime/camera.py +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.3}/src/tilemap_parser/runtime/collision_cache.py +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.3}/src/tilemap_parser/runtime/map_loader.py +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.3}/src/tilemap_parser/runtime/particles.py +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.3}/src/tilemap_parser/runtime/renderer.py +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.3}/src/tilemap_parser/utils/__init__.py +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.3}/src/tilemap_parser/utils/geometry.py +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.3}/src/tilemap_parser.egg-info/dependency_links.txt +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.3}/src/tilemap_parser.egg-info/requires.txt +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.3}/src/tilemap_parser.egg-info/top_level.txt +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.3}/tests/test_camera.py +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.3}/tests/test_collision.py +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.3}/tests/test_geometry.py +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.3}/tests/test_gid_collision.py +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.3}/tests/test_integration_animation.py +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.3}/tests/test_integration_collision.py +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.3}/tests/test_integration_map_loader.py +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.3}/tests/test_map_loader.py +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.3}/tests/test_move_grounded.py +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.3}/tests/test_object_collision.py +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.3}/tests/test_object_surfaces.py +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.3}/tests/test_particle_emitter.py +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.3}/tests/test_render_scale.py +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.3}/tests/test_tile_collision.py +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.3}/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.3
|
|
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.3"
|
|
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"
|
|
@@ -18,6 +18,7 @@ from .tile_collision import (
|
|
|
18
18
|
MovementMode,
|
|
19
19
|
rect_vs_tilemap,
|
|
20
20
|
)
|
|
21
|
+
from .protocols import ICollidable
|
|
21
22
|
from .map_loader import TilemapData, load_map
|
|
22
23
|
from .object_collision import (
|
|
23
24
|
CollisionHit,
|
|
@@ -45,6 +46,7 @@ __all__ = [
|
|
|
45
46
|
"CollisionHit",
|
|
46
47
|
"CollisionResult",
|
|
47
48
|
"CollisionRunner",
|
|
49
|
+
"ICollidable",
|
|
48
50
|
"ICollidableObject",
|
|
49
51
|
"ICollidableSprite",
|
|
50
52
|
"LayerRenderStats",
|
|
@@ -18,6 +18,7 @@ from __future__ import annotations
|
|
|
18
18
|
from pathlib import Path
|
|
19
19
|
from typing import Dict, List, Optional, Union
|
|
20
20
|
|
|
21
|
+
import pygame
|
|
21
22
|
from pygame import Surface
|
|
22
23
|
|
|
23
24
|
from ..parser.collision import CollisionPolygon, ObjectCollisionData
|
|
@@ -31,6 +32,12 @@ class MapObject:
|
|
|
31
32
|
A game object loaded from a tilemap, carrying a surface for
|
|
32
33
|
rendering and collision data for physics.
|
|
33
34
|
|
|
35
|
+
All spatial data (``x``, ``y``, ``surface`` size, and collision
|
|
36
|
+
polygon vertices) is pre-scaled by the map's ``render_scale``.
|
|
37
|
+
Units are in *effective* pixels (``render_scale × tile_size``).
|
|
38
|
+
The object is ready for direct use in a game loop that runs in
|
|
39
|
+
effective-pixel space — no additional scaling needed.
|
|
40
|
+
|
|
34
41
|
Satisfies the :class:`ICollidableObject` protocol so it can be
|
|
35
42
|
added directly to an :class:`ObjectCollisionManager`.
|
|
36
43
|
|
|
@@ -89,9 +96,26 @@ def load_map_objects(
|
|
|
89
96
|
|
|
90
97
|
Iterates every object layer in *tilemap_data*, resolves the
|
|
91
98
|
corresponding ``.object_collision.json`` from *collision_dir*, and
|
|
92
|
-
builds :class:`MapObject` instances with surfaces,
|
|
93
|
-
collision shapes
|
|
94
|
-
|
|
99
|
+
builds :class:`MapObject` instances with pre-scaled surfaces,
|
|
100
|
+
positions, and collision shapes.
|
|
101
|
+
|
|
102
|
+
**render_scale transparency**
|
|
103
|
+
|
|
104
|
+
All spatial data is automatically scaled by the map's
|
|
105
|
+
``render_scale`` (from ``tilemap_data.render_scale``):
|
|
106
|
+
|
|
107
|
+
* ``MapObject.x`` / ``MapObject.y`` — raw map coords × ``rs``
|
|
108
|
+
(stored as ``float``; fractional pixel positions are preserved).
|
|
109
|
+
* ``MapObject.surface`` — surface is scaled by ``rs`` via
|
|
110
|
+
:func:`pygame.transform.scale` (no-op when ``rs == 1.0``).
|
|
111
|
+
Raster dimensions are truncated to integers via ``int()``
|
|
112
|
+
to satisfy :func:`pygame.transform.scale` requirements.
|
|
113
|
+
* Collision polygon vertices and ``region_rect`` offsets are
|
|
114
|
+
multiplied by ``rs`` via :meth:`CollisionPolygon.transform`.
|
|
115
|
+
|
|
116
|
+
The returned objects are ready for a game loop that runs in
|
|
117
|
+
effective-pixel space (``render_scale × tile_size``). No
|
|
118
|
+
additional scaling is required by the caller.
|
|
95
119
|
|
|
96
120
|
Collision data is cached per tileset index so the same file is
|
|
97
121
|
never loaded twice.
|
|
@@ -123,6 +147,13 @@ def load_map_objects(
|
|
|
123
147
|
continue
|
|
124
148
|
|
|
125
149
|
surf, x, y = surf_x_y
|
|
150
|
+
rs = tilemap_data.render_scale
|
|
151
|
+
x = x * rs
|
|
152
|
+
y = y * rs
|
|
153
|
+
|
|
154
|
+
if rs != 1.0 and surf is not None:
|
|
155
|
+
w, h = surf.get_size()
|
|
156
|
+
surf = pygame.transform.scale(surf, (int(w * rs), int(h * rs)))
|
|
126
157
|
|
|
127
158
|
ttype = obj.ttype
|
|
128
159
|
if ttype not in loaded_collision:
|
|
@@ -142,9 +173,9 @@ def load_map_objects(
|
|
|
142
173
|
if not world_shapes:
|
|
143
174
|
region_layer = region.collision_layer
|
|
144
175
|
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)
|
|
176
|
+
ox = region.region_rect[0] * rs
|
|
177
|
+
oy = region.region_rect[1] * rs
|
|
178
|
+
world_shapes.extend(shape.transform(ox, oy, rs) for shape in region.shapes)
|
|
148
179
|
if not world_shapes:
|
|
149
180
|
continue
|
|
150
181
|
|
{tilemap_parser-4.2.1 → tilemap_parser-4.2.3}/src/tilemap_parser/runtime/object_collision.py
RENAMED
|
@@ -33,27 +33,20 @@ from ..utils.geometry import (
|
|
|
33
33
|
rect_vs_circle,
|
|
34
34
|
rect_vs_rect,
|
|
35
35
|
)
|
|
36
|
+
from .protocols import ICollidable
|
|
36
37
|
|
|
37
38
|
|
|
38
|
-
class ICollidableObject(Protocol):
|
|
39
|
+
class ICollidableObject(ICollidable, Protocol):
|
|
39
40
|
"""
|
|
40
41
|
Protocol for objects that can collide.
|
|
41
42
|
|
|
42
|
-
|
|
43
|
-
x: World X position
|
|
44
|
-
y: World Y position
|
|
45
|
-
collision_shape: Shape for collision
|
|
46
|
-
(RectangleShape, CircleShape, CapsuleShape, or CollisionPolygon)
|
|
43
|
+
All required attributes (x, y, collision_shape) are inherited from ICollidable.
|
|
47
44
|
|
|
48
45
|
Optional attributes (with defaults):
|
|
49
46
|
collision_layer: Layer this object is on (default: 1)
|
|
50
47
|
collision_mask: Layers to collide with (default: 0xFFFFFFFF)
|
|
51
48
|
"""
|
|
52
49
|
|
|
53
|
-
x: float
|
|
54
|
-
y: float
|
|
55
|
-
collision_shape: Union[RectangleShape, CircleShape, CapsuleShape, CollisionPolygon]
|
|
56
|
-
|
|
57
50
|
|
|
58
51
|
@dataclass(slots=True)
|
|
59
52
|
class CollisionHit:
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Protocol, Union
|
|
4
|
+
|
|
5
|
+
from ..parser.collision import (
|
|
6
|
+
CapsuleShape,
|
|
7
|
+
CircleShape,
|
|
8
|
+
CollisionPolygon,
|
|
9
|
+
RectangleShape,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ICollidable(Protocol):
|
|
14
|
+
"""Base protocol for anything with a world position and collision shape.
|
|
15
|
+
|
|
16
|
+
This is the minimal interface for participating in collision detection.
|
|
17
|
+
All collidable protocols (ICollidableSprite, ICollidableObject) extend this.
|
|
18
|
+
|
|
19
|
+
Required attributes:
|
|
20
|
+
x (float): World X position
|
|
21
|
+
y (float): World Y position
|
|
22
|
+
collision_shape: Shape used for collision detection
|
|
23
|
+
(RectangleShape, CircleShape, CapsuleShape, or CollisionPolygon)
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
x: float
|
|
27
|
+
y: float
|
|
28
|
+
collision_shape: Union[RectangleShape, CircleShape, CapsuleShape, CollisionPolygon]
|
|
@@ -23,12 +23,13 @@ from ..parser.collision import (
|
|
|
23
23
|
RectangleShape,
|
|
24
24
|
TilesetCollision,
|
|
25
25
|
)
|
|
26
|
+
from .protocols import ICollidable
|
|
26
27
|
|
|
27
28
|
Point = Tuple[float, float]
|
|
28
29
|
Vector2 = Tuple[float, float]
|
|
29
30
|
|
|
30
31
|
|
|
31
|
-
class ICollidableSprite(Protocol):
|
|
32
|
+
class ICollidableSprite(ICollidable, Protocol):
|
|
32
33
|
"""
|
|
33
34
|
Interface that any sprite/character class must implement to use collision runners.
|
|
34
35
|
|
|
@@ -303,7 +304,7 @@ def _circle_polygon_collision_offset(
|
|
|
303
304
|
return False
|
|
304
305
|
|
|
305
306
|
|
|
306
|
-
def get_shape_bounds(sprite:
|
|
307
|
+
def get_shape_bounds(sprite: ICollidable) -> Tuple[float, float, float, float]:
|
|
307
308
|
"""Get AABB bounds for sprite (left, top, right, bottom)"""
|
|
308
309
|
shape = sprite.collision_shape
|
|
309
310
|
if isinstance(shape, RectangleShape):
|
|
@@ -328,7 +329,7 @@ def get_shape_bounds(sprite: ICollidableSprite) -> Tuple[float, float, float, fl
|
|
|
328
329
|
|
|
329
330
|
|
|
330
331
|
def check_sprite_polygon_collision(
|
|
331
|
-
sprite:
|
|
332
|
+
sprite: ICollidable, polygon: CollisionPolygon
|
|
332
333
|
) -> bool:
|
|
333
334
|
"""Check if sprite collides with a world-space polygon (legacy / public API)."""
|
|
334
335
|
shape = sprite.collision_shape
|
|
@@ -349,7 +350,7 @@ def check_sprite_polygon_collision(
|
|
|
349
350
|
|
|
350
351
|
|
|
351
352
|
def _check_sprite_polygon_offset(
|
|
352
|
-
sprite:
|
|
353
|
+
sprite: ICollidable,
|
|
353
354
|
polygon: CollisionPolygon,
|
|
354
355
|
ox: float,
|
|
355
356
|
oy: float,
|
|
@@ -489,7 +490,7 @@ class CollisionRunner:
|
|
|
489
490
|
self,
|
|
490
491
|
tileset_collision: TilesetCollision,
|
|
491
492
|
tile_map: dict,
|
|
492
|
-
sprite:
|
|
493
|
+
sprite: ICollidable,
|
|
493
494
|
margin: int = 1,
|
|
494
495
|
) -> List[CollisionPolygon]:
|
|
495
496
|
"""
|
|
@@ -529,7 +530,7 @@ class CollisionRunner:
|
|
|
529
530
|
|
|
530
531
|
def _collides_at(
|
|
531
532
|
self,
|
|
532
|
-
sprite:
|
|
533
|
+
sprite: ICollidable,
|
|
533
534
|
tileset_collision: TilesetCollision,
|
|
534
535
|
tile_map: dict,
|
|
535
536
|
margin: int = 1,
|
|
@@ -567,7 +568,7 @@ class CollisionRunner:
|
|
|
567
568
|
|
|
568
569
|
def _first_colliding_shape(
|
|
569
570
|
self,
|
|
570
|
-
sprite:
|
|
571
|
+
sprite: ICollidable,
|
|
571
572
|
tileset_collision: TilesetCollision,
|
|
572
573
|
tile_map: dict,
|
|
573
574
|
margin: int = 1,
|
|
@@ -603,7 +604,7 @@ class CollisionRunner:
|
|
|
603
604
|
|
|
604
605
|
def move_and_slide(
|
|
605
606
|
self,
|
|
606
|
-
sprite:
|
|
607
|
+
sprite: ICollidable,
|
|
607
608
|
tileset_collision: TilesetCollision,
|
|
608
609
|
tile_map: dict,
|
|
609
610
|
delta_x: float,
|
|
@@ -729,7 +730,7 @@ class CollisionRunner:
|
|
|
729
730
|
|
|
730
731
|
def _get_collision_normal_from_motion(
|
|
731
732
|
self,
|
|
732
|
-
sprite:
|
|
733
|
+
sprite: ICollidable,
|
|
733
734
|
polygon: CollisionPolygon,
|
|
734
735
|
ox: float,
|
|
735
736
|
oy: float,
|
|
@@ -1745,7 +1746,7 @@ class CollisionRunner:
|
|
|
1745
1746
|
|
|
1746
1747
|
def move_rpg(
|
|
1747
1748
|
self,
|
|
1748
|
-
sprite:
|
|
1749
|
+
sprite: ICollidable,
|
|
1749
1750
|
tileset_collision: TilesetCollision,
|
|
1750
1751
|
tile_map: dict,
|
|
1751
1752
|
delta_x: float,
|
|
@@ -1814,7 +1815,7 @@ class CollisionRunner:
|
|
|
1814
1815
|
|
|
1815
1816
|
def move(
|
|
1816
1817
|
self,
|
|
1817
|
-
sprite:
|
|
1818
|
+
sprite: ICollidable,
|
|
1818
1819
|
tileset_collision: TilesetCollision,
|
|
1819
1820
|
tile_map: dict,
|
|
1820
1821
|
delta_x: float = 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.3
|
|
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
|
|
@@ -24,6 +24,7 @@ src/tilemap_parser/runtime/map_loader.py
|
|
|
24
24
|
src/tilemap_parser/runtime/map_object.py
|
|
25
25
|
src/tilemap_parser/runtime/object_collision.py
|
|
26
26
|
src/tilemap_parser/runtime/particles.py
|
|
27
|
+
src/tilemap_parser/runtime/protocols.py
|
|
27
28
|
src/tilemap_parser/runtime/renderer.py
|
|
28
29
|
src/tilemap_parser/runtime/tile_collision.py
|
|
29
30
|
src/tilemap_parser/utils/__init__.py
|
|
@@ -652,6 +652,601 @@ class TestLoadMapObjects:
|
|
|
652
652
|
hit = check_collision(loaded, manual)
|
|
653
653
|
assert hit is not None, "loaded and manual object at same world pos should collide"
|
|
654
654
|
|
|
655
|
+
def test_default_scale_no_op(self, basic_map):
|
|
656
|
+
"""render_scale=1.0 (default) behaves identically to before."""
|
|
657
|
+
td, collision_dir, _ = basic_map
|
|
658
|
+
assert td.render_scale == 1.0
|
|
659
|
+
objects = load_map_objects(td, collision_dir)
|
|
660
|
+
assert len(objects) == 1
|
|
661
|
+
obj = objects[0]
|
|
662
|
+
# Positions are raw (no scaling)
|
|
663
|
+
assert obj.x == 100
|
|
664
|
+
assert obj.y == 200
|
|
665
|
+
# Vertices are raw
|
|
666
|
+
verts = obj.collision_shape.vertices
|
|
667
|
+
assert verts[0] == (0, 0)
|
|
668
|
+
assert verts[1] == (16, 0)
|
|
669
|
+
assert verts[2] == (16, 16)
|
|
670
|
+
assert verts[3] == (0, 16)
|
|
671
|
+
|
|
672
|
+
def test_render_scale_3x(self):
|
|
673
|
+
"""render_scale=3.0 scales positions and polygon vertices."""
|
|
674
|
+
from tilemap_parser.runtime.map_object import load_map_objects
|
|
675
|
+
from tilemap_parser.runtime.map_loader import TilemapData
|
|
676
|
+
|
|
677
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
678
|
+
tmp = Path(tmp)
|
|
679
|
+
assets_dir = tmp / "assets"
|
|
680
|
+
data_dir = tmp / "data"
|
|
681
|
+
collision_dir = tmp / "data" / "collision"
|
|
682
|
+
assets_dir.mkdir(parents=True)
|
|
683
|
+
data_dir.mkdir(parents=True)
|
|
684
|
+
collision_dir.mkdir(parents=True)
|
|
685
|
+
|
|
686
|
+
png = assets_dir / "tileset.png"
|
|
687
|
+
_make_minimal_png(png, (32, 16))
|
|
688
|
+
|
|
689
|
+
collision_payload = {
|
|
690
|
+
"tileset_name": "tileset",
|
|
691
|
+
"regions": {
|
|
692
|
+
"region_r": {
|
|
693
|
+
"region_id": "region_r",
|
|
694
|
+
"region_rect": [5, 10, 16, 16],
|
|
695
|
+
"name": "R",
|
|
696
|
+
"shapes": [{"type": "polygon", "vertices": [[0, 0], [16, 0], [16, 16], [0, 16]], "one_way": False}],
|
|
697
|
+
"properties": {},
|
|
698
|
+
},
|
|
699
|
+
},
|
|
700
|
+
}
|
|
701
|
+
coll_path = collision_dir / "tileset.object_collision.json"
|
|
702
|
+
with open(coll_path, "w") as f:
|
|
703
|
+
json.dump(collision_payload, f, indent=2)
|
|
704
|
+
|
|
705
|
+
# Map with render_scale=3.0
|
|
706
|
+
payload = {
|
|
707
|
+
"meta": {
|
|
708
|
+
"tile_size": "16;16",
|
|
709
|
+
"map_size": "10;10",
|
|
710
|
+
"version": "1.1",
|
|
711
|
+
"render_scale": 3.0,
|
|
712
|
+
},
|
|
713
|
+
"resources": {"tilesets": [{"path": "../assets/tileset.png", "type": "object"}]},
|
|
714
|
+
"project_state": {"rules": [], "groups": []},
|
|
715
|
+
"data": {
|
|
716
|
+
"ongrid": {},
|
|
717
|
+
"layers": [
|
|
718
|
+
{
|
|
719
|
+
"name": "Object Layer",
|
|
720
|
+
"type": "object",
|
|
721
|
+
"visible": True,
|
|
722
|
+
"locked": False,
|
|
723
|
+
"opacity": 1.0,
|
|
724
|
+
"z_index": 0,
|
|
725
|
+
"properties": {},
|
|
726
|
+
"tiles": {},
|
|
727
|
+
"objects": {
|
|
728
|
+
"1": {
|
|
729
|
+
"area": {"x": 100, "y": 200, "w": 16, "h": 16},
|
|
730
|
+
"ttype": 0,
|
|
731
|
+
"tileset_type": "object",
|
|
732
|
+
"variant": 0,
|
|
733
|
+
"properties": {},
|
|
734
|
+
},
|
|
735
|
+
},
|
|
736
|
+
"next_object_id": 2,
|
|
737
|
+
},
|
|
738
|
+
],
|
|
739
|
+
},
|
|
740
|
+
}
|
|
741
|
+
map_path = data_dir / "test_map.json"
|
|
742
|
+
with open(map_path, "w") as f:
|
|
743
|
+
json.dump(payload, f, indent=2)
|
|
744
|
+
|
|
745
|
+
td = TilemapData.load(map_path)
|
|
746
|
+
assert td.render_scale == 3.0
|
|
747
|
+
objects = load_map_objects(td, collision_dir)
|
|
748
|
+
assert len(objects) == 1
|
|
749
|
+
obj = objects[0]
|
|
750
|
+
|
|
751
|
+
# Position scaled by 3x
|
|
752
|
+
assert obj.x == 300 # 100 * 3
|
|
753
|
+
assert obj.y == 600 # 200 * 3
|
|
754
|
+
|
|
755
|
+
# region_rect [5, 10] scaled by 3x → ox=15, oy=30
|
|
756
|
+
# Vertices scaled by 3x: (0,0)→(0,0), (16,0)→(48,0), (16,16)→(48,48), (0,16)→(0,48)
|
|
757
|
+
# World = ox + vx*rs: (15+0, 30+0) = (15, 30)
|
|
758
|
+
# (15+48, 30+0) = (63, 30)
|
|
759
|
+
# (15+48, 30+48) = (63, 78)
|
|
760
|
+
# (15+0, 30+48) = (15, 78)
|
|
761
|
+
verts = obj.collision_shape.vertices
|
|
762
|
+
assert len(verts) == 4
|
|
763
|
+
assert verts[0] == pytest.approx((15.0, 30.0))
|
|
764
|
+
assert verts[1] == pytest.approx((63.0, 30.0))
|
|
765
|
+
assert verts[2] == pytest.approx((63.0, 78.0))
|
|
766
|
+
assert verts[3] == pytest.approx((15.0, 78.0))
|
|
767
|
+
|
|
768
|
+
# Surface scaled by 3x: tileset is 32×16, variant 0 is 16×16
|
|
769
|
+
assert obj.surface.get_size() == (48, 48) # 16*3, 16*3
|
|
770
|
+
|
|
771
|
+
def test_render_scale_surface_scaled(self):
|
|
772
|
+
"""Surface is scaled by render_scale in load_map_objects."""
|
|
773
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
774
|
+
tmp = Path(tmp)
|
|
775
|
+
assets_dir = tmp / "assets"
|
|
776
|
+
data_dir = tmp / "data"
|
|
777
|
+
collision_dir = tmp / "data" / "collision"
|
|
778
|
+
assets_dir.mkdir(parents=True)
|
|
779
|
+
data_dir.mkdir(parents=True)
|
|
780
|
+
collision_dir.mkdir(parents=True)
|
|
781
|
+
|
|
782
|
+
png = assets_dir / "tileset.png"
|
|
783
|
+
# Create a 64×32 tileset with 2 variants (16×16 each)
|
|
784
|
+
surf = pygame.Surface((64, 32))
|
|
785
|
+
surf.fill((255, 0, 255))
|
|
786
|
+
pygame.image.save(surf, str(png))
|
|
787
|
+
|
|
788
|
+
collision_payload = {
|
|
789
|
+
"tileset_name": "tileset",
|
|
790
|
+
"regions": {
|
|
791
|
+
"region_r": {
|
|
792
|
+
"region_id": "region_r",
|
|
793
|
+
"region_rect": [0, 0, 16, 16],
|
|
794
|
+
"name": "R",
|
|
795
|
+
"shapes": [{"type": "polygon", "vertices": [[0, 0], [16, 0], [16, 16], [0, 16]], "one_way": False}],
|
|
796
|
+
"properties": {},
|
|
797
|
+
},
|
|
798
|
+
},
|
|
799
|
+
}
|
|
800
|
+
coll_path = collision_dir / "tileset.object_collision.json"
|
|
801
|
+
with open(coll_path, "w") as f:
|
|
802
|
+
json.dump(collision_payload, f, indent=2)
|
|
803
|
+
|
|
804
|
+
for rs_val, expected_w, expected_h in [(1.0, 16, 16), (2.0, 32, 32), (3.0, 48, 48), (1.5, 24, 24)]:
|
|
805
|
+
payload = {
|
|
806
|
+
"meta": {
|
|
807
|
+
"tile_size": "16;16",
|
|
808
|
+
"map_size": "10;10",
|
|
809
|
+
"version": "1.1",
|
|
810
|
+
"render_scale": rs_val,
|
|
811
|
+
},
|
|
812
|
+
"resources": {"tilesets": [{"path": "../assets/tileset.png", "type": "object"}]},
|
|
813
|
+
"project_state": {"rules": [], "groups": []},
|
|
814
|
+
"data": {
|
|
815
|
+
"ongrid": {},
|
|
816
|
+
"layers": [
|
|
817
|
+
{
|
|
818
|
+
"name": "Object Layer",
|
|
819
|
+
"type": "object",
|
|
820
|
+
"visible": True,
|
|
821
|
+
"locked": False,
|
|
822
|
+
"opacity": 1.0,
|
|
823
|
+
"z_index": 0,
|
|
824
|
+
"properties": {},
|
|
825
|
+
"tiles": {},
|
|
826
|
+
"objects": {
|
|
827
|
+
"1": {
|
|
828
|
+
"area": {"x": 10, "y": 20, "w": 16, "h": 16},
|
|
829
|
+
"ttype": 0,
|
|
830
|
+
"tileset_type": "object",
|
|
831
|
+
"variant": 0,
|
|
832
|
+
"properties": {},
|
|
833
|
+
},
|
|
834
|
+
},
|
|
835
|
+
"next_object_id": 2,
|
|
836
|
+
},
|
|
837
|
+
],
|
|
838
|
+
},
|
|
839
|
+
}
|
|
840
|
+
map_path = data_dir / "test_map.json"
|
|
841
|
+
with open(map_path, "w") as f:
|
|
842
|
+
json.dump(payload, f, indent=2)
|
|
843
|
+
|
|
844
|
+
td = TilemapData.load(map_path)
|
|
845
|
+
objects = load_map_objects(td, collision_dir)
|
|
846
|
+
assert len(objects) == 1
|
|
847
|
+
obj = objects[0]
|
|
848
|
+
assert obj.surface.get_size() == (expected_w, expected_h), \
|
|
849
|
+
f"rs={rs_val}: expected ({expected_w},{expected_h}) got {obj.surface.get_size()}"
|
|
850
|
+
assert obj.x == 10 * rs_val, f"rs={rs_val}: x={obj.x} != {10 * rs_val}"
|
|
851
|
+
assert obj.y == 20 * rs_val, f"rs={rs_val}: y={obj.y} != {20 * rs_val}"
|
|
852
|
+
|
|
853
|
+
# Fractional rs with odd dimensions to exercise surface truncation
|
|
854
|
+
odd_png = assets_dir / "odd_tileset.png"
|
|
855
|
+
odd_surf = pygame.Surface((17, 35))
|
|
856
|
+
odd_surf.fill((0, 255, 0))
|
|
857
|
+
pygame.image.save(odd_surf, str(odd_png))
|
|
858
|
+
|
|
859
|
+
odd_collision = {
|
|
860
|
+
"tileset_name": "odd_tileset",
|
|
861
|
+
"regions": {
|
|
862
|
+
"region_r": {
|
|
863
|
+
"region_id": "region_r",
|
|
864
|
+
"region_rect": [0, 0, 17, 35],
|
|
865
|
+
"name": "R",
|
|
866
|
+
"shapes": [{"type": "polygon", "vertices": [[0, 0], [17, 0], [17, 35], [0, 35]], "one_way": False}],
|
|
867
|
+
"properties": {},
|
|
868
|
+
},
|
|
869
|
+
},
|
|
870
|
+
}
|
|
871
|
+
odd_coll_path = collision_dir / "odd_tileset.object_collision.json"
|
|
872
|
+
with open(odd_coll_path, "w") as f:
|
|
873
|
+
json.dump(odd_collision, f, indent=2)
|
|
874
|
+
|
|
875
|
+
payload_odd = {
|
|
876
|
+
"meta": {
|
|
877
|
+
"tile_size": "16;16",
|
|
878
|
+
"map_size": "10;10",
|
|
879
|
+
"version": "1.1",
|
|
880
|
+
"render_scale": 1.5,
|
|
881
|
+
},
|
|
882
|
+
"resources": {"tilesets": [{"path": "../assets/odd_tileset.png", "type": "object"}]},
|
|
883
|
+
"project_state": {"rules": [], "groups": []},
|
|
884
|
+
"data": {
|
|
885
|
+
"ongrid": {},
|
|
886
|
+
"layers": [
|
|
887
|
+
{
|
|
888
|
+
"name": "Object Layer",
|
|
889
|
+
"type": "object",
|
|
890
|
+
"visible": True,
|
|
891
|
+
"locked": False,
|
|
892
|
+
"opacity": 1.0,
|
|
893
|
+
"z_index": 0,
|
|
894
|
+
"properties": {},
|
|
895
|
+
"tiles": {},
|
|
896
|
+
"objects": {
|
|
897
|
+
"1": {
|
|
898
|
+
"area": {"x": 11, "y": 21, "w": 17, "h": 35},
|
|
899
|
+
"ttype": 0,
|
|
900
|
+
"tileset_type": "object",
|
|
901
|
+
"variant": 0,
|
|
902
|
+
"properties": {},
|
|
903
|
+
},
|
|
904
|
+
},
|
|
905
|
+
"next_object_id": 2,
|
|
906
|
+
},
|
|
907
|
+
],
|
|
908
|
+
},
|
|
909
|
+
}
|
|
910
|
+
odd_map_path = data_dir / "odd_test_map.json"
|
|
911
|
+
with open(odd_map_path, "w") as f:
|
|
912
|
+
json.dump(payload_odd, f, indent=2)
|
|
913
|
+
|
|
914
|
+
td_odd = TilemapData.load(odd_map_path)
|
|
915
|
+
objects_odd = load_map_objects(td_odd, collision_dir)
|
|
916
|
+
assert len(objects_odd) == 1
|
|
917
|
+
obj = objects_odd[0]
|
|
918
|
+
assert obj.x == 11 * 1.5, f"fractional x: {obj.x} != {11 * 1.5}"
|
|
919
|
+
assert obj.y == 21 * 1.5, f"fractional y: {obj.y} != {21 * 1.5}"
|
|
920
|
+
assert obj.surface.get_size() == (25, 52), \
|
|
921
|
+
f"truncated surface: {obj.surface.get_size()} != (25, 52) [int(17*1.5)=25, int(35*1.5)=52]"
|
|
922
|
+
|
|
923
|
+
def test_render_scale_collision_consistency(self):
|
|
924
|
+
"""At render_scale=3.0, a loaded object collides with a manually-placed
|
|
925
|
+
probe object if and only if they overlap in effective (scaled) space."""
|
|
926
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
927
|
+
tmp = Path(tmp)
|
|
928
|
+
assets_dir = tmp / "assets"
|
|
929
|
+
data_dir = tmp / "data"
|
|
930
|
+
collision_dir = tmp / "data" / "collision"
|
|
931
|
+
assets_dir.mkdir(parents=True)
|
|
932
|
+
data_dir.mkdir(parents=True)
|
|
933
|
+
collision_dir.mkdir(parents=True)
|
|
934
|
+
|
|
935
|
+
png = assets_dir / "tileset.png"
|
|
936
|
+
_make_minimal_png(png, (32, 16))
|
|
937
|
+
|
|
938
|
+
# Collision region at (0,0) with a 16x16 square polygon
|
|
939
|
+
collision_payload = {
|
|
940
|
+
"tileset_name": "tileset",
|
|
941
|
+
"regions": {
|
|
942
|
+
"region_r": {
|
|
943
|
+
"region_id": "region_r",
|
|
944
|
+
"region_rect": [0, 0, 16, 16],
|
|
945
|
+
"name": "R",
|
|
946
|
+
"shapes": [{"type": "polygon", "vertices": [[0, 0], [16, 0], [16, 16], [0, 16]], "one_way": False}],
|
|
947
|
+
"properties": {},
|
|
948
|
+
},
|
|
949
|
+
},
|
|
950
|
+
}
|
|
951
|
+
coll_path = collision_dir / "tileset.object_collision.json"
|
|
952
|
+
with open(coll_path, "w") as f:
|
|
953
|
+
json.dump(collision_payload, f, indent=2)
|
|
954
|
+
|
|
955
|
+
payload = {
|
|
956
|
+
"meta": {
|
|
957
|
+
"tile_size": "16;16",
|
|
958
|
+
"map_size": "10;10",
|
|
959
|
+
"version": "1.1",
|
|
960
|
+
"render_scale": 3.0,
|
|
961
|
+
},
|
|
962
|
+
"resources": {"tilesets": [{"path": "../assets/tileset.png", "type": "object"}]},
|
|
963
|
+
"project_state": {"rules": [], "groups": []},
|
|
964
|
+
"data": {
|
|
965
|
+
"ongrid": {},
|
|
966
|
+
"layers": [
|
|
967
|
+
{
|
|
968
|
+
"name": "Object Layer",
|
|
969
|
+
"type": "object",
|
|
970
|
+
"visible": True,
|
|
971
|
+
"locked": False,
|
|
972
|
+
"opacity": 1.0,
|
|
973
|
+
"z_index": 0,
|
|
974
|
+
"properties": {},
|
|
975
|
+
"tiles": {},
|
|
976
|
+
"objects": {
|
|
977
|
+
"1": {
|
|
978
|
+
"area": {"x": 100, "y": 200, "w": 16, "h": 16},
|
|
979
|
+
"ttype": 0,
|
|
980
|
+
"tileset_type": "object",
|
|
981
|
+
"variant": 0,
|
|
982
|
+
"properties": {},
|
|
983
|
+
},
|
|
984
|
+
},
|
|
985
|
+
"next_object_id": 2,
|
|
986
|
+
},
|
|
987
|
+
],
|
|
988
|
+
},
|
|
989
|
+
}
|
|
990
|
+
map_path = data_dir / "test_map.json"
|
|
991
|
+
with open(map_path, "w") as f:
|
|
992
|
+
json.dump(payload, f, indent=2)
|
|
993
|
+
|
|
994
|
+
td = TilemapData.load(map_path)
|
|
995
|
+
objects = load_map_objects(td, collision_dir)
|
|
996
|
+
assert len(objects) == 1
|
|
997
|
+
loaded = objects[0]
|
|
998
|
+
|
|
999
|
+
# Loaded object position = (300, 600) in effective space
|
|
1000
|
+
# Collision polygon covers (300, 600) to (300+48, 600+48) = (348, 648)
|
|
1001
|
+
assert loaded.x == 300
|
|
1002
|
+
assert loaded.y == 600
|
|
1003
|
+
|
|
1004
|
+
# Probe at (310, 610) with a 16x16 rect in effective space must collide
|
|
1005
|
+
probe_surf = pygame.Surface((16, 16))
|
|
1006
|
+
probe_shape = CollisionPolygon(vertices=[(0, 0), (16, 0), (16, 16), (0, 16)])
|
|
1007
|
+
probe = MapObject(x=310, y=610, surface=probe_surf, collision_shape=probe_shape)
|
|
1008
|
+
|
|
1009
|
+
from tilemap_parser.runtime.object_collision import check_collision
|
|
1010
|
+
|
|
1011
|
+
hit = check_collision(loaded, probe)
|
|
1012
|
+
assert hit is not None, "expected collision in effective space"
|
|
1013
|
+
|
|
1014
|
+
# Probe far away must NOT collide
|
|
1015
|
+
probe_far = MapObject(x=1000, y=1000, surface=probe_surf, collision_shape=probe_shape)
|
|
1016
|
+
hit_far = check_collision(loaded, probe_far)
|
|
1017
|
+
assert hit_far is None, "no collision expected for far probe"
|
|
1018
|
+
|
|
1019
|
+
def test_render_scale_region_offset_scaled(self):
|
|
1020
|
+
"""Non-zero region_rect offset is scaled by render_scale."""
|
|
1021
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
1022
|
+
tmp = Path(tmp)
|
|
1023
|
+
assets_dir = tmp / "assets"
|
|
1024
|
+
data_dir = tmp / "data"
|
|
1025
|
+
collision_dir = tmp / "data" / "collision"
|
|
1026
|
+
assets_dir.mkdir(parents=True)
|
|
1027
|
+
data_dir.mkdir(parents=True)
|
|
1028
|
+
collision_dir.mkdir(parents=True)
|
|
1029
|
+
|
|
1030
|
+
png = assets_dir / "tileset.png"
|
|
1031
|
+
_make_minimal_png(png, (32, 16))
|
|
1032
|
+
|
|
1033
|
+
# region_rect offset of (5,10), polygon at origin within that region
|
|
1034
|
+
collision_payload = {
|
|
1035
|
+
"tileset_name": "tileset",
|
|
1036
|
+
"regions": {
|
|
1037
|
+
"region_r": {
|
|
1038
|
+
"region_id": "region_r",
|
|
1039
|
+
"region_rect": [5, 10, 16, 16],
|
|
1040
|
+
"name": "R",
|
|
1041
|
+
"shapes": [{"type": "polygon", "vertices": [[0, 0], [8, 0], [8, 8], [0, 8]], "one_way": False}],
|
|
1042
|
+
"properties": {},
|
|
1043
|
+
},
|
|
1044
|
+
},
|
|
1045
|
+
}
|
|
1046
|
+
coll_path = collision_dir / "tileset.object_collision.json"
|
|
1047
|
+
with open(coll_path, "w") as f:
|
|
1048
|
+
json.dump(collision_payload, f, indent=2)
|
|
1049
|
+
|
|
1050
|
+
payload = {
|
|
1051
|
+
"meta": {
|
|
1052
|
+
"tile_size": "16;16",
|
|
1053
|
+
"map_size": "10;10",
|
|
1054
|
+
"version": "1.1",
|
|
1055
|
+
"render_scale": 3.0,
|
|
1056
|
+
},
|
|
1057
|
+
"resources": {"tilesets": [{"path": "../assets/tileset.png", "type": "object"}]},
|
|
1058
|
+
"project_state": {"rules": [], "groups": []},
|
|
1059
|
+
"data": {
|
|
1060
|
+
"ongrid": {},
|
|
1061
|
+
"layers": [
|
|
1062
|
+
{
|
|
1063
|
+
"name": "Object Layer",
|
|
1064
|
+
"type": "object",
|
|
1065
|
+
"visible": True,
|
|
1066
|
+
"locked": False,
|
|
1067
|
+
"opacity": 1.0,
|
|
1068
|
+
"z_index": 0,
|
|
1069
|
+
"properties": {},
|
|
1070
|
+
"tiles": {},
|
|
1071
|
+
"objects": {
|
|
1072
|
+
"1": {
|
|
1073
|
+
"area": {"x": 100, "y": 200, "w": 16, "h": 16},
|
|
1074
|
+
"ttype": 0,
|
|
1075
|
+
"tileset_type": "object",
|
|
1076
|
+
"variant": 0,
|
|
1077
|
+
"properties": {},
|
|
1078
|
+
},
|
|
1079
|
+
},
|
|
1080
|
+
"next_object_id": 2,
|
|
1081
|
+
},
|
|
1082
|
+
],
|
|
1083
|
+
},
|
|
1084
|
+
}
|
|
1085
|
+
map_path = data_dir / "test_map.json"
|
|
1086
|
+
with open(map_path, "w") as f:
|
|
1087
|
+
json.dump(payload, f, indent=2)
|
|
1088
|
+
|
|
1089
|
+
td = TilemapData.load(map_path)
|
|
1090
|
+
objects = load_map_objects(td, collision_dir)
|
|
1091
|
+
assert len(objects) == 1
|
|
1092
|
+
obj = objects[0]
|
|
1093
|
+
|
|
1094
|
+
# Position scaled: (300, 600)
|
|
1095
|
+
# region_rect offset scaled: ox=15, oy=30
|
|
1096
|
+
# Vertices scaled: (0,0)→(0,0), (8,0)→(24,0), (8,8)→(24,24), (0,8)→(0,24)
|
|
1097
|
+
# World: (15, 30), (39, 30), (39, 54), (15, 54)
|
|
1098
|
+
verts = obj.collision_shape.vertices
|
|
1099
|
+
assert verts[0] == pytest.approx((15.0, 30.0))
|
|
1100
|
+
assert verts[1] == pytest.approx((39.0, 30.0))
|
|
1101
|
+
assert verts[2] == pytest.approx((39.0, 54.0))
|
|
1102
|
+
assert verts[3] == pytest.approx((15.0, 54.0))
|
|
1103
|
+
|
|
1104
|
+
# Probe at object center must collide
|
|
1105
|
+
probe_surf = pygame.Surface((16, 16))
|
|
1106
|
+
probe_shape = CollisionPolygon(vertices=[(0, 0), (16, 0), (16, 16), (0, 16)])
|
|
1107
|
+
# Loaded world AABB: (300+15, 600+30) to (300+39, 600+54) = (315, 630) to (339, 654)
|
|
1108
|
+
probe = MapObject(x=315, y=630, surface=probe_surf, collision_shape=probe_shape)
|
|
1109
|
+
|
|
1110
|
+
from tilemap_parser.runtime.object_collision import check_collision
|
|
1111
|
+
|
|
1112
|
+
hit = check_collision(obj, probe)
|
|
1113
|
+
assert hit is not None, "expected collision with region offset"
|
|
1114
|
+
|
|
1115
|
+
def test_render_scale_visual_collision_alignment(self):
|
|
1116
|
+
"""At render_scale=N, collision AABB is fully inside visual rect
|
|
1117
|
+
and vertex positions are pixel-accurate for known values."""
|
|
1118
|
+
from tilemap_parser.runtime.map_object import load_map_objects
|
|
1119
|
+
from tilemap_parser.runtime.map_loader import TilemapData
|
|
1120
|
+
|
|
1121
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
1122
|
+
tmp = Path(tmp)
|
|
1123
|
+
assets_dir = tmp / "assets"
|
|
1124
|
+
data_dir = tmp / "data"
|
|
1125
|
+
collision_dir = tmp / "data" / "collision"
|
|
1126
|
+
assets_dir.mkdir(parents=True)
|
|
1127
|
+
data_dir.mkdir(parents=True)
|
|
1128
|
+
collision_dir.mkdir(parents=True)
|
|
1129
|
+
|
|
1130
|
+
# Tileset: 64x96 with a known shape
|
|
1131
|
+
tileset_surf = pygame.Surface((64, 96))
|
|
1132
|
+
tileset_surf.fill((0, 255, 0))
|
|
1133
|
+
png = assets_dir / "tree.png"
|
|
1134
|
+
pygame.image.save(tileset_surf, str(png))
|
|
1135
|
+
|
|
1136
|
+
# Collision: region_rect=[0,3,64,93] with polygon
|
|
1137
|
+
collision_payload = {
|
|
1138
|
+
"tileset_name": "tree",
|
|
1139
|
+
"regions": {
|
|
1140
|
+
"region_r": {
|
|
1141
|
+
"region_id": "region_r",
|
|
1142
|
+
"region_rect": [0, 3, 64, 93],
|
|
1143
|
+
"name": "R",
|
|
1144
|
+
"shapes": [
|
|
1145
|
+
{
|
|
1146
|
+
"type": "polygon",
|
|
1147
|
+
"vertices": [[32.0, 2.75], [8.0, 30.0], [10.5, 58.75],
|
|
1148
|
+
[22.5, 65.25], [20.0, 88.25], [33.5, 81.75],
|
|
1149
|
+
[47.5, 87.75], [41.5, 65.75], [58.5, 48.75],
|
|
1150
|
+
[52.0, 25.0]],
|
|
1151
|
+
"one_way": False,
|
|
1152
|
+
}
|
|
1153
|
+
],
|
|
1154
|
+
"properties": {"collision_layer": 1, "collision_mask": 65535},
|
|
1155
|
+
},
|
|
1156
|
+
},
|
|
1157
|
+
}
|
|
1158
|
+
coll_path = collision_dir / "tree.object_collision.json"
|
|
1159
|
+
with open(coll_path, "w") as f:
|
|
1160
|
+
json.dump(collision_payload, f, indent=2)
|
|
1161
|
+
|
|
1162
|
+
for rs_val in [1.0, 2.0, 3.0, 4.0]:
|
|
1163
|
+
payload = {
|
|
1164
|
+
"meta": {
|
|
1165
|
+
"tile_size": "16;16",
|
|
1166
|
+
"map_size": "10;10",
|
|
1167
|
+
"version": "1.1",
|
|
1168
|
+
"render_scale": rs_val,
|
|
1169
|
+
},
|
|
1170
|
+
"resources": {"tilesets": [{"path": "../assets/tree.png", "type": "object"}]},
|
|
1171
|
+
"project_state": {"rules": [], "groups": []},
|
|
1172
|
+
"data": {
|
|
1173
|
+
"ongrid": {},
|
|
1174
|
+
"layers": [
|
|
1175
|
+
{
|
|
1176
|
+
"name": "Objects",
|
|
1177
|
+
"type": "object",
|
|
1178
|
+
"visible": True,
|
|
1179
|
+
"locked": False,
|
|
1180
|
+
"opacity": 1.0,
|
|
1181
|
+
"z_index": 0,
|
|
1182
|
+
"properties": {},
|
|
1183
|
+
"tiles": {},
|
|
1184
|
+
"objects": {
|
|
1185
|
+
"1": {
|
|
1186
|
+
"area": {"x": 27, "y": 82, "w": 64, "h": 96},
|
|
1187
|
+
"ttype": 0,
|
|
1188
|
+
"tileset_type": "object",
|
|
1189
|
+
"variant": 0,
|
|
1190
|
+
"properties": {},
|
|
1191
|
+
},
|
|
1192
|
+
},
|
|
1193
|
+
"next_object_id": 2,
|
|
1194
|
+
},
|
|
1195
|
+
],
|
|
1196
|
+
},
|
|
1197
|
+
}
|
|
1198
|
+
map_path = data_dir / "test_map.json"
|
|
1199
|
+
with open(map_path, "w") as f:
|
|
1200
|
+
json.dump(payload, f, indent=2)
|
|
1201
|
+
|
|
1202
|
+
td = TilemapData.load(map_path)
|
|
1203
|
+
objects = load_map_objects(td, collision_dir)
|
|
1204
|
+
assert len(objects) == 1, f"rs={rs_val}: expected 1 object"
|
|
1205
|
+
obj = objects[0]
|
|
1206
|
+
|
|
1207
|
+
# Verify surface size is pixel-accurate at integer render_scale
|
|
1208
|
+
expected_w = int(64 * rs_val)
|
|
1209
|
+
expected_h = int(96 * rs_val)
|
|
1210
|
+
assert obj.surface.get_size() == (expected_w, expected_h), \
|
|
1211
|
+
f"rs={rs_val}: surface {obj.surface.get_size()} != ({expected_w},{expected_h})"
|
|
1212
|
+
|
|
1213
|
+
# Verify position is pixel-accurate (float)
|
|
1214
|
+
assert obj.x == 27 * rs_val, f"rs={rs_val}: x={obj.x} != {27 * rs_val}"
|
|
1215
|
+
assert obj.y == 82 * rs_val, f"rs={rs_val}: y={obj.y} != {82 * rs_val}"
|
|
1216
|
+
|
|
1217
|
+
# Verify every collision vertex is pixel-accurate for known values
|
|
1218
|
+
shape = obj.collision_shapes[0]
|
|
1219
|
+
# First vertex: (32.0, 2.75) + region_rect (0, 3) = world (32*rs, (3+2.75)*rs)
|
|
1220
|
+
expected_v0 = (32.0 * rs_val, (3.0 + 2.75) * rs_val)
|
|
1221
|
+
actual_v0 = shape.vertices[0]
|
|
1222
|
+
assert abs(actual_v0[0] - expected_v0[0]) < 0.001, \
|
|
1223
|
+
f"rs={rs_val}: vertex[0].x {actual_v0[0]} != {expected_v0[0]}"
|
|
1224
|
+
assert abs(actual_v0[1] - expected_v0[1]) < 0.001, \
|
|
1225
|
+
f"rs={rs_val}: vertex[0].y {actual_v0[1]} != {expected_v0[1]}"
|
|
1226
|
+
|
|
1227
|
+
# Verify collision AABB is inside visual rect
|
|
1228
|
+
xs = [v[0] for v in shape.vertices]
|
|
1229
|
+
ys = [v[1] for v in shape.vertices]
|
|
1230
|
+
coll_min_x = obj.x + min(xs)
|
|
1231
|
+
coll_max_x = obj.x + max(xs)
|
|
1232
|
+
coll_min_y = obj.y + min(ys)
|
|
1233
|
+
coll_max_y = obj.y + max(ys)
|
|
1234
|
+
|
|
1235
|
+
vis_left = obj.x
|
|
1236
|
+
vis_top = obj.y
|
|
1237
|
+
vis_right = obj.x + expected_w
|
|
1238
|
+
vis_bottom = obj.y + expected_h
|
|
1239
|
+
|
|
1240
|
+
margin = -0.001 # allow 0px floating point tolerance
|
|
1241
|
+
assert coll_min_x >= vis_left + margin, \
|
|
1242
|
+
f"rs={rs_val}: collision left {coll_min_x} < visual left {vis_left}"
|
|
1243
|
+
assert coll_min_y >= vis_top + margin, \
|
|
1244
|
+
f"rs={rs_val}: collision top {coll_min_y} < visual top {vis_top}"
|
|
1245
|
+
assert coll_max_x <= vis_right - margin, \
|
|
1246
|
+
f"rs={rs_val}: collision right {coll_max_x} > visual right {vis_right}"
|
|
1247
|
+
assert coll_max_y <= vis_bottom - margin, \
|
|
1248
|
+
f"rs={rs_val}: collision bottom {coll_max_y} > visual bottom {vis_bottom}"
|
|
1249
|
+
|
|
655
1250
|
def test_invalid_tileset_index_skipped(self):
|
|
656
1251
|
"""Object with ttype out of range is skipped."""
|
|
657
1252
|
with tempfile.TemporaryDirectory() as tmp:
|
|
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.1 → tilemap_parser-4.2.3}/src/tilemap_parser/runtime/animation_player.py
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
|
{tilemap_parser-4.2.1 → tilemap_parser-4.2.3}/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
|