"""Render the shared pixel maps to PNG without Aseprite. Same source of truth as gen_sprites.lua (tools/aseprite/maps/); use this for headless preview on boxes with no Aseprite. Output is _ph placeholder-grade per the asset policy. uv run --with pillow python tools/aseprite/preview.py \ [--maps tools/aseprite/maps] [--out assets/sprites_ph] [--zoom 8] Writes .png (1x) and, with --zoom N > 1, @Nx.png (nearest-neighbor upscale). """ import argparse from pathlib import Path from PIL import Image SPRITES = ["m03_ph", "grunt_ph", "burster_ph", "slug_ph", "scrap_ph"] def parse_palette(path: Path) -> dict[str, tuple[int, int, int, int]]: pal = {} for line in path.read_text().splitlines(): parts = line.split() if len(parts) == 2 and len(parts[0]) == 1 and parts[0] != "#": v = int(parts[1], 16) pal[parts[0]] = ((v >> 16) & 255, (v >> 8) & 255, v & 255, 255) return pal def parse_map(path: Path) -> list[str]: rows = [line for line in path.read_text().splitlines() if line] for i, row in enumerate(rows): if len(row) != len(rows[0]): raise ValueError(f"{path}: row {i + 1} width {len(row)} != {len(rows[0])}") return rows def main() -> None: ap = argparse.ArgumentParser() ap.add_argument("--maps", default="tools/aseprite/maps") ap.add_argument("--out", default="assets/sprites_ph") ap.add_argument("--zoom", type=int, default=8) args = ap.parse_args() maps_dir = Path(args.maps) out_dir = Path(args.out) out_dir.mkdir(parents=True, exist_ok=True) pal = parse_palette(maps_dir / "palette.txt") for name in SPRITES: rows = parse_map(maps_dir / f"{name}.txt") w, h = len(rows[0]), len(rows) img = Image.new("RGBA", (w, h), (0, 0, 0, 0)) for y, row in enumerate(rows): for x, ch in enumerate(row): if ch != ".": if ch not in pal: raise ValueError(f"{name}: unknown palette char {ch!r}") img.putpixel((x, y), pal[ch]) img.save(out_dir / f"{name}.png") if args.zoom > 1: big = img.resize((w * args.zoom, h * args.zoom), Image.Resampling.NEAREST) big.save(out_dir / f"{name}@{args.zoom}x.png") print(f"wrote {out_dir}/{name}.png {w}x{h}") if __name__ == "__main__": main()