Compare commits

..

11 Commits

Author SHA1 Message Date
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
118 changed files with 18188 additions and 488 deletions
+4 -6
View File
@@ -5,7 +5,9 @@
name: build
on:
# Branches only: the release's own tag (build-N) mustn't start another build.
push:
branches: ['**']
workflow_dispatch:
env:
@@ -22,12 +24,8 @@ jobs:
run: |
curl -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --target wasm32-unknown-unknown
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
# Инструмент упаковки (cargo-horizon-mod без пробного запуска) — единственное, что стоит кэшировать.
- uses: actions/cache@v3
continue-on-error: true
with:
path: ~/.cargo/bin/cargo-horizon-mod
key: tool-${{ hashFiles('HORIZON_COMMIT', 'horizon-patches/*') }}
# Без actions/cache: кэш-сервер раннера из контейнеров задач недоступен — восстановление
# падало по таймауту, а сохранение вешало «Complete job» на пару минут. Инструмент собирается заново.
- name: Форк Horizon
run: nice -n 19 python3 tools/airdef.py fork
- name: Мод
+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/beam"
}
}
}
@@ -0,0 +1,7 @@
{
"variants": {
"": {
"model": "airdef:block/buk_chassis"
}
}
}
@@ -0,0 +1,7 @@
{
"variants": {
"": {
"model": "airdef:block/buk_rails"
}
}
}
@@ -0,0 +1,7 @@
{
"variants": {
"": {
"model": "airdef:block/buk_turret"
}
}
}
@@ -0,0 +1,7 @@
{
"variants": {
"": {
"model": "airdef:block/depot"
}
}
}
@@ -0,0 +1,7 @@
{
"variants": {
"": {
"model": "airdef:block/ew_truck"
}
}
}
@@ -0,0 +1,7 @@
{
"variants": {
"": {
"model": "airdef:block/flare"
}
}
}
@@ -0,0 +1,7 @@
{
"variants": {
"": {
"model": "airdef:block/irist_chassis"
}
}
}
@@ -0,0 +1,7 @@
{
"variants": {
"": {
"model": "airdef:block/irist_tubes"
}
}
}
@@ -0,0 +1,7 @@
{
"variants": {
"": {
"model": "airdef:block/nasams_launcher"
}
}
}
@@ -0,0 +1,7 @@
{
"variants": {
"": {
"model": "airdef:block/nasams_trailer"
}
}
}
@@ -0,0 +1,7 @@
{
"variants": {
"": {
"model": "airdef:block/p1sun"
}
}
}
@@ -0,0 +1,7 @@
{
"variants": {
"": {
"model": "airdef:block/p1sun_launcher"
}
}
}
@@ -0,0 +1,7 @@
{
"variants": {
"": {
"model": "airdef:block/radar_truck"
}
}
}
@@ -0,0 +1,7 @@
{
"variants": {
"": {
"model": "airdef:block/searchlight"
}
}
}
@@ -0,0 +1,7 @@
{
"variants": {
"": {
"model": "airdef:block/searchlight_head"
}
}
}
@@ -0,0 +1,7 @@
{
"variants": {
"": {
"model": "airdef:block/tor_chassis"
}
}
}
@@ -0,0 +1,7 @@
{
"variants": {
"": {
"model": "airdef:block/tor_turret"
}
}
}
@@ -0,0 +1,7 @@
{
"variants": {
"": {
"model": "airdef:block/tzm_truck"
}
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "airdef:item/deploy_buk"
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "airdef:item/deploy_depot"
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "airdef:item/deploy_ew"
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "airdef:item/deploy_irist"
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "airdef:item/deploy_light"
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "airdef:item/deploy_nasams"
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "airdef:item/deploy_p1sun"
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "airdef:item/deploy_radar"
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "airdef:item/deploy_tor"
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "airdef:item/deploy_tzm"
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "airdef:item/flare_gun"
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "airdef:item/mover"
}
}
+31
View File
@@ -39,6 +39,25 @@
"block.airdef.iskander": "Искандер-М",
"block.airdef.kinzhal": "Кинжал",
"block.airdef.tracer": "Трассер",
"block.airdef.irist_chassis": "Шасси IRIS-T SLM",
"block.airdef.irist_tubes": "Пакет IRIS-T",
"block.airdef.nasams_trailer": "Прицеп NASAMS",
"block.airdef.nasams_launcher": "Пусковая NASAMS",
"block.airdef.buk_chassis": "Шасси Бука",
"block.airdef.buk_turret": "Башня Бука",
"block.airdef.buk_rails": "Ракеты Бука",
"block.airdef.tor_chassis": "Шасси Тора",
"block.airdef.tor_turret": "Башня Тора",
"block.airdef.p1sun_launcher": "Пусковая P1-SUN",
"block.airdef.p1sun": "Дрон-перехватчик P1-SUN",
"block.airdef.radar_truck": "Шасси РЛС",
"block.airdef.depot": "Склад боеприпасов",
"block.airdef.tzm_truck": "Транспортно-заряжающая машина",
"block.airdef.ew_truck": "Станция РЭБ",
"block.airdef.searchlight": "Прожектор",
"block.airdef.searchlight_head": "Голова прожектора",
"block.airdef.beam": "Луч прожектора",
"block.airdef.flare": "Осветительная ракета",
"block.airdef.reentry": "Боевой блок",
"item.airdef.designator": "Целеуказатель",
"item.airdef.launch_shahed": "Пуск: Шахед-136",
@@ -56,6 +75,18 @@
"item.airdef.launch_iskander": "Пуск: Искандер-М",
"item.airdef.launch_kinzhal": "Пуск: Кинжал",
"item.airdef.launch_oreshnik": "Пуск: Орешник",
"item.airdef.deploy_irist": "Развернуть IRIS-T SLM",
"item.airdef.deploy_nasams": "Развернуть NASAMS",
"item.airdef.deploy_buk": "Развернуть Бук-М2",
"item.airdef.deploy_tor": "Развернуть Тор-М2",
"item.airdef.deploy_p1sun": "Развернуть P1-SUN",
"item.airdef.deploy_radar": "Развернуть РЛС дальнего обнаружения",
"item.airdef.deploy_depot": "Развернуть склад боеприпасов",
"item.airdef.deploy_tzm": "Развернуть ТЗМ",
"item.airdef.deploy_ew": "Развернуть станцию РЭБ",
"item.airdef.deploy_light": "Развернуть прожектор",
"item.airdef.mover": "Пульт перемещения",
"item.airdef.flare_gun": "Осветительная ракета",
"item.airdef.remover": "Свернуть ПВО",
"item.airdef.cam_shahed": "Шахед-136",
"item.airdef.cam_geran3": "Герань-3",
+31
View File
@@ -39,6 +39,25 @@
"block.airdef.iskander": "Искандер-М",
"block.airdef.kinzhal": "Кинжал",
"block.airdef.tracer": "Трассер",
"block.airdef.irist_chassis": "Шасси IRIS-T SLM",
"block.airdef.irist_tubes": "Пакет IRIS-T",
"block.airdef.nasams_trailer": "Прицеп NASAMS",
"block.airdef.nasams_launcher": "Пусковая NASAMS",
"block.airdef.buk_chassis": "Шасси Бука",
"block.airdef.buk_turret": "Башня Бука",
"block.airdef.buk_rails": "Ракеты Бука",
"block.airdef.tor_chassis": "Шасси Тора",
"block.airdef.tor_turret": "Башня Тора",
"block.airdef.p1sun_launcher": "Пусковая P1-SUN",
"block.airdef.p1sun": "Дрон-перехватчик P1-SUN",
"block.airdef.radar_truck": "Шасси РЛС",
"block.airdef.depot": "Склад боеприпасов",
"block.airdef.tzm_truck": "Транспортно-заряжающая машина",
"block.airdef.ew_truck": "Станция РЭБ",
"block.airdef.searchlight": "Прожектор",
"block.airdef.searchlight_head": "Голова прожектора",
"block.airdef.beam": "Луч прожектора",
"block.airdef.flare": "Осветительная ракета",
"block.airdef.reentry": "Боевой блок",
"item.airdef.designator": "Целеуказатель",
"item.airdef.launch_shahed": "Пуск: Шахед-136",
@@ -56,6 +75,18 @@
"item.airdef.launch_iskander": "Пуск: Искандер-М",
"item.airdef.launch_kinzhal": "Пуск: Кинжал",
"item.airdef.launch_oreshnik": "Пуск: Орешник",
"item.airdef.deploy_irist": "Развернуть IRIS-T SLM",
"item.airdef.deploy_nasams": "Развернуть NASAMS",
"item.airdef.deploy_buk": "Развернуть Бук-М2",
"item.airdef.deploy_tor": "Развернуть Тор-М2",
"item.airdef.deploy_p1sun": "Развернуть P1-SUN",
"item.airdef.deploy_radar": "Развернуть РЛС дальнего обнаружения",
"item.airdef.deploy_depot": "Развернуть склад боеприпасов",
"item.airdef.deploy_tzm": "Развернуть ТЗМ",
"item.airdef.deploy_ew": "Развернуть станцию РЭБ",
"item.airdef.deploy_light": "Развернуть прожектор",
"item.airdef.mover": "Пульт перемещения",
"item.airdef.flare_gun": "Осветительная ракета",
"item.airdef.remover": "Свернуть ПВО",
"item.airdef.cam_shahed": "Шахед-136",
"item.airdef.cam_geran3": "Герань-3",
@@ -0,0 +1,77 @@
{
"ambientocclusion": false,
"textures": {
"beam": "airdef:block/beam",
"particle": "airdef:block/beam"
},
"elements": [
{
"from": [
6.5,
6.5,
-16.0
],
"to": [
9.5,
9.5,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#beam"
},
"south": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#beam"
},
"east": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#beam"
},
"west": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#beam"
},
"up": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#beam"
},
"down": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#beam"
}
}
}
]
}
@@ -0,0 +1,487 @@
{
"ambientocclusion": false,
"textures": {
"ru_green": "airdef:block/ru_green",
"dark_metal": "airdef:block/dark_metal",
"track": "airdef:block/track",
"particle": "airdef:block/ru_green"
},
"elements": [
{
"from": [
-10.0,
4.0,
-14.0
],
"to": [
26.0,
15.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
11.0
],
"texture": "#ru_green"
},
"south": {
"uv": [
0,
0,
16,
11.0
],
"texture": "#ru_green"
},
"east": {
"uv": [
0,
0,
16,
11.0
],
"texture": "#ru_green"
},
"west": {
"uv": [
0,
0,
16,
11.0
],
"texture": "#ru_green"
},
"up": {
"uv": [
0,
0,
16,
16
],
"texture": "#ru_green"
},
"down": {
"uv": [
0,
0,
16,
16
],
"texture": "#ru_green"
}
}
},
{
"from": [
-10.0,
4.0,
-16.0
],
"to": [
26.0,
10.0,
-14.0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
6.0
],
"texture": "#ru_green"
},
"south": {
"uv": [
0,
0,
16,
6.0
],
"texture": "#ru_green"
},
"east": {
"uv": [
0,
0,
2.0,
6.0
],
"texture": "#ru_green"
},
"west": {
"uv": [
0,
0,
2.0,
6.0
],
"texture": "#ru_green"
},
"up": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#ru_green"
},
"down": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#ru_green"
}
}
},
{
"from": [
-8.0,
15.0,
22.0
],
"to": [
24.0,
16.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
1.0
],
"texture": "#dark_metal"
},
"south": {
"uv": [
0,
0,
16,
1.0
],
"texture": "#dark_metal"
},
"east": {
"uv": [
0,
0,
10.0,
1.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
10.0,
1.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
16,
10.0
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
16,
10.0
],
"texture": "#dark_metal"
}
}
},
{
"from": [
-14.0,
0.0,
-15.0
],
"to": [
-10.0,
9.0,
31.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
9.0
],
"texture": "#track"
},
"south": {
"uv": [
0,
0,
4.0,
9.0
],
"texture": "#track"
},
"east": {
"uv": [
0,
0,
16,
9.0
],
"texture": "#track"
},
"west": {
"uv": [
0,
0,
16,
9.0
],
"texture": "#track"
},
"up": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#track"
},
"down": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#track"
}
}
},
{
"from": [
26.0,
0.0,
-15.0
],
"to": [
30.0,
9.0,
31.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
9.0
],
"texture": "#track"
},
"south": {
"uv": [
0,
0,
4.0,
9.0
],
"texture": "#track"
},
"east": {
"uv": [
0,
0,
16,
9.0
],
"texture": "#track"
},
"west": {
"uv": [
0,
0,
16,
9.0
],
"texture": "#track"
},
"up": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#track"
},
"down": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#track"
}
}
},
{
"from": [
-14.5,
5.0,
-15.0
],
"to": [
-10.0,
10.0,
31.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.5,
5.0
],
"texture": "#ru_green"
},
"south": {
"uv": [
0,
0,
4.5,
5.0
],
"texture": "#ru_green"
},
"east": {
"uv": [
0,
0,
16,
5.0
],
"texture": "#ru_green"
},
"west": {
"uv": [
0,
0,
16,
5.0
],
"texture": "#ru_green"
},
"up": {
"uv": [
0,
0,
4.5,
16
],
"texture": "#ru_green"
},
"down": {
"uv": [
0,
0,
4.5,
16
],
"texture": "#ru_green"
}
}
},
{
"from": [
26.0,
5.0,
-15.0
],
"to": [
30.5,
10.0,
31.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.5,
5.0
],
"texture": "#ru_green"
},
"south": {
"uv": [
0,
0,
4.5,
5.0
],
"texture": "#ru_green"
},
"east": {
"uv": [
0,
0,
16,
5.0
],
"texture": "#ru_green"
},
"west": {
"uv": [
0,
0,
16,
5.0
],
"texture": "#ru_green"
},
"up": {
"uv": [
0,
0,
4.5,
16
],
"texture": "#ru_green"
},
"down": {
"uv": [
0,
0,
4.5,
16
],
"texture": "#ru_green"
}
}
}
]
}
@@ -0,0 +1,896 @@
{
"ambientocclusion": false,
"textures": {
"missile_ru": "airdef:block/missile_ru",
"booster": "airdef:block/booster",
"dark_metal": "airdef:block/dark_metal",
"ru_green": "airdef:block/ru_green",
"particle": "airdef:block/missile_ru"
},
"elements": [
{
"from": [
-10.5,
4.0,
-16.0
],
"to": [
-7.5,
7.0,
26.0
],
"faces": {
"north": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#missile_ru"
},
"south": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#missile_ru"
},
"east": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#missile_ru"
},
"west": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#missile_ru"
},
"up": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#missile_ru"
},
"down": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#missile_ru"
}
}
},
{
"from": [
-10.8,
3.5,
20.0
],
"to": [
-7.2,
7.5,
27.0
],
"faces": {
"north": {
"uv": [
0,
0,
3.6000000000000005,
4.0
],
"texture": "#booster"
},
"south": {
"uv": [
0,
0,
3.6000000000000005,
4.0
],
"texture": "#booster"
},
"east": {
"uv": [
0,
0,
7.0,
4.0
],
"texture": "#booster"
},
"west": {
"uv": [
0,
0,
7.0,
4.0
],
"texture": "#booster"
},
"up": {
"uv": [
0,
0,
3.6000000000000005,
7.0
],
"texture": "#booster"
},
"down": {
"uv": [
0,
0,
3.6000000000000005,
7.0
],
"texture": "#booster"
}
}
},
{
"from": [
-9.5,
2.0,
-6.0
],
"to": [
-8.5,
4.0,
16.0
],
"faces": {
"north": {
"uv": [
0,
0,
1.0,
2.0
],
"texture": "#dark_metal"
},
"south": {
"uv": [
0,
0,
1.0,
2.0
],
"texture": "#dark_metal"
},
"east": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
1.0,
16
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
1.0,
16
],
"texture": "#dark_metal"
}
}
},
{
"from": [
-4.5,
4.0,
-16.0
],
"to": [
-1.5,
7.0,
26.0
],
"faces": {
"north": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#missile_ru"
},
"south": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#missile_ru"
},
"east": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#missile_ru"
},
"west": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#missile_ru"
},
"up": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#missile_ru"
},
"down": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#missile_ru"
}
}
},
{
"from": [
-4.8,
3.5,
20.0
],
"to": [
-1.2,
7.5,
27.0
],
"faces": {
"north": {
"uv": [
0,
0,
3.5999999999999996,
4.0
],
"texture": "#booster"
},
"south": {
"uv": [
0,
0,
3.5999999999999996,
4.0
],
"texture": "#booster"
},
"east": {
"uv": [
0,
0,
7.0,
4.0
],
"texture": "#booster"
},
"west": {
"uv": [
0,
0,
7.0,
4.0
],
"texture": "#booster"
},
"up": {
"uv": [
0,
0,
3.5999999999999996,
7.0
],
"texture": "#booster"
},
"down": {
"uv": [
0,
0,
3.5999999999999996,
7.0
],
"texture": "#booster"
}
}
},
{
"from": [
-3.5,
2.0,
-6.0
],
"to": [
-2.5,
4.0,
16.0
],
"faces": {
"north": {
"uv": [
0,
0,
1.0,
2.0
],
"texture": "#dark_metal"
},
"south": {
"uv": [
0,
0,
1.0,
2.0
],
"texture": "#dark_metal"
},
"east": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
1.0,
16
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
1.0,
16
],
"texture": "#dark_metal"
}
}
},
{
"from": [
11.5,
4.0,
-16.0
],
"to": [
14.5,
7.0,
26.0
],
"faces": {
"north": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#missile_ru"
},
"south": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#missile_ru"
},
"east": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#missile_ru"
},
"west": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#missile_ru"
},
"up": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#missile_ru"
},
"down": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#missile_ru"
}
}
},
{
"from": [
11.2,
3.5,
20.0
],
"to": [
14.8,
7.5,
27.0
],
"faces": {
"north": {
"uv": [
0,
0,
3.6000000000000014,
4.0
],
"texture": "#booster"
},
"south": {
"uv": [
0,
0,
3.6000000000000014,
4.0
],
"texture": "#booster"
},
"east": {
"uv": [
0,
0,
7.0,
4.0
],
"texture": "#booster"
},
"west": {
"uv": [
0,
0,
7.0,
4.0
],
"texture": "#booster"
},
"up": {
"uv": [
0,
0,
3.6000000000000014,
7.0
],
"texture": "#booster"
},
"down": {
"uv": [
0,
0,
3.6000000000000014,
7.0
],
"texture": "#booster"
}
}
},
{
"from": [
12.5,
2.0,
-6.0
],
"to": [
13.5,
4.0,
16.0
],
"faces": {
"north": {
"uv": [
0,
0,
1.0,
2.0
],
"texture": "#dark_metal"
},
"south": {
"uv": [
0,
0,
1.0,
2.0
],
"texture": "#dark_metal"
},
"east": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
1.0,
16
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
1.0,
16
],
"texture": "#dark_metal"
}
}
},
{
"from": [
17.5,
4.0,
-16.0
],
"to": [
20.5,
7.0,
26.0
],
"faces": {
"north": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#missile_ru"
},
"south": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#missile_ru"
},
"east": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#missile_ru"
},
"west": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#missile_ru"
},
"up": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#missile_ru"
},
"down": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#missile_ru"
}
}
},
{
"from": [
17.2,
3.5,
20.0
],
"to": [
20.8,
7.5,
27.0
],
"faces": {
"north": {
"uv": [
0,
0,
3.6000000000000014,
4.0
],
"texture": "#booster"
},
"south": {
"uv": [
0,
0,
3.6000000000000014,
4.0
],
"texture": "#booster"
},
"east": {
"uv": [
0,
0,
7.0,
4.0
],
"texture": "#booster"
},
"west": {
"uv": [
0,
0,
7.0,
4.0
],
"texture": "#booster"
},
"up": {
"uv": [
0,
0,
3.6000000000000014,
7.0
],
"texture": "#booster"
},
"down": {
"uv": [
0,
0,
3.6000000000000014,
7.0
],
"texture": "#booster"
}
}
},
{
"from": [
18.5,
2.0,
-6.0
],
"to": [
19.5,
4.0,
16.0
],
"faces": {
"north": {
"uv": [
0,
0,
1.0,
2.0
],
"texture": "#dark_metal"
},
"south": {
"uv": [
0,
0,
1.0,
2.0
],
"texture": "#dark_metal"
},
"east": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
1.0,
16
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
1.0,
16
],
"texture": "#dark_metal"
}
}
},
{
"from": [
-12.0,
0.0,
-2.0
],
"to": [
28.0,
2.0,
14.0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#ru_green"
},
"south": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#ru_green"
},
"east": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#ru_green"
},
"west": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#ru_green"
},
"up": {
"uv": [
0,
0,
16,
16
],
"texture": "#ru_green"
},
"down": {
"uv": [
0,
0,
16,
16
],
"texture": "#ru_green"
}
}
}
]
}
@@ -0,0 +1,214 @@
{
"ambientocclusion": false,
"textures": {
"ru_green": "airdef:block/ru_green",
"radar_face": "airdef:block/radar_face",
"particle": "airdef:block/ru_green"
},
"elements": [
{
"from": [
-4.0,
0.0,
-4.0
],
"to": [
20.0,
9.0,
20.0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
9.0
],
"texture": "#ru_green"
},
"south": {
"uv": [
0,
0,
16,
9.0
],
"texture": "#ru_green"
},
"east": {
"uv": [
0,
0,
16,
9.0
],
"texture": "#ru_green"
},
"west": {
"uv": [
0,
0,
16,
9.0
],
"texture": "#ru_green"
},
"up": {
"uv": [
0,
0,
16,
16
],
"texture": "#ru_green"
},
"down": {
"uv": [
0,
0,
16,
16
],
"texture": "#ru_green"
}
}
},
{
"from": [
1.0,
2.0,
-10.0
],
"to": [
15.0,
14.0,
-4.0
],
"faces": {
"north": {
"uv": [
0,
0,
14.0,
12.0
],
"texture": "#radar_face"
},
"south": {
"uv": [
0,
0,
14.0,
12.0
],
"texture": "#radar_face"
},
"east": {
"uv": [
0,
0,
6.0,
12.0
],
"texture": "#radar_face"
},
"west": {
"uv": [
0,
0,
6.0,
12.0
],
"texture": "#radar_face"
},
"up": {
"uv": [
0,
0,
14.0,
6.0
],
"texture": "#radar_face"
},
"down": {
"uv": [
0,
0,
14.0,
6.0
],
"texture": "#radar_face"
}
}
},
{
"from": [
-2.0,
9.0,
6.0
],
"to": [
18.0,
11.0,
18.0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#ru_green"
},
"south": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#ru_green"
},
"east": {
"uv": [
0,
0,
12.0,
2.0
],
"texture": "#ru_green"
},
"west": {
"uv": [
0,
0,
12.0,
2.0
],
"texture": "#ru_green"
},
"up": {
"uv": [
0,
0,
16,
12.0
],
"texture": "#ru_green"
},
"down": {
"uv": [
0,
0,
16,
12.0
],
"texture": "#ru_green"
}
}
}
]
}
@@ -0,0 +1,488 @@
{
"ambientocclusion": false,
"textures": {
"concrete": "airdef:block/concrete",
"crate": "airdef:block/crate",
"tube": "airdef:block/tube",
"tube_cap": "airdef:block/tube_cap",
"particle": "airdef:block/concrete"
},
"elements": [
{
"from": [
-14.0,
0.0,
-14.0
],
"to": [
30.0,
2.0,
30.0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#concrete"
},
"south": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#concrete"
},
"east": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#concrete"
},
"west": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#concrete"
},
"up": {
"uv": [
0,
0,
16,
16
],
"texture": "#concrete"
},
"down": {
"uv": [
0,
0,
16,
16
],
"texture": "#concrete"
}
}
},
{
"from": [
-12.0,
2.0,
-12.0
],
"to": [
0.0,
10.0,
0.0
],
"faces": {
"north": {
"uv": [
0,
0,
12.0,
8.0
],
"texture": "#crate"
},
"south": {
"uv": [
0,
0,
12.0,
8.0
],
"texture": "#crate"
},
"east": {
"uv": [
0,
0,
12.0,
8.0
],
"texture": "#crate"
},
"west": {
"uv": [
0,
0,
12.0,
8.0
],
"texture": "#crate"
},
"up": {
"uv": [
0,
0,
12.0,
12.0
],
"texture": "#crate"
},
"down": {
"uv": [
0,
0,
12.0,
12.0
],
"texture": "#crate"
}
}
},
{
"from": [
-12.0,
2.0,
4.0
],
"to": [
0.0,
10.0,
16.0
],
"faces": {
"north": {
"uv": [
0,
0,
12.0,
8.0
],
"texture": "#crate"
},
"south": {
"uv": [
0,
0,
12.0,
8.0
],
"texture": "#crate"
},
"east": {
"uv": [
0,
0,
12.0,
8.0
],
"texture": "#crate"
},
"west": {
"uv": [
0,
0,
12.0,
8.0
],
"texture": "#crate"
},
"up": {
"uv": [
0,
0,
12.0,
12.0
],
"texture": "#crate"
},
"down": {
"uv": [
0,
0,
12.0,
12.0
],
"texture": "#crate"
}
}
},
{
"from": [
4.0,
2.0,
-12.0
],
"to": [
16.0,
10.0,
0.0
],
"faces": {
"north": {
"uv": [
0,
0,
12.0,
8.0
],
"texture": "#crate"
},
"south": {
"uv": [
0,
0,
12.0,
8.0
],
"texture": "#crate"
},
"east": {
"uv": [
0,
0,
12.0,
8.0
],
"texture": "#crate"
},
"west": {
"uv": [
0,
0,
12.0,
8.0
],
"texture": "#crate"
},
"up": {
"uv": [
0,
0,
12.0,
12.0
],
"texture": "#crate"
},
"down": {
"uv": [
0,
0,
12.0,
12.0
],
"texture": "#crate"
}
}
},
{
"from": [
6.0,
2.0,
6.0
],
"to": [
28.0,
7.0,
10.0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
5.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
16,
5.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
4.0,
5.0
],
"texture": "#tube"
},
"west": {
"uv": [
0,
0,
4.0,
5.0
],
"texture": "#tube"
},
"up": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#tube"
},
"down": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#tube"
}
}
},
{
"from": [
6.0,
7.0,
6.0
],
"to": [
28.0,
12.0,
10.0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
5.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
16,
5.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
4.0,
5.0
],
"texture": "#tube"
},
"west": {
"uv": [
0,
0,
4.0,
5.0
],
"texture": "#tube"
},
"up": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#tube"
},
"down": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#tube"
}
}
},
{
"from": [
6.0,
12.0,
6.0
],
"to": [
28.0,
17.0,
10.0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
5.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
16,
5.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
4.0,
5.0
],
"texture": "#tube"
},
"west": {
"uv": [
0,
0,
4.0,
5.0
],
"texture": "#tube"
},
"up": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#tube"
},
"down": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#tube"
}
}
}
]
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,146 @@
{
"ambientocclusion": false,
"textures": {
"lamp": "airdef:block/lamp",
"antenna": "airdef:block/antenna",
"particle": "airdef:block/lamp"
},
"elements": [
{
"from": [
6.5,
6.5,
6.5
],
"to": [
9.5,
9.5,
9.5
],
"faces": {
"north": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#lamp"
},
"south": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#lamp"
},
"east": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#lamp"
},
"west": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#lamp"
},
"up": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#lamp"
},
"down": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#lamp"
}
}
},
{
"from": [
7.6,
9.5,
7.6
],
"to": [
8.4,
16.0,
8.4
],
"faces": {
"north": {
"uv": [
0,
0,
0.8000000000000007,
6.5
],
"texture": "#antenna"
},
"south": {
"uv": [
0,
0,
0.8000000000000007,
6.5
],
"texture": "#antenna"
},
"east": {
"uv": [
0,
0,
0.8000000000000007,
6.5
],
"texture": "#antenna"
},
"west": {
"uv": [
0,
0,
0.8000000000000007,
6.5
],
"texture": "#antenna"
},
"up": {
"uv": [
0,
0,
0.8000000000000007,
0.8000000000000007
],
"texture": "#antenna"
},
"down": {
"uv": [
0,
0,
0.8000000000000007,
0.8000000000000007
],
"texture": "#antenna"
}
}
}
]
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,691 @@
{
"ambientocclusion": false,
"textures": {
"canister": "airdef:block/canister",
"tube_cap": "airdef:block/tube_cap",
"dark_metal": "airdef:block/dark_metal",
"particle": "airdef:block/canister"
},
"elements": [
{
"from": [
-1.5,
0.0,
-16.0
],
"to": [
2.5,
4.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"west": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"up": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
},
"down": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
}
}
},
{
"from": [
-1.5,
5.0,
-16.0
],
"to": [
2.5,
9.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"west": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"up": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
},
"down": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
}
}
},
{
"from": [
3.5,
0.0,
-16.0
],
"to": [
7.5,
4.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"west": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"up": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
},
"down": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
}
}
},
{
"from": [
3.5,
5.0,
-16.0
],
"to": [
7.5,
9.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"west": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"up": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
},
"down": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
}
}
},
{
"from": [
8.5,
0.0,
-16.0
],
"to": [
12.5,
4.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"west": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"up": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
},
"down": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
}
}
},
{
"from": [
8.5,
5.0,
-16.0
],
"to": [
12.5,
9.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"west": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"up": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
},
"down": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
}
}
},
{
"from": [
13.5,
0.0,
-16.0
],
"to": [
17.5,
4.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"west": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"up": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
},
"down": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
}
}
},
{
"from": [
13.5,
5.0,
-16.0
],
"to": [
17.5,
9.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"west": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"up": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
},
"down": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
}
}
},
{
"from": [
-2.5,
0.0,
26.0
],
"to": [
18.5,
9.0,
28.0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
9.0
],
"texture": "#dark_metal"
},
"south": {
"uv": [
0,
0,
16,
9.0
],
"texture": "#dark_metal"
},
"east": {
"uv": [
0,
0,
2.0,
9.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
2.0,
9.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#dark_metal"
}
}
},
{
"from": [
-2.5,
0.0,
-2.0
],
"to": [
18.5,
9.0,
0.0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
9.0
],
"texture": "#dark_metal"
},
"south": {
"uv": [
0,
0,
16,
9.0
],
"texture": "#dark_metal"
},
"east": {
"uv": [
0,
0,
2.0,
9.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
2.0,
9.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#dark_metal"
}
}
}
]
}
@@ -0,0 +1,555 @@
{
"ambientocclusion": false,
"textures": {
"canister": "airdef:block/canister",
"tube_cap": "airdef:block/tube_cap",
"dark_metal": "airdef:block/dark_metal",
"particle": "airdef:block/canister"
},
"elements": [
{
"from": [
1.0,
0.0,
-16.0
],
"to": [
5.0,
4.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"west": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"up": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
},
"down": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
}
}
},
{
"from": [
1.0,
5.0,
-16.0
],
"to": [
5.0,
9.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"west": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"up": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
},
"down": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
}
}
},
{
"from": [
6.0,
0.0,
-16.0
],
"to": [
10.0,
4.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"west": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"up": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
},
"down": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
}
}
},
{
"from": [
6.0,
5.0,
-16.0
],
"to": [
10.0,
9.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"west": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"up": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
},
"down": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
}
}
},
{
"from": [
11.0,
0.0,
-16.0
],
"to": [
15.0,
4.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"west": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"up": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
},
"down": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
}
}
},
{
"from": [
11.0,
5.0,
-16.0
],
"to": [
15.0,
9.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"west": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"up": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
},
"down": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
}
}
},
{
"from": [
0.0,
0.0,
26.0
],
"to": [
16.0,
9.0,
28.0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
9.0
],
"texture": "#dark_metal"
},
"south": {
"uv": [
0,
0,
16,
9.0
],
"texture": "#dark_metal"
},
"east": {
"uv": [
0,
0,
2.0,
9.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
2.0,
9.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#dark_metal"
}
}
},
{
"from": [
0.0,
0.0,
-2.0
],
"to": [
16.0,
9.0,
0.0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
9.0
],
"texture": "#dark_metal"
},
"south": {
"uv": [
0,
0,
16,
9.0
],
"texture": "#dark_metal"
},
"east": {
"uv": [
0,
0,
2.0,
9.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
2.0,
9.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#dark_metal"
}
}
}
]
}
@@ -0,0 +1,488 @@
{
"ambientocclusion": false,
"textures": {
"nato_green": "airdef:block/nato_green",
"dark_metal": "airdef:block/dark_metal",
"metal": "airdef:block/metal",
"tire": "airdef:block/tire",
"particle": "airdef:block/nato_green"
},
"elements": [
{
"from": [
-6.0,
5.0,
-10.0
],
"to": [
22.0,
9.0,
26.0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#nato_green"
},
"south": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#nato_green"
},
"east": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#nato_green"
},
"west": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#nato_green"
},
"up": {
"uv": [
0,
0,
16,
16
],
"texture": "#nato_green"
},
"down": {
"uv": [
0,
0,
16,
16
],
"texture": "#nato_green"
}
}
},
{
"from": [
-4.0,
9.0,
10.0
],
"to": [
20.0,
13.0,
24.0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#dark_metal"
},
"south": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#dark_metal"
},
"east": {
"uv": [
0,
0,
14.0,
4.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
14.0,
4.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
16,
14.0
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
16,
14.0
],
"texture": "#dark_metal"
}
}
},
{
"from": [
6.0,
3.0,
-16.0
],
"to": [
10.0,
6.0,
-10.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
3.0
],
"texture": "#metal"
},
"south": {
"uv": [
0,
0,
4.0,
3.0
],
"texture": "#metal"
},
"east": {
"uv": [
0,
0,
6.0,
3.0
],
"texture": "#metal"
},
"west": {
"uv": [
0,
0,
6.0,
3.0
],
"texture": "#metal"
},
"up": {
"uv": [
0,
0,
4.0,
6.0
],
"texture": "#metal"
},
"down": {
"uv": [
0,
0,
4.0,
6.0
],
"texture": "#metal"
}
}
},
{
"from": [
-9.0,
0.0,
4.0
],
"to": [
-5.5,
9.0,
11.0
],
"faces": {
"north": {
"uv": [
0,
0,
3.5,
9.0
],
"texture": "#tire"
},
"south": {
"uv": [
0,
0,
3.5,
9.0
],
"texture": "#tire"
},
"east": {
"uv": [
0,
0,
7.0,
9.0
],
"texture": "#tire"
},
"west": {
"uv": [
0,
0,
7.0,
9.0
],
"texture": "#tire"
},
"up": {
"uv": [
0,
0,
3.5,
7.0
],
"texture": "#tire"
},
"down": {
"uv": [
0,
0,
3.5,
7.0
],
"texture": "#tire"
}
}
},
{
"from": [
21.5,
0.0,
4.0
],
"to": [
25.0,
9.0,
11.0
],
"faces": {
"north": {
"uv": [
0,
0,
3.5,
9.0
],
"texture": "#tire"
},
"south": {
"uv": [
0,
0,
3.5,
9.0
],
"texture": "#tire"
},
"east": {
"uv": [
0,
0,
7.0,
9.0
],
"texture": "#tire"
},
"west": {
"uv": [
0,
0,
7.0,
9.0
],
"texture": "#tire"
},
"up": {
"uv": [
0,
0,
3.5,
7.0
],
"texture": "#tire"
},
"down": {
"uv": [
0,
0,
3.5,
7.0
],
"texture": "#tire"
}
}
},
{
"from": [
-9.0,
0.0,
14.0
],
"to": [
-5.5,
9.0,
21.0
],
"faces": {
"north": {
"uv": [
0,
0,
3.5,
9.0
],
"texture": "#tire"
},
"south": {
"uv": [
0,
0,
3.5,
9.0
],
"texture": "#tire"
},
"east": {
"uv": [
0,
0,
7.0,
9.0
],
"texture": "#tire"
},
"west": {
"uv": [
0,
0,
7.0,
9.0
],
"texture": "#tire"
},
"up": {
"uv": [
0,
0,
3.5,
7.0
],
"texture": "#tire"
},
"down": {
"uv": [
0,
0,
3.5,
7.0
],
"texture": "#tire"
}
}
},
{
"from": [
21.5,
0.0,
14.0
],
"to": [
25.0,
9.0,
21.0
],
"faces": {
"north": {
"uv": [
0,
0,
3.5,
9.0
],
"texture": "#tire"
},
"south": {
"uv": [
0,
0,
3.5,
9.0
],
"texture": "#tire"
},
"east": {
"uv": [
0,
0,
7.0,
9.0
],
"texture": "#tire"
},
"west": {
"uv": [
0,
0,
7.0,
9.0
],
"texture": "#tire"
},
"up": {
"uv": [
0,
0,
3.5,
7.0
],
"texture": "#tire"
},
"down": {
"uv": [
0,
0,
3.5,
7.0
],
"texture": "#tire"
}
}
}
]
}
@@ -0,0 +1,691 @@
{
"ambientocclusion": false,
"textures": {
"dark_metal": "airdef:block/dark_metal",
"missile": "airdef:block/missile",
"prop": "airdef:block/prop",
"particle": "airdef:block/dark_metal"
},
"elements": [
{
"from": [
6.5,
6.5,
-10.0
],
"to": [
9.5,
9.5,
10.0
],
"faces": {
"north": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#dark_metal"
},
"south": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#dark_metal"
},
"east": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#dark_metal"
}
}
},
{
"from": [
7.0,
7.0,
-14.0
],
"to": [
9.0,
9.0,
-10.0
],
"faces": {
"north": {
"uv": [
0,
0,
2.0,
2.0
],
"texture": "#missile"
},
"south": {
"uv": [
0,
0,
2.0,
2.0
],
"texture": "#missile"
},
"east": {
"uv": [
0,
0,
4.0,
2.0
],
"texture": "#missile"
},
"west": {
"uv": [
0,
0,
4.0,
2.0
],
"texture": "#missile"
},
"up": {
"uv": [
0,
0,
2.0,
4.0
],
"texture": "#missile"
},
"down": {
"uv": [
0,
0,
2.0,
4.0
],
"texture": "#missile"
}
}
},
{
"from": [
2.0,
2.0,
0.0
],
"to": [
8.0,
8.0,
1.5
],
"faces": {
"north": {
"uv": [
0,
0,
6.0,
6.0
],
"texture": "#dark_metal"
},
"south": {
"uv": [
0,
0,
6.0,
6.0
],
"texture": "#dark_metal"
},
"east": {
"uv": [
0,
0,
1.5,
6.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
1.5,
6.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
6.0,
1.5
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
6.0,
1.5
],
"texture": "#dark_metal"
}
}
},
{
"from": [
-0.5,
-0.5,
0.0
],
"to": [
4.5,
4.5,
0.6
],
"faces": {
"north": {
"uv": [
0,
0,
5.0,
5.0
],
"texture": "#prop"
},
"south": {
"uv": [
0,
0,
5.0,
5.0
],
"texture": "#prop"
},
"east": {
"uv": [
0,
0,
0.6,
5.0
],
"texture": "#prop"
},
"west": {
"uv": [
0,
0,
0.6,
5.0
],
"texture": "#prop"
},
"up": {
"uv": [
0,
0,
5.0,
0.6
],
"texture": "#prop"
},
"down": {
"uv": [
0,
0,
5.0,
0.6
],
"texture": "#prop"
}
}
},
{
"from": [
2.0,
8.0,
0.0
],
"to": [
8.0,
14.0,
1.5
],
"faces": {
"north": {
"uv": [
0,
0,
6.0,
6.0
],
"texture": "#dark_metal"
},
"south": {
"uv": [
0,
0,
6.0,
6.0
],
"texture": "#dark_metal"
},
"east": {
"uv": [
0,
0,
1.5,
6.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
1.5,
6.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
6.0,
1.5
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
6.0,
1.5
],
"texture": "#dark_metal"
}
}
},
{
"from": [
-0.5,
11.5,
0.0
],
"to": [
4.5,
16.5,
0.6
],
"faces": {
"north": {
"uv": [
0,
0,
5.0,
5.0
],
"texture": "#prop"
},
"south": {
"uv": [
0,
0,
5.0,
5.0
],
"texture": "#prop"
},
"east": {
"uv": [
0,
0,
0.6,
5.0
],
"texture": "#prop"
},
"west": {
"uv": [
0,
0,
0.6,
5.0
],
"texture": "#prop"
},
"up": {
"uv": [
0,
0,
5.0,
0.6
],
"texture": "#prop"
},
"down": {
"uv": [
0,
0,
5.0,
0.6
],
"texture": "#prop"
}
}
},
{
"from": [
8.0,
2.0,
0.0
],
"to": [
14.0,
8.0,
1.5
],
"faces": {
"north": {
"uv": [
0,
0,
6.0,
6.0
],
"texture": "#dark_metal"
},
"south": {
"uv": [
0,
0,
6.0,
6.0
],
"texture": "#dark_metal"
},
"east": {
"uv": [
0,
0,
1.5,
6.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
1.5,
6.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
6.0,
1.5
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
6.0,
1.5
],
"texture": "#dark_metal"
}
}
},
{
"from": [
11.5,
-0.5,
0.0
],
"to": [
16.5,
4.5,
0.6
],
"faces": {
"north": {
"uv": [
0,
0,
5.0,
5.0
],
"texture": "#prop"
},
"south": {
"uv": [
0,
0,
5.0,
5.0
],
"texture": "#prop"
},
"east": {
"uv": [
0,
0,
0.6,
5.0
],
"texture": "#prop"
},
"west": {
"uv": [
0,
0,
0.6,
5.0
],
"texture": "#prop"
},
"up": {
"uv": [
0,
0,
5.0,
0.6
],
"texture": "#prop"
},
"down": {
"uv": [
0,
0,
5.0,
0.6
],
"texture": "#prop"
}
}
},
{
"from": [
8.0,
8.0,
0.0
],
"to": [
14.0,
14.0,
1.5
],
"faces": {
"north": {
"uv": [
0,
0,
6.0,
6.0
],
"texture": "#dark_metal"
},
"south": {
"uv": [
0,
0,
6.0,
6.0
],
"texture": "#dark_metal"
},
"east": {
"uv": [
0,
0,
1.5,
6.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
1.5,
6.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
6.0,
1.5
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
6.0,
1.5
],
"texture": "#dark_metal"
}
}
},
{
"from": [
11.5,
11.5,
0.0
],
"to": [
16.5,
16.5,
0.6
],
"faces": {
"north": {
"uv": [
0,
0,
5.0,
5.0
],
"texture": "#prop"
},
"south": {
"uv": [
0,
0,
5.0,
5.0
],
"texture": "#prop"
},
"east": {
"uv": [
0,
0,
0.6,
5.0
],
"texture": "#prop"
},
"west": {
"uv": [
0,
0,
0.6,
5.0
],
"texture": "#prop"
},
"up": {
"uv": [
0,
0,
5.0,
0.6
],
"texture": "#prop"
},
"down": {
"uv": [
0,
0,
5.0,
0.6
],
"texture": "#prop"
}
}
}
]
}
@@ -0,0 +1,690 @@
{
"ambientocclusion": false,
"textures": {
"dark_metal": "airdef:block/dark_metal",
"tube_cap": "airdef:block/tube_cap",
"particle": "airdef:block/dark_metal"
},
"elements": [
{
"from": [
0.5,
0.0,
-16.0
],
"to": [
3.5,
3.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#dark_metal"
}
}
},
{
"from": [
0.5,
4.0,
-16.0
],
"to": [
3.5,
7.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#dark_metal"
}
}
},
{
"from": [
4.5,
0.0,
-16.0
],
"to": [
7.5,
3.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#dark_metal"
}
}
},
{
"from": [
4.5,
4.0,
-16.0
],
"to": [
7.5,
7.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#dark_metal"
}
}
},
{
"from": [
8.5,
0.0,
-16.0
],
"to": [
11.5,
3.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#dark_metal"
}
}
},
{
"from": [
8.5,
4.0,
-16.0
],
"to": [
11.5,
7.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#dark_metal"
}
}
},
{
"from": [
12.5,
0.0,
-16.0
],
"to": [
15.5,
3.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#dark_metal"
}
}
},
{
"from": [
12.5,
4.0,
-16.0
],
"to": [
15.5,
7.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#dark_metal"
}
}
},
{
"from": [
-0.5,
0.0,
26.0
],
"to": [
16.5,
7.0,
28.0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
7.0
],
"texture": "#dark_metal"
},
"south": {
"uv": [
0,
0,
16,
7.0
],
"texture": "#dark_metal"
},
"east": {
"uv": [
0,
0,
2.0,
7.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
2.0,
7.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#dark_metal"
}
}
},
{
"from": [
-0.5,
0.0,
-2.0
],
"to": [
16.5,
7.0,
0.0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
7.0
],
"texture": "#dark_metal"
},
"south": {
"uv": [
0,
0,
16,
7.0
],
"texture": "#dark_metal"
},
"east": {
"uv": [
0,
0,
2.0,
7.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
2.0,
7.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#dark_metal"
}
}
}
]
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,145 @@
{
"ambientocclusion": false,
"textures": {
"dark_metal": "airdef:block/dark_metal",
"particle": "airdef:block/dark_metal"
},
"elements": [
{
"from": [
2.0,
0.0,
2.0
],
"to": [
14.0,
3.0,
14.0
],
"faces": {
"north": {
"uv": [
0,
0,
12.0,
3.0
],
"texture": "#dark_metal"
},
"south": {
"uv": [
0,
0,
12.0,
3.0
],
"texture": "#dark_metal"
},
"east": {
"uv": [
0,
0,
12.0,
3.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
12.0,
3.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
12.0,
12.0
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
12.0,
12.0
],
"texture": "#dark_metal"
}
}
},
{
"from": [
6.0,
3.0,
6.0
],
"to": [
10.0,
12.0,
10.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
9.0
],
"texture": "#dark_metal"
},
"south": {
"uv": [
0,
0,
4.0,
9.0
],
"texture": "#dark_metal"
},
"east": {
"uv": [
0,
0,
4.0,
9.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
4.0,
9.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#dark_metal"
}
}
}
]
}
@@ -0,0 +1,146 @@
{
"ambientocclusion": false,
"textures": {
"dark_metal": "airdef:block/dark_metal",
"lamp": "airdef:block/lamp",
"particle": "airdef:block/dark_metal"
},
"elements": [
{
"from": [
3.0,
3.0,
-2.0
],
"to": [
13.0,
13.0,
14.0
],
"faces": {
"north": {
"uv": [
0,
0,
10.0,
10.0
],
"texture": "#dark_metal"
},
"south": {
"uv": [
0,
0,
10.0,
10.0
],
"texture": "#dark_metal"
},
"east": {
"uv": [
0,
0,
16,
10.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
16,
10.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
10.0,
16
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
10.0,
16
],
"texture": "#dark_metal"
}
}
},
{
"from": [
3.5,
3.5,
-2.5
],
"to": [
12.5,
12.5,
-2.0
],
"faces": {
"north": {
"uv": [
0,
0,
9.0,
9.0
],
"texture": "#lamp"
},
"south": {
"uv": [
0,
0,
9.0,
9.0
],
"texture": "#lamp"
},
"east": {
"uv": [
0,
0,
0.5,
9.0
],
"texture": "#lamp"
},
"west": {
"uv": [
0,
0,
0.5,
9.0
],
"texture": "#lamp"
},
"up": {
"uv": [
0,
0,
9.0,
0.5
],
"texture": "#lamp"
},
"down": {
"uv": [
0,
0,
9.0,
0.5
],
"texture": "#lamp"
}
}
}
]
}
@@ -0,0 +1,487 @@
{
"ambientocclusion": false,
"textures": {
"ru_khaki": "airdef:block/ru_khaki",
"dark_metal": "airdef:block/dark_metal",
"track": "airdef:block/track",
"particle": "airdef:block/ru_khaki"
},
"elements": [
{
"from": [
-10.0,
4.0,
-14.0
],
"to": [
26.0,
15.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
11.0
],
"texture": "#ru_khaki"
},
"south": {
"uv": [
0,
0,
16,
11.0
],
"texture": "#ru_khaki"
},
"east": {
"uv": [
0,
0,
16,
11.0
],
"texture": "#ru_khaki"
},
"west": {
"uv": [
0,
0,
16,
11.0
],
"texture": "#ru_khaki"
},
"up": {
"uv": [
0,
0,
16,
16
],
"texture": "#ru_khaki"
},
"down": {
"uv": [
0,
0,
16,
16
],
"texture": "#ru_khaki"
}
}
},
{
"from": [
-10.0,
4.0,
-16.0
],
"to": [
26.0,
10.0,
-14.0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
6.0
],
"texture": "#ru_khaki"
},
"south": {
"uv": [
0,
0,
16,
6.0
],
"texture": "#ru_khaki"
},
"east": {
"uv": [
0,
0,
2.0,
6.0
],
"texture": "#ru_khaki"
},
"west": {
"uv": [
0,
0,
2.0,
6.0
],
"texture": "#ru_khaki"
},
"up": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#ru_khaki"
},
"down": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#ru_khaki"
}
}
},
{
"from": [
-8.0,
15.0,
22.0
],
"to": [
24.0,
16.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
1.0
],
"texture": "#dark_metal"
},
"south": {
"uv": [
0,
0,
16,
1.0
],
"texture": "#dark_metal"
},
"east": {
"uv": [
0,
0,
10.0,
1.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
10.0,
1.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
16,
10.0
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
16,
10.0
],
"texture": "#dark_metal"
}
}
},
{
"from": [
-14.0,
0.0,
-15.0
],
"to": [
-10.0,
9.0,
31.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
9.0
],
"texture": "#track"
},
"south": {
"uv": [
0,
0,
4.0,
9.0
],
"texture": "#track"
},
"east": {
"uv": [
0,
0,
16,
9.0
],
"texture": "#track"
},
"west": {
"uv": [
0,
0,
16,
9.0
],
"texture": "#track"
},
"up": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#track"
},
"down": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#track"
}
}
},
{
"from": [
26.0,
0.0,
-15.0
],
"to": [
30.0,
9.0,
31.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
9.0
],
"texture": "#track"
},
"south": {
"uv": [
0,
0,
4.0,
9.0
],
"texture": "#track"
},
"east": {
"uv": [
0,
0,
16,
9.0
],
"texture": "#track"
},
"west": {
"uv": [
0,
0,
16,
9.0
],
"texture": "#track"
},
"up": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#track"
},
"down": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#track"
}
}
},
{
"from": [
-14.5,
5.0,
-15.0
],
"to": [
-10.0,
10.0,
31.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.5,
5.0
],
"texture": "#ru_khaki"
},
"south": {
"uv": [
0,
0,
4.5,
5.0
],
"texture": "#ru_khaki"
},
"east": {
"uv": [
0,
0,
16,
5.0
],
"texture": "#ru_khaki"
},
"west": {
"uv": [
0,
0,
16,
5.0
],
"texture": "#ru_khaki"
},
"up": {
"uv": [
0,
0,
4.5,
16
],
"texture": "#ru_khaki"
},
"down": {
"uv": [
0,
0,
4.5,
16
],
"texture": "#ru_khaki"
}
}
},
{
"from": [
26.0,
5.0,
-15.0
],
"to": [
30.5,
10.0,
31.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.5,
5.0
],
"texture": "#ru_khaki"
},
"south": {
"uv": [
0,
0,
4.5,
5.0
],
"texture": "#ru_khaki"
},
"east": {
"uv": [
0,
0,
16,
5.0
],
"texture": "#ru_khaki"
},
"west": {
"uv": [
0,
0,
16,
5.0
],
"texture": "#ru_khaki"
},
"up": {
"uv": [
0,
0,
4.5,
16
],
"texture": "#ru_khaki"
},
"down": {
"uv": [
0,
0,
4.5,
16
],
"texture": "#ru_khaki"
}
}
}
]
}
@@ -0,0 +1,760 @@
{
"ambientocclusion": false,
"textures": {
"ru_khaki": "airdef:block/ru_khaki",
"radar_face": "airdef:block/radar_face",
"dark_metal": "airdef:block/dark_metal",
"tube_cap": "airdef:block/tube_cap",
"particle": "airdef:block/ru_khaki"
},
"elements": [
{
"from": [
-4.0,
0.0,
-6.0
],
"to": [
20.0,
16.0,
22.0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
16
],
"texture": "#ru_khaki"
},
"south": {
"uv": [
0,
0,
16,
16
],
"texture": "#ru_khaki"
},
"east": {
"uv": [
0,
0,
16,
16
],
"texture": "#ru_khaki"
},
"west": {
"uv": [
0,
0,
16,
16
],
"texture": "#ru_khaki"
},
"up": {
"uv": [
0,
0,
16,
16
],
"texture": "#ru_khaki"
},
"down": {
"uv": [
0,
0,
16,
16
],
"texture": "#ru_khaki"
}
}
},
{
"from": [
-2.0,
2.0,
-11.0
],
"to": [
18.0,
15.0,
-6.0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
13.0
],
"texture": "#radar_face"
},
"south": {
"uv": [
0,
0,
16,
13.0
],
"texture": "#radar_face"
},
"east": {
"uv": [
0,
0,
5.0,
13.0
],
"texture": "#radar_face"
},
"west": {
"uv": [
0,
0,
5.0,
13.0
],
"texture": "#radar_face"
},
"up": {
"uv": [
0,
0,
16,
5.0
],
"texture": "#radar_face"
},
"down": {
"uv": [
0,
0,
16,
5.0
],
"texture": "#radar_face"
}
}
},
{
"from": [
-3.0,
16.0,
2.0
],
"to": [
19.0,
17.0,
20.0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
1.0
],
"texture": "#dark_metal"
},
"south": {
"uv": [
0,
0,
16,
1.0
],
"texture": "#dark_metal"
},
"east": {
"uv": [
0,
0,
16,
1.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
16,
1.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
16,
16
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
16,
16
],
"texture": "#dark_metal"
}
}
},
{
"from": [
-1.0,
16.5,
4.0
],
"to": [
3.0,
17.5,
10.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
1.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
4.0,
1.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
6.0,
1.0
],
"texture": "#tube_cap"
},
"west": {
"uv": [
0,
0,
6.0,
1.0
],
"texture": "#tube_cap"
},
"up": {
"uv": [
0,
0,
4.0,
6.0
],
"texture": "#tube_cap"
},
"down": {
"uv": [
0,
0,
4.0,
6.0
],
"texture": "#tube_cap"
}
}
},
{
"from": [
-1.0,
16.5,
12.0
],
"to": [
3.0,
17.5,
18.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
1.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
4.0,
1.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
6.0,
1.0
],
"texture": "#tube_cap"
},
"west": {
"uv": [
0,
0,
6.0,
1.0
],
"texture": "#tube_cap"
},
"up": {
"uv": [
0,
0,
4.0,
6.0
],
"texture": "#tube_cap"
},
"down": {
"uv": [
0,
0,
4.0,
6.0
],
"texture": "#tube_cap"
}
}
},
{
"from": [
4.0,
16.5,
4.0
],
"to": [
8.0,
17.5,
10.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
1.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
4.0,
1.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
6.0,
1.0
],
"texture": "#tube_cap"
},
"west": {
"uv": [
0,
0,
6.0,
1.0
],
"texture": "#tube_cap"
},
"up": {
"uv": [
0,
0,
4.0,
6.0
],
"texture": "#tube_cap"
},
"down": {
"uv": [
0,
0,
4.0,
6.0
],
"texture": "#tube_cap"
}
}
},
{
"from": [
4.0,
16.5,
12.0
],
"to": [
8.0,
17.5,
18.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
1.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
4.0,
1.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
6.0,
1.0
],
"texture": "#tube_cap"
},
"west": {
"uv": [
0,
0,
6.0,
1.0
],
"texture": "#tube_cap"
},
"up": {
"uv": [
0,
0,
4.0,
6.0
],
"texture": "#tube_cap"
},
"down": {
"uv": [
0,
0,
4.0,
6.0
],
"texture": "#tube_cap"
}
}
},
{
"from": [
9.0,
16.5,
4.0
],
"to": [
13.0,
17.5,
10.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
1.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
4.0,
1.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
6.0,
1.0
],
"texture": "#tube_cap"
},
"west": {
"uv": [
0,
0,
6.0,
1.0
],
"texture": "#tube_cap"
},
"up": {
"uv": [
0,
0,
4.0,
6.0
],
"texture": "#tube_cap"
},
"down": {
"uv": [
0,
0,
4.0,
6.0
],
"texture": "#tube_cap"
}
}
},
{
"from": [
9.0,
16.5,
12.0
],
"to": [
13.0,
17.5,
18.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
1.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
4.0,
1.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
6.0,
1.0
],
"texture": "#tube_cap"
},
"west": {
"uv": [
0,
0,
6.0,
1.0
],
"texture": "#tube_cap"
},
"up": {
"uv": [
0,
0,
4.0,
6.0
],
"texture": "#tube_cap"
},
"down": {
"uv": [
0,
0,
4.0,
6.0
],
"texture": "#tube_cap"
}
}
},
{
"from": [
14.0,
16.5,
4.0
],
"to": [
18.0,
17.5,
10.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
1.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
4.0,
1.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
6.0,
1.0
],
"texture": "#tube_cap"
},
"west": {
"uv": [
0,
0,
6.0,
1.0
],
"texture": "#tube_cap"
},
"up": {
"uv": [
0,
0,
4.0,
6.0
],
"texture": "#tube_cap"
},
"down": {
"uv": [
0,
0,
4.0,
6.0
],
"texture": "#tube_cap"
}
}
},
{
"from": [
14.0,
16.5,
12.0
],
"to": [
18.0,
17.5,
18.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
1.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
4.0,
1.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
6.0,
1.0
],
"texture": "#tube_cap"
},
"west": {
"uv": [
0,
0,
6.0,
1.0
],
"texture": "#tube_cap"
},
"up": {
"uv": [
0,
0,
4.0,
6.0
],
"texture": "#tube_cap"
},
"down": {
"uv": [
0,
0,
4.0,
6.0
],
"texture": "#tube_cap"
}
}
}
]
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "airdef:item/deploy_buk"
}
}
@@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "airdef:item/deploy_depot"
}
}
@@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "airdef:item/deploy_ew"
}
}
@@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "airdef:item/deploy_irist"
}
}
@@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "airdef:item/deploy_light"
}
}
@@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "airdef:item/deploy_nasams"
}
}
@@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "airdef:item/deploy_p1sun"
}
}
@@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "airdef:item/deploy_radar"
}
}
@@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "airdef:item/deploy_tor"
}
}
@@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "airdef:item/deploy_tzm"
}
}
@@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "airdef:item/flare_gun"
}
}
@@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "airdef:item/mover"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 342 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 374 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 328 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 404 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 397 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 400 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 400 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 388 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 395 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 386 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 403 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 401 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 400 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 386 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 404 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 407 B

After

Width:  |  Height:  |  Size: 411 B

+7 -3
View File
@@ -115,7 +115,8 @@ fn post(uv: vec2<f32>, pixel: vec2<f32>) -> vec4<f32> {
// the pixel sees, much stronger at night.
let night = clamp(0.35 - frame.time.z * 1.8, 0.0, 1.0);
let strength = mix(0.35, 2.2, night);
if wall < 1.0e5 {
// Under the HD lighting (params[0].w) the engine lights the world from the same lights.
if wall < 1.0e5 && frame.params[0].w < 0.5 {
let p = dir * wall;
var light = vec3<f32>(0.0);
for (var i = 0; i < blasts; i++) {
@@ -196,7 +197,10 @@ fn post(uv: vec2<f32>, pixel: vec2<f32>) -> vec4<f32> {
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);
// 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 dd = length(px - p.xy * frame.resolution.xy) / r;
@@ -210,7 +214,7 @@ fn post(uv: vec2<f32>, pixel: vec2<f32>) -> vec4<f32> {
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; }
if scene_distance(p.xy) < length(a.xyz - frame.camera.xyz) - 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 flicker = 0.85 + 0.15 * noise3(vec3<f32>(a.xyz * 0.7 + vec3<f32>(t_now * 1.3)));
+298 -44
View File
@@ -7,7 +7,7 @@ use horizon_mod_sdk::{
Mod, Registration, chat, command, game,
hud::{self, Corner},
input::{self, Event},
render::{self, ModelInstance, ParticleSpawn},
render::{self, Lighting, ModelInstance, ParticleSpawn, PointLight},
storage,
ui::{self, ButtonWidget, ItemWidget, ListWidget, Screen, Widget},
};
@@ -20,7 +20,7 @@ mod sim;
mod wave;
use sim::{Class, DroneKind, Ground, Out, Sam, SamKind, Sim};
const ITEMS: [&str; 17] = [
const ITEMS: [&str; 29] = [
"airdef:designator",
"airdef:launch_shahed",
"airdef:launch_geran3",
@@ -37,6 +37,18 @@ const ITEMS: [&str; 17] = [
"airdef:deploy_pantsir",
"airdef:deploy_s400",
"airdef:deploy_s500",
"airdef:deploy_irist",
"airdef:deploy_nasams",
"airdef:deploy_buk",
"airdef:deploy_tor",
"airdef:deploy_p1sun",
"airdef:deploy_radar",
"airdef:deploy_depot",
"airdef:deploy_tzm",
"airdef:deploy_ew",
"airdef:deploy_light",
"airdef:mover",
"airdef:flare_gun",
"airdef:remover",
];
@@ -44,6 +56,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 +73,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 +85,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 +226,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 +290,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 +390,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 +408,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 +461,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 +469,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;
}
@@ -355,7 +508,24 @@ impl AirDef {
fn save(&mut self) {
let mut text = String::new();
for s in &self.sim.sams {
text += &format!("sam {} {} {} {} {} {} {}\n", s.kind.key(), s.pos.x, s.pos.y, s.pos.z, s.heading, s.roles, s.health);
// Then its ammunition (missiles, bursts, out and waiting for a truck, reload ticks
// left) and where it was driving, if anywhere.
let dest = s.dest.map_or("- - -".to_string(), |d| format!("{} {} {}", d.x, d.y, d.z));
text += &format!(
"sam {} {} {} {} {} {} {} {} {} {} {} {} {dest}\n",
s.kind.key(),
s.pos.x,
s.pos.y,
s.pos.z,
s.heading,
s.roles,
s.health,
s.cargo,
s.missiles,
s.bursts,
u8::from(s.empty),
s.reloading,
);
}
for (i, t) in self.sim.targets.iter().enumerate() {
let h = self.sim.target_health.get(i).copied().unwrap_or(100.0);
@@ -363,13 +533,21 @@ impl AirDef {
}
text += &format!("damage {}\n", self.sim.damage);
text += &format!("far {}\n", self.far);
text += &format!("effects {} {}\n", self.sim.shader, self.hd);
for (kind, roles) in &self.sim.roles {
text += &format!("roles {} {}\n", kind.key(), roles);
}
if let Err(e) = storage::set(&self.state_key(), text.as_bytes()) {
horizon_mod_sdk::warn(&format!("could not save: {e}"));
// 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) {
@@ -395,7 +573,25 @@ impl AirDef {
if let Some(h) = num(7) {
sam.health = h.clamp(0.0, 1.0);
}
if let Some(c) = num(8) {
sam.cargo = c as u32;
}
// Older saves have no ammunition: full, as before.
if let (Some(m), Some(b)) = (num(9), num(10)) {
sam.missiles = (m as u32).min(sam.missiles);
sam.bursts = (b as u32).min(sam.bursts);
sam.empty = num(11) == Some(1.0);
sam.reloading = num(12).map_or(0, |r| r as u32);
}
let dest = match (num(13), num(14), num(15)) {
(Some(x), Some(y), Some(z)) => Some(DVec3::new(x, y, z)),
_ => None,
};
self.sim.sams.push(sam);
if let Some(to) = dest {
let i = self.sim.sams.len() - 1;
self.sim.order_move(i, to);
}
}
}
Some(&"target") => {
@@ -406,6 +602,11 @@ impl AirDef {
}
Some(&"damage") => self.sim.damage = w.get(1) != Some(&"false"),
Some(&"far") => self.far = w.get(1) != Some(&"false"),
Some(&"effects") => {
let shader = w.get(1) != Some(&"false");
let hd = w.get(2) == Some(&"true");
self.set_effects(shader, hd);
}
Some(&"roles") => {
if let (Some(kind), Some(r)) = (w.get(1).and_then(|k| SamKind::from_key(k)), w.get(2).and_then(|v| v.parse().ok())) {
self.sim.roles.insert(kind, r);
@@ -425,6 +626,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 +662,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 +708,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 +749,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 +766,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 +799,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 +844,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 +910,7 @@ impl AirDef {
}
}
const HELP: &str = "/airdef menu | monitor | grid | replay [map|full|reel|stop] | raid | repair | import | target here | give | deploy <комплекс> | wave <тип|mix> [кол-во] [дальность] | clear | targets clear | damage on|off | shader on|off | hold on|off | roles [комплекс классы] | stats";
const HELP: &str = "/airdef menu | monitor | grid | replay [map|full|reel|stop] | raid | repair | import | ew on|off | flare | move <номер> | target here | give | deploy <комплекс> | wave <тип|mix> [кол-во] [дальность] | clear | targets clear | damage on|off | shader on|off|hd | hold on|off | roles [комплекс классы] | stats";
impl Mod for AirDef {
fn init() -> Result<(Self, Registration), String> {
@@ -693,6 +919,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 +928,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 +940,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 +982,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 +999,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 +1011,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 +1061,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 +1088,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 +1186,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 +1245,8 @@ impl Mod for AirDef {
}
["deploy", kind] => {
// A system where the player stands, a few blocks ahead.
let kind = SamKind::from_key(kind).ok_or("Комплексы: patriot, gepard, pantsir, s400, s500")?;
let kind = SamKind::from_key(kind)
.ok_or("Техника: patriot, gepard, pantsir, s400, s500, irist, nasams, buk, tor, p1sun, radar, depot, tzm, ew, light")?;
let p = player().ok_or("Нет игрока")?;
let d = view(&p);
let flat = DVec3::new(d.x, 0.0, d.z).normalize_or(DVec3::NEG_Z);
@@ -1050,6 +1290,25 @@ impl Mod for AirDef {
self.launch_wave(&mut out)?;
None
}
["move", n] => {
// Unit n (as the summary numbers them) drives to where the player stands.
let p = player().ok_or("Нет игрока")?;
let i = n.parse::<usize>().ok().and_then(|n| n.checked_sub(1)).ok_or("/airdef move <номер>")?;
if self.sim.order_move(i, DVec3::new(p.x, p.y, p.z)) {
Some(format!("{} №{}: на марш", self.sim.sams[i].kind.name(), i + 1))
} else {
Some("Эта техника не может ехать".into())
}
}
["ew", v @ ("on" | "off")] => {
let (n, hot) = self.sim.set_ew(*v == "on");
Some(format!("РЭБ: включено станций {n}{}", if hot > 0 { format!(", остывают {hot}") } else { String::new() }))
}
["flare"] => {
let p = player().ok_or("Нет игрока")?;
self.sim.fire_flare(eye(&p) + view(&p), view(&p), &mut out);
None
}
["import"] => {
// The state the mod kept before worlds had their own: brought into this world.
let before = self.sim.sams.len();
@@ -1085,7 +1344,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 +1353,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 +1379,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!(
"В воздухе: {}, ракет: {}, ПВО: {}, сбито: {}, поражений: {}",
+28 -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;
+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
}
+68 -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));
@@ -718,6 +711,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 }
+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;
}
}
}
+149
View File
@@ -0,0 +1,149 @@
//! Night fighting: guns aim by sight, and at night they hit far less unless
//! the target is lit. Searchlights follow the network's tracks with their
//! beams; illumination flares (fired by the guns on their own, or by the
//! player) hang over the sky for half a minute and light everything round.
use super::*;
/// A flare's life, how far it lights, and when it stops climbing.
const FLARE_LIFE: u32 = 600;
const FLARE_LIGHT: f64 = 90.0;
const FLARE_CLIMB: u32 = 28;
/// How far a searchlight's beam reaches.
pub const BEAM: f64 = 450.0;
/// Guns' hits at night on an unlit target.
pub const DARK_HITS: f64 = 0.4;
/// Ticks between a gun's own flares.
const FLARE_EVERY: u32 = 500;
pub struct Flare {
id: u32,
pub pos: DVec3,
vel: DVec3,
age: u32,
}
impl Sim {
/// Fires an illumination flare from `from` towards `dir`: it climbs, bursts and hangs.
pub fn fire_flare(&mut self, from: DVec3, dir: DVec3, out: &mut Out) {
let id = self.id();
let vel = dir.normalize_or(DVec3::Y) * 2.4 + DVec3::Y * 1.4;
self.flares.push(Flare { id, pos: from, vel, age: 0 });
out.sound("minecraft:entity.firework_rocket.launch", from, 2.0, 0.6);
if self.flares.len() > 12 {
self.flares.remove(0);
}
}
/// The flares and the searchlights, and what they light; the guns' own flares.
pub(super) fn step_lights(&mut self, out: &mut Out) {
self.lit.clear();
let now = self.tick;
// Flares: up, a burst, then down slowly, swaying, lighting all round.
let mut keep = Vec::with_capacity(self.flares.len());
for mut f in std::mem::take(&mut self.flares) {
f.age += 1;
if f.age < FLARE_CLIMB {
f.pos += f.vel;
f.vel = f.vel * 0.94 - DVec3::Y * 0.04;
} else {
if f.age == FLARE_CLIMB {
out.sound("minecraft:entity.firework_rocket.blast", f.pos, 3.0, 0.5);
out.particles("minecraft:firework", f.pos, 1.0, DVec3::ZERO, 0.15, 30);
}
let sway = (f.age as f64 * 0.05 + f.id as f64).sin() * 0.04;
f.pos += DVec3::new(sway, -0.06, sway * 0.5);
for d in self.drones.iter().filter(|d| d.alive() && d.pos.distance(f.pos) < FLARE_LIGHT) {
self.lit.push(d.id);
}
if now % 3 == 0 {
out.particles("minecraft:firework", f.pos, 0.2, DVec3::new(0.0, -0.05, 0.0), 0.02, 1);
}
}
let fade = if f.age > FLARE_LIFE - 60 { (FLARE_LIFE - f.age) as f64 / 60.0 } else { 1.0 };
out.model(f.id, "airdef:flare", f.pos, DQuat::IDENTITY, 1.4 * fade.max(0.05), true);
if f.age < FLARE_LIFE {
keep.push(f);
}
}
self.flares = keep;
// What the network tracks, for the searchlights.
let tracked: Vec<u32> = self.sams.iter().flat_map(|s| s.seen.iter().copied()).collect();
let gun_targets: Vec<u32> = self.sams.iter().filter_map(|s| s.gun_target).collect();
for i in 0..self.sams.len() {
if self.sams[i].kind != SamKind::Light || !self.sams[i].alive() || self.sams[i].moving() {
continue;
}
let (pos, id) = (self.sams[i].pos, self.sams[i].id);
if !self.night {
let sam = &mut self.sams[i];
sam.target = None;
sam.pitch += (0.0 - sam.pitch) * 0.05;
continue;
}
// The guns' target first, else the nearest tracked drone in reach.
let target = self
.drones
.iter()
.filter(|d| d.alive() && tracked.contains(&d.id) && d.pos.distance(pos) < BEAM)
.min_by(|a, b| {
let key = |d: &Drone| d.pos.distance(pos) * if gun_targets.contains(&d.id) { 0.3 } else { 1.0 };
key(a).total_cmp(&key(b))
})
.map(|d| (d.id, d.pos));
let sam = &mut self.sams[i];
let (head, _) = sam.mount();
let (want_yaw, want_pitch, reach) = match target {
Some((_, at)) => {
let to = at - head;
(heading_of(to), (to.y / to.length().max(0.01)).asin(), to.length())
}
// No target: a slow sweep of the sky.
None => (now as f64 * 0.01 + id as f64, 0.9 + (now as f64 * 0.013).sin() * 0.3, 260.0),
};
sam.yaw = slew(sam.yaw, want_yaw, 0.12);
sam.pitch = slew(sam.pitch, want_pitch.clamp(0.05, 1.5), 0.08);
sam.target = target.map(|t| t.0);
// The beam: segments widening with distance, out to the target.
let (head, rot) = self.sams[i].mount();
let dir = rot * DVec3::NEG_Z;
// The target is lit once the beam is on it, not while it still swings round.
if let Some((t, at)) = target
&& dir.angle_between(at - head) < 0.08
{
self.lit.push(t);
}
let mut along = 1.0;
// Up to 14 segments (the unit's part ids 2..15), each longer than the last: one
// reaches about 550 blocks, past the beam's reach, about 9 blocks wide out there.
for k in 0..14u32 {
if along >= reach {
break;
}
let size = 0.8 + along * 0.12;
let len = 3.0 * size;
out.model(id + 2 + k, "airdef:beam", head + dir * (along + len / 2.0), rot, size, true);
along += len;
}
}
// Guns at night light their own target with a flare now and then.
if self.night {
let mut fire: Vec<(DVec3, DVec3)> = Vec::new();
for sam in &mut self.sams {
sam.flare_cd = sam.flare_cd.saturating_sub(1);
if sam.kind.spec().gun.is_none() || !sam.alive() || sam.flare_cd > 0 {
continue;
}
let Some(t) = sam.gun_target.and_then(|t| self.drones.iter().find(|d| d.id == t)) else { continue };
if self.lit.contains(&t.id) || self.flares.iter().any(|f| f.pos.distance(t.pos) < FLARE_LIGHT) {
continue;
}
sam.flare_cd = FLARE_EVERY;
let from = sam.pos + DVec3::Y * 2.0;
fire.push((from, (t.pos + DVec3::Y * 25.0) - from));
}
for (from, dir) in fire {
self.fire_flare(from, dir, out);
}
}
}
}
+122
View File
@@ -0,0 +1,122 @@
//! Logistics: once a depot stands, magazines are no longer refilled from
//! nowhere. A system out of missiles waits; one within reach of a depot
//! reloads from its stock; the reload trucks fill up at a depot and drive
//! reloads to the systems that wait, nearest first, and back.
use super::*;
/// How near a depot a system reloads straight from it.
const DEPOT_REACH: f64 = 60.0;
/// How near a truck must come to hand a reload over, or to load up at a depot.
const HANDOVER: f64 = 10.0;
/// What a truck carries.
const TRUCK_LOAD: u32 = 2;
impl Sim {
/// Whether ammunition is counted: a depot was set up (without one, magazines refill as
/// before). A destroyed depot still counts: losing the last one leaves the systems short,
/// not supplied from nowhere again.
pub fn logistics(&self) -> bool {
self.sams.iter().any(|s| s.kind == SamKind::Depot)
}
/// Reloads from depots near the systems, and the trucks' rounds.
pub(super) fn step_supply(&mut self, out: &mut Out) {
if !self.logistics() {
for s in &mut self.sams {
s.empty = false;
}
return;
}
let now = self.tick;
// Systems by a depot reload from it.
for i in 0..self.sams.len() {
if !self.sams[i].empty || !self.sams[i].alive() {
continue;
}
let pos = self.sams[i].pos;
if let Some(d) = self
.sams
.iter()
.position(|d| d.kind == SamKind::Depot && d.alive() && d.cargo > 0 && d.pos.distance(pos) < DEPOT_REACH)
{
self.sams[d].cargo -= 1;
self.reload_now(i, out);
}
}
// A destroyed truck drops its job, for another to take.
for s in self.sams.iter_mut().filter(|s| s.kind == SamKind::Tzm && !s.alive()) {
s.job = None;
}
// The trucks: a job each, when idle.
for t in 0..self.sams.len() {
let truck = &self.sams[t];
if truck.kind != SamKind::Tzm || !truck.alive() || truck.moving() || now % 10 != t as u64 % 10 {
continue;
}
let at = truck.pos;
// Where it was going: hand over, or load up.
if let Some(job) = truck.job
&& let Some(j) = self.sams.iter().position(|s| s.id == job)
{
if self.sams[j].pos.distance(at) <= HANDOVER {
if self.sams[j].kind == SamKind::Depot {
let take = TRUCK_LOAD.saturating_sub(self.sams[t].cargo).min(self.sams[j].cargo);
self.sams[j].cargo -= take;
self.sams[t].cargo += take;
out.sound("minecraft:block.piston.extend", at, 1.5, 0.8);
} else if self.sams[j].empty && self.sams[t].cargo > 0 {
self.sams[t].cargo -= 1;
self.reload_now(j, out);
}
self.sams[t].job = None;
} else if self.sams[j].alive() && (self.sams[j].empty || self.sams[j].kind == SamKind::Depot) {
// Stopped short of it (it drove off meanwhile): on to where it stands now.
let to = self.sams[j].pos;
let side = horizontal(at - to).normalize_or(DVec3::X) * (HANDOVER * 0.6);
self.order_move(t, to + side);
continue;
} else {
self.sams[t].job = None;
}
}
// A new job: the nearest waiting system no other truck is driving to, or a depot to load at.
let taken: Vec<u32> = self.sams.iter().filter(|s| s.kind == SamKind::Tzm && s.alive()).filter_map(|s| s.job).collect();
let next = if self.sams[t].cargo > 0 {
self.sams
.iter()
.filter(|s| s.empty && s.alive() && !s.moving() && !taken.contains(&s.id))
.min_by(|a, b| a.pos.distance(at).total_cmp(&b.pos.distance(at)))
.map(|s| (s.id, s.pos))
} else {
None
};
let next = next.or_else(|| {
(self.sams[t].cargo < TRUCK_LOAD)
.then(|| {
self.sams
.iter()
.filter(|d| d.kind == SamKind::Depot && d.alive() && d.cargo > 0)
.min_by(|a, b| a.pos.distance(at).total_cmp(&b.pos.distance(at)))
.map(|d| (d.id, d.pos))
})
.flatten()
});
if let Some((id, to)) = next {
self.sams[t].job = Some(id);
// Stop beside it, not on it.
let side = horizontal(at - to).normalize_or(DVec3::X) * (HANDOVER * 0.6);
if to.distance(at) > HANDOVER {
self.order_move(t, to + side);
}
}
}
}
/// System `i` gets a reload: its magazine fills after its reload time.
fn reload_now(&mut self, i: usize, out: &mut Out) {
let sam = &mut self.sams[i];
sam.empty = false;
sam.reloading = sam.kind.spec().reload.max(1);
out.sound("minecraft:block.piston.extend", sam.pos, 2.0, 0.6);
}
}
+434 -313
View File
File diff suppressed because it is too large Load Diff
+117
View File
@@ -0,0 +1,117 @@
//! Move orders: a unit packs up (its launcher down, radar silent), drives
//! over the land to the point it was given, and deploys there; it fights
//! again only once deployed. The reload trucks drive the same way.
use super::*;
/// Blocks a tick a vehicle drives (about 25 km/h).
const SPEED: f64 = 0.35;
impl SamKind {
/// Ticks to pack up for the march, and to deploy again.
pub fn march_ticks(self) -> u32 {
match self.spec().family {
Family::Vertical => 200,
Family::Trainable => 120,
Family::Static => 160,
Family::Turret | Family::Gun => 40,
Family::Light => 40,
Family::Truck => 10,
}
}
/// A depot doesn't drive.
pub fn mobile(self) -> bool {
self != SamKind::Depot
}
}
impl Sam {
/// Packing up, driving or deploying: not fighting.
pub fn moving(&self) -> bool {
matches!(self.state, SamState::Packing | SamState::Moving | SamState::Deploying)
}
}
impl Sim {
/// Orders unit `i` to drive to `to`. False for one that can't (a depot, a wreck).
pub fn order_move(&mut self, i: usize, to: DVec3) -> bool {
let Some(sam) = self.sams.get_mut(i) else { return false };
if !sam.alive() || !sam.kind.mobile() {
return false;
}
sam.dest = Some(to);
sam.target = None;
sam.gun_target = None;
if sam.state != SamState::Moving {
sam.state = SamState::Packing;
sam.march = sam.kind.march_ticks();
}
true
}
/// Packing, driving and deploying, once a tick.
pub(super) fn step_moves(&mut self, ground: &mut Ground, out: &mut Out) {
let now = self.tick;
for sam in &mut self.sams {
if !sam.alive() {
continue;
}
let spec = sam.kind.spec();
// The march and the packing drive the mount themselves: no speed carries over into or out of them.
if matches!(sam.state, SamState::Packing | SamState::Moving | SamState::Deploying) {
sam.halt_mount();
}
match sam.state {
SamState::Packing => {
// The launcher comes down for the march.
sam.pitch += (0.0 - sam.pitch) * 0.06;
sam.yaw = slew(sam.yaw, sam.heading, 0.04);
sam.march = sam.march.saturating_sub(1);
if sam.march == 0 {
sam.state = SamState::Moving;
}
}
SamState::Moving => {
let Some(to) = sam.dest else {
sam.state = SamState::Deploying;
sam.march = sam.kind.march_ticks();
continue;
};
let flat = horizontal(to - sam.pos);
if flat.length() < SPEED * 2.0 {
sam.pos = DVec3::new(to.x, sam.pos.y, to.z);
sam.dest = None;
sam.state = SamState::Deploying;
sam.march = sam.kind.march_ticks();
out.sound("minecraft:block.piston.contract", sam.pos, 2.0, 0.6);
continue;
}
// Turn towards it, then drive: slower while turning, over the land's height.
sam.heading = slew(sam.heading, heading_of(flat), 0.06);
let ahead = DQuat::from_rotation_y(sam.heading) * DVec3::NEG_Z;
let turning = angle_diff(sam.heading, heading_of(flat)).abs();
let step = SPEED * (1.0 - turning / std::f64::consts::PI).max(0.2);
let next = sam.pos + ahead * step;
let y = ground.at(next, now).unwrap_or(sam.pos.y);
sam.pos = DVec3::new(next.x, sam.pos.y + (y - sam.pos.y).clamp(-0.5, 0.5), next.z);
sam.yaw = sam.heading;
if now % 4 == 0 {
out.particles("minecraft:campfire_cosy_smoke", sam.pos - ahead * 2.0 * spec.scale, 0.3, DVec3::new(0.0, 0.03, 0.0), 0.01, 1);
}
if now % 30 == 0 {
out.sound("minecraft:entity.minecart.riding", sam.pos, 0.6, 0.5);
}
}
SamState::Deploying => {
sam.pitch += (spec.rest_pitch - sam.pitch) * 0.05;
sam.march = sam.march.saturating_sub(1);
if sam.march == 0 {
sam.state = SamState::Search;
sam.pitch = spec.rest_pitch;
sam.yaw = sam.heading;
}
}
_ => {}
}
}
}
}
+89 -21
View File
@@ -98,20 +98,20 @@ pub(super) struct WaveStats {
}
/// The planner's grid: cells this big, at most this many a side.
const CELL: f64 = 16.0;
const CELLS: i32 = 140;
pub(super) const CELL: f64 = 16.0;
pub(super) const CELLS: i32 = 140;
/// The planner's work a tick (cells priced and opened): a raid takes a few ticks to plan, never a long one.
const WORK_PER_TICK: usize = 40_000;
pub(super) const WORK_PER_TICK: usize = 40_000;
/// The planner's grid over a raid: its corner, its cells' size and how many a side.
struct Grid {
min: DVec3,
cell: f64,
size: i32,
pub(super) struct Grid {
pub(super) min: DVec3,
pub(super) cell: f64,
pub(super) size: i32,
}
impl Grid {
fn index(&self, p: DVec3) -> (i32, i32) {
pub(super) fn index(&self, p: DVec3) -> (i32, i32) {
let n = self.size;
((((p.x - self.min.x) / self.cell) as i32).clamp(0, n - 1), (((p.z - self.min.z) / self.cell) as i32).clamp(0, n - 1))
}
@@ -121,7 +121,7 @@ impl Grid {
}
/// The cheapest way over `grid` from `from` to `to` by its prices (A*): points along it, `to` last.
fn plan_route(grid: &Grid, prices: &[f32], from: DVec3, to: DVec3) -> (Vec<DVec3>, usize) {
pub(super) fn plan_route(grid: &Grid, prices: &[f32], from: DVec3, to: DVec3) -> (Vec<DVec3>, usize) {
let mut opened = 0;
let n = grid.size;
let at = |(i, j): (i32, i32)| (i * n + j) as usize;
@@ -195,13 +195,19 @@ impl Sim {
/// (each route found in full, a few a tick) and launched when its plan is ready.
pub fn order_raid(&mut self, order: &RaidOrder, objects: &[DVec3], out: &mut Out) -> u32 {
let Some(&centre) = objects.first() else { return 0 };
// One raid is planned at a time: another order waits for this one's drones to be sent.
if self.plan.is_some() {
out.messages.push("Противник ещё готовит прошлый налёт: подожди пару секунд".into());
return 0;
}
let total: u32 = order.kinds.iter().map(|k| k.1).sum();
if total == 0 {
return 0;
}
let bearing = self.rng.f() * TAU;
let second = bearing + std::f64::consts::PI * (0.6 + self.rng.f() * 0.6);
let smart = order.aim == Aim::Smart;
// A smart enemy comes from where its raids got through best.
let bearing = if smart { self.best_bearing() } else { self.rng.f() * TAU };
let second = bearing + std::f64::consts::PI * (0.6 + self.rng.f() * 0.6);
// One grid over the whole raid; each kind's prices are worked out once, routes are shared
// by the drones of a kind going from the same bearing to the same target.
let span = order.dist + 450.0;
@@ -222,7 +228,8 @@ impl Sim {
let dist = spec.ballistic.map_or(order.dist, |far| far.max(order.dist)) + self.rng.f() * 50.0;
let height = if spec.ballistic.is_some() { 10.0 } else if spec.cruise > 0.0 { spec.cruise } else { 25.0 };
let from = centre + DVec3::new(a.sin() * dist, height + self.rng.s() * 6.0, a.cos() * dist);
let d = self.make_drone(kind, from, horizontal(aim - from), aim);
let mut d = self.make_drone(kind, from, horizontal(aim - from), aim);
d.sector = ew::sector(a) as u8;
let routed = smart && spec.ballistic.is_none() && !spec.decoy && !at_sam.is_some_and(|_| kind == DroneKind::Lancet);
let key = routed.then(|| (kind, (a.rem_euclid(TAU) / 0.35) as i32, (target.x / 24.0) as i32, (target.z / 24.0) as i32));
// Decoys come 15 s ahead, the rest together; "waves" one kind after another.
@@ -296,7 +303,8 @@ impl Sim {
// Timing: smart raids arrive at once; others leave at once (a moment apart).
let now = self.tick;
let arrive = plan.done.iter().map(|(_, t, e, g)| t + e - g).fold(0.0, f64::max);
for (d, time, early, gap) in plan.done {
for (mut d, time, early, gap) in plan.done {
d.smart = plan.smart;
let wait = if plan.smart { arrive - (early - gap) - time } else { gap + self.rng.f() * 40.0 };
self.queue.push((now + wait.clamp(0.0, 2400.0) as u64, d));
}
@@ -344,6 +352,9 @@ impl Sim {
engaged: 0,
route: Vec::new(),
low: false,
jammed: false,
sector: 0,
smart: false,
}
}
@@ -365,9 +376,19 @@ impl Sim {
if aim == Aim::ObjectsAndSams {
return if roll < 0.33 { pick(&alive, r).map_or((object, None), |(p, id)| (p, Some(id))) } else { (object, None) };
}
// Smart: each kind at what it is good against.
let guns: Vec<&Sam> = alive.iter().copied().filter(|s| matches!(s.kind, SamKind::Gepard | SamKind::Pantsir)).collect();
let long: Vec<&Sam> = alive.iter().copied().filter(|s| matches!(s.kind, SamKind::S400 | SamKind::S500 | SamKind::Patriot)).collect();
// Smart: each kind at what it is good against; what gave itself away first (a jamming
// station, a radar, a system that fired).
let known = self.known_units();
let known_ew: Vec<&Sam> = alive.iter().copied().filter(|s| s.kind == SamKind::Ew && known.iter().any(|k| k.0 == s.id)).collect();
if matches!(kind, DroneKind::Lancet | DroneKind::Kh22) && !known_ew.is_empty() && roll < 0.6 {
let s = known_ew[(r * known_ew.len() as f64) as usize % known_ew.len()];
return (s.pos, Some(s.id));
}
// Short-range systems (guns, Tor, P1-SUN) for the Lancets; long-range ones and the radars
// that feed them for the fast missiles.
let guns: Vec<&Sam> = alive.iter().copied().filter(|s| s.kind.armed() && s.kind.range() <= 200.0).collect();
let long: Vec<&Sam> =
alive.iter().copied().filter(|s| s.kind.range() >= 300.0 || s.kind == SamKind::Radar).collect();
let choice = match kind {
DroneKind::Lancet if roll < 0.75 => pick(&guns, r).or_else(|| pick(&alive, r)),
DroneKind::Kh22 | DroneKind::Zircon if roll < 0.45 => pick(&long, r),
@@ -380,18 +401,41 @@ impl Sim {
/// The price of each cell of `grid` for this kind flying low: 1, and much more where a
/// system sees it there (its radar's reach for it, the land) and can shoot it.
fn price_grid(&self, grid: &Grid, kind: DroneKind) -> (Vec<f32>, usize) {
pub(super) fn price_grid(&self, grid: &Grid, kind: DroneKind) -> (Vec<f32>, usize) {
let mut cost = 0;
let spec = kind.spec();
let height = spec.clearance.max(6.0);
let n = grid.size;
let mut prices = vec![1.0f32; (n * n) as usize];
// Known jamming stations: drones that navigate by satellite keep away.
if kind.jammable() {
let known = self.known_units();
for s in self.sams.iter().filter(|s| s.kind == SamKind::Ew && s.alive() && known.iter().any(|k| k.0 == s.id)) {
let (ci, cj) = grid.index(s.pos);
let r = (EW_RADIUS / grid.cell).ceil() as i32;
for i in (ci - r).max(0)..=(ci + r).min(n - 1) {
for j in (cj - r).max(0)..=(cj + r).min(n - 1) {
if horizontal(grid.centre((i, j), s.pos.y) - s.pos).length() < EW_RADIUS {
prices[(i * n + j) as usize] += 12.0;
}
}
}
}
}
for s in self.sams.iter().filter(|s| s.alive() && s.roles & spec.class.bit() != 0) {
let sp = s.kind.spec();
let (lo, hi) = sp.missile.as_ref().map_or((0.0, 0.0), |m| (m.min, m.max));
let gun = sp.gun.as_ref().map_or(0.0, |g| g.range);
let radar = sight::reach(s, kind, height);
let far = hi.max(gun).min(radar);
// A gun needs its own radar; a missile also flies on the tracks of any radar within data-link range.
let own = sight::reach(s, kind, height);
let net = self
.sams
.iter()
.filter(|r| r.alive() && r.id != s.id && r.pos.distance(s.pos) <= sight::LINK)
.map(|r| sight::reach(r, kind, height))
.fold(0.0, f64::max);
let (hi, gun) = (hi.min(own.max(net)), gun.min(own));
let far = hi.max(gun);
let at = sight::antenna(s);
let horizon = self.horizons.get(&s.id);
// Only the cells round it.
@@ -442,7 +486,8 @@ impl Sim {
let kills = self.kills - w.kills;
let through = self.hits - w.hits;
let launched = self.launched - w.launched;
let fired: u32 = self.sams.iter().map(|s| s.fired).sum::<u32>() - w.fired;
// A system taken away during the raid takes its count along: never below none.
let fired: u32 = self.sams.iter().map(|s| s.fired).sum::<u32>().saturating_sub(w.fired);
let lost = w.sams.saturating_sub(self.sams.iter().filter(|s| s.alive()).count());
let objects = if self.target_health.is_empty() {
String::new()
@@ -508,6 +553,29 @@ mod tests {
eprintln!("4000 ticks in {:?}, kills {} hits {}", t.elapsed(), sim.kills, sim.hits);
assert!(sim.queue.is_empty() && !sim.drones.iter().any(|d| d.alive()), "the raid never ended");
}
#[test]
fn systems_ready_together_send_one_missile_at_a_drone() {
let mut sim = Sim::new(3);
// Vertical launchers of one kind side by side: the drone comes into their reach in the
// same tick, and they fire without turning.
for i in 0..6 {
let id = sim.id();
sim.sams.push(Sam::new(id, SamKind::Tor, DVec3::new(2.0 * i as f64, 70.0, 0.0), 0.0));
}
let mut ground = Ground::default();
let mut out = Out::default();
for _ in 0..30 {
sim.step(&mut ground, None, &mut out);
}
sim.add_drone(DroneKind::Shahed, DVec3::new(0.0, 120.0, -400.0), DVec3::Z, DVec3::new(0.0, 70.0, 0.0));
let id = sim.drones[0].id;
for _ in 0..1200 {
sim.step(&mut ground, None, &mut out);
let on_it = sim.missiles.iter().filter(|m| m.target == id).count();
assert!(on_it <= 1, "{on_it} missiles on one Shahed");
}
assert!(sim.sams.iter().any(|s| s.fired > 0), "nothing was fired");
}
}
#[cfg(test)]
@@ -518,7 +586,7 @@ mod big {
fn sixty_one_systems_and_a_big_smart_raid() {
let mut sim = Sim::new(11);
for i in 0..61 {
let kind = SamKind::ALL[i % 5];
let kind = SamKind::ARMED[i % 5];
let id = sim.id();
let p = DVec3::new(((i % 8) as f64 - 4.0) * 40.0, 70.0, ((i / 8) as f64 - 4.0) * 40.0);
sim.sams.push(Sam::new(id, kind, p, 0.0));
+8 -1
View File
@@ -142,6 +142,13 @@ impl Recording {
}
}
impl Sim {
/// The recording of raid `number`, while it is kept.
pub fn replay(&self, number: u32) -> Option<&Recording> {
self.replays.iter().find(|r| r.number == number)
}
}
impl Sim {
/// Something worth a replay happened.
pub(super) fn log(&mut self, pos: DVec3, kind: EventKind) {
@@ -237,7 +244,7 @@ impl Sim {
let spec = kind.spec();
let rot = look(dir, 0.0) * DQuat::from_rotation_z(t * 0.3);
out.model(id, spec.model, pos, rot, spec.scale, false);
if t - tr.first as f64 > spec.cold as f64 {
if t - tr.first as f64 > spec.cold as f64 && spec.flame > 0.0 {
let back = (spec.rear - 8.0) / 16.0 * spec.scale + spec.flame;
out.model(id + 1, "airdef:flame", pos + rot * DVec3::new(0.0, 0.0, back), rot, spec.flame, true);
}
+224
View File
@@ -0,0 +1,224 @@
//! Regression tests for the issues filed on Gitea (each names its issue).
use super::*;
use super::raid::Grid;
// #1: [bug] Запросы рельефа голодают: часть направлений РЛС никогда не получает высоты
#[test]
fn terrain_queries_must_eventually_cover_all_radar_bearings() {
let mut sim=Sim::new(1);
for i in 0..10 {
let id=sim.id();sim.sams.push(Sam::new(id,SamKind::Radar,DVec3::new(i as f64*5000.,70.,0.),0.));
}
let mut ground=Ground::default();
for now in 1..=2500 {
sim.tick=now;sim.update_sight(&mut ground);
let answers=ground.take_wanted().into_iter().map(|(x,z)|(x,z,200)).collect();
ground.answer(answers,now);
}
let visible:Vec<usize>=sim.horizons.values().map(|h|h.rays.iter().filter(|r|r.last().is_some_and(|v|v.is_finite())).count()).collect();
eprintln!("radar terrain coverage after 125 seconds: {:?} / 48 bearings each",visible);
assert!(visible.iter().all(|n|*n==48),"refreshes of early rays starve unknown terrain in later rays");
}
// #2: [bug] Уничтоженная ТЗМ навсегда резервирует заявку на подвоз боеприпасов
#[test]
fn dead_truck_must_not_reserve_a_waiting_system() {
let mut sim=Sim::new(1);
let depot=sim.id(); sim.sams.push(Sam::new(depot,SamKind::Depot,DVec3::new(500.,70.,0.),0.));
let target=sim.id(); let mut s=Sam::new(target,SamKind::Tor,DVec3::new(0.,70.,0.),0.);s.empty=true;s.missiles=0;sim.sams.push(s);
let dead=sim.id();let mut s=Sam::new(dead,SamKind::Tzm,DVec3::new(100.,70.,0.),0.);s.job=Some(target);s.health=0.;sim.sams.push(s);
let live=sim.id();sim.sams.push(Sam::new(live,SamKind::Tzm,DVec3::new(30.,70.,0.),0.));
sim.tick=3;sim.step_supply(&mut Out::default());
assert_eq!(sim.sams[3].job,Some(target),"a live loaded truck must take the waiting system");
}
// #3: [bug] ТЗМ застревает, если обслуживаемая установка сменила позицию
#[test]
fn truck_must_follow_a_target_that_moved_away() {
let mut sim=Sim::new(1);
let depot=sim.id();sim.sams.push(Sam::new(depot,SamKind::Depot,DVec3::new(500.,70.,0.),0.));
let target=sim.id();let mut s=Sam::new(target,SamKind::Tor,DVec3::new(100.,70.,0.),0.);s.empty=true;s.missiles=0;sim.sams.push(s);
let truck=sim.id();let mut s=Sam::new(truck,SamKind::Tzm,DVec3::new(0.,70.,0.),0.);s.job=Some(target);sim.sams.push(s);
sim.tick=2;sim.step_supply(&mut Out::default());
assert!(sim.sams[2].moving(),"truck at obsolete rendezvous must get a new move order");
}
// #4: [bug] Планировщик умного налёта не учитывает пусковую с внешним целеуказанием
#[test]
fn network_launcher_must_contribute_to_route_risk() {
let mut sim=Sim::new(1);
let id=sim.id();sim.sams.push(Sam::new(id,SamKind::P1sun,DVec3::new(0.,70.,0.),0.));
let id=sim.id();sim.sams.push(Sam::new(id,SamKind::Radar,DVec3::new(0.,70.,0.),0.));
let grid=Grid{min:DVec3::new(-160.,0.,-160.),cell:16.,size:20};
let (prices,_)=sim.price_grid(&grid,DroneKind::Shahed);
assert!(prices.iter().any(|p|*p>1.),"armed launcher with a network radar is invisible to route risk");
}
// #5: [bug] После ремонта комплекс снова уничтожается от собственной отложенной детонации
#[test]
fn repair_must_cancel_pending_magazine_explosions() {
let mut sim=Sim::new(1);
let id=sim.id();sim.sams.push(Sam::new(id,SamKind::Patriot,DVec3::new(0.,70.,0.),0.));
sim.blast_damage(DVec3::new(0.,70.,0.),4,&mut Out::default());
assert!(!sim.later.is_empty());sim.repair();
sim.tick=3; sim.step_later(&mut Ground::default(),None,&mut Out::default());
assert!(sim.sams[0].alive(),"a fully repaired launcher was destroyed again by its own pre-repair cook-off");
}
// #34: drones and ballistic missiles burst where their target's platform used to be.
#[test]
fn munitions_must_go_down_to_the_land_left_under_a_destroyed_target() {
for (kind, from, vel) in [
(DroneKind::Shahed, DVec3::new(0.0, 121.0, 0.0), DVec3::ZERO),
(DroneKind::OreshnikSub, DVec3::new(0.0, 124.0, 0.0), DVec3::ZERO),
(DroneKind::Iskander, DVec3::new(100.0, 121.0, 0.0), DVec3::new(0.0, -4.0, 0.0)),
] {
let mut sim = Sim::new(1);
sim.damage = false;
let mut ground = Ground::default();
// The land under the old platform (at y 120) is at 64 now.
for x in -120..=120 {
for z in -8..=8 {
ground.answer(vec![(x, z, 64)], 0);
}
}
sim.add_drone(kind, from, DVec3::NEG_Y, DVec3::new(0.0, 120.0, 0.0));
if vel != DVec3::ZERO {
sim.drones[0].vel = vel;
sim.drones[0].flight = Flight::Ballistic;
}
let mut out = Out::default();
for _ in 0..400 {
if sim.drones.is_empty() {
break;
}
sim.step(&mut ground, None, &mut out);
}
let blast = sim.blasts.first().map(|b| b.at);
assert!(blast.is_some_and(|b| b.y < 80.0), "{kind:?} burst at {blast:?}, in the air over the land");
}
}
// #15: lights of blasts in one tick at the same whole X must stay apart
#[test]
fn blasts_in_one_tick_have_their_own_ids() {
let mut sim = Sim::new(1);
sim.tick = 100;
sim.fireball(DVec3::new(0.0, 70.0, 100.0), 4.0, 10);
sim.fireball(DVec3::new(0.0, 70.0, 200.0), 4.0, 10);
assert_ne!(sim.blasts[0].id, sim.blasts[1].id);
// A blast going away does not hand its id to the next one.
let gone = sim.blasts.remove(0).id;
sim.fireball(DVec3::new(0.0, 70.0, 300.0), 4.0, 10);
assert!(sim.blasts.iter().all(|b| b.id != gone));
}
// #21: a smart drone sent at a new target gets a route again, not a straight line
#[test]
fn retargeted_smart_drone_gets_a_route_round_the_defences() {
let mut sim = Sim::new(1);
// A system in the way, between the drone and the unit it is sent at.
let id = sim.id();
sim.sams.push(Sam::new(id, SamKind::Patriot, DVec3::new(0.0, 70.0, 0.0), 0.0));
let id = sim.id();
sim.sams.push(Sam::new(id, SamKind::Radar, DVec3::new(0.0, 70.0, 0.0), 0.0));
let goal = sim.id();
sim.sams.push(Sam::new(goal, SamKind::Ew, DVec3::new(0.0, 70.0, 1000.0), 0.0));
sim.intel.revealed.push((goal, DVec3::new(0.0, 70.0, 1000.0), 0));
// Its first target, a system that is destroyed by now.
let id = sim.id();
let mut wreck = Sam::new(id, SamKind::Patriot, DVec3::new(3000.0, 70.0, -1000.0), 0.0);
wreck.health = 0.0;
sim.sams.push(wreck);
sim.add_drone(DroneKind::Shahed, DVec3::new(0.0, 100.0, -1000.0), DVec3::Z, DVec3::new(3000.0, 70.0, -1000.0));
sim.drones[0].pos = DVec3::new(0.0, 100.0, -1000.0);
sim.drones[0].smart = true;
sim.tick = 40;
sim.adapt();
let d = &sim.drones[0];
assert!(d.target.distance(DVec3::new(0.0, 70.0, 1000.0)) < 1.0, "not sent at the unit");
assert!(d.route.len() > 1, "no route after the new target: it flies straight through the defences");
}
// #31: a big cache of fresh columns is not scanned on every answer
#[test]
fn big_ground_cache_is_swept_rarely() {
let mut ground = Ground::default();
ground.known.extend((0..300_001).map(|x| ((x, 0), (64, 2000))));
ground.known.insert((-1, 0), (64, 0));
// The first answer past the threshold sweeps what is stale and keeps what is fresh.
ground.answer(vec![(-2, 0, 64)], 2100);
assert!(!ground.known.contains_key(&(-1, 0)), "a stale column was never swept");
assert!(ground.known.len() > 300_000);
// The next ones wait their pause: a stale column added now survives them.
ground.known.insert((-1, 0), (64, 0));
let swept = ground.swept;
ground.answer(vec![(-3, 0, 64)], 2101);
assert_eq!(ground.swept, swept, "swept again at once");
assert!(ground.known.contains_key(&(-1, 0)));
ground.answer(vec![(-4, 0, 64)], 2100 + SWEEP_EVERY);
assert!(!ground.known.contains_key(&(-1, 0)), "never swept again");
}
// #32: the pieces of an Oreshnik teach the enemy about the sector it came from
#[test]
fn oreshnik_pieces_keep_the_sector() {
let mut sim = Sim::new(1);
sim.add_drone(DroneKind::Oreshnik, DVec3::new(100.0, 300.0, 0.0), DVec3::NEG_Y, DVec3::new(0.0, 70.0, 0.0));
sim.drones[0].vel = DVec3::new(-1.0, -1.0, 0.0);
sim.drones[0].sector = 7;
let mut out = Out::default();
sim.step_drones(&mut Ground::default(), None, &mut out);
let pieces: Vec<_> = sim.drones.iter().filter(|d| d.kind == DroneKind::OreshnikBlock).collect();
assert_eq!(pieces.len(), 6);
assert!(pieces.iter().all(|d| d.sector == 7));
}
// #33: a turret does not fire before it has turned to the target
#[test]
fn turret_waits_for_its_aim() {
let mut sim = Sim::new(1);
let id = sim.id();
let mut s = Sam::new(id, SamKind::Gepard, DVec3::new(0.0, 70.0, 0.0), 0.0);
s.cooldown = 0;
sim.sams.push(s);
let id = sim.id();
sim.sams.push(Sam::new(id, SamKind::Radar, DVec3::new(0.0, 70.0, 0.0), 0.0));
// Straight behind the turret's heading, a slow target well inside the gun's reach.
sim.add_drone(DroneKind::Shahed, DVec3::new(0.0, 75.0, 60.0), DVec3::ZERO, DVec3::new(0.0, 70.0, 900.0));
sim.drones[0].vel = DVec3::ZERO;
let (mut ground, mut out) = (Ground::default(), Out::default());
let mut turned = 0;
for _ in 0..200 {
sim.step(&mut ground, None, &mut out);
let g = &sim.sams[0];
let want = heading_of(sim.drones[0].pos - g.mount().0);
if angle_diff(g.yaw, want).abs() > 0.1 {
assert_eq!(g.bursts, 160, "fired with the turret still turning");
turned += 1;
}
if g.bursts < 160 || sim.drones.is_empty() {
break;
}
}
assert!(turned > 20, "the turret turned too fast to tell ({turned} ticks)");
assert!(sim.sams[0].bursts < 160, "never fired");
}
// #33: the drive speeds up, brakes in time and does not overshoot
#[test]
fn drive_arrives_without_overshoot() {
for err in [std::f64::consts::PI, 1.57, 0.3, 0.02] {
let (mut vel, mut left) = (0.0, err);
let (speed, accel) = (0.088, 0.011);
let mut ticks = 0;
while left > 0.0 {
let step = drive(&mut vel, left, speed, accel);
assert!(step.abs() <= speed + 1e-9 && step >= 0.0, "step {step} at {left} left");
left -= step;
ticks += 1;
assert!(ticks < 400, "never arrived for {err}");
}
assert!(left.abs() < 1e-9 && vel == 0.0);
}
}
+39 -14
View File
@@ -8,8 +8,8 @@ use super::*;
/// Rays round a radar, and blocks between the land's samples along them.
const RAYS: usize = 48;
const STEP: f64 = 16.0;
/// The antenna's height over the vehicle's ground.
const MAST: f64 = 5.0;
/// How far units share their tracks (the network's data links).
pub(super) const LINK: f64 = 1500.0;
/// Radio horizon: `HORIZON * (√mast + √height)` blocks.
const HORIZON: f64 = 35.0;
/// Anything this near is seen (and heard), whatever hides it.
@@ -17,7 +17,7 @@ const NEAR: f64 = 24.0;
pub(super) struct Horizon {
/// Per ray, per sample out: the steepest rise of the land so far (tan of its elevation).
rays: Vec<Vec<f32>>,
pub(super) rays: Vec<Vec<f32>>,
}
impl Horizon {
@@ -58,16 +58,20 @@ impl Horizon {
}
}
/// Where a system's radar is.
/// Where a unit's radar antenna is.
pub(super) fn antenna(sam: &Sam) -> DVec3 {
sam.pos + DVec3::Y * MAST
sam.pos + DVec3::Y * sam.kind.spec().mast
}
/// How far a system's search radar reaches for this kind at `height` over the land:
/// its own reach (worse when damaged) for the target's size, under the radio horizon.
pub(super) fn reach(sam: &Sam, kind: DroneKind, height: f64) -> f64 {
let radar = sam.kind.range() * 1.3 * sam.fitness() * kind.rcs().powf(0.25);
let horizon = HORIZON * (MAST.sqrt() + height.max(0.0).sqrt());
let spec = sam.kind.spec();
if spec.sensor <= 0.0 {
return 0.0;
}
let radar = spec.sensor * sam.fitness() * kind.rcs().powf(0.25);
let horizon = HORIZON * (spec.mast.sqrt() + height.max(0.0).sqrt());
radar.min(horizon).max(NEAR)
}
@@ -77,21 +81,24 @@ impl Sim {
let now = self.tick;
// One horizon every few ticks: the land answers come in over time.
if !self.sams.is_empty() && now % 3 == 0 {
let alive: Vec<usize> = (0..self.sams.len()).filter(|&i| self.sams[i].alive()).collect();
if !alive.is_empty() {
let i = alive[(now / 3) as usize % alive.len()];
let radars: Vec<usize> =
(0..self.sams.len()).filter(|&i| self.sams[i].alive() && self.sams[i].kind.spec().sensor > 0.0).collect();
if !radars.is_empty() {
let i = radars[(now / 3) as usize % radars.len()];
let sam = &self.sams[i];
let h = Horizon::build(antenna(sam), sam.kind.range() * 1.3, ground, now);
let h = Horizon::build(antenna(sam), sam.kind.spec().sensor, ground, now);
self.horizons.insert(sam.id, h);
}
}
self.horizons.retain(|id, _| self.sams.iter().any(|s| s.id == *id && s.alive()));
let alive: std::collections::HashSet<u32> = self.sams.iter().filter(|s| s.alive()).map(|s| s.id).collect();
self.horizons.retain(|id, _| alive.contains(id));
// Each drone's height over the land under it (or over the systems' ground, unknown).
let heights: Vec<Option<f64>> =
self.drones.iter().map(|d| ground.at(d.pos, now).map(|g| d.pos.y - g)).collect();
for sam in &mut self.sams {
sam.seen.clear();
if !sam.alive() {
sam.own.clear();
if !sam.alive() || sam.kind.spec().sensor <= 0.0 || sam.moving() {
continue;
}
let at = antenna(sam);
@@ -108,7 +115,25 @@ impl Sim {
if dist > NEAR && horizon.is_some_and(|hz| hz.hides(at, d.pos)) {
continue;
}
sam.seen.push(d.id);
sam.own.push(d.id);
}
}
// The network: every unit gets the tracks of the radars within data-link range.
let tracks: Vec<(DVec3, Vec<u32>)> =
self.sams.iter().filter(|s| !s.own.is_empty()).map(|s| (s.pos, s.own.clone())).collect();
// Merged in the radars' order, each track once: a set marks what is in already.
let mut have: std::collections::HashSet<u32> = std::collections::HashSet::new();
for sam in &mut self.sams {
sam.seen.clone_from(&sam.own);
if !sam.alive() || sam.moving() || !sam.kind.armed() {
continue;
}
have.clear();
have.extend(sam.own.iter().copied());
for (at, ids) in &tracks {
if at.distance(sam.pos) <= LINK {
sam.seen.extend(ids.iter().copied().filter(|id| have.insert(*id)));
}
}
}
}

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