Files
DramaticShapeVoxelMod/tools/building_images.py
T
DramaticShape 702d8049f0 initial commit
2026-07-26 16:43:53 -04:00

113 lines
4.1 KiB
Python

"""Composite the reference PNGs that
assets/docs/buildings/B##-unnamed-building.md files link to (img/B##.png,
img/B##_x{scale}.png, img/B##_atlas.png) but the repo does not ship, since
they land in the gitignored .scratchpad/ -- run this to regenerate them
locally.
Parses each doc's raw tile-id grid (the `local rows = {...}` Lua array), its
named tileset, and its scale factor straight out of the markdown, then
composites the building out of the tileset atlas the same way
building_voxels.py's sprite() does: paste each tile's 8x8 block, 16 tiles
per row. Emits, per building:
- B##.png native resolution
- B##_x{N}.png nearest-neighbor upscale, at the scale the doc references
- B##_atlas.png a strip of the building's distinct tiles, first-appearance
order (matches the doc's legend lettering)
python mods/DRAMATIC_SHAPE/tools/building_images.py [outdir]
outdir defaults to .scratchpad/img at the repo root.
"""
import os
import re
import sys
import glob
from PIL import Image
MOD_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
ROOT = os.path.dirname(os.path.dirname(MOD_ROOT))
DOCS = os.path.join(MOD_ROOT, "assets", "docs", "buildings")
TILESETS = os.path.join(ROOT, "assets", "generated", "tilesets")
PER_ROW = 16
def parse_doc(path):
text = open(path, encoding="utf-8").read()
bid = re.search(r"^# (B\d+)", text, re.M).group(1)
scale = int(re.search(r"img/B\d+_x(\d+)\.png", text).group(1))
tileset = re.search(r"assets/generated/tilesets/(\w+)\.png", text).group(1)
lua = re.search(r"local rows = \{(.*?)\n\}", text, re.S).group(1)
rows = []
for line in lua.strip().splitlines():
code = line.split("--")[0]
nums = re.findall(r"\d+", code)
if nums:
rows.append([int(n) for n in nums])
return bid, scale, tileset, rows
def composite(rows, atlas):
h, w = len(rows), len(rows[0])
im = Image.new("RGB", (w * 8, h * 8))
for r, row in enumerate(rows):
for c, t in enumerate(row):
ax, ay = (t % PER_ROW) * 8, (t // PER_ROW) * 8
im.paste(atlas.crop((ax, ay, ax + 8, ay + 8)), (c * 8, r * 8))
return im
def atlas_strip(rows, atlas, cell=32, pad=2, per_row=12):
"""Every distinct tile the building uses, in first-appearance (row-major)
order -- the same order the doc's legend assigns A, B, C, ... to."""
seen, seenset = [], set()
for row in rows:
for t in row:
if t not in seenset:
seenset.add(t)
seen.append(t)
n = len(seen)
cols = min(n, per_row)
lines = (n + per_row - 1) // per_row
W = cols * cell + (cols + 1) * pad
H = lines * cell + (lines + 1) * pad
img = Image.new("RGB", (W, H), (255, 255, 255))
for i, t in enumerate(seen):
ax, ay = (t % PER_ROW) * 8, (t // PER_ROW) * 8
tile = atlas.crop((ax, ay, ax + 8, ay + 8)).resize((cell, cell), Image.NEAREST)
col, line = i % per_row, i // per_row
img.paste(tile, (pad + col * (cell + pad), pad + line * (cell + pad)))
return img
def main():
out = sys.argv[1] if len(sys.argv) > 1 else os.path.join(ROOT, ".scratchpad", "img")
os.makedirs(out, exist_ok=True)
atlas_cache = {}
docs = sorted(
glob.glob(os.path.join(DOCS, "B*-unnamed-building.md")),
key=lambda p: int(re.search(r"B(\d+)", os.path.basename(p)).group(1)),
)
for path in docs:
bid, scale, tileset, rows = parse_doc(path)
if tileset not in atlas_cache:
atlas_cache[tileset] = Image.open(
os.path.join(TILESETS, tileset + ".png")
).convert("RGB")
atlas = atlas_cache[tileset]
native = composite(rows, atlas)
native.save(os.path.join(out, f"{bid}.png"))
big = native.resize((native.width * scale, native.height * scale), Image.NEAREST)
big.save(os.path.join(out, f"{bid}_x{scale}.png"))
atl = atlas_strip(rows, atlas)
atl.save(os.path.join(out, f"{bid}_atlas.png"))
print(f"{bid}: {tileset} {native.size} -> x{scale} {big.size}, atlas {atl.size}")
if __name__ == "__main__":
main()