Files
heros_web/scripts/render-unit-sprite-review-boards.py

203 lines
7.5 KiB
Python

from __future__ import annotations
import json
from collections import defaultdict
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont
ROOT = Path(__file__).resolve().parents[1]
UNIT_DIR = ROOT / "src" / "assets" / "images" / "units"
DOCS_DIR = ROOT / "docs"
AUDIT_JSON = DOCS_DIR / "unit-sprite-rework-audit.json"
FRAME = 313
def font(size: int = 12):
for candidate in (r"C:\Windows\Fonts\malgun.ttf", r"C:\Windows\Fonts\arial.ttf"):
try:
return ImageFont.truetype(candidate, size)
except OSError:
continue
return ImageFont.load_default()
def checker_background(size: tuple[int, int], tile: int = 8) -> Image.Image:
image = Image.new("RGBA", size, (57, 76, 48, 255))
draw = ImageDraw.Draw(image)
for y in range(0, size[1], tile):
for x in range(0, size[0], tile):
color = (68, 90, 55, 255) if ((x // tile) + (y // tile)) % 2 == 0 else (48, 64, 43, 255)
draw.rectangle((x, y, x + tile - 1, y + tile - 1), fill=color)
return image
def frame_for(stem: str, row = 0, col = 0) -> Image.Image:
image = Image.open(UNIT_DIR / f"{stem}.png").convert("RGBA")
return image.crop((col * FRAME, row * FRAME, (col + 1) * FRAME, (row + 1) * FRAME))
def render_sprite(frame: Image.Image, size: int) -> Image.Image:
tile = checker_background((size, size))
scaled = frame.resize((size, size), Image.Resampling.LANCZOS)
tile.alpha_composite(scaled, (0, 0))
return tile
def label_for(stem: str) -> str:
return stem.replace("unit-", "")
def stage_label(stage: str) -> str:
return {
"approved-baseline": "approved baseline",
"pending-approval": "pending sample",
"queued-rework": "old queued asset",
}.get(stage, stage)
def stage_color(stage: str) -> tuple[int, int, int, int]:
if stage == "approved-baseline":
return (103, 151, 93, 255)
if stage == "pending-approval":
return (218, 176, 84, 255)
return (90, 101, 91, 255)
def render_batch_board(batch: str, rows: list[dict]) -> Path:
rows = sorted(rows, key=lambda item: item["stem"])
columns = 5
card_w = 172
card_h = 142
pad = 18
header_h = 54
width = pad + columns * card_w + pad
height = header_h + ((len(rows) + columns - 1) // columns) * card_h + pad
out = Image.new("RGBA", (width, height), (18, 22, 20, 255))
draw = ImageDraw.Draw(out)
title_font = font(18)
body_font = font(11)
small_font = font(9)
draw.text((pad, 16), f"{batch} review board", fill=(240, 226, 178, 255), font=title_font)
for index, row in enumerate(rows):
x = pad + (index % columns) * card_w
y = header_h + (index // columns) * card_h
stem = row["stem"]
base_status = row["base"]["status"]
action_status = row["actions"]["status"]
status_ok = base_status == "ok" and action_status == "ok"
stage = row.get("review_stage", "queued-rework")
border = stage_color(stage) if status_ok else (156, 105, 67, 255)
draw.rectangle((x, y, x + card_w - 12, y + card_h - 12), fill=(24, 30, 27, 255), outline=border, width=1)
frame = frame_for(stem)
out.alpha_composite(render_sprite(frame, 72), (x + 10, y + 10))
out.alpha_composite(render_sprite(frame, 50), (x + 94, y + 21))
draw.text((x + 10, y + 88), label_for(stem), fill=(238, 224, 178, 255), font=body_font)
status_text = stage_label(stage) if status_ok else "technical issue"
draw.text((x + 10, y + 108), status_text, fill=border, font=small_font)
if stage == "queued-rework":
draw.text((x + 94, y + 75), "not new", fill=(151, 137, 105, 255), font=small_font)
path = DOCS_DIR / f"unit-sprite-review-{batch}.png"
out.convert("RGB").save(path, optimize=True)
return path
def render_deliverable_board(rows: list[dict]) -> Path:
deliverables = [
row
for row in rows
if row.get("review_stage") in {"approved-baseline", "pending-approval"}
]
order = {"approved-baseline": 0, "pending-approval": 1}
deliverables = sorted(deliverables, key=lambda item: (order[item["review_stage"]], item["stem"]))
columns = 5
card_w = 172
card_h = 148
pad = 18
header_h = 70
width = pad + columns * card_w + pad
height = header_h + ((len(deliverables) + columns - 1) // columns) * card_h + pad
out = Image.new("RGBA", (width, height), (18, 22, 20, 255))
draw = ImageDraw.Draw(out)
draw.text((pad, 16), "Approved + Pending Sprite Deliverables", fill=(240, 226, 178, 255), font=font(18))
draw.text(
(pad, 42),
"Queued old assets are intentionally excluded from this board.",
fill=(174, 161, 124, 255),
font=font(11),
)
for index, row in enumerate(deliverables):
x = pad + (index % columns) * card_w
y = header_h + (index // columns) * card_h
stem = row["stem"]
stage = row.get("review_stage", "queued-rework")
border = stage_color(stage)
draw.rectangle((x, y, x + card_w - 12, y + card_h - 12), fill=(24, 30, 27, 255), outline=border, width=1)
frame = frame_for(stem)
out.alpha_composite(render_sprite(frame, 72), (x + 10, y + 10))
out.alpha_composite(render_sprite(frame, 50), (x + 94, y + 21))
draw.text((x + 10, y + 88), label_for(stem), fill=(238, 224, 178, 255), font=font(11))
draw.text((x + 10, y + 110), stage_label(stage), fill=border, font=font(9))
path = DOCS_DIR / "unit-sprite-review-approved-and-pending.png"
out.convert("RGB").save(path, optimize=True)
return path
def render_all_board(groups: dict[str, list[dict]], paths: list[Path]) -> Path:
thumb_w = 212
thumb_h = 118
pad = 18
cols = 2
width = pad + cols * thumb_w + pad
height = 58 + ((len(paths) + cols - 1) // cols) * thumb_h + pad
out = Image.new("RGBA", (width, height), (18, 22, 20, 255))
draw = ImageDraw.Draw(out)
draw.text((pad, 16), "Unit Sprite Batch Review Index", fill=(240, 226, 178, 255), font=font(18))
for index, path in enumerate(paths):
batch = path.stem.replace("unit-sprite-review-", "")
x = pad + (index % cols) * thumb_w
y = 58 + (index // cols) * thumb_h
preview = Image.open(path).convert("RGBA")
preview.thumbnail((190, 84), Image.Resampling.LANCZOS)
draw.rectangle((x, y, x + thumb_w - 12, y + thumb_h - 12), fill=(24, 30, 27, 255), outline=(85, 95, 75, 255))
out.alpha_composite(preview, (x + 10, y + 10))
approved_count = sum(1 for row in groups[batch] if row.get("review_stage") == "approved-baseline")
pending_count = sum(1 for row in groups[batch] if row.get("review_stage") == "pending-approval")
draw.text(
(x + 10, y + 92),
f"{batch}: {approved_count} approved, {pending_count} pending",
fill=(238, 224, 178, 255),
font=font(10),
)
path = DOCS_DIR / "unit-sprite-review-index.png"
out.convert("RGB").save(path, optimize=True)
return path
def main() -> None:
audit = json.loads(AUDIT_JSON.read_text(encoding="utf-8"))
groups: dict[str, list[dict]] = defaultdict(list)
all_rows: list[dict] = []
for row in audit["rows"]:
groups[row["batch"]].append(row)
all_rows.append(row)
paths = [render_batch_board(batch, groups[batch]) for batch in sorted(groups)]
deliverables = render_deliverable_board(all_rows)
index = render_all_board(groups, paths)
print(f"Wrote {index}")
print(f"Wrote {deliverables}")
for path in paths:
print(f"Wrote {path}")
if __name__ == "__main__":
main()