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.
- tilemap_parser/__init__.py +61 -0
- tilemap_parser/animation.py +306 -0
- tilemap_parser/map_loader.py +209 -0
- tilemap_parser/map_parse.py +402 -0
- tilemap_parser/renderer.py +82 -0
- tilemap_parser-1.0.0.dist-info/METADATA +80 -0
- tilemap_parser-1.0.0.dist-info/RECORD +10 -0
- tilemap_parser-1.0.0.dist-info/WHEEL +5 -0
- tilemap_parser-1.0.0.dist-info/licenses/LICENSE +674 -0
- tilemap_parser-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -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,,
|