Решили: поглощение щита получило автора. Реестр тика отдаёт наружу новый вызов 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.