Approve Shu infantry handpainted sprite v8

This commit is contained in:
2026-07-01 10:40:38 +09:00
parent 7b27d7aa9a
commit 199bc47192
51 changed files with 10323 additions and 1 deletions

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