Решили: раскладку призванных тел забрал единственный владелец SummonLayout, и она стала зеркальной: отход назад — вдоль тыла команды (FleeSteering.HomeDir), веер — поперёк фронта. Раньше отход шёл по Y, а веер по X, и обе оси были одинаковы у обеих команд.

Почему: прежняя формула жила в двух файлах и оба комментария утверждали одно — «чистая от состояния мира, поэтому одинакова у обеих команд». Одинаковость и была дефектом: зеркалу нужна отражённость. У обоих призывателей ростера призыв ровно один, side для него всегда -1, поэтому тело левого вставало за спиной, а тело правого — впереди хозяина, между ним и врагом. Дальше расходилось всё: дистанция, выбор цели, расталкивание. Сторож ловил это как «дуэль Некроманта разошлась на тике 7 по позиции X» — десять красных из девятнадцати в дереве.

Отвергнут вариант «просто домножить offset.x на знак команды»: зеркало он чинит, но оставляет раскладку боковой, а комментарий про «веером за спиной» — неправдой. Разворот осей делает зеркальность следствием геометрии, а не заплаткой: X меняет знак вместе со стороной, Y у отражённых сторон совпадает — ровно то, что сверяет MirrorFixture.FirstDifference.

Сказано: «Развернуть веер (рекомендую)» — выбор Макса из двух предложенных вариантов правки (04.08.2026, сессия e1beddbf). Инженерная часть — моя, развилка про геометрию была за ним, потому что двигает позиции скелетов и с ними баланс обоих китов.

Грабли: симптом читался как «левый юнит не выбрал цель» (tgt=? в слепке), и это уводило в сторону выбора цели. На деле MirrorFixture.Name() печатает ? для юнита, которого нет в списке tracked, — то есть для рождённого мимо расстановки. Цель была выбрана, просто ею оказалось призванное тело. Ещё одна ложная тропа: «отмотать боевой код и сравнить» здесь невозможно в принципе — старая CombatSimulation больше не реализует ITickLedgerSink, интерфейс успел обрасти OnShieldAbsorbed.

Владелец правды: SummonLayout.cs, тесты SummonLayoutTests (раскладка) и MirrorMatchTests (бой целиком).