Решили: тип урона — единственная ось, которую задаёт автор контента, и он обязателен у каждого
источника (автоатака, каждая способность, каждый бьющий компонент эффекта). Школа брони выводится
из типа функцией 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 сродство».