Approve Shu infantry handpainted sprite v8
This commit is contained in:
202
scripts/render-unit-sprite-review-boards.py
Normal file
202
scripts/render-unit-sprite-review-boards.py
Normal file
@@ -0,0 +1,202 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user