Решили: оба порога «Изморози» судят по снимку начала тика (ctx.Stacks), а не по живому числу
(ctx.Effect.Stacks); запрет на прямую форму живого чтения в компонентах эффектов держит гейт
EffectStacksVisibilityGateTests.
Почему: это второй раз, когда открывается ровно эта дыра. Первый — «Угли», развели зеркало на тике
300 и породили сам закон видимости; тогда починили один компонент и записали правило в
EffectContext.Stacks. «Изморозь» несла ту же ошибку в обоих порогах и не была покрыта ни одним
тестом. Отвергнутая альтернатива — «починить и здесь, оставив правило комментарием»: комментарий стоит
в EffectContext, а нарушают его в чужом файле, где ctx.Effect лежит под рукой и выглядит тем же
самым числом. Инвариант кросс-файловый, значит его место в тесте.
Гейт намеренно ловит только прямую форму .Effect.Stacks. Обращение через локальную
(var eff = ctx.Effect; eff.RemoveStacks(1); if (eff.Stacks <= 0)) — это управление сходом стаков, и
там живое число единственно верное. Полного статического анализа у нас нет, и заводить его ради этого
правила дороже, чем оно стоит: наивный путь закрыт, а изощрённый в этот класс ошибок не приводит.
Грабли: дыру нашла панель аудита, а не тест и не игра, — потому что видимого симптома у неё нет.
Разойтись могли только два клиента коопа или два прогона зеркала, а зеркальные тесты «Изморозь» не
гоняют: у неё вообще не было тестов. Сценарий отказа при этом бытовой — цель на девяти стаках, десятый
кладут в тике N, и ледяные удары этого же тика уже усилены; величина зависит от круга
ResolveCombatRounds, в котором разрешён удар.
Владелец правды: Combat/Effects/Components/FrostComponent.cs, Combat/Effects/EffectContext.cs;
тесты FrostTests, EffectStacksVisibilityGateTests.