KAZIK/tools/generators/pack_hall_assets.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

155 lines
7.6 KiB
Python

"""Offline alpha/gutter normalization for the MASM hall animation packs."""
import json
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont
from pack_arrival_assets import solid_alpha, write_pack
import pack_arrival_assets as packs
ROOT = Path(__file__).resolve().parents[2]
SOURCE = ROOT / "assets/sprites/hall"
OUTPUT = ROOT / "assets/animations/hall"
def extract(name, columns, rows):
source = solid_alpha(Image.open(SOURCE / name).convert("RGBA"))
row_spans, start = [], None
alpha = source.getchannel("A")
for y in range(source.height + 1):
occupied = y < source.height and alpha.crop((0, y, source.width, y + 1)).getbbox()
if occupied and start is None:
start = y
if not occupied and start is not None:
if y - start > 30:
row_spans.append((start, y))
start = None
if len(row_spans) != rows:
row_spans = [(row * source.height // rows, (row + 1) * source.height // rows)
for row in range(rows)]
sprites = []
for row, (top, bottom) in enumerate(row_spans):
strip = source.crop((0, top, source.width, bottom))
mask = strip.getchannel("A")
spans, start = [], None
for x in range(strip.width + 1):
occupied = x < strip.width and mask.crop((x, 0, x + 1, strip.height)).getbbox()
if occupied and start is None:
start = x
if not occupied and start is not None:
if x - start > 24:
spans.append((start, x))
start = None
assert len(spans) == columns, (name, row, spans)
for left, right in spans:
sprite = strip.crop((left, 0, right, strip.height))
sprites.append(sprite.crop(sprite.getchannel("A").getbbox()))
return sprites
def normalize(sprites, size, common=True):
scale = min((size[0] - 8) / max(s.width for s in sprites),
(size[1] - 8) / max(s.height for s in sprites))
frames = []
for sprite in sprites:
factor = scale if common else min((size[0] - 8) / sprite.width,
(size[1] - 8) / sprite.height)
sprite = sprite.resize((round(sprite.width * factor), round(sprite.height * factor)),
Image.Resampling.NEAREST)
frame = Image.new("RGBA", size)
frame.alpha_composite(sprite, ((size[0] - sprite.width) // 2, size[1] - 4 - sprite.height))
frames.append(frame)
return frames
def main():
OUTPUT.mkdir(parents=True, exist_ok=True)
packs.OUTPUT = OUTPUT
props = extract("props-v1.png", 3, 3)
# A detached cue below the table is a separate sheet item, not its floor anchor.
alpha = props[4].getchannel("A")
spans, start = [], None
for y in range(alpha.height + 1):
occupied = y < alpha.height and alpha.crop((0, y, alpha.width, y + 1)).getbbox()
if occupied and start is None:
start = y
if not occupied and start is not None:
spans.append((start, y))
start = None
top, bottom = max(spans, key=lambda span: span[1] - span[0])
props[4] = props[4].crop((0, top, props[4].width, bottom))
frames = {
"bartender.bin": normalize(extract("bartender-v1.png", 4, 4), (256, 384)),
"props.bin": normalize(props, (256, 256), False),
"drink.bin": normalize(extract("drink-hand-v1.png", 4, 2), (256, 256)),
}
patrons = extract("patrons-v2.png", 4, 4)
frames["patrons.bin"] = (normalize(patrons[:4], (192, 320)) +
normalize(patrons[4:8], (192, 320)) +
normalize(patrons[8:], (192, 320), False))
materials = Image.open(SOURCE / "room-materials-v2.png").convert("RGBA")
surfaces = []
for row in range(3):
for column in range(4):
tile = materials.crop((column * materials.width // 4, row * materials.height // 3,
(column + 1) * materials.width // 4, (row + 1) * materials.height // 3))
tile = tile.crop((3, 3, tile.width - 3, tile.height - 3))
surfaces.append(tile.resize((128, 128), Image.Resampling.LANCZOS))
frames["room-materials.bin"] = surfaces
frames["room-materials-hd.bin"] = []
architecture = Image.open(SOURCE / "trader-architecture-v3.png").convert("RGBA")
frames["architecture.bin"], frames["architecture-hd.bin"] = [], []
for source, low, high in ((materials, None, "room-materials-hd.bin"),
(architecture, "architecture.bin", "architecture-hd.bin")):
for row in range(3):
for column in range(4):
tile = source.crop((column * source.width // 4, row * source.height // 3,
(column + 1) * source.width // 4, (row + 1) * source.height // 3))
tile = tile.crop((3, 3, tile.width - 3, tile.height - 3))
if low:
frames[low].append(tile.resize((128, 128), Image.Resampling.LANCZOS))
frames[high].append(tile.resize((256, 256), Image.Resampling.LANCZOS))
# Individual bounds keep the bay-window finial and desk lamp, which cross nominal rows.
prop_source = solid_alpha(Image.open(SOURCE / "architecture-props-v3.png").convert("RGBA"))
bounds = [(0, 0, 560, 525), (560, 0, 1030, 525), (1030, 0, 1536, 590),
(0, 580, 515, 1024), (515, 555, 1040, 1024), (1040, 590, 1536, 1024)]
decor = []
for index, bound in enumerate(bounds):
item = prop_source.crop(bound)
item = item.crop(item.getchannel("A").getbbox())
if index == 4:
item = item.crop((0, 0, item.width, round(item.height * .78)))
decor.append(item)
frames["architecture-props.bin"] = normalize(decor, (256, 256), False)
for name, items in frames.items():
write_pack(name, items)
preview = Image.new("RGBA", (items[0].width * 4, items[0].height * ((len(items) + 3) // 4)), (24, 24, 26, 255))
for index, frame in enumerate(items):
preview.alpha_composite(frame, ((index % 4) * frame.width, (index // 4) * frame.height))
preview.save(ROOT / "build/hall" / (name + ".png"))
sign = Image.new("RGBA", (384, 128), (54, 17, 23, 255))
draw = ImageDraw.Draw(sign)
draw.rectangle((2, 2, 381, 125), outline=(182, 146, 72), width=5)
font = ImageFont.truetype("C:/Windows/Fonts/arialbd.ttf", 77)
box = draw.textbbox((0, 0), "CASINO", font=font)
draw.text(((384 - (box[2] - box[0])) // 2, (128 - (box[3] - box[1])) // 2 - box[1]),
"CASINO", font=font, fill=(252, 227, 153), stroke_width=1)
write_pack("sign.bin", [sign.crop((i * 128, 0, (i + 1) * 128, 128)) for i in range(3)])
sign.save(ROOT / "build/hall/sign.png")
tiles = Image.open(ROOT / "assets/sprites/arrival/city-tiles-pixel-v4.png").convert("RGBA")
detailed = []
for row in range(4):
for column in range(4):
tile = tiles.crop((column * tiles.width // 4, row * tiles.height // 4,
(column + 1) * tiles.width // 4, (row + 1) * tiles.height // 4))
inset = max(2, min(tile.size) // 80)
detailed.append(tile.crop((inset, inset, tile.width - inset, tile.height - inset))
.resize((256, 256), Image.Resampling.NEAREST))
write_pack("city-detail.bin", detailed)
(OUTPUT / "manifest.json").write_text(json.dumps({name: {"frames": len(items), "size": items[0].size}
for name, items in frames.items()}, indent=2), encoding="utf-8")
print("Packed bartender (16), hall props (9), drinking hand (8)")
if __name__ == "__main__":
main()