Решили: Атака может состоять из нескольких Ударов. Контакты размечаются маркерами клипа (все, а не первый), позиции считаются в нормированном времени × длительность свинга, а сила каждого Удара задаётся лично — по умолчанию полная, не «поровну» (вердикт Макса 2026-07-31). Первый носитель — «Двойной удар» Монаха: два маркера и доли 0.5 / 0.5.
Почему так, а не иначе. Три развилки, каждая решалась отдельно:
- Где живут позиции контактов. Взяты маркеры клипа, потому что тайминг удара уже приходит
оттуда —
UnitVisualне зря лежит вData, симуляция читает разметку до боя. Альтернатива «список долей в данных юнита» отклонена: она завела бы второго владельца того же факта, и показ скрабил бы клип по своему маркеру, пока урон шёл бы по числам из ассета. - Как серия соотносится с замахом. Серия висит на обычном замахе: первый контакт там же, где был бы единственный, остальные отстоят от него на расстояние из клипа. Поэтому ускорение атаки сжимает серию целиком, а ускоренный замах (разбег, взведённое комбо) двигает её как одно целое. Альтернатива «считать каждый контакт от начала свинга независимо» растягивала бы промежутки при любом множителе замаха и ломала бы ритм двойки.
- Как делится урон. Никак: движок не делит. Два контакта без настройки бьют вдвое, и это осознанно — силу серии объявляет автор кита, а не выводит формула из числа маркеров. Половинки Монаха живут в его данных.
Инварианты, которые пришлось ввести: соседние контакты отстоят минимум на тик (иначе второй Удар не
виден ни показу, ни игроку), последний приходит не позже интервал − 1 (удар не совпадает со стартом
следующей Атаки). Отсюда потолок скорости многоударного кита: атак/сек ≤ TickRate / N. Хвост-доигрыш
считается от последнего контакта, якорь интервала остаётся на первом — темп Атак не зависит от
числа Ударов внутри.
Грабли:
- Рефанд кулдауна пришлось сузить до пустого свинга прямо в этом заходе, а не потом: как только контактов стало несколько, старое поведение превратилось в эксплойт — микростан после первого Удара обнулял таймер, и контроль ускорял жертву. Инвариант держал guard-тест, написанный заранее.
- Одиночный удар обязан идти прежним путём бит-в-бит. Расчёт серии включается только при двух и более маркерах; иначе поехали бы тайминги всего ростера, а заметили бы это по чужому балансу.
- Валидатор на длину долей нужен именно потому, что обе ошибки невидимы в игре. Список короче разметки — часть Ударов молча уходит в полную силу; длиннее — лишние числа не играют никогда, и автор крутит их, не понимая, почему ничего не меняется.
Чего этот заход НЕ сделал: показ по-прежнему скрабит клип к первому маркеру, то есть два Удара Монаха видны двумя цифрами урона, но не двумя взмахами. Клип покадровый, второго взмаха в нём не нарисовано — это работа арта, а не тайминга.
Владелец правды: ClipMarkers.MarkerNormalizedAll, UnitVisual.AttackHitPositions,
AttackTiming.ContactTicks (<remarks> — оба инварианта и потолок скорости),
UnitData.HitDamageShare, AutoAttackSystem.Interrupt (правило рефанда); тесты — группа «Атака из
нескольких Ударов» в WindupAutoAttackTests и AnimationValidationTests.HitDamageShares_MatchMarkerCount.