Решили: состояние юнита заводится ТОЛЬКО после того, как решение о нём принято, и снимается ТОЛЬКО когда ушёл последний, кто его держал. Проверено на смещениях: Displace теперь отказывает по тому же условию, что и DisplacementSystem.Add, а конец полёта объявляется, лишь когда у тела не осталось активных смещений.

Почему: обе половины ломались одинаково тихо. Маркер «в полёте» — постоянный, неснимаемый, с полным контролем — вешался ДО того, как система примет запрос; система молча отказывалась при нулевой дистанции, а снимает маркер только конец полёта, которого уже не будет. Цель оставалась выключенной до конца боя. С другой стороны, флаг полёта у юнита один, а смещений по одному телу может идти несколько (толчок способности и цепное отбрасывание «ядра» в те же тики) — первый закончившийся полёт снимал флаг с тела, которое ещё тащит второй.

Грабли: и то, и другое латентно ровно до одной строки в ассете. «Толкает» и «на сколько» — два независимых поля, и нулевая дистанция при взведённом флаге это не выдумка, а один промах в инспекторе. Тем же заходом нашлись два родственника: у эффекта снимок стаков стартовал единицей, хотя «снимка ещё нет» кодируется нулём (эффект с несколькими стаками влиял как один), и трекинг- снаряд с пробиванием не гас на попадании — встав в цель, он бил её каждый следующий тик. Общее у всех четырёх одно: пара «завести/снять» разъехалась, и ни одна половина об этом не сообщает.

Владелец правды: CombatSimulation.Displace (единое условие приёма), DisplacementSystem.HasActiveDisplacement, RuntimeEffect.StacksAtTickStart, ProjectileSystem.ApplyHit.