Compare commits

...

7 Commits

Author SHA1 Message Date
chexit 90379f6e17 Fixes from review: replays found by raid number, no fired-count underflow, one raid planned at a time, repair resets waiting and marches, clear removes cook-offs and flares, beams reach their targets, tracer direction never NaN, the fork's previous branch always kept
build / mod (push) Successful in 2m24s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 14:29:38 +03:00
chexit bb35975642 Fixes from review: one missile per target when systems fire in the same tick, moves and ammunition saved, searchlight spot and lighting once the beam is on, retargeting only for smart raids, no free reloads after the last depot falls, fork rebuilt when its commit or patches change
build / mod (push) Successful in 2m24s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 14:25:14 +03:00
chexit f2838a8f08 HD stage 2: sun and moon shadows, the world relit, light shafts, fog and a colour grade (fork patch 12)
build / mod (push) Successful in 2m28s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 13:59:51 +03:00
chexit c42d97af08 HD effects preset: mod lights for flames, flares, blasts, wrecks, searchlights; searchlight beams no longer add shader lights (FPS)
build / mod (push) Successful in 2m26s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 13:18:43 +03:00
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
chexit a2f31c831c CI: build on branch pushes only, not on release tags
build / mod (push) Successful in 5m7s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 12:19:03 +03:00
114 changed files with 16720 additions and 351 deletions
+4 -6
View File
@@ -5,7 +5,9 @@
name: build
on:
# Branches only: the release's own tag (build-N) mustn't start another build.
push:
branches: ['**']
workflow_dispatch:
env:
@@ -22,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: Мод
+49 -1
View File
@@ -68,6 +68,45 @@ 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 м.
**Графика: три пресета** (меню N → «Огонь, ремонт, эффекты…» → «Эффекты», или `/airdef shader off|on|hd`):
выкл — ванильная картинка; обычные — шейдерные огненные шары, ударная волна и подсветка; **HD** — свой рендер
освещения в форке: мир дополнительно рисуется в G-буфер (цвет без света и нормали), и все источники света мода
освещают его попиксельно в HDR — факелы ракет и двигатели, раскалённые боевые блоки, осветительные ракеты, вспышки
выстрелов, трассеры, взрывы (вспышка и догорающий шар), горящие остовы, пятна прожекторов, — затем bloom и мягкий
тонмаппинг (ванильный диапазон не трогается, сжимается только то, что ярче). Источники передаются движку новым
API мода `render.set-lights` (до 512, плавно интерполируются), режим — `render.set-lighting`.
**Сетка J** после сбития ещё 3,5 с держит камеру на сбитой цели, и комплекс в это время не пропадает из сетки.
**Повреждения.** Взрыв рядом с комплексом повреждает его: он хуже поворачивается, хуже видит и попадает,
дымит, ниже 35% горит. Уничтоженный остаётся горящим остовом, его боекомплект детонирует: чем больше ракет
оставалось, тем мощнее взрыв и дольше вторичные (у Гепарда рвутся снаряды); соседние комплексы тоже могут
@@ -103,7 +142,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 +191,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

+2 -1
View File
@@ -115,7 +115,8 @@ fn post(uv: vec2<f32>, pixel: vec2<f32>) -> vec4<f32> {
// the pixel sees, much stronger at night.
let night = clamp(0.35 - frame.time.z * 1.8, 0.0, 1.0);
let strength = mix(0.35, 2.2, night);
if wall < 1.0e5 {
// Under the HD lighting (params[0].w) the engine lights the world from the same lights.
if wall < 1.0e5 && frame.params[0].w < 0.5 {
let p = dir * wall;
var light = vec3<f32>(0.0);
for (var i = 0; i < blasts; i++) {
+252 -33
View File
@@ -7,7 +7,7 @@ use horizon_mod_sdk::{
Mod, Registration, chat, command, game,
hud::{self, Corner},
input::{self, Event},
render::{self, ModelInstance, ParticleSpawn},
render::{self, Lighting, ModelInstance, ParticleSpawn, PointLight},
storage,
ui::{self, ButtonWidget, ItemWidget, ListWidget, Screen, Widget},
};
@@ -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,10 @@ 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 HD lighting preset (the engine's lights, bloom, tone mapping).
hd: bool,
/// 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 +214,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
@@ -221,11 +278,80 @@ impl AirDef {
self.emit(out);
}
/// The HD lighting's lights: flames, glowing warheads, flares, muzzle flashes, tracers,
/// blasts (a flash, then the fireball's glow dying down), burning wrecks and the
/// searchlights' spots.
fn lights(&self, out: &Out) -> Vec<PointLight> {
let mut lights: Vec<PointLight> = Vec::new();
let mut add = |id: u32, at: DVec3, rgb: (f32, f32, f32), intensity: f32, radius: f32| {
if lights.len() < 500 && intensity > 0.05 {
lights.push(PointLight { id, x: at.x, y: at.y, z: at.z, color: rgb, intensity, radius: radius.clamp(1.0, 256.0) });
}
};
let tick = self.sim.tick;
let flicker = |id: u32| 0.85 + 0.15 * (((tick as u32).wrapping_mul(2654435761) ^ id.wrapping_mul(97)) % 100) as f32 / 100.0;
for m in out.models.iter().filter(|m| m.glow) {
let s = m.scale.max(0.1);
match m.name {
"airdef:flame" => add(m.id, m.at, (1.0, 0.55, 0.2), 45.0 * s * flicker(m.id), 32.0 * s.sqrt()),
"airdef:geran3_flame" => add(m.id, m.at, (1.0, 0.6, 0.25), 25.0 * s * flicker(m.id), 22.0),
"airdef:reentry" => add(m.id, m.at, (1.0, 0.6, 0.32), 60.0 * s, 40.0),
"airdef:flare" => add(m.id, m.at, (1.0, 0.93, 0.78), 420.0 * s, 110.0),
"airdef:flash" => add(m.id, m.at, (1.0, 0.8, 0.45), 30.0, 14.0),
"airdef:tracer" => add(m.id, m.at, (1.0, 0.25, 0.1), 2.5, 6.0),
_ => {}
}
}
for b in &self.sim.blasts {
let age = tick.saturating_sub(b.start) as f32;
let size = b.size;
let glow = (-age / (b.life as f32 * 0.15).max(1.0)).exp();
let flash = (-age * 0.35).exp() * 4.0;
let id = 0x6100_0000 ^ (b.start as u32).wrapping_mul(31) ^ (b.at.x as i32 as u32).wrapping_mul(7);
add(id, b.at + DVec3::Y * size as f64 * 0.5, (1.0, 0.58, 0.25), size * size * 18.0 * (glow + flash), size * 14.0);
}
for s in &self.sim.sams {
if !s.alive() && s.burning() {
add(s.id + 15, s.pos + DVec3::Y * 1.5, (1.0, 0.5, 0.18), 80.0 * flicker(s.id), 30.0);
}
// A searchlight's spot where its beam meets the target.
if s.kind == SamKind::Light
&& let Some(t) = s.target.and_then(|t| self.sim.drones.iter().find(|d| d.id == t))
&& self.sim.lit.contains(&t.id)
{
add(s.id + 14, t.pos, (0.95, 0.97, 1.0), 220.0, 30.0);
}
}
lights
}
/// The effects preset: none, the post pass, or the post pass under the HD lighting.
fn set_effects(&mut self, shader: bool, hd: bool) {
self.sim.shader = shader;
if shader {
render::set_post(include_str!("../shaders/effects.wgsl"));
} else {
render::clear_post();
}
self.hd = shader && hd;
self.apply_hd();
self.dirty = true;
}
/// Switches the HD lighting.
fn apply_hd(&self) {
render::set_lighting(Lighting { hd: self.hd, exposure: 1.0, bloom: 0.55 });
if !self.hd {
render::set_lights(&[]);
}
}
/// Hands the simulation's output to the game.
/// The post pass's parameters: blasts and glowing parts (shaders/effects.wgsl).
fn shader_params(&self, out: &Out) -> Vec<f32> {
let mut p = vec![0.0f32; 4];
p[0] = self.sim.tick as f32;
p[3] = if self.hd { 1.0 } else { 0.0 };
let blasts: Vec<&sim::Blast> = self.sim.blasts.iter().rev().take(48).collect();
p[1] = blasts.len() as f32;
for b in &blasts {
@@ -244,9 +370,13 @@ 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),
_ => 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;
}
@@ -258,6 +388,9 @@ impl AirDef {
if self.sim.shader {
render::set_params(&self.shader_params(&out));
}
if self.hd {
render::set_lights(&self.lights(&out));
}
let models: Vec<ModelInstance> = out
.models
.into_iter()
@@ -355,7 +488,24 @@ 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);
// Then its ammunition (missiles, bursts, out and waiting for a truck, reload ticks
// left) and where it was driving, if anywhere.
let dest = s.dest.map_or("- - -".to_string(), |d| format!("{} {} {}", d.x, d.y, d.z));
text += &format!(
"sam {} {} {} {} {} {} {} {} {} {} {} {} {dest}\n",
s.kind.key(),
s.pos.x,
s.pos.y,
s.pos.z,
s.heading,
s.roles,
s.health,
s.cargo,
s.missiles,
s.bursts,
u8::from(s.empty),
s.reloading,
);
}
for (i, t) in self.sim.targets.iter().enumerate() {
let h = self.sim.target_health.get(i).copied().unwrap_or(100.0);
@@ -363,6 +513,7 @@ impl AirDef {
}
text += &format!("damage {}\n", self.sim.damage);
text += &format!("far {}\n", self.far);
text += &format!("effects {} {}\n", self.sim.shader, self.hd);
for (kind, roles) in &self.sim.roles {
text += &format!("roles {} {}\n", kind.key(), roles);
}
@@ -395,7 +546,25 @@ 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;
}
// Older saves have no ammunition: full, as before.
if let (Some(m), Some(b)) = (num(9), num(10)) {
sam.missiles = (m as u32).min(sam.missiles);
sam.bursts = (b as u32).min(sam.bursts);
sam.empty = num(11) == Some(1.0);
sam.reloading = num(12).map_or(0, |r| r as u32);
}
let dest = match (num(13), num(14), num(15)) {
(Some(x), Some(y), Some(z)) => Some(DVec3::new(x, y, z)),
_ => None,
};
self.sim.sams.push(sam);
if let Some(to) = dest {
let i = self.sim.sams.len() - 1;
self.sim.order_move(i, to);
}
}
}
Some(&"target") => {
@@ -406,6 +575,11 @@ impl AirDef {
}
Some(&"damage") => self.sim.damage = w.get(1) != Some(&"false"),
Some(&"far") => self.far = w.get(1) != Some(&"false"),
Some(&"effects") => {
let shader = w.get(1) != Some(&"false");
let hd = w.get(2) == Some(&"true");
self.set_effects(shader, hd);
}
Some(&"roles") => {
if let (Some(kind), Some(r)) = (w.get(1).and_then(|k| SamKind::from_key(k)), w.get(2).and_then(|v| v.parse().ok())) {
self.sim.roles.insert(kind, r);
@@ -425,6 +599,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()) {
@@ -487,7 +674,7 @@ impl AirDef {
}
_ => replay::Mode::Map,
};
self.replay = Some(replay::Player::new(n - 1, mode, &self.sim));
self.replay = Some(replay::Player::new(rec.number, mode, &self.sim));
self.grid = false;
input::claim_wheel(true);
Ok(())
@@ -528,13 +715,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 +732,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 +765,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();
@@ -619,7 +810,8 @@ impl AirDef {
}));
let broken = self.sim.sams.iter().filter(|s| s.health < 1.0).count();
widgets.push(Widget::Button(ButtonWidget { id: BTN_REPAIR, label: format!("Ремонт всех (повреждено: {broken})") }));
widgets.push(Widget::Button(ButtonWidget { id: BTN_SHADER, label: format!("Эффекты: {}", onoff(self.sim.shader)) }));
let fx = if !self.sim.shader { "выкл" } else if self.hd { "HD (свет, bloom)" } else { "обычные" };
widgets.push(Widget::Button(ButtonWidget { id: BTN_SHADER, label: format!("Эффекты: {fx}") }));
widgets.push(Widget::Button(ButtonWidget { id: BTN_DAMAGE, label: format!("Разрушения: {}", onoff(self.sim.damage)) }));
widgets.push(Widget::Button(ButtonWidget { id: BTN_FAR, label: format!("Дальний ландшафт: {}", onoff(self.far)) }));
widgets.push(Widget::Button(ButtonWidget { id: BTN_BACK, label: "← Назад".into() }));
@@ -684,7 +876,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|hd | hold on|off | roles [комплекс классы] | stats";
impl Mod for AirDef {
fn init() -> Result<(Self, Registration), String> {
@@ -701,6 +893,8 @@ impl Mod for AirDef {
wave: wave::WaveConfig::default(),
replay: None,
far: true,
moving: None,
hd: false,
world: None,
health_mark: 0,
menu: None,
@@ -711,6 +905,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");
@@ -752,13 +947,14 @@ impl Mod for AirDef {
_ => replay::Control::Pause,
};
if let Some(r) = &mut self.replay
&& let Some(rec) = self.sim.replays.get(r.rec)
&& let Some(rec) = self.sim.replay(r.rec)
{
r.control(c, rec);
}
}
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();
@@ -768,7 +964,7 @@ impl Mod for AirDef {
}
Event::Wheel(w) => {
if let Some(r) = &mut self.replay
&& let Some(rec) = self.sim.replays.get(r.rec)
&& let Some(rec) = self.sim.replay(r.rec)
{
r.wheel(w, rec);
}
@@ -780,6 +976,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() {
@@ -829,12 +1026,18 @@ impl Mod for AirDef {
}
}
}
// Damage is saved too: a destroyed system stays destroyed.
// Damage is saved too: a destroyed system stays destroyed. So are where the units stand
// (or drive to: not every step of the way), their ammunition and the depots' stock.
let mark = self
.sim
.sams
.iter()
.map(|s| (s.health * 1000.0) as u64)
.map(|s| {
let at = s.dest.unwrap_or(s.pos);
let place = (at.x * 10.0) as i64 as u64 ^ ((at.z * 10.0) as i64 as u64).rotate_left(21) ^ (if s.dest.is_some() { 0 } else { (s.heading * 100.0) as i64 as u64 }).rotate_left(42);
let ammo = u64::from(s.missiles) | u64::from(s.bursts) << 16 | u64::from(s.cargo) << 32 | u64::from(s.empty) << 48 | u64::from(s.reloading > 0) << 49;
((s.health * 1000.0) as u64).wrapping_mul(1_000_003) ^ place ^ ammo.rotate_left(7)
})
.chain(self.sim.target_health.iter().map(|h| (h * 10.0) as u64))
.fold(self.sim.sams.len() as u64, |a, h| a.wrapping_mul(31).wrapping_add(h));
if mark != self.health_mark {
@@ -850,7 +1053,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;
@@ -948,12 +1151,13 @@ impl Mod for AirDef {
return;
}
(Page::More, BTN_SHADER) => {
self.sim.shader = !self.sim.shader;
if self.sim.shader {
render::set_post(include_str!("../shaders/effects.wgsl"));
} else {
render::clear_post();
}
// Off → effects → HD → off.
let (shader, hd) = match (self.sim.shader, self.hd) {
(false, _) => (true, false),
(true, false) => (true, true),
(true, true) => (false, false),
};
self.set_effects(shader, hd);
}
(Page::More, BTN_FAR) => {
self.far = !self.far;
@@ -1006,7 +1210,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 +1255,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 +1309,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 +1318,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" {
@@ -1120,14 +1344,9 @@ impl Mod for AirDef {
self.sim.hold = *v == "on";
Some(if self.sim.hold { "ПВО: огонь запрещён".into() } else { "ПВО: огонь разрешён".into() })
}
["shader", v @ ("on" | "off")] => {
self.sim.shader = *v == "on";
if self.sim.shader {
render::set_post(include_str!("../shaders/effects.wgsl"));
} else {
render::clear_post();
}
Some(format!("Шейдерные эффекты: {}", if self.sim.shader { "вкл" } else { "выкл" }))
["shader", v @ ("on" | "off" | "hd")] => {
self.set_effects(*v != "off", *v == "hd");
Some(format!("Эффекты: {}", match *v { "off" => "выкл", "hd" => "HD", _ => "обычные" }))
}
["stats"] => Some(format!(
"В воздухе: {}, ракет: {}, ПВО: {}, сбито: {}, поражений: {}",
+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)));
}
+5 -5
View File
@@ -31,8 +31,8 @@ pub struct Clip {
}
pub struct Player {
/// Which of the sim's recordings.
pub rec: usize,
/// Which raid's recording (its number: the list drops the oldest as new ones come).
pub rec: u32,
pub mode: Mode,
t: f64,
speed: f64,
@@ -188,7 +188,7 @@ pub fn reel(rec: &Recording) -> Vec<Clip> {
}
impl Player {
pub fn new(rec: usize, mode: Mode, sim: &Sim) -> Self {
pub fn new(rec: u32, mode: Mode, sim: &Sim) -> Self {
let start = match &mode {
Mode::Clips { list, .. } => list.first().map_or(0.0, |c| c.from),
_ => 0.0,
@@ -291,7 +291,7 @@ impl Player {
/// One tick: time runs, the ghosts fly, the director films. Models and sounds go to `out`.
pub fn tick(&mut self, sim: &Sim, listener: DVec3, out: &mut Out) {
let Some(rec) = sim.replays.get(self.rec) else {
let Some(rec) = sim.replay(self.rec) else {
self.done = true;
return;
};
@@ -491,7 +491,7 @@ impl Player {
/// The replay's screen: the film with its titles, or the map.
pub fn hud(&self, sim: &Sim, (gw, gh): (f32, f32)) -> Vec<Shape> {
let Some(rec) = sim.replays.get(self.rec) else { return Vec::new() };
let Some(rec) = sim.replay(self.rec) else { return Vec::new() };
if matches!(self.mode, Mode::Map) {
return self.map(rec, gw, gh);
}
+21 -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 };
@@ -169,6 +179,10 @@ impl Sim {
sam.burn = 0;
sam.state = SamState::Search;
sam.reloading = 0;
sam.empty = false;
// Rebuilt where it stands: a march under way is called off.
sam.dest = None;
sam.march = 0;
sam.missiles = spec.missile.as_ref().map_or(0, |m| m.magazine);
sam.bursts = spec.gun.as_ref().map_or(0, |g| g.bursts);
}
@@ -186,14 +200,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 smart raid: its 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().filter(|(_, d)| d.smart) {
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() && d.smart) {
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();
}
}
}
}
}
+149
View File
@@ -0,0 +1,149 @@
//! 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);
// The beam: segments widening with distance, out to the target.
let (head, rot) = self.sams[i].mount();
let dir = rot * DVec3::NEG_Z;
// The target is lit once the beam is on it, not while it still swings round.
if let Some((t, at)) = target
&& dir.angle_between(at - head) < 0.08
{
self.lit.push(t);
}
let mut along = 1.0;
// Up to 14 segments (the unit's part ids 2..15), each longer than the last: one
// reaches about 550 blocks, past the beam's reach, about 9 blocks wide out there.
for k in 0..14u32 {
if along >= reach {
break;
}
let size = 0.8 + along * 0.12;
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);
}
}
}
}
+114
View File
@@ -0,0 +1,114 @@
//! 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 was set up (without one, magazines refill as
/// before). A destroyed depot still counts: losing the last one leaves the systems short,
/// not supplied from nowhere again.
pub fn logistics(&self) -> bool {
self.sams.iter().any(|s| s.kind == SamKind::Depot)
}
/// 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);
}
}
+283 -255
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,12 @@ 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,
/// Sent by the smart enemy: it may be turned on systems that give themselves away.
smart: bool,
}
impl Drone {
@@ -667,6 +681,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 +715,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 +778,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 +866,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 +878,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 +898,10 @@ impl SamState {
SamState::Reload => "ПЕРЕЗАРЯДКА",
SamState::Hold => "ОГОНЬ ЗАПРЕЩЁН",
SamState::Destroyed => "УНИЧТОЖЕН",
SamState::Packing => "СВЁРТЫВАНИЕ",
SamState::Moving => "НА МАРШЕ",
SamState::Deploying => "РАЗВЁРТЫВАНИЕ",
SamState::Waiting => "ЖДЁТ БК",
}
}
pub fn color(self) -> u32 {
@@ -924,6 +914,8 @@ impl SamState {
SamState::Reload => 0x80A0FF,
SamState::Hold => 0xA0A0A0,
SamState::Destroyed => 0xFF2020,
SamState::Packing | SamState::Moving | SamState::Deploying => 0xC0A0FF,
SamState::Waiting => 0xFF9040,
}
}
}
@@ -959,8 +951,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 +1003,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 +1027,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 +1118,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>,
pub lit: Vec<u32>,
}
/// Ids: objects get multiples of 16, their parts the low bits.
@@ -1162,6 +1158,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 +1172,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 +1185,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 +1230,9 @@ impl Sim {
engaged: 0,
route: Vec::new(),
low: false,
jammed: false,
sector: 0,
smart: false,
});
self.launched += 1;
}
@@ -1247,16 +1272,25 @@ impl Sim {
self.queue.clear();
self.plan = None;
self.wave = None;
// Ammunition still to cook off, flares in the sky.
self.later.clear();
self.flares.clear();
self.lit.clear();
}
/// One game tick.
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 +1418,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 +1491,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 +1524,9 @@ impl Sim {
engaged: 0,
route: Vec::new(),
low: false,
jammed: false,
sector: 0,
smart: false,
});
}
for d in &self.drones {
@@ -1597,6 +1641,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 +1680,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 +1717,24 @@ impl Sim {
}
}
}
if self.sams[i].moving() {
continue;
}
if !self.sams[i].kind.armed() {
let sam = &mut self.sams[i];
sam.state = SamState::Search;
// A searchlight's target is its beam's (step_lights): kept for its spot of light.
if sam.kind != SamKind::Light {
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 +1764,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 +1778,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 +1800,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 +1813,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 +1839,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;
@@ -1773,7 +1849,7 @@ impl Sim {
};
for b in barrels {
let muzzle = mount + rot * *b * s;
let dir = (lead - muzzle).normalize();
let dir = (lead - muzzle).normalize_or(DVec3::NEG_Z);
let len = muzzle.distance(lead).min(gun.range * 1.2);
// Every round is a red tracer flying out at the shell's speed.
let scatter = DVec3::new(self.rng.s(), self.rng.s(), self.rng.s()) * 0.012;
@@ -1787,7 +1863,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,39 +1877,44 @@ 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 {
let n = load.magazine - sam.missiles;
let (muzzle, dir) = sam.muzzle(n);
launches.push((load.kind, muzzle, dir, mt, sam.id));
// Its missile is on it: the next launch looks for another target.
// Its missile is on it from now, so the systems after this one this tick see
// the target taken; this one's next launch looks for another.
if let Some(d) = self.drones.iter_mut().find(|d| d.id == mt) {
d.engaged += 1;
}
sam.target = None;
sam.launched = 12;
sam.missiles -= 1;
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 };
self.missiles.push(Missile { id, kind, pos: muzzle, vel: dir * v0, target, sam, age: 0 });
if let Some(d) = self.drones.iter_mut().find(|d| d.id == target) {
d.engaged += 1;
}
if spec.cold > 0 {
// Thrown out by gas: a puff, a thump; the motor lights later.
out.particles("minecraft:cloud", muzzle, 0.6, dir * 0.05, 0.05, 30);
@@ -1858,69 +1941,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 +1991,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 +2045,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;
}
}
_ => {}
}
}
}
}
+69 -9
View File
@@ -195,13 +195,19 @@ impl Sim {
/// (each route found in full, a few a tick) and launched when its plan is ready.
pub fn order_raid(&mut self, order: &RaidOrder, objects: &[DVec3], out: &mut Out) -> u32 {
let Some(&centre) = objects.first() else { return 0 };
// One raid is planned at a time: another order waits for this one's drones to be sent.
if self.plan.is_some() {
out.messages.push("Противник ещё готовит прошлый налёт: подожди пару секунд".into());
return 0;
}
let total: u32 = order.kinds.iter().map(|k| k.1).sum();
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 +228,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.
@@ -296,7 +303,8 @@ impl Sim {
// Timing: smart raids arrive at once; others leave at once (a moment apart).
let now = self.tick;
let arrive = plan.done.iter().map(|(_, t, e, g)| t + e - g).fold(0.0, f64::max);
for (d, time, early, gap) in plan.done {
for (mut d, time, early, gap) in plan.done {
d.smart = plan.smart;
let wait = if plan.smart { arrive - (early - gap) - time } else { gap + self.rng.f() * 40.0 };
self.queue.push((now + wait.clamp(0.0, 2400.0) as u64, d));
}
@@ -344,6 +352,9 @@ impl Sim {
engaged: 0,
route: Vec::new(),
low: false,
jammed: false,
sector: 0,
smart: false,
}
}
@@ -365,9 +376,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 +407,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));
@@ -442,7 +478,8 @@ impl Sim {
let kills = self.kills - w.kills;
let through = self.hits - w.hits;
let launched = self.launched - w.launched;
let fired: u32 = self.sams.iter().map(|s| s.fired).sum::<u32>() - w.fired;
// A system taken away during the raid takes its count along: never below none.
let fired: u32 = self.sams.iter().map(|s| s.fired).sum::<u32>().saturating_sub(w.fired);
let lost = w.sams.saturating_sub(self.sams.iter().filter(|s| s.alive()).count());
let objects = if self.target_health.is_empty() {
String::new()
@@ -508,6 +545,29 @@ mod tests {
eprintln!("4000 ticks in {:?}, kills {} hits {}", t.elapsed(), sim.kills, sim.hits);
assert!(sim.queue.is_empty() && !sim.drones.iter().any(|d| d.alive()), "the raid never ended");
}
#[test]
fn systems_ready_together_send_one_missile_at_a_drone() {
let mut sim = Sim::new(3);
// Vertical launchers of one kind side by side: the drone comes into their reach in the
// same tick, and they fire without turning.
for i in 0..6 {
let id = sim.id();
sim.sams.push(Sam::new(id, SamKind::Tor, DVec3::new(2.0 * i as f64, 70.0, 0.0), 0.0));
}
let mut ground = Ground::default();
let mut out = Out::default();
for _ in 0..30 {
sim.step(&mut ground, None, &mut out);
}
sim.add_drone(DroneKind::Shahed, DVec3::new(0.0, 120.0, -400.0), DVec3::Z, DVec3::new(0.0, 70.0, 0.0));
let id = sim.drones[0].id;
for _ in 0..1200 {
sim.step(&mut ground, None, &mut out);
let on_it = sim.missiles.iter().filter(|m| m.target == id).count();
assert!(on_it <= 1, "{on_it} missiles on one Shahed");
}
assert!(sim.sams.iter().any(|s| s.fired > 0), "nothing was fired");
}
}
#[cfg(test)]
@@ -518,7 +578,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));
+8 -1
View File
@@ -142,6 +142,13 @@ impl Recording {
}
}
impl Sim {
/// The recording of raid `number`, while it is kept.
pub fn replay(&self, number: u32) -> Option<&Recording> {
self.replays.iter().find(|r| r.number == number)
}
}
impl Sim {
/// Something worth a replay happened.
pub(super) fn log(&mut self, pos: DVec3, kind: EventKind) {
@@ -237,7 +244,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);
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More