Files
airdef/tools/pilot_models.py
T
chexit 5620347ffe Add three art pilots and derive chassis envelopes from final models (#115 #114)
Keep authored JSON separate from generated output. Export rotated model bounds for SDK-free navigation and check grazing voxel corners. Include unretouched in-game before/after captures and explicit art/performance limits; pilots are an intermediate pass, not completion of the full asset set.

Co-Authored-By: Codex <noreply@openai.com>
2026-11-02 21:33:59 +03:00

125 lines
5.9 KiB
Python

#!/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():
parts = [p for p in g.maz_chassis("ru_green") if p["tex"] != "tire"]
for z in [-10.5, -1.5, 16.5, 25.5]:
parts += tire(-9, -5.5, z)
# Cab roof and inset windscreen separate the cabin from the flat cargo deck.
for x0, x1 in [(-5.7, 2.7), (13.3, 21.7)]:
parts += [g.el((x0, 19, -15), (x1, 19.5, -7), "ru_green"),
g.el((x0 + .5, 15, -15.8), (x1 - .5, 18.5, -15.55), "glass_cab", rot=("x", 22.5))]
parts += g.both(g.B(-14.5, 8.3, -12.5, -14, 8.8, -7, "dark_metal"))
parts += g.both(g.B(-14.4, 13, -10.2, -14.1, 13.4, -8.4, "bare_antenna"))
# Underbody rails and an engine-deck grille read at normal gameplay distance.
parts += [g.el((1, 4.2, -8), (3, 5, 29), "dark_metal"),
g.el((13, 4.2, -8), (15, 5, 29), "dark_metal"),
g.el((-3, 13.1, -3), (19, 13.5, 3), "bare_nozzle")]
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", {"north": "tube_cap", "south": "tube_cap"}))
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((-10, 4, -8), (26, 15, 30), "flecktarn"),
g.el((-10, 4, -15), (26, 9, -8), "flecktarn"),
g.el((-9.5, 9, -14), (25.5, 13, -7), "flecktarn", rot=("x", -22.5)),
g.el((-9, 15, -4), (25, 16.5, 18), "flecktarn"),
g.el((-8, 15, 18), (24, 16, 30), "dark_metal"),
g.el((-6, 16.6, -6), (3, 17.2, -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), "flecktarn"))
for z in [20, 24, 28]:
parts += [g.el((-6, 16.1, z), (22, 16.3, 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), "flecktarn"),
g.el((-2.5, 5, -5), (18.5, 12, 15), "flecktarn"),
g.el((-2, 12, -2), (18, 13, 14), "flecktarn"),
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), "flecktarn", 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 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, 12, 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 (ROOT / "assets/airdef/textures" / (value.split(":", 1)[1] + ".png")).exists():
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), ("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")