Решили: поглощение щита получило автора. Реестр тика отдаёт наружу новый вызов
ITickLedgerSink.OnShieldAbsorbed(target, absorbed), сим раскладывает поглощённое по держащим
эффектам пропорционально удерживаемым долям и поднимает событие OnShieldAbsorbed(автор, носитель, сколько). В таблицах рейтинга рядом с HealDone встали ShieldHeld и SupportHPS — лечение и работа
щитов вместе, на секунду боя.
Почему: «сколько кит нахилял» не отвечалось для тех, кто не лечит вовсе. Монах воды в отряде 4v4 выигрывал 96.15% боёв со строкой «Лечил 0», потому что его поддержка — щит на 600 (на себя 900) раз в семь секунд, а поглощённое им копилось в метрике ЦЕЛИ и в рейтинг не выводилось совсем. Кит с лучшим винрейтом ростера выглядел китом без поддержки.
Отвергнутая альтернатива — считать ВЫДАННЫЙ щит вместо поглощённого: дешевле (величина известна на наложении, шва не нужно), но награждает за холостой каст. Щит, который никто не пробил, команде ничего не сберёг. Вторая отвергнутая — списывать щиты по порядку («сначала верхний»): порядка в модели нет, тик поглощает урон разом, и «первым съели вот этот» было бы решением, которого никто не принимал.
Грабли:
- По дороге нашёлся дефект щита, который прятал кламп в ноль.
RuntimeEffect.HeldShieldпомнил ВЫДАННОЕ, а не оставшееся: поглощение уменьшало общий пул и не трогало счёт эффекта. Два щита по 600, поглощено 600 — истечение первого вычитало из пула его полные 600, и второй исчезал целиком, аMathf.Max(0, …)делал это беззвучным. Теперь есть глаголSpendHeldShield, и списывается ровно израсходованное. Исход боёв со щитами от этого сдвинулся — это исправление, а не побочный эффект. - Щит без держащего эффекта остаётся без автора и в счёт не идёт. Отдать его первому попавшемуся было бы хуже, чем не отдать никому: неверная атрибуция читается как факт.
Владелец правды: TickLedger.Resolve, CombatSimulation.OnShieldAbsorbed,
RuntimeEffect.SpendHeldShield, тест ShieldAttributionTests.