#!/usr/bin/env python3 """Генерирует контент-мод airdef-content: текстуры, модели и блоки с вариантами поворота. Horizon не рисует display-сущности, но рисует falling_block с моделью любого блока. Поэтому каждый поворот дрона/ракеты/башни — отдельный блок: модель повёрнута элементами (rotation x/y), а блокстейт facing докручивает по 90°. Итог: 16 курсов × N углов тангажа. Запуск: python3 tools/gen_content.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-content")) NS = "airdef" # Знаки поворота элементов подобраны по скриншотам в игре. PITCH_SIGN = 1 YAW_SIGN = -1 YAW_OFFS = [0, 22.5, 45, 67.5] DRONE_PITCH = [20, 0, -20, -40, -60, -80] MISSILE_PITCH = [90, 67.5, 45, 22.5, 0, -22.5, -45, -67.5, -90] GEPARD_ELEV = [75, 50, 25, 0] PATRIOT_ELEV = 38 FACINGS = [("north", 0), ("east", 90), ("south", 180), ("west", 270)] # ---------------------------------------------------------------- текстуры 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(max(0, min(255, c + d)) for c in base) + (255,)) return im def tex_panels(base, seed): im = noise(base, 5, seed) line = tuple(max(0, c - 18) for c in base) + (255,) for i in range(16): im.putpixel((i, 5), line) im.putpixel((11, i), line) return im def tex_camo(colors, seed): rnd = random.Random(seed) im = noise(colors[0], 4, seed) for col in colors[1:]: for _ in range(5): cx, cy, r = rnd.randint(0, 15), rnd.randint(0, 15), rnd.randint(1, 3) for y in range(cy - r, cy + r + 1): for x in range(cx - r - 1, cx + r + 2): if (x - cx) ** 2 / 1.6 + (y - cy) ** 2 <= r * r: im.putpixel((x % 16, y % 16), tuple(max(0, min(255, c + rnd.randint(-4, 4))) for c in col) + (255,)) return im def tex_track(): im = noise((38, 38, 36), 4, 7) for y in range(0, 16, 3): for x in range(16): im.putpixel((x, y), (70, 70, 66, 255)) return im def tex_canister_front(): im = noise((96, 104, 74), 4, 11) for cx in (4, 11): for cy in (4, 11): for y in range(cy - 3, cy + 3): for x in range(cx - 3, cx + 3): im.putpixel((x, y), (40, 42, 34, 255)) return im def tex_flame(): im = Image.new("RGBA", (16, 16)) for y in range(16): for x in range(16): t = y / 15 im.putpixel((x, y), (255, int(230 - 120 * t), int(120 - 100 * t), 255)) return im def tex_missile(): im = noise((228, 228, 222), 3, 21) for x in range(16): im.putpixel((x, 2), (180, 40, 40, 255)) im.putpixel((x, 3), (180, 40, 40, 255)) return im def tex_glass(): im = noise((44, 62, 82), 6, 31) for i in range(16): im.putpixel((i, 15 - i), (120, 150, 170, 255)) return im TEXTURES = { "shahed": lambda: tex_panels((64, 66, 68), 1), "shahed_nose": lambda: noise((96, 98, 100), 4, 2), "geran": lambda: tex_panels((30, 30, 33), 3), "geran_engine": lambda: noise((52, 52, 56), 5, 4), "prop": lambda: noise((18, 18, 18), 3, 5), "missile": tex_missile, "flame": tex_flame, "nozzle": lambda: noise((40, 40, 40), 4, 6), "nato_green": lambda: tex_camo([(76, 84, 58), (58, 66, 44), (40, 44, 32)], 8), "flecktarn": lambda: tex_camo([(92, 98, 62), (64, 74, 44), (100, 82, 54), (32, 34, 28)], 9), "track": tex_track, "tire": lambda: noise((26, 26, 26), 3, 10), "canister": lambda: tex_panels((96, 104, 74), 12), "canister_front": tex_canister_front, "glass": tex_glass, "metal": lambda: noise((122, 124, 126), 6, 13), "radar": lambda: noise((160, 162, 160), 5, 14), "barrel": lambda: noise((44, 46, 42), 3, 15), } # ---------------------------------------------------------------- модели def el(frm, to, tex, group="body"): return {"from": list(frm), "to": list(to), "tex": tex, "group": group} def mirror_x(e): """Зеркальная копия элемента относительно центра блока (x=8).""" return {**e, "from": [16 - e["to"][0], e["from"][1], e["from"][2]], "to": [16 - e["from"][0], e["to"][1], e["to"][2]]} 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.5, min(16, w)), max(0.5, min(16, h))] def build_model(parts, rot_for_group, origin=(8, 8, 8)): textures = {} elements = [] for p in parts: textures[p["tex"]] = f"{NS}:block/{p['tex']}" faces = {f: {"uv": face_uv(p["from"], p["to"], f), "texture": "#" + p["tex"]} for f in ("north", "south", "east", "west", "up", "down")} e = {"from": p["from"], "to": p["to"], "faces": faces} rx, ry = rot_for_group(p["group"]) if rx or ry: rot = {"origin": list(origin)} if rx: rot["x"] = rx if ry: rot["y"] = ry e["rotation"] = rot elements.append(e) textures["particle"] = f"{NS}:block/{parts[0]['tex']}" return {"textures": textures, "elements": elements} def delta_wing(tex, z_lead, z_tip, z_trail, half_span, steps=6, y0=7.4, y1=8.2): """Дельта-крыло лесенкой: передняя кромка от (8, z_lead) до законцовки half_span на z_tip.""" parts = [] for i in range(steps): za = z_lead + (z_tip - z_lead) * i / steps zb = z_lead + (z_tip - z_lead) * (i + 1) / steps s = half_span * (i + 1) / steps parts.append(el((8 - s, y0, za), (8 + s, y1, zb), tex)) parts.append(el((8 - half_span, y0, z_tip), (8 + half_span, y1, z_trail), tex)) return parts def shahed_parts(): p = [ el((6.5, 6.5, -11), (9.5, 9.5, 21), "shahed"), el((7, 7, -14), (9, 9, -11), "shahed_nose"), el((6.8, 6.8, 21), (9.2, 9.2, 24), "prop"), el((7.6, 2.5, 24), (8.4, 13.5, 24.6), "prop"), el((2.5, 7.6, 24), (13.5, 8.4, 24.6), "prop"), ] p += delta_wing("shahed", -3, 15, 21, 19) fin = el((-11.5, 7, 13), (-10.5, 12, 21), "shahed") p += [fin, mirror_x(fin)] return p def geran3_parts(): p = [ el((6.5, 6.5, -12), (9.5, 9.5, 20), "geran"), el((7, 7, -15), (9, 9, -12), "geran_engine"), el((6, 9.5, 6), (10, 12.5, 22), "geran_engine"), el((6.5, 10, 22), (9.5, 12, 23), "nozzle"), ] p += delta_wing("geran", -2, 13, 19, 15, steps=5) fin = el((-7.5, 7, 12), (-6.5, 11, 19), "geran") p += [fin, mirror_x(fin)] return p def pac3_parts(): p = [ el((6.75, 6.75, -13), (9.25, 9.25, 14), "missile"), el((7.25, 7.25, -16), (8.75, 8.75, -13), "missile"), el((7.1, 7.1, 14), (8.9, 8.9, 16), "nozzle"), el((7.3, 7.3, 16), (8.7, 8.7, 23), "flame"), el((3.5, 7.75, 9), (12.5, 8.25, 14), "missile"), el((7.75, 3.5, 9), (8.25, 12.5, 14), "missile"), ] return p def patriot_box_parts(): """Пакет из 4 ТПК: ось вращения — центр блока, наклон задаётся тангажом.""" return [ el((-3, 2, -14), (19, 22, 28), "canister"), el((-3.2, 1.8, -14.4), (19.2, 22.2, -14), "canister_front"), el((-4, 11.5, -14), (20, 12.5, 28), "metal"), ] def gepard_turret_parts(): turret = [ el((-3, 2, -2), (19, 14, 20), "flecktarn"), el((1, 14, 4), (15, 16, 16), "flecktarn"), el((4, 6, -6), (12, 13, -2), "radar", "turret"), el((7.5, 16, 15), (8.5, 22, 16), "metal"), el((1, 21, 14), (15, 24, 16), "radar"), ] for e in turret: e["group"] = "turret" gun = el((-7, 7, -16), (-4.5, 9.5, 10), "barrel", "gun") mount = el((-8, 5.5, 2), (-3, 11, 12), "flecktarn", "gun") return turret + [gun, mirror_x(gun), mount, mirror_x(mount)] def patriot_truck_parts(): p = [ el((-6, 5, -16), (22, 9, 32), "nato_green"), el((-6, 9, -15.5), (22, 24, -2), "nato_green"), el((-5, 15, -16), (21, 22, -15.5), "glass"), el((-6.2, 15, -14), (-5.8, 22, -6), "glass"), el((21.8, 15, -14), (22.2, 22, -6), "glass"), el((-4, 9, 0), (20, 16, 10), "nato_green"), ] for z in (-12, 16, 25): w = el((-8, 0, z), (-5, 8, z + 7), "tire") p += [w, mirror_x(w)] return p def patriot_trailer_parts(): p = [ el((-6, 5, -16), (22, 10, 32), "nato_green"), el((2, 10, 10), (14, 14, 22), "metal"), el((-6, 2, -16), (22, 6, -12), "metal"), ] for z in (14, 23): w = el((-8, 0, z), (-5, 8, z + 7), "tire") p += [w, mirror_x(w)] return p def gepard_front_parts(): p = [ el((-12, 4, -12), (28, 18, 32), "flecktarn"), el((-12, 4, -16), (28, 13, -12), "flecktarn"), el((-10, 18, 20), (26, 19, 32), "metal"), ] t = el((-16, 0, -14), (-12, 11, 32), "track") return p + [t, mirror_x(t)] def gepard_rear_parts(): p = [ el((-12, 4, -16), (28, 18, 26), "flecktarn"), el((-10, 18, -16), (26, 19, 20), "flecktarn"), el((-6, 18, 4), (22, 19.5, 18), "metal"), ] t = el((-16, 0, -16), (-12, 11, 28), "track") return p + [t, mirror_x(t)] # ---------------------------------------------------------------- запись blocks = {} # имя -> (model, rotate_with_facing) names = {} def rot_all(pitch, yaw): return lambda group: (PITCH_SIGN * pitch, YAW_SIGN * yaw) def add_rotating(prefix, parts, pitches, title, origin=(8, 8, 8), pitch_groups=None): """Блоки prefix_p{i}_y{j}: тангаж pitches[i], доворот курса YAW_OFFS[j].""" for i, pitch in enumerate(pitches): for j, yaw in enumerate(YAW_OFFS): if pitch_groups is None: rot = rot_all(pitch, yaw) else: rot = (lambda pi, ya: lambda g: (PITCH_SIGN * pi if g in pitch_groups else 0, YAW_SIGN * ya))(pitch, yaw) name = f"{prefix}_p{i}_y{j}" blocks[name] = build_model(parts, rot, origin) names[name] = title def add_static(name, parts, title): blocks[name] = build_model(parts, lambda g: (0, 0)) names[name] = title add_rotating("shahed", shahed_parts(), DRONE_PITCH, "Шахед-136") add_rotating("geran3", geran3_parts(), DRONE_PITCH, "Герань-3") add_rotating("pac3", pac3_parts(), MISSILE_PITCH, "Ракета PAC-3") add_rotating("patriot_box", patriot_box_parts(), [PATRIOT_ELEV], "ПУ Patriot") add_rotating("gepard_turret", gepard_turret_parts(), GEPARD_ELEV, "Башня Гепарда", pitch_groups={"gun"}) add_static("patriot_truck", patriot_truck_parts(), "Тягач Patriot") add_static("patriot_trailer", patriot_trailer_parts(), "Полуприцеп Patriot") add_static("gepard_front", gepard_front_parts(), "Корпус Гепарда (нос)") add_static("gepard_rear", gepard_rear_parts(), "Корпус Гепарда (корма)") 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, separators=(",", ":")) def main(): for sub in ("assets", "data"): shutil.rmtree(os.path.join(ROOT, sub), ignore_errors=True) tex_dir = os.path.join(ROOT, "assets", NS, "textures", "block") os.makedirs(tex_dir, exist_ok=True) for name, fn in TEXTURES.items(): fn().save(os.path.join(tex_dir, name + ".png")) lang = {} for name, model in blocks.items(): write_json(os.path.join(ROOT, "assets", NS, "models", "block", name + ".json"), model) variants = {} for facing, y in FACINGS: v = {"model": f"{NS}:block/{name}"} if y: v["y"] = y variants[f"facing={facing}"] = v write_json(os.path.join(ROOT, "assets", NS, "blockstates", name + ".json"), {"variants": variants}) write_json(os.path.join(ROOT, "data", NS, "horizon", "block", name + ".json"), {"template": "minecraft:carved_pumpkin", "item": False, "properties": {"hardness": 2.0, "sounds": "minecraft:iron_block"}}) lang[f"block.{NS}.{name}"] = names[name] write_json(os.path.join(ROOT, "assets", NS, "lang", "en_us.json"), lang) write_json(os.path.join(ROOT, "assets", NS, "lang", "ru_ru.json"), lang) print(f"{len(blocks)} блоков, {len(TEXTURES)} текстур -> {ROOT}") if __name__ == "__main__": main()