tilemap-parser 1.0.0__py3-none-any.whl

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.
@@ -0,0 +1,402 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import re
5
+ import string
6
+ from dataclasses import dataclass, field
7
+ from pathlib import Path
8
+ from typing import Any, Dict, List, Optional, Tuple, Union
9
+
10
+ JsonDict = Dict[str, Any]
11
+ Point = Tuple[int, int]
12
+ TilesetRef = Union[int, str]
13
+
14
+
15
+ class MapParseError(ValueError):
16
+ pass
17
+
18
+
19
+ def _ctx(path: str, detail: str) -> str:
20
+ return f"{path}: {detail}"
21
+
22
+
23
+ def _require_dict(value: Any, path: str) -> JsonDict:
24
+ if not isinstance(value, dict):
25
+ raise MapParseError(_ctx(path, "expected object"))
26
+ return value
27
+
28
+
29
+ def _require_list(value: Any, path: str) -> List[Any]:
30
+ if not isinstance(value, list):
31
+ raise MapParseError(_ctx(path, "expected array"))
32
+ return value
33
+
34
+
35
+ def _require_str(value: Any, path: str) -> str:
36
+ if not isinstance(value, str):
37
+ raise MapParseError(_ctx(path, "expected string"))
38
+ return value
39
+
40
+
41
+ def _coerce_int(value: Any, path: str) -> int:
42
+ if isinstance(value, bool):
43
+ raise MapParseError(_ctx(path, "expected int (got bool)"))
44
+ if isinstance(value, int):
45
+ return value
46
+ if isinstance(value, str):
47
+ try:
48
+ return int(value, 10)
49
+ except ValueError as e:
50
+ raise MapParseError(_ctx(path, "expected int")) from e
51
+ raise MapParseError(_ctx(path, "expected int"))
52
+
53
+
54
+ def _coerce_float(value: Any, path: str) -> float:
55
+ if isinstance(value, bool):
56
+ raise MapParseError(_ctx(path, "expected number (got bool)"))
57
+ if isinstance(value, (int, float)):
58
+ return float(value)
59
+ if isinstance(value, str):
60
+ try:
61
+ return float(value)
62
+ except ValueError as e:
63
+ raise MapParseError(_ctx(path, "expected number")) from e
64
+ raise MapParseError(_ctx(path, "expected number"))
65
+
66
+
67
+ def _coerce_bool(value: Any, path: str) -> bool:
68
+ if isinstance(value, bool):
69
+ return value
70
+ if isinstance(value, (int, float)) and value in (0, 1):
71
+ return bool(value)
72
+ raise MapParseError(_ctx(path, "expected bool"))
73
+
74
+
75
+ def _optional_dict(value: Any, path: str) -> Optional[JsonDict]:
76
+ if value is None:
77
+ return None
78
+ return _require_dict(value, path)
79
+
80
+
81
+ def _parse_point(text: str, path: str) -> Point:
82
+ if not isinstance(text, str):
83
+ raise MapParseError(_ctx(path, "expected point string"))
84
+ matched = re.search(rf"(-?\d+)[{re.escape(string.punctuation)}](-?\d+)$", text.strip())
85
+ if matched is None:
86
+ raise MapParseError(_ctx(path, f"invalid point {text!r}"))
87
+ return int(matched.group(1)), int(matched.group(2))
88
+
89
+
90
+ def _parse_point_field(raw: Any, path: str, default: Optional[str] = None) -> Point:
91
+ if raw is None and default is not None:
92
+ return _parse_point(default, path)
93
+ if not isinstance(raw, str):
94
+ raise MapParseError(_ctx(path, "expected serialized point string"))
95
+ return _parse_point(raw, path)
96
+
97
+
98
+ @dataclass
99
+ class ParsedTile:
100
+ pos: Point
101
+ ttype: TilesetRef
102
+ variant: int
103
+ properties: Optional[JsonDict] = None
104
+
105
+
106
+ @dataclass
107
+ class ParsedObjectArea:
108
+ x: int
109
+ y: int
110
+ w: int
111
+ h: int
112
+
113
+
114
+ @dataclass
115
+ class ParsedObject:
116
+ area: ParsedObjectArea
117
+ ttype: int
118
+ tileset_type: str
119
+ variant: int
120
+ properties: Optional[JsonDict] = None
121
+
122
+
123
+ @dataclass
124
+ class ParsedTileset:
125
+ path: str
126
+ type: str
127
+ properties: JsonDict = field(default_factory=dict)
128
+ tile_properties: Dict[str, JsonDict] = field(default_factory=dict)
129
+
130
+
131
+ @dataclass
132
+ class ParsedLayer:
133
+ id: int
134
+ name: str
135
+ layer_type: str
136
+ visible: bool
137
+ locked: bool
138
+ opacity: float
139
+ z_index: int
140
+ properties: JsonDict = field(default_factory=dict)
141
+ tiles: Dict[Point, ParsedTile] = field(default_factory=dict)
142
+ objects: Dict[int, ParsedObject] = field(default_factory=dict)
143
+ next_object_id: Optional[int] = None
144
+
145
+
146
+ @dataclass
147
+ class ParsedAutotileRule:
148
+ name: str
149
+ neighbors: List[Point]
150
+ tileset_path: str
151
+ tileset_index: Optional[int]
152
+ variant_ids: List[int]
153
+ group_id: Optional[Any] = None
154
+ raw: JsonDict = field(default_factory=dict)
155
+
156
+
157
+ @dataclass
158
+ class ParsedAutotileGroup:
159
+ name: str
160
+ rules: List[ParsedAutotileRule]
161
+
162
+
163
+ @dataclass
164
+ class ParsedMeta:
165
+ tile_size: Point
166
+ map_size: Point
167
+ initial_map_size: Point
168
+ zoom_level: float
169
+ scroll: Point
170
+ version: str
171
+
172
+
173
+ @dataclass
174
+ class ParsedProjectState:
175
+ rules: List[ParsedAutotileRule]
176
+ groups: List[ParsedAutotileGroup]
177
+ automap_rules: Any
178
+
179
+
180
+ @dataclass
181
+ class ParsedMap:
182
+ meta: ParsedMeta
183
+ layers: List[ParsedLayer]
184
+ tilesets: List[ParsedTileset]
185
+ project_state: ParsedProjectState
186
+ raw: JsonDict
187
+
188
+
189
+ def _parse_tile(tile_data: JsonDict, ctx: str) -> ParsedTile:
190
+ pos = _parse_point(_require_str(tile_data.get("pos"), f"{ctx}.pos"), f"{ctx}.pos")
191
+ variant = _coerce_int(tile_data.get("variant"), f"{ctx}.variant")
192
+ ttype_raw = tile_data.get("ttype", 0)
193
+ if isinstance(ttype_raw, str):
194
+ ttype: TilesetRef = ttype_raw
195
+ else:
196
+ ttype = _coerce_int(ttype_raw, f"{ctx}.ttype")
197
+ props = _optional_dict(tile_data.get("properties"), f"{ctx}.properties")
198
+ return ParsedTile(pos=pos, ttype=ttype, variant=variant, properties=props)
199
+
200
+
201
+ def _parse_tiles(tiles_obj: JsonDict, ctx: str) -> Dict[Point, ParsedTile]:
202
+ result: Dict[Point, ParsedTile] = {}
203
+ for key, value in tiles_obj.items():
204
+ tile_dict = _require_dict(value, f"{ctx}[{key!r}]")
205
+ tile = _parse_tile(tile_dict, f"{ctx}[{key!r}]")
206
+ result[tile.pos] = tile
207
+ return result
208
+
209
+
210
+ def _parse_object_area(area_obj: JsonDict, ctx: str) -> ParsedObjectArea:
211
+ return ParsedObjectArea(
212
+ x=_coerce_int(area_obj.get("x"), f"{ctx}.x"),
213
+ y=_coerce_int(area_obj.get("y"), f"{ctx}.y"),
214
+ w=_coerce_int(area_obj.get("w"), f"{ctx}.w"),
215
+ h=_coerce_int(area_obj.get("h"), f"{ctx}.h"),
216
+ )
217
+
218
+
219
+ def _parse_objects(objs_obj: JsonDict, ctx: str) -> Dict[int, ParsedObject]:
220
+ result: Dict[int, ParsedObject] = {}
221
+ for key, value in objs_obj.items():
222
+ oid = _coerce_int(key, f"{ctx}.<id>")
223
+ obj_dict = _require_dict(value, f"{ctx}.{key}")
224
+ area_dict = _require_dict(obj_dict.get("area"), f"{ctx}.{key}.area")
225
+ area = _parse_object_area(area_dict, f"{ctx}.{key}.area")
226
+ ttype = _coerce_int(obj_dict.get("ttype"), f"{ctx}.{key}.ttype")
227
+ tileset_type = _require_str(obj_dict.get("tileset_type", "object"), f"{ctx}.{key}.tileset_type")
228
+ variant = _coerce_int(obj_dict.get("variant"), f"{ctx}.{key}.variant")
229
+ props = _optional_dict(obj_dict.get("properties"), f"{ctx}.{key}.properties")
230
+ result[oid] = ParsedObject(
231
+ area=area,
232
+ ttype=ttype,
233
+ tileset_type=tileset_type,
234
+ variant=variant,
235
+ properties=props,
236
+ )
237
+ return result
238
+
239
+
240
+ def _parse_layer(layer_obj: JsonDict, layer_id: int, ctx: str) -> ParsedLayer:
241
+ layer = ParsedLayer(
242
+ id=layer_id,
243
+ name=_require_str(layer_obj.get("name"), f"{ctx}.name"),
244
+ layer_type=_require_str(layer_obj.get("type"), f"{ctx}.type"),
245
+ visible=_coerce_bool(layer_obj.get("visible", True), f"{ctx}.visible"),
246
+ locked=_coerce_bool(layer_obj.get("locked", False), f"{ctx}.locked"),
247
+ opacity=_coerce_float(layer_obj.get("opacity", 1.0), f"{ctx}.opacity"),
248
+ z_index=_coerce_int(layer_obj.get("z_index", layer_id), f"{ctx}.z_index"),
249
+ properties=_optional_dict(layer_obj.get("properties"), f"{ctx}.properties") or {},
250
+ )
251
+ layer.tiles = _parse_tiles(_require_dict(layer_obj.get("tiles", {}), f"{ctx}.tiles"), f"{ctx}.tiles")
252
+
253
+ if layer.layer_type == "object":
254
+ layer.objects = _parse_objects(_require_dict(layer_obj.get("objects", {}), f"{ctx}.objects"), f"{ctx}.objects")
255
+ if "next_object_id" in layer_obj and layer_obj["next_object_id"] is not None:
256
+ layer.next_object_id = _coerce_int(layer_obj["next_object_id"], f"{ctx}.next_object_id")
257
+ return layer
258
+
259
+
260
+ def _parse_rule(rule_obj: JsonDict, ctx: str) -> ParsedAutotileRule:
261
+ neighbors_raw = _require_list(rule_obj.get("neighbors", []), f"{ctx}.neighbors")
262
+ neighbors: List[Point] = []
263
+ for idx, pair in enumerate(neighbors_raw):
264
+ pair_list = _require_list(pair, f"{ctx}.neighbors[{idx}]")
265
+ if len(pair_list) != 2:
266
+ raise MapParseError(_ctx(f"{ctx}.neighbors[{idx}]", "expected [x, y]"))
267
+ neighbors.append((_coerce_int(pair_list[0], f"{ctx}.neighbors[{idx}][0]"), _coerce_int(pair_list[1], f"{ctx}.neighbors[{idx}][1]")))
268
+
269
+ variants_raw = _require_list(rule_obj.get("variant_ids", []), f"{ctx}.variant_ids")
270
+ variant_ids = [_coerce_int(v, f"{ctx}.variant_ids[{i}]") for i, v in enumerate(variants_raw)]
271
+ tileset_path = _require_str(rule_obj.get("tileset_path", ""), f"{ctx}.tileset_path")
272
+ tileset_index_raw = rule_obj.get("tileset_index")
273
+ tileset_index = _coerce_int(tileset_index_raw, f"{ctx}.tileset_index") if tileset_index_raw is not None else None
274
+ return ParsedAutotileRule(
275
+ name=_require_str(rule_obj.get("name"), f"{ctx}.name"),
276
+ neighbors=neighbors,
277
+ tileset_path=tileset_path,
278
+ tileset_index=tileset_index,
279
+ variant_ids=variant_ids,
280
+ group_id=rule_obj.get("group_id"),
281
+ raw=dict(rule_obj),
282
+ )
283
+
284
+
285
+ def _parse_group(group_obj: JsonDict, ctx: str) -> ParsedAutotileGroup:
286
+ rules_raw = _require_list(group_obj.get("rules", []), f"{ctx}.rules")
287
+ rules = [_parse_rule(_require_dict(r, f"{ctx}.rules[{i}]"), f"{ctx}.rules[{i}]") for i, r in enumerate(rules_raw)]
288
+ return ParsedAutotileGroup(name=_require_str(group_obj.get("name"), f"{ctx}.name"), rules=rules)
289
+
290
+
291
+ def _parse_tilesets_list(tilesets_raw: List[Any], ctx: str) -> List[ParsedTileset]:
292
+ out: List[ParsedTileset] = []
293
+ for i, ts in enumerate(tilesets_raw):
294
+ if isinstance(ts, str):
295
+ out.append(ParsedTileset(path=ts, type="tile"))
296
+ continue
297
+ ts_obj = _require_dict(ts, f"{ctx}[{i}]")
298
+ path = _require_str(ts_obj.get("path"), f"{ctx}[{i}].path")
299
+ ts_type = _require_str(ts_obj.get("type", "tile"), f"{ctx}[{i}].type")
300
+ props = _optional_dict(ts_obj.get("properties"), f"{ctx}[{i}].properties") or {}
301
+ tile_props: Dict[str, JsonDict] = {}
302
+ raw_tile_props = ts_obj.get("tile_properties")
303
+ if raw_tile_props is not None:
304
+ tp_obj = _require_dict(raw_tile_props, f"{ctx}[{i}].tile_properties")
305
+ for k, v in tp_obj.items():
306
+ tile_props[str(k)] = _require_dict(v, f"{ctx}[{i}].tile_properties[{k!r}]")
307
+ out.append(ParsedTileset(path=path, type=ts_type, properties=props, tile_properties=tile_props))
308
+ return out
309
+
310
+
311
+ def _parse_resources(resources_raw: Any, ctx: str) -> List[ParsedTileset]:
312
+ if isinstance(resources_raw, list):
313
+ return _parse_tilesets_list(resources_raw, f"{ctx} (list form)")
314
+ resources_obj = _require_dict(resources_raw, ctx)
315
+ tilesets_raw = _require_list(resources_obj.get("tilesets", []), f"{ctx}.tilesets")
316
+ return _parse_tilesets_list(tilesets_raw, f"{ctx}.tilesets")
317
+
318
+
319
+ def _expand_ongrid_to_layer(data_obj: JsonDict, ctx: str) -> List[ParsedLayer]:
320
+ raw_ongrid = _require_dict(data_obj.get("ongrid", {}), f"{ctx}.ongrid")
321
+ layer = ParsedLayer(
322
+ id=0,
323
+ name="Terrain",
324
+ layer_type="tile",
325
+ visible=True,
326
+ locked=False,
327
+ opacity=1.0,
328
+ z_index=0,
329
+ properties={},
330
+ )
331
+ for loc_str, tile_data in raw_ongrid.items():
332
+ tile_dict = _require_dict(tile_data, f"{ctx}.ongrid[{loc_str!r}]")
333
+ if "pos" not in tile_dict:
334
+ tile_dict = {**tile_dict, "pos": str(loc_str)}
335
+ tile = _parse_tile(tile_dict, f"{ctx}.ongrid[{loc_str!r}]")
336
+ layer.tiles[tile.pos] = tile
337
+ return [layer]
338
+
339
+
340
+ def parse_map_dict(root: JsonDict) -> ParsedMap:
341
+ root = _require_dict(root, "root")
342
+ meta_obj = _require_dict(root.get("meta"), "meta")
343
+ tile_size = _parse_point_field(meta_obj.get("tile_size"), "meta.tile_size")
344
+ map_size = _parse_point_field(meta_obj.get("map_size"), "meta.map_size", default=f"{tile_size[0]};{tile_size[1]}")
345
+ init_raw = meta_obj.get("initial_map_size")
346
+ initial_map_size = map_size if init_raw is None else _parse_point_field(init_raw, "meta.initial_map_size")
347
+
348
+ meta = ParsedMeta(
349
+ tile_size=tile_size,
350
+ map_size=map_size,
351
+ initial_map_size=initial_map_size,
352
+ zoom_level=_coerce_float(meta_obj.get("zoom_level", 1.0), "meta.zoom_level"),
353
+ scroll=_parse_point_field(meta_obj.get("scroll"), "meta.scroll", default="0;0"),
354
+ version=_require_str(meta_obj.get("version", "1.1"), "meta.version"),
355
+ )
356
+
357
+ data_obj = _require_dict(root.get("data"), "data")
358
+ layers_raw = data_obj.get("layers")
359
+ if layers_raw is None:
360
+ layers: List[ParsedLayer] = []
361
+ else:
362
+ layers = [
363
+ _parse_layer(_require_dict(layer, f"data.layers[{i}]"), i, f"data.layers[{i}]")
364
+ for i, layer in enumerate(_require_list(layers_raw, "data.layers"))
365
+ ]
366
+ if not layers:
367
+ layers = _expand_ongrid_to_layer(data_obj, "data")
368
+
369
+ project_obj = _require_dict(root.get("project_state", {}), "project_state")
370
+ rules = [
371
+ _parse_rule(_require_dict(rule, f"project_state.rules[{i}]"), f"project_state.rules[{i}]")
372
+ for i, rule in enumerate(_require_list(project_obj.get("rules", []), "project_state.rules"))
373
+ ]
374
+ groups = [
375
+ _parse_group(_require_dict(group, f"project_state.groups[{i}]"), f"project_state.groups[{i}]")
376
+ for i, group in enumerate(_require_list(project_obj.get("groups", []), "project_state.groups"))
377
+ ]
378
+ project_state = ParsedProjectState(rules=rules, groups=groups, automap_rules=project_obj.get("automap_rules"))
379
+
380
+ tilesets = _parse_resources(root.get("resources", {}), "resources")
381
+ return ParsedMap(meta=meta, layers=layers, tilesets=tilesets, project_state=project_state, raw=root)
382
+
383
+
384
+ def parse_map_json(text: str) -> ParsedMap:
385
+ try:
386
+ payload = json.loads(text)
387
+ except json.JSONDecodeError as e:
388
+ raise MapParseError(f"Invalid JSON: {e}") from e
389
+ return parse_map_dict(_require_dict(payload, "root"))
390
+
391
+
392
+ def parse_map_file(path: Union[str, Path]) -> ParsedMap:
393
+ p = Path(path)
394
+ if not p.is_file():
395
+ raise MapParseError(f"Not a file: {p}")
396
+ if p.suffix.lower() != ".json":
397
+ raise MapParseError(f"Expected .json map file, got {p.suffix!r}")
398
+ try:
399
+ text = p.read_text(encoding="utf-8")
400
+ except OSError as e:
401
+ raise MapParseError(f"Cannot read {p}: {e}") from e
402
+ return parse_map_json(text)
@@ -0,0 +1,82 @@
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
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__(self, data: TilemapData, *, include_hidden_layers: bool = False) -> None:
20
+ self.data = data
21
+ self.tile_layers = data.get_tile_layers_dict(include_hidden=include_hidden_layers)
22
+ self._sorted_layer_ids = sorted(self.tile_layers.keys(), key=lambda lid: (self.tile_layers[lid].z_index, lid))
23
+ self._variant_cache: Dict[Tuple[int, int], Optional[Surface]] = {}
24
+ self._tile_w, self._tile_h = data.tile_size
25
+
26
+ def get_layer_dict(self) -> Dict[int, object]:
27
+ return dict(self.tile_layers)
28
+
29
+ def _get_cached_variant(self, ttype: int, variant: int) -> Optional[Surface]:
30
+ key = (ttype, variant)
31
+ if key not in self._variant_cache:
32
+ self._variant_cache[key] = self.data.get_tile_surface(ttype, variant, copy_surface=True)
33
+ return self._variant_cache[key]
34
+
35
+ def warm_cache(self) -> None:
36
+ for layer_id in self._sorted_layer_ids:
37
+ layer = self.tile_layers[layer_id]
38
+ for tile in layer.tiles.values():
39
+ if isinstance(tile.ttype, int):
40
+ self._get_cached_variant(tile.ttype, tile.variant)
41
+
42
+ def render(
43
+ self,
44
+ target: Surface,
45
+ camera_xy: Union[Tuple[float, float], Tuple[int, int]] = (0, 0),
46
+ viewport_size: Optional[Tuple[int, int]] = None,
47
+ ) -> LayerRenderStats:
48
+ cam_x, cam_y = float(camera_xy[0]), float(camera_xy[1])
49
+ if viewport_size is None:
50
+ viewport = target.get_rect()
51
+ else:
52
+ viewport = Rect(0, 0, viewport_size[0], viewport_size[1])
53
+
54
+ min_x = int(cam_x // self._tile_w) - 1
55
+ max_x = int((cam_x + viewport.width) // self._tile_w) + 1
56
+ min_y = int(cam_y // self._tile_h) - 1
57
+ max_y = int((cam_y + viewport.height) // self._tile_h) + 1
58
+
59
+ drawn = 0
60
+ skipped = 0
61
+ visible_layers = 0
62
+
63
+ for layer_id in self._sorted_layer_ids:
64
+ layer = self.tile_layers[layer_id]
65
+ if not layer.visible:
66
+ continue
67
+ visible_layers += 1
68
+ for (x, y), tile in layer.tiles.items():
69
+ if x < min_x or x > max_x or y < min_y or y > max_y:
70
+ skipped += 1
71
+ continue
72
+ if not isinstance(tile.ttype, int):
73
+ skipped += 1
74
+ continue
75
+ cell = self._get_cached_variant(tile.ttype, tile.variant)
76
+ if cell is None:
77
+ skipped += 1
78
+ continue
79
+ target.blit(cell, (x * self._tile_w - cam_x, y * self._tile_h - cam_y))
80
+ drawn += 1
81
+
82
+ return LayerRenderStats(drawn_tiles=drawn, skipped_tiles=skipped, visible_layers=visible_layers)
@@ -0,0 +1,80 @@
1
+ Metadata-Version: 2.4
2
+ Name: tilemap-parser
3
+ Version: 1.0.0
4
+ Summary: Standalone parser/loader for tilemap-editor JSON maps and sprite animation JSON.
5
+ Author: tilemap parser contributors
6
+ Classifier: Programming Language :: Python :: 3
7
+ Classifier: Programming Language :: Python :: 3 :: Only
8
+ Classifier: Topic :: Games/Entertainment
9
+ Classifier: Topic :: Software Development
10
+ Requires-Python: >=3.10
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: pygame-ce>=2.5
14
+ Dynamic: license-file
15
+
16
+ # tilemap-parser
17
+
18
+ Standalone parser/loader package for map JSON produced by `tilemap-editor` plus sprite animation JSON.
19
+
20
+ ## Install
21
+
22
+ ```bash
23
+ pip install -e .
24
+ ```
25
+
26
+ ## Load map + inspect layers
27
+
28
+ ```python
29
+ from tilemap_parser import load_map
30
+
31
+ data = load_map("assets/maps/level_1.json")
32
+
33
+ layers = data.get_layers(sort_by_zindex=True)
34
+ for layer in layers:
35
+ print(layer.id, layer.name, layer.layer_type, layer.z_index, layer.properties)
36
+ ```
37
+
38
+ ## Tile image fetch API (variant + tileset)
39
+
40
+ ```python
41
+ tile_surface = data.get_image(variant=12, ttype=0)
42
+ tile_surface2 = data.get_tile_surface(0, 12)
43
+ cell_surface = data.get_tile_surface_at("Ground", 10, 4)
44
+ ```
45
+
46
+ ## Raw payload access for debugging
47
+
48
+ ```python
49
+ raw = data.get_raw()
50
+ ```
51
+
52
+ `get_raw()` returns a deep-copied complete parsed root payload (including editor/project fields), so callers can inspect everything safely.
53
+
54
+ ## Animation loading + playback helper
55
+
56
+ ```python
57
+ from tilemap_parser import SpriteAnimationSet, AnimationPlayer
58
+
59
+ anim_set = SpriteAnimationSet.load("assets/anims/hero.anim.json")
60
+ player = AnimationPlayer(anim_set, "idle")
61
+
62
+ player.update(16.67)
63
+ frame_surface = player.get_current_image()
64
+ ```
65
+
66
+ ## Optional renderer for tile layers
67
+
68
+ ```python
69
+ import pygame
70
+ from tilemap_parser import TileLayerRenderer, load_map
71
+
72
+ data = load_map("assets/maps/level_1.json")
73
+ renderer = TileLayerRenderer(data)
74
+ renderer.warm_cache()
75
+
76
+ screen = pygame.display.set_mode((1280, 720))
77
+ stats = renderer.render(screen, camera_xy=(camera_x, camera_y))
78
+ ```
79
+
80
+ The renderer pre-indexes tile layers as `dict[layer_id, layer]`, sorts by `z_index`, ignores object layers, and caches `(tileset, variant)` cell surfaces to reduce repeated subsurface extraction.
@@ -0,0 +1,10 @@
1
+ tilemap_parser/__init__.py,sha256=Qa0QDxHc-iKdzAKCsuu9zTAKpfV1bIXRwF8zNUxeqvI,1315
2
+ tilemap_parser/animation.py,sha256=J4x1QIGv8i8GSApQGwEx7anHoPg9_ItJdnVWlHbPXAw,10363
3
+ tilemap_parser/map_loader.py,sha256=G5i63JnVe8Ea6ZFLMTuTNnVb21W1RrFpsuP7bgucJiI,7446
4
+ tilemap_parser/map_parse.py,sha256=LHvfgMERnO9GUqCfdVKY2am0lxHHesAYKQk2F1Fax_s,14568
5
+ tilemap_parser/renderer.py,sha256=dsNnDC2REsWl5khm_3kPQ_1m8WBIhYoRmF-zUbhjVVs,3075
6
+ tilemap_parser-1.0.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
7
+ tilemap_parser-1.0.0.dist-info/METADATA,sha256=7tECu0saLhF_XV-O8VSzKNH_11b8lL2Dj9nCP8v35w4,2202
8
+ tilemap_parser-1.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
9
+ tilemap_parser-1.0.0.dist-info/top_level.txt,sha256=VOScMmS9EE8Z6kFSJPMwiiFEqpnH2rjLF6cU8M_oFeU,15
10
+ tilemap_parser-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+