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.
155 lines
7.6 KiB
Python
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()
|