Files
airdef/tools/vehicle_models.py
chexit e84a5e9fa7 Refine ground vehicle silhouettes and publish in-game model checks (#115)
Preserve launcher grids and authored materials; fix the Buk radar intersection and regenerate geometry bounds. Register the separate RM48U game visual for the upcoming carrier path.

Co-Authored-By: Codex <noreply@openai.com>
2026-11-02 22:22:54 +03:00

147 lines
6.3 KiB
Python

#!/usr/bin/env python3
"""Optional authored vehicle pass. Does not run during normal regeneration.
Stylistic references: KAMAZ-based Pantsir-S1, HEMTT M983/M901 Patriot,
truck-mounted IRIS-T SLM, and NASAMS multi-missile launcher. Existing part
origins and launch grids are intentionally retained; these are not scale replicas.
"""
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 wheels(zs, x=-8.5, radius=4):
out = []
for z in zs:
for a, b, w in [(-1, -.6, .6), (-.6, -.2, .92), (-.2, .2, 1), (.2, .6, .92), (.6, 1, .6)]:
out += g.both(g.el((x, radius + a * radius, z - radius * w),
(x + 3, radius + b * radius, z + radius * w), 'tire'))
out += g.both(g.el((x - .06, radius - 1.8, z - 1.8), (x, radius + 1.8, z + 1.8), 'hub'))
return out
def cab(tex, top=20):
# Split windscreen and side windows on a full-width forward-control cab.
return [g.el((-5, 8, -16), (21, top - 4, -6), tex),
g.el((-4.5, top - 4, -15.5), (20.5, top, -6), tex),
g.el((-4, top - 4, -15.99), (7.5, top - .7, -15.82), 'glass_cab'),
g.el((8.5, top - 4, -15.99), (20, top - .7, -15.82), 'glass_cab'),
*g.both(g.el((-5.08, top - 4, -14.5), (-4.99, top - 1, -8), 'glass_cab')),
g.el((-5.5, 7, -16), (21.5, 8, -15.3), 'dark_metal'),
*g.both(g.el((-4.5, 9, -16), (-1.5, 10.2, -15.8), 'bare_lamp'))]
def pantsir_truck():
# KAMAZ 8x8 family: four axles, short front cab, equipment volume under turret.
p = [g.el((-5, 5, -15), (21, 9, 32), 'pilot_olive'),
*cab('pilot_olive', 20),
g.el((-4, 9, -5), (20, 16.5, 5), 'pilot_olive'),
g.el((-3, 9, 5), (19, 12, 28), 'dark_metal'),
g.el((-5, 8, 28), (21, 11, 32), 'pilot_olive'),
*g.both(g.el((-7, 8, 6), (-5, 11, 14), 'dark_metal'))]
return p + wheels([-11, -1.5, 17, 26])
def pantsir_turret():
# Maintain the radar/mast and gun-pack attachment volumes.
return [g.el((-2, 0, -5), (18, 7, 16), 'pilot_olive'),
g.el((0, 7, -4), (16, 10, 14), 'pilot_olive'),
g.el((1, 1, -11), (15, 11, -6), 'radar_face'),
g.el((2, 1, -6), (14, 11, -5), 'dark_metal'),
g.el((7, 10, 4), (9, 17, 6), 'metal'),
g.el((1, 10, 8), (15, 12, 16), 'pilot_olive'),
g.el((5, 10, -4), (11, 13, 0), 'dark_metal'),
g.el((6, 10.5, -4.4), (10, 12.5, -4), 'bare_lamp')]
def pantsir_pack():
# Keep every barrel and canister position; match the turret's material.
return [{**part, 'tex': 'pilot_olive' if part['tex'] == 'ru_green' else part['tex']}
for part in g.pantsir_pack()]
def patriot_truck():
# HEMTT M983 family; existing generator had only three axles.
p = [g.el((-5, 5, -15), (21, 9, 32), 'pilot_nato'),
*cab('pilot_nato', 22),
g.el((-4, 9, -5), (20, 15, 7), 'pilot_nato'),
g.el((-3, 9, 8), (19, 11, 28), 'dark_metal'),
g.el((2, 11, 18), (14, 12, 26), 'metal'),
*g.both(g.el((-7, 9, 1), (-5, 12, 9), 'dark_metal'))]
return p + wheels([-11, -2, 17, 26])
def patriot_trailer():
p = [g.el((-6, 6, -16), (22, 9, 32), 'pilot_nato'),
g.el((-4, 9, -12), (20, 11, 24), 'dark_metal'),
g.el((-1, 11, 14), (17, 16, 30), 'metal'),
g.el((-7, 3, -16), (23, 7, -12), 'dark_metal'),
*g.both(g.el((-8, 0, -6), (-6, 6, -4), 'metal'))]
return p + wheels([19, 27], -9, 3.7)
def irist_chassis():
# Four-axle truck with ISO-frame equipment deck, preserving launcher origin.
p = [g.el((-5, 5, -15), (21, 9, 32), 'pilot_nato'), *cab('pilot_nato', 20),
g.el((-4, 9, -5), (20, 11, 29), 'pilot_nato'),
g.el((-3, 11, -4), (19, 13, 28), 'dark_metal'),
*g.both(g.el((-7, 6, 4), (-5, 9, 14), 'dark_metal')),
*g.both(g.el((-7, 0, 25), (-5, 6, 27), 'metal'))]
return p + wheels([-11, -2, 17, 26])
def nasams_trailer():
# Towable, low frame, two axles and separate rear mounting base.
p = [g.el((-5, 5, -10), (21, 8, 26), 'pilot_nato'),
g.el((-4, 8, -8), (20, 9, 23), 'dark_metal'),
g.el((-4, 9, 10), (20, 13, 24), 'pilot_nato'),
g.el((6, 3, -16), (10, 6, -10), 'metal'),
*g.both(g.el((-7, 0, -6), (-5, 5, -4), 'metal')),
*g.both(g.el((-7, 0, 23), (-5, 5, 25), 'metal'))]
return p + wheels([8, 18], -8.5, 3.7)
def package(nx):
# Exactly the existing Grid spacing and tube end positions.
p = g.tubes(nx, 2, 4, 'canister')
for z in (-2, 22):
p.append(g.el((8 - (nx * 5 - 1) / 2 - .4, 0, z),
(8 + (nx * 5 - 1) / 2 + .4, 9, z + .5), 'dark_metal'))
return p
def export(name, parts, burnt=False):
model = g.model(parts, burnt=burnt)
for key, value in model['textures'].items():
rel = value.split(':', 1)[1] + '.png'
if not any((root / rel).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, wreck in [('pantsir_truck', pantsir_truck, True),
('pantsir_turret', pantsir_turret, True),
('pantsir_pack', pantsir_pack, True),
('patriot_truck', patriot_truck, False),
('patriot_trailer', patriot_trailer, True),
('irist_chassis', irist_chassis, True),
('irist_tubes', lambda: package(4), True),
('nasams_trailer', nasams_trailer, True),
('nasams_launcher', lambda: package(3), True)]:
parts = make()
export(name, parts)
if wreck:
export(name + '_burnt', g.burnt_parts(parts, zlib.crc32(name.encode())), True)
print(name, len(parts))