tilemap-parser 3.1.21__tar.gz → 3.1.23__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.21 → tilemap_parser-3.1.23}/PKG-INFO +1 -1
- {tilemap_parser-3.1.21 → tilemap_parser-3.1.23}/pyproject.toml +1 -1
- {tilemap_parser-3.1.21 → tilemap_parser-3.1.23}/src/tilemap_parser/parser/particle.py +17 -0
- {tilemap_parser-3.1.21 → tilemap_parser-3.1.23}/src/tilemap_parser/runtime/__init__.py +2 -0
- tilemap_parser-3.1.23/src/tilemap_parser/runtime/camera.py +176 -0
- {tilemap_parser-3.1.21 → tilemap_parser-3.1.23}/src/tilemap_parser/runtime/map_loader.py +83 -0
- {tilemap_parser-3.1.21 → tilemap_parser-3.1.23}/src/tilemap_parser/runtime/object_collision.py +0 -2
- {tilemap_parser-3.1.21 → tilemap_parser-3.1.23}/src/tilemap_parser/runtime/tile_collision.py +165 -0
- {tilemap_parser-3.1.21 → tilemap_parser-3.1.23}/src/tilemap_parser.egg-info/PKG-INFO +1 -1
- {tilemap_parser-3.1.21 → tilemap_parser-3.1.23}/src/tilemap_parser.egg-info/SOURCES.txt +7 -0
- tilemap_parser-3.1.23/tests/test_camera.py +334 -0
- tilemap_parser-3.1.23/tests/test_integration_animation.py +179 -0
- tilemap_parser-3.1.23/tests/test_integration_collision.py +132 -0
- tilemap_parser-3.1.23/tests/test_integration_map_loader.py +148 -0
- {tilemap_parser-3.1.21 → tilemap_parser-3.1.23}/tests/test_map_loader.py +81 -8
- tilemap_parser-3.1.23/tests/test_move_grounded.py +308 -0
- {tilemap_parser-3.1.21 → tilemap_parser-3.1.23}/tests/test_object_surfaces.py +0 -8
- tilemap_parser-3.1.23/tests/test_particle_emitter.py +74 -0
- {tilemap_parser-3.1.21 → tilemap_parser-3.1.23}/LICENSE +0 -0
- {tilemap_parser-3.1.21 → tilemap_parser-3.1.23}/README.md +0 -0
- {tilemap_parser-3.1.21 → tilemap_parser-3.1.23}/setup.cfg +0 -0
- {tilemap_parser-3.1.21 → tilemap_parser-3.1.23}/src/tilemap_parser/__init__.py +0 -0
- {tilemap_parser-3.1.21 → tilemap_parser-3.1.23}/src/tilemap_parser/parser/__init__.py +0 -0
- {tilemap_parser-3.1.21 → tilemap_parser-3.1.23}/src/tilemap_parser/parser/animation.py +0 -0
- {tilemap_parser-3.1.21 → tilemap_parser-3.1.23}/src/tilemap_parser/parser/collision.py +0 -0
- {tilemap_parser-3.1.21 → tilemap_parser-3.1.23}/src/tilemap_parser/parser/collision_loader.py +0 -0
- {tilemap_parser-3.1.21 → tilemap_parser-3.1.23}/src/tilemap_parser/parser/map_parse.py +0 -0
- {tilemap_parser-3.1.21 → tilemap_parser-3.1.23}/src/tilemap_parser/parser/node_parse.py +0 -0
- {tilemap_parser-3.1.21 → tilemap_parser-3.1.23}/src/tilemap_parser/runtime/animation_player.py +0 -0
- {tilemap_parser-3.1.21 → tilemap_parser-3.1.23}/src/tilemap_parser/runtime/area_node.py +0 -0
- {tilemap_parser-3.1.21 → tilemap_parser-3.1.23}/src/tilemap_parser/runtime/collision_cache.py +0 -0
- {tilemap_parser-3.1.21 → tilemap_parser-3.1.23}/src/tilemap_parser/runtime/particles.py +0 -0
- {tilemap_parser-3.1.21 → tilemap_parser-3.1.23}/src/tilemap_parser/runtime/renderer.py +0 -0
- {tilemap_parser-3.1.21 → tilemap_parser-3.1.23}/src/tilemap_parser/utils/__init__.py +0 -0
- {tilemap_parser-3.1.21 → tilemap_parser-3.1.23}/src/tilemap_parser/utils/geometry.py +0 -0
- {tilemap_parser-3.1.21 → tilemap_parser-3.1.23}/src/tilemap_parser.egg-info/dependency_links.txt +0 -0
- {tilemap_parser-3.1.21 → tilemap_parser-3.1.23}/src/tilemap_parser.egg-info/requires.txt +0 -0
- {tilemap_parser-3.1.21 → tilemap_parser-3.1.23}/src/tilemap_parser.egg-info/top_level.txt +0 -0
- {tilemap_parser-3.1.21 → tilemap_parser-3.1.23}/tests/test_collision.py +0 -0
- {tilemap_parser-3.1.21 → tilemap_parser-3.1.23}/tests/test_geometry.py +0 -0
- {tilemap_parser-3.1.21 → tilemap_parser-3.1.23}/tests/test_object_collision.py +0 -0
- {tilemap_parser-3.1.21 → tilemap_parser-3.1.23}/tests/test_render_scale.py +0 -0
- {tilemap_parser-3.1.21 → tilemap_parser-3.1.23}/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.
|
|
3
|
+
Version: 3.1.23
|
|
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.23"
|
|
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"
|
|
@@ -53,6 +53,23 @@ class ParticleSystemConfig:
|
|
|
53
53
|
rotation_speed: float = 0.0
|
|
54
54
|
alpha_fade: str = "fade_out"
|
|
55
55
|
|
|
56
|
+
def apply_render_scale(self, scale: float) -> None:
|
|
57
|
+
"""Scale dimensionful fields by *scale* (typically ``render_scale``).
|
|
58
|
+
|
|
59
|
+
Modifies ``particle_size_min``, ``particle_size_max``,
|
|
60
|
+
``speed_min``, ``speed_max``, ``gravity_x``, and ``gravity_y``
|
|
61
|
+
in place. Call once after loading config.
|
|
62
|
+
|
|
63
|
+
``start_scale``, ``end_scale``, lifetimes, colors, angles, and
|
|
64
|
+
counts are intentionally left untouched.
|
|
65
|
+
"""
|
|
66
|
+
self.particle_size_min = max(1, int(self.particle_size_min * scale))
|
|
67
|
+
self.particle_size_max = max(1, int(self.particle_size_max * scale))
|
|
68
|
+
self.speed_min *= scale
|
|
69
|
+
self.speed_max *= scale
|
|
70
|
+
self.gravity_x *= scale
|
|
71
|
+
self.gravity_y *= scale
|
|
72
|
+
|
|
56
73
|
def to_dict(self) -> JsonDict:
|
|
57
74
|
return {
|
|
58
75
|
"emission_shape": self.emission_shape,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from .camera import Camera
|
|
1
2
|
from .animation_player import AnimationPlayer, SpriteAnimationSet
|
|
2
3
|
from .collision_cache import (
|
|
3
4
|
CollisionCache,
|
|
@@ -38,6 +39,7 @@ from .particles import (
|
|
|
38
39
|
__all__ = [
|
|
39
40
|
"AnimationPlayer",
|
|
40
41
|
"AreaNode",
|
|
42
|
+
"Camera",
|
|
41
43
|
"CollisionCache",
|
|
42
44
|
"CollisionHit",
|
|
43
45
|
"CollisionResult",
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Camera with centered or deadzone follow, lerp smoothing, screen-shake, and bounds clamping.
|
|
3
|
+
|
|
4
|
+
Usage::
|
|
5
|
+
|
|
6
|
+
camera = Camera(800, 600, mode="centered")
|
|
7
|
+
camera.follow(player)
|
|
8
|
+
|
|
9
|
+
# each frame:
|
|
10
|
+
camera.update(dt)
|
|
11
|
+
|
|
12
|
+
# render with camera.offset:
|
|
13
|
+
tile_renderer.render(screen, camera.offset)
|
|
14
|
+
player.render(screen, camera.offset)
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import math
|
|
20
|
+
import random
|
|
21
|
+
from typing import Optional, Tuple
|
|
22
|
+
|
|
23
|
+
import pygame
|
|
24
|
+
|
|
25
|
+
from ..utils.geometry import get_shape_aabb
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Camera:
|
|
29
|
+
__slots__ = (
|
|
30
|
+
"x",
|
|
31
|
+
"y",
|
|
32
|
+
"viewport_w",
|
|
33
|
+
"viewport_h",
|
|
34
|
+
"mode",
|
|
35
|
+
"target",
|
|
36
|
+
"lerp_speed",
|
|
37
|
+
"deadzone",
|
|
38
|
+
"bounds",
|
|
39
|
+
"_shake_timer",
|
|
40
|
+
"_shake_intensity",
|
|
41
|
+
"_shake_ox",
|
|
42
|
+
"_shake_oy",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
viewport_width: int,
|
|
48
|
+
viewport_height: int,
|
|
49
|
+
mode: str = "centered",
|
|
50
|
+
):
|
|
51
|
+
if mode not in ("centered", "deadzone"):
|
|
52
|
+
raise ValueError(f"Camera mode must be 'centered' or 'deadzone', got {mode!r}")
|
|
53
|
+
|
|
54
|
+
self.x = 0.0
|
|
55
|
+
self.y = 0.0
|
|
56
|
+
self.viewport_w = viewport_width
|
|
57
|
+
self.viewport_h = viewport_height
|
|
58
|
+
self.mode = mode
|
|
59
|
+
self.target = None
|
|
60
|
+
self.lerp_speed = 0.0
|
|
61
|
+
self.bounds = None
|
|
62
|
+
|
|
63
|
+
if mode == "deadzone":
|
|
64
|
+
dw = viewport_width * 0.5
|
|
65
|
+
dh = viewport_height * 0.5
|
|
66
|
+
self.deadzone = pygame.Rect(
|
|
67
|
+
(viewport_width - dw) / 2,
|
|
68
|
+
(viewport_height - dh) / 2,
|
|
69
|
+
dw,
|
|
70
|
+
dh,
|
|
71
|
+
)
|
|
72
|
+
else:
|
|
73
|
+
self.deadzone = None
|
|
74
|
+
|
|
75
|
+
self._shake_timer = 0.0
|
|
76
|
+
self._shake_intensity = 0.0
|
|
77
|
+
self._shake_ox = 0.0
|
|
78
|
+
self._shake_oy = 0.0
|
|
79
|
+
|
|
80
|
+
def follow(self, target) -> None:
|
|
81
|
+
"""Set the entity the camera should follow.
|
|
82
|
+
|
|
83
|
+
*target* must have ``x``, ``y``, and ``collision_shape`` attributes
|
|
84
|
+
(any sprite managed by the collision runner satisfies this).
|
|
85
|
+
"""
|
|
86
|
+
self.target = target
|
|
87
|
+
|
|
88
|
+
def shake(self, duration: float, intensity: float) -> None:
|
|
89
|
+
"""Trigger a screen-shake effect.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
duration: Seconds the shake lasts.
|
|
93
|
+
intensity: Maximum pixel offset applied each frame.
|
|
94
|
+
"""
|
|
95
|
+
self._shake_timer = duration
|
|
96
|
+
self._shake_intensity = intensity
|
|
97
|
+
|
|
98
|
+
def update(self, dt: float) -> None:
|
|
99
|
+
"""Advance the camera by *dt* seconds.
|
|
100
|
+
|
|
101
|
+
Computes the new position based on the follow mode, applies lerp,
|
|
102
|
+
clamps to bounds, and updates the screen-shake.
|
|
103
|
+
"""
|
|
104
|
+
if self.target is not None:
|
|
105
|
+
l, t, r, b = get_shape_aabb(
|
|
106
|
+
self.target.x,
|
|
107
|
+
self.target.y,
|
|
108
|
+
self.target.collision_shape,
|
|
109
|
+
)
|
|
110
|
+
cx = (l + r) * 0.5
|
|
111
|
+
cy = (t + b) * 0.5
|
|
112
|
+
|
|
113
|
+
if self.mode == "centered":
|
|
114
|
+
self._move_toward(
|
|
115
|
+
cx - self.viewport_w / 2,
|
|
116
|
+
cy - self.viewport_h / 2,
|
|
117
|
+
dt,
|
|
118
|
+
)
|
|
119
|
+
elif self.mode == "deadzone" and self.deadzone is not None:
|
|
120
|
+
self._follow_deadzone(cx, cy, dt)
|
|
121
|
+
|
|
122
|
+
# Bounds clamp
|
|
123
|
+
if self.bounds is not None:
|
|
124
|
+
min_x, min_y, max_x, max_y = self.bounds
|
|
125
|
+
self.x = max(min_x, min(self.x, max_x - self.viewport_w))
|
|
126
|
+
self.y = max(min_y, min(self.y, max_y - self.viewport_h))
|
|
127
|
+
|
|
128
|
+
# Shake
|
|
129
|
+
if self._shake_timer > 0:
|
|
130
|
+
self._shake_timer -= dt
|
|
131
|
+
i = self._shake_intensity
|
|
132
|
+
self._shake_ox = random.uniform(-i, i)
|
|
133
|
+
self._shake_oy = random.uniform(-i, i)
|
|
134
|
+
if self._shake_timer <= 0:
|
|
135
|
+
self._shake_timer = 0.0
|
|
136
|
+
self._shake_ox = 0.0
|
|
137
|
+
self._shake_oy = 0.0
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def offset(self) -> Tuple[float, float]:
|
|
141
|
+
"""Camera offset including active screen-shake.
|
|
142
|
+
|
|
143
|
+
Pass this directly to render calls::
|
|
144
|
+
|
|
145
|
+
tile_renderer.render(screen, camera.offset)
|
|
146
|
+
player.render(screen, camera.offset)
|
|
147
|
+
"""
|
|
148
|
+
return (self.x + self._shake_ox, self.y + self._shake_oy)
|
|
149
|
+
|
|
150
|
+
def _move_toward(self, target_x: float, target_y: float, dt: float) -> None:
|
|
151
|
+
if self.lerp_speed > 0:
|
|
152
|
+
t = 1.0 - math.exp(-self.lerp_speed * dt)
|
|
153
|
+
self.x += (target_x - self.x) * t
|
|
154
|
+
self.y += (target_y - self.y) * t
|
|
155
|
+
else:
|
|
156
|
+
self.x = target_x
|
|
157
|
+
self.y = target_y
|
|
158
|
+
|
|
159
|
+
def _follow_deadzone(self, cx: float, cy: float, dt: float) -> None:
|
|
160
|
+
# Target position in screen space
|
|
161
|
+
sx = cx - self.x
|
|
162
|
+
sy = cy - self.y
|
|
163
|
+
dz = self.deadzone
|
|
164
|
+
dx = dy = 0.0
|
|
165
|
+
|
|
166
|
+
if sx < dz.left:
|
|
167
|
+
dx = sx - dz.left
|
|
168
|
+
elif sx > dz.right:
|
|
169
|
+
dx = sx - dz.right
|
|
170
|
+
|
|
171
|
+
if sy < dz.top:
|
|
172
|
+
dy = sy - dz.top
|
|
173
|
+
elif sy > dz.bottom:
|
|
174
|
+
dy = sy - dz.bottom
|
|
175
|
+
|
|
176
|
+
self._move_toward(self.x + dx, self.y + dy, dt)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
+
import math
|
|
4
5
|
from copy import deepcopy
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
from typing import Dict, List, Optional, Tuple, Union
|
|
@@ -31,6 +32,8 @@ class TilemapData:
|
|
|
31
32
|
self.resolved_paths = resolved_paths
|
|
32
33
|
self.warnings = warnings
|
|
33
34
|
self.map_path = map_path
|
|
35
|
+
# Pixel/world offset applied while normalizing negative source coordinates.
|
|
36
|
+
self.origin_offset = (0, 0)
|
|
34
37
|
self.area_nodes: List[AreaNode] = []
|
|
35
38
|
self.particle_emitters: List[ParticleEmitterNode] = []
|
|
36
39
|
self._tw, self._th = parsed.meta.tile_size
|
|
@@ -98,7 +101,9 @@ class TilemapData:
|
|
|
98
101
|
warnings.append(f"Failed to load nodes: {e}")
|
|
99
102
|
break
|
|
100
103
|
|
|
104
|
+
origin_offset = _normalize_origin(parsed)
|
|
101
105
|
result = cls(parsed, surfaces, resolved_paths, warnings, map_path=p)
|
|
106
|
+
result.origin_offset = origin_offset
|
|
102
107
|
result.area_nodes = [AreaNode(n) for n in parsed.nodes if n.node_type == "area"]
|
|
103
108
|
result.particle_emitters = [
|
|
104
109
|
ParticleEmitterNode(n) for n in parsed.nodes if n.node_type == "particle_emitter"
|
|
@@ -312,5 +317,83 @@ def _resolve_resource_path(path_str: str, map_dir: Path, extra_search_base: Opti
|
|
|
312
317
|
return candidate
|
|
313
318
|
|
|
314
319
|
|
|
320
|
+
def _normalize_origin(parsed: ParsedMap) -> Tuple[int, int]:
|
|
321
|
+
tw, th = parsed.meta.tile_size
|
|
322
|
+
rs = parsed.meta.render_scale
|
|
323
|
+
eff_w = int(tw * rs)
|
|
324
|
+
eff_h = int(th * rs)
|
|
325
|
+
if eff_w <= 0 or eff_h <= 0:
|
|
326
|
+
return (0, 0)
|
|
327
|
+
|
|
328
|
+
min_x = 0
|
|
329
|
+
min_y = 0
|
|
330
|
+
max_x = parsed.meta.map_size[0]
|
|
331
|
+
max_y = parsed.meta.map_size[1]
|
|
332
|
+
|
|
333
|
+
for layer in parsed.layers:
|
|
334
|
+
for x, y in layer.tiles.keys():
|
|
335
|
+
min_x = min(min_x, x)
|
|
336
|
+
min_y = min(min_y, y)
|
|
337
|
+
max_x = max(max_x, x + 1)
|
|
338
|
+
max_y = max(max_y, y + 1)
|
|
339
|
+
|
|
340
|
+
for obj in layer.objects.values():
|
|
341
|
+
left = math.floor(obj.area.x / eff_w)
|
|
342
|
+
top = math.floor(obj.area.y / eff_h)
|
|
343
|
+
right = math.ceil((obj.area.x + obj.area.w) / eff_w)
|
|
344
|
+
bottom = math.ceil((obj.area.y + obj.area.h) / eff_h)
|
|
345
|
+
min_x = min(min_x, left)
|
|
346
|
+
min_y = min(min_y, top)
|
|
347
|
+
max_x = max(max_x, right)
|
|
348
|
+
max_y = max(max_y, bottom)
|
|
349
|
+
|
|
350
|
+
for node in parsed.nodes:
|
|
351
|
+
left = math.floor(node.area.x / eff_w)
|
|
352
|
+
top = math.floor(node.area.y / eff_h)
|
|
353
|
+
right = math.ceil((node.area.x + node.area.w) / eff_w)
|
|
354
|
+
bottom = math.ceil((node.area.y + node.area.h) / eff_h)
|
|
355
|
+
min_x = min(min_x, left)
|
|
356
|
+
min_y = min(min_y, top)
|
|
357
|
+
max_x = max(max_x, right)
|
|
358
|
+
max_y = max(max_y, bottom)
|
|
359
|
+
|
|
360
|
+
if min_x >= 0 and min_y >= 0:
|
|
361
|
+
parsed.meta.map_size = (max_x, max_y)
|
|
362
|
+
return (0, 0)
|
|
363
|
+
|
|
364
|
+
shift_x = -min_x
|
|
365
|
+
shift_y = -min_y
|
|
366
|
+
pixel_shift_x = shift_x * eff_w
|
|
367
|
+
pixel_shift_y = shift_y * eff_h
|
|
368
|
+
|
|
369
|
+
for layer in parsed.layers:
|
|
370
|
+
if layer.tiles:
|
|
371
|
+
shifted_tiles = {}
|
|
372
|
+
for (x, y), tile in layer.tiles.items():
|
|
373
|
+
new_pos = (x + shift_x, y + shift_y)
|
|
374
|
+
tile.pos = new_pos
|
|
375
|
+
shifted_tiles[new_pos] = tile
|
|
376
|
+
layer.tiles = shifted_tiles
|
|
377
|
+
|
|
378
|
+
for obj in layer.objects.values():
|
|
379
|
+
obj.area.x += pixel_shift_x
|
|
380
|
+
obj.area.y += pixel_shift_y
|
|
381
|
+
|
|
382
|
+
for node in parsed.nodes:
|
|
383
|
+
node.area.x += pixel_shift_x
|
|
384
|
+
node.area.y += pixel_shift_y
|
|
385
|
+
|
|
386
|
+
parsed.meta.map_size = (max_x + shift_x, max_y + shift_y)
|
|
387
|
+
parsed.meta.initial_map_size = (
|
|
388
|
+
parsed.meta.initial_map_size[0] + shift_x,
|
|
389
|
+
parsed.meta.initial_map_size[1] + shift_y,
|
|
390
|
+
)
|
|
391
|
+
parsed.meta.scroll = (
|
|
392
|
+
parsed.meta.scroll[0] + pixel_shift_x,
|
|
393
|
+
parsed.meta.scroll[1] + pixel_shift_y,
|
|
394
|
+
)
|
|
395
|
+
return (pixel_shift_x, pixel_shift_y)
|
|
396
|
+
|
|
397
|
+
|
|
315
398
|
def load_map(path: PathLike, *, extra_search_base: Optional[Path] = None, skip_missing_images: bool = True, nodes_dir: Optional[PathLike] = None) -> TilemapData:
|
|
316
399
|
return TilemapData.load(path, extra_search_base=extra_search_base, skip_missing_images=skip_missing_images, nodes_dir=nodes_dir)
|
{tilemap_parser-3.1.21 → tilemap_parser-3.1.23}/src/tilemap_parser/runtime/object_collision.py
RENAMED
|
@@ -224,9 +224,7 @@ def check_collision(
|
|
|
224
224
|
depth=result.depth,
|
|
225
225
|
)
|
|
226
226
|
|
|
227
|
-
# ------------------------------------------------------------------
|
|
228
227
|
# Capsule pairs
|
|
229
|
-
# ------------------------------------------------------------------
|
|
230
228
|
elif isinstance(shape_a, CapsuleShape) and isinstance(shape_b, CapsuleShape):
|
|
231
229
|
p1 = (obj_a.x + shape_a.offset[0], obj_a.y + shape_a.offset[1])
|
|
232
230
|
p2 = (p1[0], p1[1] + shape_a.height)
|
{tilemap_parser-3.1.21 → tilemap_parser-3.1.23}/src/tilemap_parser/runtime/tile_collision.py
RENAMED
|
@@ -790,6 +790,171 @@ class CollisionRunner:
|
|
|
790
790
|
|
|
791
791
|
return best_edge
|
|
792
792
|
|
|
793
|
+
def move_grounded(
|
|
794
|
+
self,
|
|
795
|
+
sprite: ICollidableSprite,
|
|
796
|
+
tileset_collision: TilesetCollision,
|
|
797
|
+
tile_map: dict,
|
|
798
|
+
dt: float,
|
|
799
|
+
velocity: Optional[Vector2] = None,
|
|
800
|
+
) -> CollisionResult:
|
|
801
|
+
"""
|
|
802
|
+
Move sprite with basic grounded physics for simple entities.
|
|
803
|
+
|
|
804
|
+
A lightweight alternative to ``move_platformer()`` and
|
|
805
|
+
``move_platformer_with_slide()`` for enemies and other
|
|
806
|
+
entities that need tilemap collision without platformer
|
|
807
|
+
features like jumping, one-way platforms, or slope walking.
|
|
808
|
+
|
|
809
|
+
Key behaviours:
|
|
810
|
+
|
|
811
|
+
* Applies gravity every frame when the sprite is not on the
|
|
812
|
+
ground.
|
|
813
|
+
* Resolves X collision (walls) by backing out the X axis and
|
|
814
|
+
zeroing horizontal velocity.
|
|
815
|
+
* Resolves Y collision via binary search so the sprite lands
|
|
816
|
+
exactly on the polygon surface --- critical for **partial
|
|
817
|
+
tiles** that only have collision in part of the tile (e.g.
|
|
818
|
+
a 32x32 tile whose collision polygon only occupies the
|
|
819
|
+
bottom 16 px).
|
|
820
|
+
* Detects walk-off-ledge: when the sprite was on the ground
|
|
821
|
+
and moves horizontally over empty space it is immediately
|
|
822
|
+
set airborne.
|
|
823
|
+
|
|
824
|
+
All collision uses the tile polygon shapes through the same
|
|
825
|
+
zero-allocation ``_collides_at`` machinery as the other
|
|
826
|
+
movement methods, so one-way platform polygons are treated
|
|
827
|
+
as solid geometry (they block both X and Y movement).
|
|
828
|
+
|
|
829
|
+
Args:
|
|
830
|
+
sprite: Sprite to move. Must expose ``x``, ``y``, ``vx``,
|
|
831
|
+
``vy``, ``on_ground``, and ``collision_shape``.
|
|
832
|
+
tileset_collision: Tileset collision data.
|
|
833
|
+
tile_map: Dictionary mapping ``(tile_x, tile_y)`` to tile id.
|
|
834
|
+
dt: Frame delta time in seconds.
|
|
835
|
+
velocity: Optional explicit ``(vx, vy)``. When provided
|
|
836
|
+
the method skips gravity and uses this velocity directly
|
|
837
|
+
instead of reading from ``sprite.vx``/``sprite.vy``.
|
|
838
|
+
Useful for custom controllers, knockback, or flying
|
|
839
|
+
entities that should not receive gravity.
|
|
840
|
+
|
|
841
|
+
Returns:
|
|
842
|
+
:class:`CollisionResult` with final position and flags.
|
|
843
|
+
"""
|
|
844
|
+
result = self._result
|
|
845
|
+
result.collided = False
|
|
846
|
+
result.hit_wall_x = False
|
|
847
|
+
result.hit_wall_y = False
|
|
848
|
+
result.hit_ceiling = False
|
|
849
|
+
result.on_ground = False
|
|
850
|
+
result.slide_vector = None
|
|
851
|
+
result.final_x = sprite.x
|
|
852
|
+
result.final_y = sprite.y
|
|
853
|
+
|
|
854
|
+
old_x, old_y = sprite.x, sprite.y
|
|
855
|
+
was_on_ground = getattr(sprite, "on_ground", False)
|
|
856
|
+
|
|
857
|
+
# --- resolve velocity -----------------------------------------------
|
|
858
|
+
if velocity is not None:
|
|
859
|
+
sprite.vx = velocity[0]
|
|
860
|
+
sprite.vy = velocity[1]
|
|
861
|
+
else:
|
|
862
|
+
if not was_on_ground:
|
|
863
|
+
sprite.vy += self.gravity * dt
|
|
864
|
+
if sprite.vy > self.max_fall_speed:
|
|
865
|
+
sprite.vy = self.max_fall_speed
|
|
866
|
+
|
|
867
|
+
delta_x = sprite.vx * dt
|
|
868
|
+
delta_y = sprite.vy * dt
|
|
869
|
+
|
|
870
|
+
# --- X axis ---------------------------------------------------------
|
|
871
|
+
if delta_x != 0.0:
|
|
872
|
+
sprite.x = old_x + delta_x
|
|
873
|
+
sprite.y = old_y
|
|
874
|
+
|
|
875
|
+
if self._collides_at(sprite, tileset_collision, tile_map):
|
|
876
|
+
sprite.x = old_x
|
|
877
|
+
sprite.vx = 0.0
|
|
878
|
+
result.hit_wall_x = True
|
|
879
|
+
result.collided = True
|
|
880
|
+
|
|
881
|
+
# --- ledge detection ------------------------------------------------
|
|
882
|
+
# If the sprite was grounded and isn't actively falling, probe 1 px
|
|
883
|
+
# downward at the *new* X position. When nothing is underneath we
|
|
884
|
+
# immediately leave the ground so gravity kicks in next frame.
|
|
885
|
+
# Not applied when velocity is explicitly passed (caller controls Y).
|
|
886
|
+
if velocity is None and was_on_ground and delta_y == 0.0:
|
|
887
|
+
saved_y = sprite.y
|
|
888
|
+
sprite.y += 1.0
|
|
889
|
+
ground_below = self._collides_at(sprite, tileset_collision, tile_map)
|
|
890
|
+
sprite.y = saved_y
|
|
891
|
+
|
|
892
|
+
if not ground_below:
|
|
893
|
+
sprite.on_ground = False
|
|
894
|
+
was_on_ground = False
|
|
895
|
+
|
|
896
|
+
# Begin falling immediately.
|
|
897
|
+
sprite.vy += self.gravity * dt
|
|
898
|
+
if sprite.vy > self.max_fall_speed:
|
|
899
|
+
sprite.vy = self.max_fall_speed
|
|
900
|
+
|
|
901
|
+
delta_y = sprite.vy * dt
|
|
902
|
+
|
|
903
|
+
# --- Y axis ---------------------------------------------------------
|
|
904
|
+
sprite.y = sprite.y + delta_y
|
|
905
|
+
|
|
906
|
+
collided_y = self._collides_at(sprite, tileset_collision, tile_map)
|
|
907
|
+
|
|
908
|
+
if collided_y:
|
|
909
|
+
result.collided = True
|
|
910
|
+
|
|
911
|
+
if delta_y >= 0.0:
|
|
912
|
+
# Falling -> binary search for ground surface
|
|
913
|
+
sprite.y = old_y
|
|
914
|
+
lo, hi = old_y, old_y + delta_y
|
|
915
|
+
|
|
916
|
+
for _ in range(8):
|
|
917
|
+
mid = (lo + hi) * 0.5
|
|
918
|
+
sprite.y = mid
|
|
919
|
+
|
|
920
|
+
if self._collides_at(sprite, tileset_collision, tile_map):
|
|
921
|
+
hi = mid
|
|
922
|
+
else:
|
|
923
|
+
lo = mid
|
|
924
|
+
|
|
925
|
+
sprite.y = lo
|
|
926
|
+
sprite.vy = 0.0
|
|
927
|
+
sprite.on_ground = True
|
|
928
|
+
result.hit_wall_y = True
|
|
929
|
+
result.on_ground = True
|
|
930
|
+
|
|
931
|
+
else:
|
|
932
|
+
# Rising -> binary search for ceiling
|
|
933
|
+
sprite.y = old_y
|
|
934
|
+
lo, hi = old_y + delta_y, old_y
|
|
935
|
+
|
|
936
|
+
for _ in range(8):
|
|
937
|
+
mid = (lo + hi) * 0.5
|
|
938
|
+
sprite.y = mid
|
|
939
|
+
|
|
940
|
+
if self._collides_at(sprite, tileset_collision, tile_map):
|
|
941
|
+
lo = mid
|
|
942
|
+
else:
|
|
943
|
+
hi = mid
|
|
944
|
+
|
|
945
|
+
sprite.y = hi
|
|
946
|
+
sprite.vy = 0.0
|
|
947
|
+
result.hit_ceiling = True
|
|
948
|
+
|
|
949
|
+
elif delta_y > 0.0:
|
|
950
|
+
sprite.on_ground = False
|
|
951
|
+
|
|
952
|
+
result.final_x = sprite.x
|
|
953
|
+
result.final_y = sprite.y
|
|
954
|
+
result.on_ground = getattr(sprite, "on_ground", False)
|
|
955
|
+
|
|
956
|
+
return result
|
|
957
|
+
|
|
793
958
|
def move_platformer(
|
|
794
959
|
self,
|
|
795
960
|
sprite: ICollidableSprite,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tilemap-parser
|
|
3
|
-
Version: 3.1.
|
|
3
|
+
Version: 3.1.23
|
|
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
|
|
@@ -17,6 +17,7 @@ src/tilemap_parser/parser/particle.py
|
|
|
17
17
|
src/tilemap_parser/runtime/__init__.py
|
|
18
18
|
src/tilemap_parser/runtime/animation_player.py
|
|
19
19
|
src/tilemap_parser/runtime/area_node.py
|
|
20
|
+
src/tilemap_parser/runtime/camera.py
|
|
20
21
|
src/tilemap_parser/runtime/collision_cache.py
|
|
21
22
|
src/tilemap_parser/runtime/map_loader.py
|
|
22
23
|
src/tilemap_parser/runtime/object_collision.py
|
|
@@ -25,10 +26,16 @@ src/tilemap_parser/runtime/renderer.py
|
|
|
25
26
|
src/tilemap_parser/runtime/tile_collision.py
|
|
26
27
|
src/tilemap_parser/utils/__init__.py
|
|
27
28
|
src/tilemap_parser/utils/geometry.py
|
|
29
|
+
tests/test_camera.py
|
|
28
30
|
tests/test_collision.py
|
|
29
31
|
tests/test_geometry.py
|
|
32
|
+
tests/test_integration_animation.py
|
|
33
|
+
tests/test_integration_collision.py
|
|
34
|
+
tests/test_integration_map_loader.py
|
|
30
35
|
tests/test_map_loader.py
|
|
36
|
+
tests/test_move_grounded.py
|
|
31
37
|
tests/test_object_collision.py
|
|
32
38
|
tests/test_object_surfaces.py
|
|
39
|
+
tests/test_particle_emitter.py
|
|
33
40
|
tests/test_render_scale.py
|
|
34
41
|
tests/test_tile_collision.py
|