# Передача разработки AirDef Это журнал фактически сделанных изменений и ограничений проверки, а не список обещанных функций. Рабочая ветка `kimi-work`; issues не закрывать, публиковать комментарии с коммитом и проверками. Движок — отдельный `chexit/horizon-airdef`, ветка `airdef`, подключён точным gitlink `horizon/`. ## Текущее состояние: 4 ноября 2026 Последнее продолжение пяти задач: [CONTINUATION-2026-11-04.md](CONTINUATION-2026-11-04.md). Движок `57f8b2853c4443f7e3ff5701600cbb262b53118f`, API29. В этом файле ниже сохранён исторический журнал предыдущих серий; старые SHA и числа тестов не заменяют новую контрольную точку. След #91 продолжен: `catalog/specs.json` — единственная копия числовых строк дронов, ракет и комплексов; `python3 tools/gen_catalog.py` генерирует `specs_generated.rs` и `unit_specs_generated.rs`, `--check` (CI) валит расхождение. Числа не менялись; сверка с литералами подтвердила 1.6 как фактический голос P1-SUN (недостижимый `P1sun => 1.7` в старом match не в счёт); на это добавлена Rust-регрессия. Остаток: варианты комплексов и игровая приёмка. ### Продолжение #129: стадии холодного пуска и разгон (6 ноября 2026) Ветка `agent/issue-129-cold-launch`, кодовый коммит `4b013fb7f30dbaa87a266b27a31911937fc523b4` (документация, измерения и кадры — следующим коммитом этой же ветки). Игра/движок не менялись: pin `278f356aa604fa21c2d15d0081823bbe0f891df2`, API30. Push не выполнялся, issues не закрывались. **Контракт стадий.** У `MissileSpec` появились `eject` и `eject_drag`; `cold` — тики физического выброса, `boost` — тики departure. `guided(age) = age >= cold + boost`; основная ветка наведения, самонаведение и облёт рельефа больше не могут включиться до зажигания (раньше было `age >= boost`, что при большом `cold` включало наведение до мотора). Зажигание — по-прежнему `age > cold`; событие `MotorIgnition`, звук, факел и голос используют текущую позицию ракеты. `departure_ramp` растёт 0→1 за `boost`; темп поворота в departure — `turn·(0.2 + 0.8·ramp)`. Тяга ракетных профилей — `accel·(0.45 + 0.55·ramp)`, на guidance — полная. P1-SUN (`flame=0`) сохраняет полный разгон без ракетной струи. **Профили.** `M48n6`: `cold=20`, `eject=2.4`, `eject_drag=0.07` — 1,00 с; `M77n6`: `cold=24`, `eject=2.6`, `eject_drag=0.07` — 1,20 с; `IrisT`/`M9m338`: `cold=8`, `eject=1.8/1.6`, `eject_drag=0.1` — 0,40 с. Рельсовые профили (`cold=0`) получили только spool тяги; ранний поворот на рельсовом пуске был проверен и отменён: он уводил Pac3 в землю на холмах и валил `an_interceptor_over_rolling_land_does_not_hit_the_ground`. `speed`, `accel`, `turn`, `pk`, `fuse` и правила столкновений не менялись. **RM48U.** `EJECT_TICKS` 5 → 20. Выброс неосвещённый и замедляется; зажигание на `EJECT_TICKS`, факел/звук/голос с этого момента; насчитанное время полёта включает выброс; дым в записи начинается от точки зажигания, а не от горловины. Live, repeat и export читают одно и то же состояние `lit`/записанное `MotorIgnition`. **Проверки.** core release **206 + 8 сценариев**, 8 ignored; адаптер **67 + 1 boundary + 5 сценариев**; настоящий `.hmod` — **30 шагов** `wasm_contract.toml`; `gen_catalog.py --check` и `gen_models.py --check` без расхождений; `test_gen_assets.py` — 10 тестов; `git diff --check` чист. Новые регрессии: стадии по порядку и отсутствие powered-эффектов до зажигания; `guided` не раньше `cold` для всех профилей; непрерывность скорости/поворота и throttled первый powered тик; P1-SUN без факела; записанное зажигание через `cold` тиков и `missile_is_lit` по нему; RM48U без голоса/факела до зажигания и ровно один звук зажигания. **Баланс.** Сетка #120 (6 вариантов × 8 seed, 2208 целей на сторону, baseline `dc36d67`): сбито 1988 → 1939, прорывов 216 → 258, ракет 3461 → 3467; `near200` 318 → 311, `far1500` 338 → 334, `hills20` 334 → 319; `energy_lost` 0 → 0, `expired` 1 → 0, scrub 2 → 4. `SAMS=20 SIZE=12`, 12 seed: 203 → 201 сбитых, 1 → 3 прорыва, 355 → 360 ракет. Отдельные CSV: [сетка](measurements/issue-129-grid.csv), [рейды](measurements/issue-129-balance.csv). Это намеренное следствие более длинного старта; скрытого повышения `pk` или максимальной скорости нет. **Парная съёмка.** Один scratch-мир `ColdLaunch129`, одно сохранение, сценарий `pair129`, seed `965460218386186365`, один клиент/движок API30, один режиссёр (`/airdef replay export … best fps=30 size=1280x720 crf=16 sec=24`), Metal/offscreen. Сценарий: С-400 `1.5 72 8.5`, роли `31`, один Х-22 с 900; мод до — `dc36d67`, после — `4b013fb`. Оба MP4: h264 1280×720 30 fps + AAC 48 кГц; до — 403 кадра/13,433 с, после — 412/13,733 с. План «Пуск» стартует на кадре 94 в обоих; первое освещение центра (окно 100×120, порог 100) до — кадр 101 (0,233 с после старта, `cold=5` = 0,25 с), после — кадр 121 (0,900 с, `cold=20` = 1,00 с); к 124 кадру устойчиво ≈198. Это подтверждает сдвиг холодной паузы на ≈0,67 с между сборками. MP4 в `docs/media/cold-launch` перекодированы в CRF26 для хранения; исходные файлы, их SHA-256, ffprobe и логика измерения — в [issue-129-pair.json](measurements/issue-129-pair.json). **Ограничение:** у агента нет ввода изображений; проверены параметры, старт плана, яркость центра и парные кадры, но композицию и красоту кадров человеком ещё нужно оценить. Предпусковые кадры не пиксельно идентичны (живой прогон, движение камеры, анимация листвы); совпадают мир/сейв/сценарий/seed/метод камеры. Отдельный very-close статичный план и live-MP4 не сняты (`--offscreen` без окна). Это не полная визуальная приёмка. ### Продолжение #122 (второй налёт из UI) Точный лог Windows: `stopped in UI event (fuel budget 1000000000, remaining Some(0))`, backtrace `known_units ← order_raid ← launch_wave ← ui_select` — другой класс, чем тик `1f28f83`. Причина: `order_raid` пересчитывал снимок известных комплексов на каждом дроне, а сам снимок был квадратичен по журналу и группировке. Исправление: один снимок на приказ (+ O(журнал+группировка) через множества, `price_grid_on` переиспользует свой снимок); семантика целеуказания и выбора носителей не менялась, сверка с прежним предикатом в нативном тесте, `/stats` в WASM совпал побайтово. Лимиты не трогали, HD/запись не отключали, размер волны не ограничивали. Воспроизведение в настоящем WASM (измеренный fuel): до — второй приказ 111 дронов исчерпывает ровно 1e9 в событии (failures=1), тот же приказ 11 дронов — 247805865; после — тот же сценарий целиком: max command 32946177/1e9, tick 236581288/300M, ошибок 0. Диагностический гость (`trigger.patch` вызывает обработчик `ui_select(BTN_LAUNCH)`) и точный сценарий: [issue-122-ui-fuel](measurements/issue-122-ui-fuel/summary.json). Воспроизвести удалось только с разнесённой группировкой: стек в одной точке гибнет от одной боеголовки (`known=0`, приказ дёшев) — у пользователя силы дожили до второго приказа. Закреплённые регрессии: нативные «эквивалентность снимка» и «второй умный приказ с толстым журналом принят и запущен»; тяжёлый WASM fixture расширен двумя умными приказами через `airdef preset` + `airdef raid` (функция кнопки ПУСК) — 4 принятых приказа, SHA fixture обновлён; лёгкий контракт 30 шагов с `preset` и отказом пустой волны. Новая команда `airdef preset ` — script-эквивалент кнопки готового состава. Последний общий прогон: core release **195 + 8 сценариев** / 7 ignored; адаптер **65 + 1 boundary + 5 сценариев**; лёгкий WASM **30 шагов**; тяжёлый WASM **283 шага** (tick max 182413686/300M, command max 6939810/1e9, failures=0), отчёт [flat-wasm-raid-budget](measurements/flat-wasm-raid-budget.json); 6 тестов отчёта budget и build-тест прошли. Ни одно issue не закрыто; пуш не выполнялся. ### Контрольная точка 3 ноября Код предыдущих четырёх задач интегрирован до `6e2d661`. Продолжение #102/обратного вывоза опубликовано до `aab3e78`; последнее продолжение расследования налётов — `2932364`. Текущий движок — `f95f03c08ec2916a0e5bd3b2d4a2a790e7937b47`, API 27. README описывает доступное поведение; `IMPLEMENTATION-STATUS.md` отделяет реализацию от приёмки. Ни одно issue не закрыто. Участники: Codex (интеграция, #119, ревью, документация) и три GPT-6.1 Terra (энергетика, типизированная доставка, излучение). Новые числа — только игровые. | Issue | Опубликованные контрольные коммиты | |---|---| | #119 | `c96676f`: укрытия, read-only geometry, четыре проверки | | #118 | `c775f31`, `952a10f`: доставка ТЗМ, возврат снятой ракеты ПВО, реальное движение и save/restore | | #121 | `26b258f`, `0df225a`, `88f9405`, `9e1e0f5`, `b377a5d`, `ce76f20`: излучение, UI/save/replay, наблюдения, last-known SEAD | | #120 | `143d9b2` → `e38ca0d`, затем `a7b8cb9`: конечное горение, coast, swept impact, диагностика, звук, исправленное ветвление фаз | | WASM | `6e2d661`: проверка On/Off и запрета управления отсутствующим радаром, 24 шага | Для обновления существующей установки сначала `git switch kimi-work`, затем `git pull --ff-only`, `python3 tools/airdef.py all`, `python3 tools/airdef.py play`. `all` обновляет точный сабмодуль, собирает игру/мод и устанавливает пакет. На Windows обычно используется `python` вместо `python3`. ### Продолжение #102 и обратного вывоза RM48U - `d3378b3`: `fork()` синхронизирует URL из `.gitmodules` даже на fast path совпавшего pin. Реальная Git-регрессия воспроизводит старый upstream URL и новый коммит, отсутствующий в локальном clone. Это исправляет наблюдавшееся на Windows `upload-pack: not our ref 86b1014…`; пользователь подтвердил ручное исправление через `git submodule sync --recursive` до публикации автоматизации. - `bb931ab`, `e9fbaba`, `b0085cb`: `ground-unload` вдали от склада резервирует одну RM48U и вызывает ТЗМ. `ground-cancel` отменяет ещё ожидающий вывоз/доставку; после забора груз продолжает путь на склад. Проверены физическое движение и полный save/restore до забора и после забора, без телепортации запаса. - `910f02c`: потеря зарезервированной ракеты вместе с ячейками сбрасывает заявку на вывоз. Иначе можно было создать сохранение, которое loader сам отвергал как несовместимое состояние. - Двигатель: `f4c1131d3`, затем `c89c78c47` опубликованы в `horizon-airdef:airdef` до обновления gitlink. API27 добавляет acoustics-status и capture-owner-status, scoped к запуску мода и поколению мира. Остановка capture проверяет владельца; смена мира снимает незапущенный запрос или посылает cancel. - `910f02c`: команда `/airdef capabilities`; дальний звуковой fallback сохраняется до подготовки акустического пути, экспорт принимает только своё подтверждённое состояние; Arming ждёт до 600 тиков. `prepared` — milestone хоста, не GPU fence, не результат каждой акустической трассы и не готовый MP4. - `4dddea2`, `8d5d0d1`: opt-in `test-budgets`, отдельный ручной workflow и JSON provenance; тесты ошибок сборки, stale identity и ненулевого runtime exit. Native success не заменяет actual WASM. - `aab3e78`: repo-local packer использует target-cache закреплённого движка; `CARGO_TARGET_DIR` не переопределяется для сборки самого мода и не уводит `.hmod` в чужое дерево. Проверки объединённого состояния: core release **190 + 4 сценария**, 7 ignored; адаптер **60 + 1 boundary + 4 сценария**; лёгкий WASM **26 шагов**, тяжёлый **69 шагов**; Git-регрессия и три теста failure report прошли. Исходный профиль запрашивал 64 ПВО + 160 целей, 602 tick / 300 frame; JSON фиксирует clean `aab3e78`, pin API27 и совпадающий build identity. Полные данные: [budget report](measurements/api27-wasm-budget.json). **Исправление оценки покрытия:** второй запрос исходного 69-шагового сценария отклонялся во время подготовки первого. Его успешный exit не подтверждает 160 запущенных или одновременно летящих целей. В архивный JSON добавлена поправка; исправленный сценарий проверяет ответы. Для настоящего stress также нужны ответы render-world: прежний sandbox их не давал и часто проверял лишь ожидание земли. Дополнительно два свежих профиля игры на Metal/no-audio: smoke off не ready, HD smoke ready; при HORIZON_ACOUSTICS=1 + HORIZON_NO_AUDIO=1 акустика enabled/request=true, prepared=false, awaiting-backend. Выход через quit подтверждён. Это не прослушивание, не benchmark и не новый MP4. Правила API, пределы проверок и команды: [ENGINE.md](ENGINE.md). ## Двойные налёты и плотность следов Перед продолжением читать [RAID-STABILITY.md](RAID-STABILITY.md): два соответствующих класса отключения теперь воспроизведены и исправлены — тик (`1f28f83`) и событие UI на втором приказе (#122, `order_raid`/known_units); приёмка конкретного Windows-сеанса остаётся. Текущий исправленный WASM stress проверяет две принятые волны, обслуживаемую плоскую геометрию и 184 воздушные цели в конкретный момент; новый [JSON](measurements/flat-wasm-raid-budget.json) заменяет прежнее недостаточное доказательство покрытия. Лимиты fuel/memory не увеличивались. `86e1d272e` в движке улучшает callback/fuel/error-chain diagnostics; `ed7615d36` добавляет только необязательную flat-world среду тестов и отказывает неожиданным ошибкам команд в Script. Основная клиентская логика сцены этим test helper не заменяется. `692ae77`: HD-треки ракет оптически плотнее на 12% через общий `rocket()` и cruise style. SAM/RM48U, крылатые/сверхзвуковые/баллистические треки покрыты; сегментов, радиусов и времени не добавлено. Пожары и дымка винтовых дронов не менялись. Обычные ванильные fallback-частицы этим множителем не меняются. Пять существующих smoke-тестов прошли; прирост FPS и приёмка новой плотности на Windows не заявлены. Последний общий прогон: core release190 + 6 сценариев, 7 ignored; адаптер60 + 1 boundary + 4 сценария; лёгкий WASM26; тяжёлый WASM271; 5 тестов отчёта. Нативные overlap-тесты — `airdef-core/tests/scenarios.rs`: два стандартных массовых пресета и два налёта с разделением «Орешников», оба со встроенной записью. Продолжение по пользовательскому HD-отключению: `1f28f83` заменяет полный `known.retain` при каждом взрыве на удаление прежнего квадрата; семантическая эквивалентность проверена. В диагностическом WASM max-menu ×2 раньше был trap tick1835 на 300M; после фикса полный 3602-tick прогон прошёл, max135273690. Гость отличается от baseline только временным trigger hook состава меню и исправлением. Это измерение fuel, не прирост FPS. Артефакты/ограничения — в RAID-STABILITY. `cc48302` добавляет публичный native max-menu regression; `3b470dd` закрепляет опциональный engine `test --fuel-profile` (не новый SDK API и не увеличение лимитов). `cbd6067` исправляет сообщение о пустом соседнем складе, показывает типизированные запасы на M, уточняет подсказку разведки и убирает устаревшую строку README о якобы отсутствующей доставке. [GROUND-STRIKES.md](GROUND-STRIKES.md) — инструкция с различием номеров своего С-400/склада и вражеской ПУ. Default RM48U=0; `ground-stock` задаёт остаток, `ground-load` грузит одну; поздний трек не даёт fix. `ab737e7`: actual WASM теперь 27 шагов с проверкой ошибки пустого соседнего склада. После фикса полный core: 192 passed + 6 сценариев / 7 ignored; новый max-menu сценарий отдельно прошёл седьмым. Адаптер60 + 1 boundary + 4 сценария; light WASM27; engine /spin fuel-profile test прошёл. ## Исходная контрольная точка этой серии Исходное состояние этой серии: `2689d93`; движок закреплён на `86b1014a9218e68041444cbf52db02a85107c815`, уровень API 26. Рабочее дерево было чистым, коммиты опубликованы. Текущий точный pin всегда смотреть в `git ls-files --stage horizon`, а происхождение собранного пакета — в `airdef-mod/target/horizon-mod/build-identity.json`. Четыре задачи были заказаны владельцем отдельно: | Issue | Заказанный остаток | |---|---| | #120 | Конечная работа двигателя перехватчика, coast, энергия манёвра, согласованные эффекты | | #118 | Типизированная доставка RM48U через ТЗМ; не создавать неподтверждённые носители | | #121 | Переключение излучения, сохранение разрешённого внешнего целеуказания, разведка излучателей | | #119 | Ослабление непрямого урона известным укрытием, ограниченная политика Unknown | Исполнители работали в отдельных worktree; их проверенные коммиты перенесены в `kimi-work`. В ветках `codex/issue-*` сохранены промежуточные версии; продолжать нужно от `kimi-work`, где разрешены конфликты общего формата сохранений и сигнатур боевых функций. ## Что уже сделано в предыдущей серии Codex - `airdef-core` компилирует исходники `airdef-mod/src/sim/mod.rs` через `#[path]`. Это единый источник симуляции, но разделение домена и представления ещё не завершено (#87). - Исправлены остаточные терминальные коллизии ответных ударов и скачок после world-query (#106/#117). Восстановление ответов мира не даёт права телепортироваться к плановой точке. - Доработаны запись ответных ударов, launch one-shot и live-след (#109), lifecycle показа и часы отложенного звука (#88/#89). Полная унификация всех потребителей сцены ещё не закончена. - Движок вынесен в отдельную репу; сборщик берётся из закреплённого форка, а не случайного PATH. Есть build identity, настоящий WASM contract и handshake доступности дыма API26 (#102/#104). - Обновлён upstream Horizon 0.2.97, исправления очереди/инвалидации акустики сохранены. Измерительные fixtures, notices и лицензии добавлены; прослушивание и массовый CPU-тест ещё нужны (#96). - Pipeline поддерживает authored overrides, manifest и экспорт итоговых габаритов в `sim/generated_bounds.rs`. Навигация учитывает footprint. Generated output вручную не редактировать. - Улучшены модели наземной техники; степень сходства и материалы ещё промежуточные (#115). Галерея: `MODEL-PILOTS.md`, `VEHICLE-MODELS.md`; это не приёмка всех летательных аппаратов. - `966b52f`: раздельные ПУ С-400/С-500, роли Radar Search/Engagement/Height/Multi. Чужая подсказка разрешает огневому радару уточнение, но не подменяет собственное измерение. - `966b52f`: резерв конкретной ячейки, отдельные крышки ТПК, выброс до включения двигателя. RM48U — условный игровой профиль С-400; AA и ground rounds делят четыре физические ячейки. Боеприпас списывается при выбросе, не при выдаче отложенного приказа; отмена не расходует его. - Конфигурация, маска использованных ячеек и положение крышек включены в save/replay. Smoke начинается в точке зажигания. С-500 ограничен двумя ракетами по числу ТПК; старое сохранение с четырьмя загруженными ракетами при загрузке ограничивается двумя. - `2689d93`: документированы источники архитектуры батареи, команды, скриншоты и границы визуальной проверки. ## Проверки и воспроизведение На `966b52f`: 169 тестов ядра + 4 сценария прошли, 7 ignored; адаптер — 53 теста, 1 boundary и 4 сценария прошли; WASM contract — 21 шаг. Это исходная контрольная точка, не результат новых четырёх задач. На `2689d93` был собран чистый пакет (`airdef_dirty=false`). На итоговом объединённом коде `6e2d661` прошли: - `airdef-core --release`: **187 passed, 7 ignored**, плюс **4 сценария**. - `airdef-mod`: **56 passed**, **1 boundary**, **4 сценария**. - Настоящий WASM-пакет: **24 шага**, включая команды излучения и typed ammunition. - `gen_models.py --check`: различий нет; в этой серии геометрия ассетов не менялась. Полный core здесь запускался с `--release`; это не прогон всех семи ignored-тестов. Отдельно выполнен сравнительный ignored-тест баланса (см. ниже). ```sh cargo test --release --manifest-path airdef-core/Cargo.toml cargo test --manifest-path airdef-mod/Cargo.toml python3 tools/airdef.py test-wasm python3 tools/gen_models.py --check python3 tools/test_gen_assets.py git diff --check ``` На этой macOS cargo находится в `~/.cargo/bin/cargo`. GUI-проверки запускать с отдельным `HORIZON_HOME` и рабочим каталогом тестового мира; не использовать реальные сохранения владельца. Metal-проверки не подтверждают Windows/Linux, HDR всех платформ или прослушивание звука. Скриншоты крышек подтверждают только их позу: полного видео выброса/зажигания пока нет. ## Технические детали новой серии ### #120: энергетика и единственный исход перехвата `MissileKind::burn_ticks` задаёт конечное время горения: простой профиль равен 2/3 игрового `spec.life`; для электрического P1-SUN используется весь срок жизни. Это не отдельная модель топлива и не реальные кривые тяги. `Missile::engine_lit` — общий источник состояния мотора. Три ветки скорости разделены явно: cold (`age <= cold`, множитель 0.97 с прежним минимумом 0.4), powered (`+accel`, ограничение `spec.speed`), coast (множитель 0.995 без холодного минимума). После выгорания доступный поворот масштабируется текущей скоростью; при падении ниже 25% игрового максимума перехват заканчивается диагностикой `MotorEnergyLost`. Срок жизни остаётся отдельным ограничением. Полноценной аэродинамики, гравитационной баллистики и расхода топлива по тяге нет. Swept fuse учитывает движение обоих объектов за тик; `Impact` получает точку closest approach и относительную скорость того же проверенного отрезка, включая удержание на Unknown. Геометрию не заменяет отдельный бонус «встречная/догонная». Один остаточный pk зависит от фактической скорости перехватчика и существующего состояния носителя/дальности. Повторный штраф по stealth и старый штраф по абсолютной скорости цели удалены из этого броска; обнаружение/сопровождение по-прежнему учитывают stealth в своих местах. `MotorBurnout` и `MotorEnergyLost` записываются в scene journal; tally отличает потерю энергии. Live и replay прекращают факел и новый дым по одному состоянию. `Out::engine_stops` немедленно снимает голос мотора на burnout, не ждёт обычной трёхтиковой grace пропавшего источника; при переходе на replay/export live-команды остановки отбрасываются вместе с live-выводом. Перемотка восстанавливает моторное состояние из записи, включая возврат до выгорания. При ревью был найден и исправлен недостижимый coast: ранний прототип проверял `!lit`, что перехватывало и cold, и coast. Не переносить один `143d9b2` без последующих исправлений. `a7b8cb9` содержит отдельный тест точных правил скорости всех трёх фаз. Сравнительная выборка **только изменения #120**, отдельно от #119/#118/#121: baseline `2689d93`, агентская итоговая ветка `508f9a0` (её исправление перенесено как `a7b8cb9`). 12 одинаковых seed, `SAMS=20 SIZE=12`: 204 цели в каждой версии; baseline — 202 сбитых, 2 прорыва, 367 ракет; новое — 203, 1, 355. Это небольшой регрессионный ориентир, не доказательство баланса всех типов/дальностей. Данные каждого seed: [CSV](measurements/issue-120-balance.csv). Расширенная сетка остатка #120 (ветка `agent/issue-120-balance`, коммит см. в git): baseline `2689d93` против текущего `e6a7000` (то есть с #118/#119/#121 и сменами движка, не только #120), 6 вариантов × 8 одинаковых seed, рейд `SIZE=36` (46 целей: Gerbera 10, Shahed 13, Geran3 5, Х-101 5, «Калибр» 4, Х-22 2, «Циркон» 1, «Искандер» 4, «Кинжаль» 2; «Орешник» harness исключает, как в `one_raid`): `batt10/20/50` (10/20/50 ЗРК, дистанция 500), `near200` (20 ЗРК, пуск в 200), `far1500` (20 ЗРК, пуск в 1500), `hills20` (20 ЗРК, холмы, 500). Кластер тот же `MIX` из 10 систем: `SAMS=10` — по одному комплексу каждого типа. Итого 2208 целей на сторону, 96 строк: [CSV](measurements/issue-120-grid.csv). Команда (игнорируемый тест `sim::balance::issue_120_grid`, только harness-поля в `Result`, поведение симуляции не менялось): ```sh SEEDS=1-8 cargo test --release --manifest-path airdef-core/Cargo.toml issue_120_grid -- --ignored --nocapture ``` Факты сетки (суммы по 8 seed каждого варианта, baseline → текущий): сбитые 297→301 (batt10), 331→334 (batt20), 357→363 (batt50), 313→318 (near200), 328→338 (far1500), 328→334 (hills20); прорывы 70→66, 37→34, 11→5, 53→48, 40→30, 39→33. Промахов по взрывателю заметно меньше (например far1500: 70→38). Расход ракет близок (516→504 … 597→583). На холмах потери у рельефа: разбито 14 и 14, самоуничтожений у хребта 6→2. Прорывы во всех вариантах — преимущественно баллистические и быстрые классы («Искандер», «Кинжаль», Х-22, «Циркон»); у медленных Shahed/Gerbera прорывов почти нет (0–2 на вариант), у ложных Gerbera — ноль ни разу. `MotorEnergyLost` не сработал ни в одном из 48 сценариев текущего кода, `expired` ≤ 1 на вариант: coast существует, но бои на этих дистанциях заканчиваются раньше, чем сопротивление 0.995/тик опустит скорость ниже порога 25% (и раньше выработки ресурса). Это измеренное свойство профиля, не подтверждённый дефект, поэтому параметры не менялись. Путь потери энергии покрыт юнит-тестом регресса (`regress.rs`), а не этой сеткой. ```sh SAMS=20 SIZE=12 cargo test --release --manifest-path airdef-core/Cargo.toml sim::balance::one_raid -- --ignored --nocapture ``` ### #118: один физический груз и восстанавливаемая заявка `ground_request` у С-400 — заявка на **одну** RM48U. Рядом со складом остаётся прямая передача; вне радиуса склада та же команда `ground-load` создаёт заявку, если есть запас и живая ТЗМ. ТЗМ забирает одну ракету из конечного запаса, едет к исправному развёрнутому носителю и передаёт её в общие ячейки. Если вытеснена ракета ПВО, ТЗМ везёт её обратно на склад. Generic reload не создаёт RM48U. Ударная заявка имеет приоритет перед generic reload. Поля `ground_stock` / `air_48n6_stock` на складе — остатки, на ТЗМ — физический груз двух типов. Заявка, груз и марш сохраняются; оперативный `job` реконструируется после загрузки. Передача запрещена во время движения, подготовки пуска, перезарядки, отказа ПУ и при отсутствии вместимости. Разрушение очищает заявку/груз; repair не восстанавливает уничтоженный typed stock. Проверены настоящее движение `Sim.step` по плоскому WorldSource, передача/возврат, истощение, прерывание, несовместимость, заполненные ячейки, unload и полный `state_text → apply_state` с продолжением маршрута. GUI-доставка на сложном рельефе ещё не проверена. Новых носителей вроде Бука не добавляли: один публичный игровой профиль не даёт основания приписывать его всем ЗРК. Обратный вывоз из удалённого носителя автоматизирован в продолжении выше. Новая команда `ground-cancel` отменяет ожидающую заявку, но не возвращает уже забранный груз в ПУ. ТЗМ вмещает два типизированных места RM48U/48Н6; прежние абстрактные generic reload-пакеты учитываются отдельно. Вывезенная ракета возвращается в конечный запас; ПВО восстанавливается при заборе только из реально имеющейся у ТЗМ ракеты 48Н6 и свободной ячейки. Резерв одной RM48U учитывается при выборе носителя удара. ### #121: излучение отдельно от сети `radar_on` — сохраняемая настройка, default=true для старых миров. Команда `/airdef radar <номер> on|off` и кнопка карты меняют её; у launcher-only нет своего активного радара. `radar_up` учитывает этот флаг. Off запрещает новые own measurements, но старые треки стареют обычно и разрешённый сетевой приём сохраняется. Это не выключение корпуса/связи/ГСН ракеты. Огневой радар, обязательный для отдельной ПУ, в Off не авторизует новые пуски. Условный наблюдатель-противник раз в 20 тиков обнаруживает работающие радары в 1600 блоках. Это упрощённый игровой датчик по расстоянию, без LOS/радиопеленгации и ошибки оценки позиции. Контакт хранится как последняя наблюдённая точка и стареет за 6000 тиков. Off не стирает контакт, не телепортирует его вслед за носителем и не предотвращает раскрытие пуском/РЭБ. В `raid::radar_strikes` удалён отдельный обход: раньше SEAD выбирал любую Radar и её настоящий `pos`, даже если общий planner уже работал по наблюдениям. Теперь SEAD берёт те же last-known контакты и их возраст. Явный sandbox-режим `Aim::ObjectsAndSams` по-прежнему выбирает расстановку напрямую; не путать его с умным противником. Проверки покрывают необнаруженную молчащую РЛС, смену позиции без наблюдения, повторное раскрытие, старение, relay freshness и seek. ### Совместимость текстового состояния Сохраняем строки `sam` расширением справа; индексы ниже включают слово `sam` как поле 0. | Индекс | Значение | |---|---| | 22–24 | ground_rounds, ground_stock, air_48n6_stock | | 25–27 | mode, radar_role, spent_cells | | 28 | ground_request: 0/1, только С-400 | | 29 | radar_on: true/false, отсутствие означает true | | 30 | ground_return: true/false, резерв вывоза одной RM48U из С-400 | | 31 | return_cargo: true/false, возвращаемая RM48U на ТЗМ | Две агентские ветки сначала заняли поле 28 одновременно; при интеграции излучение перенесено в 29, опубликованная совместимость заявки сохранена. Проверка полного roundtrip одновременно содержит ожидающую доставку и выключенный радар. Новые значения входят в durable mark; настройка излучения также в `SamKey` и ghost replay. Не переиспользовать эти позиции для нового поля. ### #119: непрямой взрыв и укрытия `WorldCache::blast_cover` читает текущую occupancy без запросов мира и без загрузки чанков. Три луча к репрезентативным точкам корпуса дают общий множитель для корпуса и узлов; для отмеченного объекта считается своя тройка. Известное укрытие оставляет 15% игрового воздействия по перекрытому лучу, открытый — 100%; края/проёмы дают промежуточное значение. Все лучи этого вызова читаются до отправки разрушения в движок. Исходный воксель пропускается, чтобы контакт на поверхности стены не закрывал взрыв сразу во все стороны. Unknown, stale и слишком старые секции считаются **неразрешёнными**. Ожидание равно нулю: для них сохраняется прежний неослабленный урон как консервативная игровая политика, а не утверждение, что там воздух. После ответа мира этот взрыв не пересчитывается. Нет очереди отложенного урона и повторного применения на world reply. Частичные блоки на этом этапе считаются занятым кубом; тонкая collision shape для blast отдельно не запрашивается. Прямой вооружённый контакт не ослабляется этим множителем. Проверки `sim::damage::cover_tests`: стена/проём для корпуса, узлов и объекта; Unknown и поздний ответ без повторного урона/запросов; прямое попадание; устаревшая геометрия; пропуск исходного блока. Все четыре прошли в объединённом наборе. ### #127: устойчивая очередь маршрутов после массового перенацеливания Реализована только узкая часть issue: `sim::ew::RouteJob` — задание с ID дрона, новой целью, ID цели-юнита, версией знаний и общим для тика снимком `known_units` (`Arc`). `adapt` по-прежнему выбирает новые цели раз в 40 тиков, но маршруты теперь достраивает `replan_retargets` **каждый тик**: одно задание или несколько, пока не исчерпан `WORK_PER_TICK`. Незавершённое задание остаётся в `Sim::retargets` (FIFO) и не теряется; повторный выбор того же дрона заменяет задание на месте, поэтому очередь не растёт. Старый маршрут не очищается при перенацеливании: дрон следует готовому безопасному участку, пока новое задание не получит маршрут. Если задание отменяется (дрон погиб, цель изменена снова, юнит уничтожен), маршрут очищается как fallback — дрон летит прямо до следующего адаптивного тика. Lancet, баллистика и decoy, как и раньше, летят прямо: для них задание не создаётся. В полёте следующая известная цель берётся только если до неё есть разворот; цель сзади внутри радиуса разворота пропускается, и дрон завершает текущий заход, а не получает невыполнимый маршрут. Повторный план использует снимок разведки исходного выбора цели и известный рельеф **той же версии задания**: `KnownRelief` (сетка и высоты) лежит в `Arc` внутри `RouteJob`, `Sim::known_relief` хранит тот же `Arc` только для новых заданий. Завершение другой умной волны заменяет глобальный указатель, но не снимок ожидающего job. Выборка рельефа, цены и кэш маршрутов в `replan_retargets` ключуются версией знаний целиком — adapt tick плюс указатели на снимки known units и рельефа, — а не только рельефом: два job с одинаковым рельефом, но разными known units, не делят price/route grid. За пределами известной сетки — Unknown, новая карта не раскрывается. Очередь этапа 1 была рантайм-состоянием планировщика: дроны и очередь запуска в `state_text` не входили, поэтому durable-очередью она не была. Этап 2 (ниже) сохраняет её вместе с дронами, очередью запуска, картиной и версиями рельефа. Регрессии этапа 1 остаются: `mass_retarget_finishes_its_route_jobs_over_the_budget` (40+ ретаргетов, прогресс каждый бюджетный тик, старый маршрут до нового, нет пустых маршрутов) и `mass_retarget_is_deterministic_and_never_duplicates_a_drone` (тот же seed — те же цели и маршруты; повторный `adapt` не создаёт дублей). ### #127, этап 1: честная EnemyPicture Журнал `intel.revealed: Vec<(id, место, тик)>` заменён картой контактов: `Intel.contacts: BTreeMap`, одна запись на юнита, последнее наблюдение побеждает. Контакт пишется только в момент раскрытия и читает истину только тогда: пуск (mod.rs/strike.rs) → `Seen::Launch`; фиксация излучения радаром рядом с чужим аппаратом (sight.rs, каждые 20 тиков, 1600 блоков) → `Seen::Emission` (отдельно помечается `emission` — вера в собственный сенсор опирается только на неё); работающая РЭБ (ew.rs) → `Seen::Jamming`; четвёртый канал — BSA: падение собственной боеголовки рядом с известным юнитом (`Sim::observe_outcomes`, радиус 16) фиксирует `destroyed` либо обновляет «жив» и место. Пока юнит незамечен, его перемещения, выключение радара, смена ролей/режима, расход БК, урон и гибель картину не меняют — это метаморфные тесты `sim::ew::picture_tests`. Из контактов решается всё: снимок `picture_of_defences()` на приказ и на адаптивный тик (newest first, при равном тике — больший id, как у старого reversed-журнала), цели `choose_target` и SEAD (`radar_strikes` — только Probable и свежее), ретаргет в полёте (цель «мертва» лишь по контакту с `destroyed`; забытого контакта дрон не замечает), живость задания очереди (`job_live` — по картине, не по истине). Уверенность — функция одного возраста: Confirmed ≤ 600, Probable ≤ 3000, Doubtful до TTL 6000 тиков (`Confidence`, диагностика и SEAD её используют, цены — нет: консервативно считаются все живые контакты). Модель возможностей в `price_grid_known` читает только картину: роли — published `kind.spec().roles` (смена `/airdef roles` невидима), дальности — `ew::published_reach` (без урона, фактора роли и мачт истины), донор — любой живой контакт вида Radar в link range от наблюдённого места (роль донора не наблюдаема: рабочий контакт РЛС предполагается кормящим сеть — это допущение, а не знание; разрыв связи/гибель донора незамеченными картину не меняют). Собственный сенсор верится, пока жива фиксация эмиссии; пуск при нуля эмиссионных фиксаций у С-400/С-500 читается как launcher-only: без донорского контакта такая ПУ в ценовой сетке «слепа», с ним бьёт на дальность донора. Горизонт РЛС игрока (`self.horizons`) из чужого планировщика убран: маскировка рельефа — тайна обороны, цены по ней консервативнее и объяснимее. Диагностика без GUI: `Sim::enemy_picture() -> EnemyPictureReport` (англ.): по каждому контакту — наблюдение (канал, место, возраст), уверенность, живость, наблюдённое уничтожение, вера в сенсор, наличие донора и текстовое «что планировщик из этого делает». Адаптация по-прежнему ограничена статистикой секторов (`best_bearing` по kills/hits): новых всеведущих данных и усиления характеристик нет. Форматы этапа 1 не менялись: intel оставался рантаймом (save хранил Layout, replay — траектории); durable-очередь появилась на этапе 2, ниже. Измерения до/после на одних seed: `docs/measurements/issue-127-enemy-picture-baseline.json` (0b645d77e) и `issue-127-enemy-picture-result.json` (эта ветка), bench — `cargo run --release --example intel_bench`. big-smart и logistics побайтово совпали с базой; ew-adaptive изменился (kills 62→59/61→59/61→56, hits 3→7/5→7/7→8, потери ЗРК 1→3/4→3/4→4) — фиксируется как факт перехода на честную картину, балансные параметры не трогали. Регрессии: метаморфные `hidden_changes_do_not_move_the_picture_or_its_decisions`, `a_new_observation_is_what_updates_the_picture`, `launcher_only_is_blind_in_the_picture_without_a_donor` (донор любой роли; разрыв связи наблюдённый/нет), `destruction_moves_the_picture_only_when_watched`, `confidence_is_a_function_of_age_alone`, `enemy_picture_report_describes_observation_and_belief`, `an_impact_watches_the_kill_and_updates_only_the_picture` (BSA end-to-end), обновлённые SEAD/#4/#21/#122 и обе регрессии очереди. Остаток #127 после этапа 1: durable save queue, structured intent, GUI. ### #127, этап 2: structured intent, durable queue и объяснимость **Намерение отделено от цели.** У дрона теперь два поля: `ew::Intent` — почему он летит туда — и `ew::Aim` — юнит, когда цель выбрана, сколько раз менялась и уверенность контакта. `Intent`: `Plan` (собственная цель плана: объект или юнит, выбранный при заказе), `Sead` (плановый удар по РЛС из `radar_strikes`), `React` (реакция на раскрытие: станция показала себя уже в воздухе) и `Replace` (картинка увидела гибель/забвение прежней цели). Случайный выбор цели (`choose_target`) больше не смешивается с реакцией: при заказе налёта intent фиксируется, а `adapt` помечает свои решения явно. Приоритет очереди — из намерения: Plan 3, Sead 2, React 1, Replace 0. **Throttle.** `SWITCH_HOLD = 160` тиков (8 с): свежее раскрытие может сменить цель только после выдержки; `SWITCH_CAP = 3` смены на дрон за жизнь; `ASSIGN_PER_TICK = 16` новых целей за адаптивный тик и `ASSIGN_PER_CONTACT = 6` на один контакт за тик — массовый ретаргет растягивается на следующие адаптивные тики, а не переписывает все маршруты сразу. Смена возможна, только если контакт показался позже текущей цели (`tick > aim.since`), он не Doubtful, и он либо достовернее текущего, либо ближе на `SWITCH_GAIN = 120` блоков: смена выбрасывает готовый маршрут и стоит планировщику нового, поэтому маргинальная не делается. Замена убитой цели выдержку не ждёт (цели уже нет), но упирается в тот же cap. В полёте, как и раньше, цель меняется только когда картинка её убила или забыла. **Явные игровые реакции без скрытых данных.** Реакции читают только поля контакта, записанные каналом наблюдения: выключенный радар остаётся в картине таким, каким его видели (эмиссия не фиксировалась — собственный сенсор не верится), разделённая батарея читается как пуск без эмиссии (launcher-only, без донора «слепа»), конечный БК наблюдается только как пуски (`Contact.launches`: счётчик увиденных пусков; остаток магазина в картину не входит никогда), энергия/нагрев РЭБ — своё состояние станции, а не раскрытие. Метаморфный тест `hidden_changes_do_not_move_the_picture_or_its_decisions` расширен: скрытые перемещение, off, роли, режим, БК, урон, гибель и нагрев/замок/выключение РЭБ не двигают ни картинку, ни цены, ни намерения/aims/очередь заданий. Повторное наблюдение двигает всё — как и должно. **Неизвестная земля не бесплатна.** `raid::Relief` получил поле `unknown`; у плана умной волны оно 0 (поведение этапа 1 сохранено), у повторного планирования задания — `UNKNOWN_LAND = 6` сверх базовой цены клетки 1. Задание, чья версия рельефа ничего не знает, планируется по полностью неизвестной земле с той же ценой, а не «как по свободной»: маршрут держится размеченной части, если она есть. Тест `unknown_land_is_not_free_in_a_replanned_route` сравнивает маршрут с ценой 0 и с ценой 6 на одной сетке: бесплатный срезает через неразмеченное, платный идёт по размеченному. **Durable queue: формат save.** `state_text` (автосейв) дописывает версионированную секцию (`sim::durable`, формат `air 1`): ``` air 1 <такт> <номер следующего задания> <дронов> <в очереди> <заданий> <контактов> <версий рельефа> airintel <12 kills> <12 hits> airglobal <версия рельефа|-> airrelief <номер> <клетка> <сторона> | <высота>x ...> aircontact <тип> <такт наблюдения> <канал> <эмиссия|-> <гибель|-> <пусков> airdrone <тип> ... <полёт> <намерение> <юнит> <смены> <уверенность> ... <маршрут: n x y z ...|-> airqueue <такт запуска> <то же тело дрона> airjob <дрон> <юнит> <версия> <приоритет> <намерение> <рельеф|-> ``` Секция пишется только автосейвом: сценарий — это стартовое состояние, его тело её не несёт (`scenario_text` → `state_text_with(..., air = false)`). Старые saves без секции читаются как прежде (пустое небо); старые сборки новые saves читают как Layout, игнорируя `air*` (Layout пропускает неизвестные строки). Всё проверяется до применения: битая строка отказывает save целиком с номером строки и полем, счётчики в заголовке сверяются с числом строк (обрезанный файл отвергается), чужая строка внутри секции — тоже. Границы: 512 дронов, 512 точек маршрута, 4096 контактов, 8 версий рельефа, 40 000 клеток; координаты пишутся до 0.01 блока (полёт этого не замечает), рельеф — RLE целых высот. Секция дополнительно ограничена 900 КБ (хост разрешает 1 МиБ на одну запись): дрон, который не влез, не пишется, и его задание тоже (восстановить его было бы нельзя). Измеренный размер секции в bench: 4.0–8.8 КБ для logistics/ew-adaptive и 61–62 КБ для big-smart (38 дронов в воздухе). **Что именно восстанавливается.** Дроны в воздухе (живые; падающие обломки — эффект, их нет), очередь запуска с тактами, задания (`id`, дрон, цель, юнит, версия, приоритет, намерение, версия рельефа), контакты картины (счётчики секторов, пуски), глобальный снимок рельефа и `job_next`. Картина задания восстанавливается «на свою версию»: контакты, наблюдённые не позже версии, плюс собственная цель задания (её последнее сохранённое наблюдение заменяет более позднее — иначе цель исчезла бы из своей же картины). Устаревшие задания отменяются при загрузке (`prune_air`): дрон исчез/погиб, цель уже другая, юнит наблюдённо уничтожен или забыт, дубль по дрону, версия старше `JOB_TTL = 1200` тиков (60 с). Маршрут отменённого «по устареванию/дублю» задания сохраняется (он ведёт к той же цели), отменённого из-за смены цели — очищается. `apply_state` сначала проверяет Layout и секцию, затем применяет обе; секция заменяет воздушное состояние, а не сливается с ним. `durable_mark` включает `air_mark` (число дронов/очереди/заданий/ контактов и `job_next`): автосейв берётся на запуске, потере, новом/завершённом задании и новом/забытом контакте, а не каждый тик полёта. Пока налёт *планируется*, периодический save откладывается (план не имеет durable-формы; поиск площадок ограничен `sites::SEARCH_TICKS = 400`); при смене мира пишется только Layout. Replay по-прежнему хранит траектории: секция пишется и читается только для живого `Sim`, записи не смешиваются с ней. Остаток и ограничения: ракеты-перехватчики в воздухе, пусковые площадки (`sites`), промежуточный план, статистика волны и состояние RNG не сохраняются — после загрузки перехваты начнутся заново, очередь стартует без площадки (свободный слот), отчёт волны не придёт, а случайные броски идут от нового генератора мира; сама воздушная обстановка (дроны, цели, задания, картина) восстанавливается точно. GUI поверх отчёта не делался. **Объяснимость.** `EnemyPictureReport` расширен без русского текста в core: у контакта — `launches`, `aimed_by` (сколько дронов на него нацелено) и `pending_jobs`; добавлены `aims` (дрон, юнит, intent, since, switches, confidence) и `pending` (id задания, дрон, юнит, точка, версия, приоритет, intent, `relief_known`, `reason` словами: ждёт бюджета / ждёт бюджета, земля неизвестна и стоит дороже / устарело), плюс `work_per_tick`. Тест `the_report_explains_aims_and_pending_jobs_in_english` проверяет и ASCII-only диагностику. **Измерения (baseline 8fc10c07a → эта ветка, `cargo run --release --example intel_bench`, seeds 1/7/42):** `docs/measurements/issue-127-intent-queue-baseline.json` и `issue-127-intent-queue-result.json`. big-smart: завершение у всех, launched 38, kills 38, hits 0, потери 0 — как в базе; ticks 1716→1676 (seed 1) и 1542/1433 без изменений, fired 49→60, 48→68, 49→71 (больше пусков ПВО). logistics: все поля совпали (27 запусков, 11/12/12 сбито, 8/7/8 прорвалось, 18 ракет, 0 потерь). ew-adaptive: kills 59→58, 59→58, 56→63; hits 7→7, 7→9, 8→3; потери ЗРК 3→3, 3→4, 4→2; ticks 8084→7146, 8193→7650, 7916→7243; fired 69→76, 81→98, 69→82. Это следствие throttle назначений и приоритетной очереди, а не подкрутки баланса: параметры ПВО/дронов не менялись, числа фиксируются как факт. В result-файле есть новые поля (`retargets_max`, `air_bytes`, `switches_max`, `intents`), у baseline их нет. **Регрессии этапа 2.** Core: `the_plan_records_sead_separately_from_its_objective`, `assignments_are_throttled_per_tick_and_per_contact`, `a_fresh_disclosure_switches_an_aim_only_after_the_hold`, `a_watched_kill_replaces_the_aim_and_queues_a_lower_priority_route`, `launched_rounds_are_observed_but_the_magazine_is_not`, `unknown_land_is_not_free_in_a_replanned_route`, `the_route_queue_is_bounded_and_refuses_a_worthless_job`, `the_same_seed_chose_the_same_intents_and_jobs`, `the_report_explains_aims_and_pending_jobs_in_english`, `the_air_state_round_trips_every_job_field`, `a_pending_route_continues_after_a_save_and_a_load` (save/load посреди незавершённой очереди: та же очередь, те же маршруты, обе кампании доводят задания, пустых маршрутов нет), `stale_ghost_and_duplicate_jobs_are_cancelled`, `a_job_for_a_unit_the_picture_lost_is_cancelled_on_load`, `a_damaged_air_section_is_refused_with_its_line_and_field`, `the_section_stays_inside_its_byte_bound` (600 дронов с длинными маршрутами: секция ≤ 900 КБ, записанные задания принадлежат записанным дронам), обновлённые массовые ретаргеты (40+ дронов: прогресс на бюджетном тике, throttle назначений, у дрона, стартовавшего без маршрута, маршрут появляется). Adapter: `the_air_section_of_a_save_is_written_and_read_by_the_adapter` (запись/чтение секции, замена не слияние, старый save без секции, сценарий без воздуха). ## Что следующему агенту проверить в первую очередь 1. На Windows: обновление сабмодуля через `all`, сохранение/перезаход, рабочий цикл игры. 2. В GUI: полный холодный пуск крупным планом; карточка On/Off; маршрут ТЗМ по сложной местности; разрушение стены одним взрывом и защита от того же взрыва по исходной геометрии. 3. Прослушать прекращение голоса двигателя при burnout и seek/экспорт вокруг этой границы. 4. Расширенная балансировка #120 (батареи/дальности/классы) записана в `measurements/issue-120-grid.csv`; остаются прослушивание голоса мотора и платформенная приёмка. 5. Продолжить оставшиеся архитектурные #87/#88/#89/#91/#102 и художественный #115 по статусной таблице. Это незавершённая платформенная/визуальная приёмка, а не отсутствие перечисленного выше кода. Прогон четырёх задач не запускал GUI/MP4. В продолжении API27 есть два коротких игровых smoke, но новый MP4, прослушивание и платформенная приёмка Windows/Linux по-прежнему не выполнены. ## Правила продолжения - Не закрывать issue даже после исправления; сообщать проверенный объём и остаток в комментарии. - Не переписывать gitlink на плавающую ветку. Изменения движка сначала публикуются отдельно. - Не путать наблюдённый трек с настоящим положением цели и `own_tick` с relay. - Не менять live-мир при seek; replay воспроизводит последствия, не применяет урон повторно. - Не считать Unknown свободным пространством или подтверждённой стеной без явной игровой политики. - Новые числа — игровой масштаб, не реальные характеристики вооружения. - Токен Gitea читать из локального файла во время запроса; не коммитить и не печатать.