651193674c
tools/aseprite/maps/ holds the source of truth: char-grid pixel maps + shared palette for five _ph placeholder sprites (M03 32px per MOE.md design law cues — round silhouette, mismatched eyes, bolted hardpoint, pillar feet; grunt 16px untagged olive; burster 20px HEAT glow band; KINETIC slug; scrap diamond). Two interchangeable renderers: gen_sprites.lua (aseprite -b, emits .aseprite + .png — run on any machine with Aseprite) and preview.py (Pillow, identical PNGs headless + nearest-neighbor zoom for review). Outputs gitignored; greybox scenes untouched — art stays out of the game per the greybox rule.
71 lines
2.3 KiB
Python
71 lines
2.3 KiB
Python
"""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 <name>.png (1x) and, with --zoom N > 1, <name>@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()
|