Files
airdef/tools/gen_models.py
T
chexit 57a5426e65
build / mod (push) Successful in 4m13s
Strikes on launch sites, command map and placement editor, fire doctrines, ground routes, acoustics switch and reverb
#101: radars locate sites from launches, /airdef sites, /airdef strike; sites launch slower (15/25/50 ticks) so a strike can come in time. #48: M opens the map on the network's picture, with an editor. #47: guard, sector, reserve and salvo per system. #94: vehicles route round slopes and water and say when they cannot get through. #96: reverb, and a menu switch for the acoustics. #92: generated files are validated before the swap, repo map.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 00:59:03 +03:00

1960 lines
83 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 [--check]
Скрипт пишет во временный каталог, проверяет набор (validate: ссылки моделей и текстур, JSON, PNG) и только потом
переносит результат в airdef-mod/; при ошибке проверки ничего не записывает. Список созданных им файлов
с sha256 лежит в airdef-mod/generated-manifest.txt (формат sha256sum). Удаляются и перезаписываются только файлы
из этого списка и только если их не правили вручную; чужой файл на пути генерируемого останавливает запуск.
--check ничего не пишет и выходит с кодом 1, если airdef-mod/ отличается от того, что дал бы запуск.
"""
import hashlib
import json
import math
import os
import sys
import random
import shutil
import tempfile
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
def launch_ramp():
"""Рампа-катапульта для дронов: основание, наклонные рельсы (нос на север поднят), опоры и ложе."""
p = [el((1, 0, -6), (15, 2, 30), "dark_metal"), el((6, 2, 0), (10, 4, 26), "metal")]
p += both(el((3, 9, -14), (5, 11, 30), "metal", rot=("x", 22.5)))
p += both(el((3, 0, -1), (5, 12, 1), "dark_metal")) + both(el((3, 0, 17), (5, 6, 19), "dark_metal"))
p += [el((4, 10, 24), (12, 14, 30), "dark_metal"), el((1, 2, 28), (15, 6, 32), "ru_khaki")]
return p
def launcher_truck():
"""Пусковая машина ракет: шасси МАЗ и наклонный пакет труб, смотрящий вперёд (на север) вверх."""
p = maz_chassis("ru_khaki")
for x in (4, 10):
p.append(el((x, 15, -4), (x + 5, 20, 30), "tube", {"north": "tube_cap", "south": "tube_cap"}, rot=("x", 45)))
p += [el((3, 13, 22), (13, 17, 28), "dark_metal")]
return p
def barge():
"""Баржа под пусковой комплект: плоская палуба 3×3 блока, корпус, борта, кнехты и скошенный нос."""
p = [el((-16, 0, -16), (32, 3, 32), "dark_metal"),
el((-16, 3, -16), (32, 4, 32), "metal"),
el((-15, 3, -15), (31, 4.4, 31), "ru_khaki")]
p += both(el((-16, 4, -16), (-14, 6, 32), "dark_metal"))
p += [el((-16, 4, 30), (32, 6, 32), "dark_metal"), el((-16, 4, -16), (32, 6, -14), "dark_metal")]
for z in (-10, 8, 26):
p += both(el((-12, 4.4, z), (-9, 6.5, z + 3), "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, "Транспортно-заряжающая машина"),
"barge": (barge, "Баржа"),
"launch_ramp": (launch_ramp, "Рампа запуска дронов"),
"launcher_truck": (launcher_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 generate(out):
# Пишет весь набор в каталог out (зеркало airdef-mod/). Звуки (tools/gen_sounds.py) и sounds.json не трогаем.
a = os.path.join(out, "assets", NS)
d = os.path.join(out, "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)
return len(MODELS), len(ITEMS), len(TEXTURES)
# ---------------------------------------------------------------- проверка набора до переноса
def check_png(path, rel):
try:
with Image.open(path) as im:
w, h = im.size
im.load()
except Exception as e: # битый файл или не PNG
return [f"{rel}: PNG не открывается: {e}"]
errs = []
if w != h:
errs.append(f"{rel}: текстура не квадратная, {w}x{h}")
if w < 8 or w > 256 or w & (w - 1):
errs.append(f"{rel}: размер {w}x{h}, ожидается степень двойки от 8 до 256")
return errs
def validate(stage):
# Проверяет набор в каталоге stage до того, как он попадёт в airdef-mod/. Возвращает список ошибок.
# Ссылки airdef:<путь> проверяем по набору; ссылки minecraft:... и без пространства имён — ванильные, их не проверяем.
errors = []
files, docs = set(), {}
for dirpath, _, names in os.walk(stage):
for name in sorted(names):
path = os.path.join(dirpath, name)
rel = os.path.relpath(path, stage).replace(os.sep, "/")
files.add(rel)
if name.endswith(".json"):
try:
with open(path, encoding="utf-8") as f:
docs[rel] = json.load(f)
except (OSError, ValueError) as e:
errors.append(f"{rel}: не разбирается как JSON: {e}")
elif name.endswith(".png"):
errors.extend(check_png(path, rel))
def check_ref(ref, kind, ext, owner):
if not isinstance(ref, str) or not ref.startswith(NS + ":"):
return
path = ref[len(NS) + 1:]
if not path or path.startswith("/") or ".." in path.split("/"):
errors.append(f"{owner}: недопустимая ссылка {ref}")
return
rel = f"assets/{NS}/{kind}/{path}{ext}"
if rel not in files:
errors.append(f"{owner}: ссылка {ref} не найдена ({rel})")
def pick(prefix):
return sorted(rel for rel in docs if rel.startswith(f"assets/{NS}/{prefix}"))
for rel in pick("models/"):
doc = docs[rel]
if not isinstance(doc, dict):
errors.append(f"{rel}: модель должна быть объектом JSON")
continue
if "parent" in doc:
check_ref(doc["parent"], "models", ".json", rel)
tex = doc.get("textures", {})
for key, val in tex.items():
if isinstance(val, str) and val.startswith("#"):
if val[1:] not in tex:
errors.append(f"{rel}: текстура {val} не объявлена")
else:
check_ref(val, "textures", ".png", rel)
for i, el in enumerate(doc.get("elements", [])):
for corner in ("from", "to"):
pos = el.get(corner, [])
if len(pos) != 3 or any(not -16 <= c <= 32 for c in pos):
errors.append(f"{rel}: элемент {i}, {corner} {pos} вне -16..32")
for face, fd in el.get("faces", {}).items():
tex_ref = fd.get("texture")
if tex_ref is None:
errors.append(f"{rel}: элемент {i}, грань {face} без texture")
elif tex_ref.startswith("#"):
if tex_ref[1:] not in tex:
errors.append(f"{rel}: элемент {i}, грань {face}: {tex_ref} не объявлена")
else:
check_ref(tex_ref, "textures", ".png", rel)
for rel in pick("blockstates/"):
for variant in docs[rel].get("variants", {}).values():
for m in (variant if isinstance(variant, list) else [variant]):
check_ref(m.get("model"), "models", ".json", rel)
for rel in pick("items/"):
check_ref(docs[rel].get("model", {}).get("model"), "models", ".json", rel)
# У каждого блока есть blockstate, и наоборот.
blocks = {rel[len(f"assets/{NS}/models/block/"):] for rel in pick("models/block/")}
states = {rel[len(f"assets/{NS}/blockstates/"):] for rel in pick("blockstates/")}
for name in sorted(blocks - states):
errors.append(f"models/block/{name}: нет blockstate")
for name in sorted(states - blocks):
errors.append(f"blockstates/{name}: нет модели блока")
en = docs.get(f"assets/{NS}/lang/en_us.json")
ru = docs.get(f"assets/{NS}/lang/ru_ru.json")
if en is None or ru is None:
errors.append("нет lang/en_us.json или lang/ru_ru.json")
elif set(en) != set(ru):
errors.append(f"lang: ключи en_us и ru_ru различаются: {sorted(set(en) ^ set(ru))[:5]}")
return errors
MANIFEST = os.path.join(ROOT, "generated-manifest.txt")
def file_sha(path):
with open(path, "rb") as f:
return hashlib.sha256(f.read()).hexdigest()
def read_manifest():
# Строки "<sha256> <путь от airdef-mod>", как у sha256sum. Только файлы, которые записал этот скрипт.
if not os.path.exists(MANIFEST):
return {}
entries = {}
with open(MANIFEST, encoding="utf-8") as f:
for line in f:
digest, rel = line.rstrip("\n").split(" ", 1)
if rel.startswith("/") or ".." in rel.split("/"):
sys.exit(f"{MANIFEST}: недопустимый путь {rel}")
entries[rel] = digest
return entries
def prune_empty(path):
d = os.path.dirname(path)
while d != ROOT and os.path.isdir(d) and not os.listdir(d):
os.rmdir(d)
d = os.path.dirname(d)
def main():
args = sys.argv[1:]
if any(arg != "--check" for arg in args):
sys.exit("Использование: python3 tools/gen_models.py [--check]")
check = "--check" in args
old = read_manifest()
with tempfile.TemporaryDirectory() as stage:
counts = generate(stage)
errors = validate(stage)
if errors:
print(f"ПРОВЕРКА НАБОРА НЕ ПРОЙДЕНА, ошибок: {len(errors)}. Ничего не записано в {ROOT}.")
for err in errors:
print(f" - {err}")
sys.exit(1)
new = {}
for dirpath, dirs, names in os.walk(stage):
dirs.sort()
for name in sorted(names):
path = os.path.join(dirpath, name)
new[os.path.relpath(path, stage).replace(os.sep, "/")] = file_sha(path)
# create: файла нет; update: файл записан генератором и с тех пор не менялся; same: уже совпадает;
# conflict: на пути лежит чужой или вручную правленный файл, поэтому ничего не пишем.
create, update, same, conflict = [], [], [], []
for rel in sorted(new):
target = os.path.join(ROOT, rel)
if not os.path.exists(target):
create.append(rel)
elif not os.path.isfile(target):
conflict.append(rel)
else:
cur = file_sha(target)
if cur == new[rel]:
same.append(rel)
elif rel in old and old[rel] == cur:
update.append(rel)
else:
conflict.append(rel)
# remove: генератор больше не создаёт файл и он не менялся; keep: не создаёт, но правлен вручную, оставляем.
remove, keep = [], []
for rel in sorted(old):
target = os.path.join(ROOT, rel)
if rel in new or not os.path.isfile(target):
continue
(remove if file_sha(target) == old[rel] else keep).append(rel)
if conflict:
for rel in conflict:
print(f"КОНФЛИКТ: {rel} занят файлом, которого нет в manifest (или он правлен вручную)")
sys.exit("Ничего не записано. Уберите или переименуйте эти файлы и запустите снова.")
manifest_text = "".join(f"{new[rel]} {rel}\n" for rel in sorted(new))
manifest_old = None
if os.path.exists(MANIFEST):
with open(MANIFEST, encoding="utf-8") as f:
manifest_old = f.read()
manifest_changed = manifest_text != manifest_old
for rel in keep:
print(f"ВНИМАНИЕ: {rel} больше не генерируется, но правлен вручную: оставлен, из manifest убран")
for label, rels in (("новый", create), ("обновить", update), ("удалить", remove)):
for rel in rels:
print(f"{label}: {rel}")
if manifest_changed:
print("manifest: обновить")
if check:
differs = create or update or remove or manifest_changed
print("различий нет" if not differs else "различия с генератором: см. выше")
sys.exit(1 if differs else 0)
for rel in create + update:
dst = os.path.join(ROOT, rel)
os.makedirs(os.path.dirname(dst), exist_ok=True)
shutil.copyfile(os.path.join(stage, rel), dst)
for rel in remove:
path = os.path.join(ROOT, rel)
os.remove(path)
prune_empty(path)
if manifest_changed:
with open(MANIFEST, "w", encoding="utf-8", newline="\n") as f:
f.write(manifest_text)
models, items, textures = counts
print(f"{models} моделей, {items} предметов, {textures} текстур -> {ROOT}: "
f"создано {len(create)}, обновлено {len(update)}, удалено {len(remove)}, без изменений {len(same)}")
if __name__ == "__main__":
main()