KAZIK/tools/generators/pack_fortune_session.py
Artvasil09 23dc40ee5c feat: implement native darts mini-game, AI observer mode, and fortune session assets
- 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
2026-10-24 22:10:25 +03:00

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