- Darts: native 500-point match against Max with sway/fatigue mouse aim, parabolic throw camera, raster arm animations, and 16 camera-relative flying dart views - AI Observer: autonomous spectator mode for Hold'em, Blackjack, Durak, and Billiards with profile isolation and playback controls - Fortune Session: raster sprite packs, mist/orb animations, tarot divination spreads, and pack tools - Hall Integration: wall dartboard in lounge, observer terminal dispatch, and bot turn hooks - Packaging & Verification: updated installer streaming hash, synchronized launcher, passing check suites and release smoke checks
63 lines
2.8 KiB
Python
63 lines
2.8 KiB
Python
"""Offline sprite-sheet compiler; runtime animation stays in MASM."""
|
|
from pathlib import Path
|
|
import json
|
|
import struct
|
|
from PIL import Image
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
ART = ROOT / "assets/sprites/fortune"
|
|
OUT = ROOT / "assets/animations/fortune"
|
|
|
|
|
|
def grid(name, columns, rows, size):
|
|
source = Image.open(ART / name).convert("RGBA")
|
|
return [source.crop((round(x*source.width/columns), round(y*source.height/rows),
|
|
round((x+1)*source.width/columns), round((y+1)*source.height/rows)))
|
|
.resize(size, Image.Resampling.LANCZOS)
|
|
for y in range(rows) for x in range(columns)]
|
|
|
|
|
|
def tween(frames, loop=False):
|
|
# One shared canvas/scale, never fit moving hands independently per frame.
|
|
output = []
|
|
for i, frame in enumerate(frames):
|
|
following = frames[(i+1) % len(frames)] if loop or i+1 < len(frames) else frame
|
|
output.extend(Image.blend(frame, following, step/4) for step in range(4))
|
|
return output
|
|
|
|
|
|
def pack(name, frames):
|
|
width, height = frames[0].size
|
|
assert len(frames) <= 64
|
|
assert all(frame.size == (width, height) for frame in frames)
|
|
payload = b"".join(frame.tobytes("raw", "BGRA") for frame in frames)
|
|
(OUT / (name + ".bin")).write_bytes(
|
|
struct.pack("<4s4I", b"ANM1", width, height, len(frames), len(payload)) + payload)
|
|
return {"file": name + ".bin", "width": width, "height": height, "frames": len(frames)}
|
|
|
|
|
|
def main():
|
|
OUT.mkdir(parents=True, exist_ok=True)
|
|
body = grid("teller-actions-v1.png", 8, 5, (256, 256))
|
|
cards = grid("tarot-symbols-v1.png", 6, 4, (96, 128))
|
|
# Orientations are real upside-down artwork, not merely a different label.
|
|
faces = cards[:22] + [c.transpose(Image.Transpose.ROTATE_180) for c in cards[:22]] + [cards[22]]
|
|
room = Image.open(ART / "seated-room-v1.png").convert("RGBA").resize((640, 480), Image.Resampling.LANCZOS)
|
|
manifest = [pack("room-v1", [room]),
|
|
pack("shuffle-v1", tween(body[:16], loop=True)),
|
|
pack("deal-v1", tween(body[16:24])),
|
|
pack("gesture-v1", tween(body[24:32], loop=True)),
|
|
pack("orb-hands-v1", tween(body[32:40], loop=True)),
|
|
pack("orb-mist-v1", grid("crystal-mist-v1.png", 4, 4, (192, 192))),
|
|
pack("cards-v1", faces)]
|
|
(ART / "packed-v1.json").write_text(json.dumps(manifest, indent=2) + "\n", encoding="ascii")
|
|
preview = room.copy()
|
|
preview.alpha_composite(body[0].resize((280, 250)), (134, 32))
|
|
for index in range(3):
|
|
preview.alpha_composite(cards[22].resize((72, 96)), (198+index*100, 302))
|
|
preview.save(ROOT / "build/hall-expansion/fortune-session-art.png")
|
|
print("Packed 160 hand poses, 16 mist frames, 44 oriented faces, back and seated room.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|