Compare commits

..

2 Commits

Author SHA1 Message Date
chexit 6efec3e546 CI: drop actions/cache (the runner's cache server times out and hangs the job's end)
build / mod (push) Successful in 2m29s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 12:52:06 +03:00
chexit d3359f4b39 New systems (IRIS-T, NASAMS, Buk, Tor, P1-SUN), air-defence network with long-range radars, logistics, move orders, EW, adaptive enemy, searchlights and flares; grid holds a kill in view
build / mod (push) Successful in 5m7s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 12:46:30 +03:00
99 changed files with 13625 additions and 304 deletions
+2 -6
View File
@@ -24,12 +24,8 @@ jobs:
run: |
curl -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --target wasm32-unknown-unknown
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
# Инструмент упаковки (cargo-horizon-mod без пробного запуска) — единственное, что стоит кэшировать.
- uses: actions/cache@v3
continue-on-error: true
with:
path: ~/.cargo/bin/cargo-horizon-mod
key: tool-${{ hashFiles('HORIZON_COMMIT', 'horizon-patches/*') }}
# Без actions/cache: кэш-сервер раннера из контейнеров задач недоступен — восстановление
# падало по таймауту, а сохранение вешало «Complete job» на пару минут. Инструмент собирается заново.
- name: Форк Horizon
run: nice -n 19 python3 tools/airdef.py fork
- name: Мод
+41 -1
View File
@@ -68,6 +68,37 @@ CI: `.gitea/workflows/build.yml` на каждый пуш собирает мо
**Сохранение.** Комплексы, объекты, их повреждения и настройки сохраняются отдельно для каждого мира (и для каждого
сервера); уничтоженный комплекс остаётся остовом и после перезахода, пока его не отремонтируют.
**Сеть ПВО.** Каждая единица делится целями со всеми в радиусе 1,5 км (линия связи): РЛС дальнего обнаружения
даёт раннее предупреждение всем, пусковая без своей РЛС (P1-SUN) стреляет по чужим данным. Пушкам (Гепард,
пушки Панциря) нужна своя цель: они стреляют только по тому, что видят сами. Уничтоженная РЛС — дыра в сети.
**Логистика.** Пока нет ни одного склада, магазины пополняются сами, как раньше. Как только поставлен склад,
боекомплект считается: комплекс без ракет пишет «ЖДЁТ БК»; в 60 м от склада он перезаряжается прямо с него, а
дальние обслуживают **ТЗМ**: заправляются на складе (2 перезарядки), сами едут к ждущим комплексам, ближайшим
первым, и возвращаются. Запас склада — 30 перезарядок, он виден в сводке. Склад и ТЗМ детонируют всем грузом.
**Перемещение.** **Пульт перемещения**: навестись на технику (выбрать), затем на точку на земле — она
сворачивается (С-400 и другие с вертикальным пуском дольше, Гепард и Панцирь быстро), едет по рельефу и
разворачивается; всё это время не стреляет и не светит РЛС. Или `/airdef move <номер>` — к тому месту, где ты стоишь.
**РЭБ.** Клавиша **U** (или `/airdef ew on|off`) включает все станции. Станция в радиусе 350 м сбивает
спутниковую навигацию у Шахедов, Герань, Гербер, Х-101 и Калибров: они теряют маршрут и уводятся с цели
(баллистике и Ланцетам с оптикой всё равно). Передатчик работает до 20 с и перегревается; остывает ~35 с,
пока не остынет — не включить. Включённая станция себя выдаёт: умный противник запоминает её и шлёт на неё
Ланцеты и Х-22, а маршруты спутниковых дронов прокладывает в обход.
**Умный противник учится.** Он запоминает, откуда его налёты сбивали, а откуда они прорывались, и следующий
налёт ведёт с лучшего направления; помнит комплексы, выдавшие себя пуском, и станции РЭБ, и во время налёта
перенацеливает ещё не взлетевшие Ланцеты и Шахеды на них; если цель-комплекс уничтожен, дроны идут к
следующему известному. Дроны под обстрелом прижимаются к земле.
**Ночь.** Ночью пушки попадают втрое хуже, если цель не освещена. Освещают **прожекторы** (луч ведёт ближайшую
цель из сети, приоритет — цели пушек) и **осветительные ракеты**: пушечные комплексы пускают их сами (раз в
25 с на цель в темноте), игрок — **осветительным пистолетом** или `/airdef flare`. Ракета висит ~30 с и
освещает всё вокруг на ~60–90 м.
**Сетка J** после сбития ещё 3,5 с держит камеру на сбитой цели, и комплекс в это время не пропадает из сетки.
**Повреждения.** Взрыв рядом с комплексом повреждает его: он хуже поворачивается, хуже видит и попадает,
дымит, ниже 35% горит. Уничтоженный остаётся горящим остовом, его боекомплект детонирует: чем больше ракет
оставалось, тем мощнее взрыв и дольше вторичные (у Гепарда рвутся снаряды); соседние комплексы тоже могут
@@ -103,7 +134,7 @@ CI: `.gitea/workflows/build.yml` на каждый пуш собирает мо
Чтобы в повторах и камерах комплексов была видна земля далеко от игрока, мод включает дальний ландшафт
(48 чанков, упрощённый); выключается в меню N → «Огонь, ремонт, эффекты…» → «Дальний ландшафт».
Клавиши N, K, J и R можно переназначить в «Настройки → Моды».
Клавиши N, K, J, R и U можно переназначить в «Настройки → Моды».
Команды:
- `/airdef wave <тип> [кол-во] [дальность]` — налёт на ближайшую цель. Типы: `shahed`, `geran3`, `lancet`, `gerbera`, `kh101`, `kalibr`, `kh22`, `zircon`, `iskander`, `kinzhal`, `oreshnik` и `mix` (комбинированный удар двумя секторами: «Герберы», шахеды, Герани-3, Х-101, «Калибры», Х-22, Циркон, «Искандер», «Кинжал», при 30+ — «Орешник»);
@@ -152,6 +183,15 @@ CI: `.gitea/workflows/build.yml` на каждый пуш собирает мо
| Панцирь-С1 | 12 × 57Э6 и 2 × 30 мм; модуль поворачивается, пакеты и стволы поднимаются | ракеты 25–150, пушки 60 |
| С-400 | 4 × 48Н6, вертикальный холодный старт, РЛС обзора на прицепе | 30–480 |
| С-500 | 4 × 77Н6, холодный старт, самые быстрые ракеты | 40–700 |
| IRIS-T SLM | 8 ракет, вертикальный пуск, быстрые и манёвренные | 15–400 |
| NASAMS | 6 × AMRAAM, наводимая пусковая на прицепе | 10–300 |
| Бук-М2 | 4 × 9М317 на направляющих, башня с РЛС подсвета | 20–450 |
| Тор-М2 | 8 × 9М338 вертикально из башни, быстрая реакция против дронов | 8–160 |
| P1-SUN | 8 дронов-перехватчиков против Шахедов и Герань; своей РЛС нет — стреляет по данным сети | 5–450 |
Обслуживающая техника (тоже пультами или `/airdef deploy <ключ>`): **РЛС дальнего обнаружения** (`radar`,
видит на 1,7 км с высокой мачты, хуже прячутся за рельефом), **склад боеприпасов** (`depot`), **ТЗМ** (`tzm`),
**станция РЭБ** (`ew`), **прожектор** (`light`). Новые комплексы: `irist`, `nasams`, `buk`, `tor`, `p1sun`.
**Согласование огня.** Комплексы делят цели между собой: на обычную цель идёт одна ракета, на баллистическую две,
пушки расходятся по разным целям. Первыми бьют по самым опасным (быстрым и тяжёлым) целям, при этом ложные
@@ -0,0 +1,7 @@
{
"variants": {
"": {
"model": "airdef:block/beam"
}
}
}
@@ -0,0 +1,7 @@
{
"variants": {
"": {
"model": "airdef:block/buk_chassis"
}
}
}
@@ -0,0 +1,7 @@
{
"variants": {
"": {
"model": "airdef:block/buk_rails"
}
}
}
@@ -0,0 +1,7 @@
{
"variants": {
"": {
"model": "airdef:block/buk_turret"
}
}
}
@@ -0,0 +1,7 @@
{
"variants": {
"": {
"model": "airdef:block/depot"
}
}
}
@@ -0,0 +1,7 @@
{
"variants": {
"": {
"model": "airdef:block/ew_truck"
}
}
}
@@ -0,0 +1,7 @@
{
"variants": {
"": {
"model": "airdef:block/flare"
}
}
}
@@ -0,0 +1,7 @@
{
"variants": {
"": {
"model": "airdef:block/irist_chassis"
}
}
}
@@ -0,0 +1,7 @@
{
"variants": {
"": {
"model": "airdef:block/irist_tubes"
}
}
}
@@ -0,0 +1,7 @@
{
"variants": {
"": {
"model": "airdef:block/nasams_launcher"
}
}
}
@@ -0,0 +1,7 @@
{
"variants": {
"": {
"model": "airdef:block/nasams_trailer"
}
}
}
@@ -0,0 +1,7 @@
{
"variants": {
"": {
"model": "airdef:block/p1sun"
}
}
}
@@ -0,0 +1,7 @@
{
"variants": {
"": {
"model": "airdef:block/p1sun_launcher"
}
}
}
@@ -0,0 +1,7 @@
{
"variants": {
"": {
"model": "airdef:block/radar_truck"
}
}
}
@@ -0,0 +1,7 @@
{
"variants": {
"": {
"model": "airdef:block/searchlight"
}
}
}
@@ -0,0 +1,7 @@
{
"variants": {
"": {
"model": "airdef:block/searchlight_head"
}
}
}
@@ -0,0 +1,7 @@
{
"variants": {
"": {
"model": "airdef:block/tor_chassis"
}
}
}
@@ -0,0 +1,7 @@
{
"variants": {
"": {
"model": "airdef:block/tor_turret"
}
}
}
@@ -0,0 +1,7 @@
{
"variants": {
"": {
"model": "airdef:block/tzm_truck"
}
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "airdef:item/deploy_buk"
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "airdef:item/deploy_depot"
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "airdef:item/deploy_ew"
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "airdef:item/deploy_irist"
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "airdef:item/deploy_light"
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "airdef:item/deploy_nasams"
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "airdef:item/deploy_p1sun"
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "airdef:item/deploy_radar"
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "airdef:item/deploy_tor"
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "airdef:item/deploy_tzm"
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "airdef:item/flare_gun"
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "airdef:item/mover"
}
}
+31
View File
@@ -39,6 +39,25 @@
"block.airdef.iskander": "Искандер-М",
"block.airdef.kinzhal": "Кинжал",
"block.airdef.tracer": "Трассер",
"block.airdef.irist_chassis": "Шасси IRIS-T SLM",
"block.airdef.irist_tubes": "Пакет IRIS-T",
"block.airdef.nasams_trailer": "Прицеп NASAMS",
"block.airdef.nasams_launcher": "Пусковая NASAMS",
"block.airdef.buk_chassis": "Шасси Бука",
"block.airdef.buk_turret": "Башня Бука",
"block.airdef.buk_rails": "Ракеты Бука",
"block.airdef.tor_chassis": "Шасси Тора",
"block.airdef.tor_turret": "Башня Тора",
"block.airdef.p1sun_launcher": "Пусковая P1-SUN",
"block.airdef.p1sun": "Дрон-перехватчик P1-SUN",
"block.airdef.radar_truck": "Шасси РЛС",
"block.airdef.depot": "Склад боеприпасов",
"block.airdef.tzm_truck": "Транспортно-заряжающая машина",
"block.airdef.ew_truck": "Станция РЭБ",
"block.airdef.searchlight": "Прожектор",
"block.airdef.searchlight_head": "Голова прожектора",
"block.airdef.beam": "Луч прожектора",
"block.airdef.flare": "Осветительная ракета",
"block.airdef.reentry": "Боевой блок",
"item.airdef.designator": "Целеуказатель",
"item.airdef.launch_shahed": "Пуск: Шахед-136",
@@ -56,6 +75,18 @@
"item.airdef.launch_iskander": "Пуск: Искандер-М",
"item.airdef.launch_kinzhal": "Пуск: Кинжал",
"item.airdef.launch_oreshnik": "Пуск: Орешник",
"item.airdef.deploy_irist": "Развернуть IRIS-T SLM",
"item.airdef.deploy_nasams": "Развернуть NASAMS",
"item.airdef.deploy_buk": "Развернуть Бук-М2",
"item.airdef.deploy_tor": "Развернуть Тор-М2",
"item.airdef.deploy_p1sun": "Развернуть P1-SUN",
"item.airdef.deploy_radar": "Развернуть РЛС дальнего обнаружения",
"item.airdef.deploy_depot": "Развернуть склад боеприпасов",
"item.airdef.deploy_tzm": "Развернуть ТЗМ",
"item.airdef.deploy_ew": "Развернуть станцию РЭБ",
"item.airdef.deploy_light": "Развернуть прожектор",
"item.airdef.mover": "Пульт перемещения",
"item.airdef.flare_gun": "Осветительная ракета",
"item.airdef.remover": "Свернуть ПВО",
"item.airdef.cam_shahed": "Шахед-136",
"item.airdef.cam_geran3": "Герань-3",
+31
View File
@@ -39,6 +39,25 @@
"block.airdef.iskander": "Искандер-М",
"block.airdef.kinzhal": "Кинжал",
"block.airdef.tracer": "Трассер",
"block.airdef.irist_chassis": "Шасси IRIS-T SLM",
"block.airdef.irist_tubes": "Пакет IRIS-T",
"block.airdef.nasams_trailer": "Прицеп NASAMS",
"block.airdef.nasams_launcher": "Пусковая NASAMS",
"block.airdef.buk_chassis": "Шасси Бука",
"block.airdef.buk_turret": "Башня Бука",
"block.airdef.buk_rails": "Ракеты Бука",
"block.airdef.tor_chassis": "Шасси Тора",
"block.airdef.tor_turret": "Башня Тора",
"block.airdef.p1sun_launcher": "Пусковая P1-SUN",
"block.airdef.p1sun": "Дрон-перехватчик P1-SUN",
"block.airdef.radar_truck": "Шасси РЛС",
"block.airdef.depot": "Склад боеприпасов",
"block.airdef.tzm_truck": "Транспортно-заряжающая машина",
"block.airdef.ew_truck": "Станция РЭБ",
"block.airdef.searchlight": "Прожектор",
"block.airdef.searchlight_head": "Голова прожектора",
"block.airdef.beam": "Луч прожектора",
"block.airdef.flare": "Осветительная ракета",
"block.airdef.reentry": "Боевой блок",
"item.airdef.designator": "Целеуказатель",
"item.airdef.launch_shahed": "Пуск: Шахед-136",
@@ -56,6 +75,18 @@
"item.airdef.launch_iskander": "Пуск: Искандер-М",
"item.airdef.launch_kinzhal": "Пуск: Кинжал",
"item.airdef.launch_oreshnik": "Пуск: Орешник",
"item.airdef.deploy_irist": "Развернуть IRIS-T SLM",
"item.airdef.deploy_nasams": "Развернуть NASAMS",
"item.airdef.deploy_buk": "Развернуть Бук-М2",
"item.airdef.deploy_tor": "Развернуть Тор-М2",
"item.airdef.deploy_p1sun": "Развернуть P1-SUN",
"item.airdef.deploy_radar": "Развернуть РЛС дальнего обнаружения",
"item.airdef.deploy_depot": "Развернуть склад боеприпасов",
"item.airdef.deploy_tzm": "Развернуть ТЗМ",
"item.airdef.deploy_ew": "Развернуть станцию РЭБ",
"item.airdef.deploy_light": "Развернуть прожектор",
"item.airdef.mover": "Пульт перемещения",
"item.airdef.flare_gun": "Осветительная ракета",
"item.airdef.remover": "Свернуть ПВО",
"item.airdef.cam_shahed": "Шахед-136",
"item.airdef.cam_geran3": "Герань-3",
@@ -0,0 +1,77 @@
{
"ambientocclusion": false,
"textures": {
"beam": "airdef:block/beam",
"particle": "airdef:block/beam"
},
"elements": [
{
"from": [
6.5,
6.5,
-16.0
],
"to": [
9.5,
9.5,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#beam"
},
"south": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#beam"
},
"east": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#beam"
},
"west": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#beam"
},
"up": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#beam"
},
"down": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#beam"
}
}
}
]
}
@@ -0,0 +1,487 @@
{
"ambientocclusion": false,
"textures": {
"ru_green": "airdef:block/ru_green",
"dark_metal": "airdef:block/dark_metal",
"track": "airdef:block/track",
"particle": "airdef:block/ru_green"
},
"elements": [
{
"from": [
-10.0,
4.0,
-14.0
],
"to": [
26.0,
15.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
11.0
],
"texture": "#ru_green"
},
"south": {
"uv": [
0,
0,
16,
11.0
],
"texture": "#ru_green"
},
"east": {
"uv": [
0,
0,
16,
11.0
],
"texture": "#ru_green"
},
"west": {
"uv": [
0,
0,
16,
11.0
],
"texture": "#ru_green"
},
"up": {
"uv": [
0,
0,
16,
16
],
"texture": "#ru_green"
},
"down": {
"uv": [
0,
0,
16,
16
],
"texture": "#ru_green"
}
}
},
{
"from": [
-10.0,
4.0,
-16.0
],
"to": [
26.0,
10.0,
-14.0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
6.0
],
"texture": "#ru_green"
},
"south": {
"uv": [
0,
0,
16,
6.0
],
"texture": "#ru_green"
},
"east": {
"uv": [
0,
0,
2.0,
6.0
],
"texture": "#ru_green"
},
"west": {
"uv": [
0,
0,
2.0,
6.0
],
"texture": "#ru_green"
},
"up": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#ru_green"
},
"down": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#ru_green"
}
}
},
{
"from": [
-8.0,
15.0,
22.0
],
"to": [
24.0,
16.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
1.0
],
"texture": "#dark_metal"
},
"south": {
"uv": [
0,
0,
16,
1.0
],
"texture": "#dark_metal"
},
"east": {
"uv": [
0,
0,
10.0,
1.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
10.0,
1.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
16,
10.0
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
16,
10.0
],
"texture": "#dark_metal"
}
}
},
{
"from": [
-14.0,
0.0,
-15.0
],
"to": [
-10.0,
9.0,
31.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
9.0
],
"texture": "#track"
},
"south": {
"uv": [
0,
0,
4.0,
9.0
],
"texture": "#track"
},
"east": {
"uv": [
0,
0,
16,
9.0
],
"texture": "#track"
},
"west": {
"uv": [
0,
0,
16,
9.0
],
"texture": "#track"
},
"up": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#track"
},
"down": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#track"
}
}
},
{
"from": [
26.0,
0.0,
-15.0
],
"to": [
30.0,
9.0,
31.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
9.0
],
"texture": "#track"
},
"south": {
"uv": [
0,
0,
4.0,
9.0
],
"texture": "#track"
},
"east": {
"uv": [
0,
0,
16,
9.0
],
"texture": "#track"
},
"west": {
"uv": [
0,
0,
16,
9.0
],
"texture": "#track"
},
"up": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#track"
},
"down": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#track"
}
}
},
{
"from": [
-14.5,
5.0,
-15.0
],
"to": [
-10.0,
10.0,
31.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.5,
5.0
],
"texture": "#ru_green"
},
"south": {
"uv": [
0,
0,
4.5,
5.0
],
"texture": "#ru_green"
},
"east": {
"uv": [
0,
0,
16,
5.0
],
"texture": "#ru_green"
},
"west": {
"uv": [
0,
0,
16,
5.0
],
"texture": "#ru_green"
},
"up": {
"uv": [
0,
0,
4.5,
16
],
"texture": "#ru_green"
},
"down": {
"uv": [
0,
0,
4.5,
16
],
"texture": "#ru_green"
}
}
},
{
"from": [
26.0,
5.0,
-15.0
],
"to": [
30.5,
10.0,
31.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.5,
5.0
],
"texture": "#ru_green"
},
"south": {
"uv": [
0,
0,
4.5,
5.0
],
"texture": "#ru_green"
},
"east": {
"uv": [
0,
0,
16,
5.0
],
"texture": "#ru_green"
},
"west": {
"uv": [
0,
0,
16,
5.0
],
"texture": "#ru_green"
},
"up": {
"uv": [
0,
0,
4.5,
16
],
"texture": "#ru_green"
},
"down": {
"uv": [
0,
0,
4.5,
16
],
"texture": "#ru_green"
}
}
}
]
}
@@ -0,0 +1,896 @@
{
"ambientocclusion": false,
"textures": {
"missile_ru": "airdef:block/missile_ru",
"booster": "airdef:block/booster",
"dark_metal": "airdef:block/dark_metal",
"ru_green": "airdef:block/ru_green",
"particle": "airdef:block/missile_ru"
},
"elements": [
{
"from": [
-10.5,
4.0,
-16.0
],
"to": [
-7.5,
7.0,
26.0
],
"faces": {
"north": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#missile_ru"
},
"south": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#missile_ru"
},
"east": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#missile_ru"
},
"west": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#missile_ru"
},
"up": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#missile_ru"
},
"down": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#missile_ru"
}
}
},
{
"from": [
-10.8,
3.5,
20.0
],
"to": [
-7.2,
7.5,
27.0
],
"faces": {
"north": {
"uv": [
0,
0,
3.6000000000000005,
4.0
],
"texture": "#booster"
},
"south": {
"uv": [
0,
0,
3.6000000000000005,
4.0
],
"texture": "#booster"
},
"east": {
"uv": [
0,
0,
7.0,
4.0
],
"texture": "#booster"
},
"west": {
"uv": [
0,
0,
7.0,
4.0
],
"texture": "#booster"
},
"up": {
"uv": [
0,
0,
3.6000000000000005,
7.0
],
"texture": "#booster"
},
"down": {
"uv": [
0,
0,
3.6000000000000005,
7.0
],
"texture": "#booster"
}
}
},
{
"from": [
-9.5,
2.0,
-6.0
],
"to": [
-8.5,
4.0,
16.0
],
"faces": {
"north": {
"uv": [
0,
0,
1.0,
2.0
],
"texture": "#dark_metal"
},
"south": {
"uv": [
0,
0,
1.0,
2.0
],
"texture": "#dark_metal"
},
"east": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
1.0,
16
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
1.0,
16
],
"texture": "#dark_metal"
}
}
},
{
"from": [
-4.5,
4.0,
-16.0
],
"to": [
-1.5,
7.0,
26.0
],
"faces": {
"north": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#missile_ru"
},
"south": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#missile_ru"
},
"east": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#missile_ru"
},
"west": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#missile_ru"
},
"up": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#missile_ru"
},
"down": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#missile_ru"
}
}
},
{
"from": [
-4.8,
3.5,
20.0
],
"to": [
-1.2,
7.5,
27.0
],
"faces": {
"north": {
"uv": [
0,
0,
3.5999999999999996,
4.0
],
"texture": "#booster"
},
"south": {
"uv": [
0,
0,
3.5999999999999996,
4.0
],
"texture": "#booster"
},
"east": {
"uv": [
0,
0,
7.0,
4.0
],
"texture": "#booster"
},
"west": {
"uv": [
0,
0,
7.0,
4.0
],
"texture": "#booster"
},
"up": {
"uv": [
0,
0,
3.5999999999999996,
7.0
],
"texture": "#booster"
},
"down": {
"uv": [
0,
0,
3.5999999999999996,
7.0
],
"texture": "#booster"
}
}
},
{
"from": [
-3.5,
2.0,
-6.0
],
"to": [
-2.5,
4.0,
16.0
],
"faces": {
"north": {
"uv": [
0,
0,
1.0,
2.0
],
"texture": "#dark_metal"
},
"south": {
"uv": [
0,
0,
1.0,
2.0
],
"texture": "#dark_metal"
},
"east": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
1.0,
16
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
1.0,
16
],
"texture": "#dark_metal"
}
}
},
{
"from": [
11.5,
4.0,
-16.0
],
"to": [
14.5,
7.0,
26.0
],
"faces": {
"north": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#missile_ru"
},
"south": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#missile_ru"
},
"east": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#missile_ru"
},
"west": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#missile_ru"
},
"up": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#missile_ru"
},
"down": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#missile_ru"
}
}
},
{
"from": [
11.2,
3.5,
20.0
],
"to": [
14.8,
7.5,
27.0
],
"faces": {
"north": {
"uv": [
0,
0,
3.6000000000000014,
4.0
],
"texture": "#booster"
},
"south": {
"uv": [
0,
0,
3.6000000000000014,
4.0
],
"texture": "#booster"
},
"east": {
"uv": [
0,
0,
7.0,
4.0
],
"texture": "#booster"
},
"west": {
"uv": [
0,
0,
7.0,
4.0
],
"texture": "#booster"
},
"up": {
"uv": [
0,
0,
3.6000000000000014,
7.0
],
"texture": "#booster"
},
"down": {
"uv": [
0,
0,
3.6000000000000014,
7.0
],
"texture": "#booster"
}
}
},
{
"from": [
12.5,
2.0,
-6.0
],
"to": [
13.5,
4.0,
16.0
],
"faces": {
"north": {
"uv": [
0,
0,
1.0,
2.0
],
"texture": "#dark_metal"
},
"south": {
"uv": [
0,
0,
1.0,
2.0
],
"texture": "#dark_metal"
},
"east": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
1.0,
16
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
1.0,
16
],
"texture": "#dark_metal"
}
}
},
{
"from": [
17.5,
4.0,
-16.0
],
"to": [
20.5,
7.0,
26.0
],
"faces": {
"north": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#missile_ru"
},
"south": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#missile_ru"
},
"east": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#missile_ru"
},
"west": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#missile_ru"
},
"up": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#missile_ru"
},
"down": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#missile_ru"
}
}
},
{
"from": [
17.2,
3.5,
20.0
],
"to": [
20.8,
7.5,
27.0
],
"faces": {
"north": {
"uv": [
0,
0,
3.6000000000000014,
4.0
],
"texture": "#booster"
},
"south": {
"uv": [
0,
0,
3.6000000000000014,
4.0
],
"texture": "#booster"
},
"east": {
"uv": [
0,
0,
7.0,
4.0
],
"texture": "#booster"
},
"west": {
"uv": [
0,
0,
7.0,
4.0
],
"texture": "#booster"
},
"up": {
"uv": [
0,
0,
3.6000000000000014,
7.0
],
"texture": "#booster"
},
"down": {
"uv": [
0,
0,
3.6000000000000014,
7.0
],
"texture": "#booster"
}
}
},
{
"from": [
18.5,
2.0,
-6.0
],
"to": [
19.5,
4.0,
16.0
],
"faces": {
"north": {
"uv": [
0,
0,
1.0,
2.0
],
"texture": "#dark_metal"
},
"south": {
"uv": [
0,
0,
1.0,
2.0
],
"texture": "#dark_metal"
},
"east": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
1.0,
16
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
1.0,
16
],
"texture": "#dark_metal"
}
}
},
{
"from": [
-12.0,
0.0,
-2.0
],
"to": [
28.0,
2.0,
14.0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#ru_green"
},
"south": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#ru_green"
},
"east": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#ru_green"
},
"west": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#ru_green"
},
"up": {
"uv": [
0,
0,
16,
16
],
"texture": "#ru_green"
},
"down": {
"uv": [
0,
0,
16,
16
],
"texture": "#ru_green"
}
}
}
]
}
@@ -0,0 +1,214 @@
{
"ambientocclusion": false,
"textures": {
"ru_green": "airdef:block/ru_green",
"radar_face": "airdef:block/radar_face",
"particle": "airdef:block/ru_green"
},
"elements": [
{
"from": [
-4.0,
0.0,
-4.0
],
"to": [
20.0,
9.0,
20.0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
9.0
],
"texture": "#ru_green"
},
"south": {
"uv": [
0,
0,
16,
9.0
],
"texture": "#ru_green"
},
"east": {
"uv": [
0,
0,
16,
9.0
],
"texture": "#ru_green"
},
"west": {
"uv": [
0,
0,
16,
9.0
],
"texture": "#ru_green"
},
"up": {
"uv": [
0,
0,
16,
16
],
"texture": "#ru_green"
},
"down": {
"uv": [
0,
0,
16,
16
],
"texture": "#ru_green"
}
}
},
{
"from": [
1.0,
2.0,
-10.0
],
"to": [
15.0,
14.0,
-4.0
],
"faces": {
"north": {
"uv": [
0,
0,
14.0,
12.0
],
"texture": "#radar_face"
},
"south": {
"uv": [
0,
0,
14.0,
12.0
],
"texture": "#radar_face"
},
"east": {
"uv": [
0,
0,
6.0,
12.0
],
"texture": "#radar_face"
},
"west": {
"uv": [
0,
0,
6.0,
12.0
],
"texture": "#radar_face"
},
"up": {
"uv": [
0,
0,
14.0,
6.0
],
"texture": "#radar_face"
},
"down": {
"uv": [
0,
0,
14.0,
6.0
],
"texture": "#radar_face"
}
}
},
{
"from": [
-2.0,
9.0,
6.0
],
"to": [
18.0,
11.0,
18.0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#ru_green"
},
"south": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#ru_green"
},
"east": {
"uv": [
0,
0,
12.0,
2.0
],
"texture": "#ru_green"
},
"west": {
"uv": [
0,
0,
12.0,
2.0
],
"texture": "#ru_green"
},
"up": {
"uv": [
0,
0,
16,
12.0
],
"texture": "#ru_green"
},
"down": {
"uv": [
0,
0,
16,
12.0
],
"texture": "#ru_green"
}
}
}
]
}
@@ -0,0 +1,488 @@
{
"ambientocclusion": false,
"textures": {
"concrete": "airdef:block/concrete",
"crate": "airdef:block/crate",
"tube": "airdef:block/tube",
"tube_cap": "airdef:block/tube_cap",
"particle": "airdef:block/concrete"
},
"elements": [
{
"from": [
-14.0,
0.0,
-14.0
],
"to": [
30.0,
2.0,
30.0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#concrete"
},
"south": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#concrete"
},
"east": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#concrete"
},
"west": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#concrete"
},
"up": {
"uv": [
0,
0,
16,
16
],
"texture": "#concrete"
},
"down": {
"uv": [
0,
0,
16,
16
],
"texture": "#concrete"
}
}
},
{
"from": [
-12.0,
2.0,
-12.0
],
"to": [
0.0,
10.0,
0.0
],
"faces": {
"north": {
"uv": [
0,
0,
12.0,
8.0
],
"texture": "#crate"
},
"south": {
"uv": [
0,
0,
12.0,
8.0
],
"texture": "#crate"
},
"east": {
"uv": [
0,
0,
12.0,
8.0
],
"texture": "#crate"
},
"west": {
"uv": [
0,
0,
12.0,
8.0
],
"texture": "#crate"
},
"up": {
"uv": [
0,
0,
12.0,
12.0
],
"texture": "#crate"
},
"down": {
"uv": [
0,
0,
12.0,
12.0
],
"texture": "#crate"
}
}
},
{
"from": [
-12.0,
2.0,
4.0
],
"to": [
0.0,
10.0,
16.0
],
"faces": {
"north": {
"uv": [
0,
0,
12.0,
8.0
],
"texture": "#crate"
},
"south": {
"uv": [
0,
0,
12.0,
8.0
],
"texture": "#crate"
},
"east": {
"uv": [
0,
0,
12.0,
8.0
],
"texture": "#crate"
},
"west": {
"uv": [
0,
0,
12.0,
8.0
],
"texture": "#crate"
},
"up": {
"uv": [
0,
0,
12.0,
12.0
],
"texture": "#crate"
},
"down": {
"uv": [
0,
0,
12.0,
12.0
],
"texture": "#crate"
}
}
},
{
"from": [
4.0,
2.0,
-12.0
],
"to": [
16.0,
10.0,
0.0
],
"faces": {
"north": {
"uv": [
0,
0,
12.0,
8.0
],
"texture": "#crate"
},
"south": {
"uv": [
0,
0,
12.0,
8.0
],
"texture": "#crate"
},
"east": {
"uv": [
0,
0,
12.0,
8.0
],
"texture": "#crate"
},
"west": {
"uv": [
0,
0,
12.0,
8.0
],
"texture": "#crate"
},
"up": {
"uv": [
0,
0,
12.0,
12.0
],
"texture": "#crate"
},
"down": {
"uv": [
0,
0,
12.0,
12.0
],
"texture": "#crate"
}
}
},
{
"from": [
6.0,
2.0,
6.0
],
"to": [
28.0,
7.0,
10.0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
5.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
16,
5.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
4.0,
5.0
],
"texture": "#tube"
},
"west": {
"uv": [
0,
0,
4.0,
5.0
],
"texture": "#tube"
},
"up": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#tube"
},
"down": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#tube"
}
}
},
{
"from": [
6.0,
7.0,
6.0
],
"to": [
28.0,
12.0,
10.0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
5.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
16,
5.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
4.0,
5.0
],
"texture": "#tube"
},
"west": {
"uv": [
0,
0,
4.0,
5.0
],
"texture": "#tube"
},
"up": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#tube"
},
"down": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#tube"
}
}
},
{
"from": [
6.0,
12.0,
6.0
],
"to": [
28.0,
17.0,
10.0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
5.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
16,
5.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
4.0,
5.0
],
"texture": "#tube"
},
"west": {
"uv": [
0,
0,
4.0,
5.0
],
"texture": "#tube"
},
"up": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#tube"
},
"down": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#tube"
}
}
}
]
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,146 @@
{
"ambientocclusion": false,
"textures": {
"lamp": "airdef:block/lamp",
"antenna": "airdef:block/antenna",
"particle": "airdef:block/lamp"
},
"elements": [
{
"from": [
6.5,
6.5,
6.5
],
"to": [
9.5,
9.5,
9.5
],
"faces": {
"north": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#lamp"
},
"south": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#lamp"
},
"east": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#lamp"
},
"west": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#lamp"
},
"up": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#lamp"
},
"down": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#lamp"
}
}
},
{
"from": [
7.6,
9.5,
7.6
],
"to": [
8.4,
16.0,
8.4
],
"faces": {
"north": {
"uv": [
0,
0,
0.8000000000000007,
6.5
],
"texture": "#antenna"
},
"south": {
"uv": [
0,
0,
0.8000000000000007,
6.5
],
"texture": "#antenna"
},
"east": {
"uv": [
0,
0,
0.8000000000000007,
6.5
],
"texture": "#antenna"
},
"west": {
"uv": [
0,
0,
0.8000000000000007,
6.5
],
"texture": "#antenna"
},
"up": {
"uv": [
0,
0,
0.8000000000000007,
0.8000000000000007
],
"texture": "#antenna"
},
"down": {
"uv": [
0,
0,
0.8000000000000007,
0.8000000000000007
],
"texture": "#antenna"
}
}
}
]
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,691 @@
{
"ambientocclusion": false,
"textures": {
"canister": "airdef:block/canister",
"tube_cap": "airdef:block/tube_cap",
"dark_metal": "airdef:block/dark_metal",
"particle": "airdef:block/canister"
},
"elements": [
{
"from": [
-1.5,
0.0,
-16.0
],
"to": [
2.5,
4.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"west": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"up": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
},
"down": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
}
}
},
{
"from": [
-1.5,
5.0,
-16.0
],
"to": [
2.5,
9.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"west": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"up": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
},
"down": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
}
}
},
{
"from": [
3.5,
0.0,
-16.0
],
"to": [
7.5,
4.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"west": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"up": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
},
"down": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
}
}
},
{
"from": [
3.5,
5.0,
-16.0
],
"to": [
7.5,
9.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"west": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"up": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
},
"down": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
}
}
},
{
"from": [
8.5,
0.0,
-16.0
],
"to": [
12.5,
4.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"west": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"up": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
},
"down": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
}
}
},
{
"from": [
8.5,
5.0,
-16.0
],
"to": [
12.5,
9.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"west": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"up": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
},
"down": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
}
}
},
{
"from": [
13.5,
0.0,
-16.0
],
"to": [
17.5,
4.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"west": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"up": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
},
"down": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
}
}
},
{
"from": [
13.5,
5.0,
-16.0
],
"to": [
17.5,
9.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"west": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"up": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
},
"down": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
}
}
},
{
"from": [
-2.5,
0.0,
26.0
],
"to": [
18.5,
9.0,
28.0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
9.0
],
"texture": "#dark_metal"
},
"south": {
"uv": [
0,
0,
16,
9.0
],
"texture": "#dark_metal"
},
"east": {
"uv": [
0,
0,
2.0,
9.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
2.0,
9.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#dark_metal"
}
}
},
{
"from": [
-2.5,
0.0,
-2.0
],
"to": [
18.5,
9.0,
0.0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
9.0
],
"texture": "#dark_metal"
},
"south": {
"uv": [
0,
0,
16,
9.0
],
"texture": "#dark_metal"
},
"east": {
"uv": [
0,
0,
2.0,
9.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
2.0,
9.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#dark_metal"
}
}
}
]
}
@@ -0,0 +1,555 @@
{
"ambientocclusion": false,
"textures": {
"canister": "airdef:block/canister",
"tube_cap": "airdef:block/tube_cap",
"dark_metal": "airdef:block/dark_metal",
"particle": "airdef:block/canister"
},
"elements": [
{
"from": [
1.0,
0.0,
-16.0
],
"to": [
5.0,
4.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"west": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"up": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
},
"down": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
}
}
},
{
"from": [
1.0,
5.0,
-16.0
],
"to": [
5.0,
9.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"west": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"up": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
},
"down": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
}
}
},
{
"from": [
6.0,
0.0,
-16.0
],
"to": [
10.0,
4.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"west": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"up": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
},
"down": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
}
}
},
{
"from": [
6.0,
5.0,
-16.0
],
"to": [
10.0,
9.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"west": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"up": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
},
"down": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
}
}
},
{
"from": [
11.0,
0.0,
-16.0
],
"to": [
15.0,
4.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"west": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"up": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
},
"down": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
}
}
},
{
"from": [
11.0,
5.0,
-16.0
],
"to": [
15.0,
9.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"west": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"up": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
},
"down": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
}
}
},
{
"from": [
0.0,
0.0,
26.0
],
"to": [
16.0,
9.0,
28.0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
9.0
],
"texture": "#dark_metal"
},
"south": {
"uv": [
0,
0,
16,
9.0
],
"texture": "#dark_metal"
},
"east": {
"uv": [
0,
0,
2.0,
9.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
2.0,
9.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#dark_metal"
}
}
},
{
"from": [
0.0,
0.0,
-2.0
],
"to": [
16.0,
9.0,
0.0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
9.0
],
"texture": "#dark_metal"
},
"south": {
"uv": [
0,
0,
16,
9.0
],
"texture": "#dark_metal"
},
"east": {
"uv": [
0,
0,
2.0,
9.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
2.0,
9.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#dark_metal"
}
}
}
]
}
@@ -0,0 +1,488 @@
{
"ambientocclusion": false,
"textures": {
"nato_green": "airdef:block/nato_green",
"dark_metal": "airdef:block/dark_metal",
"metal": "airdef:block/metal",
"tire": "airdef:block/tire",
"particle": "airdef:block/nato_green"
},
"elements": [
{
"from": [
-6.0,
5.0,
-10.0
],
"to": [
22.0,
9.0,
26.0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#nato_green"
},
"south": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#nato_green"
},
"east": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#nato_green"
},
"west": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#nato_green"
},
"up": {
"uv": [
0,
0,
16,
16
],
"texture": "#nato_green"
},
"down": {
"uv": [
0,
0,
16,
16
],
"texture": "#nato_green"
}
}
},
{
"from": [
-4.0,
9.0,
10.0
],
"to": [
20.0,
13.0,
24.0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#dark_metal"
},
"south": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#dark_metal"
},
"east": {
"uv": [
0,
0,
14.0,
4.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
14.0,
4.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
16,
14.0
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
16,
14.0
],
"texture": "#dark_metal"
}
}
},
{
"from": [
6.0,
3.0,
-16.0
],
"to": [
10.0,
6.0,
-10.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
3.0
],
"texture": "#metal"
},
"south": {
"uv": [
0,
0,
4.0,
3.0
],
"texture": "#metal"
},
"east": {
"uv": [
0,
0,
6.0,
3.0
],
"texture": "#metal"
},
"west": {
"uv": [
0,
0,
6.0,
3.0
],
"texture": "#metal"
},
"up": {
"uv": [
0,
0,
4.0,
6.0
],
"texture": "#metal"
},
"down": {
"uv": [
0,
0,
4.0,
6.0
],
"texture": "#metal"
}
}
},
{
"from": [
-9.0,
0.0,
4.0
],
"to": [
-5.5,
9.0,
11.0
],
"faces": {
"north": {
"uv": [
0,
0,
3.5,
9.0
],
"texture": "#tire"
},
"south": {
"uv": [
0,
0,
3.5,
9.0
],
"texture": "#tire"
},
"east": {
"uv": [
0,
0,
7.0,
9.0
],
"texture": "#tire"
},
"west": {
"uv": [
0,
0,
7.0,
9.0
],
"texture": "#tire"
},
"up": {
"uv": [
0,
0,
3.5,
7.0
],
"texture": "#tire"
},
"down": {
"uv": [
0,
0,
3.5,
7.0
],
"texture": "#tire"
}
}
},
{
"from": [
21.5,
0.0,
4.0
],
"to": [
25.0,
9.0,
11.0
],
"faces": {
"north": {
"uv": [
0,
0,
3.5,
9.0
],
"texture": "#tire"
},
"south": {
"uv": [
0,
0,
3.5,
9.0
],
"texture": "#tire"
},
"east": {
"uv": [
0,
0,
7.0,
9.0
],
"texture": "#tire"
},
"west": {
"uv": [
0,
0,
7.0,
9.0
],
"texture": "#tire"
},
"up": {
"uv": [
0,
0,
3.5,
7.0
],
"texture": "#tire"
},
"down": {
"uv": [
0,
0,
3.5,
7.0
],
"texture": "#tire"
}
}
},
{
"from": [
-9.0,
0.0,
14.0
],
"to": [
-5.5,
9.0,
21.0
],
"faces": {
"north": {
"uv": [
0,
0,
3.5,
9.0
],
"texture": "#tire"
},
"south": {
"uv": [
0,
0,
3.5,
9.0
],
"texture": "#tire"
},
"east": {
"uv": [
0,
0,
7.0,
9.0
],
"texture": "#tire"
},
"west": {
"uv": [
0,
0,
7.0,
9.0
],
"texture": "#tire"
},
"up": {
"uv": [
0,
0,
3.5,
7.0
],
"texture": "#tire"
},
"down": {
"uv": [
0,
0,
3.5,
7.0
],
"texture": "#tire"
}
}
},
{
"from": [
21.5,
0.0,
14.0
],
"to": [
25.0,
9.0,
21.0
],
"faces": {
"north": {
"uv": [
0,
0,
3.5,
9.0
],
"texture": "#tire"
},
"south": {
"uv": [
0,
0,
3.5,
9.0
],
"texture": "#tire"
},
"east": {
"uv": [
0,
0,
7.0,
9.0
],
"texture": "#tire"
},
"west": {
"uv": [
0,
0,
7.0,
9.0
],
"texture": "#tire"
},
"up": {
"uv": [
0,
0,
3.5,
7.0
],
"texture": "#tire"
},
"down": {
"uv": [
0,
0,
3.5,
7.0
],
"texture": "#tire"
}
}
}
]
}
@@ -0,0 +1,691 @@
{
"ambientocclusion": false,
"textures": {
"dark_metal": "airdef:block/dark_metal",
"missile": "airdef:block/missile",
"prop": "airdef:block/prop",
"particle": "airdef:block/dark_metal"
},
"elements": [
{
"from": [
6.5,
6.5,
-10.0
],
"to": [
9.5,
9.5,
10.0
],
"faces": {
"north": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#dark_metal"
},
"south": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#dark_metal"
},
"east": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#dark_metal"
}
}
},
{
"from": [
7.0,
7.0,
-14.0
],
"to": [
9.0,
9.0,
-10.0
],
"faces": {
"north": {
"uv": [
0,
0,
2.0,
2.0
],
"texture": "#missile"
},
"south": {
"uv": [
0,
0,
2.0,
2.0
],
"texture": "#missile"
},
"east": {
"uv": [
0,
0,
4.0,
2.0
],
"texture": "#missile"
},
"west": {
"uv": [
0,
0,
4.0,
2.0
],
"texture": "#missile"
},
"up": {
"uv": [
0,
0,
2.0,
4.0
],
"texture": "#missile"
},
"down": {
"uv": [
0,
0,
2.0,
4.0
],
"texture": "#missile"
}
}
},
{
"from": [
2.0,
2.0,
0.0
],
"to": [
8.0,
8.0,
1.5
],
"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,
1.5,
6.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
1.5,
6.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
6.0,
1.5
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
6.0,
1.5
],
"texture": "#dark_metal"
}
}
},
{
"from": [
-0.5,
-0.5,
0.0
],
"to": [
4.5,
4.5,
0.6
],
"faces": {
"north": {
"uv": [
0,
0,
5.0,
5.0
],
"texture": "#prop"
},
"south": {
"uv": [
0,
0,
5.0,
5.0
],
"texture": "#prop"
},
"east": {
"uv": [
0,
0,
0.6,
5.0
],
"texture": "#prop"
},
"west": {
"uv": [
0,
0,
0.6,
5.0
],
"texture": "#prop"
},
"up": {
"uv": [
0,
0,
5.0,
0.6
],
"texture": "#prop"
},
"down": {
"uv": [
0,
0,
5.0,
0.6
],
"texture": "#prop"
}
}
},
{
"from": [
2.0,
8.0,
0.0
],
"to": [
8.0,
14.0,
1.5
],
"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,
1.5,
6.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
1.5,
6.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
6.0,
1.5
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
6.0,
1.5
],
"texture": "#dark_metal"
}
}
},
{
"from": [
-0.5,
11.5,
0.0
],
"to": [
4.5,
16.5,
0.6
],
"faces": {
"north": {
"uv": [
0,
0,
5.0,
5.0
],
"texture": "#prop"
},
"south": {
"uv": [
0,
0,
5.0,
5.0
],
"texture": "#prop"
},
"east": {
"uv": [
0,
0,
0.6,
5.0
],
"texture": "#prop"
},
"west": {
"uv": [
0,
0,
0.6,
5.0
],
"texture": "#prop"
},
"up": {
"uv": [
0,
0,
5.0,
0.6
],
"texture": "#prop"
},
"down": {
"uv": [
0,
0,
5.0,
0.6
],
"texture": "#prop"
}
}
},
{
"from": [
8.0,
2.0,
0.0
],
"to": [
14.0,
8.0,
1.5
],
"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,
1.5,
6.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
1.5,
6.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
6.0,
1.5
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
6.0,
1.5
],
"texture": "#dark_metal"
}
}
},
{
"from": [
11.5,
-0.5,
0.0
],
"to": [
16.5,
4.5,
0.6
],
"faces": {
"north": {
"uv": [
0,
0,
5.0,
5.0
],
"texture": "#prop"
},
"south": {
"uv": [
0,
0,
5.0,
5.0
],
"texture": "#prop"
},
"east": {
"uv": [
0,
0,
0.6,
5.0
],
"texture": "#prop"
},
"west": {
"uv": [
0,
0,
0.6,
5.0
],
"texture": "#prop"
},
"up": {
"uv": [
0,
0,
5.0,
0.6
],
"texture": "#prop"
},
"down": {
"uv": [
0,
0,
5.0,
0.6
],
"texture": "#prop"
}
}
},
{
"from": [
8.0,
8.0,
0.0
],
"to": [
14.0,
14.0,
1.5
],
"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,
1.5,
6.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
1.5,
6.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
6.0,
1.5
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
6.0,
1.5
],
"texture": "#dark_metal"
}
}
},
{
"from": [
11.5,
11.5,
0.0
],
"to": [
16.5,
16.5,
0.6
],
"faces": {
"north": {
"uv": [
0,
0,
5.0,
5.0
],
"texture": "#prop"
},
"south": {
"uv": [
0,
0,
5.0,
5.0
],
"texture": "#prop"
},
"east": {
"uv": [
0,
0,
0.6,
5.0
],
"texture": "#prop"
},
"west": {
"uv": [
0,
0,
0.6,
5.0
],
"texture": "#prop"
},
"up": {
"uv": [
0,
0,
5.0,
0.6
],
"texture": "#prop"
},
"down": {
"uv": [
0,
0,
5.0,
0.6
],
"texture": "#prop"
}
}
}
]
}
@@ -0,0 +1,690 @@
{
"ambientocclusion": false,
"textures": {
"dark_metal": "airdef:block/dark_metal",
"tube_cap": "airdef:block/tube_cap",
"particle": "airdef:block/dark_metal"
},
"elements": [
{
"from": [
0.5,
0.0,
-16.0
],
"to": [
3.5,
3.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#dark_metal"
}
}
},
{
"from": [
0.5,
4.0,
-16.0
],
"to": [
3.5,
7.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#dark_metal"
}
}
},
{
"from": [
4.5,
0.0,
-16.0
],
"to": [
7.5,
3.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#dark_metal"
}
}
},
{
"from": [
4.5,
4.0,
-16.0
],
"to": [
7.5,
7.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#dark_metal"
}
}
},
{
"from": [
8.5,
0.0,
-16.0
],
"to": [
11.5,
3.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#dark_metal"
}
}
},
{
"from": [
8.5,
4.0,
-16.0
],
"to": [
11.5,
7.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#dark_metal"
}
}
},
{
"from": [
12.5,
0.0,
-16.0
],
"to": [
15.5,
3.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#dark_metal"
}
}
},
{
"from": [
12.5,
4.0,
-16.0
],
"to": [
15.5,
7.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#dark_metal"
}
}
},
{
"from": [
-0.5,
0.0,
26.0
],
"to": [
16.5,
7.0,
28.0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
7.0
],
"texture": "#dark_metal"
},
"south": {
"uv": [
0,
0,
16,
7.0
],
"texture": "#dark_metal"
},
"east": {
"uv": [
0,
0,
2.0,
7.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
2.0,
7.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#dark_metal"
}
}
},
{
"from": [
-0.5,
0.0,
-2.0
],
"to": [
16.5,
7.0,
0.0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
7.0
],
"texture": "#dark_metal"
},
"south": {
"uv": [
0,
0,
16,
7.0
],
"texture": "#dark_metal"
},
"east": {
"uv": [
0,
0,
2.0,
7.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
2.0,
7.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#dark_metal"
}
}
}
]
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,145 @@
{
"ambientocclusion": false,
"textures": {
"dark_metal": "airdef:block/dark_metal",
"particle": "airdef:block/dark_metal"
},
"elements": [
{
"from": [
2.0,
0.0,
2.0
],
"to": [
14.0,
3.0,
14.0
],
"faces": {
"north": {
"uv": [
0,
0,
12.0,
3.0
],
"texture": "#dark_metal"
},
"south": {
"uv": [
0,
0,
12.0,
3.0
],
"texture": "#dark_metal"
},
"east": {
"uv": [
0,
0,
12.0,
3.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
12.0,
3.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
12.0,
12.0
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
12.0,
12.0
],
"texture": "#dark_metal"
}
}
},
{
"from": [
6.0,
3.0,
6.0
],
"to": [
10.0,
12.0,
10.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
9.0
],
"texture": "#dark_metal"
},
"south": {
"uv": [
0,
0,
4.0,
9.0
],
"texture": "#dark_metal"
},
"east": {
"uv": [
0,
0,
4.0,
9.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
4.0,
9.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#dark_metal"
}
}
}
]
}
@@ -0,0 +1,146 @@
{
"ambientocclusion": false,
"textures": {
"dark_metal": "airdef:block/dark_metal",
"lamp": "airdef:block/lamp",
"particle": "airdef:block/dark_metal"
},
"elements": [
{
"from": [
3.0,
3.0,
-2.0
],
"to": [
13.0,
13.0,
14.0
],
"faces": {
"north": {
"uv": [
0,
0,
10.0,
10.0
],
"texture": "#dark_metal"
},
"south": {
"uv": [
0,
0,
10.0,
10.0
],
"texture": "#dark_metal"
},
"east": {
"uv": [
0,
0,
16,
10.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
16,
10.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
10.0,
16
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
10.0,
16
],
"texture": "#dark_metal"
}
}
},
{
"from": [
3.5,
3.5,
-2.5
],
"to": [
12.5,
12.5,
-2.0
],
"faces": {
"north": {
"uv": [
0,
0,
9.0,
9.0
],
"texture": "#lamp"
},
"south": {
"uv": [
0,
0,
9.0,
9.0
],
"texture": "#lamp"
},
"east": {
"uv": [
0,
0,
0.5,
9.0
],
"texture": "#lamp"
},
"west": {
"uv": [
0,
0,
0.5,
9.0
],
"texture": "#lamp"
},
"up": {
"uv": [
0,
0,
9.0,
0.5
],
"texture": "#lamp"
},
"down": {
"uv": [
0,
0,
9.0,
0.5
],
"texture": "#lamp"
}
}
}
]
}
@@ -0,0 +1,487 @@
{
"ambientocclusion": false,
"textures": {
"ru_khaki": "airdef:block/ru_khaki",
"dark_metal": "airdef:block/dark_metal",
"track": "airdef:block/track",
"particle": "airdef:block/ru_khaki"
},
"elements": [
{
"from": [
-10.0,
4.0,
-14.0
],
"to": [
26.0,
15.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
11.0
],
"texture": "#ru_khaki"
},
"south": {
"uv": [
0,
0,
16,
11.0
],
"texture": "#ru_khaki"
},
"east": {
"uv": [
0,
0,
16,
11.0
],
"texture": "#ru_khaki"
},
"west": {
"uv": [
0,
0,
16,
11.0
],
"texture": "#ru_khaki"
},
"up": {
"uv": [
0,
0,
16,
16
],
"texture": "#ru_khaki"
},
"down": {
"uv": [
0,
0,
16,
16
],
"texture": "#ru_khaki"
}
}
},
{
"from": [
-10.0,
4.0,
-16.0
],
"to": [
26.0,
10.0,
-14.0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
6.0
],
"texture": "#ru_khaki"
},
"south": {
"uv": [
0,
0,
16,
6.0
],
"texture": "#ru_khaki"
},
"east": {
"uv": [
0,
0,
2.0,
6.0
],
"texture": "#ru_khaki"
},
"west": {
"uv": [
0,
0,
2.0,
6.0
],
"texture": "#ru_khaki"
},
"up": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#ru_khaki"
},
"down": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#ru_khaki"
}
}
},
{
"from": [
-8.0,
15.0,
22.0
],
"to": [
24.0,
16.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
1.0
],
"texture": "#dark_metal"
},
"south": {
"uv": [
0,
0,
16,
1.0
],
"texture": "#dark_metal"
},
"east": {
"uv": [
0,
0,
10.0,
1.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
10.0,
1.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
16,
10.0
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
16,
10.0
],
"texture": "#dark_metal"
}
}
},
{
"from": [
-14.0,
0.0,
-15.0
],
"to": [
-10.0,
9.0,
31.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
9.0
],
"texture": "#track"
},
"south": {
"uv": [
0,
0,
4.0,
9.0
],
"texture": "#track"
},
"east": {
"uv": [
0,
0,
16,
9.0
],
"texture": "#track"
},
"west": {
"uv": [
0,
0,
16,
9.0
],
"texture": "#track"
},
"up": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#track"
},
"down": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#track"
}
}
},
{
"from": [
26.0,
0.0,
-15.0
],
"to": [
30.0,
9.0,
31.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
9.0
],
"texture": "#track"
},
"south": {
"uv": [
0,
0,
4.0,
9.0
],
"texture": "#track"
},
"east": {
"uv": [
0,
0,
16,
9.0
],
"texture": "#track"
},
"west": {
"uv": [
0,
0,
16,
9.0
],
"texture": "#track"
},
"up": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#track"
},
"down": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#track"
}
}
},
{
"from": [
-14.5,
5.0,
-15.0
],
"to": [
-10.0,
10.0,
31.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.5,
5.0
],
"texture": "#ru_khaki"
},
"south": {
"uv": [
0,
0,
4.5,
5.0
],
"texture": "#ru_khaki"
},
"east": {
"uv": [
0,
0,
16,
5.0
],
"texture": "#ru_khaki"
},
"west": {
"uv": [
0,
0,
16,
5.0
],
"texture": "#ru_khaki"
},
"up": {
"uv": [
0,
0,
4.5,
16
],
"texture": "#ru_khaki"
},
"down": {
"uv": [
0,
0,
4.5,
16
],
"texture": "#ru_khaki"
}
}
},
{
"from": [
26.0,
5.0,
-15.0
],
"to": [
30.5,
10.0,
31.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.5,
5.0
],
"texture": "#ru_khaki"
},
"south": {
"uv": [
0,
0,
4.5,
5.0
],
"texture": "#ru_khaki"
},
"east": {
"uv": [
0,
0,
16,
5.0
],
"texture": "#ru_khaki"
},
"west": {
"uv": [
0,
0,
16,
5.0
],
"texture": "#ru_khaki"
},
"up": {
"uv": [
0,
0,
4.5,
16
],
"texture": "#ru_khaki"
},
"down": {
"uv": [
0,
0,
4.5,
16
],
"texture": "#ru_khaki"
}
}
}
]
}
@@ -0,0 +1,760 @@
{
"ambientocclusion": false,
"textures": {
"ru_khaki": "airdef:block/ru_khaki",
"radar_face": "airdef:block/radar_face",
"dark_metal": "airdef:block/dark_metal",
"tube_cap": "airdef:block/tube_cap",
"particle": "airdef:block/ru_khaki"
},
"elements": [
{
"from": [
-4.0,
0.0,
-6.0
],
"to": [
20.0,
16.0,
22.0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
16
],
"texture": "#ru_khaki"
},
"south": {
"uv": [
0,
0,
16,
16
],
"texture": "#ru_khaki"
},
"east": {
"uv": [
0,
0,
16,
16
],
"texture": "#ru_khaki"
},
"west": {
"uv": [
0,
0,
16,
16
],
"texture": "#ru_khaki"
},
"up": {
"uv": [
0,
0,
16,
16
],
"texture": "#ru_khaki"
},
"down": {
"uv": [
0,
0,
16,
16
],
"texture": "#ru_khaki"
}
}
},
{
"from": [
-2.0,
2.0,
-11.0
],
"to": [
18.0,
15.0,
-6.0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
13.0
],
"texture": "#radar_face"
},
"south": {
"uv": [
0,
0,
16,
13.0
],
"texture": "#radar_face"
},
"east": {
"uv": [
0,
0,
5.0,
13.0
],
"texture": "#radar_face"
},
"west": {
"uv": [
0,
0,
5.0,
13.0
],
"texture": "#radar_face"
},
"up": {
"uv": [
0,
0,
16,
5.0
],
"texture": "#radar_face"
},
"down": {
"uv": [
0,
0,
16,
5.0
],
"texture": "#radar_face"
}
}
},
{
"from": [
-3.0,
16.0,
2.0
],
"to": [
19.0,
17.0,
20.0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
1.0
],
"texture": "#dark_metal"
},
"south": {
"uv": [
0,
0,
16,
1.0
],
"texture": "#dark_metal"
},
"east": {
"uv": [
0,
0,
16,
1.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
16,
1.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
16,
16
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
16,
16
],
"texture": "#dark_metal"
}
}
},
{
"from": [
-1.0,
16.5,
4.0
],
"to": [
3.0,
17.5,
10.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
1.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
4.0,
1.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
6.0,
1.0
],
"texture": "#tube_cap"
},
"west": {
"uv": [
0,
0,
6.0,
1.0
],
"texture": "#tube_cap"
},
"up": {
"uv": [
0,
0,
4.0,
6.0
],
"texture": "#tube_cap"
},
"down": {
"uv": [
0,
0,
4.0,
6.0
],
"texture": "#tube_cap"
}
}
},
{
"from": [
-1.0,
16.5,
12.0
],
"to": [
3.0,
17.5,
18.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
1.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
4.0,
1.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
6.0,
1.0
],
"texture": "#tube_cap"
},
"west": {
"uv": [
0,
0,
6.0,
1.0
],
"texture": "#tube_cap"
},
"up": {
"uv": [
0,
0,
4.0,
6.0
],
"texture": "#tube_cap"
},
"down": {
"uv": [
0,
0,
4.0,
6.0
],
"texture": "#tube_cap"
}
}
},
{
"from": [
4.0,
16.5,
4.0
],
"to": [
8.0,
17.5,
10.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
1.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
4.0,
1.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
6.0,
1.0
],
"texture": "#tube_cap"
},
"west": {
"uv": [
0,
0,
6.0,
1.0
],
"texture": "#tube_cap"
},
"up": {
"uv": [
0,
0,
4.0,
6.0
],
"texture": "#tube_cap"
},
"down": {
"uv": [
0,
0,
4.0,
6.0
],
"texture": "#tube_cap"
}
}
},
{
"from": [
4.0,
16.5,
12.0
],
"to": [
8.0,
17.5,
18.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
1.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
4.0,
1.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
6.0,
1.0
],
"texture": "#tube_cap"
},
"west": {
"uv": [
0,
0,
6.0,
1.0
],
"texture": "#tube_cap"
},
"up": {
"uv": [
0,
0,
4.0,
6.0
],
"texture": "#tube_cap"
},
"down": {
"uv": [
0,
0,
4.0,
6.0
],
"texture": "#tube_cap"
}
}
},
{
"from": [
9.0,
16.5,
4.0
],
"to": [
13.0,
17.5,
10.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
1.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
4.0,
1.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
6.0,
1.0
],
"texture": "#tube_cap"
},
"west": {
"uv": [
0,
0,
6.0,
1.0
],
"texture": "#tube_cap"
},
"up": {
"uv": [
0,
0,
4.0,
6.0
],
"texture": "#tube_cap"
},
"down": {
"uv": [
0,
0,
4.0,
6.0
],
"texture": "#tube_cap"
}
}
},
{
"from": [
9.0,
16.5,
12.0
],
"to": [
13.0,
17.5,
18.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
1.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
4.0,
1.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
6.0,
1.0
],
"texture": "#tube_cap"
},
"west": {
"uv": [
0,
0,
6.0,
1.0
],
"texture": "#tube_cap"
},
"up": {
"uv": [
0,
0,
4.0,
6.0
],
"texture": "#tube_cap"
},
"down": {
"uv": [
0,
0,
4.0,
6.0
],
"texture": "#tube_cap"
}
}
},
{
"from": [
14.0,
16.5,
4.0
],
"to": [
18.0,
17.5,
10.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
1.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
4.0,
1.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
6.0,
1.0
],
"texture": "#tube_cap"
},
"west": {
"uv": [
0,
0,
6.0,
1.0
],
"texture": "#tube_cap"
},
"up": {
"uv": [
0,
0,
4.0,
6.0
],
"texture": "#tube_cap"
},
"down": {
"uv": [
0,
0,
4.0,
6.0
],
"texture": "#tube_cap"
}
}
},
{
"from": [
14.0,
16.5,
12.0
],
"to": [
18.0,
17.5,
18.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
1.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
4.0,
1.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
6.0,
1.0
],
"texture": "#tube_cap"
},
"west": {
"uv": [
0,
0,
6.0,
1.0
],
"texture": "#tube_cap"
},
"up": {
"uv": [
0,
0,
4.0,
6.0
],
"texture": "#tube_cap"
},
"down": {
"uv": [
0,
0,
4.0,
6.0
],
"texture": "#tube_cap"
}
}
}
]
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "airdef:item/deploy_buk"
}
}
@@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "airdef:item/deploy_depot"
}
}
@@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "airdef:item/deploy_ew"
}
}
@@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "airdef:item/deploy_irist"
}
}
@@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "airdef:item/deploy_light"
}
}
@@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "airdef:item/deploy_nasams"
}
}
@@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "airdef:item/deploy_p1sun"
}
}
@@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "airdef:item/deploy_radar"
}
}
@@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "airdef:item/deploy_tor"
}
}
@@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "airdef:item/deploy_tzm"
}
}
@@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "airdef:item/flare_gun"
}
}
@@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "airdef:item/mover"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 342 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 374 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 328 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 404 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 397 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 400 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 400 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 388 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 395 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 386 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 403 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 401 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 400 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 386 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 404 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 407 B

After

Width:  |  Height:  |  Size: 411 B

+116 -12
View File
@@ -20,7 +20,7 @@ mod sim;
mod wave;
use sim::{Class, DroneKind, Ground, Out, Sam, SamKind, Sim};
const ITEMS: [&str; 17] = [
const ITEMS: [&str; 29] = [
"airdef:designator",
"airdef:launch_shahed",
"airdef:launch_geran3",
@@ -37,6 +37,18 @@ const ITEMS: [&str; 17] = [
"airdef:deploy_pantsir",
"airdef:deploy_s400",
"airdef:deploy_s500",
"airdef:deploy_irist",
"airdef:deploy_nasams",
"airdef:deploy_buk",
"airdef:deploy_tor",
"airdef:deploy_p1sun",
"airdef:deploy_radar",
"airdef:deploy_depot",
"airdef:deploy_tzm",
"airdef:deploy_ew",
"airdef:deploy_light",
"airdef:mover",
"airdef:flare_gun",
"airdef:remover",
];
@@ -58,6 +70,8 @@ struct AirDef {
replay: Option<replay::Player>,
/// The world drawn beyond the render distance (replays and cameras see far off).
far: bool,
/// The unit the move remote picked (its id), waiting for where to go.
moving: Option<u32>,
/// The world whose state is loaded (`game.world-id`), once one is.
world: Option<Option<String>>,
/// The systems' and objects' health as last saved, to save again when it changes.
@@ -198,6 +212,47 @@ impl AirDef {
None => say("Наведитесь на землю, где поставить комплекс"),
}
}
"airdef:mover" => {
let Some(at) = aim() else {
say("Наведитесь на технику или на точку на земле");
return;
};
let near = self
.sim
.sams
.iter()
.enumerate()
.filter(|(_, s)| s.pos.distance(at) < 6.0 && s.alive())
.min_by(|a, b| a.1.pos.distance(at).total_cmp(&b.1.pos.distance(at)))
.map(|(i, s)| (i, s.id));
match (near, self.moving) {
// A unit pointed at: picked (or another picked instead).
(Some((i, id)), _) => {
let s = &self.sim.sams[i];
if s.kind.mobile() {
self.moving = Some(id);
say(&format!("{} №{} выбран: укажите, куда ехать", s.kind.name(), i + 1));
} else {
say(&format!("{} не перемещается", s.kind.name()));
}
}
// The ground: the picked unit drives there.
(None, Some(id)) => {
if let Some(i) = self.sim.sams.iter().position(|s| s.id == id)
&& self.sim.order_move(i, at)
{
let s = &self.sim.sams[i];
say(&format!("{} №{}: свёртывание и марш, {}", s.kind.name(), i + 1, panel::distance(s.pos.distance(at))));
out.sound("minecraft:ui.button.click", at, 1.0, 1.0);
}
self.moving = None;
}
(None, None) => say("Сначала наведитесь на технику, которую перемещать"),
}
}
"airdef:flare_gun" => {
self.sim.fire_flare(eye(&p) + view(&p), view(&p), &mut out);
}
"airdef:remover" => {
let at = aim().unwrap_or(eye(&p));
if let Some(i) = self
@@ -244,9 +299,14 @@ impl AirDef {
"airdef:target" => (1.0, 0.1, 0.1, 0.5),
"airdef:tracer" => (1.0, 0.12, 0.05, 0.9),
"airdef:reentry" => (1.0, 0.6, 0.3, 1.4),
"airdef:flare" => (1.0, 0.94, 0.78, 1.2),
"airdef:beam" => (1.0, 0.97, 0.88, 0.15),
_ => continue,
};
p.extend([m.at.x as f32, m.at.y as f32, m.at.z as f32, m.scale * 1.6]);
// The shader lights about six times the radius round a light and draws a glow of
// its radius: a flare lights some sixty blocks round, the rest about their size.
let radius = if m.name == "airdef:flare" { 10.0 } else { m.scale * 1.6 };
p.extend([m.at.x as f32, m.at.y as f32, m.at.z as f32, radius]);
p.extend([r, g, b, i]);
lights += 1;
}
@@ -355,7 +415,7 @@ impl AirDef {
fn save(&mut self) {
let mut text = String::new();
for s in &self.sim.sams {
text += &format!("sam {} {} {} {} {} {} {}\n", s.kind.key(), s.pos.x, s.pos.y, s.pos.z, s.heading, s.roles, s.health);
text += &format!("sam {} {} {} {} {} {} {} {}\n", s.kind.key(), s.pos.x, s.pos.y, s.pos.z, s.heading, s.roles, s.health, s.cargo);
}
for (i, t) in self.sim.targets.iter().enumerate() {
let h = self.sim.target_health.get(i).copied().unwrap_or(100.0);
@@ -395,6 +455,9 @@ impl AirDef {
if let Some(h) = num(7) {
sam.health = h.clamp(0.0, 1.0);
}
if let Some(c) = num(8) {
sam.cargo = c as u32;
}
self.sim.sams.push(sam);
}
}
@@ -425,6 +488,19 @@ impl AirDef {
}
impl AirDef {
/// The EW stations: on if none is, else off.
fn toggle_ew(&mut self) {
let on = !self.sim.ew_active();
let (n, hot) = self.sim.set_ew(on);
if !self.sim.sams.iter().any(|s| s.kind == SamKind::Ew && s.alive()) {
say("Нет станций РЭБ: поставьте их пультом");
} else if on {
say(&format!("РЭБ включён: станций {n}{}", if hot > 0 { format!(", остывают {hot}") } else { String::new() }));
} else {
say("РЭБ выключен");
}
}
/// Where the open world's state is kept: a key of its own per world.
fn state_key(&self) -> String {
match self.world.as_ref().and_then(|w| w.as_ref()) {
@@ -528,13 +604,15 @@ impl AirDef {
/// monitored one always; those shown keep a head start, so the grid doesn't flicker.
fn pick_grid(&mut self) {
let shown = &self.grid_ids;
// Units that shoot, and the radars; depots, trucks and lights have nothing to show.
let mut scored: Vec<(f64, u32)> = self
.sim
.sams
.iter()
.enumerate()
.filter(|(_, s)| s.kind.armed() || s.kind == SamKind::Radar)
.map(|(i, s)| {
let busy = match s.state {
let busy = if self.sim.showing_kill(s) { 98.0 } else { match s.state {
sim::SamState::Launch | sim::SamState::Fire => 100.0,
sim::SamState::Lock => 85.0,
sim::SamState::Slew => 75.0,
@@ -543,7 +621,9 @@ impl AirDef {
sim::SamState::Search => 10.0,
sim::SamState::Hold => 5.0,
sim::SamState::Destroyed => if s.burning() { 90.0 } else { 1.0 },
};
sim::SamState::Waiting => 35.0,
sim::SamState::Packing | sim::SamState::Moving | sim::SamState::Deploying => 40.0,
} };
let near = self.sim.watched(s).map_or(0.0, |d| 15.0 * (1.0 - (d.pos.distance(s.pos) / s.kind.range()).min(1.0)));
let hurt = if s.alive() && s.health < 0.7 { 15.0 } else { 0.0 };
let keep = if shown.contains(&s.id) { 12.0 } else { 0.0 };
@@ -574,7 +654,7 @@ impl AirDef {
match page {
Page::Main => {
title = "AirDef — командный пункт (N меню, K монитор)".to_string();
let rows = SamKind::ALL
let rows = SamKind::ARMED
.iter()
.map(|k| {
let n = self.sim.sams.iter().filter(|s| s.kind == *k).count();
@@ -684,7 +764,7 @@ impl AirDef {
}
}
const HELP: &str = "/airdef menu | monitor | grid | replay [map|full|reel|stop] | raid | repair | import | target here | give | deploy <комплекс> | wave <тип|mix> [кол-во] [дальность] | clear | targets clear | damage on|off | shader on|off | hold on|off | roles [комплекс классы] | stats";
const HELP: &str = "/airdef menu | monitor | grid | replay [map|full|reel|stop] | raid | repair | import | ew on|off | flare | move <номер> | target here | give | deploy <комплекс> | wave <тип|mix> [кол-во] [дальность] | clear | targets clear | damage on|off | shader on|off | hold on|off | roles [комплекс классы] | stats";
impl Mod for AirDef {
fn init() -> Result<(Self, Registration), String> {
@@ -701,6 +781,7 @@ impl Mod for AirDef {
wave: wave::WaveConfig::default(),
replay: None,
far: true,
moving: None,
world: None,
health_mark: 0,
menu: None,
@@ -711,6 +792,7 @@ impl Mod for AirDef {
let _ = input::register_key("monitor", "Монитор ПВО: следующий комплекс", "key.keyboard.k");
let _ = input::register_key("grid", "Командный пункт: все комплексы", "key.keyboard.j");
let _ = input::register_key("replay", "Повтор налёта: карта / выход", "key.keyboard.r");
let _ = input::register_key("ew", "РЭБ: включить / выключить", "key.keyboard.u");
// The replay's own keys (they do nothing outside a replay).
let _ = input::register_key("rp_back", "Повтор: назад", "key.keyboard.left");
let _ = input::register_key("rp_forward", "Повтор: вперёд", "key.keyboard.right");
@@ -759,6 +841,7 @@ impl Mod for AirDef {
}
Event::KeyDown(k) if k == "monitor" => self.cycle_monitor(),
Event::KeyDown(k) if k == "grid" => self.grid = !self.grid,
Event::KeyDown(k) if k == "ew" => self.toggle_ew(),
Event::KeyDown(k) if k == "replay" => {
if self.replay.is_some() {
self.stop_replay();
@@ -780,6 +863,7 @@ impl Mod for AirDef {
self.ground.answer(render::ground(), now);
let mut out = Out::default();
self.listener = DVec3::new(p.x, p.y + 1.6, p.z);
self.sim.night = game::get_clock().is_some_and(|c| (13000..23000).contains(&(c.day_time % 24000)));
self.sim.step(&mut self.ground, Some(DVec3::new(p.x, p.y, p.z)), &mut out);
let wanted = self.ground.take_wanted();
if !wanted.is_empty() {
@@ -850,7 +934,7 @@ impl Mod for AirDef {
let page = self.menu.unwrap_or(Page::Main);
match (page, id) {
(Page::Main, MENU_KINDS) => {
if let Some(k) = SamKind::ALL.get(row as usize) {
if let Some(k) = SamKind::ARMED.get(row as usize) {
self.open_menu(Page::Roles(*k));
}
return;
@@ -1006,7 +1090,8 @@ impl Mod for AirDef {
}
["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")?;
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);
@@ -1050,6 +1135,25 @@ impl Mod for AirDef {
self.launch_wave(&mut out)?;
None
}
["move", n] => {
// Unit n (as the summary numbers them) drives to where the player stands.
let p = player().ok_or("Нет игрока")?;
let i = n.parse::<usize>().ok().and_then(|n| n.checked_sub(1)).ok_or("/airdef move <номер>")?;
if self.sim.order_move(i, DVec3::new(p.x, p.y, p.z)) {
Some(format!("{} №{}: на марш", self.sim.sams[i].kind.name(), i + 1))
} else {
Some("Эта техника не может ехать".into())
}
}
["ew", v @ ("on" | "off")] => {
let (n, hot) = self.sim.set_ew(*v == "on");
Some(format!("РЭБ: включено станций {n}{}", if hot > 0 { format!(", остывают {hot}") } else { String::new() }))
}
["flare"] => {
let p = player().ok_or("Нет игрока")?;
self.sim.fire_flare(eye(&p) + view(&p), view(&p), &mut out);
None
}
["import"] => {
// The state the mod kept before worlds had their own: brought into this world.
let before = self.sim.sams.len();
@@ -1085,7 +1189,7 @@ impl Mod for AirDef {
Some(format!("Разрушения: {}", if self.sim.damage { "вкл" } else { "выкл" }))
}
["roles"] => {
for kind in SamKind::ALL {
for kind in SamKind::ARMED {
let r = self.sim.roles.get(&kind).copied().unwrap_or(kind.spec().roles);
let names: Vec<&str> = Class::ALL.iter().filter(|c| r & c.bit() != 0).map(|c| c.name()).collect();
say(&format!("{}: {}", kind.name(), if names.is_empty() { "ничего".into() } else { names.join(", ") }));
@@ -1094,9 +1198,9 @@ impl Mod for AirDef {
}
["roles", who, what] => {
let kinds: Vec<SamKind> = if *who == "all" {
SamKind::ALL.to_vec()
SamKind::ARMED.to_vec()
} else {
vec![SamKind::from_key(who).ok_or("Комплекс: patriot, gepard, pantsir, s400, s500 или all")?]
vec![SamKind::from_key(who).ok_or("Комплекс: patriot, gepard, pantsir, s400, s500, irist, nasams, buk, tor, p1sun или all")?]
};
for kind in kinds {
let mask = if *what == "default" {
+4 -4
View File
@@ -53,9 +53,9 @@ const PICTURE: (u32, u32) = (320, 200);
/// target fills a good part of the picture; else ahead and a little up.
pub fn camera(sim: &Sim, sam: &Sam) -> CameraView {
let eye = sam.pos + DVec3::Y * 3.5;
let (dir, fov) = match sim.watched(sam) {
Some(d) => {
let rel = d.pos - eye;
let (dir, fov) = match sim.focus(sam) {
Some(p) => {
let rel = p - eye;
let dist = rel.length().max(1.0);
// About 14 blocks of view round a missile far off, wider when it is near.
let fov = (2.0 * (7.0 / dist).atan()).to_degrees().clamp(1.5, 50.0);
@@ -180,7 +180,7 @@ fn cell(sim: &Sim, index: usize, x: f32, y: f32, w: f32, h: f32, s: &mut Vec<Sha
if lock {
s.push(text("ЗАХВАТ", cx + cw / 2.0 - 18.0, cy + 3.0, color));
}
if !t.alive() {
if !t.alive() || sim.showing_kill(sam) {
s.push(text("ПОРАЖЕНА", cx + cw / 2.0 - 24.0, cy + ch / 2.0 + bh / 2.0 + 2.0, 0xFF60FF60));
}
let rel = t.pos - sam.pos;
+5 -1
View File
@@ -78,7 +78,11 @@ pub fn summary(sim: &Sim, air: &[&Drone], player: DVec3) -> Vec<Line> {
lines.push(line(format!("Перехватчиков в полёте: {}", sim.missiles.len()), YELLOW));
}
for sam in sim.sams.iter().take(8) {
let mut text = format!("{} [{}]", sam.status(), crate::sim::roles_text(sam.roles));
let mut text = if sam.kind.armed() {
format!("{} [{}]", sam.status(), crate::sim::roles_text(sam.roles))
} else {
sam.status()
};
if let Some(t) = sam.gun_target.or(sam.target).and_then(|t| sim.drones.iter().find(|d| d.id == t)) {
text += &format!(" > {} {}", t.kind.spec().name, distance(t.pos.distance(sam.pos)));
}
+17 -10
View File
@@ -19,6 +19,11 @@ impl MissileKind {
MissileKind::M57e6 => 0.45,
MissileKind::M48n6 => 1.8,
MissileKind::M77n6 => 2.2,
MissileKind::IrisT => 0.6,
MissileKind::Amraam => 0.5,
MissileKind::M9m317 => 1.2,
MissileKind::M9m338 => 0.4,
MissileKind::P1sun => 0.1,
}
}
}
@@ -82,11 +87,16 @@ impl Sim {
let pos = sam.pos + DVec3::Y;
out.messages.push(format!("{} №{} УНИЧТОЖЕН", sam.kind.name(), i + 1));
let (id, kind) = (sam.id, sam.kind);
let charge = spec.missile.as_ref().map_or(0.0, |m| m.kind.charge() * missiles as f64) + bursts as f64 / 60.0;
// A depot's stock (or a truck's load) goes up with it.
let stock = if matches!(self.sams[i].kind, SamKind::Depot | SamKind::Tzm) { self.sams[i].cargo } else { 0 };
self.sams[i].cargo = 0;
let missiles = missiles + stock * 2;
let charge = spec.missile.as_ref().map_or(0.0, |m| m.kind.charge() * missiles as f64) + bursts as f64 / 60.0
+ stock as f64 * 1.5;
// The vehicle's own fuel and the first of its load at once, then the rest cooking off.
let first = (1.0 + charge * 0.6).round().clamp(1.0, 8.0) as u32;
self.later.push((now + 2, pos, first, false));
for k in 0..missiles {
for k in 0..missiles.min(40) {
let when = now + 15 + (k as u64) * 12 + (self.rng.f() * 25.0) as u64;
let at = pos + DVec3::new(self.rng.s() * 3.0, self.rng.f() * 2.0, self.rng.s() * 3.0);
let tnt = if charge > 4.0 && self.rng.chance(0.4) { 2 } else { 1 };
@@ -186,14 +196,11 @@ impl Sim {
let hull = sam.pos + DVec3::Y * (0.5 * s - 0.35);
let side = DQuat::from_rotation_y(sam.heading) * DVec3::new(1.8 * s, 0.0, 0.6 * s);
let thrown = DQuat::from_rotation_y(sam.heading + 1.1) * DQuat::from_rotation_z(1.2);
let (body, part) = match sam.kind {
SamKind::Patriot => ("airdef:patriot_trailer", "airdef:patriot_launcher"),
SamKind::Gepard => ("airdef:gepard_hull", "airdef:gepard_turret"),
SamKind::Pantsir => ("airdef:pantsir_truck", "airdef:pantsir_turret"),
SamKind::S400 => ("airdef:s400_chassis", "airdef:s400_tubes"),
SamKind::S500 => ("airdef:s500_chassis", "airdef:s500_tubes"),
};
let parts = &sam.kind.spec().parts;
let (body, part) = (parts.body, parts.launcher.or(parts.pack));
out.model(sam.id, body, hull, tilt, s, false);
out.model(sam.id + 1, part, sam.pos + side + DVec3::Y * 0.4 * s, thrown, s, false);
if let Some(part) = part {
out.model(sam.id + 1, part, sam.pos + side + DVec3::Y * 0.4 * s, thrown, s, false);
}
}
}
+190
View File
@@ -0,0 +1,190 @@
//! Electronic warfare, and what the enemy learns.
//!
//! An EW station jams the satellite navigation of the drones and cruise
//! missiles near it (ballistic missiles and the Lancet's optics don't care):
//! they lose their route and drift off their aim. It runs only a short while
//! before its transmitter overheats, and while it transmits it gives itself
//! away. The enemy keeps what it learns — which systems fired and which
//! stations jammed (its next drones go for them), and from which bearings its
//! raids were shot down or got through (its next raid comes from the best).
use super::*;
/// How far a station jams.
pub const EW_RADIUS: f64 = 350.0;
/// Heat a tick on (20 s to overheat), and cooling a tick off (45 s from hot).
const HEAT: f64 = 1.0 / 400.0;
const COOL: f64 = 1.0 / 900.0;
/// After overheating it may switch on again only below this.
const READY: f64 = 0.3;
/// Approach sectors the enemy keeps score of.
pub const SECTORS: usize = 12;
#[derive(Default)]
pub(super) struct Intel {
/// Shot down and got through, by approach sector, over every raid.
pub kills: [u32; SECTORS],
pub hits: [u32; SECTORS],
/// Units that gave themselves away: (id, where, when) — launches, jamming.
pub revealed: Vec<(u32, DVec3, u64)>,
}
impl DroneKind {
/// It navigates by satellite: EW throws it off.
pub fn jammable(self) -> bool {
matches!(self, DroneKind::Shahed | DroneKind::Geran3 | DroneKind::Gerbera | DroneKind::Kh101 | DroneKind::Kalibr)
}
}
/// The sector of a bearing (radians).
pub fn sector(bearing: f64) -> usize {
((bearing.rem_euclid(TAU) / TAU) * SECTORS as f64) as usize % SECTORS
}
impl Sim {
/// Switches every EW station on or off; one cooling down after overheating stays off.
/// Returns how many are now on, and how many are still too hot.
pub fn set_ew(&mut self, on: bool) -> (u32, u32) {
let (mut n, mut hot) = (0, 0);
for s in self.sams.iter_mut().filter(|s| s.kind == SamKind::Ew && s.alive()) {
if on && s.ew_lock {
hot += 1;
continue;
}
s.ew_on = on && !s.moving();
n += u32::from(s.ew_on);
}
(n, hot)
}
pub fn ew_active(&self) -> bool {
self.sams.iter().any(|s| s.ew_on)
}
/// The stations' heat, and their jamming.
pub(super) fn step_ew(&mut self, out: &mut Out) {
let now = self.tick;
let mut on: Vec<DVec3> = Vec::new();
for s in self.sams.iter_mut().filter(|s| s.kind == SamKind::Ew) {
if !s.alive() || s.moving() {
s.ew_on = false;
}
if s.ew_on {
s.heat += HEAT;
if s.heat >= 1.0 {
s.heat = 1.0;
s.ew_on = false;
s.ew_lock = true;
out.messages.push("РЭБ: передатчик перегрет, выключен".into());
out.sound("minecraft:block.fire.extinguish", s.pos, 2.0, 0.7);
}
} else {
s.heat = (s.heat - COOL).max(0.0);
if s.ew_lock && s.heat < READY {
s.ew_lock = false;
}
}
if s.ew_on {
let top = s.pos + DVec3::Y * 2.6 * s.kind.spec().scale;
on.push(s.pos);
if now % 3 == 0 {
out.particles("minecraft:electric_spark", top, 0.6, DVec3::ZERO, 0.2, 3);
}
if now % 40 == 0 {
out.sound("minecraft:block.beacon.ambient", top, 2.0, 1.4);
}
// A transmitter on the air: the enemy finds it.
if now % 20 == 0 {
self.intel.revealed.push((s.id, s.pos, now));
}
}
}
if on.is_empty() {
return;
}
for d in self.drones.iter_mut().filter(|d| d.alive() && d.kind.jammable()) {
let Some(k) = on.iter().map(|p| 1.0 - horizontal(d.pos - *p).length() / EW_RADIUS).reduce(f64::max) else { continue };
if k <= 0.0 {
continue;
}
// Its fix wanders: the aim drifts, the route is lost.
d.route.clear();
d.target += DVec3::new(self.rng.s(), 0.0, self.rng.s()) * (1.2 * k);
d.jammed = true;
}
}
/// What the enemy's raids learned: their drones' fates by sector.
pub(super) fn learn(&mut self, sector: usize, killed: bool) {
if killed {
self.intel.kills[sector % SECTORS] += 1;
} else {
self.intel.hits[sector % SECTORS] += 1;
}
}
/// The bearing a smart raid comes from: the sector its raids got through best, a little random.
pub(super) fn best_bearing(&mut self) -> f64 {
let score = |i: usize| {
let (k, h) = (self.intel.kills[i] as f64, self.intel.hits[i] as f64);
(h + 1.0) / (k + h + 2.0)
};
let best = (0..SECTORS).max_by(|&a, &b| score(a).total_cmp(&score(b))).unwrap_or(0);
// Unknown sectors are worth a look too.
let pick = if self.rng.chance(0.3) { (self.rng.f() * SECTORS as f64) as usize } else { best };
(pick as f64 + 0.5 + self.rng.s() * 0.4) / SECTORS as f64 * TAU
}
/// Units the enemy knows of and would go for, newest first: (id, where).
pub(super) fn known_units(&self) -> Vec<(u32, DVec3)> {
let mut out: Vec<(u32, DVec3)> = Vec::new();
for &(id, at, _) in self.intel.revealed.iter().rev() {
if !out.iter().any(|o| o.0 == id) && self.sams.iter().any(|s| s.id == id && s.alive()) {
out.push((id, at));
}
}
out
}
/// During a raid: drones not yet launched go for what just gave itself away (a jamming station
/// first, then systems that fired), and drones whose system target is gone pick another.
pub(super) fn adapt(&mut self) {
if self.tick % 40 != 0 {
return;
}
self.intel.revealed.retain(|r| self.tick - r.2 < 6000);
let known = self.known_units();
let ew: Vec<(u32, DVec3)> =
known.iter().copied().filter(|(id, _)| self.sams.iter().any(|s| s.id == *id && s.kind == SamKind::Ew)).collect();
if known.is_empty() {
return;
}
let pick = |i: usize, list: &[(u32, DVec3)]| list[i % list.len()].1;
let mut n = 0usize;
for (_, d) in self.queue.iter_mut() {
let hunter = matches!(d.kind, DroneKind::Lancet | DroneKind::Kh22 | DroneKind::Zircon);
if !hunter && !(d.kind == DroneKind::Shahed && n % 4 == 0) {
n += 1;
continue;
}
// Only those not yet sent at a unit.
if self.sams.iter().any(|s| s.pos.distance(d.target) < 6.0) {
continue;
}
let to = if !ew.is_empty() { pick(n, &ew) } else { pick(n, &known) };
d.target = to;
d.route.clear();
n += 1;
}
// In flight: a target system destroyed — the next known one, or keep going.
for d in self.drones.iter_mut().filter(|d| d.alive()) {
let aimed_at_wreck = self.sams.iter().any(|s| !s.alive() && s.pos.distance(d.target) < 6.0);
if aimed_at_wreck {
let next = known.iter().min_by(|a, b| a.1.distance(d.pos).total_cmp(&b.1.distance(d.pos)));
if let Some((_, at)) = next {
d.target = *at;
d.route.clear();
}
}
}
}
}
+144
View File
@@ -0,0 +1,144 @@
//! Night fighting: guns aim by sight, and at night they hit far less unless
//! the target is lit. Searchlights follow the network's tracks with their
//! beams; illumination flares (fired by the guns on their own, or by the
//! player) hang over the sky for half a minute and light everything round.
use super::*;
/// A flare's life, how far it lights, and when it stops climbing.
const FLARE_LIFE: u32 = 600;
const FLARE_LIGHT: f64 = 90.0;
const FLARE_CLIMB: u32 = 28;
/// How far a searchlight's beam reaches.
pub const BEAM: f64 = 450.0;
/// Guns' hits at night on an unlit target.
pub const DARK_HITS: f64 = 0.4;
/// Ticks between a gun's own flares.
const FLARE_EVERY: u32 = 500;
pub struct Flare {
id: u32,
pub pos: DVec3,
vel: DVec3,
age: u32,
}
impl Sim {
/// Fires an illumination flare from `from` towards `dir`: it climbs, bursts and hangs.
pub fn fire_flare(&mut self, from: DVec3, dir: DVec3, out: &mut Out) {
let id = self.id();
let vel = dir.normalize_or(DVec3::Y) * 2.4 + DVec3::Y * 1.4;
self.flares.push(Flare { id, pos: from, vel, age: 0 });
out.sound("minecraft:entity.firework_rocket.launch", from, 2.0, 0.6);
if self.flares.len() > 12 {
self.flares.remove(0);
}
}
/// The flares and the searchlights, and what they light; the guns' own flares.
pub(super) fn step_lights(&mut self, out: &mut Out) {
self.lit.clear();
let now = self.tick;
// Flares: up, a burst, then down slowly, swaying, lighting all round.
let mut keep = Vec::with_capacity(self.flares.len());
for mut f in std::mem::take(&mut self.flares) {
f.age += 1;
if f.age < FLARE_CLIMB {
f.pos += f.vel;
f.vel = f.vel * 0.94 - DVec3::Y * 0.04;
} else {
if f.age == FLARE_CLIMB {
out.sound("minecraft:entity.firework_rocket.blast", f.pos, 3.0, 0.5);
out.particles("minecraft:firework", f.pos, 1.0, DVec3::ZERO, 0.15, 30);
}
let sway = (f.age as f64 * 0.05 + f.id as f64).sin() * 0.04;
f.pos += DVec3::new(sway, -0.06, sway * 0.5);
for d in self.drones.iter().filter(|d| d.alive() && d.pos.distance(f.pos) < FLARE_LIGHT) {
self.lit.push(d.id);
}
if now % 3 == 0 {
out.particles("minecraft:firework", f.pos, 0.2, DVec3::new(0.0, -0.05, 0.0), 0.02, 1);
}
}
let fade = if f.age > FLARE_LIFE - 60 { (FLARE_LIFE - f.age) as f64 / 60.0 } else { 1.0 };
out.model(f.id, "airdef:flare", f.pos, DQuat::IDENTITY, 1.4 * fade.max(0.05), true);
if f.age < FLARE_LIFE {
keep.push(f);
}
}
self.flares = keep;
// What the network tracks, for the searchlights.
let tracked: Vec<u32> = self.sams.iter().flat_map(|s| s.seen.iter().copied()).collect();
let gun_targets: Vec<u32> = self.sams.iter().filter_map(|s| s.gun_target).collect();
for i in 0..self.sams.len() {
if self.sams[i].kind != SamKind::Light || !self.sams[i].alive() || self.sams[i].moving() {
continue;
}
let (pos, id) = (self.sams[i].pos, self.sams[i].id);
if !self.night {
let sam = &mut self.sams[i];
sam.target = None;
sam.pitch += (0.0 - sam.pitch) * 0.05;
continue;
}
// The guns' target first, else the nearest tracked drone in reach.
let target = self
.drones
.iter()
.filter(|d| d.alive() && tracked.contains(&d.id) && d.pos.distance(pos) < BEAM)
.min_by(|a, b| {
let key = |d: &Drone| d.pos.distance(pos) * if gun_targets.contains(&d.id) { 0.3 } else { 1.0 };
key(a).total_cmp(&key(b))
})
.map(|d| (d.id, d.pos));
let sam = &mut self.sams[i];
let (head, _) = sam.mount();
let (want_yaw, want_pitch, reach) = match target {
Some((_, at)) => {
let to = at - head;
(heading_of(to), (to.y / to.length().max(0.01)).asin(), to.length())
}
// No target: a slow sweep of the sky.
None => (now as f64 * 0.01 + id as f64, 0.9 + (now as f64 * 0.013).sin() * 0.3, 260.0),
};
sam.yaw = slew(sam.yaw, want_yaw, 0.12);
sam.pitch = slew(sam.pitch, want_pitch.clamp(0.05, 1.5), 0.08);
sam.target = target.map(|t| t.0);
if let Some((t, _)) = target {
self.lit.push(t);
}
// The beam: segments widening with distance, out to the target.
let (head, rot) = self.sams[i].mount();
let dir = rot * DVec3::NEG_Z;
let mut along = 1.0;
for k in 0..10u32 {
if along >= reach {
break;
}
let size = 0.8 + along * 0.025;
let len = 3.0 * size;
out.model(id + 2 + k, "airdef:beam", head + dir * (along + len / 2.0), rot, size, true);
along += len;
}
}
// Guns at night light their own target with a flare now and then.
if self.night {
let mut fire: Vec<(DVec3, DVec3)> = Vec::new();
for sam in &mut self.sams {
sam.flare_cd = sam.flare_cd.saturating_sub(1);
if sam.kind.spec().gun.is_none() || !sam.alive() || sam.flare_cd > 0 {
continue;
}
let Some(t) = sam.gun_target.and_then(|t| self.drones.iter().find(|d| d.id == t)) else { continue };
if self.lit.contains(&t.id) || self.flares.iter().any(|f| f.pos.distance(t.pos) < FLARE_LIGHT) {
continue;
}
sam.flare_cd = FLARE_EVERY;
let from = sam.pos + DVec3::Y * 2.0;
fire.push((from, (t.pos + DVec3::Y * 25.0) - from));
}
for (from, dir) in fire {
self.fire_flare(from, dir, out);
}
}
}
}
+112
View File
@@ -0,0 +1,112 @@
//! Logistics: once a depot stands, magazines are no longer refilled from
//! nowhere. A system out of missiles waits; one within reach of a depot
//! reloads from its stock; the reload trucks fill up at a depot and drive
//! reloads to the systems that wait, nearest first, and back.
use super::*;
/// How near a depot a system reloads straight from it.
const DEPOT_REACH: f64 = 60.0;
/// How near a truck must come to hand a reload over, or to load up at a depot.
const HANDOVER: f64 = 10.0;
/// What a truck carries.
const TRUCK_LOAD: u32 = 2;
impl Sim {
/// Whether ammunition is counted: a depot stands (without one, magazines refill as before).
pub fn logistics(&self) -> bool {
self.sams.iter().any(|s| s.kind == SamKind::Depot && s.alive())
}
/// Reloads from depots near the systems, and the trucks' rounds.
pub(super) fn step_supply(&mut self, out: &mut Out) {
if !self.logistics() {
for s in &mut self.sams {
s.empty = false;
}
return;
}
let now = self.tick;
// Systems by a depot reload from it.
for i in 0..self.sams.len() {
if !self.sams[i].empty || !self.sams[i].alive() {
continue;
}
let pos = self.sams[i].pos;
if let Some(d) = self
.sams
.iter()
.position(|d| d.kind == SamKind::Depot && d.alive() && d.cargo > 0 && d.pos.distance(pos) < DEPOT_REACH)
{
self.sams[d].cargo -= 1;
self.reload_now(i, out);
}
}
// The trucks: a job each, when idle.
for t in 0..self.sams.len() {
let truck = &self.sams[t];
if truck.kind != SamKind::Tzm || !truck.alive() || truck.moving() || now % 10 != t as u64 % 10 {
continue;
}
let at = truck.pos;
// Where it was going: hand over, or load up.
if let Some(job) = truck.job
&& let Some(j) = self.sams.iter().position(|s| s.id == job)
{
if self.sams[j].pos.distance(at) <= HANDOVER {
if self.sams[j].kind == SamKind::Depot {
let take = TRUCK_LOAD.saturating_sub(self.sams[t].cargo).min(self.sams[j].cargo);
self.sams[j].cargo -= take;
self.sams[t].cargo += take;
out.sound("minecraft:block.piston.extend", at, 1.5, 0.8);
} else if self.sams[j].empty && self.sams[t].cargo > 0 {
self.sams[t].cargo -= 1;
self.reload_now(j, out);
}
self.sams[t].job = None;
} else if self.sams[j].alive() && (self.sams[j].empty || self.sams[j].kind == SamKind::Depot) {
continue; // still on its way (a new order follows when it arrives)
} else {
self.sams[t].job = None;
}
}
// A new job: the nearest waiting system no other truck is driving to, or a depot to load at.
let taken: Vec<u32> = self.sams.iter().filter(|s| s.kind == SamKind::Tzm).filter_map(|s| s.job).collect();
let next = if self.sams[t].cargo > 0 {
self.sams
.iter()
.filter(|s| s.empty && s.alive() && !s.moving() && !taken.contains(&s.id))
.min_by(|a, b| a.pos.distance(at).total_cmp(&b.pos.distance(at)))
.map(|s| (s.id, s.pos))
} else {
None
};
let next = next.or_else(|| {
(self.sams[t].cargo < TRUCK_LOAD)
.then(|| {
self.sams
.iter()
.filter(|d| d.kind == SamKind::Depot && d.alive() && d.cargo > 0)
.min_by(|a, b| a.pos.distance(at).total_cmp(&b.pos.distance(at)))
.map(|d| (d.id, d.pos))
})
.flatten()
});
if let Some((id, to)) = next {
self.sams[t].job = Some(id);
// Stop beside it, not on it.
let side = horizontal(at - to).normalize_or(DVec3::X) * (HANDOVER * 0.6);
if to.distance(at) > HANDOVER {
self.order_move(t, to + side);
}
}
}
}
/// System `i` gets a reload: its magazine fills after its reload time.
fn reload_now(&mut self, i: usize, out: &mut Out) {
let sam = &mut self.sams[i];
sam.empty = false;
sam.reloading = sam.kind.spec().reload.max(1);
out.sound("minecraft:block.piston.extend", sam.pos, 2.0, 0.6);
}
}
+266 -250
View File
@@ -13,6 +13,14 @@ mod damage;
mod raid;
pub mod record;
mod sight;
mod ew;
mod lights;
mod logistics;
mod orders;
mod units;
pub use ew::EW_RADIUS;
pub use units::{Family, SamKind};
use units::Muzzle;
pub use raid::{Approach, Aim, RaidOrder};
pub const TAU: f64 = std::f64::consts::TAU;
@@ -645,6 +653,10 @@ pub struct Drone {
route: Vec<DVec3>,
/// Hugs the land all the way, under the radars.
low: bool,
/// Its navigation is being jammed.
pub jammed: bool,
/// The approach sector it came from (the enemy keeps score).
sector: u8,
}
impl Drone {
@@ -667,6 +679,12 @@ pub enum MissileKind {
M57e6,
M48n6,
M77n6,
IrisT,
Amraam,
M9m317,
M9m338,
/// Not a missile: an interceptor drone, slow and cheap, that rams Shaheds.
P1sun,
}
struct MissileSpec {
@@ -695,6 +713,9 @@ impl MissileKind {
MissileKind::M57e6 => "airdef:launch.pantsir",
MissileKind::M48n6 => "airdef:launch.s400_eject",
MissileKind::M77n6 => "airdef:launch.s500_eject",
MissileKind::IrisT | MissileKind::M9m338 => "airdef:launch.s400_eject",
MissileKind::Amraam | MissileKind::M9m317 => "airdef:launch.patriot",
MissileKind::P1sun => "airdef:drone.lancet",
}
}
fn spec(self) -> &'static MissileSpec {
@@ -755,6 +776,77 @@ impl MissileKind {
flame: 1.6,
rear: 32.0,
},
MissileKind::IrisT => &MissileSpec {
model: "airdef:pac3",
speed: 7.0,
accel: 0.5,
turn: 0.35,
boost: 6,
cold: 4,
pk: 0.9,
fuse: 4.0,
life: 260,
scale: 0.95,
flame: 0.9,
rear: 28.0,
},
MissileKind::Amraam => &MissileSpec {
model: "airdef:pac3",
speed: 6.0,
accel: 0.45,
turn: 0.3,
boost: 6,
cold: 0,
pk: 0.85,
fuse: 4.0,
life: 260,
scale: 0.9,
flame: 0.9,
rear: 28.0,
},
MissileKind::M9m317 => &MissileSpec {
model: "airdef:m48n6",
speed: 6.5,
accel: 0.5,
turn: 0.25,
boost: 8,
cold: 0,
pk: 0.85,
fuse: 5.0,
life: 300,
scale: 1.1,
flame: 1.2,
rear: 32.0,
},
MissileKind::M9m338 => &MissileSpec {
model: "airdef:m57e6",
speed: 5.5,
accel: 0.6,
turn: 0.4,
boost: 4,
cold: 4,
pk: 0.9,
fuse: 3.0,
life: 160,
scale: 1.0,
flame: 0.8,
rear: 28.0,
},
MissileKind::P1sun => &MissileSpec {
model: "airdef:p1sun",
speed: 2.8,
accel: 0.12,
turn: 0.3,
boost: 4,
cold: 0,
pk: 0.8,
fuse: 3.0,
life: 900,
scale: 0.8,
// Electric: no flame.
flame: 0.0,
rear: 10.0,
},
}
}
}
@@ -772,120 +864,6 @@ pub struct Missile {
// ---------------------------------------------------------------- air defence
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
pub enum SamKind {
Patriot,
Gepard,
Pantsir,
S400,
S500,
}
struct MissileLoad {
kind: MissileKind,
magazine: u32,
min: f64,
max: f64,
/// Ticks between launches.
interval: u32,
}
struct GunSpec {
range: f64,
/// Starts tracking from here.
track: f64,
/// Chance per burst at point blank.
p: f64,
bursts: u32,
/// Shell speed for the lead, blocks a tick.
shell: f64,
slew: (f64, f64),
}
pub struct SamSpec {
pub name: &'static str,
pub key: &'static str,
missile: Option<MissileLoad>,
gun: Option<GunSpec>,
reload: u32,
scale: f64,
/// Where it points its launcher at rest (pitch, radians).
rest_pitch: f64,
/// Which classes of target it engages unless told otherwise.
pub roles: u8,
}
impl SamKind {
pub const ALL: [SamKind; 5] = [SamKind::Patriot, SamKind::Gepard, SamKind::Pantsir, SamKind::S400, SamKind::S500];
pub fn spec(self) -> &'static SamSpec {
match self {
SamKind::Patriot => &SamSpec {
name: "Patriot",
key: "patriot",
missile: Some(MissileLoad { kind: MissileKind::Pac3, magazine: 4, min: 10.0, max: 300.0, interval: 16 }),
gun: None,
reload: 220,
scale: 1.3,
rest_pitch: 0.66,
roles: 30,
},
SamKind::Gepard => &SamSpec {
name: "Гепард",
key: "gepard",
missile: None,
gun: Some(GunSpec { range: 90.0, track: 130.0, p: 0.3, bursts: 160, shell: 10.0, slew: (0.22, 0.16) }),
reload: 160,
scale: 1.25,
rest_pitch: 0.0,
roles: 7,
},
SamKind::Pantsir => &SamSpec {
name: "Панцирь-С1",
key: "pantsir",
missile: Some(MissileLoad { kind: MissileKind::M57e6, magazine: 12, min: 25.0, max: 150.0, interval: 14 }),
gun: Some(GunSpec { range: 60.0, track: 90.0, p: 0.25, bursts: 140, shell: 9.0, slew: (0.25, 0.18) }),
reload: 280,
scale: 1.3,
rest_pitch: 0.0,
roles: 15,
},
SamKind::S400 => &SamSpec {
name: "С-400",
key: "s400",
missile: Some(MissileLoad { kind: MissileKind::M48n6, magazine: 4, min: 30.0, max: 480.0, interval: 20 }),
gun: None,
reload: 420,
scale: 1.5,
rest_pitch: HALF_PI - 0.02,
roles: 30,
},
SamKind::S500 => &SamSpec {
name: "С-500",
key: "s500",
missile: Some(MissileLoad { kind: MissileKind::M77n6, magazine: 4, min: 40.0, max: 700.0, interval: 20 }),
gun: None,
reload: 520,
scale: 1.6,
rest_pitch: HALF_PI - 0.02,
roles: 24,
},
}
}
pub fn name(self) -> &'static str {
self.spec().name
}
pub fn key(self) -> &'static str {
self.spec().key
}
pub fn from_key(key: &str) -> Option<Self> {
Self::ALL.into_iter().find(|k| k.key() == key)
}
pub fn range(self) -> f64 {
let s = self.spec();
s.missile.as_ref().map_or(0.0, |m| m.max).max(s.gun.as_ref().map_or(0.0, |g| g.range))
}
}
/// What a system is doing, for the monitor.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum SamState {
@@ -898,6 +876,12 @@ pub enum SamState {
Reload,
Hold,
Destroyed,
/// A move order: packing up, on the march, deploying again.
Packing,
Moving,
Deploying,
/// Out of missiles, waiting for a reload truck (logistics on: a depot stands).
Waiting,
}
impl SamState {
@@ -912,6 +896,10 @@ impl SamState {
SamState::Reload => "ПЕРЕЗАРЯДКА",
SamState::Hold => "ОГОНЬ ЗАПРЕЩЁН",
SamState::Destroyed => "УНИЧТОЖЕН",
SamState::Packing => "СВЁРТЫВАНИЕ",
SamState::Moving => "НА МАРШЕ",
SamState::Deploying => "РАЗВЁРТЫВАНИЕ",
SamState::Waiting => "ЖДЁТ БК",
}
}
pub fn color(self) -> u32 {
@@ -924,6 +912,8 @@ impl SamState {
SamState::Reload => 0x80A0FF,
SamState::Hold => 0xA0A0A0,
SamState::Destroyed => 0xFF2020,
SamState::Packing | SamState::Moving | SamState::Deploying => 0xC0A0FF,
SamState::Waiting => 0xFF9040,
}
}
}
@@ -959,8 +949,28 @@ pub struct Sam {
pub health: f64,
/// Ticks its wreck still burns.
burn: u32,
/// The drones its radar holds this tick.
/// The drones it has tracks of this tick: its own radar's and the network's.
pub seen: Vec<u32>,
/// The drones its own radar (or optics) holds: guns need these.
pub own: Vec<u32>,
/// What it just shot down, where, and till when its camera stays on it.
pub kill_view: Option<(u32, DVec3, u64)>,
/// A move order's destination, and ticks left packing up or deploying.
pub dest: Option<DVec3>,
march: u32,
/// A depot's reloads in stock; a reload truck's on board.
pub cargo: u32,
/// A reload truck's job: the system it is driving to, or a depot.
job: Option<u32>,
/// Out of ammunition, waiting for a reload (logistics on).
pub empty: bool,
/// An EW station: jamming, and its transmitter's heat (0 cold, 1 forced off).
pub ew_on: bool,
pub heat: f64,
/// Overheated: off until it cools.
pub ew_lock: bool,
/// Ticks till a gun fires its next illumination flare.
flare_cd: u32,
}
impl Sam {
@@ -991,6 +1001,21 @@ impl Sam {
health: 1.0,
burn: 0,
seen: Vec::new(),
own: Vec::new(),
kill_view: None,
dest: None,
march: 0,
cargo: match kind {
SamKind::Depot => 30,
SamKind::Tzm => 2,
_ => 0,
},
job: None,
empty: false,
ew_on: false,
heat: 0.0,
ew_lock: false,
flare_cd: 0,
}
}
pub fn alive(&self) -> bool {
@@ -1000,67 +1025,29 @@ impl Sam {
fn fitness(&self) -> f64 {
0.45 + 0.55 * self.health.clamp(0.0, 1.0)
}
fn base(&self) -> DQuat {
DQuat::from_rotation_y(self.heading)
}
fn scale(&self) -> f64 {
self.kind.spec().scale
}
/// Where shells and missiles leave: the centre of the turret or the
/// launcher's pivot, and the rotation they leave with.
fn mount(&self) -> (DVec3, DQuat) {
let s = self.scale();
let base = self.base();
match self.kind {
SamKind::Patriot => (
self.pos + base * DVec3::new(0.0, 0.69 * s, 1.2 * s),
DQuat::from_rotation_y(self.yaw) * DQuat::from_rotation_x(self.pitch),
),
SamKind::Gepard => (
self.pos + DVec3::new(0.0, 1.55 * s, 0.0) + base * DVec3::new(0.0, 0.0, 0.25 * s),
DQuat::from_rotation_y(self.yaw) * DQuat::from_rotation_x(self.pitch),
),
SamKind::Pantsir => (
self.pos + DVec3::new(0.0, 1.35 * s, 0.0) + base * DVec3::new(0.0, 0.0, 0.69 * s),
DQuat::from_rotation_y(self.yaw) * DQuat::from_rotation_x(self.pitch),
),
SamKind::S400 | SamKind::S500 => (
self.pos + base * DVec3::new(0.0, 0.7 * s, 1.45 * s),
base * DQuat::from_rotation_x(self.pitch),
),
}
}
/// Where missile `n` of the magazine leaves (world), and which way.
fn muzzle(&self, n: u32) -> (DVec3, DVec3) {
let (at, rot) = self.mount();
let s = self.scale();
let local = match self.kind {
SamKind::Patriot => DVec3::new(
if n % 2 == 0 { -0.42 } else { 0.42 },
0.625 + if n % 4 < 2 { 0.28 } else { -0.28 },
-2.8,
),
SamKind::Pantsir => {
let side = if n % 2 == 0 { -0.94 } else { 0.94 };
let row = (n / 2) % 6;
DVec3::new(side + ((row % 3) as f64 - 1.0) * 0.15, 0.1 + if row < 3 { 0.15 } else { -0.15 }, -1.5)
}
SamKind::S400 => {
let i = n % 4;
// From the pivot (rear bottom of the pack) to the top of tube i.
DVec3::new(if i % 2 == 0 { -0.25 } else { 0.25 }, if i < 2 { 0.22 } else { 0.72 }, -3.0)
}
SamKind::S500 => DVec3::new(if n % 2 == 0 { -0.33 } else { 0.33 }, 0.3, -3.0),
SamKind::Gepard => DVec3::ZERO,
};
(at + rot * local * s, rot * DVec3::NEG_Z)
}
pub fn status(&self) -> String {
let spec = self.kind.spec();
let mut text = spec.name.to_string();
if !self.alive() {
return text + " УНИЧТОЖЕН";
}
if self.moving() {
return text + " " + &self.state.name().to_lowercase();
}
match self.kind {
SamKind::Depot => return format!("{text}: запас {} перезарядок", self.cargo),
SamKind::Tzm => return format!("{text}: на борту {}/2{}", self.cargo, if self.job.is_some() { ", в пути" } else { "" }),
SamKind::Ew => {
let state = if self.ew_on { "ГЛУШИТ" } else if self.ew_lock { "остывает" } else { "готова" };
return format!("{text}: {state}, нагрев {:.0}%", self.heat * 100.0);
}
SamKind::Radar => return format!("{text}: целей {}", self.own.len()),
SamKind::Light => return format!("{text}{}", if self.target.is_some() { ": в луче цель" } else { "" }),
_ => {}
}
if self.empty && self.missiles == 0 {
text += " ЖДЁТ БК";
}
if self.health < 1.0 {
text += &format!(" ({:.0}%)", self.health * 100.0);
}
@@ -1129,6 +1116,13 @@ pub struct Sim {
rec: Option<record::Recording>,
pub replays: Vec<record::Recording>,
recordings: u32,
/// What the enemy has learned.
intel: ew::Intel,
/// Night: guns need light (the mod sets it from the game's clock).
pub night: bool,
/// Illumination flares in the sky, and the drones lit this tick.
pub flares: Vec<lights::Flare>,
lit: Vec<u32>,
}
/// Ids: objects get multiples of 16, their parts the low bits.
@@ -1162,6 +1156,10 @@ impl Sim {
rec: None,
replays: Vec::new(),
recordings: 0,
intel: ew::Intel::default(),
night: false,
flares: Vec::new(),
lit: Vec::new(),
}
}
pub fn id(&mut self) -> u32 {
@@ -1172,6 +1170,12 @@ impl Sim {
/// What a system's camera follows: its guns' or missiles' target, else what its missiles in
/// flight are after.
pub fn watched(&self, sam: &Sam) -> Option<&Drone> {
// A kill stays in view a few seconds: the wreck falling, before the next target.
if let Some((id, _, until)) = sam.kill_view
&& self.tick < until
{
return self.drones.iter().find(|d| d.id == id);
}
let id = sam
.gun_target
.or(sam.target)
@@ -1179,6 +1183,22 @@ impl Sim {
self.drones.iter().find(|d| d.id == id)
}
/// Where a system's camera looks: what it watches, or where its last kill was while that
/// stays in view (the wreck may be gone).
pub fn focus(&self, sam: &Sam) -> Option<DVec3> {
if let Some((_, at, until)) = sam.kill_view
&& self.tick < until
{
return Some(self.watched(sam).map_or(at, |d| d.pos));
}
self.watched(sam).map(|d| d.pos)
}
/// Whether a system's camera is showing its kill.
pub fn showing_kill(&self, sam: &Sam) -> bool {
sam.kill_view.is_some_and(|(_, _, until)| self.tick < until)
}
pub fn nearest_target(&self, to: DVec3) -> Option<DVec3> {
self.targets.iter().copied().min_by(|a, b| a.distance_squared(to).total_cmp(&b.distance_squared(to)))
}
@@ -1208,6 +1228,8 @@ impl Sim {
engaged: 0,
route: Vec::new(),
low: false,
jammed: false,
sector: 0,
});
self.launched += 1;
}
@@ -1253,10 +1275,15 @@ impl Sim {
pub fn step(&mut self, ground: &mut Ground, player: Option<DVec3>, out: &mut Out) {
self.tick += 1;
self.plan_step(out);
self.adapt();
self.release_queue();
self.step_drones(ground, player, out);
self.step_later(ground, player, out);
self.step_moves(ground, out);
self.step_supply(out);
self.step_ew(out);
self.update_sight(ground);
self.step_lights(out);
self.step_sams(out);
self.step_missiles(out);
self.step_fx(ground, out);
@@ -1384,6 +1411,10 @@ impl Sim {
if d.flight == Flight::Cruise && d.route.len() <= 1 && horizontal(d.target - d.pos).length() < spec.dive {
d.flight = Flight::Dive;
}
// An interceptor coming: drones drop to the treetops (radars lose them sooner).
if d.engaged > 0 && matches!(spec.class, Class::Drone | Class::Jet) {
d.low = true;
}
// A planned route: the next point, then the next.
// Reached when near (as near as it can turn), or passed: behind it and close.
let radius = (spec.speed / spec.turn.max(0.01)).max(24.0);
@@ -1453,6 +1484,9 @@ impl Sim {
self.log(pos, record::EventKind::Split { drone });
}
for (drone, kind, pos, tnt) in impacts {
if let Some(sector) = self.drones.iter().find(|x| x.id == drone).map(|x| x.sector as usize) {
self.learn(sector, false);
}
let power = match tnt {
0 => 0,
1 | 2 => 1,
@@ -1483,6 +1517,8 @@ impl Sim {
engaged: 0,
route: Vec::new(),
low: false,
jammed: false,
sector: 0,
});
}
for d in &self.drones {
@@ -1597,6 +1633,12 @@ impl Sim {
if !self.drones[idx].alive() {
return;
}
let sector = self.drones[idx].sector as usize;
self.learn(sector, true);
let until = self.tick + 70;
if let Some(s) = self.sams.iter_mut().find(|s| s.id == by) {
s.kill_view = Some((self.drones[idx].id, self.drones[idx].pos, until));
}
if let Some((i, s)) = self.sams.iter().enumerate().find(|(_, s)| s.id == by) {
let (drone, kind, pos) = (self.drones[idx].id, self.drones[idx].kind, self.drones[idx].pos);
let kill = record::EventKind::Kill { drone, kind, sam: by, sam_kind: s.kind, sam_index: i, missile };
@@ -1630,13 +1672,14 @@ impl Sim {
fn step_sams(&mut self, out: &mut Out) {
if self.hold {
for i in 0..self.sams.len() {
if self.sams[i].alive() {
if self.sams[i].alive() && !self.sams[i].moving() {
self.sams[i].state = SamState::Hold;
}
self.draw_sam(i, out);
}
return;
}
let logistics = self.logistics();
let mut launches: Vec<(MissileKind, DVec3, DVec3, u32, u32)> = Vec::new();
let mut gun_hits: Vec<(u32, u32)> = Vec::new();
let mut tracers: Vec<(u32, DVec3, DVec3, u32)> = Vec::new();
@@ -1666,10 +1709,21 @@ impl Sim {
}
}
}
if self.sams[i].moving() {
continue;
}
if !self.sams[i].kind.armed() {
let sam = &mut self.sams[i];
sam.state = SamState::Search;
sam.target = None;
sam.gun_target = None;
continue;
}
let pos = self.sams[i].pos;
let me = self.sams[i].id;
let roles = self.sams[i].roles;
let seen = self.sams[i].seen.clone();
let own = self.sams[i].own.clone();
let allowed = |d: &Drone| d.alive() && roles & d.kind.spec().class.bit() != 0 && seen.contains(&d.id);
// Interceptors a target has or will have: in flight, and other systems' launchers on it.
let claimed = |d: &Drone| {
@@ -1699,7 +1753,7 @@ impl Sim {
self.sams[i].gun_target = self
.drones
.iter()
.filter(|d| allowed(d) && d.pos.distance(pos) < gun.track)
.filter(|d| allowed(d) && own.contains(&d.id) && d.pos.distance(pos) < gun.track)
.map(|d| {
let busy = 1.0 + 2.0 * guns_on(d) + if d.engaged > 0 { 1.5 } else { 0.0 };
(d.pos.distance(pos) * busy, d.id)
@@ -1713,10 +1767,16 @@ impl Sim {
sam.launched = sam.launched.saturating_sub(1);
let Some(aim) = aim else {
// Out of missiles and nothing to shoot: reload now, guns or not.
if spec.missile.is_some() && sam.missiles == 0 && sam.reloading == 0 {
sam.reloading = spec.reload;
if spec.missile.is_some() && sam.missiles == 0 && sam.reloading == 0 && !sam.empty {
if logistics { sam.empty = true } else { sam.reloading = spec.reload }
}
sam.state = if sam.reloading > 0 { SamState::Reload } else { SamState::Search };
sam.state = if sam.reloading > 0 {
SamState::Reload
} else if sam.empty {
SamState::Waiting
} else {
SamState::Search
};
sam.yaw = slew(sam.yaw, sam.heading, 0.03);
sam.pitch += (spec.rest_pitch - sam.pitch) * 0.05;
continue;
@@ -1729,9 +1789,12 @@ impl Sim {
let want_pitch = (to.y / to.length().max(0.01)).asin().clamp(-0.1, 1.45);
let (yaw_rate, pitch_rate) = spec.gun.as_ref().map_or((0.05, 0.05), |g| g.slew);
let (yaw_rate, pitch_rate) = (yaw_rate * fit, pitch_rate * fit);
match sam.kind {
SamKind::S400 | SamKind::S500 => {} // vertical launch: no aiming
SamKind::Patriot => sam.yaw = slew(sam.yaw, want_yaw, 0.12 * fit),
let cells = matches!(spec.muzzle, Muzzle::Cells);
match spec.family {
// Vertical launch: no aiming (the Tor's turret turns its radar to the target).
Family::Vertical if cells => sam.yaw = slew(sam.yaw, want_yaw, 0.15 * fit),
Family::Vertical => {}
Family::Trainable => sam.yaw = slew(sam.yaw, want_yaw, 0.12 * fit),
_ => {
sam.yaw = slew(sam.yaw, want_yaw, yaw_rate);
sam.pitch = slew(sam.pitch, want_pitch, pitch_rate);
@@ -1739,9 +1802,11 @@ impl Sim {
}
let aim_error = angle_diff(sam.yaw, want_yaw).abs()
+ if spec.gun.is_some() { (want_pitch - sam.pitch).abs() } else { 0.0 };
let vertical = matches!(sam.kind, SamKind::S400 | SamKind::S500);
let vertical = spec.family == Family::Vertical;
sam.state = if sam.reloading > 0 {
SamState::Reload
} else if sam.empty && sam.missiles == 0 && sam.bursts == 0 {
SamState::Waiting
} else if sam.launched > 0 {
SamState::Launch
} else if sam.flash > 0 {
@@ -1763,7 +1828,7 @@ impl Sim {
sam.flash = 2;
sam.recoil = 1.0;
if sam.bursts == 0 && sam.missiles == 0 {
sam.reloading = spec.reload;
if logistics { sam.empty = true } else { sam.reloading = spec.reload }
}
let (_, rot) = sam.mount();
let s = spec.scale;
@@ -1787,7 +1852,9 @@ impl Sim {
let event = if sam.kind == SamKind::Pantsir { "airdef:gun.pantsir" } else { "airdef:gun.gepard" };
out.sound(event, mount, 1.0, 0.95 + self.rng.f() as f32 * 0.1);
}
let p = gun.p * fit / t.kind.spec().stealth * (1.0 - dist / (gun.range * 1.3)).max(0.35);
// At night a gun sees what is lit (a searchlight, a flare) and little else.
let dark = if self.night && !self.lit.contains(&t.id) { lights::DARK_HITS } else { 1.0 };
let p = gun.p * fit * dark / t.kind.spec().stealth * (1.0 - dist / (gun.range * 1.3)).max(0.35);
if self.rng.chance(p) {
gun_hits.push((gt, sam.id));
} else {
@@ -1799,13 +1866,13 @@ impl Sim {
// Missiles.
if let (Some(load), Some(mt)) = (&spec.missile, sam.target) {
let ready = sam.cooldown == 0 && sam.reloading == 0 && sam.missiles > 0;
let aligned = match sam.kind {
SamKind::Patriot => {
let aligned = match spec.family {
Family::Trainable => {
let t = self.drones.iter().find(|d| d.id == mt);
// The missile turns after its boost: the launcher needn't be exact.
t.is_some_and(|t| angle_diff(sam.yaw, heading_of(t.pos - mount)).abs() < 0.45)
}
SamKind::Pantsir => aim.id == mt && aim_error < 0.2,
Family::Turret => aim.id == mt && aim_error < 0.2,
_ => true,
};
if ready && aligned {
@@ -1819,12 +1886,16 @@ impl Sim {
sam.fired += 1;
sam.cooldown = load.interval;
if sam.missiles == 0 && (spec.gun.is_none() || sam.bursts == 0) {
sam.reloading = spec.reload;
if logistics { sam.empty = true } else { sam.reloading = spec.reload }
}
}
}
}
for (kind, muzzle, dir, target, sam) in launches {
// A launch gives the system away.
if let Some(s) = self.sams.iter().find(|s| s.id == sam) {
self.intel.revealed.push((sam, s.pos, self.tick));
}
let spec = kind.spec();
let id = self.id();
let v0 = if spec.cold > 0 { 0.7 } else { 0.8 };
@@ -1858,69 +1929,6 @@ impl Sim {
}
}
fn draw_sam(&mut self, i: usize, out: &mut Out) {
if !self.sams[i].alive() {
self.draw_wreck(i, out);
return;
}
let sam = &self.sams[i];
let base = sam.base();
let s = sam.scale();
let up = DVec3::Y * 0.5 * s;
let id = sam.id;
let (mount, rot) = sam.mount();
let turret = DQuat::from_rotation_y(sam.yaw);
match sam.kind {
SamKind::Patriot => {
out.model(id, "airdef:patriot_trailer", sam.pos + up, base, s, false);
out.model(id + 1, "airdef:patriot_truck", sam.pos + up + base * DVec3::new(0.0, 0.0, -3.0 * s), base, s, false);
// The launcher's pivot is its back bottom edge (0, -0.5, 1.375).
out.model(id + 2, "airdef:patriot_launcher", mount - rot * DVec3::new(0.0, -0.5, 1.375) * s, rot, s, false);
// Its radar stands right behind the trailer, facing the same way.
out.model(id + 3, "airdef:patriot_radar", sam.pos + up + base * DVec3::new(0.0, 0.0, 3.6 * s), base, s, false);
}
SamKind::Gepard => {
out.model(id, "airdef:gepard_hull", sam.pos + up, base, s, false);
out.model(id + 1, "airdef:gepard_turret", mount + DVec3::Y * 0.12 * s, turret, s, false);
let recoil = rot * DVec3::new(0.0, 0.0, 0.15 * sam.recoil * s);
out.model(id + 2, "airdef:gepard_guns", mount + recoil, rot, s, false);
out.model(id + 3, "airdef:gepard_dish", mount + turret * DVec3::new(0.0, 0.95 * s, 0.6 * s), DQuat::from_rotation_y(sam.dish), s, false);
}
SamKind::Pantsir => {
out.model(id, "airdef:pantsir_truck", sam.pos + up, base, s, false);
out.model(id + 1, "airdef:pantsir_turret", mount - DVec3::Y * 0.2 * s, turret, s, false);
let recoil = rot * DVec3::new(0.0, 0.0, 0.1 * sam.recoil * s);
out.model(id + 2, "airdef:pantsir_pack", mount + DVec3::Y * 0.1 * s + recoil, rot, s, false);
out.model(id + 3, "airdef:pantsir_dish", mount + turret * DVec3::new(0.0, 0.85 * s, 0.3 * s), DQuat::from_rotation_y(sam.dish) * DQuat::from_rotation_x(0.3), s, false);
}
SamKind::S400 | SamKind::S500 => {
let (chassis, tubes) = if sam.kind == SamKind::S400 {
("airdef:s400_chassis", "airdef:s400_tubes")
} else {
("airdef:s500_chassis", "airdef:s500_tubes")
};
out.model(id, chassis, sam.pos + up, base, s, false);
// The pack's pivot is its back bottom edge (0, -0.5, 1.5).
out.model(id + 1, tubes, mount - rot * DVec3::new(0.0, -0.5, 1.5) * s, rot, s, false);
// The battery's search radar behind it, turning.
let radar = sam.pos + DVec3::Y * 0.5 * s + base * DVec3::new(0.0, 0.0, 4.2 * s);
out.model(id + 2, "airdef:radar_trailer", radar, base, s, false);
out.model(id + 3, "airdef:radar_panel", radar + DVec3::Y * 0.9 * s, DQuat::from_rotation_y(sam.dish * 0.6) * DQuat::from_rotation_x(-0.3), s * 0.9, false);
}
}
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)],
SamKind::Gepard => &[DVec3::new(-0.92, -0.11, -1.7), DVec3::new(0.92, -0.11, -1.7)],
_ => &[],
};
for (k, b) in barrels.iter().enumerate() {
let r = rot * DQuat::from_rotation_z(self.rng.f() * TAU);
out.model(id + 4 + k as u32, "airdef:flash", mount + rot * *b * s, r, 0.9, true);
}
}
}
// ------------------------------------------------ interceptors
fn step_missiles(&mut self, out: &mut Out) {
@@ -1971,13 +1979,21 @@ impl Sim {
done.push((m.id, None, false, m.sam));
continue;
}
if lit {
if lit && spec.flame == 0.0 {
if m.age % 20 == 1 {
out.sound("airdef:drone.lancet", m.pos, 0.8, 1.6);
}
} else if lit {
if (m.age - spec.cold) % 46 == 1 {
let pitch = match m.kind {
MissileKind::Pac3 => 1.0,
MissileKind::M57e6 => 1.12,
MissileKind::M48n6 => 0.88,
MissileKind::M77n6 => 0.8,
MissileKind::IrisT | MissileKind::Amraam => 1.05,
MissileKind::M9m317 => 0.9,
MissileKind::M9m338 => 1.15,
MissileKind::P1sun => 1.7,
};
out.sound("airdef:missile.flight", m.pos, 1.0, pitch);
}
@@ -2017,7 +2033,7 @@ impl Sim {
let spec = m.kind.spec();
let rot = look(m.vel, 0.0) * DQuat::from_rotation_z(m.age as f64 * 0.3);
out.model(m.id, spec.model, m.pos, rot, spec.scale, false);
if m.age > spec.cold {
if m.age > spec.cold && spec.flame > 0.0 {
let back = (spec.rear - 8.0) / 16.0 * spec.scale + spec.flame;
let size = spec.flame * if m.age < 40 { 1.2 } else { 0.8 } * (0.85 + 0.15 * self.rng.f());
out.model(m.id + 1, "airdef:flame", m.pos + rot * DVec3::new(0.0, 0.0, back), rot, size, true);
+113
View File
@@ -0,0 +1,113 @@
//! Move orders: a unit packs up (its launcher down, radar silent), drives
//! over the land to the point it was given, and deploys there; it fights
//! again only once deployed. The reload trucks drive the same way.
use super::*;
/// Blocks a tick a vehicle drives (about 25 km/h).
const SPEED: f64 = 0.35;
impl SamKind {
/// Ticks to pack up for the march, and to deploy again.
pub fn march_ticks(self) -> u32 {
match self.spec().family {
Family::Vertical => 200,
Family::Trainable => 120,
Family::Static => 160,
Family::Turret | Family::Gun => 40,
Family::Light => 40,
Family::Truck => 10,
}
}
/// A depot doesn't drive.
pub fn mobile(self) -> bool {
self != SamKind::Depot
}
}
impl Sam {
/// Packing up, driving or deploying: not fighting.
pub fn moving(&self) -> bool {
matches!(self.state, SamState::Packing | SamState::Moving | SamState::Deploying)
}
}
impl Sim {
/// Orders unit `i` to drive to `to`. False for one that can't (a depot, a wreck).
pub fn order_move(&mut self, i: usize, to: DVec3) -> bool {
let Some(sam) = self.sams.get_mut(i) else { return false };
if !sam.alive() || !sam.kind.mobile() {
return false;
}
sam.dest = Some(to);
sam.target = None;
sam.gun_target = None;
if sam.state != SamState::Moving {
sam.state = SamState::Packing;
sam.march = sam.kind.march_ticks();
}
true
}
/// Packing, driving and deploying, once a tick.
pub(super) fn step_moves(&mut self, ground: &mut Ground, out: &mut Out) {
let now = self.tick;
for sam in &mut self.sams {
if !sam.alive() {
continue;
}
let spec = sam.kind.spec();
match sam.state {
SamState::Packing => {
// The launcher comes down for the march.
sam.pitch += (0.0 - sam.pitch) * 0.06;
sam.yaw = slew(sam.yaw, sam.heading, 0.04);
sam.march = sam.march.saturating_sub(1);
if sam.march == 0 {
sam.state = SamState::Moving;
}
}
SamState::Moving => {
let Some(to) = sam.dest else {
sam.state = SamState::Deploying;
sam.march = sam.kind.march_ticks();
continue;
};
let flat = horizontal(to - sam.pos);
if flat.length() < SPEED * 2.0 {
sam.pos = DVec3::new(to.x, sam.pos.y, to.z);
sam.dest = None;
sam.state = SamState::Deploying;
sam.march = sam.kind.march_ticks();
out.sound("minecraft:block.piston.contract", sam.pos, 2.0, 0.6);
continue;
}
// Turn towards it, then drive: slower while turning, over the land's height.
sam.heading = slew(sam.heading, heading_of(flat), 0.06);
let ahead = DQuat::from_rotation_y(sam.heading) * DVec3::NEG_Z;
let turning = angle_diff(sam.heading, heading_of(flat)).abs();
let step = SPEED * (1.0 - turning / std::f64::consts::PI).max(0.2);
let next = sam.pos + ahead * step;
let y = ground.at(next, now).unwrap_or(sam.pos.y);
sam.pos = DVec3::new(next.x, sam.pos.y + (y - sam.pos.y).clamp(-0.5, 0.5), next.z);
sam.yaw = sam.heading;
if now % 4 == 0 {
out.particles("minecraft:campfire_cosy_smoke", sam.pos - ahead * 2.0 * spec.scale, 0.3, DVec3::new(0.0, 0.03, 0.0), 0.01, 1);
}
if now % 30 == 0 {
out.sound("minecraft:entity.minecart.riding", sam.pos, 0.6, 0.5);
}
}
SamState::Deploying => {
sam.pitch += (spec.rest_pitch - sam.pitch) * 0.05;
sam.march = sam.march.saturating_sub(1);
if sam.march == 0 {
sam.state = SamState::Search;
sam.pitch = spec.rest_pitch;
sam.yaw = sam.heading;
}
}
_ => {}
}
}
}
}
+36 -7
View File
@@ -199,9 +199,10 @@ impl Sim {
if total == 0 {
return 0;
}
let bearing = self.rng.f() * TAU;
let second = bearing + std::f64::consts::PI * (0.6 + self.rng.f() * 0.6);
let smart = order.aim == Aim::Smart;
// A smart enemy comes from where its raids got through best.
let bearing = if smart { self.best_bearing() } else { self.rng.f() * TAU };
let second = bearing + std::f64::consts::PI * (0.6 + self.rng.f() * 0.6);
// One grid over the whole raid; each kind's prices are worked out once, routes are shared
// by the drones of a kind going from the same bearing to the same target.
let span = order.dist + 450.0;
@@ -222,7 +223,8 @@ impl Sim {
let dist = spec.ballistic.map_or(order.dist, |far| far.max(order.dist)) + self.rng.f() * 50.0;
let height = if spec.ballistic.is_some() { 10.0 } else if spec.cruise > 0.0 { spec.cruise } else { 25.0 };
let from = centre + DVec3::new(a.sin() * dist, height + self.rng.s() * 6.0, a.cos() * dist);
let d = self.make_drone(kind, from, horizontal(aim - from), aim);
let mut d = self.make_drone(kind, from, horizontal(aim - from), aim);
d.sector = ew::sector(a) as u8;
let routed = smart && spec.ballistic.is_none() && !spec.decoy && !at_sam.is_some_and(|_| kind == DroneKind::Lancet);
let key = routed.then(|| (kind, (a.rem_euclid(TAU) / 0.35) as i32, (target.x / 24.0) as i32, (target.z / 24.0) as i32));
// Decoys come 15 s ahead, the rest together; "waves" one kind after another.
@@ -344,6 +346,8 @@ impl Sim {
engaged: 0,
route: Vec::new(),
low: false,
jammed: false,
sector: 0,
}
}
@@ -365,9 +369,19 @@ impl Sim {
if aim == Aim::ObjectsAndSams {
return if roll < 0.33 { pick(&alive, r).map_or((object, None), |(p, id)| (p, Some(id))) } else { (object, None) };
}
// Smart: each kind at what it is good against.
let guns: Vec<&Sam> = alive.iter().copied().filter(|s| matches!(s.kind, SamKind::Gepard | SamKind::Pantsir)).collect();
let long: Vec<&Sam> = alive.iter().copied().filter(|s| matches!(s.kind, SamKind::S400 | SamKind::S500 | SamKind::Patriot)).collect();
// Smart: each kind at what it is good against; what gave itself away first (a jamming
// station, a radar, a system that fired).
let known = self.known_units();
let known_ew: Vec<&Sam> = alive.iter().copied().filter(|s| s.kind == SamKind::Ew && known.iter().any(|k| k.0 == s.id)).collect();
if matches!(kind, DroneKind::Lancet | DroneKind::Kh22) && !known_ew.is_empty() && roll < 0.6 {
let s = known_ew[(r * known_ew.len() as f64) as usize % known_ew.len()];
return (s.pos, Some(s.id));
}
// Short-range systems (guns, Tor, P1-SUN) for the Lancets; long-range ones and the radars
// that feed them for the fast missiles.
let guns: Vec<&Sam> = alive.iter().copied().filter(|s| s.kind.armed() && s.kind.range() <= 200.0).collect();
let long: Vec<&Sam> =
alive.iter().copied().filter(|s| s.kind.range() >= 300.0 || s.kind == SamKind::Radar).collect();
let choice = match kind {
DroneKind::Lancet if roll < 0.75 => pick(&guns, r).or_else(|| pick(&alive, r)),
DroneKind::Kh22 | DroneKind::Zircon if roll < 0.45 => pick(&long, r),
@@ -386,6 +400,21 @@ impl Sim {
let height = spec.clearance.max(6.0);
let n = grid.size;
let mut prices = vec![1.0f32; (n * n) as usize];
// Known jamming stations: drones that navigate by satellite keep away.
if kind.jammable() {
let known = self.known_units();
for s in self.sams.iter().filter(|s| s.kind == SamKind::Ew && s.alive() && known.iter().any(|k| k.0 == s.id)) {
let (ci, cj) = grid.index(s.pos);
let r = (EW_RADIUS / grid.cell).ceil() as i32;
for i in (ci - r).max(0)..=(ci + r).min(n - 1) {
for j in (cj - r).max(0)..=(cj + r).min(n - 1) {
if horizontal(grid.centre((i, j), s.pos.y) - s.pos).length() < EW_RADIUS {
prices[(i * n + j) as usize] += 12.0;
}
}
}
}
}
for s in self.sams.iter().filter(|s| s.alive() && s.roles & spec.class.bit() != 0) {
let sp = s.kind.spec();
let (lo, hi) = sp.missile.as_ref().map_or((0.0, 0.0), |m| (m.min, m.max));
@@ -518,7 +547,7 @@ mod big {
fn sixty_one_systems_and_a_big_smart_raid() {
let mut sim = Sim::new(11);
for i in 0..61 {
let kind = SamKind::ALL[i % 5];
let kind = SamKind::ARMED[i % 5];
let id = sim.id();
let p = DVec3::new(((i % 8) as f64 - 4.0) * 40.0, 70.0, ((i / 8) as f64 - 4.0) * 40.0);
sim.sams.push(Sam::new(id, kind, p, 0.0));
+1 -1
View File
@@ -237,7 +237,7 @@ impl Sim {
let spec = kind.spec();
let rot = look(dir, 0.0) * DQuat::from_rotation_z(t * 0.3);
out.model(id, spec.model, pos, rot, spec.scale, false);
if t - tr.first as f64 > spec.cold as f64 {
if t - tr.first as f64 > spec.cold as f64 && spec.flame > 0.0 {
let back = (spec.rear - 8.0) / 16.0 * spec.scale + spec.flame;
out.model(id + 1, "airdef:flame", pos + rot * DVec3::new(0.0, 0.0, back), rot, spec.flame, true);
}
+34 -12
View File
@@ -8,8 +8,8 @@ use super::*;
/// Rays round a radar, and blocks between the land's samples along them.
const RAYS: usize = 48;
const STEP: f64 = 16.0;
/// The antenna's height over the vehicle's ground.
const MAST: f64 = 5.0;
/// How far units share their tracks (the network's data links).
pub(super) const LINK: f64 = 1500.0;
/// Radio horizon: `HORIZON * (√mast + √height)` blocks.
const HORIZON: f64 = 35.0;
/// Anything this near is seen (and heard), whatever hides it.
@@ -58,16 +58,20 @@ impl Horizon {
}
}
/// Where a system's radar is.
/// Where a unit's radar antenna is.
pub(super) fn antenna(sam: &Sam) -> DVec3 {
sam.pos + DVec3::Y * MAST
sam.pos + DVec3::Y * sam.kind.spec().mast
}
/// How far a system's search radar reaches for this kind at `height` over the land:
/// its own reach (worse when damaged) for the target's size, under the radio horizon.
pub(super) fn reach(sam: &Sam, kind: DroneKind, height: f64) -> f64 {
let radar = sam.kind.range() * 1.3 * sam.fitness() * kind.rcs().powf(0.25);
let horizon = HORIZON * (MAST.sqrt() + height.max(0.0).sqrt());
let spec = sam.kind.spec();
if spec.sensor <= 0.0 {
return 0.0;
}
let radar = spec.sensor * sam.fitness() * kind.rcs().powf(0.25);
let horizon = HORIZON * (spec.mast.sqrt() + height.max(0.0).sqrt());
radar.min(horizon).max(NEAR)
}
@@ -77,11 +81,12 @@ impl Sim {
let now = self.tick;
// One horizon every few ticks: the land answers come in over time.
if !self.sams.is_empty() && now % 3 == 0 {
let alive: Vec<usize> = (0..self.sams.len()).filter(|&i| self.sams[i].alive()).collect();
if !alive.is_empty() {
let i = alive[(now / 3) as usize % alive.len()];
let radars: Vec<usize> =
(0..self.sams.len()).filter(|&i| self.sams[i].alive() && self.sams[i].kind.spec().sensor > 0.0).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.range() * 1.3, ground, now);
let h = Horizon::build(antenna(sam), sam.kind.spec().sensor, ground, now);
self.horizons.insert(sam.id, h);
}
}
@@ -91,7 +96,8 @@ impl Sim {
self.drones.iter().map(|d| ground.at(d.pos, now).map(|g| d.pos.y - g)).collect();
for sam in &mut self.sams {
sam.seen.clear();
if !sam.alive() {
sam.own.clear();
if !sam.alive() || sam.kind.spec().sensor <= 0.0 || sam.moving() {
continue;
}
let at = antenna(sam);
@@ -108,9 +114,25 @@ impl Sim {
if dist > NEAR && horizon.is_some_and(|hz| hz.hides(at, d.pos)) {
continue;
}
sam.seen.push(d.id);
sam.own.push(d.id);
}
}
// The network: every unit gets the tracks of the radars within data-link range.
let tracks: Vec<(DVec3, Vec<u32>)> =
self.sams.iter().filter(|s| !s.own.is_empty()).map(|s| (s.pos, s.own.clone())).collect();
for sam in &mut self.sams {
if !sam.alive() || sam.moving() || !sam.kind.armed() {
sam.seen = sam.own.clone();
continue;
}
let mut seen = sam.own.clone();
for (at, ids) in &tracks {
if at.distance(sam.pos) <= LINK {
seen.extend(ids.iter().copied().filter(|id| !seen.contains(id)).collect::<Vec<_>>());
}
}
sam.seen = seen;
}
}
/// Whether any system's radar holds this drone (the player's radar shows only those).
+690
View File
@@ -0,0 +1,690 @@
//! Every unit the player deploys, as data: the air-defence systems (their
//! weapons, radar and how their vehicles are put together and drawn) and the
//! units that serve them — long-range radars, ammunition depots, reload
//! trucks, EW stations and searchlights. They share a position, health,
//! wrecks, saving and move orders; only what they carry differs.
use super::*;
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
pub enum SamKind {
Patriot,
Gepard,
Pantsir,
S400,
S500,
IrisT,
Nasams,
Buk,
Tor,
P1sun,
/// A long-range search radar: no weapons, it feeds the network.
Radar,
/// An ammunition depot: the reload trucks fill up there.
Depot,
/// A transport-loader: drives missiles from a depot to the systems.
Tzm,
/// An electronic-warfare station: jams the drones' navigation while switched on.
Ew,
/// A searchlight: lights up targets at night for the guns.
Light,
}
/// How a vehicle is put together: how its launcher or turret moves and is drawn.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Family {
/// A launcher on a trailer that trains on the target (Patriot, NASAMS, P1-SUN).
Trainable,
/// A gun turret with a radar dish (Gepard).
Gun,
/// A turret with an elevating pack (Pantsir, Buk).
Turret,
/// Vertical launch from a raised pack (S-400, S-500, IRIS-T) or a turning turret (Tor).
Vertical,
/// Nothing that aims: a radar, a depot, an EW station.
Static,
/// A truck (the transport-loader).
Truck,
/// A searchlight.
Light,
}
/// Where missile `n` leaves the launcher, as the launcher's model lays its tubes out.
#[derive(Clone, Copy)]
pub enum Muzzle {
None,
Patriot,
Pantsir,
/// `cols × rows` tubes `size` model units wide, from the pack's pivot to its far end.
Grid(u32, u32, f64),
/// Missiles on rails, at these x (model units).
Rails(&'static [f64]),
/// The Tor's eight vertical cells on top of its turret.
Cells,
}
/// The models a unit is drawn with.
pub struct Parts {
/// The chassis, trailer or base.
pub body: &'static str,
/// The launcher, the tubes or the turret.
pub launcher: Option<&'static str>,
/// A turret's elevating pack or guns.
pub pack: Option<&'static str>,
/// A dish on the turret: model, height and forward offset (× scale), tilt.
pub dish: Option<(&'static str, f64, f64, f64)>,
/// Other fixed models: model, height, backward offset (× scale).
pub fixed: &'static [(&'static str, f64, f64)],
/// A turning antenna: model, height, backward offset, size, tilt.
pub spinner: Option<(&'static str, f64, f64, f64, f64)>,
}
pub(super) struct MissileLoad {
pub kind: MissileKind,
pub magazine: u32,
pub min: f64,
pub max: f64,
/// Ticks between launches.
pub interval: u32,
}
pub(super) struct GunSpec {
pub range: f64,
/// Starts tracking from here.
pub track: f64,
/// Chance per burst at point blank.
pub p: f64,
pub bursts: u32,
/// Shell speed for the lead, blocks a tick.
pub shell: f64,
pub slew: (f64, f64),
}
pub struct SamSpec {
pub name: &'static str,
pub key: &'static str,
pub(super) missile: Option<MissileLoad>,
pub(super) gun: Option<GunSpec>,
pub(super) reload: u32,
pub(super) scale: f64,
/// Where it points its launcher at rest (pitch, radians).
pub(super) rest_pitch: f64,
/// Which classes of target it engages unless told otherwise.
pub roles: u8,
pub family: Family,
pub parts: Parts,
/// The launcher's or turret's centre over the vehicle's centre: up and forward, × scale.
pub(super) mount: (f64, f64),
/// The launcher model's pivot from its centre (blocks).
pub(super) pivot: DVec3,
pub(super) muzzle: Muzzle,
/// Its own search radar's reach (0: none, it fires on the network's tracks), and the antenna's height.
pub sensor: f64,
pub mast: f64,
}
const NO_PARTS: Parts = Parts { body: "", launcher: None, pack: None, dish: None, fixed: &[], spinner: None };
/// The battery radar trailer behind a vertical launcher.
const BATTERY_RADAR: &[(&str, f64, f64)] = &[("airdef:radar_trailer", 0.0, 4.2)];
const BATTERY_SPINNER: Option<(&str, f64, f64, f64, f64)> = Some(("airdef:radar_panel", 0.9, 4.2, 0.9, -0.3));
const TUBE_PIVOT: DVec3 = DVec3::new(0.0, -0.5, 1.5);
impl SamKind {
pub const ALL: [SamKind; 15] = [
SamKind::Patriot,
SamKind::Gepard,
SamKind::Pantsir,
SamKind::S400,
SamKind::S500,
SamKind::IrisT,
SamKind::Nasams,
SamKind::Buk,
SamKind::Tor,
SamKind::P1sun,
SamKind::Radar,
SamKind::Depot,
SamKind::Tzm,
SamKind::Ew,
SamKind::Light,
];
/// The ones that shoot (the roles menu lists these).
pub const ARMED: [SamKind; 10] = [
SamKind::Patriot,
SamKind::Gepard,
SamKind::Pantsir,
SamKind::S400,
SamKind::S500,
SamKind::IrisT,
SamKind::Nasams,
SamKind::Buk,
SamKind::Tor,
SamKind::P1sun,
];
pub fn spec(self) -> &'static SamSpec {
match self {
SamKind::Patriot => {
const S: SamSpec = SamSpec {
name: "Patriot",
key: "patriot",
missile: Some(MissileLoad { kind: MissileKind::Pac3, magazine: 4, min: 10.0, max: 300.0, interval: 16 }),
gun: None,
reload: 220,
scale: 1.3,
rest_pitch: 0.66,
roles: 30,
family: Family::Trainable,
parts: Parts {
body: "airdef:patriot_trailer",
launcher: Some("airdef:patriot_launcher"),
fixed: &[("airdef:patriot_truck", 0.0, -3.0), ("airdef:patriot_radar", 0.0, 3.6)],
..NO_PARTS
},
mount: (0.69, 1.2),
pivot: DVec3::new(0.0, -0.5, 1.375),
muzzle: Muzzle::Patriot,
sensor: 390.0,
mast: 5.0,
};
&S
}
SamKind::Gepard => {
const S: SamSpec = SamSpec {
name: "Гепард",
key: "gepard",
missile: None,
gun: Some(GunSpec { range: 90.0, track: 130.0, p: 0.3, bursts: 160, shell: 10.0, slew: (0.22, 0.16) }),
reload: 160,
scale: 1.25,
rest_pitch: 0.0,
roles: 7,
family: Family::Gun,
parts: Parts {
body: "airdef:gepard_hull",
launcher: Some("airdef:gepard_turret"),
pack: Some("airdef:gepard_guns"),
dish: Some(("airdef:gepard_dish", 0.95, 0.6, 0.0)),
..NO_PARTS
},
mount: (1.55, 0.25),
pivot: DVec3::ZERO,
muzzle: Muzzle::None,
sensor: 170.0,
mast: 3.0,
};
&S
}
SamKind::Pantsir => {
const S: SamSpec = SamSpec {
name: "Панцирь-С1",
key: "pantsir",
missile: Some(MissileLoad { kind: MissileKind::M57e6, magazine: 12, min: 25.0, max: 150.0, interval: 14 }),
gun: Some(GunSpec { range: 60.0, track: 90.0, p: 0.25, bursts: 140, shell: 9.0, slew: (0.25, 0.18) }),
reload: 280,
scale: 1.3,
rest_pitch: 0.0,
roles: 15,
family: Family::Turret,
parts: Parts {
body: "airdef:pantsir_truck",
launcher: Some("airdef:pantsir_turret"),
pack: Some("airdef:pantsir_pack"),
dish: Some(("airdef:pantsir_dish", 0.85, 0.3, 0.3)),
..NO_PARTS
},
mount: (1.35, 0.69),
pivot: DVec3::ZERO,
muzzle: Muzzle::Pantsir,
sensor: 195.0,
mast: 4.0,
};
&S
}
SamKind::S400 => {
const S: SamSpec = SamSpec {
name: "С-400",
key: "s400",
missile: Some(MissileLoad { kind: MissileKind::M48n6, magazine: 4, min: 30.0, max: 480.0, interval: 20 }),
gun: None,
reload: 420,
scale: 1.5,
rest_pitch: HALF_PI - 0.02,
roles: 30,
family: Family::Vertical,
parts: Parts {
body: "airdef:s400_chassis",
launcher: Some("airdef:s400_tubes"),
fixed: BATTERY_RADAR,
spinner: BATTERY_SPINNER,
..NO_PARTS
},
mount: (0.7, 1.45),
pivot: TUBE_PIVOT,
muzzle: Muzzle::Grid(2, 2, 7.0),
sensor: 624.0,
mast: 5.0,
};
&S
}
SamKind::S500 => {
const S: SamSpec = SamSpec {
name: "С-500",
key: "s500",
missile: Some(MissileLoad { kind: MissileKind::M77n6, magazine: 4, min: 40.0, max: 700.0, interval: 20 }),
gun: None,
reload: 520,
scale: 1.6,
rest_pitch: HALF_PI - 0.02,
roles: 24,
family: Family::Vertical,
parts: Parts {
body: "airdef:s500_chassis",
launcher: Some("airdef:s500_tubes"),
fixed: BATTERY_RADAR,
spinner: BATTERY_SPINNER,
..NO_PARTS
},
mount: (0.7, 1.45),
pivot: TUBE_PIVOT,
muzzle: Muzzle::Grid(2, 1, 9.5),
sensor: 910.0,
mast: 5.0,
};
&S
}
SamKind::IrisT => {
const S: SamSpec = SamSpec {
name: "IRIS-T SLM",
key: "irist",
missile: Some(MissileLoad { kind: MissileKind::IrisT, magazine: 8, min: 15.0, max: 400.0, interval: 10 }),
gun: None,
reload: 300,
scale: 1.4,
rest_pitch: HALF_PI - 0.02,
roles: 15,
family: Family::Vertical,
parts: Parts {
body: "airdef:irist_chassis",
launcher: Some("airdef:irist_tubes"),
fixed: BATTERY_RADAR,
spinner: BATTERY_SPINNER,
..NO_PARTS
},
mount: (0.7, 1.45),
pivot: TUBE_PIVOT,
muzzle: Muzzle::Grid(4, 2, 4.0),
sensor: 520.0,
mast: 5.0,
};
&S
}
SamKind::Nasams => {
const S: SamSpec = SamSpec {
name: "NASAMS",
key: "nasams",
missile: Some(MissileLoad { kind: MissileKind::Amraam, magazine: 6, min: 10.0, max: 300.0, interval: 14 }),
gun: None,
reload: 260,
scale: 1.3,
rest_pitch: 0.5,
roles: 14,
family: Family::Trainable,
parts: Parts {
body: "airdef:nasams_trailer",
launcher: Some("airdef:nasams_launcher"),
fixed: &[("airdef:radar_trailer", 0.0, 3.4)],
spinner: Some(("airdef:radar_panel", 0.9, 3.4, 0.6, -0.3)),
..NO_PARTS
},
mount: (0.62, 0.4),
pivot: TUBE_PIVOT,
muzzle: Muzzle::Grid(3, 2, 4.0),
sensor: 390.0,
mast: 5.0,
};
&S
}
SamKind::Buk => {
const S: SamSpec = SamSpec {
name: "Бук-М2",
key: "buk",
missile: Some(MissileLoad { kind: MissileKind::M9m317, magazine: 4, min: 20.0, max: 450.0, interval: 18 }),
gun: None,
reload: 400,
scale: 1.4,
rest_pitch: 0.0,
roles: 14,
family: Family::Turret,
parts: Parts {
body: "airdef:buk_chassis",
launcher: Some("airdef:buk_turret"),
pack: Some("airdef:buk_rails"),
..NO_PARTS
},
mount: (1.55, 0.2),
pivot: DVec3::ZERO,
muzzle: Muzzle::Rails(&[-9.0, -3.0, 13.0, 19.0]),
sensor: 585.0,
mast: 4.0,
};
&S
}
SamKind::Tor => {
const S: SamSpec = SamSpec {
name: "Тор-М2",
key: "tor",
missile: Some(MissileLoad { kind: MissileKind::M9m338, magazine: 8, min: 8.0, max: 160.0, interval: 8 }),
gun: None,
reload: 300,
scale: 1.3,
rest_pitch: 0.0,
roles: 15,
family: Family::Vertical,
parts: Parts { body: "airdef:tor_chassis", launcher: Some("airdef:tor_turret"), ..NO_PARTS },
mount: (1.5, 0.0),
pivot: DVec3::ZERO,
muzzle: Muzzle::Cells,
sensor: 210.0,
mast: 3.5,
};
&S
}
SamKind::P1sun => {
const S: SamSpec = SamSpec {
name: "P1-SUN",
key: "p1sun",
missile: Some(MissileLoad { kind: MissileKind::P1sun, magazine: 8, min: 5.0, max: 450.0, interval: 12 }),
gun: None,
reload: 240,
scale: 1.1,
rest_pitch: 0.35,
roles: 1,
family: Family::Trainable,
parts: Parts { body: "airdef:nasams_trailer", launcher: Some("airdef:p1sun_launcher"), ..NO_PARTS },
mount: (0.6, 0.3),
pivot: TUBE_PIVOT,
muzzle: Muzzle::Grid(4, 2, 3.0),
// No radar of its own: it fires on the network's tracks.
sensor: 0.0,
mast: 2.0,
};
&S
}
SamKind::Radar => {
const S: SamSpec = SamSpec {
name: "РЛС дальнего обнаружения",
key: "radar",
missile: None,
gun: None,
reload: 0,
scale: 1.5,
rest_pitch: 0.0,
roles: 0,
family: Family::Static,
parts: Parts {
body: "airdef:radar_truck",
spinner: Some(("airdef:radar_panel", 1.2, 1.2, 1.5, -0.25)),
..NO_PARTS
},
mount: (0.0, 0.0),
pivot: DVec3::ZERO,
muzzle: Muzzle::None,
sensor: 1700.0,
mast: 16.0,
};
&S
}
SamKind::Depot => {
const S: SamSpec = SamSpec {
name: "Склад боеприпасов",
key: "depot",
missile: None,
gun: None,
reload: 0,
scale: 1.5,
rest_pitch: 0.0,
roles: 0,
family: Family::Static,
parts: Parts { body: "airdef:depot", ..NO_PARTS },
mount: (0.0, 0.0),
pivot: DVec3::ZERO,
muzzle: Muzzle::None,
sensor: 0.0,
mast: 1.0,
};
&S
}
SamKind::Tzm => {
const S: SamSpec = SamSpec {
name: "ТЗМ",
key: "tzm",
missile: None,
gun: None,
reload: 0,
scale: 1.3,
rest_pitch: 0.0,
roles: 0,
family: Family::Truck,
parts: Parts { body: "airdef:tzm_truck", ..NO_PARTS },
mount: (0.0, 0.0),
pivot: DVec3::ZERO,
muzzle: Muzzle::None,
sensor: 0.0,
mast: 2.0,
};
&S
}
SamKind::Ew => {
const S: SamSpec = SamSpec {
name: "Станция РЭБ",
key: "ew",
missile: None,
gun: None,
reload: 0,
scale: 1.3,
rest_pitch: 0.0,
roles: 0,
family: Family::Static,
parts: Parts { body: "airdef:ew_truck", ..NO_PARTS },
mount: (0.0, 0.0),
pivot: DVec3::ZERO,
muzzle: Muzzle::None,
sensor: 0.0,
mast: 3.0,
};
&S
}
SamKind::Light => {
const S: SamSpec = SamSpec {
name: "Прожектор",
key: "light",
missile: None,
gun: None,
reload: 0,
scale: 1.2,
rest_pitch: 0.6,
roles: 0,
family: Family::Light,
parts: Parts { body: "airdef:searchlight", launcher: Some("airdef:searchlight_head"), ..NO_PARTS },
mount: (0.85, 0.0),
pivot: DVec3::ZERO,
muzzle: Muzzle::None,
sensor: 0.0,
mast: 1.5,
};
&S
}
}
}
pub fn name(self) -> &'static str {
self.spec().name
}
pub fn key(self) -> &'static str {
self.spec().key
}
pub fn from_key(key: &str) -> Option<Self> {
Self::ALL.into_iter().find(|k| k.key() == key)
}
/// How far it reaches: its missiles or guns; 0 for a unit with neither.
pub fn range(self) -> f64 {
let s = self.spec();
s.missile.as_ref().map_or(0.0, |m| m.max).max(s.gun.as_ref().map_or(0.0, |g| g.range))
}
pub fn armed(self) -> bool {
let s = self.spec();
s.missile.is_some() || s.gun.is_some()
}
}
impl Sam {
pub(super) fn base(&self) -> DQuat {
DQuat::from_rotation_y(self.heading)
}
pub(super) fn scale(&self) -> f64 {
self.kind.spec().scale
}
/// Where shells and missiles leave: the centre of the turret or the launcher's pivot, and the
/// rotation they leave with.
pub(super) fn mount(&self) -> (DVec3, DQuat) {
let spec = self.kind.spec();
let s = spec.scale;
let base = self.base();
let (up, fwd) = spec.mount;
let aim = DQuat::from_rotation_y(self.yaw) * DQuat::from_rotation_x(self.pitch);
match spec.family {
Family::Trainable => (self.pos + base * DVec3::new(0.0, up * s, fwd * s), aim),
Family::Gun | Family::Turret | Family::Light => {
(self.pos + DVec3::new(0.0, up * s, 0.0) + base * DVec3::new(0.0, 0.0, fwd * s), aim)
}
Family::Vertical if matches!(spec.muzzle, Muzzle::Cells) => {
(self.pos + DVec3::new(0.0, up * s, 0.0), DQuat::from_rotation_y(self.yaw))
}
Family::Vertical => (self.pos + base * DVec3::new(0.0, up * s, fwd * s), base * DQuat::from_rotation_x(self.pitch)),
Family::Static | Family::Truck => (self.pos + DVec3::Y * s, base),
}
}
/// Where missile `n` of the magazine leaves (world), and which way.
pub(super) fn muzzle(&self, n: u32) -> (DVec3, DVec3) {
let (at, rot) = self.mount();
let s = self.scale();
let local = match self.kind.spec().muzzle {
Muzzle::Patriot => DVec3::new(
if n % 2 == 0 { -0.42 } else { 0.42 },
0.625 + if n % 4 < 2 { 0.28 } else { -0.28 },
-2.8,
),
Muzzle::Pantsir => {
let side = if n % 2 == 0 { -0.94 } else { 0.94 };
let row = (n / 2) % 6;
DVec3::new(side + ((row % 3) as f64 - 1.0) * 0.15, 0.1 + if row < 3 { 0.15 } else { -0.15 }, -1.5)
}
Muzzle::Grid(cols, rows, size) => {
// From the pivot (rear bottom of the pack) to the far end of tube n.
let (i, j) = (n % cols, (n / cols) % rows);
DVec3::new(
(i as f64 - (cols - 1) as f64 / 2.0) * (size + 1.0) / 16.0,
(j as f64 * (size + 1.0) + size / 2.0) / 16.0,
-3.0,
)
}
Muzzle::Rails(xs) => {
let x = xs[n as usize % xs.len()];
DVec3::new((x - 8.0) / 16.0, 0.1 - 2.5 / 16.0, -1.5)
}
Muzzle::Cells => {
let (i, j) = (n % 4, (n / 4) % 2);
DVec3::new((i as f64 * 5.0 + 1.0 - 8.0) / 16.0, 0.6, (j as f64 * 8.0 + 7.0 - 8.0) / 16.0)
}
Muzzle::None => DVec3::ZERO,
};
// Cells launch straight up whichever way the turret faces.
let dir = if matches!(self.kind.spec().muzzle, Muzzle::Cells) { DVec3::Y } else { rot * DVec3::NEG_Z };
(at + rot * local * s, dir)
}
}
impl Sim {
/// Draws unit `i`: its vehicle, its launcher or turret where it points, its antennas turning.
pub(super) fn draw_sam(&mut self, i: usize, out: &mut Out) {
if !self.sams[i].alive() {
self.draw_wreck(i, out);
return;
}
let sam = &self.sams[i];
let spec = sam.kind.spec();
let parts = &spec.parts;
let base = sam.base();
let s = spec.scale;
let up = DVec3::Y * 0.5 * s;
let id = sam.id;
let (mount, rot) = sam.mount();
let turret = DQuat::from_rotation_y(sam.yaw);
// A unit on the move is packed: launcher down, turret ahead.
out.model(id, parts.body, sam.pos + up, base, s, false);
let mut k = 1;
let mut part = |out: &mut Out, model: &'static str, at: DVec3, r: DQuat, size: f64| {
out.model(id + k, model, at, r, size, false);
k += 1;
};
match spec.family {
Family::Trainable => {
if let Some(l) = parts.launcher {
part(out, l, mount - rot * spec.pivot * s, rot, s);
}
}
Family::Vertical if !matches!(spec.muzzle, Muzzle::Cells) => {
if let Some(l) = parts.launcher {
part(out, l, mount - rot * spec.pivot * s, rot, s);
}
}
Family::Vertical => {
if let Some(l) = parts.launcher {
part(out, l, mount, turret, s);
}
}
Family::Gun => {
if let Some(l) = parts.launcher {
part(out, l, mount + DVec3::Y * 0.12 * s, turret, s);
}
let recoil = rot * DVec3::new(0.0, 0.0, 0.15 * sam.recoil * s);
if let Some(p) = parts.pack {
part(out, p, mount + recoil, rot, s);
}
}
Family::Turret => {
if let Some(l) = parts.launcher {
part(out, l, mount - DVec3::Y * 0.2 * s, turret, s);
}
let recoil = rot * DVec3::new(0.0, 0.0, 0.1 * sam.recoil * s);
if let Some(p) = parts.pack {
part(out, p, mount + DVec3::Y * 0.1 * s + recoil, rot, s);
}
}
Family::Light => {
if let Some(l) = parts.launcher {
part(out, l, mount, rot, s);
}
}
Family::Static | Family::Truck => {}
}
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);
}
for &(model, h, back) in parts.fixed {
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);
}
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)],
SamKind::Gepard => &[DVec3::new(-0.92, -0.11, -1.7), DVec3::new(0.92, -0.11, -1.7)],
_ => &[],
};
for (n, b) in barrels.iter().enumerate() {
let r = rot * DQuat::from_rotation_z(self.rng.f() * TAU);
out.model(id + 12 + n as u32, "airdef:flash", mount + rot * *b * s, r, 0.9, true);
}
}
}
}
+149
View File
@@ -221,6 +221,12 @@ TEXTURES = {
"tracer": lambda: noise((255, 40, 20), 6, 60),
"tracer_hot": lambda: noise((255, 190, 150), 6, 61),
"white": lambda: noise((250, 250, 246), 2, 19),
"nato_tan": lambda: camo([(150, 130, 96), (124, 106, 76), (96, 84, 62)], 62, blobs=5),
"crate": lambda: stripes((92, 78, 48), (70, 58, 34), 5, 63),
"concrete": lambda: panels((132, 130, 124), 64),
"lamp": lambda: gradient((255, 255, 240), (255, 230, 170)),
"beam": lambda: gradient((255, 250, 225), (255, 240, 200)),
"antenna": lambda: noise((70, 72, 70), 3, 65),
}
@@ -626,6 +632,118 @@ def kinzhal():
return p
def tracked(tex):
"""Гусеничное шасси (Бук, Тор): корпус, гусеницы и надгусеничные полки."""
p = [el((-10, 4, -14), (26, 15, 32), tex),
el((-10, 4, -16), (26, 10, -14), tex),
el((-8, 15, 22), (24, 16, 32), "dark_metal")]
p += both(el((-14, 0, -15), (-10, 9, 31), "track"))
p += both(el((-14.5, 5, -15), (-10, 10, 31), tex))
return p
def buk_turret():
return [el((-4, 0, -4), (20, 9, 20), "ru_green"),
el((1, 2, -10), (15, 14, -4), "radar_face"), # РЛС подсвета на башне
el((-2, 9, 6), (18, 11, 18), "ru_green")]
def buk_rails():
"""Четыре ракеты 9М317 на направляющих; ось качания — центр."""
p = []
for x in (-9, -3, 13, 19):
p += [el((x - 1.5, 4, -16), (x + 1.5, 7, 26), "missile_ru"),
el((x - 1.8, 3.5, 20), (x + 1.8, 7.5, 27), "booster"),
el((x - 0.5, 2, -6), (x + 0.5, 4, 16), "dark_metal")]
p += [el((-12, 0, -2), (28, 2, 14), "ru_green")]
return p
def tor_turret():
"""Башня Тора: блок вертикального пуска (8 ячеек сверху) и РЛС спереди; вращается целиком."""
p = [el((-4, 0, -6), (20, 16, 22), "ru_khaki"),
el((-2, 2, -11), (18, 15, -6), "radar_face"),
el((-3, 16, 2), (19, 17, 20), "dark_metal")]
for i in range(4):
for j in range(2):
x, z = -1 + i * 5, 4 + j * 8
p.append(el((x, 16.5, z), (x + 4, 17.5, z + 6), "tube_cap"))
return p
def nasams_trailer():
p = [el((-6, 5, -10), (22, 9, 26), "nato_green"),
el((-4, 9, 10), (20, 13, 24), "dark_metal"),
el((6, 3, -16), (10, 6, -10), "metal")]
return p + wheels((4, 14))
def p1sun():
"""Дрон-перехватчик: короткий корпус, X-образные лучи с моторами; нос — север."""
p = [el((6.5, 6.5, -10), (9.5, 9.5, 10), "dark_metal"),
el((7, 7, -14), (9, 9, -10), "missile")]
for dx, dy in ((-1, -1), (-1, 1), (1, -1), (1, 1)):
x, y = 8 + dx * 6, 8 + dy * 6
p += [el((min(8, x) , min(8, y), 0), (max(8, x), max(8, y), 1.5), "dark_metal", rot=None),
el((x - 2.5, y - 2.5, 0), (x + 2.5, y + 2.5, 0.6), "prop")]
return p
def radar_truck():
return maz_chassis("ru_khaki")
def depot():
"""Склад боеприпасов: бетонная площадка, штабели ящиков и контейнеров ракет."""
p = [el((-14, 0, -14), (30, 2, 30), "concrete")]
for x, z in ((-12, -12), (-12, 4), (4, -12)):
p.append(el((x, 2, z), (x + 12, 10, z + 12), "crate"))
for k in range(3):
p.append(el((6, 2 + k * 5, 6), (28, 7 + k * 5, 10), "tube", {"north": "tube_cap", "south": "tube_cap"}))
return p
def tzm_truck():
"""Транспортно-заряжающая машина: кабина, кузов с ракетами и кран."""
p = [el((-6, 5, -16), (22, 9, 32), "ru_green"),
el((-6, 9, -16), (22, 20, -6), "ru_green", {"north": "glass"}),
el((-4, 9, -2), (20, 12, 30), "dark_metal")]
for x in (-3, 5, 13):
p.append(el((x, 12, -1), (x + 6, 18, 29), "tube", {"north": "tube_cap", "south": "tube_cap"}))
p += [el((18, 12, -4), (21, 26, -1), "metal"), el((6, 24, -4), (21, 26, -1), "metal")]
return p + wheels((-12, -3, 16, 24))
def ew_truck():
"""Станция РЭБ: кузов-фургон, мачта с антенными решётками."""
p = [el((-6, 5, -16), (22, 9, 32), "ru_khaki"),
el((-6, 9, -16), (22, 19, -6), "ru_khaki", {"north": "glass"}),
el((-5, 9, -4), (21, 22, 30), "ru_khaki"),
el((7, 22, 10), (9, 32, 12), "antenna")]
for y in (24, 28):
p += both(el((-4, y, 9), (7, y + 3, 13), "antenna"))
return p + wheels((-12, -3, 16, 24))
def searchlight():
"""Прожектор на тумбе: тумба и голова отдельно (голова — `searchlight_head`)."""
return [el((2, 0, 2), (14, 3, 14), "dark_metal"), el((6, 3, 6), (10, 12, 10), "dark_metal")]
def searchlight_head():
"""Голова прожектора: нос (стекло) смотрит на север; ось — центр."""
return [el((3, 3, -2), (13, 13, 14), "dark_metal"), el((3.5, 3.5, -2.5), (12.5, 12.5, -2), "lamp")]
def beam():
"""Луч прожектора: длинная светящаяся трубка вдоль z, от z=32 к -16 (масштабируется по длине)."""
return [el((6.5, 6.5, -16), (9.5, 9.5, 32), "beam")]
def flare():
return [el((6.5, 6.5, 6.5), (9.5, 9.5, 9.5), "lamp"), el((7.6, 9.5, 7.6), (8.4, 16, 8.4), "antenna")]
MODELS = {
"shahed": (shahed, "Шахед-136"),
"shahed_prop": (shahed_prop, "Винт"),
@@ -668,6 +786,25 @@ MODELS = {
"kinzhal": (kinzhal, "Кинжал"),
"tracer": (lambda: [el((7.6, 7.6, -16), (8.4, 8.4, 32), "tracer"), el((7.3, 7.3, -16), (8.7, 8.7, -4), "tracer_hot")], "Трассер"),
# Боевой блок «Орешника»: раскалённое ядро и длинный плазменный хвост.
"irist_chassis": (lambda: maz_chassis("nato_green"), "Шасси IRIS-T SLM"),
"irist_tubes": (lambda: tubes(4, 2, 4, "canister"), "Пакет IRIS-T"),
"nasams_trailer": (nasams_trailer, "Прицеп NASAMS"),
"nasams_launcher": (lambda: tubes(3, 2, 4, "canister"), "Пусковая NASAMS"),
"buk_chassis": (lambda: tracked("ru_green"), "Шасси Бука"),
"buk_turret": (buk_turret, "Башня Бука"),
"buk_rails": (buk_rails, "Ракеты Бука"),
"tor_chassis": (lambda: tracked("ru_khaki"), "Шасси Тора"),
"tor_turret": (tor_turret, "Башня Тора"),
"p1sun_launcher": (lambda: tubes(4, 2, 3, "dark_metal"), "Пусковая P1-SUN"),
"p1sun": (p1sun, "Дрон-перехватчик P1-SUN"),
"radar_truck": (radar_truck, "Шасси РЛС"),
"depot": (depot, "Склад боеприпасов"),
"tzm_truck": (tzm_truck, "Транспортно-заряжающая машина"),
"ew_truck": (ew_truck, "Станция РЭБ"),
"searchlight": (searchlight, "Прожектор"),
"searchlight_head": (searchlight_head, "Голова прожектора"),
"beam": (beam, "Луч прожектора"),
"flare": (flare, "Осветительная ракета"),
"reentry": (lambda: [el((6.5, 6.5, -16), (9.5, 9.5, -8), "white"), el((7, 7, -8), (9, 9, 12), "tracer_hot"),
el((7.5, 7.5, 12), (8.5, 8.5, 32), "flame")], "Боевой блок"),
}
@@ -692,6 +829,18 @@ ITEMS = [
("launch_iskander", "Пуск: Искандер-М", (108, 110, 80)),
("launch_kinzhal", "Пуск: Кинжал", (60, 62, 60)),
("launch_oreshnik", "Пуск: Орешник", (230, 120, 40)),
("deploy_irist", "Развернуть IRIS-T SLM", (76, 86, 58)),
("deploy_nasams", "Развернуть NASAMS", (76, 86, 58)),
("deploy_buk", "Развернуть Бук-М2", (82, 96, 60)),
("deploy_tor", "Развернуть Тор-М2", (108, 110, 80)),
("deploy_p1sun", "Развернуть P1-SUN", (52, 54, 52)),
("deploy_radar", "Развернуть РЛС дальнего обнаружения", (108, 110, 80)),
("deploy_depot", "Развернуть склад боеприпасов", (92, 78, 48)),
("deploy_tzm", "Развернуть ТЗМ", (82, 96, 60)),
("deploy_ew", "Развернуть станцию РЭБ", (108, 110, 80)),
("deploy_light", "Развернуть прожектор", (52, 54, 52)),
("mover", "Пульт перемещения", (60, 140, 220)),
("flare_gun", "Осветительная ракета", (255, 240, 180)),
("remover", "Свернуть ПВО", (200, 200, 200)),
]