KAZIK/tools/generators/pack_dice_assets.py
Игорь Федосеев ca62db53e9 feat: implement native billiards, scratch tickets, first-person dice, and city wake scene
- Billiards: add native Russian Pyramid & American Eight-Ball pool with physics prediction, controls, and regression tests
- Scratch Tickets: add dedicated counter, mat presentation, elliptical coin scraping, 4 ticket types, and payout effects
- First-Person Dice: add native dice scene, hand anchor frames, throwing animation, and table perspective
- City Wake & Weather: add arrival cutscene, alley geometry, weather/rain effects, and lighter/rocker animation
- World Dressing: add hall paintings, furniture packs, hall lighting, and scene regression tests
- Build & Tooling: update build.bat for modern MSVC toolchain, update launcher hash, and refresh shipping binaries
2026-10-16 12:37:40 +03:00

66 lines
2.7 KiB
Python

"""Offline hand normalization; dice geometry and all animation remain MASM."""
from pathlib import Path
import struct
import json
from collections import deque
from PIL import Image
from pack_city_wake import figures
ROOT = Path(__file__).resolve().parents[2]
def dice_bounds(frame):
points = {(x,y) for y in range(frame.height) for x in range(frame.width)
if (lambda p: p[3] == 255 and p[0] > 210 and p[1] > 200 and p[2] > 180)(frame.getpixel((x,y)))}
cubes = []
while points:
start = points.pop()
queue = deque([start])
body = [start]
while queue:
x,y = queue.popleft()
for at in ((x-1,y),(x+1,y),(x,y-1),(x,y+1)):
if at in points:
points.remove(at)
queue.append(at)
body.append(at)
if len(body) >= 100:
xs,ys = zip(*body)
cubes.append((min(xs),min(ys),max(xs)+1,max(ys)+1))
assert len(cubes) == 2, cubes
return sorted(cubes)
def main():
# Reuse the connected-silhouette recovery without nominal grid clipping.
import pack_city_wake
pack_city_wake.SRC = ROOT / "assets/sprites/dice"
bodies = figures("hands-v2.png")
scale = min(240/max(b.width for b in bodies),300/max(b.height for b in bodies))
frames = []
for body in bodies:
body = body.resize((round(body.width*scale),round(body.height*scale)),Image.Resampling.NEAREST)
frame = Image.new("RGBA",(256,320))
frame.paste(body,(256-body.width,320-body.height))
frames.append(frame)
data = b"".join(f.tobytes("raw","BGRA") for f in frames)
(ROOT / "assets/animations/dice/hands-v2.bin").write_bytes(struct.pack("<4s4I",b"ANM1",256,320,16,len(data))+data)
frames[0].save(ROOT / "build/dice/hand-ready.png")
bounds = [dice_bounds(f) for f in frames[:4]]
# Release geometry starts at the centres of the last held pose, in the
# same logical coordinate system as the MASM cube renderer.
anchors = [((320+(b[0]+b[2])//2)//2,(90+(b[1]+b[3])//2)//2) for b in bounds[-1]]
source = "; Generated by pack_dice_assets.py from the last held pose.\n"
for i,(x,y) in enumerate(anchors,1):
source += f"DICE_RELEASE_X{i} EQU {x}\nDICE_RELEASE_Y{i} EQU {y}\n"
(ROOT / "src_graphics/dice_anchors.inc").write_text(source,encoding="ascii")
(ROOT / "build/dice/held-bounds.json").write_text(json.dumps(bounds,indent=2))
sheet = Image.new("RGBA",(1024,1280))
for i,frame in enumerate(frames):
sheet.paste(frame,((i%4)*256,(i//4)*320))
sheet.save(ROOT / "build/dice/hands-normalized.png")
print("Packed sixteen full-cuff poses, two palm-contact dice and measured release anchors",anchors)
if __name__ == "__main__":
main()