Compare commits

...

12 Commits

Author SHA1 Message Date
chexit ed2db8523b Shaders, profiling and the rest of the issues: GTAO and HD GPU pass timing in the fork (patch 15; #9), explosion effects culled early per pixel (#11), a native sim runner with scenario tests run in CI (#8), named scenarios (#17), /airdef why and failure counters in raid reports (#18), IRIS-T and AMRAAM models and launch sounds by kind (#28)
build / mod (push) Successful in 3m24s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 16:43:48 +03:00
chexit 67a2a523b4 P2/P3 issues: network-fed launchers priced into smart routes (#4), stable blast light ids (#15), retargeted smart drones replanned round the defences (#21), sim tick without per-unit clones and linear scans (#26), ground cache swept rarely (#31), Oreshnik pieces keep their sector (#32), drives with acceleration and fire only once aimed (#33), radar monitor range (#19), delayed sound volume (#22), world change clears transient state (#23), score line always shown (#24), replay map without full paths (#25), text fit by bisection (#27), docs (#29), glow occlusion by ray distance (#14), blasts composited nearest first (#16)
build / mod (push) Successful in 2m26s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 16:28:24 +03:00
chexit a806635681 HD lights culled per tile (fork patch 14; #10)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 16:25:44 +03:00
chexit 0092ec64a9 HD renderer: cached bind groups, shadow casters only (fork patch 13; #12, #13)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 16:19:53 +03:00
chexit e08fc65ac8 P1 issues: ground queries keep room for unknown land (#1), a destroyed truck frees its job (#2), trucks follow a system that moved (#3), repair cancels cook-offs (#5), a failed save is retried and autosaves are paced (#6, #7), a combined system reloads when either weapon is spent (#20), munitions go down to the land left under a destroyed target (#34); regression tests from the issues
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 16:18:33 +03:00
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
130 changed files with 22044 additions and 519 deletions
+11 -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,14 +24,17 @@ 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
# Чистая симуляция тестируется нативно (хост-таргет раннера, без WASM): регрессии из sim/regress.rs
# и сценарии tests/scenarios.rs (NaN, налёт кончается, ТЗМ не зависают). Нужен форк: SDK подключён путём.
- name: Тесты мода
working-directory: airdef-mod
run: |
HOST=$(rustc -vV | sed -n 's/^host: //p')
nice -n 19 cargo test --target "$HOST"
- name: Мод
run: nice -n 19 python3 tools/airdef.py mod
- name: Релиз
+54 -5
View File
@@ -23,9 +23,10 @@ python3 tools/airdef.py play # запустить (аргументы игр
(`tools/gen_sounds.py`, нужны numpy и ffmpeg), собирает и ставит мод. Сгенерированные файлы лежат в репозитории,
так что для обычной сборки нужен только Rust.
CI: `.gitea/workflows/build.yml` на каждый пуш собирает мод и игру для Linux, а пуш в `main` публикует релиз
`build-N`; `.gitea/workflows/desktop.yml` (запуск вручную с тегом релиза) собирает игру для macOS и Windows на
раннерах с метками `macos-latest` / `windows-latest` и дописывает архивы в релиз.
CI: `.gitea/workflows/build.yml` на каждый пуш собирает только мод (`airdef.hmod`), а пуш в `main` публикует его
релизом `build-N`. Игру-форк собирают вручную: `.gitea/workflows/game.yml` (Linux) и `.gitea/workflows/desktop.yml`
(macOS и Windows, на раннерах с метками `macos-latest` / `windows-latest`); оба запускаются из Actions с тегом
готового релиза и дописывают свои архивы в него.
## Как играть
@@ -68,6 +69,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% горит. Уничтоженный остаётся горящим остовом, его боекомплект детонирует: чем больше ракет
оставалось, тем мощнее взрыв и дольше вторичные (у Гепарда рвутся снаряды); соседние комплексы тоже могут
@@ -91,7 +131,7 @@ CI: `.gitea/workflows/build.yml` на каждый пуш собирает мо
перехватчиков (жёлтые), сбития (x), прилёты, шкала времени с событиями; время идёт само, колесо мыши перематывает;
- **повтор момента** — клик по событию в списке: «призраки» дронов и ракет снова летят в мире, камера идёт за
целью, потом за перехватчиком (или смотрит из пушки, со стороны на прилёт), в момент сбития время замедляется;
- **полный повтор** — весь налёт, камера-режиссёр сама выбирает, за чем следить; колесо — скорость (×0.5–×16);
- **полный повтор** — весь налёт, камера-режиссёр сама выбирает, за чем следить; колесо — перемотка, стрелки — скорость;
- **лучшие моменты** — нарезка до 8 самых интересных событий (перехват баллистики и гиперзвука, поражения
объектов, потерянные комплексы, разделение «Орешника») с титрами и замедлением; колесо листает моменты.
@@ -103,7 +143,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 +192,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/amraam_missile"
}
}
}
@@ -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_missile"
}
}
}
@@ -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"
}
}
+33
View File
@@ -4,6 +4,8 @@
"block.airdef.geran3": "Герань-3",
"block.airdef.geran3_flame": "Факел",
"block.airdef.pac3": "PAC-3 MSE",
"block.airdef.irist_missile": "IRIS-T SLM",
"block.airdef.amraam_missile": "AMRAAM",
"block.airdef.flame": "Факел",
"block.airdef.patriot_launcher": "ПУ M903",
"block.airdef.patriot_truck": "Тягач",
@@ -39,6 +41,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 +77,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",
+33
View File
@@ -4,6 +4,8 @@
"block.airdef.geran3": "Герань-3",
"block.airdef.geran3_flame": "Факел",
"block.airdef.pac3": "PAC-3 MSE",
"block.airdef.irist_missile": "IRIS-T SLM",
"block.airdef.amraam_missile": "AMRAAM",
"block.airdef.flame": "Факел",
"block.airdef.patriot_launcher": "ПУ M903",
"block.airdef.patriot_truck": "Тягач",
@@ -39,6 +41,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 +77,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,895 @@
{
"ambientocclusion": false,
"textures": {
"nose_dark": "airdef:block/nose_dark",
"white": "airdef:block/white",
"nozzle": "airdef:block/nozzle",
"particle": "airdef:block/nose_dark"
},
"elements": [
{
"from": [
7.333333333333333,
7.333333333333333,
-16.0
],
"to": [
8.666666666666666,
8.666666666666666,
-13.666666666666666
],
"faces": {
"north": {
"uv": [
0,
0,
1.333333333333333,
1.333333333333333
],
"texture": "#nose_dark"
},
"south": {
"uv": [
0,
0,
1.333333333333333,
1.333333333333333
],
"texture": "#nose_dark"
},
"east": {
"uv": [
0,
0,
2.333333333333334,
1.333333333333333
],
"texture": "#nose_dark"
},
"west": {
"uv": [
0,
0,
2.333333333333334,
1.333333333333333
],
"texture": "#nose_dark"
},
"up": {
"uv": [
0,
0,
1.333333333333333,
2.333333333333334
],
"texture": "#nose_dark"
},
"down": {
"uv": [
0,
0,
1.333333333333333,
2.333333333333334
],
"texture": "#nose_dark"
}
}
},
{
"from": [
7.0,
7.0,
-13.666666666666666
],
"to": [
9.0,
9.0,
-11.333333333333332
],
"faces": {
"north": {
"uv": [
0,
0,
2.0,
2.0
],
"texture": "#nose_dark"
},
"south": {
"uv": [
0,
0,
2.0,
2.0
],
"texture": "#nose_dark"
},
"east": {
"uv": [
0,
0,
2.333333333333334,
2.0
],
"texture": "#nose_dark"
},
"west": {
"uv": [
0,
0,
2.333333333333334,
2.0
],
"texture": "#nose_dark"
},
"up": {
"uv": [
0,
0,
2.0,
2.333333333333334
],
"texture": "#nose_dark"
},
"down": {
"uv": [
0,
0,
2.0,
2.333333333333334
],
"texture": "#nose_dark"
}
}
},
{
"from": [
6.666666666666666,
6.666666666666666,
-11.333333333333332
],
"to": [
9.333333333333334,
9.333333333333334,
-9.0
],
"faces": {
"north": {
"uv": [
0,
0,
2.666666666666668,
2.666666666666668
],
"texture": "#nose_dark"
},
"south": {
"uv": [
0,
0,
2.666666666666668,
2.666666666666668
],
"texture": "#nose_dark"
},
"east": {
"uv": [
0,
0,
2.333333333333332,
2.666666666666668
],
"texture": "#nose_dark"
},
"west": {
"uv": [
0,
0,
2.333333333333332,
2.666666666666668
],
"texture": "#nose_dark"
},
"up": {
"uv": [
0,
0,
2.666666666666668,
2.333333333333332
],
"texture": "#nose_dark"
},
"down": {
"uv": [
0,
0,
2.666666666666668,
2.333333333333332
],
"texture": "#nose_dark"
}
}
},
{
"from": [
6.5,
6.5,
-9.0
],
"to": [
9.5,
9.5,
26.0
],
"faces": {
"north": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#white"
},
"south": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#white"
},
"east": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#white"
},
"west": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#white"
},
"up": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#white"
},
"down": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#white"
}
}
},
{
"from": [
1.0,
7.6,
2.0
],
"to": [
6.5,
8.4,
9.0
],
"faces": {
"north": {
"uv": [
0,
0,
5.5,
0.8000000000000007
],
"texture": "#white"
},
"south": {
"uv": [
0,
0,
5.5,
0.8000000000000007
],
"texture": "#white"
},
"east": {
"uv": [
0,
0,
7.0,
0.8000000000000007
],
"texture": "#white"
},
"west": {
"uv": [
0,
0,
7.0,
0.8000000000000007
],
"texture": "#white"
},
"up": {
"uv": [
0,
0,
5.5,
7.0
],
"texture": "#white"
},
"down": {
"uv": [
0,
0,
5.5,
7.0
],
"texture": "#white"
}
}
},
{
"from": [
9.5,
7.6,
2.0
],
"to": [
15.0,
8.4,
9.0
],
"faces": {
"north": {
"uv": [
0,
0,
5.5,
0.8000000000000007
],
"texture": "#white"
},
"south": {
"uv": [
0,
0,
5.5,
0.8000000000000007
],
"texture": "#white"
},
"east": {
"uv": [
0,
0,
7.0,
0.8000000000000007
],
"texture": "#white"
},
"west": {
"uv": [
0,
0,
7.0,
0.8000000000000007
],
"texture": "#white"
},
"up": {
"uv": [
0,
0,
5.5,
7.0
],
"texture": "#white"
},
"down": {
"uv": [
0,
0,
5.5,
7.0
],
"texture": "#white"
}
}
},
{
"from": [
7.6,
1.0,
2.0
],
"to": [
8.4,
6.5,
9.0
],
"faces": {
"north": {
"uv": [
0,
0,
0.8000000000000007,
5.5
],
"texture": "#white"
},
"south": {
"uv": [
0,
0,
0.8000000000000007,
5.5
],
"texture": "#white"
},
"east": {
"uv": [
0,
0,
7.0,
5.5
],
"texture": "#white"
},
"west": {
"uv": [
0,
0,
7.0,
5.5
],
"texture": "#white"
},
"up": {
"uv": [
0,
0,
0.8000000000000007,
7.0
],
"texture": "#white"
},
"down": {
"uv": [
0,
0,
0.8000000000000007,
7.0
],
"texture": "#white"
}
}
},
{
"from": [
7.6,
9.5,
2.0
],
"to": [
8.4,
15.0,
9.0
],
"faces": {
"north": {
"uv": [
0,
0,
0.8000000000000007,
5.5
],
"texture": "#white"
},
"south": {
"uv": [
0,
0,
0.8000000000000007,
5.5
],
"texture": "#white"
},
"east": {
"uv": [
0,
0,
7.0,
5.5
],
"texture": "#white"
},
"west": {
"uv": [
0,
0,
7.0,
5.5
],
"texture": "#white"
},
"up": {
"uv": [
0,
0,
0.8000000000000007,
7.0
],
"texture": "#white"
},
"down": {
"uv": [
0,
0,
0.8000000000000007,
7.0
],
"texture": "#white"
}
}
},
{
"from": [
4.0,
7.6,
19.0
],
"to": [
6.5,
8.4,
25.0
],
"faces": {
"north": {
"uv": [
0,
0,
2.5,
0.8000000000000007
],
"texture": "#white"
},
"south": {
"uv": [
0,
0,
2.5,
0.8000000000000007
],
"texture": "#white"
},
"east": {
"uv": [
0,
0,
6.0,
0.8000000000000007
],
"texture": "#white"
},
"west": {
"uv": [
0,
0,
6.0,
0.8000000000000007
],
"texture": "#white"
},
"up": {
"uv": [
0,
0,
2.5,
6.0
],
"texture": "#white"
},
"down": {
"uv": [
0,
0,
2.5,
6.0
],
"texture": "#white"
}
}
},
{
"from": [
9.5,
7.6,
19.0
],
"to": [
12.0,
8.4,
25.0
],
"faces": {
"north": {
"uv": [
0,
0,
2.5,
0.8000000000000007
],
"texture": "#white"
},
"south": {
"uv": [
0,
0,
2.5,
0.8000000000000007
],
"texture": "#white"
},
"east": {
"uv": [
0,
0,
6.0,
0.8000000000000007
],
"texture": "#white"
},
"west": {
"uv": [
0,
0,
6.0,
0.8000000000000007
],
"texture": "#white"
},
"up": {
"uv": [
0,
0,
2.5,
6.0
],
"texture": "#white"
},
"down": {
"uv": [
0,
0,
2.5,
6.0
],
"texture": "#white"
}
}
},
{
"from": [
7.6,
4.0,
19.0
],
"to": [
8.4,
6.5,
25.0
],
"faces": {
"north": {
"uv": [
0,
0,
0.8000000000000007,
2.5
],
"texture": "#white"
},
"south": {
"uv": [
0,
0,
0.8000000000000007,
2.5
],
"texture": "#white"
},
"east": {
"uv": [
0,
0,
6.0,
2.5
],
"texture": "#white"
},
"west": {
"uv": [
0,
0,
6.0,
2.5
],
"texture": "#white"
},
"up": {
"uv": [
0,
0,
0.8000000000000007,
6.0
],
"texture": "#white"
},
"down": {
"uv": [
0,
0,
0.8000000000000007,
6.0
],
"texture": "#white"
}
}
},
{
"from": [
7.6,
9.5,
19.0
],
"to": [
8.4,
12.0,
25.0
],
"faces": {
"north": {
"uv": [
0,
0,
0.8000000000000007,
2.5
],
"texture": "#white"
},
"south": {
"uv": [
0,
0,
0.8000000000000007,
2.5
],
"texture": "#white"
},
"east": {
"uv": [
0,
0,
6.0,
2.5
],
"texture": "#white"
},
"west": {
"uv": [
0,
0,
6.0,
2.5
],
"texture": "#white"
},
"up": {
"uv": [
0,
0,
0.8000000000000007,
6.0
],
"texture": "#white"
},
"down": {
"uv": [
0,
0,
0.8000000000000007,
6.0
],
"texture": "#white"
}
}
},
{
"from": [
7.0,
7.0,
26.0
],
"to": [
9.0,
9.0,
28.0
],
"faces": {
"north": {
"uv": [
0,
0,
2.0,
2.0
],
"texture": "#nozzle"
},
"south": {
"uv": [
0,
0,
2.0,
2.0
],
"texture": "#nozzle"
},
"east": {
"uv": [
0,
0,
2.0,
2.0
],
"texture": "#nozzle"
},
"west": {
"uv": [
0,
0,
2.0,
2.0
],
"texture": "#nozzle"
},
"up": {
"uv": [
0,
0,
2.0,
2.0
],
"texture": "#nozzle"
},
"down": {
"uv": [
0,
0,
2.0,
2.0
],
"texture": "#nozzle"
}
}
}
]
}
@@ -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,895 @@
{
"ambientocclusion": false,
"textures": {
"white": "airdef:block/white",
"missile_grey": "airdef:block/missile_grey",
"nozzle": "airdef:block/nozzle",
"particle": "airdef:block/white"
},
"elements": [
{
"from": [
7.333333333333333,
7.333333333333333,
-16.0
],
"to": [
8.666666666666666,
8.666666666666666,
-14.0
],
"faces": {
"north": {
"uv": [
0,
0,
1.333333333333333,
1.333333333333333
],
"texture": "#white"
},
"south": {
"uv": [
0,
0,
1.333333333333333,
1.333333333333333
],
"texture": "#white"
},
"east": {
"uv": [
0,
0,
2.0,
1.333333333333333
],
"texture": "#white"
},
"west": {
"uv": [
0,
0,
2.0,
1.333333333333333
],
"texture": "#white"
},
"up": {
"uv": [
0,
0,
1.333333333333333,
2.0
],
"texture": "#white"
},
"down": {
"uv": [
0,
0,
1.333333333333333,
2.0
],
"texture": "#white"
}
}
},
{
"from": [
7.0,
7.0,
-14.0
],
"to": [
9.0,
9.0,
-12.0
],
"faces": {
"north": {
"uv": [
0,
0,
2.0,
2.0
],
"texture": "#white"
},
"south": {
"uv": [
0,
0,
2.0,
2.0
],
"texture": "#white"
},
"east": {
"uv": [
0,
0,
2.0,
2.0
],
"texture": "#white"
},
"west": {
"uv": [
0,
0,
2.0,
2.0
],
"texture": "#white"
},
"up": {
"uv": [
0,
0,
2.0,
2.0
],
"texture": "#white"
},
"down": {
"uv": [
0,
0,
2.0,
2.0
],
"texture": "#white"
}
}
},
{
"from": [
6.666666666666666,
6.666666666666666,
-12.0
],
"to": [
9.333333333333334,
9.333333333333334,
-10.0
],
"faces": {
"north": {
"uv": [
0,
0,
2.666666666666668,
2.666666666666668
],
"texture": "#white"
},
"south": {
"uv": [
0,
0,
2.666666666666668,
2.666666666666668
],
"texture": "#white"
},
"east": {
"uv": [
0,
0,
2.0,
2.666666666666668
],
"texture": "#white"
},
"west": {
"uv": [
0,
0,
2.0,
2.666666666666668
],
"texture": "#white"
},
"up": {
"uv": [
0,
0,
2.666666666666668,
2.0
],
"texture": "#white"
},
"down": {
"uv": [
0,
0,
2.666666666666668,
2.0
],
"texture": "#white"
}
}
},
{
"from": [
6.5,
6.5,
-10.0
],
"to": [
9.5,
9.5,
24.0
],
"faces": {
"north": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#white"
},
"south": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#white"
},
"east": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#white"
},
"west": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#white"
},
"up": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#white"
},
"down": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#white"
}
}
},
{
"from": [
3.0,
7.6,
-9.0
],
"to": [
6.5,
8.4,
-5.0
],
"faces": {
"north": {
"uv": [
0,
0,
3.5,
0.8000000000000007
],
"texture": "#missile_grey"
},
"south": {
"uv": [
0,
0,
3.5,
0.8000000000000007
],
"texture": "#missile_grey"
},
"east": {
"uv": [
0,
0,
4.0,
0.8000000000000007
],
"texture": "#missile_grey"
},
"west": {
"uv": [
0,
0,
4.0,
0.8000000000000007
],
"texture": "#missile_grey"
},
"up": {
"uv": [
0,
0,
3.5,
4.0
],
"texture": "#missile_grey"
},
"down": {
"uv": [
0,
0,
3.5,
4.0
],
"texture": "#missile_grey"
}
}
},
{
"from": [
9.5,
7.6,
-9.0
],
"to": [
13.0,
8.4,
-5.0
],
"faces": {
"north": {
"uv": [
0,
0,
3.5,
0.8000000000000007
],
"texture": "#missile_grey"
},
"south": {
"uv": [
0,
0,
3.5,
0.8000000000000007
],
"texture": "#missile_grey"
},
"east": {
"uv": [
0,
0,
4.0,
0.8000000000000007
],
"texture": "#missile_grey"
},
"west": {
"uv": [
0,
0,
4.0,
0.8000000000000007
],
"texture": "#missile_grey"
},
"up": {
"uv": [
0,
0,
3.5,
4.0
],
"texture": "#missile_grey"
},
"down": {
"uv": [
0,
0,
3.5,
4.0
],
"texture": "#missile_grey"
}
}
},
{
"from": [
7.6,
3.0,
-9.0
],
"to": [
8.4,
6.5,
-5.0
],
"faces": {
"north": {
"uv": [
0,
0,
0.8000000000000007,
3.5
],
"texture": "#missile_grey"
},
"south": {
"uv": [
0,
0,
0.8000000000000007,
3.5
],
"texture": "#missile_grey"
},
"east": {
"uv": [
0,
0,
4.0,
3.5
],
"texture": "#missile_grey"
},
"west": {
"uv": [
0,
0,
4.0,
3.5
],
"texture": "#missile_grey"
},
"up": {
"uv": [
0,
0,
0.8000000000000007,
4.0
],
"texture": "#missile_grey"
},
"down": {
"uv": [
0,
0,
0.8000000000000007,
4.0
],
"texture": "#missile_grey"
}
}
},
{
"from": [
7.6,
9.5,
-9.0
],
"to": [
8.4,
13.0,
-5.0
],
"faces": {
"north": {
"uv": [
0,
0,
0.8000000000000007,
3.5
],
"texture": "#missile_grey"
},
"south": {
"uv": [
0,
0,
0.8000000000000007,
3.5
],
"texture": "#missile_grey"
},
"east": {
"uv": [
0,
0,
4.0,
3.5
],
"texture": "#missile_grey"
},
"west": {
"uv": [
0,
0,
4.0,
3.5
],
"texture": "#missile_grey"
},
"up": {
"uv": [
0,
0,
0.8000000000000007,
4.0
],
"texture": "#missile_grey"
},
"down": {
"uv": [
0,
0,
0.8000000000000007,
4.0
],
"texture": "#missile_grey"
}
}
},
{
"from": [
2.0,
7.6,
18.0
],
"to": [
6.5,
8.4,
26.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.5,
0.8000000000000007
],
"texture": "#white"
},
"south": {
"uv": [
0,
0,
4.5,
0.8000000000000007
],
"texture": "#white"
},
"east": {
"uv": [
0,
0,
8.0,
0.8000000000000007
],
"texture": "#white"
},
"west": {
"uv": [
0,
0,
8.0,
0.8000000000000007
],
"texture": "#white"
},
"up": {
"uv": [
0,
0,
4.5,
8.0
],
"texture": "#white"
},
"down": {
"uv": [
0,
0,
4.5,
8.0
],
"texture": "#white"
}
}
},
{
"from": [
9.5,
7.6,
18.0
],
"to": [
14.0,
8.4,
26.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.5,
0.8000000000000007
],
"texture": "#white"
},
"south": {
"uv": [
0,
0,
4.5,
0.8000000000000007
],
"texture": "#white"
},
"east": {
"uv": [
0,
0,
8.0,
0.8000000000000007
],
"texture": "#white"
},
"west": {
"uv": [
0,
0,
8.0,
0.8000000000000007
],
"texture": "#white"
},
"up": {
"uv": [
0,
0,
4.5,
8.0
],
"texture": "#white"
},
"down": {
"uv": [
0,
0,
4.5,
8.0
],
"texture": "#white"
}
}
},
{
"from": [
7.6,
2.0,
18.0
],
"to": [
8.4,
6.5,
26.0
],
"faces": {
"north": {
"uv": [
0,
0,
0.8000000000000007,
4.5
],
"texture": "#white"
},
"south": {
"uv": [
0,
0,
0.8000000000000007,
4.5
],
"texture": "#white"
},
"east": {
"uv": [
0,
0,
8.0,
4.5
],
"texture": "#white"
},
"west": {
"uv": [
0,
0,
8.0,
4.5
],
"texture": "#white"
},
"up": {
"uv": [
0,
0,
0.8000000000000007,
8.0
],
"texture": "#white"
},
"down": {
"uv": [
0,
0,
0.8000000000000007,
8.0
],
"texture": "#white"
}
}
},
{
"from": [
7.6,
9.5,
18.0
],
"to": [
8.4,
14.0,
26.0
],
"faces": {
"north": {
"uv": [
0,
0,
0.8000000000000007,
4.5
],
"texture": "#white"
},
"south": {
"uv": [
0,
0,
0.8000000000000007,
4.5
],
"texture": "#white"
},
"east": {
"uv": [
0,
0,
8.0,
4.5
],
"texture": "#white"
},
"west": {
"uv": [
0,
0,
8.0,
4.5
],
"texture": "#white"
},
"up": {
"uv": [
0,
0,
0.8000000000000007,
8.0
],
"texture": "#white"
},
"down": {
"uv": [
0,
0,
0.8000000000000007,
8.0
],
"texture": "#white"
}
}
},
{
"from": [
7.0,
7.0,
24.0
],
"to": [
9.0,
9.0,
28.0
],
"faces": {
"north": {
"uv": [
0,
0,
2.0,
2.0
],
"texture": "#nozzle"
},
"south": {
"uv": [
0,
0,
2.0,
2.0
],
"texture": "#nozzle"
},
"east": {
"uv": [
0,
0,
4.0,
2.0
],
"texture": "#nozzle"
},
"west": {
"uv": [
0,
0,
4.0,
2.0
],
"texture": "#nozzle"
},
"up": {
"uv": [
0,
0,
2.0,
4.0
],
"texture": "#nozzle"
},
"down": {
"uv": [
0,
0,
2.0,
4.0
],
"texture": "#nozzle"
}
}
}
]
}
@@ -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: 346 B

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

+413
View File
@@ -0,0 +1,413 @@
//! Headless driver for the pure simulation (`airdef::sim`): builds a scenario, steps the
//! simulation like the mod's tick does (answering ground queries from a height function),
//! and measures. Shared by the `sim_run` example and `tests/scenarios.rs`.
//!
//! Native timings say nothing about the WASM build or the GPU; use them to compare runs of the
//! same machine.
#![allow(dead_code)]
use airdef::sim::{Aim, Approach, DroneKind, Ground, Out, RaidOrder, Sam, SamKind, Sim};
use glam::DVec3;
use std::collections::HashMap;
use std::time::{Duration, Instant};
/// Height of the land under a column (what `render.ground` would answer).
#[derive(Clone, Copy, Debug)]
pub enum Terrain {
Flat(i32),
/// Smooth hills: `base + amp * (sin(x/period) + cos(z/period)) / 2`.
Hills { base: i32, amp: f64, period: f64 },
}
impl Terrain {
pub fn height(self, x: i32, z: i32) -> i32 {
match self {
Terrain::Flat(h) => h,
Terrain::Hills { base, amp, period } => {
base + (amp * ((f64::from(x) / period).sin() + (f64::from(z) / period).cos()) / 2.0).round() as i32
}
}
}
}
#[derive(Clone, Debug)]
pub struct RaidSpec {
pub kinds: Vec<(DroneKind, u32)>,
pub approach: Approach,
pub aim: Aim,
pub dist: f64,
/// The tick the raid is ordered at (a raid waits for the previous one to be sent).
pub at: u64,
}
#[derive(Clone, Debug)]
pub struct Unit {
pub kind: SamKind,
pub pos: DVec3,
pub heading: f64,
/// Starts with an empty magazine, waiting for a reload (logistics).
pub empty: bool,
}
#[derive(Clone, Debug)]
pub struct Scenario {
pub name: String,
pub seed: u64,
pub units: Vec<Unit>,
pub objects: Vec<DVec3>,
pub raids: Vec<RaidSpec>,
pub terrain: Terrain,
/// Hard limit of ticks (20 a second).
pub ticks: u64,
/// Stop as soon as every raid has ended.
pub stop_at_end: bool,
pub ew: bool,
/// Ticks a truck on a job may stand still before it counts as stuck.
pub stuck_after: u64,
}
impl Scenario {
pub fn new(name: &str) -> Self {
Scenario {
name: name.into(),
seed: 1,
units: Vec::new(),
objects: vec![DVec3::new(0.0, 70.0, 0.0)],
raids: Vec::new(),
terrain: Terrain::Flat(70),
ticks: 20 * 60 * 10,
stop_at_end: true,
ew: false,
stuck_after: 600,
}
}
pub fn unit(&mut self, kind: SamKind, x: f64, z: f64) -> &mut Unit {
self.units.push(Unit { kind, pos: DVec3::new(x, 70.0, z), heading: 0.0, empty: false });
self.units.last_mut().unwrap()
}
}
#[derive(Clone, Debug, Default)]
pub struct Report {
pub name: String,
pub seed: u64,
pub ticks_run: u64,
pub units: usize,
pub launched: u32,
pub kills: u32,
pub hits: u32,
/// Missiles fired by air defence (sum of the systems' `fired`).
pub fired: u32,
pub ground_queries: u64,
pub ground_batches: u64,
pub tick_p50: Duration,
pub tick_p95: Duration,
pub tick_max: Duration,
pub tick_total: Duration,
/// Time spent in ticks that began with a raid being planned.
pub planning: Duration,
pub planning_ticks: u64,
pub nan_positions: u64,
/// Ids of trucks that stood on a job without progress for too long.
pub stuck_trucks: Vec<u32>,
/// Truck-ticks spent on a job or driving (shows the logistics actually ran).
pub truck_busy_ticks: u64,
pub raids_ordered: usize,
pub raids_ended: usize,
pub all_ended: bool,
}
fn finite(v: DVec3) -> bool {
v.x.is_finite() && v.y.is_finite() && v.z.is_finite()
}
pub fn run(sc: &Scenario) -> Report {
let mut sim = Sim::new(sc.seed);
sim.damage = false; // explosions would change the land the height function describes
for u in &sc.units {
let id = sim.id();
let mut s = Sam::new(id, u.kind, u.pos, u.heading);
if u.empty {
s.empty = true;
s.missiles = 0;
}
sim.sams.push(s);
}
sim.targets = sc.objects.clone();
sim.target_health = vec![100.0; sc.objects.len()];
if sc.ew {
sim.set_ew(true);
}
let mut ground = Ground::default();
let mut pending: Vec<(i32, i32, i32)> = Vec::new();
let mut r = Report { name: sc.name.clone(), seed: sc.seed, units: sc.units.len(), ..Default::default() };
let mut times: Vec<Duration> = Vec::with_capacity(sc.ticks as usize);
let mut ordered = vec![false; sc.raids.len()];
let mut launched_before = vec![0u32; sc.raids.len()];
let mut ended = vec![false; sc.raids.len()];
// truck id -> (last position, ticks standing while busy)
let mut standing: HashMap<u32, (DVec3, u64)> = HashMap::new();
let mut out = Out::default();
for now in 1..=sc.ticks {
// Raids come in order: one is ordered once the previous one has been sent.
for (i, spec) in sc.raids.iter().enumerate() {
if !ordered[i] && now >= spec.at && !sim.planning() && (i == 0 || ended[i - 1]) {
let order = RaidOrder { kinds: spec.kinds.clone(), approach: spec.approach, dist: spec.dist, aim: spec.aim };
launched_before[i] = sim.launched;
sim.order_raid(&order, &sc.objects, &mut out);
ordered[i] = true;
}
}
ground.answer(std::mem::take(&mut pending), now);
let planning = sim.planning();
out = Out::default();
let t = Instant::now();
sim.step(&mut ground, None, &mut out);
let wanted = ground.take_wanted();
let dt = t.elapsed();
times.push(dt);
if planning {
r.planning += dt;
r.planning_ticks += 1;
}
if !wanted.is_empty() {
r.ground_batches += 1;
r.ground_queries += wanted.len() as u64;
pending = wanted.into_iter().map(|(x, z)| (x, z, sc.terrain.height(x, z))).collect();
}
r.ticks_run = now;
// NaN check on everything with a position.
let bad = sim.drones.iter().filter(|d| !finite(d.pos) || !finite(d.vel)).count()
+ sim.missiles.iter().filter(|m| !finite(m.pos)).count()
+ sim.sams.iter().filter(|s| !finite(s.pos)).count()
+ sim.blasts.iter().filter(|b| !finite(b.at)).count()
+ out.models.iter().filter(|m| !finite(m.at)).count();
r.nan_positions += bad as u64;
// Trucks with a job or on the move, standing still.
for s in sim.sams.iter().filter(|s| s.kind == SamKind::Tzm && s.alive()) {
let busy = s.moving() || s.job().is_some();
r.truck_busy_ticks += u64::from(busy);
let e = standing.entry(s.id).or_insert((s.pos, 0));
if !busy || e.0.distance(s.pos) > 0.05 {
*e = (s.pos, 0);
} else {
e.1 += 1;
if e.1 == sc.stuck_after && !r.stuck_trucks.contains(&s.id) {
r.stuck_trucks.push(s.id);
}
}
}
// A raid has ended when it was ordered, planned, flown and nothing is left in the air.
let quiet = !sim.planning() && sim.queue.is_empty() && !sim.drones.iter().any(|d| d.alive()) && sim.missiles.is_empty();
for i in 0..sc.raids.len() {
if ordered[i] && !ended[i] && quiet && sim.launched > launched_before[i] {
ended[i] = true;
}
}
if sc.stop_at_end && ended.iter().all(|e| *e) && !sc.raids.is_empty() {
break;
}
}
r.launched = sim.launched;
r.kills = sim.kills;
r.hits = sim.hits;
r.fired = sim.sams.iter().map(|s| s.fired).sum();
r.raids_ordered = ordered.iter().filter(|o| **o).count();
r.raids_ended = ended.iter().filter(|o| **o).count();
r.all_ended = r.raids_ended == sc.raids.len();
r.tick_total = times.iter().sum();
times.sort();
if !times.is_empty() {
let at = |q: f64| times[((times.len() - 1) as f64 * q).round() as usize];
r.tick_p50 = at(0.5);
r.tick_p95 = at(0.95);
r.tick_max = *times.last().unwrap();
}
r
}
fn ms(d: Duration) -> f64 {
d.as_secs_f64() * 1000.0
}
impl Report {
/// Problems that fail a scenario: empty when it is healthy.
pub fn problems(&self) -> Vec<String> {
let mut p = Vec::new();
if self.nan_positions > 0 {
p.push(format!("{} non-finite positions", self.nan_positions));
}
if !self.stuck_trucks.is_empty() {
p.push(format!("stuck trucks: {:?}", self.stuck_trucks));
}
if !self.all_ended {
p.push(format!("{} of {} raids ended in {} ticks", self.raids_ended, self.raids_ended.max(self.raids_ordered), self.ticks_run));
}
p
}
pub fn text(&self) -> String {
format!(
"scenario {name} (seed {seed}): {units} units, {ticks} ticks ({secs:.0} s of game time)\n\
raids: {ro} ordered, {re} ended\n\
launched {l}, shot down {k}, got through {h}, air-defence missiles fired {f}\n\
tick time ms: p50 {p50:.3} p95 {p95:.3} max {max:.3} total {tot:.0}\n\
planning: {pl:.1} ms over {pt} ticks\n\
ground queries: {gq} in {gb} batches\n\
non-finite positions: {nan}\n\
stuck trucks: {stuck:?} (busy truck-ticks {busy})\n",
name = self.name,
seed = self.seed,
units = self.units,
ticks = self.ticks_run,
secs = self.ticks_run as f64 / 20.0,
ro = self.raids_ordered,
re = self.raids_ended,
l = self.launched,
k = self.kills,
h = self.hits,
f = self.fired,
p50 = ms(self.tick_p50),
p95 = ms(self.tick_p95),
max = ms(self.tick_max),
tot = ms(self.tick_total),
pl = ms(self.planning),
pt = self.planning_ticks,
gq = self.ground_queries,
gb = self.ground_batches,
nan = self.nan_positions,
stuck = self.stuck_trucks,
busy = self.truck_busy_ticks,
)
}
pub fn json(&self) -> String {
format!(
"{{\"scenario\":{:?},\"seed\":{},\"units\":{},\"ticks\":{},\"raids_ordered\":{},\"raids_ended\":{},\
\"launched\":{},\"kills\":{},\"hits\":{},\"fired\":{},\
\"tick_ms\":{{\"p50\":{:.4},\"p95\":{:.4},\"max\":{:.4},\"total\":{:.2}}},\
\"planning_ms\":{:.2},\"planning_ticks\":{},\"ground_queries\":{},\"ground_batches\":{},\
\"nan_positions\":{},\"stuck_trucks\":{:?},\"truck_busy_ticks\":{}}}",
self.name,
self.seed,
self.units,
self.ticks_run,
self.raids_ordered,
self.raids_ended,
self.launched,
self.kills,
self.hits,
self.fired,
ms(self.tick_p50),
ms(self.tick_p95),
ms(self.tick_max),
ms(self.tick_total),
ms(self.planning),
self.planning_ticks,
self.ground_queries,
self.ground_batches,
self.nan_positions,
self.stuck_trucks,
self.truck_busy_ticks,
)
}
}
// ------------------------------------------------------------ ready-made scenarios
fn ring(sc: &mut Scenario, kinds: &[SamKind], count: usize, r0: f64, r1: f64) {
for i in 0..count {
let a = i as f64 * 2.399_963; // golden angle: an even spread without randomness
let r = r0 + (r1 - r0) * (i as f64 / count.max(1) as f64);
let kind = kinds[i % kinds.len()];
sc.unit(kind, a.sin() * r, a.cos() * r).heading = a;
}
}
/// A big smart raid on 61 systems.
pub fn big_smart() -> Scenario {
let mut sc = Scenario::new("big-smart");
sc.terrain = Terrain::Hills { base: 70, amp: 14.0, period: 90.0 };
for (x, z) in [(0.0, 0.0), (120.0, 80.0), (-150.0, -60.0)] {
sc.objects.push(DVec3::new(x, 70.0, z));
}
ring(&mut sc, &[SamKind::Radar], 6, 150.0, 500.0);
ring(&mut sc, &[SamKind::Ew], 2, 100.0, 300.0);
ring(&mut sc, &SamKind::ARMED, 53, 60.0, 450.0);
assert_eq!(sc.units.len(), 61);
sc.raids.push(RaidSpec {
kinds: vec![
(DroneKind::Gerbera, 8),
(DroneKind::Shahed, 12),
(DroneKind::Geran3, 4),
(DroneKind::Lancet, 4),
(DroneKind::Kh101, 4),
(DroneKind::Kalibr, 4),
(DroneKind::Iskander, 2),
],
approach: Approach::AllSides,
aim: Aim::Smart,
dist: 500.0,
at: 1,
});
sc.ticks = 20 * 60 * 8;
sc
}
/// Trucks and a depot supplying systems that start with empty magazines, under a raid.
pub fn logistics() -> Scenario {
let mut sc = Scenario::new("logistics");
sc.unit(SamKind::Radar, 0.0, -40.0);
sc.unit(SamKind::Depot, 300.0, 300.0);
for (x, z) in [(310.0, 320.0), (290.0, 330.0)] {
sc.unit(SamKind::Tzm, x, z);
}
sc.unit(SamKind::Tzm, 280.0, 280.0);
for (i, kind) in [SamKind::Patriot, SamKind::Tor, SamKind::Buk, SamKind::Nasams, SamKind::Pantsir, SamKind::S400].into_iter().enumerate() {
let a = i as f64 * 1.05;
let u = sc.unit(kind, a.sin() * 120.0, a.cos() * 120.0);
// Every other system starts out of missiles: the trucks have work from the first second.
u.empty = i % 2 == 0;
}
sc.raids.push(RaidSpec {
kinds: vec![(DroneKind::Shahed, 16), (DroneKind::Gerbera, 8), (DroneKind::Kh101, 3)],
approach: Approach::TwoSectors,
aim: Aim::Objects,
dist: 400.0,
at: 1,
});
sc.ticks = 20 * 60 * 6;
sc
}
/// EW stations on and a smart enemy that adapts over three raids.
pub fn ew_adaptive() -> Scenario {
let mut sc = Scenario::new("ew-adaptive");
sc.ew = true;
sc.unit(SamKind::Radar, 0.0, 0.0);
sc.unit(SamKind::Radar, 200.0, -200.0);
sc.unit(SamKind::Ew, 80.0, 0.0);
sc.unit(SamKind::Ew, -80.0, 60.0);
for (i, kind) in [SamKind::Pantsir, SamKind::Gepard, SamKind::Tor, SamKind::Patriot, SamKind::Buk, SamKind::Gepard].into_iter().enumerate() {
let a = i as f64 * 1.05;
sc.unit(kind, a.sin() * 150.0, a.cos() * 150.0);
}
for at in [1, 1, 1] {
sc.raids.push(RaidSpec {
kinds: vec![(DroneKind::Gerbera, 6), (DroneKind::Shahed, 10), (DroneKind::Kh101, 4), (DroneKind::Kalibr, 3)],
approach: Approach::AllSides,
aim: Aim::Smart,
dist: 450.0,
at,
});
}
sc.ticks = 20 * 60 * 12;
sc
}
pub fn preset(name: &str) -> Option<Scenario> {
match name {
"big-smart" => Some(big_smart()),
"logistics" => Some(logistics()),
"ew-adaptive" => Some(ew_adaptive()),
_ => None,
}
}
pub const PRESETS: [&str; 3] = ["big-smart", "logistics", "ew-adaptive"];
+156
View File
@@ -0,0 +1,156 @@
//! Native headless runner for the simulation.
//!
//! cargo run --release --example sim_run -- [options]
//!
//! Options (all optional; a preset gives the defaults, the others override it):
//! --scenario NAME big-smart | logistics | ew-adaptive (default big-smart)
//! --seed N random seed
//! --layout FILE systems from a text file, replacing the preset's; one per line:
//! `KIND X Z [HEADING_RAD] [empty]` (`#` starts a comment)
//! KIND is a system key (patriot, tor, radar, depot, tzm, ew, ...)
//! --raid SPEC replaces the preset's raids; repeatable. SPEC is
//! `kind=count,kind=count[;dist=500][;aim=smart|objects|sams][;approach=sector|two|all|waves][;at=TICK]`
//! --terrain T flat[:HEIGHT] | hills[:BASE:AMP:PERIOD]
//! --ticks N tick limit (20 a second)
//! --run-on do not stop when the raids have ended
//! --ew / --no-ew EW stations on or off
//! --json one JSON object instead of text
//! --list list scenarios, system and drone kinds
//! Exit code 1 when the run has problems (NaN positions, stuck trucks, a raid that never ended).
//! Native timings are for comparing runs on one machine, not an estimate of WASM/GPU cost.
mod harness;
use airdef::sim::{Aim, Approach, DroneKind, SamKind};
use glam::DVec3;
use harness::*;
fn die(msg: &str) -> ! {
eprintln!("sim_run: {msg}");
std::process::exit(2);
}
fn parse_raid(spec: &str) -> RaidSpec {
let mut r = RaidSpec { kinds: Vec::new(), approach: Approach::Sector, aim: Aim::Smart, dist: 400.0, at: 1 };
for (i, part) in spec.split(';').enumerate() {
if i == 0 {
for kc in part.split(',').filter(|s| !s.is_empty()) {
let (k, c) = kc.split_once('=').unwrap_or_else(|| die(&format!("raid kind `{kc}` needs =count")));
let kind = DroneKind::from_key(k).unwrap_or_else(|| die(&format!("unknown drone kind `{k}` (see --list)")));
r.kinds.push((kind, c.parse().unwrap_or_else(|_| die(&format!("bad count `{c}`")))));
}
continue;
}
let (k, v) = part.split_once('=').unwrap_or_else(|| die(&format!("bad raid option `{part}`")));
match k {
"dist" => r.dist = v.parse().unwrap_or_else(|_| die("bad dist")),
"at" => r.at = v.parse().unwrap_or_else(|_| die("bad at")),
"aim" => {
r.aim = match v {
"smart" => Aim::Smart,
"objects" => Aim::Objects,
"sams" => Aim::ObjectsAndSams,
_ => die("aim: smart | objects | sams"),
}
}
"approach" => {
r.approach = match v {
"sector" => Approach::Sector,
"two" => Approach::TwoSectors,
"all" => Approach::AllSides,
"waves" => Approach::Waves,
_ => die("approach: sector | two | all | waves"),
}
}
_ => die(&format!("unknown raid option `{k}`")),
}
}
r
}
fn parse_terrain(t: &str) -> Terrain {
let n: Vec<&str> = t.split(':').collect();
let num = |i: usize, d: f64| n.get(i).map_or(d, |s| s.parse().unwrap_or_else(|_| die("bad terrain number")));
match n[0] {
"flat" => Terrain::Flat(num(1, 70.0) as i32),
"hills" => Terrain::Hills { base: num(1, 70.0) as i32, amp: num(2, 14.0), period: num(3, 90.0) },
_ => die("terrain: flat[:H] | hills[:BASE:AMP:PERIOD]"),
}
}
fn parse_layout(path: &str) -> Vec<Unit> {
let text = std::fs::read_to_string(path).unwrap_or_else(|e| die(&format!("{path}: {e}")));
let mut units = Vec::new();
for (n, line) in text.lines().enumerate() {
let line = line.split('#').next().unwrap_or("").trim();
if line.is_empty() {
continue;
}
let w: Vec<&str> = line.split_whitespace().collect();
let bad = || die(&format!("{path}:{}: expected `KIND X Z [HEADING] [empty]`", n + 1));
if w.len() < 3 {
bad();
}
let kind = SamKind::from_key(w[0]).unwrap_or_else(|| die(&format!("{path}:{}: unknown system `{}`", n + 1, w[0])));
let f = |s: &str| s.parse::<f64>().unwrap_or_else(|_| bad());
let heading = w.get(3).filter(|s| **s != "empty").map_or(0.0, |s| f(s));
units.push(Unit { kind, pos: DVec3::new(f(w[1]), 70.0, f(w[2])), heading, empty: w.contains(&"empty") });
}
units
}
fn main() {
let args: Vec<String> = std::env::args().skip(1).collect();
let mut name = "big-smart".to_string();
let mut i = 0;
// The scenario first, so the other options override it whatever their order.
while i < args.len() {
if args[i] == "--scenario" {
name = args.get(i + 1).cloned().unwrap_or_else(|| die("--scenario needs a name"));
}
i += 1;
}
let mut sc = preset(&name).unwrap_or_else(|| die(&format!("unknown scenario `{name}`; one of {PRESETS:?}")));
let (mut json, mut raids) = (false, Vec::new());
let mut it = args.iter();
while let Some(arg) = it.next() {
let mut value = || it.next().cloned().unwrap_or_else(|| die(&format!("{arg} needs a value")));
match arg.as_str() {
"--scenario" => {
value();
}
"--seed" => sc.seed = value().parse().unwrap_or_else(|_| die("bad seed")),
"--ticks" => sc.ticks = value().parse().unwrap_or_else(|_| die("bad ticks")),
"--layout" => sc.units = parse_layout(&value()),
"--raid" => raids.push(parse_raid(&value())),
"--terrain" => sc.terrain = parse_terrain(&value()),
"--run-on" => sc.stop_at_end = false,
"--ew" => sc.ew = true,
"--no-ew" => sc.ew = false,
"--json" => json = true,
"--list" => {
println!("scenarios: {PRESETS:?}");
println!("systems: {:?}", SamKind::ALL.iter().map(|k| k.key()).collect::<Vec<_>>());
println!("drones: {:?}", DroneKind::ALL.iter().map(|k| k.spec().key).collect::<Vec<_>>());
return;
}
"-h" | "--help" => {
println!("{}", include_str!("main.rs").lines().take_while(|l| l.starts_with("//")).map(|l| l.trim_start_matches("//!").trim_start_matches("//").to_string() + "\n").collect::<String>());
return;
}
other => die(&format!("unknown option `{other}` (try --help)")),
}
}
if !raids.is_empty() {
sc.raids = raids;
}
let r = run(&sc);
if json {
println!("{}", r.json());
} else {
print!("{}", r.text());
}
let problems = r.problems();
if !problems.is_empty() {
eprintln!("PROBLEMS: {}", problems.join("; "));
std::process::exit(1);
}
}
+71 -19
View File
@@ -59,6 +59,26 @@ fn screen_radius(world: vec3<f32>, radius: f32) -> f32 {
return length((b.xy - a.xy) * frame.resolution.xy);
}
// The clip-space position of a world point (what `project` starts from).
fn clip_of(world: vec3<f32>) -> vec4<f32> {
return frame.view_proj * vec4<f32>(world - frame.camera.xyz, 1.0);
}
// A clip position on the screen in uv.
fn clip_uv(c: vec4<f32>) -> vec2<f32> {
let ndc = c.xy / max(abs(c.w), 1e-5);
return vec2<f32>(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
}
// `screen_radius` for a point whose clip position is known (the projection is linear,
// so the offset to the right is added in clip space): saves two projections and a matrix
// product for every effect at every pixel.
fn screen_radius_clip(c0: vec4<f32>, radius: f32) -> f32 {
let right = frame.view_inverse[0].xyz;
let cb = c0 + frame.view_proj * vec4<f32>(right * radius, 0.0);
return length((clip_uv(cb) - clip_uv(c0)) * frame.resolution.xy);
}
fn now() -> f32 {
return frame.params[0].x + frame.time.y;
}
@@ -86,22 +106,27 @@ fn post(uv: vec2<f32>, pixel: vec2<f32>) -> vec4<f32> {
let b = frame.params[BLAST_BASE + 2 * i + 1];
let age = t_now - b.x;
if age < 0.0 { continue; }
// Past both windows (the ring's second, the haze over a hot ball) it bends nothing.
let heat = 1.0 - smoothstep(0.0, 0.5, age / b.y);
if age >= 24.0 && heat <= 0.0 { continue; }
let centre = a.xyz;
let p = project(centre);
if p.z <= 0.5 { continue; }
let to = px - p.xy * frame.resolution.xy;
let c0 = clip_of(centre);
if c0.w <= 0.5 { continue; }
let to = px - clip_uv(c0) * frame.resolution.xy;
let dist = length(to);
// The shock: a ring racing out, fading in a second.
if age < 24.0 {
let ring = screen_radius(centre, age * 3.2 * (0.6 + a.w * 0.1));
let ring = screen_radius_clip(c0, age * 3.2 * (0.6 + a.w * 0.1));
let width = 6.0 + ring * 0.08;
let x = (dist - ring) / width;
let strength = (1.0 - age / 24.0) * min(a.w / 4.0, 2.0) * 10.0;
offset += normalize(to + vec2<f32>(1e-4)) * exp(-x * x) * x * strength * frame.resolution.zw;
// exp(-x * x) * x is below 1e-8 past six widths.
if abs(x) < 6.0 {
let strength = (1.0 - age / 24.0) * min(a.w / 4.0, 2.0) * 10.0;
offset += normalize(to + vec2<f32>(1e-4)) * exp(-x * x) * x * strength * frame.resolution.zw;
}
}
// Heat haze over a hot fireball.
let heat = 1.0 - smoothstep(0.0, 0.5, age / b.y);
let r = screen_radius(centre, a.w * 2.0);
let r = screen_radius_clip(c0, a.w * 2.0);
if heat > 0.0 && dist < r {
let n = noise3(vec3<f32>(px * 0.04, t_now * 0.35)) - 0.5;
offset += vec2<f32>(n, -abs(n)) * heat * (1.0 - dist / r) * 6.0 * frame.resolution.zw;
@@ -115,7 +140,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++) {
@@ -148,8 +174,23 @@ fn post(uv: vec2<f32>, pixel: vec2<f32>) -> vec4<f32> {
let age = t_now - b.x;
if age < 0.0 { continue; }
let life = b.y;
// Burnt out: no density and no fire light.
if age >= life { continue; }
let k = clamp(age / life, 0.0, 1.0);
let size = a.w;
// The ball and its smoke stay inside 4.5 sizes of the blast (radius 2.28 sizes, risen
// 2.2 sizes): rays that miss that sphere cannot reach the volume. Only the
// pixels' own direction is needed, no volume maths.
let rel = a.xyz - frame.camera.xyz;
let reach = size * 4.5 + 0.1;
let cc = dot(rel, rel);
let along = dot(rel, dir);
if cc > reach * reach && (along < 0.0 || cc - along * along > reach * reach) {
// Out of reach of the volume; the fire's glow still counts.
let h0 = 1.0 - smoothstep(0.05, 0.55, k);
glow += vec3<f32>(1.0, 0.5, 0.15) * h0 * h0 * size * 0.05 / (1.0 + length(rel + vec3<f32>(0.0, k * k * size * 2.2, 0.0)) * 0.03);
continue;
}
let seed = vec3<f32>(a.x * 0.37, a.y * 0.11, a.z * 0.53);
// The ball swells fast, then rises and spreads as smoke.
let radius = size * (0.3 + 0.9 * (1.0 - exp(-age / 3.0))) * (1.0 + k * 0.9);
@@ -194,13 +235,20 @@ fn post(uv: vec2<f32>, pixel: vec2<f32>) -> vec4<f32> {
let b = frame.params[BLAST_BASE + 2 * i + 1];
let age = t_now - b.x;
if age < 0.0 || age > 10.0 { continue; }
let p = project(a.xyz);
if p.z <= 0.5 { continue; }
let visible = select(0.25, 1.0, scene_distance(p.xy) > p.z - a.w);
let c0 = clip_of(a.xyz);
if c0.w <= 0.5 { continue; }
let p = vec3<f32>(clip_uv(c0), c0.w);
// Ray distance on both sides: `p.z` is the depth along the view axis, which is shorter
// than the distance along the ray towards the screen edge.
let dist = length(a.xyz - frame.camera.xyz);
let visible = select(0.25, 1.0, scene_distance(p.xy) > dist - a.w);
let f = exp(-age * 0.7) * min(a.w / 4.0, 2.5) * visible;
let r = screen_radius(a.xyz, a.w * 2.5) + 20.0;
let r = screen_radius_clip(c0, a.w * 2.5) + 20.0;
let dd = length(px - p.xy * frame.resolution.xy) / r;
color += vec3<f32>(1.0, 0.85, 0.6) * f * (exp(-dd * dd * 3.0) * 2.5 + exp(-dd * 0.8) * 0.25);
// The bloom's tail is below 5e-4 (under 8 bits) past nine radii.
if dd < 9.0 {
color += vec3<f32>(1.0, 0.85, 0.6) * f * (exp(-dd * dd * 3.0) * 2.5 + exp(-dd * 0.8) * 0.25);
}
color += vec3<f32>(1.0, 0.8, 0.55) * f * 0.12 / (1.0 + p.z * 0.01);
}
@@ -208,11 +256,15 @@ fn post(uv: vec2<f32>, pixel: vec2<f32>) -> vec4<f32> {
for (var j = 0; j < lights; j++) {
let a = frame.params[LIGHT_BASE + 2 * j];
let b = frame.params[LIGHT_BASE + 2 * j + 1];
let p = project(a.xyz);
if p.z <= 0.3 { continue; }
if scene_distance(p.xy) < p.z - a.w * 1.5 { continue; }
let r = screen_radius(a.xyz, a.w) + 3.0;
let dd = length(px - p.xy * frame.resolution.xy) / r;
let c0 = clip_of(a.xyz);
if c0.w <= 0.3 { continue; }
let p = clip_uv(c0);
let r = screen_radius_clip(c0, a.w) + 3.0;
let dd = length(px - p * frame.resolution.xy) / r;
// Far from the glow (intensity at most 2): its tail is below 5e-4, under 8 bits.
// This is checked before the depth lookup, which most pixels then never make.
if dd > 6.0 { continue; }
if scene_distance(p) < length(a.xyz - frame.camera.xyz) - a.w * 1.5 { continue; }
let flicker = 0.85 + 0.15 * noise3(vec3<f32>(a.xyz * 0.7 + vec3<f32>(t_now * 1.3)));
color += b.rgb * b.w * flicker * (exp(-dd * dd * 4.0) * 1.2 + exp(-dd * 1.2) * 0.18);
}
+346 -49
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},
};
@@ -16,11 +16,12 @@ mod monitor;
mod panel;
mod radar;
mod replay;
mod sim;
pub mod sim;
mod scenario;
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 +38,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",
];
@@ -44,6 +57,9 @@ struct AirDef {
sim: Sim,
ground: Ground,
dirty: bool,
/// Wall-clock-free pacing of autosaves: the tick before which none is written (at most one a
/// second while guns fire, and a pause after a failed write).
next_save: u64,
hud_shown: bool,
/// The system the monitor shows (K cycles), none: hidden.
monitor: Option<usize>,
@@ -58,6 +74,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.
@@ -66,8 +86,17 @@ struct AirDef {
menu: Option<Page>,
/// Where the player is, for the sounds' delay and far variants.
listener: DVec3,
/// Sounds on their way to the player at the speed of sound: (tick, event, at, pitch).
pending: Vec<(u64, String, DVec3, f32)>,
/// Sounds on their way to the player at the speed of sound.
pending: Vec<PendingSound>,
}
/// A sound heard later: when, what, where, and how loud and high it was.
struct PendingSound {
due: u64,
name: String,
at: DVec3,
volume: f32,
pitch: f32,
}
/// Blocks a tick the sound travels (343 m/s).
@@ -198,6 +227,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,12 +291,89 @@ 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;
// Its own id range: the models' and the systems' ids are small numbers.
let id = 0x6100_0000 | (b.id & 0x00ff_ffff);
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;
let blasts: Vec<&sim::Blast> = self.sim.blasts.iter().rev().take(48).collect();
p[3] = if self.hd { 1.0 } else { 0.0 };
let mut blasts: Vec<&sim::Blast> = self.sim.blasts.iter().rev().take(48).collect();
// The shader composites the volumes front to back with one shared transmittance, so
// the nearest to the player goes first. This is exact for volumes that do not
// overlap; where two intersect, no order by centre is right along every ray.
if let Some(pl) = player() {
let e = eye(&pl);
blasts.sort_by(|a, b| a.at.distance_squared(e).total_cmp(&b.at.distance_squared(e)));
}
p[1] = blasts.len() as f32;
for b in &blasts {
p.extend([b.at.x as f32, b.at.y as f32, b.at.z as f32, b.size]);
@@ -244,9 +391,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 +409,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()
@@ -308,7 +462,7 @@ impl AirDef {
if delay == 0 {
render::play_sound(&name, at.x, at.y, at.z, volume, pitch);
} else if self.pending.len() < 256 {
self.pending.push((now + delay, name, at, pitch));
self.pending.push(PendingSound { due: now + delay, name, at, volume, pitch });
}
} else {
render::play_sound(event, at.x, at.y, at.z, volume, pitch);
@@ -316,9 +470,9 @@ impl AirDef {
}
let mut i = 0;
while i < self.pending.len() {
if self.pending[i].0 <= now {
let (_, name, at, pitch) = self.pending.swap_remove(i);
render::play_sound(&name, at.x, at.y, at.z, 1.0, pitch);
if self.pending[i].due <= now {
let s = self.pending.swap_remove(i);
render::play_sound(&s.name, s.at.x, s.at.y, s.at.z, s.volume, s.pitch);
} else {
i += 1;
}
@@ -352,24 +506,64 @@ impl AirDef {
// ------------------------------------------------ saving
fn save(&mut self) {
/// The state as text, lines of words: the autosave's format, and the body of a scenario.
/// `fresh`: every system and object whole, magazines full (a scenario to replay from the start).
fn state_text(&self, fresh: bool, view: bool) -> String {
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.filter(|_| !fresh).map_or("- - -".to_string(), |d| format!("{} {} {}", d.x, d.y, d.z));
let new = Sam::new(0, s.kind, s.pos, s.heading);
let (health, cargo, missiles, bursts, empty, reloading) = if fresh {
(new.health, new.cargo, new.missiles, new.bursts, false, 0)
} else {
(s.health, s.cargo, s.missiles, s.bursts, s.empty, s.reloading)
};
text += &format!(
"sam {} {} {} {} {} {} {} {} {} {} {} {} {dest}\n",
s.kind.key(),
s.pos.x,
s.pos.y,
s.pos.z,
s.heading,
s.roles,
health,
cargo,
missiles,
bursts,
u8::from(empty),
reloading,
);
}
for (i, t) in self.sim.targets.iter().enumerate() {
let h = self.sim.target_health.get(i).copied().unwrap_or(100.0);
let h = if fresh { 100.0 } else { self.sim.target_health.get(i).copied().unwrap_or(100.0) };
text += &format!("target {} {} {} {h}\n", t.x, t.y, t.z);
}
text += &format!("damage {}\n", self.sim.damage);
text += &format!("far {}\n", self.far);
if view {
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);
}
if let Err(e) = storage::set(&self.state_key(), text.as_bytes()) {
horizon_mod_sdk::warn(&format!("could not save: {e}"));
text
}
fn save(&mut self) {
let text = self.state_text(false, true);
// Clean only once written: a failed write is tried again after a pause.
match storage::set(&self.state_key(), text.as_bytes()) {
Ok(()) => {
self.dirty = false;
self.next_save = self.sim.tick + 20;
}
Err(e) => {
horizon_mod_sdk::warn(&format!("could not save: {e}"));
self.next_save = self.sim.tick + 200;
}
}
self.dirty = false;
}
fn load(&mut self) {
@@ -378,7 +572,12 @@ impl AirDef {
fn load_from(&mut self, key: &str) {
let Some(bytes) = storage::get(key) else { return };
let text = String::from_utf8_lossy(&bytes);
self.apply_state(&String::from_utf8_lossy(&bytes));
}
/// Adds the state in `text` (`state_text`'s lines; unknown lines are skipped, so older and
/// newer files read alike).
fn apply_state(&mut self, text: &str) {
for line in text.lines() {
let w: Vec<&str> = line.split_whitespace().collect();
let num = |i: usize| w.get(i).and_then(|v| v.parse::<f64>().ok());
@@ -395,7 +594,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") => {
@@ -405,7 +622,19 @@ impl AirDef {
}
}
Some(&"damage") => self.sim.damage = w.get(1) != Some(&"false"),
// A scenario's: the random generator's state, and the wave menu's raid.
Some(&"seed") => {
if let Some(v) = w.get(1).and_then(|v| v.parse::<u64>().ok()) {
self.sim.rng = sim::Rng::new(v);
}
}
Some(&"wave") => self.wave.parse(&w[1..]),
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 +654,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()) {
@@ -448,11 +690,18 @@ impl AirDef {
let shader = self.sim.shader;
let seed = game::get_clock().map_or(0x5eed, |c| c.game_time.wrapping_mul(2654435761) ^ 0x9e37);
self.sim = Sim::new(seed);
self.next_save = 0;
self.sim.shader = shader;
self.ground = Ground::default();
self.monitor = None;
self.grid = false;
self.grid_ids.clear();
// The last world's transient state: its sounds were due on its own
// clock, its picked unit may have another meaning in this one, and
// its models would stay until the next full model list.
self.pending.clear();
self.moving = None;
render::set_models(&[]);
self.far = true;
self.world = Some(world);
self.load();
@@ -487,7 +736,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 +777,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 +794,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 +827,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 +872,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 +938,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] | scenario save|load|list|delete | why [номер] | 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> {
@@ -693,6 +947,7 @@ impl Mod for AirDef {
sim: Sim::new(seed),
ground: Ground::default(),
dirty: false,
next_save: 0,
hud_shown: false,
monitor: None,
monitor_shown: false,
@@ -701,6 +956,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 +968,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 +1010,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 +1027,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 +1039,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,19 +1089,25 @@ 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 {
self.health_mark = mark;
self.dirty = true;
}
if self.dirty {
if self.dirty && self.sim.tick >= self.next_save {
self.save();
}
}
@@ -850,7 +1116,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 +1214,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 +1273,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);
@@ -1046,10 +1314,44 @@ impl Mod for AirDef {
}
None
}
["scenario", rest @ ..] => Some(self.scenario_command(rest)?),
["why", rest @ ..] => {
// The system n of the summary, else the monitored one.
let i = match rest.first() {
Some(n) => n.parse::<usize>().ok().and_then(|n| n.checked_sub(1)).ok_or("/airdef why <номер>")?,
None => self.monitor.ok_or("/airdef why <номер> (или откройте монитор K)")?,
};
if i >= self.sim.sams.len() {
return Err("Нет такой техники".into());
}
for c in self.sim.why(i) {
say(&format!("{} {}", if c.ok { "+" } else { "-" }, c.text));
}
None
}
["raid"] => {
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 +1387,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 +1396,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 +1422,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!(
"В воздухе: {}, ракет: {}, ПВО: {}, сбито: {}, поражений: {}",
+35 -11
View File
@@ -25,11 +25,22 @@ fn fit(t: String, w: f32) -> String {
if render::text_width(&t) <= w {
return t;
}
let mut cut: String = t;
while !cut.is_empty() && render::text_width(&format!("{cut}…")) > w {
cut.pop();
// The width grows with the prefix: bisect over its char starts, so a long
// label costs a few host calls rather than one per dropped char.
let starts: Vec<usize> = t.char_indices().map(|(i, _)| i).collect();
let (mut lo, mut hi) = (0, starts.len());
while lo < hi {
let mid = (lo + hi) / 2;
if render::text_width(&format!("{}…", &t[..starts[mid]])) <= w {
lo = mid + 1;
} else {
hi = mid;
}
}
format!("{cut}…")
if lo == 0 {
return "…".into();
}
format!("{}…", &t[..starts[lo - 1]])
}
fn argb(rgb: u32, a: u32) -> u32 {
@@ -53,9 +64,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);
@@ -101,7 +112,13 @@ fn cell(sim: &Sim, index: usize, x: f32, y: f32, w: f32, h: f32, s: &mut Vec<Sha
s.push(quad(p(a0, r - 0.4), p(a0, r + 0.4), p(a1, r + 0.4), p(a1, r - 0.4), 0x9040FF60));
}
}
let reach = sam.kind.range();
// A system that only fires (no radar) has no reach: a radar's sensor
// range stands in, and nothing is divided by zero.
let reach = match sam.kind.range() {
r if r > 0.0 => r,
_ => spec.sensor,
};
let scale = if reach > 0.0 { rr as f64 / (reach * 1.25) } else { 0.0 };
let sweep = (tick as f32 * 0.12 + index as f32) % std::f32::consts::TAU;
for k in 0..6 {
let a1 = sweep - k as f32 * 0.09;
@@ -115,11 +132,11 @@ fn cell(sim: &Sim, index: usize, x: f32, y: f32, w: f32, h: f32, s: &mut Vec<Sha
for d in sim.drones.iter().filter(|d| d.alive() && sam.seen.contains(&d.id)) {
let rel = d.pos - sam.pos;
let flat = (rel.x * rel.x + rel.z * rel.z).sqrt();
if flat > reach * 1.25 {
if reach <= 0.0 || flat > reach * 1.25 {
continue;
}
seen += 1;
let k = (rr as f64 / (reach * 1.25)) as f32;
let k = scale as f32;
let (bx, by) = (rx + rel.x as f32 * k, ry + rel.z as f32 * k);
let allowed = sam.roles & d.kind.spec().class.bit() != 0;
let color = if Some(d.id) == target.map(|t| t.id) {
@@ -180,7 +197,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;
@@ -213,6 +230,13 @@ fn cell(sim: &Sim, index: usize, x: f32, y: f32, w: f32, h: f32, s: &mut Vec<Sha
pub fn draw(sim: &Sim, index: usize) -> Vec<Shape> {
let mut s = Vec::new();
cell(sim, index, 4.0, 4.0, 300.0, 150.0, &mut s);
// Why it fires or doesn't: the conditions the simulation decides by, worked out now for this one.
let checks = sim.why(index);
s.push(fill(4.0, 156.0, 300.0, 4.0 + 10.0 * checks.len() as f32, 0xA0080C0A));
for (k, c) in checks.iter().enumerate() {
let color = if c.ok { 0xFF80FF90 } else { 0xFFFF9060 };
s.push(text(fit(format!("{} {}", if c.ok { "+" } else { "-" }, c.text), 294.0), 7.0, 158.0 + 10.0 * k as f32, color));
}
s
}
+20 -5
View File
@@ -10,6 +10,8 @@ const CYAN: u32 = 0x9FE0FF;
const GREY: u32 = 0xB0B0B0;
const YELLOW: u32 = 0xFFE070;
const GREEN: u32 = 0x80FF90;
/// The most lines the summary holds (the HUD's room).
const MAX_LINES: usize = 16;
/// Metres (a block is a metre) as a short distance.
pub fn distance(m: f64) -> String {
@@ -77,8 +79,20 @@ pub fn summary(sim: &Sim, air: &[&Drone], player: DVec3) -> Vec<Line> {
if !sim.missiles.is_empty() {
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));
// The score is the last line and always shown: the unit list gets what
// room is left, and cuts its own tail.
let room = (MAX_LINES - 1).saturating_sub(lines.len());
let total = sim.sams.len();
let mut shown = total.min(8);
if shown + usize::from(total > 8) > room {
shown = room.saturating_sub(1);
}
for sam in sim.sams.iter().take(shown) {
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)));
}
@@ -87,15 +101,16 @@ pub fn summary(sim: &Sim, air: &[&Drone], player: DVec3) -> Vec<Line> {
}
lines.push(line(text, CYAN));
}
if sim.sams.len() > 8 {
lines.push(line(format!("…и ещё {} комплексов", sim.sams.len() - 8), CYAN));
if total > shown && room > 0 {
lines.push(line(format!("…и ещё {} комплексов", total - shown), CYAN));
}
// Room for the score whatever came before.
lines.truncate(MAX_LINES - 1);
let real = sim.launched.saturating_sub(0);
let rate = if real > 0 { sim.kills as f64 * 100.0 / real as f64 } else { 0.0 };
lines.push(line(
format!("Сбито {} из {} ({rate:.0}%) · прорвалось {}", sim.kills, sim.launched, sim.hits),
GREY,
));
lines.truncate(16);
lines
}
+73 -40
View File
@@ -5,7 +5,8 @@
//! behind the drone, then behind the interceptor or from the side — slowing
//! time round the moment itself.
use crate::panel::distance;
use crate::sim::record::{EventKind, Recording, What};
use crate::sim::record::{EventKind, Recording, STEP, What};
use std::cell::Cell;
use crate::sim::{Class, DroneKind, Out, Sim};
use glam::DVec3;
use horizon_mod_sdk::render::{CameraView, FillShape, Point, QuadShape, Rect, Shape, TextShape, ViewShape};
@@ -31,8 +32,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,
@@ -55,6 +56,8 @@ pub struct Player {
/// Ticks the controls stay on screen.
help: u32,
pub done: bool,
/// The map's area, worked out on the first map frame.
area: Cell<Option<[f64; 4]>>,
}
/// What the replay's keys do.
@@ -188,7 +191,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,
@@ -201,6 +204,7 @@ impl Player {
};
let _ = sim;
Player {
area: Cell::new(None),
rec,
mode,
t: start,
@@ -291,7 +295,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 +495,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);
}
@@ -581,34 +585,16 @@ impl Player {
fn map(&self, rec: &Recording, gw: f32, gh: f32) -> Vec<Shape> {
let mut s = vec![fill(0.0, 0.0, gw, gh, 0xF0060A08)];
let o = rec.origin;
// The area: everything that flew, but not far beyond the systems.
let mut lo = (f64::MAX, f64::MAX);
let mut hi = (f64::MIN, f64::MIN);
let mut grow = |p: DVec3| {
lo = (lo.0.min(p.x), lo.1.min(p.z));
hi = (hi.0.max(p.x), hi.1.max(p.z));
// The area is the same every tick: worked out once for the recording.
let [l0, l1, h0, h1] = match self.area.get() {
Some(a) => a,
None => {
let a = area(rec, o);
self.area.set(Some(a));
a
}
};
for tr in &rec.tracks {
if let What::Drone(k) = tr.what
&& k.spec().ballistic.is_some()
{
continue;
}
for p in tr.path(o, 20, rec.len as f64) {
grow(p);
}
}
for sam in &rec.sams {
grow(sam.2 - DVec3::splat(60.0));
grow(sam.2 + DVec3::splat(60.0));
}
for p in &rec.objects {
grow(*p);
}
if lo.0 > hi.0 {
lo = (o.x - 200.0, o.z - 200.0);
hi = (o.x + 200.0, o.z + 200.0);
}
let (lo, hi) = ((l0, l1), (h0, h1));
let (top, bottom) = (20.0f32, 34.0f32);
let (aw, ah) = (gw - 16.0, gh - top - bottom);
let k = (aw as f64 / (hi.0 - lo.0).max(1.0)).min(ah as f64 / (hi.1 - lo.1).max(1.0));
@@ -638,7 +624,9 @@ impl Player {
let (x, y) = at(*p);
s.push(fill(x - 3.0, y - 3.0, 6.0, 6.0, 0xFFFFFFFF));
}
// Tracks up to now: a budget of segments shared out.
// Tracks up to now: a budget of segments shared out. The stride is
// worked out from the points there are, so only the segments drawn
// are ever copied.
let n = rec.tracks.len().max(1);
let per = (640 / n).clamp(2, 14);
for tr in &rec.tracks {
@@ -647,16 +635,21 @@ impl Player {
What::Drone(_) => (0x90FF4040, 0xFFFF6060),
What::Missile(..) => (0x90FFD040, 0xFFFFE070),
};
let pts = tr.path(o, 1, self.t);
let upto = ((self.t - tr.first as f64) / STEP as f64).floor();
if upto < 0.0 {
continue;
}
let last = ((tr.last() - tr.first) / STEP) as f64;
let count = upto.min(last) as usize + 1;
if count < 2 {
continue;
}
let pts = tr.path(o, count.div_ceil(per).max(1), self.t);
if pts.len() < 2 {
continue;
}
let every = pts.len().div_ceil(per).max(1);
let mut prev = at(pts[0]);
for (i, p) in pts.iter().enumerate().skip(1) {
if i % every != 0 && i != pts.len() - 1 {
continue;
}
for p in pts.iter().skip(1) {
let q = at(*p);
if inside(prev) && inside(q) {
s.push(line(prev, q, 1.0, color));
@@ -699,6 +692,11 @@ impl Player {
}
let keys = format!("{KEYS_HELP} · колесо: время");
s.push(text(keys, gw / 2.0, gh - 12.0, 0xC0FFFFFF, true));
// What went wrong for the air defence, as counted during the raid.
let tally = rec.tally.text();
if !tally.is_empty() && horizon_mod_sdk::render::text_width(&tally) < gw - 24.0 - lw {
s.push(text(tally, 8.0, 6.0 + 11.0, 0xB0FFC080, false));
}
let (tx, ty, tw) = (8.0, gh - bottom + 6.0, gw - 16.0);
s.push(fill(tx, ty, tw, 3.0, 0x60FFFFFF));
let len = rec.len.max(1) as f32;
@@ -718,6 +716,41 @@ impl Player {
}
}
/// The ground the map shows, as [lo x, lo z, hi x, hi z]: everything that
/// flew (not the ballistic falls), with the systems and objects, or round the
/// origin when nothing did.
fn area(rec: &Recording, o: DVec3) -> [f64; 4] {
let mut lo = (f64::MAX, f64::MAX);
let mut hi = (f64::MIN, f64::MIN);
let mut grow = |p: DVec3| {
lo = (lo.0.min(p.x), lo.1.min(p.z));
hi = (hi.0.max(p.x), hi.1.max(p.z));
};
for tr in &rec.tracks {
if let What::Drone(k) = tr.what
&& k.spec().ballistic.is_some()
{
continue;
}
for p in tr.path(o, 20, rec.len as f64) {
grow(p);
}
}
// The systems, with some room round them.
for sam in &rec.sams {
grow(sam.2 - DVec3::splat(60.0));
grow(sam.2 + DVec3::splat(60.0));
}
for p in &rec.objects {
grow(*p);
}
if lo.0 > hi.0 {
lo = (o.x - 200.0, o.z - 200.0);
hi = (o.x + 200.0, o.z + 200.0);
}
[lo.0, lo.1, hi.0, hi.1]
}
impl Player {
pub fn event_clip(rec: &Recording, i: usize) -> Mode {
Mode::Clips { list: vec![clip(rec, i)], at: 0, reel: false }
+122
View File
@@ -0,0 +1,122 @@
//! Named scenarios: a snapshot of the layout (systems, objects), the wave menu's raid and the
//! random generator, kept under a name and loaded back later. The body is the autosave's text
//! (`state_text`); a header names the format's version and the world it was made in.
use super::*;
/// The format this build writes and reads.
const VERSION: u32 = 1;
/// Names of the saved scenarios, one a line.
const INDEX: &str = "scenarios";
fn key(name: &str) -> String {
format!("scn-{name}")
}
fn valid(name: &str) -> Result<String, String> {
let n = name.to_lowercase();
if n.is_empty() || n.len() > 32 || !n.chars().all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_') {
return Err("Имя сценария: до 32 символов, латиница, цифры, - и _".into());
}
Ok(n)
}
fn names() -> Vec<String> {
storage::get(INDEX).map(|b| String::from_utf8_lossy(&b).lines().map(str::to_string).filter(|l| !l.is_empty()).collect()).unwrap_or_default()
}
impl AirDef {
/// The scenario as text.
fn scenario_text(&self, name: &str, fresh: bool) -> String {
let mut text = format!("scenario {VERSION}\nname {name}\nworld {}\n", self.state_key());
text += &self.state_text(fresh, false);
text += &format!("seed {}\nwave {}\n", self.sim.rng.state(), self.wave.text());
text
}
/// Replaces the world's layout with the scenario `text`; nothing changes if it is refused.
fn apply_scenario(&mut self, text: &str, force: bool) -> Result<(), String> {
let mut lines = text.lines();
let version = lines
.next()
.and_then(|l| l.strip_prefix("scenario "))
.ok_or("Это не сценарий (нет строки версии)")?
.trim();
match version.parse::<u32>() {
Ok(VERSION) => {}
_ => return Err(format!("Сценарий формата {version}: эта версия мода понимает только формат {VERSION}")),
}
let world = text.lines().find_map(|l| l.strip_prefix("world ")).unwrap_or("").trim();
if !force && world != self.state_key() {
return Err("Сценарий сделан в другом мире: координаты могут не подойти (загрузить всё равно: добавьте force)".into());
}
if self.replay.is_some() {
self.stop_replay();
}
// Everything of the last layout goes: its systems with their jobs and targets, the sky,
// the cook-offs to come; ids are made anew with the new systems.
self.sim.clear();
self.sim.sams.clear();
self.sim.targets.clear();
self.sim.target_health.clear();
self.sim.roles.clear();
self.sim.kills = 0;
self.sim.hits = 0;
self.sim.launched = 0;
self.monitor = None;
self.grid_ids.clear();
self.moving = None;
self.pending.clear();
render::set_models(&[]);
self.apply_state(text);
self.dirty = true;
Ok(())
}
/// `/airdef scenario save|load|list|delete`.
pub(crate) fn scenario_command(&mut self, args: &[&str]) -> Result<String, String> {
match args {
["save", name, flags @ ..] => {
let name = valid(name)?;
let fresh = flags.contains(&"fresh");
let text = self.scenario_text(&name, fresh);
storage::set(&key(&name), text.as_bytes()).map_err(|e| format!("Не удалось сохранить: {e}"))?;
let mut all = names();
if !all.contains(&name) {
all.push(name.clone());
storage::set(INDEX, all.join("\n").as_bytes()).map_err(|e| format!("Не удалось записать список: {e}"))?;
}
Ok(format!(
"Сценарий «{name}» сохранён: комплексов {}, объектов {}{}",
self.sim.sams.len(),
self.sim.targets.len(),
if fresh { " (целые, с полным боезапасом)" } else { "" }
))
}
["load", name, flags @ ..] => {
let name = valid(name)?;
let bytes = storage::get(&key(&name)).ok_or(format!("Нет сценария «{name}» (/airdef scenario list)"))?;
self.apply_scenario(&String::from_utf8_lossy(&bytes), flags.contains(&"force"))?;
Ok(format!("Сценарий «{name}» загружен: комплексов {}, объектов {}", self.sim.sams.len(), self.sim.targets.len()))
}
["list"] => {
let all = names();
if all.is_empty() {
return Ok("Сценариев нет: /airdef scenario save <имя>".into());
}
Ok(format!("Сценарии: {}", all.join(", ")))
}
["delete", name] => {
let name = valid(name)?;
let mut all = names();
if !all.contains(&name) {
return Err(format!("Нет сценария «{name}»"));
}
all.retain(|n| *n != name);
storage::delete(&key(&name));
storage::set(INDEX, all.join("\n").as_bytes()).map_err(|e| format!("Не удалось записать список: {e}"))?;
Ok(format!("Сценарий «{name}» удалён"))
}
_ => Err("/airdef scenario save <имя> [fresh] | load <имя> [force] | list | delete <имя>".into()),
}
}
}
+23 -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,12 +179,18 @@ 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);
}
for h in &mut self.target_health {
*h = 100.0;
}
// The wrecks' ammunition still to cook off goes with them.
self.later.clear();
n
}
@@ -186,14 +202,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);
}
}
}
+254
View File
@@ -0,0 +1,254 @@
//! 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;
// The drones sent at a new target: (waiting in the queue, index), to get a new route.
let mut retargeted: Vec<(bool, usize)> = Vec::new();
let mut n = 0usize;
for (qi, (_, d)) in self.queue.iter_mut().enumerate().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();
retargeted.push((true, qi));
n += 1;
}
// In flight: a target system destroyed — the next known one, or keep going.
for (di, d) in self.drones.iter_mut().enumerate().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();
retargeted.push((false, di));
}
}
}
self.replan_routes(&retargeted);
}
/// Routes round the known defences for drones that were just sent at a new target, from where they are
/// (or wait). One grid and one set of prices cover them all, and the work is the planner's share for a
/// tick: those left over fly straight, as they did before there was a replan. A Lancet's strike on a
/// unit and the ballistic missiles go straight on purpose, as in the raid's own plan.
fn replan_routes(&mut self, jobs: &[(bool, usize)]) {
let drone = |s: &Sim, (queued, i): (bool, usize)| if queued { s.queue[i].1.clone() } else { s.drones[i].clone() };
let jobs: Vec<(bool, usize)> = jobs
.iter()
.copied()
.filter(|&j| {
let d = drone(self, j);
let spec = d.kind.spec();
d.smart && spec.ballistic.is_none() && !spec.decoy && d.kind != DroneKind::Lancet
})
.collect();
if jobs.is_empty() {
return;
}
// One grid over all their starts and goals, with room round them to go by.
let (mut lo, mut hi) = (DVec3::splat(f64::INFINITY), DVec3::splat(f64::NEG_INFINITY));
for &j in &jobs {
let d = drone(self, j);
for p in [d.pos, d.target] {
lo = lo.min(p);
hi = hi.max(p);
}
}
let span = (hi.x - lo.x).max(hi.z - lo.z) / 2.0 + 450.0;
let cell = raid::CELL.max(2.0 * span / raid::CELLS as f64);
let centre = (lo + hi) / 2.0;
let grid = raid::Grid { min: centre - DVec3::new(span, 0.0, span), cell, size: (2.0 * span / cell) as i32 + 1 };
let mut prices: HashMap<DroneKind, Vec<f32>> = HashMap::new();
let mut routes: HashMap<(DroneKind, (i32, i32), (i32, i32)), Vec<DVec3>> = HashMap::new();
let mut work = 0usize;
for j in jobs {
let d = drone(self, j);
if work >= raid::WORK_PER_TICK {
break;
}
let key = (d.kind, grid.index(d.pos), grid.index(d.target));
let route = match routes.get(&key) {
Some(r) => r.clone(),
None => {
if !prices.contains_key(&d.kind) {
let (p, cost) = self.price_grid(&grid, d.kind);
work += cost;
prices.insert(d.kind, p);
}
let (r, cost) = raid::plan_route(&grid, &prices[&d.kind], d.pos, d.target);
work += cost;
routes.insert(key, r.clone());
r
}
};
let d = if j.0 { &mut self.queue[j.1].1 } else { &mut self.drones[j.1] };
d.route = route;
}
}
}

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