#!/usr/bin/env python3 """Генерирует контент мода AirDef (airdef-mod/assets и data): текстуры, модели частей, предметы. Каждая часть — блок со своей моделью в одной ориентации: нос смотрит на север (-z), центр — (8, 8, 8). Поворачивает и масштабирует их движок форка (render.set-models), так что ракурсы больше не нужны. Координаты элементов — в пределах -16..32 (иначе Horizon отбрасывает модель). Запуск: python3 tools/gen_models.py """ import json import os import random import shutil from PIL import Image ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", "airdef-mod")) NS = "airdef" # ---------------------------------------------------------------- текстуры def clamp(v): return max(0, min(255, int(v))) def noise(base, amp, seed, size=16): rnd = random.Random(seed) im = Image.new("RGBA", (size, size)) for y in range(size): for x in range(size): d = rnd.randint(-amp, amp) im.putpixel((x, y), tuple(clamp(c + d) for c in base) + (255,)) return im def panels(base, seed, size=16, every=8): im = noise(base, 4, seed, size) line = tuple(clamp(c - 16) for c in base) + (255,) lite = tuple(clamp(c + 10) for c in base) + (255,) for i in range(size): for k in range(0, size, every): im.putpixel((i, k), line) im.putpixel((k, i), line) if k + 1 < size: im.putpixel((i, k + 1), lite) rnd = random.Random(seed + 99) for _ in range(size // 2): # заклёпки x, y = rnd.randrange(size), rnd.randrange(size) im.putpixel((x, y), lite) return im def camo(colors, seed, size=32, blobs=7): rnd = random.Random(seed) im = noise(colors[0], 4, seed, size) for col in colors[1:]: for _ in range(blobs): cx, cy = rnd.randrange(size), rnd.randrange(size) rx, ry = rnd.randint(2, 6), rnd.randint(1, 4) for y in range(cy - ry, cy + ry + 1): for x in range(cx - rx, cx + rx + 1): if ((x - cx) / rx) ** 2 + ((y - cy) / ry) ** 2 <= 1 + rnd.random() * 0.3: im.putpixel((x % size, y % size), tuple(clamp(c + rnd.randint(-5, 5)) for c in col) + (255,)) return im def stripes(base, line, step, seed, size=16): im = noise(base, 3, seed, size) for y in range(0, size, step): for x in range(size): im.putpixel((x, y), line + (255,)) return im def gradient(top, bottom, size=16): im = Image.new("RGBA", (size, size)) for y in range(size): t = y / (size - 1) c = tuple(clamp(top[i] + (bottom[i] - top[i]) * t) for i in range(3)) for x in range(size): im.putpixel((x, y), c + (255,)) return im def canister_front(): im = noise((92, 100, 72), 3, 11) for cx in (4, 11): for cy in (4, 11): for y in range(16): for x in range(16): d = (x - cx + 0.5) ** 2 + (y - cy + 0.5) ** 2 if d <= 9: im.putpixel((x, y), (36, 38, 30, 255)) elif d <= 12: im.putpixel((x, y), (120, 124, 100, 255)) return im def radar_face(): im = noise((70, 76, 56), 3, 17) for y in range(1, 15): for x in range(1, 15): if (x + y) % 3 == 0: im.putpixel((x, y), (54, 58, 44, 255)) return im def glass(): im = noise((40, 58, 78), 5, 31) for i in range(16): im.putpixel((i, 15 - i), (130, 160, 180, 255)) if i < 15: im.putpixel((i + 1, 15 - i), (100, 130, 150, 255)) return im def missile(): im = noise((232, 232, 226), 3, 21) for x in range(16): for y in (3, 4): im.putpixel((x, y), (40, 40, 40, 255)) return im def target(): im = Image.new("RGBA", (16, 16)) for y in range(16): for x in range(16): d = abs(x - 7.5) + abs(y - 7.5) im.putpixel((x, y), (255, clamp(40 + d * 12), 40, 255)) return im def fire(seed): rnd = random.Random(seed) im = Image.new("RGBA", (16, 16)) for y in range(16): for x in range(16): t = rnd.random() im.putpixel((x, y), (255, clamp(140 + 110 * t), clamp(30 + 120 * t * t), 255)) return im def missile_ru(): im = noise((226, 228, 222), 3, 49) for x in range(16): im.putpixel((x, 5), (180, 40, 40, 255)) im.putpixel((x, 11), (40, 40, 40, 255)) return im def tube_cap(): im = noise((80, 88, 60), 3, 50) for y in range(16): for x in range(16): d = (x - 7.5) ** 2 + (y - 7.5) ** 2 if d <= 36: im.putpixel((x, y), (44, 48, 36, 255)) elif d <= 48: im.putpixel((x, y), (120, 128, 96, 255)) return im def tubes6(): im = noise((82, 96, 60), 3, 51) for cx in (3, 8, 13): for cy in (4, 11): for y in range(16): for x in range(16): if (x - cx) ** 2 + (y - cy) ** 2 <= 4: im.putpixel((x, y), (30, 32, 26, 255)) return im def radar_big(): im = noise((92, 100, 70), 3, 52) for y in range(16): for x in range(16): if x % 2 == 0 and y % 2 == 0: im.putpixel((x, y), (60, 66, 46, 255)) return im TEXTURES = { "shahed": lambda: panels((66, 68, 70), 1), "shahed_nose": lambda: noise((104, 106, 108), 4, 2), "geran": lambda: panels((28, 28, 31), 3), "geran_engine": lambda: panels((60, 60, 64), 4), "intake": lambda: noise((14, 14, 14), 2, 5), "prop": lambda: noise((22, 22, 22), 3, 6), "missile": missile, "nozzle": lambda: noise((44, 42, 40), 4, 7), "flame": lambda: gradient((255, 250, 210), (255, 120, 20)), "fire": lambda: fire(8), "nato_green": lambda: camo([(76, 86, 58), (58, 68, 44), (44, 48, 34)], 9), "flecktarn": lambda: camo([(92, 100, 62), (62, 74, 44), (104, 84, 54), (30, 32, 26)], 10), "track": lambda: stripes((36, 36, 34), (74, 74, 70), 3, 11), "tire": lambda: stripes((24, 24, 24), (40, 40, 40), 4, 12), "canister": lambda: panels((94, 102, 74), 13), "canister_front": canister_front, "radar_face": radar_face, "glass": glass, "metal": lambda: panels((118, 120, 122), 14), "dark_metal": lambda: panels((52, 54, 52), 15), "barrel": lambda: noise((40, 42, 38), 3, 16), "dish": lambda: noise((176, 178, 174), 5, 18), "target": target, "foam": lambda: panels((196, 180, 150), 40), "kh101": lambda: panels((84, 88, 92), 41), "kh101_belly": lambda: noise((150, 152, 150), 4, 42), "lancet": lambda: panels((70, 78, 66), 43), "ru_green": lambda: camo([(82, 96, 60), (54, 70, 40), (110, 96, 64), (34, 36, 28)], 44, blobs=6), "ru_khaki": lambda: camo([(108, 110, 80), (84, 90, 60), (60, 64, 44)], 45, blobs=5), "missile_ru": missile_ru, "missile_grey": lambda: panels((150, 154, 156), 46), "booster": lambda: noise((60, 62, 58), 3, 47), "tube": lambda: stripes((96, 106, 74), (70, 78, 54), 4, 48), "tube_cap": tube_cap, "tubes6": tubes6, "radar_big": radar_big, "tracer": lambda: noise((255, 40, 20), 6, 60), "tracer_hot": lambda: noise((255, 190, 150), 6, 61), "white": lambda: noise((250, 250, 246), 2, 19), "nato_tan": lambda: camo([(150, 130, 96), (124, 106, 76), (96, 84, 62)], 62, blobs=5), "crate": lambda: stripes((92, 78, 48), (70, 58, 34), 5, 63), "concrete": lambda: panels((132, 130, 124), 64), "lamp": lambda: gradient((255, 255, 240), (255, 230, 170)), "beam": lambda: gradient((255, 250, 225), (255, 240, 200)), "antenna": lambda: noise((70, 72, 70), 3, 65), "nose_dark": lambda: noise((58, 60, 62), 3, 66), } # ---------------------------------------------------------------- модели def el(frm, to, tex, faces=None, rot=None): """Элемент; rot = (ось, угол) — поворот вокруг центра элемента.""" return {"from": [float(v) for v in frm], "to": [float(v) for v in to], "tex": tex, "faces": faces, "rot": rot} def mx(e): """Зеркальная копия относительно x = 8.""" rot = e.get("rot") if rot and rot[0] in ("y", "z"): rot = (rot[0], -rot[1]) return {**e, "from": [16 - e["to"][0], e["from"][1], e["from"][2]], "to": [16 - e["from"][0], e["to"][1], e["to"][2]], "rot": rot} def both(e): return [e, mx(e)] def face_uv(frm, to, face): dx, dy, dz = (abs(to[i] - frm[i]) for i in range(3)) w, h = {"north": (dx, dy), "south": (dx, dy), "east": (dz, dy), "west": (dz, dy), "up": (dx, dz), "down": (dx, dz)}[face] return [0, 0, max(0.25, min(16, w)), max(0.25, min(16, h))] def model(parts): 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: textures[p["tex"]] = f"{NS}:block/{p['tex']}" faces = {} for f in ("north", "south", "east", "west", "up", "down"): tex = (p["faces"] or {}).get(f, p["tex"]) textures[tex] = f"{NS}:block/{tex}" faces[f] = {"uv": face_uv(p["from"], p["to"], f), "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"] = f"{NS}:block/{parts[0]['tex']}" return {"ambientocclusion": False, "textures": textures, "elements": elements} 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 += both(el((-9, 0, z), (-5.5, 9, z + 8), "tire")) 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 += both(el((-10, 0, z), (-6.5, 9, z + 8), "tire")) 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 += both(el((-9, 0, z), (-5.5, 9, z + 8), "tire")) return p def gepard_hull(): p = [el((-10, 4, -12), (26, 17, 30), "flecktarn"), el((-10, 4, -16), (26, 12, -12), "flecktarn"), el((-8, 17, 18), (24, 18, 30), "dark_metal"), el((-6, 17, -10), (4, 18, -4), "metal")] p += both(el((-14, 0, -14), (-10, 10, 31), "track")) p += both(el((-14.5, 6, -14), (-10, 11, 31), "flecktarn")) # надгусеничные полки return p def gepard_turret(): p = [el((-4, 0, -6), (20, 12, 16), "flecktarn"), el((-2, 12, -2), (18, 14, 14), "flecktarn"), el((3, 4, -10), (13, 12, -6), "radar_face"), # РЛС сопровождения el((7, 14, 10), (9, 18, 12), "metal")] return p def gepard_guns(): """Две 35-мм пушки по бортам башни; ось качания — центр модели.""" p = both(el((-8, 5, -16), (-5.5, 7.5, 12), "barrel")) p += both(el((-9, 3.5, 2), (-4, 9, 14), "flecktarn")) p += both(el((-8.5, 4.5, -16), (-5, 8, -13), "dark_metal")) # дульные тормоза return p def gepard_dish(): """Обзорная РЛС: вращается вокруг вертикальной оси.""" return [el((-4, 6, 6), (20, 14, 8), "dish"), el((7, 0, 6), (9, 8, 8), "metal")] def target_marker(): return [el((7, -8, 7), (9, 24, 9), "target"), el((2, 7.5, 2), (14, 8.5, 14), "target"), el((5, 3, 5), (11, 13, 11), "target")] def fireball(): return [el((2, 2, 2), (14, 14, 14), "fire"), el((0, 4, 4), (16, 12, 12), "fire"), el((4, 0, 4), (12, 16, 12), "fire"), el((4, 4, 0), (12, 12, 16), "fire")] def flash(): return [el((7, 7, -4), (9, 9, 20), "white"), el((-4, 7, 7), (20, 9, 9), "white"), el((7, -4, 7), (9, 20, 9), "white"), el((5, 5, 5), (11, 11, 11), "flame")] def debris(): return [el((5, 6, 3), (11, 9, 13), "dark_metal"), el((2, 7, 6), (14, 8, 10), "shahed")] def lancet(): p = taper("lancet", -16, -10, 2, 3.5, 2, 3.5, 3) p += [el((6.2, 6.2, -10), (9.8, 9.8, 20), "lancet"), el((6.8, 4.8, -9), (9.2, 6.2, -5), "intake")] # оптика for z0, z1, half in ((-5, 1, 13), (12, 18, 11)): # Х-образные крылья for ang in (45, -45): p.append(el((8 - half, 7.7, z0), (8 + half, 8.3, z1), "lancet", rot=("z", ang))) p += [el((6.8, 6.8, 20), (9.2, 9.2, 22), "dark_metal")] return p def gerbera(): p = taper("foam", -15, -10, 2.5, 4.5, 2.5, 4.5, 3) p += [el((5.8, 5.8, -10), (10.2, 10.2, 20), "foam")] p += delta("foam", 0, 18, 22, 17, 7, 7.3, 8.5) p += both(el((-9.6, 6.5, 15), (-8.6, 13, 22), "foam")) p += [el((6.5, 6.5, 20), (9.5, 9.5, 24), "dark_metal")] return p def kh101(): p = taper("kh101", -16, -9, 2, 6, 2, 4.5, 4, y=7.5) p += [el((5, 5.5, -9), (11, 8, 30), "kh101", {"down": "kh101_belly"}), el((6, 8, -6), (10, 10.5, 28), "kh101")] p += [el((-12, 7.1, 4), (28, 7.7, 10), "kh101")] # крылья p += [el((7.6, 10.5, 24), (8.4, 15, 30), "kh101")] # киль p += both(el((1, 7.2, 25), (6, 7.8, 30), "kh101")) p += [el((6, 2, 15), (10, 5.5, 28), "dark_metal"), # двигатель под хвостом el((6.5, 2.5, 14), (9.5, 5, 15), "intake"), el((6.5, 2.5, 28), (9.5, 5, 29.5), "nozzle")] return p def wheels(zs, x0=-9, x1=-5.5, tex="tire"): out = [] for z in zs: out += both(el((x0, 0, z), (x1, 9, z + 7), tex)) return out def pantsir_truck(): p = [el((-6, 5, -16), (22, 9, 32), "ru_green"), el((-6, 9, -16), (22, 21, -6), "ru_green", {"north": "glass"}), el((-5, 13, -16), (21, 20, -15.9), "glass"), el((-5, 9, -5), (21, 18, 6), "ru_green"), el((-4, 9, 8), (20, 12, 30), "dark_metal"), el((-6.5, 3, 26), (22.5, 6, 32), "dark_metal")] return p + wheels((-13, -4, 16, 24)) def pantsir_turret(): return [el((-2, 0, -6), (18, 10, 16), "ru_green"), el((1, 1, -11), (15, 11, -6), "radar_face"), el((7, 10, 4), (9, 17, 6), "metal"), el((0, 10, 8), (16, 12, 16), "ru_green")] def pantsir_pack(): """Пакеты ракет 57Э6 по бортам и спаренные 30-мм пушки над ними; ось качания — центр.""" p = both(el((-11, 2, -16), (-3, 12, 12), "ru_green", {"north": "tubes6"})) p += both(el((-9.5, 12.5, -16), (-8, 14, 6), "barrel")) p += both(el((-6.5, 12.5, -16), (-5, 14, 6), "barrel")) p += both(el((-10, 12, 2), (-4, 15, 10), "dark_metal")) return p def flat_radar(tex="radar_face"): """Плоская вращающаяся антенна (ось — вертикаль через центр модели).""" return [el((-6, 2, 7), (22, 16, 9), tex), el((-6.5, 1.5, 9), (22.5, 16.5, 10), "dark_metal"), el((7, -8, 8), (9, 2, 10), "metal")] def maz_chassis(tex): """8×8 шасси МАЗ-543/БАЗ с раздельными кабинами; рама под пусковой контейнер.""" p = [el((-6, 5, -16), (22, 10, 32), tex), el((-6, 10, -16), (3, 19, -7), tex, {"north": "glass"}), el((13, 10, -16), (22, 19, -7), tex, {"north": "glass"}), el((3, 10, -16), (13, 15, -6), "dark_metal"), el((-5, 10, -6), (21, 13, 4), tex), el((-3, 10, 26), (19, 14, 32), "dark_metal")] p += both(el((-8, 0, 28), (-6, 6, 31), "metal")) # опоры return p + wheels((-14, -5, 13, 22)) def tubes(n_wide, n_high, size, tex="tube"): """Пакет ТПК: n_wide × n_high труб сечением size, от z=-16 до 32. Ось подъёма — задняя нижняя кромка.""" p = [] w = n_wide * size + (n_wide - 1) x0 = 8 - w / 2 for i in range(n_wide): for j in range(n_high): x = x0 + i * (size + 1) y = j * (size + 1) p.append(el((x, y, -16), (x + size, y + size, 32), tex, {"north": "tube_cap", "south": "tube_cap"})) p += [el((x0 - 1, 0, 26), (x0 + w + 1, n_high * (size + 1) - 1, 28), "dark_metal"), el((x0 - 1, 0, -2), (x0 + w + 1, n_high * (size + 1) - 1, 0), "dark_metal")] return p def radar_trailer(): p = [el((-6, 5, -12), (22, 10, 26), "ru_khaki"), el((2, 10, 2), (14, 16, 14), "dark_metal"), el((-5, 10, 16), (21, 18, 26), "ru_khaki")] return p + wheels((-9, 17)) def big_radar_panel(): return [el((-10, 0, 7), (26, 26, 9), "radar_big"), el((-10.5, -0.5, 9), (26.5, 26.5, 10), "dark_metal"), el((6, -6, 8), (10, 0, 12), "metal")] def missile_57e6(): p = taper("missile_ru", -16, -12, 0.5, 2, 0.5, 2, 2) p += [el((7, 7, -12), (9, 9, 4), "missile_ru"), el((6.4, 6.4, 4), (9.6, 9.6, 28), "booster")] p += both(el((3.5, 7.7, 22), (6.4, 8.3, 28), "booster")) p += [el((7.7, 3.5, 22), (8.3, 6.4, 28), "booster"), el((7.7, 9.6, 22), (8.3, 12.5, 28), "booster")] return p def missile_48n6(): p = taper("missile_ru", -16, -11, 1, 3.5, 1, 3.5, 3) p += [el((6.25, 6.25, -11), (9.75, 9.75, 28), "missile_ru"), el((6.5, 6.5, 28), (9.5, 9.5, 30), "nozzle")] p += both(el((1.5, 7.7, 20), (6.25, 8.3, 28), "missile_ru")) p += [el((7.7, 1.5, 20), (8.3, 6.25, 28), "missile_ru"), el((7.7, 9.75, 20), (8.3, 14.5, 28), "missile_ru")] return p def missile_77n6(): p = taper("missile_grey", -16, -10, 1, 4, 1, 4, 3) p += [el((6, 6, -10), (10, 10, 30), "missile_grey"), el((6, 6, 4), (10, 10, 5), "booster"), el((6.5, 6.5, 30), (9.5, 9.5, 32), "nozzle")] p += both(el((3, 7.7, 24), (6, 8.3, 30), "missile_grey")) p += [el((7.7, 3, 24), (8.3, 6, 30), "missile_grey"), el((7.7, 10, 24), (8.3, 13, 30), "missile_grey")] return p def kalibr(): p = taper("missile_grey", -16, -11, 1.5, 4.5, 1.5, 4.5, 3) p += [el((5.75, 5.75, -11), (10.25, 10.25, 30), "missile_grey"), el((-6, 7.6, 6), (22, 8.4, 11), "missile_grey"), el((7.6, 10.25, 25), (8.4, 14, 30), "missile_grey"), el((6.5, 3, 18), (9.5, 5.75, 26), "dark_metal")] p += both(el((2, 7.6, 26), (5.75, 8.4, 30), "missile_grey")) return p def kh22(): """Х-22/Х-32: толстое тело, треугольные крылья, киль сверху и снизу.""" p = taper("missile_ru", -16, -8, 1.5, 6, 1.5, 6, 4) p += [el((5, 5, -8), (11, 11, 30), "missile_ru"), el((7.5, 11, 14), (8.5, 17, 30), "missile_ru"), el((7.5, -1, 18), (8.5, 5, 30), "missile_ru"), el((5.5, 5.5, 30), (10.5, 10.5, 32), "nozzle")] p += delta("missile_ru", 4, 22, 28, 12, 5, 7.6, 8.4) return p def zircon(): p = taper("dark_metal", -16, -6, 1, 5, 1, 4, 4) p += [el((5.5, 6, -6), (10.5, 10, 30), "kh101"), el((6, 3.5, 0), (10, 6, 24), "intake"), el((5.5, 6, 30), (10.5, 10, 32), "nozzle")] p += both(el((1, 7.6, 22), (5.5, 8.4, 30), "kh101")) p += [el((7.6, 10, 22), (8.4, 14, 30), "kh101")] return p def iskander(): """9М723: толстая ракета с конической головной частью и решётчатыми рулями.""" p = taper("ru_khaki", -16, -6, 1.5, 7, 1.5, 7, 5) p += [el((4.5, 4.5, -6), (11.5, 11.5, 30), "ru_khaki"), el((4.4, 4.4, 4), (11.6, 11.6, 5), "dark_metal"), el((5, 5, 30), (11, 11, 32), "nozzle")] p += both(el((1, 7.5, 24), (4.5, 8.5, 30), "ru_khaki")) p += [el((7.5, 1, 24), (8.5, 4.5, 30), "ru_khaki"), el((7.5, 11.5, 24), (8.5, 15, 30), "ru_khaki")] return p def kinzhal(): p = taper("missile_grey", -16, -8, 1.5, 6.5, 1.5, 6.5, 4) p += [el((4.75, 4.75, -8), (11.25, 11.25, 30), "dark_metal"), el((4.6, 4.6, -4), (11.4, 11.4, -3), "missile_grey"), el((5.5, 5.5, 30), (10.5, 10.5, 32), "nozzle")] p += both(el((0, 7.6, 22), (4.75, 8.4, 30), "dark_metal")) p += [el((7.6, 0, 22), (8.4, 4.75, 30), "dark_metal"), el((7.6, 11.25, 22), (8.4, 16, 30), "dark_metal")] return p def tracked(tex): """Гусеничное шасси (Бук, Тор): корпус, гусеницы и надгусеничные полки.""" p = [el((-10, 4, -14), (26, 15, 32), tex), el((-10, 4, -16), (26, 10, -14), tex), el((-8, 15, 22), (24, 16, 32), "dark_metal")] p += both(el((-14, 0, -15), (-10, 9, 31), "track")) p += both(el((-14.5, 5, -15), (-10, 10, 31), tex)) return p def buk_turret(): return [el((-4, 0, -4), (20, 9, 20), "ru_green"), el((1, 2, -10), (15, 14, -4), "radar_face"), # РЛС подсвета на башне el((-2, 9, 6), (18, 11, 18), "ru_green")] def buk_rails(): """Четыре ракеты 9М317 на направляющих; ось качания — центр.""" p = [] for x in (-9, -3, 13, 19): p += [el((x - 1.5, 4, -16), (x + 1.5, 7, 26), "missile_ru"), el((x - 1.8, 3.5, 20), (x + 1.8, 7.5, 27), "booster"), el((x - 0.5, 2, -6), (x + 0.5, 4, 16), "dark_metal")] p += [el((-12, 0, -2), (28, 2, 14), "ru_green")] return p def tor_turret(): """Башня Тора: блок вертикального пуска (8 ячеек сверху) и РЛС спереди; вращается целиком.""" p = [el((-4, 0, -6), (20, 16, 22), "ru_khaki"), el((-2, 2, -11), (18, 15, -6), "radar_face"), el((-3, 16, 2), (19, 17, 20), "dark_metal")] for i in range(4): for j in range(2): x, z = -1 + i * 5, 4 + j * 8 p.append(el((x, 16.5, z), (x + 4, 17.5, z + 6), "tube_cap")) return p def nasams_trailer(): p = [el((-6, 5, -10), (22, 9, 26), "nato_green"), el((-4, 9, 10), (20, 13, 24), "dark_metal"), el((6, 3, -16), (10, 6, -10), "metal")] return p + wheels((4, 14)) def p1sun(): """Дрон-перехватчик: короткий корпус, X-образные лучи с моторами; нос — север.""" p = [el((6.5, 6.5, -10), (9.5, 9.5, 10), "dark_metal"), el((7, 7, -14), (9, 9, -10), "missile")] for dx, dy in ((-1, -1), (-1, 1), (1, -1), (1, 1)): x, y = 8 + dx * 6, 8 + dy * 6 p += [el((min(8, x) , min(8, y), 0), (max(8, x), max(8, y), 1.5), "dark_metal", rot=None), el((x - 2.5, y - 2.5, 0), (x + 2.5, y + 2.5, 0.6), "prop")] return p def radar_truck(): return maz_chassis("ru_khaki") def depot(): """Склад боеприпасов: бетонная площадка, штабели ящиков и контейнеров ракет.""" p = [el((-14, 0, -14), (30, 2, 30), "concrete")] for x, z in ((-12, -12), (-12, 4), (4, -12)): p.append(el((x, 2, z), (x + 12, 10, z + 12), "crate")) for k in range(3): p.append(el((6, 2 + k * 5, 6), (28, 7 + k * 5, 10), "tube", {"north": "tube_cap", "south": "tube_cap"})) return p def tzm_truck(): """Транспортно-заряжающая машина: кабина, кузов с ракетами и кран.""" p = [el((-6, 5, -16), (22, 9, 32), "ru_green"), el((-6, 9, -16), (22, 20, -6), "ru_green", {"north": "glass"}), el((-4, 9, -2), (20, 12, 30), "dark_metal")] for x in (-3, 5, 13): p.append(el((x, 12, -1), (x + 6, 18, 29), "tube", {"north": "tube_cap", "south": "tube_cap"})) p += [el((18, 12, -4), (21, 26, -1), "metal"), el((6, 24, -4), (21, 26, -1), "metal")] return p + wheels((-12, -3, 16, 24)) def ew_truck(): """Станция РЭБ: кузов-фургон, мачта с антенными решётками.""" p = [el((-6, 5, -16), (22, 9, 32), "ru_khaki"), el((-6, 9, -16), (22, 19, -6), "ru_khaki", {"north": "glass"}), el((-5, 9, -4), (21, 22, 30), "ru_khaki"), el((7, 22, 10), (9, 32, 12), "antenna")] for y in (24, 28): p += both(el((-4, y, 9), (7, y + 3, 13), "antenna")) return p + wheels((-12, -3, 16, 24)) def searchlight(): """Прожектор на тумбе: тумба и голова отдельно (голова — `searchlight_head`).""" return [el((2, 0, 2), (14, 3, 14), "dark_metal"), el((6, 3, 6), (10, 12, 10), "dark_metal")] def searchlight_head(): """Голова прожектора: нос (стекло) смотрит на север; ось — центр.""" return [el((3, 3, -2), (13, 13, 14), "dark_metal"), el((3.5, 3.5, -2.5), (12.5, 12.5, -2), "lamp")] def beam(): """Луч прожектора: длинная светящаяся трубка вдоль z, от z=32 к -16 (масштабируется по длине).""" return [el((6.5, 6.5, -16), (9.5, 9.5, 32), "beam")] def flare(): return [el((6.5, 6.5, 6.5), (9.5, 9.5, 9.5), "lamp"), el((7.6, 9.5, 7.6), (8.4, 16, 8.4), "antenna")] MODELS = { "shahed": (shahed, "Шахед-136"), "shahed_prop": (shahed_prop, "Винт"), "geran3": (geran3, "Герань-3"), "geran3_flame": (geran3_flame, "Факел"), "pac3": (pac3, "PAC-3 MSE"), "irist_missile": (irist_missile, "IRIS-T SLM"), "amraam_missile": (amraam_missile, "AMRAAM"), "flame": (flame, "Факел"), "patriot_launcher": (patriot_launcher, "ПУ M903"), "patriot_truck": (patriot_truck, "Тягач"), "patriot_trailer": (patriot_trailer, "Полуприцеп"), "patriot_radar": (patriot_radar, "РЛС AN/MPQ-65"), "gepard_hull": (gepard_hull, "Корпус Гепарда"), "gepard_turret": (gepard_turret, "Башня Гепарда"), "gepard_guns": (gepard_guns, "Пушки Гепарда"), "gepard_dish": (gepard_dish, "РЛС Гепарда"), "target": (target_marker, "Цель"), "fireball": (fireball, "Огненный шар"), "flash": (flash, "Вспышка"), "debris": (debris, "Обломки"), "lancet": (lancet, "Ланцет-3"), "gerbera": (gerbera, "Гербера"), "kh101": (kh101, "Х-101"), "pantsir_truck": (pantsir_truck, "Шасси Панциря"), "pantsir_turret": (pantsir_turret, "Модуль Панциря"), "pantsir_pack": (pantsir_pack, "Ракеты и пушки Панциря"), "pantsir_dish": (flat_radar, "РЛС Панциря"), "s400_chassis": (lambda: maz_chassis("ru_green"), "Шасси С-400"), "s400_tubes": (lambda: tubes(2, 2, 7), "ТПК 48Н6"), "s500_chassis": (lambda: maz_chassis("ru_khaki"), "Шасси С-500"), "s500_tubes": (lambda: tubes(2, 1, 9.5), "ТПК 77Н6"), "radar_trailer": (radar_trailer, "Прицеп РЛС"), "radar_panel": (big_radar_panel, "РЛС обзора"), "m57e6": (missile_57e6, "57Э6"), "m48n6": (missile_48n6, "48Н6"), "m77n6": (missile_77n6, "77Н6"), "kalibr": (kalibr, "Калибр"), "kh22": (kh22, "Х-22"), "zircon": (zircon, "Циркон"), "iskander": (iskander, "Искандер-М"), "kinzhal": (kinzhal, "Кинжал"), "tracer": (lambda: [el((7.6, 7.6, -16), (8.4, 8.4, 32), "tracer"), el((7.3, 7.3, -16), (8.7, 8.7, -4), "tracer_hot")], "Трассер"), # Боевой блок «Орешника»: раскалённое ядро и длинный плазменный хвост. "irist_chassis": (lambda: maz_chassis("nato_green"), "Шасси IRIS-T SLM"), "irist_tubes": (lambda: tubes(4, 2, 4, "canister"), "Пакет IRIS-T"), "nasams_trailer": (nasams_trailer, "Прицеп NASAMS"), "nasams_launcher": (lambda: tubes(3, 2, 4, "canister"), "Пусковая NASAMS"), "buk_chassis": (lambda: tracked("ru_green"), "Шасси Бука"), "buk_turret": (buk_turret, "Башня Бука"), "buk_rails": (buk_rails, "Ракеты Бука"), "tor_chassis": (lambda: tracked("ru_khaki"), "Шасси Тора"), "tor_turret": (tor_turret, "Башня Тора"), "p1sun_launcher": (lambda: tubes(4, 2, 3, "dark_metal"), "Пусковая P1-SUN"), "p1sun": (p1sun, "Дрон-перехватчик P1-SUN"), "radar_truck": (radar_truck, "Шасси РЛС"), "depot": (depot, "Склад боеприпасов"), "tzm_truck": (tzm_truck, "Транспортно-заряжающая машина"), "ew_truck": (ew_truck, "Станция РЭБ"), "searchlight": (searchlight, "Прожектор"), "searchlight_head": (searchlight_head, "Голова прожектора"), "beam": (beam, "Луч прожектора"), "flare": (flare, "Осветительная ракета"), "reentry": (lambda: [el((6.5, 6.5, -16), (9.5, 9.5, -8), "white"), el((7, 7, -8), (9, 9, 12), "tracer_hot"), el((7.5, 7.5, 12), (8.5, 8.5, 32), "flame")], "Боевой блок"), } CAM_MODELS = ["shahed", "geran3", "lancet", "gerbera", "kh101", "kalibr", "kh22", "zircon", "iskander", "kinzhal", "reentry"] # Предметы: (id, название, цвет иконки) ITEMS = [ ("designator", "Целеуказатель", (220, 50, 50)), ("launch_shahed", "Пуск: Шахед-136", (90, 92, 96)), ("launch_geran3", "Пуск: Герань-3", (30, 30, 34)), ("deploy_patriot", "Развернуть Patriot", (76, 86, 58)), ("deploy_gepard", "Развернуть Гепард", (104, 84, 54)), ("deploy_pantsir", "Развернуть Панцирь-С1", (82, 96, 60)), ("deploy_s400", "Развернуть С-400", (60, 80, 50)), ("deploy_s500", "Развернуть С-500", (108, 110, 80)), ("launch_lancet", "Пуск: Ланцет-3", (70, 78, 66)), ("launch_kh101", "Пуск: Х-101", (84, 88, 92)), ("launch_kalibr", "Пуск: Калибр", (150, 154, 156)), ("launch_kh22", "Пуск: Х-22", (226, 228, 222)), ("launch_zircon", "Пуск: Циркон", (52, 54, 52)), ("launch_iskander", "Пуск: Искандер-М", (108, 110, 80)), ("launch_kinzhal", "Пуск: Кинжал", (60, 62, 60)), ("launch_oreshnik", "Пуск: Орешник", (230, 120, 40)), ("deploy_irist", "Развернуть IRIS-T SLM", (76, 86, 58)), ("deploy_nasams", "Развернуть NASAMS", (76, 86, 58)), ("deploy_buk", "Развернуть Бук-М2", (82, 96, 60)), ("deploy_tor", "Развернуть Тор-М2", (108, 110, 80)), ("deploy_p1sun", "Развернуть P1-SUN", (52, 54, 52)), ("deploy_radar", "Развернуть РЛС дальнего обнаружения", (108, 110, 80)), ("deploy_depot", "Развернуть склад боеприпасов", (92, 78, 48)), ("deploy_tzm", "Развернуть ТЗМ", (82, 96, 60)), ("deploy_ew", "Развернуть станцию РЭБ", (108, 110, 80)), ("deploy_light", "Развернуть прожектор", (52, 54, 52)), ("mover", "Пульт перемещения", (60, 140, 220)), ("flare_gun", "Осветительная ракета", (255, 240, 180)), ("remover", "Свернуть ПВО", (200, 200, 200)), ] def item_icon(color, seed): rnd = random.Random(seed) im = Image.new("RGBA", (16, 16), (0, 0, 0, 0)) for y in range(2, 14): for x in range(4, 12): im.putpixel((x, y), tuple(clamp(c + rnd.randint(-10, 10)) for c in color) + (255,)) for x in range(5, 11): im.putpixel((x, 3), (230, 230, 230, 255)) im.putpixel((7, 6), (255, 60, 60, 255)) im.putpixel((8, 6), (255, 60, 60, 255)) for y in range(13, 16): im.putpixel((7, y), (40, 40, 40, 255)) im.putpixel((8, y), (40, 40, 40, 255)) return im def write_json(path, obj): os.makedirs(os.path.dirname(path), exist_ok=True) with open(path, "w", encoding="utf-8") as f: json.dump(obj, f, ensure_ascii=False, indent=1) def main(): # Только своё: звуки (tools/gen_sounds.py) и sounds.json не трогаем. for sub in ("blockstates", "items", "lang", "models", "textures"): shutil.rmtree(os.path.join(ROOT, "assets", NS, sub), ignore_errors=True) shutil.rmtree(os.path.join(ROOT, "data"), ignore_errors=True) a = os.path.join(ROOT, "assets", NS) d = os.path.join(ROOT, "data", NS, "horizon") os.makedirs(os.path.join(a, "textures", "block")) os.makedirs(os.path.join(a, "textures", "item")) for name, fn in TEXTURES.items(): fn().save(os.path.join(a, "textures", "block", name + ".png")) lang = {} for name, (fn, title) in MODELS.items(): write_json(os.path.join(a, "models", "block", name + ".json"), model(fn())) write_json(os.path.join(a, "blockstates", name + ".json"), {"variants": {"": {"model": f"{NS}:block/{name}"}}}) write_json(os.path.join(d, "block", name + ".json"), {"template": "minecraft:stone", "item": False}) lang[f"block.{NS}.{name}"] = title for i, (name, title, color) in enumerate(ITEMS): item_icon(color, i).save(os.path.join(a, "textures", "item", name + ".png")) write_json(os.path.join(a, "models", "item", name + ".json"), {"parent": "minecraft:item/generated", "textures": {"layer0": f"{NS}:item/{name}"}}) write_json(os.path.join(a, "items", name + ".json"), {"model": {"type": "minecraft:model", "model": f"{NS}:item/{name}"}}) write_json(os.path.join(d, "item", name + ".json"), {"template": "minecraft:stick", "max_stack_size": 1}) lang[f"item.{NS}.{name}"] = title # Предметы-иконки для камеры монитора: 3D-модель цели в интерфейсе. for name in CAM_MODELS: title = MODELS[name][1] write_json(os.path.join(a, "models", "item", "cam_" + name + ".json"), {"parent": f"{NS}:block/{name}", "display": {"gui": {"rotation": [20, 145, 0], "translation": [0, 0, 0], "scale": [0.36, 0.36, 0.36]}}}) write_json(os.path.join(a, "items", "cam_" + name + ".json"), {"model": {"type": "minecraft:model", "model": f"{NS}:item/cam_{name}"}}) write_json(os.path.join(d, "item", "cam_" + name + ".json"), {"template": "minecraft:stick", "max_stack_size": 1}) lang[f"item.{NS}.cam_{name}"] = title write_json(os.path.join(a, "lang", "en_us.json"), lang) write_json(os.path.join(a, "lang", "ru_ru.json"), lang) print(f"{len(MODELS)} моделей, {len(ITEMS)} предметов, {len(TEXTURES)} текстур -> {ROOT}") if __name__ == "__main__": main()