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