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

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()