Files
florian.berthold 18c7792935 switch to keystone.guru tiles + enemy data
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.
2026-04-25 22:11:17 +02:00

80 lines
2.6 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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())