This commit is contained in:
Gabrieal Jimmy
2026-10-01 15:15:20 -05:00
commit 97a27f9a42
327 changed files with 7075 additions and 0 deletions

172
build_parts.py Executable file
View File

@@ -0,0 +1,172 @@
#!/usr/bin/env python3
"""
Compile text-grid part sources into palette-key PNGs.
Sources live in parts_src/<slot>/<name>.txt and compile to parts/<slot>/<name>.png.
Every non-blank line is one pixel row; lines starting with '#' are comments. All rows
must be the same width.
Legend
. transparent o outline
1 main_light 2 main_base 3 main_dark
a accent_light b accent_base c accent_dark
x type_light y type_base z type_dark
w white p pupil
P pivot T tail B back A aura ^ head_top
Layout sources (parts_src/<slot>/<name>.layout) assemble stamps onto a fixed canvas,
used for the 84x84 auras:
size 84 84
stamp <stamp-name> <x> <y> [flip] # parts_src/<slot>/stamps/<stamp-name>.txt, top-left at x,y
marker <char> <x> <y> # e.g. "marker P 42 42"
Later stamps paint over earlier ones; '.' in a stamp never paints.
Usage
python build_parts.py # build everything
python build_parts.py body/quad_slim # build some parts
python build_parts.py mirror FILE # print FILE + its horizontal mirror (shared center column)
python build_parts.py mirror FILE --gap N # mirror with N transparent columns between halves
python build_parts.py flip FILE # print FILE flipped horizontally
"""
import os
import sys
from pathlib import Path
from PIL import Image
from generate_sprite import KEYS, MARKERS, PUPIL, EYE_WHITE
# PARTS_SRC / PARTS_DIR let experiments build into a separate library (e.g. proto/).
SRC = Path(os.environ.get("PARTS_SRC", "parts_src"))
OUT = Path(os.environ.get("PARTS_DIR", "parts"))
LEGEND = {
"o": KEYS["outline"],
"1": KEYS["main_light"], "2": KEYS["main_base"], "3": KEYS["main_dark"],
"a": KEYS["accent_light"], "b": KEYS["accent_base"], "c": KEYS["accent_dark"],
"x": KEYS["type_light"], "y": KEYS["type_base"], "z": KEYS["type_dark"],
"w": EYE_WHITE, "p": PUPIL,
"P": MARKERS["pivot"], "T": MARKERS["tail"], "B": MARKERS["back"],
"A": MARKERS["aura"], "^": MARKERS["head_top"],
}
class SourceError(Exception):
pass
def read_grid(path):
rows = []
for n, line in enumerate(Path(path).read_text().splitlines(), 1):
line = line.rstrip()
if not line or line.startswith("#"):
continue
bad = sorted({c for c in line if c != "." and c not in LEGEND})
if bad:
raise SourceError(f"{path}:{n}: unknown characters {bad}")
rows.append(line)
if not rows:
raise SourceError(f"{path}: empty grid")
widths = {len(r) for r in rows}
if len(widths) != 1:
for n, r in enumerate(rows):
if len(r) != len(rows[0]):
raise SourceError(f"{path}: row {n} is {len(r)} wide, row 0 is {len(rows[0])}")
return rows
def paint(canvas, rows, x0, y0, flip=False):
for y, row in enumerate(rows):
if flip:
row = row[::-1]
for x, c in enumerate(row):
if c == ".":
continue
px, py = x0 + x, y0 + y
if not (0 <= px < len(canvas[0]) and 0 <= py < len(canvas)):
raise SourceError(f"stamp pixel ({px},{py}) outside canvas")
canvas[py][px] = c
def read_layout(path):
path = Path(path)
canvas = None
for n, line in enumerate(path.read_text().splitlines(), 1):
parts = line.split("#")[0].split()
if not parts:
continue
cmd, args = parts[0], parts[1:]
try:
if cmd == "size":
w, h = int(args[0]), int(args[1])
canvas = [["."] * w for _ in range(h)]
elif cmd == "stamp":
rows = read_grid(path.parent / "stamps" / f"{args[0]}.txt")
paint(canvas, rows, int(args[1]), int(args[2]), flip=len(args) > 3 and args[3] == "flip")
elif cmd == "marker":
paint(canvas, [args[0]], int(args[1]), int(args[2]))
else:
raise SourceError(f"unknown command {cmd!r}")
except (IndexError, ValueError, TypeError) as e:
raise SourceError(f"{path}:{n}: {e}")
if canvas is None:
raise SourceError(f"{path}: missing 'size'")
return ["".join(r) for r in canvas]
def to_image(rows):
h, w = len(rows), len(rows[0])
img = Image.new("RGBA", (w, h), (0, 0, 0, 0))
px = img.load()
for y, row in enumerate(rows):
for x, c in enumerate(row):
if c != ".":
px[x, y] = LEGEND[c] + (255,)
return img
def sources():
for p in sorted(SRC.glob("*/*")):
if p.suffix in (".txt", ".layout"):
yield p
def build(path):
rows = read_layout(path) if path.suffix == ".layout" else read_grid(path)
out = OUT / path.parent.name / f"{path.stem}.png"
out.parent.mkdir(parents=True, exist_ok=True)
to_image(rows).save(out)
return out, len(rows[0]), len(rows)
def main(argv):
if argv and argv[0] in ("mirror", "flip"):
rows = read_grid(argv[1])
if argv[0] == "flip":
print("\n".join(r[::-1] for r in rows))
else:
gap = int(argv[3]) if len(argv) > 3 and argv[2] == "--gap" else None
for r in rows:
print(r + "." * gap + r[::-1] if gap is not None else r + r[-2::-1])
return 0
wanted = set(argv)
errors = 0
built = 0
for path in sources():
key = f"{path.parent.name}/{path.stem}"
if wanted and key not in wanted:
continue
try:
out, w, h = build(path)
built += 1
if wanted:
print(f"{out} {w}x{h}")
except SourceError as e:
print(f"ERROR {e}", file=sys.stderr)
errors += 1
print(f"built {built} part(s), {errors} error(s)")
return 1 if errors else 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))