Решили: «враг применил способность» стало боевым событием CombatEvent.AbilityCast, и оно единственное в очереди доставляется НЕ одному носителю, а всем живым врагам кастующего (рассылка — в CombatSimulation.DrainEventQueue). Щит с фильтром по школе живёт компонентом SchoolShieldComponent со своим пулом в эффекте, а не новым полем в RuntimeUnit.

Почему: реакция на каст висит на противнике, а кастующий про неё ничего не знает — значит либо событие рассылает очередь, либо каждый реактив сам обходит список юнитов. Второе отвергнуто: обход внутри компонента делает порядок юнитов частью исхода боя, а это ровно тот класс дефектов, который ловит зеркальный тест.

Со щитом развилка была между тремя вариантами: (1) общий CurrentShield — школы не различает, щит от магии съедал бы физические удары; (2) второй и третий пул в RuntimeUnit — плодит поля под каждую будущую школу, а пайплайн урона придётся учить порядку их трат; (3) поглощение через pre-damage. Выбран третий: хватило запаса — удар отменяется, не хватило — множителем срезается покрытая доля. Арифметически тот же вычет, но без нового понятия в ядре.

«Перегрузка» считает ПОГЛОЩЁННЫЙ урон (RuntimeUnit.AbsorbedByWard), а не размер поднятых щитов — в отличие от взрыва щита, который платит за прочность. Разница смысловая: взрыв награждает за то, что щит пришлось пробивать, ответ Антимага — за то, что в него реально били магией.

Грабли: удар, съеденный фильтрованным щитом полностью, для реактивов «на удар» выглядит отменённым — шипы о него не колются, как и о любой негейт. Цена принята. И расширение ICombatContext чинится в семи тестовых заглушках руками: дефолтная реализация в интерфейсе спрятала бы шов от читателя.

Владелец правды: CombatSimulation.ReportAbilityCast/DrainEventQueue, SchoolShieldComponent.cs, OverloadStrikeComponent.cs.