tilemap-parser 4.2.1__tar.gz → 4.2.2__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.2}/PKG-INFO +1 -1
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.2}/pyproject.toml +1 -1
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.2}/src/tilemap_parser/runtime/map_object.py +37 -6
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.2}/src/tilemap_parser.egg-info/PKG-INFO +1 -1
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.2}/tests/test_map_object.py +595 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.2}/LICENSE +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.2}/README.md +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.2}/setup.cfg +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.2}/src/tilemap_parser/__init__.py +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.2}/src/tilemap_parser/parser/__init__.py +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.2}/src/tilemap_parser/parser/animation.py +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.2}/src/tilemap_parser/parser/collision.py +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.2}/src/tilemap_parser/parser/collision_loader.py +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.2}/src/tilemap_parser/parser/map_parse.py +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.2}/src/tilemap_parser/parser/node_parse.py +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.2}/src/tilemap_parser/parser/particle.py +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.2}/src/tilemap_parser/parser/tmx_converter.py +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.2}/src/tilemap_parser/runtime/__init__.py +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.2}/src/tilemap_parser/runtime/animation_player.py +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.2}/src/tilemap_parser/runtime/area_node.py +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.2}/src/tilemap_parser/runtime/camera.py +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.2}/src/tilemap_parser/runtime/collision_cache.py +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.2}/src/tilemap_parser/runtime/map_loader.py +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.2}/src/tilemap_parser/runtime/object_collision.py +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.2}/src/tilemap_parser/runtime/particles.py +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.2}/src/tilemap_parser/runtime/renderer.py +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.2}/src/tilemap_parser/runtime/tile_collision.py +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.2}/src/tilemap_parser/utils/__init__.py +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.2}/src/tilemap_parser/utils/geometry.py +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.2}/src/tilemap_parser.egg-info/SOURCES.txt +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.2}/src/tilemap_parser.egg-info/dependency_links.txt +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.2}/src/tilemap_parser.egg-info/requires.txt +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.2}/src/tilemap_parser.egg-info/top_level.txt +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.2}/tests/test_camera.py +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.2}/tests/test_collision.py +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.2}/tests/test_geometry.py +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.2}/tests/test_gid_collision.py +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.2}/tests/test_integration_animation.py +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.2}/tests/test_integration_collision.py +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.2}/tests/test_integration_map_loader.py +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.2}/tests/test_map_loader.py +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.2}/tests/test_move_grounded.py +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.2}/tests/test_object_collision.py +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.2}/tests/test_object_surfaces.py +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.2}/tests/test_particle_emitter.py +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.2}/tests/test_render_scale.py +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.2}/tests/test_tile_collision.py +0 -0
- {tilemap_parser-4.2.1 → tilemap_parser-4.2.2}/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.2
|
|
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.2"
|
|
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 __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
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tilemap-parser
|
|
3
|
-
Version: 4.2.
|
|
3
|
+
Version: 4.2.2
|
|
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
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
{tilemap_parser-4.2.1 → tilemap_parser-4.2.2}/src/tilemap_parser/runtime/animation_player.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tilemap_parser-4.2.1 → tilemap_parser-4.2.2}/src/tilemap_parser/runtime/object_collision.py
RENAMED
|
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.2}/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
|