Решили: «спящих не выбирают целью» живёт в самом целеуказании (ProfileBrain.PickTarget, рядом с
таким же правилом для скрытности), а не в эффекте сна. Исключение для того, кто спящих ищет
намеренно, объявляется ДАННЫМИ: PreferTagged + TargetTag = Sleep.
Почему: сон трогает выбор цели у всех китов, включая ещё не написанных. Свойством эффекта это не
выразить — эффект висит на цели, а решение принимает чужой мозг. Альтернатива «настроить каждого кита
руками» отвергнута сразу: она разъезжается на первом же новом ките, и разъезд молчит. Исключение
через данные, а не if (relic.Id == "relic.dreameater"), по той же причине: правило должно работать
для любого будущего охотника за спящими.
Сон собран из существующего ControlComponent (запрет действий) плюс новый SleepComponent — только
выход из сна. Разделение важно: «что запрещено» уже умеет система контроля, а «что рвёт сон» —
свойство именно сна, и его цена (пробуждающий удар получает ×2) должна быть в одном месте.
Своя позиция в EffectTag понадобилась дважды: диспел по Control уносил бы заодно оглушения, а
целеуказанию нужно отличать спящего от обездвиженного.
Грабли: нарастающий DoT считает разгон из ПРОЖИТОГО времени эффекта (BaseDuration минус
остаток), а не из числа сработавших тиков — счётчик пришлось бы держать в RuntimeEffect, а
компоненты stateless. Побочное следствие: подкрепление, обновившее длительность, сбрасывает разгон.
Для «Кошмара» это верно по смыслу (сон начался заново), но для другого нарастающего эффекта может
оказаться неожиданным — поэтому названо в тултипе поля.
Вторые грабли операционные: ассет с [SerializeReference] на НОВЫЙ тип, импортированный до того, как
редактор скомпилировал этот тип, получает null в компонентах — и ловит это только
ContentValidationTests. Причина была в отсутствующем .meta у нового .cs: без него редактор файл
не видел вовсе. Лечится .meta + refresh + повторный ImportAsset(ForceUpdate) ассета.
Владелец правды: ProfileBrain.PickTarget/HuntsSleepers, SleepComponent.cs,
PeriodicDamageComponent.Growth.