Решили: облачение (OutfitData) прячет часть тела, обнуляя её спрайт, а не выключая рендерер; надевается в UnitView.InitVisual и зовётся ДАЖЕ когда облачения нет; раздаётся юнитам по механике, а не по списку имён.

Почему: заказ «щиты только у носителя» — первый частный случай системы оружия (Planning - Weapon System §3). Отвергнуты: отдельный префаб-вариант без щита (хореография «меча и щита» от наличия щита в кадре не меняется, а вариантов пришлось бы плодить по числу сочетаний) и отдельный архетип «только меч» (это дубль набора клипов ради одного пустого спрайта).

Грабли:

  • enabled — чужой канал, и он бы всё сломал. SetVisible гасит и зажигает ВСЕ части разом: разлёт осколков, возврат вида в пул. Спрячь щит выключателем — первый же возврат видимости надел бы его обратно, причём не сразу, а через смерть-возрождение, то есть выглядело бы как «иногда щит появляется». Спрайт этим каналом не пользуется, поэтому прячем им.
  • Облачение обязано зваться и когда его НЕТ. Вид переиспользуется пулом: без явного возврата к префабным спрайтам Защитник, занявший вид убитого лучника, вышел бы на арену без щита. Отсюда снимок _bareSprites — точка возврата, снимаемая один раз до первого облачения.
  • Два разных «ничего» пришлось развести явно. Записи о части нет — часть не трогаем, играет префаб. Запись есть, а спрайт пуст — часть прячем. Без этого различения «облачение» стало бы полной заменой тела, и каждое пришлось бы описывать целиком.
  • Раздача по механике поймала мою же ошибку в первый прогон. Гейт (OutfitCoverageTests) читает «кто носит щит» из кита — у кого эффект-«Оплот» с BlockComponent, — и сразу нашёл дев-дуэлянта: у него кит ЗАЩИТНИКА, а я надела на него «только меч». Список имён такого не заметил бы никогда, а в игре это выглядело бы как «боец блокирует пустой рукой».
  • Брузер тоже блокирует, но щита у него нет — парирующая стойка. Поэтому признак сужен до эффектов с «Bulwark» в имени: блок вообще ≠ блок щитом.
  • Программное CreateAsset не зовёт OnValidate, поэтому id контент-ассета остаётся пустым. Заполнять тем же ContentDomains.MakeId, что и автозаполнение, — иначе ассет попадает в базу с пустым id и валидация ругается уже на нём.
  • Новый тип контента — это ТРИ места, а не одно: класс, ContentDomains (тип → домен) и ContentPaths (домен → папка). Пропустишь второе — ассет не получит id; пропустишь третье — менеджер положит его в Misc.

Владелец правды: OutfitData (контракт «пусто = спрятать»), SkeletalBodyVisual.ApplyOutfit (канал спрайта и точка возврата), тест OutfitCoverageTests (обе стороны шва: имя части существует, щит совпадает с механикой).