Add native assembly scenes and assets for Slots (777), Coin flip, and European Roulette; add 3D hall furniture packs and prompts; improve Russian UI strings in CP1251; update launcher hash and rebuild shipping binaries.
110 lines
4.8 KiB
Python
110 lines
4.8 KiB
Python
"""Normalize generated art offline; all toss animation and play are MASM."""
|
|
import hashlib
|
|
import json
|
|
from collections import deque
|
|
from pathlib import Path
|
|
|
|
from PIL import Image
|
|
import pack_arrival_assets as packs
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
SOURCE = ROOT / "assets/sprites/coin"
|
|
|
|
|
|
def isolated_hand(frame):
|
|
"""Remove disconnected previous-row cuff spill without changing the hand anchor."""
|
|
width, height = frame.size
|
|
alpha = frame.getchannel("A").tobytes()
|
|
visited = bytearray(width * height)
|
|
largest = []
|
|
for seed, value in enumerate(alpha):
|
|
if not value or visited[seed]:
|
|
continue
|
|
visited[seed] = 1
|
|
queue = deque([seed])
|
|
component = []
|
|
while queue:
|
|
at = queue.popleft()
|
|
component.append(at)
|
|
x, y = at % width, at // width
|
|
for dx, dy in ((-1, 0), (1, 0), (0, -1), (0, 1)):
|
|
nx, ny = x + dx, y + dy
|
|
if 0 <= nx < width and 0 <= ny < height:
|
|
neighbor = ny * width + nx
|
|
if alpha[neighbor] and not visited[neighbor]:
|
|
visited[neighbor] = 1
|
|
queue.append(neighbor)
|
|
if len(component) > len(largest):
|
|
largest = component
|
|
mask = bytearray(width * height)
|
|
for at in largest:
|
|
mask[at] = 255
|
|
frame.putalpha(Image.frombytes("L", frame.size, bytes(mask)))
|
|
return frame
|
|
|
|
|
|
def main():
|
|
packs.OUTPUT = ROOT / "assets/animations/coin"
|
|
preview = ROOT / "build/coin"
|
|
preview.mkdir(parents=True, exist_ok=True)
|
|
background = Image.open(SOURCE / "table-v1.png").convert("RGBA")
|
|
background = background.resize((640, 480), Image.Resampling.NEAREST)
|
|
# Reserve the renderer's 40-pixel header without cropping the dealer's head.
|
|
framed = Image.new("RGBA", (640, 480), (20, 25, 29, 255))
|
|
framed.paste(background.crop((0, 0, 640, 440)), (0, 40))
|
|
background = framed
|
|
background.putalpha(255)
|
|
packs.write_pack("table-v1.bin", [background])
|
|
background.save(preview / "table.png")
|
|
source = Image.open(SOURCE / "faces-v1.png").convert("RGBA")
|
|
assert source.getchannel("A").getextrema()[0] == 0
|
|
faces = []
|
|
for column in range(2):
|
|
frame = source.crop((column * source.width // 2, 0,
|
|
(column + 1) * source.width // 2, source.height))
|
|
frame = frame.crop(frame.getchannel("A").getbbox())
|
|
frame = frame.resize((128, 128), Image.Resampling.NEAREST)
|
|
frame = packs.solid_alpha(frame)
|
|
faces.append(frame)
|
|
frame.save(preview / ("face-" + str(column) + ".png"))
|
|
packs.write_pack("faces-v1.bin", faces)
|
|
source = Image.open(SOURCE / "hands-v2.png").convert("RGBA")
|
|
assert source.getchannel("A").getextrema()[0] == 0
|
|
hands = []
|
|
held_centers = [(79, 70), (76, 77), (74, 82), (77, 96), (70, 98)]
|
|
for row in range(4):
|
|
for column in range(4):
|
|
frame = source.crop((column * source.width // 4, row * source.height // 4,
|
|
(column + 1) * source.width // 4, (row + 1) * source.height // 4))
|
|
# Keep common cell scale rather than stretching each pose's alpha bounds.
|
|
frame = frame.resize((224, 224), Image.Resampling.NEAREST)
|
|
frame = packs.solid_alpha(frame)
|
|
# Remove the generator's saturated red alpha-matte fringe, not skin highlights.
|
|
frame.putdata([(r, g, b, 0 if r > 120 and g < 50 and b < 50 else a)
|
|
for r, g, b, a in frame.get_flattened_data()])
|
|
index = len(hands)
|
|
if index >= len(held_centers):
|
|
frame = isolated_hand(frame)
|
|
if index < len(held_centers):
|
|
# The held coin and fingers share one sprite and the release-point anchor.
|
|
cx, cy = held_centers[index]
|
|
aligned = Image.new("RGBA", (224, 224))
|
|
aligned.paste(frame, (88 - cx, 24 - cy))
|
|
frame = aligned
|
|
frame.putdata([(r, g, b, a) if a else (0, 0, 0, 0)
|
|
for r, g, b, a in frame.get_flattened_data()])
|
|
assert frame.getchannel("A").getbbox()
|
|
hands.append(frame)
|
|
frame.save(preview / ("hand-" + str(len(hands) - 1) + ".png"))
|
|
assert len({hashlib.sha256(frame.tobytes()).hexdigest() for frame in hands}) == 16
|
|
packs.write_pack("hands-v2.bin", hands)
|
|
(packs.OUTPUT / "manifest-v1.json").write_text(json.dumps({
|
|
"table": [640, 480, 1], "faces": [128, 128, 2], "hands": [224, 224, 16],
|
|
"hand_anchor": "held coin (88,24); empty hand shared grid cell",
|
|
"held_frames": 5, "alpha": "opaque table, binary cutouts"
|
|
}, indent=2) + "\n", encoding="ascii")
|
|
print("Packed coin table, two embossed sides and sixteen distinct hand poses")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|