Files
aetherbound-guild/tools/build_bat002_reconstruction_package.py
T

210 lines
9.8 KiB
Python

#!/usr/bin/env python3
"""Build deterministic BAT-002 reconstruction evidence from accepted assets.
This tool creates review artifacts only: a layer-ownership board, a
color-coded dynamic-region audit mask and an asset provenance contact sheet.
It never edits or generates player-facing art.
"""
from __future__ import annotations
import argparse
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont
ROOT = Path(__file__).resolve().parents[1]
PAGE = ROOT / "docs/design-authority/pages/BAT-002-live-battle"
APPROVED = PAGE / "approved/v001"
RECON = PAGE / "reconstruction/v001"
COLORS = {
"world": (47, 137, 171),
"rails": (111, 80, 166),
"actors": (204, 75, 84),
"event": (215, 139, 36),
"text": (31, 40, 47),
"controls": (46, 132, 101),
"panel": (245, 247, 244),
"ink": (23, 35, 43),
}
def font(size: int, bold: bool = False):
candidates = [
"/System/Library/Fonts/Supplemental/Arial Bold.ttf"
if bold
else "/System/Library/Fonts/Supplemental/Arial.ttf",
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf"
if bold
else "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
]
for candidate in candidates:
path = Path(candidate)
if path.is_file():
return ImageFont.truetype(str(path), size)
return ImageFont.load_default()
REGIONS = [
("01", "PAINTED FIELD", [0.0, 0.0, 1.0, 1.0], "world", "static_master"),
("02", "TOP STATUS / PLAYBACK RAILS", [0.0, 0.0, 1.0, 0.125], "rails", "static_master + runtime"),
("03", "OBJECTIVE + URGENT CAUSE", [0.025, 0.135, 0.31, 0.13], "text", "runtime"),
("04", "COMPLETE ALLIED LINE", [0.03, 0.36, 0.52, 0.36], "actors", "runtime"),
("05", "CENTRAL ACTION GAP", [0.44, 0.28, 0.22, 0.42], "event", "runtime"),
("06", "COMPLETE ENEMY LINE", [0.56, 0.29, 0.42, 0.43], "actors", "runtime"),
("07", "EVENT / VALUE PROJECTION", [0.03, 0.25, 0.94, 0.16], "event", "runtime"),
("08", "BOTTOM ACTOR-STATE RAIL", [0.0, 0.80, 1.0, 0.20], "controls", "runtime"),
]
def fit(image: Image.Image, width: int, height: int, background: str = "#182830") -> Image.Image:
copy = image.convert("RGBA")
copy.thumbnail((width, height), Image.Resampling.LANCZOS)
canvas = Image.new("RGB", (width, height), background)
canvas.paste(copy, ((width - copy.width) // 2, (height - copy.height) // 2), copy)
return canvas
def wrap_text(text: str, limit: int) -> list[str]:
words = text.split()
lines: list[str] = []
current = ""
for word in words:
candidate = f"{current} {word}".strip()
if current and len(candidate) > limit:
lines.append(current)
current = word
else:
current = candidate
if current:
lines.append(current)
return lines
def build_board(source: Path, output: Path) -> None:
original = Image.open(source).convert("RGB")
image_w = 1160
image_h = round(original.height * image_w / original.width)
margin = 28
panel_w = 520
board = Image.new("RGB", (image_w + panel_w + margin * 3, max(image_h + margin * 2, 780)), COLORS["panel"])
board.paste(original.resize((image_w, image_h), Image.Resampling.LANCZOS), (margin, margin))
draw = ImageDraw.Draw(board, "RGBA")
draw.rectangle((margin, margin, margin + image_w, margin + image_h), outline=COLORS["ink"], width=3)
for number, label, bounds, color_name, owner in REGIONS:
x, y, width, height = bounds
left = margin + round(x * image_w)
top = margin + round(y * image_h)
right = margin + round((x + width) * image_w)
bottom = margin + round((y + height) * image_h)
color = COLORS[color_name]
draw.rectangle((left, top, right, bottom), outline=color + (220,), width=4)
draw.rounded_rectangle((left + 8, top + 8, left + min(340, max(150, 16 * len(label) + 78)), top + 40), 5, fill=color + (225,))
draw.text((left + 16, top + 13), f"{number} {label}", fill=(255, 255, 255), font=font(16, True))
panel_x = margin * 2 + image_w
draw.text((panel_x, margin), "BAT-002 / DECONSTRUCTION", fill=COLORS["text"], font=font(24, True))
draw.text((panel_x, margin + 39), "Approved composition is a coordinate reference; state stays in Godot.", fill=(92, 108, 114), font=font(13))
y = margin + 82
details = {
"01": "Existing Windglass field; editable static base, no actors or HUD baked.",
"02": "Continuous compact rails; shell may be rebuilt, values and actions are engine-owned.",
"03": "Objective, tick, rule, current cause and waiting/error copy are engine text.",
"04": "Exact ordered Party snapshot; ordinary scale and front-right position 0 stay visible.",
"05": "Forecast, source-to-target path, contact and protection projection; no drag lane.",
"06": "Exact committed enemy offer, including legal boss footprint and facing relation.",
"07": "Floating values and event sequence reserve; effects never cover actor anchors.",
"08": "Timer/rule plus ordered actor state cells; commands use separate transparent hit areas.",
}
for number, label, _bounds, color_name, owner in REGIONS:
color = COLORS[color_name]
draw.rectangle((panel_x, y + 2, panel_x + 34, y + 34), fill=color)
draw.text((panel_x + 8, y + 8), number, fill=(255, 255, 255), font=font(14, True))
draw.text((panel_x + 48, y), label, fill=COLORS["text"], font=font(14, True))
detail_lines = wrap_text(f"{owner}: {details[number]}", 54)
for line_index, line in enumerate(detail_lines):
draw.text((panel_x + 48, y + 21 + line_index * 14), line, fill=(92, 108, 114), font=font(10))
y += max(58, 25 + 14 * len(detail_lines))
y += 8
draw.text((panel_x, y), "FROZEN ANTI-GOALS", fill=COLORS["text"], font=font(15, True))
y += 26
for line in (
"Flattened approved PNG as Runtime background",
"Sparse four-actor or oversized ordinary-unit tableau",
"Separate event-card wall or parchment diagnostic panel",
"Baked names, values, controls, targets or pseudo-text",
"Pixel conversion, nearest-neighbor scaling or post-process pixelation",
):
draw.text((panel_x, y), "- " + line, fill=(92, 108, 114), font=font(11))
y += 24
output.parent.mkdir(parents=True, exist_ok=True)
board.save(output, format="PNG", optimize=True)
def build_mask(output: Path) -> None:
image = Image.new("RGB", (2048, 1152), (18, 29, 36))
draw = ImageDraw.Draw(image)
colors = {"world": (34, 56, 66), "rails": (115, 74, 165), "actors": (198, 67, 77), "event": (215, 139, 36), "text": (44, 137, 171), "controls": (46, 132, 101)}
for _number, _label, bounds, color_name, owner in REGIONS:
if owner == "static_master":
continue
x, y, width, height = bounds
rect = (round(x * 2048), round(y * 1152), round((x + width) * 2048) - 1, round((y + height) * 1152) - 1)
draw.rectangle(rect, fill=colors[color_name], outline=(235, 235, 220), width=2)
output.parent.mkdir(parents=True, exist_ok=True)
image.save(output, format="PNG", optimize=True)
def build_contact_sheet(output: Path) -> None:
assets = [
("approved master / composition authority", APPROVED / "master.png"),
("scale proof / complete-line scale", APPROVED / "scale-proof.png"),
("clean static field / reused unchanged", RECON / "clean-static-master.png"),
("dynamic-region ownership mask", RECON / "dynamic-regions.png"),
("hearthguard / accepted Recruit", ROOT / "runtime/assets/actors/hearthguard.png"),
("wayrunner / accepted Recruit", ROOT / "runtime/assets/actors/wayrunner.png"),
("farstring / accepted Recruit", ROOT / "runtime/assets/actors/farstring.png"),
("kindhand / accepted Recruit", ROOT / "runtime/assets/actors/kindhand.png"),
("riftblade idle / enemy family", ROOT / "runtime/assets/enemies/riftblade/idle.png"),
("riftblade attack / enemy family", ROOT / "runtime/assets/enemies/riftblade/attack.png"),
("protect absorb / existing VFX", ROOT / "runtime/assets/vfx/protect_absorb.png"),
("damage heavy / existing VFX", ROOT / "runtime/assets/vfx/damage_heavy.png"),
]
width, tile_w, tile_h, margin = 1640, 390, 300, 32
rows = (len(assets) + 3) // 4
height = 110 + rows * (tile_h + 58) + margin
sheet = Image.new("RGB", (width, height), "#182830")
draw = ImageDraw.Draw(sheet)
draw.text((margin, 22), "BAT-002 ASSET CONTACT SHEET", fill="#f5e3a9", font=font(29, True))
draw.text((margin, 62), "No new image generation. Existing accepted assets and engine-owned projections only.", fill="#d3e0e2", font=font(14))
for index, (label, path) in enumerate(assets):
x = margin + (index % 4) * tile_w
y = 104 + (index // 4) * (tile_h + 58)
draw.rectangle((x, y, x + tile_w - 14, y + tile_h + 34), fill="#223943", outline="#b68c4b", width=2)
if path.is_file():
sheet.paste(fit(Image.open(path), tile_w - 28, tile_h - 28), (x + 7, y + 7))
draw.text((x + 12, y + tile_h + 8), label, fill="#f1eee0", font=font(12, True))
output.parent.mkdir(parents=True, exist_ok=True)
sheet.save(output, format="PNG", optimize=True)
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--source", type=Path, default=APPROVED / "master.png")
parser.add_argument("--board", type=Path, default=RECON / "deconstruction-board.png")
parser.add_argument("--mask", type=Path, default=RECON / "dynamic-regions.png")
parser.add_argument("--contact", type=Path, default=RECON / "asset-contact-sheet.png")
args = parser.parse_args()
build_board(args.source, args.board)
build_mask(args.mask)
build_contact_sheet(args.contact)
print(f"BAT002_RECONSTRUCTION_ARTIFACTS_OK board={args.board} mask={args.mask} contact={args.contact}")
if __name__ == "__main__":
main()