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.
248 lines
13 KiB
Python
248 lines
13 KiB
Python
"""Build the native Buckshot Roulette first-person scene animation atlas."""
|
|
from pathlib import Path
|
|
import random
|
|
|
|
from PIL import Image, ImageDraw, ImageFilter
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
W, H = 640, 480
|
|
FRAME_COUNT = 8
|
|
|
|
|
|
def polygon(draw, points, fill, outline=(12, 12, 13, 255), width=2):
|
|
draw.polygon(points, fill=fill)
|
|
if outline:
|
|
draw.line(points + [points[0]], fill=outline, width=width, joint="curve")
|
|
|
|
|
|
def base_scene(seed):
|
|
rng = random.Random(seed)
|
|
image = Image.new("RGBA", (W, H), (10, 12, 15, 255))
|
|
d = ImageDraw.Draw(image)
|
|
|
|
# Paneled back wall, dim red leather and a single overhead pool of light.
|
|
for y in range(0, 174):
|
|
shade = 17 + y // 20
|
|
d.line((0, y, W, y), fill=(shade + 6, shade, shade - 2, 255))
|
|
for x in range(-12, W + 20, 54):
|
|
d.rectangle((x, 20, x + 44, 150), fill=(38, 25, 23, 255),
|
|
outline=(83, 55, 38, 255), width=2)
|
|
d.line((x + 5, 25, x + 5, 147), fill=(19, 17, 18, 255), width=2)
|
|
d.line((x + 38, 24, x + 38, 148), fill=(91, 61, 41, 255), width=2)
|
|
d.line((x + 2, 36, x + 41, 36), fill=(67, 42, 34, 255), width=2)
|
|
d.rectangle((0, 148, W, 176), fill=(22, 17, 16, 255))
|
|
d.line((0, 151, W, 151), fill=(154, 105, 48, 255), width=3)
|
|
d.line((0, 173, W, 173), fill=(76, 50, 35, 255), width=2)
|
|
|
|
# Table perspective, leather rail and worn green felt.
|
|
polygon(d, [(0, 171), (640, 171), (640, 480), (0, 480)],
|
|
(14, 47, 37, 255), outline=(7, 13, 13, 255), width=3)
|
|
polygon(d, [(0, 172), (640, 172), (640, 200), (0, 200)],
|
|
(77, 47, 30, 255), outline=(126, 87, 47, 255), width=2)
|
|
d.line((0, 198, 640, 198), fill=(180, 133, 69, 255), width=4)
|
|
d.line((0, 204, 640, 204), fill=(35, 74, 54, 255), width=2)
|
|
for _ in range(11000):
|
|
x, y = rng.randrange(W), rng.randrange(206, H)
|
|
green = rng.choice((0, 1, 2))
|
|
color = ((17 + green * 2, 54 + green * 4, 40 + green * 2, 255)
|
|
if rng.random() < .76 else (35, 65, 48, 255))
|
|
d.point((x, y), fill=color)
|
|
for x in range(-200, 900, 94):
|
|
d.line((320 + (x - 320) // 3, 205, x, 480), fill=(22, 59, 45, 110), width=1)
|
|
|
|
# Dealer's dark suit and hands across the far rail give the scene a human scale.
|
|
polygon(d, [(219, 43), (248, 21), (391, 21), (421, 47), (409, 145),
|
|
(226, 145)], (29, 33, 38, 255), outline=(10, 12, 15, 255), width=3)
|
|
d.polygon([(294, 29), (319, 58), (345, 29), (337, 137), (301, 137)],
|
|
fill=(97, 31, 32, 255))
|
|
d.ellipse((291, 20, 347, 77), fill=(133, 91, 69, 255), outline=(54, 39, 34, 255), width=3)
|
|
d.ellipse((302, 38, 309, 44), fill=(13, 15, 17, 255))
|
|
d.ellipse((329, 38, 336, 44), fill=(13, 15, 17, 255))
|
|
d.line((309, 58, 329, 58), fill=(53, 29, 27, 255), width=3)
|
|
for hand_x in (138, 492):
|
|
d.rounded_rectangle((hand_x - 27, 127, hand_x + 30, 169), 14,
|
|
fill=(126, 84, 63, 255), outline=(56, 40, 34, 255), width=2)
|
|
for finger in range(4):
|
|
fx = hand_x - 22 + finger * 14
|
|
d.line((fx, 144, fx + 1, 169), fill=(174, 119, 88, 255), width=5)
|
|
d.line((fx + 2, 150, fx + 2, 164), fill=(74, 48, 42, 255), width=1)
|
|
|
|
# Brass lamp glow and scattered shells/chips around the play area.
|
|
glow = Image.new("RGBA", (W, H), (0, 0, 0, 0))
|
|
gd = ImageDraw.Draw(glow)
|
|
gd.ellipse((236, -78, 405, 127), fill=(245, 176, 79, 36))
|
|
glow = glow.filter(ImageFilter.GaussianBlur(36))
|
|
image.alpha_composite(glow)
|
|
d = ImageDraw.Draw(image)
|
|
d.ellipse((282, 8, 359, 35), fill=(108, 70, 32, 255), outline=(220, 169, 84, 255), width=3)
|
|
d.ellipse((295, 13, 346, 29), fill=(255, 204, 104, 255))
|
|
for x, y, color in ((76, 284, (174, 54, 45, 255)), (548, 300, (188, 150, 65, 255)),
|
|
(90, 331, (205, 150, 64, 255)), (560, 257, (146, 49, 37, 255))):
|
|
d.ellipse((x - 10, y - 4, x + 10, y + 5), fill=color, outline=(232, 189, 108, 255), width=2)
|
|
d.ellipse((x - 7, y - 2, x + 7, y + 3), fill=(49, 37, 29, 255))
|
|
return image
|
|
|
|
|
|
def draw_shell(draw, x, y, scale=1.0, brass=(177, 48, 37, 255)):
|
|
h, w = int(48 * scale), int(15 * scale)
|
|
draw.rounded_rectangle((x, y, x + w, y + h), max(2, int(4 * scale)),
|
|
fill=(111, 34, 29, 255), outline=(29, 20, 18, 255), width=2)
|
|
draw.rectangle((x + 1, y + h - 10 * scale, x + w - 1, y + h - 4 * scale),
|
|
fill=brass, outline=(235, 183, 85, 255))
|
|
draw.ellipse((x + 2, y + 2, x + w - 2, y + 9 * scale),
|
|
fill=(190, 68, 47, 255), outline=(230, 132, 85, 255), width=1)
|
|
draw.line((x + 4, y + 11, x + 4, y + h - 13), fill=(232, 123, 86, 190), width=1)
|
|
|
|
|
|
def draw_gun(image, stage):
|
|
layer = Image.new("RGBA", (W, H), (0, 0, 0, 0))
|
|
d = ImageDraw.Draw(layer)
|
|
recoil = 12 if stage in (5, 6, 7) else 0
|
|
dx, dy = -recoil, recoil // 2
|
|
|
|
# Walnut stock with layered grain, shoulder pad, cheek contour and brass inlay.
|
|
stock = [(142 + dx, 387 + dy), (187 + dx, 350 + dy), (250 + dx, 364 + dy),
|
|
(281 + dx, 394 + dy), (226 + dx, 444 + dy), (169 + dx, 434 + dy)]
|
|
polygon(d, stock, (104, 55, 31, 255), (24, 19, 17, 255), 5)
|
|
d.line([(151 + dx, 391 + dy), (185 + dx, 365 + dy), (236 + dx, 375 + dy),
|
|
(263 + dx, 395 + dy)], fill=(180, 111, 55, 255), width=4, joint="curve")
|
|
d.line([(174 + dx, 420 + dy), (215 + dx, 427 + dy), (254 + dx, 399 + dy)],
|
|
fill=(65, 34, 22, 255), width=3, joint="curve")
|
|
d.ellipse((184 + dx, 385 + dy, 206 + dx, 407 + dy), fill=(202, 158, 74, 255),
|
|
outline=(52, 36, 23, 255), width=3)
|
|
d.ellipse((191 + dx, 392 + dy, 199 + dx, 400 + dy), fill=(53, 39, 27, 255))
|
|
d.polygon([(139 + dx, 388 + dy), (151 + dx, 379 + dy), (166 + dx, 430 + dy),
|
|
(154 + dx, 437 + dy)], fill=(43, 42, 43, 255), outline=(151, 151, 145, 255))
|
|
for i in range(5):
|
|
d.line((158 + dx + i * 4, 395 + dy, 171 + dx + i * 4, 428 + dy),
|
|
fill=(105, 105, 99, 255), width=2)
|
|
|
|
# Receiver and trigger guard.
|
|
receiver = [(230 + dx, 353 + dy), (280 + dx, 328 + dy), (326 + dx, 345 + dy),
|
|
(321 + dx, 377 + dy), (273 + dx, 397 + dy), (237 + dx, 382 + dy)]
|
|
polygon(d, receiver, (55, 59, 60, 255), (14, 16, 18, 255), 4)
|
|
d.polygon([(240 + dx, 357 + dy), (281 + dx, 337 + dy), (314 + dx, 349 + dy),
|
|
(310 + dx, 360 + dy), (273 + dx, 376 + dy), (241 + dx, 371 + dy)],
|
|
fill=(103, 106, 104, 255))
|
|
d.line((246 + dx, 360 + dy, 302 + dx, 344 + dy), fill=(191, 190, 171, 255), width=3)
|
|
d.ellipse((273 + dx, 364 + dy, 281 + dx, 372 + dy), fill=(210, 166, 75, 255))
|
|
d.arc((258 + dx, 371 + dy, 297 + dx, 412 + dy), 5, 170, fill=(188, 163, 105, 255), width=4)
|
|
d.line((272 + dx, 378 + dy, 274 + dx, 390 + dy), fill=(206, 181, 117, 255), width=4)
|
|
|
|
# Twin blued-steel barrels and ribbing; the muzzles separate visibly in perspective.
|
|
hinge_x, hinge_y = 307 + dx, 347 + dy
|
|
muzzle_x, muzzle_y = 472 + dx, 201 + dy
|
|
if stage in (1, 2):
|
|
muzzle_x, muzzle_y = 430 + dx, 176 + dy
|
|
for offset in (-7, 7):
|
|
start = (hinge_x, hinge_y + offset)
|
|
end = (muzzle_x, muzzle_y + offset * 2)
|
|
d.line((start, end), fill=(17, 19, 21, 255), width=16)
|
|
d.line((start, end), fill=(70, 76, 78, 255), width=10)
|
|
d.line((start[0] + 3, start[1] - 2, end[0] - 4, end[1] - 2),
|
|
fill=(177, 182, 175, 255), width=2)
|
|
d.line((start[0], start[1] + 5, end[0] - 4, end[1] + 5),
|
|
fill=(30, 33, 35, 255), width=2)
|
|
d.line((330 + dx, 326 + dy, 446 + dx, 218 + dy), fill=(126, 133, 131, 255), width=4)
|
|
d.line((341 + dx, 324 + dy, 351 + dx, 332 + dy), fill=(192, 169, 111, 255), width=4)
|
|
for bx, by in ((452 + dx, 218 + dy), (464 + dx, 207 + dy)):
|
|
d.ellipse((bx - 10, by - 7, bx + 10, by + 7), fill=(12, 14, 16, 255),
|
|
outline=(154, 159, 153, 255), width=3)
|
|
d.ellipse((bx - 5, by - 4, bx + 5, by + 4), fill=(2, 4, 5, 255),
|
|
outline=(71, 78, 79, 255), width=1)
|
|
if stage in (1, 2):
|
|
# Hinged breech, visible ejectors and open chamber mouths.
|
|
d.line((308 + dx, 347 + dy, 368 + dx, 298 + dy), fill=(201, 174, 109, 255), width=5)
|
|
d.ellipse((301 + dx, 341 + dy, 319 + dx, 359 + dy), fill=(207, 166, 76, 255),
|
|
outline=(30, 25, 20, 255), width=3)
|
|
image.alpha_composite(layer)
|
|
|
|
|
|
def draw_hands(image, stage):
|
|
d = ImageDraw.Draw(image)
|
|
if stage in (1, 2, 3):
|
|
# Left hand feeds brass-capped red shells into the opened breech.
|
|
polygon(d, [(31, 401), (48, 371), (85, 344), (111, 350), (121, 372),
|
|
(101, 395), (74, 416), (46, 430)], (155, 105, 79, 255), (56, 39, 32, 255), 3)
|
|
for i in range(3):
|
|
d.line((54 + i * 12, 392 - i * 5, 82 + i * 10, 365 - i * 4),
|
|
fill=(203, 151, 119, 255), width=10)
|
|
d.line((57 + i * 12, 391 - i * 5, 83 + i * 10, 366 - i * 4),
|
|
fill=(87, 55, 45, 255), width=2)
|
|
if stage in (2, 3):
|
|
draw_shell(d, 360, 296, .85)
|
|
draw_shell(d, 382, 280, .85)
|
|
else:
|
|
draw_shell(d, 372, 290, .8)
|
|
elif stage in (0, 4, 5, 6, 7):
|
|
# Right hand wraps the grip; knuckles, creases and cuff are individually shaded.
|
|
polygon(d, [(430, 480), (413, 436), (399, 407), (397, 384), (409, 374),
|
|
(420, 392), (419, 365), (432, 353), (443, 365), (447, 392),
|
|
(453, 365), (467, 359), (478, 373), (473, 400), (485, 381),
|
|
(499, 384), (504, 402), (490, 432), (477, 461), (472, 480)],
|
|
(176, 119, 88, 255), (60, 40, 32, 255), 3)
|
|
d.polygon([(423, 458), (451, 447), (480, 453), (480, 480), (430, 480)],
|
|
fill=(24, 29, 35, 255), outline=(9, 12, 15, 255))
|
|
for x, y in ((421, 389), (447, 390), (469, 389), (491, 403)):
|
|
d.line((x, y, x + 12, y + 8), fill=(226, 169, 130, 255), width=2)
|
|
d.line((x + 4, y + 10, x + 15, y + 18), fill=(92, 59, 48, 255), width=2)
|
|
if stage == 5:
|
|
# Muzzle flash layered behind the weapon, with hot core and flying sparks.
|
|
flash = Image.new("RGBA", (W, H), (0, 0, 0, 0))
|
|
fd = ImageDraw.Draw(flash)
|
|
fd.polygon([(457, 206), (500, 171), (492, 198), (548, 182), (516, 210),
|
|
(558, 224), (505, 222), (530, 254), (488, 235), (480, 267),
|
|
(469, 235)], fill=(255, 130, 43, 220))
|
|
fd.ellipse((460, 195, 503, 232), fill=(255, 229, 148, 245))
|
|
for sx, sy in ((544, 189), (558, 218), (535, 246), (517, 166), (492, 161)):
|
|
fd.ellipse((sx - 3, sy - 3, sx + 3, sy + 3), fill=(255, 202, 90, 255))
|
|
image.alpha_composite(flash)
|
|
elif stage == 6:
|
|
smoke = Image.new("RGBA", (W, H), (0, 0, 0, 0))
|
|
sd = ImageDraw.Draw(smoke)
|
|
for cx, cy, r in ((477, 207, 17), (501, 187, 20), (523, 168, 24), (542, 145, 18)):
|
|
sd.ellipse((cx-r, cy-r, cx+r, cy+r), fill=(177, 184, 180, 82))
|
|
image.alpha_composite(smoke.filter(ImageFilter.GaussianBlur(8)))
|
|
|
|
|
|
def render_frame(stage):
|
|
image = base_scene(1200 + stage)
|
|
d = ImageDraw.Draw(image)
|
|
# Recessed ceiling light and a readable pool of light fall on the gun/table.
|
|
d.polygon([(0, 160), (190, 168), (320, 190), (462, 168), (640, 156),
|
|
(640, 194), (0, 194)], fill=(21, 52, 40, 80))
|
|
for i in range(6):
|
|
x = 45 + i * 108
|
|
d.ellipse((x, 178, x + 15, 182), fill=(226, 184, 103, 180))
|
|
if stage == 3:
|
|
# A brief push-in on the shells at the moment they seat in the chambers.
|
|
d.line((343, 287, 379, 258), fill=(219, 170, 86, 255), width=3)
|
|
if stage in (1, 2, 3):
|
|
draw_hands(image, stage)
|
|
draw_gun(image, stage)
|
|
if stage in (0, 4, 5, 6, 7):
|
|
draw_hands(image, stage)
|
|
d = ImageDraw.Draw(image)
|
|
# Vignette is subtle and hand-built so pixel detail remains crisp.
|
|
vignette = Image.new("RGBA", (W, H), (0, 0, 0, 0))
|
|
vd = ImageDraw.Draw(vignette)
|
|
vd.rectangle((0, 0, W - 1, H - 1), outline=(2, 4, 6, 65), width=26)
|
|
image.alpha_composite(vignette)
|
|
return image
|
|
|
|
|
|
def main():
|
|
out_dir = ROOT / "assets/animations/buckshot"
|
|
out_dir.mkdir(parents=True, exist_ok=True)
|
|
frames = [render_frame(i) for i in range(FRAME_COUNT)]
|
|
atlas = Image.new("RGBA", (W * 4, H * 2), (8, 10, 12, 255))
|
|
for i, frame in enumerate(frames):
|
|
atlas.alpha_composite(frame, ((i % 4) * W, (i // 4) * H))
|
|
atlas.save(out_dir / "buckshot-atlas-v1.png", optimize=True)
|
|
(out_dir / "buckshot-scenes.bin").write_bytes(
|
|
b"".join(frame.tobytes("raw", "BGRA") for frame in frames))
|
|
print(f"Generated {FRAME_COUNT} {W}x{H} BGRA scenes in {out_dir}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|