Решили: форма авто-атаки живёт РАНТАЙМ-СНИМКОМ на RuntimeUnit (тип урона, доставка, on-hit эффекты, канал), а боевая стойка (AttackStanceComponent) по дистанции устанавливает профиль целиком — с гистерезисом (вход 3, выход 4.5) и только в AttackPhase.Idle. Все части снимка берутся из кита одним вызовом RuntimeUnit.AdoptKit.

Почему:

  • Установка, а не «наложить/снять эффект-форму». Запись профиля целиком идемпотентна: у неё нет обратной операции, нет порядка наложений и нет вопроса «что если форму сорвали диспелом посреди боя». Отвергнутая пара «эффект ближней стойки / эффект дальней стойки» приносит всё это разом, причём молча: сорванная стойка оставила бы юнита с профилем кита, то есть с третьей, невидимой формой.
  • Снимок, а не чтение кита. Прецедент уже стоял в коде — AutoAttackDamageType был полем, «снимком кита на бой». Читай стойка кит напрямую, переключение форм пришлось бы выражать вторым китом, потому что кит — общий ассет на всех носителей.
  • Профиль формы ПОЛНЫЙ намеренно. Форма, задающая часть профиля, оставила бы остальное от кита — и кит снова стал бы невидимой третьей формой, различия которой всплывали бы только в бою.
  • Форма не меняется посреди удара. Замах и канал уже сняли цифры с прежней формы (тип урона едет с ударом до прилёта — это отдельное осознанное решение), поэтому смена в полёте дала бы удар, начатый одной формой и завершённый другой. Ждём окна покоя.
  • Статы формы — одной группой на ключе эффекта-стойки. Снятие и наложение по ключу означает, что «сменить форму наполовину» невозможно; отложенно (deferred: true), как любой баф, — иначе форма менялась бы посреди тика и исход зависел бы от места юнита в обходе.

Грабли: перевод доставки и on-hit в снимок уронил два теста, собиравших RuntimeUnit руками (ChargeAttackTests, CryomancerSliceTests): они молча получили ближний бой и пустой список on-hit. Отсюда AdoptKit — не ради краткости, а потому что частей у формы четыре, и собрать бойца, забыв одну, можно бесшумно: ни компилятор, ни бой этого не заметят. Второе: стойка НЕ меняет фокус авто-атаки (карточка Десятины просит разный фокус по формам) — фокус живёт в AIProfile, который мозг читает один раз при сборке, это отдельный шов и отдельная работа.

Владелец правды: RuntimeUnit.AdoptKit и поля-снимки рядом с ним, AttackStanceComponent, AutoAttackSystem (читает снимок, а не unit.Unit.*); тесты AttackStanceTests, ChannelledAutoAttackTests.