KAZIK/tools/generators/pack_roulette_scenes.py
Artvasil09 3ad5bf4c9d Develop assembly-only casino scenes and native games
Add native Durak, poker, blackjack, roulette, Buckshot and billiards scenes with animation assets and regression tests. Improve arrival, city, hall, profiles, shops, audio and exit handling; remove the portable C game core and refresh shipping executables.
2026-10-08 22:39:19 +03:00

80 lines
3.5 KiB
Python

"""Offline packer for the native MASM roulette sprite renderer."""
from pathlib import Path
from collections import deque
from PIL import Image
ROOT = Path(__file__).resolve().parents[2]
SIZE = 256
def cells(path):
image = Image.open(path).convert("RGBA")
for index in range(8):
col, row = index % 4, index // 4
yield image.crop((round(col * image.width / 4),
round(row * image.height / 2),
round((col + 1) * image.width / 4),
round((row + 1) * image.height / 2)))
def main():
sprites = ROOT / "assets/sprites/roulette"
frames = []
weapon_cells = list(cells(sprites / "revolver-actions-v1.png"))
for frame_index, image in enumerate(weapon_cells):
# Remove tiny disconnected atlas fragments, retaining muzzle flash components.
pixels = bytearray(image.getchannel("A").point(lambda a: 255 if a >= 128 else 0).tobytes())
clean = bytearray(len(pixels))
components = []
for start, opacity in enumerate(pixels):
if not opacity:
continue
component, queue = [], deque([start])
pixels[start] = 0
while queue:
index = queue.popleft()
component.append(index)
x, y = index % image.width, index // image.width
neighbors = []
if x: neighbors.append(index - 1)
if x + 1 < image.width: neighbors.append(index + 1)
if y: neighbors.append(index - image.width)
if y + 1 < image.height: neighbors.append(index + image.width)
for neighbor in neighbors:
if pixels[neighbor]:
pixels[neighbor] = 0
queue.append(neighbor)
components.append(component)
largest = max(map(len, components), default=0)
for component in components:
if len(component) == largest or (frame_index == 5 and len(component) >= largest * 0.07):
for index in component:
clean[index] = 255
image.putalpha(Image.frombytes("L", image.size, bytes(clean)))
# Match the runtime alpha test so faint atlas noise cannot alter the anchor.
bounds = image.getchannel("A").point(lambda a: 255 if a >= 128 else 0).getbbox()
if bounds is None:
raise ValueError("Empty revolver frame")
image = image.crop(bounds)
image.thumbnail((238, 238), Image.Resampling.LANCZOS)
frame = Image.new("RGBA", (SIZE, SIZE))
frame.alpha_composite(image, ((SIZE - image.width) // 2,
(SIZE - image.height) // 2))
frames.append(frame)
for image in cells(sprites / "gman-talk-v1.png"):
frame = image.resize((SIZE, SIZE), Image.Resampling.LANCZOS)
frame.putalpha(255)
frames.append(frame)
target = ROOT / "assets/animations/roulette/roulette-scenes.bin"
target.parent.mkdir(parents=True, exist_ok=True)
target.write_bytes(b"".join(frame.tobytes("raw", "BGRA") for frame in frames))
assert target.stat().st_size == 16 * SIZE * SIZE * 4
contact = Image.new("RGBA", (4 * SIZE, 4 * SIZE), (20, 32, 30, 255))
for index, frame in enumerate(frames):
contact.alpha_composite(frame, ((index % 4) * SIZE, (index // 4) * SIZE))
contact.convert("RGB").save(ROOT / "build/actions/roulette-contact.png")
print(f"Packed {len(frames)} frames: {target}")
if __name__ == "__main__":
main()