Статус: Living — живой реестр, правится при закрытии пункта.
Сознательно отложенный технический долг и главная будущая таска. Живой документ — в отличие от Meta - Tech Changelog (архив историй) и папки
journal/(записи о принятых решениях, append-only).Выделено из
tech-changelog2026-07-30 при разделении журнала: история и открытый долг — разной природы, у первой нет владельца-в-коде, у второго есть срок жизни.
Легенда:
✅значит «баг существует», а не «исправлен»Пункт со статусом
✅ подтверждён по коду— это живой дефект; исправленные помечены словом ЗАКРЫТО с датой сверки. Перед починкой пункта всё равно сверяйся с живым кодом: реестр отстаёт по устройству, и это его нормальное состояние, а не поломка.Ресверка §3.8 сделана 07.08.2026 (панель аудита): C1, H1, H4, H6 закрыты, H10 неверен на текущем дереве, C2–C6 и H2/H3/H5/H7/H8/H9 живы. Сетевые пункты не сверялись — кооп в работе.
Нумерация разделов (§3, §4) сохранена от исходного tech-changelog — на подпункты ссылаются
внешние аудиты (docs/audits/) и якоря внутри этого файла. Переименование порвало бы ссылки без
выигрыша.
3. Открытый техдолг (сознательно отложено)
Эти пункты не баги прямо сейчас — это запланированный долг. Чинить, когда дойдёшь до соответствующей фазы, с тестом на руках.
3.0 ЗАКРЫТО 30.07: зеркало расходилось на хиле Друида
MirrorMatchTests.Mirror_SquadSeries_NeverDiverges(5) падал детерминированно на тике 543: у левого
Defender висел effect.spore_mend, у правого нет. Причина — чтение живого списка эффектов вместо
состояния начала тика: «Взрыв спор» считал уникальные яды на цели своим обходом ActiveEffects, а
чужой клинз, прошедший раньше в том же тике, яд уже снимал. Левый Друид успевал детонировать, правый
бил в пустоту — исход решало место юнита в обходе.
Починено тем же приёмом, что уже стоял в диспеле: счёт судит по началу тика
(EffectSystem.CountUniqueTaggedAtTickStart) — эффекты, легшие раньше этого тика, плюс снятые в этом
тике. Владелец правила один, локальная копия в AbilitySystem удалена. Держит
PoisonBurnThornsSliceTests.UniqueTagCount_CountsWhatWasThereAtTickStart.
Разбор — запись журнала «A Cleanse Must Not Rob A Detonation Of The Same Tick» (2026-07-30).
3.1 ⑩ Разбухание CombatSimulation (god-object)
CombatSimulation совмещает: тик-цикл, реализацию ICombatContext, очередь команд, хаб C#-событий, фабрику снарядов, расчёт checksum (~340+ строк). Пока читаемо, но это место будет пухнуть. Когда дойдёт (а не сейчас — иначе преждевременная абстракция): вынести очередь команд и checksum в отдельные классы. Приоритет — низкий, до появления реальной боли.
3.2 Аллокации в Stats.RebuildCache
Пять массивов на каждую пересборку кэша статов: четыре new float[StatCount] (flat,
percentAdd, multAccum, overrideVal) и один new bool[StatCount] (hasOverride) —
Combat/Stats/Stats.cs:210-214. (Сверено 07.08.2026; раньше здесь стояло «три» — число отстало от
кода, каскад с тех пор оброс Override-группами.) В Фазе 1 пересборка одноразовая (модификаторы
ставятся раз при создании юнита) — неважно. В Фазе 2 эффекты будут часто менять статы →
пересборки участятся → перевести на переиспользуемые инстанс-буферы. Триггер: подключение
стат-меняющих эффектов в контент.
3.3 Грубый alpha интерполяции презентации
CombatPresenter.Update берёт alpha = Time.deltaTime / TickDelta — это не корректная доля интерполяции (правильная — accumulator / TickDelta из CombatLoopService). Движение сглажено, но не идеально по фазе. Фикс: пробросить дробную часть аккумулятора из loop в презентер. Косметика, не срочно.
3.4 Таргетинг O(n²)
Выбор цели идёт полным перебором. На десятках юнитов это дешевле запроса к SpatialHash —
осознанно. Когда число юнитов вырастет (Фаза 3) — перевести на SpatialHash.QueryRadius.
Сверено 07.08.2026: пункт называл владельцем TargetingSystem, а такого класса в дереве нет с
Фазы 3 — его «поглотил» BrainSystem, и живой перебор сидит в ProfileBrain. TODO в коде, на
который ссылался пункт, тоже не нашёлся. Строка X3 ниже сообщала об этом же расхождении и потому
снята: реестр не должен спорить сам с собой.
3.5 Валидация контента эффектов
Правка ⑤ убрала утечку мгновенных эффектов рантайм-способом (OnApply→OnExpire). Но лучше бы ещё запрещать на этапе авторинга ставить stateful-компоненты на мгновенный эффект (валидация в EffectData/редакторе). Аналогично — предупреждать о бессмысленных комбинациях. Задача редакторного тулинга (Фаза 4, Odin-авторинг).
3.6 Вампиризм применяется ко всему урону
Правка ② вешает lifesteal на любой нанесённый урон (включая DoT/шипы), т.к. у урона пока нет тегов источника. Сузить до авто-атак/способностей — когда появятся теги источника урона (Фаза 2).
3.7 Примирить LifestealComponent под моделью B
Реактивный LifestealComponent лечит напрямую, минуя стат — под моделью «эффекты кормят статы» он избыточен и создаёт риск двойного учёта (стат-путь + компонент на одном юните). Пока только помечен баннером (на нём держится ReactiveEffectTests). Когда начнётся авторинг контента: удалить компонент, заменить вампирик-баффы на StatModifierComponent(+Lifesteal), мигрировать тест (оставить в нём ThornsComponent как пример реактивности). Триггер: первый реальный контент с вампиризмом.
3.9 Атака из нескольких Ударов — СДЕЛАНО 2026-07-30/31, открыты четыре следствия
Реализовано (коммит 858cd5413, журнал «An Attack May Be Several
Hits»): клип размечается несколькими маркерами
Hit, AttackTiming.ContactTicks раскладывает их по тикам свинга, AutoAttackSystem.Resolve
разрешает каждый контакт своим расчётом урона и своими on-hit, доля силы каждого Удара задаётся в
UnitData._hitDamageShares. Рефанд кулдауна получает только пустой свинг. Первый носитель — Монах,
два маркера с долями 0.5 / 0.5.
Раздел оставлен ради вердиктов и открытых следствий ниже; описание «как зашито было» снято, чтобы не читалось как текущее состояние.
Вердикты Макса (2026-07-30), четыре из четырёх:
- Досягаемость проверяется один раз на свинг — так и оставляем. Промах по убежавшей цели это не
дефект, а событие:
Evade, и оно должно явно писаться (боевая цифра / лента, как остальные исходы). Дизайнерски это подарок: кайт становится видимой контригрой против длинных серий. - Контроль прерывает анимацию: сделал один удар из двух — второй не доигрывает. Требование при этом жёсткое: микростаны не должны рекастить авто-атаку и тем ускорять противника. Решение — ниже.
- Баланс — отдельным заходом, когда тему будут реализовывать в тех-плане. Здесь только зафиксировано.
- Детерминизм — целочисленные тики, подтверждено.
Решение по (2): якорь интервала — ПЕРВЫЙ контакт свинга.
Механизм ускорения существует и найден в коде: Interrupt делает рефанд — AttackCooldownTicks = 0
(AutoAttackSystem.cs:442). Для одиночного удара это честно: свинг не нанёс ничего, значит и не
потерял ничего. При серии — эксплойт: первый контакт уже записан, микростан рвёт свинг, кулдаун
обнуляется, юнит выходит из стана и начинает новый свинг снова с первого контакта. Частота
первого удара становится «стан + замах» вместо интервала.
Правило, снимающее это одной строкой: рефанд положен только пустому свингу.
| Состояние свинга при прерывании | Что делаем |
|---|---|
| ни одного контакта не разрешено | как сейчас: AttackCooldownTicks = 0, потери нет |
| хотя бы один контакт разрешён | рефанда нет; кулдаун идёт от первого контакта свинга |
Тогда период «первый контакт → первый контакт следующего свинга» равен интервалу всегда, сколько бы ударов серии ни съел контроль. Микростан отнимает у жертвы хвост серии, то есть замедляет её — ровно то, чего от контроля и ждут.
- Кулдаун остаётся замороженным на время стана (как сейчас:
continueдо его декремента). Альтернатива «пусть тикает под станом» делает короткие станы бесплатными по темпу и обесценивает контроль как механику. Evadeсчитается состоявшимся контактом. Связка с вердиктом (1): если промах оставить «пустым», свинг получит рефанд, и кайт начнёт ускорять атаки того, от кого убегают. Контакт засчитывается по факту разрешения, а не по факту попадания.- Инвариант «стан не ускоряет атаку» живёт тестом, а не комментарием: он кросс-файловый
(
AutoAttackSystem+AttackTiming+ длительность контроля из эффектов).
Ещё вопросы, которые серия трогает молча. Первые четыре Макс закрыл в тот же день, остальные ждут захода.
- Два контакта в одном тике — РЕШЕНО (2026-07-30). Кадры контакта считаются в нормированном
времени клипа (
ClipMarkers.MarkerNormalizedуже это умеет) и умножаются наattackDurationTicks— серия сжимается вместе со свингом. Дальше целочисленное округление и раздвижка:contact[i] = max(contact[i-1] + 1, computed[i]), верхний кламп — последний контакт не позжеattackDurationTicks − 1(тот же принцип, что у нынешнего клампа замаха: удар не совпадает со стартом следующей атаки).- Почему это обязан быть рантайм-инвариант, а не правило авторинга: свинг сжимается
(
attackDurationTicks = min(MaxAttackAnimTicks=30, intervalTicks)), а скорость атаки — величина рантайма, её крутят баффы. Любые два маркера при достаточном сжатии схлопнутся, и валидатор в редакторе этого не увидит. Аудит клипов остаётся второй линией: предупреждать, если маркеры стоят ближе тика уже при эталонном темпе класса. - Следствие — потолок скорости для многоударных китов: N контактов требуют минимум N тиков
свинга, то есть
атак/сек ≤ TickRate / N. При наших темпах (0.55–1.0 атак/сек, свинг 30 тиков) три контакта влезают с запасом в десять раз; правило записано, чтобы не всплыло внезапно на бафах. - Отбрасывать лишние контакты молча нельзя: при вердикте «on-hit на каждом контакте» потерянный контакт — это потерянный стак, то есть тихий нерф. Если контакты не влезли, это событие для отчёта аудита анимаций.
- Почему это обязан быть рантайм-инвариант, а не правило авторинга: свинг сжимается
(
- On-hit на каждом контакте — РЕШЕНО (Макс, 2026-07-30): да, на каждый. Поджоги, метки, стаки льда Криоманта множатся числом ударов сознательно. Прямое следствие — пункт 1: терять контакт нельзя, и балансный заход должен считать стаки за свинг, а не за удар.
- Что считается «первой атакой» для заявки о скейлах первой атаки (Combat - Stats §На будущее): бонус «первая атака по цели» при серии либо срабатывает один раз на свинг, либо утраивается. Дефолт скрайба — свинг считается одной атакой; Макс заявку не помнил, вердикта пока нет, но по умолчанию берём этот.
- Каст между контактами — РЕШЕНО (Макс, 2026-07-30): нет, только с новой атаки. Занесённый свинг доигрывает целиком, способность не втискивается между контактами. Подтверждает существующее правило M18 и распространяет его на серию.
- Charge-свинг, стартующий вне досягаемости и въезжающий в неё: дистанция меняется между контактами.
- Hitstop и slowmo — три замирания подряд внутри одного свинга читаются как лаг; политика значимости должна знать про серию.
- Звук — три сэмпла подряд рискуют звучать пулемётом.
- Вампиризм и «дань» множатся автоматически — следствие пункта 2, отдельного решения может не потребовать, но проверить надо.
Что осталось открытым: пункты 3 и 5–8 выше — «первая атака по цели» при серии (дефолт скрайба принят по умолчанию), charge-свинг с меняющейся дистанцией между контактами, политика hitstop/slowmo, звук и множители вампиризма. Все четыре всплывут на подаче и балансе, а не в симуляции.
Владельцы правды в коде: AttackTiming.ContactTicks (раскладка контактов и оба инварианта),
UnitData.HitDamageShare, тесты WindupAutoAttackTests группы «Атака из нескольких Ударов» и
AnimationValidationTests.HitDamageShares_MatchMarkerCount. Инвариант «стан не ускоряет атаку»
держит WindupAutoAttackTests.Stun_AfterHit_DoesNotRushNextAttack.
3.10 Четыре механики ждут общих примитивов, а три уже размножены копиями
Сверено по коду 2026-07-31 при разборе заметок Макса о ритме атаки, парировании, блоке, маскировке и рекасте. Дизайн-развилки и вопросы — Meta - Open Forks §1; здесь только то, что видно из кода: где сегодня два владельца одного правила.
| Правило | Где живёт сейчас | Куда должно уехать |
|---|---|---|
СДЕЛАНО 2026-07-31: один глагол RuntimeUnit.RecastAttack, объявляемый флагом EmpowerNextAttackComponent._recastImmediately; ускорение фаз — две ручки SimTuningConfig. Авто-рекаст «по факту урона у активки» удалён, инвариант держит AbilitySystemTests.DamagingAbility_DoesNotSkipTheAttackQueue_RecastMustBeDeclared | — | |
| Реакция на входящий удар | три IPreDamageComponent с зарядами: BlockComponent (переименован из BulwarkComponent 2026-07-31), DodgeComponent, ParryComponent (2026-07-31). Заряды, ICD, фильтр прямого попадания и гейт дееспособности у каждого свои — скопированы трижды | вынести общий каркас «заряд + триггер + ICD», оставив компонентам только их реакцию. Пока это три копии одного пролога, а не три механики |
СДЕЛАНО 2026-07-31: общий ассет effect.micro_stun (0.4 с), на него ссылаются и «Вихревой заход», и парирование | — | |
| Счёт «каждая N-я атака» | EveryNthAttackComponent считает по событию урона, то есть по Ударам | серия уже в движке (§3.9), поэтому долг стал живым: Драугр на многоударном ките триггерит втрое чаще и молча. Считать Атаки |
СДЕЛАНО 2026-07-31: ConcealmentSystem считает, кто скрыт; фильтр стоит в единственной точке выбора цели (ProfileBrain.SelectBest). StealthComponent по-прежнему отвечает только за то, когда Убийца уходит в тень | — |
Все четыре механики темы сделаны 2026-07-30/31: ритм (CombatIdle), Атака из нескольких Ударов,
Блок, рекаст, парирование и Маскировка.
Два долга, оставшихся от парирования:
- Носителя нет. Примитив
effect.parryне объявлен ни одним китом, то есть в бою не участвует. Заявленный носитель — Разбойник-дуэлянт, у которого пока нет карточки (Meta - Open Forks). - Разгон не плавный. Дизайн требует входа и выхода «не рывком», а
effect.parry_hasteдаёт плоскую прибавку на секунду. Рампа в проекте есть ровно одна — у спринта (SimTuning.SprintRampAt), и она живёт в движении, а не в статах. Общий «баф с рампой» — отдельная задача, и до неё парирование ускоряет щелчком.
Долг Маскировки: носителей, кроме Убийцы, нет — Инвиз стоит на его «Скрытности», а ступени Слабая, Средняя и Сильная не объявляет никто. И не решено, что делают враги, когда невидимы все: сейчас они просто остаются без цели, то есть бой из двух замаскированных отрядов встаёт (Meta - Open Forks).
Остаток темы — контент, а не механика: обе новые системы ждут китов-носителей.
3.11 У скелетного рига нет клипа смерти — в слоте стоит Idle
Заглушка поставлена сознательно 2026-07-31 (решение Макса), и она видна в игре. У рига
(Prefabs/Bones/) есть Idle, Walk, Attack, Attack2, Attack3, AttackCharge, Block, CombatIdle, Sprint,
Stun — клипа смерти нет вовсе. Валидатор контента требует Idle/Run/Attack/Death у каждого
архетипа анимаций, поэтому в слот Death у AnimationArchetypes/SwordShield.asset положен Idle.anim.
(Имена обновлены 06.08.2026: UnitVisual → AnimationArchetypeData, папка Visuals →
AnimationArchetypes, ассет BoneStandart → SwordShield. Суть долга не изменилась.)
Что это значит на экране: путь смерти играет клип перед разлётом на осколки
(UnitView.DriveDeath, фаза Dying), то есть скелетный юнит перед распадом будет стоять в позе покоя
вместо падения. На дев-бойце это терпимо, на боевом контенте — нет.
Чинится заходом в Animation Lab: поза падения по риг-API, контактный лист на смотр, клип в слот Death. Пока клипа нет, заглушку не считать нормой — она стоит здесь именно затем, чтобы её не приняли за настройку.
3.12 LitMotion занят одним файлом — три места, где он окупится, и три, где он вреден
Сверено по коду 07.08.2026. Пакет подключён только к Guildmaster.Presentation.asmdef и вызывается
ровно из Presentation/UnitView.cs (вспышка удара, сплющивание, разворот, телеграф). CLAUDE.md уже
числит его «живущим точечно», а UI-анимации и боевые цифры — непереведёнными.
Куда стоит занять:
Presentation/FloatingText.cs— главный кандидат. Боевые цифры крутят свойUpdateс ручным_elapsed, самодельным ease-out-cubic и самодельным ease-out-back с овершутом (EaseOutBack(p, _popOvershoot)). Это пуловый объект, которых в кадре бывает десятками, и каждый несёт свойUpdate. Твин снимает его целиком, обе кривые есть вEase. Готча:ZoomFactor()пересчитывается каждый кадр (камера зумится), поэтому он остаётся ВНУТРИBind, а не берётся один раз на старте твина.- Числа, меняющиеся скачком: золото после награды, полоска опыта, вместимость запаса. USS-переход тут не поможет — он анимирует стиль, а не данные.
- UI-джус, которого USS не умеет: овершут посадки пластины, пружина карточки мементо при взятии,
тряска поля при отказе. Наведение, нажатие и появление панели USS закрывает сам и лучше — туда не
лезть. Потребует дописать
LitMotionвGuildmaster.UI.asmdef(сейчас ссылки нет).
Куда НЕ надо:
Presentation/Transition/ScreenTransitionRunner.cs— у негоTick(float dt)с явной длительностью кадра сделан ИМЕННО ради тестируемости («время — единственное, что у него снаружи»). Твин отнимет проверяемость и не даст ничего взамен.- Камера — ею владеет Cinemachine.
- Симуляция — детерминизм; там нет и не будет покадровых величин.
Открытая развилка: если джус UI откладывается надолго, LitMotion можно вынести целиком, как вынесли
Easy Save и Feel — один файл не окупает зависимость. Аргумент против: он zero-alloc, уже вшит в правила
слоёв, а FloatingText — работа на полчаса с видимым в бою результатом.
Владелец правды: Presentation/UnitView.cs (единственный нынешний носитель),
Presentation/FloatingText.cs (кандидат №1).
3.13 Сорок один журнал захода в docs/, и механически их не разобрать
Правило «леса сносятся по закрытии захода» не исполняется: в docs/ лежит 41 файл *-progress.md.
Гейт ./scripts/journals.ps1 -Check при этом зелёный, и это не случайность — он мерит возраст файла
по LastWriteTime, а два squash-мержа (34fecc74a от 28.07 и 67abad9c4 от 03.08) обнулили эту
дату сразу тридцати файлам; у двадцати четырёх squash — вообще единственный коммит в истории dev.
Разобрать пачкой нельзя, и это проверено на себе 07.08.2026. Признак «в файле нет незакрытых
чекбоксов» кажется годным и негоден: в восьми отобранных по нему файлах не было галочек вовсе, а по
содержанию среди них оказались skeletal-animation-progress.md (853 строки, статус «в работе»,
открыты шаги 5 и 7), ap-and-skills-progress.md (404 строки, статус proposed — числа ждут вердикта
Макса), performance-progress.md (начат за день до того) и meta-progression-progress.md, на который
ссылается живой журнал ГД. Снос по признаку потерял бы и незакрытые заходы, и вопросы, адресованные
человеку.
Снят по итогам ровно один — ui-layout-handoff-progress.md: он сам объявлял себя лесами,
перечислял закоммиченное и указывал два журнала, в которые уехала правда (оба на месте), и ни один
файл на него не ссылался.
Что делать:
- Разбор — заход с чтением каждого файла, а не скрипт. По каждому: заход закрыт? невыполненное
переносится в
tech-debtили в открытые развилки ГДД? кто-нибудь на него ссылается? Коопные журналы (coop-*,qa-coop) в этот заход НЕ берутся, пока идёт работа по сети. - Гейт починить — мерить возраст по дате последнего содержательного коммита
(
git log -1 --format=%ad -- <файл>), а не по mtime, и дополнительно ловить самообъявленное закрытие внутри файла. Порог по mtime вдобавок вот-вот сработает вхолостую: шесть журналов уже на 10–13 днях.
Владелец правды: scripts/journals.ps1, сам каталог docs/*-progress.md.
3.16 «Читает мир и пишет в него в одном обходе» — класс, давший четыре расхождения зеркала
Сверено 09.08.2026 после четвёртого случая. Система идёт циклом по юнитам, внутри цикла пишет состояние, а следующая итерация читает уже изменённый мир. Порядок обхода у отражённых сторон ОДИНАКОВЫЙ, а не зеркальный, — поэтому сторона, идущая первой, получает преимущество, и зеркало разъезжается.
Три первых случая породили сам сторож MirrorMatchTests (см. его <remarks>), четвёртый —
проход блинков в AutoAttackSystem (журнал
«One Path Knew About The Rake»).
Вылечены и служат образцом: MovementSystem (два прохода через буфер _next),
SeparationSystem (вклады копятся, применяются после обхода),
CombatSimulation.ApplyPendingTeleports (все точки от снимка, потом запись),
AutoAttackSystem (то же, с 09.08.2026).
Остался один кандидат — DisplacementSystem. В цикле по _active пишется u.Position, и там же
Cannonball читает мир: второй летящий в том же тике увидит уже сдвинутого первого.
Замером НЕ подтверждено: зеркальные тесты зелёные, включая дуэль Монаха вихря против себя, где
оба тела летят одновременно. Это подозрение по форме кода. Чинить — только после того, как найдётся
состав, который расходится: правка ради формы здесь стоит дороже, чем выглядит (ядро бьёт по линии
полёта, и порядок применения меняет, кого задело).
Как искать следующий случай — метод, который сработал: сузить составом (все пары и тройки из
расходящейся четвёрки), потом читать полный отчёт MirrorFixture — он печатает слепок обеих сторон
и журнал ударов тика. Гипотезы по коду до этого отвергались замером дважды подряд.
Владелец правды: MirrorMatchTests (сторож и история класса), Combat/Systems/MovementSystem.cs
(образец двух проходов).
3.15 Vector2.right фолбэком — одиннадцать мест, и каждое это мина под зеркалом
Найдено 08.08.2026 при разборе расхождения зеркала. В боевом коде одиннадцать вырожденных веток вида
dir = v.sqrMagnitude > 1e-6f ? v.normalized : Vector2.right. Правильно сделана ровно одна —
FleeSteering.HomeDir (unit.Team == 0 ? Vector2.left : Vector2.right): она выбирает направление
по стороне команды, поэтому у отражённых сторон получается зеркально.
Остальные берут мировое направление. Пока ветка недостижима, всё тихо; как только она сработает
у обеих сторон — обе поедут ВПРАВО вместо противоположных, и зеркало разойдётся. Ровно этот механизм
уже ловили в расталкивании (остаток BAL-014): там его вылечили DegenerateDir, и приём с тех пор
в проекте есть.
Семь двигают тело или решают исход — это и есть мины:
DisplacementSystem.cs:66 · AbilitySystem.cs:496 (бросок), :502 (рывок), :761 ·
ChargeThroughOnBattleStartComponent.cs:87 · ThrowAllyComponent.cs:85 ·
DodgeComponent.cs:124 (кувырок) · плюс CombatSimulation.cs:532 (скорость снаряда без цели),
:609 (направление линейного запроса) и CombatPositioning.cs:157 («в спину» — от него зависит
бонус за удар в тыл).
Три безобидны: AreaHit.cs:34 и WhirlDashLandingComponent.cs:76 — дев-оверлей и показ.
Чинить не пачкой. У каждого свой вопрос «а куда по умолчанию»: кувырку — вперёд или к тылу,
броску — от кого, запросу линии — есть ли вообще осмысленный дефолт или это признак того, что звать
его в таком состоянии нельзя. Пачка одинаковым HomeDir закроет тест и спрячет три разных решения
под одно.
Готча на будущее: достижимость ветки неочевидна. У кувырка стоит фильтр «только автоатаки»
(DodgeComponent.cs:72), из-за которого источник удара есть всегда, — то есть его ветка выглядит
мёртвой, хотя intent и позиция атакующего могут совпасть. Прежде чем объявлять такую ветку
недостижимой, нужен замер, а не рассуждение.
Владелец правды: Combat/FleeSteering.cs (HomeDir — образец), Combat/Systems/SeparationSystem.cs
(DegenerateDir — как это лечится), тест MirrorMatchTests.
3.14 Что панель аудита 07.08.2026 не посмотрела — и почему это важнее половины найденного
Восемь линз прошли по коду, данным, тестам и докам. Критик полноты назвал дыры; полный разбор —
docs/audits/2026-08-07/coverage-gaps.md. Здесь то, что стоит отдельного захода.
1. Строковый контракт UXML × Q<>("…") — второй по величине после локализации, и без стража.
148 строковых поисков элементов в Scripts/UI и Scripts/Presentation против 189 уникальных name=
в разметке. Компилятор к этому шву слеп: переименованный элемент даёт молчаливый null и мёртвую
панель в рантайме. Локализацию мы закрыли гейтом (UiLocalizationCoverageTests) — здесь нужен такой
же, и делается он тем же приёмом.
2. Префабы и сцены не читал никто. Двадцать префабов, пять сцен. Минимум два вердикта панели
висят на этом условно: «не проверял, назначен ли _unitViewPrefab в сценах билда» и
«_spatialHashCellSize проверен только со стороны кода». Незаполненный [SerializeField] — ровно
тот класс дефектов, который панель искала в коде и физически не могла увидеть в YAML.
3. Полоса, где теряются данные игрока. Все DTO сейчас [SaveSchema(1)], миграций нет: один бамп
версии — и каждый существующий сейв становится Unsupported. Сам сервис честен и громок, но куда
результат уходит дальше и что игра делает с Unsupported / TooNew / Corrupted, не проследил
никто. Экрана «сейв заблокирован» тоже нет — вместо него лог (GameFlow.cs:263-268).
Не смотрел никто: весь Art/** (2553 файла: шейдеры, материалы, импорт-настройки), 106 файлов
редакторного кода — того самого, который пишет ассеты, внутренности Presentation/** (20
Update на частоте рендера в мире, который по конституции не выгружается), кластеры
Tags/AiPresets/Keywords/Species/Outfits/Vfx, Tests/PlayMode/Battle.
Классы проблем, которых не искала ни одна линза: производительность и аллокации, утечки подписок
(в непрерывном мире это первый класс дефектов), импорт-настройки ассетов, мёртвые ассеты по guid,
состав билда, доступность (colorblind, fontScale, rebind — ноль вхождений во всём Scripts),
корректность редакторного тулинга.
§3.21 и §3.22 закрываются заходом по иерархии скоупов, принятым Максом 02.08.2026 — Planning - Scope Hierarchy §6. Отдельно их не чиним: корень у обоих один — у боевого скоупа нет границы боя. Очередь команд сносится в фазе 1 («никакого легаси»), сброс у гостя исчезает сам, когда лента начнёт рождаться вместе с боем.
3.21 Очередь команд симуляции осталась без единого пользователя
Открылось 02.08.2026 при переписывании сетевой паузы. CombatSimulation.EnqueueCommand,
ApplyDueCommands, ICombatCommand и обе его реализации (PauseCommand, ResumeCommand) существуют
с Фазы 1, но звал их только NetworkCommandRelay — удалённое lockstep-легаси. В продакшн-коде
пользователей теперь ноль; держат механизм два теста (CombatSimulationTests.PauseCommand_StopsTick
и соседний), то есть он проверяется ради себя самого.
Заодно от него зависит ветка в CombatSimulation.Tick: «пока в очереди есть команды, счётчик тиков
идёт даже на паузе, иначе ResumeCommand с будущим TargetTick никогда не наступит». Без очереди
эта оговорка не нужна.
Почему не снесла сразу: это правка боевого ядра, а не сети, и у неё есть дизайн-сторона — отложенная команда на будущий тик была бы швом под приказы игрока в бою (Manual Action Charges, ГД: Roadmap Блок 3, отложены Максом). Решение «снести и завести заново, когда понадобится» против «оставить пустой механизм» стоит вердикта, а не срезки в чужом заходе.
Что известно точно: пауза игрока — свойство показа (TimeScaleService), сетевая пауза — тоже
(BattleControlRelay), так что паузу через очередь команд не вернёт никто. Если механизм
оставляем, обе команды всё равно лишние.
3.22 Повторный бой у гостя: сбрасывать приёмник и реестр некому
Открылось 02.08.2026 при проводе коопа в бой. У гостя нет тикающей CombatSimulation, значит
OnBattleReset у него не стреляет — а на этом событии висят ровно те сбросы, которые нужны новому
бою: TapeIntake.Reset (забыть применённые номера чанков) и BattleUnitRegistry.Clear (забыть
состав). Хост при рестарте начинает нумерацию с нуля, гость примет чанк №0 за дубль и нового боя
не увидит вовсе — тихо, без единой ошибки в консоли.
На первый бой сессии это не влияет, поэтому и не блокирует: вертикаль проверяется одним боем.
Способ починки изменился 02.08.2026. Прежний план — слать сигнал «бой начался заново» и сбрасывать обе половины на приёме — больше не нужен: у гостя бой теперь тоже скоуп, а новый бой это новый скоуп. Приёмник и реестр рождаются вместе с ним, и забывать им нечего по устройству. Значит долг закрывается не сообщением про сброс, а сообщением про границы боя — «бой открылся / закрылся», — которое гостю нужно и само по себе: без него он не знает, когда открывать арену (шаг «мероприятие у гостя»).
Владелец правды: Net/Tape/TapeIntake.cs, Combat/Tape/BattleUnitRegistry.cs,
Net/BattleControlRelay.cs; состояние захода — docs/coop-slice-progress.md.
3.23 IBattleAuthority спрашивают каждый кадр, хотя роль уже известна на входе — ЗАКРЫТ 02.08.2026
Закрыт в тот же день, что и открылся. Состав боевого скоупа выбирается ролью сеанса (
CombatLifetimeScope.RegisterCoop): владелец получает петлю тика, стример, вещателя и объявителя состава, гость — приёмник чанков, его насос, приёмник состава иGuestPlaybackLoopвместо цикла.IBattleAuthority,NetBattleAuthorityиBattleRoleудалены;BattleRosterRelay, делавший обе стороны в одном классе, разделён наBattleRosterAnnouncerиBattleRosterIntake. Соло отдельной ролью НЕ стало: это тот же владелец, просто без поднятого транспорта — разница между «играю один» и «играю хостом» вся в том, есть ли кому слать. Иначе включение лобби посреди забега требовало бы переоткрыть сеанс. Единственная оставшаяся проверка живёт у вещателя и спрашивает соединение, а не роль.
Открылось 02.08.2026 вместе со скоупом Сессии. NetBattleAuthority выводит роль узла (соло / хост /
гость) из транспорта и отвечает на вопрос при каждом обращении; вокруг него живут ветвления в
CombatLoopService, BattleTapeBroadcast, BattleRosterRelay, TapeIntakePump.
Обоснование у этого было честное и записано в самом корне: боевой скоуп поднимался на буте, когда
сети ещё нет, — решить роль регистрацией было нельзя. Причина отпала. Бой рождается по требованию
внутри мероприятия, мероприятие — внутри сессии, а у сессии роль выбрана в момент открытия
(SessionRole) и за её жизнь не меняется: сменить роль можно только новым сеансом.
Значит боевой инсталлер может собирать РАЗНЫЙ состав по роли — вещатель ленты у хоста, приёмник у гостя, ни того ни другого в соло, — и ветвления исчезают вместе с самим интерфейсом. Заход трогает весь Net-слой и потому отложен: он не мешает ни соло, ни текущей кооп-вертикали.
Владелец правды: Core/Net/IBattleAuthority.cs, Net/NetBattleAuthority.cs,
Game/Session/SessionInstaller.cs; план уровней — 40-planning/scope-hierarchy.
3.8 Реестр внешнего аудита (2026-07-10)
Сведено из пяти аудитов (docs/audits/), дедуплицировано, каждый пункт сверен с рабочим деревом. Статусы: ✅ подтверждён по коду · 🔍 правдоподобно, нужна проверка · ⚠️ устарел/неверен на текущем дереве · 🔗 уже описан выше.
Подтверждённые баги (это НЕ отложенный долг — реальная корректность): — B1–B7 исправлены 2026-07-10, см. §2.5.
| ID | Баг | Где | Статус | Приоритет | Нашёл |
|---|---|---|---|---|---|
| B1 | Рестак щита пере-вычитает: Reapply зовёт OnExpire с новым Stacks, Mathf.Max(0,…) маскирует, но съедает вклад второго источника щита | EffectSystem.cs:375-406, ShieldComponent.cs:27 | ✅ исправлен §2.5 | P0 | Claude Opus (только она) |
| B2 | Рестак Dodge бесплатно перезаряжает все заряды негейта: OnApply безусловно new int[maxCharges], Reapply его зовёт. Нюанс: только Stack/StackAndRefresh, чистый Refresh не триггерит | DodgeComponent.cs:27, EffectSystem.cs:389-406 | ✅ исправлен §2.5 | P0 | Claude Opus |
| B3 | Корень B1/B2: Reapply = слепой OnExpire→OnApply ради смены числа стаков; per-component state живёт в общем RuntimeEffect. Фикс — OnStacksChanged(old,new) + вынести state | EffectSystem.cs:389-406, RuntimeEffect | ✅ исправлен §2.5 (дельта-шов; вынос state отложен) | P0 | Claude Opus |
| B4 | Kite-поля мёртвые: MoveKite использует магическое range*0.6f + AttackRange, а Kite.FleeDist/FallbackDist из AIProfile нигде не читаются. Данные выглядят рабочими — но no-op | MovementSystem.cs:85-100, AIProfile | ✅ исправлен §2.5 | P1 | GPT-5.6 |
| B5 | Broad-phase линейного запроса теряет цели в дальних углах: радиус length вместо length+halfWidth → юнит на (≈length, ≈halfWidth) отсекается до narrow-phase | CombatSimulation.QueryUnitsInLine (~:295) | ✅ исправлен §2.5 | P1 | Claude Opus |
| B6 | DrainEventQueue после капа MaxEventsPerDrain=512 делает silent Clear() — исход боя зависит от «уложились ли в 512». На текущем контенте 512/тик недостижимо → future-major, не critical-сейчас. Фикс: dev-assert + лог | CombatSimulation.cs:441 | ✅ исправлен §2.5 | P1 | Grok, GPT |
| B7 | Iron Spearman: _resourceType: None, но _resourceOnHit:5, MaxResource:30, активка стоит 30 — противоречивый контракт ресурса | IronSpearman.asset | ✅ исправлен §2.5 (Rage) | P1 | GPT-5.6 |
Консистентность данных (ретрофит дёшев, пока ассетов мало):
Гигиена / мёртвый код / швы:
Инфраструктура / инструменты:
| ID | Находка | Статус | Приоритет |
|---|---|---|---|
| I1 | run-tests.ps1 хардкодит Unity 6000.0.23f1, проект на 6000.4.8f1 → локальный прогон падает Unity not found. Читать версию из ProjectVersion.txt | ✅ исправлен §2.5 | P0 (5 мин) |
| I2 | Билд первой сценой ставит CoreScene (EditorBuildSettings), GPT утверждает что она пустая, а root — в BattleScene → boot-flow не замкнут. Запуск из BattleScene — осознанный dev-harness | ✅ снят 2026-07-26: CoreScene держит [Root] и [Bootstrap], обе игровые сцены грузятся с бута — flow замкнут (сцены проекта (Assets/_Project/Scenes/)) | P3 |
Устаревшее в аудитах / неверное на текущем дереве:
| ID | Клейм | Реальность |
|---|---|---|
| X1 | «EditMode красный, падает SerializeReferenceSpikeTests из-за пути Assets/Tests/EditMode» (GPT, Codex) | ✅ GPT/Codex правы на fd858f8 — тест молча падал (путь стал stale после переноса в 143535d). Починен в незакоммиченной арена-работе (:20 теперь Assets/_Project/Tests/…, suite 184/184). Т.е. старый клейм «171/171 green» был неверен; текущее рабочее дерево зелёное |
| X2 | «Арены нет в коде, ±200 хардкод, spawn из кода» (Composer) | ⚠️ Арена закодирована в рабочем дереве (uncommitted): ArenaLayoutData/ArenaBounds/DeploymentService |
| X3 | §3.4 этого дока ссылается на TargetingSystem | Учтено 07.08.2026: §3.4 переписан, класса нет с Фазы 3, перебор живёт в ProfileBrain. Строка оставлена как след: реестр полтора месяца спорил сам с собой в двух своих разделах |
Сетевой долг (host-auth vs lockstep-наследие NetworkCommandRelay, authority-гейт тика, полный порядок команд (tick,player,seq), float-checksum) — все 5 подтвердили; относится к 4. Главная будущая таска (вынесено отдельно), не трогаем в одиночке.
3.20 Бут-экран: геймпадом не закрывается, нажатие молчит, три ключа не заведены
Найдено 31.07.2026 при разборе старта игры; сверено по живому коду, взято в работу не было.
- С геймпада титул не закрыть — это блокер, а не мелочь.
TitleCardScreenViewслушаетPointerDownEvent,ClickEventиKeyDownEvent; геймпад в UITK приходит навигационными событиями, аNavigationSubmitEventне обрабатывается нигде в проекте (ноль вхождений при живомInputSystemUIInputModuleвCoreScene). Кнопки меню выживают только потому, что UITK-кнопка ловит Submit сама. Игрок с геймпадом застревает на первом экране игры. - Первое действие игрока не отвечает звуком. Появление титула озвучено
(
menu.title_card.stingerвRunAudioPresenter), а dismiss — нет, хотя остальные экраны звучат на действие (camp.action.ui,chest.open.stinger). ui.boot.title/ui.boot.hint/ui.boot.loadingв таблицеUIне заведены — экран держится на RU-литералах в коде.ui.boot.legalзаведён 31.07, соседние трогать не стали.- Мельче: спиннер декоративен (реального прогресса загрузки нет — шов понадобится, когда вырастет
контент), версия показана только в главном меню, курсор системный (
defaultCursorпуст), профиль создаётся молча как «Профиль 1» — экрана первого запуска нет.
Открытая возможность оттуда же: своя двухфазная заставка вместо Unity-сплэша (знак студии → титул, обе фазы скипаются) — см. Journal - Splash Shows The Studio Once.
4. Главная будущая таска (вынесено отдельно)
🎯 Сетевой кооп — host-authoritative (Фаза MP). Репликация состояния юнитов через NGO, клиент рендерит из реплик-состояния, гейт тика на хосте, релей команд для полного ввода, синхрон паузы, late-join/реконнект, сид боя от хоста. Полный состав — Journal - Host-Authoritative, Not Lockstep §4. Сейчас не трогаем (в одиночке сеть спит).
Уточнено 01.08.2026: авторитет хоста остался, а «репликация состояния юнитов через NGO каждый тик» из состава выбыла — бой едет чанками ленты (§4.1). Что делаем вместо этого и в каком порядке — Planning - Coop Vertical.
4.1 ЗАКРЫТО 01.08.2026: боевая лента раздаётся чанками — события плюс сжатые снимки
Вердикт и его цена — Journal - The Tape Ships As Events Plus Packed Snapshots; формат чанка, лимит и порядок работ — Planning - Coop Vertical. Ниже сохранён разбор развилки: он объясняет, почему третий вариант нельзя было отвергнуть причиной lockstep-а, и чем в итоге пришлось заплатить.
Поднят дизайном (кооп-кластер ГДД), вердикт был за тех-стороной. Формулировка-источник — Coop - Sync Model, раздел «Класс 1».
Механизм наполовину есть: локально сим пишет боевую ленту, показ читает её с лагом — события лежат за весь бой, снимки состояния идут кольцевым окном вокруг момента показа. Сим уже умеет уезжать вперёд хоть до конца боя. Вопрос в том, что именно летит по сети:
| Вариант | Плюс | Чем упирается |
|---|---|---|
| состояние каждый тик (как предполагает решение 19.06) | ничего не менять в модели | самый дорогой по трафику |
| события ленты одним пакетом | дёшевы и уже хранятся целиком: «кино» раздаётся за секунду до боя | нужен контракт сериализации события и порядок доставки |
| вход + сид, бой проигрывается локально | минимальный трафик | тот же float, из-за которого отклонён lockstep |
Отличие от lockstep, из-за которого третий вариант нельзя отвергать той же причиной: здесь расхождение — не «кошмар отладки», а разные картинки без последствий для правил, потому что исход считает хост. Цена ошибки другая.
Что обязан соблюсти любой вариант (дизайн-требования того же раздела): все видят один и тот же бой — тайминги, смерти, исход; пауза общая и видно, кто нажал, а скорости показа не существует вовсе; камера личная; отстающий по показу догоняет сам, а не тормозит группу; задержка перед боем допустима, внутри — нет; пока открыты ставки, лента не уходит игрокам даже фрагментом.
5. Хвосты закрытых заходов
Пункты пережили заходы, которые их родили; сами журналы заходов удалены 30.07.2026 как леса.
5.1 ЗАКРЫТО 30.07: реализационные скиллы сверены с кодом (5 из 5)
Скиллы предписывают, а не описывают, поэтому заморозка справочника их не касается: расхождение с кодом здесь настоящий дефект — агент получает инструкцию, которой код не соответствует.
| Скилл | Статус |
|---|---|
uitk | сверен 30.07 — витрина-фантом UiGalleryScreen.uxml заменена на стенд UiPreviewCatalog, сняты IMenuRouter и LoadoutHubView |
audio | сверен 30.07 — снята заглушка UnityAudioService (удалена 26.07), единственная реализация FmodAudioService |
data-authoring | сверен 30.07 — папки Data/Editor/Migrations/ нет, массовая правка идёт через ContentEditService/ContentEditBatch/ContentCrudService |
gamefeel-vfx | сверен 30.07 — шов префаб-VFX числился «не построенным», PixelBurst* и NullScreenShake удалены из описания как удалённые из кода |
combat-sim | сверен 30.07 — один мёртвый путь (PixelBurstPreset); API-утверждения сошлись с кодом |
Пункт закрыт: 5 из 5. Возвращаться к нему при следующем крупном рефакторе подсистемы — метод сверки описан выше и занимает минуты, а не заход.
Метод, который сработал: вытащить из скилла все имена в бэктиках и все пути, сверить с индексом типов кода и с файловой системой. Ловит именно дрейф и не требует читать подсистему целиком. Готча: не считать дрейфом Unity/FMOD API, методы (индекс типов их не содержит) и то, что скилл сам помечает как удалённое или как ещё не реализованное.
5.4 ЗАКРЫТО 30.07: дубль конфигов снят переносом ссылок в ассет
StatsConfig и ClassBalanceConfig объявлены полями в двух скоупах: Root собирает из них превью
статов для интерфейса, боевой скоуп берёт константу брони и реген ресурса и отдаёт их фабрике. Сам SO в
контейнере не лежит нигде — дублируется только сериализованная ссылка сцены, и расхождение держит
SceneWiringTests (требует один и тот же ассет во всех сценах).
Свести к одной регистрации мешает то, что боевой скоуп обязан подниматься без Root: standalone
dev-арена запускается отдельной сценой, и без этих полей симуляция не соберётся вовсе. Решено 30.07 третьим путём: ссылки на StatsConfig/ClassBalanceConfig уехали
из сцен в GameConfig, скоупы и бенчи берут их оттуда. Дубль ссылки на один ассет остался, но второго
владельца факта больше нет. Тем же движением закрылся C3 (armorK в двух местах) и перестали угадывать
бенчи. Провенанс и цена —
Journal - Config References Live In The Asset.
EditMode 775/775.
5.5 Инвариант «Combat не знает MessagePipe» держится соглашением, а не тестом
Боевая сборка отдаёт наружу голые C#-события и не зависит от пакета шины — проверяется тем, что
using MessagePipe не встречается в Scripts/Combat ни разу. Первый, кто его там напишет, ничего не
сломает и не заметит. Нужен guard-тест на границу сборок (по нашему правилу кросс-файловый инвариант
живёт в тесте). Почему так устроено —
Journal - The Bus Stops At The Combat Assembly.
5.6 В докстрингах кода живут ссылки на мёртвую нумерацию вики — и их становится больше
«вики "12" §2.4», «вики "13" §3.1» и подобное. Такой раскладки нет с 16.07.2026 (вики перевезена
в Diátaxis-кластеры со слагами), а после 30.07 половина целей ещё и заморожена. По правилу «код
владеет правдой о коде» такие ссылки не нужны в принципе: это остаток модели, где объяснение жило в
вики. Чинить массово — вырезать упоминание, оставив сам текст докстринга; смысл не теряется,
потому что текст самодостаточен.
Число здесь не пишем — оно само становится вторым владельцем факта. В пункте полтора месяца стояло «221 строка в 133 файлах»; замер 07.08.2026 дал 238 в 151, то есть долг не чинили и он рос, а записанное число выглядело стабильным. Мерить так:
rg 'вики «\d+»' Assets/_Project --statsДолг течёт по своей же причине: код пишется «как соседний» (code-standards §2), поэтому новая
правка легально копирует ссылку из строки рядом. Массовая чистка без guard-теста даст ту же картину
через месяц.
5.2 Дыра журнала решений: 12–16.07 и 20–25.07
За эти два окна нет ни одного «почему», а развилок было много: persist-мир, UI-реворк, модель урона,
каскад статов, теги юнита, VFX-шов, карта акта, палитра. Восстановимо, пока помнится — потом
только из кода, где «почему» не лежит. Заход: git log по диапазону → запись-файл на развилку в
00-meta/journal/. Порог — как в CLAUDE.md.
5.3 Реестр внешних аудитов остаётся на диске
docs/audits/ (216 КБ, три даты) не удалён осознанно: он — evidence под открытые пункты §3.8
(C1–C6, H1–H10). Удалять после того, как эти пункты закроются или будут признаны неверными.
5.7 «Спереди» имеет двух владельцев: приватный InFront и конвенция показа
Facing/InFront живут приватно в ParryComponent (сектор из _frontalDegrees, направление — по цели,
а если её нет, по движению). Показ выводит «спереди» по своему правилу, и совпадение держится только
комментарием в том же компоненте: «здесь берётся то же правило, чтобы “спереди” в бою совпадало со
“спереди” на экране». С решением 2026-07-31/92 (направленный блок) правило понадобится третьему
потребителю — BlockComponent, — и копия появится сама.
Чинить так: вынести направление и фронтальный сектор в общий помощник боевой сборки, оба компонента
читают его; сверху guard-тест «спереди в симуляции = спереди на экране», потому что это кросс-файловый
инвариант, а такие у нас живут в тесте, а не в комментарии. Ориентации как состояния в RuntimeUnit
по-прежнему нет и заводить её не нужно — правило вывода одно, владелец должен быть один.
5.8 Хвосты захода по UI-состояниям (06.08.2026)
Заход закрыл перечень элементов, гейты и расчистку отменённого; ниже — то, что он вскрыл, но не починил. Каждый пункт назван вслух, потому что молчаливый долг здесь уже стоил полутора месяцев жизни отменённого регистра.
1. — ЗАКРЫТО 06.08.2026. Галерея
компонентов (UiPreviewCatalog пережил свою задачу и теперь ВРЁТ["gallery"], 153 строки вместе с витриной подсказок) вырезана: она показывала 7
позиций из 14 контролов и собирала вкладку голым Button там, где экраны используют
gm:PlateButton. Её задачу делает контактный лист (Alebardium → UI → Contact Sheet), и делает
верно, потому что строит образцы через UiSampleFactory.
Сам UiPreviewCatalog ОСТАЛСЯ и нужен: он собирает одиннадцать ЦЕЛЫХ ЭКРАНОВ на стендовых
данных (лавка, награда, событие, сундук, итог, меню, Двор, дев-консоль…) — «посмотреть экран, не
гоняя весь флоу». Его зовут DevEncounterPanel, UiPreviewHost и UiPreviewMenu. Формулировка
«снести витрину» была неточной: врала одна её часть, а не инструмент.
2. Восемь экранов лепят разметку в C# — ProfileScreenView, GuildSelectScreenView,
ShopScreenView, RewardScreenView, EventScreenView, CampScreenView, LoadoutInventoryView,
PeerLostDialogView. Это прямое нарушение правила «разметка и стиль — только UXML/USS». Цена уже
уплачена: витрина лавки строит свою карточку вместо RelicCard, поэтому её состояния пришлось
писать отдельными правилами в screens/shop.uss вместо наследования от карточки.
3. «Таб» по-прежнему существует в трёх классах — gm-tab (пластина), gm-filter-tab и
gm-runbar__tab (оба чипы). Гейт состояний их закрывает через поле Base, то есть они больше не
расходятся молча, но единого источника у них нет. Заявка Макса от 24.07.2026 дословно: «все 3 (теги,
табы инвентаря и верхней панели) — должны иметь один источник правды. Просто разные режимы работы».
Не сделано.
4. Роль --gm-color-focus-ring осиротела. Фокус везде рисуется --gm-color-hover-border (тот же
знак, что у наведения) — это осознанно и консистентно, но роль под фокус заведена отдельно и не
используется. Либо занять её, либо снести: сейчас она читается как «фокус имеет свой цвет», чего в
дереве нет.
5.9 Типографика привязана к экранам — ЗАКРЫТО 06.08.2026
Сведено в тот же день. Девять ролей (gm-text-display / -title / -subtitle / -name / -body / -caption / -note / -label / -code) держат гарнитуру и кегль, семь меток (gm-text--*) — цвет,
класс экрана — только раскладку. Держит UiTypographyGateTests.
Что осталось от пункта: карточки и строки живут той же болезнью — gm-shop__card рядом с
gm-card, gm-shop__stash-row рядом с gm-reward-drop__row (см. §5.8 п.2). Приём отработан
трижды, осталось применить.
Урок, который стоит помнить при следующем сведении: ярус гарнитуры — ОТДЕЛЬНЫЙ владелец, и по
инвентарю «кегль + цвет» его не видно. Групповой селектор в components/text.uss назначал сериф
списку блоков, из-за чего gm-shop__section-title по числам выглядел «телом текста», хотя это
антиква. Перевод на роль-гротеск сменил бы гарнитуру молча. Отсюда девятая роль (-subtitle) и
обязательная чистка яруса: класс, получивший роль, из списка убирается.
5.10 Хвосты захода по контактному листу (07.08.2026)
Заход закрыт, леса docs/ui-sheet-rework-progress.md снесены. Осталось три вещи, каждая ждёт
глаза, а не работы:
1. Экраны после смены рампы поверхностей не смотрены в игре. Фон панели ушёл на 18%, кнопки и
чипы встали на raised 26%, утопленное — на 15%. Числа контраста считаны и записаны в журнал
«The Button And Its Ground Were The Same Colour»,
но вместимость и читаемость — разные вопросы. Глубину можно двигать по кадру
Alebardium → UI → Lightness Ladder.
2. Карточка Мементо 112x168 не смотрена в игре. Размер выведен из содержимого (имя не влезало
ни одно, спрайту оставалось около 20px), высота грида в LoadoutScreen.uxml пересчитана под два
ряда. Если панель распирает — двигать оба числа вместе, они связаны комментарием с обеих сторон.
3. Тёплая половина двуцветия — это арт, а не тема. Договорённость 06.08: интерфейс синеватый,
мир тёплый. Тема свою часть держит — не держала до 21.08.2026: поверхности оставались тёплыми
(тон 32°), холодным был только акцент, и запись эта врала три недели. Тема свою часть выполнила
21.08 — рампа ink переехала на 188°
(запись). Ждёт художника по-прежнему тёплая
половина: фоны мира, текстуры и обрамление.
5.11 Хвосты захода по дуге за клинком (07.08.2026)
Заход закрыт, леса docs/bloom-showcase-progress.md снесены. Что сделано и почему — четыре записи
журнала за 06–07.08: шов стенда,
след во времени,
раскадровка и фиктивное время,
направление клинка. Осталось пять вещей.
1. Громкость блума — ЗАКРЫТО Максом 07.08.2026. Вердикт: «Блум я настроил так как считал
нужным, все хорошо». Лечило растекание — scatter: 0.12 в
Assets/Settings/PostFX/BattlePostFX_Base.asset, — а не громкость: жалоба была «ореол большой,
мылит», и порог с интенсивностью тут ни при чём. Заход начинался именно с этого вопроса и три дня
шёл мимо него: чинились дуга, стенд и риг, потому что эффекты, по которым судят о свете, показывали
неправду.
2. Клинок нарисован по диагонали своего кадра (40.1°). Направление меча в мире = поворот узла арта (29.38°) плюс эта диагональ. Код от этого больше не зависит — направление берётся с рисунка, каким бы он ни был, — но «наклон меча одним читаемым числом» до перерисовки недостижим. Работа художника; после неё поворот узла станет единственным владельцем наклона.
3. Предплечье и клинок под 132° друг к другу (локоть→кисть смотрит на −67°, клинок на +65°, поза
покоя). Макс правил рисунок кисти Hand_R_Art (25.36°) — держится меч теперь красиво, но соосности
«предплечье → меч» это не дало. Нужен ли такой инвариант вообще — вопрос без вердикта: дуге он не
нужен, она берёт плечо и остриё.
4. Позы четырёх клипов после переноса поворота никто не смотрел глазами. Idle, Sprint,
Block, Stun — кривые сложены математически точно (значения и тангенсы), тесты зелёные, но кисть
теперь несёт поворот меча, и это ВИДНО. Атака проверена раскадровкой, остальные нет: стенд
показывает эффекты, а не анимации.
5. Лестница яркости эффектов не сделана. Семь множителей свечения лежат в коридоре 0.8–2.2 —
меньше полутора стопов, — и вспышка смерти при этом самая тусклая из всех. Правило ступеней записано
(code-standards §9), развести по стопам — отдельная работа.