Решили: период между ударами канала едет в снимке числом, а показ крутит клип свинга циклом
от маркера контакта через конец обратно к маркеру — один оборот на один период. Боевую стойку
(CombatIdle) показ выбирает по наличию цели в снимке, а не по фазе атаки.
Почему: альтернатива — дать показу пересчитать период из AttackSpeed. Формула
AttackTiming.IntervalTicks уже имеет владельца, и вторая её копия разъехалась бы на первом же
бафе скорости, причём молча: урон продолжал бы идти правильно, а кадр контакта уезжал бы мимо.
Признак «в бою ли юнит» брали не из AttackPhase по той же причине разделения смыслов: фаза
описывает свинг, и по ней боец между редкими ударами опускал бы клинок каждый раз заново.
Грабли: счётчик потока перезаряжается в том же тике, в котором бьёт
(AutoAttackSystem.TickChannel), поэтому снимок ударного тика несёт уже полный период, а нуля в
нём не бывает вовсе. Остаток берётся по модулю периода ровно из-за этого — без модуля ударный тик
начинал бы новый оборот с нуля, и клип прыгал бы на кадр контакта, не дойдя до него: удар выглядел
бы обрубленным ровно в момент удара. Тест, написанный на «период минус один», ошибался про сим, а
не про код, и час ушёл на проверку того, кто из них прав.
Владелец правды: UnitAnimation.cs (ChannelClipTime), UnitSnapshot.cs
(AttackChannelTickPeriod), тест UnitAnimationSelectorTests.