5620347ffe
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>
125 lines
5.9 KiB
Python
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")
|