Решили: тип урона — единственная ось, которую задаёт автор контента, и он обязателен у каждого источника (автоатака, каждая способность, каждый бьющий компонент эффекта). Школа брони выводится из типа функцией DamageTypes.SchoolOf и отдельным полем не живёт нигде. Наследование типа от юнита (Inherit) снято целиком.

Почему: прежняя модель складывала тип из четырёх независимых полей — школа, физ-подтип, магическая стихия, сродство. Она позволяла выразить бессмыслицу («физический огонь»), молча гасила её нормализацией в конструкторе и, главное, допускала «просто физический» урон: поле подтипа оставалось дефолтным, и это не краснело ни в тестах, ни в консоли. Цена оказалась не в числах, а в связках: взрыв костяного щита Некроманта летел «физикой без подтипа» и не попадал в +20% хрупкости холодной статуи — состав «некромант + крио» молча не работал. Замер по контенту показал 16 источников с незаданной конкретикой, и по числу нельзя было отличить «магия, подтип неприменим» от «физика, автор забыл».

Отвергнутые варианты:

  • Оставить четыре оси, добавив валидатор. Валидатор проверяет то, что модель разрешает выразить; правило «подтип обязателен при физической школе» пришлось бы держать вторым владельцем рядом с нормализацией, которая делает противоположное — обнуляет ось.
  • Яд отдельным полем школы поверх одного значения Poison. Канон требует «школа по конкретному яду, Физическая или Магическая, но не Чистый» (решение 2026-07-25/4). Отдельное поле возвращает опциональность через заднюю дверь: правило «тип → школа» перестаёт быть тотальным, и «а тут спецом» однажды забудут снова. Поэтому яд занимает два значения — исключение видно в списке типов, а не спрятано в ветке кода.
  • Кровотечение как Pierce. Так оно и было заведено, и это врало дважды: рана не колет, а будущая «уязвимость к колющему» качала бы доты. Плюс «нежить не кровоточит» стало бы невыразимо ничем, кроме тега эффекта — то есть вторым владельцем.
  • Свет и Тьма как магия. Ошибка чтения канона с моей стороны: stats.md прямым текстом держит их Чистыми, мимо брони, и код так и был сделан.

Грабли:

  • Обязательность выражается тремя замками, и одного мало. Отсутствие дефолта в конструкторе DamageRequest ведёт компилятор по всем 14 точкам создания урона; Undefined = 0 шумит в консоль, потому что это дефолт любого незаполненного поля; но инвариант между файлами держит только тестDamageTypeCoverageTests сканирует весь контент и спрашивает тип у тех, кто бьёт. Именно этого замка не было, когда подтип у костей остался дефолтным.
  • Замок сразу нашёл, где болванки невалидны. Debug.LogError в конструкторе завалил 25 тестов: синтетика стенда и тестовые заготовки создавали бойцов без кита, у которых тип урона брать негде. Поэтому RuntimeUnit.AutoAttackDamageType стал полем — снимком кита на бой, как Stats, — и тот, кто создаёт бойца, обязан назвать тип сам. Ослаблять лог было нельзя: он и есть смысл реформы.
  • Тег типа может отсутствовать при живом типе. Bleed завели, а ассета tag.bleed не было — тип работал бы в бою, а чип на карточке не появился бы, и заметил бы это только игрок. Теперь расхождение ловит тот же тест покрытия.
  • Имена полей разъехались раньше модели: в большинстве компонентов ось звалась _physicalSubtype, в ShieldBurstComponent_subtype. Мигратор ассетов, написанный по первому имени, тихо пропускал второй компонент. Разъехавшиеся имена одного и того же — предвестник того, что поле забудут.
  • Способность без прямого урона законно остаётся Undefined. Урона нет — типа урона нет; тест спрашивает тип только у тех, у кого множитель больше нуля. Иначе автору пришлось бы выбирать тип для лечения.

Владелец правды: Data/Definitions/DamageType.cs (список типов и SchoolOf), тесты DamageTypeResolverTests (тотальность правила, яд не чистый, Свет и Тьма мимо брони) и DamageTypeCoverageTests (ни один бьющий источник в контенте не без типа). Канон-дом дизайна — Combat - Stats §«Школа vs сродство».