tilemap-parser 3.1.14__tar.gz → 3.1.15__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {tilemap_parser-3.1.14 → tilemap_parser-3.1.15}/PKG-INFO +1 -1
- {tilemap_parser-3.1.14 → tilemap_parser-3.1.15}/pyproject.toml +1 -1
- {tilemap_parser-3.1.14 → tilemap_parser-3.1.15}/src/tilemap_parser/runtime/tile_collision.py +638 -94
- {tilemap_parser-3.1.14 → tilemap_parser-3.1.15}/src/tilemap_parser/utils/geometry.py +4 -4
- {tilemap_parser-3.1.14 → tilemap_parser-3.1.15}/src/tilemap_parser.egg-info/PKG-INFO +1 -1
- {tilemap_parser-3.1.14 → tilemap_parser-3.1.15}/tests/test_geometry.py +4 -4
- {tilemap_parser-3.1.14 → tilemap_parser-3.1.15}/tests/test_object_collision.py +20 -0
- {tilemap_parser-3.1.14 → tilemap_parser-3.1.15}/tests/test_tile_collision.py +118 -1
- {tilemap_parser-3.1.14 → tilemap_parser-3.1.15}/LICENSE +0 -0
- {tilemap_parser-3.1.14 → tilemap_parser-3.1.15}/README.md +0 -0
- {tilemap_parser-3.1.14 → tilemap_parser-3.1.15}/setup.cfg +0 -0
- {tilemap_parser-3.1.14 → tilemap_parser-3.1.15}/src/tilemap_parser/__init__.py +0 -0
- {tilemap_parser-3.1.14 → tilemap_parser-3.1.15}/src/tilemap_parser/parser/__init__.py +0 -0
- {tilemap_parser-3.1.14 → tilemap_parser-3.1.15}/src/tilemap_parser/parser/animation.py +0 -0
- {tilemap_parser-3.1.14 → tilemap_parser-3.1.15}/src/tilemap_parser/parser/collision.py +0 -0
- {tilemap_parser-3.1.14 → tilemap_parser-3.1.15}/src/tilemap_parser/parser/collision_loader.py +0 -0
- {tilemap_parser-3.1.14 → tilemap_parser-3.1.15}/src/tilemap_parser/parser/map_parse.py +0 -0
- {tilemap_parser-3.1.14 → tilemap_parser-3.1.15}/src/tilemap_parser/parser/node_parse.py +0 -0
- {tilemap_parser-3.1.14 → tilemap_parser-3.1.15}/src/tilemap_parser/parser/particle.py +0 -0
- {tilemap_parser-3.1.14 → tilemap_parser-3.1.15}/src/tilemap_parser/runtime/__init__.py +0 -0
- {tilemap_parser-3.1.14 → tilemap_parser-3.1.15}/src/tilemap_parser/runtime/animation_player.py +0 -0
- {tilemap_parser-3.1.14 → tilemap_parser-3.1.15}/src/tilemap_parser/runtime/area_node.py +0 -0
- {tilemap_parser-3.1.14 → tilemap_parser-3.1.15}/src/tilemap_parser/runtime/collision_cache.py +0 -0
- {tilemap_parser-3.1.14 → tilemap_parser-3.1.15}/src/tilemap_parser/runtime/map_loader.py +0 -0
- {tilemap_parser-3.1.14 → tilemap_parser-3.1.15}/src/tilemap_parser/runtime/object_collision.py +0 -0
- {tilemap_parser-3.1.14 → tilemap_parser-3.1.15}/src/tilemap_parser/runtime/particles.py +0 -0
- {tilemap_parser-3.1.14 → tilemap_parser-3.1.15}/src/tilemap_parser/runtime/renderer.py +0 -0
- {tilemap_parser-3.1.14 → tilemap_parser-3.1.15}/src/tilemap_parser/utils/__init__.py +0 -0
- {tilemap_parser-3.1.14 → tilemap_parser-3.1.15}/src/tilemap_parser.egg-info/SOURCES.txt +0 -0
- {tilemap_parser-3.1.14 → tilemap_parser-3.1.15}/src/tilemap_parser.egg-info/dependency_links.txt +0 -0
- {tilemap_parser-3.1.14 → tilemap_parser-3.1.15}/src/tilemap_parser.egg-info/requires.txt +0 -0
- {tilemap_parser-3.1.14 → tilemap_parser-3.1.15}/src/tilemap_parser.egg-info/top_level.txt +0 -0
- {tilemap_parser-3.1.14 → tilemap_parser-3.1.15}/tests/test_collision.py +0 -0
- {tilemap_parser-3.1.14 → tilemap_parser-3.1.15}/tests/test_map_loader.py +0 -0
- {tilemap_parser-3.1.14 → tilemap_parser-3.1.15}/tests/test_render_scale.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tilemap-parser
|
|
3
|
-
Version: 3.1.
|
|
3
|
+
Version: 3.1.15
|
|
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 = "3.1.
|
|
7
|
+
version = "3.1.15"
|
|
8
8
|
description = "Standalone parser/loader for tilemap-editor JSON maps, sprite animations, and collision detection runtime."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
{tilemap_parser-3.1.14 → tilemap_parser-3.1.15}/src/tilemap_parser/runtime/tile_collision.py
RENAMED
|
@@ -97,6 +97,27 @@ def _point_in_polygon_offset(
|
|
|
97
97
|
return inside
|
|
98
98
|
|
|
99
99
|
|
|
100
|
+
def _segments_intersect(
|
|
101
|
+
ax: float,
|
|
102
|
+
ay: float,
|
|
103
|
+
bx: float,
|
|
104
|
+
by: float,
|
|
105
|
+
cx: float,
|
|
106
|
+
cy: float,
|
|
107
|
+
dx: float,
|
|
108
|
+
dy: float,
|
|
109
|
+
) -> bool:
|
|
110
|
+
"""Check if segment AB intersects CD (open — ignores collinear/endpoint)."""
|
|
111
|
+
o1 = (bx - ax) * (cy - ay) - (by - ay) * (cx - ax)
|
|
112
|
+
o2 = (bx - ax) * (dy - ay) - (by - ay) * (dx - ax)
|
|
113
|
+
o3 = (dx - cx) * (ay - cy) - (dy - cy) * (ax - cx)
|
|
114
|
+
o4 = (dx - cx) * (by - cy) - (dy - cy) * (bx - cx)
|
|
115
|
+
if (o1 > 0 and o2 < 0) or (o1 < 0 and o2 > 0):
|
|
116
|
+
if (o3 > 0 and o4 < 0) or (o3 < 0 and o4 > 0):
|
|
117
|
+
return True
|
|
118
|
+
return False
|
|
119
|
+
|
|
120
|
+
|
|
100
121
|
def rect_polygon_collision(
|
|
101
122
|
rect_x: float, rect_y: float, rect_w: float, rect_h: float, vertices: List[Point]
|
|
102
123
|
) -> bool:
|
|
@@ -115,29 +136,39 @@ def rect_polygon_collision(
|
|
|
115
136
|
min_vy = vy
|
|
116
137
|
elif vy > max_vy:
|
|
117
138
|
max_vy = vy
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
or rect_y > max_vy
|
|
122
|
-
or rect_y + rect_h < min_vy
|
|
123
|
-
):
|
|
139
|
+
rx2 = rect_x + rect_w
|
|
140
|
+
ry2 = rect_y + rect_h
|
|
141
|
+
if rect_x > max_vx or rx2 < min_vx or rect_y > max_vy or ry2 < min_vy:
|
|
124
142
|
return False
|
|
125
143
|
|
|
126
144
|
# Corner tests — no tuple allocation
|
|
127
145
|
if point_in_polygon((rect_x, rect_y), vertices):
|
|
128
146
|
return True
|
|
129
|
-
if point_in_polygon((
|
|
147
|
+
if point_in_polygon((rx2, rect_y), vertices):
|
|
130
148
|
return True
|
|
131
|
-
if point_in_polygon((rect_x,
|
|
149
|
+
if point_in_polygon((rect_x, ry2), vertices):
|
|
132
150
|
return True
|
|
133
|
-
if point_in_polygon((
|
|
151
|
+
if point_in_polygon((rx2, ry2), vertices):
|
|
134
152
|
return True
|
|
135
153
|
|
|
136
154
|
# Vertex-in-rect
|
|
137
|
-
rx2, ry2 = rect_x + rect_w, rect_y + rect_h
|
|
138
155
|
for vx, vy in vertices:
|
|
139
156
|
if rect_x <= vx <= rx2 and rect_y <= vy <= ry2:
|
|
140
157
|
return True
|
|
158
|
+
|
|
159
|
+
# Edge-edge intersection (catches triangle-vs-rectangle cases)
|
|
160
|
+
rect_edges = (
|
|
161
|
+
(rect_x, rect_y, rx2, rect_y),
|
|
162
|
+
(rx2, rect_y, rx2, ry2),
|
|
163
|
+
(rx2, ry2, rect_x, ry2),
|
|
164
|
+
(rect_x, ry2, rect_x, rect_y),
|
|
165
|
+
)
|
|
166
|
+
for rax, ray, rbx, rby in rect_edges:
|
|
167
|
+
for i in range(n):
|
|
168
|
+
p1x, p1y = vertices[i]
|
|
169
|
+
p2x, p2y = vertices[(i + 1) % n]
|
|
170
|
+
if _segments_intersect(rax, ray, rbx, rby, p1x, p1y, p2x, p2y):
|
|
171
|
+
return True
|
|
141
172
|
return False
|
|
142
173
|
|
|
143
174
|
|
|
@@ -191,6 +222,21 @@ def _rect_polygon_collision_offset(
|
|
|
191
222
|
wx, wy = vx * scale + ox, vy * scale + oy
|
|
192
223
|
if rect_x <= wx <= rx2 and rect_y <= wy <= ry2:
|
|
193
224
|
return True
|
|
225
|
+
|
|
226
|
+
# Edge-edge intersection (catches triangle-vs-rectangle cases)
|
|
227
|
+
for i in range(n):
|
|
228
|
+
p1x = vertices[i][0] * scale + ox
|
|
229
|
+
p1y = vertices[i][1] * scale + oy
|
|
230
|
+
p2x = vertices[(i + 1) % n][0] * scale + ox
|
|
231
|
+
p2y = vertices[(i + 1) % n][1] * scale + oy
|
|
232
|
+
if _segments_intersect(rect_x, rect_y, rx2, rect_y, p1x, p1y, p2x, p2y):
|
|
233
|
+
return True
|
|
234
|
+
if _segments_intersect(rx2, rect_y, rx2, ry2, p1x, p1y, p2x, p2y):
|
|
235
|
+
return True
|
|
236
|
+
if _segments_intersect(rx2, ry2, rect_x, ry2, p1x, p1y, p2x, p2y):
|
|
237
|
+
return True
|
|
238
|
+
if _segments_intersect(rect_x, ry2, rect_x, rect_y, p1x, p1y, p2x, p2y):
|
|
239
|
+
return True
|
|
194
240
|
return False
|
|
195
241
|
|
|
196
242
|
|
|
@@ -399,6 +445,8 @@ class CollisionRunner:
|
|
|
399
445
|
self.ground_snap_tolerance = 2.0
|
|
400
446
|
self.step_height = 4.0
|
|
401
447
|
|
|
448
|
+
self.max_walk_angle = 60.0 # degrees from horizontal; steeper = wall
|
|
449
|
+
|
|
402
450
|
self.slide_friction = 0.1
|
|
403
451
|
|
|
404
452
|
self.rpg_snap_to_grid = False
|
|
@@ -656,6 +704,18 @@ class CollisionRunner:
|
|
|
656
704
|
sprite.y = old_y
|
|
657
705
|
result.hit_wall_y = True
|
|
658
706
|
|
|
707
|
+
if not x_collided and not y_collided:
|
|
708
|
+
if abs(delta_x) >= abs(delta_y):
|
|
709
|
+
sprite.y = old_y
|
|
710
|
+
y_collided = True
|
|
711
|
+
result.hit_wall_y = True
|
|
712
|
+
result.slide_vector = (delta_x, 0.0)
|
|
713
|
+
else:
|
|
714
|
+
sprite.x = old_x
|
|
715
|
+
x_collided = True
|
|
716
|
+
result.hit_wall_x = True
|
|
717
|
+
result.slide_vector = (0.0, delta_y)
|
|
718
|
+
|
|
659
719
|
result.final_x = sprite.x
|
|
660
720
|
result.final_y = sprite.y
|
|
661
721
|
|
|
@@ -777,17 +837,22 @@ class CollisionRunner:
|
|
|
777
837
|
delta_x = sprite.vx * dt
|
|
778
838
|
delta_y = sprite.vy * dt
|
|
779
839
|
old_x, old_y = sprite.x, sprite.y
|
|
840
|
+
_, _, _, old_bottom = get_shape_bounds(sprite)
|
|
780
841
|
|
|
781
842
|
# X axis
|
|
782
843
|
sprite.x = old_x + delta_x
|
|
783
844
|
# Lift above ground snap overlap so ground doesn't block horizontal movement
|
|
784
845
|
sprite.y = old_y - self.ground_snap_tolerance
|
|
785
846
|
stepped_up = False
|
|
786
|
-
if self.
|
|
847
|
+
if self._collides_at_platformer(
|
|
848
|
+
sprite, tileset_collision, tile_map, include_one_way=False
|
|
849
|
+
):
|
|
787
850
|
if delta_x != 0:
|
|
788
851
|
# Try stepping up onto slope/stairs
|
|
789
852
|
sprite.y = old_y - self.ground_snap_tolerance - self.step_height
|
|
790
|
-
if not self.
|
|
853
|
+
if not self._collides_at_platformer(
|
|
854
|
+
sprite, tileset_collision, tile_map, include_one_way=False
|
|
855
|
+
):
|
|
791
856
|
sprite.y = old_y - self.step_height
|
|
792
857
|
stepped_up = True
|
|
793
858
|
else:
|
|
@@ -853,7 +918,9 @@ class CollisionRunner:
|
|
|
853
918
|
for _ in range(8):
|
|
854
919
|
mid = (lo + hi) * 0.5
|
|
855
920
|
sprite.y = mid
|
|
856
|
-
if self.
|
|
921
|
+
if self._collides_at_platformer(
|
|
922
|
+
sprite, tileset_collision, tile_map, include_one_way=False
|
|
923
|
+
):
|
|
857
924
|
hi = mid
|
|
858
925
|
else:
|
|
859
926
|
lo = mid
|
|
@@ -871,7 +938,13 @@ class CollisionRunner:
|
|
|
871
938
|
for _ in range(8):
|
|
872
939
|
mid = (lo + hi) * 0.5
|
|
873
940
|
sprite.y = mid
|
|
874
|
-
if self.
|
|
941
|
+
if self._collides_at_platformer(
|
|
942
|
+
sprite,
|
|
943
|
+
tileset_collision,
|
|
944
|
+
tile_map,
|
|
945
|
+
include_one_way=True,
|
|
946
|
+
previous_bottom=old_bottom,
|
|
947
|
+
):
|
|
875
948
|
hi = mid
|
|
876
949
|
else:
|
|
877
950
|
lo = mid
|
|
@@ -887,14 +960,67 @@ class CollisionRunner:
|
|
|
887
960
|
|
|
888
961
|
downward_travel = max(0.0, sprite.vy) * dt
|
|
889
962
|
if not sprite.on_ground and 0 <= downward_travel <= self.ground_snap_tolerance:
|
|
890
|
-
if self.
|
|
963
|
+
if self._collides_at_platformer(
|
|
964
|
+
sprite,
|
|
965
|
+
tileset_collision,
|
|
966
|
+
tile_map,
|
|
967
|
+
include_one_way=True,
|
|
968
|
+
previous_bottom=old_bottom,
|
|
969
|
+
):
|
|
970
|
+
saved_y = sprite.y
|
|
971
|
+
sprite.y = saved_y - self.ground_snap_tolerance
|
|
972
|
+
if not self._collides_at_platformer(
|
|
973
|
+
sprite,
|
|
974
|
+
tileset_collision,
|
|
975
|
+
tile_map,
|
|
976
|
+
include_one_way=True,
|
|
977
|
+
previous_bottom=old_bottom,
|
|
978
|
+
):
|
|
979
|
+
lo, hi = sprite.y, saved_y
|
|
980
|
+
for _ in range(8):
|
|
981
|
+
mid = (lo + hi) * 0.5
|
|
982
|
+
sprite.y = mid
|
|
983
|
+
if self._collides_at_platformer(
|
|
984
|
+
sprite,
|
|
985
|
+
tileset_collision,
|
|
986
|
+
tile_map,
|
|
987
|
+
include_one_way=True,
|
|
988
|
+
previous_bottom=old_bottom,
|
|
989
|
+
):
|
|
990
|
+
hi = mid
|
|
991
|
+
else:
|
|
992
|
+
lo = mid
|
|
993
|
+
sprite.y = lo
|
|
994
|
+
else:
|
|
995
|
+
sprite.y = saved_y
|
|
891
996
|
sprite.on_ground = True
|
|
892
997
|
result.on_ground = True
|
|
893
998
|
sprite.vy = 0.0
|
|
894
999
|
else:
|
|
895
1000
|
saved_y = sprite.y
|
|
896
1001
|
sprite.y += self.ground_snap_tolerance
|
|
897
|
-
if self.
|
|
1002
|
+
if self._collides_at_platformer(
|
|
1003
|
+
sprite,
|
|
1004
|
+
tileset_collision,
|
|
1005
|
+
tile_map,
|
|
1006
|
+
include_one_way=True,
|
|
1007
|
+
previous_bottom=old_bottom,
|
|
1008
|
+
):
|
|
1009
|
+
lo, hi = saved_y, sprite.y
|
|
1010
|
+
for _ in range(8):
|
|
1011
|
+
mid = (lo + hi) * 0.5
|
|
1012
|
+
sprite.y = mid
|
|
1013
|
+
if self._collides_at_platformer(
|
|
1014
|
+
sprite,
|
|
1015
|
+
tileset_collision,
|
|
1016
|
+
tile_map,
|
|
1017
|
+
include_one_way=True,
|
|
1018
|
+
previous_bottom=old_bottom,
|
|
1019
|
+
):
|
|
1020
|
+
hi = mid
|
|
1021
|
+
else:
|
|
1022
|
+
lo = mid
|
|
1023
|
+
sprite.y = lo
|
|
898
1024
|
sprite.on_ground = True
|
|
899
1025
|
result.on_ground = True
|
|
900
1026
|
sprite.vy = 0.0
|
|
@@ -906,6 +1032,291 @@ class CollisionRunner:
|
|
|
906
1032
|
result.collided = result.hit_wall_x or collided_y
|
|
907
1033
|
return result
|
|
908
1034
|
|
|
1035
|
+
def _walkable_slope_at(
|
|
1036
|
+
self,
|
|
1037
|
+
sprite: ICollidableSprite,
|
|
1038
|
+
tileset_collision: TilesetCollision,
|
|
1039
|
+
tile_map: dict,
|
|
1040
|
+
motion_x: float,
|
|
1041
|
+
motion_y: float,
|
|
1042
|
+
walk_upness: float,
|
|
1043
|
+
) -> Optional[float]:
|
|
1044
|
+
"""Check if there is a walkable slope at the sprite's current position.
|
|
1045
|
+
Iterates all overlapping tiles and picks the best walkable edge.
|
|
1046
|
+
Returns the Y offset to apply, or None if not a walkable slope.
|
|
1047
|
+
"""
|
|
1048
|
+
left, top, right, bottom = get_shape_bounds(sprite)
|
|
1049
|
+
tw, th = self._eff_tw, self._eff_th
|
|
1050
|
+
min_tile_x = int(left // tw) - 1
|
|
1051
|
+
max_tile_x = int(right // tw) + 1
|
|
1052
|
+
min_tile_y = int(top // th) - 1
|
|
1053
|
+
max_tile_y = int(bottom // th) + 1
|
|
1054
|
+
|
|
1055
|
+
best_adj_y = None
|
|
1056
|
+
|
|
1057
|
+
for tile_y in range(min_tile_y, max_tile_y + 1):
|
|
1058
|
+
for tile_x in range(min_tile_x, max_tile_x + 1):
|
|
1059
|
+
tile_id = tile_map.get((tile_x, tile_y))
|
|
1060
|
+
if tile_id is None:
|
|
1061
|
+
continue
|
|
1062
|
+
tile_data = tileset_collision.tiles.get(tile_id)
|
|
1063
|
+
if tile_data is None:
|
|
1064
|
+
continue
|
|
1065
|
+
ox = tile_x * tw
|
|
1066
|
+
oy = tile_y * th
|
|
1067
|
+
for poly in tile_data.shapes:
|
|
1068
|
+
if not poly.is_valid():
|
|
1069
|
+
continue
|
|
1070
|
+
if self._is_full_rect(poly):
|
|
1071
|
+
continue
|
|
1072
|
+
if not _check_sprite_polygon_offset(
|
|
1073
|
+
sprite, poly, ox, oy, self.render_scale
|
|
1074
|
+
):
|
|
1075
|
+
continue
|
|
1076
|
+
|
|
1077
|
+
# Non-full-rect polygon overlapping the sprite — find the best edge.
|
|
1078
|
+
verts = poly.vertices
|
|
1079
|
+
n = len(verts)
|
|
1080
|
+
cx = sum(verts[j][0] for j in range(n)) / n * self.render_scale + ox
|
|
1081
|
+
cy = sum(verts[j][1] for j in range(n)) / n * self.render_scale + oy
|
|
1082
|
+
for i in range(n):
|
|
1083
|
+
v1x = verts[i][0] * self.render_scale + ox
|
|
1084
|
+
v1y = verts[i][1] * self.render_scale + oy
|
|
1085
|
+
v2x = verts[(i + 1) % n][0] * self.render_scale + ox
|
|
1086
|
+
v2y = verts[(i + 1) % n][1] * self.render_scale + oy
|
|
1087
|
+
|
|
1088
|
+
ex = v2x - v1x
|
|
1089
|
+
ey = v2y - v1y
|
|
1090
|
+
e_len = math.sqrt(ex * ex + ey * ey)
|
|
1091
|
+
if e_len < 0.01:
|
|
1092
|
+
continue
|
|
1093
|
+
|
|
1094
|
+
# Two perpendicular normals
|
|
1095
|
+
nx_a = -ey / e_len
|
|
1096
|
+
ny_a = ex / e_len
|
|
1097
|
+
nx_b = -nx_a
|
|
1098
|
+
ny_b = -ny_a
|
|
1099
|
+
|
|
1100
|
+
# Pick the one pointing INTO the solid (toward polygon interior).
|
|
1101
|
+
# Use the polygon centroid to determine which side is interior.
|
|
1102
|
+
mx = (v1x + v2x) * 0.5
|
|
1103
|
+
my = (v1y + v2y) * 0.5
|
|
1104
|
+
to_centroid_x = cx - mx
|
|
1105
|
+
to_centroid_y = cy - my
|
|
1106
|
+
if nx_a * to_centroid_x + ny_a * to_centroid_y < 0:
|
|
1107
|
+
nx_a = -nx_a
|
|
1108
|
+
ny_a = -ny_a
|
|
1109
|
+
nx_b = -nx_b
|
|
1110
|
+
ny_b = -ny_b
|
|
1111
|
+
|
|
1112
|
+
# nx_a/ny_a now points away from centroid (outward).
|
|
1113
|
+
# nx_b/ny_b points toward centroid (inward) = into the solid.
|
|
1114
|
+
|
|
1115
|
+
# For a walkable surface, the inward normal should push the
|
|
1116
|
+
# player UP (negative screen y when inward is upward).
|
|
1117
|
+
# Inward normal = (nx_b, ny_b) = (-nx_a, -ny_a).
|
|
1118
|
+
# "Upward" in the inward sense means the y-component of
|
|
1119
|
+
# inward normal is negative (pointing up on screen).
|
|
1120
|
+
inward_up = -ny_b # inward normal y component, negated
|
|
1121
|
+
# Actually: for a floor, inward normal = (0, 1) pointing DOWN.
|
|
1122
|
+
# -inward_y = -1, which is wrong.
|
|
1123
|
+
# Better: just use the outward normal's upness.
|
|
1124
|
+
# Outward normal = (nx_a, ny_a).
|
|
1125
|
+
# For a floor, outward = (0, -1). upness = -(-1) = 1. ✓
|
|
1126
|
+
# For this diagonal, outward = ? Let ny_a determine outward.
|
|
1127
|
+
|
|
1128
|
+
outward_ny = ny_a
|
|
1129
|
+
upness = -outward_ny
|
|
1130
|
+
if upness < walk_upness:
|
|
1131
|
+
continue
|
|
1132
|
+
|
|
1133
|
+
# Check motion is INTO the surface (dot with outward normal < 0).
|
|
1134
|
+
out_nx = nx_a
|
|
1135
|
+
out_ny = ny_a
|
|
1136
|
+
dot = motion_x * out_nx + motion_y * out_ny
|
|
1137
|
+
if dot >= 0:
|
|
1138
|
+
continue
|
|
1139
|
+
|
|
1140
|
+
# Project motion along the slope surface.
|
|
1141
|
+
adj_x = motion_x - out_nx * dot
|
|
1142
|
+
adj_y = motion_y - out_ny * dot
|
|
1143
|
+
dy_off = adj_y - motion_y
|
|
1144
|
+
if best_adj_y is None or dy_off > best_adj_y:
|
|
1145
|
+
best_adj_y = dy_off
|
|
1146
|
+
|
|
1147
|
+
if best_adj_y is not None:
|
|
1148
|
+
return best_adj_y
|
|
1149
|
+
return None
|
|
1150
|
+
|
|
1151
|
+
def _is_full_rect(self, poly: CollisionPolygon) -> bool:
|
|
1152
|
+
"""Check if a polygon is a full-tile rectangle."""
|
|
1153
|
+
if len(poly.vertices) != 4:
|
|
1154
|
+
return False
|
|
1155
|
+
verts = [
|
|
1156
|
+
(vx * self.render_scale, vy * self.render_scale) for vx, vy in poly.vertices
|
|
1157
|
+
]
|
|
1158
|
+
min_x = min(v[0] for v in verts)
|
|
1159
|
+
max_x = max(v[0] for v in verts)
|
|
1160
|
+
min_y = min(v[1] for v in verts)
|
|
1161
|
+
max_y = max(v[1] for v in verts)
|
|
1162
|
+
return (
|
|
1163
|
+
abs(max_x - min_x - self._eff_tw) < 1.0
|
|
1164
|
+
and abs(max_y - min_y - self._eff_th) < 1.0
|
|
1165
|
+
)
|
|
1166
|
+
|
|
1167
|
+
def _collides_at_platformer(
|
|
1168
|
+
self,
|
|
1169
|
+
sprite: ICollidableSprite,
|
|
1170
|
+
tileset_collision: TilesetCollision,
|
|
1171
|
+
tile_map: dict,
|
|
1172
|
+
include_one_way: bool = False,
|
|
1173
|
+
previous_bottom: Optional[float] = None,
|
|
1174
|
+
) -> bool:
|
|
1175
|
+
"""Collision query for platformers, with one-way platforms gated by approach."""
|
|
1176
|
+
left, top, right, bottom = get_shape_bounds(sprite)
|
|
1177
|
+
tw, th = self._eff_tw, self._eff_th
|
|
1178
|
+
|
|
1179
|
+
min_tile_x = int(left // tw) - 1
|
|
1180
|
+
max_tile_x = int(right // tw) + 1
|
|
1181
|
+
min_tile_y = int(top // th) - 1
|
|
1182
|
+
max_tile_y = int(bottom // th) + 1
|
|
1183
|
+
|
|
1184
|
+
for tile_y in range(min_tile_y, max_tile_y + 1):
|
|
1185
|
+
for tile_x in range(min_tile_x, max_tile_x + 1):
|
|
1186
|
+
tile_id = tile_map.get((tile_x, tile_y))
|
|
1187
|
+
if tile_id is None:
|
|
1188
|
+
continue
|
|
1189
|
+
tile_data = tileset_collision.tiles.get(tile_id)
|
|
1190
|
+
if tile_data is None:
|
|
1191
|
+
continue
|
|
1192
|
+
ox = tile_x * tw
|
|
1193
|
+
oy = tile_y * th
|
|
1194
|
+
for poly in tile_data.shapes:
|
|
1195
|
+
if not poly.is_valid():
|
|
1196
|
+
continue
|
|
1197
|
+
if poly.one_way:
|
|
1198
|
+
if not include_one_way:
|
|
1199
|
+
continue
|
|
1200
|
+
platform_y = (
|
|
1201
|
+
min(v[1] for v in poly.vertices) * self.render_scale + oy
|
|
1202
|
+
)
|
|
1203
|
+
if (
|
|
1204
|
+
previous_bottom is not None
|
|
1205
|
+
and previous_bottom > platform_y + 0.5
|
|
1206
|
+
):
|
|
1207
|
+
continue
|
|
1208
|
+
if _check_sprite_polygon_offset(
|
|
1209
|
+
sprite, poly, ox, oy, self.render_scale
|
|
1210
|
+
):
|
|
1211
|
+
return True
|
|
1212
|
+
return False
|
|
1213
|
+
|
|
1214
|
+
def _walkable_edge_y_at_x(
|
|
1215
|
+
self,
|
|
1216
|
+
poly: CollisionPolygon,
|
|
1217
|
+
ox: float,
|
|
1218
|
+
oy: float,
|
|
1219
|
+
world_x: float,
|
|
1220
|
+
edge_index: int,
|
|
1221
|
+
min_upness: float,
|
|
1222
|
+
) -> Optional[float]:
|
|
1223
|
+
"""Return the world Y for a walkable polygon edge at world_x."""
|
|
1224
|
+
verts = poly.vertices
|
|
1225
|
+
n = len(verts)
|
|
1226
|
+
v1x = verts[edge_index][0] * self.render_scale + ox
|
|
1227
|
+
v1y = verts[edge_index][1] * self.render_scale + oy
|
|
1228
|
+
v2x = verts[(edge_index + 1) % n][0] * self.render_scale + ox
|
|
1229
|
+
v2y = verts[(edge_index + 1) % n][1] * self.render_scale + oy
|
|
1230
|
+
|
|
1231
|
+
min_x = min(v1x, v2x)
|
|
1232
|
+
max_x = max(v1x, v2x)
|
|
1233
|
+
if world_x < min_x - 0.01 or world_x > max_x + 0.01:
|
|
1234
|
+
return None
|
|
1235
|
+
|
|
1236
|
+
edge_x = v2x - v1x
|
|
1237
|
+
edge_y = v2y - v1y
|
|
1238
|
+
edge_len = math.sqrt(edge_x * edge_x + edge_y * edge_y)
|
|
1239
|
+
if edge_len < 0.01:
|
|
1240
|
+
return None
|
|
1241
|
+
|
|
1242
|
+
# Vertical faces are walls, never floors.
|
|
1243
|
+
if abs(edge_x) < 0.01:
|
|
1244
|
+
return None
|
|
1245
|
+
|
|
1246
|
+
normal_x = -edge_y / edge_len
|
|
1247
|
+
normal_y = edge_x / edge_len
|
|
1248
|
+
|
|
1249
|
+
cx = sum(v[0] for v in verts) / n * self.render_scale + ox
|
|
1250
|
+
cy = sum(v[1] for v in verts) / n * self.render_scale + oy
|
|
1251
|
+
mid_x = (v1x + v2x) * 0.5
|
|
1252
|
+
mid_y = (v1y + v2y) * 0.5
|
|
1253
|
+
|
|
1254
|
+
# Flip to outward normal when the candidate points toward the centroid.
|
|
1255
|
+
if normal_x * (cx - mid_x) + normal_y * (cy - mid_y) > 0:
|
|
1256
|
+
normal_x = -normal_x
|
|
1257
|
+
normal_y = -normal_y
|
|
1258
|
+
|
|
1259
|
+
upness = -normal_y
|
|
1260
|
+
if upness < min_upness:
|
|
1261
|
+
return None
|
|
1262
|
+
|
|
1263
|
+
t = (world_x - v1x) / edge_x
|
|
1264
|
+
return v1y + (v2y - v1y) * t
|
|
1265
|
+
|
|
1266
|
+
def _find_walkable_ground_y(
|
|
1267
|
+
self,
|
|
1268
|
+
sprite: ICollidableSprite,
|
|
1269
|
+
tileset_collision: TilesetCollision,
|
|
1270
|
+
tile_map: dict,
|
|
1271
|
+
max_up: float,
|
|
1272
|
+
max_down: float,
|
|
1273
|
+
include_one_way: bool = True,
|
|
1274
|
+
previous_bottom: Optional[float] = None,
|
|
1275
|
+
) -> Optional[float]:
|
|
1276
|
+
"""Find the nearest walkable floor surface under or just above the sprite."""
|
|
1277
|
+
left, top, right, bottom = get_shape_bounds(sprite)
|
|
1278
|
+
sample_xs = (left, (left + right) * 0.5, right)
|
|
1279
|
+
|
|
1280
|
+
tw, th = self._eff_tw, self._eff_th
|
|
1281
|
+
min_tile_x = int((left - 1.0) // tw) - 1
|
|
1282
|
+
max_tile_x = int((right + 1.0) // tw) + 1
|
|
1283
|
+
min_tile_y = int((bottom - max_up - th) // th) - 1
|
|
1284
|
+
max_tile_y = int((bottom + max_down + th) // th) + 1
|
|
1285
|
+
min_upness = math.cos(math.radians(self.max_walk_angle))
|
|
1286
|
+
|
|
1287
|
+
best_y: Optional[float] = None
|
|
1288
|
+
for tile_y in range(min_tile_y, max_tile_y + 1):
|
|
1289
|
+
for tile_x in range(min_tile_x, max_tile_x + 1):
|
|
1290
|
+
tile_id = tile_map.get((tile_x, tile_y))
|
|
1291
|
+
if tile_id is None:
|
|
1292
|
+
continue
|
|
1293
|
+
tile_data = tileset_collision.tiles.get(tile_id)
|
|
1294
|
+
if tile_data is None:
|
|
1295
|
+
continue
|
|
1296
|
+
ox = tile_x * tw
|
|
1297
|
+
oy = tile_y * th
|
|
1298
|
+
for poly in tile_data.shapes:
|
|
1299
|
+
if not poly.is_valid():
|
|
1300
|
+
continue
|
|
1301
|
+
if poly.one_way and not include_one_way:
|
|
1302
|
+
continue
|
|
1303
|
+
for sample_x in sample_xs:
|
|
1304
|
+
for i in range(len(poly.vertices)):
|
|
1305
|
+
ground_y = self._walkable_edge_y_at_x(
|
|
1306
|
+
poly, ox, oy, sample_x, i, min_upness
|
|
1307
|
+
)
|
|
1308
|
+
if ground_y is None:
|
|
1309
|
+
continue
|
|
1310
|
+
one_way_from_above = True
|
|
1311
|
+
if poly.one_way and previous_bottom is not None:
|
|
1312
|
+
one_way_from_above = previous_bottom <= ground_y + 0.5
|
|
1313
|
+
if not one_way_from_above:
|
|
1314
|
+
continue
|
|
1315
|
+
if bottom - max_up <= ground_y <= bottom + max_down:
|
|
1316
|
+
if best_y is None or ground_y < best_y:
|
|
1317
|
+
best_y = ground_y
|
|
1318
|
+
return best_y
|
|
1319
|
+
|
|
909
1320
|
def move_platformer_with_slide(
|
|
910
1321
|
self,
|
|
911
1322
|
sprite: ICollidableSprite,
|
|
@@ -914,116 +1325,232 @@ class CollisionRunner:
|
|
|
914
1325
|
dt: float,
|
|
915
1326
|
input_x: float = 0.0,
|
|
916
1327
|
jump_pressed: bool = False,
|
|
917
|
-
max_iterations: int = 4,
|
|
918
1328
|
) -> CollisionResult:
|
|
919
1329
|
"""
|
|
920
|
-
|
|
1330
|
+
Slope-aware platformer movement.
|
|
921
1331
|
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
1332
|
+
Supports:
|
|
1333
|
+
- gravity and jumping
|
|
1334
|
+
- one-way platforms
|
|
1335
|
+
- walkable slopes
|
|
1336
|
+
- stair stepping
|
|
1337
|
+
- smooth ground following
|
|
925
1338
|
|
|
926
|
-
|
|
927
|
-
|
|
1339
|
+
Unlike move_platformer(), this mode follows polygon floor
|
|
1340
|
+
surfaces and prevents steep slopes from being treated as
|
|
1341
|
+
walkable terrain.
|
|
928
1342
|
|
|
929
1343
|
Args:
|
|
930
|
-
sprite:
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
1344
|
+
sprite:
|
|
1345
|
+
Sprite being simulated. Expected to provide position,
|
|
1346
|
+
velocity, and ground state attributes.
|
|
1347
|
+
|
|
1348
|
+
tileset_collision:
|
|
1349
|
+
Collision definitions for tiles in the map.
|
|
1350
|
+
|
|
1351
|
+
tile_map:
|
|
1352
|
+
Mapping of (tile_x, tile_y) coordinates to tile identifiers.
|
|
1353
|
+
|
|
1354
|
+
dt:
|
|
1355
|
+
Frame delta time in seconds.
|
|
1356
|
+
|
|
1357
|
+
input_x:
|
|
1358
|
+
Horizontal movement input, typically in the range [-1, 1].
|
|
1359
|
+
|
|
1360
|
+
jump_pressed:
|
|
1361
|
+
True if jump was pressed during this frame.
|
|
937
1362
|
|
|
938
1363
|
Returns:
|
|
939
|
-
CollisionResult
|
|
1364
|
+
CollisionResult describing the resolved movement and collision
|
|
1365
|
+
state after simulation.
|
|
940
1366
|
"""
|
|
1367
|
+
|
|
941
1368
|
result = self._result
|
|
942
|
-
result.collided
|
|
943
|
-
result.hit_wall_x
|
|
944
|
-
result.hit_wall_y
|
|
1369
|
+
result.collided = False
|
|
1370
|
+
result.hit_wall_x = False
|
|
1371
|
+
result.hit_wall_y = False
|
|
945
1372
|
result.hit_ceiling = False
|
|
946
|
-
result.on_ground
|
|
1373
|
+
result.on_ground = False
|
|
947
1374
|
result.slide_vector = None
|
|
948
1375
|
result.final_x = sprite.x
|
|
949
1376
|
result.final_y = sprite.y
|
|
950
1377
|
|
|
951
|
-
|
|
1378
|
+
skin = 0.01
|
|
1379
|
+
old_x, old_y = sprite.x, sprite.y
|
|
1380
|
+
_, _, _, old_bottom = get_shape_bounds(sprite)
|
|
1381
|
+
was_on_ground = getattr(sprite, "on_ground", False)
|
|
1382
|
+
jumped = False
|
|
1383
|
+
|
|
1384
|
+
if jump_pressed and was_on_ground:
|
|
1385
|
+
sprite.vy = self.jump_strength
|
|
1386
|
+
sprite.on_ground = False
|
|
1387
|
+
jumped = True
|
|
1388
|
+
elif not was_on_ground:
|
|
952
1389
|
sprite.vy += self.gravity * dt
|
|
953
1390
|
if sprite.vy > self.max_fall_speed:
|
|
954
1391
|
sprite.vy = self.max_fall_speed
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
sprite.vy = self.jump_strength
|
|
1392
|
+
else:
|
|
1393
|
+
sprite.vy = min(sprite.vy, 0.0)
|
|
958
1394
|
|
|
959
1395
|
sprite.vx = input_x * 200.0
|
|
960
|
-
|
|
961
1396
|
delta_x = sprite.vx * dt
|
|
962
1397
|
delta_y = sprite.vy * dt
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
if delta_x == 0 and delta_y == 0:
|
|
966
|
-
return result
|
|
967
|
-
|
|
968
|
-
motion_x, motion_y = delta_x, delta_y
|
|
969
|
-
collided = False
|
|
1398
|
+
bottom_offset = old_bottom - old_y
|
|
970
1399
|
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
1400
|
+
slope_follow = abs(delta_x) * math.tan(math.radians(self.max_walk_angle))
|
|
1401
|
+
max_ground_up = max(self.step_height, slope_follow + skin)
|
|
1402
|
+
max_ground_down = max(self.ground_snap_tolerance, slope_follow + skin)
|
|
974
1403
|
|
|
975
|
-
|
|
976
|
-
|
|
1404
|
+
# Horizontal movement first. Grounded sprites are allowed to follow
|
|
1405
|
+
# walkable floor contours, but only when a jump did not start this frame.
|
|
1406
|
+
if delta_x != 0.0:
|
|
1407
|
+
sprite.x = old_x + delta_x
|
|
1408
|
+
sprite.y = old_y
|
|
977
1409
|
|
|
978
|
-
|
|
979
|
-
if
|
|
980
|
-
|
|
1410
|
+
followed_ground = False
|
|
1411
|
+
if was_on_ground and not jumped:
|
|
1412
|
+
ground_y = self._find_walkable_ground_y(
|
|
1413
|
+
sprite,
|
|
1414
|
+
tileset_collision,
|
|
1415
|
+
tile_map,
|
|
1416
|
+
max_up=max_ground_up,
|
|
1417
|
+
max_down=max_ground_down,
|
|
1418
|
+
include_one_way=True,
|
|
1419
|
+
previous_bottom=old_bottom,
|
|
1420
|
+
)
|
|
1421
|
+
if ground_y is not None:
|
|
1422
|
+
sprite.y = ground_y - bottom_offset - skin
|
|
1423
|
+
followed_ground = True
|
|
1424
|
+
|
|
1425
|
+
if self._collides_at_platformer(
|
|
1426
|
+
sprite, tileset_collision, tile_map, include_one_way=False
|
|
1427
|
+
):
|
|
1428
|
+
sprite.x = old_x + delta_x
|
|
1429
|
+
sprite.y = old_y - self.step_height
|
|
1430
|
+
step_ground_y = self._find_walkable_ground_y(
|
|
1431
|
+
sprite,
|
|
1432
|
+
tileset_collision,
|
|
1433
|
+
tile_map,
|
|
1434
|
+
max_up=self.step_height + skin,
|
|
1435
|
+
max_down=self.step_height + skin,
|
|
1436
|
+
include_one_way=False,
|
|
1437
|
+
previous_bottom=old_bottom,
|
|
1438
|
+
)
|
|
1439
|
+
if step_ground_y is not None:
|
|
1440
|
+
sprite.y = step_ground_y - bottom_offset - skin
|
|
1441
|
+
if step_ground_y is None or self._collides_at_platformer(
|
|
1442
|
+
sprite, tileset_collision, tile_map, include_one_way=False
|
|
1443
|
+
):
|
|
1444
|
+
sprite.x = old_x
|
|
1445
|
+
sprite.y = old_y
|
|
1446
|
+
sprite.vx = 0.0
|
|
1447
|
+
result.collided = True
|
|
1448
|
+
result.hit_wall_x = True
|
|
1449
|
+
else:
|
|
1450
|
+
followed_ground = True
|
|
981
1451
|
|
|
1452
|
+
if followed_ground:
|
|
1453
|
+
sprite.on_ground = True
|
|
1454
|
+
result.on_ground = True
|
|
1455
|
+
else:
|
|
982
1456
|
sprite.x = old_x
|
|
983
1457
|
sprite.y = old_y
|
|
984
|
-
collided = True
|
|
985
1458
|
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
1459
|
+
y_before_vertical = sprite.y
|
|
1460
|
+
_, _, _, previous_bottom = get_shape_bounds(sprite)
|
|
1461
|
+
|
|
1462
|
+
if jumped or sprite.vy < 0.0:
|
|
1463
|
+
sprite.y = y_before_vertical + delta_y
|
|
1464
|
+
if self._collides_at_platformer(
|
|
1465
|
+
sprite, tileset_collision, tile_map, include_one_way=False
|
|
1466
|
+
):
|
|
1467
|
+
lo = y_before_vertical + delta_y
|
|
1468
|
+
hi = y_before_vertical
|
|
1469
|
+
for _ in range(10):
|
|
1470
|
+
mid = (lo + hi) * 0.5
|
|
1471
|
+
sprite.y = mid
|
|
1472
|
+
if self._collides_at_platformer(
|
|
1473
|
+
sprite, tileset_collision, tile_map, include_one_way=False
|
|
1474
|
+
):
|
|
1475
|
+
lo = mid
|
|
1476
|
+
else:
|
|
1477
|
+
hi = mid
|
|
1478
|
+
sprite.y = hi
|
|
1479
|
+
sprite.vy = 0.0
|
|
1480
|
+
sprite.on_ground = False
|
|
1481
|
+
result.collided = True
|
|
1482
|
+
result.hit_ceiling = True
|
|
1483
|
+
else:
|
|
1484
|
+
sprite.on_ground = False
|
|
1485
|
+
elif sprite.vy > 0.0:
|
|
1486
|
+
sprite.y = y_before_vertical + delta_y
|
|
1487
|
+
ground_y = self._find_walkable_ground_y(
|
|
1488
|
+
sprite,
|
|
1489
|
+
tileset_collision,
|
|
1490
|
+
tile_map,
|
|
1491
|
+
max_up=abs(delta_y) + max_ground_up,
|
|
1492
|
+
max_down=skin,
|
|
1493
|
+
include_one_way=True,
|
|
1494
|
+
previous_bottom=previous_bottom,
|
|
989
1495
|
)
|
|
990
|
-
if
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
1496
|
+
if ground_y is not None:
|
|
1497
|
+
sprite.y = ground_y - bottom_offset - skin
|
|
1498
|
+
sprite.vy = 0.0
|
|
1499
|
+
sprite.on_ground = True
|
|
1500
|
+
result.on_ground = True
|
|
1501
|
+
result.collided = True
|
|
1502
|
+
result.hit_wall_y = True
|
|
1503
|
+
elif self._collides_at_platformer(
|
|
1504
|
+
sprite,
|
|
1505
|
+
tileset_collision,
|
|
1506
|
+
tile_map,
|
|
1507
|
+
include_one_way=True,
|
|
1508
|
+
previous_bottom=previous_bottom,
|
|
1509
|
+
):
|
|
1510
|
+
lo = y_before_vertical
|
|
1511
|
+
hi = y_before_vertical + delta_y
|
|
1512
|
+
for _ in range(10):
|
|
1513
|
+
mid = (lo + hi) * 0.5
|
|
1514
|
+
sprite.y = mid
|
|
1515
|
+
if self._collides_at_platformer(
|
|
1516
|
+
sprite,
|
|
1517
|
+
tileset_collision,
|
|
1518
|
+
tile_map,
|
|
1519
|
+
include_one_way=True,
|
|
1520
|
+
previous_bottom=previous_bottom,
|
|
1521
|
+
):
|
|
1522
|
+
hi = mid
|
|
1523
|
+
else:
|
|
1524
|
+
lo = mid
|
|
1525
|
+
sprite.y = lo
|
|
1526
|
+
sprite.vy = 0.0
|
|
1527
|
+
sprite.on_ground = True
|
|
1528
|
+
result.on_ground = True
|
|
1529
|
+
result.collided = True
|
|
1530
|
+
result.hit_wall_y = True
|
|
997
1531
|
else:
|
|
998
|
-
|
|
1532
|
+
sprite.on_ground = False
|
|
1533
|
+
elif not jumped:
|
|
1534
|
+
ground_y = self._find_walkable_ground_y(
|
|
1535
|
+
sprite,
|
|
1536
|
+
tileset_collision,
|
|
1537
|
+
tile_map,
|
|
1538
|
+
max_up=max_ground_up,
|
|
1539
|
+
max_down=max_ground_down,
|
|
1540
|
+
include_one_way=True,
|
|
1541
|
+
previous_bottom=previous_bottom,
|
|
1542
|
+
)
|
|
1543
|
+
if ground_y is not None:
|
|
1544
|
+
sprite.y = ground_y - bottom_offset - skin
|
|
1545
|
+
sprite.vy = 0.0
|
|
1546
|
+
sprite.on_ground = True
|
|
1547
|
+
result.on_ground = True
|
|
1548
|
+
else:
|
|
1549
|
+
sprite.on_ground = False
|
|
999
1550
|
|
|
1000
1551
|
result.final_x = sprite.x
|
|
1001
1552
|
result.final_y = sprite.y
|
|
1002
|
-
result.
|
|
1003
|
-
|
|
1004
|
-
if collided:
|
|
1005
|
-
if sprite.vy >= 0:
|
|
1006
|
-
sprite.y += 1.0
|
|
1007
|
-
if self._collides_at(sprite, tileset_collision, tile_map):
|
|
1008
|
-
result.on_ground = True
|
|
1009
|
-
sprite.on_ground = True
|
|
1010
|
-
sprite.vy = 0.0
|
|
1011
|
-
else:
|
|
1012
|
-
sprite.on_ground = False
|
|
1013
|
-
sprite.y -= 1.0
|
|
1014
|
-
|
|
1015
|
-
if sprite.vy < 0:
|
|
1016
|
-
sprite.y -= 1.0
|
|
1017
|
-
if self._collides_at(sprite, tileset_collision, tile_map):
|
|
1018
|
-
result.hit_ceiling = True
|
|
1019
|
-
sprite.vy = 0.0
|
|
1020
|
-
sprite.y += 1.0
|
|
1021
|
-
|
|
1022
|
-
if abs(result.final_x - old_x) < 0.01 and abs(delta_x) > 0.01:
|
|
1023
|
-
result.hit_wall_x = True
|
|
1024
|
-
else:
|
|
1025
|
-
sprite.on_ground = False
|
|
1026
|
-
|
|
1553
|
+
result.on_ground = getattr(sprite, "on_ground", False)
|
|
1027
1554
|
return result
|
|
1028
1555
|
|
|
1029
1556
|
def move_rpg(
|
|
@@ -1070,8 +1597,25 @@ class CollisionRunner:
|
|
|
1070
1597
|
sprite.x = old_x
|
|
1071
1598
|
sprite.y = old_y
|
|
1072
1599
|
result.collided = True
|
|
1073
|
-
|
|
1074
|
-
|
|
1600
|
+
|
|
1601
|
+
x_blocked = False
|
|
1602
|
+
y_blocked = False
|
|
1603
|
+
if delta_x != 0:
|
|
1604
|
+
sprite.x = old_x + delta_x
|
|
1605
|
+
sprite.y = old_y
|
|
1606
|
+
x_blocked = self._collides_at(sprite, tileset_collision, tile_map)
|
|
1607
|
+
if delta_y != 0:
|
|
1608
|
+
sprite.x = old_x
|
|
1609
|
+
sprite.y = old_y + delta_y
|
|
1610
|
+
y_blocked = self._collides_at(sprite, tileset_collision, tile_map)
|
|
1611
|
+
sprite.x = old_x
|
|
1612
|
+
sprite.y = old_y
|
|
1613
|
+
|
|
1614
|
+
if not x_blocked and not y_blocked:
|
|
1615
|
+
x_blocked = delta_x != 0
|
|
1616
|
+
y_blocked = delta_y != 0
|
|
1617
|
+
result.hit_wall_x = x_blocked
|
|
1618
|
+
result.hit_wall_y = y_blocked
|
|
1075
1619
|
else:
|
|
1076
1620
|
result.final_x = sprite.x
|
|
1077
1621
|
result.final_y = sprite.y
|
|
@@ -193,16 +193,16 @@ def rect_vs_circle(
|
|
|
193
193
|
|
|
194
194
|
if min_dist == dist_left:
|
|
195
195
|
normal = (-1.0, 0.0)
|
|
196
|
-
depth = circle_radius
|
|
196
|
+
depth = circle_radius + dist_left
|
|
197
197
|
elif min_dist == dist_right:
|
|
198
198
|
normal = (1.0, 0.0)
|
|
199
|
-
depth = circle_radius
|
|
199
|
+
depth = circle_radius + dist_right
|
|
200
200
|
elif min_dist == dist_top:
|
|
201
201
|
normal = (0.0, -1.0)
|
|
202
|
-
depth = circle_radius
|
|
202
|
+
depth = circle_radius + dist_top
|
|
203
203
|
else:
|
|
204
204
|
normal = (0.0, 1.0)
|
|
205
|
-
depth = circle_radius
|
|
205
|
+
depth = circle_radius + dist_bottom
|
|
206
206
|
else:
|
|
207
207
|
depth = circle_radius - dist
|
|
208
208
|
normal = (dx / dist, dy / dist)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tilemap-parser
|
|
3
|
-
Version: 3.1.
|
|
3
|
+
Version: 3.1.15
|
|
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
|
|
@@ -390,9 +390,9 @@ class TestRectVsCircle:
|
|
|
390
390
|
result = rect_vs_circle(rect, center, 8.0)
|
|
391
391
|
assert result is not None
|
|
392
392
|
# Closest edge is left (dist_left = 5.0), so normal = (-1, 0)
|
|
393
|
-
# depth
|
|
393
|
+
# depth separates the full circle out of the rect: radius + dist_left.
|
|
394
394
|
assert result.normal == (-1.0, 0.0)
|
|
395
|
-
assert result.depth == pytest.approx(
|
|
395
|
+
assert result.depth == pytest.approx(13.0)
|
|
396
396
|
|
|
397
397
|
def test_circle_inside_rect_closest_top(self):
|
|
398
398
|
rect = (0.0, 0.0, 20.0, 20.0)
|
|
@@ -401,7 +401,7 @@ class TestRectVsCircle:
|
|
|
401
401
|
assert result is not None
|
|
402
402
|
# Closest edge is top (dist_top = 3.0)
|
|
403
403
|
assert result.normal == (0.0, -1.0)
|
|
404
|
-
assert result.depth == pytest.approx(
|
|
404
|
+
assert result.depth == pytest.approx(11.0)
|
|
405
405
|
|
|
406
406
|
def test_circle_inside_rect_closest_bottom(self):
|
|
407
407
|
rect = (0.0, 0.0, 20.0, 20.0)
|
|
@@ -410,7 +410,7 @@ class TestRectVsCircle:
|
|
|
410
410
|
assert result is not None
|
|
411
411
|
# Closest edge is bottom (dist_bottom = 3.0)
|
|
412
412
|
assert result.normal == (0.0, 1.0)
|
|
413
|
-
assert result.depth == pytest.approx(
|
|
413
|
+
assert result.depth == pytest.approx(11.0)
|
|
414
414
|
|
|
415
415
|
def test_circle_center_on_edge(self):
|
|
416
416
|
"""Circle center exactly on rect edge."""
|
|
@@ -141,6 +141,16 @@ class TestCheckCollisionRectCircle:
|
|
|
141
141
|
c = _make_circle(30, 5, 5)
|
|
142
142
|
assert check_collision(r, c) is None
|
|
143
143
|
|
|
144
|
+
def test_circle_fully_inside_rect_has_positive_depth(self):
|
|
145
|
+
r = _make_rect(0, 0, 100, 100)
|
|
146
|
+
c = _make_circle(50, 50, 5)
|
|
147
|
+
|
|
148
|
+
hit = check_collision(r, c)
|
|
149
|
+
|
|
150
|
+
assert hit is not None
|
|
151
|
+
assert hit.depth == pytest.approx(55.0)
|
|
152
|
+
assert hit.normal == (-1.0, 0.0)
|
|
153
|
+
|
|
144
154
|
|
|
145
155
|
class TestCheckCollisionCircleRect:
|
|
146
156
|
def test_normal_flipped(self):
|
|
@@ -152,6 +162,16 @@ class TestCheckCollisionCircleRect:
|
|
|
152
162
|
# Normal should point from circle toward rect (left, so negative X)
|
|
153
163
|
assert hit.normal[0] == pytest.approx(-1.0)
|
|
154
164
|
|
|
165
|
+
def test_rect_fully_contains_circle_has_positive_depth(self):
|
|
166
|
+
c = _make_circle(50, 50, 5)
|
|
167
|
+
r = _make_rect(0, 0, 100, 100)
|
|
168
|
+
|
|
169
|
+
hit = check_collision(c, r)
|
|
170
|
+
|
|
171
|
+
assert hit is not None
|
|
172
|
+
assert hit.depth == pytest.approx(55.0)
|
|
173
|
+
assert hit.normal == (1.0, -0.0)
|
|
174
|
+
|
|
155
175
|
|
|
156
176
|
class TestCheckCollisionLayerFiltering:
|
|
157
177
|
def test_layer_blocks(self):
|
|
@@ -207,6 +207,18 @@ class TestMoveAndSlide:
|
|
|
207
207
|
|
|
208
208
|
assert result.final_y == 56
|
|
209
209
|
|
|
210
|
+
def test_diagonal_corner_collision_keeps_safe_slide_axis(self):
|
|
211
|
+
tile_map = {(4, 4): 0}
|
|
212
|
+
sprite = MockSprite(x=100, y=92)
|
|
213
|
+
|
|
214
|
+
result = self.runner.move_and_slide(sprite, self.tileset, tile_map, 5, 5)
|
|
215
|
+
|
|
216
|
+
assert result.collided is True
|
|
217
|
+
assert result.final_x == 105
|
|
218
|
+
assert result.final_y == 92
|
|
219
|
+
assert result.slide_vector == (5, 0.0)
|
|
220
|
+
assert not self.runner._collides_at(sprite, self.tileset, tile_map)
|
|
221
|
+
|
|
210
222
|
|
|
211
223
|
# ===========================================================================
|
|
212
224
|
# Runner: move_platformer Tests
|
|
@@ -227,6 +239,101 @@ class TestMovePlatformer:
|
|
|
227
239
|
|
|
228
240
|
assert sprite.vy > 0
|
|
229
241
|
|
|
242
|
+
def test_ground_snap_does_not_embed_sprite(self):
|
|
243
|
+
tile_map = {(0, 5): 0}
|
|
244
|
+
sprite = MockSprite(x=4, y=127)
|
|
245
|
+
sprite.vy = 0
|
|
246
|
+
sprite.on_ground = False
|
|
247
|
+
|
|
248
|
+
result = self.runner.move_platformer(
|
|
249
|
+
sprite, self.tileset, tile_map, dt=0.016, input_x=0, jump_pressed=False
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
assert result.on_ground is True
|
|
253
|
+
assert sprite.on_ground is True
|
|
254
|
+
assert sprite.y < 129
|
|
255
|
+
assert not self.runner._collides_at(sprite, self.tileset, tile_map)
|
|
256
|
+
|
|
257
|
+
def test_one_way_platform_does_not_block_horizontal_movement(self):
|
|
258
|
+
tile_map = make_tilemap_with_one_way()
|
|
259
|
+
sprite = MockSprite(x=137, y=160)
|
|
260
|
+
sprite.vy = 0
|
|
261
|
+
sprite.on_ground = True
|
|
262
|
+
|
|
263
|
+
result = self.runner.move_platformer(
|
|
264
|
+
sprite, self.tileset, tile_map, dt=0.016, input_x=1, jump_pressed=False
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
assert sprite.x > 137
|
|
268
|
+
assert result.hit_wall_x is False
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
class TestMovePlatformerWithSlide:
|
|
272
|
+
def setup_method(self):
|
|
273
|
+
self.runner = CollisionRunner.from_game_type("platformer", (32, 32))
|
|
274
|
+
self.tileset = make_tileset_with_floor()
|
|
275
|
+
|
|
276
|
+
def test_moves_and_resets_reused_result(self):
|
|
277
|
+
blocking_map = make_tilemap_floor_only()
|
|
278
|
+
blocked_sprite = MockSprite(x=96, y=32)
|
|
279
|
+
first = self.runner.move_rpg(blocked_sprite, self.tileset, blocking_map, 5, 0)
|
|
280
|
+
assert first.collided is True
|
|
281
|
+
|
|
282
|
+
sprite = MockSprite(x=100, y=100)
|
|
283
|
+
sprite.vy = 0
|
|
284
|
+
sprite.on_ground = False
|
|
285
|
+
|
|
286
|
+
result = self.runner.move_platformer_with_slide(
|
|
287
|
+
sprite, self.tileset, {}, dt=0.016, input_x=1, jump_pressed=False
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
assert sprite.x > 100
|
|
291
|
+
assert sprite.y > 100
|
|
292
|
+
assert result.collided is False
|
|
293
|
+
assert result.hit_wall_x is False
|
|
294
|
+
|
|
295
|
+
def test_jump_takes_priority_over_ground_following(self):
|
|
296
|
+
sprite = MockSprite(x=100, y=100)
|
|
297
|
+
sprite.vy = 0
|
|
298
|
+
sprite.on_ground = True
|
|
299
|
+
|
|
300
|
+
result = self.runner.move_platformer_with_slide(
|
|
301
|
+
sprite, self.tileset, {}, dt=0.016, input_x=0, jump_pressed=True
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
assert sprite.y < 100
|
|
305
|
+
assert sprite.vy < 0
|
|
306
|
+
assert sprite.on_ground is False
|
|
307
|
+
assert result.on_ground is False
|
|
308
|
+
|
|
309
|
+
def test_follows_walkable_slope_up(self):
|
|
310
|
+
tile_map = {(5, 5): 2}
|
|
311
|
+
sprite = MockSprite(x=160, y=169.99, shape=RectangleShape(width=8, height=16))
|
|
312
|
+
sprite.vy = 0
|
|
313
|
+
sprite.on_ground = True
|
|
314
|
+
|
|
315
|
+
result = self.runner.move_platformer_with_slide(
|
|
316
|
+
sprite, self.tileset, tile_map, dt=0.016, input_x=1, jump_pressed=False
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
assert sprite.x > 160
|
|
320
|
+
assert sprite.y < 169.99
|
|
321
|
+
assert sprite.on_ground is True
|
|
322
|
+
assert result.on_ground is True
|
|
323
|
+
|
|
324
|
+
def test_steep_wall_blocks_horizontal_motion(self):
|
|
325
|
+
tile_map = {(4, 2): 0}
|
|
326
|
+
sprite = MockSprite(x=103, y=80)
|
|
327
|
+
sprite.vy = 0
|
|
328
|
+
sprite.on_ground = True
|
|
329
|
+
|
|
330
|
+
result = self.runner.move_platformer_with_slide(
|
|
331
|
+
sprite, self.tileset, tile_map, dt=0.016, input_x=1, jump_pressed=False
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
assert sprite.x == 103
|
|
335
|
+
assert result.hit_wall_x is True
|
|
336
|
+
|
|
230
337
|
|
|
231
338
|
class TestMoveRpg:
|
|
232
339
|
def setup_method(self):
|
|
@@ -241,6 +348,16 @@ class TestMoveRpg:
|
|
|
241
348
|
|
|
242
349
|
assert result.final_x == 100
|
|
243
350
|
|
|
351
|
+
def test_reports_blocked_axis_for_diagonal_move(self):
|
|
352
|
+
tile_map = {(4, 2): 0}
|
|
353
|
+
sprite = MockSprite(x=103, y=80)
|
|
354
|
+
|
|
355
|
+
result = self.runner.move_rpg(sprite, self.tileset, tile_map, 5, 5)
|
|
356
|
+
|
|
357
|
+
assert result.collided is True
|
|
358
|
+
assert result.hit_wall_x is True
|
|
359
|
+
assert result.hit_wall_y is False
|
|
360
|
+
|
|
244
361
|
|
|
245
362
|
# ===========================================================================
|
|
246
363
|
# Runner: One-Way Platforms
|
|
@@ -297,4 +414,4 @@ class TestRealisticMap:
|
|
|
297
414
|
assert self.tileset.has_collision(8) is True
|
|
298
415
|
tile_data = self.tileset.tiles.get(8)
|
|
299
416
|
assert tile_data is not None
|
|
300
|
-
assert tile_data.shapes[0].one_way is True
|
|
417
|
+
assert tile_data.shapes[0].one_way is True
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tilemap_parser-3.1.14 → tilemap_parser-3.1.15}/src/tilemap_parser/parser/collision_loader.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tilemap_parser-3.1.14 → tilemap_parser-3.1.15}/src/tilemap_parser/runtime/animation_player.py
RENAMED
|
File without changes
|
|
File without changes
|
{tilemap_parser-3.1.14 → tilemap_parser-3.1.15}/src/tilemap_parser/runtime/collision_cache.py
RENAMED
|
File without changes
|
|
File without changes
|
{tilemap_parser-3.1.14 → tilemap_parser-3.1.15}/src/tilemap_parser/runtime/object_collision.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tilemap_parser-3.1.14 → tilemap_parser-3.1.15}/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
|