#!/usr/bin/env python3 """Export the three art pilots to authored JSON. Run gen_models.py afterwards. The JSON under source-assets is the editable deliverable. This optional authoring helper only rewrites its explicitly named pilots; the normal generator never calls it. Existing origins, launcher grid and animation parts are retained. """ import json from pathlib import Path import zlib import gen_models as g ROOT = Path(g.ROOT) SOURCE = ROOT / "source-assets/assets/airdef/models/block" def tire(x0, x1, z, radius=4.5): """Five voxel strips: rounded silhouette, without overlapping rotated squares' star tips.""" parts = [] for lo, hi, width in [(-1, -.6, .6), (-.6, -.2, .92), (-.2, .2, 1), (.2, .6, .92), (.6, 1, .6)]: parts += g.both(g.el((x0, radius + lo * radius, z - width * radius), (x1, radius + hi * radius, z + width * radius), "tire")) parts += g.both(g.el((x0 - .05, radius - 2.2, z - 2.2), (x0, radius + 2.2, z + 2.2), "hub")) return parts def s400_chassis(): # Compact 8x8 TEL: full-width cab, narrow rails and a clear gap between axles. # The launcher grid and vehicle origin remain untouched. parts = [g.el((-2, 5, -15), (18, 9, 32), "pilot_olive"), g.el((-3, 9, -16), (19, 15, -7), "pilot_olive"), g.el((-2.5, 15, -15), (18.5, 19, -7), "pilot_olive"), g.el((-2, 15.2, -15.95), (7.5, 18, -15.8), "glass_cab"), g.el((8.5, 15.2, -15.95), (18, 18, -15.8), "glass_cab"), g.el((-3.06, 14, -14), (-2.95, 17.5, -8), "glass_cab"), g.el((18.95, 14, -14), (19.06, 17.5, -8), "glass_cab"), g.el((-2.5, 19, -15), (18.5, 19.4, -7), "pilot_olive"), g.el((-2, 9, -6.5), (18, 11, 7), "pilot_olive"), g.el((0, 9.1, 7), (16, 10, 28), "dark_metal"), g.el((-3, 9, 28), (19, 12, 32), "pilot_olive"), g.el((-3.5, 7, -16), (19.5, 8, -15.4), "dark_metal")] for z in [-10.5, -1.5, 16.5, 25.5]: parts += tire(-6, -3, z, 4.1) parts += g.both(g.el((-5, 7, 4), (-2.5, 10, 12), "dark_metal")) parts += g.both(g.el((-4.5, 10, -14), (-3, 10.8, -12), "bare_lamp")) parts += g.both(g.el((-4, 12, -8.2), (-3, 12.6, -7.8), "dark_metal")) return parts def s400_tubes(): parts = [] # Same four centres and 7-unit diameter as Muzzle::Grid; pivots are unchanged. for x in [.5, 8.5]: for y in [0, 8]: for lo, hi, width in [(-1, -.6, .6), (-.6, -.2, .92), (-.2, .2, 1), (.2, .6, .92), (.6, 1, .6)]: parts.append(g.el((x + 3.5 - 3.5 * width, y + 3.5 + lo * 3.5, -16), (x + 3.5 + 3.5 * width, y + 3.5 + hi * 3.5, 32), "tube", {"south": "tube_cap"}, omit_faces=("north",))) for z in [-1, 26]: parts.append(g.el((x - .15, y + 2.6, z), (x + 7.15, y + 4.4, z + 1), "dark_metal")) parts += [g.el((-.5, 0, 26), (16.5, 1, 29), "dark_metal")] return parts def gepard_hull(): parts = [g.el((-9, 4, -8), (25, 13.5, 30), "pilot_nato"), g.el((-9, 4, -15), (25, 9, -8), "pilot_nato"), g.el((-8.5, 9, -14), (24.5, 13, -7), "pilot_nato", rot=("x", -22.5)), g.el((-8, 13.5, -4), (24, 15.2, 18), "pilot_nato"), g.el((-7, 13.5, 18), (23, 14.5, 30), "dark_metal"), g.el((-6, 15.3, -6), (3, 15.9, -1), "metal")] # Side tracks remain a continuous loop; visible road wheels replace a featureless slab. parts += g.both(g.el((-14, 0, -14), (-10, 8.8, 31), "track")) for z in [-10, -4, 2, 8, 14, 20, 26]: for lo, hi, width in [(-1, -.5, .65), (-.5, .5, 1), (.5, 1, .65)]: parts += g.both(g.el((-14.08, 4.3 + lo * 3.5, z - width * 2.8), (-14.02, 4.3 + hi * 3.5, z + width * 2.8), "hub")) parts += g.both(g.el((-14.5, 8.6, -14), (-10, 10, 31), "pilot_nato")) for z in [20, 24, 28]: parts += [g.el((-6, 14.6, z), (22, 14.8, z + 1), "bare_nozzle")] return parts def gepard_turret(): # Preserve the aiming origin, gun pack, tracking radar and search-dish attachment. parts = [g.el((-4, 0, -5), (20, 5, 16), "pilot_nato"), g.el((-2.5, 5, -5), (18.5, 12, 15), "pilot_nato"), g.el((-2, 12, -2), (18, 13, 14), "pilot_nato"), g.el((3, 4, -10), (13, 12, -6), "radar_face"), g.el((7, 13, 10), (9, 18, 12), "metal")] parts += g.both(g.el((-4.7, 4.8, -4), (-2.5, 10.7, 14), "pilot_nato", rot=("z", 22.5))) parts += [g.el((0, 13.05, 2), (5, 13.7, 8), "dark_metal"), g.el((10, 13.05, 2), (15, 13.7, 8), "dark_metal")] parts += g.decal("mk_de", 6, 9, -4, 2, 10.5) return parts def gepard_guns(): # Preserve both bore/aim origins; match the new turret's painted housings. return [{**part, "tex": "pilot_nato" if part["tex"] == "flecktarn" else part["tex"]} for part in g.gepard_guns()] def shahed(): parts = g.ogive(-16, -9, 2.5, "shahed_nose", 4) + g.oct_z(-9, 22, 2.5, "shahed") parts += g.delta2("shahed", "kh101_belly", -2, 21, 26, 21, 28, 7.5, 8.3) parts += g.both(g.B(-20.8, 7.6, 18, -20.1, 13.2, 26, "shahed")) parts += g.oct_z(22, 28, 1.8, "dark_metal") parts += g.both(g.B(.8, 5.5, 24, 1.5, 6.6, 28, "bare_nozzle")) parts += [g.B(-1.3, 10.5, -3, 1.3, 10.8, 7, "shahed")] return parts def export(name, parts, burnt=False): model = g.model(parts, burnt=burnt) # The authoring tool reuses existing textures; it never creates untracked palette variants. for key, value in model["textures"].items(): if not any((root / (value.split(":", 1)[1] + ".png")).exists() for root in (ROOT / "assets/airdef/textures", ROOT / "source-assets/assets/airdef/textures")): base = g.VARIANTS[key][0] candidate = base + "_b" if burnt else base if not (ROOT / "assets/airdef/textures/block" / (candidate + ".png")).exists(): candidate = base model["textures"][key] = "airdef:block/" + candidate SOURCE.mkdir(parents=True, exist_ok=True) (SOURCE / (name + ".json")).write_text(json.dumps(model, indent=1) + "\n", encoding="utf-8") if __name__ == "__main__": for name, make in [("s400_chassis", s400_chassis), ("s400_tubes", s400_tubes), ("gepard_hull", gepard_hull), ("gepard_turret", gepard_turret), ("gepard_guns", gepard_guns), ("shahed", shahed)]: parts = make() export(name, parts) if name != "shahed": export(name + "_burnt", g.burnt_parts(parts, zlib.crc32(name.encode())), burnt=True) print(name, len(parts), "elements")