Решили: удар с разбега начинает замах ЗА границей досягаемости и доезжает остаток дистанции ходом, поэтому кадр контакта приходится ровно на момент, когда дистанции хватает для удара. Заряд разбега переехал со взвода «по прибытии» на взвод «по полному разгону», а рут на время такого замаха снят.

Почему: жалоба была визуальная — «бежит, упирается в юнита и только потом бьёт», — но кадр удара оказался ни при чём: маркер контакта скрабится по WindupRemaining и всегда совпадает с сим-тиком урона. Стоял сам замах: гейт пускал свинг только из reach, движение рутовало юнита на замахе, а заряд взводился прибытием, то есть появлялся, когда бежать уже некуда. Три вещи вместе делали «добежал → встал → 0.6 с замаха на месте → удар» единственно возможным поведением, и никакой правкой клипа это не лечится: позицией владеет сим, показ её не переписывает.

Отвергли два дешёвых варианта. Первый — только укоротить ChargeAttackWindupMult: стояние сокращается вдвое, но остаётся стоянием. Второй — оставить рут и «доиграть» въезд анимацией: это ровно то враньё показа о позиции, которое у нас запрещено.

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

Грабли: разбег на заряженном замахе пришлось ЗАМОРАЗИТЬ, а не пересчитывать. Гистерезис гасит разгон, когда зазор падает ниже SprintExitGap, — то есть ровно в тиках подъезда. Скорость упала бы на треть уже после того, как гейт посчитал по ней точку старта, и удар не доезжал бы. По той же причине въезду не выдаётся штраф «стрельбы на ходу».

Второе: перенос взвода заряда в разгон сломал бы правило «разбег, оборванный не прибытием, заряда не оставляет» — раньше оно держалось порядком строк (заряд просто не успевал взвестись). Понадобился отдельный глагол RuntimeUnit.AbortSprint, и теперь каждый обрыв гасит заряд явно.

Третье, организационное: параллельная сессия закоммитила свой рефакторинг и утащила часть этих правок в чужой коммит 98a1fecc7. git commit берёт весь индекс — при параллельной работе коммитить только git commit -F - -- <пути>.

Владелец правды: CombatPositioning.CanCloseIntoReach, AutoAttackSystem.ChargesIntoReach, MovementSystem (флаг chargingIn), тест ChargeAttackTests.