Барьеры — план реализации (Стена и Купол)

2026-07-26. Дизайн принят и зафиксирован — Combat - Barriers (две формы, решение Макса, не развилка). Здесь — инженерная часть: швы, гочи, порядок, тесты. Код ещё не написан.

Порядок: заход идёт после позиционики. Причина дизайнерская (обходить барьер должно быть кому), но она же и инженерная: скоринг «линия огня перекрыта» встраивается в тот же Filter / Score / Override, который позиционика и приносит.

Почему это дёшево

Симуляция уже даёт всё, на чём такие барьеры строятся, и ни один из пунктов не надо добывать:

  • Своя тиковая сим без Unity-физики (CombatSimulation, фикс. TickDelta). Барьер — ещё одна геометрическая сущность в тике, а не коллайдер и не Physics2D-запрос.
  • Снаряды летят по настоящей траектории и уже проверяются swept circle-segment (ProjectileSystem.SweptCircleHit). «Поймать снаряд» = пересечь тот же отрезок с геометрией барьера.
  • АОЕ уже описан геометриейAreaShape.Circle / Line, AreaHit, AbilitySystem.ApplyCircle. Значит «купол закрыл от взрыва» — фильтр по геометрии, а не переписывание способностей.
  • Единая точка мутацийICombatContext. Всё, что барьер обязан перехватывать, проходит через один интерфейс.
  • Shapes уже в проекте (Guildmaster.Presentation) → векторная отрисовка без VFX Graph.

Модель данных

Combat/Barriers/Barrier.cs        — данные одного барьера
Combat/Barriers/BarrierShape.cs   — enum { Arc, Dome }
Combat/Barriers/BarrierField.cs   — запросы к набору барьеров (чистые функции)
Combat/Systems/BarrierSystem.cs   — жизненный цикл в тике

Barrier: Id, Team, Shape, CurrentHP, ExpiresAtTick, Anchor (носитель или null для статичной), геометрия (Center + Radius для купола; Center + Normal + HalfLength для стены), флаги источника.

BarrierField — два запроса, оба чистые и детерминированные:

  • bool TryBlockSegment(Vector2 from, Vector2 to, int sourceTeam, out Barrier hit, out Vector2 point) — для снарядов и линейных зон;
  • bool IsShieldedFrom(Vector2 targetPos, Vector2 originPos, int sourceTeam, out Barrier hit) — для круговых АОЕ: цель внутри купола, центр эффекта — снаружи.

Порядок обхода — по индексу списка, тай-брейк по Id. Никакого Random, иначе повтор боя разъедется (и это же требование к реплеям и к хост-авторитетной сети).

Швы

Шов 1 — ProjectileSystem. Перед проверкой юнитов прогнать отрезок [PreviousPosition, newPos] через BarrierField. Пересёк вражеский барьер — урон уходит в барьер, снаряд гаснет там же.

Гоча, и главная. AdvanceTracking (ProjectileSystem.cs) для самонаводящегося снаряда телепортирует его в цель, не проверяя ничего по пути: при dist <= travel + radius позиция просто присваивается позиции цели. Барьер такой снаряд не поймает вообще — а трекинг у нас основной способ доставки. Ветку надо переучить на ту же сегментную проверку, что и AdvanceFreeFlying. Без этого фича выглядит сломанной ровно там, где её будут смотреть первым делом.

Шов 2 — применение площадного урона. Фильтр ставится в точках нанесения, а не внутри QueryUnitsInRadius / QueryUnitsInLine: те же запросы используются для поиска целей, условий каста и союзных аур — отфильтруешь там, сломаешь выбор цели и хилы. Точки:

  • AbilitySystem.ApplyCircle — круговой АОЕ → IsShieldedFrom;
  • линейная авто-атака (AutoAttackSystem, «Размашистый выпад») → TryBlockSegment, полоса обрезается по стене;
  • ApplyAllWithTagне фильтруется: масс-каст по тегу геометрии не имеет (§9.10), барьер по дизайну его не ловит.

Шов 3 — жизненный цикл. BarrierSystem.Tick: истечение срока, подтяжка позиции к носителю, гашение при смерти носителя и при CurrentHP <= 0, удаление из списка (тот же паттерн, что ProjectileSystem.RemoveDead).

Шов 4 — контекст. В ICombatContext: SpawnBarrier(in BarrierSpawn spawn) и fire-and-forget ReportBarrierHit(...) для презентации (по образцу ReportAreaHit — не мутирует сим, детерминизма не касается).

Шов 5 — данные. BarrierComponent : IRuntimeEffectComponent — эффект ставит барьер, прочность через ScalableValue (скейлится статами источника, как ShieldComponent). Никакой новой ветки авторинга: барьер приезжает обычным EffectData.

Шов 6 — AI. Блок скоринга «линия огня перекрыта» + опция «бить сам барьер» + предпочтение цели под куполом у мили. Едет вместе с позиционикой, в её же Filter / Score / Override. Это половина всей работы и главный риск фичи — см. требования в дизайне.

Шов 7 — презентация. BarrierView на Shapes: геометрия, полоска прочности, рябь в точке попадания по ReportBarrierHit, заметное гашение при разрушении.

Что НЕ трогаем

  • Навигация. Тела ходят сквозь барьеры, SeparationSystem и MovementSystem о них не знают. Зона контроля отклонена, барьеры её не возвращают.
  • DamagePipeline. Барьер перехватывает до пайплайна: брони, школы и сродства он не касается, поглощает урон после эффективности источника. Пайплайн остаётся чистым.
  • SpatialHash. Барьеров на арене единицы; линейный обход дешевле индекса и детерминирован без оговорок. Если когда-нибудь станет десятки — тогда и подумаем.

Тесты (EditMode, рядом с ProjectileTests)

  1. Свободнолетящий снаряд гаснет о вражескую стену, урон ушёл в стену, цель цела.
  2. Самонаводящийся снаряд гаснет о стену (регресс-тест на гочу трекинга).
  3. Снаряд команды-владельца проходит сквозь свой барьер.
  4. Выстрел изнутри купола наружу проходит.
  5. Круговой АОЕ с центром снаружи не задевает цель под куполом; с центром внутри — задевает.
  6. Стена не блокирует круговой АОЕ.
  7. Хил-снаряд проходит сквозь любой барьер в обе стороны.
  8. Источник со свойством «игнорирует барьеры» проходит насквозь.
  9. Разрушение пула: снаряд, добивший барьер, гаснет; следующий летит свободно.
  10. Истечение срока и смерть носителя гасят купол.
  11. Два барьера на траектории: гасится о первый по ходу движения; при совпадении — тай-брейк по Id.
  12. Детерминизм: два прогона одного боя с барьерами дают одинаковую чек-сумму.

Оценка

КусокУсилия
Ядро: данные, BarrierField, BarrierSystem, швы 1–5, тесты 1–12~1 заход
AI-осведомлённость (шов 6)вместе с позиционикой, отдельно не считается
Презентация (шов 7)~0.5 захода
Баланс: прочность, радиусы, число носителей в ростерепо петле баланса, не разово