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.
80 lines
3.5 KiB
Python
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()
|