tilemap-parser 3.1.7__tar.gz → 3.1.9__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 (33) hide show
  1. {tilemap_parser-3.1.7 → tilemap_parser-3.1.9}/PKG-INFO +1 -1
  2. {tilemap_parser-3.1.7 → tilemap_parser-3.1.9}/pyproject.toml +1 -1
  3. {tilemap_parser-3.1.7 → tilemap_parser-3.1.9}/src/tilemap_parser/__init__.py +4 -0
  4. {tilemap_parser-3.1.7 → tilemap_parser-3.1.9}/src/tilemap_parser/parser/__init__.py +6 -0
  5. {tilemap_parser-3.1.7 → tilemap_parser-3.1.9}/src/tilemap_parser/parser/map_parse.py +29 -2
  6. tilemap_parser-3.1.9/src/tilemap_parser/parser/node_parse.py +77 -0
  7. {tilemap_parser-3.1.7 → tilemap_parser-3.1.9}/src/tilemap_parser/runtime/__init__.py +2 -0
  8. tilemap_parser-3.1.9/src/tilemap_parser/runtime/area_node.py +38 -0
  9. {tilemap_parser-3.1.7 → tilemap_parser-3.1.9}/src/tilemap_parser/runtime/map_loader.py +36 -0
  10. {tilemap_parser-3.1.7 → tilemap_parser-3.1.9}/src/tilemap_parser/runtime/renderer.py +41 -1
  11. {tilemap_parser-3.1.7 → tilemap_parser-3.1.9}/src/tilemap_parser.egg-info/PKG-INFO +1 -1
  12. {tilemap_parser-3.1.7 → tilemap_parser-3.1.9}/src/tilemap_parser.egg-info/SOURCES.txt +2 -0
  13. {tilemap_parser-3.1.7 → tilemap_parser-3.1.9}/LICENSE +0 -0
  14. {tilemap_parser-3.1.7 → tilemap_parser-3.1.9}/README.md +0 -0
  15. {tilemap_parser-3.1.7 → tilemap_parser-3.1.9}/setup.cfg +0 -0
  16. {tilemap_parser-3.1.7 → tilemap_parser-3.1.9}/src/tilemap_parser/parser/animation.py +0 -0
  17. {tilemap_parser-3.1.7 → tilemap_parser-3.1.9}/src/tilemap_parser/parser/collision.py +0 -0
  18. {tilemap_parser-3.1.7 → tilemap_parser-3.1.9}/src/tilemap_parser/parser/collision_loader.py +0 -0
  19. {tilemap_parser-3.1.7 → tilemap_parser-3.1.9}/src/tilemap_parser/runtime/animation_player.py +0 -0
  20. {tilemap_parser-3.1.7 → tilemap_parser-3.1.9}/src/tilemap_parser/runtime/collision_cache.py +0 -0
  21. {tilemap_parser-3.1.7 → tilemap_parser-3.1.9}/src/tilemap_parser/runtime/object_collision.py +0 -0
  22. {tilemap_parser-3.1.7 → tilemap_parser-3.1.9}/src/tilemap_parser/runtime/tile_collision.py +0 -0
  23. {tilemap_parser-3.1.7 → tilemap_parser-3.1.9}/src/tilemap_parser/utils/__init__.py +0 -0
  24. {tilemap_parser-3.1.7 → tilemap_parser-3.1.9}/src/tilemap_parser/utils/geometry.py +0 -0
  25. {tilemap_parser-3.1.7 → tilemap_parser-3.1.9}/src/tilemap_parser.egg-info/dependency_links.txt +0 -0
  26. {tilemap_parser-3.1.7 → tilemap_parser-3.1.9}/src/tilemap_parser.egg-info/requires.txt +0 -0
  27. {tilemap_parser-3.1.7 → tilemap_parser-3.1.9}/src/tilemap_parser.egg-info/top_level.txt +0 -0
  28. {tilemap_parser-3.1.7 → tilemap_parser-3.1.9}/tests/test_collision.py +0 -0
  29. {tilemap_parser-3.1.7 → tilemap_parser-3.1.9}/tests/test_geometry.py +0 -0
  30. {tilemap_parser-3.1.7 → tilemap_parser-3.1.9}/tests/test_map_loader.py +0 -0
  31. {tilemap_parser-3.1.7 → tilemap_parser-3.1.9}/tests/test_object_collision.py +0 -0
  32. {tilemap_parser-3.1.7 → tilemap_parser-3.1.9}/tests/test_render_scale.py +0 -0
  33. {tilemap_parser-3.1.7 → tilemap_parser-3.1.9}/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.7
3
+ Version: 3.1.9
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"
7
+ version = "3.1.9"
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"
@@ -9,6 +9,7 @@ __all__ = [
9
9
  "AnimationMarker",
10
10
  "AnimationParseError",
11
11
  "AnimationPlayer",
12
+ "AreaNode",
12
13
  "CapsuleShape",
13
14
  "CharacterCollision",
14
15
  "CircleShape",
@@ -32,6 +33,7 @@ __all__ = [
32
33
  "ParsedLayer",
33
34
  "ParsedMap",
34
35
  "ParsedMeta",
36
+ "ParsedNode",
35
37
  "ParsedObject",
36
38
  "ParsedObjectArea",
37
39
  "ParsedProjectState",
@@ -62,6 +64,8 @@ __all__ = [
62
64
  "parse_map_dict",
63
65
  "parse_map_file",
64
66
  "parse_map_json",
67
+ "parse_nodes_dict",
68
+ "parse_nodes_file",
65
69
  "parse_object_collision",
66
70
  "parse_tileset_collision",
67
71
  "polygon_vs_circle",
@@ -40,10 +40,12 @@ from .map_parse import (
40
40
  ParsedProjectState,
41
41
  ParsedTile,
42
42
  ParsedTileset,
43
+ TilesetAnimation,
43
44
  parse_map_dict,
44
45
  parse_map_file,
45
46
  parse_map_json,
46
47
  )
48
+ from .node_parse import ParsedNode, parse_nodes_dict, parse_nodes_file
47
49
 
48
50
  __all__ = [
49
51
  "AnimationClip",
@@ -60,6 +62,7 @@ __all__ = [
60
62
  "ObjectCollisionData",
61
63
  "ObjectCollisionRegionData",
62
64
  "ParsedAutotileGroup",
65
+ "ParsedNode",
63
66
  "ParsedAutotileRule",
64
67
  "ParsedLayer",
65
68
  "ParsedMap",
@@ -70,6 +73,7 @@ __all__ = [
70
73
  "ParsedTile",
71
74
  "ParsedTileset",
72
75
  "RectangleShape",
76
+ "TilesetAnimation",
73
77
  "TileCollisionData",
74
78
  "TilesetCollision",
75
79
  "parse_animation_dict",
@@ -82,6 +86,8 @@ __all__ = [
82
86
  "parse_map_dict",
83
87
  "parse_map_file",
84
88
  "parse_map_json",
89
+ "parse_nodes_dict",
90
+ "parse_nodes_file",
85
91
  "parse_object_collision",
86
92
  "parse_tileset_collision",
87
93
  ]
@@ -5,7 +5,10 @@ import re
5
5
  import string
6
6
  from dataclasses import dataclass, field
7
7
  from pathlib import Path
8
- from typing import Any, Dict, List, Optional, Tuple, Union
8
+ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union
9
+
10
+ if TYPE_CHECKING:
11
+ from .node_parse import ParsedNode
9
12
 
10
13
  JsonDict = Dict[str, Any]
11
14
  Point = Tuple[int, int]
@@ -122,12 +125,22 @@ class ParsedObject:
122
125
  properties: Optional[JsonDict] = None
123
126
 
124
127
 
128
+ @dataclass
129
+ class TilesetAnimation:
130
+ frame_count: int
131
+ frame_duration_ms: float
132
+ frame_stride: int
133
+ loop: bool = True
134
+ animation_mode: str = "default"
135
+
136
+
125
137
  @dataclass
126
138
  class ParsedTileset:
127
139
  path: str
128
140
  type: str
129
141
  properties: JsonDict = field(default_factory=dict)
130
142
  tile_properties: Dict[str, JsonDict] = field(default_factory=dict)
143
+ animation: Optional[TilesetAnimation] = None
131
144
 
132
145
 
133
146
  @dataclass
@@ -187,6 +200,8 @@ class ParsedMap:
187
200
  tilesets: List[ParsedTileset]
188
201
  project_state: ParsedProjectState
189
202
  raw: JsonDict
203
+ nodes: List["ParsedNode"] = field(default_factory=list)
204
+ node_groups: List[str] = field(default_factory=list)
190
205
 
191
206
 
192
207
  def _parse_tile(tile_data: JsonDict, ctx: str) -> ParsedTile:
@@ -335,9 +350,21 @@ def _parse_tilesets_list(tilesets_raw: List[Any], ctx: str) -> List[ParsedTilese
335
350
  tile_props[str(k)] = _require_dict(
336
351
  v, f"{ctx}[{i}].tile_properties[{k!r}]"
337
352
  )
353
+ animation = None
354
+ animation_raw = ts_obj.get("animation")
355
+ if animation_raw is not None:
356
+ anim_obj = _require_dict(animation_raw, f"{ctx}[{i}].animation")
357
+ animation = TilesetAnimation(
358
+ frame_count=_coerce_int(anim_obj.get("frame_count"), f"{ctx}[{i}].animation.frame_count"),
359
+ frame_duration_ms=_coerce_float(anim_obj.get("frame_duration_ms"), f"{ctx}[{i}].animation.frame_duration_ms"),
360
+ frame_stride=_coerce_int(anim_obj.get("frame_stride"), f"{ctx}[{i}].animation.frame_stride"),
361
+ loop=_coerce_bool(anim_obj.get("loop", True), f"{ctx}[{i}].animation.loop"),
362
+ animation_mode=_require_str(anim_obj.get("animation_mode", "default"), f"{ctx}[{i}].animation.animation_mode"),
363
+ )
338
364
  out.append(
339
365
  ParsedTileset(
340
- path=path, type=ts_type, properties=props, tile_properties=tile_props
366
+ path=path, type=ts_type, properties=props, tile_properties=tile_props,
367
+ animation=animation,
341
368
  )
342
369
  )
343
370
  return out
@@ -0,0 +1,77 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from dataclasses import dataclass, field
5
+ from pathlib import Path
6
+ from typing import Any, Dict, List, Optional, Union
7
+
8
+ from .map_parse import (
9
+ JsonDict,
10
+ MapParseError,
11
+ ParsedObjectArea,
12
+ _coerce_bool,
13
+ _coerce_float,
14
+ _coerce_int,
15
+ _optional_dict,
16
+ _require_dict,
17
+ _require_list,
18
+ _require_str,
19
+ _ctx,
20
+ )
21
+
22
+
23
+ @dataclass
24
+ class ParsedNode:
25
+ node_id: str
26
+ name: str
27
+ node_type: str
28
+ area: ParsedObjectArea
29
+ layer_name: str
30
+ properties: JsonDict = field(default_factory=dict)
31
+ group: Optional[str] = None
32
+
33
+
34
+ def _parse_area(raw: Any, ctx: str) -> ParsedObjectArea:
35
+ d = _require_dict(raw, ctx)
36
+ return ParsedObjectArea(
37
+ x=_coerce_int(d.get("x"), f"{ctx}.x"),
38
+ y=_coerce_int(d.get("y"), f"{ctx}.y"),
39
+ w=_coerce_int(d.get("w"), f"{ctx}.w"),
40
+ h=_coerce_int(d.get("h"), f"{ctx}.h"),
41
+ )
42
+
43
+
44
+ def _parse_node(raw: Any, ctx: str) -> ParsedNode:
45
+ d = _require_dict(raw, ctx)
46
+ group_raw = d.get("group")
47
+ group = str(group_raw) if group_raw is not None else None
48
+ return ParsedNode(
49
+ node_id=_require_str(d.get("node_id"), f"{ctx}.node_id"),
50
+ name=_require_str(d.get("name"), f"{ctx}.name"),
51
+ node_type=_require_str(d.get("node_type", "area"), f"{ctx}.node_type"),
52
+ area=_parse_area(d.get("area"), f"{ctx}.area"),
53
+ layer_name=_require_str(d.get("layer_name", ""), f"{ctx}.layer_name"),
54
+ properties=_optional_dict(d.get("properties"), f"{ctx}.properties") or {},
55
+ group=group,
56
+ )
57
+
58
+
59
+ def parse_nodes_dict(root: JsonDict) -> List[ParsedNode]:
60
+ root = _require_dict(root, "root")
61
+ raw_nodes = _require_list(root.get("nodes", []), "root.nodes")
62
+ return [_parse_node(item, f"root.nodes[{i}]") for i, item in enumerate(raw_nodes)]
63
+
64
+
65
+ def parse_nodes_file(path: Union[str, Path]) -> List[ParsedNode]:
66
+ p = Path(path)
67
+ if not p.is_file():
68
+ raise MapParseError(f"Not a file: {p}")
69
+ try:
70
+ text = p.read_text(encoding="utf-8")
71
+ except OSError as e:
72
+ raise MapParseError(f"Cannot read {p}: {e}") from e
73
+ try:
74
+ payload = json.loads(text)
75
+ except json.JSONDecodeError as e:
76
+ raise MapParseError(f"Invalid JSON in {p}: {e}") from e
77
+ return parse_nodes_dict(payload)
@@ -23,9 +23,11 @@ from .object_collision import (
23
23
  check_collision,
24
24
  )
25
25
  from .renderer import LayerRenderStats, TileLayerRenderer
26
+ from .area_node import AreaNode
26
27
 
27
28
  __all__ = [
28
29
  "AnimationPlayer",
30
+ "AreaNode",
29
31
  "CollisionCache",
30
32
  "CollisionHit",
31
33
  "CollisionResult",
@@ -0,0 +1,38 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Optional, Tuple
4
+
5
+ from pygame import Rect
6
+
7
+ from ..parser.node_parse import ParsedNode
8
+
9
+
10
+ class AreaNode:
11
+ def __init__(self, parsed: ParsedNode) -> None:
12
+ self.node_id = parsed.node_id
13
+ self.name = parsed.name
14
+ self.node_type = parsed.node_type
15
+ self._rect = Rect(parsed.area.x, parsed.area.y, parsed.area.w, parsed.area.h)
16
+ self.layer_name = parsed.layer_name
17
+ self.properties = dict(parsed.properties)
18
+ self.group = parsed.group
19
+
20
+ @property
21
+ def rect(self) -> Rect:
22
+ return self._rect
23
+
24
+ @rect.setter
25
+ def rect(self, r: Rect) -> None:
26
+ self._rect = r
27
+
28
+ def contains_point(self, point: Tuple[float, float]) -> bool:
29
+ return self._rect.collidepoint(point)
30
+
31
+ def overlaps_rect(self, other: Rect) -> bool:
32
+ return self._rect.colliderect(other)
33
+
34
+ def __repr__(self) -> str:
35
+ return (
36
+ f"AreaNode(id={self.node_id!r}, name={self.name!r}, "
37
+ f"rect={self._rect}, layer={self.layer_name!r})"
38
+ )
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import json
3
4
  from copy import deepcopy
4
5
  from pathlib import Path
5
6
  from typing import Dict, List, Optional, Tuple, Union
@@ -8,6 +9,7 @@ import pygame
8
9
  from pygame import Rect, Surface
9
10
 
10
11
  from ..parser.map_parse import MapParseError, ParsedLayer, ParsedMap, ParsedTile, parse_map_file
12
+ from ..parser.node_parse import parse_nodes_dict
11
13
 
12
14
  PathLike = Union[str, Path]
13
15
 
@@ -66,6 +68,27 @@ class TilemapData:
66
68
  raise MapParseError(msg) from e
67
69
  surfaces.append(None)
68
70
 
71
+ nodes_name = f"{p.stem}.nodes.json"
72
+ nodes_candidates = [
73
+ map_dir / nodes_name,
74
+ map_dir.parent / "nodes" / nodes_name,
75
+ ]
76
+ if extra_search_base is not None:
77
+ nodes_candidates.append(extra_search_base / "nodes" / nodes_name)
78
+ for nodes_path in nodes_candidates:
79
+ if nodes_path.is_file():
80
+ try:
81
+ nodes_text = nodes_path.read_text(encoding="utf-8")
82
+ nodes_raw = json.loads(nodes_text)
83
+ parsed.nodes = parse_nodes_dict(nodes_raw)
84
+ groups_raw = nodes_raw.get("groups", [])
85
+ if not isinstance(groups_raw, list):
86
+ raise MapParseError("root.groups must be a list")
87
+ parsed.node_groups = groups_raw
88
+ except (json.JSONDecodeError, OSError, MapParseError) as e:
89
+ warnings.append(f"Failed to load nodes: {e}")
90
+ break
91
+
69
92
  return cls(parsed, surfaces, resolved_paths, warnings, map_path=p)
70
93
 
71
94
  def _build_path_index(self) -> None:
@@ -175,6 +198,19 @@ class TilemapData:
175
198
  return None
176
199
  return self.get_tile_surface(tile.ttype, tile.variant)
177
200
 
201
+ def get_tileset_animation(self, ttype: int) -> Optional[dict]:
202
+ if 0 <= ttype < len(self.parsed.tilesets):
203
+ anim = self.parsed.tilesets[ttype].animation
204
+ if anim is not None:
205
+ return {
206
+ "frame_count": anim.frame_count,
207
+ "frame_duration_ms": anim.frame_duration_ms,
208
+ "frame_stride": anim.frame_stride,
209
+ "loop": anim.loop,
210
+ "animation_mode": anim.animation_mode,
211
+ }
212
+ return None
213
+
178
214
 
179
215
  def _variant_surface(
180
216
  surf: Surface,
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  from dataclasses import dataclass
4
4
  from typing import Dict, Optional, Tuple, Union
5
5
 
6
+ import pygame
6
7
  from pygame import Rect, Surface, transform
7
8
 
8
9
  from .map_loader import TilemapData
@@ -40,6 +41,17 @@ class TileLayerRenderer:
40
41
  f"got tile_size=({self._tile_w}, {self._tile_h}) render_scale={self._rs}"
41
42
  )
42
43
 
44
+ self._tileset_animations: Dict[int, dict] = {}
45
+ for ts_idx, ts in enumerate(data.parsed.tilesets):
46
+ if ts.animation is not None:
47
+ self._tileset_animations[ts_idx] = {
48
+ "frame_count": ts.animation.frame_count,
49
+ "frame_duration_ms": ts.animation.frame_duration_ms,
50
+ "frame_stride": ts.animation.frame_stride,
51
+ "loop": ts.animation.loop,
52
+ "animation_mode": ts.animation.animation_mode,
53
+ }
54
+
43
55
  def get_layer_dict(self) -> Dict[int, object]:
44
56
  return dict(self.tile_layers)
45
57
 
@@ -62,11 +74,32 @@ class TileLayerRenderer:
62
74
  self._get_cached_variant(tile.ttype, tile.variant)
63
75
  self.data = None
64
76
 
77
+ def _compute_display_variant(
78
+ self,
79
+ variant: int,
80
+ ttype: int,
81
+ x: int,
82
+ y: int,
83
+ time_ms: int,
84
+ ) -> int:
85
+ anim = self._tileset_animations.get(ttype)
86
+ if anim is None:
87
+ return variant
88
+ stride = anim["frame_stride"]
89
+ frame_count = anim["frame_count"]
90
+ frame_idx = int(time_ms / anim["frame_duration_ms"]) % frame_count
91
+ if anim.get("animation_mode") == "random_start_times":
92
+ phase = hash((x, y, ttype)) % frame_count
93
+ frame_idx = (frame_idx + phase) % frame_count
94
+ return variant + frame_idx * stride
95
+
65
96
  def render(
66
97
  self,
67
98
  target: Surface,
68
99
  camera_xy: Union[Tuple[float, float], Tuple[int, int]] = (0, 0),
69
100
  viewport_size: Optional[Tuple[int, int]] = None,
101
+ *,
102
+ current_time_ms: Optional[float] = None,
70
103
  ) -> LayerRenderStats:
71
104
  cam_x, cam_y = float(camera_xy[0]), float(camera_xy[1])
72
105
  if viewport_size is None:
@@ -79,6 +112,10 @@ class TileLayerRenderer:
79
112
  min_y = int(cam_y // self._eff_h) - 1
80
113
  max_y = int((cam_y + viewport.height) // self._eff_h) + 1
81
114
 
115
+ if current_time_ms is None:
116
+ current_time_ms = pygame.time.get_ticks()
117
+ time_ms = int(current_time_ms)
118
+
82
119
  drawn = 0
83
120
  skipped = 0
84
121
  visible_layers = 0
@@ -95,7 +132,10 @@ class TileLayerRenderer:
95
132
  if not isinstance(tile.ttype, int):
96
133
  skipped += 1
97
134
  continue
98
- cell = self._get_cached_variant(tile.ttype, tile.variant)
135
+ display_variant = self._compute_display_variant(
136
+ tile.variant, tile.ttype, x, y, time_ms
137
+ )
138
+ cell = self._get_cached_variant(tile.ttype, display_variant)
99
139
  if cell is None:
100
140
  skipped += 1
101
141
  continue
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tilemap-parser
3
- Version: 3.1.7
3
+ Version: 3.1.9
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
@@ -12,8 +12,10 @@ src/tilemap_parser/parser/animation.py
12
12
  src/tilemap_parser/parser/collision.py
13
13
  src/tilemap_parser/parser/collision_loader.py
14
14
  src/tilemap_parser/parser/map_parse.py
15
+ src/tilemap_parser/parser/node_parse.py
15
16
  src/tilemap_parser/runtime/__init__.py
16
17
  src/tilemap_parser/runtime/animation_player.py
18
+ src/tilemap_parser/runtime/area_node.py
17
19
  src/tilemap_parser/runtime/collision_cache.py
18
20
  src/tilemap_parser/runtime/map_loader.py
19
21
  src/tilemap_parser/runtime/object_collision.py
File without changes
File without changes
File without changes