Files
airdef/legacy/gen_content.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

362 lines
13 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-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()