Files
airdef/docs/CODEX-HANDOFF.md
T

58 KiB
Raw Blame History

Передача разработки AirDef

Это журнал фактически сделанных изменений и ограничений проверки, а не список обещанных функций. Рабочая ветка kimi-work; issues не закрывать, публиковать комментарии с коммитом и проверками. Движок — отдельный chexit/horizon-airdef, ветка airdef, подключён точным gitlink horizon/.

Текущее состояние: 4 ноября 2026

Последнее продолжение пяти задач: 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: сетка, рейды. Это намеренное следствие более длинного старта; скрытого повышения 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. Ограничение: у агента нет ввода изображений; проверены параметры, старт плана, яркость центра и парные кадры, но композицию и красоту кадров человеком ещё нужно оценить. Предпусковые кадры не пиксельно идентичны (живой прогон, движение камеры, анимация листвы); совпадают мир/сейв/сценарий/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. Воспроизвести удалось только с разнесённой группировкой: стек в одной точке гибнет от одной боеголовки (known=0, приказ дёшев) — у пользователя силы дожили до второго приказа.

Закреплённые регрессии: нативные «эквивалентность снимка» и «второй умный приказ с толстым журналом принят и запущен»; тяжёлый WASM fixture расширен двумя умными приказами через airdef preset + airdef raid (функция кнопки ПУСК) — 4 принятых приказа, SHA fixture обновлён; лёгкий контракт 30 шагов с preset и отказом пустой волны. Новая команда airdef preset <n> — 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; 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. Исправление оценки покрытия: второй запрос исходного 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.

Двойные налёты и плотность следов

Перед продолжением читать RAID-STABILITY.md: два соответствующих класса отключения теперь воспроизведены и исправлены — тик (1f28f83) и событие UI на втором приказе (#122, order_raid/known_units); приёмка конкретного Windows-сеанса остаётся. Текущий исправленный WASM stress проверяет две принятые волны, обслуживаемую плоскую геометрию и 184 воздушные цели в конкретный момент; новый 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 — инструкция с различием номеров своего С-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-тест баланса (см. ниже).

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.

Расширенная сетка остатка #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. Команда (игнорируемый тест sim::balance::issue_120_grid, только harness-поля в Result, поведение симуляции не менялось):

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), а не этой сеткой.

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, новая карта не раскрывается.

Очередь — рантайм-состояние планировщика. Дроны и очередь запуска в текущий state_text не входят, поэтому формат save не менялся и старые saves читаются как прежде; запись replay по-прежнему хранит траектории, а не задания. Регрессии: 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<u32, Contact>, одна запись на юнита, последнее наблюдение побеждает. Контакт пишется только в момент раскрытия и читает истину только тогда: пуск (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): новых всеведущих данных и усиления характеристик нет.

Форматы не изменились: intel — рантайм (save хранит Layout, replay — траектории), durable-очередь по-прежнему не делается. Измерения до/после на одних 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: durable save queue (новый формат save), полный structured intent, GUI поверх enemy_picture(), оценка баланса ew-adaptive.

Что следующему агенту проверить в первую очередь

  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 читать из локального файла во время запроса; не коммитить и не печатать.