18c7792935
Replaces the upreza-derived 4K dungeon textures + AtlasLoot boss-coord overlay (which had a consistent positional offset against texture skulls) with keystone.guru's z=4 tile pyramid stitched to 6144x4096 WebP per floor. kg's split_floors.js gives per-dungeon enemies, packs (polygons), patrols (polylines), and map icons calibrated to those tiles, so overlays align pixel-perfectly. 27/29 classic dungeons now have full enemy/pack data; ZG + Sunken Temple have maps only. Pipeline: tools/kg_fetch.py -> tools/kg_stitch.py -> tools/kg_build_data.py.
80 lines
2.6 KiB
Python
80 lines
2.6 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
Stitch keystone.guru tile pyramid (z=4 by default) into one WebP per
|
||
dungeon-floor. Reads `data/kg/_summary.json` (produced by kg_fetch.py) for
|
||
the grid bounds.
|
||
|
||
Output: web/assets/maps/<tile_key>[_floor<n>].webp
|
||
"""
|
||
from __future__ import annotations
|
||
import argparse
|
||
import concurrent.futures
|
||
import json
|
||
import sys
|
||
from pathlib import Path
|
||
from PIL import Image
|
||
|
||
ROOT = Path(__file__).resolve().parent.parent
|
||
KG_DIR = ROOT / "data" / "kg"
|
||
WEB_MAPS = ROOT / "web" / "assets" / "maps"
|
||
|
||
TILE_W, TILE_H = 384, 256
|
||
|
||
|
||
def stitch_one(args):
|
||
tile_key, floor_info, zoom, all_floors_count, quality = args
|
||
floor = floor_info["index"]
|
||
cols, rows = floor_info["cols"], floor_info["rows"]
|
||
src_dir = KG_DIR / tile_key / f"floor{floor}" / f"z{zoom}"
|
||
|
||
canvas = Image.new("RGBA", (cols * TILE_W, rows * TILE_H), (0, 0, 0, 255))
|
||
missing = 0
|
||
for x in range(cols):
|
||
for y in range(rows):
|
||
tile_path = src_dir / f"{x}_{y}.png"
|
||
if not tile_path.exists():
|
||
missing += 1
|
||
continue
|
||
with Image.open(tile_path) as t:
|
||
canvas.paste(t.convert("RGBA"), (x * TILE_W, y * TILE_H))
|
||
|
||
suffix = f"_floor{floor}" if all_floors_count > 1 else ""
|
||
out = WEB_MAPS / f"{tile_key}{suffix}.webp"
|
||
out.parent.mkdir(parents=True, exist_ok=True)
|
||
canvas.convert("RGB").save(out, "WEBP", quality=quality, method=4)
|
||
return tile_key, floor, canvas.size, missing
|
||
|
||
|
||
def main() -> int:
|
||
ap = argparse.ArgumentParser()
|
||
ap.add_argument("--quality", type=int, default=85)
|
||
ap.add_argument("--workers", type=int, default=4)
|
||
args = ap.parse_args()
|
||
|
||
summary_path = KG_DIR / "_summary.json"
|
||
if not summary_path.exists():
|
||
print(f"missing {summary_path} — run kg_fetch.py first", file=sys.stderr)
|
||
return 1
|
||
summary = json.loads(summary_path.read_text())
|
||
|
||
jobs = []
|
||
for d in summary["dungeons"]:
|
||
if not d.get("floors"):
|
||
continue
|
||
for floor_info in d["floors"]:
|
||
jobs.append((d["tile_key"], floor_info, d["max_zoom"],
|
||
len(d["floors"]), args.quality))
|
||
|
||
print(f"stitching {len(jobs)} dungeon-floors with {args.workers} workers...")
|
||
with concurrent.futures.ProcessPoolExecutor(max_workers=args.workers) as pool:
|
||
for tile_key, floor, size, missing in pool.map(stitch_one, jobs):
|
||
note = f" ({missing} missing tiles)" if missing else ""
|
||
print(f" {tile_key} f{floor}: {size[0]}×{size[1]}{note}")
|
||
|
||
print(f"done → {WEB_MAPS}")
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
raise SystemExit(main())
|