Решили: раскладку призванных тел забрал единственный владелец 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
(бой целиком).