Files
forgotten_caves/tools/mktemplate.py
T
irrlichtandClaude Opus 5 8bf086b06a Add the entities.png manifest, paste-in template and sprite loader
The sheet becomes a fixed grid (one row per entity, one column slot per
animation state) described by a sidecar entities.toml that only maps
names to rows; the loader reads present states and frame counts off the
pixels, so assembling the sheet by hand needs no bookkeeping.
tools/mktemplate.py renders the labelled template with low-alpha slot
guides. The CGA atlas is kept for larger UI text.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 20:25:26 +02:00

116 lines
4.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Draw the paste-in template for entities.png from its manifest.
Reads assets/tilesets/entities.toml and writes entities-template.png: a label
column and header (outside the loaded area), and per-state slot guides tinted at
low alpha — below the loader's opacity threshold, so guides left under a sprite are
harmless. If entities.png already exists in this layout, its sprites are carried
over, so the template can be regenerated after adding rows to the manifest.
python3 tools/mktemplate.py
"""
import tomllib
from PIL import Image
MANIFEST = "assets/tilesets/entities.toml"
SHEET = "assets/tilesets/entities.png"
OUT = "assets/tilesets/entities-template.png"
FONT = "assets/fonts/micro-chat-8x9.png"
GUIDE_ALPHA = 48 # < 128: transparent for the game
GUIDE_TINTS = [(90, 160, 255), (255, 200, 90), (120, 230, 120), (255, 120, 120),
(200, 120, 255), (255, 160, 60), (160, 160, 160)]
LABEL_COLOR = (0, 0, 0, 255)
GRID_COLOR = (0, 0, 0, 90)
class Font:
"""Minimal reader for the CP437 atlas in font.rs's layout (32 columns)."""
def __init__(self, path, gw, gh):
self.img = Image.open(path).convert("L")
self.gw, self.gh = gw, gh
self.left, self.width = {}, {}
for code in range(256):
cx, cy = (code % 32) * gw, (code // 32) * gh
xs = [x for y in range(gh) for x in range(gw) if self.img.getpixel((cx + x, cy + y)) > 128]
if xs:
self.left[code], self.width[code] = min(xs), max(xs) - min(xs) + 1
def draw(self, dst, x, y, text, color):
for ch in text:
code = ord(ch) if ord(ch) < 128 else ord("?")
w = self.width.get(code, 0)
if w:
cx, cy = (code % 32) * self.gw + self.left[code], (code // 32) * self.gh
for gy in range(self.gh):
for gx in range(w):
if self.img.getpixel((cx + gx, cy + gy)) > 128 \
and x + gx < dst.size[0] and y + gy < dst.size[1]:
dst.putpixel((x + gx, y + gy), color)
x += w + 1 if w else self.gw // 2
return x
def main():
m = tomllib.load(open(MANIFEST, "rb"))
cell = m["layout"]["cell"]
ox, oy = m["layout"]["origin"]
states = list(m["layout"]["states"].items())
entities = sorted(m["entities"].items(), key=lambda kv: kv[1])
rows = max(r for _, r in entities) + 1
cols = max(col + n for _, (col, n) in states)
w, h = ox + cols * cell, oy + rows * cell
# A little margin right of the sprite area so the last header label fits.
out = Image.new("RGBA", (w + 8, h), (0, 0, 0, 0))
font = Font(FONT, 8, 9)
# Slot guides: one tint per state, alternating rows slightly for readability.
for i, (name, (col, n)) in enumerate(states):
r, g, b = GUIDE_TINTS[i % len(GUIDE_TINTS)]
for row in range(rows):
a = GUIDE_ALPHA if row % 2 == 0 else GUIDE_ALPHA * 2 // 3
x0, y0 = ox + col * cell, oy + row * cell
for y in range(y0, y0 + cell):
for x in range(x0, x0 + n * cell):
out.putpixel((x, y), (r, g, b, a))
# Header label, staggered over two lines so narrow slots don't collide, and a
# separator line at the slot's left edge.
label = {"action1": "act1", "action2": "act2"}.get(name, name)
font.draw(out, ox + col * cell + 1, 1 if i % 2 == 0 else 8, label, LABEL_COLOR)
for y in range(0, h):
out.putpixel((ox + col * cell, y), GRID_COLOR)
for y in range(0, h):
out.putpixel((w, y), GRID_COLOR)
# Cell grid lines (every cell, faint) inside the sprite area.
for x in range(ox, w, cell):
for y in range(oy, h):
if out.getpixel((x, y))[3] < GRID_COLOR[3]:
out.putpixel((x, y), GRID_COLOR)
for y in range(oy, h, cell):
for x in range(ox, w):
out.putpixel((x, y), GRID_COLOR)
# Row labels.
for name, row in entities:
font.draw(out, 1, oy + row * cell, name.replace("_", " "), LABEL_COLOR)
# Carry over sprites from an existing sheet in this layout.
try:
old = Image.open(SHEET).convert("RGBA")
if old.size[0] >= w and old.size[1] >= oy:
region = old.crop((ox, oy, w, min(h, old.size[1])))
out.paste(region, (ox, oy), region)
print(f"carried over sprites from {SHEET}")
except FileNotFoundError:
pass
out.save(OUT)
print(f"wrote {OUT}: {w}×{h}, {rows} rows, {cols} columns")
if __name__ == "__main__":
main()