- 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
66 lines
2.7 KiB
Python
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()
|