Files
airdef/tools/gen_models.py
T
chexit a6a13003c2
build / mod (push) Failing after 5m36s
build / game-linux (push) Has been skipped
build / game-macos (push) Has been cancelled
build / game-windows (push) Has been cancelled
build / release (push) Has been cancelled
AirDef: drones and air defence for a Horizon fork
The mod (airdef-mod/), its generated models and sounds, the fork's patches
over upstream Horizon (horizon-patches/, HORIZON_COMMIT), a cross-platform
build/install/run script (tools/airdef.py), CI that builds and releases on
every push (.gitea/workflows/build.yml) and a setup guide (docs/INSTALL.md).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 19:16:52 +03:00

760 lines
31 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Генерирует контент мода AirDef (airdef-mod/assets и data): текстуры, модели частей, предметы.
Каждая часть — блок со своей моделью в одной ориентации: нос смотрит на север (-z), центр — (8, 8, 8).
Поворачивает и масштабирует их движок форка (render.set-models), так что ракурсы больше не нужны.
Координаты элементов — в пределах -16..32 (иначе Horizon отбрасывает модель).
Запуск: python3 tools/gen_models.py
"""
import json
import 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),
}
# ---------------------------------------------------------------- модели
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 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
MODELS = {
"shahed": (shahed, "Шахед-136"),
"shahed_prop": (shahed_prop, "Винт"),
"geran3": (geran3, "Герань-3"),
"geran3_flame": (geran3_flame, "Факел"),
"pac3": (pac3, "PAC-3 MSE"),
"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")], "Трассер"),
}
CAM_MODELS = ["shahed", "geran3", "lancet", "gerbera", "kh101", "kalibr", "kh22", "zircon", "iskander", "kinzhal"]
# Предметы: (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)),
("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()