Решили: период между ударами канала едет в снимке числом, а показ крутит клип свинга циклом от маркера контакта через конец обратно к маркеру — один оборот на один период. Боевую стойку (CombatIdle) показ выбирает по наличию цели в снимке, а не по фазе атаки.

Почему: альтернатива — дать показу пересчитать период из AttackSpeed. Формула AttackTiming.IntervalTicks уже имеет владельца, и вторая её копия разъехалась бы на первом же бафе скорости, причём молча: урон продолжал бы идти правильно, а кадр контакта уезжал бы мимо. Признак «в бою ли юнит» брали не из AttackPhase по той же причине разделения смыслов: фаза описывает свинг, и по ней боец между редкими ударами опускал бы клинок каждый раз заново.

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

Владелец правды: UnitAnimation.cs (ChannelClipTime), UnitSnapshot.cs (AttackChannelTickPeriod), тест UnitAnimationSelectorTests.