Статус: Archive — исторические записи до 2026-07-30. Не пополняется.


Куда писать теперь

Этот файл — архив записей за 19.06–28.07.2026: зафиксированные решения (§1) и changelog-и починок (§2.x). Он не пополняется и не сверяется с кодом.

  • Новое решение или грабли → новый файл в journal/ (одна запись = один файл, имя ГГГГ-ММ-ДД-slug.md). Правило и шаблон — CLAUDE.md.
  • Открытый техдолг и будущая таскаMeta - Tech Debt (вынесены отсюда 30.07).
  • «Как работает код сейчас» → сам код: <summary>/<remarks> и тесты. Справочник вики (10-reference, 20-explanation) заморожен — читать как замысел, не как правду.

Почему так — Journal - Code Owns Truth, Journal Owns Why.

Записи ниже с кодом не сверялись

Имена классов и путей — на дату записи. TargetingSystem, например, уже удалён (таргетинг в ProfileBrain). Ценность архива — в «почему так решили», не в «как называется класс».

1. Зафиксированные решения

ДатаРешениеГде подробно
2026-06-19Сетевая модель — host-authoritative (не lockstep)Journal - Host-Authoritative, Not Lockstep
2026-06-19SimSyncProbe запаркован в Net/_Parked/ (lockstep-инструмент)Journal - Host-Authoritative, Not Lockstep
2026-06-19«Эффекты кормят статы»: Lifesteal/реген — статы-накопители; временные баффы = эффект со StatModifierComponent. LifestealComponent избыточенJournal - The Stats Pipeline Neither Reorders Nor Clamps §4, Journal - Effects Are Ordered By Id, Attributed By Weight
2026-06-19Анимация юнитов — пиксель-кадровая (sprite sheets), не скелетнаяслой презентации (код Assets/_Project/Scripts/Presentation/)
2026-06-19SO-слой — простой сейчас, моддинг потом (прямые ссылки, id по мере нужды; UGC — post-festival)Reference - Steam Workshop
Симуляция отделена от презентации (чистый C#-сим, read-only визуал)Journal - Why The Tick Order Is This Order, слой презентации (код Assets/_Project/Scripts/Presentation/)
Никаких синглтонов — только DI через VContainerJournal - The Bus Stops At The Combat Assembly
Потенция эффекта — снимок при наложении, не пересчитывается при стакеJournal - Effects Are Ordered By Id, Attributed By Weight §4.2
2026-07-10Смещение — это эффект (тег KnockUp, Neutral → длительность не скейлится; траекторию всё равно ведёт DisplacementSystem). Единый шов EffectExpired вместо UnitDisplaced — реактивы фильтруют по тегам эффекта + команде юнита§2.6
2026-07-11Контент-каркас (Фаза 4): ContentDefinition-база + стабильные id domain.name, ContentDatabase единым плоским списком (не per-domain), StatsConfig с базой статов + реликвии как Flat-диффы, IAudioServiceCore.Audio, часть тайминг-констант оставлена в SimConstants§2.7, дата-слой (код Assets/_Project/Scripts/Data/, правила — скилл xgaida-x-nixi-data-authoring) §12
2026-07-17SimBench (инструменты баланса): headless-стенд поверх боевого ядра (editor-only, Guildmaster.Balance[.Editor]), метрики через outward-events, бенчи процедурные, экспорт CSV/MD. Причина: ядро уже headless-совместимо (даром не использовать грех), нужен стартовый баланс как доп-инфо для ГД. Стенд не решает баланс, сочетания не ловит. Регрессия баланса в CI — вырезана (наблюдательность на Максе). Оговорка: бой сейчас RNG-free → Monte-Carlo по сидам вырожден, закладываем шов на будущееPlanning - SimBench Balance Harness
2026-07-17DamageResult.SourceKind — эхо DamageRequest.SourceKind в результате урона, чтобы метрики/презентация раскладывали урон по источнику (авто/абилка/DoT), НЕ меняя сигнатуру OnDamageDealt. Причина: шов sim→presentation неизменен (правило 1 combat-sim), одна точка конструкции DamageResult в DamagePipeline. Combat 217/217 зелёныеPlanning - SimBench Balance Harness
2026-07-17ContentEditService (Guildmaster.Data.Editor) — write-сторона баланса: правка ЗНАЧЕНИЙ контент-SO через SerializedObject+Undo, change-log в BalanceReports/. Сосед ContentCrudService (тот — жизненный цикл ассета/id, этот — значения). Причина: у БД-тулинга не было безопасной правки значений; SimBench(read)+ContentEditService(write) = петля баланса.claude/skills/xgaida-x-nixi-balance/DRAFT.md
2026-07-182D-свет — направление зафиксировано (planned): динамический Light2D на URP 2D Renderer (уже в проекте), иногда тёмные сцены для читаемости, объём через normal-карты как secondary textures. Тулинг normal-карт — Laigter (opensource, CLI-батч + пресеты; принято Максом), чтобы НЕ рисовать normal руками на каждый кадр; Aseprite — запасной точечный путь, 3D→bake отложен. Причина: инфра под свет (Light2D) уже даром стоит, ручная отрисовка normal на анимацию нежизнеспособна. Детали (палитра, тёмные арены, нужны ли normal на всех) — обговариваютсяPlanning - 2D Lighting
2026-07-19UI-реворк — стек-навигатор + ввод = f(верх стека, фаза): UiNavigator (типы экранов Page/Modal/Sheet + 8 слоёв-контейнеров) стал единственным владельцем видимости и ввода. GameplaySuppressed/Context не мутируются, а вычисляются; смену фазы навигатор ловит событием IBattleClock.PhaseChanged (K8 закрыт: DeploymentController/BattleInputController больше не зовут SetContext, только SetPhase). Растворены мёртвые слои IRunTopBar/RunTopBarView (легаси-панель) и IMenuRouter (никем не потреблялся как абстракция). Причина: у UI не было единого владельца состояния — «что показано/что глушится/что поверх» складывалось из ~17 переменных в 5 подсистемах, каждая точечная правка ломала соседнюю (регрессии раунда 5). EditMode 418/418Journal - The World Replicates, The View Does Not
2026-07-26Звук: пайплайн из одной карты + микс в FMOD. scripts/audio/audio_map.py — единственный источник правды (ключ → категория → сэмплы); из него генерируются нормализованные исходники (−23 dB RMS активной части, потолок −1 dBTP), manifest.json, populate.js, банки и AudioCatalog. В FMOD заведены шины bus:/SFX/{Combat,UI,Ambient,Stingers} + bus:/Music, категорийные offset’ы, рандомизация питча/громкости и voice-макросы (max instances / stealing / cooldown / priority), плюс глобальный параметр TimeScale с кривой питча боевой шины. Причина: слайдеры «Музыка» и «Звук» писали в несуществующие шины (все события роутились в Master), разброс громкости сэмплов достигал 19.3 dB внутри одного мульти-инструмента, а параметр TimeScale, который игра шлёт с самого начала, никто не принимал — слоумо было не слышно. Событий 31 → 109, EditMode 542/542Reference - Audio Inventory, Planning - SFX Round 2
2026-07-26Звук вне боя: два новых шва вместо вызовов по экранам. UiSoundSystem слушает корень UITK-панели (клик, наведение, табы, тумблеры, слайдер, скролл), RunAudioPresenter живёт в root-скоупе и ведёт экраны, карту, переходы, исход и музыку по существующим MessagePipe-сообщениям и IBattleClock.PhaseChanged. EffectSystem получил OnEffectApplied/OnEffectEnded с определением эффекта (старое OnEffectExpired с одними тегами не тронуто — на нём висят боевые реактивы). Килл-стингер переехал в CombatFeelDirector под тот же кулдаун, что и слоумо. Причина: AudioPresenter жил только в боевом скоупе и умирал вместе с боевой сценой, поэтому меню, карта, награда и весь UI были немыми; а базового класса кнопки в проекте нет, и озвучивать клики пришлось бы сотней вызовов по экранамReference - Audio Inventory
2026-07-26Отбор сэмплов автоматизирован там, где это возможно, и честно ограничен там, где нет. audit_samples.py ловит технический брак числами; clap_pick.py (CLAP, локально) подбирает кандидатов по описанию и сверяет назначенные сэмплы со смыслом ключа; freesound_fetch.py тянет CC0-кандидатов; генерация — sfx_generate.py (ElevenLabs) и sfx_generate_local.py (Stable Audio Open). Причина: у контура нет ушей, а проверяемое числами проверять надо — первый прогон аудита нашёл 26 сэмплов с 30–80 мс тишины в начале (звук опаздывал за ударом) и 5 за пиковым потолком, всё это теперь чинится в самой нормализации. Оговорка (замерено): CLAP ненадёжен на коротких сухих one-shot — корреляция длительности со скором +0.47, поэтому сэмплы короче 0.6 с помечаются, а не оцениваютсяPlanning - SFX Round 2 §8a
2026-07-26Фолбэки: три полосы, и граница по источнику отказа. Фолбэк допустим только там, где отказ приходит ИЗВНЕ игры (диск, ОС, локаль игрока, банк FMOD); всё, что внутри нашего авторства — конфиг, ссылка сцены, DI-регистрация, id контента, лок-ключ — фолбэка не имеет: значение есть, либо это баг разводки, громкий и пойманный guard-тестом до запуска. Оставшийся фолбэк обязан деградировать в «явно неправильно», а не в правдоподобное. Разнесено по коду: 14 гардов MenuRouter идут через CannotShow (LogError вместо молчания), обязательные UXML — в реестре SceneWiringTests; новый ScopeWiring.Require/Optional вместо асимметрии скоупов; удалены NullScreenShake, UnityAudioService, дефолт beat = null в ActRunner, ветки Shader.Find в обоих барах; включена fallback-локаль (m_UseFallback: 1 + FallbackLocale en→ru); ClassBaseline, RunStateService, NodeResolver перестали подставлять молча. Причина: тихий фолбэк на собственное авторское значение делает баг разводки невидимым — в шипящейся CoreScene не был назначен _campScreen, и обязательный узел привала (этажи 8 и 13 каждого акта) молча засчитывал сам себя; там же награда резолвилась в Skip, а главное меню отвечало Quit, то есть игра закрывалась сама, без единой строки в логе. Две оговорки, добытые заходом: громкий отказ ≠ отказ выполнить шаг (незавершённый колбэк вешает петлю забега навсегда, что для игрока хуже пропущенного экрана — поэтому лог + шаг завершается, а блокирует guard-тест ДО запуска); уровень сигнала выбирается по тому, где стоит настоящий гейт (LogError в ClassBaseline уронил 9 тестов, строящих юнита без каскада намеренно, и понижен до warning, раз Require сделал случай невозможным в игре). EditMode 598/598Journal - The Bus Stops At The Combat Assembly §1.3, разбор фолбэков 07-26 (удалён 30.07.2026, история в git), правило — .cursor/rules/project-context.mdc
2026-07-26Сцены: три persist вместо «сцена на бой». Состав сцен зафиксирован — CoreScene (вход + сессионный DI + рантайм-UI), WorldScene (камера-риг, арена, карта, фон меню), CombatSystemsScene (бывшая BattleScene: боевой скоуп, презентер, дев-инструменты). Обе аддитивные грузятся ОДИН раз на буте и не выгружаются; цепочка скоупов Root → World → Combat. Снят legacy-путь «загрузить сцену на бой → выгрузить после» (GameFlow.BootAsync/OnBattleEndedAsync, флаг _legacyBattleScene, ISceneLoader.UnloadBattleAsync), вместе с ним из BattleFlow/NodeResolver ушла мёртвая зависимость от загрузчика. Снесены BootScene (пустышка вне билда) и UiGallery с её UXML (вторая витрина компонентов, разошедшаяся с кодовой). Причина: имя сцены обещало то, чего нет — бой это команда в живую симуляцию, а не загрузка; два сосуществующих способа жизни одной сцены плодили путаницу, а выгрузка сцены на узел прямо противоречит persist-миру (отряд, камера и арена обязаны пережить узел). EditMode 534/534сцены проекта (Assets/_Project/Scenes/)
2026-07-26Направленный импакт-фидбэк: право у прямого попадания, направление у снаряда. Искры, отброс тела, выпад атакующего и пыль под ногами рисуются только на DamageResult.IsDirectHit (авто-атака или способность); тик яда, горение и ответка шипов получают лишь цифру и вспышку. Дальний удар берёт направление из траектории снаряда, а не из вектора «стрелок → цель». Причина: у периодического урона нет ни автора в моменте, ни стороны, а бьёт он так же часто, как удары — искры на нём читались как удар, которого не было (отдельной глупостью был выпад: отравитель дёргался к жертве через полкарты на каждом тике). Вектор «стрелок → цель» врёт ровно в тех случаях, ради которых считается: Следопыт стреляет на ходу и за время полёта смещается, цель уходит вбок. Готча: снаряд к моменту события урона уже мёртв (сим ставит точку удара и IsAlive=false в том же тике) — направление приходится помнить заранее, по цели, и чистить на сбросе боя. Свой язык для DoT — открытый дизайн-вопрос, отложен сознательно. EditMode 638/639 (падение MirrorMatchTests — из параллельной сессии по балансу)слой презентации (код Assets/_Project/Scripts/Presentation/) §2.1
2026-07-26Цвет VFX: яркость щедро, насыщенность как валюта. Цвет боевых эффектов живёт на юните двумя полями — _vfxColor (главный: тело снаряда, след, контур каста) и _vfxPalette (диапазон случайного разброса частиц). Норма авторинга: насыщенность 70–90%, ни один канал не в нуле, яркость выше порога bloom (главный ×2.0, насыщенный конец палитры ×2.2, пересвет — смесь с белым 75% и ×2.6). Ростер раскрашен: герои разведены по спектру, враги — единая красная семья. Причина: «максимально ярко И максимально насыщенно» — конфликт в одном пикселе (чистый канал не имеет градаций, форма исчезает), а базовая планка на максимуме съедает шкалу значимости, которой мы уже говорим: автоатака тише, каст ярче, смерть пробивает экран. Внешний канон (стайлгайд League) требует того же — прочь от 100% и 0% насыщенности. Готча: и частицы, и шейдер осколков читают у палитры ТОЛЬКО концы (Evaluate(0/1)) как границы MinMaxGradient — промежуточные ключи не работают; Color.white в главном поле означает «не задан» и молча уводит эффекты в тинт телаVision - VFX Color, Reference - VFX Color
2026-07-26Сохранения: иерархия профиль → гильдия → забег, две шкалы версий, Auto-Cloud. Профиль — мета аккаунта (до 4, переключаемый: соло / игры с друзьями), внутри — до 8 гильдий, в гильдии — не более одного активного забега; лимиты в GameConfig. Слот сохранения отдельной сущностью не заводится: слот и есть гильдия. Версия игры (SemVer) и schemaVersion (своя у каждого типа файла) разведены — на загрузку смотрит только вторая, первая пишется для багрепортов. Steam Cloud — Auto-Cloud по маске Saves/**/*.json; настройки разделены на синхронизируемые prefs и локальные machine. Выход посреди боя откатывает к началу узла (снапшот живой симуляции отклонён: бой детерминирован саб-сидом, ретрай = тот же бой). Ломать run при мажорных правках можно, profile мигрируется всегда. В коопе пишет забег только хост, клиент — свой профиль. Причина: durable-состояние жило одним файлом run без меты вовсе, SchemaVersion писался и не читался, а SettingsService вёл вторую запись на диск мимо ISaveService — без атомарности, .bak и версии. Готча, найденная заходом: persistentDataPath завязан на productName, а имя игры недавно меняли — смена после релиза убивает и сейвы, и маску Auto-Cloud. Дрейф док↔код: доки заявляли три точки автосейва, в коде Autosave() зовётся из девяти мест — фактическая политика «после каждого значимого изменения», доку привести к кодуPlanning - Save System

| 2026-07-27 | Тик разрешается как один момент, а не как очередь по списку. Введён инвариант: система, которая читает чужое состояние и пишет своё, обязана быть двухфазной — намерения от общего снимка, применение после обхода (MovementSystem, AbilitySystem, AutoAttackSystem; SeparationSystem был приведён раньше). Введён закон видимости эффектов: наложенный эффект меняет статы и маску тегов не раньше шага CommitEffects в конце тика, как это давно делали флаги контроля; единственное исключение — pre-damage реактивы («Оплот» ловит тот самый удар, что его разбудил), они синхронны сознательно. Принято, но ещё не реализовано: урон, лечение и щит копятся за тик и применяются одним коммитом — дельты складываются (успевший хилер спасает от смертельного удара), урон по щиту суммируется, фактическое лечение делится между источниками пропорционально вкладу, реактивы отвечают в том же тике раундами с капом. Причина: зеркальный бой трижды оказался решён не бойцами, а порядком обработки — счёт 59.7% против нуля; каждое «место в списке» у отражённых сторон обратное, поэтому асимметрия копилась лавинообразно. Замеры причин: движение расходилось на ПЕРВОМ тике (Y −0.5566 против −0.5543), криомант терял каст от чужого стана, ослабление срезало удар ровно в ×0.7 (146.9 против 102.8), лечение упиралось в потолок HP раньше, чем прилетал урон (7.3 вместо 80). Побочно найден настоящий баг: пассивки правятся отложенно, а фабрика тика не ждёт — юнит рождался с недобранным MaxHP (300 вместо 500); закрыто вызовом EffectSystem.CommitPending при сборке. Готча реализации: отложенные правки статов нельзя раскладывать на «сначала все снятия, потом все наложения» — стакающийся эффект снимает свою же группу и вешает пересчитанную, и в разделённом виде складываются ВСЕ группы (три стака по 5 дают 30 вместо 15); очередь одна, порядок значим. EditMode 643/643; зеркальная серия по ростеру ещё красная на гранях, ждущих фаз Г–Ж | Planning - Tick Resolution, Journal - Why The Tick Order Is This Order §3.1a–3.1b |

| 2026-07-28 | Зеркальность требует не только двухфазности, но и порядка сложения — SeparationSystem. Дополняет запись 2026-07-27, где сепарация числилась «приведённой раньше»: двухфазной она была, и этого не хватило. Каждый юнит теперь набирает все свои вклады сам (пара считается ДВАЖДЫ, по разу с каждой стороны, вместо взаимного зачёта половин по b.Id > a.Id), а соседи обходятся в каноническом ОТНОСИТЕЛЬНОМ порядке — сначала свои, потом чужие, внутри группы по Id, сортировкой вставками (не List.Sort(Comparison): тот аллоцирует обёртку-компаратор на горячем пути 30 Гц). Вырожденный случай (тела совпали точь-в-точь) берёт направление от стороны команды через FleeSteering.HomeDir, поднятый из private в public как единственный владелец конвенции «команда 0 слева». Причина: сложение float неассоциативно, поэтому порядок слагаемых решал исход. Соседи из SpatialHash приходят в порядке ячеек сетки, то есть растут из координат — у отражённых сторон он обратный; а взаимный зачёт добирал юниту часть слагаемых из ЧУЖИХ итераций внешнего цикла, до или после своих смотря по Id, и у зеркальных сторон это ровно наоборот. Замеры: сторож рапортовал расхождение на тике 116 по Y Ассасина, но это место, где дрейф перерос эпсилон Mathf.Approximately; точное сравнение показало рождение на тике 68 в последнем бите X Дефендера (0xbe5f6cac против 0xbe5f6cad, дельта 1.49e-8). Источник подтверждён отключением расталкивания, а не рассуждением. Вырожденная ветка ломала зеркало грубее: -2.15 против 2.05 вместо 2.15 уже на тике 0. Обе части фикса проверены как несущие по отдельности: снять канонический порядок — расхождение на тик 68; вернуть взаимный зачёт с сортировкой — туда же, бит в бит. Готчи: сторож на слипшиеся тела обязан ставить юнитов ОДНОГО класса, иначе разная скорость растаскивает их до расталкивания и ветка не вызывается (первая редакция была зелёной всегда); run_tests сразу после refresh_unity может взять прежнюю сборку и дать ложный зелёный — ждать compilation.is_compiling: false. Линейку сторожа решено оставить с допуском, точное сравнение живёт в [Explicit]-зонде MirrorBitProbe: лишняя краснота дороже раннего сигнала. [Ignore("BAL-014")] снят, зеркальная серия зелёная на всех восьми окнах, точное зеркало держится весь бой (7200 тиков) по всему ростеру. EditMode 679/679 | Journal - Why The Tick Order Is This Order §3.1a, docs/balance-issues.md BAL-014 |

| 2026-07-27 | Урон и лечение применяются разом — TickLedger. Системы за тик только ЗАЯВЛЯЮТ урон и лечение (DamagePipeline из «выполнить» стал «посчитать» — щит и HP он больше не трогает), реестр складывает заявки по каждой цели и применяет одним коммитом: щит вычитает сумму, дельты HP складываются с клампом по MaxHP, фактические числа делятся между источниками пропорционально вкладу, убийство достаётся наибольшей доле (тай-брейк по Id). Реактивы отвечают в том же тике — ResolveCombatRounds крутит «применить → доставить события → применить» до 16 кругов. Переходы «за спину» ушли за шов (ICombatContext.TeleportBehind): заявки копятся, точки считаются от общего снимка, применяются разом. Порядок ActiveEffects нормализован по id эффекта. Причина: серия зеркальных боёв по всему ростеру вскрыла, что три прежние правки были верхушкой — порядок решал исход всюду, где операция нелинейна. Замеры: лечение упиралось в потолок HP раньше, чем приходил предназначенный цели урон (7.3 вместо 80 — разрыв 72.6 HP из ничего); блинк ассасина двигал тело ПОСЛЕ перестройки SpatialHash, и сетка врала до конца тика (расхождение на тике 29); встречный заход двух монахов целился в уже уехавшее тело (X −8.20 против −8.70 вместо −8.20 против +8.20); порядок эффектов хранил историю наложения, а не состояние. Готча: взаимные шипы затухают геометрически, но нуля не достигают — ряд упирается в предел числа, а не в конец боя, поэтому введён порог значимости заявки 0.05 HP; кап раундов оставлен страховкой и предупреждает, а не ругается ошибкой (длинный ряд — не обязательно баг). Зеркальная серия (составы скользящим окном по ростеру × три строя, сверка КАЖДЫЙ тик) зелёная целиком; EditMode 643/643 | Planning - Tick Resolution |


2. Changelog ревизии 2026-06-19 (что починили)

Критический разбор кода выявил ряд проблем; ниже — что исправлено в этой ревизии. Тесты при этом не писали (по договорённости — оценим по факту), но компиляция зелёная, существующие тесты обновлены под новый конструктор.

#ПроблемаИсправлениеФайлы
Аккумулятор боевого цикла без потолка → «спираль смерти» на хитчеКап MaxCatchUpTicksPerFrame = 5, сброс долга при упореSimConstants, CombatLoopService
Lifesteal/HpRegenFlat/HpRegenPct помечены [Ф1], но ни один код их не читалВампиризм в DealDamage; новый RegenSystem в тикеCombatSimulation, RegenSystem (new)
Пустой бой (0 юнитов) крутится вечно вхолостуюGuard _hasSpawned вместо проверки _units.Count == 0CombatSimulation
Диспел последнего контроль-эффекта → флаги CanAct/Move/Cast залипают навсегдаRecomputeControl в Dispel/RemoveEffectSystem
Мгновенный эффект со stateful-компонентом течёт (вечный бафф)Instant → после OnApply сразу OnExpireEffectSystem
Heal не клал событие Healed → on-heal реактивы мертвыEnqueue CombatEvent.Healed по фактически вылеченномуCombatSimulation
Неясность семантики снимка потенции при стакеЗафиксировано правило комментарием (поведение не менялось)EffectSystem
Ручной DI-бойлерплейт (10 Resolve на конструктор)WithParameter("armorK", …) + авторазрешениеCombatLifetimeScope
LifestealComponent дублирует стат-путь под моделью BПомечен баннером «избыточен, см. 07 §3.7» (удаление — позже)LifestealComponent

⑧ (сетевая модель) — не «фикс», а решение: см. Journal - Host-Authoritative, Not Lockstep.


2.1 Changelog 2026-07-09 — Светлый пастырь (§10.1, шаг 6)

Первый срез шага 6 (первая играбельная Обычная реликвия): хилер «Светлый пастырь». Затянуты движковые расширения §9.2 (хил-АА снарядом) и §9.10 (глобальная одиночная лечащая активка). 144/144 EditMode зелёные (+8 ShepherdSliceTests).

ЧтоГде
Флаг полезной нагрузки хил-снаряда IsHeal; ветка ProjectileSystem.ApplyHit лечит вместо уронаProjectile, ProjectileSpawn, ProjectileSystem, CombatSimulation
Хил-режим авто-атаки: гейт замаха и снапшот по AutoAttackTarget (раненый союзник), спавн tracking-хил-снаряда, payload = сырое AutoAttackDamageAutoAttackSystem
Активка «Длань жизни»: цель LowestHpAlly (глобально), хил HealFlat + HealPctTargetMissingHp × недостающее, условие AllyTargetHpBelowPct (блок D), блок E — свой низкий HP разворачивает лечащую активку на себяAbilityData, AbilityTargetMode, CastCondition, AbilitySystem
Контент + харнесс: LightShepherd.asset (провизорные статы), команда gm_spawn_shepherd, ссылка в BattleSceneGuildmasterCommands, ScriptableObjects, BattleScene

Решение по эффективностям (важно на будущее): ctx.Heal уже домножает на обе эффективности (HealShieldDealtEff источника × HealShieldTakenEff цели). Поэтому доставка (снаряд/активка) кладёт сырое значение — не домноженное. Формула «amount = AA × DealtEff» из §9.2 буквально дала бы двойной учёт dealtEff. NoRangeLimit-поле НЕ вводили: гейта дальности в AbilitySystem нет, LowestHpAlly и так глобален (мёртвое поле не заводим).


2.2 Changelog 2026-07-09 — Криомант (§10.2, шаг 6)

Второй срез шага 6: контроллер «Криомант». Затянуты §9.1 (on-hit эффекты авто-атаки) и §9.10 (масс-каст по тегу + конверсия тега). 150/150 EditMode зелёные (+7 CryomancerSliceTests).

ЧтоГде
On-hit эффекты авто-атаки (§9.1): RelicData.AutoAttackEffects[], едут на снаряде (OnHitEffects), накладываются в ProjectileSystem.ApplyHit; симметрично реализован и мили single/LineRelicData, ProjectileSpawn, Projectile, ProjectileSystem, AutoAttackSystem, CombatSimulation
Масс-каст по тегу (§9.10): AbilityTargetMode.AllEnemiesWithTag, AbilityData.TriggerTag/ConsumesTriggerTag, CastCondition.EnemiesWithTagCount; ApplyAllWithTag бьёт всех тегнутых глобально + ctx.Dispel тега (конверсия «Заморозки» в стан)AbilityData, CastCondition, AbilitySystem
Инверсия приоритета TargetingMode.PreferUntagged (ветка-зеркало PreferTagged) — Криомант не добивает уже замороженногоTargetingMode, ProfileBrain
Контент + харнесс: Cryomancer.asset, Effects/Frozen.asset, Effects/IceChainsStun.asset, команда gm_spawn_cryomancer, ссылка в BattleSceneGuildmasterCommands, ScriptableObjects, BattleScene

2.3 Changelog 2026-07-09 — Защитник / Следопыт / Убийца / Монах + dev-VFX (§10.3–10.6, закрытие шага 6)

Оставшиеся 4 Обычных реликвии + движковые расширения §9 (E1–E10 полностью) + dev-подсветка боя. 171/171 EditMode зелёные (+7 Defender, +5 Ranger, +5 Assassin, +5 Monk слайс-тестов).

РеликвияДвижок (§9)Ключевое
§10.3 Защитник§9.3 pre-damage хукIPreDamageComponent + EffectSystem.RunPreDamage перед DamagePipeline.Execute; «Оплот» = BulwarkComponent (щит 20+15%·недост.HP до удара, внутр. КД в RuntimeEffect.ReactiveReadyTick) + MissingHpShieldComponent; ульта — стан+−30% DamageDealtEff. Спайк S5 влит в срез-тесты.
§10.4 Следопыт§9.5/9.7/9.8Кайт/отступление в MovementSystem (полоса [AttackRange×0.6, R]); RelicData.CanAttackWhileMoving+MovingAttackSpeedPenaltyPct; MarkTransferComponent (перенос метки при смерти носителя через CombatEvent.UnitDied, диспатч мёртвому).
§10.5 Убийца§9.6/9.4/9.3RuntimeUnit.EmpowerDamageMult (усиленный первый удар, снимает Stealth); DodgeComponent — negate входящего с зарядами (RuntimeEffect.ChargeReadyTicks, PreDamageResult.Negated); StealthComponent + CombatEvent.UnitKilled (рестелс по своему убийству).
§10.6 Монах§9.9/9.6DisplaceRequest+DisplacementSystem+ICombatContext.Displace (детерм. перемещение, «ядро» по врагам на линии, DisplacedTicksRemaining стан-в-полёте); VortexEntryComponent (телепорт-в-спину ×2 на UnitDisplaced, без КД).

dev-VFX батч (презентация, чтение сима, интерфейс не расширялся кроме fire-and-forget OnAttackEvaded): цифры урона по DamageResult (синий -N щит, оранжевый -N HP, сплит с задержкой, +N хил, «evade» при полном негейте); FloatingText запулен (ObjectPool). Новый CombatStatusOverlay (Shapes-кольца: метка/стан/щит/Frozen/Empower, создаётся в рантайме); инвиз ассасина — альфа спрайта в UnitView; линия толчка Монаха — ReportAreaHit. gm_toggle_status, F5 = быстрый рестарт (gm_restart, new Input System).

Отложенные упрощения (к правкам — след. сессия): блок C (своя цель ульты) сведён к CurrentTarget; кайт по AttackRange, а не Kite.FleeDist/FallbackDist; заряды реактивно в DodgeComponent, а не generic AbilityData.Charges; у Монаха нет рывка-в-сторону перед толчком и умного выбора точки. Спрайты реликвий не назначены. Баланс-числа провизорные. Кольца-цвета — константы в коде. Осталось §11 (закрытие Ф3: roadmap §3, ретро, синхрон AI-словаря 0.3).

Решение по гейту масс-каста: при нуле тегнутых врагов способность НЕ кастуется (трактуется как «нет валидной цели») — не жжёт КД/ману в пустоту даже под панику блока E. Условие EnemiesWithTagCount ≥ X слоится сверху. Провизорно (баланс Ф4/Ф8): Frozen = StackRule.Refresh (повторная АА обновляет 4с, не копит слоу), X = 2 замороженных, статы (HP 120 / урон 12 / MoveSpeed 2.8). Спрайт не назначен.


2.4 Внешний аудит — 09–10.07.2026 (5 моделей)

Пять независимых моделей (Claude Opus 4.8, Codex 5.3, Composer, Cursor Grok 4.5, GPT-5.6 Sol) прогнали аудит Assets/_Project/Scripts на dev HEAD fd858f8. Исходники аудитов — docs/audits/ (архив снимка). Консенсус 5/5: фундамент здоровый, ядро переписывать не надо, риски — на швах (seed, netcode, презентация), а не в архитектуре.

Важно про снимок: аудиты читали коммит fd858f8, но рабочее дерево на момент сведения уже ушло вперёд (незакоммичены арена + движение: CombatSimulation, MovementSystem, DisplacementSystem, RelicData, CombatLifetimeScope). Часть находок про «нет арены / ±200 хардкод» уже неактуальна — арена закодирована (ArenaLayoutData/ArenaBounds/DeploymentService).

Дедуплицированный реестр находок с проверкой по коду — §3.8 ниже.


2.5 Changelog 2026-07-10 — Разбор аудита: фиксы B1–B7 + I1

Закрыты все подтверждённые баги реестра §3.8 (P0-корректность + P1) плюс дешёвый инфра-долг I1. 187/187 EditMode зелёные (+3 регресс-теста: щит-рестак, дож-рестак, кайт по профилю). Каждая находка сверена по коду перед правкой — аудиты читали снимок fd858f8, рабочее дерево ушло вперёд.

IDБылоСталоФайлы
B1/B2/B3Рестак стака слепо звал OnExpire→OnApply с новым Stacks: щит пере-вычитался (Mathf.Max съедал частично израсходованный пул), заряды Доджа бесплатно перезаряжалисьНовый опциональный шов IStackableComponent.OnStacksChanged(prev, ctx); Reapply диспатчит в него, иначе прежний дефолт (keyed-снятие вроде StatModifier не трогаем). Щит правит вклад дельтой Potency×(new−prev); Додж — no-op (заряды не сбрасываются)IRuntimeEffectComponent, EffectSystem, ShieldComponent, DodgeComponent
B4MoveKite игнорировал Kite.FleeDist/FallbackDist, использовал магию AttackRange×0.6 / AttackRangeПолоса кайта = [FleeDist, FallbackDist] из профиля; фолбэк на радиус атаки при пустом/битом контентеMovementSystem
B5Broad-phase линии брал радиус length → цель у дальнего угла (√(length²+halfWidth²) > length) отсекалась до narrow-phaseРадиус length + halfWidth (консервативный супермножество)CombatSimulation.QueryUnitsInLine
B6Кап дренажа MaxEventsPerDrain=512 делал молчаливый Clear() — исход тихо зависел от «уложились ли»Debug.LogError (тик + число сброшенных) перед Clear() — dev-сигнал, не молчаливая потеряCombatSimulation.DrainEventQueue
B7IronSpearman: _resourceType: None, но копит (_resourceOnHit:5, MaxResource:30) и тратит (активка 30) ресурс — противоречивый контракт_resourceType: Rage (build-and-spend). Провизорно — баланс Ф4/Ф8IronSpearman.asset
I1run-tests.ps1 хардкодил Unity 6000.0.23f1 → падал Unity not found на 6000.4.8f1Версия парсится из ProjectSettings/ProjectVersion.txtrun-tests.ps1

По B1/B2/B3: полный вынос per-component state из общего RuntimeEffect (крайняя форма B3) НЕ делался — для корректности хватает дельта-шва. Поля состояния (ChargeReadyTicks, PendingShield) остаются на RuntimeEffect. Дефолтный OnExpire→OnApply сохранён для keyed-компонентов (снятие по ключу-эффекту идемпотентно). По B7: ни AutoAttackSystem.GainResourceOnHit, ни AbilitySystem сейчас не гейтят на ResourceType (проверяют только ResourceOnHit/CurrentResource), поэтому правка не меняет рантайм — чинит контракт данных на будущее (UI ресурс-бара, валидатор C2). Направление (Rage, не Mana) — по модели «копится от ударов, тратится на ульту».


2.6 Changelog 2026-07-10 — Смещение-как-эффект, переработка монаха, презентация снаряда, батч UX

Ветка fix/combat-camera-ai-mana. 190/190 EditMode зелёные (+3: репозиция у стены ×2, полная цепочка монаха). Крупное решение — смещение переехало в систему эффектов.

Решение: смещение — это ЭФФЕКТ, а не жёсткое состояние. Раньше полёт жил в DisplacementSystem как хард-стейт (DisplacedTicksRemaining) с бесшовным событием UnitDisplaced. Теперь:

  • ctx.Displace вешает на цель системный эффект-маркер «в полёте» (строится в коде EffectData.CreateRuntime, тег KnockUp | Control, ControlComponent(act/move/cast), Unremovable, Neutral → длительность не скейлится ReceiveDebuffEff — единственное исключение смещения). Траекторию/«ядро»/тайминг по-прежнему интегрирует DisplacementSystem в своей фазе тика (позицию нельзя выразить пассивным компонентом; per-cast динамика не влезает в shared-компонент SO).
  • В конце полёта DisplacementSystem.OnDisplacementEndedEffectSystem.RemoveByTag(target, KnockUp) → единый EffectExpired(unit, source, tags) (заменил UnitDisplaced). Носитель-получатель = источник эффекта; реактив фильтрует по тегам эффекта + команде юнита, на котором он закончился.
  • EffectExpired — общий шов «эффект закончился»: будущее «Заморозка истекла → осколки» ложится сюда же. Заведён по факту (монах), не про запас.
ЧтоГде
Событие EffectExpired (+ CombatEventData.Tags), убрано UnitDisplaced; EffectSystem.OnEffectExpired + RemoveByTag; фабрика EffectData.CreateRuntime; публичный ctor ControlComponentCombatEvent, EffectSystem, EffectData, ControlComponent, CombatSimulation
Монах §10.6 — правильная цепочка: рывок → фиксация → отбрасывание → телепорт. Активка «Шквальный толчок» = гейт EnemiesInRadius(6) + рывок самого монаха (self-displacement) к цели + фиксация цели оцепенением (VortexHold, preventMove). Приземление рывка (WhirlDashLandingComponent, реагирует на EffectExpired/KnockUp где смещённый = сам носитель) → отбрасывание ближайшего врага «от себя вперёд» (в строй) + «ядро». Задетые «ядром» получают урон И слабо цепляются отбрасыванием (DisplaceRequest.ChainDistance/ChainTicks, cannonball:false → без каскада) — что тоже триггерит телепорт; цепь слабая, чтобы финальный телепорт сел на исходную цель. Конец отбрасывания (VortexEntryComponent, смещённый = враг) → телепорт в спину + усиление ×2. Разведение фаз — по команде смещённого, без искусственных флаговAbilitySystem, VortexEntryComponent, WhirlDashLandingComponent (new), WhirlMonk.asset, VortexHold.asset (new), VortexDashLanding.asset (new)
Монаху дана стартовая мана (StartResource 0→30) — «толчок» стал инструментом захода, а не поздней опциейWhirlMonk.asset
Презентация снаряда (её не было): CombatSimulation.OnProjectileSpawned; ProjectileView следует за ссылкой на симовый Projectile интерполяцией + поворот по скорости; тинт = цвет юнита-источника. Синхрон импакта: при попадании сим ставит Position = точка удара и IsAlive=false в тот же тик, что и OnDamageDealt → вид снапается в точку удара и гаснет (визуальный импакт = реальный эффект). Bullet на слое OverheadFXCombatSimulation, ProjectileView (new), CombatPresenter, Bullet.prefab, BattleScene
Батч UX: экшн-камера плавнее (снап на 1-м кадре + сглаживание разброса + дедзона зума); репозиция у стены (кайт/отступление, упёршись в стену, скользят вдоль неё к центру); мана-бар (ManaBarView, зеркало HP); гейт ввода при открытой QFSW-консоли (пан/зум/Space/Tab/R глушатся, F5 живёт)CombatFocusTarget, CameraModeController, MovementSystem, ManaBarView (new), IInputService/InputService, GuildmasterCommands

Почему смещение НЕ «тикается» внутри общего эффект-тика: EffectSystem.Tick идёт поздней фазой; если двигать полёт там, сместится момент обновления позиции летящих относительно авто-атак/снарядов (детерминизм, ловят two-run тесты). Поэтому DisplacementSystem сохраняет свою фазу (после Movement, до перестройки spatial hash) и лишь читает/снимает эффект-маркер.

Follow-up (тот же день):

  • R = рестарт боя НА МЕСТЕ, без перезагрузки сцены (убран костыль: static-флаг _replayLastBattleOnStart + SceneManager.LoadScene). CombatSimulation.ResetBattle() чистит юнитов/снаряды/очереди и возвращает Ongoing + событие OnBattleReset (презентер снимает виды). CombatLoopService теперь живёт весь скоуп и тикает только при Ongoing (иначе простаивает) — поэтому после ResetBattle тик сам возобновляется. Камера/арена не трогаются. F5 остаётся полным релоадом сцены. | CombatSimulation, CombatLoopService, CombatPresenter, GuildmasterCommands |
    • Аудит отката (что забывали чистить, раз релоад сцены больше не «чинит» всё за нас): (1) RuntimeUnitFactory._nextId — счётчик Id фабрики уезжал от схемы Id болванчиков (Units.Count+1) → коллизия Id → в презентере два юнита на один ключ, вид одного (монаха) осиротевал и висел на месте (HP-бар не реагировал), хотя сим шёл верно. Добавлен ResetIds(), зовётся в R. (2) DisplacementSystem._active — незавершённые полёты держали ссылки на удалённых юнитов; добавлен Clear() в ResetBattle. Прочие системы stateless (только переиспользуемые буферы) — чистить нечего.
  • Тюнинг: MoveSpeed всех реликвий ×0.6 (бегали слишком быстро) + болванчики 3→1.8; gm_spawn_monk — интервал болванчиков 0.7→1.6 и параметр урона enemyDamage (дефолт 5, бой дольше). | 7 relic .asset, GuildmasterCommands |
  • Монах — заход/бросок: монах бьёт ТОЛЬКО прямо от себя → позицию рывка выбираем так, чтобы «монах → цель» смотрела в ближайшего ДРУГОГО врага («наковальню», в поиске исключается сама цель), и бросок летит в него, а не в пустоту. Толкаемая цель запоминается (RuntimeUnit.PendingEngageTarget) на касте — приземление толкает именно её, а не «ближайшего» (тот мог разъехаться). | AbilitySystem, WhirlDashLandingComponent, RuntimeUnit |
  • Dev-оверлеи (Shapes) прятались за спрайтом арены: зоны ударов (CombatAreaFlash) и кольца статусов (CombatStatusOverlay) рисовались на слое сортировки Default (самый нижний), арена — на Background → перекрывала. Заведён верхний слой DevOverlay (TagManager), оверлеи вешаются на него (SortingLayerID по имени, ленивый резолв). | TagManager.asset, CombatAreaFlash, CombatStatusOverlay |

2.7 Changelog 2026-07-11 — Фаза 4 «Контент-каркас» (пакеты 0–7)

Ветка feat/content-framework (13 коммитов над dev). Построен весь слой данных по дата-слой (код Assets/_Project/Scripts/Data/, правила — скилл xgaida-x-nixi-data-authoring) §10, порядок 0→7 пройден целиком. 221/221 EditMode зелёные. Дизайн-файл 13 переведён в статус «Реализовано» (+§12 отступления).

ПакетЧтоКлючевое
0 9e62302cФикс-лист §4.2: один источник tuningarmorK из StatsConfig, DefaultPassiveThreshold/GlobalSearchRadius/снарядные консты, деспаун снаряда привязан к границам арены, мёртвые DamageNumber* удалены
1 2669c8d7ContentDefinition + жизненный цикл id + реестр + валидацияБаза с _id (+Odin id-drawer), ContentDomains.MakeId, IContentDatabase/ContentDatabase в DI, Relic/Vessel/Effect → ContentDefinition (убран DisplayNameKey), 20 ассетов получили канон-id + folder-per-type, ContentValidationTests (§8 правила 1,2,3,5)
2 15770403SimTuningConfig-снапшот + GameConfig + бейк-шовRead-only снапшот тюнинга через ICombatContext.Tuning; gm_tuning_rebake; ConfigValidationTests. Checksum не изменился
3 c855c3fb6e393d35Иерархия UnitDataRelicData/EnemyData + мета-типы + база статов7 новых SO-типов, TagData/TraitData/ConsequenceData/AIPresetData (+миграция inline-AI 7 ассетов), RuntimeUnit.RelicUnit переименование (~15 мест), StatsConfig.Defaults = база + реликвии как Flat-диффы, глоссарий GDD Perk→Trait/+Consequence
4 dd6d69a4 (+сцена 6173f89e)UnitVisual → Unity Animation: клипы-слоты, AnimatorController + override, бейк маркеровКлип = единственный источник; кадр/хит атаки выводятся из клипа в рантайме, fireEvents = false; AbilityData.VisualSlot; data-driven RelicData._visual; EnemyData enemy.training_dummy; NPE-фикс MovementSystem (Ai может быть null). Визуальный гейт принят Максом вживую
5 c725ce3aКонтент-менеджер §9OdinMenuEditorWindow (дерево по доменам) + GUI-free ContentCrudService (Create/Duplicate с регеном id/Delete под FindUsages/Sync) — шов на замену IMGUI→UITK
6 e5c0cca1Localization EN/RU + Addressables-плумбингAddressables 2.3.1→2.3.16; таблицы Content(+UI-stub), Locales en/ru; ILocalizationService (Core) + LocalizationService (Game); лок-помощник Name/Desc в инспекторе + create-missing-keys; валидация ключей §8.4 (7 реликвий name+desc, 13 эффектов name)
7 cd70c751AudioCatalog + резолвер (FMOD-шов)AudioAction-enum, AudioCatalog SO, AudioResolver ({id}.{action}→дефолт действия→тишина+лог, чисто-тестируемый), AudioPresenter (MessagePipe DamageDealt→hit / UnitDied→death). Звука ещё нет — IAudioService = Debug.Log-stub

Отступления от дизайна (полная таблица — дата-слой (код Assets/_Project/Scripts/Data/, правила — скилл xgaida-x-nixi-data-authoring) §12): (1) ContentDatabase единым плоским списком, не per-domain §3.6; (2) DisplayNameKey убран — имя только через лок-ключи; (3) StatsConfig получил базу статов, реликвии переписаны как Flat-диффы (числа провизорные, баланс за Максом); (4) IAudioService переехал в Core.Audio (зеркалит ILocalizationService); (5) AiTickRate/MaxAttackAnimTicks/MinWindupTicks оставлены в SimConstants (структурные/тайминговые, не балансные ручки); (6) деспаун снаряда по CameraZone, не по краю арены.

NB по контенту: база = 28 записей (7 реликвий + 13 эффектов + 7 AI-пресетов + 1 болванчик enemy.training_dummy). VesselData/TraitData/ItemData-ассетов ещё нет — типы готовы, наполнение по мере строительства флоу. EN-переводы реликвий — черновик (Макс полирует позже). FMOD-звук и VFX-каталог — Фаза 7/9. Опциональный хвост Фазы 4: перевод реликвий с общего temp-визуала (MedievalWarrior) на свои по мере готовности арта (Pack 2/3 нарезаны).


5. Как пополнять этот документ

Не пополнять — файл закрыт 2026-07-30. Новая запись = новый файл в journal/, порог и шаблон в CLAUDE.md. Открытый долг — Meta - Tech Debt.