Решили: точка удара выбирается из трёх зон тела (голова 5% / корпус 80% / ноги 15%), но заявленный вес каждой умножается на долю её накрытия кругом атаки — досягаемость владеет распределением, а не украшает его.

Почему: прежний разброс вокруг груди не умел ответить «мечник достаёт великану только до ног». Отвергнуты две альтернативы. Выключатель «зона доступна / нет» — зона на границе мерцала бы между 0 и 100% от одного шага юнита. Деление цели на восемь сторон света (предложение Макса, снято после разбора рисков, о которых он сам и попросил) — у плоского спрайта нет глубины, «сзади-слева» и «слева» рисуются одним пикселем, а секторы «сверху/снизу» спорили бы с зонами за вертикальную ось. Вместо этого оси разведены: вертикаль держат зоны, горизонталь — запрет на дальний край силуэта.

Сказано: «Надо рандомить точку удара, до которой юнит из его положения РЕАЛЬНО может достать. Понимаешь? […] По этой же логике, если будет великан, а мечник достает лишь до его ног - то он бьет чисто по ногам!» и отдельно про вырожденный случай: «МЫ ОРЕМ ГРОМКО. Чтобы такое не пропустить случайно.» (06.08.2026, сессия 1d0b7283).

Грабли:

  1. Круг атаки от ПОЗИЦИИ не дотягивается до чужой груди никогда. Позиция юнита в симе — это ноги, а зона корпуса висит на 0.62 роста, то есть на метр выше. Первая версия центрировала круг на позиции атакующего, и «вырожденный случай» орал бы на каждом ударе вместо настоящей поломки. Круг идёт от корпуса атакующего, радиус — буквально AttackRange + r_свой + r_чужой, формула симуляции.
  2. Линейный множитель сохраняет перевес базового веса. Корпус, накрытый на 28%, бился вдвое чаще ног, накрытых на 76%: заявленные веса различаются в шестнадцать раз. Мой первый тест ожидал обратного и упал — упало ОЖИДАНИЕ, а не код. По решению Макса резкость вынесена ручкой и поднята до квадрата; тест теперь проверяет монотонность ручки, а не одну точку: привязка к «ноги победили» ломалась от сдвига радиуса на сантиметр, потому что перелом в этой геометрии приходится на 33% накрытия корпуса — то есть ровно туда, где считался тест.
  3. Радиусы зон замерены по арту, а не выбраны на глаз (полуразмер головы 0.099 роста, торса 0.190, верхней ноги 0.115) — и замер поймал промах в дефолте ног на четверть.
  4. Ассет забрал старые дефолты раньше, чем я их поправила. Рефреш прошёл между правками, поля появились в .asset со значениями первой версии, и правка инициализаторов в C# на них уже не влияла — играет ассет. Замеренные числа дописаны прямо в него.
  5. Живой якорь в весах ломал кооператив, и это чинится разделением, а не выбором. Первая версия считала веса по мировым позициям костей — то есть по позе, которая у двух игроков отличается на долю кадра. Непрерывную часть (место внутри зоны) это сдвигает на миллиметры, а дискретную (какая зона) — перебрасывает целиком: один видит удар в корпус, другой в ногу. Разведено: вес считается по долям роста от позиции из ленты, точка ставится вокруг живого якоря. Держит ZoneChoice_IgnoresLiveAnchorDrift — он гоняет одни и те же сиды по «спокойной» и «уехавшей» позе и требует одинакового ВЫБОРА.
  6. У долей роста и якорей появился общий шов, и он тоже в тесте. Доли живут в конфиге, якоря — в префабе, правятся порознь; разъедутся — вес посчитается по груди, а вспышка придёт в живот. ImpactZoneRigTests роняет сборку при расхождении больше 5% роста (сейчас 0.4–0.7 см).

Владелец правды: ImpactZoneSolver, тесты ImpactZoneTests и ImpactZoneRigTests, конфиг CombatFeelConfig.asset, якоря Aim_Head/Aim_Body/Aim_Legs в BoneUnit_SwordShield.