Files
airdef/tools/gen_models.py
chexit 966b52f9fc
build / mod (push) Failing after 18s
feat: add shared radar batteries and animated cold launch
Separate search and engagement roles, support launcher-only SAMs, animate individual tube lids and delay ground strike spending until ejection. Preserve battery configuration and launch poses in saves and replay.

Co-authored-by: Codex <codex@openai.com>
2026-11-02 23:40:40 +03:00

1277 lines
57 KiB
Python
Raw Permalink 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
"""Генерирует модели и registries AirDef, делегируя raster output `gen_tex.py`.
Каждая часть — блок со своей моделью в одной ориентации: нос смотрит на север (-z), центр — (8, 8, 8).
Поворачивает и масштабирует их движок форка (render.set-models), так что ракурсы больше не нужны.
Координаты элементов — в пределах -16..32 (иначе Horizon отбрасывает модель).
Запуск: python3 tools/gen_models.py [--check]
Скрипт строит набор во временном каталоге, накладывает `airdef-mod/source-assets/assets/`, проверяет ссылки
моделей и текстур, JSON и PNG, затем транзакционно переносит результат в airdef-mod/. Список управляемых файлов
с sha256 лежит в airdef-mod/generated-manifest.txt (формат sha256sum). Удаляются и перезаписываются только файлы
из этого списка и только если их не правили вручную; чужой файл на пути генерируемого останавливает запуск.
--check ничего не пишет и выходит с кодом 1, если airdef-mod/ отличается от того, что дал бы запуск.
"""
import json
import os
import sys
import random
import tempfile
import zlib
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from PIL import Image
import gen_tex as GT
from airdef_assets import pipeline
from airdef_assets.bounds import write_rust_bounds
from airdef_assets.registry import write_json, write_registry
from airdef_assets.texture_catalog import RINGED, STRETCH_FN, TEXTURES
from airdef_assets.icons import ITEMS, render_icon
ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", "airdef-mod"))
NS = "airdef"
# ---------------------------------------------------------------- модели
def el(frm, to, tex, faces=None, rot=None, omit_faces=()):
"""Элемент; rot = (ось, угол) — поворот вокруг центра элемента."""
return {"from": [float(v) for v in frm], "to": [float(v) for v in to], "tex": tex,
"faces": faces, "rot": rot, "omit_faces": tuple(omit_faces)}
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"):
if f in p.get("omit_faces", ()):
continue
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", front_cap=True):
"""Пакет ТПК: 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)
if front_cap:
p.append(el((x, y, -16), (x + size, y + size, 32), tex,
{"north": "tube_cap", "south": "tube_cap"}))
else:
p.append(el((x, y, -16), (x + size, y + size, 32), tex,
{"south": "tube_cap"}, omit_faces=("north",)))
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 tpk_cap():
"""Reusable stylized octagonal cap, centered at (8, 8, 8), for animated TPK covers."""
z0, z_face, z1 = 7.7, 8.25, 8.3
# Three cuboids form a flat octagonal silhouette while keeping the geometry blocky.
return [el((2.5, 0, z0), (13.5, 16, z_face), "tube_cap"),
el((0, 2.5, z0), (2.5, 13.5, z_face), "tube_cap"),
el((13.5, 2.5, z0), (16, 13.5, z_face), "tube_cap"),
el((5, 5, z_face), (11, 11, z1), "dark_metal")]
def radar_mast():
"""Generic two-block telescoping radar mast segment; stackable for role-based height."""
return [el((4, 0, 4), (12, 3, 12), "dark_metal"),
el((5.5, 3, 5.5), (10.5, 18, 10.5), "metal"),
el((4.5, 18, 4.5), (11.5, 21, 11.5), "dark_metal"),
el((6.5, 21, 6.5), (9.5, 30, 9.5), "dark_metal"),
el((5.5, 30, 5.5), (10.5, 32, 10.5), "metal"),
el((7, 31.5, 7), (9, 32, 9), "dark_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, front_cap=False), "ТПК 77Н6"),
"tpk_cap": (tpk_cap, "Крышка ТПК (анимация)"),
"radar_mast": (radar_mast, "Радиомачта (секция высоты)"),
"radar_trailer": (radar_trailer, "Прицеп РЛС"),
"radar_panel": (big_radar_panel, "РЛС обзора"),
"m57e6": (missile_57e6, "57Э6"),
"m48n6": (missile_48n6, "48Н6"),
# Own identity for the coordinate-strike game profile; stylized geometry based on the existing family.
"rm48u": (lambda: missile_48n6() + [el((5.9, 5.9, 2), (10.1, 10.1, 3), "missile_grey")], "RM48U (игровой профиль)"),
"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", "tpk_cap", "radar_mast",
"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"]
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"))
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, _) in MODELS.items():
write_json(os.path.join(a, "models", "block", name + ".json"), model(fn(), name.endswith("_burnt")))
GT.write_block_textures(a, TEXTURES, VARIANTS, STRETCH_FN)
for name, _, color in ITEMS:
GT.write_item_icon(a, name, render_icon(name, color, zlib.crc32(name.encode())))
write_registry(out, NS, [(name, title) for name, (_, title) in MODELS.items()], ITEMS, CAM_MODELS)
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
def prune_empty(root, 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 regenerate(root=ROOT, source_root=None, manifest=None, *, check=False, report=False):
"""Build, validate, plan and install one complete generated runtime tree."""
root = os.path.abspath(root)
source_root = source_root or os.path.join(root, "source-assets")
manifest = manifest or os.path.join(root, "generated-manifest.txt")
try:
old = pipeline.read_manifest(manifest)
except ValueError as error:
raise ValueError(str(error)) from error
with tempfile.TemporaryDirectory() as stage:
counts = generate(stage)
pipeline.overlay_sources(stage, source_root)
errors = validate(stage)
if errors:
details = "\n".join(f" - {err}" for err in errors)
raise ValueError(f"ПРОВЕРКА НАБОРА НЕ ПРОЙДЕНА, ошибок: {len(errors)}. Ничего не записано в {root}.\n{details}")
write_rust_bounds(stage)
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, "/")] = pipeline.sha256(path)
plan = pipeline.make_plan(root, old, new)
create, update, same = plan.create, plan.update, plan.same
conflict, remove, keep = plan.conflict, plan.remove, plan.keep
if conflict:
names = "\n".join(f" - {rel}" for rel in conflict)
raise RuntimeError(f"КОНФЛИКТ: файлы заняты или правлены вручную:\n{names}\nНичего не записано.")
manifest_text = pipeline.manifest_text(new)
if os.path.exists(manifest):
with open(manifest, encoding="utf-8") as stream:
manifest_old = stream.read()
else:
manifest_old = None
manifest_changed = manifest_text != manifest_old
differs = bool(create or update or remove or manifest_changed)
if report:
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:
print("различий нет" if not differs else "различия с генератором: см. выше")
if not check and differs:
pipeline.install(root, stage, manifest, plan, new)
for rel in remove:
prune_empty(root, os.path.join(root, rel))
models, items, textures = counts
if report and not check:
print(f"{models} моделей, {items} предметов, {textures} текстур -> {root}: "
f"создано {len(create)}, обновлено {len(update)}, удалено {len(remove)}, без изменений {len(same)}")
return differs
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
try:
differs = regenerate(check=check, report=True)
except (ValueError, RuntimeError) as error:
sys.exit(str(error))
if check and differs:
sys.exit(1)
if __name__ == "__main__":
main()