Решили: страховки «ассет равен код-дефолтам» перечисляют поля SimTuning рефлексией, а не
списком в тесте; связь двух лестниц дальности (AttackRangeBand и CastRangeBand) держит отдельный
тест.
Почему: рукописный перечень полей в тесте — сам по себе второй владелец факта «из чего состоит конфиг». Он отстаёт молча: проверялось 14 полей из 32, и восемнадцать позднейших ручек (смещение, овертайм, спринт, рекаст, маскировка, комбо, отступление) могли разъехаться между ассетом и кодом, не потревожив никого. Цена расхождения тут не косметическая: балансный стенд и зеркальные тесты гоняют бой на код-дефолтах, а игра — на ассете. Отвергнутая альтернатива — «дописать восемнадцать строк»: она чинит сегодняшний разрыв и оставляет механизм, который породит следующий.
Лестницы дальности связаны одной строкой приведения в RuntimeUnitFactory
((AttackRangeBand)(ability.CastRange - 1)). Компилятор такого приведения не проверяет, поэтому
ступень, добавленная в один enum и забытая во втором, сдвинет дальность каста у всех умений разом —
и выглядеть это будет как «лучник почему-то кастует с дистанции метателя».
Грабли: дрейфа на момент правки не было ни в одном из тридцати двух полей, то есть найти это через красный тест было нельзя в принципе — только пересчитав, что именно страховка покрывает. Обе дыры пришли из панели аудита, а не из игры.
Владелец правды: Core/Simulation/SimTuning.cs, Data/Definitions/{AttackRangeBand,CastRangeBand}.cs;
тесты ConfigValidationTests.SimTuningConfig_MatchesCodeDefaults,
ConfigValidationTests.RangeBands_CastLadderMirrorsAttackLadder.