squad-proto/docs/08-decisions.md

369 lines
28 KiB
Markdown
Raw Normal View History

Прототип боевого ядра: отряд, хоррор-слой, ближний бой Тактический отряд с автоматическим огнём в изометрии, собственный софтверный растеризатор (raylib только окно, ввод и финальный блит). Ядро (спек, раздел 6): - LaneClear: проверка линии огня по своим, стенам и дальности; - решатель огневых позиций с гистерезисом и коммитом; - резервирование линий огня и расступание из чужих секторов; - пули-снаряды, дружественный урон физически возможен. Хоррор-слой (render-only): свет и туман войны с памятью карты, светящиеся трассеры, кровь, виньетка, зерно, напряжение. Ближний бой: сектор удара, три фазы, связка из трёх ударов, своя дисциплина «не бить сквозь своего». Проверка: squad_proto.exe --accept прогоняет критерии приёмки раздела 11 плюс блок M7 по ближнему бою — все PASS, FRIENDLY_HITS = 0 за 3 минуты боя. Сборка: cmake -S . -B build && cmake --build build --config Release raylib 6.0 подтягивается через FetchContent. Документация — CLAUDE.md как оглавление, содержание в docs/. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 17:00:49 +07:00
# 08 — Решения и отклонения от спека
Спек в разделе 12 требует: раздел 6 реализуется как написано, а если считаешь,
что где-то ошибка — сначала скажи, потом делай. Здесь перечислено **всё**, что
отличается от буквы спека, с причиной и замерами.
Отклонения разделены на две группы: содержательные (меняют поведение) и
структурные (не меняют).
---
## A. Содержательные
### A1. Пуля не летит дальше проверенного отрезка
**Спек**: 6.9 — снаряд со скоростью 22 юнита/с.
**Сделано**: время жизни пули = длина отрезка «дуло → цель» / скорость,
а не `дальность оружия / скорость`.
**Почему**. `LaneClear` проверяет отрезок до цели. Если цель успевает умереть от
чужой пули, снаряд летит **дальше** — в пространство, которое никто не проверял.
Ровно так возникало последнее упорное попадание по своим: агент стоял внутри
кучки мишеней, пуля прошла место умершей мишени насквозь и нашла его.
**Замер**: до фикса — 1 попадание за 300 с, стабильно воспроизводимое
(`t=94.49, #1 -> #2 @ (30.6,21.7)`); после — 0 за 300 с.
### A2. Спуск учитывает время полёта пули (`AllyInterceptsShot`)
**Спек**: 6.3 проверяет линию в момент выстрела.
**Сделано**: дополнительно проверяется сближение пули и союзника как двух
движущихся точек на всём времени полёта, с радиусом безопасности, растущим со
временем (`shotPredictSlack` — запас на то, что союзник может и повернуть).
**Почему**. 6.3 даёт мгновенный снимок, а 6.9 делает пулю реальным снарядом:
до 0.4 с полёта, за которые союзник проходит больше метра. Союзник, честно
чистый в момент нажатия, спокойно входит в коридор уже после выстрела.
Реактивное расступание (6.6) этот случай не закрывает по определению — оно
срабатывает, только когда тело уже в линии.
Само правило 6.3 при этом **не ослаблено**: это дополнительное условие спуска,
а не замена. `LaneClear` продолжает использоваться и в целеуказании, и в оценке
кандидатов, и в реестре линий — в том виде, как написано в спеке.
**Замер**: журнал попаданий показывал, что жертва **всегда** была `ADVANCING`
то есть именно вбегала под летящую пулю.
### A3. В реестр линий попадают линии, перекрытые своим
**Спек**: 6.6 — «список активных линий огня — сегментов от **стреляющих** агентов».
**Сделано**: линия регистрируется и тогда, когда у агента есть цель в радиусе,
стена не мешает, а мешает **только свой** (`Agent::laneIntent`).
**Почему**. При буквальном прочтении механика самоотменяется: стоит кому-то
влезть в линию — стрелок теряет линию, перестаёт быть «стреляющим», отрезок
исчезает из реестра, и перекрывшего **ничто не выталкивает**. Сектор обстрела не
освобождается никогда, а обещание 6.6 («отряд сам расплывается, освобождая
сектора обстрела») и критерий 11.4 становятся недостижимыми.
**Замер**: `FIRE UPTIME` на маршруте вырос с 33 % до 49 %, худший uptime на
стоянке — с 38 % до 51 %.
### A4. Резерв линии шире порога блокировки (`laneReserveScale = 1.8`)
**Спек**: 6.6 — импульс, если `dist < bodyRadius + laneHalfWidth + SAFETY_MARGIN`.
**Сделано**: импульс расступания начинается на `laneReserveScale` шире этого
порога. Сам порог блокировки в 6.3 не тронут.
**Почему**. Раздел называется «**Резервирование** линий». Если отжимать агента
ровно с той границы, за которой он уже блокирует выстрел, — это не резерв, а
реакция постфактум: агент успевает войти в сектор, где летит пуля.
**Замер** (300 с, с A1 и A2):
| `laneReserve` | FRIENDLY_HITS | худший uptime на стоянке |
|---|---|---|
| 1.0 (буква спека) | 1 | 42.5 % |
| 1.4 | 0 | 50.0 % |
| **1.8** (принято) | 0 | 50.4 % |
### A5. Касательная составляющая обхода стен (`wallSlide`)
**Спек**: 6.7, слой 4 — «wallAvoidance + скольжение вдоль стен при коллизии».
**Сделано**: помимо скольжения при коллизии, `WallAvoidForce` добавляет
касательную силу в сторону цели, когда стена стоит между агентом и целью.
**Почему**. Чисто отталкивающее избегание даёт классический локальный минимум:
seek тянет вперёд, отталкивание держит — агент залипает в «тени» препятствия.
В прогонах это ловилось буквально: агент оставался в северной комнате
(`y=12`), когда отряд уже стоял на арене (`y=26`).
### A6. Агент без целей в радиусе возвращается в строй
**Спек**: случай не описан. 6.5 запускает решатель, когда линии нет, но не
говорит, что делать, если стрелять вообще не по чему.
**Сделано**: если решатель не нашёл ни одной цели в пределах
`weaponRange + ring3`, `postPos` = слот строя.
**Почему**. Иначе отставший агент стоит на месте вечно: в `HOLDING` он идёт
только к `postPos`, а `postPos` равен его же позиции.
### A7. Дуло считается в сторону цели, а не по `facing`
**Спек**: 6.3 — «`muzzle()` — точка в 0.3 юнита от центра агента по `facing`».
**Сделано**: для проверки линии и для спавна пули дуло считается по направлению
на цель; выстрел разрешён только когда ствол доведён до цели с точностью ±5°
(`aimToleranceDeg`).
**Почему**. `facing` доворачивается плавно и на шаг отстаёт от направления на
цель. Если проверять линию по одному направлению, а стрелять по другому, пуля
летит не по проверенному отрезку — то есть гарантия «не сквозь своих» перестаёт
быть гарантией. Сейчас проверенный отрезок и траектория пули — буквально один и
тот же отрезок.
### A8. Пуля не бьёт собственного стрелка
**Спек**: 6.9 — «коллизия со **всеми** капсулами, включая союзников».
**Сделано**: стрелок исключён из коллизий своей пули.
**Почему**. Дуло вынесено на 0.3 юнита при радиусе тела 0.28 — пуля стартует
почти вплотную к своему же телу. Самопопадание было бы вырожденным шумом в
`FRIENDLY_HITS`, главной метрике корректности. Все **остальные** капсулы,
включая союзников, проверяются как написано.
### A9. Северная кучка мишеней стоит напротив прохода
**Спек**: 4 — 12 мишеней кучками, конкретные места на усмотрение реализации.
**Сделано**: кучка из 3 мишеней в северной комнате поставлена напротив прохода
`x=30..31`, так что из коридора до неё есть чистая линия.
**Почему**. Спек называет узкий проход важным тест-кейсом, а критерий 11.6
требует, чтобы в проходе отряд вытягивался в колонну и стрелял только передний.
Без цели, простреливаемой сквозь проход, проверять нечего.
### A10. Вторичные цели в `HOLDING` при ручном прицеливании
**Спек**: 6.8 — флаг `freeFireSecondary` описан только для `ADVANCING`.
**Сделано**: буквально — в `HOLDING` агент без линии до назначенной цели молчит
и перестраивается решателем, по вторичным целям не стреляет.
**Почему**. Иначе агент нашёл бы вторичную цель, получил бы `hasLane`, и решатель
для него никогда бы не запустился — то есть требование «в `HOLDING`
перестраивается через решатель» не выполнялось бы.
### A11. Туман войны и освещение вопреки разделу 12
**Спек**: 12 — «не добавлять освещение, туман войны».
**Сделано**: добавлено и то, и другое.
**Почему**. Заказчик изменил требования: прототип разворачивается в хоррор, где
ограниченный обзор и темнота — не украшение, а основа подачи. Правила стрельбы
(раздел 6) при этом не тронуты ни одной строкой, критерии приёмки остались
зелёными. Устройство слоя — [10-atmosphere.md](10-atmosphere.md).
### A12. Конус зрения не влияет на ИИ
**Сделано**: у агента есть конус обзора для КАРТИНКИ, но выбор целей и стрельба
по-прежнему работают по LOS и дальности оружия.
**Почему**. Если завязать целеуказание на видимость, меняются все критерии
приёмки, а отряд начинает «слепнуть» от поворота ствола — при том что `LaneClear`
и так требует чистой линии, то есть отсутствия стены. Чтобы стрельба в темноту
не выглядела абсурдом, мишень показывается, если освещена **или** если по ней уже
работает кто-то из отряда.
### A13. Shift — рывок, а не медленный шаг
**Спек**: 5 — «Shift (удерж.) — медленный шаг».
**Сделано**: базовая скорость снижена с 3.2 до 2.0, а Shift даёт рывок 3.2.
**Почему**. Заказчик просил «менее казуальное управление, помедленнее». Медленный
шаг при уже медленной базе — мёртвая клавиша. Инверсия сохраняет обе скорости из
спека (2.0 и 3.2), но делает осторожность режимом по умолчанию, а спешку —
осознанным решением игрока. Побочный эффект: проверка спуска на время полёта пули
обязана считать по МАКСИМАЛЬНОЙ скорости (`MaxAgentSpeed()`), иначе рывок союзника
не учитывается и возвращаются попадания по своим.
### A14. Дальность оружия 9.0 -> 7.2
**Сделано**: минус 20 % по требованию заказчика; вместе с ней отмасштабированы
`minEngageDist` (2.0 → 1.6) и кольца решателя (0.8/1.6/2.4 → 0.7/1.4/2.1),
а старт отряда сдвинут в (26.5, 26.5) ближе к кучке мишеней.
**Замер**: без сдвига старта и масштабирования колец сокращение дальности само по
себе давало 1 попадание по своим за 300 с и просадку uptime — агенты сбивались в
кучу у цели. С пакетом целиком: 0 попаданий, uptime в стоячем бою 100 %.
### A15. `candMinAlly` 0.55 -> 0.60
Порог «кандидат слишком близко к союзнику» был **меньше диаметра тела**
(2 × 0.28 = 0.56), то есть допускал позиции, где агенты физически пересекаются.
### A16. Клинок союзникам урона не наносит вообще
**Спек**: ближнего боя не описывает.
**Сделано**: сектор удара повторяет правило 6.3 в угловой форме — свой в
секторе — не бить. Но урон применяется **только к мишеням**: даже влезший под
лезвие союзник не получает ничего, взмах просто обрывается.
**Почему**. Соблазн был сделать «сильную» версию: клинок бьёт всех, а от
попаданий по своим спасает дисциплина сектора. Но у клинка нет времени полёта,
за которое можно передумать: союзник входит в дугу за 1/60 с уже после того, как
взмах начат, и в этот момент отменять нечего. «Сильная» версия означала бы
`FRIENDLY_HITS > 0` в любой плотной свалке, то есть провал главного критерия
прототипа ради механики, которой в спеке нет.
Обрыв взмаха даёт то же самое ощущение (боец отдёргивает клинок, серая вспышка,
`MELEE_ABORT` в метриках) и при этом не может сломать критерий 11.2.
**Замер**: 81 взмах за 30 с в плотной свалке, `FRIENDLY_HITS = 0`,
16 взмахов оборвано.
### A17. Старт замаха смотрит на союзника вперёд
**Сделано**: `MeleeArcClear` проверяет союзника в двух точках — где он сейчас и
где будет через `windup + strike`. Ровно та же поправка, что A2 для пули.
**Почему**. Взмах живёт 0.30 с, за которые союзник проходит полметра. Проверять
только текущее положение — значит начинать удары, которые всё равно придётся
обрывать.
**Замер** (сценарий M7, 30 с): без поправки и без резерва дуги — 21 оборванный
взмах из 77; с поправкой и резервом — 16 из 81, попаданий 81 против 81, убийств
23 против 23. То есть взмахов стало больше при том же результате.
### A18. Резерв в реестре линий получил явную полуширину
**Сделано**: у `FireLane` появилось поле `reserve`. Линия огня по-прежнему берёт
ширину из правила 6.3, клинок задаёт свою — долю бокового размаха сектора
(`meleeReserveFrac` 0.55).
**Почему**. Клинок резервирует отрезок вдоль оси удара, а поражает сектор ±50°.
Союзник, отжатый строго перпендикулярно оси, попадал ровно во фланг дуги — то
есть механизм расступания работал против себя.
**Замер**: `meleeReserve` 0 → 0.55 дало 16 оборванных взмахов вместо 21 при
большем числе взмахов, а худший `FIRE UPTIME` на стоянке маршрута — 23.3 %
вместо 0 % (стрелки перестали стоять в дугах и молчать).
### A19. Жёсткая граница огневой позиции (`postLeash`)
**Спек**: 6.5 — цена за удаление от якоря.
**Сделано**: дополнительно кандидат дальше `cohesionRadius + postLeash` от якоря
отбраковывается совсем.
**Почему**. Цена — мягкая: линия огня стоит 100 очков, а уход на 1.5 юнита за
круг — 36. Решатель всегда выбирал линию, и отряд расползался.
**Замер**: максимум удаления стрелка от диска 3.65 → 2.81 юнита. При этом
`FIRE UPTIME` на 180-секундном маршруте не упал, а вырос: на стоянках у мишеней
82.9 / 93.2 / 90.7 / 86.0 % — плотный отряд перекрывает друг другу линии реже,
чем расползшийся.
### A20. Мишени — тела, и они получают отдачу
**Сделано**: слой движения `TargetPushForce` (мишень расталкивает агентов) и
импульс от удара с возвратом на исходное место (`Target::home`).
**Почему**. Без расталкивания боец стоит **внутри** того, кого рубит, и удар не
читается вообще. Без отдачи попадание в упор визуально не отличается от промаха.
Возврат на `home` обязателен: расстановка уровня захардкожена, и уползание мишеней
поломало бы все замеры разом. Респавн дополнительно ставит `pos = home`.
### A21. Критерий 11.6 считает только выстрелы вперёд
**Было**: тест требовал, чтобы в коридоре передний агент стрелял минимум вдвое
чаще хвостовых, и считал ВСЕ выстрелы внутри прохода.
**Стало**: считаются только выстрелы **по ходу движения**. Выстрелы назад
выводятся в отчёт отдельно.
**Почему**. Диагностика `DIAG` показала, что почти все «нарушения» — это
хвостовой агент, стреляющий назад, в оставленную позади комнату, по совершенно
другой цели. Впереди по такой линии никого из отряда нет по построению, и к
правилу «не стрелять сквозь своего» это отношения не имеет. Порог ловил не
поведение, а геометрию расстановки мишеней.
**Замер**: на прежней базе тюнинга новый счёт даёт 4/0 (было 12/4), на текущей —
5/0. То есть критерий стал строже, а не мягче: вперёд хвост не стреляет вообще.
---
Поле путей: отставший возвращается в строй (M37) Отряд из пяти не проходил первый этаж ни на одном сиде, а одиночка проходил. Дело было не в ширине коридоров: у бойцов не было поиска пути вообще. Боец, оказавшийся за стеной от своего слота, seek'ом тянулся В СТЕНУ, обход стены толкал его обратно, и он замирал в углу навсегда. Теперь у каждого бойца своё поле путей (BFS от клетки его цели) с натягиванием верёвки: он берёт самую дальнюю точку пути, которую видит. Включается обход по двум условиям сразу — боец упёрся (хочет идти и не едет) И потерял строй. Счётчик упора сбрасывается по факту «цель видно», а не «поехал»: сброс по скорости давал дребезг, и боец полз шаг за две секунды. По дороге вскрылись две неверные МЕРКИ: * критерий 11.6 считал колонной «разброс вдоль больше, чем поперёк» — и потому награждал отряд, у которого двое навсегда остались по ту сторону стены. Теперь колонна меряется самим проёмом: разом в горловине не больше двух, и прошли ВСЕ. * зонд --wall читал «стены не нашлось» (ноль) как «стена на нулевом этаже» и объявлял пятёрку, не сломавшуюся нигде, худшей из трёх. Обе переписаны и проверены на сломанной сборке. Замеры: --play 6 5 на восьми сидах 47/48 (было 0/48), --accept PASS, шаг 6.1 мкс, --wall 30 одиночка D20 / трое D20 / пятеро D31 (не найдена), --descend 8/30, hollow --descend 6, --ui-check, оба проекта --check — без ошибок. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 22:31:34 +07:00
### A22. Критерий 11.6: «колонна» меряется проёмом, а не разбросом
**Было**: колонной считалось «разброс вдоль движения больше, чем поперёк,
в полтора раза» — по бойцам в створе прохода вместе с подходами.
**Стало**: колонна — это про сам проём. Одновременно в горловине не больше
двух (проём шириной два тайла), и через неё прошли **все** живые бойцы.
**Почему**. Старая мерка награждала ровно ту поломку, ради которой писалась.
Отряд, у которого двое навсегда остались по ту сторону стены (у них не было
поиска пути и вернуться они не могли), растянут вдоль на шесть тайлов — и
проверку ПРОХОДИЛ. Тот же отряд, когда отставшие научились обходить стену и
собираться, стал компактным — и проверку ронял. Замер трасс показал это прямо:
без обхода бойцы 3 и 4 все 26 секунд стоят на y≈16.3 и y≈16.9, южнее стены.
**Замер**: на исправленной сборке — прошли 5 из 5, в горловине разом до 2,
FRIENDLY_HITS=0, сквозь своего 0. На сборке без обхода (`--set navStuckTime=99`)
критерий падает: прошли 3 из 5. То есть мерка стала строже: раньше она
пропускала брошенных за стеной, теперь нет.
---
### A23. Зонд `--wall`: «стены не нашлось» — это не «стена на нулевом этаже»
**Было**: глубина первой стены хранилась нулём как «ещё не найдена», и итог
сравнивал `wall[пятеро] <= wall[одиночка]` буквально.
**Стало**: не найденная стена читается как «глубже последней проверенной»
(`maxDepth + 1`) и печатается как `D31 (не найдена)`.
**Почему**. Пятёрка перестала ломаться на всех проверяемых глубинах — то есть
показала лучший возможный результат, — и зонд объявил это провалом: ноль меньше
двадцати. Ошибка мерки, а не игры, того же рода, что и A22.
**Замер**: `--wall 30` — одиночка D20, трое D20, пятеро D31 (не найдена),
код возврата 0.
---
Прототип боевого ядра: отряд, хоррор-слой, ближний бой Тактический отряд с автоматическим огнём в изометрии, собственный софтверный растеризатор (raylib только окно, ввод и финальный блит). Ядро (спек, раздел 6): - LaneClear: проверка линии огня по своим, стенам и дальности; - решатель огневых позиций с гистерезисом и коммитом; - резервирование линий огня и расступание из чужих секторов; - пули-снаряды, дружественный урон физически возможен. Хоррор-слой (render-only): свет и туман войны с памятью карты, светящиеся трассеры, кровь, виньетка, зерно, напряжение. Ближний бой: сектор удара, три фазы, связка из трёх ударов, своя дисциплина «не бить сквозь своего». Проверка: squad_proto.exe --accept прогоняет критерии приёмки раздела 11 плюс блок M7 по ближнему бою — все PASS, FRIENDLY_HITS = 0 за 3 минуты боя. Сборка: cmake -S . -B build && cmake --build build --config Release raylib 6.0 подтягивается через FetchContent. Документация — CLAUDE.md как оглавление, содержание в docs/. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 17:00:49 +07:00
## B. Структурные (поведение не меняют)
### B1. Дополнительные файлы
Спек перечисляет структуру проекта; добавлены четыре файла:
| Файл | Зачем |
|---|---|
| `game.h/.cpp` | склейка подсистем и порядок шага. Без него это переехало бы в `main.cpp`, а спек требует, чтобы там были только окно и цикл |
| `render/scene.h/.cpp` | проход по полу и список сортировки; отдельно от `iso.h`, который остаётся чистой математикой |
| `core/input.h` | `InputState` нужен и `main.cpp`, и симуляции, и headless-прогонам |
| `debug/harness.h/.cpp` | прогоны `--headless` / `--accept` |
| `sim/loadout.h` | класс оружия и его параметры; сюда приедут разные пушки и апгрейды |
| `ai/melee.h/.cpp` | ближний бой; зеркало `firing_solver` для угловой геометрии |
### B2. У `Sprite` есть точка привязки
Спек: `Sprite { int w, h; std::vector<uint32_t> px; }`. Добавлены поля `ax, ay`
точка внутри спрайта, совмещаемая с проекцией мировой позиции. Иначе смещения
пришлось бы размазывать по коду отрисовки для каждого типа спрайта.
### B3. Параметры оружия синхронизируются из `Tuning` каждый шаг
`Agent::weapon` существует как в спеке, но перед движением поля копируются из
`g_tune` (`SyncLoadoutFromTuning`). Без этого runtime-подкрутка (раздел 8 спека)
не влияла бы на уже созданных агентов. Состояние ближнего боя — фаза, таймеры,
цель — при этом не трогается: копируются только параметры.
### B4. Dev-инструменты
`--headless`, `--accept`, `--probe`, `--set`, `--shot`, тумблеры `--no-solver` /
`--no-laneavoid`. В спеке их нет; они не влияют на игровую сборку и нужны, чтобы
критерии приёмки из раздела 11 проверялись цифрами, а не на глаз.
---
## Что осталось честно неидеальным
- `FIRE UPTIME` считает «нет живой цели в радиусе» простоем. На маршруте между
комнатами это даёт низкие значения, которые к качеству ИИ отношения не имеют.
- `SETTLE TIME` по той же причине растёт, если отряд остановился там, где стрелять
не по чему; в общем прогоне попадаются значения до 5.7 с. В обстановке, к
которой относится критерий 11.1 (мишени в радиусе), — 0.02 с.
Поле путей: отставший возвращается в строй (M37) Отряд из пяти не проходил первый этаж ни на одном сиде, а одиночка проходил. Дело было не в ширине коридоров: у бойцов не было поиска пути вообще. Боец, оказавшийся за стеной от своего слота, seek'ом тянулся В СТЕНУ, обход стены толкал его обратно, и он замирал в углу навсегда. Теперь у каждого бойца своё поле путей (BFS от клетки его цели) с натягиванием верёвки: он берёт самую дальнюю точку пути, которую видит. Включается обход по двум условиям сразу — боец упёрся (хочет идти и не едет) И потерял строй. Счётчик упора сбрасывается по факту «цель видно», а не «поехал»: сброс по скорости давал дребезг, и боец полз шаг за две секунды. По дороге вскрылись две неверные МЕРКИ: * критерий 11.6 считал колонной «разброс вдоль больше, чем поперёк» — и потому награждал отряд, у которого двое навсегда остались по ту сторону стены. Теперь колонна меряется самим проёмом: разом в горловине не больше двух, и прошли ВСЕ. * зонд --wall читал «стены не нашлось» (ноль) как «стена на нулевом этаже» и объявлял пятёрку, не сломавшуюся нигде, худшей из трёх. Обе переписаны и проверены на сломанной сборке. Замеры: --play 6 5 на восьми сидах 47/48 (было 0/48), --accept PASS, шаг 6.1 мкс, --wall 30 одиночка D20 / трое D20 / пятеро D31 (не найдена), --descend 8/30, hollow --descend 6, --ui-check, оба проекта --check — без ошибок. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 22:31:34 +07:00
- Поиск пути включается ТОЛЬКО для отставшего, который упёрся (см.
[05-squad-ai.md](05-squad-ai.md), «поле путей»). Боец в строю по-прежнему
ходит локальными слоями, и это осознанно: строй должен вести себя как строй,
а не как пять независимых искателей пути.