Решили: GameConfig держит ссылки на StatsConfig и ClassBalanceConfig; скоупы и
editor-бенчи берут их оттуда. Ни RootLifetimeScope, ни CombatLifetimeScope больше не
сериализуют стат-конфиги своими полями.
Почему: дубль был не небрежностью разводки, а следствием: сериализованная ссылка живёт в сцене, а
автономных сцен две — CoreScene и CombatSystemsScene (standalone dev-арена обязана подниматься без
Root). Пока ссылка в сцене, дубль неизбежен, и выбор стоял между «дубль под присмотром теста» и
«отказаться от запуска боевой сцены без CoreScene». Оба варианта плохи: первый лечит симптом (тест
сверяет две пары полей, и каждое новое общее SO добавит ещё пару), второй платит рабочим инструментом
за чистоту.
Третий путь убирает корень: ссылка уезжает из сцены в ассет. Дубль ссылки на один GameConfig
остаётся, но это уже не второй владелец факта — расходиться нечему, потому что «какой StatsConfig
играет» решает ассет.
Заодно закрылись две вещи. armorK перестал иметь вторую копию (пункт C3 долга): боевой скоуп больше
не владеет StatsConfig вообще. И бенчи перестали угадывать — BalanceAssets брал «первый по алфавиту
ассет типа StatsConfig», то есть второй такой ассет молча увёл бы балансные отчёты на конфиг, которым
игра не играет. Это худшая форма расхождения: врёт не код, а инструмент измерения, и ошибка
выглядит как цифра в отчёте.
Цена, названная сразу: один уровень косвенности (gameConfig.Stats вместо поля) и GameConfig
становится корневым конфиг-узлом. Правило, чтобы он не стал свалкой: только ссылки и глобальные
скаляры, никакой логики.
Грабли:
- Тест был владельцем инварианта, которого больше нет.
SceneWiringTestsохранял «тот же ассет в обеих сценах». После миграции сцены о стат-конфигах не знают вовсе — и если бы я просто убрала оттуда имена полей, миграция забрала бы охрану и не поставила ничего взамен. Поэтому пустоту ссылок внутри ассета теперь проверяетConfigValidationTests, сверяя их с единственными ассетами проекта. - Где искать компонент — спрашивают у Unity, а не у YAML. Grep имени типа по сцене находит и
parentReference(VContainer пишет туда имя РОДИТЕЛЬСКОГО скоупа строкой), поэтому по имениRootLifetimeScope«находится» в трёх сценах, а физически живёт в одной. Надёжный текстовый признак — guid скрипта вm_Script: он есть только там, где есть компонент. Так и подтвердилось: боевой скоуп — только вCombatSystemsScene. - Компиляцию в момент правки роняли чужие ошибки параллельной сессии (
IStatWriter,PhysicalSubtype). Свой код проверяется отдельно —validate_scriptпо каждому файлу; ждать зелёной сборки, чтобы узнать про свою опечатку, не нужно.
Владелец правды: Assets/_Project/Scripts/Data/Definitions/GameConfig.cs (свойства Stats,
ClassBalance с <remarks> о причине), тесты ConfigValidationTests.GameConfig_PointsAtTheProjectBalanceConfigs
и SceneWiringTests.