#!/usr/bin/env python3 """Curate the Space's assets from the full auto-battler set — manifest-driven. The Space ships a SUBSET of auto-battler's 65 MB asset library: only the files the app actually references. Rather than per-feature copy logic, this resolves a UNION of asset URLs from one or more manifests and copies exactly those, decoding %-escapes so URL-encoded folder names (e.g. "Carnival%20NPCs", "_Premade%20Scene") match on disk. Manifests: • web/assets/characters.json — every sprite sheet the curated characters reference (built in; body + shadows + extras + companions). • extra URL-list files (argv) — newline-delimited /assets/... URLs, e.g. the map's MAP_ASSET_URLS dumped from auto-battler. Comment/blank lines (#, empty) are skipped. AB=../auto-battler python3 curate_assets.py [urls1.txt urls2.txt ...] Run from the tiny-army dir; idempotent. Driven by build.sh so it stays reproducible. """ import json import os import shutil import sys from urllib.parse import unquote HERE = os.path.dirname(os.path.abspath(__file__)) AB = os.environ.get("AB", os.path.join(HERE, "..", "auto-battler")) SRC_ROOT = os.path.join(AB, "public", "assets") DST_ROOT = os.path.join(HERE, "web", "assets") MANIFEST = os.path.join(DST_ROOT, "characters.json") def character_urls(manifest): """Every /assets/... sheet URL characters.json points at, across all sheet kinds.""" urls = set() for pack in manifest["packs"]: for c in pack["characters"]: for k in ("idle", "walk", "attack", "dmg", "die", "attackDiagonal", "attackEffect", "attackProjectile", "attackImpact"): if c.get(k): urls.add(c[k]) for u in (c.get("shadows") or {}).values(): urls.add(u) for e in (c.get("extras") or []): for k in ("url", "effect", "projectile", "impact", "shadow"): if e.get(k): urls.add(e[k]) return urls def file_urls(path): """Newline-delimited /assets/... URLs from an extra manifest (skip blanks/comments).""" with open(path) as f: return {ln.strip() for ln in f if ln.strip() and not ln.startswith("#")} def json_asset_urls(path): """Every /assets/....png|jpg URL anywhere in a JSON data file (recursive walk). Used for effects.json — the classes/enemies skill cards render its effect + status-effect icons.""" urls = set() def walk(o): if isinstance(o, str): if o.startswith("/assets/") and o.lower().endswith((".png", ".jpg", ".jpeg", ".webp")): urls.add(o) elif isinstance(o, dict): for v in o.values(): walk(v) elif isinstance(o, list): for v in o: walk(v) if os.path.exists(path): walk(json.load(open(path))) return urls def main(): urls = character_urls(json.load(open(MANIFEST))) urls |= json_asset_urls(os.path.join(DST_ROOT, "effects.json")) # effect + status-effect icons for path in sys.argv[1:]: urls |= file_urls(path) copied = skipped = absent = 0 for url in sorted(urls): # URLs are URL-encoded; decode so the path matches real on-disk folder names (with # spaces). The static server decodes requests the same way, so files land where the # browser asks. Strip the leading /assets/ (or any leading slash) to get the rel path. rel = unquote(url[len("/assets/"):] if url.startswith("/assets/") else url.lstrip("/")) src = os.path.join(SRC_ROOT, rel) dst = os.path.join(DST_ROOT, rel) if os.path.exists(dst): skipped += 1 continue if not os.path.exists(src): absent += 1 # referenced but missing from the full set too — skip continue os.makedirs(os.path.dirname(dst), exist_ok=True) shutil.copy2(src, dst) copied += 1 print(f"curate: copied={copied} already-present={skipped} missing-from-source={absent}") if __name__ == "__main__": main()