tilemap-parser 3.1.8__tar.gz → 3.1.10__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 (34) hide show
  1. {tilemap_parser-3.1.8 → tilemap_parser-3.1.10}/PKG-INFO +1 -1
  2. {tilemap_parser-3.1.8 → tilemap_parser-3.1.10}/pyproject.toml +1 -1
  3. {tilemap_parser-3.1.8 → tilemap_parser-3.1.10}/src/tilemap_parser/parser/__init__.py +2 -0
  4. {tilemap_parser-3.1.8 → tilemap_parser-3.1.10}/src/tilemap_parser/parser/map_parse.py +23 -1
  5. {tilemap_parser-3.1.8 → tilemap_parser-3.1.10}/src/tilemap_parser/runtime/map_loader.py +13 -0
  6. tilemap_parser-3.1.10/src/tilemap_parser/runtime/renderer.py +192 -0
  7. {tilemap_parser-3.1.8 → tilemap_parser-3.1.10}/src/tilemap_parser.egg-info/PKG-INFO +1 -1
  8. tilemap_parser-3.1.8/src/tilemap_parser/runtime/renderer.py +0 -107
  9. {tilemap_parser-3.1.8 → tilemap_parser-3.1.10}/LICENSE +0 -0
  10. {tilemap_parser-3.1.8 → tilemap_parser-3.1.10}/README.md +0 -0
  11. {tilemap_parser-3.1.8 → tilemap_parser-3.1.10}/setup.cfg +0 -0
  12. {tilemap_parser-3.1.8 → tilemap_parser-3.1.10}/src/tilemap_parser/__init__.py +0 -0
  13. {tilemap_parser-3.1.8 → tilemap_parser-3.1.10}/src/tilemap_parser/parser/animation.py +0 -0
  14. {tilemap_parser-3.1.8 → tilemap_parser-3.1.10}/src/tilemap_parser/parser/collision.py +0 -0
  15. {tilemap_parser-3.1.8 → tilemap_parser-3.1.10}/src/tilemap_parser/parser/collision_loader.py +0 -0
  16. {tilemap_parser-3.1.8 → tilemap_parser-3.1.10}/src/tilemap_parser/parser/node_parse.py +0 -0
  17. {tilemap_parser-3.1.8 → tilemap_parser-3.1.10}/src/tilemap_parser/runtime/__init__.py +0 -0
  18. {tilemap_parser-3.1.8 → tilemap_parser-3.1.10}/src/tilemap_parser/runtime/animation_player.py +0 -0
  19. {tilemap_parser-3.1.8 → tilemap_parser-3.1.10}/src/tilemap_parser/runtime/area_node.py +0 -0
  20. {tilemap_parser-3.1.8 → tilemap_parser-3.1.10}/src/tilemap_parser/runtime/collision_cache.py +0 -0
  21. {tilemap_parser-3.1.8 → tilemap_parser-3.1.10}/src/tilemap_parser/runtime/object_collision.py +0 -0
  22. {tilemap_parser-3.1.8 → tilemap_parser-3.1.10}/src/tilemap_parser/runtime/tile_collision.py +0 -0
  23. {tilemap_parser-3.1.8 → tilemap_parser-3.1.10}/src/tilemap_parser/utils/__init__.py +0 -0
  24. {tilemap_parser-3.1.8 → tilemap_parser-3.1.10}/src/tilemap_parser/utils/geometry.py +0 -0
  25. {tilemap_parser-3.1.8 → tilemap_parser-3.1.10}/src/tilemap_parser.egg-info/SOURCES.txt +0 -0
  26. {tilemap_parser-3.1.8 → tilemap_parser-3.1.10}/src/tilemap_parser.egg-info/dependency_links.txt +0 -0
  27. {tilemap_parser-3.1.8 → tilemap_parser-3.1.10}/src/tilemap_parser.egg-info/requires.txt +0 -0
  28. {tilemap_parser-3.1.8 → tilemap_parser-3.1.10}/src/tilemap_parser.egg-info/top_level.txt +0 -0
  29. {tilemap_parser-3.1.8 → tilemap_parser-3.1.10}/tests/test_collision.py +0 -0
  30. {tilemap_parser-3.1.8 → tilemap_parser-3.1.10}/tests/test_geometry.py +0 -0
  31. {tilemap_parser-3.1.8 → tilemap_parser-3.1.10}/tests/test_map_loader.py +0 -0
  32. {tilemap_parser-3.1.8 → tilemap_parser-3.1.10}/tests/test_object_collision.py +0 -0
  33. {tilemap_parser-3.1.8 → tilemap_parser-3.1.10}/tests/test_render_scale.py +0 -0
  34. {tilemap_parser-3.1.8 → tilemap_parser-3.1.10}/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.8
3
+ Version: 3.1.10
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.8"
7
+ version = "3.1.10"
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"
@@ -40,6 +40,7 @@ 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,
@@ -72,6 +73,7 @@ __all__ = [
72
73
  "ParsedTile",
73
74
  "ParsedTileset",
74
75
  "RectangleShape",
76
+ "TilesetAnimation",
75
77
  "TileCollisionData",
76
78
  "TilesetCollision",
77
79
  "parse_animation_dict",
@@ -125,12 +125,22 @@ class ParsedObject:
125
125
  properties: Optional[JsonDict] = None
126
126
 
127
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
+
128
137
  @dataclass
129
138
  class ParsedTileset:
130
139
  path: str
131
140
  type: str
132
141
  properties: JsonDict = field(default_factory=dict)
133
142
  tile_properties: Dict[str, JsonDict] = field(default_factory=dict)
143
+ animation: Optional[TilesetAnimation] = None
134
144
 
135
145
 
136
146
  @dataclass
@@ -340,9 +350,21 @@ def _parse_tilesets_list(tilesets_raw: List[Any], ctx: str) -> List[ParsedTilese
340
350
  tile_props[str(k)] = _require_dict(
341
351
  v, f"{ctx}[{i}].tile_properties[{k!r}]"
342
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
+ )
343
364
  out.append(
344
365
  ParsedTileset(
345
- 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,
346
368
  )
347
369
  )
348
370
  return out
@@ -198,6 +198,19 @@ class TilemapData:
198
198
  return None
199
199
  return self.get_tile_surface(tile.ttype, tile.variant)
200
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
+
201
214
 
202
215
  def _variant_surface(
203
216
  surf: Surface,
@@ -0,0 +1,192 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Dict, List, Optional, Tuple, Union
5
+
6
+ import pygame
7
+ from pygame import Rect, Surface, transform
8
+
9
+ from .map_loader import TilemapData
10
+
11
+ CHUNK_SIZE = 32
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class LayerRenderStats:
16
+ drawn_tiles: int
17
+ skipped_tiles: int
18
+ visible_layers: int
19
+
20
+
21
+ class TileLayerRenderer:
22
+ def __init__(
23
+ self, data: TilemapData, *, include_hidden_layers: bool = False
24
+ ) -> None:
25
+ self.data = data
26
+ self.tile_layers = data.get_tile_layers_dict(
27
+ include_hidden=include_hidden_layers
28
+ )
29
+ self._sorted_layer_ids = sorted(
30
+ self.tile_layers.keys(),
31
+ key=lambda lid: (self.tile_layers[lid].z_index, lid),
32
+ )
33
+
34
+ self._variant_cache: Dict[Tuple[int, int], Optional[Surface]] = {}
35
+
36
+ self._tile_w, self._tile_h = data.tile_size
37
+ self._rs = data.render_scale
38
+ if self._rs <= 0:
39
+ raise ValueError(f"render_scale must be positive, got {self._rs}")
40
+ self._eff_w = int(self._tile_w * self._rs)
41
+ self._eff_h = int(self._tile_h * self._rs)
42
+ if self._eff_w <= 0 or self._eff_h <= 0:
43
+ raise ValueError(
44
+ f"effective tile size ({self._eff_w}, {self._eff_h}) must be positive; "
45
+ f"got tile_size=({self._tile_w}, {self._tile_h}) render_scale={self._rs}"
46
+ )
47
+
48
+ self._tileset_animations: Dict[int, dict] = {}
49
+ for ts_idx, ts in enumerate(data.parsed.tilesets):
50
+ if ts.animation is not None:
51
+ self._tileset_animations[ts_idx] = {
52
+ "frame_count": ts.animation.frame_count,
53
+ "frame_duration_ms": ts.animation.frame_duration_ms,
54
+ "frame_stride": ts.animation.frame_stride,
55
+ "loop": ts.animation.loop,
56
+ "animation_mode": ts.animation.animation_mode,
57
+ }
58
+
59
+ self._layer_chunks: Dict[int, Dict[Tuple[int, int], List[Tuple[int, int]]]] = {}
60
+ for layer_id, layer in self.tile_layers.items():
61
+ chunks: Dict[Tuple[int, int], List[Tuple[int, int]]] = {}
62
+ for (x, y), tile in layer.tiles.items():
63
+ if not isinstance(tile.ttype, int):
64
+ continue
65
+ cx, cy = x // CHUNK_SIZE, y // CHUNK_SIZE
66
+ chunk_key = (cx, cy)
67
+ if chunk_key not in chunks:
68
+ chunks[chunk_key] = []
69
+ chunks[chunk_key].append((x, y))
70
+ self._layer_chunks[layer_id] = chunks
71
+
72
+ def get_layer_dict(self) -> Dict[int, object]:
73
+ return dict(self.tile_layers)
74
+
75
+ def _get_cached_variant(self, ttype: int, variant: int) -> Optional[Surface]:
76
+ key = (ttype, variant)
77
+ if key not in self._variant_cache:
78
+ cell = self.data.get_tile_surface(ttype, variant, copy_surface=True)
79
+ if cell is not None and self._rs != 1.0:
80
+ cell = transform.scale(cell, (self._eff_w, self._eff_h))
81
+ self._variant_cache[key] = cell
82
+ return self._variant_cache[key]
83
+
84
+ def warm_cache(self) -> None:
85
+ for layer_id in self._sorted_layer_ids:
86
+ layer = self.tile_layers[layer_id]
87
+ for tile in layer.tiles.values():
88
+ if not isinstance(tile.ttype, int):
89
+ continue
90
+ anim = self._tileset_animations.get(tile.ttype)
91
+ if anim is not None:
92
+ frame_count = anim["frame_count"]
93
+ stride = anim["frame_stride"]
94
+ for f in range(frame_count):
95
+ self._get_cached_variant(tile.ttype, tile.variant + f * stride)
96
+ else:
97
+ self._get_cached_variant(tile.ttype, tile.variant)
98
+ self.data = None
99
+
100
+ def _compute_display_variant(
101
+ self,
102
+ variant: int,
103
+ ttype: int,
104
+ x: int,
105
+ y: int,
106
+ time_ms: int,
107
+ ) -> int:
108
+ anim = self._tileset_animations.get(ttype)
109
+ if anim is None:
110
+ return variant
111
+
112
+ frame_count = anim["frame_count"]
113
+ frame_idx = (time_ms // anim["frame_duration_ms"]) % frame_count
114
+
115
+ if anim.get("animation_mode") == "random_start_times":
116
+ phase = ((x * 73856093) ^ (y * 19349663) ^ (ttype * 83492791)) % frame_count
117
+ frame_idx = (frame_idx + phase) % frame_count
118
+
119
+ return variant + frame_idx * anim["frame_stride"]
120
+
121
+ def render(
122
+ self,
123
+ target: Surface,
124
+ camera_xy: Union[Tuple[float, float], Tuple[int, int]] = (0, 0),
125
+ viewport_size: Optional[Tuple[int, int]] = None,
126
+ *,
127
+ current_time_ms: Optional[float] = None,
128
+ ) -> LayerRenderStats:
129
+ """Render visible tile layers.
130
+
131
+ Note: ``skipped_tiles`` is a debugging indicator, not a whole-map
132
+ accumulated count. It tracks null surfaces and invalid tiles within
133
+ the active chunk range — enough to spot rendering issues without
134
+ the cost of a full scan.
135
+ """
136
+ cam_x, cam_y = float(camera_xy[0]), float(camera_xy[1])
137
+ if viewport_size is None:
138
+ viewport = target.get_rect()
139
+ else:
140
+ viewport = Rect(0, 0, viewport_size[0], viewport_size[1])
141
+
142
+ min_x = int(cam_x // self._eff_w) - 1
143
+ max_x = int((cam_x + viewport.width) // self._eff_w) + 1
144
+ min_y = int(cam_y // self._eff_h) - 1
145
+ max_y = int((cam_y + viewport.height) // self._eff_h) + 1
146
+
147
+ if current_time_ms is None:
148
+ current_time_ms = pygame.time.get_ticks()
149
+ time_ms = int(current_time_ms)
150
+
151
+ drawn = 0
152
+ skipped = 0
153
+ visible_layers = 0
154
+
155
+ min_cx = min_x // CHUNK_SIZE
156
+ max_cx = max_x // CHUNK_SIZE
157
+ min_cy = min_y // CHUNK_SIZE
158
+ max_cy = max_y // CHUNK_SIZE
159
+
160
+ for layer_id in self._sorted_layer_ids:
161
+ layer = self.tile_layers[layer_id]
162
+ if not layer.visible:
163
+ continue
164
+ visible_layers += 1
165
+
166
+ chunks = self._layer_chunks[layer_id]
167
+
168
+ for cx in range(min_cx, max_cx + 1):
169
+ for cy in range(min_cy, max_cy + 1):
170
+ chunk = chunks.get((cx, cy))
171
+ if not chunk:
172
+ continue
173
+
174
+ for x, y in chunk:
175
+ tile = layer.tiles[(x, y)]
176
+ display_variant = self._compute_display_variant(
177
+ tile.variant, tile.ttype, x, y, time_ms
178
+ )
179
+ cell = self._get_cached_variant(tile.ttype, display_variant)
180
+ if cell is None:
181
+ skipped += 1
182
+ continue
183
+
184
+ target.blit(
185
+ cell,
186
+ (x * self._eff_w - cam_x, y * self._eff_h - cam_y),
187
+ )
188
+ drawn += 1
189
+
190
+ return LayerRenderStats(
191
+ drawn_tiles=drawn, skipped_tiles=skipped, visible_layers=visible_layers
192
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tilemap-parser
3
- Version: 3.1.8
3
+ Version: 3.1.10
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
@@ -1,107 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from dataclasses import dataclass
4
- from typing import Dict, Optional, Tuple, Union
5
-
6
- from pygame import Rect, Surface, transform
7
-
8
- from .map_loader import TilemapData
9
-
10
-
11
- @dataclass(frozen=True)
12
- class LayerRenderStats:
13
- drawn_tiles: int
14
- skipped_tiles: int
15
- visible_layers: int
16
-
17
-
18
- class TileLayerRenderer:
19
- def __init__(
20
- self, data: TilemapData, *, include_hidden_layers: bool = False
21
- ) -> None:
22
- self.data = data
23
- self.tile_layers = data.get_tile_layers_dict(
24
- include_hidden=include_hidden_layers
25
- )
26
- self._sorted_layer_ids = sorted(
27
- self.tile_layers.keys(),
28
- key=lambda lid: (self.tile_layers[lid].z_index, lid),
29
- )
30
- self._variant_cache: Dict[Tuple[int, int], Optional[Surface]] = {}
31
- self._tile_w, self._tile_h = data.tile_size
32
- self._rs = data.render_scale
33
- if self._rs <= 0:
34
- raise ValueError(f"render_scale must be positive, got {self._rs}")
35
- self._eff_w = int(self._tile_w * self._rs)
36
- self._eff_h = int(self._tile_h * self._rs)
37
- if self._eff_w <= 0 or self._eff_h <= 0:
38
- raise ValueError(
39
- f"effective tile size ({self._eff_w}, {self._eff_h}) must be positive; "
40
- f"got tile_size=({self._tile_w}, {self._tile_h}) render_scale={self._rs}"
41
- )
42
-
43
- def get_layer_dict(self) -> Dict[int, object]:
44
- return dict(self.tile_layers)
45
-
46
- def _get_cached_variant(self, ttype: int, variant: int) -> Optional[Surface]:
47
- key = (ttype, variant)
48
- if key not in self._variant_cache:
49
- cell = self.data.get_tile_surface(
50
- ttype, variant, copy_surface=True
51
- )
52
- if cell is not None and self._rs != 1.0:
53
- cell = transform.scale(cell, (self._eff_w, self._eff_h))
54
- self._variant_cache[key] = cell
55
- return self._variant_cache[key]
56
-
57
- def warm_cache(self) -> None:
58
- for layer_id in self._sorted_layer_ids:
59
- layer = self.tile_layers[layer_id]
60
- for tile in layer.tiles.values():
61
- if isinstance(tile.ttype, int):
62
- self._get_cached_variant(tile.ttype, tile.variant)
63
- self.data = None
64
-
65
- def render(
66
- self,
67
- target: Surface,
68
- camera_xy: Union[Tuple[float, float], Tuple[int, int]] = (0, 0),
69
- viewport_size: Optional[Tuple[int, int]] = None,
70
- ) -> LayerRenderStats:
71
- cam_x, cam_y = float(camera_xy[0]), float(camera_xy[1])
72
- if viewport_size is None:
73
- viewport = target.get_rect()
74
- else:
75
- viewport = Rect(0, 0, viewport_size[0], viewport_size[1])
76
-
77
- min_x = int(cam_x // self._eff_w) - 1
78
- max_x = int((cam_x + viewport.width) // self._eff_w) + 1
79
- min_y = int(cam_y // self._eff_h) - 1
80
- max_y = int((cam_y + viewport.height) // self._eff_h) + 1
81
-
82
- drawn = 0
83
- skipped = 0
84
- visible_layers = 0
85
-
86
- for layer_id in self._sorted_layer_ids:
87
- layer = self.tile_layers[layer_id]
88
- if not layer.visible:
89
- continue
90
- visible_layers += 1
91
- for (x, y), tile in layer.tiles.items():
92
- if x < min_x or x > max_x or y < min_y or y > max_y:
93
- skipped += 1
94
- continue
95
- if not isinstance(tile.ttype, int):
96
- skipped += 1
97
- continue
98
- cell = self._get_cached_variant(tile.ttype, tile.variant)
99
- if cell is None:
100
- skipped += 1
101
- continue
102
- target.blit(cell, (x * self._eff_w - cam_x, y * self._eff_h - cam_y))
103
- drawn += 1
104
-
105
- return LayerRenderStats(
106
- drawn_tiles=drawn, skipped_tiles=skipped, visible_layers=visible_layers
107
- )
File without changes