tilemap-parser 5.0.0__tar.gz → 5.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {tilemap_parser-5.0.0 → tilemap_parser-5.0.1}/PKG-INFO +35 -1
- {tilemap_parser-5.0.0 → tilemap_parser-5.0.1}/README.md +34 -0
- {tilemap_parser-5.0.0 → tilemap_parser-5.0.1}/pyproject.toml +1 -1
- {tilemap_parser-5.0.0 → tilemap_parser-5.0.1}/src/tilemap_parser/__init__.py +1 -0
- {tilemap_parser-5.0.0 → tilemap_parser-5.0.1}/src/tilemap_parser/parser/collision.py +39 -1
- {tilemap_parser-5.0.0 → tilemap_parser-5.0.1}/src/tilemap_parser/parser/map_parse.py +16 -0
- {tilemap_parser-5.0.0 → tilemap_parser-5.0.1}/src/tilemap_parser/parser/tmx_converter.py +1 -0
- {tilemap_parser-5.0.0 → tilemap_parser-5.0.1}/src/tilemap_parser/runtime/__init__.py +13 -9
- {tilemap_parser-5.0.0 → tilemap_parser-5.0.1}/src/tilemap_parser/runtime/area_node.py +9 -2
- tilemap_parser-5.0.1/src/tilemap_parser/runtime/body.py +198 -0
- tilemap_parser-5.0.1/src/tilemap_parser/runtime/collision/__init__.py +10 -0
- tilemap_parser-5.0.1/src/tilemap_parser/runtime/collision/hit.py +150 -0
- tilemap_parser-5.0.1/src/tilemap_parser/runtime/collision/manager.py +184 -0
- tilemap_parser-5.0.1/src/tilemap_parser/runtime/collision/shapes.py +170 -0
- {tilemap_parser-5.0.0 → tilemap_parser-5.0.1}/src/tilemap_parser/runtime/map_loader.py +62 -14
- tilemap_parser-5.0.1/src/tilemap_parser/runtime/map_object.py +255 -0
- tilemap_parser-5.0.1/src/tilemap_parser/runtime/movement/__init__.py +11 -0
- tilemap_parser-5.0.1/src/tilemap_parser/runtime/movement/grounded.py +181 -0
- tilemap_parser-5.0.1/src/tilemap_parser/runtime/movement/platformer.py +533 -0
- tilemap_parser-5.0.1/src/tilemap_parser/runtime/movement/queries.py +264 -0
- tilemap_parser-5.0.1/src/tilemap_parser/runtime/movement/rpg.py +87 -0
- tilemap_parser-5.0.1/src/tilemap_parser/runtime/movement/runner.py +488 -0
- tilemap_parser-5.0.1/src/tilemap_parser/runtime/movement/slide.py +209 -0
- tilemap_parser-5.0.1/src/tilemap_parser/runtime/movement/types.py +30 -0
- tilemap_parser-5.0.1/src/tilemap_parser/runtime/navigation/__init__.py +3 -0
- tilemap_parser-5.0.1/src/tilemap_parser/runtime/navigation/nav_grid.py +184 -0
- tilemap_parser-5.0.1/src/tilemap_parser/runtime/navigation/path_follower.py +76 -0
- tilemap_parser-5.0.1/src/tilemap_parser/runtime/navigation/pathfinder.py +75 -0
- tilemap_parser-5.0.1/src/tilemap_parser/runtime/object_collision.py +38 -0
- tilemap_parser-5.0.1/src/tilemap_parser/runtime/polygon_query.py +490 -0
- tilemap_parser-5.0.1/src/tilemap_parser/runtime/protocols.py +71 -0
- {tilemap_parser-5.0.0 → tilemap_parser-5.0.1}/src/tilemap_parser/runtime/renderer.py +43 -28
- tilemap_parser-5.0.1/src/tilemap_parser/runtime/tile_collision.py +62 -0
- tilemap_parser-5.0.1/src/tilemap_parser/runtime/world.py +146 -0
- {tilemap_parser-5.0.0 → tilemap_parser-5.0.1}/src/tilemap_parser.egg-info/PKG-INFO +35 -1
- {tilemap_parser-5.0.0 → tilemap_parser-5.0.1}/src/tilemap_parser.egg-info/SOURCES.txt +26 -0
- tilemap_parser-5.0.1/tests/test_body.py +802 -0
- tilemap_parser-5.0.1/tests/test_gid_collision.py +260 -0
- {tilemap_parser-5.0.0 → tilemap_parser-5.0.1}/tests/test_map_loader.py +99 -0
- tilemap_parser-5.0.1/tests/test_map_object.py +1379 -0
- {tilemap_parser-5.0.0 → tilemap_parser-5.0.1}/tests/test_move_grounded.py +1 -1
- tilemap_parser-5.0.1/tests/test_navigation.py +317 -0
- {tilemap_parser-5.0.0 → tilemap_parser-5.0.1}/tests/test_object_collision.py +3 -3
- {tilemap_parser-5.0.0 → tilemap_parser-5.0.1}/tests/test_object_surfaces.py +93 -6
- {tilemap_parser-5.0.0 → tilemap_parser-5.0.1}/tests/test_render_scale.py +2 -3
- {tilemap_parser-5.0.0 → tilemap_parser-5.0.1}/tests/test_tile_collision.py +2 -3
- tilemap_parser-5.0.1/tests/test_tile_layer_renderer_y_sort.py +348 -0
- tilemap_parser-5.0.0/src/tilemap_parser/runtime/object_collision.py +0 -475
- tilemap_parser-5.0.0/src/tilemap_parser/runtime/tile_collision.py +0 -2156
- {tilemap_parser-5.0.0 → tilemap_parser-5.0.1}/LICENSE +0 -0
- {tilemap_parser-5.0.0 → tilemap_parser-5.0.1}/setup.cfg +0 -0
- {tilemap_parser-5.0.0 → tilemap_parser-5.0.1}/src/tilemap_parser/parser/__init__.py +0 -0
- {tilemap_parser-5.0.0 → tilemap_parser-5.0.1}/src/tilemap_parser/parser/animation.py +0 -0
- {tilemap_parser-5.0.0 → tilemap_parser-5.0.1}/src/tilemap_parser/parser/collision_loader.py +0 -0
- {tilemap_parser-5.0.0 → tilemap_parser-5.0.1}/src/tilemap_parser/parser/node_parse.py +0 -0
- {tilemap_parser-5.0.0 → tilemap_parser-5.0.1}/src/tilemap_parser/parser/particle.py +0 -0
- {tilemap_parser-5.0.0 → tilemap_parser-5.0.1}/src/tilemap_parser/runtime/animation_player.py +0 -0
- {tilemap_parser-5.0.0 → tilemap_parser-5.0.1}/src/tilemap_parser/runtime/camera.py +0 -0
- {tilemap_parser-5.0.0 → tilemap_parser-5.0.1}/src/tilemap_parser/runtime/collision_cache.py +0 -0
- {tilemap_parser-5.0.0 → tilemap_parser-5.0.1}/src/tilemap_parser/runtime/particles.py +0 -0
- {tilemap_parser-5.0.0 → tilemap_parser-5.0.1}/src/tilemap_parser/utils/__init__.py +0 -0
- {tilemap_parser-5.0.0 → tilemap_parser-5.0.1}/src/tilemap_parser/utils/geometry.py +0 -0
- {tilemap_parser-5.0.0 → tilemap_parser-5.0.1}/src/tilemap_parser.egg-info/dependency_links.txt +0 -0
- {tilemap_parser-5.0.0 → tilemap_parser-5.0.1}/src/tilemap_parser.egg-info/requires.txt +0 -0
- {tilemap_parser-5.0.0 → tilemap_parser-5.0.1}/src/tilemap_parser.egg-info/top_level.txt +0 -0
- {tilemap_parser-5.0.0 → tilemap_parser-5.0.1}/tests/test_camera.py +0 -0
- {tilemap_parser-5.0.0 → tilemap_parser-5.0.1}/tests/test_collision.py +0 -0
- {tilemap_parser-5.0.0 → tilemap_parser-5.0.1}/tests/test_geometry.py +0 -0
- {tilemap_parser-5.0.0 → tilemap_parser-5.0.1}/tests/test_integration_animation.py +0 -0
- {tilemap_parser-5.0.0 → tilemap_parser-5.0.1}/tests/test_integration_collision.py +0 -0
- {tilemap_parser-5.0.0 → tilemap_parser-5.0.1}/tests/test_integration_map_loader.py +0 -0
- {tilemap_parser-5.0.0 → tilemap_parser-5.0.1}/tests/test_particle_emitter.py +0 -0
- {tilemap_parser-5.0.0 → tilemap_parser-5.0.1}/tests/test_tmx_converter.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tilemap-parser
|
|
3
|
-
Version: 5.0.
|
|
3
|
+
Version: 5.0.1
|
|
4
4
|
Summary: Standalone parser/loader for tilemap-editor JSON maps, sprite animations, and collision detection runtime.
|
|
5
5
|
Author: tilemap parser contributors
|
|
6
6
|
License: GNU GENERAL PUBLIC LICENSE
|
|
@@ -765,6 +765,40 @@ particle_config = ParticleSystemConfig(
|
|
|
765
765
|
particles = ParticleSystem(particle_config)
|
|
766
766
|
```
|
|
767
767
|
|
|
768
|
+
## Physics bodies
|
|
769
|
+
|
|
770
|
+
A `PhysicsWorld` is a single space holding the tile layer plus solid `Body` objects (static walls, kinematic crates). Attach a `CollisionRunner` to the world once and every move method resolves tiles and bodies through it — no per-call tile arguments.
|
|
771
|
+
|
|
772
|
+
> Full contract — object↔world↔`move_*`↔tiles↔rendering: see [docs/physics-world.md](docs/physics-world.md) and `examples/physics-crate/main.py`.
|
|
773
|
+
|
|
774
|
+
```python
|
|
775
|
+
from tilemap_parser import (
|
|
776
|
+
Body, PhysicsWorld, CollisionRunner, RectangleShape,
|
|
777
|
+
TilemapData, TilesetCollision, CollisionCache,
|
|
778
|
+
)
|
|
779
|
+
|
|
780
|
+
cache = CollisionCache()
|
|
781
|
+
tileset = cache.get_tileset_collision("data/collision/tileset.collision.json")
|
|
782
|
+
world = PhysicsWorld.from_map(game_data, tileset) # adopts tile_size / render_scale
|
|
783
|
+
|
|
784
|
+
crate = Body(RectangleShape(width=16, height=16), x=320, y=480, mode="kinematic")
|
|
785
|
+
world.add_body(crate)
|
|
786
|
+
|
|
787
|
+
runner = CollisionRunner.from_world(world) # or runner.attach(world)
|
|
788
|
+
```
|
|
789
|
+
|
|
790
|
+
Pushable crates are just kinematic bodies moved with an explicit velocity (gravity is skipped when `velocity=` is given):
|
|
791
|
+
|
|
792
|
+
```python
|
|
793
|
+
# player pressed against the crate -> hand the player's vx over
|
|
794
|
+
crate.vx = player.vx
|
|
795
|
+
result = runner.move_grounded(crate, None, None, dt, velocity=(crate.vx, 0))
|
|
796
|
+
if result.hit_wall_x:
|
|
797
|
+
crate.vx = 0 # crate stopped against a tile wall or another body
|
|
798
|
+
```
|
|
799
|
+
|
|
800
|
+
`Body` accepts primitive shapes only (`RectangleShape`, `CircleShape`, `CapsuleShape`); use `MapObject` for polygon solids. Collision pairs honor both sides' `collision_layer` / `collision_mask`, and bodies are landing surfaces for platformer step-up (small crates are climbed, tall ones block like walls).
|
|
801
|
+
|
|
768
802
|
## Links
|
|
769
803
|
|
|
770
804
|
- **Docs**: [https://deepwiki.com/FluffyBrudy/tilemap-parser](https://deepwiki.com/FluffyBrudy/tilemap-parser)
|
|
@@ -66,6 +66,40 @@ particle_config = ParticleSystemConfig(
|
|
|
66
66
|
particles = ParticleSystem(particle_config)
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
+
## Physics bodies
|
|
70
|
+
|
|
71
|
+
A `PhysicsWorld` is a single space holding the tile layer plus solid `Body` objects (static walls, kinematic crates). Attach a `CollisionRunner` to the world once and every move method resolves tiles and bodies through it — no per-call tile arguments.
|
|
72
|
+
|
|
73
|
+
> Full contract — object↔world↔`move_*`↔tiles↔rendering: see [docs/physics-world.md](docs/physics-world.md) and `examples/physics-crate/main.py`.
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from tilemap_parser import (
|
|
77
|
+
Body, PhysicsWorld, CollisionRunner, RectangleShape,
|
|
78
|
+
TilemapData, TilesetCollision, CollisionCache,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
cache = CollisionCache()
|
|
82
|
+
tileset = cache.get_tileset_collision("data/collision/tileset.collision.json")
|
|
83
|
+
world = PhysicsWorld.from_map(game_data, tileset) # adopts tile_size / render_scale
|
|
84
|
+
|
|
85
|
+
crate = Body(RectangleShape(width=16, height=16), x=320, y=480, mode="kinematic")
|
|
86
|
+
world.add_body(crate)
|
|
87
|
+
|
|
88
|
+
runner = CollisionRunner.from_world(world) # or runner.attach(world)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Pushable crates are just kinematic bodies moved with an explicit velocity (gravity is skipped when `velocity=` is given):
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
# player pressed against the crate -> hand the player's vx over
|
|
95
|
+
crate.vx = player.vx
|
|
96
|
+
result = runner.move_grounded(crate, None, None, dt, velocity=(crate.vx, 0))
|
|
97
|
+
if result.hit_wall_x:
|
|
98
|
+
crate.vx = 0 # crate stopped against a tile wall or another body
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
`Body` accepts primitive shapes only (`RectangleShape`, `CircleShape`, `CapsuleShape`); use `MapObject` for polygon solids. Collision pairs honor both sides' `collision_layer` / `collision_mask`, and bodies are landing surfaces for platformer step-up (small crates are climbed, tall ones block like walls).
|
|
102
|
+
|
|
69
103
|
## Links
|
|
70
104
|
|
|
71
105
|
- **Docs**: [https://deepwiki.com/FluffyBrudy/tilemap-parser](https://deepwiki.com/FluffyBrudy/tilemap-parser)
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "tilemap-parser"
|
|
7
|
-
version = "5.0.
|
|
7
|
+
version = "5.0.1"
|
|
8
8
|
description = "Standalone parser/loader for tilemap-editor JSON maps, sprite animations, and collision detection runtime."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -77,6 +77,44 @@ class TilesetCollision:
|
|
|
77
77
|
return []
|
|
78
78
|
return [shape.transform(tile_x, tile_y, scale) for shape in tile_data.shapes]
|
|
79
79
|
|
|
80
|
+
@classmethod
|
|
81
|
+
def merge(
|
|
82
|
+
cls,
|
|
83
|
+
collisions: List["TilesetCollision"],
|
|
84
|
+
firstgids: List[int],
|
|
85
|
+
) -> "TilesetCollision":
|
|
86
|
+
"""Merge multiple tileset collisions into one, offsetting keys by firstgid.
|
|
87
|
+
|
|
88
|
+
Each collision's tiles are re-keyed as ``firstgid + local_id`` so that
|
|
89
|
+
the resulting dict uses global tile IDs for lookup.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
collisions: List of TilesetCollision objects to merge.
|
|
93
|
+
firstgids: List of firstgid offsets, one per collision, in the
|
|
94
|
+
same order as *collisions*. The offset is added to
|
|
95
|
+
each tile's local ID to produce the global key.
|
|
96
|
+
|
|
97
|
+
Usage::
|
|
98
|
+
|
|
99
|
+
merged = TilesetCollision.merge([ts0, ts1], [0, 200])
|
|
100
|
+
"""
|
|
101
|
+
if not collisions:
|
|
102
|
+
return cls(tileset_name="merged", tile_size=(0, 0))
|
|
103
|
+
tile_size = collisions[0].tile_size
|
|
104
|
+
merged_tiles: Dict[int, TileCollisionData] = {}
|
|
105
|
+
for coll, offset in zip(collisions, firstgids, strict=True):
|
|
106
|
+
for local_id, data in coll.tiles.items():
|
|
107
|
+
gid = offset + local_id
|
|
108
|
+
merged_tiles[gid] = TileCollisionData(
|
|
109
|
+
tile_id=gid,
|
|
110
|
+
shapes=data.shapes[:],
|
|
111
|
+
)
|
|
112
|
+
return cls(
|
|
113
|
+
tileset_name="merged",
|
|
114
|
+
tile_size=tile_size,
|
|
115
|
+
tiles=merged_tiles,
|
|
116
|
+
)
|
|
117
|
+
|
|
80
118
|
|
|
81
119
|
@dataclass
|
|
82
120
|
class RectangleShape:
|
|
@@ -211,7 +249,7 @@ def parse_tileset_collision(data: JsonDict) -> TilesetCollision:
|
|
|
211
249
|
tiles[tile_id] = TileCollisionData(tile_id=tile_id, shapes=shapes)
|
|
212
250
|
|
|
213
251
|
return TilesetCollision(
|
|
214
|
-
tileset_name=tileset_name, tile_size=tile_size, tiles=tiles
|
|
252
|
+
tileset_name=tileset_name, tile_size=tile_size, tiles=tiles,
|
|
215
253
|
)
|
|
216
254
|
except (KeyError, ValueError, TypeError) as e:
|
|
217
255
|
raise CollisionParseError(f"Invalid tileset collision data: {e}") from e
|
|
@@ -105,6 +105,7 @@ class ParsedTile:
|
|
|
105
105
|
pos: Point
|
|
106
106
|
ttype: TilesetRef
|
|
107
107
|
variant: int
|
|
108
|
+
gid: Optional[int] = None
|
|
108
109
|
properties: Optional[JsonDict] = None
|
|
109
110
|
flip_h: bool = False
|
|
110
111
|
flip_v: bool = False
|
|
@@ -142,6 +143,8 @@ class TilesetAnimation:
|
|
|
142
143
|
class ParsedTileset:
|
|
143
144
|
path: str
|
|
144
145
|
type: str
|
|
146
|
+
tile_count: int = 0
|
|
147
|
+
firstgid: int = 0
|
|
145
148
|
properties: JsonDict = field(default_factory=dict)
|
|
146
149
|
tile_properties: Dict[str, JsonDict] = field(default_factory=dict)
|
|
147
150
|
animation: Optional[TilesetAnimation] = None
|
|
@@ -156,10 +159,13 @@ class ParsedLayer:
|
|
|
156
159
|
locked: bool
|
|
157
160
|
opacity: float
|
|
158
161
|
z_index: int
|
|
162
|
+
y_sort: bool = False
|
|
163
|
+
y_sort_origin: int = 0
|
|
159
164
|
properties: JsonDict = field(default_factory=dict)
|
|
160
165
|
tiles: Dict[Point, ParsedTile] = field(default_factory=dict)
|
|
161
166
|
objects: Dict[int, ParsedObject] = field(default_factory=dict)
|
|
162
167
|
next_object_id: Optional[int] = None
|
|
168
|
+
ttypes: set[int] = field(default_factory=set)
|
|
163
169
|
|
|
164
170
|
|
|
165
171
|
@dataclass
|
|
@@ -217,10 +223,13 @@ def _parse_tile(tile_data: JsonDict, ctx: str) -> ParsedTile:
|
|
|
217
223
|
else:
|
|
218
224
|
ttype = _coerce_int(ttype_raw, f"{ctx}.ttype")
|
|
219
225
|
props = _optional_dict(tile_data.get("properties"), f"{ctx}.properties")
|
|
226
|
+
gid_raw = tile_data.get("gid")
|
|
227
|
+
gid = _coerce_int(gid_raw, f"{ctx}.gid") if gid_raw is not None else None
|
|
220
228
|
return ParsedTile(
|
|
221
229
|
pos=pos,
|
|
222
230
|
ttype=ttype,
|
|
223
231
|
variant=variant,
|
|
232
|
+
gid=gid,
|
|
224
233
|
properties=props,
|
|
225
234
|
flip_h=tile_data.get("flip_h", False),
|
|
226
235
|
flip_v=tile_data.get("flip_v", False),
|
|
@@ -279,6 +288,8 @@ def _parse_layer(layer_obj: JsonDict, layer_id: int, ctx: str) -> ParsedLayer:
|
|
|
279
288
|
locked=_coerce_bool(layer_obj.get("locked", False), f"{ctx}.locked"),
|
|
280
289
|
opacity=_coerce_float(layer_obj.get("opacity", 1.0), f"{ctx}.opacity"),
|
|
281
290
|
z_index=_coerce_int(layer_obj.get("z_index", layer_id), f"{ctx}.z_index"),
|
|
291
|
+
y_sort=_coerce_bool(layer_obj.get("y_sort", False), f"{ctx}.y_sort"),
|
|
292
|
+
y_sort_origin=layer_obj.get("y_sort_origin", 0),
|
|
282
293
|
properties=_optional_dict(layer_obj.get("properties"), f"{ctx}.properties")
|
|
283
294
|
or {},
|
|
284
295
|
)
|
|
@@ -291,6 +302,8 @@ def _parse_layer(layer_obj: JsonDict, layer_id: int, ctx: str) -> ParsedLayer:
|
|
|
291
302
|
_require_dict(layer_obj.get("objects", {}), f"{ctx}.objects"),
|
|
292
303
|
f"{ctx}.objects",
|
|
293
304
|
)
|
|
305
|
+
for obj in layer.objects.values():
|
|
306
|
+
layer.ttypes.add(obj.ttype)
|
|
294
307
|
if "next_object_id" in layer_obj and layer_obj["next_object_id"] is not None:
|
|
295
308
|
layer.next_object_id = _coerce_int(
|
|
296
309
|
layer_obj["next_object_id"], f"{ctx}.next_object_id"
|
|
@@ -378,6 +391,8 @@ def _parse_tilesets_list(tilesets_raw: List[Any], ctx: str) -> List[ParsedTilese
|
|
|
378
391
|
ParsedTileset(
|
|
379
392
|
path=path, type=ts_type, properties=props, tile_properties=tile_props,
|
|
380
393
|
animation=animation,
|
|
394
|
+
tile_count=_coerce_int(ts_obj.get("tile_count", 0), f"{ctx}[{i}].tile_count"),
|
|
395
|
+
firstgid=_coerce_int(ts_obj.get("firstgid", 0), f"{ctx}[{i}].firstgid"),
|
|
381
396
|
)
|
|
382
397
|
)
|
|
383
398
|
return out
|
|
@@ -401,6 +416,7 @@ def _expand_ongrid_to_layer(data_obj: JsonDict, ctx: str) -> List[ParsedLayer]:
|
|
|
401
416
|
locked=False,
|
|
402
417
|
opacity=1.0,
|
|
403
418
|
z_index=0,
|
|
419
|
+
y_sort=False, y_sort_origin=0,
|
|
404
420
|
properties={},
|
|
405
421
|
)
|
|
406
422
|
for loc_str, tile_data in raw_ongrid.items():
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from .camera import Camera
|
|
2
2
|
from .animation_player import AnimationPlayer, SpriteAnimationSet
|
|
3
|
+
from .body import Body
|
|
4
|
+
from .world import PhysicsWorld
|
|
3
5
|
from .collision_cache import (
|
|
4
6
|
CollisionCache,
|
|
5
7
|
clear_collision_cache,
|
|
@@ -10,22 +12,19 @@ from .collision_cache import (
|
|
|
10
12
|
load_object_collision,
|
|
11
13
|
load_tileset_collision,
|
|
12
14
|
)
|
|
13
|
-
from .
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
MovementMode,
|
|
18
|
-
rect_vs_tilemap,
|
|
19
|
-
)
|
|
15
|
+
from .map_object import MapObject, load_map_objects
|
|
16
|
+
from .movement import CollisionResult, CollisionRunner, MovementMode
|
|
17
|
+
from .polygon_query import rect_vs_tilemap
|
|
18
|
+
from .protocols import ICollidable, ICollidableObject, ICollidableSprite
|
|
20
19
|
from .map_loader import TilemapData, load_map
|
|
21
|
-
from .
|
|
20
|
+
from .collision import (
|
|
22
21
|
CollisionHit,
|
|
23
|
-
ICollidableObject,
|
|
24
22
|
ObjectCollisionManager,
|
|
25
23
|
check_collision,
|
|
26
24
|
)
|
|
27
25
|
from .renderer import LayerRenderStats, TileLayerRenderer
|
|
28
26
|
from .area_node import AreaNode
|
|
27
|
+
from . import navigation
|
|
29
28
|
from .particles import (
|
|
30
29
|
Particle,
|
|
31
30
|
ParticleEmitter,
|
|
@@ -39,14 +38,17 @@ from .particles import (
|
|
|
39
38
|
__all__ = [
|
|
40
39
|
"AnimationPlayer",
|
|
41
40
|
"AreaNode",
|
|
41
|
+
"Body",
|
|
42
42
|
"Camera",
|
|
43
43
|
"CollisionCache",
|
|
44
44
|
"CollisionHit",
|
|
45
45
|
"CollisionResult",
|
|
46
46
|
"CollisionRunner",
|
|
47
|
+
"ICollidable",
|
|
47
48
|
"ICollidableObject",
|
|
48
49
|
"ICollidableSprite",
|
|
49
50
|
"LayerRenderStats",
|
|
51
|
+
"MapObject",
|
|
50
52
|
"MovementMode",
|
|
51
53
|
"ObjectCollisionManager",
|
|
52
54
|
"SpriteAnimationSet",
|
|
@@ -59,6 +61,7 @@ __all__ = [
|
|
|
59
61
|
"get_cached_tileset_collision",
|
|
60
62
|
"load_character_collision",
|
|
61
63
|
"load_map",
|
|
64
|
+
"load_map_objects",
|
|
62
65
|
"load_object_collision",
|
|
63
66
|
"load_tileset_collision",
|
|
64
67
|
"Particle",
|
|
@@ -66,6 +69,7 @@ __all__ = [
|
|
|
66
69
|
"ParticleEmitterNode",
|
|
67
70
|
"ParticleRenderer",
|
|
68
71
|
"ParticleSystem",
|
|
72
|
+
"PhysicsWorld",
|
|
69
73
|
"rect_vs_tilemap",
|
|
70
74
|
"SpriteBatchRenderer",
|
|
71
75
|
"clear_texture_caches",
|
|
@@ -8,11 +8,18 @@ from ..parser.node_parse import ParsedNode
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class AreaNode:
|
|
11
|
-
def __init__(self, parsed: ParsedNode) -> None:
|
|
11
|
+
def __init__(self, parsed: ParsedNode, render_scale: float = 1.0) -> None:
|
|
12
12
|
self.node_id = parsed.node_id
|
|
13
13
|
self.name = parsed.name
|
|
14
14
|
self.node_type = parsed.node_type
|
|
15
|
-
self.
|
|
15
|
+
self.render_scale = render_scale
|
|
16
|
+
rs = render_scale
|
|
17
|
+
self._rect = Rect(
|
|
18
|
+
int(parsed.area.x * rs),
|
|
19
|
+
int(parsed.area.y * rs),
|
|
20
|
+
int(parsed.area.w * rs),
|
|
21
|
+
int(parsed.area.h * rs),
|
|
22
|
+
)
|
|
16
23
|
self.layer_name = parsed.layer_name
|
|
17
24
|
self.properties = dict(parsed.properties)
|
|
18
25
|
self.group = parsed.group
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""Physics bodies authored into a :class:`~.world.PhysicsWorld`.
|
|
2
|
+
|
|
3
|
+
A :class:`Body` is the authoring surface for a solid in the world. It owns
|
|
4
|
+
a single primitive collision shape (rectangle, circle, or capsule — polygon
|
|
5
|
+
shapes stay in the ``MapObject`` lane) plus its position and velocity, and
|
|
6
|
+
participates in collision detection through the same
|
|
7
|
+
``ICollidableObject`` contract as :class:`~.map_object.MapObject`
|
|
8
|
+
(owner-local shape, ``obj.x + vertex`` applied once by the narrowphase).
|
|
9
|
+
|
|
10
|
+
Bodies are NOT self-moving. ``mode == "kinematic"`` marks a body the game
|
|
11
|
+
moves explicitly each frame (e.g. a crate pushed with ``move_grounded``);
|
|
12
|
+
``mode == "static"`` marks a body that never moves (e.g. scenery, furniture).
|
|
13
|
+
Neither mode implies physics-engine dynamics — velocity is scripted, Godot
|
|
14
|
+
``StaticBody2D`` / ``CharacterBody2D`` style.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import math
|
|
20
|
+
from typing import Optional, Tuple
|
|
21
|
+
|
|
22
|
+
from ..parser.collision import (
|
|
23
|
+
CapsuleShape,
|
|
24
|
+
CircleShape,
|
|
25
|
+
CollisionPolygon,
|
|
26
|
+
RectangleShape,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
BodyMode = str # "static" | "kinematic"
|
|
30
|
+
|
|
31
|
+
BODY_MODES = ("static", "kinematic")
|
|
32
|
+
|
|
33
|
+
BodyShape = RectangleShape | CircleShape | CapsuleShape
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class Body:
|
|
37
|
+
"""A solid body with a single primitive collision shape."""
|
|
38
|
+
|
|
39
|
+
__slots__ = (
|
|
40
|
+
"collision_layer",
|
|
41
|
+
"collision_mask",
|
|
42
|
+
"collision_shape",
|
|
43
|
+
"game_id",
|
|
44
|
+
"mode",
|
|
45
|
+
"on_ground",
|
|
46
|
+
"vx",
|
|
47
|
+
"vy",
|
|
48
|
+
"x",
|
|
49
|
+
"y",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
def __init__(
|
|
53
|
+
self,
|
|
54
|
+
collision_shape: BodyShape,
|
|
55
|
+
x: float = 0.0,
|
|
56
|
+
y: float = 0.0,
|
|
57
|
+
*,
|
|
58
|
+
vx: float = 0.0,
|
|
59
|
+
vy: float = 0.0,
|
|
60
|
+
mode: BodyMode = "static",
|
|
61
|
+
collision_layer: int = 1,
|
|
62
|
+
collision_mask: int = 0xFFFFFFFF,
|
|
63
|
+
game_id: str = "",
|
|
64
|
+
):
|
|
65
|
+
"""
|
|
66
|
+
Create a body.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
collision_shape: Primitive shape (RectangleShape, CircleShape,
|
|
70
|
+
or CapsuleShape). Polygon shapes are not supported on
|
|
71
|
+
bodies — use :class:`MapObject` for polygon solids.
|
|
72
|
+
x: World X position of the shape origin (top-left / center
|
|
73
|
+
per shape offset semantics).
|
|
74
|
+
y: World Y position.
|
|
75
|
+
vx: X velocity (for kinematic bodies).
|
|
76
|
+
vy: Y velocity (for kinematic bodies).
|
|
77
|
+
mode: ``"static"`` (never moves) or ``"kinematic"`` (moved
|
|
78
|
+
explicitly by the game).
|
|
79
|
+
collision_layer: Layer this body is on (default 1).
|
|
80
|
+
collision_mask: Layers this body collides with (default all).
|
|
81
|
+
game_id: Optional label for debugging.
|
|
82
|
+
"""
|
|
83
|
+
if not isinstance(collision_shape, (RectangleShape, CircleShape, CapsuleShape)):
|
|
84
|
+
raise TypeError(
|
|
85
|
+
"Body requires a primitive shape (RectangleShape, CircleShape, "
|
|
86
|
+
f"or CapsuleShape), got {type(collision_shape).__name__}"
|
|
87
|
+
)
|
|
88
|
+
if mode not in BODY_MODES:
|
|
89
|
+
raise ValueError(
|
|
90
|
+
f"mode must be one of {BODY_MODES}, got {mode!r}"
|
|
91
|
+
)
|
|
92
|
+
self.collision_shape = collision_shape
|
|
93
|
+
self.x = x
|
|
94
|
+
self.y = y
|
|
95
|
+
self.vx = vx
|
|
96
|
+
self.vy = vy
|
|
97
|
+
self.mode = mode
|
|
98
|
+
self.collision_layer = collision_layer
|
|
99
|
+
self.collision_mask = collision_mask
|
|
100
|
+
self.game_id = game_id
|
|
101
|
+
self.on_ground = False
|
|
102
|
+
|
|
103
|
+
def __repr__(self) -> str:
|
|
104
|
+
return (
|
|
105
|
+
f"Body(shape={type(self.collision_shape).__name__}, x={self.x}, "
|
|
106
|
+
f"y={self.y}, mode={self.mode!r}, game_id={self.game_id!r})"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# ------------------------------------------------------------------
|
|
110
|
+
# Geometry helpers used by the movement resolver
|
|
111
|
+
# ------------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
def top_y_at(self, world_x: float) -> Optional[float]:
|
|
114
|
+
"""Return the top-surface world Y of this body at *world_x*, or None.
|
|
115
|
+
|
|
116
|
+
Only the top surface is sampled — bodies are never one-way, but the
|
|
117
|
+
resolver only needs the topmost surface for ground landing.
|
|
118
|
+
"""
|
|
119
|
+
shape = self.collision_shape
|
|
120
|
+
if isinstance(shape, RectangleShape):
|
|
121
|
+
left = self.x + shape.offset[0]
|
|
122
|
+
if left <= world_x <= left + shape.width:
|
|
123
|
+
return self.y + shape.offset[1]
|
|
124
|
+
return None
|
|
125
|
+
|
|
126
|
+
if isinstance(shape, CircleShape):
|
|
127
|
+
cx = self.x + shape.offset[0]
|
|
128
|
+
cy = self.y + shape.offset[1]
|
|
129
|
+
return _circle_top_y(cx, cy, shape.radius, world_x)
|
|
130
|
+
|
|
131
|
+
# CapsuleShape — vertical segment (top cap center, radius, height)
|
|
132
|
+
px = self.x + shape.offset[0]
|
|
133
|
+
py = self.y + shape.offset[1]
|
|
134
|
+
return _circle_top_y(px, py, shape.radius, world_x)
|
|
135
|
+
|
|
136
|
+
def as_polygon(self) -> CollisionPolygon:
|
|
137
|
+
"""World-space polygon approximation of this body's shape.
|
|
138
|
+
|
|
139
|
+
Used only by slide-mode normal computation (the tile resolver works
|
|
140
|
+
on polygon edges). Circles/capsules are approximated with enough
|
|
141
|
+
edges that the closest-edge normal is visually exact.
|
|
142
|
+
"""
|
|
143
|
+
shape = self.collision_shape
|
|
144
|
+
if isinstance(shape, RectangleShape):
|
|
145
|
+
left = self.x + shape.offset[0]
|
|
146
|
+
top = self.y + shape.offset[1]
|
|
147
|
+
return CollisionPolygon(
|
|
148
|
+
vertices=[
|
|
149
|
+
(left, top),
|
|
150
|
+
(left + shape.width, top),
|
|
151
|
+
(left + shape.width, top + shape.height),
|
|
152
|
+
(left, top + shape.height),
|
|
153
|
+
]
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
if isinstance(shape, CircleShape):
|
|
157
|
+
cx = self.x + shape.offset[0]
|
|
158
|
+
cy = self.y + shape.offset[1]
|
|
159
|
+
return CollisionPolygon(
|
|
160
|
+
vertices=_ngon(cx, cy, shape.radius, 16)
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
# Capsule — top cap semicircle, implicit side edges, bottom cap
|
|
164
|
+
px = self.x + shape.offset[0]
|
|
165
|
+
py = self.y + shape.offset[1]
|
|
166
|
+
bx = px
|
|
167
|
+
by = py + shape.height
|
|
168
|
+
r = shape.radius
|
|
169
|
+
steps = 4
|
|
170
|
+
verts: list[Tuple[float, float]] = []
|
|
171
|
+
# Top cap — left (pi) to right (0) through the top (3pi/2 = up)
|
|
172
|
+
for k in range(steps + 1):
|
|
173
|
+
a = math.pi + (math.pi * k / steps)
|
|
174
|
+
verts.append((px + r * math.cos(a), py + r * math.sin(a)))
|
|
175
|
+
# Bottom cap — right (0) to left (pi) through the bottom (pi/2 = down)
|
|
176
|
+
for k in range(steps + 1):
|
|
177
|
+
a = math.pi * k / steps
|
|
178
|
+
verts.append((bx + r * math.cos(a), by + r * math.sin(a)))
|
|
179
|
+
return CollisionPolygon(vertices=verts)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _circle_top_y(cx: float, cy: float, radius: float, world_x: float) -> Optional[float]:
|
|
183
|
+
"""Top-surface Y of a circle at *world_x* (upper semicircle), or None."""
|
|
184
|
+
dx = world_x - cx
|
|
185
|
+
if abs(dx) > radius:
|
|
186
|
+
return None
|
|
187
|
+
return cy - math.sqrt(radius * radius - dx * dx)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _ngon(cx: float, cy: float, radius: float, edges: int) -> list[Tuple[float, float]]:
|
|
191
|
+
"""Vertices of a regular polygon approximating a circle."""
|
|
192
|
+
return [
|
|
193
|
+
(
|
|
194
|
+
cx + radius * math.cos(2 * math.pi * i / edges),
|
|
195
|
+
cy + radius * math.sin(2 * math.pi * i / edges),
|
|
196
|
+
)
|
|
197
|
+
for i in range(edges)
|
|
198
|
+
]
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Object-to-object collision package.
|
|
2
|
+
|
|
3
|
+
Shape-level narrowphase lives in :mod:`shapes`, hit results and pair
|
|
4
|
+
queries in :mod:`hit`, and the spatial-grid manager in :mod:`manager`.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .hit import CollisionHit, check_collision, should_collide
|
|
8
|
+
from .manager import ObjectCollisionManager
|
|
9
|
+
|
|
10
|
+
__all__ = ["CollisionHit", "ObjectCollisionManager", "check_collision", "should_collide"]
|