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()