Compare commits

..

23 Commits

Author SHA1 Message Date
chexit e3148bf458 HD render fixes (#35-#39, #41) and replay time of day (#57) (fork patch 25); radar scan cycles with ageing tracks (#46), damage to individual system parts (#50), long-lasting fires (#52)
build / mod (push) Successful in 3m22s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 22:56:15 +03:00
chexit d0b9498aaf tools/hd_bench.py: a graphics test range — fixed shots in Polygon and Hills, before/after diffs against a baseline, still-frame flicker, GPU time and per-pass budget, a Markdown report (#44)
build / mod (push) Successful in 3m27s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 22:32:53 +03:00
chexit 8065f2225c Sunsets, volumetric clouds and fog in HD (fork patch 23, #53); missiles and drones collide with the world through new world queries (patch 24, API level 12; #42, #43); a direct hit destroys a system, strikes scatter by kind with a small miss chance (#49, #59)
build / mod (push) Successful in 3m24s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 21:44:59 +03:00
chexit a8551b5c28 CI: game and desktop builds check out exactly inputs.tag, verify its release was made by build.yml from that commit, and upload only to it (#40)
build / mod (push) Successful in 3m24s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 21:20:57 +03:00
chexit f443f0a94a HD water darker and clearer, higher ripples (fork patch 22)
build / mod (push) Successful in 3m23s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 19:02:16 +03:00
chexit e33daac636 HD sky, atmosphere and warmer soft sunlight; lighter rippled water (fork patch 21); doc updated
build / mod (push) Successful in 3m20s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 18:38:20 +03:00
chexit 661c852bbf HD water darker, without the vanilla texture (fork patch 20)
build / mod (push) Successful in 3m18s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 17:56:05 +03:00
chexit 428713d4bc HD water: reflections, refraction, depth colour, sun glint (fork patch 19); doc updated
build / mod (push) Successful in 3m22s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 17:47:52 +03:00
chexit 5839da129a HD: temporal upscaler and the near-shadow flicker fixed (fork patch 18); doc updated
build / mod (push) Successful in 3m17s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 17:20:08 +03:00
chexit ab4a7899af TAA and cheaper shadows in HD (patch 16), model handles in the mod API so the mod sends no model names each tick (patch 17, #30); doc updated
build / mod (push) Successful in 3m25s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 17:02:30 +03:00
chexit 65dd84646a docs: HD renderer architecture, status and plan, kept up to date for handover
build / mod (push) Successful in 3m20s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 16:55:15 +03:00
chexit ed2db8523b Shaders, profiling and the rest of the issues: GTAO and HD GPU pass timing in the fork (patch 15; #9), explosion effects culled early per pixel (#11), a native sim runner with scenario tests run in CI (#8), named scenarios (#17), /airdef why and failure counters in raid reports (#18), IRIS-T and AMRAAM models and launch sounds by kind (#28)
build / mod (push) Successful in 3m24s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 16:43:48 +03:00
chexit 67a2a523b4 P2/P3 issues: network-fed launchers priced into smart routes (#4), stable blast light ids (#15), retargeted smart drones replanned round the defences (#21), sim tick without per-unit clones and linear scans (#26), ground cache swept rarely (#31), Oreshnik pieces keep their sector (#32), drives with acceleration and fire only once aimed (#33), radar monitor range (#19), delayed sound volume (#22), world change clears transient state (#23), score line always shown (#24), replay map without full paths (#25), text fit by bisection (#27), docs (#29), glow occlusion by ray distance (#14), blasts composited nearest first (#16)
build / mod (push) Successful in 2m26s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 16:28:24 +03:00
chexit a806635681 HD lights culled per tile (fork patch 14; #10)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 16:25:44 +03:00
chexit 0092ec64a9 HD renderer: cached bind groups, shadow casters only (fork patch 13; #12, #13)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 16:19:53 +03:00
chexit e08fc65ac8 P1 issues: ground queries keep room for unknown land (#1), a destroyed truck frees its job (#2), trucks follow a system that moved (#3), repair cancels cook-offs (#5), a failed save is retried and autosaves are paced (#6, #7), a combined system reloads when either weapon is spent (#20), munitions go down to the land left under a destroyed target (#34); regression tests from the issues
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 16:18:33 +03:00
chexit 90379f6e17 Fixes from review: replays found by raid number, no fired-count underflow, one raid planned at a time, repair resets waiting and marches, clear removes cook-offs and flares, beams reach their targets, tracer direction never NaN, the fork's previous branch always kept
build / mod (push) Successful in 2m24s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 14:29:38 +03:00
chexit bb35975642 Fixes from review: one missile per target when systems fire in the same tick, moves and ammunition saved, searchlight spot and lighting once the beam is on, retargeting only for smart raids, no free reloads after the last depot falls, fork rebuilt when its commit or patches change
build / mod (push) Successful in 2m24s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 14:25:14 +03:00
chexit f2838a8f08 HD stage 2: sun and moon shadows, the world relit, light shafts, fog and a colour grade (fork patch 12)
build / mod (push) Successful in 2m28s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 13:59:51 +03:00
chexit c42d97af08 HD effects preset: mod lights for flames, flares, blasts, wrecks, searchlights; searchlight beams no longer add shader lights (FPS)
build / mod (push) Successful in 2m26s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 13:18:43 +03:00
chexit 6efec3e546 CI: drop actions/cache (the runner's cache server times out and hangs the job's end)
build / mod (push) Successful in 2m29s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 12:52:06 +03:00
chexit d3359f4b39 New systems (IRIS-T, NASAMS, Buk, Tor, P1-SUN), air-defence network with long-range radars, logistics, move orders, EW, adaptive enemy, searchlights and flares; grid holds a kill in view
build / mod (push) Successful in 5m7s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 12:46:30 +03:00
chexit a2f31c831c CI: build on branch pushes only, not on release tags
build / mod (push) Successful in 5m7s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 12:19:03 +03:00
150 changed files with 33199 additions and 593 deletions
+11 -6
View File
@@ -5,7 +5,9 @@
name: build
on:
# Branches only: the release's own tag (build-N) mustn't start another build.
push:
branches: ['**']
workflow_dispatch:
env:
@@ -22,14 +24,17 @@ jobs:
run: |
curl -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --target wasm32-unknown-unknown
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
# Инструмент упаковки (cargo-horizon-mod без пробного запуска) — единственное, что стоит кэшировать.
- uses: actions/cache@v3
continue-on-error: true
with:
path: ~/.cargo/bin/cargo-horizon-mod
key: tool-${{ hashFiles('HORIZON_COMMIT', 'horizon-patches/*') }}
# Без actions/cache: кэш-сервер раннера из контейнеров задач недоступен — восстановление
# падало по таймауту, а сохранение вешало «Complete job» на пару минут. Инструмент собирается заново.
- name: Форк Horizon
run: nice -n 19 python3 tools/airdef.py fork
# Чистая симуляция тестируется нативно (хост-таргет раннера, без WASM): регрессии из sim/regress.rs
# и сценарии tests/scenarios.rs (NaN, налёт кончается, ТЗМ не зависают). Нужен форк: SDK подключён путём.
- name: Тесты мода
working-directory: airdef-mod
run: |
HOST=$(rustc -vV | sed -n 's/^host: //p')
nice -n 19 cargo test --target "$HOST"
- name: Мод
run: nice -n 19 python3 tools/airdef.py mod
- name: Релиз
+63 -7
View File
@@ -22,7 +22,39 @@ jobs:
if: inputs.os == 'macos' || inputs.os == 'both'
runs-on: macos-latest
steps:
- name: Проверка релиза
env:
TOKEN: ${{ secrets.RELEASE_TOKEN || gitea.token }}
API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }}
TAG: ${{ inputs.tag }}
run: |
set -e
if ! [[ "$TAG" =~ ^build-[0-9]+$ ]]; then echo "::error::тег '$TAG' не похож на build-N"; exit 1; fi
REL_JSON=$(mktemp)
CODE=$(curl -s -o "$REL_JSON" -w '%{http_code}' -H "Authorization: token $TOKEN" "$API/releases/tags/$TAG")
if [ "$CODE" != "200" ]; then echo "::error::релиза с тегом $TAG нет (HTTP $CODE): сначала дождись сборки build.yml на main"; exit 1; fi
python3 - "$TAG" "$REL_JSON" <<'PY'
import json, os, sys
tag, path = sys.argv[1], sys.argv[2]
r = json.load(open(path))
assets = [a.get('name') for a in (r.get('assets') or [])]
if r.get('tag_name') != tag or r.get('name') != tag or 'airdef.hmod' not in assets:
print('::error::релиз %s не создан build.yml (нужны имя = тег и вложение airdef.hmod): чужой релиз не трогаем' % tag)
sys.exit(1)
with open(os.environ['GITHUB_ENV'], 'a') as f:
f.write('RELEASE_ID=%s\nRELEASE_SHA=%s\n' % (r['id'], r.get('target_commitish', '')))
PY
- uses: actions/checkout@v4
with:
ref: refs/tags/${{ inputs.tag }}
- name: Проверка коммита
env:
TAG: ${{ inputs.tag }}
run: |
set -e
HEAD_SHA=$(git rev-parse HEAD)
if [[ "$RELEASE_SHA" =~ ^[0-9a-f]{40}$ ]] && [ "$HEAD_SHA" != "$RELEASE_SHA" ]; then echo "::error::тег $TAG указывает на $HEAD_SHA, а релиз создан из $RELEASE_SHA"; exit 1; fi
echo "собираю $TAG = $HEAD_SHA"
- name: Rust
run: |
command -v cargo || curl -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal
@@ -47,11 +79,10 @@ jobs:
env:
TOKEN: ${{ secrets.RELEASE_TOKEN || gitea.token }}
API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }}
TAG: ${{ inputs.tag }}
run: |
set -e
ID=$(curl -sf -H "Authorization: token $TOKEN" "$API/releases/tags/$TAG" | python3 -c 'import json,sys;print(json.load(sys.stdin)["id"])')
curl -sf -X POST -H "Authorization: token $TOKEN" -F "attachment=@out/airdef-macos-arm64.tar.gz" "$API/releases/$ID/assets?name=airdef-macos-arm64.tar.gz" > /dev/null
# RELEASE_ID взят в шаге «Проверка релиза» (релиз build-N, созданный build.yml)
curl -sf -X POST -H "Authorization: token $TOKEN" -F "attachment=@out/airdef-macos-arm64.tar.gz" "$API/releases/$RELEASE_ID/assets?name=airdef-macos-arm64.tar.gz" > /dev/null
game-windows:
if: inputs.os == 'windows' || inputs.os == 'both'
@@ -60,7 +91,34 @@ jobs:
run:
shell: pwsh
steps:
- name: Проверка релиза
env:
TOKEN: ${{ secrets.RELEASE_TOKEN || gitea.token }}
API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }}
TAG: ${{ inputs.tag }}
run: |
$ErrorActionPreference = 'Stop'
if ($env:TAG -notmatch '^build-\d+$') { Write-Output "::error::тег '$env:TAG' не похож на build-N"; exit 1 }
$h = @{ Authorization = "token $env:TOKEN" }
try { $r = Invoke-RestMethod -Headers $h "$env:API/releases/tags/$env:TAG" }
catch { Write-Output "::error::релиза с тегом $env:TAG нет: сначала дождись сборки build.yml на main"; exit 1 }
$assets = @($r.assets | ForEach-Object { $_.name })
if ($r.tag_name -ne $env:TAG -or $r.name -ne $env:TAG -or ($assets -notcontains 'airdef.hmod')) {
Write-Output "::error::релиз $env:TAG не создан build.yml (нужны имя = тег и вложение airdef.hmod): чужой релиз не трогаем"; exit 1
}
"RELEASE_ID=$($r.id)" | Out-File -Append -Encoding utf8 $env:GITHUB_ENV
"RELEASE_SHA=$($r.target_commitish)" | Out-File -Append -Encoding utf8 $env:GITHUB_ENV
- uses: actions/checkout@v4
with:
ref: refs/tags/${{ inputs.tag }}
- name: Проверка коммита
env:
TAG: ${{ inputs.tag }}
run: |
$ErrorActionPreference = 'Stop'
$head = git rev-parse HEAD
if ($env:RELEASE_SHA -match '^[0-9a-f]{40}$' -and $head -ne $env:RELEASE_SHA) { Write-Output "::error::тег $env:TAG указывает на $head, а релиз создан из $env:RELEASE_SHA"; exit 1 }
Write-Output "собираю $env:TAG = $head"
- name: Rust
run: |
if (-not (Get-Command cargo -ErrorAction SilentlyContinue)) {
@@ -88,9 +146,7 @@ jobs:
env:
TOKEN: ${{ secrets.RELEASE_TOKEN || gitea.token }}
API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }}
TAG: ${{ inputs.tag }}
run: |
$h = @{ Authorization = "token $env:TOKEN" }
$id = (Invoke-RestMethod -Headers $h "$env:API/releases/tags/$env:TAG").id
curl.exe -sf -X POST -H "Authorization: token $env:TOKEN" -F "attachment=@out\airdef-windows-x86_64.zip" "$env:API/releases/$id/assets?name=airdef-windows-x86_64.zip"
# RELEASE_ID взят в шаге «Проверка релиза» (релиз build-N, созданный build.yml)
curl.exe -sf -X POST -H "Authorization: token $env:TOKEN" -F "attachment=@out\airdef-windows-x86_64.zip" "$env:API/releases/$env:RELEASE_ID/assets?name=airdef-windows-x86_64.zip"
+34 -3
View File
@@ -25,7 +25,39 @@ jobs:
FREE=$(df --output=avail -BG / | tail -1 | tr -dc 0-9)
echo "свободно: ${FREE} ГБ"
if [ "$FREE" -lt 15 ]; then echo "::error::на сервере свободно ${FREE} ГБ, нужно хотя бы 15: сборка игры забьёт диск и уронит Gitea"; exit 1; fi
- name: Проверка релиза
env:
TOKEN: ${{ secrets.RELEASE_TOKEN || gitea.token }}
API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }}
TAG: ${{ inputs.tag }}
run: |
set -e
if ! [[ "$TAG" =~ ^build-[0-9]+$ ]]; then echo "::error::тег '$TAG' не похож на build-N"; exit 1; fi
REL_JSON=$(mktemp)
CODE=$(curl -s -o "$REL_JSON" -w '%{http_code}' -H "Authorization: token $TOKEN" "$API/releases/tags/$TAG")
if [ "$CODE" != "200" ]; then echo "::error::релиза с тегом $TAG нет (HTTP $CODE): сначала дождись сборки build.yml на main"; exit 1; fi
python3 - "$TAG" "$REL_JSON" <<'PY'
import json, os, sys
tag, path = sys.argv[1], sys.argv[2]
r = json.load(open(path))
assets = [a.get('name') for a in (r.get('assets') or [])]
if r.get('tag_name') != tag or r.get('name') != tag or 'airdef.hmod' not in assets:
print('::error::релиз %s не создан build.yml (нужны имя = тег и вложение airdef.hmod): чужой релиз не трогаем' % tag)
sys.exit(1)
with open(os.environ['GITHUB_ENV'], 'a') as f:
f.write('RELEASE_ID=%s\nRELEASE_SHA=%s\n' % (r['id'], r.get('target_commitish', '')))
PY
- uses: actions/checkout@v4
with:
ref: refs/tags/${{ inputs.tag }}
- name: Проверка коммита
env:
TAG: ${{ inputs.tag }}
run: |
set -e
HEAD_SHA=$(git rev-parse HEAD)
if [[ "$RELEASE_SHA" =~ ^[0-9a-f]{40}$ ]] && [ "$HEAD_SHA" != "$RELEASE_SHA" ]; then echo "::error::тег $TAG указывает на $HEAD_SHA, а релиз создан из $RELEASE_SHA"; exit 1; fi
echo "собираю $TAG = $HEAD_SHA"
- name: Системные библиотеки
run: |
SUDO=$(command -v sudo || true)
@@ -45,12 +77,11 @@ jobs:
env:
TOKEN: ${{ secrets.RELEASE_TOKEN || gitea.token }}
API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }}
TAG: ${{ inputs.tag }}
run: |
set -e
D=airdef-linux-x86_64
mkdir -p "$D" out
cp horizon/target/fast/horizon-client horizon/target/fast/horizon-ctl airdef-mod/target/horizon-mod/airdef.hmod dist/play.sh docs/INSTALL.md "$D/"
tar czf "out/$D.tar.gz" "$D"
ID=$(curl -sf -H "Authorization: token $TOKEN" "$API/releases/tags/$TAG" | python3 -c 'import json,sys;print(json.load(sys.stdin)["id"])')
curl -sf -X POST -H "Authorization: token $TOKEN" -F "attachment=@out/$D.tar.gz" "$API/releases/$ID/assets?name=$D.tar.gz" > /dev/null
# RELEASE_ID взят в шаге «Проверка релиза» (релиз build-N, созданный build.yml)
curl -sf -X POST -H "Authorization: token $TOKEN" -F "attachment=@out/$D.tar.gz" "$API/releases/$RELEASE_ID/assets?name=$D.tar.gz" > /dev/null
+1
View File
@@ -5,3 +5,4 @@ airdef-mod/target/
__pycache__/
/out/
.DS_Store
run-home-bench/
+57 -5
View File
@@ -11,6 +11,7 @@
## Запуск
Пошаговый гайд для Windows и macOS (готовая сборка из релиза или сборка из исходников): [docs/INSTALL.md](docs/INSTALL.md).
Устройство HD-рендера, его статус и план доработки: [docs/HD-RENDERER.md](docs/HD-RENDERER.md).
Коротко, из исходников:
@@ -23,9 +24,12 @@ 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 с тегом
готового релиза `build-N`. Перед сборкой workflow проверяет, что такой релиз создан `build.yml` (имя релиза = тег,
есть `airdef.hmod`), иначе падает с ошибкой; затем выкачивает ровно коммит этого тега (а не ветку, с которой запущен),
сверяет его с коммитом релиза и дописывает архивы только в этот релиз.
## Как играть
@@ -68,6 +72,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 +134,7 @@ CI: `.gitea/workflows/build.yml` на каждый пуш собирает мо
перехватчиков (жёлтые), сбития (x), прилёты, шкала времени с событиями; время идёт само, колесо мыши перематывает;
- **повтор момента** — клик по событию в списке: «призраки» дронов и ракет снова летят в мире, камера идёт за
целью, потом за перехватчиком (или смотрит из пушки, со стороны на прилёт), в момент сбития время замедляется;
- **полный повтор** — весь налёт, камера-режиссёр сама выбирает, за чем следить; колесо — скорость (×0.5–×16);
- **полный повтор** — весь налёт, камера-режиссёр сама выбирает, за чем следить; колесо — перемотка, стрелки — скорость;
- **лучшие моменты** — нарезка до 8 самых интересных событий (перехват баллистики и гиперзвука, поражения
объектов, потерянные комплексы, разделение «Орешника») с титрами и замедлением; колесо листает моменты.
@@ -103,7 +146,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 +195,15 @@ CI: `.gitea/workflows/build.yml` на каждый пуш собирает мо
| Панцирь-С1 | 12 × 57Э6 и 2 × 30 мм; модуль поворачивается, пакеты и стволы поднимаются | ракеты 25–150, пушки 60 |
| С-400 | 4 × 48Н6, вертикальный холодный старт, РЛС обзора на прицепе | 30–480 |
| С-500 | 4 × 77Н6, холодный старт, самые быстрые ракеты | 40–700 |
| IRIS-T SLM | 8 ракет, вертикальный пуск, быстрые и манёвренные | 15–400 |
| NASAMS | 6 × AMRAAM, наводимая пусковая на прицепе | 10–300 |
| Бук-М2 | 4 × 9М317 на направляющих, башня с РЛС подсвета | 20–450 |
| Тор-М2 | 8 × 9М338 вертикально из башни, быстрая реакция против дронов | 8–160 |
| P1-SUN | 8 дронов-перехватчиков против Шахедов и Герань; своей РЛС нет — стреляет по данным сети | 5–450 |
Обслуживающая техника (тоже пультами или `/airdef deploy <ключ>`): **РЛС дальнего обнаружения** (`radar`,
видит на 1,7 км с высокой мачты, хуже прячутся за рельефом), **склад боеприпасов** (`depot`), **ТЗМ** (`tzm`),
**станция РЭБ** (`ew`), **прожектор** (`light`). Новые комплексы: `irist`, `nasams`, `buk`, `tor`, `p1sun`.
**Согласование огня.** Комплексы делят цели между собой: на обычную цель идёт одна ракета, на баллистическую две,
пушки расходятся по разным целям. Первыми бьют по самым опасным (быстрым и тяжёлым) целям, при этом ложные
@@ -0,0 +1,7 @@
{
"variants": {
"": {
"model": "airdef:block/amraam_missile"
}
}
}
@@ -0,0 +1,7 @@
{
"variants": {
"": {
"model": "airdef:block/beam"
}
}
}
@@ -0,0 +1,7 @@
{
"variants": {
"": {
"model": "airdef:block/buk_chassis"
}
}
}
@@ -0,0 +1,7 @@
{
"variants": {
"": {
"model": "airdef:block/buk_rails"
}
}
}
@@ -0,0 +1,7 @@
{
"variants": {
"": {
"model": "airdef:block/buk_turret"
}
}
}
@@ -0,0 +1,7 @@
{
"variants": {
"": {
"model": "airdef:block/depot"
}
}
}
@@ -0,0 +1,7 @@
{
"variants": {
"": {
"model": "airdef:block/ew_truck"
}
}
}
@@ -0,0 +1,7 @@
{
"variants": {
"": {
"model": "airdef:block/flare"
}
}
}
@@ -0,0 +1,7 @@
{
"variants": {
"": {
"model": "airdef:block/irist_chassis"
}
}
}
@@ -0,0 +1,7 @@
{
"variants": {
"": {
"model": "airdef:block/irist_missile"
}
}
}
@@ -0,0 +1,7 @@
{
"variants": {
"": {
"model": "airdef:block/irist_tubes"
}
}
}
@@ -0,0 +1,7 @@
{
"variants": {
"": {
"model": "airdef:block/nasams_launcher"
}
}
}
@@ -0,0 +1,7 @@
{
"variants": {
"": {
"model": "airdef:block/nasams_trailer"
}
}
}
@@ -0,0 +1,7 @@
{
"variants": {
"": {
"model": "airdef:block/p1sun"
}
}
}
@@ -0,0 +1,7 @@
{
"variants": {
"": {
"model": "airdef:block/p1sun_launcher"
}
}
}
@@ -0,0 +1,7 @@
{
"variants": {
"": {
"model": "airdef:block/radar_truck"
}
}
}
@@ -0,0 +1,7 @@
{
"variants": {
"": {
"model": "airdef:block/searchlight"
}
}
}
@@ -0,0 +1,7 @@
{
"variants": {
"": {
"model": "airdef:block/searchlight_head"
}
}
}
@@ -0,0 +1,7 @@
{
"variants": {
"": {
"model": "airdef:block/tor_chassis"
}
}
}
@@ -0,0 +1,7 @@
{
"variants": {
"": {
"model": "airdef:block/tor_turret"
}
}
}
@@ -0,0 +1,7 @@
{
"variants": {
"": {
"model": "airdef:block/tzm_truck"
}
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "airdef:item/deploy_buk"
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "airdef:item/deploy_depot"
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "airdef:item/deploy_ew"
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "airdef:item/deploy_irist"
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "airdef:item/deploy_light"
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "airdef:item/deploy_nasams"
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "airdef:item/deploy_p1sun"
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "airdef:item/deploy_radar"
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "airdef:item/deploy_tor"
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "airdef:item/deploy_tzm"
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "airdef:item/flare_gun"
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "airdef:item/mover"
}
}
+33
View File
@@ -4,6 +4,8 @@
"block.airdef.geran3": "Герань-3",
"block.airdef.geran3_flame": "Факел",
"block.airdef.pac3": "PAC-3 MSE",
"block.airdef.irist_missile": "IRIS-T SLM",
"block.airdef.amraam_missile": "AMRAAM",
"block.airdef.flame": "Факел",
"block.airdef.patriot_launcher": "ПУ M903",
"block.airdef.patriot_truck": "Тягач",
@@ -39,6 +41,25 @@
"block.airdef.iskander": "Искандер-М",
"block.airdef.kinzhal": "Кинжал",
"block.airdef.tracer": "Трассер",
"block.airdef.irist_chassis": "Шасси IRIS-T SLM",
"block.airdef.irist_tubes": "Пакет IRIS-T",
"block.airdef.nasams_trailer": "Прицеп NASAMS",
"block.airdef.nasams_launcher": "Пусковая NASAMS",
"block.airdef.buk_chassis": "Шасси Бука",
"block.airdef.buk_turret": "Башня Бука",
"block.airdef.buk_rails": "Ракеты Бука",
"block.airdef.tor_chassis": "Шасси Тора",
"block.airdef.tor_turret": "Башня Тора",
"block.airdef.p1sun_launcher": "Пусковая P1-SUN",
"block.airdef.p1sun": "Дрон-перехватчик P1-SUN",
"block.airdef.radar_truck": "Шасси РЛС",
"block.airdef.depot": "Склад боеприпасов",
"block.airdef.tzm_truck": "Транспортно-заряжающая машина",
"block.airdef.ew_truck": "Станция РЭБ",
"block.airdef.searchlight": "Прожектор",
"block.airdef.searchlight_head": "Голова прожектора",
"block.airdef.beam": "Луч прожектора",
"block.airdef.flare": "Осветительная ракета",
"block.airdef.reentry": "Боевой блок",
"item.airdef.designator": "Целеуказатель",
"item.airdef.launch_shahed": "Пуск: Шахед-136",
@@ -56,6 +77,18 @@
"item.airdef.launch_iskander": "Пуск: Искандер-М",
"item.airdef.launch_kinzhal": "Пуск: Кинжал",
"item.airdef.launch_oreshnik": "Пуск: Орешник",
"item.airdef.deploy_irist": "Развернуть IRIS-T SLM",
"item.airdef.deploy_nasams": "Развернуть NASAMS",
"item.airdef.deploy_buk": "Развернуть Бук-М2",
"item.airdef.deploy_tor": "Развернуть Тор-М2",
"item.airdef.deploy_p1sun": "Развернуть P1-SUN",
"item.airdef.deploy_radar": "Развернуть РЛС дальнего обнаружения",
"item.airdef.deploy_depot": "Развернуть склад боеприпасов",
"item.airdef.deploy_tzm": "Развернуть ТЗМ",
"item.airdef.deploy_ew": "Развернуть станцию РЭБ",
"item.airdef.deploy_light": "Развернуть прожектор",
"item.airdef.mover": "Пульт перемещения",
"item.airdef.flare_gun": "Осветительная ракета",
"item.airdef.remover": "Свернуть ПВО",
"item.airdef.cam_shahed": "Шахед-136",
"item.airdef.cam_geran3": "Герань-3",
+33
View File
@@ -4,6 +4,8 @@
"block.airdef.geran3": "Герань-3",
"block.airdef.geran3_flame": "Факел",
"block.airdef.pac3": "PAC-3 MSE",
"block.airdef.irist_missile": "IRIS-T SLM",
"block.airdef.amraam_missile": "AMRAAM",
"block.airdef.flame": "Факел",
"block.airdef.patriot_launcher": "ПУ M903",
"block.airdef.patriot_truck": "Тягач",
@@ -39,6 +41,25 @@
"block.airdef.iskander": "Искандер-М",
"block.airdef.kinzhal": "Кинжал",
"block.airdef.tracer": "Трассер",
"block.airdef.irist_chassis": "Шасси IRIS-T SLM",
"block.airdef.irist_tubes": "Пакет IRIS-T",
"block.airdef.nasams_trailer": "Прицеп NASAMS",
"block.airdef.nasams_launcher": "Пусковая NASAMS",
"block.airdef.buk_chassis": "Шасси Бука",
"block.airdef.buk_turret": "Башня Бука",
"block.airdef.buk_rails": "Ракеты Бука",
"block.airdef.tor_chassis": "Шасси Тора",
"block.airdef.tor_turret": "Башня Тора",
"block.airdef.p1sun_launcher": "Пусковая P1-SUN",
"block.airdef.p1sun": "Дрон-перехватчик P1-SUN",
"block.airdef.radar_truck": "Шасси РЛС",
"block.airdef.depot": "Склад боеприпасов",
"block.airdef.tzm_truck": "Транспортно-заряжающая машина",
"block.airdef.ew_truck": "Станция РЭБ",
"block.airdef.searchlight": "Прожектор",
"block.airdef.searchlight_head": "Голова прожектора",
"block.airdef.beam": "Луч прожектора",
"block.airdef.flare": "Осветительная ракета",
"block.airdef.reentry": "Боевой блок",
"item.airdef.designator": "Целеуказатель",
"item.airdef.launch_shahed": "Пуск: Шахед-136",
@@ -56,6 +77,18 @@
"item.airdef.launch_iskander": "Пуск: Искандер-М",
"item.airdef.launch_kinzhal": "Пуск: Кинжал",
"item.airdef.launch_oreshnik": "Пуск: Орешник",
"item.airdef.deploy_irist": "Развернуть IRIS-T SLM",
"item.airdef.deploy_nasams": "Развернуть NASAMS",
"item.airdef.deploy_buk": "Развернуть Бук-М2",
"item.airdef.deploy_tor": "Развернуть Тор-М2",
"item.airdef.deploy_p1sun": "Развернуть P1-SUN",
"item.airdef.deploy_radar": "Развернуть РЛС дальнего обнаружения",
"item.airdef.deploy_depot": "Развернуть склад боеприпасов",
"item.airdef.deploy_tzm": "Развернуть ТЗМ",
"item.airdef.deploy_ew": "Развернуть станцию РЭБ",
"item.airdef.deploy_light": "Развернуть прожектор",
"item.airdef.mover": "Пульт перемещения",
"item.airdef.flare_gun": "Осветительная ракета",
"item.airdef.remover": "Свернуть ПВО",
"item.airdef.cam_shahed": "Шахед-136",
"item.airdef.cam_geran3": "Герань-3",
@@ -0,0 +1,895 @@
{
"ambientocclusion": false,
"textures": {
"nose_dark": "airdef:block/nose_dark",
"white": "airdef:block/white",
"nozzle": "airdef:block/nozzle",
"particle": "airdef:block/nose_dark"
},
"elements": [
{
"from": [
7.333333333333333,
7.333333333333333,
-16.0
],
"to": [
8.666666666666666,
8.666666666666666,
-13.666666666666666
],
"faces": {
"north": {
"uv": [
0,
0,
1.333333333333333,
1.333333333333333
],
"texture": "#nose_dark"
},
"south": {
"uv": [
0,
0,
1.333333333333333,
1.333333333333333
],
"texture": "#nose_dark"
},
"east": {
"uv": [
0,
0,
2.333333333333334,
1.333333333333333
],
"texture": "#nose_dark"
},
"west": {
"uv": [
0,
0,
2.333333333333334,
1.333333333333333
],
"texture": "#nose_dark"
},
"up": {
"uv": [
0,
0,
1.333333333333333,
2.333333333333334
],
"texture": "#nose_dark"
},
"down": {
"uv": [
0,
0,
1.333333333333333,
2.333333333333334
],
"texture": "#nose_dark"
}
}
},
{
"from": [
7.0,
7.0,
-13.666666666666666
],
"to": [
9.0,
9.0,
-11.333333333333332
],
"faces": {
"north": {
"uv": [
0,
0,
2.0,
2.0
],
"texture": "#nose_dark"
},
"south": {
"uv": [
0,
0,
2.0,
2.0
],
"texture": "#nose_dark"
},
"east": {
"uv": [
0,
0,
2.333333333333334,
2.0
],
"texture": "#nose_dark"
},
"west": {
"uv": [
0,
0,
2.333333333333334,
2.0
],
"texture": "#nose_dark"
},
"up": {
"uv": [
0,
0,
2.0,
2.333333333333334
],
"texture": "#nose_dark"
},
"down": {
"uv": [
0,
0,
2.0,
2.333333333333334
],
"texture": "#nose_dark"
}
}
},
{
"from": [
6.666666666666666,
6.666666666666666,
-11.333333333333332
],
"to": [
9.333333333333334,
9.333333333333334,
-9.0
],
"faces": {
"north": {
"uv": [
0,
0,
2.666666666666668,
2.666666666666668
],
"texture": "#nose_dark"
},
"south": {
"uv": [
0,
0,
2.666666666666668,
2.666666666666668
],
"texture": "#nose_dark"
},
"east": {
"uv": [
0,
0,
2.333333333333332,
2.666666666666668
],
"texture": "#nose_dark"
},
"west": {
"uv": [
0,
0,
2.333333333333332,
2.666666666666668
],
"texture": "#nose_dark"
},
"up": {
"uv": [
0,
0,
2.666666666666668,
2.333333333333332
],
"texture": "#nose_dark"
},
"down": {
"uv": [
0,
0,
2.666666666666668,
2.333333333333332
],
"texture": "#nose_dark"
}
}
},
{
"from": [
6.5,
6.5,
-9.0
],
"to": [
9.5,
9.5,
26.0
],
"faces": {
"north": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#white"
},
"south": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#white"
},
"east": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#white"
},
"west": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#white"
},
"up": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#white"
},
"down": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#white"
}
}
},
{
"from": [
1.0,
7.6,
2.0
],
"to": [
6.5,
8.4,
9.0
],
"faces": {
"north": {
"uv": [
0,
0,
5.5,
0.8000000000000007
],
"texture": "#white"
},
"south": {
"uv": [
0,
0,
5.5,
0.8000000000000007
],
"texture": "#white"
},
"east": {
"uv": [
0,
0,
7.0,
0.8000000000000007
],
"texture": "#white"
},
"west": {
"uv": [
0,
0,
7.0,
0.8000000000000007
],
"texture": "#white"
},
"up": {
"uv": [
0,
0,
5.5,
7.0
],
"texture": "#white"
},
"down": {
"uv": [
0,
0,
5.5,
7.0
],
"texture": "#white"
}
}
},
{
"from": [
9.5,
7.6,
2.0
],
"to": [
15.0,
8.4,
9.0
],
"faces": {
"north": {
"uv": [
0,
0,
5.5,
0.8000000000000007
],
"texture": "#white"
},
"south": {
"uv": [
0,
0,
5.5,
0.8000000000000007
],
"texture": "#white"
},
"east": {
"uv": [
0,
0,
7.0,
0.8000000000000007
],
"texture": "#white"
},
"west": {
"uv": [
0,
0,
7.0,
0.8000000000000007
],
"texture": "#white"
},
"up": {
"uv": [
0,
0,
5.5,
7.0
],
"texture": "#white"
},
"down": {
"uv": [
0,
0,
5.5,
7.0
],
"texture": "#white"
}
}
},
{
"from": [
7.6,
1.0,
2.0
],
"to": [
8.4,
6.5,
9.0
],
"faces": {
"north": {
"uv": [
0,
0,
0.8000000000000007,
5.5
],
"texture": "#white"
},
"south": {
"uv": [
0,
0,
0.8000000000000007,
5.5
],
"texture": "#white"
},
"east": {
"uv": [
0,
0,
7.0,
5.5
],
"texture": "#white"
},
"west": {
"uv": [
0,
0,
7.0,
5.5
],
"texture": "#white"
},
"up": {
"uv": [
0,
0,
0.8000000000000007,
7.0
],
"texture": "#white"
},
"down": {
"uv": [
0,
0,
0.8000000000000007,
7.0
],
"texture": "#white"
}
}
},
{
"from": [
7.6,
9.5,
2.0
],
"to": [
8.4,
15.0,
9.0
],
"faces": {
"north": {
"uv": [
0,
0,
0.8000000000000007,
5.5
],
"texture": "#white"
},
"south": {
"uv": [
0,
0,
0.8000000000000007,
5.5
],
"texture": "#white"
},
"east": {
"uv": [
0,
0,
7.0,
5.5
],
"texture": "#white"
},
"west": {
"uv": [
0,
0,
7.0,
5.5
],
"texture": "#white"
},
"up": {
"uv": [
0,
0,
0.8000000000000007,
7.0
],
"texture": "#white"
},
"down": {
"uv": [
0,
0,
0.8000000000000007,
7.0
],
"texture": "#white"
}
}
},
{
"from": [
4.0,
7.6,
19.0
],
"to": [
6.5,
8.4,
25.0
],
"faces": {
"north": {
"uv": [
0,
0,
2.5,
0.8000000000000007
],
"texture": "#white"
},
"south": {
"uv": [
0,
0,
2.5,
0.8000000000000007
],
"texture": "#white"
},
"east": {
"uv": [
0,
0,
6.0,
0.8000000000000007
],
"texture": "#white"
},
"west": {
"uv": [
0,
0,
6.0,
0.8000000000000007
],
"texture": "#white"
},
"up": {
"uv": [
0,
0,
2.5,
6.0
],
"texture": "#white"
},
"down": {
"uv": [
0,
0,
2.5,
6.0
],
"texture": "#white"
}
}
},
{
"from": [
9.5,
7.6,
19.0
],
"to": [
12.0,
8.4,
25.0
],
"faces": {
"north": {
"uv": [
0,
0,
2.5,
0.8000000000000007
],
"texture": "#white"
},
"south": {
"uv": [
0,
0,
2.5,
0.8000000000000007
],
"texture": "#white"
},
"east": {
"uv": [
0,
0,
6.0,
0.8000000000000007
],
"texture": "#white"
},
"west": {
"uv": [
0,
0,
6.0,
0.8000000000000007
],
"texture": "#white"
},
"up": {
"uv": [
0,
0,
2.5,
6.0
],
"texture": "#white"
},
"down": {
"uv": [
0,
0,
2.5,
6.0
],
"texture": "#white"
}
}
},
{
"from": [
7.6,
4.0,
19.0
],
"to": [
8.4,
6.5,
25.0
],
"faces": {
"north": {
"uv": [
0,
0,
0.8000000000000007,
2.5
],
"texture": "#white"
},
"south": {
"uv": [
0,
0,
0.8000000000000007,
2.5
],
"texture": "#white"
},
"east": {
"uv": [
0,
0,
6.0,
2.5
],
"texture": "#white"
},
"west": {
"uv": [
0,
0,
6.0,
2.5
],
"texture": "#white"
},
"up": {
"uv": [
0,
0,
0.8000000000000007,
6.0
],
"texture": "#white"
},
"down": {
"uv": [
0,
0,
0.8000000000000007,
6.0
],
"texture": "#white"
}
}
},
{
"from": [
7.6,
9.5,
19.0
],
"to": [
8.4,
12.0,
25.0
],
"faces": {
"north": {
"uv": [
0,
0,
0.8000000000000007,
2.5
],
"texture": "#white"
},
"south": {
"uv": [
0,
0,
0.8000000000000007,
2.5
],
"texture": "#white"
},
"east": {
"uv": [
0,
0,
6.0,
2.5
],
"texture": "#white"
},
"west": {
"uv": [
0,
0,
6.0,
2.5
],
"texture": "#white"
},
"up": {
"uv": [
0,
0,
0.8000000000000007,
6.0
],
"texture": "#white"
},
"down": {
"uv": [
0,
0,
0.8000000000000007,
6.0
],
"texture": "#white"
}
}
},
{
"from": [
7.0,
7.0,
26.0
],
"to": [
9.0,
9.0,
28.0
],
"faces": {
"north": {
"uv": [
0,
0,
2.0,
2.0
],
"texture": "#nozzle"
},
"south": {
"uv": [
0,
0,
2.0,
2.0
],
"texture": "#nozzle"
},
"east": {
"uv": [
0,
0,
2.0,
2.0
],
"texture": "#nozzle"
},
"west": {
"uv": [
0,
0,
2.0,
2.0
],
"texture": "#nozzle"
},
"up": {
"uv": [
0,
0,
2.0,
2.0
],
"texture": "#nozzle"
},
"down": {
"uv": [
0,
0,
2.0,
2.0
],
"texture": "#nozzle"
}
}
}
]
}
@@ -0,0 +1,77 @@
{
"ambientocclusion": false,
"textures": {
"beam": "airdef:block/beam",
"particle": "airdef:block/beam"
},
"elements": [
{
"from": [
6.5,
6.5,
-16.0
],
"to": [
9.5,
9.5,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#beam"
},
"south": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#beam"
},
"east": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#beam"
},
"west": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#beam"
},
"up": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#beam"
},
"down": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#beam"
}
}
}
]
}
@@ -0,0 +1,487 @@
{
"ambientocclusion": false,
"textures": {
"ru_green": "airdef:block/ru_green",
"dark_metal": "airdef:block/dark_metal",
"track": "airdef:block/track",
"particle": "airdef:block/ru_green"
},
"elements": [
{
"from": [
-10.0,
4.0,
-14.0
],
"to": [
26.0,
15.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
11.0
],
"texture": "#ru_green"
},
"south": {
"uv": [
0,
0,
16,
11.0
],
"texture": "#ru_green"
},
"east": {
"uv": [
0,
0,
16,
11.0
],
"texture": "#ru_green"
},
"west": {
"uv": [
0,
0,
16,
11.0
],
"texture": "#ru_green"
},
"up": {
"uv": [
0,
0,
16,
16
],
"texture": "#ru_green"
},
"down": {
"uv": [
0,
0,
16,
16
],
"texture": "#ru_green"
}
}
},
{
"from": [
-10.0,
4.0,
-16.0
],
"to": [
26.0,
10.0,
-14.0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
6.0
],
"texture": "#ru_green"
},
"south": {
"uv": [
0,
0,
16,
6.0
],
"texture": "#ru_green"
},
"east": {
"uv": [
0,
0,
2.0,
6.0
],
"texture": "#ru_green"
},
"west": {
"uv": [
0,
0,
2.0,
6.0
],
"texture": "#ru_green"
},
"up": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#ru_green"
},
"down": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#ru_green"
}
}
},
{
"from": [
-8.0,
15.0,
22.0
],
"to": [
24.0,
16.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
1.0
],
"texture": "#dark_metal"
},
"south": {
"uv": [
0,
0,
16,
1.0
],
"texture": "#dark_metal"
},
"east": {
"uv": [
0,
0,
10.0,
1.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
10.0,
1.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
16,
10.0
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
16,
10.0
],
"texture": "#dark_metal"
}
}
},
{
"from": [
-14.0,
0.0,
-15.0
],
"to": [
-10.0,
9.0,
31.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
9.0
],
"texture": "#track"
},
"south": {
"uv": [
0,
0,
4.0,
9.0
],
"texture": "#track"
},
"east": {
"uv": [
0,
0,
16,
9.0
],
"texture": "#track"
},
"west": {
"uv": [
0,
0,
16,
9.0
],
"texture": "#track"
},
"up": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#track"
},
"down": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#track"
}
}
},
{
"from": [
26.0,
0.0,
-15.0
],
"to": [
30.0,
9.0,
31.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
9.0
],
"texture": "#track"
},
"south": {
"uv": [
0,
0,
4.0,
9.0
],
"texture": "#track"
},
"east": {
"uv": [
0,
0,
16,
9.0
],
"texture": "#track"
},
"west": {
"uv": [
0,
0,
16,
9.0
],
"texture": "#track"
},
"up": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#track"
},
"down": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#track"
}
}
},
{
"from": [
-14.5,
5.0,
-15.0
],
"to": [
-10.0,
10.0,
31.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.5,
5.0
],
"texture": "#ru_green"
},
"south": {
"uv": [
0,
0,
4.5,
5.0
],
"texture": "#ru_green"
},
"east": {
"uv": [
0,
0,
16,
5.0
],
"texture": "#ru_green"
},
"west": {
"uv": [
0,
0,
16,
5.0
],
"texture": "#ru_green"
},
"up": {
"uv": [
0,
0,
4.5,
16
],
"texture": "#ru_green"
},
"down": {
"uv": [
0,
0,
4.5,
16
],
"texture": "#ru_green"
}
}
},
{
"from": [
26.0,
5.0,
-15.0
],
"to": [
30.5,
10.0,
31.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.5,
5.0
],
"texture": "#ru_green"
},
"south": {
"uv": [
0,
0,
4.5,
5.0
],
"texture": "#ru_green"
},
"east": {
"uv": [
0,
0,
16,
5.0
],
"texture": "#ru_green"
},
"west": {
"uv": [
0,
0,
16,
5.0
],
"texture": "#ru_green"
},
"up": {
"uv": [
0,
0,
4.5,
16
],
"texture": "#ru_green"
},
"down": {
"uv": [
0,
0,
4.5,
16
],
"texture": "#ru_green"
}
}
}
]
}
@@ -0,0 +1,896 @@
{
"ambientocclusion": false,
"textures": {
"missile_ru": "airdef:block/missile_ru",
"booster": "airdef:block/booster",
"dark_metal": "airdef:block/dark_metal",
"ru_green": "airdef:block/ru_green",
"particle": "airdef:block/missile_ru"
},
"elements": [
{
"from": [
-10.5,
4.0,
-16.0
],
"to": [
-7.5,
7.0,
26.0
],
"faces": {
"north": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#missile_ru"
},
"south": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#missile_ru"
},
"east": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#missile_ru"
},
"west": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#missile_ru"
},
"up": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#missile_ru"
},
"down": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#missile_ru"
}
}
},
{
"from": [
-10.8,
3.5,
20.0
],
"to": [
-7.2,
7.5,
27.0
],
"faces": {
"north": {
"uv": [
0,
0,
3.6000000000000005,
4.0
],
"texture": "#booster"
},
"south": {
"uv": [
0,
0,
3.6000000000000005,
4.0
],
"texture": "#booster"
},
"east": {
"uv": [
0,
0,
7.0,
4.0
],
"texture": "#booster"
},
"west": {
"uv": [
0,
0,
7.0,
4.0
],
"texture": "#booster"
},
"up": {
"uv": [
0,
0,
3.6000000000000005,
7.0
],
"texture": "#booster"
},
"down": {
"uv": [
0,
0,
3.6000000000000005,
7.0
],
"texture": "#booster"
}
}
},
{
"from": [
-9.5,
2.0,
-6.0
],
"to": [
-8.5,
4.0,
16.0
],
"faces": {
"north": {
"uv": [
0,
0,
1.0,
2.0
],
"texture": "#dark_metal"
},
"south": {
"uv": [
0,
0,
1.0,
2.0
],
"texture": "#dark_metal"
},
"east": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
1.0,
16
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
1.0,
16
],
"texture": "#dark_metal"
}
}
},
{
"from": [
-4.5,
4.0,
-16.0
],
"to": [
-1.5,
7.0,
26.0
],
"faces": {
"north": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#missile_ru"
},
"south": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#missile_ru"
},
"east": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#missile_ru"
},
"west": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#missile_ru"
},
"up": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#missile_ru"
},
"down": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#missile_ru"
}
}
},
{
"from": [
-4.8,
3.5,
20.0
],
"to": [
-1.2,
7.5,
27.0
],
"faces": {
"north": {
"uv": [
0,
0,
3.5999999999999996,
4.0
],
"texture": "#booster"
},
"south": {
"uv": [
0,
0,
3.5999999999999996,
4.0
],
"texture": "#booster"
},
"east": {
"uv": [
0,
0,
7.0,
4.0
],
"texture": "#booster"
},
"west": {
"uv": [
0,
0,
7.0,
4.0
],
"texture": "#booster"
},
"up": {
"uv": [
0,
0,
3.5999999999999996,
7.0
],
"texture": "#booster"
},
"down": {
"uv": [
0,
0,
3.5999999999999996,
7.0
],
"texture": "#booster"
}
}
},
{
"from": [
-3.5,
2.0,
-6.0
],
"to": [
-2.5,
4.0,
16.0
],
"faces": {
"north": {
"uv": [
0,
0,
1.0,
2.0
],
"texture": "#dark_metal"
},
"south": {
"uv": [
0,
0,
1.0,
2.0
],
"texture": "#dark_metal"
},
"east": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
1.0,
16
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
1.0,
16
],
"texture": "#dark_metal"
}
}
},
{
"from": [
11.5,
4.0,
-16.0
],
"to": [
14.5,
7.0,
26.0
],
"faces": {
"north": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#missile_ru"
},
"south": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#missile_ru"
},
"east": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#missile_ru"
},
"west": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#missile_ru"
},
"up": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#missile_ru"
},
"down": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#missile_ru"
}
}
},
{
"from": [
11.2,
3.5,
20.0
],
"to": [
14.8,
7.5,
27.0
],
"faces": {
"north": {
"uv": [
0,
0,
3.6000000000000014,
4.0
],
"texture": "#booster"
},
"south": {
"uv": [
0,
0,
3.6000000000000014,
4.0
],
"texture": "#booster"
},
"east": {
"uv": [
0,
0,
7.0,
4.0
],
"texture": "#booster"
},
"west": {
"uv": [
0,
0,
7.0,
4.0
],
"texture": "#booster"
},
"up": {
"uv": [
0,
0,
3.6000000000000014,
7.0
],
"texture": "#booster"
},
"down": {
"uv": [
0,
0,
3.6000000000000014,
7.0
],
"texture": "#booster"
}
}
},
{
"from": [
12.5,
2.0,
-6.0
],
"to": [
13.5,
4.0,
16.0
],
"faces": {
"north": {
"uv": [
0,
0,
1.0,
2.0
],
"texture": "#dark_metal"
},
"south": {
"uv": [
0,
0,
1.0,
2.0
],
"texture": "#dark_metal"
},
"east": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
1.0,
16
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
1.0,
16
],
"texture": "#dark_metal"
}
}
},
{
"from": [
17.5,
4.0,
-16.0
],
"to": [
20.5,
7.0,
26.0
],
"faces": {
"north": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#missile_ru"
},
"south": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#missile_ru"
},
"east": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#missile_ru"
},
"west": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#missile_ru"
},
"up": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#missile_ru"
},
"down": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#missile_ru"
}
}
},
{
"from": [
17.2,
3.5,
20.0
],
"to": [
20.8,
7.5,
27.0
],
"faces": {
"north": {
"uv": [
0,
0,
3.6000000000000014,
4.0
],
"texture": "#booster"
},
"south": {
"uv": [
0,
0,
3.6000000000000014,
4.0
],
"texture": "#booster"
},
"east": {
"uv": [
0,
0,
7.0,
4.0
],
"texture": "#booster"
},
"west": {
"uv": [
0,
0,
7.0,
4.0
],
"texture": "#booster"
},
"up": {
"uv": [
0,
0,
3.6000000000000014,
7.0
],
"texture": "#booster"
},
"down": {
"uv": [
0,
0,
3.6000000000000014,
7.0
],
"texture": "#booster"
}
}
},
{
"from": [
18.5,
2.0,
-6.0
],
"to": [
19.5,
4.0,
16.0
],
"faces": {
"north": {
"uv": [
0,
0,
1.0,
2.0
],
"texture": "#dark_metal"
},
"south": {
"uv": [
0,
0,
1.0,
2.0
],
"texture": "#dark_metal"
},
"east": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
1.0,
16
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
1.0,
16
],
"texture": "#dark_metal"
}
}
},
{
"from": [
-12.0,
0.0,
-2.0
],
"to": [
28.0,
2.0,
14.0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#ru_green"
},
"south": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#ru_green"
},
"east": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#ru_green"
},
"west": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#ru_green"
},
"up": {
"uv": [
0,
0,
16,
16
],
"texture": "#ru_green"
},
"down": {
"uv": [
0,
0,
16,
16
],
"texture": "#ru_green"
}
}
}
]
}
@@ -0,0 +1,214 @@
{
"ambientocclusion": false,
"textures": {
"ru_green": "airdef:block/ru_green",
"radar_face": "airdef:block/radar_face",
"particle": "airdef:block/ru_green"
},
"elements": [
{
"from": [
-4.0,
0.0,
-4.0
],
"to": [
20.0,
9.0,
20.0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
9.0
],
"texture": "#ru_green"
},
"south": {
"uv": [
0,
0,
16,
9.0
],
"texture": "#ru_green"
},
"east": {
"uv": [
0,
0,
16,
9.0
],
"texture": "#ru_green"
},
"west": {
"uv": [
0,
0,
16,
9.0
],
"texture": "#ru_green"
},
"up": {
"uv": [
0,
0,
16,
16
],
"texture": "#ru_green"
},
"down": {
"uv": [
0,
0,
16,
16
],
"texture": "#ru_green"
}
}
},
{
"from": [
1.0,
2.0,
-10.0
],
"to": [
15.0,
14.0,
-4.0
],
"faces": {
"north": {
"uv": [
0,
0,
14.0,
12.0
],
"texture": "#radar_face"
},
"south": {
"uv": [
0,
0,
14.0,
12.0
],
"texture": "#radar_face"
},
"east": {
"uv": [
0,
0,
6.0,
12.0
],
"texture": "#radar_face"
},
"west": {
"uv": [
0,
0,
6.0,
12.0
],
"texture": "#radar_face"
},
"up": {
"uv": [
0,
0,
14.0,
6.0
],
"texture": "#radar_face"
},
"down": {
"uv": [
0,
0,
14.0,
6.0
],
"texture": "#radar_face"
}
}
},
{
"from": [
-2.0,
9.0,
6.0
],
"to": [
18.0,
11.0,
18.0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#ru_green"
},
"south": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#ru_green"
},
"east": {
"uv": [
0,
0,
12.0,
2.0
],
"texture": "#ru_green"
},
"west": {
"uv": [
0,
0,
12.0,
2.0
],
"texture": "#ru_green"
},
"up": {
"uv": [
0,
0,
16,
12.0
],
"texture": "#ru_green"
},
"down": {
"uv": [
0,
0,
16,
12.0
],
"texture": "#ru_green"
}
}
}
]
}
@@ -0,0 +1,488 @@
{
"ambientocclusion": false,
"textures": {
"concrete": "airdef:block/concrete",
"crate": "airdef:block/crate",
"tube": "airdef:block/tube",
"tube_cap": "airdef:block/tube_cap",
"particle": "airdef:block/concrete"
},
"elements": [
{
"from": [
-14.0,
0.0,
-14.0
],
"to": [
30.0,
2.0,
30.0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#concrete"
},
"south": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#concrete"
},
"east": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#concrete"
},
"west": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#concrete"
},
"up": {
"uv": [
0,
0,
16,
16
],
"texture": "#concrete"
},
"down": {
"uv": [
0,
0,
16,
16
],
"texture": "#concrete"
}
}
},
{
"from": [
-12.0,
2.0,
-12.0
],
"to": [
0.0,
10.0,
0.0
],
"faces": {
"north": {
"uv": [
0,
0,
12.0,
8.0
],
"texture": "#crate"
},
"south": {
"uv": [
0,
0,
12.0,
8.0
],
"texture": "#crate"
},
"east": {
"uv": [
0,
0,
12.0,
8.0
],
"texture": "#crate"
},
"west": {
"uv": [
0,
0,
12.0,
8.0
],
"texture": "#crate"
},
"up": {
"uv": [
0,
0,
12.0,
12.0
],
"texture": "#crate"
},
"down": {
"uv": [
0,
0,
12.0,
12.0
],
"texture": "#crate"
}
}
},
{
"from": [
-12.0,
2.0,
4.0
],
"to": [
0.0,
10.0,
16.0
],
"faces": {
"north": {
"uv": [
0,
0,
12.0,
8.0
],
"texture": "#crate"
},
"south": {
"uv": [
0,
0,
12.0,
8.0
],
"texture": "#crate"
},
"east": {
"uv": [
0,
0,
12.0,
8.0
],
"texture": "#crate"
},
"west": {
"uv": [
0,
0,
12.0,
8.0
],
"texture": "#crate"
},
"up": {
"uv": [
0,
0,
12.0,
12.0
],
"texture": "#crate"
},
"down": {
"uv": [
0,
0,
12.0,
12.0
],
"texture": "#crate"
}
}
},
{
"from": [
4.0,
2.0,
-12.0
],
"to": [
16.0,
10.0,
0.0
],
"faces": {
"north": {
"uv": [
0,
0,
12.0,
8.0
],
"texture": "#crate"
},
"south": {
"uv": [
0,
0,
12.0,
8.0
],
"texture": "#crate"
},
"east": {
"uv": [
0,
0,
12.0,
8.0
],
"texture": "#crate"
},
"west": {
"uv": [
0,
0,
12.0,
8.0
],
"texture": "#crate"
},
"up": {
"uv": [
0,
0,
12.0,
12.0
],
"texture": "#crate"
},
"down": {
"uv": [
0,
0,
12.0,
12.0
],
"texture": "#crate"
}
}
},
{
"from": [
6.0,
2.0,
6.0
],
"to": [
28.0,
7.0,
10.0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
5.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
16,
5.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
4.0,
5.0
],
"texture": "#tube"
},
"west": {
"uv": [
0,
0,
4.0,
5.0
],
"texture": "#tube"
},
"up": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#tube"
},
"down": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#tube"
}
}
},
{
"from": [
6.0,
7.0,
6.0
],
"to": [
28.0,
12.0,
10.0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
5.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
16,
5.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
4.0,
5.0
],
"texture": "#tube"
},
"west": {
"uv": [
0,
0,
4.0,
5.0
],
"texture": "#tube"
},
"up": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#tube"
},
"down": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#tube"
}
}
},
{
"from": [
6.0,
12.0,
6.0
],
"to": [
28.0,
17.0,
10.0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
5.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
16,
5.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
4.0,
5.0
],
"texture": "#tube"
},
"west": {
"uv": [
0,
0,
4.0,
5.0
],
"texture": "#tube"
},
"up": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#tube"
},
"down": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#tube"
}
}
}
]
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,146 @@
{
"ambientocclusion": false,
"textures": {
"lamp": "airdef:block/lamp",
"antenna": "airdef:block/antenna",
"particle": "airdef:block/lamp"
},
"elements": [
{
"from": [
6.5,
6.5,
6.5
],
"to": [
9.5,
9.5,
9.5
],
"faces": {
"north": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#lamp"
},
"south": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#lamp"
},
"east": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#lamp"
},
"west": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#lamp"
},
"up": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#lamp"
},
"down": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#lamp"
}
}
},
{
"from": [
7.6,
9.5,
7.6
],
"to": [
8.4,
16.0,
8.4
],
"faces": {
"north": {
"uv": [
0,
0,
0.8000000000000007,
6.5
],
"texture": "#antenna"
},
"south": {
"uv": [
0,
0,
0.8000000000000007,
6.5
],
"texture": "#antenna"
},
"east": {
"uv": [
0,
0,
0.8000000000000007,
6.5
],
"texture": "#antenna"
},
"west": {
"uv": [
0,
0,
0.8000000000000007,
6.5
],
"texture": "#antenna"
},
"up": {
"uv": [
0,
0,
0.8000000000000007,
0.8000000000000007
],
"texture": "#antenna"
},
"down": {
"uv": [
0,
0,
0.8000000000000007,
0.8000000000000007
],
"texture": "#antenna"
}
}
}
]
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,895 @@
{
"ambientocclusion": false,
"textures": {
"white": "airdef:block/white",
"missile_grey": "airdef:block/missile_grey",
"nozzle": "airdef:block/nozzle",
"particle": "airdef:block/white"
},
"elements": [
{
"from": [
7.333333333333333,
7.333333333333333,
-16.0
],
"to": [
8.666666666666666,
8.666666666666666,
-14.0
],
"faces": {
"north": {
"uv": [
0,
0,
1.333333333333333,
1.333333333333333
],
"texture": "#white"
},
"south": {
"uv": [
0,
0,
1.333333333333333,
1.333333333333333
],
"texture": "#white"
},
"east": {
"uv": [
0,
0,
2.0,
1.333333333333333
],
"texture": "#white"
},
"west": {
"uv": [
0,
0,
2.0,
1.333333333333333
],
"texture": "#white"
},
"up": {
"uv": [
0,
0,
1.333333333333333,
2.0
],
"texture": "#white"
},
"down": {
"uv": [
0,
0,
1.333333333333333,
2.0
],
"texture": "#white"
}
}
},
{
"from": [
7.0,
7.0,
-14.0
],
"to": [
9.0,
9.0,
-12.0
],
"faces": {
"north": {
"uv": [
0,
0,
2.0,
2.0
],
"texture": "#white"
},
"south": {
"uv": [
0,
0,
2.0,
2.0
],
"texture": "#white"
},
"east": {
"uv": [
0,
0,
2.0,
2.0
],
"texture": "#white"
},
"west": {
"uv": [
0,
0,
2.0,
2.0
],
"texture": "#white"
},
"up": {
"uv": [
0,
0,
2.0,
2.0
],
"texture": "#white"
},
"down": {
"uv": [
0,
0,
2.0,
2.0
],
"texture": "#white"
}
}
},
{
"from": [
6.666666666666666,
6.666666666666666,
-12.0
],
"to": [
9.333333333333334,
9.333333333333334,
-10.0
],
"faces": {
"north": {
"uv": [
0,
0,
2.666666666666668,
2.666666666666668
],
"texture": "#white"
},
"south": {
"uv": [
0,
0,
2.666666666666668,
2.666666666666668
],
"texture": "#white"
},
"east": {
"uv": [
0,
0,
2.0,
2.666666666666668
],
"texture": "#white"
},
"west": {
"uv": [
0,
0,
2.0,
2.666666666666668
],
"texture": "#white"
},
"up": {
"uv": [
0,
0,
2.666666666666668,
2.0
],
"texture": "#white"
},
"down": {
"uv": [
0,
0,
2.666666666666668,
2.0
],
"texture": "#white"
}
}
},
{
"from": [
6.5,
6.5,
-10.0
],
"to": [
9.5,
9.5,
24.0
],
"faces": {
"north": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#white"
},
"south": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#white"
},
"east": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#white"
},
"west": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#white"
},
"up": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#white"
},
"down": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#white"
}
}
},
{
"from": [
3.0,
7.6,
-9.0
],
"to": [
6.5,
8.4,
-5.0
],
"faces": {
"north": {
"uv": [
0,
0,
3.5,
0.8000000000000007
],
"texture": "#missile_grey"
},
"south": {
"uv": [
0,
0,
3.5,
0.8000000000000007
],
"texture": "#missile_grey"
},
"east": {
"uv": [
0,
0,
4.0,
0.8000000000000007
],
"texture": "#missile_grey"
},
"west": {
"uv": [
0,
0,
4.0,
0.8000000000000007
],
"texture": "#missile_grey"
},
"up": {
"uv": [
0,
0,
3.5,
4.0
],
"texture": "#missile_grey"
},
"down": {
"uv": [
0,
0,
3.5,
4.0
],
"texture": "#missile_grey"
}
}
},
{
"from": [
9.5,
7.6,
-9.0
],
"to": [
13.0,
8.4,
-5.0
],
"faces": {
"north": {
"uv": [
0,
0,
3.5,
0.8000000000000007
],
"texture": "#missile_grey"
},
"south": {
"uv": [
0,
0,
3.5,
0.8000000000000007
],
"texture": "#missile_grey"
},
"east": {
"uv": [
0,
0,
4.0,
0.8000000000000007
],
"texture": "#missile_grey"
},
"west": {
"uv": [
0,
0,
4.0,
0.8000000000000007
],
"texture": "#missile_grey"
},
"up": {
"uv": [
0,
0,
3.5,
4.0
],
"texture": "#missile_grey"
},
"down": {
"uv": [
0,
0,
3.5,
4.0
],
"texture": "#missile_grey"
}
}
},
{
"from": [
7.6,
3.0,
-9.0
],
"to": [
8.4,
6.5,
-5.0
],
"faces": {
"north": {
"uv": [
0,
0,
0.8000000000000007,
3.5
],
"texture": "#missile_grey"
},
"south": {
"uv": [
0,
0,
0.8000000000000007,
3.5
],
"texture": "#missile_grey"
},
"east": {
"uv": [
0,
0,
4.0,
3.5
],
"texture": "#missile_grey"
},
"west": {
"uv": [
0,
0,
4.0,
3.5
],
"texture": "#missile_grey"
},
"up": {
"uv": [
0,
0,
0.8000000000000007,
4.0
],
"texture": "#missile_grey"
},
"down": {
"uv": [
0,
0,
0.8000000000000007,
4.0
],
"texture": "#missile_grey"
}
}
},
{
"from": [
7.6,
9.5,
-9.0
],
"to": [
8.4,
13.0,
-5.0
],
"faces": {
"north": {
"uv": [
0,
0,
0.8000000000000007,
3.5
],
"texture": "#missile_grey"
},
"south": {
"uv": [
0,
0,
0.8000000000000007,
3.5
],
"texture": "#missile_grey"
},
"east": {
"uv": [
0,
0,
4.0,
3.5
],
"texture": "#missile_grey"
},
"west": {
"uv": [
0,
0,
4.0,
3.5
],
"texture": "#missile_grey"
},
"up": {
"uv": [
0,
0,
0.8000000000000007,
4.0
],
"texture": "#missile_grey"
},
"down": {
"uv": [
0,
0,
0.8000000000000007,
4.0
],
"texture": "#missile_grey"
}
}
},
{
"from": [
2.0,
7.6,
18.0
],
"to": [
6.5,
8.4,
26.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.5,
0.8000000000000007
],
"texture": "#white"
},
"south": {
"uv": [
0,
0,
4.5,
0.8000000000000007
],
"texture": "#white"
},
"east": {
"uv": [
0,
0,
8.0,
0.8000000000000007
],
"texture": "#white"
},
"west": {
"uv": [
0,
0,
8.0,
0.8000000000000007
],
"texture": "#white"
},
"up": {
"uv": [
0,
0,
4.5,
8.0
],
"texture": "#white"
},
"down": {
"uv": [
0,
0,
4.5,
8.0
],
"texture": "#white"
}
}
},
{
"from": [
9.5,
7.6,
18.0
],
"to": [
14.0,
8.4,
26.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.5,
0.8000000000000007
],
"texture": "#white"
},
"south": {
"uv": [
0,
0,
4.5,
0.8000000000000007
],
"texture": "#white"
},
"east": {
"uv": [
0,
0,
8.0,
0.8000000000000007
],
"texture": "#white"
},
"west": {
"uv": [
0,
0,
8.0,
0.8000000000000007
],
"texture": "#white"
},
"up": {
"uv": [
0,
0,
4.5,
8.0
],
"texture": "#white"
},
"down": {
"uv": [
0,
0,
4.5,
8.0
],
"texture": "#white"
}
}
},
{
"from": [
7.6,
2.0,
18.0
],
"to": [
8.4,
6.5,
26.0
],
"faces": {
"north": {
"uv": [
0,
0,
0.8000000000000007,
4.5
],
"texture": "#white"
},
"south": {
"uv": [
0,
0,
0.8000000000000007,
4.5
],
"texture": "#white"
},
"east": {
"uv": [
0,
0,
8.0,
4.5
],
"texture": "#white"
},
"west": {
"uv": [
0,
0,
8.0,
4.5
],
"texture": "#white"
},
"up": {
"uv": [
0,
0,
0.8000000000000007,
8.0
],
"texture": "#white"
},
"down": {
"uv": [
0,
0,
0.8000000000000007,
8.0
],
"texture": "#white"
}
}
},
{
"from": [
7.6,
9.5,
18.0
],
"to": [
8.4,
14.0,
26.0
],
"faces": {
"north": {
"uv": [
0,
0,
0.8000000000000007,
4.5
],
"texture": "#white"
},
"south": {
"uv": [
0,
0,
0.8000000000000007,
4.5
],
"texture": "#white"
},
"east": {
"uv": [
0,
0,
8.0,
4.5
],
"texture": "#white"
},
"west": {
"uv": [
0,
0,
8.0,
4.5
],
"texture": "#white"
},
"up": {
"uv": [
0,
0,
0.8000000000000007,
8.0
],
"texture": "#white"
},
"down": {
"uv": [
0,
0,
0.8000000000000007,
8.0
],
"texture": "#white"
}
}
},
{
"from": [
7.0,
7.0,
24.0
],
"to": [
9.0,
9.0,
28.0
],
"faces": {
"north": {
"uv": [
0,
0,
2.0,
2.0
],
"texture": "#nozzle"
},
"south": {
"uv": [
0,
0,
2.0,
2.0
],
"texture": "#nozzle"
},
"east": {
"uv": [
0,
0,
4.0,
2.0
],
"texture": "#nozzle"
},
"west": {
"uv": [
0,
0,
4.0,
2.0
],
"texture": "#nozzle"
},
"up": {
"uv": [
0,
0,
2.0,
4.0
],
"texture": "#nozzle"
},
"down": {
"uv": [
0,
0,
2.0,
4.0
],
"texture": "#nozzle"
}
}
}
]
}
@@ -0,0 +1,691 @@
{
"ambientocclusion": false,
"textures": {
"canister": "airdef:block/canister",
"tube_cap": "airdef:block/tube_cap",
"dark_metal": "airdef:block/dark_metal",
"particle": "airdef:block/canister"
},
"elements": [
{
"from": [
-1.5,
0.0,
-16.0
],
"to": [
2.5,
4.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"west": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"up": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
},
"down": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
}
}
},
{
"from": [
-1.5,
5.0,
-16.0
],
"to": [
2.5,
9.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"west": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"up": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
},
"down": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
}
}
},
{
"from": [
3.5,
0.0,
-16.0
],
"to": [
7.5,
4.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"west": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"up": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
},
"down": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
}
}
},
{
"from": [
3.5,
5.0,
-16.0
],
"to": [
7.5,
9.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"west": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"up": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
},
"down": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
}
}
},
{
"from": [
8.5,
0.0,
-16.0
],
"to": [
12.5,
4.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"west": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"up": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
},
"down": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
}
}
},
{
"from": [
8.5,
5.0,
-16.0
],
"to": [
12.5,
9.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"west": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"up": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
},
"down": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
}
}
},
{
"from": [
13.5,
0.0,
-16.0
],
"to": [
17.5,
4.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"west": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"up": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
},
"down": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
}
}
},
{
"from": [
13.5,
5.0,
-16.0
],
"to": [
17.5,
9.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"west": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"up": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
},
"down": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
}
}
},
{
"from": [
-2.5,
0.0,
26.0
],
"to": [
18.5,
9.0,
28.0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
9.0
],
"texture": "#dark_metal"
},
"south": {
"uv": [
0,
0,
16,
9.0
],
"texture": "#dark_metal"
},
"east": {
"uv": [
0,
0,
2.0,
9.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
2.0,
9.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#dark_metal"
}
}
},
{
"from": [
-2.5,
0.0,
-2.0
],
"to": [
18.5,
9.0,
0.0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
9.0
],
"texture": "#dark_metal"
},
"south": {
"uv": [
0,
0,
16,
9.0
],
"texture": "#dark_metal"
},
"east": {
"uv": [
0,
0,
2.0,
9.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
2.0,
9.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#dark_metal"
}
}
}
]
}
@@ -0,0 +1,555 @@
{
"ambientocclusion": false,
"textures": {
"canister": "airdef:block/canister",
"tube_cap": "airdef:block/tube_cap",
"dark_metal": "airdef:block/dark_metal",
"particle": "airdef:block/canister"
},
"elements": [
{
"from": [
1.0,
0.0,
-16.0
],
"to": [
5.0,
4.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"west": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"up": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
},
"down": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
}
}
},
{
"from": [
1.0,
5.0,
-16.0
],
"to": [
5.0,
9.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"west": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"up": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
},
"down": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
}
}
},
{
"from": [
6.0,
0.0,
-16.0
],
"to": [
10.0,
4.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"west": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"up": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
},
"down": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
}
}
},
{
"from": [
6.0,
5.0,
-16.0
],
"to": [
10.0,
9.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"west": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"up": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
},
"down": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
}
}
},
{
"from": [
11.0,
0.0,
-16.0
],
"to": [
15.0,
4.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"west": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"up": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
},
"down": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
}
}
},
{
"from": [
11.0,
5.0,
-16.0
],
"to": [
15.0,
9.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"west": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#canister"
},
"up": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
},
"down": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#canister"
}
}
},
{
"from": [
0.0,
0.0,
26.0
],
"to": [
16.0,
9.0,
28.0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
9.0
],
"texture": "#dark_metal"
},
"south": {
"uv": [
0,
0,
16,
9.0
],
"texture": "#dark_metal"
},
"east": {
"uv": [
0,
0,
2.0,
9.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
2.0,
9.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#dark_metal"
}
}
},
{
"from": [
0.0,
0.0,
-2.0
],
"to": [
16.0,
9.0,
0.0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
9.0
],
"texture": "#dark_metal"
},
"south": {
"uv": [
0,
0,
16,
9.0
],
"texture": "#dark_metal"
},
"east": {
"uv": [
0,
0,
2.0,
9.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
2.0,
9.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#dark_metal"
}
}
}
]
}
@@ -0,0 +1,488 @@
{
"ambientocclusion": false,
"textures": {
"nato_green": "airdef:block/nato_green",
"dark_metal": "airdef:block/dark_metal",
"metal": "airdef:block/metal",
"tire": "airdef:block/tire",
"particle": "airdef:block/nato_green"
},
"elements": [
{
"from": [
-6.0,
5.0,
-10.0
],
"to": [
22.0,
9.0,
26.0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#nato_green"
},
"south": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#nato_green"
},
"east": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#nato_green"
},
"west": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#nato_green"
},
"up": {
"uv": [
0,
0,
16,
16
],
"texture": "#nato_green"
},
"down": {
"uv": [
0,
0,
16,
16
],
"texture": "#nato_green"
}
}
},
{
"from": [
-4.0,
9.0,
10.0
],
"to": [
20.0,
13.0,
24.0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#dark_metal"
},
"south": {
"uv": [
0,
0,
16,
4.0
],
"texture": "#dark_metal"
},
"east": {
"uv": [
0,
0,
14.0,
4.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
14.0,
4.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
16,
14.0
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
16,
14.0
],
"texture": "#dark_metal"
}
}
},
{
"from": [
6.0,
3.0,
-16.0
],
"to": [
10.0,
6.0,
-10.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
3.0
],
"texture": "#metal"
},
"south": {
"uv": [
0,
0,
4.0,
3.0
],
"texture": "#metal"
},
"east": {
"uv": [
0,
0,
6.0,
3.0
],
"texture": "#metal"
},
"west": {
"uv": [
0,
0,
6.0,
3.0
],
"texture": "#metal"
},
"up": {
"uv": [
0,
0,
4.0,
6.0
],
"texture": "#metal"
},
"down": {
"uv": [
0,
0,
4.0,
6.0
],
"texture": "#metal"
}
}
},
{
"from": [
-9.0,
0.0,
4.0
],
"to": [
-5.5,
9.0,
11.0
],
"faces": {
"north": {
"uv": [
0,
0,
3.5,
9.0
],
"texture": "#tire"
},
"south": {
"uv": [
0,
0,
3.5,
9.0
],
"texture": "#tire"
},
"east": {
"uv": [
0,
0,
7.0,
9.0
],
"texture": "#tire"
},
"west": {
"uv": [
0,
0,
7.0,
9.0
],
"texture": "#tire"
},
"up": {
"uv": [
0,
0,
3.5,
7.0
],
"texture": "#tire"
},
"down": {
"uv": [
0,
0,
3.5,
7.0
],
"texture": "#tire"
}
}
},
{
"from": [
21.5,
0.0,
4.0
],
"to": [
25.0,
9.0,
11.0
],
"faces": {
"north": {
"uv": [
0,
0,
3.5,
9.0
],
"texture": "#tire"
},
"south": {
"uv": [
0,
0,
3.5,
9.0
],
"texture": "#tire"
},
"east": {
"uv": [
0,
0,
7.0,
9.0
],
"texture": "#tire"
},
"west": {
"uv": [
0,
0,
7.0,
9.0
],
"texture": "#tire"
},
"up": {
"uv": [
0,
0,
3.5,
7.0
],
"texture": "#tire"
},
"down": {
"uv": [
0,
0,
3.5,
7.0
],
"texture": "#tire"
}
}
},
{
"from": [
-9.0,
0.0,
14.0
],
"to": [
-5.5,
9.0,
21.0
],
"faces": {
"north": {
"uv": [
0,
0,
3.5,
9.0
],
"texture": "#tire"
},
"south": {
"uv": [
0,
0,
3.5,
9.0
],
"texture": "#tire"
},
"east": {
"uv": [
0,
0,
7.0,
9.0
],
"texture": "#tire"
},
"west": {
"uv": [
0,
0,
7.0,
9.0
],
"texture": "#tire"
},
"up": {
"uv": [
0,
0,
3.5,
7.0
],
"texture": "#tire"
},
"down": {
"uv": [
0,
0,
3.5,
7.0
],
"texture": "#tire"
}
}
},
{
"from": [
21.5,
0.0,
14.0
],
"to": [
25.0,
9.0,
21.0
],
"faces": {
"north": {
"uv": [
0,
0,
3.5,
9.0
],
"texture": "#tire"
},
"south": {
"uv": [
0,
0,
3.5,
9.0
],
"texture": "#tire"
},
"east": {
"uv": [
0,
0,
7.0,
9.0
],
"texture": "#tire"
},
"west": {
"uv": [
0,
0,
7.0,
9.0
],
"texture": "#tire"
},
"up": {
"uv": [
0,
0,
3.5,
7.0
],
"texture": "#tire"
},
"down": {
"uv": [
0,
0,
3.5,
7.0
],
"texture": "#tire"
}
}
}
]
}
@@ -0,0 +1,691 @@
{
"ambientocclusion": false,
"textures": {
"dark_metal": "airdef:block/dark_metal",
"missile": "airdef:block/missile",
"prop": "airdef:block/prop",
"particle": "airdef:block/dark_metal"
},
"elements": [
{
"from": [
6.5,
6.5,
-10.0
],
"to": [
9.5,
9.5,
10.0
],
"faces": {
"north": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#dark_metal"
},
"south": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#dark_metal"
},
"east": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#dark_metal"
}
}
},
{
"from": [
7.0,
7.0,
-14.0
],
"to": [
9.0,
9.0,
-10.0
],
"faces": {
"north": {
"uv": [
0,
0,
2.0,
2.0
],
"texture": "#missile"
},
"south": {
"uv": [
0,
0,
2.0,
2.0
],
"texture": "#missile"
},
"east": {
"uv": [
0,
0,
4.0,
2.0
],
"texture": "#missile"
},
"west": {
"uv": [
0,
0,
4.0,
2.0
],
"texture": "#missile"
},
"up": {
"uv": [
0,
0,
2.0,
4.0
],
"texture": "#missile"
},
"down": {
"uv": [
0,
0,
2.0,
4.0
],
"texture": "#missile"
}
}
},
{
"from": [
2.0,
2.0,
0.0
],
"to": [
8.0,
8.0,
1.5
],
"faces": {
"north": {
"uv": [
0,
0,
6.0,
6.0
],
"texture": "#dark_metal"
},
"south": {
"uv": [
0,
0,
6.0,
6.0
],
"texture": "#dark_metal"
},
"east": {
"uv": [
0,
0,
1.5,
6.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
1.5,
6.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
6.0,
1.5
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
6.0,
1.5
],
"texture": "#dark_metal"
}
}
},
{
"from": [
-0.5,
-0.5,
0.0
],
"to": [
4.5,
4.5,
0.6
],
"faces": {
"north": {
"uv": [
0,
0,
5.0,
5.0
],
"texture": "#prop"
},
"south": {
"uv": [
0,
0,
5.0,
5.0
],
"texture": "#prop"
},
"east": {
"uv": [
0,
0,
0.6,
5.0
],
"texture": "#prop"
},
"west": {
"uv": [
0,
0,
0.6,
5.0
],
"texture": "#prop"
},
"up": {
"uv": [
0,
0,
5.0,
0.6
],
"texture": "#prop"
},
"down": {
"uv": [
0,
0,
5.0,
0.6
],
"texture": "#prop"
}
}
},
{
"from": [
2.0,
8.0,
0.0
],
"to": [
8.0,
14.0,
1.5
],
"faces": {
"north": {
"uv": [
0,
0,
6.0,
6.0
],
"texture": "#dark_metal"
},
"south": {
"uv": [
0,
0,
6.0,
6.0
],
"texture": "#dark_metal"
},
"east": {
"uv": [
0,
0,
1.5,
6.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
1.5,
6.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
6.0,
1.5
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
6.0,
1.5
],
"texture": "#dark_metal"
}
}
},
{
"from": [
-0.5,
11.5,
0.0
],
"to": [
4.5,
16.5,
0.6
],
"faces": {
"north": {
"uv": [
0,
0,
5.0,
5.0
],
"texture": "#prop"
},
"south": {
"uv": [
0,
0,
5.0,
5.0
],
"texture": "#prop"
},
"east": {
"uv": [
0,
0,
0.6,
5.0
],
"texture": "#prop"
},
"west": {
"uv": [
0,
0,
0.6,
5.0
],
"texture": "#prop"
},
"up": {
"uv": [
0,
0,
5.0,
0.6
],
"texture": "#prop"
},
"down": {
"uv": [
0,
0,
5.0,
0.6
],
"texture": "#prop"
}
}
},
{
"from": [
8.0,
2.0,
0.0
],
"to": [
14.0,
8.0,
1.5
],
"faces": {
"north": {
"uv": [
0,
0,
6.0,
6.0
],
"texture": "#dark_metal"
},
"south": {
"uv": [
0,
0,
6.0,
6.0
],
"texture": "#dark_metal"
},
"east": {
"uv": [
0,
0,
1.5,
6.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
1.5,
6.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
6.0,
1.5
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
6.0,
1.5
],
"texture": "#dark_metal"
}
}
},
{
"from": [
11.5,
-0.5,
0.0
],
"to": [
16.5,
4.5,
0.6
],
"faces": {
"north": {
"uv": [
0,
0,
5.0,
5.0
],
"texture": "#prop"
},
"south": {
"uv": [
0,
0,
5.0,
5.0
],
"texture": "#prop"
},
"east": {
"uv": [
0,
0,
0.6,
5.0
],
"texture": "#prop"
},
"west": {
"uv": [
0,
0,
0.6,
5.0
],
"texture": "#prop"
},
"up": {
"uv": [
0,
0,
5.0,
0.6
],
"texture": "#prop"
},
"down": {
"uv": [
0,
0,
5.0,
0.6
],
"texture": "#prop"
}
}
},
{
"from": [
8.0,
8.0,
0.0
],
"to": [
14.0,
14.0,
1.5
],
"faces": {
"north": {
"uv": [
0,
0,
6.0,
6.0
],
"texture": "#dark_metal"
},
"south": {
"uv": [
0,
0,
6.0,
6.0
],
"texture": "#dark_metal"
},
"east": {
"uv": [
0,
0,
1.5,
6.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
1.5,
6.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
6.0,
1.5
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
6.0,
1.5
],
"texture": "#dark_metal"
}
}
},
{
"from": [
11.5,
11.5,
0.0
],
"to": [
16.5,
16.5,
0.6
],
"faces": {
"north": {
"uv": [
0,
0,
5.0,
5.0
],
"texture": "#prop"
},
"south": {
"uv": [
0,
0,
5.0,
5.0
],
"texture": "#prop"
},
"east": {
"uv": [
0,
0,
0.6,
5.0
],
"texture": "#prop"
},
"west": {
"uv": [
0,
0,
0.6,
5.0
],
"texture": "#prop"
},
"up": {
"uv": [
0,
0,
5.0,
0.6
],
"texture": "#prop"
},
"down": {
"uv": [
0,
0,
5.0,
0.6
],
"texture": "#prop"
}
}
}
]
}
@@ -0,0 +1,690 @@
{
"ambientocclusion": false,
"textures": {
"dark_metal": "airdef:block/dark_metal",
"tube_cap": "airdef:block/tube_cap",
"particle": "airdef:block/dark_metal"
},
"elements": [
{
"from": [
0.5,
0.0,
-16.0
],
"to": [
3.5,
3.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#dark_metal"
}
}
},
{
"from": [
0.5,
4.0,
-16.0
],
"to": [
3.5,
7.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#dark_metal"
}
}
},
{
"from": [
4.5,
0.0,
-16.0
],
"to": [
7.5,
3.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#dark_metal"
}
}
},
{
"from": [
4.5,
4.0,
-16.0
],
"to": [
7.5,
7.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#dark_metal"
}
}
},
{
"from": [
8.5,
0.0,
-16.0
],
"to": [
11.5,
3.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#dark_metal"
}
}
},
{
"from": [
8.5,
4.0,
-16.0
],
"to": [
11.5,
7.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#dark_metal"
}
}
},
{
"from": [
12.5,
0.0,
-16.0
],
"to": [
15.5,
3.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#dark_metal"
}
}
},
{
"from": [
12.5,
4.0,
-16.0
],
"to": [
15.5,
7.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
3.0,
3.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
16,
3.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
3.0,
16
],
"texture": "#dark_metal"
}
}
},
{
"from": [
-0.5,
0.0,
26.0
],
"to": [
16.5,
7.0,
28.0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
7.0
],
"texture": "#dark_metal"
},
"south": {
"uv": [
0,
0,
16,
7.0
],
"texture": "#dark_metal"
},
"east": {
"uv": [
0,
0,
2.0,
7.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
2.0,
7.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#dark_metal"
}
}
},
{
"from": [
-0.5,
0.0,
-2.0
],
"to": [
16.5,
7.0,
0.0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
7.0
],
"texture": "#dark_metal"
},
"south": {
"uv": [
0,
0,
16,
7.0
],
"texture": "#dark_metal"
},
"east": {
"uv": [
0,
0,
2.0,
7.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
2.0,
7.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#dark_metal"
}
}
}
]
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,145 @@
{
"ambientocclusion": false,
"textures": {
"dark_metal": "airdef:block/dark_metal",
"particle": "airdef:block/dark_metal"
},
"elements": [
{
"from": [
2.0,
0.0,
2.0
],
"to": [
14.0,
3.0,
14.0
],
"faces": {
"north": {
"uv": [
0,
0,
12.0,
3.0
],
"texture": "#dark_metal"
},
"south": {
"uv": [
0,
0,
12.0,
3.0
],
"texture": "#dark_metal"
},
"east": {
"uv": [
0,
0,
12.0,
3.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
12.0,
3.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
12.0,
12.0
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
12.0,
12.0
],
"texture": "#dark_metal"
}
}
},
{
"from": [
6.0,
3.0,
6.0
],
"to": [
10.0,
12.0,
10.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
9.0
],
"texture": "#dark_metal"
},
"south": {
"uv": [
0,
0,
4.0,
9.0
],
"texture": "#dark_metal"
},
"east": {
"uv": [
0,
0,
4.0,
9.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
4.0,
9.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
4.0,
4.0
],
"texture": "#dark_metal"
}
}
}
]
}
@@ -0,0 +1,146 @@
{
"ambientocclusion": false,
"textures": {
"dark_metal": "airdef:block/dark_metal",
"lamp": "airdef:block/lamp",
"particle": "airdef:block/dark_metal"
},
"elements": [
{
"from": [
3.0,
3.0,
-2.0
],
"to": [
13.0,
13.0,
14.0
],
"faces": {
"north": {
"uv": [
0,
0,
10.0,
10.0
],
"texture": "#dark_metal"
},
"south": {
"uv": [
0,
0,
10.0,
10.0
],
"texture": "#dark_metal"
},
"east": {
"uv": [
0,
0,
16,
10.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
16,
10.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
10.0,
16
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
10.0,
16
],
"texture": "#dark_metal"
}
}
},
{
"from": [
3.5,
3.5,
-2.5
],
"to": [
12.5,
12.5,
-2.0
],
"faces": {
"north": {
"uv": [
0,
0,
9.0,
9.0
],
"texture": "#lamp"
},
"south": {
"uv": [
0,
0,
9.0,
9.0
],
"texture": "#lamp"
},
"east": {
"uv": [
0,
0,
0.5,
9.0
],
"texture": "#lamp"
},
"west": {
"uv": [
0,
0,
0.5,
9.0
],
"texture": "#lamp"
},
"up": {
"uv": [
0,
0,
9.0,
0.5
],
"texture": "#lamp"
},
"down": {
"uv": [
0,
0,
9.0,
0.5
],
"texture": "#lamp"
}
}
}
]
}
@@ -0,0 +1,487 @@
{
"ambientocclusion": false,
"textures": {
"ru_khaki": "airdef:block/ru_khaki",
"dark_metal": "airdef:block/dark_metal",
"track": "airdef:block/track",
"particle": "airdef:block/ru_khaki"
},
"elements": [
{
"from": [
-10.0,
4.0,
-14.0
],
"to": [
26.0,
15.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
11.0
],
"texture": "#ru_khaki"
},
"south": {
"uv": [
0,
0,
16,
11.0
],
"texture": "#ru_khaki"
},
"east": {
"uv": [
0,
0,
16,
11.0
],
"texture": "#ru_khaki"
},
"west": {
"uv": [
0,
0,
16,
11.0
],
"texture": "#ru_khaki"
},
"up": {
"uv": [
0,
0,
16,
16
],
"texture": "#ru_khaki"
},
"down": {
"uv": [
0,
0,
16,
16
],
"texture": "#ru_khaki"
}
}
},
{
"from": [
-10.0,
4.0,
-16.0
],
"to": [
26.0,
10.0,
-14.0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
6.0
],
"texture": "#ru_khaki"
},
"south": {
"uv": [
0,
0,
16,
6.0
],
"texture": "#ru_khaki"
},
"east": {
"uv": [
0,
0,
2.0,
6.0
],
"texture": "#ru_khaki"
},
"west": {
"uv": [
0,
0,
2.0,
6.0
],
"texture": "#ru_khaki"
},
"up": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#ru_khaki"
},
"down": {
"uv": [
0,
0,
16,
2.0
],
"texture": "#ru_khaki"
}
}
},
{
"from": [
-8.0,
15.0,
22.0
],
"to": [
24.0,
16.0,
32.0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
1.0
],
"texture": "#dark_metal"
},
"south": {
"uv": [
0,
0,
16,
1.0
],
"texture": "#dark_metal"
},
"east": {
"uv": [
0,
0,
10.0,
1.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
10.0,
1.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
16,
10.0
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
16,
10.0
],
"texture": "#dark_metal"
}
}
},
{
"from": [
-14.0,
0.0,
-15.0
],
"to": [
-10.0,
9.0,
31.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
9.0
],
"texture": "#track"
},
"south": {
"uv": [
0,
0,
4.0,
9.0
],
"texture": "#track"
},
"east": {
"uv": [
0,
0,
16,
9.0
],
"texture": "#track"
},
"west": {
"uv": [
0,
0,
16,
9.0
],
"texture": "#track"
},
"up": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#track"
},
"down": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#track"
}
}
},
{
"from": [
26.0,
0.0,
-15.0
],
"to": [
30.0,
9.0,
31.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
9.0
],
"texture": "#track"
},
"south": {
"uv": [
0,
0,
4.0,
9.0
],
"texture": "#track"
},
"east": {
"uv": [
0,
0,
16,
9.0
],
"texture": "#track"
},
"west": {
"uv": [
0,
0,
16,
9.0
],
"texture": "#track"
},
"up": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#track"
},
"down": {
"uv": [
0,
0,
4.0,
16
],
"texture": "#track"
}
}
},
{
"from": [
-14.5,
5.0,
-15.0
],
"to": [
-10.0,
10.0,
31.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.5,
5.0
],
"texture": "#ru_khaki"
},
"south": {
"uv": [
0,
0,
4.5,
5.0
],
"texture": "#ru_khaki"
},
"east": {
"uv": [
0,
0,
16,
5.0
],
"texture": "#ru_khaki"
},
"west": {
"uv": [
0,
0,
16,
5.0
],
"texture": "#ru_khaki"
},
"up": {
"uv": [
0,
0,
4.5,
16
],
"texture": "#ru_khaki"
},
"down": {
"uv": [
0,
0,
4.5,
16
],
"texture": "#ru_khaki"
}
}
},
{
"from": [
26.0,
5.0,
-15.0
],
"to": [
30.5,
10.0,
31.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.5,
5.0
],
"texture": "#ru_khaki"
},
"south": {
"uv": [
0,
0,
4.5,
5.0
],
"texture": "#ru_khaki"
},
"east": {
"uv": [
0,
0,
16,
5.0
],
"texture": "#ru_khaki"
},
"west": {
"uv": [
0,
0,
16,
5.0
],
"texture": "#ru_khaki"
},
"up": {
"uv": [
0,
0,
4.5,
16
],
"texture": "#ru_khaki"
},
"down": {
"uv": [
0,
0,
4.5,
16
],
"texture": "#ru_khaki"
}
}
}
]
}
@@ -0,0 +1,760 @@
{
"ambientocclusion": false,
"textures": {
"ru_khaki": "airdef:block/ru_khaki",
"radar_face": "airdef:block/radar_face",
"dark_metal": "airdef:block/dark_metal",
"tube_cap": "airdef:block/tube_cap",
"particle": "airdef:block/ru_khaki"
},
"elements": [
{
"from": [
-4.0,
0.0,
-6.0
],
"to": [
20.0,
16.0,
22.0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
16
],
"texture": "#ru_khaki"
},
"south": {
"uv": [
0,
0,
16,
16
],
"texture": "#ru_khaki"
},
"east": {
"uv": [
0,
0,
16,
16
],
"texture": "#ru_khaki"
},
"west": {
"uv": [
0,
0,
16,
16
],
"texture": "#ru_khaki"
},
"up": {
"uv": [
0,
0,
16,
16
],
"texture": "#ru_khaki"
},
"down": {
"uv": [
0,
0,
16,
16
],
"texture": "#ru_khaki"
}
}
},
{
"from": [
-2.0,
2.0,
-11.0
],
"to": [
18.0,
15.0,
-6.0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
13.0
],
"texture": "#radar_face"
},
"south": {
"uv": [
0,
0,
16,
13.0
],
"texture": "#radar_face"
},
"east": {
"uv": [
0,
0,
5.0,
13.0
],
"texture": "#radar_face"
},
"west": {
"uv": [
0,
0,
5.0,
13.0
],
"texture": "#radar_face"
},
"up": {
"uv": [
0,
0,
16,
5.0
],
"texture": "#radar_face"
},
"down": {
"uv": [
0,
0,
16,
5.0
],
"texture": "#radar_face"
}
}
},
{
"from": [
-3.0,
16.0,
2.0
],
"to": [
19.0,
17.0,
20.0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
1.0
],
"texture": "#dark_metal"
},
"south": {
"uv": [
0,
0,
16,
1.0
],
"texture": "#dark_metal"
},
"east": {
"uv": [
0,
0,
16,
1.0
],
"texture": "#dark_metal"
},
"west": {
"uv": [
0,
0,
16,
1.0
],
"texture": "#dark_metal"
},
"up": {
"uv": [
0,
0,
16,
16
],
"texture": "#dark_metal"
},
"down": {
"uv": [
0,
0,
16,
16
],
"texture": "#dark_metal"
}
}
},
{
"from": [
-1.0,
16.5,
4.0
],
"to": [
3.0,
17.5,
10.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
1.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
4.0,
1.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
6.0,
1.0
],
"texture": "#tube_cap"
},
"west": {
"uv": [
0,
0,
6.0,
1.0
],
"texture": "#tube_cap"
},
"up": {
"uv": [
0,
0,
4.0,
6.0
],
"texture": "#tube_cap"
},
"down": {
"uv": [
0,
0,
4.0,
6.0
],
"texture": "#tube_cap"
}
}
},
{
"from": [
-1.0,
16.5,
12.0
],
"to": [
3.0,
17.5,
18.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
1.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
4.0,
1.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
6.0,
1.0
],
"texture": "#tube_cap"
},
"west": {
"uv": [
0,
0,
6.0,
1.0
],
"texture": "#tube_cap"
},
"up": {
"uv": [
0,
0,
4.0,
6.0
],
"texture": "#tube_cap"
},
"down": {
"uv": [
0,
0,
4.0,
6.0
],
"texture": "#tube_cap"
}
}
},
{
"from": [
4.0,
16.5,
4.0
],
"to": [
8.0,
17.5,
10.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
1.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
4.0,
1.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
6.0,
1.0
],
"texture": "#tube_cap"
},
"west": {
"uv": [
0,
0,
6.0,
1.0
],
"texture": "#tube_cap"
},
"up": {
"uv": [
0,
0,
4.0,
6.0
],
"texture": "#tube_cap"
},
"down": {
"uv": [
0,
0,
4.0,
6.0
],
"texture": "#tube_cap"
}
}
},
{
"from": [
4.0,
16.5,
12.0
],
"to": [
8.0,
17.5,
18.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
1.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
4.0,
1.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
6.0,
1.0
],
"texture": "#tube_cap"
},
"west": {
"uv": [
0,
0,
6.0,
1.0
],
"texture": "#tube_cap"
},
"up": {
"uv": [
0,
0,
4.0,
6.0
],
"texture": "#tube_cap"
},
"down": {
"uv": [
0,
0,
4.0,
6.0
],
"texture": "#tube_cap"
}
}
},
{
"from": [
9.0,
16.5,
4.0
],
"to": [
13.0,
17.5,
10.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
1.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
4.0,
1.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
6.0,
1.0
],
"texture": "#tube_cap"
},
"west": {
"uv": [
0,
0,
6.0,
1.0
],
"texture": "#tube_cap"
},
"up": {
"uv": [
0,
0,
4.0,
6.0
],
"texture": "#tube_cap"
},
"down": {
"uv": [
0,
0,
4.0,
6.0
],
"texture": "#tube_cap"
}
}
},
{
"from": [
9.0,
16.5,
12.0
],
"to": [
13.0,
17.5,
18.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
1.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
4.0,
1.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
6.0,
1.0
],
"texture": "#tube_cap"
},
"west": {
"uv": [
0,
0,
6.0,
1.0
],
"texture": "#tube_cap"
},
"up": {
"uv": [
0,
0,
4.0,
6.0
],
"texture": "#tube_cap"
},
"down": {
"uv": [
0,
0,
4.0,
6.0
],
"texture": "#tube_cap"
}
}
},
{
"from": [
14.0,
16.5,
4.0
],
"to": [
18.0,
17.5,
10.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
1.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
4.0,
1.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
6.0,
1.0
],
"texture": "#tube_cap"
},
"west": {
"uv": [
0,
0,
6.0,
1.0
],
"texture": "#tube_cap"
},
"up": {
"uv": [
0,
0,
4.0,
6.0
],
"texture": "#tube_cap"
},
"down": {
"uv": [
0,
0,
4.0,
6.0
],
"texture": "#tube_cap"
}
}
},
{
"from": [
14.0,
16.5,
12.0
],
"to": [
18.0,
17.5,
18.0
],
"faces": {
"north": {
"uv": [
0,
0,
4.0,
1.0
],
"texture": "#tube_cap"
},
"south": {
"uv": [
0,
0,
4.0,
1.0
],
"texture": "#tube_cap"
},
"east": {
"uv": [
0,
0,
6.0,
1.0
],
"texture": "#tube_cap"
},
"west": {
"uv": [
0,
0,
6.0,
1.0
],
"texture": "#tube_cap"
},
"up": {
"uv": [
0,
0,
4.0,
6.0
],
"texture": "#tube_cap"
},
"down": {
"uv": [
0,
0,
4.0,
6.0
],
"texture": "#tube_cap"
}
}
}
]
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "airdef:item/deploy_buk"
}
}
@@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "airdef:item/deploy_depot"
}
}
@@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "airdef:item/deploy_ew"
}
}
@@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "airdef:item/deploy_irist"
}
}
@@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "airdef:item/deploy_light"
}
}
@@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "airdef:item/deploy_nasams"
}
}
@@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "airdef:item/deploy_p1sun"
}
}
@@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "airdef:item/deploy_radar"
}
}
@@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "airdef:item/deploy_tor"
}
}
@@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "airdef:item/deploy_tzm"
}
}
@@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "airdef:item/flare_gun"
}
}
@@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "airdef:item/mover"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 342 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 374 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 328 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 346 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 404 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 397 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 400 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 400 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 388 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 395 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 386 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 403 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 401 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 400 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 386 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 404 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 407 B

After

Width:  |  Height:  |  Size: 411 B

+59
View File
@@ -0,0 +1,59 @@
//! Rough cost of handing 2000 model instances to the game each tick: allocations and bytes the mod
//! builds (counting allocator) and the bytes the canonical ABI then copies across (wasm32 layout:
//! `model-instance` 64 bytes + its name, `model-instance-id` 56 bytes).
//! Run: `cargo run --release --example emit_cost`.
use horizon_mod_sdk::render::{ModelInstance, ModelInstanceId};
use std::alloc::{GlobalAlloc, Layout, System};
use std::sync::atomic::{AtomicUsize, Ordering::Relaxed};
static COUNT: AtomicUsize = AtomicUsize::new(0);
static BYTES: AtomicUsize = AtomicUsize::new(0);
struct Counting;
unsafe impl GlobalAlloc for Counting {
unsafe fn alloc(&self, l: Layout) -> *mut u8 {
COUNT.fetch_add(1, Relaxed);
BYTES.fetch_add(l.size(), Relaxed);
unsafe { System.alloc(l) }
}
unsafe fn dealloc(&self, p: *mut u8, l: Layout) {
unsafe { System.dealloc(p, l) }
}
}
#[global_allocator]
static A: Counting = Counting;
const NAMES: [&str; 4] = ["airdef:shahed", "airdef:geran_2", "airdef:iris_t", "airdef:explosion_core"];
fn measure(f: impl FnOnce()) -> (usize, usize) {
let (c, b) = (COUNT.load(Relaxed), BYTES.load(Relaxed));
f();
(COUNT.load(Relaxed) - c, BYTES.load(Relaxed) - b)
}
fn main() {
const N: usize = 2000;
let (mut old, mut by_id): (Vec<ModelInstance>, Vec<ModelInstanceId>) = (Vec::new(), Vec::new());
by_id.reserve(N);
let inst = |i: usize| (i as u32, NAMES[i % 4], i as f64);
let wire_old: usize = (0..N).map(|i| 64 + NAMES[i % 4].len()).sum();
let (c, b) = measure(|| {
old = (0..N)
.map(|i| {
let (id, n, x) = inst(i);
ModelInstance { id, model: n.to_string(), x, y: x, z: x, rotation: (0., 0., 0., 1.), scale: 1., glow: false }
})
.collect();
});
println!("old : {c} allocations, {b} bytes built per tick; {wire_old} bytes across, {N} strings lowered");
drop(old);
for tick in 0..2 {
let (c, b) = measure(|| {
by_id.clear();
for i in 0..N {
let (id, _, x) = inst(i);
by_id.push(ModelInstanceId { id, model: (i % 4) as u32, x, y: x, z: x, rotation: (0., 0., 0., 1.), scale: 1., glow: false });
}
});
println!("new : tick {tick}: {c} allocations, {b} bytes built; {} bytes across, no strings", N * 56);
}
}
+430
View File
@@ -0,0 +1,430 @@
//! Headless driver for the pure simulation (`airdef::sim`): builds a scenario, steps the
//! simulation like the mod's tick does (answering ground queries from a height function),
//! and measures. Shared by the `sim_run` example and `tests/scenarios.rs`.
//!
//! Native timings say nothing about the WASM build or the GPU; use them to compare runs of the
//! same machine.
#![allow(dead_code)]
use airdef::sim::{Aim, Approach, DroneKind, Ground, Out, RaidOrder, Sam, SamKind, Sim};
use glam::DVec3;
use std::collections::HashMap;
use std::time::{Duration, Instant};
/// Height of the land under a column (what `render.ground` would answer).
#[derive(Clone, Copy, Debug)]
pub enum Terrain {
Flat(i32),
/// Smooth hills: `base + amp * (sin(x/period) + cos(z/period)) / 2`.
Hills { base: i32, amp: f64, period: f64 },
}
/// The land as blocks: solid at and below the column's height (what the game would answer `render.sections` with).
impl airdef::sim::world::WorldSource for Terrain {
fn solid(&self, x: i32, y: i32, z: i32) -> Option<bool> {
Some(y < self.height(x, z))
}
}
impl Terrain {
pub fn height(self, x: i32, z: i32) -> i32 {
match self {
Terrain::Flat(h) => h,
Terrain::Hills { base, amp, period } => {
base + (amp * ((f64::from(x) / period).sin() + (f64::from(z) / period).cos()) / 2.0).round() as i32
}
}
}
}
#[derive(Clone, Debug)]
pub struct RaidSpec {
pub kinds: Vec<(DroneKind, u32)>,
pub approach: Approach,
pub aim: Aim,
pub dist: f64,
/// The tick the raid is ordered at (a raid waits for the previous one to be sent).
pub at: u64,
}
#[derive(Clone, Debug)]
pub struct Unit {
pub kind: SamKind,
pub pos: DVec3,
pub heading: f64,
/// Starts with an empty magazine, waiting for a reload (logistics).
pub empty: bool,
}
#[derive(Clone, Debug)]
pub struct Scenario {
pub name: String,
pub seed: u64,
pub units: Vec<Unit>,
pub objects: Vec<DVec3>,
pub raids: Vec<RaidSpec>,
pub terrain: Terrain,
/// Hard limit of ticks (20 a second).
pub ticks: u64,
/// Stop as soon as every raid has ended.
pub stop_at_end: bool,
pub ew: bool,
/// Ticks a truck on a job may stand still before it counts as stuck.
pub stuck_after: u64,
}
impl Scenario {
pub fn new(name: &str) -> Self {
Scenario {
name: name.into(),
seed: 1,
units: Vec::new(),
objects: vec![DVec3::new(0.0, 70.0, 0.0)],
raids: Vec::new(),
terrain: Terrain::Flat(70),
ticks: 20 * 60 * 10,
stop_at_end: true,
ew: false,
stuck_after: 600,
}
}
pub fn unit(&mut self, kind: SamKind, x: f64, z: f64) -> &mut Unit {
self.units.push(Unit { kind, pos: DVec3::new(x, 70.0, z), heading: 0.0, empty: false });
self.units.last_mut().unwrap()
}
}
#[derive(Clone, Debug, Default)]
pub struct Report {
pub name: String,
pub seed: u64,
pub ticks_run: u64,
pub units: usize,
pub launched: u32,
pub kills: u32,
pub hits: u32,
/// Missiles fired by air defence (sum of the systems' `fired`).
pub fired: u32,
pub ground_queries: u64,
/// The block cache's counters (issue #43) and the drones that struck the land (issue #42).
pub world: String,
pub crashes: u32,
pub ground_batches: u64,
pub tick_p50: Duration,
pub tick_p95: Duration,
pub tick_max: Duration,
pub tick_total: Duration,
/// Time spent in ticks that began with a raid being planned.
pub planning: Duration,
pub planning_ticks: u64,
pub nan_positions: u64,
/// Ids of trucks that stood on a job without progress for too long.
pub stuck_trucks: Vec<u32>,
/// Truck-ticks spent on a job or driving (shows the logistics actually ran).
pub truck_busy_ticks: u64,
pub raids_ordered: usize,
pub raids_ended: usize,
pub all_ended: bool,
}
fn finite(v: DVec3) -> bool {
v.x.is_finite() && v.y.is_finite() && v.z.is_finite()
}
pub fn run(sc: &Scenario) -> Report {
let mut sim = Sim::new(sc.seed);
sim.damage = false; // explosions would change the land the height function describes
for u in &sc.units {
let id = sim.id();
let mut s = Sam::new(id, u.kind, u.pos, u.heading);
if u.empty {
s.empty = true;
s.missiles = 0;
}
sim.sams.push(s);
}
sim.targets = sc.objects.clone();
sim.target_health = vec![100.0; sc.objects.len()];
if sc.ew {
sim.set_ew(true);
}
let mut ground = Ground::default();
let mut pending: Vec<(i32, i32, i32)> = Vec::new();
let mut r = Report { name: sc.name.clone(), seed: sc.seed, units: sc.units.len(), ..Default::default() };
let mut times: Vec<Duration> = Vec::with_capacity(sc.ticks as usize);
let mut ordered = vec![false; sc.raids.len()];
let mut launched_before = vec![0u32; sc.raids.len()];
let mut ended = vec![false; sc.raids.len()];
// truck id -> (last position, ticks standing while busy)
let mut standing: HashMap<u32, (DVec3, u64)> = HashMap::new();
let mut out = Out::default();
for now in 1..=sc.ticks {
// Raids come in order: one is ordered once the previous one has been sent.
for (i, spec) in sc.raids.iter().enumerate() {
if !ordered[i] && now >= spec.at && !sim.planning() && (i == 0 || ended[i - 1]) {
let order = RaidOrder { kinds: spec.kinds.clone(), approach: spec.approach, dist: spec.dist, aim: spec.aim };
launched_before[i] = sim.launched;
sim.order_raid(&order, &sc.objects, &mut out);
ordered[i] = true;
}
}
ground.answer(std::mem::take(&mut pending), now);
let planning = sim.planning();
out = Out::default();
let t = Instant::now();
sim.step(&mut ground, None, &mut out);
let wanted = ground.take_wanted();
let sections = ground.world.take_wanted(now);
ground.world.serve(&sc.terrain, sections, now);
let dt = t.elapsed();
times.push(dt);
if planning {
r.planning += dt;
r.planning_ticks += 1;
}
if !wanted.is_empty() {
r.ground_batches += 1;
r.ground_queries += wanted.len() as u64;
pending = wanted.into_iter().map(|(x, z)| (x, z, sc.terrain.height(x, z))).collect();
}
r.ticks_run = now;
// NaN check on everything with a position.
let bad = sim.drones.iter().filter(|d| !finite(d.pos) || !finite(d.vel)).count()
+ sim.missiles.iter().filter(|m| !finite(m.pos)).count()
+ sim.sams.iter().filter(|s| !finite(s.pos)).count()
+ sim.blasts.iter().filter(|b| !finite(b.at)).count()
+ out.models.iter().filter(|m| !finite(m.at)).count();
r.nan_positions += bad as u64;
// Trucks with a job or on the move, standing still.
for s in sim.sams.iter().filter(|s| s.kind == SamKind::Tzm && s.alive()) {
let busy = s.moving() || s.job().is_some();
r.truck_busy_ticks += u64::from(busy);
let e = standing.entry(s.id).or_insert((s.pos, 0));
if !busy || e.0.distance(s.pos) > 0.05 {
*e = (s.pos, 0);
} else {
e.1 += 1;
if e.1 == sc.stuck_after && !r.stuck_trucks.contains(&s.id) {
r.stuck_trucks.push(s.id);
}
}
}
// A raid has ended when it was ordered, planned, flown and nothing is left in the air.
let quiet = !sim.planning() && sim.queue.is_empty() && !sim.drones.iter().any(|d| d.alive()) && sim.missiles.is_empty();
for i in 0..sc.raids.len() {
if ordered[i] && !ended[i] && quiet && sim.launched > launched_before[i] {
ended[i] = true;
}
}
if sc.stop_at_end && ended.iter().all(|e| *e) && !sc.raids.is_empty() {
break;
}
}
r.launched = sim.launched;
r.kills = sim.kills;
r.hits = sim.hits;
r.fired = sim.sams.iter().map(|s| s.fired).sum();
r.raids_ordered = ordered.iter().filter(|o| **o).count();
r.world = ground.world.text();
r.crashes = sim.crashes;
r.raids_ended = ended.iter().filter(|o| **o).count();
r.all_ended = r.raids_ended == sc.raids.len();
r.tick_total = times.iter().sum();
times.sort();
if !times.is_empty() {
let at = |q: f64| times[((times.len() - 1) as f64 * q).round() as usize];
r.tick_p50 = at(0.5);
r.tick_p95 = at(0.95);
r.tick_max = *times.last().unwrap();
}
r
}
fn ms(d: Duration) -> f64 {
d.as_secs_f64() * 1000.0
}
impl Report {
/// Problems that fail a scenario: empty when it is healthy.
pub fn problems(&self) -> Vec<String> {
let mut p = Vec::new();
if self.nan_positions > 0 {
p.push(format!("{} non-finite positions", self.nan_positions));
}
if !self.stuck_trucks.is_empty() {
p.push(format!("stuck trucks: {:?}", self.stuck_trucks));
}
if !self.all_ended {
p.push(format!("{} of {} raids ended in {} ticks", self.raids_ended, self.raids_ended.max(self.raids_ordered), self.ticks_run));
}
p
}
pub fn text(&self) -> String {
format!(
"scenario {name} (seed {seed}): {units} units, {ticks} ticks ({secs:.0} s of game time)\n\
raids: {ro} ordered, {re} ended\n\
launched {l}, shot down {k}, got through {h}, air-defence missiles fired {f}\n\
tick time ms: p50 {p50:.3} p95 {p95:.3} max {max:.3} total {tot:.0}\n\
planning: {pl:.1} ms over {pt} ticks\n\
ground queries: {gq} in {gb} batches\n\
non-finite positions: {nan}\n\
stuck trucks: {stuck:?} (busy truck-ticks {busy})\n\
crashes into the land: {crashes}; {world}\n",
name = self.name,
seed = self.seed,
units = self.units,
ticks = self.ticks_run,
secs = self.ticks_run as f64 / 20.0,
ro = self.raids_ordered,
re = self.raids_ended,
l = self.launched,
k = self.kills,
h = self.hits,
f = self.fired,
p50 = ms(self.tick_p50),
p95 = ms(self.tick_p95),
max = ms(self.tick_max),
tot = ms(self.tick_total),
pl = ms(self.planning),
pt = self.planning_ticks,
gq = self.ground_queries,
gb = self.ground_batches,
nan = self.nan_positions,
stuck = self.stuck_trucks,
busy = self.truck_busy_ticks,
crashes = self.crashes,
world = self.world,
)
}
pub fn json(&self) -> String {
format!(
"{{\"scenario\":{:?},\"seed\":{},\"units\":{},\"ticks\":{},\"raids_ordered\":{},\"raids_ended\":{},\
\"launched\":{},\"kills\":{},\"hits\":{},\"fired\":{},\
\"tick_ms\":{{\"p50\":{:.4},\"p95\":{:.4},\"max\":{:.4},\"total\":{:.2}}},\
\"planning_ms\":{:.2},\"planning_ticks\":{},\"ground_queries\":{},\"ground_batches\":{},\
\"nan_positions\":{},\"stuck_trucks\":{:?},\"truck_busy_ticks\":{}}}",
self.name,
self.seed,
self.units,
self.ticks_run,
self.raids_ordered,
self.raids_ended,
self.launched,
self.kills,
self.hits,
self.fired,
ms(self.tick_p50),
ms(self.tick_p95),
ms(self.tick_max),
ms(self.tick_total),
ms(self.planning),
self.planning_ticks,
self.ground_queries,
self.ground_batches,
self.nan_positions,
self.stuck_trucks,
self.truck_busy_ticks,
)
}
}
// ------------------------------------------------------------ ready-made scenarios
fn ring(sc: &mut Scenario, kinds: &[SamKind], count: usize, r0: f64, r1: f64) {
for i in 0..count {
let a = i as f64 * 2.399_963; // golden angle: an even spread without randomness
let r = r0 + (r1 - r0) * (i as f64 / count.max(1) as f64);
let kind = kinds[i % kinds.len()];
sc.unit(kind, a.sin() * r, a.cos() * r).heading = a;
}
}
/// A big smart raid on 61 systems.
pub fn big_smart() -> Scenario {
let mut sc = Scenario::new("big-smart");
sc.terrain = Terrain::Hills { base: 70, amp: 14.0, period: 90.0 };
for (x, z) in [(0.0, 0.0), (120.0, 80.0), (-150.0, -60.0)] {
sc.objects.push(DVec3::new(x, 70.0, z));
}
ring(&mut sc, &[SamKind::Radar], 6, 150.0, 500.0);
ring(&mut sc, &[SamKind::Ew], 2, 100.0, 300.0);
ring(&mut sc, &SamKind::ARMED, 53, 60.0, 450.0);
assert_eq!(sc.units.len(), 61);
sc.raids.push(RaidSpec {
kinds: vec![
(DroneKind::Gerbera, 8),
(DroneKind::Shahed, 12),
(DroneKind::Geran3, 4),
(DroneKind::Lancet, 4),
(DroneKind::Kh101, 4),
(DroneKind::Kalibr, 4),
(DroneKind::Iskander, 2),
],
approach: Approach::AllSides,
aim: Aim::Smart,
dist: 500.0,
at: 1,
});
sc.ticks = 20 * 60 * 8;
sc
}
/// Trucks and a depot supplying systems that start with empty magazines, under a raid.
pub fn logistics() -> Scenario {
let mut sc = Scenario::new("logistics");
sc.unit(SamKind::Radar, 0.0, -40.0);
sc.unit(SamKind::Depot, 300.0, 300.0);
for (x, z) in [(310.0, 320.0), (290.0, 330.0)] {
sc.unit(SamKind::Tzm, x, z);
}
sc.unit(SamKind::Tzm, 280.0, 280.0);
for (i, kind) in [SamKind::Patriot, SamKind::Tor, SamKind::Buk, SamKind::Nasams, SamKind::Pantsir, SamKind::S400].into_iter().enumerate() {
let a = i as f64 * 1.05;
let u = sc.unit(kind, a.sin() * 120.0, a.cos() * 120.0);
// Every other system starts out of missiles: the trucks have work from the first second.
u.empty = i % 2 == 0;
}
sc.raids.push(RaidSpec {
kinds: vec![(DroneKind::Shahed, 16), (DroneKind::Gerbera, 8), (DroneKind::Kh101, 3)],
approach: Approach::TwoSectors,
aim: Aim::Objects,
dist: 400.0,
at: 1,
});
sc.ticks = 20 * 60 * 6;
sc
}
/// EW stations on and a smart enemy that adapts over three raids.
pub fn ew_adaptive() -> Scenario {
let mut sc = Scenario::new("ew-adaptive");
sc.ew = true;
sc.unit(SamKind::Radar, 0.0, 0.0);
sc.unit(SamKind::Radar, 200.0, -200.0);
sc.unit(SamKind::Ew, 80.0, 0.0);
sc.unit(SamKind::Ew, -80.0, 60.0);
for (i, kind) in [SamKind::Pantsir, SamKind::Gepard, SamKind::Tor, SamKind::Patriot, SamKind::Buk, SamKind::Gepard].into_iter().enumerate() {
let a = i as f64 * 1.05;
sc.unit(kind, a.sin() * 150.0, a.cos() * 150.0);
}
for at in [1, 1, 1] {
sc.raids.push(RaidSpec {
kinds: vec![(DroneKind::Gerbera, 6), (DroneKind::Shahed, 10), (DroneKind::Kh101, 4), (DroneKind::Kalibr, 3)],
approach: Approach::AllSides,
aim: Aim::Smart,
dist: 450.0,
at,
});
}
sc.ticks = 20 * 60 * 12;
sc
}
pub fn preset(name: &str) -> Option<Scenario> {
match name {
"big-smart" => Some(big_smart()),
"logistics" => Some(logistics()),
"ew-adaptive" => Some(ew_adaptive()),
_ => None,
}
}
pub const PRESETS: [&str; 3] = ["big-smart", "logistics", "ew-adaptive"];
+156
View File
@@ -0,0 +1,156 @@
//! Native headless runner for the simulation.
//!
//! cargo run --release --example sim_run -- [options]
//!
//! Options (all optional; a preset gives the defaults, the others override it):
//! --scenario NAME big-smart | logistics | ew-adaptive (default big-smart)
//! --seed N random seed
//! --layout FILE systems from a text file, replacing the preset's; one per line:
//! `KIND X Z [HEADING_RAD] [empty]` (`#` starts a comment)
//! KIND is a system key (patriot, tor, radar, depot, tzm, ew, ...)
//! --raid SPEC replaces the preset's raids; repeatable. SPEC is
//! `kind=count,kind=count[;dist=500][;aim=smart|objects|sams][;approach=sector|two|all|waves][;at=TICK]`
//! --terrain T flat[:HEIGHT] | hills[:BASE:AMP:PERIOD]
//! --ticks N tick limit (20 a second)
//! --run-on do not stop when the raids have ended
//! --ew / --no-ew EW stations on or off
//! --json one JSON object instead of text
//! --list list scenarios, system and drone kinds
//! Exit code 1 when the run has problems (NaN positions, stuck trucks, a raid that never ended).
//! Native timings are for comparing runs on one machine, not an estimate of WASM/GPU cost.
mod harness;
use airdef::sim::{Aim, Approach, DroneKind, SamKind};
use glam::DVec3;
use harness::*;
fn die(msg: &str) -> ! {
eprintln!("sim_run: {msg}");
std::process::exit(2);
}
fn parse_raid(spec: &str) -> RaidSpec {
let mut r = RaidSpec { kinds: Vec::new(), approach: Approach::Sector, aim: Aim::Smart, dist: 400.0, at: 1 };
for (i, part) in spec.split(';').enumerate() {
if i == 0 {
for kc in part.split(',').filter(|s| !s.is_empty()) {
let (k, c) = kc.split_once('=').unwrap_or_else(|| die(&format!("raid kind `{kc}` needs =count")));
let kind = DroneKind::from_key(k).unwrap_or_else(|| die(&format!("unknown drone kind `{k}` (see --list)")));
r.kinds.push((kind, c.parse().unwrap_or_else(|_| die(&format!("bad count `{c}`")))));
}
continue;
}
let (k, v) = part.split_once('=').unwrap_or_else(|| die(&format!("bad raid option `{part}`")));
match k {
"dist" => r.dist = v.parse().unwrap_or_else(|_| die("bad dist")),
"at" => r.at = v.parse().unwrap_or_else(|_| die("bad at")),
"aim" => {
r.aim = match v {
"smart" => Aim::Smart,
"objects" => Aim::Objects,
"sams" => Aim::ObjectsAndSams,
_ => die("aim: smart | objects | sams"),
}
}
"approach" => {
r.approach = match v {
"sector" => Approach::Sector,
"two" => Approach::TwoSectors,
"all" => Approach::AllSides,
"waves" => Approach::Waves,
_ => die("approach: sector | two | all | waves"),
}
}
_ => die(&format!("unknown raid option `{k}`")),
}
}
r
}
fn parse_terrain(t: &str) -> Terrain {
let n: Vec<&str> = t.split(':').collect();
let num = |i: usize, d: f64| n.get(i).map_or(d, |s| s.parse().unwrap_or_else(|_| die("bad terrain number")));
match n[0] {
"flat" => Terrain::Flat(num(1, 70.0) as i32),
"hills" => Terrain::Hills { base: num(1, 70.0) as i32, amp: num(2, 14.0), period: num(3, 90.0) },
_ => die("terrain: flat[:H] | hills[:BASE:AMP:PERIOD]"),
}
}
fn parse_layout(path: &str) -> Vec<Unit> {
let text = std::fs::read_to_string(path).unwrap_or_else(|e| die(&format!("{path}: {e}")));
let mut units = Vec::new();
for (n, line) in text.lines().enumerate() {
let line = line.split('#').next().unwrap_or("").trim();
if line.is_empty() {
continue;
}
let w: Vec<&str> = line.split_whitespace().collect();
let bad = || die(&format!("{path}:{}: expected `KIND X Z [HEADING] [empty]`", n + 1));
if w.len() < 3 {
bad();
}
let kind = SamKind::from_key(w[0]).unwrap_or_else(|| die(&format!("{path}:{}: unknown system `{}`", n + 1, w[0])));
let f = |s: &str| s.parse::<f64>().unwrap_or_else(|_| bad());
let heading = w.get(3).filter(|s| **s != "empty").map_or(0.0, |s| f(s));
units.push(Unit { kind, pos: DVec3::new(f(w[1]), 70.0, f(w[2])), heading, empty: w.contains(&"empty") });
}
units
}
fn main() {
let args: Vec<String> = std::env::args().skip(1).collect();
let mut name = "big-smart".to_string();
let mut i = 0;
// The scenario first, so the other options override it whatever their order.
while i < args.len() {
if args[i] == "--scenario" {
name = args.get(i + 1).cloned().unwrap_or_else(|| die("--scenario needs a name"));
}
i += 1;
}
let mut sc = preset(&name).unwrap_or_else(|| die(&format!("unknown scenario `{name}`; one of {PRESETS:?}")));
let (mut json, mut raids) = (false, Vec::new());
let mut it = args.iter();
while let Some(arg) = it.next() {
let mut value = || it.next().cloned().unwrap_or_else(|| die(&format!("{arg} needs a value")));
match arg.as_str() {
"--scenario" => {
value();
}
"--seed" => sc.seed = value().parse().unwrap_or_else(|_| die("bad seed")),
"--ticks" => sc.ticks = value().parse().unwrap_or_else(|_| die("bad ticks")),
"--layout" => sc.units = parse_layout(&value()),
"--raid" => raids.push(parse_raid(&value())),
"--terrain" => sc.terrain = parse_terrain(&value()),
"--run-on" => sc.stop_at_end = false,
"--ew" => sc.ew = true,
"--no-ew" => sc.ew = false,
"--json" => json = true,
"--list" => {
println!("scenarios: {PRESETS:?}");
println!("systems: {:?}", SamKind::ALL.iter().map(|k| k.key()).collect::<Vec<_>>());
println!("drones: {:?}", DroneKind::ALL.iter().map(|k| k.spec().key).collect::<Vec<_>>());
return;
}
"-h" | "--help" => {
println!("{}", include_str!("main.rs").lines().take_while(|l| l.starts_with("//")).map(|l| l.trim_start_matches("//!").trim_start_matches("//").to_string() + "\n").collect::<String>());
return;
}
other => die(&format!("unknown option `{other}` (try --help)")),
}
}
if !raids.is_empty() {
sc.raids = raids;
}
let r = run(&sc);
if json {
println!("{}", r.json());
} else {
print!("{}", r.text());
}
let problems = r.problems();
if !problems.is_empty() {
eprintln!("PROBLEMS: {}", problems.join("; "));
std::process::exit(1);
}
}
+71 -19
View File
@@ -59,6 +59,26 @@ fn screen_radius(world: vec3<f32>, radius: f32) -> f32 {
return length((b.xy - a.xy) * frame.resolution.xy);
}
// The clip-space position of a world point (what `project` starts from).
fn clip_of(world: vec3<f32>) -> vec4<f32> {
return frame.view_proj * vec4<f32>(world - frame.camera.xyz, 1.0);
}
// A clip position on the screen in uv.
fn clip_uv(c: vec4<f32>) -> vec2<f32> {
let ndc = c.xy / max(abs(c.w), 1e-5);
return vec2<f32>(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
}
// `screen_radius` for a point whose clip position is known (the projection is linear,
// so the offset to the right is added in clip space): saves two projections and a matrix
// product for every effect at every pixel.
fn screen_radius_clip(c0: vec4<f32>, radius: f32) -> f32 {
let right = frame.view_inverse[0].xyz;
let cb = c0 + frame.view_proj * vec4<f32>(right * radius, 0.0);
return length((clip_uv(cb) - clip_uv(c0)) * frame.resolution.xy);
}
fn now() -> f32 {
return frame.params[0].x + frame.time.y;
}
@@ -86,22 +106,27 @@ fn post(uv: vec2<f32>, pixel: vec2<f32>) -> vec4<f32> {
let b = frame.params[BLAST_BASE + 2 * i + 1];
let age = t_now - b.x;
if age < 0.0 { continue; }
// Past both windows (the ring's second, the haze over a hot ball) it bends nothing.
let heat = 1.0 - smoothstep(0.0, 0.5, age / b.y);
if age >= 24.0 && heat <= 0.0 { continue; }
let centre = a.xyz;
let p = project(centre);
if p.z <= 0.5 { continue; }
let to = px - p.xy * frame.resolution.xy;
let c0 = clip_of(centre);
if c0.w <= 0.5 { continue; }
let to = px - clip_uv(c0) * frame.resolution.xy;
let dist = length(to);
// The shock: a ring racing out, fading in a second.
if age < 24.0 {
let ring = screen_radius(centre, age * 3.2 * (0.6 + a.w * 0.1));
let ring = screen_radius_clip(c0, age * 3.2 * (0.6 + a.w * 0.1));
let width = 6.0 + ring * 0.08;
let x = (dist - ring) / width;
let strength = (1.0 - age / 24.0) * min(a.w / 4.0, 2.0) * 10.0;
offset += normalize(to + vec2<f32>(1e-4)) * exp(-x * x) * x * strength * frame.resolution.zw;
// exp(-x * x) * x is below 1e-8 past six widths.
if abs(x) < 6.0 {
let strength = (1.0 - age / 24.0) * min(a.w / 4.0, 2.0) * 10.0;
offset += normalize(to + vec2<f32>(1e-4)) * exp(-x * x) * x * strength * frame.resolution.zw;
}
}
// Heat haze over a hot fireball.
let heat = 1.0 - smoothstep(0.0, 0.5, age / b.y);
let r = screen_radius(centre, a.w * 2.0);
let r = screen_radius_clip(c0, a.w * 2.0);
if heat > 0.0 && dist < r {
let n = noise3(vec3<f32>(px * 0.04, t_now * 0.35)) - 0.5;
offset += vec2<f32>(n, -abs(n)) * heat * (1.0 - dist / r) * 6.0 * frame.resolution.zw;
@@ -115,7 +140,8 @@ fn post(uv: vec2<f32>, pixel: vec2<f32>) -> vec4<f32> {
// the pixel sees, much stronger at night.
let night = clamp(0.35 - frame.time.z * 1.8, 0.0, 1.0);
let strength = mix(0.35, 2.2, night);
if wall < 1.0e5 {
// Under the HD lighting (params[0].w) the engine lights the world from the same lights.
if wall < 1.0e5 && frame.params[0].w < 0.5 {
let p = dir * wall;
var light = vec3<f32>(0.0);
for (var i = 0; i < blasts; i++) {
@@ -148,8 +174,23 @@ fn post(uv: vec2<f32>, pixel: vec2<f32>) -> vec4<f32> {
let age = t_now - b.x;
if age < 0.0 { continue; }
let life = b.y;
// Burnt out: no density and no fire light.
if age >= life { continue; }
let k = clamp(age / life, 0.0, 1.0);
let size = a.w;
// The ball and its smoke stay inside 4.5 sizes of the blast (radius 2.28 sizes, risen
// 2.2 sizes): rays that miss that sphere cannot reach the volume. Only the
// pixels' own direction is needed, no volume maths.
let rel = a.xyz - frame.camera.xyz;
let reach = size * 4.5 + 0.1;
let cc = dot(rel, rel);
let along = dot(rel, dir);
if cc > reach * reach && (along < 0.0 || cc - along * along > reach * reach) {
// Out of reach of the volume; the fire's glow still counts.
let h0 = 1.0 - smoothstep(0.05, 0.55, k);
glow += vec3<f32>(1.0, 0.5, 0.15) * h0 * h0 * size * 0.05 / (1.0 + length(rel + vec3<f32>(0.0, k * k * size * 2.2, 0.0)) * 0.03);
continue;
}
let seed = vec3<f32>(a.x * 0.37, a.y * 0.11, a.z * 0.53);
// The ball swells fast, then rises and spreads as smoke.
let radius = size * (0.3 + 0.9 * (1.0 - exp(-age / 3.0))) * (1.0 + k * 0.9);
@@ -194,13 +235,20 @@ fn post(uv: vec2<f32>, pixel: vec2<f32>) -> vec4<f32> {
let b = frame.params[BLAST_BASE + 2 * i + 1];
let age = t_now - b.x;
if age < 0.0 || age > 10.0 { continue; }
let p = project(a.xyz);
if p.z <= 0.5 { continue; }
let visible = select(0.25, 1.0, scene_distance(p.xy) > p.z - a.w);
let c0 = clip_of(a.xyz);
if c0.w <= 0.5 { continue; }
let p = vec3<f32>(clip_uv(c0), c0.w);
// Ray distance on both sides: `p.z` is the depth along the view axis, which is shorter
// than the distance along the ray towards the screen edge.
let dist = length(a.xyz - frame.camera.xyz);
let visible = select(0.25, 1.0, scene_distance(p.xy) > dist - a.w);
let f = exp(-age * 0.7) * min(a.w / 4.0, 2.5) * visible;
let r = screen_radius(a.xyz, a.w * 2.5) + 20.0;
let r = screen_radius_clip(c0, a.w * 2.5) + 20.0;
let dd = length(px - p.xy * frame.resolution.xy) / r;
color += vec3<f32>(1.0, 0.85, 0.6) * f * (exp(-dd * dd * 3.0) * 2.5 + exp(-dd * 0.8) * 0.25);
// The bloom's tail is below 5e-4 (under 8 bits) past nine radii.
if dd < 9.0 {
color += vec3<f32>(1.0, 0.85, 0.6) * f * (exp(-dd * dd * 3.0) * 2.5 + exp(-dd * 0.8) * 0.25);
}
color += vec3<f32>(1.0, 0.8, 0.55) * f * 0.12 / (1.0 + p.z * 0.01);
}
@@ -208,11 +256,15 @@ fn post(uv: vec2<f32>, pixel: vec2<f32>) -> vec4<f32> {
for (var j = 0; j < lights; j++) {
let a = frame.params[LIGHT_BASE + 2 * j];
let b = frame.params[LIGHT_BASE + 2 * j + 1];
let p = project(a.xyz);
if p.z <= 0.3 { continue; }
if scene_distance(p.xy) < p.z - a.w * 1.5 { continue; }
let r = screen_radius(a.xyz, a.w) + 3.0;
let dd = length(px - p.xy * frame.resolution.xy) / r;
let c0 = clip_of(a.xyz);
if c0.w <= 0.3 { continue; }
let p = clip_uv(c0);
let r = screen_radius_clip(c0, a.w) + 3.0;
let dd = length(px - p * frame.resolution.xy) / r;
// Far from the glow (intensity at most 2): its tail is below 5e-4, under 8 bits.
// This is checked before the depth lookup, which most pixels then never make.
if dd > 6.0 { continue; }
if scene_distance(p) < length(a.xyz - frame.camera.xyz) - a.w * 1.5 { continue; }
let flicker = 0.85 + 0.15 * noise3(vec3<f32>(a.xyz * 0.7 + vec3<f32>(t_now * 1.3)));
color += b.rgb * b.w * flicker * (exp(-dd * dd * 4.0) * 1.2 + exp(-dd * 1.2) * 0.18);
}
+446 -71
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, ModelInstanceId, ParticleSpawn, PointLight},
storage,
ui::{self, ButtonWidget, ItemWidget, ListWidget, Screen, Widget},
};
@@ -16,11 +16,12 @@ mod monitor;
mod panel;
mod radar;
mod replay;
mod sim;
pub mod sim;
mod scenario;
mod wave;
use sim::{Class, DroneKind, Ground, Out, Sam, SamKind, Sim};
const ITEMS: [&str; 17] = [
const ITEMS: [&str; 29] = [
"airdef:designator",
"airdef:launch_shahed",
"airdef:launch_geran3",
@@ -37,6 +38,18 @@ const ITEMS: [&str; 17] = [
"airdef:deploy_pantsir",
"airdef:deploy_s400",
"airdef:deploy_s500",
"airdef:deploy_irist",
"airdef:deploy_nasams",
"airdef:deploy_buk",
"airdef:deploy_tor",
"airdef:deploy_p1sun",
"airdef:deploy_radar",
"airdef:deploy_depot",
"airdef:deploy_tzm",
"airdef:deploy_ew",
"airdef:deploy_light",
"airdef:mover",
"airdef:flare_gun",
"airdef:remover",
];
@@ -44,6 +57,9 @@ struct AirDef {
sim: Sim,
ground: Ground,
dirty: bool,
/// Wall-clock-free pacing of autosaves: the tick before which none is written (at most one a
/// second while guns fire, and a pause after a failed write).
next_save: u64,
hud_shown: bool,
/// The system the monitor shows (K cycles), none: hidden.
monitor: Option<usize>,
@@ -58,6 +74,10 @@ struct AirDef {
replay: Option<replay::Player>,
/// The world drawn beyond the render distance (replays and cameras see far off).
far: bool,
/// The unit the move remote picked (its id), waiting for where to go.
moving: Option<u32>,
/// The HD lighting preset (the engine's lights, bloom, tone mapping).
hd: bool,
/// The world whose state is loaded (`game.world-id`), once one is.
world: Option<Option<String>>,
/// The systems' and objects' health as last saved, to save again when it changes.
@@ -66,8 +86,23 @@ 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>,
/// The handles `render.model-id` gave the model names (valid under `model_generation`); none:
/// a name the game doesn't know.
model_ids: std::collections::HashMap<&'static str, Option<u32>>,
model_generation: u32,
/// The instances handed to the game, kept between ticks so nothing is allocated for them.
by_id: Vec<ModelInstanceId>,
}
/// 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 +233,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 +297,99 @@ 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);
}
// Fires burn on for minutes: a flickering, wide glow that dies down with the flames.
for f in &self.sim.fires.list {
let (fl, sm) = f.level(tick);
if f.quiet {
continue;
}
let glow = fl.max(0.12 * sm * (1.0 - (tick.saturating_sub(f.start)) as f32 / f.life as f32));
let id = 0x6200_0000 | (f.id & 0x00ff_ffff);
add(id, f.at + DVec3::Y * 1.2, (1.0, 0.5, 0.17), 70.0 * f.size * glow * flicker(f.id), 20.0 + 10.0 * f.size);
}
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 +407,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;
}
@@ -254,29 +421,47 @@ impl AirDef {
p
}
/// The models to the game by handle: each name crosses once, the buffer stays allocated.
fn send_models(&mut self, models: &[sim::Model]) {
let generation = render::model_generation();
if generation != self.model_generation {
// The game's registry changed (or this is the first tick): ask again.
self.model_generation = generation;
self.model_ids.clear();
}
self.by_id.clear();
for m in models {
let ids = &mut self.model_ids;
let handle = *ids.entry(m.name).or_insert_with(|| render::model_id(m.name));
// A name the game's registry doesn't have draws nothing, by name too.
let Some(model) = handle else { continue };
let r = m.rotation.as_quat();
self.by_id.push(ModelInstanceId {
id: m.id,
model,
x: m.at.x,
y: m.at.y,
z: m.at.z,
rotation: (r.x, r.y, r.z, r.w),
scale: m.scale,
glow: m.glow,
});
}
if !render::set_models_by_id(generation, &self.by_id) {
// The registry changed between the two calls: ask again next tick.
self.model_generation = u32::MAX;
}
}
fn emit(&mut self, out: Out) {
if self.sim.shader {
render::set_params(&self.shader_params(&out));
}
let models: Vec<ModelInstance> = out
.models
.into_iter()
.map(|m| {
let r = m.rotation.as_quat();
ModelInstance {
id: m.id,
model: m.name.to_string(),
x: m.at.x,
y: m.at.y,
z: m.at.z,
rotation: (r.x, r.y, r.z, r.w),
scale: m.scale,
glow: m.glow,
}
})
.collect();
if !models.is_empty() || self.sim.tick % 20 == 0 {
render::set_models(&models);
if self.hd {
render::set_lights(&self.lights(&out));
}
if !out.models.is_empty() || self.sim.tick % 20 == 0 {
self.send_models(&out.models);
}
if !out.particles.is_empty() {
let spawns: Vec<ParticleSpawn> = out
@@ -308,7 +493,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 +501,9 @@ impl AirDef {
}
let mut i = 0;
while i < self.pending.len() {
if self.pending[i].0 <= now {
let (_, name, at, pitch) = self.pending.swap_remove(i);
render::play_sound(&name, at.x, at.y, at.z, 1.0, pitch);
if self.pending[i].due <= now {
let s = self.pending.swap_remove(i);
render::play_sound(&s.name, s.at.x, s.at.y, s.at.z, s.volume, s.pitch);
} else {
i += 1;
}
@@ -352,24 +537,67 @@ impl AirDef {
// ------------------------------------------------ saving
fn save(&mut self) {
/// The state as text, lines of words: the autosave's format, and the body of a scenario.
/// `fresh`: every system and object whole, magazines full (a scenario to replay from the start).
fn state_text(&self, fresh: bool, view: bool) -> String {
let mut text = String::new();
for s in &self.sim.sams {
text += &format!("sam {} {} {} {} {} {} {}\n", s.kind.key(), s.pos.x, s.pos.y, s.pos.z, s.heading, s.roles, s.health);
// Then its ammunition (missiles, bursts, out and waiting for a truck, reload ticks
// left) and where it was driving, if anywhere.
let dest = s.dest.filter(|_| !fresh).map_or("- - -".to_string(), |d| format!("{} {} {}", d.x, d.y, d.z));
let new = Sam::new(0, s.kind, s.pos, s.heading);
let (health, cargo, missiles, bursts, empty, reloading) = if fresh {
(new.health, new.cargo, new.missiles, new.bursts, false, 0)
} else {
(s.health, s.cargo, s.missiles, s.bursts, s.empty, s.reloading)
};
// The parts' states (sensor, mount, launcher, gun, tracks, comms) close the line; older saves lack them.
let comps = if fresh { new.comps } else { s.comps };
let parts = comps.0.iter().map(|v| format!("{v:.3}")).collect::<Vec<_>>().join(" ");
text += &format!(
"sam {} {} {} {} {} {} {} {} {} {} {} {} {dest} {parts}\n",
s.kind.key(),
s.pos.x,
s.pos.y,
s.pos.z,
s.heading,
s.roles,
health,
cargo,
missiles,
bursts,
u8::from(empty),
reloading,
);
}
for (i, t) in self.sim.targets.iter().enumerate() {
let h = self.sim.target_health.get(i).copied().unwrap_or(100.0);
let h = if fresh { 100.0 } else { self.sim.target_health.get(i).copied().unwrap_or(100.0) };
text += &format!("target {} {} {} {h}\n", t.x, t.y, t.z);
}
text += &format!("damage {}\n", self.sim.damage);
text += &format!("far {}\n", self.far);
if view {
text += &format!("far {}\n", self.far);
text += &format!("effects {} {}\n", self.sim.shader, self.hd);
}
for (kind, roles) in &self.sim.roles {
text += &format!("roles {} {}\n", kind.key(), roles);
}
if let Err(e) = storage::set(&self.state_key(), text.as_bytes()) {
horizon_mod_sdk::warn(&format!("could not save: {e}"));
text
}
fn save(&mut self) {
let text = self.state_text(false, true);
// Clean only once written: a failed write is tried again after a pause.
match storage::set(&self.state_key(), text.as_bytes()) {
Ok(()) => {
self.dirty = false;
self.next_save = self.sim.tick + 20;
}
Err(e) => {
horizon_mod_sdk::warn(&format!("could not save: {e}"));
self.next_save = self.sim.tick + 200;
}
}
self.dirty = false;
}
fn load(&mut self) {
@@ -378,7 +606,12 @@ impl AirDef {
fn load_from(&mut self, key: &str) {
let Some(bytes) = storage::get(key) else { return };
let text = String::from_utf8_lossy(&bytes);
self.apply_state(&String::from_utf8_lossy(&bytes));
}
/// Adds the state in `text` (`state_text`'s lines; unknown lines are skipped, so older and
/// newer files read alike).
fn apply_state(&mut self, text: &str) {
for line in text.lines() {
let w: Vec<&str> = line.split_whitespace().collect();
let num = |i: usize| w.get(i).and_then(|v| v.parse::<f64>().ok());
@@ -395,7 +628,32 @@ 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,
};
// The parts: absent in older saves, which load with every part whole.
for (k, v) in sam.comps.0.iter_mut().enumerate() {
if let Some(c) = num(16 + k) {
*v = c.clamp(0.0, 1.0);
}
}
sam.fit_cells();
self.sim.sams.push(sam);
if let Some(to) = dest {
let i = self.sim.sams.len() - 1;
self.sim.order_move(i, to);
}
}
}
Some(&"target") => {
@@ -405,7 +663,19 @@ impl AirDef {
}
}
Some(&"damage") => self.sim.damage = w.get(1) != Some(&"false"),
// A scenario's: the random generator's state, and the wave menu's raid.
Some(&"seed") => {
if let Some(v) = w.get(1).and_then(|v| v.parse::<u64>().ok()) {
self.sim.rng = sim::Rng::new(v);
}
}
Some(&"wave") => self.wave.parse(&w[1..]),
Some(&"far") => self.far = w.get(1) != Some(&"false"),
Some(&"effects") => {
let shader = w.get(1) != Some(&"false");
let hd = w.get(2) == Some(&"true");
self.set_effects(shader, hd);
}
Some(&"roles") => {
if let (Some(kind), Some(r)) = (w.get(1).and_then(|k| SamKind::from_key(k)), w.get(2).and_then(|v| v.parse().ok())) {
self.sim.roles.insert(kind, r);
@@ -425,6 +695,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 +731,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 +777,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(())
@@ -495,6 +785,7 @@ impl AirDef {
fn stop_replay(&mut self) {
self.replay = None;
render::set_time_override(None);
input::claim_wheel(false);
render::set_hud(&[]);
render::set_views(&[]);
@@ -528,13 +819,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 +836,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 +869,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 +914,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 +980,7 @@ impl AirDef {
}
}
const HELP: &str = "/airdef menu | monitor | grid | replay [map|full|reel|stop] | raid | repair | import | target here | give | deploy <комплекс> | wave <тип|mix> [кол-во] [дальность] | clear | targets clear | damage on|off | shader on|off | hold on|off | roles [комплекс классы] | stats";
const HELP: &str = "/airdef menu | monitor | grid | replay [map|full|reel|stop] | scenario save|load|list|delete | why [номер] | raid | repair | import | ew on|off | flare | move <номер> | target here | give | deploy <комплекс> | wave <тип|mix> [кол-во] [дальность] | clear | targets clear | damage on|off | truth on|off | shader on|off|hd | hold on|off | roles [комплекс классы] | stats";
impl Mod for AirDef {
fn init() -> Result<(Self, Registration), String> {
@@ -693,6 +989,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,16 +998,22 @@ impl Mod for AirDef {
wave: wave::WaveConfig::default(),
replay: None,
far: true,
moving: None,
hd: false,
world: None,
health_mark: 0,
menu: None,
listener: DVec3::ZERO,
pending: Vec::new(),
model_ids: std::collections::HashMap::new(),
model_generation: u32::MAX,
by_id: Vec::new(),
};
let _ = input::register_key("menu", "Меню AirDef", "key.keyboard.n");
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 +1055,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 +1072,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);
}
@@ -778,13 +1082,38 @@ impl Mod for AirDef {
}
let now = self.sim.tick;
self.ground.answer(render::ground(), now);
// The blocks themselves (issues #42, #43): what changed, and the sections asked for last tick.
let changes = render::world_changes();
self.ground.world.changed(changes.epoch, changes.overflow, &changes.sections);
let sections = render::sections()
.into_iter()
.map(|s| sim::world::SectionAnswer {
sec: (s.x, s.y, s.z),
fill: match s.fill {
render::SectionFill::Unknown => sim::world::Fill::Unknown,
render::SectionFill::Empty => sim::world::Fill::Empty,
render::SectionFill::Full => sim::world::Fill::Full,
render::SectionFill::Mixed => match <[u8; 512]>::try_from(s.bits) {
Ok(bits) => sim::world::Fill::Mixed(Box::new(bits)),
Err(_) => sim::world::Fill::Unknown,
},
},
})
.collect();
self.ground.world.answer(sections, 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.day_time = game::get_clock().map(|c| c.day_time);
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() {
render::query_ground(&wanted);
}
let sections = self.ground.world.take_wanted(now);
if !sections.is_empty() {
render::query_sections(render::Policy::Collision, &sections);
}
if let Some(mut r) = self.replay.take() {
r.tick(&self.sim, self.listener, &mut out);
if r.done {
@@ -801,6 +1130,8 @@ impl Mod for AirDef {
let size = render::gui_size();
render::set_hud(&r.hud(&self.sim, size));
render::set_views(&r.views(size));
// The sky of the raid, not of now (None for a raid whose clock was unknown).
render::set_time_override(r.day_time(&self.sim));
self.monitor_shown = true;
} else if self.grid {
let stale = self.grid_ids.iter().any(|id| !self.sim.sams.iter().any(|s| s.id == *id));
@@ -829,19 +1160,26 @@ 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;
let parts = s.comps.0.iter().fold(0u64, |a, v| a.wrapping_mul(131).wrapping_add((v * 100.0) as u64));
((s.health * 1000.0) as u64).wrapping_mul(1_000_003) ^ place ^ ammo.rotate_left(7) ^ parts.rotate_left(13)
})
.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 +1188,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 +1286,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 +1345,8 @@ impl Mod for AirDef {
}
["deploy", kind] => {
// A system where the player stands, a few blocks ahead.
let kind = SamKind::from_key(kind).ok_or("Комплексы: patriot, gepard, pantsir, s400, s500")?;
let kind = SamKind::from_key(kind)
.ok_or("Техника: patriot, gepard, pantsir, s400, s500, irist, nasams, buk, tor, p1sun, radar, depot, tzm, ew, light")?;
let p = player().ok_or("Нет игрока")?;
let d = view(&p);
let flat = DVec3::new(d.x, 0.0, d.z).normalize_or(DVec3::NEG_Z);
@@ -1046,10 +1386,44 @@ impl Mod for AirDef {
}
None
}
["scenario", rest @ ..] => Some(self.scenario_command(rest)?),
["why", rest @ ..] => {
// The system n of the summary, else the monitored one.
let i = match rest.first() {
Some(n) => n.parse::<usize>().ok().and_then(|n| n.checked_sub(1)).ok_or("/airdef why <номер>")?,
None => self.monitor.ok_or("/airdef why <номер> (или откройте монитор K)")?,
};
if i >= self.sim.sams.len() {
return Err("Нет такой техники".into());
}
for c in self.sim.why(i) {
say(&format!("{} {}", if c.ok { "+" } else { "-" }, c.text));
}
None
}
["raid"] => {
self.launch_wave(&mut out)?;
None
}
["move", n] => {
// Unit n (as the summary numbers them) drives to where the player stands.
let p = player().ok_or("Нет игрока")?;
let i = n.parse::<usize>().ok().and_then(|n| n.checked_sub(1)).ok_or("/airdef move <номер>")?;
if self.sim.order_move(i, DVec3::new(p.x, p.y, p.z)) {
Some(format!("{} №{}: на марш", self.sim.sams[i].kind.name(), i + 1))
} else {
Some("Эта техника не может ехать".into())
}
}
["ew", v @ ("on" | "off")] => {
let (n, hot) = self.sim.set_ew(*v == "on");
Some(format!("РЭБ: включено станций {n}{}", if hot > 0 { format!(", остывают {hot}") } else { String::new() }))
}
["flare"] => {
let p = player().ok_or("Нет игрока")?;
self.sim.fire_flare(eye(&p) + view(&p), view(&p), &mut out);
None
}
["import"] => {
// The state the mod kept before worlds had their own: brought into this world.
let before = self.sim.sams.len();
@@ -1060,7 +1434,7 @@ impl Mod for AirDef {
["repair"] => {
let n = self.sim.repair();
self.dirty = true;
Some(format!("Отремонтировано комплексов: {n}; объекты восстановлены"))
Some(format!("Отремонтировано комплексов: {n} (корпус и все узлы: РЛС, привод, пусковая, орудие, ходовая, связь); объекты восстановлены"))
}
["clear"] => {
self.sim.clear();
@@ -1079,13 +1453,17 @@ impl Mod for AirDef {
self.dirty = true;
Some("Цели убраны".to_string())
}
["truth", v @ ("on" | "off")] => {
self.sim.truth = *v == "on";
Some(format!("Истинные данные на радаре: {} (боевую логику не меняют)", if self.sim.truth { "вкл" } else { "выкл" }))
}
["damage", v @ ("on" | "off")] => {
self.sim.damage = *v == "on";
self.dirty = true;
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 +1472,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,22 +1498,19 @@ 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!(
"В воздухе: {}, ракет: {}, ПВО: {}, сбито: {}, поражений: {}",
"В воздухе: {}, ракет: {}, ПВО: {}, сбито: {}, поражений: {}, разбились о препятствия: {}; {}",
self.sim.drones.iter().filter(|d| d.alive()).count(),
self.sim.missiles.len(),
self.sim.sams.len(),
self.sim.kills,
self.sim.hits
self.sim.hits,
self.sim.crashes,
self.ground.world.text()
)),
_ => return Err(HELP.to_string()),
};
+80 -19
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,9 +112,17 @@ 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();
let sweep = (tick as f32 * 0.12 + index as f32) % std::f32::consts::TAU;
for k in 0..6 {
// 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 };
// The beam is the radar's real one: targets are measured as it passes them.
let sweep = sam.beam() as f32;
let radar_up = sam.radar_up();
for k in 0..if radar_up { 6 } else { 0 } {
let a1 = sweep - k as f32 * 0.09;
let a0 = a1 - 0.09;
let p = |a: f32| (rx + a.sin() * rr, ry - a.cos() * rr);
@@ -112,14 +131,20 @@ fn cell(sim: &Sim, index: usize, x: f32, y: f32, w: f32, h: f32, s: &mut Vec<Sha
let target = sim.watched(sam);
let mut seen = 0;
// Only what its own radar holds.
for d in sim.drones.iter().filter(|d| d.alive() && sam.seen.contains(&d.id)) {
let rel = d.pos - sam.pos;
for (d, tr) in sim.drones.iter().filter(|d| d.alive()).filter_map(|d| sam.track(d.id).map(|t| (d, t))) {
// Where the system believes it is: the last measurement carried on; a forecast is drawn hollow.
let est = tr.est(tick);
if sim.truth {
let r = d.pos - sam.pos;
s.push(fill(rx + r.x as f32 * scale as f32 - 0.5, ry + r.z as f32 * scale as f32 - 0.5, 1.0, 1.0, 0xFFFFFFFF));
}
let rel = est - 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) {
@@ -129,10 +154,24 @@ fn cell(sim: &Sim, index: usize, x: f32, y: f32, w: f32, h: f32, s: &mut Vec<Sha
} else {
0xFF808080
};
s.push(fill(bx - 1.5, by - 1.5, 3.0, 3.0, color));
if tr.forecast(tick) {
s.push(fill(bx - 1.5, by - 1.5, 3.0, 1.0, color));
s.push(fill(bx - 1.5, by + 0.5, 3.0, 1.0, color));
s.push(fill(bx - 1.5, by - 0.5, 1.0, 1.0, color));
s.push(fill(bx + 0.5, by - 0.5, 1.0, 1.0, color));
} else {
s.push(fill(bx - 1.5, by - 1.5, 3.0, 3.0, color));
}
}
s.push(fill(rx - 1.5, ry - 1.5, 3.0, 3.0, 0xFF40A0FF));
s.push(text(fit(format!("РЛС {} · {}", distance(reach), seen), rr * 2.0 + 4.0), x + 4.0, y + h - 10.0, 0xFF80FF90));
let radar_text = if spec.sensor <= 0.0 {
format!("сеть · {seen}")
} else if radar_up {
format!("РЛС {} · {}", distance(reach), seen)
} else {
"РЛС ОТКАЗ".to_string()
};
s.push(text(fit(radar_text, rr * 2.0 + 4.0), x + 4.0, y + h - 10.0, if spec.sensor > 0.0 && !radar_up { 0xFFFF5040 } else { 0xFF80FF90 }));
// Its health under the radar: a bar, red when it is low.
if sam.alive() && sam.health < 1.0 {
@@ -167,6 +206,11 @@ fn cell(sim: &Sim, index: usize, x: f32, y: f32, w: f32, h: f32, s: &mut Vec<Sha
(false, false) => 0,
};
s.push(Shape::View(ViewShape { view: sam.id, rect: Rect { x: cx, y: cy, w: cw, h: ch }, mode }));
// Worn or failed parts, over the picture: orange, red when one has failed.
if !sam.comps.whole() {
let failed = crate::sim::Part::ALL.iter().any(|p| sam.part_state(*p) == crate::sim::PartState::Failed);
s.push(text(fit(sam.parts_text(), cw * 0.55), cx + 3.0, cy + 3.0, if failed { 0xFFFF4040 } else { 0xFFFFB030 }));
}
// Crosshair.
s.push(fill(cx + cw / 2.0 - 0.5, cy + 3.0, 1.0, ch * 0.18, 0x90FFFFFF));
s.push(fill(cx + cw / 2.0 - 0.5, cy + ch * 0.82 - 3.0, 1.0, ch * 0.18, 0x90FFFFFF));
@@ -180,15 +224,25 @@ 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;
// The system knows the target by its track: the forecast of the last measurement.
let track = sam.track(t.id);
let at = track.map_or(t.pos, |tr| tr.est(tick));
let rel = at - sam.pos;
let bearing = (heading_of(rel).to_degrees() + 180.0).rem_euclid(360.0);
if ch > 50.0 {
s.push(text(fit(t.kind.spec().name.into(), cw - 6.0), cx + 3.0, cy + ch - 20.0, 0xFFFFFFFF));
}
let mut info = format!("{} в{:.0} {:.0}км/ч", distance(rel.length()), t.pos.y - sam.pos.y, t.vel.length() * 72.0);
let mut info = format!("{} в{:.0} {:.0}км/ч", distance(rel.length()), at.y - sam.pos.y, t.vel.length() * 72.0);
if let Some(tr) = track {
if tr.forecast(tick) {
info += &format!(" прогноз {:.1}с", tr.age(tick) as f64 / 20.0);
} else if cw > 200.0 {
info += &format!(" {}", tr.ident().name());
}
}
if cw > 220.0 {
info += &format!(" П{bearing:03.0}");
}
@@ -213,6 +267,13 @@ fn cell(sim: &Sim, index: usize, x: f32, y: f32, w: f32, h: f32, s: &mut Vec<Sha
pub fn draw(sim: &Sim, index: usize) -> Vec<Shape> {
let mut s = Vec::new();
cell(sim, index, 4.0, 4.0, 300.0, 150.0, &mut s);
// Why it fires or doesn't: the conditions the simulation decides by, worked out now for this one.
let checks = sim.why(index);
s.push(fill(4.0, 156.0, 300.0, 4.0 + 10.0 * checks.len() as f32, 0xA0080C0A));
for (k, c) in checks.iter().enumerate() {
let color = if c.ok { 0xFF80FF90 } else { 0xFFFF9060 };
s.push(text(fit(format!("{} {}", if c.ok { "+" } else { "-" }, c.text), 294.0), 7.0, 158.0 + 10.0 * k as f32, color));
}
s
}
+20 -5
View File
@@ -10,6 +10,8 @@ const CYAN: u32 = 0x9FE0FF;
const GREY: u32 = 0xB0B0B0;
const YELLOW: u32 = 0xFFE070;
const GREEN: u32 = 0x80FF90;
/// The most lines the summary holds (the HUD's room).
const MAX_LINES: usize = 16;
/// Metres (a block is a metre) as a short distance.
pub fn distance(m: f64) -> String {
@@ -77,8 +79,20 @@ pub fn summary(sim: &Sim, air: &[&Drone], player: DVec3) -> Vec<Line> {
if !sim.missiles.is_empty() {
lines.push(line(format!("Перехватчиков в полёте: {}", sim.missiles.len()), YELLOW));
}
for sam in sim.sams.iter().take(8) {
let mut text = format!("{} [{}]", sam.status(), crate::sim::roles_text(sam.roles));
// The score is the last line and always shown: the unit list gets what
// room is left, and cuts its own tail.
let room = (MAX_LINES - 1).saturating_sub(lines.len());
let total = sim.sams.len();
let mut shown = total.min(8);
if shown + usize::from(total > 8) > room {
shown = room.saturating_sub(1);
}
for sam in sim.sams.iter().take(shown) {
let mut text = if sam.kind.armed() {
format!("{} [{}]", sam.status(), crate::sim::roles_text(sam.roles))
} else {
sam.status()
};
if let Some(t) = sam.gun_target.or(sam.target).and_then(|t| sim.drones.iter().find(|d| d.id == t)) {
text += &format!(" > {} {}", t.kind.spec().name, distance(t.pos.distance(sam.pos)));
}
@@ -87,15 +101,16 @@ pub fn summary(sim: &Sim, air: &[&Drone], player: DVec3) -> Vec<Line> {
}
lines.push(line(text, CYAN));
}
if sim.sams.len() > 8 {
lines.push(line(format!("…и ещё {} комплексов", sim.sams.len() - 8), CYAN));
if total > shown && room > 0 {
lines.push(line(format!("…и ещё {} комплексов", total - shown), CYAN));
}
// Room for the score whatever came before.
lines.truncate(MAX_LINES - 1);
let real = sim.launched.saturating_sub(0);
let rate = if real > 0 { sim.kills as f64 * 100.0 / real as f64 } else { 0.0 };
lines.push(line(
format!("Сбито {} из {} ({rate:.0}%) · прорвалось {}", sim.kills, sim.launched, sim.hits),
GREY,
));
lines.truncate(16);
lines
}

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