Files
airdef/tools/gen_models.py
T
2026-10-30 15:07:54 +03:00

1713 lines
70 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Генерирует контент мода AirDef (airdef-mod/assets и data): текстуры, модели частей, предметы.
Каждая часть — блок со своей моделью в одной ориентации: нос смотрит на север (-z), центр — (8, 8, 8).
Поворачивает и масштабирует их движок форка (render.set-models), так что ракурсы больше не нужны.
Координаты элементов — в пределах -16..32 (иначе Horizon отбрасывает модель).
Запуск: python3 tools/gen_models.py
"""
import json
import math
import os
import sys
import random
import shutil
import zlib
from PIL import Image
import gen_tex as GT
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", "airdef-mod"))
NS = "airdef"
# ---------------------------------------------------------------- текстуры
def clamp(v):
return max(0, min(255, int(v)))
def noise(base, amp, seed, size=16):
rnd = random.Random(seed)
im = Image.new("RGBA", (size, size))
for y in range(size):
for x in range(size):
d = rnd.randint(-amp, amp)
im.putpixel((x, y), tuple(clamp(c + d) for c in base) + (255,))
return im
def panels(base, seed, size=16, every=8):
im = noise(base, 4, seed, size)
line = tuple(clamp(c - 16) for c in base) + (255,)
lite = tuple(clamp(c + 10) for c in base) + (255,)
for i in range(size):
for k in range(0, size, every):
im.putpixel((i, k), line)
im.putpixel((k, i), line)
if k + 1 < size:
im.putpixel((i, k + 1), lite)
rnd = random.Random(seed + 99)
for _ in range(size // 2): # заклёпки
x, y = rnd.randrange(size), rnd.randrange(size)
im.putpixel((x, y), lite)
return im
def camo(colors, seed, size=32, blobs=7):
rnd = random.Random(seed)
im = noise(colors[0], 4, seed, size)
for col in colors[1:]:
for _ in range(blobs):
cx, cy = rnd.randrange(size), rnd.randrange(size)
rx, ry = rnd.randint(2, 6), rnd.randint(1, 4)
for y in range(cy - ry, cy + ry + 1):
for x in range(cx - rx, cx + rx + 1):
if ((x - cx) / rx) ** 2 + ((y - cy) / ry) ** 2 <= 1 + rnd.random() * 0.3:
im.putpixel((x % size, y % size), tuple(clamp(c + rnd.randint(-5, 5)) for c in col) + (255,))
return im
def stripes(base, line, step, seed, size=16):
im = noise(base, 3, seed, size)
for y in range(0, size, step):
for x in range(size):
im.putpixel((x, y), line + (255,))
return im
def gradient(top, bottom, size=16):
im = Image.new("RGBA", (size, size))
for y in range(size):
t = y / (size - 1)
c = tuple(clamp(top[i] + (bottom[i] - top[i]) * t) for i in range(3))
for x in range(size):
im.putpixel((x, y), c + (255,))
return im
def canister_front():
im = noise((92, 100, 72), 3, 11)
for cx in (4, 11):
for cy in (4, 11):
for y in range(16):
for x in range(16):
d = (x - cx + 0.5) ** 2 + (y - cy + 0.5) ** 2
if d <= 9:
im.putpixel((x, y), (36, 38, 30, 255))
elif d <= 12:
im.putpixel((x, y), (120, 124, 100, 255))
return im
def radar_face():
im = noise((70, 76, 56), 3, 17)
for y in range(1, 15):
for x in range(1, 15):
if (x + y) % 3 == 0:
im.putpixel((x, y), (54, 58, 44, 255))
return im
def glass():
im = noise((40, 58, 78), 5, 31)
for i in range(16):
im.putpixel((i, 15 - i), (130, 160, 180, 255))
if i < 15:
im.putpixel((i + 1, 15 - i), (100, 130, 150, 255))
return im
def missile():
im = noise((232, 232, 226), 3, 21)
for x in range(16):
for y in (3, 4):
im.putpixel((x, y), (40, 40, 40, 255))
return im
def target():
im = Image.new("RGBA", (16, 16))
for y in range(16):
for x in range(16):
d = abs(x - 7.5) + abs(y - 7.5)
im.putpixel((x, y), (255, clamp(40 + d * 12), 40, 255))
return im
def fire(seed):
rnd = random.Random(seed)
im = Image.new("RGBA", (16, 16))
for y in range(16):
for x in range(16):
t = rnd.random()
im.putpixel((x, y), (255, clamp(140 + 110 * t), clamp(30 + 120 * t * t), 255))
return im
def missile_ru():
im = noise((226, 228, 222), 3, 49)
for x in range(16):
im.putpixel((x, 5), (180, 40, 40, 255))
im.putpixel((x, 11), (40, 40, 40, 255))
return im
def tube_cap():
im = noise((80, 88, 60), 3, 50)
for y in range(16):
for x in range(16):
d = (x - 7.5) ** 2 + (y - 7.5) ** 2
if d <= 36:
im.putpixel((x, y), (44, 48, 36, 255))
elif d <= 48:
im.putpixel((x, y), (120, 128, 96, 255))
return im
def tubes6():
im = noise((82, 96, 60), 3, 51)
for cx in (3, 8, 13):
for cy in (4, 11):
for y in range(16):
for x in range(16):
if (x - cx) ** 2 + (y - cy) ** 2 <= 4:
im.putpixel((x, y), (30, 32, 26, 255))
return im
def radar_big():
im = noise((92, 100, 70), 3, 52)
for y in range(16):
for x in range(16):
if x % 2 == 0 and y % 2 == 0:
im.putpixel((x, y), (60, 66, 46, 255))
return im
TEXTURES = {
"shahed": lambda: panels((66, 68, 70), 1),
"shahed_nose": lambda: noise((104, 106, 108), 4, 2),
"geran": lambda: panels((28, 28, 31), 3),
"geran_engine": lambda: panels((60, 60, 64), 4),
"intake": lambda: noise((14, 14, 14), 2, 5),
"prop": lambda: noise((22, 22, 22), 3, 6),
"missile": missile,
"nozzle": lambda: noise((44, 42, 40), 4, 7),
"flame": lambda: gradient((255, 250, 210), (255, 120, 20)),
"fire": lambda: fire(8),
"nato_green": lambda: camo([(76, 86, 58), (58, 68, 44), (44, 48, 34)], 9),
"flecktarn": lambda: camo([(92, 100, 62), (62, 74, 44), (104, 84, 54), (30, 32, 26)], 10),
"track": lambda: stripes((36, 36, 34), (74, 74, 70), 3, 11),
"tire": lambda: stripes((24, 24, 24), (40, 40, 40), 4, 12),
"canister": lambda: panels((94, 102, 74), 13),
"canister_front": canister_front,
"radar_face": radar_face,
"glass": glass,
"metal": lambda: panels((118, 120, 122), 14),
"dark_metal": lambda: panels((52, 54, 52), 15),
"barrel": lambda: noise((40, 42, 38), 3, 16),
"dish": lambda: noise((176, 178, 174), 5, 18),
"target": target,
"foam": lambda: panels((196, 180, 150), 40),
"kh101": lambda: panels((84, 88, 92), 41),
"kh101_belly": lambda: noise((150, 152, 150), 4, 42),
"lancet": lambda: panels((70, 78, 66), 43),
"ru_green": lambda: camo([(82, 96, 60), (54, 70, 40), (110, 96, 64), (34, 36, 28)], 44, blobs=6),
"ru_khaki": lambda: camo([(108, 110, 80), (84, 90, 60), (60, 64, 44)], 45, blobs=5),
"missile_ru": missile_ru,
"missile_grey": lambda: panels((150, 154, 156), 46),
"booster": lambda: noise((60, 62, 58), 3, 47),
"tube": lambda: stripes((96, 106, 74), (70, 78, 54), 4, 48),
"tube_cap": tube_cap,
"tubes6": tubes6,
"radar_big": radar_big,
"tracer": lambda: noise((255, 40, 20), 6, 60),
"tracer_hot": lambda: noise((255, 190, 150), 6, 61),
"white": lambda: noise((250, 250, 246), 2, 19),
"nato_tan": lambda: camo([(150, 130, 96), (124, 106, 76), (96, 84, 62)], 62, blobs=5),
"crate": lambda: stripes((92, 78, 48), (70, 58, 34), 5, 63),
"concrete": lambda: panels((132, 130, 124), 64),
"lamp": lambda: gradient((255, 255, 240), (255, 230, 170)),
"beam": lambda: gradient((255, 250, 225), (255, 240, 200)),
"antenna": lambda: noise((70, 72, 70), 3, 65),
"nose_dark": lambda: noise((58, 60, 62), 3, 66),
}
RINGED = set()
def _ring(name, base, bands, seed, text=None):
RINGED.add(name)
TEXTURES[name] = lambda: GT.band_tex(base, bands, seed, text=text)
TEXTURES[name + "_t"] = lambda: GT.band_tex(base, bands, seed, transpose=True, text=text)
STRETCH_FN = {}
for _i, _k in enumerate(("nato_green", "flecktarn", "ru_green", "ru_khaki", "nato_tan")):
STRETCH_FN[_k] = (lambda sx, sy, k=_k, i=_i: GT.camo(k, 100 + i, 64, sx, sy))
TEXTURES[_k] = (lambda k=_k, i=_i: GT.camo(k, 100 + i, 64))
STRETCH_FN["track_side"] = lambda sx, sy: GT.track_side(7, 32, sx, sy)
PAL_RU, PAL_RU2 = GT.PAL["ru_green"][0], GT.PAL["ru_khaki"][0]
TEXTURES.update({
"track_side": lambda: GT.track_side(7),
"track": lambda: GT.track_tex(8),
"tire": lambda: GT.tire_tex(9),
"hub": lambda: GT.hub_tex(10),
"glass_cab": lambda: GT.glass_tex(11),
"bare_lamp": lambda: GT.glass_tex(12, tint=(200, 205, 190), refl=False),
"bare_barrel": lambda: GT.metal_tex((44, 46, 44), 13),
"bare_nozzle": lambda: GT.metal_tex((66, 58, 52), 14),
"bare_dish": lambda: GT.metal_tex((178, 180, 176), 15),
"bare_antenna": lambda: GT.metal_tex((112, 114, 112), 16),
"canister_front": lambda: GT.cap_tex(28),
"mk_ru": lambda: GT.marking("digits", PAL_RU, 40, "312"),
"mk_ru2": lambda: GT.marking("digits", PAL_RU2, 41, "507"),
"mk_us": lambda: GT.marking("digits", (76, 86, 58), 42, "1-7"),
"mk_star": lambda: GT.marking("star", PAL_RU, 43),
"mk_de": lambda: GT.marking("cross", (88, 98, 62), 44),
"glow_ember": lambda: GT.Canvas(16, 1, 1, (255, 170, 80), 17).out(),
"metal": lambda: GT.painted((118, 120, 122), 18, lift=1.15),
"dark_metal": lambda: GT.painted((52, 54, 52), 19, lift=1.1),
"canister": lambda: GT.painted((92, 102, 74), 20),
"shahed": lambda: GT.painted((92, 96, 98), 21, panel=(20, 10), lift=1.2),
"geran": lambda: GT.painted((28, 28, 31), 22, panel=(20, 10), lift=1.0),
"kh101": lambda: GT.painted((84, 88, 92), 23, panel=(18, 10)),
"lancet": lambda: GT.painted((74, 82, 70), 24),
"missile_grey": lambda: GT.painted((150, 154, 156), 25, lift=1.0),
"radar_face": lambda: GT.lattice(26, 32, (96, 104, 76), (170, 176, 150)),
"radar_big": lambda: GT.lattice(27, 32, (112, 124, 92), (190, 196, 168)),
})
_ring("rm_pac3", (228, 228, 222), [(0.12, 2, (30, 30, 30)), (0.78, 1.5, (200, 170, 40)), (0.9, 1, (30, 30, 30))], 30, [(8, 14, 10)])
_ring("rm_white", (232, 232, 226), [(0.1, 2, (200, 40, 36)), (0.82, 1.5, (40, 40, 44))], 31, [(10, 12, 8)])
_ring("rm_ru", (226, 228, 222), [(0.15, 2, (170, 40, 36)), (0.55, 1, (40, 40, 40)), (0.85, 2, (170, 40, 36))], 32, [(9, 20, 10)])
_ring("rm_grey", (148, 152, 154), [(0.2, 1.5, (210, 210, 205)), (0.8, 1.5, (210, 210, 205))], 33, [(6, 14, 12)])
_ring("rm_khaki", (110, 112, 84), [(0.25, 2, (60, 62, 48)), (0.7, 1.5, (200, 190, 150))], 34, [(8, 18, 8)])
_ring("rm_dark", (58, 60, 62), [(0.3, 1.5, (150, 150, 146)), (0.85, 1.5, (150, 150, 146))], 35)
# ---------------------------------------------------------------- модели
def el(frm, to, tex, faces=None, rot=None):
"""Элемент; rot = (ось, угол) — поворот вокруг центра элемента."""
return {"from": [float(v) for v in frm], "to": [float(v) for v in to], "tex": tex, "faces": faces, "rot": rot}
def mx(e):
"""Зеркальная копия относительно x = 8."""
rot = e.get("rot")
if rot and rot[0] in ("y", "z"):
rot = (rot[0], -rot[1])
return {**e, "from": [16 - e["to"][0], e["from"][1], e["from"][2]],
"to": [16 - e["from"][0], e["to"][1], e["to"][2]], "rot": rot}
def both(e):
return [e, mx(e)]
def face_uv(frm, to, face):
dx, dy, dz = (abs(to[i] - frm[i]) for i in range(3))
w, h = {"north": (dx, dy), "south": (dx, dy), "east": (dz, dy), "west": (dz, dy),
"up": (dx, dz), "down": (dx, dz)}[face]
return [0, 0, max(0.25, min(16, w)), max(0.25, min(16, h))]
VARIANTS = {}
TEXMAP = {"barrel": "bare_barrel", "nozzle": "bare_nozzle", "dish": "bare_dish", "antenna": "bare_antenna",
"glass": "glass_cab", "intake": "bare_nozzle"}
STRETCH = {"nato_green", "flecktarn", "ru_green", "ru_khaki", "nato_tan", "track_side"}
def _q(v):
return 1 if v < 1.5 else 2 if v < 2.5 else 3
def vname(tex, qx, qy, burnt):
tex = TEXMAP.get(tex, tex)
if burnt and tex.startswith("glow_"):
tex = "bare_nozzle"
n = tex
if tex in STRETCH and (qx > 1 or qy > 1):
n += f"_s{qx}{qy}"
if burnt:
n += "_b"
if n != tex:
VARIANTS[n] = (tex, qx, qy, burnt)
return n
def model(parts, burnt=False):
for p in parts:
for i in range(3):
assert -16 <= p["from"][i] <= 32 and -16 <= p["to"][i] <= 32, p
textures, elements = {}, []
for p in parts:
faces = {}
for f in ("north", "south", "east", "west", "up", "down"):
tex = (p["faces"] or {}).get(f, p["tex"])
uv = face_uv(p["from"], p["to"], f)
dx, dy, dz = (abs(p["to"][i] - p["from"][i]) for i in range(3))
w, h = {"north": (dx, dy), "south": (dx, dy), "east": (dz, dy), "west": (dz, dy),
"up": (dx, dz), "down": (dx, dz)}[f]
tex = vname(tex, _q(w / 16), _q(h / 16), burnt)
textures[tex] = f"{NS}:block/{tex}"
faces[f] = {"uv": uv, "texture": "#" + tex}
e = {"from": p["from"], "to": p["to"], "faces": faces}
if p.get("rot"):
axis, angle = p["rot"]
origin = [(p["from"][i] + p["to"][i]) / 2 for i in range(3)]
e["rotation"] = {"angle": angle, "axis": axis, "origin": origin}
elements.append(e)
textures["particle"] = next(iter(textures.values()))
return {"ambientocclusion": False, "textures": textures, "elements": elements}
def burnt_parts(parts, seed):
"""Остов: часть мелких деталей оторвана, антенны сорваны; текстуры — обгоревшие варианты (см. model(burnt=True))."""
rnd = random.Random(seed)
out = []
for i, p in enumerate(parts):
vol = 1.0
for k in range(3):
vol *= abs(p["to"][k] - p["from"][k])
if i > 2 and ((vol < 40 and rnd.random() < 0.3) or p["tex"] in ("antenna", "dish")):
continue
out.append(p)
return out
def taper(tex, z0, z1, w0, w1, h0, h1, steps, y=8.0):
"""Корпус-веретено лесенкой: ширина w0→w1 и высота h0→h1 вдоль z0→z1."""
out = []
for i in range(steps):
za = z0 + (z1 - z0) * i / steps
zb = z0 + (z1 - z0) * (i + 1) / steps
t = (i + 0.5) / steps
w, h = w0 + (w1 - w0) * t, h0 + (h1 - h0) * t
out.append(el((8 - w / 2, y - h / 2, za), (8 + w / 2, y + h / 2, zb), tex))
return out
def delta(tex, z_root, z_tip, z_trail, half, steps, y0, y1):
"""Дельта-крыло: передняя кромка от корня (z_root) к законцовке (half, z_tip), задняя кромка — z_trail."""
out = []
for i in range(steps):
za = z_root + (z_tip - z_root) * i / steps
s = half * (i + 1) / steps
out.append(el((8 - s, y0, za), (8 + s, y1, z_trail), tex))
return out
def shahed():
p = taper("shahed_nose", -16, -10, 2.5, 5, 2.5, 5, 3)
p += [el((5.5, 5.5, -10), (10.5, 10.5, 24), "shahed")]
p += [el((6, 10.5, -6), (10, 11.5, 4), "dark_metal")] # люк ГСН/антенны
p += delta("shahed", -2, 21, 26, 21, 9, 7.2, 8.4)
p += both(el((-13.6, 6.5, 17), (-12.6, 14, 26), "shahed")) # законцовки-кили
p += [el((6, 6, 24), (10, 10, 29), "dark_metal")] # двигатель
p += both(el((6.5, 4, 25), (7.5, 6, 28), "dark_metal")) # выхлопы
return p
def shahed_prop():
return [el((7.6, -4, 7.5), (8.4, 20, 8.5), "prop"), el((-4, 7.6, 7.5), (20, 8.4, 8.5), "prop"),
el((7, 7, 7), (9, 9, 9.5), "dark_metal")]
def geran3():
p = taper("geran", -16, -9, 2.5, 5.5, 2.5, 5.5, 3)
p += [el((5, 5, -9), (11, 11, 26), "geran")]
p += delta("geran", 0, 20, 26, 18, 8, 7.2, 8.4)
p += both(el((-10.6, 6.5, 18), (-9.6, 13, 26), "geran"))
p += [el((4.5, 11, 6), (11.5, 16, 28), "geran_engine"), # реактивный двигатель сверху
el((5, 11.5, 5), (11, 15.5, 6), "intake"),
el((5.5, 11.5, 28), (10.5, 15.5, 30), "nozzle")]
return p
def geran3_flame():
return taper("flame", 6, 24, 4, 1, 4, 1, 5)
def pac3():
p = taper("missile", -16, -11, 1, 3, 1, 3, 3)
p += [el((6.5, 6.5, -11), (9.5, 9.5, 26), "missile")]
p += both(el((1.5, 7.6, 20), (6.5, 8.4, 26), "missile"))
p += [el((7.6, 1.5, 20), (8.4, 6.5, 26), "missile"), el((7.6, 9.5, 20), (8.4, 14.5, 26), "missile")]
p += both(el((4.5, 7.7, -6), (6.5, 8.3, -2), "missile")) # рули ГСН
p += [el((7, 7, 26), (9, 9, 28), "nozzle")]
return p
def irist_missile():
"""IRIS-T SLM: тонкая белая ракета, две пары малых канардов у носа и четыре хвостовых киля."""
p = taper("white", -16, -10, 1, 3, 1, 3, 3)
p += [el((6.5, 6.5, -10), (9.5, 9.5, 24), "white")]
p += both(el((3, 7.6, -9), (6.5, 8.4, -5), "missile_grey")) # канарды у носа
p += [el((7.6, 3, -9), (8.4, 6.5, -5), "missile_grey"), el((7.6, 9.5, -9), (8.4, 13, -5), "missile_grey")]
p += both(el((2, 7.6, 18), (6.5, 8.4, 26), "white")) # хвостовые киля
p += [el((7.6, 2, 18), (8.4, 6.5, 26), "white"), el((7.6, 9.5, 18), (8.4, 14, 26), "white")]
p += [el((7, 7, 24), (9, 9, 28), "nozzle")]
return p
def amraam_missile():
"""AMRAAM: белое тело с тёмным носом, четыре крыла на середине корпуса и четыре хвостовых киля."""
p = taper("nose_dark", -16, -9, 1, 3, 1, 3, 3)
p += [el((6.5, 6.5, -9), (9.5, 9.5, 26), "white")]
p += both(el((1, 7.6, 2), (6.5, 8.4, 9), "white")) # среднекорпусные крылья
p += [el((7.6, 1, 2), (8.4, 6.5, 9), "white"), el((7.6, 9.5, 2), (8.4, 15, 9), "white")]
p += both(el((4, 7.6, 19), (6.5, 8.4, 25), "white")) # хвостовые киля
p += [el((7.6, 4, 19), (8.4, 6.5, 25), "white"), el((7.6, 9.5, 19), (8.4, 12, 25), "white")]
p += [el((7, 7, 26), (9, 9, 28), "nozzle")]
return p
def flame():
"""Факел двигателя: светящаяся «капля», хвост — на юг (+z)."""
return taper("flame", -8, 26, 5, 1, 5, 1, 8)
def patriot_launcher():
"""Пакет 4 ТПК M903. Ось качания — задняя нижняя кромка (z = 30, y = 0)."""
p = [el((-6, 1, -14), (22, 19, 30), "canister", {"north": "canister_front"})]
p += [el((-6.5, 9.5, -14), (22.5, 10.5, 30), "dark_metal"),
el((7.5, 1, -14), (8.5, 19, 30), "dark_metal"),
el((-7, 0, 24), (23, 2, 30), "metal")]
p += both(el((-8, 2, -10), (-6, 17, -6), "metal")) + both(el((-8, 2, 20), (-6, 17, 24), "metal"))
return p
def patriot_truck():
"""Тягач (HEMTT): кабина, рама, колёса. Длина 3 блока при масштабе 1."""
p = [el((-6, 5, -16), (22, 9, 32), "nato_green"),
el((-6, 9, -15), (22, 24, -1), "nato_green"),
el((-5, 16, -16), (21, 23, -15), "glass"),
el((-6.2, 16, -13), (-6, 23, -4), "glass"), el((22, 16, -13), (22.2, 23, -4), "glass"),
el((-6, 9, 2), (22, 15, 16), "nato_green"),
el((-2, 24, -12), (18, 25, -4), "dark_metal"),
el((2, 9, 20), (14, 13, 30), "metal")]
for z in (-12, 14, 23):
p += wheel(-9, -5.5, z + 4)
return p
def patriot_trailer():
p = [el((-7, 6, -16), (23, 11, 32), "nato_green"),
el((-1, 11, 14), (17, 16, 30), "metal"),
el((-8, 3, -16), (24, 7, -12), "dark_metal")]
for z in (14, 23):
p += wheel(-10, -6.5, z + 4)
p += both(el((-9, 0, -6), (-7, 6, -4), "metal")) # опоры
return p
def patriot_radar():
"""РЛС AN/MPQ-65: фазированная решётка на прицепе."""
p = [el((-6, 6, -10), (22, 10, 26), "nato_green"),
el((-4, 10, -6), (20, 26, 8), "nato_green"),
el((-5, 12, -7), (21, 30, -6), "radar_face"),
el((-2, 10, 10), (18, 18, 24), "metal")]
for z in (14,):
p += wheel(-9, -5.5, z + 4)
return p
def gepard_hull():
p = [el((-10, 4, -12), (26, 17, 30), "flecktarn"),
el((-10, 4, -16), (26, 12, -12), "flecktarn"),
el((-8, 17, 18), (24, 18, 30), "dark_metal"),
el((-6, 17, -10), (4, 18, -4), "metal")]
p += both(el((-14, 0, -14), (-10, 10, 31), "track"))
p += both(el((-14.5, 6, -14), (-10, 11, 31), "flecktarn")) # надгусеничные полки
return p
def gepard_turret():
p = [el((-4, 0, -6), (20, 12, 16), "flecktarn"),
el((-2, 12, -2), (18, 14, 14), "flecktarn"),
el((3, 4, -10), (13, 12, -6), "radar_face"), # РЛС сопровождения
el((7, 14, 10), (9, 18, 12), "metal")]
return p
def gepard_guns():
"""Две 35-мм пушки по бортам башни; ось качания — центр модели."""
p = both(el((-8, 5, -16), (-5.5, 7.5, 12), "barrel"))
p += both(el((-9, 3.5, 2), (-4, 9, 14), "flecktarn"))
p += both(el((-8.5, 4.5, -16), (-5, 8, -13), "dark_metal")) # дульные тормоза
return p
def gepard_dish():
"""Обзорная РЛС: вращается вокруг вертикальной оси."""
return [el((-4, 6, 6), (20, 14, 8), "dish"), el((7, 0, 6), (9, 8, 8), "metal")]
def target_marker():
return [el((7, -8, 7), (9, 24, 9), "target"),
el((2, 7.5, 2), (14, 8.5, 14), "target"),
el((5, 3, 5), (11, 13, 11), "target")]
def fireball():
return [el((2, 2, 2), (14, 14, 14), "fire"), el((0, 4, 4), (16, 12, 12), "fire"),
el((4, 0, 4), (12, 16, 12), "fire"), el((4, 4, 0), (12, 12, 16), "fire")]
def flash():
return [el((7, 7, -4), (9, 9, 20), "white"), el((-4, 7, 7), (20, 9, 9), "white"),
el((7, -4, 7), (9, 20, 9), "white"), el((5, 5, 5), (11, 11, 11), "flame")]
def debris():
return [el((5, 6, 3), (11, 9, 13), "dark_metal"), el((2, 7, 6), (14, 8, 10), "shahed")]
def lancet():
p = taper("lancet", -16, -10, 2, 3.5, 2, 3.5, 3)
p += [el((6.2, 6.2, -10), (9.8, 9.8, 20), "lancet"),
el((6.8, 4.8, -9), (9.2, 6.2, -5), "intake")] # оптика
for z0, z1, half in ((-5, 1, 13), (12, 18, 11)): # Х-образные крылья
for ang in (45, -45):
p.append(el((8 - half, 7.7, z0), (8 + half, 8.3, z1), "lancet", rot=("z", ang)))
p += [el((6.8, 6.8, 20), (9.2, 9.2, 22), "dark_metal")]
return p
def gerbera():
p = taper("foam", -15, -10, 2.5, 4.5, 2.5, 4.5, 3)
p += [el((5.8, 5.8, -10), (10.2, 10.2, 20), "foam")]
p += delta("foam", 0, 18, 22, 17, 7, 7.3, 8.5)
p += both(el((-9.6, 6.5, 15), (-8.6, 13, 22), "foam"))
p += [el((6.5, 6.5, 20), (9.5, 9.5, 24), "dark_metal")]
return p
def kh101():
p = taper("kh101", -16, -9, 2, 6, 2, 4.5, 4, y=7.5)
p += [el((5, 5.5, -9), (11, 8, 30), "kh101", {"down": "kh101_belly"}),
el((6, 8, -6), (10, 10.5, 28), "kh101")]
p += [el((-12, 7.1, 4), (28, 7.7, 10), "kh101")] # крылья
p += [el((7.6, 10.5, 24), (8.4, 15, 30), "kh101")] # киль
p += both(el((1, 7.2, 25), (6, 7.8, 30), "kh101"))
p += [el((6, 2, 15), (10, 5.5, 28), "dark_metal"), # двигатель под хвостом
el((6.5, 2.5, 14), (9.5, 5, 15), "intake"),
el((6.5, 2.5, 28), (9.5, 5, 29.5), "nozzle")]
return p
def wheels(zs, x0=-9, x1=-5.5, tex="tire"):
out = []
for z in zs:
out += both(el((x0, 0, z), (x1, 9, z + 7), tex))
return out
def pantsir_truck():
p = [el((-6, 5, -16), (22, 9, 32), "ru_green"),
el((-6, 9, -16), (22, 21, -6), "ru_green", {"north": "glass"}),
el((-5, 13, -16), (21, 20, -15.9), "glass"),
el((-5, 9, -5), (21, 18, 6), "ru_green"),
el((-4, 9, 8), (20, 12, 30), "dark_metal"),
el((-6.5, 3, 26), (22.5, 6, 32), "dark_metal")]
return p + wheels((-13, -4, 16, 24))
def pantsir_turret():
return [el((-2, 0, -6), (18, 10, 16), "ru_green"),
el((1, 1, -11), (15, 11, -6), "radar_face"),
el((7, 10, 4), (9, 17, 6), "metal"),
el((0, 10, 8), (16, 12, 16), "ru_green")]
def pantsir_pack():
"""Пакеты ракет 57Э6 по бортам и спаренные 30-мм пушки над ними; ось качания — центр."""
p = both(el((-11, 2, -16), (-3, 12, 12), "ru_green", {"north": "tubes6"}))
p += both(el((-9.5, 12.5, -16), (-8, 14, 6), "barrel"))
p += both(el((-6.5, 12.5, -16), (-5, 14, 6), "barrel"))
p += both(el((-10, 12, 2), (-4, 15, 10), "dark_metal"))
return p
def flat_radar(tex="radar_face"):
"""Плоская вращающаяся антенна (ось — вертикаль через центр модели)."""
return [el((-6, 2, 7), (22, 16, 9), tex), el((-6.5, 1.5, 9), (22.5, 16.5, 10), "dark_metal"),
el((7, -8, 8), (9, 2, 10), "metal")]
def maz_chassis(tex):
"""8×8 шасси МАЗ-543/БАЗ с раздельными кабинами; рама под пусковой контейнер."""
p = [el((-6, 5, -16), (22, 10, 32), tex),
el((-6, 10, -16), (3, 19, -7), tex, {"north": "glass"}),
el((13, 10, -16), (22, 19, -7), tex, {"north": "glass"}),
el((3, 10, -16), (13, 15, -6), "dark_metal"),
el((-5, 10, -6), (21, 13, 4), tex),
el((-3, 10, 26), (19, 14, 32), "dark_metal")]
p += both(el((-8, 0, 28), (-6, 6, 31), "metal")) # опоры
return p + wheels((-14, -5, 13, 22))
def tubes(n_wide, n_high, size, tex="tube"):
"""Пакет ТПК: n_wide × n_high труб сечением size, от z=-16 до 32. Ось подъёма — задняя нижняя кромка."""
p = []
w = n_wide * size + (n_wide - 1)
x0 = 8 - w / 2
for i in range(n_wide):
for j in range(n_high):
x = x0 + i * (size + 1)
y = j * (size + 1)
p.append(el((x, y, -16), (x + size, y + size, 32), tex, {"north": "tube_cap", "south": "tube_cap"}))
p += [el((x0 - 1, 0, 26), (x0 + w + 1, n_high * (size + 1) - 1, 28), "dark_metal"),
el((x0 - 1, 0, -2), (x0 + w + 1, n_high * (size + 1) - 1, 0), "dark_metal")]
return p
def radar_trailer():
p = [el((-6, 5, -12), (22, 10, 26), "ru_khaki"),
el((2, 10, 2), (14, 16, 14), "dark_metal"),
el((-5, 10, 16), (21, 18, 26), "ru_khaki")]
return p + wheels((-9, 17))
def big_radar_panel():
return [el((-10, 0, 7), (26, 26, 9), "radar_big"), el((-10.5, -0.5, 9), (26.5, 26.5, 10), "dark_metal"),
el((6, -6, 8), (10, 0, 12), "metal")]
def missile_57e6():
p = taper("missile_ru", -16, -12, 0.5, 2, 0.5, 2, 2)
p += [el((7, 7, -12), (9, 9, 4), "missile_ru"), el((6.4, 6.4, 4), (9.6, 9.6, 28), "booster")]
p += both(el((3.5, 7.7, 22), (6.4, 8.3, 28), "booster"))
p += [el((7.7, 3.5, 22), (8.3, 6.4, 28), "booster"), el((7.7, 9.6, 22), (8.3, 12.5, 28), "booster")]
return p
def missile_48n6():
p = taper("missile_ru", -16, -11, 1, 3.5, 1, 3.5, 3)
p += [el((6.25, 6.25, -11), (9.75, 9.75, 28), "missile_ru"),
el((6.5, 6.5, 28), (9.5, 9.5, 30), "nozzle")]
p += both(el((1.5, 7.7, 20), (6.25, 8.3, 28), "missile_ru"))
p += [el((7.7, 1.5, 20), (8.3, 6.25, 28), "missile_ru"), el((7.7, 9.75, 20), (8.3, 14.5, 28), "missile_ru")]
return p
def missile_77n6():
p = taper("missile_grey", -16, -10, 1, 4, 1, 4, 3)
p += [el((6, 6, -10), (10, 10, 30), "missile_grey"),
el((6, 6, 4), (10, 10, 5), "booster"),
el((6.5, 6.5, 30), (9.5, 9.5, 32), "nozzle")]
p += both(el((3, 7.7, 24), (6, 8.3, 30), "missile_grey"))
p += [el((7.7, 3, 24), (8.3, 6, 30), "missile_grey"), el((7.7, 10, 24), (8.3, 13, 30), "missile_grey")]
return p
def kalibr():
p = taper("missile_grey", -16, -11, 1.5, 4.5, 1.5, 4.5, 3)
p += [el((5.75, 5.75, -11), (10.25, 10.25, 30), "missile_grey"),
el((-6, 7.6, 6), (22, 8.4, 11), "missile_grey"),
el((7.6, 10.25, 25), (8.4, 14, 30), "missile_grey"),
el((6.5, 3, 18), (9.5, 5.75, 26), "dark_metal")]
p += both(el((2, 7.6, 26), (5.75, 8.4, 30), "missile_grey"))
return p
def kh22():
"""Х-22/Х-32: толстое тело, треугольные крылья, киль сверху и снизу."""
p = taper("missile_ru", -16, -8, 1.5, 6, 1.5, 6, 4)
p += [el((5, 5, -8), (11, 11, 30), "missile_ru"),
el((7.5, 11, 14), (8.5, 17, 30), "missile_ru"),
el((7.5, -1, 18), (8.5, 5, 30), "missile_ru"),
el((5.5, 5.5, 30), (10.5, 10.5, 32), "nozzle")]
p += delta("missile_ru", 4, 22, 28, 12, 5, 7.6, 8.4)
return p
def zircon():
p = taper("dark_metal", -16, -6, 1, 5, 1, 4, 4)
p += [el((5.5, 6, -6), (10.5, 10, 30), "kh101"),
el((6, 3.5, 0), (10, 6, 24), "intake"),
el((5.5, 6, 30), (10.5, 10, 32), "nozzle")]
p += both(el((1, 7.6, 22), (5.5, 8.4, 30), "kh101"))
p += [el((7.6, 10, 22), (8.4, 14, 30), "kh101")]
return p
def iskander():
"""9М723: толстая ракета с конической головной частью и решётчатыми рулями."""
p = taper("ru_khaki", -16, -6, 1.5, 7, 1.5, 7, 5)
p += [el((4.5, 4.5, -6), (11.5, 11.5, 30), "ru_khaki"),
el((4.4, 4.4, 4), (11.6, 11.6, 5), "dark_metal"),
el((5, 5, 30), (11, 11, 32), "nozzle")]
p += both(el((1, 7.5, 24), (4.5, 8.5, 30), "ru_khaki"))
p += [el((7.5, 1, 24), (8.5, 4.5, 30), "ru_khaki"), el((7.5, 11.5, 24), (8.5, 15, 30), "ru_khaki")]
return p
def kinzhal():
p = taper("missile_grey", -16, -8, 1.5, 6.5, 1.5, 6.5, 4)
p += [el((4.75, 4.75, -8), (11.25, 11.25, 30), "dark_metal"),
el((4.6, 4.6, -4), (11.4, 11.4, -3), "missile_grey"),
el((5.5, 5.5, 30), (10.5, 10.5, 32), "nozzle")]
p += both(el((0, 7.6, 22), (4.75, 8.4, 30), "dark_metal"))
p += [el((7.6, 0, 22), (8.4, 4.75, 30), "dark_metal"), el((7.6, 11.25, 22), (8.4, 16, 30), "dark_metal")]
return p
def tracked(tex):
"""Гусеничное шасси (Бук, Тор): корпус, гусеницы и надгусеничные полки."""
p = [el((-10, 4, -14), (26, 15, 32), tex),
el((-10, 4, -16), (26, 10, -14), tex),
el((-8, 15, 22), (24, 16, 32), "dark_metal")]
p += both(el((-14, 0, -15), (-10, 9, 31), "track"))
p += both(el((-14.5, 5, -15), (-10, 10, 31), tex))
return p
def buk_turret():
return [el((-4, 0, -4), (20, 9, 20), "ru_green"),
el((1, 2, -10), (15, 14, -4), "radar_face"), # РЛС подсвета на башне
el((-2, 9, 6), (18, 11, 18), "ru_green")]
def buk_rails():
"""Четыре ракеты 9М317 на направляющих; ось качания — центр."""
p = []
for x in (-9, -3, 13, 19):
p += [el((x - 1.5, 4, -16), (x + 1.5, 7, 26), "missile_ru"),
el((x - 1.8, 3.5, 20), (x + 1.8, 7.5, 27), "booster"),
el((x - 0.5, 2, -6), (x + 0.5, 4, 16), "dark_metal")]
p += [el((-12, 0, -2), (28, 2, 14), "ru_green")]
return p
def tor_turret():
"""Башня Тора: блок вертикального пуска (8 ячеек сверху) и РЛС спереди; вращается целиком."""
p = [el((-4, 0, -6), (20, 16, 22), "ru_khaki"),
el((-2, 2, -11), (18, 15, -6), "radar_face"),
el((-3, 16, 2), (19, 17, 20), "dark_metal")]
for i in range(4):
for j in range(2):
x, z = -1 + i * 5, 4 + j * 8
p.append(el((x, 16.5, z), (x + 4, 17.5, z + 6), "tube_cap"))
return p
def nasams_trailer():
p = [el((-6, 5, -10), (22, 9, 26), "nato_green"),
el((-4, 9, 10), (20, 13, 24), "dark_metal"),
el((6, 3, -16), (10, 6, -10), "metal")]
return p + wheels((4, 14))
def p1sun():
"""Дрон-перехватчик: короткий корпус, X-образные лучи с моторами; нос — север."""
p = [el((6.5, 6.5, -10), (9.5, 9.5, 10), "dark_metal"),
el((7, 7, -14), (9, 9, -10), "missile")]
for dx, dy in ((-1, -1), (-1, 1), (1, -1), (1, 1)):
x, y = 8 + dx * 6, 8 + dy * 6
p += [el((min(8, x) , min(8, y), 0), (max(8, x), max(8, y), 1.5), "dark_metal", rot=None),
el((x - 2.5, y - 2.5, 0), (x + 2.5, y + 2.5, 0.6), "prop")]
return p
def radar_truck():
return maz_chassis("ru_khaki")
def depot():
"""Склад боеприпасов: бетонная площадка, штабели ящиков и контейнеров ракет."""
p = [el((-14, 0, -14), (30, 2, 30), "concrete")]
for x, z in ((-12, -12), (-12, 4), (4, -12)):
p.append(el((x, 2, z), (x + 12, 10, z + 12), "crate"))
for k in range(3):
p.append(el((6, 2 + k * 5, 6), (28, 7 + k * 5, 10), "tube", {"north": "tube_cap", "south": "tube_cap"}))
return p
def tzm_truck():
"""Транспортно-заряжающая машина: кабина, кузов с ракетами и кран."""
p = [el((-6, 5, -16), (22, 9, 32), "ru_green"),
el((-6, 9, -16), (22, 20, -6), "ru_green", {"north": "glass"}),
el((-4, 9, -2), (20, 12, 30), "dark_metal")]
for x in (-3, 5, 13):
p.append(el((x, 12, -1), (x + 6, 18, 29), "tube", {"north": "tube_cap", "south": "tube_cap"}))
p += [el((18, 12, -4), (21, 26, -1), "metal"), el((6, 24, -4), (21, 26, -1), "metal")]
return p + wheels((-12, -3, 16, 24))
def ew_truck():
"""Станция РЭБ: кузов-фургон, мачта с антенными решётками."""
p = [el((-6, 5, -16), (22, 9, 32), "ru_khaki"),
el((-6, 9, -16), (22, 19, -6), "ru_khaki", {"north": "glass"}),
el((-5, 9, -4), (21, 22, 30), "ru_khaki"),
el((7, 22, 10), (9, 32, 12), "antenna")]
for y in (24, 28):
p += both(el((-4, y, 9), (7, y + 3, 13), "antenna"))
return p + wheels((-12, -3, 16, 24))
def searchlight():
"""Прожектор на тумбе: тумба и голова отдельно (голова — `searchlight_head`)."""
return [el((2, 0, 2), (14, 3, 14), "dark_metal"), el((6, 3, 6), (10, 12, 10), "dark_metal")]
def searchlight_head():
"""Голова прожектора: нос (стекло) смотрит на север; ось — центр."""
return [el((3, 3, -2), (13, 13, 14), "dark_metal"), el((3.5, 3.5, -2.5), (12.5, 12.5, -2), "lamp")]
def beam():
"""Луч прожектора: длинная светящаяся трубка вдоль z, от z=32 к -16 (масштабируется по длине)."""
return [el((6.5, 6.5, -16), (9.5, 9.5, 32), "beam")]
def flare():
return [el((6.5, 6.5, 6.5), (9.5, 9.5, 9.5), "lamp"), el((7.6, 9.5, 7.6), (8.4, 16, 8.4), "antenna")]
# ---------------------------------------------------------------- обновлённая геометрия
def B(x0, y0, z0, x1, y1, z1, tex, faces=None, rot=None):
"""Элемент с x относительно центра модели."""
return el((8 + x0, y0, z0), (8 + x1, y1, z1), tex, faces, rot)
def wheel(x0, x1, zc, r=4.6, tex="tire", hub="hub"):
"""Колесо-восьмиугольник (квадрат + квадрат под 45 градусов) с диском на боковой грани, обе стороны.
x0..x1 — абсолютные координаты левого колеса; ось колеса лежит на y = r."""
s = r * 1.8478
faces = {"east": hub, "west": hub}
a = el((x0, r - s / 2, zc - s / 2), (x1, r + s / 2, zc + s / 2), tex, faces)
b = el((x0 + 0.03, r - s / 2, zc - s / 2), (x1 - 0.03, r + s / 2, zc + s / 2), tex, faces, rot=("x", 45))
return both(a) + both(b)
def wheels(zs, x0=-9, x1=-5.5, tex="tire"):
out = []
for z in zs:
out += wheel(x0, x1, z + 3.5, 4.5, tex)
return out
def tracked(tex):
"""Гусеничное шасси (Бук, Тор): корпус, скошенный лоб, гусеницы с катками, решётки МТО, люки."""
p = [el((-10, 4, -14), (26, 15, 32), tex),
el((-10, 4, -16), (26, 10, -14), tex),
el((-9, 10, -15), (25, 14, -12), tex, rot=("x", -22.5)), # лобовой лист
el((-8, 15, 22), (24, 16, 32), "dark_metal"),
el((-5, 15.2, 24), (21, 15.8, 31), "bare_nozzle"), # решётка МТО
el((-1, 15, -10), (9, 16, -4), "dark_metal")]
p += both(el((-14, 0, -15), (-10, 9, 31), "track", {"west": "track_side", "east": "track_side"}))
p += both(el((-14.5, 5, -15), (-10, 10, 31), tex))
p += both(el((-4, 11, -16), (-1, 13, -15), "bare_lamp"))
return p
def oct_z(z0, z1, r, tex, x=0.0, y=8.0):
"""Восьмигранная «труба» вдоль z (квадрат + квадрат под 45 градусов): корпус ракеты."""
s = r * 1.8478
f = {"up": tex + "_t", "down": tex + "_t"} if tex in RINGED else None
return [B(x - s / 2, y - s / 2, z0, x + s / 2, y + s / 2, z1, tex, f),
B(x - s / 2, y - s / 2, z0 + 0.03, x + s / 2, y + s / 2, z1 - 0.03, tex, f, rot=("z", 45))]
def ogive(z_tip, z_base, rb, tex, steps=4):
out = []
for i in range(steps):
a = z_tip + (z_base - z_tip) * i / steps
b = z_tip + (z_base - z_tip) * (i + 1) / steps
t = (i + 0.7) / steps
out += oct_z(a, b, max(0.45, rb * min(1.0, t ** 0.55)), tex)
return out
def fins4(z0, z1, r, span, tex, sweep=3.0, th=0.7, y=8.0):
"""Четыре стреловидных оперения крестом: корневая и концевая части."""
m, k = r * 0.9, span * 0.55
p = both(B(m, y - th / 2, z0, m + k, y + th / 2, z1, tex))
p += both(B(m + k, y - th / 2, z0 + sweep, m + span, y + th / 2, z1, tex))
for sg in (1, -1):
lo, hi = (y + m, y + m + k) if sg > 0 else (y - m - k, y - m)
p.append(B(-th / 2, lo, z0, th / 2, hi, z1, tex))
lo, hi = (y + m + k, y + m + span) if sg > 0 else (y - m - span, y - m - k)
p.append(B(-th / 2, lo, z0 + sweep, th / 2, hi, z1, tex))
return p
def nozzle(z, r, y=8.0):
"""Сопло с раскалённым срезом (glow_ember — светящийся материал)."""
return [B(-r * .8, y - r * .8, z, r * .8, y + r * .8, z + 1.6, "nozzle"),
B(-r * .55, y - r * .55, z + 1.6, r * .55, y + r * .55, z + 1.9, "glow_ember")]
def pac3():
p = ogive(-16, -9, 1.7, "nose_dark", 3) + oct_z(-9, 24, 1.7, "rm_pac3")
p += oct_z(-8, -7, 2.2, "dark_metal") # кольцо двигателей ориентации
p += fins4(18, 24, 1.7, 5, "missile", 3.5) + nozzle(24, 1.7)
return p
def irist_missile():
p = ogive(-16, -9, 1.7, "white", 3) + oct_z(-9, 22, 1.7, "rm_white")
p += fins4(-8, -5.5, 1.7, 3.6, "missile_grey", 1.0) + fins4(16, 22, 1.7, 5, "white", 3.5) + nozzle(22, 1.7)
return p
def amraam_missile():
p = ogive(-16, -9, 1.7, "nose_dark", 3) + oct_z(-9, 24, 1.7, "rm_white")
p += fins4(2, 8, 1.7, 5.5, "white", 3.5) + fins4(19, 24, 1.7, 3.8, "white", 2.5) + nozzle(24, 1.7)
return p
def missile_57e6():
p = ogive(-16, -9, 1.2, "missile_ru", 3) + oct_z(-9, 4, 1.2, "missile_ru") + oct_z(4, 25, 1.7, "rm_ru")
p += fins4(18, 25, 1.7, 5, "booster", 3.0) + nozzle(25, 1.7)
return p
def missile_48n6():
p = ogive(-16, -9, 1.9, "missile_ru", 3) + oct_z(-9, 26, 1.9, "rm_ru")
p += fins4(18, 26, 1.9, 6.2, "missile_ru", 4.0) + fins4(-6, -3, 1.9, 2.5, "missile_ru", 1.0) + nozzle(26, 1.9)
return p
def missile_77n6():
p = ogive(-16, -8, 2.2, "missile_grey", 4) + oct_z(-8, 28, 2.2, "rm_grey")
p += oct_z(4, 6, 2.4, "booster") + fins4(22, 28, 2.2, 5.2, "missile_grey", 3.0) + nozzle(28, 2.2)
return p
def kalibr():
p = ogive(-16, -9, 2.2, "missile_grey", 4) + oct_z(-9, 28, 2.2, "rm_grey")
p += [B(-14, 7.6, 6, 14, 8.4, 11, "missile_grey"), B(-0.8, 3.2, 17, 0.8, 5.4, 26, "dark_metal")]
p += fins4(23, 28, 2.2, 4.5, "missile_grey", 2.5) + nozzle(28, 2.0)
return p
def iskander():
p = ogive(-16, -5, 3.6, "rm_khaki", 6) + oct_z(-5, 28, 3.6, "rm_khaki")
p += oct_z(3, 4, 3.8, "dark_metal") + fins4(23, 28, 3.6, 6.5, "ru_khaki", 2.0, 0.9) + nozzle(28, 3.4)
return p
def kinzhal():
p = ogive(-16, -6, 3.4, "rm_dark", 5) + oct_z(-6, 28, 3.4, "rm_dark")
p += fins4(20, 28, 3.4, 8.0, "dark_metal", 4.0, 0.9) + nozzle(28, 3.0)
return p
def zircon():
p = ogive(-16, -6, 2.6, "rm_dark", 4) + oct_z(-6, 28, 2.6, "rm_dark")
p += [B(-1.8, 3.4, 0, 1.8, 5.2, 24, "intake")]
p += fins4(22, 28, 2.6, 5.5, "dark_metal", 3.0) + nozzle(28, 2.4)
return p
# ---------------------------------------------------------------- часть 2: беспилотники, Х-101/Х-22, детали техники
def delta2(tex, under, z_root, z_tip, z_trail, half, steps, y0, y1):
"""Дельта-крыло со своей нижней обшивкой."""
out = []
for i in range(steps):
za = z_root + (z_tip - z_root) * i / steps
sp = half * (i + 1) / steps
out.append(B(-sp, y0, za, sp, y1, z_trail, tex, {"down": under}))
return out
def decal(tex, y0, y1, z0, z1, x, w=0.12):
"""Маркировка на борту (тонкая плита, обе стороны), x — внешняя грань корпуса от центра."""
return both(B(x, y0, z0, x + w, y1, z1, tex))
def shahed():
p = ogive(-16, -9, 2.6, "shahed_nose", 3) + oct_z(-9, 22, 2.6, "shahed")
p += delta2("shahed", "kh101_belly", -2, 21, 26, 21, 9, 7.2, 8.4)
p += both(B(-20.8, 5.5, 17, -19.8, 13.5, 26, "shahed")) # шайбы-кили на законцовках
p += oct_z(22, 28, 1.9, "dark_metal") + both(B(0.8, 5.2, 24, 1.6, 6.8, 28, "bare_nozzle"))
p += [B(-1.5, 10.5, -4, 1.5, 12, 8, "dark_metal"), # люк антенны ГНСС
B(-0.3, 12, 2, 0.3, 14.5, 3, "bare_antenna"),
B(-1.2, 4.8, -12, 1.2, 5.6, -6, "bare_lamp")] # оптика под носом
return p
def geran3():
p = ogive(-16, -9, 2.8, "geran", 3) + oct_z(-9, 26, 2.8, "geran")
p += delta2("geran", "geran", 0, 20, 26, 18, 8, 7.2, 8.4)
p += both(B(-10.6, 6.5, 18, -9.6, 13, 26, "geran"))
p += oct_z(6, 28, 2.3, "geran_engine", 0, 14) # турбореактивный двигатель на пилоне
p += [B(-1.2, 10.5, 8, 1.2, 12, 22, "geran"), B(-2, 12, 4, 2, 16, 6, "bare_nozzle"),
B(-1.7, 12.3, 28, 1.7, 15.7, 29.5, "bare_nozzle"), B(-1.2, 12.8, 29.5, 1.2, 15.2, 29.9, "glow_ember"),
B(-0.3, 10.8, -6, 0.3, 12.8, -5, "bare_antenna")]
return p
def lancet():
p = ogive(-16, -10, 2.0, "lancet", 3) + oct_z(-10, 20, 2.0, "lancet")
p += [B(-1.1, 6.8, -16.4 + 0.4, 1.1, 8.8, -15.6, "bare_glass_dark")] if False else []
p += [B(-1.2, 5.2, -14, 1.2, 6.4, -9, "bare_lamp")] # оптоэлектронная головка
for z0, z1, half in ((-5, 1, 13), (12, 18, 11)): # Х-образные крылья
for ang in (45, -45):
p.append(B(-half, 7.7, z0, half, 8.3, z1, "lancet", rot=("z", ang)))
p += oct_z(20, 24, 1.5, "dark_metal")
p += [B(-5, 7.85, 24, 5, 8.15, 24.5, "prop"), B(-0.15, 3, 24, 0.15, 13, 24.5, "prop")]
return p
def gerbera():
p = ogive(-15, -10, 2.3, "foam", 3) + oct_z(-10, 20, 2.3, "foam")
p += delta2("foam", "foam", 0, 18, 22, 17, 7, 7.3, 8.5)
p += both(B(-9.6, 6.5, 15, -8.6, 13, 22, "foam"))
p += [B(-1.5, 10, -6, 1.5, 11.6, 4, "dark_metal"), B(-1.4, 5.0, -14, 1.4, 6.2, -9, "bare_lamp")]
p += oct_z(20, 24, 1.6, "dark_metal") + [B(-0.2, 3, 24, 0.2, 13, 24.5, "prop"), B(-5, 7.8, 24, 5, 8.2, 24.5, "prop")]
return p
def kh101():
p = ogive(-16, -8, 2.9, "kh101", 4, ) + oct_z(-8, 27, 2.9, "kh101")
p = [dict(e, **{"from": [e["from"][0], e["from"][1] - 0.5, e["from"][2]], "to": [e["to"][0], e["to"][1] - 0.5, e["to"][2]]}) for e in p]
p += both(B(2.5, 6.9, 2, 14, 7.6, 11, "kh101", {"down": "kh101_belly"})) # крылья: корень и конец
p += both(B(14, 6.9, 5, 20.5, 7.4, 11, "kh101", {"down": "kh101_belly"}))
p += [B(-0.4, 10, 20, 0.4, 16, 28, "kh101")] + both(B(1.5, 6.9, 22, 8, 7.4, 28, "kh101"))
p += [B(-1.9, 3.2, 12, 1.9, 5.4, 26, "dark_metal"), B(-1.6, 3.5, 11.4, 1.6, 5.1, 12, "bare_nozzle"),
B(-1.5, 3.6, 26, 1.5, 5, 28, "bare_nozzle"), B(-1.1, 3.9, 28, 1.1, 4.7, 28.4, "glow_ember"),
B(-1, 10, -2, 1, 10.6, 10, "dark_metal")]
return p
def kh22():
p = ogive(-16, -8, 3.0, "rm_ru", 4) + oct_z(-8, 28, 3.0, "rm_ru")
p += both(B(3, 7.6, 4, 14, 8.4, 16, "missile_ru")) + both(B(14, 7.6, 8, 18, 8.2, 16, "missile_ru"))
p += fins4(21, 28, 3.0, 6.5, "missile_ru", 4.0)
p += [B(-0.5, 11, 14, 0.5, 17, 28, "missile_ru"), B(-0.5, -1, 16, 0.5, 5, 28, "missile_ru")]
p += nozzle(28, 2.6)
return p
def patriot_launcher():
"""Пакет 4 ТПК M903: четыре отдельных контейнера с торцами, ремни, рама. Центры труб — как у `Muzzle::Patriot`."""
p = []
for x0, x1 in ((-6, 7.8), (8.2, 22)):
for y0, y1 in ((1.5, 9.5), (10.5, 18.5)):
p.append(el((x0, y0, -14), (x1, y1, 30), "canister", {"north": "canister_front", "south": "dark_metal"}))
for z in (-6, 8, 20):
p.append(el((x0 - 0.2, y0 - 0.2, z), (x1 + 0.2, y1 + 0.2, z + 1.5), "dark_metal"))
p += [el((7.5, 1, -14), (8.5, 19, 30), "dark_metal"), el((-7, 0, 24), (23, 2, 30), "metal")]
p += both(el((-8, 2, -10), (-6, 17, -6), "metal")) + both(el((-8, 2, 20), (-6, 17, 24), "metal"))
return p
def _plus(fn, extra):
"""Обёртка: те же детали и ещё несколько."""
return lambda *a: fn(*a) + extra()
# кабины: фары, бампер, зеркала, выхлоп, бак, номера
_maz0 = maz_chassis
def maz_chassis(tex):
p = _maz0(tex)
p += both(B(-12.5, 11, -16, -10.5, 12.6, -15.6, "bare_lamp")) + both(B(-12.5, 14.5, -16, -10.5, 15.5, -15.6, "bare_lamp"))
p += [B(-9, 5.5, -16, 9, 7.5, -15, "dark_metal")] # бампер (z до -17 нельзя: ниже -16 отсекается)
p += both(B(-18.5, 7, 4, -15, 10, 14, "dark_metal")) # топливные баки
p += both(B(-14.5, 15, -9, -13.5, 22, -8, "bare_antenna")) # выхлопные трубы
p += decal("mk_ru", 11, 15, -15, -7, 14)
return p
_tr0 = tracked
def tracked(tex):
p = _tr0(tex)
p += decal("mk_ru2", 11, 14.5, 0, 8, 18)
p += both(B(-5, 16, 2, -1, 17.4, 8, "dark_metal")) + [B(-3, 16, -8, 3, 17, -4, "bare_lamp")]
return p
_pt0 = patriot_truck
def patriot_truck():
p = _pt0()
p += both(B(-14.2, 10, -12, -14, 14, -4, "glass_cab"))
p += decal("mk_us", 11, 15, -12, -4, 14)
p += both(B(-14, 7, 6, -17, 11, 14, "dark_metal"))
p += [B(-9, 5.5, -16, 9, 7.5, -15.2, "dark_metal")]
p += both(B(-12.5, 10.5, -16, -10.5, 12.2, -15.4, "bare_lamp"))
return p
_pz0 = pantsir_truck
def pantsir_truck():
p = _pz0()
p += decal("mk_star", 12, 16, -14, -6, 14)
p += both(B(-12.5, 10.5, -16, -10.5, 12.2, -15.4, "bare_lamp")) + both(B(-18.5, 7, 6, -15, 10, 14, "dark_metal"))
return p
_gt0 = gepard_turret
def gepard_turret():
p = _gt0()
p += decal("mk_de", 3, 6.5, -5.5, 1.5, 12)
p += [B(-2.5, 14, 2, 2.5, 14.8, 7, "metal"), B(-6, 14, 5, -5, 16, 6, "bare_antenna")]
p += both(B(-12.8, 7, -5, -12, 10, -2, "dark_metal")) + both(B(-12.8, 7, -1.5, -12, 10, 0.5, "dark_metal")) # дымовые гранатомёты
return p
_pu0 = pantsir_turret
def pantsir_turret():
p = _pu0()
p += [el((5, 10, -4), (11, 13, 0), "dark_metal"), el((6, 10.5, -4.4), (10, 12.5, -4), "bare_lamp"),
el((1, 12, 10), (15, 12.6, 15), "metal")]
p += decal("mk_ru", 3, 6.5, -4, 3, 10)
return p
_tz0 = tzm_truck
def tzm_truck():
p = _tz0()
p += decal("mk_ru2", 11, 15, -14, -7, 14) + both(B(-12.5, 10, -16, -10.5, 11.6, -15.4, "bare_lamp"))
return p
_ew0 = ew_truck
def ew_truck():
p = _ew0()
p += decal("mk_ru", 11, 15, -14, -7, 14) + both(B(-12.5, 10, -16, -10.5, 11.6, -15.4, "bare_lamp"))
p += [B(-2, 22, -4, 2, 23.5, 0, "dark_metal")]
return p
MODELS = {
"shahed": (shahed, "Шахед-136"),
"shahed_prop": (shahed_prop, "Винт"),
"geran3": (geran3, "Герань-3"),
"geran3_flame": (geran3_flame, "Факел"),
"pac3": (pac3, "PAC-3 MSE"),
"irist_missile": (irist_missile, "IRIS-T SLM"),
"amraam_missile": (amraam_missile, "AMRAAM"),
"flame": (flame, "Факел"),
"patriot_launcher": (patriot_launcher, "ПУ M903"),
"patriot_truck": (patriot_truck, "Тягач"),
"patriot_trailer": (patriot_trailer, "Полуприцеп"),
"patriot_radar": (patriot_radar, "РЛС AN/MPQ-65"),
"gepard_hull": (gepard_hull, "Корпус Гепарда"),
"gepard_turret": (gepard_turret, "Башня Гепарда"),
"gepard_guns": (gepard_guns, "Пушки Гепарда"),
"gepard_dish": (gepard_dish, "РЛС Гепарда"),
"target": (target_marker, "Цель"),
"fireball": (fireball, "Огненный шар"),
"flash": (flash, "Вспышка"),
"debris": (debris, "Обломки"),
"lancet": (lancet, "Ланцет-3"),
"gerbera": (gerbera, "Гербера"),
"kh101": (kh101, "Х-101"),
"pantsir_truck": (pantsir_truck, "Шасси Панциря"),
"pantsir_turret": (pantsir_turret, "Модуль Панциря"),
"pantsir_pack": (pantsir_pack, "Ракеты и пушки Панциря"),
"pantsir_dish": (flat_radar, "РЛС Панциря"),
"s400_chassis": (lambda: maz_chassis("ru_green"), "Шасси С-400"),
"s400_tubes": (lambda: tubes(2, 2, 7), "ТПК 48Н6"),
"s500_chassis": (lambda: maz_chassis("ru_khaki"), "Шасси С-500"),
"s500_tubes": (lambda: tubes(2, 1, 9.5), "ТПК 77Н6"),
"radar_trailer": (radar_trailer, "Прицеп РЛС"),
"radar_panel": (big_radar_panel, "РЛС обзора"),
"m57e6": (missile_57e6, "57Э6"),
"m48n6": (missile_48n6, "48Н6"),
"m77n6": (missile_77n6, "77Н6"),
"kalibr": (kalibr, "Калибр"),
"kh22": (kh22, "Х-22"),
"zircon": (zircon, "Циркон"),
"iskander": (iskander, "Искандер-М"),
"kinzhal": (kinzhal, "Кинжал"),
"tracer": (lambda: [el((7.6, 7.6, -16), (8.4, 8.4, 32), "tracer"), el((7.3, 7.3, -16), (8.7, 8.7, -4), "tracer_hot")], "Трассер"),
# Боевой блок «Орешника»: раскалённое ядро и длинный плазменный хвост.
"irist_chassis": (lambda: maz_chassis("nato_green"), "Шасси IRIS-T SLM"),
"irist_tubes": (lambda: tubes(4, 2, 4, "canister"), "Пакет IRIS-T"),
"nasams_trailer": (nasams_trailer, "Прицеп NASAMS"),
"nasams_launcher": (lambda: tubes(3, 2, 4, "canister"), "Пусковая NASAMS"),
"buk_chassis": (lambda: tracked("ru_green"), "Шасси Бука"),
"buk_turret": (buk_turret, "Башня Бука"),
"buk_rails": (buk_rails, "Ракеты Бука"),
"tor_chassis": (lambda: tracked("ru_khaki"), "Шасси Тора"),
"tor_turret": (tor_turret, "Башня Тора"),
"p1sun_launcher": (lambda: tubes(4, 2, 3, "dark_metal"), "Пусковая P1-SUN"),
"p1sun": (p1sun, "Дрон-перехватчик P1-SUN"),
"radar_truck": (radar_truck, "Шасси РЛС"),
"depot": (depot, "Склад боеприпасов"),
"tzm_truck": (tzm_truck, "Транспортно-заряжающая машина"),
"ew_truck": (ew_truck, "Станция РЭБ"),
"searchlight": (searchlight, "Прожектор"),
"searchlight_head": (searchlight_head, "Голова прожектора"),
"beam": (beam, "Луч прожектора"),
"flare": (flare, "Осветительная ракета"),
"reentry": (lambda: [el((6.5, 6.5, -16), (9.5, 9.5, -8), "white"), el((7, 7, -8), (9, 9, 12), "tracer_hot"),
el((7.5, 7.5, 12), (8.5, 8.5, 32), "flame")], "Боевой блок"),
}
WRECKS = ["patriot_trailer", "patriot_launcher", "gepard_hull", "gepard_turret", "gepard_guns", "pantsir_truck",
"pantsir_turret", "pantsir_pack", "s400_chassis", "s400_tubes", "s500_chassis", "s500_tubes",
"irist_chassis", "irist_tubes", "nasams_trailer", "nasams_launcher", "buk_chassis", "buk_turret",
"buk_rails", "tor_chassis", "tor_turret", "p1sun_launcher", "radar_truck", "depot", "tzm_truck",
"ew_truck", "searchlight", "searchlight_head"]
CAM_MODELS = ["shahed", "geran3", "lancet", "gerbera", "kh101", "kalibr", "kh22", "zircon", "iskander", "kinzhal", "reentry"]
# Предметы: (id, название, цвет иконки)
ITEMS = [
("designator", "Целеуказатель", (220, 50, 50)),
("launch_shahed", "Пуск: Шахед-136", (90, 92, 96)),
("launch_geran3", "Пуск: Герань-3", (30, 30, 34)),
("deploy_patriot", "Развернуть Patriot", (76, 86, 58)),
("deploy_gepard", "Развернуть Гепард", (104, 84, 54)),
("deploy_pantsir", "Развернуть Панцирь-С1", (82, 96, 60)),
("deploy_s400", "Развернуть С-400", (60, 80, 50)),
("deploy_s500", "Развернуть С-500", (108, 110, 80)),
("launch_lancet", "Пуск: Ланцет-3", (70, 78, 66)),
("launch_kh101", "Пуск: Х-101", (84, 88, 92)),
("launch_kalibr", "Пуск: Калибр", (150, 154, 156)),
("launch_kh22", "Пуск: Х-22", (226, 228, 222)),
("launch_zircon", "Пуск: Циркон", (52, 54, 52)),
("launch_iskander", "Пуск: Искандер-М", (108, 110, 80)),
("launch_kinzhal", "Пуск: Кинжал", (60, 62, 60)),
("launch_oreshnik", "Пуск: Орешник", (230, 120, 40)),
("deploy_irist", "Развернуть IRIS-T SLM", (76, 86, 58)),
("deploy_nasams", "Развернуть NASAMS", (76, 86, 58)),
("deploy_buk", "Развернуть Бук-М2", (82, 96, 60)),
("deploy_tor", "Развернуть Тор-М2", (108, 110, 80)),
("deploy_p1sun", "Развернуть P1-SUN", (52, 54, 52)),
("deploy_radar", "Развернуть РЛС дальнего обнаружения", (108, 110, 80)),
("deploy_depot", "Развернуть склад боеприпасов", (92, 78, 48)),
("deploy_tzm", "Развернуть ТЗМ", (82, 96, 60)),
("deploy_ew", "Развернуть станцию РЭБ", (108, 110, 80)),
("deploy_light", "Развернуть прожектор", (52, 54, 52)),
("mover", "Пульт перемещения", (60, 140, 220)),
("flare_gun", "Осветительная ракета", (255, 240, 180)),
("remover", "Свернуть ПВО", (200, 200, 200)),
]
# Иконки 16×16 без сглаживания. Каждый предмет рисуется своей функцией: категория задаёт основу
# (машина ПВО, пуск с пламенем, РЛС, склад, прожектор, целеуказатель, стрелки, сворачивание),
# внутри категории — мини-силуэт типа. Цвет предмета — только заливка, форма различает предметы.
class _Icon:
def __init__(self, color, seed):
self.rnd = random.Random(seed)
self.im = Image.new("RGBA", (16, 16), (0, 0, 0, 0))
self.pal = {
"b": tuple(color),
"d": tuple(clamp(v * 0.6) for v in color),
"l": tuple(clamp(v + 45) for v in color),
"k": (30, 30, 34), "r": (255, 60, 60), "o": (255, 150, 40),
"y": (255, 232, 120), "w": (236, 236, 236),
}
def px(self, x, y, key):
if 0 <= x < 16 and 0 <= y < 16:
c = self.pal[key]
if key in ("b", "d", "l"):
c = tuple(clamp(v + self.rnd.randint(-6, 6)) for v in c)
self.im.putpixel((x, y), c + (255,))
def rect(self, x0, y0, x1, y1, key):
for y in range(y0, y1 + 1):
for x in range(x0, x1 + 1):
self.px(x, y, key)
def line(self, x0, y0, x1, y1, key):
dx, dy = abs(x1 - x0), -abs(y1 - y0)
sx, sy = (1 if x0 < x1 else -1), (1 if y0 < y1 else -1)
err = dx + dy
while True:
self.px(x0, y0, key)
if x0 == x1 and y0 == y1:
break
e2 = 2 * err
if e2 >= dy:
err += dy
x0 += sx
if e2 <= dx:
err += dx
y0 += sy
def disc(self, cx, cy, r, key):
for y in range(cy - r - 1, cy + r + 2):
for x in range(cx - r - 1, cx + r + 2):
if (x - cx) ** 2 + (y - cy) ** 2 <= r * r + 0.5 * r:
self.px(x, y, key)
def ring(self, cx, cy, lo, hi, key):
for y in range(16):
for x in range(16):
if lo <= (x - cx) ** 2 + (y - cy) ** 2 <= hi:
self.px(x, y, key)
def poly(self, pts, key):
# Выпуклый многоугольник: заливка по строкам между крайними пересечениями рёбер.
n = len(pts)
for y in range(min(p[1] for p in pts), max(p[1] for p in pts) + 1):
xs = []
for i in range(n):
(x1, y1), (x2, y2) = pts[i], pts[(i + 1) % n]
if y1 == y2:
if y1 == y:
xs += [x1, x2]
elif min(y1, y2) <= y <= max(y1, y2):
xs.append(x1 + (y - y1) * (x2 - x1) / (y2 - y1))
if xs:
for x in range(math.ceil(min(xs) - 1e-9), math.floor(max(xs) + 1e-9) + 1):
self.px(x, y, key)
def _sam_base(c):
c.rect(2, 8, 13, 11, "b")
c.rect(3, 12, 4, 13, "k")
c.rect(11, 12, 12, 13, "k")
# Развёртывание ПВО: машина + пусковая установка.
def _gepard(c): # ствольная турель, два длинных ствола
_sam_base(c)
c.disc(7, 6, 2, "b")
c.line(9, 5, 15, 5, "d")
c.line(9, 7, 15, 7, "d")
def _pantsir(c): # ствольная турель с ракетными контейнерами сверху
_sam_base(c)
c.disc(7, 7, 2, "b")
c.rect(4, 3, 5, 5, "l")
c.rect(9, 3, 10, 5, "l")
c.line(9, 6, 11, 6, "d")
def _s400(c): # три вертикальных контейнера
_sam_base(c)
for x in (3, 6, 9):
c.rect(x, 2, x + 1, 7, "b")
c.rect(x, 2, x + 1, 2, "l")
def _s500(c): # четыре вертикальных контейнера
_sam_base(c)
for x in (2, 5, 8, 11):
c.rect(x, 2, x + 1, 7, "b")
c.rect(x, 2, x + 1, 2, "l")
def _patriot(c): # наклонный пусковой короб
_sam_base(c)
c.poly([(3, 8), (11, 3), (13, 5), (5, 10)], "b")
c.line(7, 7, 9, 5, "d")
def _buk(c): # наклонные направляющие с ракетой
_sam_base(c)
c.line(3, 8, 10, 2, "d")
c.line(5, 9, 12, 3, "d")
c.disc(12, 2, 1, "w")
def _irist(c): # два наклонных контейнера
_sam_base(c)
c.poly([(3, 8), (7, 3), (9, 4), (5, 9)], "b")
c.poly([(7, 9), (11, 3), (13, 4), (9, 10)], "b")
def _nasams(c): # два горизонтальных контейнера
_sam_base(c)
c.rect(3, 2, 12, 3, "b")
c.rect(3, 5, 12, 6, "b")
c.line(3, 4, 12, 4, "d")
def _tor(c): # сетка коротких вертикальных ячеек
_sam_base(c)
for x in (3, 6, 9):
c.rect(x, 2, x + 1, 3, "b")
c.rect(x, 5, x + 1, 6, "b")
def _p1sun(c): # короб с мачтой
_sam_base(c)
c.rect(3, 5, 12, 7, "b")
c.line(7, 1, 7, 4, "d")
c.px(7, 1, "r")
# Радиолокация и РЭБ.
def _radar(c): # параболическая антенна на стойке
c.disc(8, 5, 3, "l")
c.line(8, 8, 8, 12, "d")
c.rect(5, 13, 11, 14, "d")
def _ew(c): # штырь-антенна с сигнальными дугами
c.line(8, 6, 8, 14, "d")
c.disc(8, 4, 1, "b")
c.line(5, 2, 4, 4, "l")
c.line(4, 4, 4, 6, "l")
c.line(4, 6, 5, 8, "l")
c.line(11, 2, 12, 4, "l")
c.line(12, 4, 12, 6, "l")
c.line(12, 6, 11, 8, "l")
# Снабжение.
def _depot(c): # штабель ящиков на поддоне
c.rect(3, 8, 12, 13, "b")
c.line(3, 10, 12, 10, "d")
c.line(3, 12, 12, 12, "d")
c.rect(2, 14, 13, 14, "d")
c.rect(4, 4, 9, 7, "l")
c.line(4, 6, 9, 6, "d")
def _tzm(c): # грузовик с кабиной и кузовом
c.rect(2, 7, 5, 11, "b")
c.rect(3, 8, 4, 9, "w")
c.rect(6, 4, 14, 11, "l")
c.rect(3, 12, 4, 13, "k")
c.rect(10, 12, 11, 13, "k")
c.rect(13, 12, 14, 13, "k")
# Прожектор.
def _searchlight(c):
c.rect(3, 4, 10, 8, "b")
c.poly([(10, 4), (15, 1), (15, 11), (10, 8)], "y")
c.line(6, 9, 6, 13, "d")
c.rect(3, 14, 9, 14, "d")
# Целеуказатель: кольцо с засечками.
def _designator(c):
c.ring(8, 8, 16, 26, "r")
c.rect(7, 1, 8, 2, "r")
c.rect(7, 13, 8, 14, "r")
c.rect(1, 7, 2, 8, "r")
c.rect(13, 7, 14, 8, "r")
c.rect(7, 7, 8, 8, "r")
# Пуски: огонь сзади, силуэт типа спереди.
def _launch_base(c):
c.rect(0, 7, 1, 8, "o")
c.px(0, 7, "y")
def _shahed(c): # треугольное крыло
_launch_base(c)
c.poly([(5, 2), (5, 13), (13, 7)], "b")
def _geran3(c): # реактивный самолёт со стреловидным крылом
_launch_base(c)
c.rect(3, 7, 13, 8, "b")
c.poly([(6, 7), (9, 2), (11, 2), (9, 7)], "d")
c.poly([(6, 8), (9, 13), (11, 13), (9, 8)], "d")
c.poly([(3, 7), (4, 3), (5, 3), (5, 7)], "d")
def _lancet(c): # малый дрон с прямым крылом
_launch_base(c)
c.rect(3, 7, 12, 8, "b")
c.rect(7, 2, 8, 13, "d")
c.rect(3, 6, 4, 9, "d")
def _kh101(c): # крылатая ракета: стреловидные крылья и хвост
_launch_base(c)
c.rect(3, 7, 12, 8, "b")
c.poly([(12, 6), (15, 7), (15, 8), (12, 9)], "b")
c.poly([(6, 7), (9, 3), (10, 3), (8, 7)], "d")
c.poly([(6, 8), (9, 12), (10, 12), (8, 8)], "d")
c.rect(2, 5, 3, 10, "d")
def _kalibr(c): # крылатая ракета: тонкий корпус и четыре хвостовых киля
_launch_base(c)
c.rect(2, 7, 13, 8, "b")
c.poly([(13, 6), (15, 7), (15, 8), (13, 9)], "b")
c.rect(2, 4, 3, 5, "d")
c.rect(2, 10, 3, 11, "d")
def _kh22(c): # толстый корпус с большими дельта-крыльями
_launch_base(c)
c.rect(3, 6, 11, 9, "b")
c.poly([(11, 6), (15, 7), (15, 8), (11, 9)], "b")
c.poly([(4, 6), (8, 2), (9, 2), (7, 6)], "d")
c.poly([(4, 9), (8, 13), (9, 13), (7, 9)], "d")
def _zircon(c): # гиперзвук: корпус с одним передним стрейком
_launch_base(c)
c.rect(2, 7, 12, 8, "b")
c.poly([(12, 7), (15, 7), (12, 8)], "b")
c.poly([(8, 7), (12, 3), (13, 3), (11, 7)], "d")
def _iskander(c): # баллистическая: длинный корпус, конус, сопло
_launch_base(c)
c.rect(3, 7, 11, 8, "b")
c.poly([(11, 6), (15, 7), (15, 8), (11, 9)], "l")
c.rect(2, 6, 2, 9, "d")
def _oreshnik(c): # баллистическая с тремя головными частями
_launch_base(c)
c.rect(2, 6, 10, 9, "b")
c.poly([(10, 5), (13, 6), (10, 7)], "l")
c.poly([(10, 8), (13, 9), (10, 10)], "l")
c.poly([(10, 6), (15, 7), (10, 8)], "b")
def _kinzhal(c): # аэробаллистическая: корпус с треугольными килями
_launch_base(c)
c.rect(3, 7, 11, 8, "b")
c.poly([(11, 6), (15, 7), (11, 9)], "b")
c.poly([(4, 7), (7, 3), (8, 3), (6, 7)], "d")
c.poly([(4, 8), (7, 12), (8, 12), (6, 8)], "d")
def _mover(c): # четыре стрелки перемещения
c.poly([(8, 0), (4, 5), (12, 5)], "b")
c.rect(7, 4, 8, 7, "b")
c.poly([(8, 15), (4, 10), (12, 10)], "b")
c.rect(7, 8, 8, 11, "b")
c.poly([(0, 8), (5, 4), (5, 12)], "b")
c.rect(4, 7, 7, 8, "b")
c.poly([(15, 8), (10, 4), (10, 12)], "b")
c.rect(8, 7, 11, 8, "b")
def _remover(c): # стрелка вниз над лотком: свёртывание
c.poly([(8, 9), (3, 4), (13, 4)], "b")
c.rect(7, 1, 8, 4, "b")
c.rect(2, 11, 13, 13, "b")
c.line(2, 11, 13, 11, "l")
def _flare(c): # звезда осветительной ракеты на стержне
c.disc(8, 7, 1, "y")
c.line(8, 2, 8, 4, "y")
c.line(8, 10, 8, 12, "y")
c.line(3, 7, 5, 7, "y")
c.line(11, 7, 13, 7, "y")
c.line(4, 4, 5, 5, "w")
c.line(11, 4, 10, 5, "w")
c.line(4, 10, 5, 9, "w")
c.line(11, 10, 10, 9, "w")
c.rect(7, 12, 8, 15, "k")
ICON_DRAW = {
"designator": _designator,
"launch_shahed": _shahed, "launch_geran3": _geran3, "launch_lancet": _lancet,
"launch_kh101": _kh101, "launch_kalibr": _kalibr, "launch_kh22": _kh22,
"launch_zircon": _zircon, "launch_iskander": _iskander, "launch_oreshnik": _oreshnik,
"launch_kinzhal": _kinzhal,
"deploy_patriot": _patriot, "deploy_gepard": _gepard, "deploy_pantsir": _pantsir,
"deploy_s400": _s400, "deploy_s500": _s500, "deploy_irist": _irist,
"deploy_nasams": _nasams, "deploy_buk": _buk, "deploy_tor": _tor, "deploy_p1sun": _p1sun,
"deploy_radar": _radar, "deploy_ew": _ew,
"deploy_depot": _depot, "deploy_tzm": _tzm,
"deploy_light": _searchlight,
"mover": _mover, "remover": _remover, "flare_gun": _flare,
}
def item_icon(name, color, seed):
c = _Icon(color, seed)
ICON_DRAW[name](c)
# Контур по краю силуэта: тёмные предметы — светлым, светлые — тёмным, чтобы читались на обоих фонах.
# Альфа-маска не меняется: контур перекрашивает только уже нарисованные пиксели.
lum = 0.3 * color[0] + 0.59 * color[1] + 0.11 * color[2]
edge = (160, 160, 160) if lum < 100 else (70, 70, 70) if lum > 190 else None
if edge:
src = c.im.copy()
def opaque(x, y):
return 0 <= x < 16 and 0 <= y < 16 and src.getpixel((x, y))[3] > 0
for y in range(16):
for x in range(16):
if opaque(x, y) and not all(opaque(x + dx, y + dy) for dx, dy in ((1, 0), (-1, 0), (0, 1), (0, -1))):
c.im.putpixel((x, y), edge + (255,))
return c.im
def write_json(path, obj):
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "w", encoding="utf-8") as f:
json.dump(obj, f, ensure_ascii=False, indent=1)
def main():
# Только своё: звуки (tools/gen_sounds.py) и sounds.json не трогаем.
for sub in ("blockstates", "items", "lang", "models", "textures"):
shutil.rmtree(os.path.join(ROOT, "assets", NS, sub), ignore_errors=True)
shutil.rmtree(os.path.join(ROOT, "data"), ignore_errors=True)
a = os.path.join(ROOT, "assets", NS)
d = os.path.join(ROOT, "data", NS, "horizon")
os.makedirs(os.path.join(a, "textures", "block"))
os.makedirs(os.path.join(a, "textures", "item"))
lang = {}
for wid in WRECKS:
fn, title = MODELS[wid]
MODELS[wid + "_burnt"] = ((lambda fn=fn, wid=wid: burnt_parts(fn(), zlib.crc32(wid.encode()))), title + " (остов)")
for name, (fn, title) in MODELS.items():
write_json(os.path.join(a, "models", "block", name + ".json"), model(fn(), name.endswith("_burnt")))
write_json(os.path.join(a, "blockstates", name + ".json"), {"variants": {"": {"model": f"{NS}:block/{name}"}}})
write_json(os.path.join(d, "block", name + ".json"),
{"template": "minecraft:stone", "item": False})
lang[f"block.{NS}.{name}"] = title
for name, fn in TEXTURES.items():
fn().save(os.path.join(a, "textures", "block", name + ".png"))
for name, (base, qx, qy, burnt) in VARIANTS.items():
im = STRETCH_FN[base](qx, qy) if (base in STRETCH_FN and (qx > 1 or qy > 1)) else TEXTURES[base]()
if burnt:
im = GT.burn(im, zlib.crc32(name.encode()), "glass" if base.startswith("glass") else "paint")
im.save(os.path.join(a, "textures", "block", name + ".png"))
for i, (name, title, color) in enumerate(ITEMS):
item_icon(name, color, zlib.crc32(name.encode())).save(os.path.join(a, "textures", "item", name + ".png"))
write_json(os.path.join(a, "models", "item", name + ".json"),
{"parent": "minecraft:item/generated", "textures": {"layer0": f"{NS}:item/{name}"}})
write_json(os.path.join(a, "items", name + ".json"),
{"model": {"type": "minecraft:model", "model": f"{NS}:item/{name}"}})
write_json(os.path.join(d, "item", name + ".json"), {"template": "minecraft:stick", "max_stack_size": 1})
lang[f"item.{NS}.{name}"] = title
# Предметы-иконки для камеры монитора: 3D-модель цели в интерфейсе.
for name in CAM_MODELS:
title = MODELS[name][1]
write_json(os.path.join(a, "models", "item", "cam_" + name + ".json"),
{"parent": f"{NS}:block/{name}",
"display": {"gui": {"rotation": [20, 145, 0], "translation": [0, 0, 0], "scale": [0.36, 0.36, 0.36]}}})
write_json(os.path.join(a, "items", "cam_" + name + ".json"),
{"model": {"type": "minecraft:model", "model": f"{NS}:item/cam_{name}"}})
write_json(os.path.join(d, "item", "cam_" + name + ".json"), {"template": "minecraft:stick", "max_stack_size": 1})
lang[f"item.{NS}.cam_{name}"] = title
write_json(os.path.join(a, "lang", "en_us.json"), lang)
write_json(os.path.join(a, "lang", "ru_ru.json"), lang)
print(f"{len(MODELS)} моделей, {len(ITEMS)} предметов, {len(TEXTURES)} текстур -> {ROOT}")
if __name__ == "__main__":
main()