Дизайн боевой арены и фазы расстановки: геометрия как данные, prefab-per-arena, зоны расстановки (Normal/Extended), boot-flow загрузки арены.
Связано: 6. Боевая модель (юниты, статы, эффекты), 02. Архитектура реализации — Фаза 1 (Боевое ядро), 05. Презентация и интерполяция, 07. Техдолг, решения и changelog, 7. Флоу забега и сессия.
Статус: дизайн согласован (2026-07-09). Шаги 1-3 (чистая логика) реализованы и покрыты тестами (2026-07-10, EditMode 184/184). Шаг 4: код авторинга (ArenaLayoutAuthoring + гизмо) и проводка в DI готовы; создание арена-префаба, адреса Addressables, drag-drop UI, сетевые команды расстановки и визуальная проверка — за Максом. Отклонения реализации от исходного эскиза помечены ниже (§3, §10).
1. Зачем этот документ
Боевого поля как сущности в коде пока нет: стартовые позиции юнитов хардкодятся в dev-командах (GuildmasterCommands), границ движения нет вообще — MoveRetreat/MoveKite уводят юнита в бесконечность, отбрасывание (Displace, §9.9) летит в пустоту. Этой информации не было в документации — фиксируем её здесь до написания кода.
Документ описывает три связанные вещи:
- Арену — границы боевого поля (клампинг движения, «стены» для отбрасывания).
- Зоны расстановки — области, куда можно ставить юнитов перед боем (свои и вражеские; Normal и Extended).
- Способ хранения и загрузки контента арен (визуал + геометрия).
2. Главный принцип: геометрия ведёт, визуал следует
Боевое поле — это данные симуляции, а не объект сцены или физики.
- Не
BoxCollider2D, неPhysics2D. Симуляция (CombatSimulation) детерминированная, headless, работает на голыхVector2, гоняется в EditMode-тестах без сцены и host-authoritative по нетокоду. Если границы боя читать с коллайдера на сцене — рушится и тестируемость, и авторитетность хоста (клиент не должен доверять своей локальной сцене). См. 02. Детерминированная симуляция и тик. - Геометрия первична, арт вторичен. Границы боя не зависят от того, как нарисован задник. Наоборот: игровая зона задана данными, а визуал подгоняется под неё. Художник двигает арт — но не имеет права молча менять границы боя.
- Авторинг — в сцене/префабе с гизмо; источник правды в рантайме — снапшот данных. Редактируем зоны руками и видим их гизмами (удобно), но на старте боя из авторинга «печётся» неизменяемый снапшот, который уходит в симуляцию. Тесты собирают тот же снапшот руками.
3. Модель данных
Чистые структуры, зависят только от Vector2 (математика, не MonoBehaviour):
// Прямоугольная область. Вся геометрия формы — здесь (одна точка правды).
readonly struct Rect2D
{
Vector2 Center;
Vector2 Size;
bool Contains(Vector2 p);
Vector2 Clamp(Vector2 p); // ближайшая точка внутри
}
// Границы боевого поля (клампинг движения + «стены» отбрасывания).
readonly struct ArenaBounds
{
Rect2D Area;
bool Contains(Vector2 p);
Vector2 Clamp(Vector2 p);
Vector2 ClosestPoint(Vector2 p);
}
enum DeploymentSide { Player, Enemy }
enum DeploymentTier { Normal, Extended }
readonly struct DeploymentZone
{
Rect2D Area;
DeploymentSide Side;
DeploymentTier Tier;
}
// Неизменяемый снапшот арены, отдаётся в симуляцию и сервис расстановки.
sealed class ArenaLayoutData
{
ArenaBounds Bounds;
IReadOnlyList<DeploymentZone> Zones;
}Зоны: одно поле «арена» (ArenaBounds) разбито внутри на список зон расстановки. У каждой зоны — область, чья сторона, tier.
- Normal — базовая зона своей стороны, доступна всем юнитам.
- Extended — расширенная зона, доступна не всем: только юниту с правом (манёвренные/убийцы или выданный навык). Право — булев флаг
RelicData.CanUseExtendedDeployment(источник данных чемпиона). Проверка — в сервисе расстановки, не в UI.- Отклонение от эскиза: флаг НЕ хранится на
RuntimeUnit. Расстановка идёт до боя,RuntimeUnitтогда ещё не создан, и в самой симуляции этот флаг никто не читает — поле на рантайм-юните было бы мёртвым. ПоэтомуDeploymentService.CanPlace(pos, side, canUseExtended)принимает право какbool, а вызывающий (UI/обработчик команды) достаёт его из того, что ставит (RelicDataпри расстановке).
- Отклонение от эскиза: флаг НЕ хранится на
Разделение ответственности: арена задаёт зоны (области). Энкаунтер (EncounterData, см. 7. Флоу забега и сессия) задаёт, кто и где из врагов стоит внутри enemy-зоны. Player-зоны — куда игрок таскает своих.
4. Хранение контента: prefab-per-arena
Ориентир — 3-4 арены на старте, у каждой уникальный визуал + уникальная геометрия. Каждая арена — отдельный префаб, грузится по адресу (Addressables) в единственную боевую сцену.
Рассмотренные варианты
| Вариант | Вердикт |
|---|---|
| Scene-per-arena (аддитивные сцены) | Отклонён. Дублирует камеру/свет/DI-инфраструктуру, тяжелее грузится, боль в тестах. При 3-4 аренах выигрыша нет. Триггер пересмотра — только baked GI (см. §7). |
| Prefab-per-arena (Addressables) | Выбран. Гизмо-авторинг работает в prefab-режиме, одна боевая сцена = один DI-scope, Addressables уже в стеке, тестируемость сохранена. |
| Чистый SO-data + общий визуал | Отклонён. Зоны гизмами в SO не поредактируешь (нет трансформа и сцены под глазами). |
| Один префаб, свап задника | Отклонён. Геометрия у арен разная, не только картинка. |
Анатомия арена-префаба
Arena_Forest (prefab)
├── ArenaLayoutAuthoring // геометрия: bounds + DeploymentZone[]; рисует OnDrawGizmos. ВЕДЁТ.
│ // BuildLayout() -> ArenaLayoutData (снапшот).
└── Visual // задник, декор, партиклы, свет, Volume (постпроцесс). СЛЕДУЕТ за геометрией.
Чистая сборка снапшота (Rect-ы → ArenaLayoutData) выносится в статику на примитивах — чтобы тест конструировал её без префаба и сцены.
5. Boot-flow боя
EncounterData→ ключ арены ("arena_forest", Addressables) + список врагов.Addressables.LoadAssetAsync(key)→Instantiate→ получаемArenaLayoutAuthoring.authoring.BuildLayout()→ArenaLayoutData(bounds + zones).- Данные в
CombatLifetimeScope: симуляция клампит движение по bounds;DeploymentServiceвалидирует расстановку по zones. - Фаза расстановки (до
CombatSimulation.Tick): игрок свободно таскает юнитов; враги преднастроены из энкаунтера в enemy-зонах. - Старт боя.
- После боя —
Addressables.Release+Destroyинстанса арены.
6. Фаза расстановки (deployment)
Свободное размещение в пределах зоны (не сетка слотов): юнит ставится в любую точку разрешённой области. Обосновано тем, что в бою юниты сразу начинают двигаться (MovementSystem), поэтому стартовая позиция — это начальные координаты, а не фиксированный тактический слот.
Правило валидации:
CanPlace(unit, pos, side):
pos внутри Normal-зоны стороны `side`
ИЛИ pos внутри Extended-зоны стороны `side`, если у unit есть право на Extended
Кооп (host-authoritative): перетаскивание юнита — это команда хосту. Игрок тащит → хост валидирует CanPlace → применяет. Клиент себе не доверяет. Ещё один довод, почему правило живёт в логике (DeploymentService), а не в UI. См. 06. Сетевая модель — решение и план.
7. Интеграция с симуляцией
- Клампинг движения.
MovementSystemв конце тика приводитunit.Position = bounds.Clamp(unit.Position).DisplacementSystem(отбрасывание/«ядро», §9.9) тоже упирается в границу — это «стена» (клампит ДО расчёта сегмента «ядра», чтобы не задевать врагов за полем). - Часть сим-состояния.
ArenaBoundsдетерминированы и одинаковы на всех клиентах. Расстановка задаёт стартовыеPosition, которые входят вComputeChecksum()наравне с остальным. - Отклонение от эскиза (механизм передачи).
CombatSimulationвладеетArenaBounds(ctor-параметрarena, дефолтArenaBounds.Unbounded) и передаёт его вMovementSystem.Tick/DisplacementSystem.Tickпараметром, а НЕ черезICombatContext.Arena. Причина: уICombatContext6-7 тестовых моков — расширение интерфейса сломало бы их все. Границы читаются и какCombatSimulation.Arena(для презентации/оверлея).
8. Свет, постпроцесс и вопрос сцен
Почти вся графика едет в префабе — сцена для неё не нужна:
- Real-time свет (
Light), постпроцесс (URPVolume+VolumeProfile), fog, партиклы, декали — всё это GameObject-based, кладётся вVisualарена-префаба. Каждая арена получает свою атмосферу, правится в prefab-режиме рядом с задником и зонами. - В один момент времени жива одна арена (грузим → бой → выгрузка), поэтому источники света и Volume между аренами не конфликтуют. Боевая сцена «пустая» по свету — Directional приходит из арены (нет двух «солнц»).
Единственный триггер миграции на аддитивные сцены — если понадобится baked GI / lightmaps или per-scene environment settings (ambient/skybox), которые привязаны к сцене, а не к префабу. Для 2D-спрайтового автобатлера это маловероятно.
Миграция при этом дешёвая, потому что арена изолирована: перенести содержимое корня в сцену и поменять способ загрузки (LoadAsset → LoadScene). Boot-flow, симуляция, зоны — не трогаются. Поэтому платить за сцены превентивно не нужно.
9. Открытые вопросы
- Анти-оверлап при свободной расстановке. Чтобы два юнита не встали в одну точку — минимальная дистанция/радиус при drop. Деталь фазы расстановки, решить при реализации §6.
- Форма зон. Пока прямоугольники (
Rect2D). Вся геометрия вContains/Clamp— смена на полигон = одно место. - Владелец
EncounterDataи связь «энкаунтер → ключ арены» — уточнить в 7. Флоу забега и сессия. - Подсветка Extended-зон в UI при перетаскивании манёвренного юнита — презентация, отдельная задача.
10. Порядок работ
| Шаг | Содержание | Статус |
|---|---|---|
| 1 | Rect2D + ArenaBounds (Contains/Clamp) в Guildmaster.Core.Arena | Готово. ArenaGeometryTests |
| 2 | Клампинг в MovementSystem + DisplacementSystem (границы — ctor-параметр CombatSimulation) | Готово. ArenaClampTests + регресс существующих слайс-тестов |
| 3 | DeploymentZone/ArenaLayoutData + DeploymentService.CanPlace + флаг RelicData.CanUseExtendedDeployment | Готово. DeploymentServiceTests |
| 4 | ArenaLayoutAuthoring (гизмо) + CombatLifetimeScope читает его и инъектит арену | Код готов; арена-префаб, адрес Addressables и визуал — за Максом |
Шаги 1-3 — чистый headless-код с тестами (EditMode 184/184 на 2026-07-10). Шаг 4 — код авторинга готов; создание префаба, настройка Addressables, drag-drop UI и визуальная проверка — в сцене (зона Макса).
Осознанно НЕ сделано в этом срезе (не костыль, а граница объёма):
- Хардкод стартовых позиций в
GuildmasterCommands/BattleSetupBuilderНЕ выпиливался: замена требует источника арены (шаг 4, префаб) и полноценной фазы расстановки. Dev-спавны теперь просто клампятся арендой, если в сцене естьArenaLayoutAuthoring. - Фаза расстановки как UI (drag-drop) и сетевые команды расстановки (host-authoritative) — отдельная задача; здесь только правило
CanPlace+ данные. CombatLifetimeScopeищетArenaLayoutAuthoringчерезFindFirstObjectByType(сцена-авторинг работает сегодня). Свап на загрузку арена-префаба через Addressables — будущий шаг, когда boot-флоу боя будет грузить арену по ключу энкаунтера.