203 lines
7.5 KiB
Python
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()
|