966b52f9fc
build / mod (push) Failing after 18s
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>
1277 lines
57 KiB
Python
1277 lines
57 KiB
Python
#!/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()
|