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

Почему: позы в момент события ещё нет. Событие приходит внутри Update презентера, у которого [DefaultExecutionOrder(-100)], — то есть до Update самих видов и заведомо до того, как Animator применит позу, потому что он делает это между Update и LateUpdate. Читаешь кости — получаешь прошлый кадр. Отвергнуты: чтение позы в момент события (то, что стояло полдня, ошибка 15–100° в зависимости от скорости атаки) и снимок в LateUpdate по прохождении маркера (дешевле, но заводит гонку: событие урона может прийти раньше, чем вид перешагнёт маркер, а фолбэк запрещён — знак удара тогда просто пропадал бы).

Сказано: «Ты уверен, что все ок? Удар сверху вниз, а этот эффект идет под очень странным углом. Ты как определяешь точку начала теперь?» (06.08.2026, сессия a4e2eebd).

Грабли:

  • Прецедент лежал в соседнем файле. SwingArcVfx читает геометрию в LateUpdate и объясняет это прямым комментарием: «плечо двигает Animator, и в Update дуга отставала бы от руки на кадр». Тот же порядок кадра ударил по форме, и его стоило проверить сразу, а не после второго скриншота.
  • Ошибка масштабируется скоростью атаки. Взмах — 200° за время замаха; у быстрого кита замах укладывается в два-три кадра показа, и «отставание на кадр» превращается в 60–100°. На медленных китах то же самое выглядит как небольшая неточность, поэтому по одному бою диагноз не ставится.
  • SampleAnimation работает ЧЕРЕЗ Animator, и на выключенном не делает ничего. Первая версия замера выключала его «чтобы не переписал позу между выборками» — и получала нулевое движение. Переписывать ему нечего: он ставит позу в своей фазе, между Update и LateUpdate, а обе выборки идут подряд.
  • Сэмплировать надо КОРЕНЬ ТЕЛА, а не объект с Animator. На префабе вида Animator сидит в соседней ветке (Visual Sprites/Body), кости живут под Visual Sprites/BoneVisual, а пути клипа начинаются с Hips/… — они разрешаются только от второго. От первого клип применяется в пустоту, и замер честно рапортует «кончик стоит на месте». Обе ошибки выглядят одинаково, поэтому диагноз по сообщению не ставится — нужен путь иерархии, и он теперь печатается в падении теста.
  • Инвариант закрыт тестом (StrikeDirectionTests): он грузит живой префаб и живой клип, гоняет тот же замер и требует, чтобы направление шло ВНИЗ. Обе тихие поломки он ловит с первого прогона.
  • Шаг замера имеет коридор. Слишком мелкий ловит шум кривой, слишком крупный усредняет дугу и врёт в ту же сторону, что и отменённая хорда. Взято два кадра показа с зажимом в [1/120, 1/30].
  • Замер живёт в координатах КОРНЯ ТЕЛА, а не вида. Разворот юнита сделан знаком масштаба на этом же узле, поэтому TransformDirection возвращает уже зеркальный вектор — отдельный флаг «смотрит влево» не нужен. Побочно: сплющивание тела слегка искажает угол, но живёт доли секунды.
  • Рука не жёсткий рычаг. Промежуточный вариант «перпендикуляр к вектору плечо → кончик» опирался на это допущение; замер живого клипа даёт разброс длины рычага в 8% между началом взмаха и контактом. Само по себе терпимо, но вместе с отставанием кадра дало наблюдаемый «странный угол».

Владелец правды: UnitView.MeasureStrikeDirection (замер) и UnitView.TryGetStrikeDirection (выдача). Дизайн-сторона — канон gdd/70-gamefeel/vfx-language §«Серп лежит вдоль направления удара» и запись журнала ГД 2026-08-06/3.