Approve Shu infantry handpainted sprite v8
This commit is contained in:
158
scripts/render-current-sprite-approval-board.py
Normal file
158
scripts/render-current-sprite-approval-board.py
Normal file
@@ -0,0 +1,158 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
DOCS_DIR = ROOT / "docs"
|
||||
MANIFEST_PATH = DOCS_DIR / "unit-sprite-rework-manifest.json"
|
||||
AUDIT_PATH = DOCS_DIR / "unit-sprite-rework-audit.json"
|
||||
OUTPUT_PATH = DOCS_DIR / "unit-sprite-current-approval-board.png"
|
||||
|
||||
|
||||
def font(size: int = 14):
|
||||
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 load_json(path: Path) -> dict[str, Any]:
|
||||
if not path.exists():
|
||||
raise FileNotFoundError(f"missing required file: {path.relative_to(ROOT)}")
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def audit_row(audit: dict[str, Any], stem: str) -> dict[str, Any] | None:
|
||||
return next((row for row in audit.get("rows", []) if row.get("stem") == stem), None)
|
||||
|
||||
|
||||
def infer_prefix(candidate: dict[str, Any]) -> str:
|
||||
artifact_prefix = str(candidate.get("artifactPrefix", "")).strip()
|
||||
if artifact_prefix:
|
||||
return artifact_prefix
|
||||
|
||||
source = Path(candidate.get("source", "")).name
|
||||
version = candidate.get("version")
|
||||
if source and version and source.endswith(f"-source-{version}.png"):
|
||||
return source.removesuffix(f"-source-{version}.png") + f"-{version}"
|
||||
if source and source.endswith(".png"):
|
||||
return source.removesuffix(".png")
|
||||
raise ValueError("cannot infer artifact prefix from currentApprovalCandidate source")
|
||||
|
||||
|
||||
def fit_image(path: Path, box: tuple[int, int], title: str) -> Image.Image:
|
||||
width, height = box
|
||||
panel = Image.new("RGBA", (width, height), (24, 30, 27, 255))
|
||||
draw = ImageDraw.Draw(panel)
|
||||
draw.rectangle((0, 0, width - 1, height - 1), outline=(105, 120, 94, 255), width=1)
|
||||
draw.text((12, 10), title, fill=(240, 226, 178, 255), font=font(14))
|
||||
|
||||
if not path.exists():
|
||||
draw.text((12, 44), f"Missing: {path.name}", fill=(230, 128, 104, 255), font=font(12))
|
||||
return panel
|
||||
|
||||
image = Image.open(path).convert("RGBA")
|
||||
image.thumbnail((width - 24, height - 56), Image.Resampling.LANCZOS)
|
||||
x = (width - image.width) // 2
|
||||
y = 44 + (height - 56 - image.height) // 2
|
||||
panel.alpha_composite(image, (x, y))
|
||||
return panel
|
||||
|
||||
|
||||
def write_text_block(draw: ImageDraw.ImageDraw, x: int, y: int, lines: list[str], fill=(222, 214, 174, 255)) -> None:
|
||||
small = font(12)
|
||||
for index, line in enumerate(lines):
|
||||
draw.text((x, y + index * 21), line, fill=fill, font=small)
|
||||
|
||||
|
||||
def render_board() -> Path:
|
||||
manifest = load_json(MANIFEST_PATH)
|
||||
audit = load_json(AUDIT_PATH)
|
||||
pending = manifest.get("pendingApproval", [])
|
||||
if not pending:
|
||||
raise ValueError("no pendingApproval unit to render")
|
||||
|
||||
stem = pending[0]
|
||||
candidates = manifest.get("currentApprovalCandidate", {})
|
||||
candidate = candidates.get(stem)
|
||||
if not candidate:
|
||||
raise ValueError(f"{stem}: missing currentApprovalCandidate entry")
|
||||
|
||||
prefix = infer_prefix(candidate)
|
||||
row = audit_row(audit, stem)
|
||||
base = row.get("base", {}) if row else {}
|
||||
|
||||
paths = {
|
||||
"Contact Sheet": DOCS_DIR / f"{prefix}-contact.png",
|
||||
"50px / 72px Scale": DOCS_DIR / f"{prefix}-scale-compare.png",
|
||||
"Debug Battle Preview": DOCS_DIR / f"{prefix}-debug-preview-battle-check.png",
|
||||
"Approved + Pending Board": DOCS_DIR / "unit-sprite-review-approved-and-pending.png",
|
||||
}
|
||||
|
||||
width = 1280
|
||||
height = 1320
|
||||
pad = 24
|
||||
out = Image.new("RGBA", (width, height), (18, 22, 20, 255))
|
||||
draw = ImageDraw.Draw(out)
|
||||
title_font = font(24)
|
||||
body_font = font(13)
|
||||
|
||||
draw.text((pad, 18), f"Current Unit Sprite Approval Board: {stem} {candidate.get('version', '')}", fill=(245, 232, 183, 255), font=title_font)
|
||||
draw.text(
|
||||
(pad, 52),
|
||||
"Generated from the current manifest. This board is for visual approval only; it does not promote, commit, push, or deploy.",
|
||||
fill=(178, 166, 127, 255),
|
||||
font=body_font,
|
||||
)
|
||||
|
||||
info_lines = [
|
||||
f"status: pending-approval",
|
||||
f"source: {Path(candidate.get('source', '')).name}",
|
||||
f"approval packet: {Path(candidate.get('approvalPacket', '')).name}",
|
||||
"art quality gate: unit-sprite-art-quality-gate.md",
|
||||
f"base status: {base.get('status', 'unknown')}",
|
||||
f"battle bbox: {base.get('battle_min_width_px')}..{base.get('battle_max_width_px')} x {base.get('battle_min_height_px')}..{base.get('battle_max_height_px')} px",
|
||||
"release gate: blocked until explicit approval",
|
||||
]
|
||||
write_text_block(draw, pad, 84, info_lines)
|
||||
|
||||
top_y = 230
|
||||
left_w = 888
|
||||
right_w = width - pad * 3 - left_w
|
||||
out.alpha_composite(fit_image(paths["Contact Sheet"], (left_w, 450), "Contact Sheet"), (pad, top_y))
|
||||
out.alpha_composite(fit_image(paths["50px / 72px Scale"], (right_w, 216), "50px / 72px Scale"), (pad * 2 + left_w, top_y))
|
||||
out.alpha_composite(fit_image(paths["Approved + Pending Board"], (right_w, 216), "Approved + Pending"), (pad * 2 + left_w, top_y + 234))
|
||||
|
||||
bottom_y = top_y + 474
|
||||
out.alpha_composite(fit_image(paths["Debug Battle Preview"], (width - pad * 2, 430), "Real Battle Debug Preview"), (pad, bottom_y))
|
||||
|
||||
command_y = bottom_y + 446
|
||||
draw.text((pad, command_y), "After explicit approval:", fill=(240, 226, 178, 255), font=font(15))
|
||||
commands = [
|
||||
f"python scripts\\promote-unit-sprite-approval.py --unit {stem} --expected-version {candidate.get('version')}",
|
||||
"python scripts\\audit-unit-sprite-sheets.py",
|
||||
"python scripts\\render-unit-sprite-review-boards.py",
|
||||
"python scripts\\check-unit-sprite-art-quality.py",
|
||||
"python scripts\\check-unit-sprite-approval-gate.py --mode release",
|
||||
]
|
||||
write_text_block(draw, pad, command_y + 28, commands, fill=(203, 214, 198, 255))
|
||||
|
||||
OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||
out.convert("RGB").save(OUTPUT_PATH, optimize=True)
|
||||
return OUTPUT_PATH
|
||||
|
||||
|
||||
def main() -> None:
|
||||
path = render_board()
|
||||
print(f"Wrote {path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user