KAZIK/tools/generators/pack_fortune_orb.py
Artvasil09 41ba8f46ed feat: add rooftop city and optimize native terrace rendering
Add rooftop elevator, lounge, volumetric city, river and animated traffic. Prune invisible mesh faces, clip CPU surfaces, streamline compositing and add automatic/F8 roof quality profiles with renderer diagnostics.

Include dartboard/reticle improvements, tracked fortune orb numerals and fate-session visuals, native regression tests, compatible jukebox IDs and updated shipping executables.
2026-10-26 23:20:35 +03:00

102 lines
4.5 KiB
Python

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