Separate search and engagement roles, support launcher-only SAMs, animate individual tube lids and delay ground strike spending until ejection. Preserve battery configuration and launch poses in saves and replay. Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
@@ -161,8 +161,9 @@ API мода `render.set-lights` (до 512, плавно интерполиру
|
||||
- `/airdef raid` — налёт, настроенный в меню; `/airdef target here` — отметить объект под ногами; `/airdef repair` — ремонт;
|
||||
- `/airdef lastchance <on|off>` — «последний шанс»: комплекс стреляет и по классу вне ролей, если цель вот-вот попадёт (по умолчанию вкл; сохраняется);
|
||||
- `/airdef sites` — известные пусковые площадки (ПУ). РЛС с прямой видимостью может заметить сам пуск и получить неточную позицию; несколько наблюдённых пусков уточняют её. Обнаруженный позже дрон даёт только слабое предположение об обратном курсе: скрытые координаты площадки и время невидимого старта не используются. Повторные треки одного полёта не создают ложную точность.
|
||||
- `/airdef strike <номер ПУ>` — ручной удар по разведанной площадке из готового физического С-400 с отдельно загруженной RM48U (игровой профиль, #118). Четыре ячейки делятся с боезапасом ПВО; приказ списывает одну ударную ракету у носителя, а не создаёт её возле игрока. Выбирается готовый комплекс с наибольшим остатком RM48U, затем ближайший к цели. Есть выброс, отложенное включение двигателя, ограниченная игровая траектория, столкновения и запись пуска. Несуществующее «ПВО площадки» не перехватывает ракету по броску вероятности.
|
||||
- `/airdef strike <номер ПУ>` — ручной удар по разведанной площадке из готового физического С-400 с отдельно загруженной RM48U (игровой профиль, #118). Четыре ячейки делятся с боезапасом ПВО; приказ резервирует носитель и ячейку, открывает крышку и списывает одну ударную ракету при выбросе. Выбирается готовый комплекс с наибольшим остатком RM48U, затем ближайший к цели. Есть открытие ТПК, выброс, отложенное включение двигателя и разгон, ограниченная игровая траектория, столкновения и запись пуска. Несуществующее «ПВО площадки» не перехватывает ракету по броску вероятности.
|
||||
- `/airdef ground-stock <номер склада> <количество>` — задать начальный запас RM48U для sandbox-сценария (по умолчанию 0, сам не пополняется). `/airdef ground-load <номер С-400>` — у живого склада в пределах 60 блоков загрузить одну RM48U, сохранив снятую ракету ПВО на складе; `/airdef ground-unload <номер С-400>` — вернуть её и восстановить снятую ракету ПВО, если она осталась. Запас сохраняется с миром. Автоматическая доставка RM48U через ТЗМ пока не реализована. Совместимость профиля основана на [публикации ГУР](https://gur.gov.ua/content/warsanctions-hur-mo-rozkryvaie-budovu-komponentnu-bazu-ta-pidpryiemstva-kooperatsii-z-vyrobnytstva-udarnykh-raket-rm48u.html); это сообщение ведомства, не независимая сертификация. Дальность, форма полёта и оформление условны для Minecraft.
|
||||
- `/airdef deploy s400_launcher` и `s500_launcher` — отдельные пусковые без своей РЛС; `radar_search`, `radar_track`, `radar_height` — обзорная, сопровождающая и мачтовая игровые РЛС. Они также доступны через меню N → «Разместить элементы батареи…». `/airdef battery` показывает состав и связь; `/airdef battery <номер> integrated|launcher` меняет конфигурацию, `/airdef radar <номер> multi|search|engagement|height` — роль отдельной РЛС. Обзорная РЛС передаёт подсказки; станция сопровождения должна сама получить свежее измерение, чтобы разрешить пуск. Старые С-400/С-500 сохраняют упрощённую сборную конфигурацию. [Устройство игровой батареи и пуска](docs/BATTERIES-AND-COLD-LAUNCH.md).
|
||||
- `/airdef roles` — показать, по каким классам целей работает каждый комплекс; `/airdef roles <patriot|gepard|pantsir|s400|s500|all> <классы|default>` — задать (классы через запятую: `drone`, `jet`, `cruise`, `supersonic`, `ballistic` или `all`), например `/airdef roles s400 cruise,ballistic`;
|
||||
- `/airdef doctrine` — доктрины комплексов (#47; в меню N: «Доктрины…»): `/airdef doctrine <номер|all> reserve <K>` — держать K ракет для срочных целей (баллистика или до удара меньше 12 с); `salvo eco|normal|heavy` — сколько перехватчиков на цель (только базовые / как обычно / на один больше); `sector <центр°> <полуширина°>` или `sector off` — сектор ответственности (0° — север); `guard <объект№>[,<№>]` или `guard off` — стрелять только по тому, что идёт на охраняемые объекты (отметка: `/airdef target here`). По умолчанию ограничений нет; последний шанс сильнее доктрины; `/airdef why <номер>` называет, какая настройка остановила или разрешила пуск; сохраняется с миром и сценариями. Карта охраны и пробелов в ней — вместе с картой (#48).
|
||||
- `/airdef deploy <patriot|gepard|pantsir|s400|s500>` — поставить комплекс в 8 блоках перед собой;
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"variants": {
|
||||
"": {
|
||||
"model": "airdef:block/radar_mast"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"variants": {
|
||||
"": {
|
||||
"model": "airdef:block/radar_mast_burnt"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"variants": {
|
||||
"": {
|
||||
"model": "airdef:block/tpk_cap"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"variants": {
|
||||
"": {
|
||||
"model": "airdef:block/tpk_cap_burnt"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,8 @@
|
||||
"block.airdef.s400_tubes": "ТПК 48Н6",
|
||||
"block.airdef.s500_chassis": "Шасси С-500",
|
||||
"block.airdef.s500_tubes": "ТПК 77Н6",
|
||||
"block.airdef.tpk_cap": "Крышка ТПК (анимация)",
|
||||
"block.airdef.radar_mast": "Радиомачта (секция высоты)",
|
||||
"block.airdef.radar_trailer": "Прицеп РЛС",
|
||||
"block.airdef.radar_panel": "РЛС обзора",
|
||||
"block.airdef.m57e6": "57Э6",
|
||||
@@ -77,6 +79,8 @@
|
||||
"block.airdef.s400_tubes_burnt": "ТПК 48Н6 (остов)",
|
||||
"block.airdef.s500_chassis_burnt": "Шасси С-500 (остов)",
|
||||
"block.airdef.s500_tubes_burnt": "ТПК 77Н6 (остов)",
|
||||
"block.airdef.tpk_cap_burnt": "Крышка ТПК (анимация) (остов)",
|
||||
"block.airdef.radar_mast_burnt": "Радиомачта (секция высоты) (остов)",
|
||||
"block.airdef.irist_chassis_burnt": "Шасси IRIS-T SLM (остов)",
|
||||
"block.airdef.irist_tubes_burnt": "Пакет IRIS-T (остов)",
|
||||
"block.airdef.nasams_trailer_burnt": "Прицеп NASAMS (остов)",
|
||||
|
||||
@@ -30,6 +30,8 @@
|
||||
"block.airdef.s400_tubes": "ТПК 48Н6",
|
||||
"block.airdef.s500_chassis": "Шасси С-500",
|
||||
"block.airdef.s500_tubes": "ТПК 77Н6",
|
||||
"block.airdef.tpk_cap": "Крышка ТПК (анимация)",
|
||||
"block.airdef.radar_mast": "Радиомачта (секция высоты)",
|
||||
"block.airdef.radar_trailer": "Прицеп РЛС",
|
||||
"block.airdef.radar_panel": "РЛС обзора",
|
||||
"block.airdef.m57e6": "57Э6",
|
||||
@@ -77,6 +79,8 @@
|
||||
"block.airdef.s400_tubes_burnt": "ТПК 48Н6 (остов)",
|
||||
"block.airdef.s500_chassis_burnt": "Шасси С-500 (остов)",
|
||||
"block.airdef.s500_tubes_burnt": "ТПК 77Н6 (остов)",
|
||||
"block.airdef.tpk_cap_burnt": "Крышка ТПК (анимация) (остов)",
|
||||
"block.airdef.radar_mast_burnt": "Радиомачта (секция высоты) (остов)",
|
||||
"block.airdef.irist_chassis_burnt": "Шасси IRIS-T SLM (остов)",
|
||||
"block.airdef.irist_tubes_burnt": "Пакет IRIS-T (остов)",
|
||||
"block.airdef.nasams_trailer_burnt": "Прицеп NASAMS (остов)",
|
||||
|
||||
@@ -0,0 +1,418 @@
|
||||
{
|
||||
"ambientocclusion": false,
|
||||
"textures": {
|
||||
"dark_metal": "airdef:block/dark_metal",
|
||||
"metal": "airdef:block/metal",
|
||||
"particle": "airdef:block/dark_metal"
|
||||
},
|
||||
"elements": [
|
||||
{
|
||||
"from": [
|
||||
4.0,
|
||||
0.0,
|
||||
4.0
|
||||
],
|
||||
"to": [
|
||||
12.0,
|
||||
3.0,
|
||||
12.0
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
8.0,
|
||||
3.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
8.0,
|
||||
3.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"east": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
8.0,
|
||||
3.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"west": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
8.0,
|
||||
3.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"up": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
8.0,
|
||||
8.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"down": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
8.0,
|
||||
8.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"from": [
|
||||
5.5,
|
||||
3.0,
|
||||
5.5
|
||||
],
|
||||
"to": [
|
||||
10.5,
|
||||
18.0,
|
||||
10.5
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
5.0,
|
||||
15.0
|
||||
],
|
||||
"texture": "#metal"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
5.0,
|
||||
15.0
|
||||
],
|
||||
"texture": "#metal"
|
||||
},
|
||||
"east": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
5.0,
|
||||
15.0
|
||||
],
|
||||
"texture": "#metal"
|
||||
},
|
||||
"west": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
5.0,
|
||||
15.0
|
||||
],
|
||||
"texture": "#metal"
|
||||
},
|
||||
"up": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
5.0,
|
||||
5.0
|
||||
],
|
||||
"texture": "#metal"
|
||||
},
|
||||
"down": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
5.0,
|
||||
5.0
|
||||
],
|
||||
"texture": "#metal"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"from": [
|
||||
4.5,
|
||||
18.0,
|
||||
4.5
|
||||
],
|
||||
"to": [
|
||||
11.5,
|
||||
21.0,
|
||||
11.5
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
7.0,
|
||||
3.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
7.0,
|
||||
3.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"east": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
7.0,
|
||||
3.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"west": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
7.0,
|
||||
3.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"up": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
7.0,
|
||||
7.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"down": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
7.0,
|
||||
7.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"from": [
|
||||
6.5,
|
||||
21.0,
|
||||
6.5
|
||||
],
|
||||
"to": [
|
||||
9.5,
|
||||
30.0,
|
||||
9.5
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
3.0,
|
||||
9.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
3.0,
|
||||
9.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"east": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
3.0,
|
||||
9.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"west": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
3.0,
|
||||
9.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"up": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
3.0,
|
||||
3.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"down": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
3.0,
|
||||
3.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"from": [
|
||||
5.5,
|
||||
30.0,
|
||||
5.5
|
||||
],
|
||||
"to": [
|
||||
10.5,
|
||||
32.0,
|
||||
10.5
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
5.0,
|
||||
2.0
|
||||
],
|
||||
"texture": "#metal"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
5.0,
|
||||
2.0
|
||||
],
|
||||
"texture": "#metal"
|
||||
},
|
||||
"east": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
5.0,
|
||||
2.0
|
||||
],
|
||||
"texture": "#metal"
|
||||
},
|
||||
"west": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
5.0,
|
||||
2.0
|
||||
],
|
||||
"texture": "#metal"
|
||||
},
|
||||
"up": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
5.0,
|
||||
5.0
|
||||
],
|
||||
"texture": "#metal"
|
||||
},
|
||||
"down": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
5.0,
|
||||
5.0
|
||||
],
|
||||
"texture": "#metal"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"from": [
|
||||
7.0,
|
||||
31.5,
|
||||
7.0
|
||||
],
|
||||
"to": [
|
||||
9.0,
|
||||
32.0,
|
||||
9.0
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
2.0,
|
||||
0.5
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
2.0,
|
||||
0.5
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"east": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
2.0,
|
||||
0.5
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"west": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
2.0,
|
||||
0.5
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"up": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
2.0,
|
||||
2.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"down": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
2.0,
|
||||
2.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,418 @@
|
||||
{
|
||||
"ambientocclusion": false,
|
||||
"textures": {
|
||||
"dark_metal_b": "airdef:block/dark_metal_b",
|
||||
"metal_b": "airdef:block/metal_b",
|
||||
"particle": "airdef:block/dark_metal_b"
|
||||
},
|
||||
"elements": [
|
||||
{
|
||||
"from": [
|
||||
4.0,
|
||||
0.0,
|
||||
4.0
|
||||
],
|
||||
"to": [
|
||||
12.0,
|
||||
3.0,
|
||||
12.0
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
8.0,
|
||||
3.0
|
||||
],
|
||||
"texture": "#dark_metal_b"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
8.0,
|
||||
3.0
|
||||
],
|
||||
"texture": "#dark_metal_b"
|
||||
},
|
||||
"east": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
8.0,
|
||||
3.0
|
||||
],
|
||||
"texture": "#dark_metal_b"
|
||||
},
|
||||
"west": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
8.0,
|
||||
3.0
|
||||
],
|
||||
"texture": "#dark_metal_b"
|
||||
},
|
||||
"up": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
8.0,
|
||||
8.0
|
||||
],
|
||||
"texture": "#dark_metal_b"
|
||||
},
|
||||
"down": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
8.0,
|
||||
8.0
|
||||
],
|
||||
"texture": "#dark_metal_b"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"from": [
|
||||
5.5,
|
||||
3.0,
|
||||
5.5
|
||||
],
|
||||
"to": [
|
||||
10.5,
|
||||
18.0,
|
||||
10.5
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
5.0,
|
||||
15.0
|
||||
],
|
||||
"texture": "#metal_b"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
5.0,
|
||||
15.0
|
||||
],
|
||||
"texture": "#metal_b"
|
||||
},
|
||||
"east": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
5.0,
|
||||
15.0
|
||||
],
|
||||
"texture": "#metal_b"
|
||||
},
|
||||
"west": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
5.0,
|
||||
15.0
|
||||
],
|
||||
"texture": "#metal_b"
|
||||
},
|
||||
"up": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
5.0,
|
||||
5.0
|
||||
],
|
||||
"texture": "#metal_b"
|
||||
},
|
||||
"down": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
5.0,
|
||||
5.0
|
||||
],
|
||||
"texture": "#metal_b"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"from": [
|
||||
4.5,
|
||||
18.0,
|
||||
4.5
|
||||
],
|
||||
"to": [
|
||||
11.5,
|
||||
21.0,
|
||||
11.5
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
7.0,
|
||||
3.0
|
||||
],
|
||||
"texture": "#dark_metal_b"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
7.0,
|
||||
3.0
|
||||
],
|
||||
"texture": "#dark_metal_b"
|
||||
},
|
||||
"east": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
7.0,
|
||||
3.0
|
||||
],
|
||||
"texture": "#dark_metal_b"
|
||||
},
|
||||
"west": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
7.0,
|
||||
3.0
|
||||
],
|
||||
"texture": "#dark_metal_b"
|
||||
},
|
||||
"up": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
7.0,
|
||||
7.0
|
||||
],
|
||||
"texture": "#dark_metal_b"
|
||||
},
|
||||
"down": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
7.0,
|
||||
7.0
|
||||
],
|
||||
"texture": "#dark_metal_b"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"from": [
|
||||
6.5,
|
||||
21.0,
|
||||
6.5
|
||||
],
|
||||
"to": [
|
||||
9.5,
|
||||
30.0,
|
||||
9.5
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
3.0,
|
||||
9.0
|
||||
],
|
||||
"texture": "#dark_metal_b"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
3.0,
|
||||
9.0
|
||||
],
|
||||
"texture": "#dark_metal_b"
|
||||
},
|
||||
"east": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
3.0,
|
||||
9.0
|
||||
],
|
||||
"texture": "#dark_metal_b"
|
||||
},
|
||||
"west": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
3.0,
|
||||
9.0
|
||||
],
|
||||
"texture": "#dark_metal_b"
|
||||
},
|
||||
"up": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
3.0,
|
||||
3.0
|
||||
],
|
||||
"texture": "#dark_metal_b"
|
||||
},
|
||||
"down": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
3.0,
|
||||
3.0
|
||||
],
|
||||
"texture": "#dark_metal_b"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"from": [
|
||||
5.5,
|
||||
30.0,
|
||||
5.5
|
||||
],
|
||||
"to": [
|
||||
10.5,
|
||||
32.0,
|
||||
10.5
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
5.0,
|
||||
2.0
|
||||
],
|
||||
"texture": "#metal_b"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
5.0,
|
||||
2.0
|
||||
],
|
||||
"texture": "#metal_b"
|
||||
},
|
||||
"east": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
5.0,
|
||||
2.0
|
||||
],
|
||||
"texture": "#metal_b"
|
||||
},
|
||||
"west": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
5.0,
|
||||
2.0
|
||||
],
|
||||
"texture": "#metal_b"
|
||||
},
|
||||
"up": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
5.0,
|
||||
5.0
|
||||
],
|
||||
"texture": "#metal_b"
|
||||
},
|
||||
"down": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
5.0,
|
||||
5.0
|
||||
],
|
||||
"texture": "#metal_b"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"from": [
|
||||
7.0,
|
||||
31.5,
|
||||
7.0
|
||||
],
|
||||
"to": [
|
||||
9.0,
|
||||
32.0,
|
||||
9.0
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
2.0,
|
||||
0.5
|
||||
],
|
||||
"texture": "#dark_metal_b"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
2.0,
|
||||
0.5
|
||||
],
|
||||
"texture": "#dark_metal_b"
|
||||
},
|
||||
"east": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
2.0,
|
||||
0.5
|
||||
],
|
||||
"texture": "#dark_metal_b"
|
||||
},
|
||||
"west": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
2.0,
|
||||
0.5
|
||||
],
|
||||
"texture": "#dark_metal_b"
|
||||
},
|
||||
"up": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
2.0,
|
||||
2.0
|
||||
],
|
||||
"texture": "#dark_metal_b"
|
||||
},
|
||||
"down": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
2.0,
|
||||
2.0
|
||||
],
|
||||
"texture": "#dark_metal_b"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -19,15 +19,6 @@
|
||||
32.0
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
4.199999999999999,
|
||||
1.4
|
||||
],
|
||||
"texture": "#tube_cap"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
@@ -87,15 +78,6 @@
|
||||
32.0
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
6.440000000000001,
|
||||
1.4
|
||||
],
|
||||
"texture": "#tube_cap"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
@@ -155,15 +137,6 @@
|
||||
32.0
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
7.0,
|
||||
1.4000000000000004
|
||||
],
|
||||
"texture": "#tube_cap"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
@@ -223,15 +196,6 @@
|
||||
32.0
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
6.440000000000001,
|
||||
1.3999999999999995
|
||||
],
|
||||
"texture": "#tube_cap"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
@@ -291,15 +255,6 @@
|
||||
32.0
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
4.199999999999999,
|
||||
1.4000000000000004
|
||||
],
|
||||
"texture": "#tube_cap"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
@@ -495,15 +450,6 @@
|
||||
32.0
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
4.199999999999999,
|
||||
1.4000000000000004
|
||||
],
|
||||
"texture": "#tube_cap"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
@@ -563,15 +509,6 @@
|
||||
32.0
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
6.440000000000001,
|
||||
1.4000000000000004
|
||||
],
|
||||
"texture": "#tube_cap"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
@@ -631,15 +568,6 @@
|
||||
32.0
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
7.0,
|
||||
1.3999999999999986
|
||||
],
|
||||
"texture": "#tube_cap"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
@@ -699,15 +627,6 @@
|
||||
32.0
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
6.440000000000001,
|
||||
1.4000000000000004
|
||||
],
|
||||
"texture": "#tube_cap"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
@@ -767,15 +686,6 @@
|
||||
32.0
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
4.199999999999999,
|
||||
1.4000000000000004
|
||||
],
|
||||
"texture": "#tube_cap"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
@@ -971,15 +881,6 @@
|
||||
32.0
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
4.199999999999999,
|
||||
1.4
|
||||
],
|
||||
"texture": "#tube_cap"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
@@ -1039,15 +940,6 @@
|
||||
32.0
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
6.440000000000001,
|
||||
1.4
|
||||
],
|
||||
"texture": "#tube_cap"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
@@ -1107,15 +999,6 @@
|
||||
32.0
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
7.0,
|
||||
1.4000000000000004
|
||||
],
|
||||
"texture": "#tube_cap"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
@@ -1175,15 +1058,6 @@
|
||||
32.0
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
6.440000000000001,
|
||||
1.3999999999999995
|
||||
],
|
||||
"texture": "#tube_cap"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
@@ -1243,15 +1117,6 @@
|
||||
32.0
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
4.199999999999999,
|
||||
1.4000000000000004
|
||||
],
|
||||
"texture": "#tube_cap"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
@@ -1447,15 +1312,6 @@
|
||||
32.0
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
4.199999999999999,
|
||||
1.4000000000000004
|
||||
],
|
||||
"texture": "#tube_cap"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
@@ -1515,15 +1371,6 @@
|
||||
32.0
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
6.440000000000001,
|
||||
1.4000000000000004
|
||||
],
|
||||
"texture": "#tube_cap"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
@@ -1583,15 +1430,6 @@
|
||||
32.0
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
7.0,
|
||||
1.3999999999999986
|
||||
],
|
||||
"texture": "#tube_cap"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
@@ -1651,15 +1489,6 @@
|
||||
32.0
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
6.440000000000001,
|
||||
1.4000000000000004
|
||||
],
|
||||
"texture": "#tube_cap"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
@@ -1719,15 +1548,6 @@
|
||||
32.0
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
4.199999999999999,
|
||||
1.4000000000000004
|
||||
],
|
||||
"texture": "#tube_cap"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
|
||||
@@ -19,15 +19,6 @@
|
||||
32.0
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
9.5,
|
||||
9.5
|
||||
],
|
||||
"texture": "#tube_cap"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
@@ -87,15 +78,6 @@
|
||||
32.0
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
9.5,
|
||||
9.5
|
||||
],
|
||||
"texture": "#tube_cap"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
|
||||
@@ -19,15 +19,6 @@
|
||||
32.0
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
9.5,
|
||||
9.5
|
||||
],
|
||||
"texture": "#tube_cap_b"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
@@ -87,15 +78,6 @@
|
||||
32.0
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
9.5,
|
||||
9.5
|
||||
],
|
||||
"texture": "#tube_cap_b"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
|
||||
@@ -0,0 +1,282 @@
|
||||
{
|
||||
"ambientocclusion": false,
|
||||
"textures": {
|
||||
"tube_cap": "airdef:block/tube_cap",
|
||||
"dark_metal": "airdef:block/dark_metal",
|
||||
"particle": "airdef:block/tube_cap"
|
||||
},
|
||||
"elements": [
|
||||
{
|
||||
"from": [
|
||||
2.5,
|
||||
0.0,
|
||||
7.7
|
||||
],
|
||||
"to": [
|
||||
13.5,
|
||||
16.0,
|
||||
8.25
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
11.0,
|
||||
16
|
||||
],
|
||||
"texture": "#tube_cap"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
11.0,
|
||||
16
|
||||
],
|
||||
"texture": "#tube_cap"
|
||||
},
|
||||
"east": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
0.5499999999999998,
|
||||
16
|
||||
],
|
||||
"texture": "#tube_cap"
|
||||
},
|
||||
"west": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
0.5499999999999998,
|
||||
16
|
||||
],
|
||||
"texture": "#tube_cap"
|
||||
},
|
||||
"up": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
11.0,
|
||||
0.5499999999999998
|
||||
],
|
||||
"texture": "#tube_cap"
|
||||
},
|
||||
"down": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
11.0,
|
||||
0.5499999999999998
|
||||
],
|
||||
"texture": "#tube_cap"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"from": [
|
||||
0.0,
|
||||
2.5,
|
||||
7.7
|
||||
],
|
||||
"to": [
|
||||
2.5,
|
||||
13.5,
|
||||
8.25
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
2.5,
|
||||
11.0
|
||||
],
|
||||
"texture": "#tube_cap"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
2.5,
|
||||
11.0
|
||||
],
|
||||
"texture": "#tube_cap"
|
||||
},
|
||||
"east": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
0.5499999999999998,
|
||||
11.0
|
||||
],
|
||||
"texture": "#tube_cap"
|
||||
},
|
||||
"west": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
0.5499999999999998,
|
||||
11.0
|
||||
],
|
||||
"texture": "#tube_cap"
|
||||
},
|
||||
"up": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
2.5,
|
||||
0.5499999999999998
|
||||
],
|
||||
"texture": "#tube_cap"
|
||||
},
|
||||
"down": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
2.5,
|
||||
0.5499999999999998
|
||||
],
|
||||
"texture": "#tube_cap"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"from": [
|
||||
13.5,
|
||||
2.5,
|
||||
7.7
|
||||
],
|
||||
"to": [
|
||||
16.0,
|
||||
13.5,
|
||||
8.25
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
2.5,
|
||||
11.0
|
||||
],
|
||||
"texture": "#tube_cap"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
2.5,
|
||||
11.0
|
||||
],
|
||||
"texture": "#tube_cap"
|
||||
},
|
||||
"east": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
0.5499999999999998,
|
||||
11.0
|
||||
],
|
||||
"texture": "#tube_cap"
|
||||
},
|
||||
"west": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
0.5499999999999998,
|
||||
11.0
|
||||
],
|
||||
"texture": "#tube_cap"
|
||||
},
|
||||
"up": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
2.5,
|
||||
0.5499999999999998
|
||||
],
|
||||
"texture": "#tube_cap"
|
||||
},
|
||||
"down": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
2.5,
|
||||
0.5499999999999998
|
||||
],
|
||||
"texture": "#tube_cap"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"from": [
|
||||
5.0,
|
||||
5.0,
|
||||
8.25
|
||||
],
|
||||
"to": [
|
||||
11.0,
|
||||
11.0,
|
||||
8.3
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
6.0,
|
||||
6.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
6.0,
|
||||
6.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"east": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
0.25,
|
||||
6.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"west": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
0.25,
|
||||
6.0
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"up": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
6.0,
|
||||
0.25
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
},
|
||||
"down": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
6.0,
|
||||
0.25
|
||||
],
|
||||
"texture": "#dark_metal"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
{
|
||||
"ambientocclusion": false,
|
||||
"textures": {
|
||||
"tube_cap_b": "airdef:block/tube_cap_b",
|
||||
"dark_metal_b": "airdef:block/dark_metal_b",
|
||||
"particle": "airdef:block/tube_cap_b"
|
||||
},
|
||||
"elements": [
|
||||
{
|
||||
"from": [
|
||||
2.5,
|
||||
0.0,
|
||||
7.7
|
||||
],
|
||||
"to": [
|
||||
13.5,
|
||||
16.0,
|
||||
8.25
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
11.0,
|
||||
16
|
||||
],
|
||||
"texture": "#tube_cap_b"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
11.0,
|
||||
16
|
||||
],
|
||||
"texture": "#tube_cap_b"
|
||||
},
|
||||
"east": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
0.5499999999999998,
|
||||
16
|
||||
],
|
||||
"texture": "#tube_cap_b"
|
||||
},
|
||||
"west": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
0.5499999999999998,
|
||||
16
|
||||
],
|
||||
"texture": "#tube_cap_b"
|
||||
},
|
||||
"up": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
11.0,
|
||||
0.5499999999999998
|
||||
],
|
||||
"texture": "#tube_cap_b"
|
||||
},
|
||||
"down": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
11.0,
|
||||
0.5499999999999998
|
||||
],
|
||||
"texture": "#tube_cap_b"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"from": [
|
||||
0.0,
|
||||
2.5,
|
||||
7.7
|
||||
],
|
||||
"to": [
|
||||
2.5,
|
||||
13.5,
|
||||
8.25
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
2.5,
|
||||
11.0
|
||||
],
|
||||
"texture": "#tube_cap_b"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
2.5,
|
||||
11.0
|
||||
],
|
||||
"texture": "#tube_cap_b"
|
||||
},
|
||||
"east": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
0.5499999999999998,
|
||||
11.0
|
||||
],
|
||||
"texture": "#tube_cap_b"
|
||||
},
|
||||
"west": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
0.5499999999999998,
|
||||
11.0
|
||||
],
|
||||
"texture": "#tube_cap_b"
|
||||
},
|
||||
"up": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
2.5,
|
||||
0.5499999999999998
|
||||
],
|
||||
"texture": "#tube_cap_b"
|
||||
},
|
||||
"down": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
2.5,
|
||||
0.5499999999999998
|
||||
],
|
||||
"texture": "#tube_cap_b"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"from": [
|
||||
13.5,
|
||||
2.5,
|
||||
7.7
|
||||
],
|
||||
"to": [
|
||||
16.0,
|
||||
13.5,
|
||||
8.25
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
2.5,
|
||||
11.0
|
||||
],
|
||||
"texture": "#tube_cap_b"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
2.5,
|
||||
11.0
|
||||
],
|
||||
"texture": "#tube_cap_b"
|
||||
},
|
||||
"east": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
0.5499999999999998,
|
||||
11.0
|
||||
],
|
||||
"texture": "#tube_cap_b"
|
||||
},
|
||||
"west": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
0.5499999999999998,
|
||||
11.0
|
||||
],
|
||||
"texture": "#tube_cap_b"
|
||||
},
|
||||
"up": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
2.5,
|
||||
0.5499999999999998
|
||||
],
|
||||
"texture": "#tube_cap_b"
|
||||
},
|
||||
"down": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
2.5,
|
||||
0.5499999999999998
|
||||
],
|
||||
"texture": "#tube_cap_b"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"from": [
|
||||
5.0,
|
||||
5.0,
|
||||
8.25
|
||||
],
|
||||
"to": [
|
||||
11.0,
|
||||
11.0,
|
||||
8.3
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
6.0,
|
||||
6.0
|
||||
],
|
||||
"texture": "#dark_metal_b"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
6.0,
|
||||
6.0
|
||||
],
|
||||
"texture": "#dark_metal_b"
|
||||
},
|
||||
"east": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
0.25,
|
||||
6.0
|
||||
],
|
||||
"texture": "#dark_metal_b"
|
||||
},
|
||||
"west": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
0.25,
|
||||
6.0
|
||||
],
|
||||
"texture": "#dark_metal_b"
|
||||
},
|
||||
"up": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
6.0,
|
||||
0.25
|
||||
],
|
||||
"texture": "#dark_metal_b"
|
||||
},
|
||||
"down": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
6.0,
|
||||
0.25
|
||||
],
|
||||
"texture": "#dark_metal_b"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"template": "minecraft:stone",
|
||||
"item": false
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"template": "minecraft:stone",
|
||||
"item": false
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"template": "minecraft:stone",
|
||||
"item": false
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"template": "minecraft:stone",
|
||||
"item": false
|
||||
}
|
||||
@@ -63,6 +63,8 @@ c3b0b6f1d9b7da434c95f2794b97b9e2c05c62ccbce9f0884df071e4ec28a468 assets/airdef/
|
||||
3ecae997dc4fe1939848fc60347979c02075396a164b31dec3995a2e907a8447 assets/airdef/blockstates/patriot_trailer.json
|
||||
31fecc6476129990a7d368f6d7cdb954b13e777b28d1396a68168770024112ce assets/airdef/blockstates/patriot_trailer_burnt.json
|
||||
183d0b2ae9c24c94c3d83f2819e1167b5e2d134cc7889e0e3bda12aa784c7baa assets/airdef/blockstates/patriot_truck.json
|
||||
421b5be0a4c80e892850f4a77d5ebb386e04153201be63241c7edc11659d03de assets/airdef/blockstates/radar_mast.json
|
||||
fcc6bbe526baf5551923e2a69cdf09f506cdfd96669e1fffbea643401dd2a1de assets/airdef/blockstates/radar_mast_burnt.json
|
||||
37714e19e555cc50a9f195e1a8845a28c9985c27ed38b577c0813f4629b66fcb assets/airdef/blockstates/radar_panel.json
|
||||
95fe14b9d3e30597f5505aa93f31c3357cebea6117596bd9f5a5bc840ab22415 assets/airdef/blockstates/radar_trailer.json
|
||||
a95a1b8731d6bedda7d8b352ff3c448ea8be691b0ead01f14acab39234590c45 assets/airdef/blockstates/radar_truck.json
|
||||
@@ -88,6 +90,8 @@ e766e18158d00f0480f6dfe32dfe142890b8d452bc493b7474fda0984cc2dd4c assets/airdef/
|
||||
ff125150e206ada5a7fb47394e809039d338c22a843207a32352e9ee59e278a3 assets/airdef/blockstates/tor_chassis_burnt.json
|
||||
04a5fb931f419086995791c1defd9486b36b3c725581bf5a10191a2c71c69984 assets/airdef/blockstates/tor_turret.json
|
||||
fdcbe36a0f4968005e8c6588f2fa5d8196c55da3183cf89760ea221a5c4cb431 assets/airdef/blockstates/tor_turret_burnt.json
|
||||
923340f2233c363a880fffeb40dba04038f26ff42b072f28418e0a6ab757aac5 assets/airdef/blockstates/tpk_cap.json
|
||||
f3e430750f5cb60f44c2a97687d7cc1ac58232b27274a82758c23861d704662a assets/airdef/blockstates/tpk_cap_burnt.json
|
||||
cf7c26abeb1d8f7d84106bd83b84bf541a3d11369b7745a77802b7e51dcdf447 assets/airdef/blockstates/tracer.json
|
||||
8b547ccbc890fa53b613cdb2cf55ceaa5833348479b0863651c6df01a9f778b3 assets/airdef/blockstates/tzm_truck.json
|
||||
11fca31a88e658c819954fbb86b53d7709b66b971af8747a5edda24be34e5dbb assets/airdef/blockstates/tzm_truck_burnt.json
|
||||
@@ -132,8 +136,8 @@ b4d98a938486d49db28c6c7a608ca3b6ab0c9c3eac266ecf12c7dd931cc28f93 assets/airdef/
|
||||
489799dcb5b441e9d4be8bfb3d73a6ebd5fdf2b5ee614b62f38b85bb2fbfad75 assets/airdef/items/launch_zircon.json
|
||||
6b6da5887aa8d9bc987b2c181c243bc2afdd5846a869818f699b59204e559929 assets/airdef/items/mover.json
|
||||
177f7535e4c52ed975d6f737c2b2acb7741b7a928955c8899aced85211443bbf assets/airdef/items/remover.json
|
||||
e96a4968089443d4128ddb883905113018533f024d143cb79ae9f6d664aee2d4 assets/airdef/lang/en_us.json
|
||||
e96a4968089443d4128ddb883905113018533f024d143cb79ae9f6d664aee2d4 assets/airdef/lang/ru_ru.json
|
||||
cede33ba58f61891a8afe42e1f4311cb293ca93f4d7eb3a57636caff17317a4d assets/airdef/lang/en_us.json
|
||||
cede33ba58f61891a8afe42e1f4311cb293ca93f4d7eb3a57636caff17317a4d assets/airdef/lang/ru_ru.json
|
||||
b9b5b5bec33a77e944552c225ee2f526ab9820fa8fca4b3ccf311f2a325925ee assets/airdef/models/block/amraam_missile.json
|
||||
c54e6b24ab69e9d9a2dbbbc63fe2ac60fa37735bf644500ddd5c3de0b91908cf assets/airdef/models/block/barge.json
|
||||
a4198ff647de5866919f9b82a3d53e6486fc56cc9f175f7907951d063a6f4526 assets/airdef/models/block/beam.json
|
||||
@@ -199,6 +203,8 @@ d01efdcfafa92243e745aea7870269c07a0224c9010010e2dbed720ced1662c5 assets/airdef/
|
||||
548bc63e46d96e5110598e39269851c48f750164981a433f75971b0229c268df assets/airdef/models/block/patriot_trailer.json
|
||||
190726b64bef4baadb0bc2a00147069685ef148100c8935402e09c3bef66e65b assets/airdef/models/block/patriot_trailer_burnt.json
|
||||
6f027b020e4066f673c376e289cc627846a084e5733d62744fe78ab23013fb08 assets/airdef/models/block/patriot_truck.json
|
||||
8070d24b511ac733d73f5961762e6f5995379814f33f54c56c1a45d1dd7ff28a assets/airdef/models/block/radar_mast.json
|
||||
7343f6085138f974d581706cc135e582040a46069c215e95ba14ad31dc1a8d63 assets/airdef/models/block/radar_mast_burnt.json
|
||||
e5733ece9b976998c9c6a6c968b17cada868b62d9e2a7bc67e1272e333e35399 assets/airdef/models/block/radar_panel.json
|
||||
b0ae344fbad3b982e7821cd1b26f9922e2a5713b38107521ddc5a5b2abd9e39c assets/airdef/models/block/radar_trailer.json
|
||||
5545094620c27fae6b3d97a71f3d68ee8e0b49804a1dcd61b79ca847277e3b61 assets/airdef/models/block/radar_truck.json
|
||||
@@ -207,12 +213,12 @@ f102765390762d45954b26405b6acdde845df7f0a319bac53d25508e768957b5 assets/airdef/
|
||||
971c11e58eafa17ad798f6d27d91a9ee0531f33692eb657d31f2d83e8cee7e39 assets/airdef/models/block/rm48u.json
|
||||
a6bb637011c176edae474ae7b5d6e3b5af0d2a448845e3f95b026f789e21560f assets/airdef/models/block/s400_chassis.json
|
||||
192076b361a57ee26a1dd449717f30f50b612ee6c88d4dc1fb9dc6aef3f3b829 assets/airdef/models/block/s400_chassis_burnt.json
|
||||
7b2cac0485b4242534e99bbfc0406ee393d508ca971d4d88caf3f13892b473c1 assets/airdef/models/block/s400_tubes.json
|
||||
44bf27184842d0e3c86bed035a7090add0a79f6b5bdea7a37e6ddaaae728d39a assets/airdef/models/block/s400_tubes.json
|
||||
973e02b2058c0b3d59c156f2ac94fbba4ac90a97736e8a4cc9be177b5e0f19a9 assets/airdef/models/block/s400_tubes_burnt.json
|
||||
088f42bb17f4d6099c16adf6cc0426b6d04ff9a43abdf125476a3571e924fcbd assets/airdef/models/block/s500_chassis.json
|
||||
3e35ed19ea22c8309ac7c0ccc9c12e31b2387de97c6b72ba519bae34f681c58c assets/airdef/models/block/s500_chassis_burnt.json
|
||||
6ce00881e2a5107874e82e94a243fcd95c3eb6e9a24e8839c11227c8e330485f assets/airdef/models/block/s500_tubes.json
|
||||
88109407ec749d6f45a4a38a9a43fd1594ac6190d7f08433b07da572c040458d assets/airdef/models/block/s500_tubes_burnt.json
|
||||
8c6bc46647f3df32febd96fccdba894e4c986b717b006e3c3e5607a73f0f252f assets/airdef/models/block/s500_tubes.json
|
||||
135d34c4059ea7aca794cce201ef27c3c17cdb21ea27205634cea95bc8d16515 assets/airdef/models/block/s500_tubes_burnt.json
|
||||
3d61559d5bde298d6dc8987122c68edc3b02bd0d1afed1d9458bd7c3315fb0fe assets/airdef/models/block/searchlight.json
|
||||
6391c155f45656f63ac579bd1ff0d3512c54830a03561b8dd04e5e63ed751cf4 assets/airdef/models/block/searchlight_burnt.json
|
||||
357165a1a1ad382257e68768bf1b4158294e0e080eab9a9570ed6bd824e7f3aa assets/airdef/models/block/searchlight_head.json
|
||||
@@ -224,6 +230,8 @@ d5b9ed76b1744d0c93a809b6a4c16bc3dfa7ff4784e3cb9968aa36b12fc30484 assets/airdef/
|
||||
cacaa7265489f1083f7de351515a7d08eac5d2b072cf58b32ee51840473ab44e assets/airdef/models/block/tor_chassis_burnt.json
|
||||
d130179a7717f1bff9f045a3e0b93788eff17737c7c1c9c3b5b287d811eb6fad assets/airdef/models/block/tor_turret.json
|
||||
a98d2a545b4bdea5cff1c5826d55dc8c3fd341ecebeb8b00ee2e1e1b0739b2cc assets/airdef/models/block/tor_turret_burnt.json
|
||||
a6785198c98f5b2f20acc26ddeca3b5432e6b521b68541ea65ca2592c3cae4a1 assets/airdef/models/block/tpk_cap.json
|
||||
0391ce9eeac9aa0cfcc05b31b0bd5594e10a5df2dc46c355b0b95a8ea8234ff5 assets/airdef/models/block/tpk_cap_burnt.json
|
||||
b2380510f56dc60e914919cb6e20b82e3613a9d62b08a1c81740f960569c3425 assets/airdef/models/block/tracer.json
|
||||
753c46e821b4fce9fd9240de2ec627a27688740a4c31c9eff24593034ffa33e7 assets/airdef/models/block/tzm_truck.json
|
||||
fa29c0224a5a7ad2e3252cde099b17c6c8241ede7cf0e52766734672843a6432 assets/airdef/models/block/tzm_truck_burnt.json
|
||||
@@ -508,6 +516,8 @@ cc0877f40434365b45b03429222926aa0cf135e504017311be53263fd27c5f8f data/airdef/ho
|
||||
cc0877f40434365b45b03429222926aa0cf135e504017311be53263fd27c5f8f data/airdef/horizon/block/patriot_trailer.json
|
||||
cc0877f40434365b45b03429222926aa0cf135e504017311be53263fd27c5f8f data/airdef/horizon/block/patriot_trailer_burnt.json
|
||||
cc0877f40434365b45b03429222926aa0cf135e504017311be53263fd27c5f8f data/airdef/horizon/block/patriot_truck.json
|
||||
cc0877f40434365b45b03429222926aa0cf135e504017311be53263fd27c5f8f data/airdef/horizon/block/radar_mast.json
|
||||
cc0877f40434365b45b03429222926aa0cf135e504017311be53263fd27c5f8f data/airdef/horizon/block/radar_mast_burnt.json
|
||||
cc0877f40434365b45b03429222926aa0cf135e504017311be53263fd27c5f8f data/airdef/horizon/block/radar_panel.json
|
||||
cc0877f40434365b45b03429222926aa0cf135e504017311be53263fd27c5f8f data/airdef/horizon/block/radar_trailer.json
|
||||
cc0877f40434365b45b03429222926aa0cf135e504017311be53263fd27c5f8f data/airdef/horizon/block/radar_truck.json
|
||||
@@ -533,6 +543,8 @@ cc0877f40434365b45b03429222926aa0cf135e504017311be53263fd27c5f8f data/airdef/ho
|
||||
cc0877f40434365b45b03429222926aa0cf135e504017311be53263fd27c5f8f data/airdef/horizon/block/tor_chassis_burnt.json
|
||||
cc0877f40434365b45b03429222926aa0cf135e504017311be53263fd27c5f8f data/airdef/horizon/block/tor_turret.json
|
||||
cc0877f40434365b45b03429222926aa0cf135e504017311be53263fd27c5f8f data/airdef/horizon/block/tor_turret_burnt.json
|
||||
cc0877f40434365b45b03429222926aa0cf135e504017311be53263fd27c5f8f data/airdef/horizon/block/tpk_cap.json
|
||||
cc0877f40434365b45b03429222926aa0cf135e504017311be53263fd27c5f8f data/airdef/horizon/block/tpk_cap_burnt.json
|
||||
cc0877f40434365b45b03429222926aa0cf135e504017311be53263fd27c5f8f data/airdef/horizon/block/tracer.json
|
||||
cc0877f40434365b45b03429222926aa0cf135e504017311be53263fd27c5f8f data/airdef/horizon/block/tzm_truck.json
|
||||
cc0877f40434365b45b03429222926aa0cf135e504017311be53263fd27c5f8f data/airdef/horizon/block/tzm_truck_burnt.json
|
||||
@@ -577,4 +589,4 @@ cc0877f40434365b45b03429222926aa0cf135e504017311be53263fd27c5f8f data/airdef/ho
|
||||
2a3912a7c9d5bb8025b1ac16a1a1d6b5eff0360bb3184e810d319e498255bf97 data/airdef/horizon/item/launch_zircon.json
|
||||
2a3912a7c9d5bb8025b1ac16a1a1d6b5eff0360bb3184e810d319e498255bf97 data/airdef/horizon/item/mover.json
|
||||
2a3912a7c9d5bb8025b1ac16a1a1d6b5eff0360bb3184e810d319e498255bf97 data/airdef/horizon/item/remover.json
|
||||
3f7b0b03582f1c63f814259b898213006f3fbdc52489bad3f7b938e2ec1e5706 src/sim/generated_bounds.rs
|
||||
5e0c0090b5aaded5b86b245cb30bcd0cbaa82f632a362d490d79bcf2794e25fd src/sim/generated_bounds.rs
|
||||
|
||||
@@ -19,15 +19,6 @@
|
||||
32.0
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
4.199999999999999,
|
||||
1.4
|
||||
],
|
||||
"texture": "#tube_cap"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
@@ -87,15 +78,6 @@
|
||||
32.0
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
6.440000000000001,
|
||||
1.4
|
||||
],
|
||||
"texture": "#tube_cap"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
@@ -155,15 +137,6 @@
|
||||
32.0
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
7.0,
|
||||
1.4000000000000004
|
||||
],
|
||||
"texture": "#tube_cap"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
@@ -223,15 +196,6 @@
|
||||
32.0
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
6.440000000000001,
|
||||
1.3999999999999995
|
||||
],
|
||||
"texture": "#tube_cap"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
@@ -291,15 +255,6 @@
|
||||
32.0
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
4.199999999999999,
|
||||
1.4000000000000004
|
||||
],
|
||||
"texture": "#tube_cap"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
@@ -495,15 +450,6 @@
|
||||
32.0
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
4.199999999999999,
|
||||
1.4000000000000004
|
||||
],
|
||||
"texture": "#tube_cap"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
@@ -563,15 +509,6 @@
|
||||
32.0
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
6.440000000000001,
|
||||
1.4000000000000004
|
||||
],
|
||||
"texture": "#tube_cap"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
@@ -631,15 +568,6 @@
|
||||
32.0
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
7.0,
|
||||
1.3999999999999986
|
||||
],
|
||||
"texture": "#tube_cap"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
@@ -699,15 +627,6 @@
|
||||
32.0
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
6.440000000000001,
|
||||
1.4000000000000004
|
||||
],
|
||||
"texture": "#tube_cap"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
@@ -767,15 +686,6 @@
|
||||
32.0
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
4.199999999999999,
|
||||
1.4000000000000004
|
||||
],
|
||||
"texture": "#tube_cap"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
@@ -971,15 +881,6 @@
|
||||
32.0
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
4.199999999999999,
|
||||
1.4
|
||||
],
|
||||
"texture": "#tube_cap"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
@@ -1039,15 +940,6 @@
|
||||
32.0
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
6.440000000000001,
|
||||
1.4
|
||||
],
|
||||
"texture": "#tube_cap"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
@@ -1107,15 +999,6 @@
|
||||
32.0
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
7.0,
|
||||
1.4000000000000004
|
||||
],
|
||||
"texture": "#tube_cap"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
@@ -1175,15 +1058,6 @@
|
||||
32.0
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
6.440000000000001,
|
||||
1.3999999999999995
|
||||
],
|
||||
"texture": "#tube_cap"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
@@ -1243,15 +1117,6 @@
|
||||
32.0
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
4.199999999999999,
|
||||
1.4000000000000004
|
||||
],
|
||||
"texture": "#tube_cap"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
@@ -1447,15 +1312,6 @@
|
||||
32.0
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
4.199999999999999,
|
||||
1.4000000000000004
|
||||
],
|
||||
"texture": "#tube_cap"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
@@ -1515,15 +1371,6 @@
|
||||
32.0
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
6.440000000000001,
|
||||
1.4000000000000004
|
||||
],
|
||||
"texture": "#tube_cap"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
@@ -1583,15 +1430,6 @@
|
||||
32.0
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
7.0,
|
||||
1.3999999999999986
|
||||
],
|
||||
"texture": "#tube_cap"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
@@ -1651,15 +1489,6 @@
|
||||
32.0
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
6.440000000000001,
|
||||
1.4000000000000004
|
||||
],
|
||||
"texture": "#tube_cap"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
@@ -1719,15 +1548,6 @@
|
||||
32.0
|
||||
],
|
||||
"faces": {
|
||||
"north": {
|
||||
"uv": [
|
||||
0,
|
||||
0,
|
||||
4.199999999999999,
|
||||
1.4000000000000004
|
||||
],
|
||||
"texture": "#tube_cap"
|
||||
},
|
||||
"south": {
|
||||
"uv": [
|
||||
0,
|
||||
|
||||
@@ -14,7 +14,7 @@ fn on_off() -> Value {
|
||||
/// `/airdef ...` as `lib.rs` accepts it.
|
||||
pub fn tree() -> Tree {
|
||||
let wave_kinds: Vec<&str> = DroneKind::ALL.iter().map(|k| k.spec().key).chain(["mix"]).collect();
|
||||
let deploy_kinds: Vec<&str> = SamKind::ALL.iter().map(|k| k.key()).collect();
|
||||
let deploy_kinds: Vec<&str> = SamKind::ALL.iter().map(|k| k.key()).chain(["s400_launcher", "s500_launcher", "radar_search", "radar_track", "radar_height"]).collect();
|
||||
let armed: Vec<&str> = SamKind::ARMED.iter().map(|k| k.key()).chain(["all"]).collect();
|
||||
let toggle = |word: &str| Node::lit(word).then(Node::arg("on/off", on_off()).runs());
|
||||
Tree::new("airdef").runs().with([
|
||||
@@ -70,6 +70,8 @@ pub fn tree() -> Tree {
|
||||
Node::lit("move").then(Node::arg("номер", Value::int_from(1)).runs()),
|
||||
Node::lit("target").then(Node::lit("here").runs()),
|
||||
Node::lit("give").runs(),
|
||||
Node::lit("battery").runs().then(Node::arg("номер", Value::int_from(1)).then(Node::arg("режим", Value::choice(["integrated", "launcher"])).runs())),
|
||||
Node::lit("radar").then(Node::arg("номер", Value::int_from(1)).then(Node::arg("роль", Value::choice(["multi", "search", "engagement", "height"])).runs())),
|
||||
Node::lit("deploy").then(Node::arg("комплекс", Value::choice(deploy_kinds)).runs()),
|
||||
Node::lit("wave").then(
|
||||
Node::arg("тип", Value::choice(wave_kinds)).runs().then(
|
||||
@@ -164,7 +166,7 @@ mod tests {
|
||||
"replay export mine all fps=30 size=1280x720 crf=18 sec=12.5 noaudio", "replay export fps=60", "replay stop",
|
||||
"scenario save a", "scenario save a fresh", "scenario load a", "scenario load a force", "scenario list",
|
||||
"scenario delete a", "why", "why 2", "ew on", "ew off", "move 4", "target here", "targets clear",
|
||||
"sites", "strike 3", "deploy patriot", "deploy light", "deploy s400", "wave mix", "wave shahed", "wave shahed 20", "wave kh101 80 800", "wave shahed 20 4000",
|
||||
"sites", "strike 3", "battery", "battery 1 launcher", "battery 1 integrated", "radar 2 engagement", "radar 2 search", "deploy s400_launcher", "deploy s500_launcher", "deploy radar_search", "deploy radar_track", "deploy radar_height", "deploy patriot", "deploy light", "deploy s400", "wave mix", "wave shahed", "wave shahed 20", "wave kh101 80 800", "wave shahed 20 4000",
|
||||
"wave oreshnik 1 60", "damage on", "damage off", "truth on", "shader hd", "shader off", "shader on", "hold on", "lastchance on", "lastchance off",
|
||||
"roles all default", "doctrine", "doctrine 1 reserve 4", "doctrine all salvo eco", "doctrine 2 sector 90 45", "doctrine 2 sector off", "doctrine 1 guard 1,2", "doctrine 1 reset", "roles patriot drone,jet", "roles pantsir all", "roles tor ballistic",
|
||||
] {
|
||||
@@ -240,7 +242,7 @@ mod tests {
|
||||
"target", "give", "deploy", "wave", "clear", "targets", "damage", "truth", "shader", "hold", "lastchance", "roles", "doctrine", "stats", "sites", "strike", "ground-stock", "ground-load", "ground-unload"] {
|
||||
assert!(top.contains(&w.to_string()), "{w} in {top:?}");
|
||||
}
|
||||
assert_eq!(top.len(), 31);
|
||||
assert_eq!(top.len(), 33);
|
||||
assert_eq!(complete("rep", &[]), ["repair", "replay"]);
|
||||
assert_eq!(complete("replay ", &[]), ["export", "film", "full", "map", "reel", "stop"]);
|
||||
assert_eq!(complete("replay film ", &[]), ["all", "best"]);
|
||||
|
||||
@@ -26,6 +26,9 @@ pub struct SamRec {
|
||||
pub ground_rounds: u32,
|
||||
pub ground_stock: u32,
|
||||
pub air_48n6_stock: u32,
|
||||
pub mode: sim::battery::SamMode,
|
||||
pub radar_role: sim::battery::RadarRole,
|
||||
pub spent_cells: u8,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -123,13 +126,31 @@ impl Layout {
|
||||
let ground_rounds = l.num(22, "заряжено RM48U", 0.0, 4.0)?.unwrap_or(0.0) as u32;
|
||||
let ground_stock = l.num(23, "запас RM48U", 0.0, COUNT)?.unwrap_or(0.0) as u32;
|
||||
let air_48n6_stock = l.num(24, "снятый боезапас ПВО", 0.0, COUNT)?.unwrap_or(0.0) as u32;
|
||||
let mode = match l.w.get(25) {
|
||||
None => sim::battery::SamMode::Integrated,
|
||||
Some(v) => sim::battery::SamMode::from_key(v).ok_or_else(|| l.bad("режим", "integrated или launcher"))?,
|
||||
};
|
||||
let radar_role = match l.w.get(26) {
|
||||
None => sim::battery::RadarRole::Multifunction,
|
||||
Some(v) => sim::battery::RadarRole::from_key(v).ok_or_else(|| l.bad("роль РЛС", "multi, search, engagement или height"))?,
|
||||
};
|
||||
let spent_cells = match l.w.get(27) {
|
||||
None => 0,
|
||||
Some(v) => v.parse::<u8>().map_err(|_| l.bad("пустые контейнеры", "нужно целое число 0..255"))?,
|
||||
};
|
||||
if radar_role != sim::battery::RadarRole::Multifunction && kind != SamKind::Radar {
|
||||
return Err(l.bad("роль РЛС", "роль поддерживается только отдельной РЛС"));
|
||||
}
|
||||
if mode == sim::battery::SamMode::LauncherOnly && !matches!(kind, SamKind::S400 | SamKind::S500) {
|
||||
return Err(l.bad("режим", "пусковая поддерживается только С-400 и С-500"));
|
||||
}
|
||||
if (kind != SamKind::S400 && ground_rounds != 0) || (kind != SamKind::Depot && (ground_stock != 0 || air_48n6_stock != 0)) {
|
||||
return Err(l.bad("боезапас", "несовместимый тип техники"));
|
||||
}
|
||||
if let Some((m, _, _, _)) = ammo && m + ground_rounds > 4 && kind == SamKind::S400 {
|
||||
return Err(l.bad("боезапас", "больше четырёх ячеек"));
|
||||
}
|
||||
out.sams.push(SamRec { kind, pos, heading, roles, health, cargo, ammo, dest, comps, ground_rounds, ground_stock, air_48n6_stock });
|
||||
out.sams.push(SamRec { kind, pos, heading, roles, health, cargo, ammo, dest, comps, ground_rounds, ground_stock, air_48n6_stock, mode, radar_role, spent_cells });
|
||||
}
|
||||
"target" => {
|
||||
if out.targets.len() >= MAX_UNITS {
|
||||
@@ -178,6 +199,20 @@ impl Layout {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn battery_modes_load_and_old_layouts_default() {
|
||||
let old = Layout::parse("sam s400 0 70 0 0").unwrap();
|
||||
assert_eq!(old.sams[0].mode, sim::battery::SamMode::Integrated);
|
||||
assert_eq!(old.sams[0].radar_role, sim::battery::RadarRole::Multifunction);
|
||||
let data = "sam s400 0 70 0 0 30 1 0 4 0 0 0 - - - 1 1 1 1 1 1 0 0 0 launcher multi 5\nsam radar 1 70 0 0 0 1 0 0 0 0 0 - - - 1 1 1 1 1 1 0 0 0 integrated engagement";
|
||||
let l = Layout::parse(data).unwrap();
|
||||
assert_eq!(l.sams[0].mode, sim::battery::SamMode::LauncherOnly);
|
||||
assert_eq!(l.sams[0].spent_cells, 5);
|
||||
assert_eq!(l.sams[1].radar_role, sim::battery::RadarRole::Engagement);
|
||||
assert!(Layout::parse(&data.replace("engagement", "invalid")).is_err());
|
||||
assert!(Layout::parse(&data.replace("launcher multi 5", "launcher multi 256")).is_err());
|
||||
}
|
||||
|
||||
fn key() -> &'static str {
|
||||
SamKind::Patriot.key()
|
||||
}
|
||||
|
||||
+123
-20
@@ -187,6 +187,7 @@ fn travels(event: &str) -> bool {
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
enum Page {
|
||||
Main,
|
||||
BatteryDeploy,
|
||||
Roles(SamKind),
|
||||
/// The raid's make-up, then its parameters and the launch.
|
||||
Wave,
|
||||
@@ -201,6 +202,27 @@ enum Page {
|
||||
}
|
||||
|
||||
const MENU_KINDS: u32 = 1;
|
||||
const BATTERY_ROWS: u32 = 5;
|
||||
const BTN_BATTERY: u32 = 22;
|
||||
const BATTERY_DEPLOY: [(&str, &str); 5] = [
|
||||
("s400_launcher", "С-400 — отдельная пусковая"),
|
||||
("s500_launcher", "С-500 — отдельная пусковая"),
|
||||
("radar_search", "РЛС обзора"),
|
||||
("radar_track", "РЛС сопровождения"),
|
||||
("radar_height", "Мачтовая РЛС"),
|
||||
];
|
||||
|
||||
fn deploy_configuration(key: &str) -> Option<(SamKind, sim::battery::SamMode, sim::battery::RadarRole)> {
|
||||
use sim::battery::{RadarRole as R, SamMode as M};
|
||||
Some(match key {
|
||||
"s400_launcher" => (SamKind::S400, M::LauncherOnly, R::Multifunction),
|
||||
"s500_launcher" => (SamKind::S500, M::LauncherOnly, R::Multifunction),
|
||||
"radar_search" => (SamKind::Radar, M::Integrated, R::Search),
|
||||
"radar_track" => (SamKind::Radar, M::Integrated, R::Engagement),
|
||||
"radar_height" => (SamKind::Radar, M::Integrated, R::Height),
|
||||
_ => (SamKind::from_key(key)?, M::Integrated, R::Multifunction),
|
||||
})
|
||||
}
|
||||
const BTN_HOLD: u32 = 10;
|
||||
const BTN_SHADER: u32 = 11;
|
||||
const BTN_DAMAGE: u32 = 12;
|
||||
@@ -875,7 +897,7 @@ impl AirDef {
|
||||
let comps = if fresh { new.comps } else { s.comps };
|
||||
let parts = comps.0.iter().map(|v| format!("{v:.3}")).collect::<Vec<_>>().join(" ");
|
||||
text += &format!(
|
||||
"sam {} {} {} {} {} {} {} {} {} {} {} {} {dest} {parts} {} {} {}\n",
|
||||
"sam {} {} {} {} {} {} {} {} {} {} {} {} {dest} {parts} {} {} {} {} {} {}\n",
|
||||
s.kind.key(),
|
||||
s.pos.x,
|
||||
s.pos.y,
|
||||
@@ -888,7 +910,7 @@ impl AirDef {
|
||||
bursts,
|
||||
u8::from(empty),
|
||||
reloading,
|
||||
s.ground_rounds, s.ground_stock, s.air_48n6_stock,
|
||||
s.ground_rounds, s.ground_stock, s.air_48n6_stock, s.mode.key(), s.radar_role.key(), if fresh { 0 } else { s.spent_cells },
|
||||
);
|
||||
}
|
||||
for (i, t) in self.sim.targets.iter().enumerate() {
|
||||
@@ -1000,6 +1022,10 @@ impl AirDef {
|
||||
sam.ground_stock = r.ground_stock;
|
||||
sam.air_48n6_stock = r.air_48n6_stock;
|
||||
sam.fit_cells();
|
||||
sam.set_mode(r.mode);
|
||||
sam.set_radar_role(r.radar_role);
|
||||
sam.spent_cells = r.spent_cells;
|
||||
sam.sync_tube_cells();
|
||||
self.sim.sams.push(sam);
|
||||
if let Some(to) = r.dest {
|
||||
let i = self.sim.sams.len() - 1;
|
||||
@@ -1093,7 +1119,7 @@ fn durable_mark(sim: &Sim) -> u64 {
|
||||
let ammo = u64::from(s.missiles) | u64::from(s.bursts) << 16 | u64::from(s.cargo) << 32 | u64::from(s.empty) << 48 | u64::from(s.reloading > 0) << 49;
|
||||
let typed = u64::from(s.ground_rounds) ^ u64::from(s.ground_stock).rotate_left(17) ^ u64::from(s.air_48n6_stock).rotate_left(39);
|
||||
let parts = s.comps.0.iter().fold(0u64, |a, v| a.wrapping_mul(131).wrapping_add((v * 100.0) as u64));
|
||||
((s.health * 1000.0) as u64).wrapping_mul(1_000_003) ^ place ^ ammo.rotate_left(7) ^ parts.rotate_left(13) ^ typed.rotate_left(29)
|
||||
((s.health * 1000.0) as u64).wrapping_mul(1_000_003) ^ place ^ ammo.rotate_left(7) ^ parts.rotate_left(13) ^ typed.rotate_left(29) ^ (s.mode as u64).rotate_left(3) ^ (s.radar_role as u64).rotate_left(11) ^ u64::from(s.spent_cells).rotate_left(19)
|
||||
})
|
||||
.chain(sim.target_health.iter().map(|h| (h * 10.0) as u64))
|
||||
.fold(sim.sams.len() as u64, |a, h| a.wrapping_mul(31).wrapping_add(h))
|
||||
@@ -1458,6 +1484,23 @@ impl AirDef {
|
||||
};
|
||||
}
|
||||
|
||||
fn deploy_configured(&mut self, key: &str) -> Result<String, String> {
|
||||
let (kind, mode, role) = deploy_configuration(key).ok_or("Неизвестный вид техники")?;
|
||||
let p = player().ok_or("Нет игрока")?;
|
||||
let d = view(&p);
|
||||
let flat = DVec3::new(d.x, 0.0, d.z).normalize_or(DVec3::NEG_Z);
|
||||
let at = DVec3::new(p.x, p.y, p.z) + flat * 8.0;
|
||||
let id = self.sim.id();
|
||||
let mut sam = Sam::new(id, kind, at, (-flat.x).atan2(-flat.z));
|
||||
sam.set_mode(mode);
|
||||
sam.set_radar_role(role);
|
||||
if let Some(r) = self.sim.roles.get(&kind) { sam.roles = *r; }
|
||||
self.sim.sams.push(sam);
|
||||
self.sim.settle(id);
|
||||
self.dirty = true;
|
||||
Ok(format!("{} развёрнут ({}, {})", kind.name(), mode.key(), role.key()))
|
||||
}
|
||||
|
||||
fn open_menu(&mut self, page: Page) {
|
||||
self.menu = Some(page);
|
||||
let onoff = |b: bool| if b { "вкл" } else { "выкл" };
|
||||
@@ -1476,6 +1519,7 @@ impl AirDef {
|
||||
.collect();
|
||||
widgets.push(Widget::List(ListWidget { id: MENU_KINDS, rows, selected: None }));
|
||||
let _ = &self.monitor;
|
||||
widgets.push(Widget::Button(ButtonWidget { id: BTN_BATTERY, label: "Разместить элементы батареи…".into() }));
|
||||
widgets.push(Widget::Button(ButtonWidget { id: BTN_GRID, label: "Командный пункт: все комплексы (J)".into() }));
|
||||
let custom = self.sim.sams.iter().filter(|s| !s.doctrine.is_default()).count();
|
||||
widgets.push(Widget::Button(ButtonWidget { id: BTN_DOCTRINE, label: format!("Доктрины: охрана, сектор, резерв (особых: {custom})…") }));
|
||||
@@ -1483,6 +1527,11 @@ impl AirDef {
|
||||
widgets.push(Widget::Button(ButtonWidget { id: BTN_REPLAYS, label: "Повторы…".into() }));
|
||||
widgets.push(Widget::Button(ButtonWidget { id: BTN_MORE, label: "Огонь, ремонт, эффекты…".into() }));
|
||||
}
|
||||
Page::BatteryDeploy => {
|
||||
title = "Размещение батареи — клик создаёт технику перед игроком".into();
|
||||
widgets.push(Widget::List(ListWidget { id: BATTERY_ROWS, rows: BATTERY_DEPLOY.iter().map(|(_, name)| (*name).to_string()).collect(), selected: None }));
|
||||
widgets.push(Widget::Button(ButtonWidget { id: BTN_BACK, label: "← Назад".into() }));
|
||||
}
|
||||
Page::Replays => {
|
||||
match self.sim.replays.last() {
|
||||
Some(rec) => {
|
||||
@@ -1999,6 +2048,23 @@ impl Mod for AirDef {
|
||||
}
|
||||
return;
|
||||
}
|
||||
(Page::Main, BTN_BATTERY) => {
|
||||
self.open_menu(Page::BatteryDeploy);
|
||||
return;
|
||||
}
|
||||
(Page::BatteryDeploy, BATTERY_ROWS) => {
|
||||
if let Some((key, _)) = BATTERY_DEPLOY.get(row as usize) {
|
||||
match self.deploy_configured(key) {
|
||||
Ok(message) => say(&message),
|
||||
Err(error) => say(&error),
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
(Page::BatteryDeploy, BTN_BACK) => {
|
||||
self.open_menu(Page::Main);
|
||||
return;
|
||||
}
|
||||
(Page::Main, BTN_DOCTRINE) => {
|
||||
self.open_menu(Page::Fleet);
|
||||
return;
|
||||
@@ -2207,24 +2273,27 @@ impl Mod for AirDef {
|
||||
self.give();
|
||||
None
|
||||
}
|
||||
["deploy", kind] => {
|
||||
// A system where the player stands, a few blocks ahead.
|
||||
let kind = SamKind::from_key(kind)
|
||||
.ok_or("Техника: patriot, gepard, pantsir, s400, s500, irist, nasams, buk, tor, p1sun, radar, depot, tzm, ew, light")?;
|
||||
let p = player().ok_or("Нет игрока")?;
|
||||
let d = view(&p);
|
||||
let flat = DVec3::new(d.x, 0.0, d.z).normalize_or(DVec3::NEG_Z);
|
||||
let at = DVec3::new(p.x, p.y, p.z) + flat * 8.0;
|
||||
let sid = self.sim.id();
|
||||
let mut sam = Sam::new(sid, kind, at, (-flat.x).atan2(-flat.z));
|
||||
if let Some(r) = self.sim.roles.get(&kind) {
|
||||
sam.roles = *r;
|
||||
}
|
||||
self.sim.sams.push(sam);
|
||||
// Standing in the ground (a chunk that had not loaded as the player fell): it comes up to the surface.
|
||||
self.sim.settle(sid);
|
||||
["deploy", kind] => Some(self.deploy_configured(kind)?),
|
||||
["battery"] => {
|
||||
let status = self.sim.battery_status();
|
||||
for line in &status { say(line); }
|
||||
Some("/airdef battery <номер> integrated|launcher; /airdef radar <номер> multi|search|engagement|height".into())
|
||||
}
|
||||
["battery", n, mode] => {
|
||||
let i = n.parse::<usize>().ok().and_then(|n| n.checked_sub(1)).ok_or("Нужен номер комплекса")?;
|
||||
let mode = sim::battery::SamMode::from_key(mode).ok_or("Режим: integrated или launcher")?;
|
||||
let sam = self.sim.sams.get_mut(i).ok_or("Нет такого комплекса")?;
|
||||
if !sam.set_mode(mode) { return Err("Отдельная пусковая доступна для С-400 и С-500".into()); }
|
||||
self.dirty = true;
|
||||
Some(format!("{} развёрнут", kind.name()))
|
||||
Some(format!("№{} {}: режим {}", i + 1, sam.kind.name(), mode.key()))
|
||||
}
|
||||
["radar", n, role] => {
|
||||
let i = n.parse::<usize>().ok().and_then(|n| n.checked_sub(1)).ok_or("Нужен номер РЛС")?;
|
||||
let role = sim::battery::RadarRole::from_key(role).ok_or("Роль: multi, search, engagement или height")?;
|
||||
let sam = self.sim.sams.get_mut(i).ok_or("Нет такого комплекса")?;
|
||||
if !sam.set_radar_role(role) { return Err("Нужна отдельная РЛС".into()); }
|
||||
self.dirty = true;
|
||||
Some(format!("№{} РЛС: роль {}", i + 1, role.key()))
|
||||
}
|
||||
["wave", kind, rest @ ..] => {
|
||||
let count = rest.first().and_then(|v| v.parse().ok()).unwrap_or(5u32).clamp(1, 80);
|
||||
@@ -2450,6 +2519,24 @@ horizon_mod_sdk::export_mod!(AirDef);
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn battery_menu_presets_leave_existing_catalog_indices_intact() {
|
||||
use super::*;
|
||||
assert_eq!(sim::catalog::INVENTORY[0], "airdef:designator");
|
||||
assert_eq!(sim::catalog::UNITS[3].kind, SamKind::S400);
|
||||
assert_eq!(sim::catalog::UNITS[4].kind, SamKind::S500);
|
||||
assert_eq!(sim::catalog::UNITS[10].kind, SamKind::Radar);
|
||||
for (key, _) in BATTERY_DEPLOY {
|
||||
assert!(deploy_configuration(key).is_some());
|
||||
}
|
||||
for kind in SamKind::ALL {
|
||||
let (actual, mode, role) = deploy_configuration(kind.key()).unwrap();
|
||||
assert_eq!(actual, kind);
|
||||
assert_eq!(mode, sim::battery::SamMode::Integrated);
|
||||
assert_eq!(role, sim::battery::RadarRole::Multifunction);
|
||||
}
|
||||
}
|
||||
|
||||
use super::*;
|
||||
|
||||
/// #83: while an export records no other replay or film replaces it; after it, they may.
|
||||
@@ -2465,6 +2552,22 @@ mod tests {
|
||||
assert_ne!(durable_mark(&sim), before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn battery_configuration_and_spent_cells_change_durable_mark() {
|
||||
let mut sim = Sim::new(1);
|
||||
sim.sams.push(Sam::new(1, SamKind::S400, DVec3::ZERO, 0.0));
|
||||
let before = durable_mark(&sim);
|
||||
sim.sams[0].set_mode(sim::battery::SamMode::LauncherOnly);
|
||||
assert_ne!(durable_mark(&sim), before);
|
||||
let before = durable_mark(&sim);
|
||||
sim.sams[0].spent_cells = 1;
|
||||
assert_ne!(durable_mark(&sim), before);
|
||||
sim.sams.push(Sam::new(2, SamKind::Radar, DVec3::ZERO, 0.0));
|
||||
let before = durable_mark(&sim);
|
||||
sim.sams[1].set_radar_role(sim::battery::RadarRole::Search);
|
||||
assert_ne!(durable_mark(&sim), before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nothing_replaces_a_running_export() {
|
||||
assert!(switch_allowed(true).is_err());
|
||||
|
||||
@@ -2322,6 +2322,11 @@ impl AirDef {
|
||||
let Some((n, sam)) = self.sim.sams.iter().enumerate().find(|(_, s)| s.id == id) else { return };
|
||||
let k = sam.kind;
|
||||
lines.push((format!("{} №{}", k.name(), n + 1), CYAN));
|
||||
if k == SamKind::Radar {
|
||||
lines.push((format!("Роль РЛС: {}", sam.radar_role.name()), WHITE));
|
||||
} else if sam.mode == sim::battery::SamMode::LauncherOnly {
|
||||
lines.push(("Отдельная пусковая: внешняя РЛС сопровождения".into(), WHITE));
|
||||
}
|
||||
lines.push((sam.status(), WHITE));
|
||||
lines.push((format!("Исправен {:.0}% · {:.0} {:.0}", sam.health * 100.0, sam.pos.x, sam.pos.z), GREY));
|
||||
if k.armed() {
|
||||
@@ -2329,8 +2334,8 @@ impl AirDef {
|
||||
lines.push((format!("Боезапас: {} ракет, {} очередей{}", sam.missiles, sam.bursts, if sam.reloading > 0 { format!(" · перезарядка {} с", sam.reloading / 20) } else { String::new() }), WHITE));
|
||||
lines.push((format!("Дальность огня: {}", distance(k.range())), WHITE));
|
||||
}
|
||||
if k.spec().sensor > 0.0 {
|
||||
lines.push((format!("Свой обзор: {} (зелёное кольцо)", distance(k.spec().sensor)), WHITE));
|
||||
if let Some(sensor) = self.sim.low_sensor(k, sam.pos, Some(sam.id)) {
|
||||
lines.push((format!("Свой обзор для низкой цели: {} (зелёное кольцо)", distance(sensor.reach)), WHITE));
|
||||
}
|
||||
if let Some(t) = sam.gun_target.or(sam.target) {
|
||||
lines.push((format!("Ведёт цель: трек №{t}"), YELLOW));
|
||||
|
||||
@@ -13,7 +13,7 @@ pub enum Source<'a> {
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum MoverKind {
|
||||
Drone(sim::DroneKind),
|
||||
Missile(sim::MissileKind),
|
||||
Missile { kind: sim::MissileKind, engine_lit: bool },
|
||||
Strike { physical: bool, engine_lit: bool },
|
||||
}
|
||||
|
||||
@@ -135,7 +135,7 @@ impl<'a> Snapshot<'a> {
|
||||
.chain(sim.missiles.iter().map(|m| Mover {
|
||||
id: m.id,
|
||||
group: 1,
|
||||
kind: MoverKind::Missile(m.kind),
|
||||
kind: MoverKind::Missile { kind: m.kind, engine_lit: m.engine_lit() },
|
||||
pos: m.pos,
|
||||
}))
|
||||
.chain(sim.strikes.flights.iter().map(|f| {
|
||||
@@ -160,7 +160,7 @@ impl<'a> Snapshot<'a> {
|
||||
let pos = tr.at(rec.origin, self.time)?;
|
||||
let (group, kind) = match tr.what {
|
||||
sim::record::What::Drone(kind) => (0, MoverKind::Drone(kind)),
|
||||
sim::record::What::Missile(kind, _) => (1, MoverKind::Missile(kind)),
|
||||
sim::record::What::Missile(kind, _) => (1, MoverKind::Missile { kind, engine_lit: rec.missile_is_lit(tr, self.time, kind.lit_after()) }),
|
||||
sim::record::What::Strike => (
|
||||
2,
|
||||
MoverKind::Strike {
|
||||
|
||||
@@ -0,0 +1,257 @@
|
||||
//! Game-only configuration for a launcher using shared radar tracks.
|
||||
use super::*;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum SamMode {
|
||||
#[default]
|
||||
Integrated,
|
||||
LauncherOnly,
|
||||
}
|
||||
|
||||
impl SamMode {
|
||||
pub fn key(self) -> &'static str {
|
||||
match self { Self::Integrated => "integrated", Self::LauncherOnly => "launcher" }
|
||||
}
|
||||
pub fn from_key(key: &str) -> Option<Self> {
|
||||
match key { "integrated" => Some(Self::Integrated), "launcher" => Some(Self::LauncherOnly), _ => None }
|
||||
}
|
||||
}
|
||||
|
||||
/// Game roles for the generic shared radar; all parameters are fictional.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum RadarRole {
|
||||
#[default]
|
||||
Multifunction,
|
||||
Search,
|
||||
Engagement,
|
||||
Height,
|
||||
}
|
||||
impl RadarRole {
|
||||
pub fn key(self) -> &'static str {
|
||||
match self { Self::Multifunction => "multi", Self::Search => "search", Self::Engagement => "engagement", Self::Height => "height" }
|
||||
}
|
||||
pub fn from_key(key: &str) -> Option<Self> {
|
||||
match key { "multi" => Some(Self::Multifunction), "search" => Some(Self::Search), "engagement" => Some(Self::Engagement), "height" => Some(Self::Height), _ => None }
|
||||
}
|
||||
pub fn name(self) -> &'static str {
|
||||
match self { Self::Multifunction => "многофункциональная", Self::Search => "обзорная", Self::Engagement => "сопровождения", Self::Height => "мачтовая" }
|
||||
}
|
||||
pub fn authorizes(self) -> bool { matches!(self, Self::Multifunction | Self::Engagement) }
|
||||
pub(super) fn reach_factor(self) -> f64 { match self { Self::Multifunction => 1.0, Self::Search => 1.15, Self::Engagement => 0.8, Self::Height => 1.05 } }
|
||||
}
|
||||
|
||||
impl Sam {
|
||||
pub fn set_radar_role(&mut self, role: RadarRole) -> bool {
|
||||
if self.kind != SamKind::Radar { return false; }
|
||||
if self.radar_role != role {
|
||||
self.radar_role = role;
|
||||
self.tracks.clear();
|
||||
self.seen.clear();
|
||||
self.own.clear();
|
||||
}
|
||||
true
|
||||
}
|
||||
/// Change the physical configuration. Invalid combinations leave the unit untouched.
|
||||
pub fn set_mode(&mut self, mode: SamMode) -> bool {
|
||||
if mode == SamMode::LauncherOnly && !matches!(self.kind, SamKind::S400 | SamKind::S500) { return false; }
|
||||
if self.mode != mode {
|
||||
self.mode = mode;
|
||||
self.tracks.clear();
|
||||
self.seen.clear();
|
||||
self.own.clear();
|
||||
self.target = None;
|
||||
self.gun_target = None;
|
||||
self.terrain.clear();
|
||||
self.waits.clear();
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl Sim {
|
||||
/// Current configurations and whether a launcher has an operational radar link.
|
||||
pub fn battery_status(&self) -> Vec<String> {
|
||||
self.sams.iter().enumerate().filter_map(|(i, sam)| {
|
||||
if sam.kind == SamKind::Radar {
|
||||
Some(format!("№{} РЛС: {}, {}", i + 1, sam.radar_role.name(), if sam.radar_up() && sam.link_range() > 0.0 { "работает" } else { "нет связи или РЛС не работает" }))
|
||||
} else if matches!(sam.kind, SamKind::S400 | SamKind::S500) {
|
||||
let linked = self.sams.iter().any(|r| r.kind == SamKind::Radar && r.radar_role.authorizes() && r.radar_up() && r.link_range() > 0.0 && sam.link_range() > 0.0 && r.pos.distance(sam.pos) <= r.link_range().min(sam.link_range()));
|
||||
Some(format!("№{} {}: {}{}", i + 1, sam.kind.name(), sam.mode.key(), if sam.mode == SamMode::LauncherOnly { if linked { ", РЛС связана" } else { ", нет связанной РЛС сопровождения" } } else { "" }))
|
||||
} else { None }
|
||||
}).collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use super::super::{sight, why};
|
||||
|
||||
fn track(now: u64) -> Track {
|
||||
Track { id: 42, kind: DroneKind::Kh101, pos: DVec3::new(0.0, 100.0, -200.0), vel: DVec3::ZERO, tick: now, own_tick: now, ttl: 400, hits: 3, locked: false }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shared_radar_serves_multiple_launchers_and_failure_blocks_cue() {
|
||||
let mut radar = Sam::new(1, SamKind::Radar, DVec3::ZERO, 0.0);
|
||||
let mut a = Sam::new(2, SamKind::S400, DVec3::new(10.0, 0.0, 0.0), 0.0);
|
||||
let mut b = Sam::new(3, SamKind::S500, DVec3::new(20.0, 0.0, 0.0), 0.0);
|
||||
assert!(a.set_mode(SamMode::LauncherOnly));
|
||||
assert!(b.set_mode(SamMode::LauncherOnly));
|
||||
assert!(!a.radar_up() && !b.radar_up());
|
||||
radar.tracks.push(track(10));
|
||||
radar.set_radar_role(RadarRole::Search);
|
||||
radar.tracks.push(track(10));
|
||||
assert!(!why::radar_cue(&a, &[&radar], 42, 10));
|
||||
radar.set_radar_role(RadarRole::Height);
|
||||
radar.tracks.push(track(10));
|
||||
assert!(!why::radar_cue(&a, &[&radar], 42, 10));
|
||||
radar.set_radar_role(RadarRole::Engagement);
|
||||
radar.tracks.push(track(10));
|
||||
assert!(why::radar_cue(&a, &[&radar], 42, 10));
|
||||
assert!(why::radar_cue(&b, &[&radar], 42, 10));
|
||||
assert!(!why::radar_cue(&a, &[&radar], 42, 10 + why::FAR_RADAR_AGE + 1));
|
||||
radar.comps.0[Part::Comms as usize] = 0.0;
|
||||
assert!(!why::radar_cue(&a, &[&radar], 42, 10));
|
||||
radar.comps.0[Part::Comms as usize] = 1.0;
|
||||
a.comps.0[Part::Comms as usize] = 0.0;
|
||||
assert!(!why::radar_cue(&a, &[&radar], 42, 10));
|
||||
a.comps.0[Part::Comms as usize] = 1.0;
|
||||
a.pos.x = sight::LINK + 1.0;
|
||||
assert!(!why::radar_cue(&a, &[&radar], 42, 10));
|
||||
a.pos.x = 10.0;
|
||||
radar.health = 0.0;
|
||||
assert!(!why::radar_cue(&a, &[&radar], 42, 10));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn network_shares_a_measurement_to_both_launchers_without_fabricating_own_tracks() {
|
||||
let mut sim = Sim::new(7);
|
||||
sim.tick = 10;
|
||||
let id = sim.id();
|
||||
sim.sams.push(Sam::new(id, SamKind::Radar, DVec3::ZERO, 0.0));
|
||||
for kind in [SamKind::S400, SamKind::S500] {
|
||||
let id = sim.id();
|
||||
let mut launcher = Sam::new(id, kind, DVec3::new(10.0, 0.0, 0.0), 0.0);
|
||||
launcher.set_mode(SamMode::LauncherOnly);
|
||||
launcher.roles = 0xff;
|
||||
sim.sams.push(launcher);
|
||||
}
|
||||
sim.add_drone(DroneKind::Kh101, DVec3::new(0.0, 100.0, -200.0), DVec3::Z, DVec3::ZERO);
|
||||
let id = sim.drones[0].id;
|
||||
let mut measurement = track(10);
|
||||
measurement.id = id;
|
||||
sim.sams[0].tracks.push(measurement);
|
||||
sim.update_sight(&mut Ground::default());
|
||||
for launcher in &sim.sams[1..] {
|
||||
let received = launcher.track(id).expect("shared measurement");
|
||||
assert_eq!(received.tick, 10);
|
||||
assert_eq!(received.own_tick, 0);
|
||||
assert!(why::radar_cue(launcher, &[&sim.sams[0]], id, 10));
|
||||
let load = launcher.kind.spec().missile.as_ref().unwrap();
|
||||
let opts = why::Opts { last_chance: false, far_shots: false };
|
||||
assert!(why::basis(launcher, load, received, &sim.drones[0], 10, 0, 1.0, opts, true, false).is_some());
|
||||
assert!(why::basis(launcher, load, received, &sim.drones[0], 10, 0, 1.0, opts, false, false).is_none());
|
||||
}
|
||||
sim.sams[0].health = 0.0;
|
||||
for launcher in &sim.sams[1..] {
|
||||
assert!(launcher.track(id).is_some());
|
||||
assert!(!why::radar_cue(launcher, &[&sim.sams[0]], id, 10));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_cues_tracking_radar_but_only_its_own_measurement_authorizes_launch() {
|
||||
for in_range in [true, false] {
|
||||
let mut sim = Sim::new(71);
|
||||
sim.tick = 10;
|
||||
let mut search = Sam::new(1, SamKind::Radar, DVec3::ZERO, 0.0);
|
||||
search.set_radar_role(RadarRole::Search);
|
||||
let mut fire_control = Sam::new(2, SamKind::Radar, DVec3::ZERO, 0.0);
|
||||
fire_control.set_radar_role(RadarRole::Engagement);
|
||||
let mut launcher = Sam::new(3, SamKind::S400, DVec3::ZERO, 0.0);
|
||||
launcher.set_mode(SamMode::LauncherOnly);
|
||||
launcher.roles = 0xff;
|
||||
sim.sams.extend([search, fire_control, launcher]);
|
||||
let distance = if in_range { 100.0 } else { 10000.0 };
|
||||
sim.add_drone(DroneKind::Kh101, DVec3::new(0.0, 70.0, -distance), DVec3::Z, DVec3::ZERO);
|
||||
let id = sim.drones[0].id;
|
||||
let mut measurement = track(10);
|
||||
measurement.id = id;
|
||||
measurement.pos = sim.drones[0].pos;
|
||||
sim.sams[0].tracks.push(measurement);
|
||||
sim.sams[0].beam = std::f64::consts::PI;
|
||||
sim.sams[1].beam = std::f64::consts::PI;
|
||||
let mut ground = Ground::default();
|
||||
sim.update_sight(&mut ground);
|
||||
assert_eq!(sim.sams[1].track(id).unwrap().own_tick, 0, "cue is not a measurement");
|
||||
assert!(!why::radar_cue(&sim.sams[2], &[&sim.sams[1]], id, sim.tick));
|
||||
for now in 11..40 {
|
||||
sim.tick = now;
|
||||
// Bearing is north (zero); force the ordinary beam away each tick.
|
||||
sim.sams[1].beam = std::f64::consts::PI;
|
||||
sim.update_sight(&mut ground);
|
||||
if sim.sams[1].track(id).is_some_and(|t| t.own_tick > 0) { break; }
|
||||
}
|
||||
let got_own = sim.sams[1].track(id).is_some_and(|t| t.own_tick > 0);
|
||||
assert_eq!(got_own, in_range);
|
||||
assert_eq!(why::radar_cue(&sim.sams[2], &[&sim.sams[1]], id, sim.tick), in_range);
|
||||
if in_range {
|
||||
let tr = sim.sams[2].track(id).unwrap();
|
||||
let load = sim.sams[2].kind.spec().missile.as_ref().unwrap();
|
||||
let opts = why::Opts { last_chance: false, far_shots: false };
|
||||
assert!(why::basis(&sim.sams[2], load, tr, &sim.drones[0], sim.tick, 0, 1.0, opts, true, false).is_some());
|
||||
let mut out = Out::default();
|
||||
for _ in 0..100 {
|
||||
sim.step(&mut ground, None, &mut out);
|
||||
if sim.sams[2].fired > 0 { break; }
|
||||
}
|
||||
assert!(sim.sams[2].fired > 0, "launcher never fired after its tracking radar measured the cue");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fictional_roles_change_scan_and_height_and_coverage() {
|
||||
let mut r = Sam::new(1, SamKind::Radar, DVec3::ZERO, 0.0);
|
||||
let base_mast = r.sensor_mast();
|
||||
let base_scan = r.scan_ticks();
|
||||
let base_reach = sight::reach(&r, DroneKind::Kh101, 5000.0);
|
||||
r.set_radar_role(RadarRole::Height);
|
||||
assert!(r.sensor_mast() > base_mast);
|
||||
r.set_radar_role(RadarRole::Engagement);
|
||||
assert!(r.scan_ticks() < base_scan && r.scan_profile().agile);
|
||||
assert!(sight::reach(&r, DroneKind::Kh101, 5000.0) < base_reach);
|
||||
r.set_radar_role(RadarRole::Search);
|
||||
assert!(sight::reach(&r, DroneKind::Kh101, 5000.0) > base_reach);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn launcher_has_no_own_map_sensor_and_mode_switch_drops_stale_tracks() {
|
||||
let mut sim = Sim::new(3);
|
||||
sim.sams.push(Sam::new(1, SamKind::S400, DVec3::ZERO, 0.0));
|
||||
assert!(sim.low_sensor(SamKind::S400, DVec3::ZERO, Some(1)).is_some());
|
||||
sim.sams[0].tracks.push(track(10));
|
||||
sim.sams[0].terrain.insert(42, (10, 1));
|
||||
assert!(sim.sams[0].set_mode(SamMode::LauncherOnly));
|
||||
assert!(sim.low_sensor(SamKind::S400, DVec3::ZERO, Some(1)).is_none());
|
||||
assert!(sim.low_sensors().is_empty());
|
||||
assert!(sim.sams[0].track(42).is_none());
|
||||
assert!(sim.sams[0].terrain.is_empty());
|
||||
sim.sams[0].set_mode(SamMode::Integrated);
|
||||
assert!(sim.low_sensor(SamKind::S400, DVec3::ZERO, Some(1)).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn configuration_clears_old_tracks_and_rejects_other_kinds() {
|
||||
let mut sam = Sam::new(1, SamKind::S400, DVec3::ZERO, 0.0);
|
||||
sam.tracks.push(track(10));
|
||||
assert!(sam.set_mode(SamMode::LauncherOnly));
|
||||
assert!(sam.tracks.is_empty());
|
||||
assert!(sam.set_mode(SamMode::Integrated));
|
||||
assert!(sam.radar_up());
|
||||
let mut other = Sam::new(2, SamKind::Patriot, DVec3::ZERO, 0.0);
|
||||
assert!(!other.set_mode(SamMode::LauncherOnly));
|
||||
assert_eq!(other.mode, SamMode::Integrated);
|
||||
}
|
||||
}
|
||||
@@ -66,6 +66,8 @@ pub fn bounds(id: &str) -> Option<([f64; 3], [f64; 3])> {
|
||||
"airdef:patriot_trailer" => ([-9.06, 0.0, -16.0], [25.06, 16.0, 32.0]),
|
||||
"airdef:patriot_trailer_burnt" => ([-9.06, 0.0, -16.0], [25.0, 16.0, 32.0]),
|
||||
"airdef:patriot_truck" => ([-8.56, 0.0, -16.0], [24.56, 22.0, 32.0]),
|
||||
"airdef:radar_mast" => ([4.0, 0.0, 4.0], [12.0, 32.0, 12.0]),
|
||||
"airdef:radar_mast_burnt" => ([4.0, 0.0, 4.0], [12.0, 32.0, 12.0]),
|
||||
"airdef:radar_panel" => ([-10.5, -6.0, 7.0], [26.5, 26.5, 12.0]),
|
||||
"airdef:radar_trailer" => ([-9.0, -1.379664, -12.0], [25.0, 18.0, 26.379664]),
|
||||
"airdef:radar_truck" => ([-8.56, 0.0, -16.0], [24.56, 20.0, 32.0]),
|
||||
@@ -91,6 +93,8 @@ pub fn bounds(id: &str) -> Option<([f64; 3], [f64; 3])> {
|
||||
"airdef:tor_chassis_burnt" => ([-13.3, 0.0, -16.0], [29.3, 15.7, 31.0]),
|
||||
"airdef:tor_turret" => ([-4.0, 0.0, -11.0], [20.0, 17.5, 22.0]),
|
||||
"airdef:tor_turret_burnt" => ([-4.0, 0.0, -11.0], [20.0, 17.5, 22.0]),
|
||||
"airdef:tpk_cap" => ([0.0, 0.0, 7.7], [16.0, 16.0, 8.3]),
|
||||
"airdef:tpk_cap_burnt" => ([0.0, 0.0, 7.7], [16.0, 16.0, 8.3]),
|
||||
"airdef:tracer" => ([7.3, 7.3, -16.0], [8.7, 8.7, 32.0]),
|
||||
"airdef:tzm_truck" => ([-8.56, 0.0, -16.0], [24.56, 26.0, 32.0]),
|
||||
"airdef:tzm_truck_burnt" => ([-8.5, 0.0, -16.0], [24.56, 26.0, 32.0]),
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
//! Short, deliberately fictional cold-launch choreography and physical tube visuals.
|
||||
//! Timing and geometry are Minecraft presentation settings, not weapon specifications.
|
||||
use super::*;
|
||||
|
||||
pub const OPEN_TICKS: u64 = 6;
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(super) struct Prepare {
|
||||
pub target: u32,
|
||||
pub cell: u32,
|
||||
pub start: u64,
|
||||
pub ground: bool,
|
||||
}
|
||||
|
||||
impl Sam {
|
||||
pub fn animated_tubes(&self) -> bool { matches!(self.kind, SamKind::S400 | SamKind::S500) }
|
||||
/// Reconcile visual occupancy with the actual shared magazine after load, repair or supply.
|
||||
pub fn sync_tube_cells(&mut self) {
|
||||
if !self.animated_tubes() { self.spent_cells = 0; return; }
|
||||
let cells = self.kind.spec().missile.as_ref().map_or(0, |m| m.magazine);
|
||||
let valid = ((1u16 << cells) - 1) as u8;
|
||||
self.spent_cells &= valid;
|
||||
let loaded = (self.missiles + self.ground_rounds).min(cells);
|
||||
let desired = cells - loaded;
|
||||
while self.spent_cells.count_ones() > desired {
|
||||
let bit = self.spent_cells.trailing_zeros();
|
||||
self.spent_cells &= !(1 << bit);
|
||||
}
|
||||
while self.spent_cells.count_ones() < desired {
|
||||
let bit = ((!self.spent_cells) & valid).trailing_zeros();
|
||||
self.spent_cells |= 1 << bit;
|
||||
}
|
||||
if self.preparing.is_none() { self.cap_open = 0.0; }
|
||||
}
|
||||
pub(super) fn next_tube_cell(&self) -> u32 {
|
||||
let cells = self.kind.spec().missile.as_ref().map_or(1, |m| m.magazine);
|
||||
(0..cells).find(|n| self.spent_cells & (1 << n) == 0).unwrap_or(0)
|
||||
}
|
||||
pub(super) fn cancel_prepare(&mut self) {
|
||||
self.preparing = None;
|
||||
self.cap_open = 0.0;
|
||||
}
|
||||
/// Opening reserves a particular cell; ammunition is consumed only at actual ejection.
|
||||
pub(super) fn prepare_tube(&mut self, target: u32, now: u64) -> bool {
|
||||
if !self.animated_tubes() { return true; }
|
||||
if self.preparing.is_none_or(|p| p.target != target) {
|
||||
self.preparing = Some(Prepare { target, cell: self.next_tube_cell(), start: now, ground: false });
|
||||
}
|
||||
let p = self.preparing.unwrap();
|
||||
self.cap_cell = p.cell as u8;
|
||||
self.cap_open = (now.saturating_sub(p.start) as f32 / OPEN_TICKS as f32).clamp(0.0, 1.0);
|
||||
self.cap_open >= 1.0
|
||||
}
|
||||
pub(super) fn eject_cell(&mut self, cell: u32) {
|
||||
if self.animated_tubes() { self.spent_cells |= 1 << cell; }
|
||||
self.cancel_prepare();
|
||||
}
|
||||
}
|
||||
|
||||
/// A hinge is a readable game abstraction. No claim is made about the real cover mechanism.
|
||||
pub(super) fn draw_caps(sam: &Sam, out: &mut Out) {
|
||||
if !sam.animated_tubes() || !sam.alive() { return; }
|
||||
let Muzzle::Grid(cols, rows, size) = sam.kind.spec().muzzle else { return };
|
||||
let (_, basis) = sam.mount();
|
||||
let side = size / 16.0 * sam.scale();
|
||||
for cell in 0..cols * rows {
|
||||
let open = if sam.spent_cells & (1 << cell) != 0 { 1.0 }
|
||||
else if sam.cap_cell as u32 == cell { sam.cap_open } else { 0.0 };
|
||||
// Grid muzzle includes TUBE_PIVOT and is exactly the front plane of the model.
|
||||
let (closed, _) = sam.muzzle(cell);
|
||||
let hinge = closed + basis * DVec3::new(-side * 0.5, 0.0, 0.0);
|
||||
let rotation = basis * DQuat::from_rotation_y((open as f64) * 1.8);
|
||||
let center = hinge + rotation * DVec3::new(side * 0.5, 0.0, 0.0);
|
||||
out.model(sam.id + 8 + cell, "airdef:tpk_cap", center, rotation, side, false);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn every_animated_magazine_has_one_round_per_drawn_tube() {
|
||||
for kind in [SamKind::S400, SamKind::S500] {
|
||||
let sam = Sam::new(16, kind, DVec3::ZERO, 0.0);
|
||||
let Muzzle::Grid(cols, rows, _) = kind.spec().muzzle else { panic!("tube grid") };
|
||||
assert_eq!(sam.missiles, cols * rows);
|
||||
let mut out = Out::default();
|
||||
draw_caps(&sam, &mut out);
|
||||
assert_eq!(out.models.len(), sam.missiles as usize);
|
||||
for (cell, model) in out.models.iter().enumerate() {
|
||||
assert!(model.at.distance(sam.muzzle(cell as u32).0) < 1e-9, "closed lid covers the real muzzle");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn opening_reserves_a_cell_and_abort_does_not_spend_rounds() {
|
||||
let mut sam = Sam::new(16, SamKind::S400, DVec3::ZERO, 0.0);
|
||||
assert!(!sam.prepare_tube(42, 10));
|
||||
assert!(!sam.prepare_tube(42, 13));
|
||||
assert_eq!(sam.cap_open, 0.5);
|
||||
let cell = sam.preparing.unwrap().cell;
|
||||
assert_eq!(sam.missiles, 4);
|
||||
sam.cancel_prepare();
|
||||
assert_eq!((sam.missiles, sam.spent_cells, sam.cap_open), (4, 0, 0.0));
|
||||
assert!(!sam.prepare_tube(42, 20));
|
||||
assert_eq!(sam.preparing.unwrap().cell, cell);
|
||||
assert!(sam.prepare_tube(42, 20 + OPEN_TICKS));
|
||||
sam.missiles -= 1;
|
||||
sam.eject_cell(cell);
|
||||
assert_ne!(sam.next_tube_cell(), cell);
|
||||
sam.sync_tube_cells();
|
||||
assert_eq!(sam.spent_cells.count_ones(), 1);
|
||||
sam.missiles = 4;
|
||||
sam.sync_tube_cells();
|
||||
assert_eq!(sam.spent_cells, 0);
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ impl Sim {
|
||||
pub fn load_ground_round(&mut self, i: usize) -> Result<String, String> {
|
||||
let s = self.sams.get(i).ok_or("Нет такого комплекса")?;
|
||||
if s.kind != SamKind::S400 { return Err("Для этого комплекса нет игрового профиля RM48U".into()); }
|
||||
if !s.alive() || s.moving() || s.reloading > 0 || s.part(Part::Launcher) <= 0.0 {
|
||||
if !s.alive() || s.moving() || s.preparing.is_some() || s.reloading > 0 || s.part(Part::Launcher) <= 0.0 {
|
||||
return Err("Комплекс должен быть исправен и развёрнут".into());
|
||||
}
|
||||
if s.ground_rounds >= s.cells() { return Err("Нет свободной ячейки для RM48U".into()); }
|
||||
@@ -43,7 +43,7 @@ impl Sim {
|
||||
/// Return a loaded ground round to a nearby depot; restore a previously removed air round if available.
|
||||
pub fn unload_ground_round(&mut self, i: usize) -> Result<String, String> {
|
||||
let s = self.sams.get(i).ok_or("Нет такого комплекса")?;
|
||||
if s.kind != SamKind::S400 || !s.alive() || s.moving() || s.reloading > 0 || s.part(Part::Launcher) <= 0.0 || s.ground_rounds == 0 {
|
||||
if s.kind != SamKind::S400 || !s.alive() || s.moving() || s.preparing.is_some() || s.reloading > 0 || s.part(Part::Launcher) <= 0.0 || s.ground_rounds == 0 {
|
||||
return Err("Нужен исправный развёрнутый С-400 с загруженной RM48U".into());
|
||||
}
|
||||
let depot = self.sams.iter().enumerate().filter(|(_, d)| d.kind == SamKind::Depot && d.alive() && d.pos.distance(s.pos) < DEPOT_REACH)
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
use glam::{DQuat, DVec3};
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub mod battery;
|
||||
pub mod launch;
|
||||
pub mod catalog;
|
||||
mod generated_bounds;
|
||||
mod damage;
|
||||
@@ -1278,6 +1280,11 @@ pub const MAX_COAST: u64 = 240;
|
||||
/// Ticks the seeker keeps a target it lost sight of (it looks again round the last place).
|
||||
const REACQUIRE: u64 = 12;
|
||||
|
||||
impl Missile {
|
||||
/// Whether the main motor is running (cold ejection has no exhaust).
|
||||
pub fn engine_lit(&self) -> bool { self.age > self.kind.spec().cold }
|
||||
}
|
||||
|
||||
impl MissileKind {
|
||||
/// How far its own seeker holds a target of average size (blocks); 0: command guidance only.
|
||||
pub fn seeker(self) -> f64 {
|
||||
@@ -1355,6 +1362,12 @@ pub struct Sam {
|
||||
launched: u32,
|
||||
pub id: u32,
|
||||
pub kind: SamKind,
|
||||
pub mode: battery::SamMode,
|
||||
pub radar_role: battery::RadarRole,
|
||||
pub spent_cells: u8,
|
||||
preparing: Option<launch::Prepare>,
|
||||
cap_cell: u8,
|
||||
cap_open: f32,
|
||||
/// The vehicle's centre on the ground.
|
||||
pub pos: DVec3,
|
||||
/// Where its nose points (radians, as `look`'s yaw).
|
||||
@@ -1441,6 +1454,12 @@ impl Sam {
|
||||
Self {
|
||||
id,
|
||||
kind,
|
||||
mode: battery::SamMode::Integrated,
|
||||
radar_role: battery::RadarRole::Multifunction,
|
||||
spent_cells: 0,
|
||||
preparing: None,
|
||||
cap_cell: 0,
|
||||
cap_open: 0.0,
|
||||
pos,
|
||||
heading,
|
||||
yaw: heading,
|
||||
@@ -1544,6 +1563,9 @@ impl Sam {
|
||||
SamKind::Light => return format!("{text}{}", if self.target.is_some() { ": в луче цель" } else { "" }),
|
||||
_ => {}
|
||||
}
|
||||
if let Some(p) = self.preparing {
|
||||
text += &format!(" · открытие ТПК №{}", p.cell + 1);
|
||||
}
|
||||
if self.empty && self.missiles == 0 {
|
||||
text += " ЖДЁТ БК";
|
||||
}
|
||||
@@ -1829,6 +1851,8 @@ impl Sim {
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.strikes.pending.clear();
|
||||
for sam in &mut self.sams { sam.cancel_prepare(); }
|
||||
self.drones.clear();
|
||||
self.missiles.clear();
|
||||
self.fx.clear();
|
||||
@@ -1889,6 +1913,7 @@ impl Sim {
|
||||
self.step_ew(out);
|
||||
self.update_sight(ground);
|
||||
self.step_lights(ground, out);
|
||||
self.prepare_ground_strikes(out);
|
||||
self.step_sams(ground, out);
|
||||
self.step_missiles(ground, out);
|
||||
self.step_strikes(ground, player, out);
|
||||
@@ -2489,6 +2514,13 @@ impl Sim {
|
||||
// ------------------------------------------------ air defence
|
||||
|
||||
fn step_sams(&mut self, ground: &mut Ground, out: &mut Out) {
|
||||
for sam in &mut self.sams {
|
||||
sam.sync_tube_cells();
|
||||
if (self.hold && !sam.preparing.is_some_and(|p| p.ground)) || !sam.alive() || sam.moving() || sam.reloading > 0 || sam.part(Part::Launcher) <= 0.0 {
|
||||
sam.cancel_prepare();
|
||||
}
|
||||
}
|
||||
|
||||
if self.hold {
|
||||
for i in 0..self.sams.len() {
|
||||
if self.sams[i].alive() && !self.sams[i].moving() {
|
||||
@@ -2573,10 +2605,10 @@ impl Sim {
|
||||
let opts = why::Opts { last_chance: self.last_chance, far_shots: self.far_shots };
|
||||
let target = spec.missile.as_ref().map(|load| {
|
||||
// A launcher with nothing to fire (reloading, empty, failed) claims no target: others take it.
|
||||
if me.missiles == 0 || me.reloading > 0 || me.part(Part::Launcher) <= 0.0 {
|
||||
if me.missiles == 0 || me.reloading > 0 || me.part(Part::Launcher) <= 0.0 || me.preparing.is_some_and(|p| p.ground) {
|
||||
return None;
|
||||
}
|
||||
let radars: Vec<&Sam> = if me.kind.far_shooter() && opts.far_shots { self.sams.iter().filter(|s| s.kind == SamKind::Radar).collect() } else { Vec::new() };
|
||||
let radars: Vec<&Sam> = if me.mode == battery::SamMode::LauncherOnly || (me.kind.far_shooter() && opts.far_shots) { self.sams.iter().filter(|s| s.kind == SamKind::Radar).collect() } else { Vec::new() };
|
||||
let cued = |id: u32| !radars.is_empty() && why::radar_cue(me, &radars, id, now);
|
||||
let basis = |tr: &Track, k: f64, role_in_reach: bool| {
|
||||
drone(tr).filter(|_| open(tr.id)).and_then(|d| why::basis(me, load, tr, d, now, claimed(d), k, opts, cued(tr.id), role_in_reach))
|
||||
@@ -2641,6 +2673,10 @@ impl Sim {
|
||||
self.sams[i].gun_target = gun_target;
|
||||
}
|
||||
let sam = &mut self.sams[i];
|
||||
if sam.preparing.is_some_and(|p| !p.ground && Some(p.target) != sam.target) { sam.cancel_prepare(); }
|
||||
if let Some(p) = sam.preparing.filter(|p| !p.ground) {
|
||||
sam.cap_open = (now.saturating_sub(p.start) as f32 / launch::OPEN_TICKS as f32).clamp(0.0, 1.0);
|
||||
}
|
||||
// What the turret or launcher points at: guns first when a target is close.
|
||||
let aim = sam
|
||||
.gun_target
|
||||
@@ -2783,11 +2819,11 @@ impl Sim {
|
||||
}
|
||||
// Missiles.
|
||||
if let (Some(load), Some(mt)) = (&spec.missile, sam.target) {
|
||||
let ready = why::missile_blocks(sam) == 0;
|
||||
let ready = why::missile_blocks(sam) == 0 && !sam.preparing.is_some_and(|p| p.ground);
|
||||
// The missile turns after its boost, but the launcher must point near the target.
|
||||
let t = sam.track(mt).filter(|_| drone_at.contains_key(&mt)).map(|tr| tr.est(now));
|
||||
let aligned = why::missile_aligned(spec.family, sam.yaw, mount, t, aim_id, mt, aim_error);
|
||||
let n = load.magazine - sam.missiles;
|
||||
let n = if sam.animated_tubes() { sam.preparing.map_or_else(|| sam.next_tube_cell(), |p| p.cell) } else { load.magazine - sam.missiles };
|
||||
let (muzzle, dir) = sam.muzzle(n);
|
||||
// The land: a target behind a hill, or a path the hill closes, is not fired at; the system looks for another.
|
||||
let land = if ready && aligned {
|
||||
@@ -2816,9 +2852,11 @@ impl Sim {
|
||||
if land == terrain::WAIT {
|
||||
// The land under the shot is not known yet: it waits.
|
||||
} else if land != 0 {
|
||||
sam.cancel_prepare();
|
||||
retarget(&mut missile_on, Some(mt), None);
|
||||
sam.target = None;
|
||||
} else if ready && aligned {
|
||||
} else if ready && aligned && sam.prepare_tube(mt, now) {
|
||||
sam.eject_cell(n);
|
||||
// Past its normal reach: a long shot, and the next waits.
|
||||
if sam.kind.far_shooter() && t.is_some_and(|t| t.distance(sam.pos) >= load.max) {
|
||||
sam.far_cooldown = why::FAR_COOLDOWN;
|
||||
|
||||
@@ -23,6 +23,11 @@ pub struct SamKey {
|
||||
/// Hull and component health at the sampled tick.
|
||||
health: f32,
|
||||
parts: [f32; 6],
|
||||
mode: battery::SamMode,
|
||||
radar_role: battery::RadarRole,
|
||||
spent_cells: u8,
|
||||
cap_cell: u8,
|
||||
cap_open: f32,
|
||||
}
|
||||
|
||||
impl SamKey {
|
||||
@@ -38,6 +43,11 @@ impl SamKey {
|
||||
beam: if sam.beam_on { ((sam.beam_free / 8.0).round() * 8.0).max(8.0) as f32 } else { 0.0 },
|
||||
health: sam.health as f32,
|
||||
parts: sam.comps.0.map(|v| v as f32),
|
||||
mode: sam.mode,
|
||||
radar_role: sam.radar_role,
|
||||
spent_cells: sam.spent_cells,
|
||||
cap_cell: sam.cap_cell,
|
||||
cap_open: sam.cap_open,
|
||||
}
|
||||
}
|
||||
/// The same pose (to within what could be seen).
|
||||
@@ -51,6 +61,9 @@ impl SamKey {
|
||||
&& self.beam == o.beam
|
||||
&& self.health == o.health
|
||||
&& self.parts == o.parts
|
||||
&& self.mode == o.mode && self.radar_role == o.radar_role
|
||||
&& self.spent_cells == o.spent_cells && self.cap_cell == o.cap_cell
|
||||
&& near(self.cap_open, o.cap_open, 0.001)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,6 +106,11 @@ impl SamTrack {
|
||||
beam: a.beam,
|
||||
health: a.health,
|
||||
parts: a.parts,
|
||||
mode: a.mode,
|
||||
radar_role: a.radar_role,
|
||||
spent_cells: a.spent_cells,
|
||||
cap_cell: a.cap_cell,
|
||||
cap_open: if a.cap_cell == b.cap_cell && a.spent_cells == b.spent_cells { l(a.cap_open, b.cap_open) } else { a.cap_open },
|
||||
})
|
||||
}
|
||||
/// The system as it stood at `t`, as a ghost to draw with the ids from `id`.
|
||||
@@ -107,6 +125,11 @@ impl SamTrack {
|
||||
sam.beam_free = k.beam as f64;
|
||||
sam.health = if self.lost.is_some_and(|l| t >= l as f64) { 0.0 } else { k.health as f64 };
|
||||
sam.comps = Comps(k.parts.map(|v| v as f64));
|
||||
sam.mode = k.mode;
|
||||
sam.radar_role = k.radar_role;
|
||||
sam.spent_cells = k.spent_cells;
|
||||
sam.cap_cell = k.cap_cell;
|
||||
sam.cap_open = k.cap_open;
|
||||
Some(sam)
|
||||
}
|
||||
}
|
||||
@@ -757,7 +780,7 @@ impl Sim {
|
||||
|
||||
/// Open before the combat update so transitions on the first active tick are not lost.
|
||||
pub(super) fn begin_recording_if_active(&mut self) {
|
||||
if self.rec.is_none() && (self.drones.iter().any(|d| d.alive()) || !self.strikes.flights.is_empty()) {
|
||||
if self.rec.is_none() && (self.drones.iter().any(|d| d.alive()) || !self.strikes.flights.is_empty() || !self.strikes.pending.is_empty()) {
|
||||
self.recordings += 1;
|
||||
self.rec = Some(Recording::new(self.recordings, self));
|
||||
}
|
||||
@@ -775,7 +798,7 @@ impl Sim {
|
||||
pub(super) fn record(&mut self) {
|
||||
// Wrecks still falling (far off they fall a long while) don't keep it going. A counter-strike is a recorded
|
||||
// object in its own right (#109): it holds the recording open until it lands, whatever the sky is doing.
|
||||
let busy = self.drones.iter().any(|d| d.alive()) || !self.missiles.is_empty() || !self.queue.is_empty() || !self.strikes.flights.is_empty();
|
||||
let busy = self.drones.iter().any(|d| d.alive()) || !self.missiles.is_empty() || !self.queue.is_empty() || !self.strikes.flights.is_empty() || !self.strikes.pending.is_empty();
|
||||
self.begin_recording_if_active();
|
||||
let Some(r) = &mut self.rec else { return };
|
||||
r.note_tracks(&self.sams);
|
||||
@@ -1006,6 +1029,26 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replay_preserves_launcher_configuration_and_individual_cover_poses() {
|
||||
let mut r = recording();
|
||||
let mut sam = sam_at(0.0, 0.0, 0.0);
|
||||
sam.set_mode(battery::SamMode::LauncherOnly);
|
||||
sam.cap_cell = 2;
|
||||
sam.cap_open = 0.5;
|
||||
sam.spent_cells = 1;
|
||||
r.note_sams(&[sam], 10);
|
||||
for t in [10.0, 40.0, 10.0] {
|
||||
let ghost = r.sam_tracks[0].ghost(r.origin, t, 160).unwrap();
|
||||
assert_eq!(ghost.mode, battery::SamMode::LauncherOnly);
|
||||
assert_eq!((ghost.spent_cells, ghost.cap_cell, ghost.cap_open), (1, 2, 0.5));
|
||||
let mut out = Out::default();
|
||||
units::draw_sam_model(&ghost, &mut Rng::new(1), &mut out);
|
||||
assert_eq!(out.models.iter().filter(|m| m.name == "airdef:tpk_cap").count(), 4);
|
||||
assert!(!out.models.iter().any(|m| m.name == "airdef:radar_panel"));
|
||||
}
|
||||
}
|
||||
|
||||
fn physical_strike(record_len: Option<u32>) -> (Sim, u32, DVec3) {
|
||||
use crate::sim::sites::{LaunchSite, SiteClass};
|
||||
use crate::sim::strike::{Estimate, Known, SiteState};
|
||||
|
||||
+54
-20
@@ -36,6 +36,8 @@ const NEAR: f64 = 24.0;
|
||||
pub(super) struct Horizon {
|
||||
/// Per ray, per sample out: the steepest rise of the land so far (tan of its elevation).
|
||||
pub(super) rays: Vec<Vec<f32>>,
|
||||
antenna: DVec3,
|
||||
reach: f64,
|
||||
}
|
||||
|
||||
impl Horizon {
|
||||
@@ -58,9 +60,12 @@ impl Horizon {
|
||||
.collect()
|
||||
})
|
||||
.collect();
|
||||
Self { rays }
|
||||
Self { rays, antenna, reach }
|
||||
}
|
||||
|
||||
fn matches(&self, sam: &Sam) -> bool {
|
||||
self.antenna == antenna(sam) && self.reach == sensor_span(sam)
|
||||
}
|
||||
/// Whether the land between the antenna and `p` rises over the line to it.
|
||||
pub(super) fn hides(&self, antenna: DVec3, p: DVec3) -> bool {
|
||||
let rel = p - antenna;
|
||||
@@ -78,7 +83,11 @@ impl Horizon {
|
||||
|
||||
/// Where a unit's radar antenna is.
|
||||
pub(super) fn antenna(sam: &Sam) -> DVec3 {
|
||||
sam.pos + DVec3::Y * sam.kind.spec().mast
|
||||
sam.pos + DVec3::Y * sam.sensor_mast()
|
||||
}
|
||||
|
||||
fn sensor_span(sam: &Sam) -> f64 {
|
||||
sam.kind.spec().sensor * if sam.kind == SamKind::Radar { sam.radar_role.reach_factor() } else { 1.0 }
|
||||
}
|
||||
|
||||
/// How far a system's search radar reaches for this kind at `height` over the land:
|
||||
@@ -88,8 +97,8 @@ pub(super) fn reach(sam: &Sam, kind: DroneKind, height: f64) -> f64 {
|
||||
if spec.sensor <= 0.0 {
|
||||
return 0.0;
|
||||
}
|
||||
let radar = spec.sensor * sam.radar_fit() * kind.rcs().powf(0.25);
|
||||
let horizon = HORIZON * (spec.mast.sqrt() + height.max(0.0).sqrt());
|
||||
let radar = spec.sensor * sam.radar_fit() * if sam.kind == SamKind::Radar { sam.radar_role.reach_factor() } else { 1.0 } * kind.rcs().powf(0.25);
|
||||
let horizon = HORIZON * (sam.sensor_mast().sqrt() + height.max(0.0).sqrt());
|
||||
radar.min(horizon).max(NEAR)
|
||||
}
|
||||
|
||||
@@ -127,15 +136,17 @@ impl Sim {
|
||||
/// A radar for a unit of `kind` at `pos` as the map draws it; `id`: the unit it is (its parts and horizon), None for a ghost.
|
||||
pub fn low_sensor(&self, kind: SamKind, pos: DVec3, id: Option<u32>) -> Option<LowSensor> {
|
||||
let spec = kind.spec();
|
||||
if spec.sensor <= 0.0 {
|
||||
if spec.sensor <= 0.0 || id.and_then(|i| self.sams.iter().find(|s| s.id == i)).is_some_and(|s| s.mode == battery::SamMode::LauncherOnly) {
|
||||
return None;
|
||||
}
|
||||
let fit = id.and_then(|i| self.sams.iter().find(|s| s.id == i)).map_or(1.0, |s| s.radar_fit());
|
||||
let height = LOW_KIND.spec().clearance;
|
||||
let radar = spec.sensor * fit * LOW_KIND.rcs().powf(0.25);
|
||||
let reach = radar.min(HORIZON * (spec.mast.sqrt() + height.sqrt())).max(NEAR);
|
||||
let terrain = id.is_some_and(|i| self.horizons.contains_key(&i));
|
||||
Some(LowSensor { id: id.unwrap_or(u32::MAX), ant: pos + DVec3::Y * spec.mast, base: pos.y, reach, terrain })
|
||||
let mast = id.and_then(|i| self.sams.iter().find(|s| s.id == i)).map_or(spec.mast, |s| s.sensor_mast());
|
||||
let factor = id.and_then(|i| self.sams.iter().find(|s| s.id == i)).filter(|s| s.kind == SamKind::Radar).map_or(1.0, |s| s.radar_role.reach_factor());
|
||||
let radar = spec.sensor * fit * factor * LOW_KIND.rcs().powf(0.25);
|
||||
let reach = radar.min(HORIZON * (mast.sqrt() + height.sqrt())).max(NEAR);
|
||||
let terrain = id.and_then(|i| self.sams.iter().find(|s| s.id == i)).is_some_and(|s| self.horizons.get(&s.id).is_some_and(|h| h.matches(s)));
|
||||
Some(LowSensor { id: id.unwrap_or(u32::MAX), ant: pos + DVec3::Y * mast, base: pos.y, reach, terrain })
|
||||
}
|
||||
|
||||
/// The radars that are up now.
|
||||
@@ -297,13 +308,26 @@ impl Track {
|
||||
}
|
||||
|
||||
impl Sam {
|
||||
/// Fictional antenna height and scan characteristics for gameplay roles.
|
||||
pub(super) fn sensor_mast(&self) -> f64 {
|
||||
self.kind.spec().mast + if self.kind == SamKind::Radar && self.radar_role == battery::RadarRole::Height { 8.0 } else { 0.0 }
|
||||
}
|
||||
pub(super) fn scan_profile(&self) -> Scan {
|
||||
if self.kind != SamKind::Radar { return self.kind.scan(); }
|
||||
match self.radar_role {
|
||||
battery::RadarRole::Multifunction => self.kind.scan(),
|
||||
battery::RadarRole::Search => Scan { period: 170, agile: false },
|
||||
battery::RadarRole::Engagement => Scan { period: 70, agile: true },
|
||||
battery::RadarRole::Height => Scan { period: 140, agile: false },
|
||||
}
|
||||
}
|
||||
/// Ticks for its beam to go round (longer when the radar is damaged).
|
||||
pub(super) fn scan_ticks(&self) -> f64 {
|
||||
self.kind.scan().period as f64 / self.radar_fit().max(0.2)
|
||||
self.scan_profile().period as f64 / self.radar_fit().max(0.2)
|
||||
}
|
||||
/// Its own radar works: it has one, it is not failed and the unit is deployed.
|
||||
pub fn radar_up(&self) -> bool {
|
||||
self.alive() && !self.moving() && self.kind.spec().sensor > 0.0 && self.part(Part::Sensor) > 0.0
|
||||
self.mode != battery::SamMode::LauncherOnly && self.alive() && !self.moving() && self.kind.spec().sensor > 0.0 && self.part(Part::Sensor) > 0.0
|
||||
}
|
||||
pub fn track(&self, id: u32) -> Option<&Track> {
|
||||
self.tracks.iter().find(|t| t.id == id)
|
||||
@@ -339,18 +363,24 @@ impl Sim {
|
||||
/// into the tracks, old tracks out, and the network's tracks shared.
|
||||
pub(super) fn update_sight(&mut self, ground: &mut Ground) {
|
||||
let now = self.tick;
|
||||
// A role, mast or location change invalidates the cached terrain immediately.
|
||||
for sam in &self.sams {
|
||||
if sam.radar_up() && self.horizons.get(&sam.id).is_some_and(|h| !h.matches(sam)) {
|
||||
self.horizons.insert(sam.id, Horizon::build(antenna(sam), sensor_span(sam), ground, now));
|
||||
}
|
||||
}
|
||||
// One horizon every few ticks: the land answers come in over time.
|
||||
if !self.sams.is_empty() && now % 3 == 0 {
|
||||
let radars: Vec<usize> =
|
||||
(0..self.sams.len()).filter(|&i| self.sams[i].alive() && self.sams[i].kind.spec().sensor > 0.0).collect();
|
||||
(0..self.sams.len()).filter(|&i| self.sams[i].radar_up()).collect();
|
||||
if !radars.is_empty() {
|
||||
let i = radars[(now / 3) as usize % radars.len()];
|
||||
let sam = &self.sams[i];
|
||||
let h = Horizon::build(antenna(sam), sam.kind.spec().sensor, ground, now);
|
||||
let h = Horizon::build(antenna(sam), sensor_span(sam), ground, now);
|
||||
self.horizons.insert(sam.id, h);
|
||||
}
|
||||
}
|
||||
let alive: std::collections::HashSet<u32> = self.sams.iter().filter(|s| s.alive()).map(|s| s.id).collect();
|
||||
let alive: std::collections::HashSet<u32> = self.sams.iter().filter(|s| s.radar_up()).map(|s| s.id).collect();
|
||||
self.horizons.retain(|id, _| alive.contains(id));
|
||||
// Each drone's height over the land under it (or over the systems' ground, unknown).
|
||||
let heights: Vec<Option<f64>> =
|
||||
@@ -371,7 +401,7 @@ impl Sim {
|
||||
index.clear();
|
||||
index.extend(sam.tracks.iter().enumerate().map(|(k, t)| (t.id, k)));
|
||||
let spec = sam.kind.spec();
|
||||
let scan = sam.kind.scan();
|
||||
let scan = sam.scan_profile();
|
||||
let period = sam.scan_ticks();
|
||||
let w = TAU / period;
|
||||
let (b0, ttl) = (sam.beam, ((period * 3.0) as u32).max(60));
|
||||
@@ -385,7 +415,7 @@ impl Sim {
|
||||
}
|
||||
let rel = d.pos - at;
|
||||
let dist = rel.length();
|
||||
if dist > spec.sensor * 1.2 {
|
||||
if dist > spec.sensor * if sam.kind == SamKind::Radar { sam.radar_role.reach_factor().max(1.0) * 1.2 } else { 1.2 } {
|
||||
continue;
|
||||
}
|
||||
let locked = sam.target == Some(d.id) || sam.gun_target == Some(d.id);
|
||||
@@ -393,7 +423,11 @@ impl Sim {
|
||||
// The beam passes its bearing this tick (the angle wraps at 2π: the difference is taken round).
|
||||
let in_beam = swept(rel.x.atan2(-rel.z), b0, w);
|
||||
let revisit = scan.agile && held.is_some_and(|t| t.own_tick > 0 && now - t.own_tick >= (period / 3.0) as u64);
|
||||
if !(in_beam || locked || revisit) {
|
||||
// A search radar's network track cues this fictional tracking radar to look, but is
|
||||
// never itself a measurement: reach, terrain, probability and noise still apply.
|
||||
let cue = sam.kind == SamKind::Radar && sam.radar_role == battery::RadarRole::Engagement
|
||||
&& held.is_some_and(|t| t.own_tick == 0 && t.age(now) <= MISSILE_AGE);
|
||||
if !(in_beam || locked || revisit || cue) {
|
||||
continue;
|
||||
}
|
||||
let height = h.unwrap_or(d.pos.y - sam.pos.y);
|
||||
@@ -412,7 +446,7 @@ impl Sim {
|
||||
if dist > NEAR && dice(seed, now, key, 0) >= p {
|
||||
continue;
|
||||
}
|
||||
let sigma = if locked { 0.3 } else { 0.8 + dist * 0.004 };
|
||||
let sigma = if locked { 0.3 } else if sam.kind == SamKind::Radar && sam.radar_role == battery::RadarRole::Engagement { 0.45 + dist * 0.002 } else { 0.8 + dist * 0.004 };
|
||||
let noise = DVec3::new(dice(seed, now, key, 1) - 0.5, (dice(seed, now, key, 2) - 0.5) * 0.5, dice(seed, now, key, 3) - 0.5) * 2.0 * sigma;
|
||||
fresh.push(Track {
|
||||
id: d.id,
|
||||
@@ -435,12 +469,12 @@ impl Sim {
|
||||
let shared: Vec<(u32, DVec3, f64, Vec<Track>)> = self
|
||||
.sams
|
||||
.iter()
|
||||
.filter(|s| s.alive() && !s.moving() && s.link_range() > 0.0)
|
||||
.filter(|s| s.radar_up() && s.link_range() > 0.0)
|
||||
.map(|s| (s.id, s.pos, s.link_range(), s.tracks.iter().filter(|t| t.own_tick == t.tick).copied().collect::<Vec<_>>()))
|
||||
.filter(|s| !s.3.is_empty())
|
||||
.collect();
|
||||
for sam in &mut self.sams {
|
||||
if sam.alive() && !sam.moving() && sam.kind.armed() && sam.link_range() > 0.0 && !shared.is_empty() {
|
||||
if sam.alive() && !sam.moving() && (sam.kind.armed() || (sam.kind == SamKind::Radar && sam.radar_role == battery::RadarRole::Engagement)) && sam.link_range() > 0.0 && !shared.is_empty() {
|
||||
index.clear();
|
||||
index.extend(sam.tracks.iter().enumerate().map(|(k, t)| (t.id, k)));
|
||||
let mine = sam.link_range();
|
||||
|
||||
+124
-27
@@ -308,6 +308,13 @@ impl Flight {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(super) struct PendingStrike {
|
||||
carrier: u32,
|
||||
site: u32,
|
||||
aim: DVec2,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Strikes {
|
||||
/// Strike missiles left, and ticks towards the next.
|
||||
@@ -315,6 +322,7 @@ pub struct Strikes {
|
||||
pub progress: u32,
|
||||
pub known: Vec<Known>,
|
||||
pub flights: Vec<Flight>,
|
||||
pub(super) pending: Vec<PendingStrike>,
|
||||
flown: Vec<Flown>,
|
||||
next: u32,
|
||||
/// Smoking wrecks: (where, until).
|
||||
@@ -581,7 +589,10 @@ impl Sim {
|
||||
let from = f.pos;
|
||||
// Time spent waiting for world data must not accumulate into a faster step.
|
||||
let delta = planned - from;
|
||||
let want = from + delta.clamp_length_max(TUNE.speed);
|
||||
let speed = if f.carrier.is_some() && age >= EJECT_TICKS {
|
||||
TUNE.speed * (0.2 + 0.8 * ((age - EJECT_TICKS) as f64 / 20.0).clamp(0.0, 1.0))
|
||||
} else { TUNE.speed };
|
||||
let want = from + delta.clamp_length_max(speed);
|
||||
// A missile that never reaches its point (the world never tells of the way) ends somewhere: it does
|
||||
// not stay in the air for ever.
|
||||
if now >= f.arrive + 600 {
|
||||
@@ -671,6 +682,7 @@ impl Sim {
|
||||
/// while these exist, and let go after.
|
||||
pub(super) fn site_refs(&mut self) -> Vec<u32> {
|
||||
let mut v: Vec<u32> = self.strikes.flights.iter().map(|f| f.site).collect();
|
||||
v.extend(self.strikes.pending.iter().map(|p| p.site));
|
||||
let alive: Vec<u32> = self.drones.iter().filter(|d| d.alive()).map(|d| d.id).collect();
|
||||
v.extend(self.strikes.scouted(&alive));
|
||||
v.extend(self.sites.flying_sites(&alive));
|
||||
@@ -679,42 +691,77 @@ impl Sim {
|
||||
v
|
||||
}
|
||||
|
||||
/// Atomic manual launch from an already prepared, compatible carrier.
|
||||
pub fn order_strike(&mut self, id: u32, _from: DVec3, out: &mut Out) -> Result<String, String> {
|
||||
/// Reserve a physical carrier and its cell; ammunition is spent only on ejection.
|
||||
pub fn order_strike(&mut self, id: u32, _from: DVec3, _out: &mut Out) -> Result<String, String> {
|
||||
let k = self.strikes.get(id).ok_or_else(|| format!("ПУ №{id} не найдена"))?;
|
||||
let est = k.est.filter(|e| e.fix).ok_or("Место ПУ неизвестно: нужна разведка")?;
|
||||
if matches!(k.state, SiteState::Destroyed(_)) { return Err("ПУ уже уничтожена".into()); }
|
||||
if self.strikes.flights.iter().any(|f| f.site == id) { return Err("По этой ПУ ракета уже в полёте".into()); }
|
||||
let site = self.sites.get(id).ok_or("ПУ уже нет на карте")?;
|
||||
let snap = Target { pos: DVec2::new(site.pos.x, site.pos.z), stock: site.stock, offmap: site.offmap, alive: site.alive };
|
||||
let (i, dist) = self.sams.iter().enumerate().filter_map(|(i,s)| {
|
||||
if self.strikes.flights.iter().any(|f| f.site == id) || self.strikes.pending.iter().any(|p| p.site == id) {
|
||||
return Err("По этой ПУ ракета уже готовится или в полёте".into());
|
||||
}
|
||||
if !self.sites.get(id).is_some_and(|s| s.alive) { return Err("ПУ уже нет на карте".into()); }
|
||||
let (i, _) = self.sams.iter().enumerate().filter_map(|(i,s)| {
|
||||
let dist = (est.pos - DVec2::new(s.pos.x, s.pos.z)).length();
|
||||
(matches!(catalog::unit(s.kind).ground_strike(), catalog::GroundStrike::Rm48uGame)
|
||||
&& s.alive() && !s.moving() && s.reloading == 0 && s.cooldown == 0
|
||||
&& s.part(Part::Launcher) > 0.0 && s.ground_rounds > 0 && dist <= TUNE.range)
|
||||
&& s.part(Part::Launcher) > 0.0 && s.ground_rounds > 0 && s.preparing.is_none() && dist <= TUNE.range)
|
||||
.then_some((i, dist))
|
||||
}).min_by(|(i,a), (j,b)| self.sams[*j].ground_rounds.cmp(&self.sams[*i].ground_rounds)
|
||||
.then(a.total_cmp(b)).then(self.sams[*i].id.cmp(&self.sams[*j].id)))
|
||||
.ok_or("Нет готового С-400 с загруженной RM48U в игровой дальности; подготовьте боезапас у склада")?;
|
||||
let sam = &mut self.sams[i];
|
||||
let carrier = sam.id;
|
||||
let (start, _) = sam.muzzle(4 - sam.ground_rounds);
|
||||
sam.ground_rounds -= 1;
|
||||
sam.cooldown = 20;
|
||||
sam.launched = 12;
|
||||
sam.fired += 1;
|
||||
self.intel.revealed.push((carrier, sam.pos, self.tick));
|
||||
self.strikes.fired += 1;
|
||||
self.strikes.next += 1;
|
||||
let ticks = (dist / TUNE.speed).ceil().max(20.0) as u64;
|
||||
let (gx, gz) = (gauss(self.seed, self.strikes.next as u64, id as u64, 7), gauss(self.seed, self.strikes.next as u64, id as u64, 8));
|
||||
self.strikes.flights.push(Flight { id: self.strikes.next, site: id, carrier: Some(carrier), from: start, pos: start, aim: est.pos,
|
||||
land: est.pos + DVec2::new(gx, gz) * TUNE.cep, start: self.tick, arrive: self.tick + ticks, snap, hold: 0 });
|
||||
if let Some(k) = self.strikes.known.iter_mut().find(|k| k.site == id) { k.aimed = true; }
|
||||
self.record_strike_launch(self.strikes.next, carrier, start);
|
||||
out.sound("airdef:launch.s400_eject", start, 1.0, 1.0);
|
||||
out.particles("minecraft:cloud", start, 0.6, DVec3::Y * 0.05, 0.05, 30);
|
||||
Ok(format!("С-400 №{} запустил RM48U по ПУ №{id}; осталось ударных: {}", i + 1, self.sams[i].ground_rounds))
|
||||
sam.sync_tube_cells();
|
||||
let cell = sam.next_tube_cell();
|
||||
sam.preparing = Some(launch::Prepare { target: id, cell, start: self.tick, ground: true });
|
||||
sam.cap_cell = cell as u8;
|
||||
sam.cap_open = 0.0;
|
||||
self.strikes.pending.push(PendingStrike { carrier: sam.id, site: id, aim: est.pos });
|
||||
Ok(format!("С-400 №{} готовит RM48U по ПУ №{id}", i + 1))
|
||||
}
|
||||
|
||||
pub(super) fn prepare_ground_strikes(&mut self, out: &mut Out) {
|
||||
let pending = std::mem::take(&mut self.strikes.pending);
|
||||
for p in pending {
|
||||
let Some(i) = self.sams.iter().position(|s| s.id == p.carrier) else { continue };
|
||||
let valid_site = self.sites.get(p.site).is_some_and(|s| s.alive)
|
||||
&& self.strikes.get(p.site).is_some_and(|k| !matches!(k.state, SiteState::Destroyed(_)));
|
||||
let sam = &self.sams[i];
|
||||
let valid = valid_site && sam.preparing.is_some_and(|q| q.ground && q.target == p.site)
|
||||
&& sam.alive() && !sam.moving() && sam.reloading == 0 && sam.cooldown == 0
|
||||
&& sam.part(Part::Launcher) > 0.0 && sam.ground_rounds > 0;
|
||||
if !valid {
|
||||
out.messages.push(format!("Подготовка RM48U по ПУ №{} отменена: изменилось состояние носителя или цели", p.site));
|
||||
if self.sams[i].preparing.is_some_and(|q| q.ground && q.target == p.site) { self.sams[i].cancel_prepare(); }
|
||||
continue;
|
||||
}
|
||||
let prep = self.sams[i].preparing.unwrap();
|
||||
self.sams[i].cap_open = (self.tick.saturating_sub(prep.start) as f32 / launch::OPEN_TICKS as f32).clamp(0.0, 1.0);
|
||||
if self.tick.saturating_sub(prep.start) < launch::OPEN_TICKS {
|
||||
self.strikes.pending.push(p);
|
||||
continue;
|
||||
}
|
||||
let site = self.sites.get(p.site).unwrap();
|
||||
let snap = Target { pos: DVec2::new(site.pos.x, site.pos.z), stock: site.stock, offmap: site.offmap, alive: site.alive };
|
||||
let sam = &mut self.sams[i];
|
||||
let (start, _) = sam.muzzle(prep.cell);
|
||||
sam.eject_cell(prep.cell);
|
||||
sam.ground_rounds -= 1;
|
||||
sam.cooldown = 20;
|
||||
sam.launched = 12;
|
||||
sam.fired += 1;
|
||||
let dist = (p.aim - DVec2::new(sam.pos.x, sam.pos.z)).length();
|
||||
self.intel.revealed.push((p.carrier, sam.pos, self.tick));
|
||||
self.strikes.fired += 1;
|
||||
self.strikes.next += 1;
|
||||
let ticks = (dist / TUNE.speed).ceil().max(20.0) as u64;
|
||||
let (gx, gz) = (gauss(self.seed, self.strikes.next as u64, p.site as u64, 7), gauss(self.seed, self.strikes.next as u64, p.site as u64, 8));
|
||||
self.strikes.flights.push(Flight { id: self.strikes.next, site: p.site, carrier: Some(p.carrier), from: start, pos: start, aim: p.aim,
|
||||
land: p.aim + DVec2::new(gx, gz) * TUNE.cep, start: self.tick, arrive: self.tick + ticks, snap, hold: 0 });
|
||||
if let Some(k) = self.strikes.known.iter_mut().find(|k| k.site == p.site) { k.aimed = true; }
|
||||
self.record_strike_launch(self.strikes.next, p.carrier, start);
|
||||
out.sound("airdef:launch.s400_eject", start, 1.0, 1.0);
|
||||
out.particles("minecraft:cloud", start, 0.6, DVec3::Y * 0.05, 0.05, 30);
|
||||
}
|
||||
}
|
||||
|
||||
/// Synthetic launcher used only by flight/impact regression fixtures; absent from production builds.
|
||||
@@ -1243,9 +1290,13 @@ mod tests {
|
||||
sim.load_ground_round(a).unwrap();
|
||||
sim.sams[a].cooldown = 0;
|
||||
sim.sams[b].cooldown = 0;
|
||||
let muzzle = sim.sams[b].muzzle(2).0;
|
||||
let cell = sim.sams[b].next_tube_cell();
|
||||
let muzzle = sim.sams[b].muzzle(cell).0;
|
||||
let old_stock = sim.strikes.stock;
|
||||
sim.order_strike(site, DVec3::new(99999.0, 0.0, 99999.0), &mut out).unwrap();
|
||||
assert_eq!((sim.sams[a].ground_rounds, sim.sams[b].ground_rounds, sim.strikes.stock), (1, 2, old_stock));
|
||||
assert!(sim.order_strike(site, DVec3::ZERO, &mut out).is_err());
|
||||
run(&mut sim, &mut ground, &mut out, launch::OPEN_TICKS as u32 + 1);
|
||||
assert_eq!((sim.sams[a].ground_rounds, sim.sams[b].ground_rounds, sim.strikes.stock), (1, 1, old_stock));
|
||||
let f = sim.strikes.flights.last().unwrap();
|
||||
assert_eq!((f.carrier, f.from), (Some(b_id), muzzle));
|
||||
@@ -1256,6 +1307,52 @@ mod tests {
|
||||
assert!(sim.sams[b].missiles + sim.sams[b].ground_rounds <= 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ground_preparation_cancels_without_spending_when_carrier_or_site_changes() {
|
||||
for reason in 0..6 {
|
||||
let mut sim = Sim::new(11);
|
||||
let site = fixed_site(&mut sim, DVec3::new(0.0, 64.0, 300.0), 0);
|
||||
let mut sam = Sam::new(123, SamKind::S400, DVec3::ZERO, 0.0);
|
||||
sam.missiles = 2; sam.ground_rounds = 2; sam.cooldown = 0;
|
||||
sim.sams.push(sam);
|
||||
let mut out = Out::default();
|
||||
sim.order_strike(site, DVec3::ZERO, &mut out).unwrap();
|
||||
match reason {
|
||||
0 => sim.sams[0].health = 0.0,
|
||||
1 => sim.sams[0].state = SamState::Moving,
|
||||
2 => sim.sams[0].reloading = 5,
|
||||
3 => sim.sams[0].cooldown = 5,
|
||||
4 => sim.sams[0].ground_rounds = 0,
|
||||
_ => sim.sites.list.iter_mut().find(|s| s.id == site).unwrap().alive = false,
|
||||
}
|
||||
sim.tick += launch::OPEN_TICKS;
|
||||
sim.prepare_ground_strikes(&mut out);
|
||||
assert!(sim.strikes.pending.is_empty() && sim.strikes.flights.is_empty());
|
||||
assert_eq!(sim.strikes.fired, 0);
|
||||
assert_eq!(sim.sams[0].ground_rounds, if reason == 4 { 0 } else { 2 });
|
||||
assert!(sim.sams[0].preparing.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ground_orders_reserve_the_carrier_and_target_until_ejection() {
|
||||
let mut sim = Sim::new(11);
|
||||
let a = fixed_site(&mut sim, DVec3::new(0.0, 64.0, 300.0), 0);
|
||||
let b = fixed_site(&mut sim, DVec3::new(0.0, 64.0, 400.0), 0);
|
||||
let mut sam = Sam::new(123, SamKind::S400, DVec3::ZERO, 0.0);
|
||||
sam.missiles = 2; sam.ground_rounds = 2; sam.cooldown = 0;
|
||||
sim.sams.push(sam);
|
||||
let mut out = Out::default();
|
||||
sim.order_strike(a, DVec3::ZERO, &mut out).unwrap();
|
||||
assert!(sim.order_strike(a, DVec3::ZERO, &mut out).is_err());
|
||||
assert!(sim.order_strike(b, DVec3::ZERO, &mut out).is_err());
|
||||
assert_eq!(sim.sams[0].ground_rounds, 2);
|
||||
sim.tick += launch::OPEN_TICKS;
|
||||
sim.prepare_ground_strikes(&mut out);
|
||||
assert_eq!((sim.sams[0].ground_rounds, sim.strikes.fired, sim.strikes.flights.len()), (1, 1, 1));
|
||||
assert!(sim.strikes.pending.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fully_loaded_ground_cells_do_not_consume_air_reload_cargo() {
|
||||
let mut sim = Sim::new(1);
|
||||
|
||||
@@ -279,7 +279,7 @@ impl SamKind {
|
||||
const S: SamSpec = SamSpec {
|
||||
name: "С-500",
|
||||
key: "s500",
|
||||
missile: Some(MissileLoad { kind: MissileKind::M77n6, magazine: 4, min: 40.0, max: 1050.0, interval: 20 }),
|
||||
missile: Some(MissileLoad { kind: MissileKind::M77n6, magazine: 2, min: 40.0, max: 1050.0, interval: 20 }),
|
||||
gun: None,
|
||||
reload: 520,
|
||||
scale: 1.6,
|
||||
@@ -681,6 +681,7 @@ pub(super) fn draw_sam_model(sam: &Sam, rng: &mut Rng, out: &mut Out) {
|
||||
}
|
||||
Family::Static | Family::Truck => {}
|
||||
}
|
||||
if sam.mode != battery::SamMode::LauncherOnly {
|
||||
if let Some((model, h, f, tilt)) = parts.dish {
|
||||
part(out, model, mount + turret * DVec3::new(0.0, h * s, f * s), DQuat::from_rotation_y(sam.dish) * DQuat::from_rotation_x(tilt), s);
|
||||
}
|
||||
@@ -688,9 +689,19 @@ pub(super) fn draw_sam_model(sam: &Sam, rng: &mut Rng, out: &mut Out) {
|
||||
part(out, model, sam.pos + up + DVec3::Y * h * s + base * DVec3::new(0.0, 0.0, back * s), base, s);
|
||||
}
|
||||
if let Some((model, h, back, size, tilt)) = parts.spinner {
|
||||
let at = sam.pos + up + DVec3::Y * h * s + base * DVec3::new(0.0, 0.0, back * s);
|
||||
part(out, model, at, DQuat::from_rotation_y(sam.dish * 0.6) * DQuat::from_rotation_x(tilt), s * size);
|
||||
let extra = if sam.kind == SamKind::Radar && sam.radar_role == battery::RadarRole::Height { 8.0 } else { 0.0 };
|
||||
let foot = sam.pos + up + DVec3::Y * h * s + base * DVec3::new(0.0, 0.0, back * s);
|
||||
for segment in 0..(extra as u32 / 2) {
|
||||
part(out, "airdef:radar_mast", foot + DVec3::Y * (segment as f64 * 2.0 + 0.5), base, 1.0);
|
||||
}
|
||||
let at = foot + DVec3::Y * extra;
|
||||
let heading = if sam.kind == SamKind::Radar && sam.radar_role == battery::RadarRole::Engagement {
|
||||
sam.tracks.first().map_or(sam.dish * 0.6, |t| heading_of(t.pos - sam.pos))
|
||||
} else { sam.dish * 0.6 };
|
||||
part(out, model, at, DQuat::from_rotation_y(heading) * DQuat::from_rotation_x(tilt), s * size);
|
||||
}
|
||||
}
|
||||
launch::draw_caps(sam, out);
|
||||
if sam.flash > 0 {
|
||||
let barrels: &[DVec3] = match sam.kind {
|
||||
SamKind::Pantsir => &[DVec3::new(-0.95, 0.33, -1.7), DVec3::new(0.95, 0.33, -1.7)],
|
||||
|
||||
@@ -92,11 +92,14 @@ impl SamKind {
|
||||
/// A live big radar in data-link reach of `sam` that measured `id` recently.
|
||||
pub(super) fn radar_cue(sam: &Sam, radars: &[&Sam], id: u32, now: u64) -> bool {
|
||||
radars.iter().any(|r| {
|
||||
r.alive()
|
||||
r.radar_role.authorizes()
|
||||
&& r.link_range() > 0.0
|
||||
&& sam.link_range() > 0.0
|
||||
&& r.alive()
|
||||
&& !r.moving()
|
||||
&& r.radar_up()
|
||||
&& r.pos.distance(sam.pos) <= r.link_range().min(sam.link_range())
|
||||
&& r.track(id).is_some_and(|t| t.own_tick > 0 && t.age(now) <= FAR_RADAR_AGE)
|
||||
&& r.track(id).is_some_and(|t| t.own_tick > 0 && now.saturating_sub(t.own_tick) <= if sam.mode == battery::SamMode::LauncherOnly { sight::MISSILE_AGE } else { FAR_RADAR_AGE })
|
||||
})
|
||||
}
|
||||
|
||||
@@ -134,6 +137,7 @@ pub(super) struct Opts {
|
||||
/// `role_in_reach` that the system has a target of its roles in range anyway (no long shots then).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(super) fn basis(sam: &Sam, load: &units::MissileLoad, track: &Track, d: &Drone, now: u64, claimed: u32, k: f64, opts: Opts, cued: bool, role_in_reach: bool) -> Option<Basis> {
|
||||
if sam.mode == battery::SamMode::LauncherOnly && !cued { return None; }
|
||||
let blocks = target_blocks(sam.roles, Some(track), now, d);
|
||||
if blocks & !ROLE != 0 {
|
||||
return None;
|
||||
@@ -390,12 +394,16 @@ impl Sim {
|
||||
_ => "не обнаружена ни своей РЛС, ни сетью".to_string(),
|
||||
};
|
||||
v.push(check(blocks & (UNSEEN | STALE) == 0, detect));
|
||||
if sam.kind.spec().sensor > 0.0 {
|
||||
if sam.mode == battery::SamMode::LauncherOnly {
|
||||
let radars: Vec<&Sam> = self.sams.iter().filter(|s| s.kind == SamKind::Radar).collect();
|
||||
let connected = radar_cue(sam, &radars, t.id, now);
|
||||
v.push(check(connected, if connected { "внешняя РЛС: свежий трек по связи" } else { "внешняя РЛС: нет свежего трека или связи" }.to_string()));
|
||||
} else if sam.kind.spec().sensor > 0.0 {
|
||||
let period = sam.scan_ticks();
|
||||
v.push(check(
|
||||
sam.radar_up(),
|
||||
if sam.radar_up() {
|
||||
format!("обзор: оборот {:.1} с{}, луч на {:.0}°", period / 20.0, if sam.kind.scan().agile { ", электронный, цели в треке чаще" } else { "" }, sam.beam.to_degrees())
|
||||
format!("обзор: оборот {:.1} с{}, луч на {:.0}°", period / 20.0, if sam.scan_profile().agile { ", электронный, цели в треке чаще" } else { "" }, sam.beam.to_degrees())
|
||||
} else {
|
||||
"обзор: РЛС не работает".to_string()
|
||||
},
|
||||
@@ -427,7 +435,7 @@ impl Sim {
|
||||
let reach = reach_blocks(load.min, load.max, 1.0, sam.pos, est);
|
||||
// Beyond the normal reach, a heavy system may fire on a big radar's cue.
|
||||
let radars: Vec<&Sam> = self.sams.iter().filter(|s| s.kind == SamKind::Radar).collect();
|
||||
let cued = sam.kind.far_shooter() && radar_cue(sam, &radars, t.id, now);
|
||||
let cued = radar_cue(sam, &radars, t.id, now);
|
||||
let role_in_reach = self.drones.iter().any(|o| {
|
||||
sam.track(o.id).is_some_and(|tr| allowed(sam.roles, tr, now, o) && in_reach(load.min, load.max, 1.0, sam.pos, tr.est(now)) && sam.doctrine.area_blocks(sam.pos, tr, now) | sam.doctrine.reserve_blocks(sam.missiles, o) == 0 && claimed(o, &missile_on, sam.target) < sam.doctrine.needed(o))
|
||||
});
|
||||
@@ -483,7 +491,7 @@ impl Sim {
|
||||
));
|
||||
}
|
||||
let doc_ok = chance || area | held_back == 0;
|
||||
let grounds = blocks & !ROLE == 0 && (blocks & ROLE == 0 || chance) && doc_ok && (reach == 0 || far_ok);
|
||||
let grounds = (sam.mode != battery::SamMode::LauncherOnly || cued) && blocks & !ROLE == 0 && (blocks & ROLE == 0 || chance) && doc_ok && (reach == 0 || far_ok);
|
||||
fires |= grounds && free < need && mb == 0 && aligned && land == 0;
|
||||
}
|
||||
if let Some(gun) = &spec.gun {
|
||||
@@ -516,6 +524,11 @@ impl Sim {
|
||||
v.push(check(gb == 0, if gb == 0 { "орудие готово".to_string() } else { format!("орудие не стреляет: {}", why.join(", ")) }));
|
||||
fires |= gb == 0 && cand;
|
||||
}
|
||||
if let Some(p) = sam.preparing {
|
||||
let opening = p.ground || now.saturating_sub(p.start) < launch::OPEN_TICKS;
|
||||
v.push(check(!opening, format!("подготовка пуска: открывается крышка ТПК №{}", p.cell + 1)));
|
||||
fires &= !opening;
|
||||
}
|
||||
v.push(check(fires, if fires { "итог: готова стрелять" } else { "итог: стрелять сейчас не может" }));
|
||||
v
|
||||
}
|
||||
@@ -686,7 +699,8 @@ mod tests {
|
||||
let (mut sim, mut ground, land, mut out) = far_setup(true, max * 1.4);
|
||||
let far = sim.drones[0].id;
|
||||
sim.add_drone(DroneKind::Kh101, DVec3::new(300.0, 120.0, 0.0), DVec3::NEG_X, DVec3::new(0.0, 70.0, 0.0));
|
||||
for _ in 0..40 {
|
||||
// The launcher must finish opening its TPK after the initial readiness cooldown.
|
||||
for _ in 0..40 + launch::OPEN_TICKS {
|
||||
radar_sees(&mut sim);
|
||||
serve(&mut ground, &land, sim.tick);
|
||||
sim.step(&mut ground, None, &mut out);
|
||||
@@ -695,6 +709,53 @@ mod tests {
|
||||
assert!(sim.sams[0].fired > 0, "the closer target was not fired at");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cold_launch_opens_before_spending_and_lights_only_after_ejection() {
|
||||
let (mut sim, mut ground, land, mut out) = setup(SamKind::S400);
|
||||
sim.last_chance = false;
|
||||
sim.sams[0].cooldown = 0;
|
||||
sim.add_drone(DroneKind::Kh101, DVec3::new(0.0, 160.0, -300.0), DVec3::Z, DVec3::ZERO);
|
||||
let initial = sim.sams[0].missiles;
|
||||
for _ in 0..200 {
|
||||
serve(&mut ground, &land, sim.tick);
|
||||
sim.step(&mut ground, None, &mut out);
|
||||
if sim.sams[0].preparing.is_some() { break; }
|
||||
}
|
||||
assert!(sim.sams[0].preparing.is_some(), "a real tracked target must open a TPK");
|
||||
assert_eq!(sim.sams[0].missiles, initial);
|
||||
assert!(sim.missiles.is_empty());
|
||||
// Loss during opening cancels without creating or consuming a missile.
|
||||
sim.sams[0].health = 0.0;
|
||||
sim.step(&mut ground, None, &mut out);
|
||||
assert!(sim.sams[0].preparing.is_none() && sim.missiles.is_empty());
|
||||
assert_eq!(sim.sams[0].missiles, initial);
|
||||
sim.sams[0].health = 1.0;
|
||||
sim.sams[0].cooldown = 0;
|
||||
for _ in 0..200 {
|
||||
out = Out::default();
|
||||
serve(&mut ground, &land, sim.tick);
|
||||
sim.step(&mut ground, None, &mut out);
|
||||
if !sim.missiles.is_empty() { break; }
|
||||
}
|
||||
let m = sim.missiles.first().expect("ejection after reopening");
|
||||
let (id, kind) = (m.id, m.kind);
|
||||
assert_eq!(sim.sams[0].missiles, initial - 1);
|
||||
assert_eq!(sim.sams[0].spent_cells.count_ones(), 1);
|
||||
assert!(!m.engine_lit());
|
||||
assert!(!out.engines.iter().any(|e| e.key == 0x4000_0000 | id));
|
||||
let mut last_speed = m.vel.length();
|
||||
for _ in 0..kind.lit_after() + 3 {
|
||||
out = Out::default();
|
||||
serve(&mut ground, &land, sim.tick);
|
||||
sim.step(&mut ground, None, &mut out);
|
||||
let m = sim.missiles.iter().find(|m| m.id == id).expect("cold launch remains in flight");
|
||||
let voice = out.engines.iter().any(|e| e.key == 0x4000_0000 | id);
|
||||
assert_eq!(voice, m.engine_lit());
|
||||
if m.engine_lit() { assert!(m.vel.length() > last_speed, "the lit motor must accelerate"); }
|
||||
last_speed = m.vel.length();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_kill_chance_falls_to_half_at_the_far_end() {
|
||||
assert_eq!(reach_fit(100.0, 80.0), 1.0);
|
||||
|
||||
@@ -228,11 +228,16 @@ fn recorded_trails(rec: &crate::sim::record::Recording, t: f64) -> Vec<SmokeSegm
|
||||
|| a.distance(b) >= 400.0
|
||||
|| (matches!(tr.what, What::StrikeCarrier(_))
|
||||
&& born < tr.first as u64 + crate::sim::strike::EJECT_TICKS)
|
||||
|| (matches!(tr.what, What::Missile(kind, _) if !rec.missile_is_lit(tr, born as f64, kind.lit_after())))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if matches!(tr.what, What::StrikeCarrier(_)) {
|
||||
let ignition = tr.first as f64 + crate::sim::strike::EJECT_TICKS as f64;
|
||||
let ignition = match tr.what {
|
||||
What::StrikeCarrier(_) => Some(tr.first as f64 + crate::sim::strike::EJECT_TICKS as f64),
|
||||
What::Missile(kind, _) => Some(rec.missile_ignition(tr.id).map_or(tr.first as f64 + kind.lit_after() as f64 + 1.0, |e| e.at as f64)),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(ignition) = ignition {
|
||||
let segment_start = tr.first as f64 + (g - 1) as f64 * stride;
|
||||
if segment_start < ignition { a = tr.at(rec.origin, ignition).unwrap_or(b); }
|
||||
}
|
||||
@@ -323,7 +328,8 @@ impl Smoke {
|
||||
drone_look(spec.class, spec.trail_scale()),
|
||||
);
|
||||
}
|
||||
MoverKind::Missile(kind) => {
|
||||
MoverKind::Missile { kind, engine_lit } => {
|
||||
if !engine_lit { continue; }
|
||||
if let Some(look) = missile_look(kind) {
|
||||
self.trail((mover.group, mover.id), mover.pos, tick, look);
|
||||
}
|
||||
|
||||
@@ -31,6 +31,25 @@ expect.feedback = "С-400 №2: загружена RM48U (игровой про
|
||||
command = "airdef ground-unload 2"
|
||||
expect.feedback = "С-400 №2: выгружена RM48U; ударных 0, ПВО 4"
|
||||
|
||||
# Split battery configuration crosses the same saved command boundary.
|
||||
[[step]]
|
||||
command = "airdef battery 2 launcher"
|
||||
expect.feedback = "№2 С-400: режим launcher"
|
||||
|
||||
[[step]]
|
||||
command = "airdef deploy radar_track"
|
||||
|
||||
[[step]]
|
||||
command = "airdef radar 4 search"
|
||||
expect.feedback = "№4 РЛС: роль search"
|
||||
|
||||
[[step]]
|
||||
command = "airdef radar 4 engagement"
|
||||
expect.feedback = "№4 РЛС: роль engagement"
|
||||
|
||||
[[step]]
|
||||
command = "airdef deploy s500_launcher"
|
||||
|
||||
[[step]]
|
||||
command = "airdef wave mix 40 1500"
|
||||
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
# Раздельная батарея и холодный пуск
|
||||
|
||||
## Из исследования в игру
|
||||
|
||||
Вместо «всё на одной машине» добавлена конфигурация с отдельными пусковыми и общими РЛС.
|
||||
Высокоуровневое разделение обзорной и огневой РЛС соответствует публичным описаниям системы;
|
||||
это не полная модель конкретной штатной батареи.
|
||||
|
||||
| Публично описываемый элемент С-400 | Его смысл | Игровая адаптация |
|
||||
|---|---|---|
|
||||
| 91Н6: обзор/обнаружение | Общая воздушная картина | `radar_search`: больший игровой обзор, передача наблюдений |
|
||||
| 92Н6: сопровождение/управление огнём | Отдельный радар огневого комплекса | `radar_track`: более частые собственные измерения, поддержка нескольких ПУ |
|
||||
| Дополнительная 96Л6 и антенные мачты | Дополнительные средства обнаружения | `radar_height`: условная мачтовая роль, улучшенный игровой радиогоризонт; это не копия 96Л6 |
|
||||
| Командный пункт 55К6 | Управление и обмен данными | Пока абстрагирован существующей сетью и распределением целей, отдельной машины КП нет |
|
||||
| Пусковая с ТПК | Боекомплект и физический пуск | `s400_launcher` / `s500_launcher`, без навесной РЛС |
|
||||
|
||||
Источники для архитектуры: оригинальная техническая публикация
|
||||
[Bundesheer, «Radars für Raketenabwehr»](https://www.bmlv.gv.at/pdf_pool/publikationen/7_rosmann.pdf)
|
||||
разделяет обзорный 91N6 и огневой 92N6; статья об С-400 в официальном
|
||||
[Memorial de Artillería 174/2, Ministerio de Defensa](https://publicaciones.defensa.gob.es/media/downloadable/files/links/m/e/memorial_artilleria_174-2.pdf)
|
||||
перечисляет КП, радары, пусковые и дополнительные средства. Это публичные обзоры,
|
||||
не эксплуатационная документация изготовителя. У чисел времени, дальности и качества в моде
|
||||
собственная игровая шкала; из этих публикаций они не калибровались.
|
||||
|
||||
Для наглядности холодного старта найден официальный выпуск
|
||||
[«С-400. Секрет ракеты», «Военная приёмка»](https://rutube.ru/video/37a2ef7d5e83ffa9ec4ba222bfd8ce5f/)
|
||||
с участием завода «Авангард». Доступная страница выпуска не содержит стенограммы;
|
||||
форма шарнира крышки и длительность игровых стадий не объявляются воспроизведением механизма прототипа.
|
||||
Для С-500 применяется стилизация существующего игрового профиля, без утверждения о механике всех реальных ракет.
|
||||
|
||||
## Как собрать
|
||||
|
||||
Через N → «Разместить элементы батареи…» можно расставить элементы в разных местах.
|
||||
Командные эквиваленты:
|
||||
|
||||
```text
|
||||
/airdef deploy radar_search
|
||||
/airdef deploy radar_track
|
||||
/airdef deploy s400_launcher
|
||||
/airdef deploy s500_launcher
|
||||
/airdef deploy radar_height
|
||||
/airdef battery
|
||||
```
|
||||
|
||||
Нумерацию существующей техники показывает `/airdef battery`. Чтобы переоборудовать её:
|
||||
|
||||
```text
|
||||
/airdef battery <номер С-400 или С-500> launcher
|
||||
/airdef radar <номер РЛС> search
|
||||
/airdef radar <номер другой РЛС> engagement
|
||||
```
|
||||
|
||||
Можно оставить одну многофункциональную РЛС (`multi`) и несколько пусковых.
|
||||
Поисковая/мачтовая РЛС даёт подсказку, но не разрешает огонь сама. РЛС сопровождения принимает
|
||||
подсказку и должна **сама измерить** цель в своей игровой досягаемости и с учётом рельефа.
|
||||
Чужой трек не превращается в собственное измерение. После этого связанные пусковые используют
|
||||
общую картину и существующее распределение целей. Если РЛС сопровождения уничтожена, свёрнута,
|
||||
потеряла связь или её измерение устарело, новые пуски прекращаются. Уже вылетевшие ракеты
|
||||
продолжают штатную игровую логику связи/памяти/самонаведения.
|
||||
|
||||
Старые миры и обычные `/airdef deploy s400` / `s500` сохраняют сборную конфигурацию `integrated`.
|
||||
Режимы, роли, боезапас и использованные контейнеры сохраняются с миром и сценариями.
|
||||
У прежнего игрового С-500 был магазин на четыре ракеты при двух нарисованных ТПК;
|
||||
теперь он согласован с двумя физическими ячейками. При загрузке старой расстановки
|
||||
его загруженный остаток ограничивается новой вместимостью, как при обычном восстановлении боезапаса.
|
||||
|
||||
## Что видно при пуске
|
||||
|
||||
1. Подготовка резервирует конкретную ячейку. Отдельная крышка ТПК открывается; ракета ещё не расходуется.
|
||||
2. При потере цели или неисправности/сворачивании носителя до выброса подготовка отменяется.
|
||||
3. Ракета выбрасывается из фактического muzzle. Боекомплект уменьшается один раз; использованный ТПК остаётся открытым.
|
||||
4. В фазе выброса отсутствуют факел, непрерывный звук двигателя и его дымовой след.
|
||||
5. После зажигания включаются эффекты, ракета разгоняется и начинает поворот по существующей игровой логике.
|
||||
|
||||
Это действует для зенитных С-400/С-500 и подготовленного ударного RM48U у С-400.
|
||||
У ручного удара носитель и ячейка резервируются до пуска; изменение состояния отменяет подготовку
|
||||
без расхода. Для RM48U добавлен плавный разгон после выброса. Задержанный ручной приказ
|
||||
не возобновляется после загрузки мира: незапущенный боеприпас остаётся в носителе.
|
||||
|
||||
В записи сохраняются конфигурация машины, использованные ячейки и положение крышки;
|
||||
повтор восстанавливает их по своей временной шкале. Записанный дым начинается у точки зажигания,
|
||||
а не рисуется задним числом от горловины контейнера.
|
||||
|
||||
## Ограничения
|
||||
|
||||
Роли РЛС и мачта пока используют общую модель машины, с разным поведением/отображением антенны.
|
||||
Отдельные точные модели 91Н6/92Н6/96Л6, машина КП, ручная принадлежность к батарее, отдельные
|
||||
каналы сопровождения и кабельное снабжение пока не добавлены. Подробные режимы и параметры реальных
|
||||
комплексов не моделируются. Конечная работа двигателя и энергетика манёвра остаются в #120;
|
||||
доставка ударных ракет через ТЗМ остаётся в #118.
|
||||
@@ -20,7 +20,7 @@
|
||||
| #114 | Навигация учитывает footprint и окончательную authored-геометрию; остановка перед unread frontier; угловые пересечения | Массовая игровая проверка всех новых шасси на сложном рельефе |
|
||||
| #115 | Три пилота с кадрами до/после, спокойные материалы; authored-проход по боевым и вспомогательным машинам | Крупные планы и точность прототипов ещё промежуточные; остальные летательные аппараты/ракеты, анимации, дальние виды и FPS |
|
||||
| #116 | Streaming не освобождает busy-guard работающего acoustic worker; проверено engine-тестами | Внешняя игровая приёмка на целевых машинах |
|
||||
| #118 | Ручной пуск из физического С-400 с отдельным RM48U; общие четыре ячейки, конечный склад, обмен/возврат; carrier в записи, холодный выброс и зажигание; фантомное ПВО удалено | Доставка RM48U через ТЗМ, расширение подтверждённых игровых профилей, включая оценку Бука; визуальная приёмка самого пуска |
|
||||
| #118 | Ручной пуск из физического С-400 с отдельным RM48U; общие четыре ячейки, конечный склад, обмен/возврат; carrier в записи, подготовка/крышки ТПК, холодный выброс, зажигание и игровой разгон; фантомное ПВО удалено | Доставка RM48U через ТЗМ, расширение подтверждённых игровых профилей, включая оценку Бука; визуальная приёмка самого пуска |
|
||||
|
||||
Для #118 RM48U — явно условный Minecraft-профиль. Существование сочетания опирается на сообщение ведомства,
|
||||
а игровые параметры не выдаются за реальные. `ground-stock` — команда подготовки sandbox-сценария,
|
||||
@@ -28,3 +28,7 @@
|
||||
|
||||
Новые находки внешнего ревью и причины отказа от неверных замечаний: [REVIEW-FOLLOWUP-2026-11-02.md](REVIEW-FOLLOWUP-2026-11-02.md).
|
||||
Картинки и ограничения художественного прохода: [MODEL-PILOTS.md](MODEL-PILOTS.md).
|
||||
|
||||
Дополнительно по просьбе владельца: [раздельная батарея и холодный пуск](BATTERIES-AND-COLD-LAUNCH.md).
|
||||
Есть отдельные ПУ С-400/С-500, роли общей РЛС, цепочка подсказка → собственное измерение → пуск,
|
||||
сохранение конфигурации и запись крышек. Это не закрывает оставшиеся пункты художественной и платформенной приёмки.
|
||||
|
||||
+36
-6
@@ -33,9 +33,10 @@ NS = "airdef"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- модели
|
||||
def el(frm, to, tex, faces=None, rot=None):
|
||||
def el(frm, to, tex, faces=None, rot=None, omit_faces=()):
|
||||
"""Элемент; rot = (ось, угол) — поворот вокруг центра элемента."""
|
||||
return {"from": [float(v) for v in frm], "to": [float(v) for v in to], "tex": tex, "faces": faces, "rot": rot}
|
||||
return {"from": [float(v) for v in frm], "to": [float(v) for v in to], "tex": tex,
|
||||
"faces": faces, "rot": rot, "omit_faces": tuple(omit_faces)}
|
||||
|
||||
|
||||
def mx(e):
|
||||
@@ -90,6 +91,8 @@ def model(parts, burnt=False):
|
||||
for p in parts:
|
||||
faces = {}
|
||||
for f in ("north", "south", "east", "west", "up", "down"):
|
||||
if f in p.get("omit_faces", ()):
|
||||
continue
|
||||
tex = (p["faces"] or {}).get(f, p["tex"])
|
||||
uv = face_uv(p["from"], p["to"], f)
|
||||
dx, dy, dz = (abs(p["to"][i] - p["from"][i]) for i in range(3))
|
||||
@@ -394,7 +397,7 @@ def maz_chassis(tex):
|
||||
return p + wheels((-14, -5, 13, 22))
|
||||
|
||||
|
||||
def tubes(n_wide, n_high, size, tex="tube"):
|
||||
def tubes(n_wide, n_high, size, tex="tube", front_cap=True):
|
||||
"""Пакет ТПК: n_wide × n_high труб сечением size, от z=-16 до 32. Ось подъёма — задняя нижняя кромка."""
|
||||
p = []
|
||||
w = n_wide * size + (n_wide - 1)
|
||||
@@ -403,7 +406,12 @@ def tubes(n_wide, n_high, size, tex="tube"):
|
||||
for j in range(n_high):
|
||||
x = x0 + i * (size + 1)
|
||||
y = j * (size + 1)
|
||||
p.append(el((x, y, -16), (x + size, y + size, 32), tex, {"north": "tube_cap", "south": "tube_cap"}))
|
||||
if front_cap:
|
||||
p.append(el((x, y, -16), (x + size, y + size, 32), tex,
|
||||
{"north": "tube_cap", "south": "tube_cap"}))
|
||||
else:
|
||||
p.append(el((x, y, -16), (x + size, y + size, 32), tex,
|
||||
{"south": "tube_cap"}, omit_faces=("north",)))
|
||||
p += [el((x0 - 1, 0, 26), (x0 + w + 1, n_high * (size + 1) - 1, 28), "dark_metal"),
|
||||
el((x0 - 1, 0, -2), (x0 + w + 1, n_high * (size + 1) - 1, 0), "dark_metal")]
|
||||
return p
|
||||
@@ -421,6 +429,26 @@ def big_radar_panel():
|
||||
el((6, -6, 8), (10, 0, 12), "metal")]
|
||||
|
||||
|
||||
def tpk_cap():
|
||||
"""Reusable stylized octagonal cap, centered at (8, 8, 8), for animated TPK covers."""
|
||||
z0, z_face, z1 = 7.7, 8.25, 8.3
|
||||
# Three cuboids form a flat octagonal silhouette while keeping the geometry blocky.
|
||||
return [el((2.5, 0, z0), (13.5, 16, z_face), "tube_cap"),
|
||||
el((0, 2.5, z0), (2.5, 13.5, z_face), "tube_cap"),
|
||||
el((13.5, 2.5, z0), (16, 13.5, z_face), "tube_cap"),
|
||||
el((5, 5, z_face), (11, 11, z1), "dark_metal")]
|
||||
|
||||
|
||||
def radar_mast():
|
||||
"""Generic two-block telescoping radar mast segment; stackable for role-based height."""
|
||||
return [el((4, 0, 4), (12, 3, 12), "dark_metal"),
|
||||
el((5.5, 3, 5.5), (10.5, 18, 10.5), "metal"),
|
||||
el((4.5, 18, 4.5), (11.5, 21, 11.5), "dark_metal"),
|
||||
el((6.5, 21, 6.5), (9.5, 30, 9.5), "dark_metal"),
|
||||
el((5.5, 30, 5.5), (10.5, 32, 10.5), "metal"),
|
||||
el((7, 31.5, 7), (9, 32, 9), "dark_metal")]
|
||||
|
||||
|
||||
def missile_57e6():
|
||||
p = taper("missile_ru", -16, -12, 0.5, 2, 0.5, 2, 2)
|
||||
p += [el((7, 7, -12), (9, 9, 4), "missile_ru"), el((6.4, 6.4, 4), (9.6, 9.6, 28), "booster")]
|
||||
@@ -997,7 +1025,9 @@ MODELS = {
|
||||
"s400_chassis": (lambda: maz_chassis("ru_green"), "Шасси С-400"),
|
||||
"s400_tubes": (lambda: tubes(2, 2, 7), "ТПК 48Н6"),
|
||||
"s500_chassis": (lambda: maz_chassis("ru_khaki"), "Шасси С-500"),
|
||||
"s500_tubes": (lambda: tubes(2, 1, 9.5), "ТПК 77Н6"),
|
||||
"s500_tubes": (lambda: tubes(2, 1, 9.5, front_cap=False), "ТПК 77Н6"),
|
||||
"tpk_cap": (tpk_cap, "Крышка ТПК (анимация)"),
|
||||
"radar_mast": (radar_mast, "Радиомачта (секция высоты)"),
|
||||
"radar_trailer": (radar_trailer, "Прицеп РЛС"),
|
||||
"radar_panel": (big_radar_panel, "РЛС обзора"),
|
||||
"m57e6": (missile_57e6, "57Э6"),
|
||||
@@ -1039,7 +1069,7 @@ MODELS = {
|
||||
}
|
||||
|
||||
WRECKS = ["patriot_trailer", "patriot_launcher", "gepard_hull", "gepard_turret", "gepard_guns", "pantsir_truck",
|
||||
"pantsir_turret", "pantsir_pack", "s400_chassis", "s400_tubes", "s500_chassis", "s500_tubes",
|
||||
"pantsir_turret", "pantsir_pack", "s400_chassis", "s400_tubes", "s500_chassis", "s500_tubes", "tpk_cap", "radar_mast",
|
||||
"irist_chassis", "irist_tubes", "nasams_trailer", "nasams_launcher", "buk_chassis", "buk_turret",
|
||||
"buk_rails", "tor_chassis", "tor_turret", "p1sun_launcher", "radar_truck", "depot", "tzm_truck",
|
||||
"ew_truck", "searchlight", "searchlight_head"]
|
||||
|
||||
@@ -56,7 +56,7 @@ def s400_tubes():
|
||||
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"}))
|
||||
"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")]
|
||||
|
||||
Reference in New Issue
Block a user