Files
airdef/tools/support_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

152 lines
6.7 KiB
Python

#!/usr/bin/env python3
"""Optional authored support and tracked-vehicle pass; no runtime IDs changed.
Buk-M2E/Tor-M2E are stylized tracked versions. Radar/EW/TZM are generic
support vehicles; depot and light are game props, not specific prototypes.
"""
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 tracked(tex, tor=False):
p = [g.el((-9, 4, -13), (25, 13, 30), tex),
g.el((-9, 4, -16), (25, 9, -13), tex),
g.el((-8, 9, -14), (24, 13, -9), tex, rot=('x', -22.5)),
g.el((-8, 13, -7), (24, 15, 20), tex),
g.el((-7, 13, 21), (23, 14, 30), 'dark_metal'),
*g.both(g.el((-13, 0, -15), (-9, 8.5, 31), 'track')),
*g.both(g.el((-13.3, 8.5, -14), (-9, 9.5, 31), tex))]
for z in (-11, -5, 1, 7, 13, 19, 25):
# Three broad strips for a readable round wheel over each continuous track.
for a, b, w in [(-1, -.5, .7), (-.5, .5, 1), (.5, 1, .7)]:
p += g.both(g.el((-13.08, 4.2 + a * 3.1, z - w * 2.5),
(-13.02, 4.2 + b * 3.1, z + w * 2.5), 'hub'))
# Different deck fittings distinguish the two tracked vehicles.
if tor:
p += [g.el((-7, 15, -10), (2, 15.7, -5), 'dark_metal'),
g.el((15, 14, 21), (22, 15, 28), 'metal')]
else:
p += [g.el((-7, 15, -10), (2, 15.7, -5), 'metal'),
g.el((3, 14, 23), (20, 14.5, 28), 'bare_nozzle')]
return p
def buk_turret():
# Phased-array face and two shoulders, while the rail pack keeps its pivot.
return [g.el((-4, 0, -4), (20, 7, 20), 'pilot_olive'),
g.el((-2, 7, 2), (18, 10, 18), 'pilot_olive'),
g.el((1, 2, -10), (11, 14, -4), 'radar_face'),
g.el((0, 1, -5), (16, 3, -3), 'dark_metal'),
*g.both(g.el((-4, 7, 4), (-2, 9, 16), 'metal'))]
def buk_rails():
# Keep four rails and their exact centres and forward endpoints.
return [{**p, 'tex': 'pilot_olive' if p['tex'] == 'ru_green' else p['tex']} for p in g.buk_rails()]
def tor_turret():
# Eight existing cells; the engine's launch grid is intentionally unchanged.
p = [g.el((-4, 0, -6), (20, 12, 22), 'pilot_olive'),
g.el((-3, 12, -3), (19, 16, 21), 'pilot_olive'),
g.el((-2, 2, -11), (18, 15, -6), 'radar_face'),
g.el((-3, 16, 2), (19, 17, 20), 'dark_metal')]
for i in range(4):
for j in range(2):
x, z = -1 + i * 5, 4 + j * 8
p.append(g.el((x, 16.5, z), (x + 4, 17.5, z + 6), 'tube_cap'))
return p
def truck(kind):
# Generic four-axle service truck, based on the project's existing dimensions.
p = [g.el((-5, 5, -15), (21, 9, 32), 'pilot_olive'),
g.el((-5, 9, -16), (21, 16, -6), 'pilot_olive'),
g.el((-4.5, 16, -15.5), (20.5, 20, -6), 'pilot_olive'),
g.el((-4, 16, -15.99), (7.5, 19.3, -15.8), 'glass_cab'),
g.el((8.5, 16, -15.99), (20, 19.3, -15.8), 'glass_cab'),
*g.both(g.el((-5.08, 16, -14), (-4.99, 19, -8), 'glass_cab')),
g.el((-5.5, 7, -16), (21.5, 8, -15.3), 'dark_metal'),
*g.both(g.el((-4, 9, -16), (-1, 10.3, -15.8), 'bare_lamp'))]
for z in (-11, -2, 17, 26):
for a, b, w in [(-1, -.6, .6), (-.6, -.2, .92), (-.2, .2, 1), (.2, .6, .92), (.6, 1, .6)]:
p += g.both(g.el((-8.5, 4 + a * 4, z - w * 4), (-5.5, 4 + b * 4, z + w * 4), 'tire'))
p += g.both(g.el((-8.56, 2.2, z - 1.8), (-8.5, 5.8, z + 1.8), 'hub'))
if kind == 'radar':
p += [g.el((-4, 9, -5), (20, 12, 30), 'pilot_olive'),
g.el((-3, 12, 4), (19, 14, 28), 'dark_metal'),
*g.both(g.el((-7, 0, 23), (-5, 6, 25), 'metal'))]
elif kind == 'tzm':
p += [g.el((-4, 9, -3), (20, 12, 30), 'dark_metal'),
g.el((18, 12, -4), (21, 26, -1), 'metal'),
g.el((6, 24, -4), (21, 26, -1), 'metal'),
g.el((6, 20, -4), (7, 24, -3), 'dark_metal')]
for x in (-3, 5, 13):
p += [g.el((x, 12, -1), (x + 6, 18, 29), 'tube', {'north':'tube_cap', 'south':'tube_cap'})]
else:
p += [g.el((-5, 9, -4), (21, 22, 30), 'pilot_olive'),
g.el((-4, 12, 30), (20, 20, 30.3), 'dark_metal'),
g.el((7, 22, 10), (9, 32, 12), 'bare_antenna')]
for y in (24, 28):
p += [g.el((-4, y, 9), (20, y + 1, 13), 'bare_antenna')]
return p
def depot():
p = [g.el((-14, 0, -14), (30, 2, 30), 'concrete'),
g.el((-13, 2, -13), (1, 12, 1), 'crate'),
g.el((-13, 2, 3), (1, 10, 17), 'crate'),
g.el((3, 2, -13), (17, 10, 1), 'crate'),
g.el((-14, 2, 27), (30, 5, 29), 'dark_metal'),
g.el((-14, 2, -14), (-12, 5, 28), 'dark_metal')]
for k in range(3):
p.append(g.el((6, 2 + k * 5, 6), (28, 7 + k * 5, 10), 'tube', {'north':'tube_cap','south':'tube_cap'}))
return p
def searchlight():
return [g.el((1, 0, 1), (15, 2, 15), 'dark_metal'),
g.el((3, 2, 3), (13, 4, 13), 'metal'),
g.el((6, 4, 6), (10, 12, 10), 'dark_metal'),
*g.both(g.el((2, 8, 7), (3, 14, 9), 'metal'))]
def searchlight_head():
return [g.el((3, 3, -2), (13, 13, 14), 'dark_metal'),
g.el((4, 2, 0), (12, 14, 12), 'dark_metal'),
g.el((3.5, 3.5, -2.5), (12.5, 12.5, -2), 'lamp'),
g.el((5, 5, 14), (11, 11, 15), 'metal')]
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 in [('buk_chassis', lambda: tracked('pilot_olive')),
('buk_turret', buk_turret), ('buk_rails', buk_rails),
('tor_chassis', lambda: tracked('pilot_olive', True)), ('tor_turret', tor_turret),
('radar_truck', lambda: truck('radar')), ('tzm_truck', lambda: truck('tzm')),
('ew_truck', lambda: truck('ew')), ('depot', depot),
('searchlight', searchlight), ('searchlight_head', searchlight_head)]:
parts = make()
export(name, parts)
export(name + '_burnt', g.burnt_parts(parts, zlib.crc32(name.encode())), True)
print(name, len(parts))