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.
122 lines
5.1 KiB
Python
122 lines
5.1 KiB
Python
"""Exact clock-board geometry plus fixed-grid full-arm animation packing."""
|
|
from pathlib import Path
|
|
import argparse
|
|
import math
|
|
import random
|
|
import struct
|
|
from PIL import Image, ImageDraw, ImageFont
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
ART = ROOT / "assets/sprites/darts"
|
|
OUT = ROOT / "assets/animations/darts"
|
|
SECTORS = [20, 1, 18, 4, 13, 6, 10, 15, 2, 17, 3, 19, 7, 16, 8, 11, 14, 9, 12, 5]
|
|
|
|
|
|
def pack(name, frames):
|
|
width, height = frames[0].size
|
|
payload = b"".join(frame.tobytes("raw", "BGRA") for frame in frames)
|
|
(OUT / name).write_bytes(struct.pack("<4s4I", b"ANM1", width, height, len(frames), len(payload)) + payload)
|
|
|
|
|
|
def board():
|
|
rng = random.Random(301)
|
|
scale = 2
|
|
center = 256*scale
|
|
image = Image.new("RGBA", (512*scale, 512*scale))
|
|
pixels = image.load()
|
|
for y in range(image.height):
|
|
for x in range(image.width):
|
|
dx, dy = (x-center)/scale, (center-y)/scale
|
|
radius = math.hypot(dx, dy)
|
|
angle = math.atan2(dx, dy)
|
|
sector = int(((angle + math.pi/20) % math.tau) / (math.tau/20))
|
|
red = sector % 2 == 0
|
|
color = (30, 36, 39)
|
|
if radius < 170:
|
|
color = (23, 29, 31) if red else (232, 225, 201)
|
|
if 99 <= radius <= 107 or 162 <= radius:
|
|
color = (191, 42, 55) if red else (24, 145, 103)
|
|
if radius <= 15.9:
|
|
color = (24, 145, 103)
|
|
if radius <= 6.35:
|
|
color = (191, 42, 55)
|
|
elif radius < 220:
|
|
color = (10, 14, 17)
|
|
elif radius < 240:
|
|
color = (72, 29, 35)
|
|
elif radius < 248:
|
|
color = (31, 37, 41)
|
|
else:
|
|
continue
|
|
grain = rng.randrange(-2, 3)
|
|
pixels[x, y] = tuple(max(0, min(255, c+grain)) for c in color) + (255,)
|
|
draw = ImageDraw.Draw(image)
|
|
wire = (139, 158, 161, 255)
|
|
for r in (6.35, 15.9, 99, 107, 162, 170):
|
|
bounds = tuple(round(v*scale) for v in (256-r, 256-r, 256+r, 256+r))
|
|
draw.ellipse(bounds, outline=wire, width=2*scale)
|
|
for index in range(20):
|
|
theta = (index*18-9)*math.pi/180
|
|
ends = [(round((256+math.sin(theta)*r)*scale),
|
|
round((256-math.cos(theta)*r)*scale)) for r in (16,170)]
|
|
draw.line(ends, fill=wire, width=2*scale)
|
|
font = ImageFont.truetype("C:/Windows/Fonts/arialbd.ttf", 32*scale)
|
|
labels = []
|
|
for index, value in enumerate(SECTORS):
|
|
theta = index*math.tau/20
|
|
point = ((256+math.sin(theta)*198)*scale, (256-math.cos(theta)*198)*scale)
|
|
labels.append(draw.textbbox(point, str(value), font=font, anchor="mm"))
|
|
draw.text(point, str(value),
|
|
font=font, fill=(249, 247, 232), anchor="mm")
|
|
for i, a in enumerate(labels):
|
|
for b in labels[i+1:]:
|
|
assert a[2] <= b[0] or b[2] <= a[0] or a[3] <= b[1] or b[3] <= a[1], "Overlapping board numbers"
|
|
draw.ellipse((12*scale, 12*scale, 500*scale, 500*scale),
|
|
outline=(104, 114, 120), width=3*scale)
|
|
image = image.resize((512,512), Image.Resampling.LANCZOS)
|
|
image.save(ART / "board-v2.png")
|
|
return image
|
|
|
|
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--board-only", action="store_true")
|
|
args = parser.parse_args()
|
|
ART.mkdir(parents=True, exist_ok=True)
|
|
OUT.mkdir(parents=True, exist_ok=True)
|
|
pack("board-v2.bin", [board()])
|
|
if args.board_only:
|
|
print("Packed sharper 512px board: unchanged footprint and scoring radii.")
|
|
return
|
|
wall = Image.open(ART / "lounge-wall-v1.png").convert("RGBA")
|
|
pack("lounge-wall-v1.bin", [wall.resize((512,512), Image.Resampling.LANCZOS)])
|
|
anchors = []
|
|
for name in ("arm-throw-v3", "dart-views-v1"):
|
|
source = ART / (name + ".png")
|
|
sheet = Image.open(source).convert("RGBA")
|
|
assert sheet.width == sheet.height
|
|
cell = sheet.width / 4
|
|
frames = [sheet.crop(tuple(round(v*cell) for v in (x, y, x+1, y+1)))
|
|
.resize((256, 256), Image.Resampling.LANCZOS) for y in range(4) for x in range(4)]
|
|
assert all(f.getchannel("A").getextrema()[0] == 0 for f in frames)
|
|
assert len({f.tobytes() for f in frames}) == 16
|
|
pack(name + ".bin", frames)
|
|
if name == "dart-views-v1":
|
|
for frame in frames:
|
|
metal = [(x,y) for y in range(256) for x in range(256)
|
|
if (lambda p: p[3] > 220 and min(p[:3]) > 50
|
|
and max(p[:3])-min(p[:3]) < 60)(frame.getpixel((x,y)))]
|
|
tip_x = min(x for x,y in metal)
|
|
ys = sorted(y for x,y in metal if x <= tip_x+2)
|
|
anchors.append((tip_x, ys[len(ys)//2]))
|
|
(ROOT / "src_graphics/darts_art.inc").write_text(
|
|
"; Generated per-view needle anchors in the 256px source canvas.\n"
|
|
+ "dtDartTipX3D dd " + ",".join(str(p[0]) for p in anchors) + "\n"
|
|
+ "dtDartTipY3D dd " + ",".join(str(p[1]) for p in anchors) + "\n", encoding="ascii")
|
|
print("Packed exact 20-sector board; whole-arm animation uses one fixed canvas and scale.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|