tilemap-parser 3.1.1__tar.gz → 3.1.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. {tilemap_parser-3.1.1 → tilemap_parser-3.1.3}/PKG-INFO +1 -1
  2. {tilemap_parser-3.1.1 → tilemap_parser-3.1.3}/pyproject.toml +1 -1
  3. {tilemap_parser-3.1.1 → tilemap_parser-3.1.3}/src/tilemap_parser/runtime/tile_collision.py +344 -75
  4. {tilemap_parser-3.1.1 → tilemap_parser-3.1.3}/src/tilemap_parser.egg-info/PKG-INFO +1 -1
  5. {tilemap_parser-3.1.1 → tilemap_parser-3.1.3}/LICENSE +0 -0
  6. {tilemap_parser-3.1.1 → tilemap_parser-3.1.3}/README.md +0 -0
  7. {tilemap_parser-3.1.1 → tilemap_parser-3.1.3}/setup.cfg +0 -0
  8. {tilemap_parser-3.1.1 → tilemap_parser-3.1.3}/src/tilemap_parser/__init__.py +0 -0
  9. {tilemap_parser-3.1.1 → tilemap_parser-3.1.3}/src/tilemap_parser/parser/__init__.py +0 -0
  10. {tilemap_parser-3.1.1 → tilemap_parser-3.1.3}/src/tilemap_parser/parser/animation.py +0 -0
  11. {tilemap_parser-3.1.1 → tilemap_parser-3.1.3}/src/tilemap_parser/parser/collision.py +0 -0
  12. {tilemap_parser-3.1.1 → tilemap_parser-3.1.3}/src/tilemap_parser/parser/collision_loader.py +0 -0
  13. {tilemap_parser-3.1.1 → tilemap_parser-3.1.3}/src/tilemap_parser/parser/map_parse.py +0 -0
  14. {tilemap_parser-3.1.1 → tilemap_parser-3.1.3}/src/tilemap_parser/runtime/__init__.py +0 -0
  15. {tilemap_parser-3.1.1 → tilemap_parser-3.1.3}/src/tilemap_parser/runtime/animation_player.py +0 -0
  16. {tilemap_parser-3.1.1 → tilemap_parser-3.1.3}/src/tilemap_parser/runtime/collision_cache.py +0 -0
  17. {tilemap_parser-3.1.1 → tilemap_parser-3.1.3}/src/tilemap_parser/runtime/map_loader.py +0 -0
  18. {tilemap_parser-3.1.1 → tilemap_parser-3.1.3}/src/tilemap_parser/runtime/object_collision.py +0 -0
  19. {tilemap_parser-3.1.1 → tilemap_parser-3.1.3}/src/tilemap_parser/runtime/renderer.py +0 -0
  20. {tilemap_parser-3.1.1 → tilemap_parser-3.1.3}/src/tilemap_parser/utils/__init__.py +0 -0
  21. {tilemap_parser-3.1.1 → tilemap_parser-3.1.3}/src/tilemap_parser/utils/geometry.py +0 -0
  22. {tilemap_parser-3.1.1 → tilemap_parser-3.1.3}/src/tilemap_parser.egg-info/SOURCES.txt +0 -0
  23. {tilemap_parser-3.1.1 → tilemap_parser-3.1.3}/src/tilemap_parser.egg-info/dependency_links.txt +0 -0
  24. {tilemap_parser-3.1.1 → tilemap_parser-3.1.3}/src/tilemap_parser.egg-info/requires.txt +0 -0
  25. {tilemap_parser-3.1.1 → tilemap_parser-3.1.3}/src/tilemap_parser.egg-info/top_level.txt +0 -0
  26. {tilemap_parser-3.1.1 → tilemap_parser-3.1.3}/tests/test_collision.py +0 -0
  27. {tilemap_parser-3.1.1 → tilemap_parser-3.1.3}/tests/test_geometry.py +0 -0
  28. {tilemap_parser-3.1.1 → tilemap_parser-3.1.3}/tests/test_map_loader.py +0 -0
  29. {tilemap_parser-3.1.1 → tilemap_parser-3.1.3}/tests/test_object_collision.py +0 -0
  30. {tilemap_parser-3.1.1 → tilemap_parser-3.1.3}/tests/test_render_scale.py +0 -0
  31. {tilemap_parser-3.1.1 → tilemap_parser-3.1.3}/tests/test_tile_collision.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tilemap-parser
3
- Version: 3.1.1
3
+ Version: 3.1.3
4
4
  Summary: Standalone parser/loader for tilemap-editor JSON maps, sprite animations, and collision detection runtime.
5
5
  Author: tilemap parser contributors
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "tilemap-parser"
7
- version = "3.1.1"
7
+ version = "3.1.3"
8
8
  description = "Standalone parser/loader for tilemap-editor JSON maps, sprite animations, and collision detection runtime."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -14,7 +14,7 @@ from __future__ import annotations
14
14
  import math
15
15
  from dataclasses import dataclass
16
16
  from enum import Enum
17
- from typing import List, Optional, Protocol, Tuple, Union
17
+ from typing import Dict, List, Optional, Protocol, Tuple, Union
18
18
 
19
19
  from ..parser.collision import (
20
20
  CapsuleShape,
@@ -71,7 +71,14 @@ def point_in_polygon(point: Point, vertices: List[Point]) -> bool:
71
71
  return inside
72
72
 
73
73
 
74
- def _point_in_polygon_offset(px: float, py: float, vertices: List[Point], ox: float, oy: float, scale: float = 1.0) -> bool:
74
+ def _point_in_polygon_offset(
75
+ px: float,
76
+ py: float,
77
+ vertices: List[Point],
78
+ ox: float,
79
+ oy: float,
80
+ scale: float = 1.0,
81
+ ) -> bool:
75
82
  """Ray-cast with tile offset applied inline — no allocation."""
76
83
  n = len(vertices)
77
84
  inside = False
@@ -100,18 +107,31 @@ def rect_polygon_collision(
100
107
  min_vy = max_vy = vertices[0][1]
101
108
  for i in range(1, n):
102
109
  vx, vy = vertices[i]
103
- if vx < min_vx: min_vx = vx
104
- elif vx > max_vx: max_vx = vx
105
- if vy < min_vy: min_vy = vy
106
- elif vy > max_vy: max_vy = vy
107
- if rect_x > max_vx or rect_x + rect_w < min_vx or rect_y > max_vy or rect_y + rect_h < min_vy:
110
+ if vx < min_vx:
111
+ min_vx = vx
112
+ elif vx > max_vx:
113
+ max_vx = vx
114
+ if vy < min_vy:
115
+ min_vy = vy
116
+ elif vy > max_vy:
117
+ max_vy = vy
118
+ if (
119
+ rect_x > max_vx
120
+ or rect_x + rect_w < min_vx
121
+ or rect_y > max_vy
122
+ or rect_y + rect_h < min_vy
123
+ ):
108
124
  return False
109
125
 
110
126
  # Corner tests — no tuple allocation
111
- if point_in_polygon((rect_x, rect_y), vertices): return True
112
- if point_in_polygon((rect_x + rect_w, rect_y), vertices): return True
113
- if point_in_polygon((rect_x, rect_y + rect_h), vertices): return True
114
- if point_in_polygon((rect_x + rect_w, rect_y + rect_h), vertices): return True
127
+ if point_in_polygon((rect_x, rect_y), vertices):
128
+ return True
129
+ if point_in_polygon((rect_x + rect_w, rect_y), vertices):
130
+ return True
131
+ if point_in_polygon((rect_x, rect_y + rect_h), vertices):
132
+ return True
133
+ if point_in_polygon((rect_x + rect_w, rect_y + rect_h), vertices):
134
+ return True
115
135
 
116
136
  # Vertex-in-rect
117
137
  rx2, ry2 = rect_x + rect_w, rect_y + rect_h
@@ -122,8 +142,14 @@ def rect_polygon_collision(
122
142
 
123
143
 
124
144
  def _rect_polygon_collision_offset(
125
- rect_x: float, rect_y: float, rect_w: float, rect_h: float,
126
- vertices: List[Point], ox: float, oy: float, scale: float = 1.0,
145
+ rect_x: float,
146
+ rect_y: float,
147
+ rect_w: float,
148
+ rect_h: float,
149
+ vertices: List[Point],
150
+ ox: float,
151
+ oy: float,
152
+ scale: float = 1.0,
127
153
  ) -> bool:
128
154
  """Rectangle vs polygon with tile offset applied inline — no allocation."""
129
155
  # AABB pre-reject with offset
@@ -133,19 +159,32 @@ def _rect_polygon_collision_offset(
133
159
  min_vy = max_vy = v0y
134
160
  for i in range(1, n):
135
161
  wx, wy = vertices[i][0] * scale + ox, vertices[i][1] * scale + oy
136
- if wx < min_vx: min_vx = wx
137
- elif wx > max_vx: max_vx = wx
138
- if wy < min_vy: min_vy = wy
139
- elif wy > max_vy: max_vy = wy
140
- if rect_x > max_vx or rect_x + rect_w < min_vx or rect_y > max_vy or rect_y + rect_h < min_vy:
162
+ if wx < min_vx:
163
+ min_vx = wx
164
+ elif wx > max_vx:
165
+ max_vx = wx
166
+ if wy < min_vy:
167
+ min_vy = wy
168
+ elif wy > max_vy:
169
+ max_vy = wy
170
+ if (
171
+ rect_x > max_vx
172
+ or rect_x + rect_w < min_vx
173
+ or rect_y > max_vy
174
+ or rect_y + rect_h < min_vy
175
+ ):
141
176
  return False
142
177
 
143
178
  # Corner tests
144
179
  rx2, ry2 = rect_x + rect_w, rect_y + rect_h
145
- if _point_in_polygon_offset(rect_x, rect_y, vertices, ox, oy, scale): return True
146
- if _point_in_polygon_offset(rx2, rect_y, vertices, ox, oy, scale): return True
147
- if _point_in_polygon_offset(rect_x, ry2, vertices, ox, oy, scale): return True
148
- if _point_in_polygon_offset(rx2, ry2, vertices, ox, oy, scale): return True
180
+ if _point_in_polygon_offset(rect_x, rect_y, vertices, ox, oy, scale):
181
+ return True
182
+ if _point_in_polygon_offset(rx2, rect_y, vertices, ox, oy, scale):
183
+ return True
184
+ if _point_in_polygon_offset(rect_x, ry2, vertices, ox, oy, scale):
185
+ return True
186
+ if _point_in_polygon_offset(rx2, ry2, vertices, ox, oy, scale):
187
+ return True
149
188
 
150
189
  # Vertex-in-rect
151
190
  for vx, vy in vertices:
@@ -184,7 +223,13 @@ def circle_polygon_collision(
184
223
 
185
224
 
186
225
  def _circle_polygon_collision_offset(
187
- cx: float, cy: float, radius: float, vertices: List[Point], ox: float, oy: float, scale: float = 1.0,
226
+ cx: float,
227
+ cy: float,
228
+ radius: float,
229
+ vertices: List[Point],
230
+ ox: float,
231
+ oy: float,
232
+ scale: float = 1.0,
188
233
  ) -> bool:
189
234
  """Circle vs polygon with tile offset applied inline — no allocation."""
190
235
  if _point_in_polygon_offset(cx, cy, vertices, ox, oy, scale):
@@ -192,7 +237,10 @@ def _circle_polygon_collision_offset(
192
237
  n = len(vertices)
193
238
  for i in range(n):
194
239
  x1, y1 = vertices[i][0] * scale + ox, vertices[i][1] * scale + oy
195
- x2, y2 = vertices[(i + 1) % n][0] * scale + ox, vertices[(i + 1) % n][1] * scale + oy
240
+ x2, y2 = (
241
+ vertices[(i + 1) % n][0] * scale + ox,
242
+ vertices[(i + 1) % n][1] * scale + oy,
243
+ )
196
244
  dx = x2 - x1
197
245
  dy = y2 - y1
198
246
  fx = cx - x1
@@ -214,7 +262,7 @@ def get_shape_bounds(sprite: ICollidableSprite) -> Tuple[float, float, float, fl
214
262
  shape = sprite.collision_shape
215
263
  if isinstance(shape, RectangleShape):
216
264
  left = sprite.x + shape.offset[0]
217
- top = sprite.y + shape.offset[1]
265
+ top = sprite.y + shape.offset[1]
218
266
  return (left, top, left + shape.width, top + shape.height)
219
267
  elif isinstance(shape, CircleShape):
220
268
  cx, cy = shape.get_center(sprite.x, sprite.y)
@@ -224,7 +272,12 @@ def get_shape_bounds(sprite: ICollidableSprite) -> Tuple[float, float, float, fl
224
272
  top_center = shape.get_top_center(sprite.x, sprite.y)
225
273
  r = shape.radius
226
274
  h = shape.height
227
- return (top_center[0] - r, top_center[1], top_center[0] + r, top_center[1] + h + r * 2)
275
+ return (
276
+ top_center[0] - r,
277
+ top_center[1] - r,
278
+ top_center[0] + r,
279
+ top_center[1] + h + r,
280
+ )
228
281
  return (sprite.x, sprite.y, sprite.x + 32, sprite.y + 32)
229
282
 
230
283
 
@@ -235,18 +288,26 @@ def check_sprite_polygon_collision(
235
288
  shape = sprite.collision_shape
236
289
  if isinstance(shape, RectangleShape):
237
290
  left, top, right, bottom = get_shape_bounds(sprite)
238
- return rect_polygon_collision(left, top, right - left, bottom - top, polygon.vertices)
291
+ return rect_polygon_collision(
292
+ left, top, right - left, bottom - top, polygon.vertices
293
+ )
239
294
  elif isinstance(shape, CircleShape):
240
295
  center = shape.get_center(sprite.x, sprite.y)
241
296
  return circle_polygon_collision(center, shape.radius, polygon.vertices)
242
297
  elif isinstance(shape, CapsuleShape):
243
298
  left, top, right, bottom = get_shape_bounds(sprite)
244
- return rect_polygon_collision(left, top, right - left, bottom - top, polygon.vertices)
299
+ return rect_polygon_collision(
300
+ left, top, right - left, bottom - top, polygon.vertices
301
+ )
245
302
  return False
246
303
 
247
304
 
248
305
  def _check_sprite_polygon_offset(
249
- sprite: ICollidableSprite, polygon: CollisionPolygon, ox: float, oy: float, scale: float = 1.0
306
+ sprite: ICollidableSprite,
307
+ polygon: CollisionPolygon,
308
+ ox: float,
309
+ oy: float,
310
+ scale: float = 1.0,
250
311
  ) -> bool:
251
312
  """
252
313
  Check if sprite collides with a tile-local polygon at world offset (ox, oy).
@@ -255,18 +316,24 @@ def _check_sprite_polygon_offset(
255
316
  shape = sprite.collision_shape
256
317
  if isinstance(shape, RectangleShape):
257
318
  left = sprite.x + shape.offset[0]
258
- top = sprite.y + shape.offset[1]
259
- return _rect_polygon_collision_offset(left, top, shape.width, shape.height, polygon.vertices, ox, oy, scale)
319
+ top = sprite.y + shape.offset[1]
320
+ return _rect_polygon_collision_offset(
321
+ left, top, shape.width, shape.height, polygon.vertices, ox, oy, scale
322
+ )
260
323
  elif isinstance(shape, CircleShape):
261
324
  cx = sprite.x + shape.offset[0]
262
325
  cy = sprite.y + shape.offset[1]
263
- return _circle_polygon_collision_offset(cx, cy, shape.radius, polygon.vertices, ox, oy, scale)
326
+ return _circle_polygon_collision_offset(
327
+ cx, cy, shape.radius, polygon.vertices, ox, oy, scale
328
+ )
264
329
  elif isinstance(shape, CapsuleShape):
265
330
  left = sprite.x + shape.offset[0] - shape.radius
266
- top = sprite.y + shape.offset[1]
267
- w = shape.radius * 2
268
- h = shape.height + shape.radius * 2
269
- return _rect_polygon_collision_offset(left, top, w, h, polygon.vertices, ox, oy, scale)
331
+ top = sprite.y + shape.offset[1] - shape.radius
332
+ w = shape.radius * 2
333
+ h = shape.height + shape.radius * 2
334
+ return _rect_polygon_collision_offset(
335
+ left, top, w, h, polygon.vertices, ox, oy, scale
336
+ )
270
337
  return False
271
338
 
272
339
 
@@ -329,6 +396,9 @@ class CollisionRunner:
329
396
  self.max_fall_speed = 600.0
330
397
  self.jump_strength = -400.0
331
398
 
399
+ self.ground_snap_tolerance = 2.0
400
+ self.step_height = 4.0
401
+
332
402
  self.slide_friction = 0.1
333
403
 
334
404
  self.rpg_snap_to_grid = False
@@ -362,7 +432,9 @@ class CollisionRunner:
362
432
  tile_world_x = tile_x * self._eff_tw
363
433
  tile_world_y = tile_y * self._eff_th
364
434
 
365
- return tileset_collision.get_world_shapes(tile_id, tile_world_x, tile_world_y, self.render_scale)
435
+ return tileset_collision.get_world_shapes(
436
+ tile_id, tile_world_x, tile_world_y, self.render_scale
437
+ )
366
438
 
367
439
  def get_nearby_tile_shapes(
368
440
  self,
@@ -381,10 +453,10 @@ class CollisionRunner:
381
453
  left, top, right, bottom = get_shape_bounds(sprite)
382
454
  tw, th = self._eff_tw, self._eff_th
383
455
 
384
- min_tile_x = int(left // tw) - margin
456
+ min_tile_x = int(left // tw) - margin
385
457
  max_tile_x = int(right // tw) + margin
386
- min_tile_y = int(top // th) - margin
387
- max_tile_y = int(bottom// th) + margin
458
+ min_tile_y = int(top // th) - margin
459
+ max_tile_y = int(bottom // th) + margin
388
460
 
389
461
  shapes = []
390
462
  for tile_y in range(min_tile_y, max_tile_y + 1):
@@ -399,7 +471,11 @@ class CollisionRunner:
399
471
  tile_world_y = tile_y * th
400
472
  for poly in tile_data.shapes:
401
473
  if poly.is_valid():
402
- shapes.append(poly.transform(tile_world_x, tile_world_y, self.render_scale))
474
+ shapes.append(
475
+ poly.transform(
476
+ tile_world_x, tile_world_y, self.render_scale
477
+ )
478
+ )
403
479
  return shapes
404
480
 
405
481
  def _collides_at(
@@ -418,10 +494,10 @@ class CollisionRunner:
418
494
  left, top, right, bottom = get_shape_bounds(sprite)
419
495
  tw, th = self._eff_tw, self._eff_th
420
496
 
421
- min_tile_x = int(left // tw) - margin
497
+ min_tile_x = int(left // tw) - margin
422
498
  max_tile_x = int(right // tw) + margin
423
- min_tile_y = int(top // th) - margin
424
- max_tile_y = int(bottom// th) + margin
499
+ min_tile_y = int(top // th) - margin
500
+ max_tile_y = int(bottom // th) + margin
425
501
 
426
502
  for tile_y in range(min_tile_y, max_tile_y + 1):
427
503
  for tile_x in range(min_tile_x, max_tile_x + 1):
@@ -434,7 +510,9 @@ class CollisionRunner:
434
510
  ox = tile_x * tw
435
511
  oy = tile_y * th
436
512
  for poly in tile_data.shapes:
437
- if poly.is_valid() and _check_sprite_polygon_offset(sprite, poly, ox, oy, self.render_scale):
513
+ if poly.is_valid() and _check_sprite_polygon_offset(
514
+ sprite, poly, ox, oy, self.render_scale
515
+ ):
438
516
  return True
439
517
  return False
440
518
 
@@ -452,10 +530,10 @@ class CollisionRunner:
452
530
  left, top, right, bottom = get_shape_bounds(sprite)
453
531
  tw, th = self._eff_tw, self._eff_th
454
532
 
455
- min_tile_x = int(left // tw) - margin
533
+ min_tile_x = int(left // tw) - margin
456
534
  max_tile_x = int(right // tw) + margin
457
- min_tile_y = int(top // th) - margin
458
- max_tile_y = int(bottom// th) + margin
535
+ min_tile_y = int(top // th) - margin
536
+ max_tile_y = int(bottom // th) + margin
459
537
 
460
538
  for tile_y in range(min_tile_y, max_tile_y + 1):
461
539
  for tile_x in range(min_tile_x, max_tile_x + 1):
@@ -468,7 +546,9 @@ class CollisionRunner:
468
546
  ox = tile_x * tw
469
547
  oy = tile_y * th
470
548
  for poly in tile_data.shapes:
471
- if poly.is_valid() and _check_sprite_polygon_offset(sprite, poly, ox, oy, self.render_scale):
549
+ if poly.is_valid() and _check_sprite_polygon_offset(
550
+ sprite, poly, ox, oy, self.render_scale
551
+ ):
472
552
  return (poly, ox, oy)
473
553
  return None
474
554
 
@@ -498,11 +578,11 @@ class CollisionRunner:
498
578
  CollisionResult with final position and collision info
499
579
  """
500
580
  result = self._result
501
- result.collided = False
581
+ result.collided = False
502
582
  result.hit_wall_x = False
503
583
  result.hit_wall_y = False
504
584
  result.hit_ceiling = False
505
- result.on_ground = False
585
+ result.on_ground = False
506
586
  result.slide_vector = None
507
587
  result.final_x = sprite.x
508
588
  result.final_y = sprite.y
@@ -618,8 +698,11 @@ class CollisionRunner:
618
698
  best_alignment = -1.0
619
699
 
620
700
  for i in range(n):
621
- v1x, v1y = vertices[i][0] * scale + ox, vertices[i][1] * scale + oy
622
- v2x, v2y = vertices[(i + 1) % n][0] * scale + ox, vertices[(i + 1) % n][1] * scale + oy
701
+ v1x, v1y = vertices[i][0] * scale + ox, vertices[i][1] * scale + oy
702
+ v2x, v2y = (
703
+ vertices[(i + 1) % n][0] * scale + ox,
704
+ vertices[(i + 1) % n][1] * scale + oy,
705
+ )
623
706
 
624
707
  edge_x = v2x - v1x
625
708
  edge_y = v2y - v1y
@@ -628,7 +711,7 @@ class CollisionRunner:
628
711
  continue
629
712
 
630
713
  normal_x = -edge_y / edge_len
631
- normal_y = edge_x / edge_len
714
+ normal_y = edge_x / edge_len
632
715
 
633
716
  edge_mid_x = (v1x + v2x) * 0.5
634
717
  edge_mid_y = (v1y + v2y) * 0.5
@@ -672,11 +755,11 @@ class CollisionRunner:
672
755
  CollisionResult with final position and collision info
673
756
  """
674
757
  result = self._result
675
- result.collided = False
676
- result.hit_wall_x = False
677
- result.hit_wall_y = False
758
+ result.collided = False
759
+ result.hit_wall_x = False
760
+ result.hit_wall_y = False
678
761
  result.hit_ceiling = False
679
- result.on_ground = False
762
+ result.on_ground = False
680
763
  result.slide_vector = None
681
764
  result.final_x = sprite.x
682
765
  result.final_y = sprite.y
@@ -697,21 +780,38 @@ class CollisionRunner:
697
780
 
698
781
  # X axis
699
782
  sprite.x = old_x + delta_x
783
+ # Lift above ground snap overlap so ground doesn't block horizontal movement
784
+ sprite.y = old_y - self.ground_snap_tolerance
785
+ stepped_up = False
700
786
  if self._collides_at(sprite, tileset_collision, tile_map):
701
- sprite.x = old_x
702
- sprite.vx = 0.0
703
- result.hit_wall_x = True
787
+ if delta_x != 0:
788
+ # Try stepping up onto slope/stairs
789
+ sprite.y = old_y - self.ground_snap_tolerance - self.step_height
790
+ if not self._collides_at(sprite, tileset_collision, tile_map):
791
+ sprite.y = old_y - self.step_height
792
+ stepped_up = True
793
+ else:
794
+ sprite.x = old_x
795
+ sprite.vx = 0.0
796
+ result.hit_wall_x = True
797
+ else:
798
+ sprite.x = old_x
799
+ sprite.vx = 0.0
800
+ result.hit_wall_x = True
704
801
 
705
802
  # Y axis — check one-way platforms
706
- sprite.y = old_y + delta_y
803
+ if stepped_up:
804
+ sprite.y = sprite.y + delta_y
805
+ else:
806
+ sprite.y = old_y + delta_y
707
807
  collided_y = False
708
808
 
709
809
  left, top, right, bottom = get_shape_bounds(sprite)
710
810
  tw, th = self._eff_tw, self._eff_th
711
- min_tile_x = int(left // tw) - 1
811
+ min_tile_x = int(left // tw) - 1
712
812
  max_tile_x = int(right // tw) + 1
713
- min_tile_y = int(top // th) - 1
714
- max_tile_y = int(bottom// th) + 1
813
+ min_tile_y = int(top // th) - 1
814
+ max_tile_y = int(bottom // th) + 1
715
815
 
716
816
  for tile_y in range(min_tile_y, max_tile_y + 1):
717
817
  for tile_x in range(min_tile_x, max_tile_x + 1):
@@ -726,12 +826,16 @@ class CollisionRunner:
726
826
  for poly in tile_data.shapes:
727
827
  if not poly.is_valid():
728
828
  continue
729
- if not _check_sprite_polygon_offset(sprite, poly, ox, oy, self.render_scale):
829
+ if not _check_sprite_polygon_offset(
830
+ sprite, poly, ox, oy, self.render_scale
831
+ ):
730
832
  continue
731
833
  if poly.one_way and sprite.vy > 0:
732
834
  # one-way: only block if sprite was above the platform top
733
- min_vy = min(v[1] for v in poly.vertices) * self.render_scale + oy
734
- if old_y + (bottom - top) <= min_vy:
835
+ min_vy = (
836
+ min(v[1] for v in poly.vertices) * self.render_scale + oy
837
+ )
838
+ if old_y + (bottom - sprite.y) <= min_vy:
735
839
  collided_y = True
736
840
  break
737
841
  elif not poly.one_way:
@@ -743,22 +847,185 @@ class CollisionRunner:
743
847
  break
744
848
 
745
849
  if collided_y:
746
- sprite.y = old_y
850
+ if stepped_up:
851
+ step_y = old_y - self.step_height
852
+ lo, hi = step_y, old_y
853
+ for _ in range(8):
854
+ mid = (lo + hi) * 0.5
855
+ sprite.y = mid
856
+ if self._collides_at(sprite, tileset_collision, tile_map):
857
+ hi = mid
858
+ else:
859
+ lo = mid
860
+ sprite.y = lo
861
+ sprite.on_ground = True
862
+ result.on_ground = True
863
+ else:
864
+ sprite.y = old_y
747
865
  if sprite.vy > 0:
866
+ fall_y = sprite.vy * dt
748
867
  sprite.vy = 0.0
749
868
  sprite.on_ground = True
750
869
  result.on_ground = True
870
+ lo, hi = old_y, old_y + fall_y
871
+ for _ in range(8):
872
+ mid = (lo + hi) * 0.5
873
+ sprite.y = mid
874
+ if self._collides_at(sprite, tileset_collision, tile_map):
875
+ hi = mid
876
+ else:
877
+ lo = mid
878
+ sprite.y = lo
751
879
  elif sprite.vy < 0:
752
880
  sprite.vy = 0.0
753
881
  result.hit_ceiling = True
882
+ else:
883
+ sprite.on_ground = True
884
+ result.on_ground = True
754
885
  else:
755
886
  sprite.on_ground = False
756
887
 
888
+ downward_travel = max(0.0, sprite.vy) * dt
889
+ if not sprite.on_ground and 0 <= downward_travel <= self.ground_snap_tolerance:
890
+ if self._collides_at(sprite, tileset_collision, tile_map):
891
+ sprite.on_ground = True
892
+ result.on_ground = True
893
+ sprite.vy = 0.0
894
+ else:
895
+ saved_y = sprite.y
896
+ sprite.y += self.ground_snap_tolerance
897
+ if self._collides_at(sprite, tileset_collision, tile_map):
898
+ sprite.on_ground = True
899
+ result.on_ground = True
900
+ sprite.vy = 0.0
901
+ else:
902
+ sprite.y = saved_y
903
+
757
904
  result.final_x = sprite.x
758
905
  result.final_y = sprite.y
759
906
  result.collided = result.hit_wall_x or collided_y
760
907
  return result
761
908
 
909
+ def move_platformer_with_slide(
910
+ self,
911
+ sprite: ICollidableSprite,
912
+ tileset_collision: TilesetCollision,
913
+ tile_map: dict,
914
+ dt: float,
915
+ input_x: float = 0.0,
916
+ jump_pressed: bool = False,
917
+ max_iterations: int = 4,
918
+ ) -> CollisionResult:
919
+ """
920
+ Move sprite with platformer physics and combined slope-sliding collision.
921
+
922
+ Uses iterative normal projection for movement resolution instead of
923
+ separate X/Y sweeps. This allows smooth movement on slopes — walking
924
+ up a slope ascends the player, walking down follows the surface.
925
+
926
+ One-way platforms are treated as solid (same as move_and_slide).
927
+ Use move_platformer() if you need one-way pass-through from below.
928
+
929
+ Args:
930
+ sprite: Sprite to move (must have vx, vy, on_ground attributes)
931
+ tileset_collision: Tileset collision data
932
+ tile_map: Dictionary mapping (tile_x, tile_y) to tile_id
933
+ dt: Delta time in seconds
934
+ input_x: Horizontal input (-1 to 1)
935
+ jump_pressed: Whether jump button is pressed
936
+ max_iterations: Max slope-slide iterations (default 4)
937
+
938
+ Returns:
939
+ CollisionResult with final position and collision info
940
+ """
941
+ result = self._result
942
+ result.collided = False
943
+ result.hit_wall_x = False
944
+ result.hit_wall_y = False
945
+ result.hit_ceiling = False
946
+ result.on_ground = False
947
+ result.slide_vector = None
948
+ result.final_x = sprite.x
949
+ result.final_y = sprite.y
950
+
951
+ if not getattr(sprite, "on_ground", False):
952
+ sprite.vy += self.gravity * dt
953
+ if sprite.vy > self.max_fall_speed:
954
+ sprite.vy = self.max_fall_speed
955
+
956
+ if jump_pressed and getattr(sprite, "on_ground", False):
957
+ sprite.vy = self.jump_strength
958
+
959
+ sprite.vx = input_x * 200.0
960
+
961
+ delta_x = sprite.vx * dt
962
+ delta_y = sprite.vy * dt
963
+ old_x, old_y = sprite.x, sprite.y
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
970
+
971
+ for _ in range(max_iterations):
972
+ if abs(motion_x) < 0.01 and abs(motion_y) < 0.01:
973
+ break
974
+
975
+ sprite.x = old_x + motion_x
976
+ sprite.y = old_y + motion_y
977
+
978
+ hit = self._first_colliding_shape(sprite, tileset_collision, tile_map)
979
+ if hit is None:
980
+ break
981
+
982
+ sprite.x = old_x
983
+ sprite.y = old_y
984
+ collided = True
985
+
986
+ poly, ox, oy = hit
987
+ normal = self._get_collision_normal_from_motion(
988
+ sprite, poly, ox, oy, motion_x, motion_y, self.render_scale
989
+ )
990
+ if normal is None:
991
+ break
992
+
993
+ dot = motion_x * normal[0] + motion_y * normal[1]
994
+ if dot < 0:
995
+ motion_x -= normal[0] * dot
996
+ motion_y -= normal[1] * dot
997
+ else:
998
+ break
999
+
1000
+ result.final_x = sprite.x
1001
+ result.final_y = sprite.y
1002
+ result.collided = collided
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
+
1027
+ return result
1028
+
762
1029
  def move_rpg(
763
1030
  self,
764
1031
  sprite: ICollidableSprite,
@@ -783,11 +1050,11 @@ class CollisionRunner:
783
1050
  CollisionResult with final position and collision info
784
1051
  """
785
1052
  result = self._result
786
- result.collided = False
787
- result.hit_wall_x = False
788
- result.hit_wall_y = False
1053
+ result.collided = False
1054
+ result.hit_wall_x = False
1055
+ result.hit_wall_y = False
789
1056
  result.hit_ceiling = False
790
- result.on_ground = False
1057
+ result.on_ground = False
791
1058
  result.slide_vector = None
792
1059
  result.final_x = sprite.x
793
1060
  result.final_y = sprite.y
@@ -802,7 +1069,7 @@ class CollisionRunner:
802
1069
  if self._collides_at(sprite, tileset_collision, tile_map):
803
1070
  sprite.x = old_x
804
1071
  sprite.y = old_y
805
- result.collided = True
1072
+ result.collided = True
806
1073
  result.hit_wall_x = delta_x != 0
807
1074
  result.hit_wall_y = delta_y != 0
808
1075
  else:
@@ -919,7 +1186,9 @@ class CollisionRunner:
919
1186
  game_type = game_type.lower()
920
1187
 
921
1188
  if game_type == "platformer":
922
- runner = cls(tile_size, mode=MovementMode.PLATFORMER, render_scale=render_scale)
1189
+ runner = cls(
1190
+ tile_size, mode=MovementMode.PLATFORMER, render_scale=render_scale
1191
+ )
923
1192
  runner.gravity = 800.0
924
1193
  runner.max_fall_speed = 600.0
925
1194
  runner.jump_strength = -400.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tilemap-parser
3
- Version: 3.1.1
3
+ Version: 3.1.3
4
4
  Summary: Standalone parser/loader for tilemap-editor JSON maps, sprite animations, and collision detection runtime.
5
5
  Author: tilemap parser contributors
6
6
  License: GNU GENERAL PUBLIC LICENSE
File without changes
File without changes
File without changes