"""Offline orb tracking metadata and antialiased numeral sprites; no runtime Python.""" from pathlib import Path import json import struct import numpy as np from PIL import Image, ImageDraw, ImageFilter, ImageFont ROOT = Path(__file__).resolve().parents[2] OUT = ROOT / "assets/animations/fortune" ART = ROOT / "assets/sprites/fortune" PREVIEW = ROOT / "build/hall-expansion" def load_orbs(): raw = (OUT / "orb-mist-v1.bin").read_bytes() magic, width, height, count, length = struct.unpack_from("<4s4I", raw) assert magic == b"ANM1" and count == 16 assert length == width * height * count * 4 == len(raw) - 20 stride = width * height * 4 return [Image.frombytes("RGBA", (width, height), raw[20+i*stride:20+(i+1)*stride], "raw", "BGRA") for i in range(count)] def sphere_geometry(frame): # Fit the upper glass silhouette, explicitly excluding the metal pedestal. alpha = np.asarray(frame.getchannel("A")) rows, midpoints, half_widths = [], [], [] for y in range(12, round(frame.height * .70)): xs = np.flatnonzero(alpha[y] >= 170) if len(xs) < frame.width * .35: continue left, right = int(xs[0]), int(xs[-1]) rows.append(y) midpoints.append((left + right) / 2) half_widths.append((right - left) / 2) a, b, c = np.polyfit(rows, np.square(half_widths), 2) assert a < 0 cy = -b / (2*a) cx = float(np.median(midpoints)) rx = float(np.sqrt(c - b*b/(4*a))) ry = float(rx / np.sqrt(-a)) assert 70 < cx < 125 and 60 < cy < 115 assert 55 < rx < 95 and 55 < ry < 95 return {"cx": cx, "cy": cy, "rx": rx, "ry": ry} def numeral(digit, font): factor = 4 mask = Image.new("L", (32*factor, 48*factor)) draw = ImageDraw.Draw(mask) box = draw.textbbox((0, 0), str(digit), font=font) x = (mask.width-(box[2]-box[0]))/2-box[0] y = (mask.height-(box[3]-box[1]))/2-box[1] draw.text((round(x), round(y)), str(digit), font=font, fill=255) mask = mask.resize((32, 48), Image.Resampling.LANCZOS) shadow = Image.new("RGBA", mask.size, (8, 30, 66, 0)) shadow.putalpha(mask.filter(ImageFilter.MaxFilter(3)).point(lambda a: round(a*.65))) halo = Image.new("RGBA", mask.size, (65, 168, 243, 0)) halo.putalpha(mask.filter(ImageFilter.GaussianBlur(2.0)).point(lambda a: round(a*.65))) ink = Image.new("RGBA", mask.size, (200, 235, 255, 0)) ink.putalpha(mask.point(lambda a: round(a*.90))) return Image.alpha_composite(Image.alpha_composite(halo, shadow), ink) def main(): PREVIEW.mkdir(parents=True, exist_ok=True) orbs = load_orbs() geometry = [sphere_geometry(frame) for frame in orbs] # Q8 offsets in the exact 128px runtime drawing rectangle, not its centre. lines = ["; Generated by tools/generators/pack_fortune_orb.py; glass only, pedestal excluded."] for name, key in (("fortuneOrbAnchorX3D", "cx"), ("fortuneOrbAnchorY3D", "cy")): values = [round(item[key] * 128 / 192 * 256) for item in geometry] lines.append(name + " dd " + ",".join(map(str, values))) (ROOT / "src_graphics/fortune_orb_art.inc").write_text("\n".join(lines)+"\n", encoding="ascii") font = ImageFont.truetype("C:/Windows/Fonts/cambria.ttc", 34*4) digits = [numeral(i, font) for i in range(10)] payload = b"".join(frame.tobytes("raw", "BGRA") for frame in digits) (OUT / "orb-numerals-v1.bin").write_bytes(struct.pack("<4s4I", b"ANM1", 32, 48, 10, len(payload)) + payload) sheet = Image.new("RGBA", (320, 48)) for i, digit in enumerate(digits): sheet.alpha_composite(digit, (32*i, 0)) sheet.save(ART / "orb-numerals-v1.png") (ART / "orb-tracking-v1.json").write_text(json.dumps({"canvas": [192, 192], "font": "Cambria Regular, rasterized at 4x; no font needed at runtime", "frames": geometry}, indent=2)+"\n", encoding="ascii") sheet = Image.new("RGB", (192*4, 192*4), (5, 15, 28)) for i, (frame, item) in enumerate(zip(orbs, geometry)): debug = frame.copy() draw = ImageDraw.Draw(debug) x, y = item["cx"], item["cy"] draw.ellipse((x-item["rx"], y-item["ry"], x+item["rx"], y+item["ry"]), outline="yellow") draw.line((x-5, y, x+5, y), fill="yellow") draw.line((x, y-5, x, y+5), fill="yellow") sheet.paste(debug, ((i%4)*192, (i//4)*192), debug) sheet.save(PREVIEW / "fortune-orb-tracking.png") print("Packed 10 antialiased Cambria numerals and 16 measured glass-centre anchors.") if __name__ == "__main__": main()