Решили: размер боевого VFX имеет ровно одного владельца — VfxData.SizeUnits, размер крупнейшей частицы в мировых единицах. Префаб задаёт пропорции частиц между собой, PooledVfx приводит их к абсолюту через масштаб трансформа, а единственный рантайм-множитель переименован из «интенсивности» в EvaluateHitVfxSizeMultiplier и живёт в feel-конфиге. Инвариантов четыре, все в VfxSizeContractTests: кривая sizeOverLifetime обязана где-то достигать 1.0; корневой localScale префаба равен единице; scalingMode не Local; бёрст гарантирует хотя бы одну частицу. Пятый тест печатает реестр размеров в пикселях боевого кадра — то, чего не было видно ни из префаба, ни из SO.

Почему: у размера было четыре владельца — startSize префаба, скрытая кривая, множитель в SO и множитель силы удара. Каждый по отдельности выглядел осмысленно, а произведение давало искру в два пикселя при заявленных в инспекторе 0.09. Отвергнута «общая VfxFactory»: CombatVfx уже фабрика (пул, спавн, применение SO), и второй слой поверх не убрал бы ни одного множителя — течёт не спавн, а число. Порог _vfxHitSizeMultMin поднят с 0.35 до 0.7: втрое урезанный размер снова уводил эффект в невидимость, и Range у поля начинается с 0.3, чтобы 0.35 нельзя было вернуть случайно.

Грабли:

  • burst.count.constantMin при режиме Constant всегда ноль — это неиспользуемое поле, а не «бёрст может выдать нуль частиц». Все наши бёрсты именно Constant, то есть количество фиксированное. Из-за этого запись в constantMin молча ничего не делает: правка «поднять минимумы» в коммите ae8ebd2ca была пустой, а вывод предыдущей записи журнала про «удар иногда без искр» — неверен. Гарантированный минимум читается по режиму: Constantconstant, TwoConstantsconstantMin.
  • Кривая Vfx_CastBurst/Core растёт (расширяющееся ядро) и упиралась в пик 0.90 — заявленный startSize был недостижим и там. Нормирована в 0.389 → 1.00: форма роста та же, пик честный, а SizeUnits уменьшен на те же 10% (0.77 → 0.69), чтобы вид не изменился.
  • compile-check.ps1 без -All даёт ложную ошибку, если редактор давно не компилировал: он ссылается на DLL из Library, и зависимая сборка не видит свойства, добавленного в другую сборку свежим коммитом. Здесь это выглядело как UnitSnapshot не содержит IsSwinging при том, что свойство в коде есть. -All собирает своими выходами и проходит.
  • Сканирование AnimationCurve дробным аккумулятором (for t += 0.05f) не доходит до t = 1, где у растущей кривой стоит пик. Шагать целыми.

Владелец правды: VfxData.SizeUnits (абсолют), PooledVfx.ResolveTransformScale (приведение пропорций), CombatFeelConfig.EvaluateHitVfxSizeMultiplier (единственная динамика), тест VfxSizeContractTests (все четыре инварианта и реестр в пикселях).