43346aecfe
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
764 lines
31 KiB
Python
764 lines
31 KiB
Python
#!/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")], "Трассер"),
|
||
# Боевой блок «Орешника»: раскалённое ядро и длинный плазменный хвост.
|
||
"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)),
|
||
("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()
|