Статус: Ready — живой документ, обновлять при изменениях


Что мы сознательно отложили, что недавно починили (и почему), и какие вопросы открыты. Живая страница: сюда сваливаем технические решения и долг, чтобы они не терялись и взрывались не внезапно, а по плану.

Связано с 00. Обзор и карта кода, 06. Сетевая модель — решение и план.


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

ДатаРешениеГде подробно
2026-06-19Сетевая модель — host-authoritative (не lockstep)06. Сетевая модель — решение и план
2026-06-19SimSyncProbe запаркован в Net/_Parked/ (lockstep-инструмент)06. Сетевая модель — решение и план
2026-06-19«Эффекты кормят статы»: Lifesteal/реген — статы-накопители; временные баффы = эффект со StatModifierComponent. LifestealComponent избыточен03. Данные, статы и урон §4, 04. Эффекты и способности
2026-06-19Анимация юнитов — пиксель-кадровая (sprite sheets), не скелетная05. Презентация и интерполяция
2026-06-19SO-слой — простой сейчас, моддинг потом (прямые ссылки, id по мере нужды; UGC — post-festival)8. Steam Workshop и UGC
Симуляция отделена от презентации (чистый C#-сим, read-only визуал)02. Детерминированная симуляция и тик, 05. Презентация и интерполяция
Никаких синглтонов — только DI через VContainer01. Dependency Injection и события
Потенция эффекта — снимок при наложении, не пересчитывается при стаке04. Эффекты и способности §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, 13. Дата-слой и контент-каркас (SO и конфиги) §12

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

⑧ (сетевая модель) — не «фикс», а решение: см. 06. Сетевая модель — решение и план.


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). Построен весь слой данных по 13. Дата-слой и контент-каркас (SO и конфиги) §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

Отступления от дизайна (полная таблица — 13. Дата-слой и контент-каркас (SO и конфиги) §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 нарезаны).


3. Открытый техдолг (сознательно отложено)

Эти пункты не баги прямо сейчас — это запланированный долг. Чинить, когда дойдёшь до соответствующей фазы, с тестом на руках.

3.1 ⑩ Разбухание CombatSimulation (god-object)

CombatSimulation совмещает: тик-цикл, реализацию ICombatContext, очередь команд, хаб C#-событий, фабрику снарядов, расчёт checksum (~340+ строк). Пока читаемо, но это место будет пухнуть. Когда дойдёт (а не сейчас — иначе преждевременная абстракция): вынести очередь команд и checksum в отдельные классы. Приоритет — низкий, до появления реальной боли.

3.2 Аллокации в Stats.RebuildCache

Три new float[StatCount] на каждую пересборку кэша статов. В Фазе 1 пересборка одноразовая (модификаторы ставятся раз при создании юнита) — неважно. В Фазе 2 эффекты будут часто менять статы → пересборки участятся → перевести на переиспользуемые инстанс-буферы. Триггер: подключение стат-меняющих эффектов в контент.

3.3 Грубый alpha интерполяции презентации

CombatPresenter.Update берёт alpha = Time.deltaTime / TickDelta — это не корректная доля интерполяции (правильная — accumulator / TickDelta из CombatLoopService). Движение сглажено, но не идеально по фазе. Фикс: пробросить дробную часть аккумулятора из loop в презентер. Косметика, не срочно.

3.4 Таргетинг O(n²)

TargetingSystem ищет ближайшего врага полным перебором. На десятках юнитов это дешевле запроса к SpatialHash — осознанно. Когда число юнитов вырастет (Фаза 3) — перевести на SpatialHash.QueryRadius (TODO уже стоит в коде).

3.5 Валидация контента эффектов

Правка ⑤ убрала утечку мгновенных эффектов рантайм-способом (OnApply→OnExpire). Но лучше бы ещё запрещать на этапе авторинга ставить stateful-компоненты на мгновенный эффект (валидация в EffectData/редакторе). Аналогично — предупреждать о бессмысленных комбинациях. Задача редакторного тулинга (Фаза 4, Odin-авторинг).

3.6 Вампиризм применяется ко всему урону

Правка ② вешает lifesteal на любой нанесённый урон (включая DoT/шипы), т.к. у урона пока нет тегов источника. Сузить до авто-атак/способностей — когда появятся теги источника урона (Фаза 2).

3.7 Примирить LifestealComponent под моделью B

Реактивный LifestealComponent лечит напрямую, минуя стат — под моделью «эффекты кормят статы» он избыточен и создаёт риск двойного учёта (стат-путь + компонент на одном юните). Пока только помечен баннером (на нём держится ReactiveEffectTests). Когда начнётся авторинг контента: удалить компонент, заменить вампирик-баффы на StatModifierComponent(+Lifesteal), мигрировать тест (оставить в нём ThornsComponent как пример реактивности). Триггер: первый реальный контент с вампиризмом.


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.5P0Claude Opus (только она)
B2Рестак Dodge бесплатно перезаряжает все заряды негейта: OnApply безусловно new int[maxCharges], Reapply его зовёт. Нюанс: только Stack/StackAndRefresh, чистый Refresh не триггеритDodgeComponent.cs:27, EffectSystem.cs:389-406✅ исправлен §2.5P0Claude Opus
B3Корень B1/B2: Reapply = слепой OnExpire→OnApply ради смены числа стаков; per-component state живёт в общем RuntimeEffect. Фикс — OnStacksChanged(old,new) + вынести stateEffectSystem.cs:389-406, RuntimeEffect✅ исправлен §2.5 (дельта-шов; вынос state отложен)P0Claude Opus
B4Kite-поля мёртвые: MoveKite использует магическое range*0.6f + AttackRange, а Kite.FleeDist/FallbackDist из AIProfile нигде не читаются. Данные выглядят рабочими — но no-opMovementSystem.cs:85-100, AIProfile✅ исправлен §2.5P1GPT-5.6
B5Broad-phase линейного запроса теряет цели в дальних углах: радиус length вместо length+halfWidth → юнит на (≈length, ≈halfWidth) отсекается до narrow-phaseCombatSimulation.QueryUnitsInLine (~:295)✅ исправлен §2.5P1Claude Opus
B6DrainEventQueue после капа MaxEventsPerDrain=512 делает silent Clear() — исход боя зависит от «уложились ли в 512». На текущем контенте 512/тик недостижимо → future-major, не critical-сейчас. Фикс: dev-assert + логCombatSimulation.cs:441✅ исправлен §2.5P1Grok, GPT
B7Iron Spearman: _resourceType: None, но _resourceOnHit:5, MaxResource:30, активка стоит 30 — противоречивый контракт ресурсаIronSpearman.asset✅ исправлен §2.5 (Rage)P1GPT-5.6

Консистентность данных (ретрофит дёшев, пока ассетов мало):

IDНаходкаСтатусПриоритет
C1Seed боя через UnityEngine.Random+UtcNow (CombatLifetimeScope.cs:113-117); нарушает «рандом только через IRngService»; блокирует репро/реплей. gm_rng_seed — stub (только лог)✅ (TODO в коде)P1
C2Нет OnValidate/content-validator на SO — битый контент проходит молча (Kite Fallback>Flee, resource/cost, диапазоны 0..1, uniq id)P1 (расширяет )
C3armorK в двух местах: сериализован на CombatLifetimeScope (:24) и в StatsConfig; оба =100, ошибка скрытаP2
C4buff/debuff смоделирован трижды (EffectPolarity + EffectTag.Buff/Debuff + DispelTargetPolarity) — два источника правды на ассетеP2
C5Два enum таргетинга (AbilityTargetMode vs TargetingMode) + устаревший докблокP2
C6Две модели скейла: ScalableValue (эффекты) vs сырые _damageMultiplier/_healFlat (активки) — payload активок не переиспользует шовP2

Гигиена / мёртвый код / швы:

IDНаходкаСтатусПриоритет
H1Мёртвый DamageNumber+DamageNumberSpawner (внутри — set через рефлексию, докстринг врёт про LitMotion); живой путь — FloatingText+пулP2
H2ISimEvent — маркер без реализаций (CombatEventData его не реализует)P2
H3Двойной канал наружу: C#-events + MessagePipe; контракты событий объявлены в тяжёлом Guildmaster.Presentation → вынести в нейтральный слойP2
H4alpha интерполяции frame-based🔗 (подтверждён 4/5)P1
H5Stats.RebuildCache — 3 массива на пересборку🔗 (подтверждён)P2
H6GlobalMessagePipe.SetProvider — мёртвая глобальная поверхность (никто не читает)P2
H7RuntimeUnit — открытый мешок mutable-полей во view (инвариант «view только читает» держится на дисциплине)P3
H8MarkTransferComponent аллоцирует new List<RuntimeUnit> на каждую смерть носителяP2 (по касанию Следопыта)
H9Cooldown активок во float-секундах (AbilitySystem), а не int-тиках — дрейф из принятой tick-дисциплиныP3
H10~1 ГБ мусорных worktrees в .claude/worktrees/ (detached, от прошлых сессий)🔍P2

Инфраструктура / инструменты:

IDНаходкаСтатусПриоритет
I1run-tests.ps1 хардкодит Unity 6000.0.23f1, проект на 6000.4.8f1 → локальный прогон падает Unity not found. Читать версию из ProjectVersion.txt✅ исправлен §2.5P0 (5 мин)
I2Билд первой сценой ставит CoreScene (EditorBuildSettings), GPT утверждает что она пустая, а root — в BattleScene → boot-flow не замкнут. Запуск из BattleScene — осознанный dev-harness🔍 (проверить в редакторе)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⚠️ Класс удалён; таргетинг теперь в ProfileBrain. Дока отстаёт (общая заметка GPT: часть вики устарела — tick order без Brain/Displacement, путь тестов Assets/Tests)

Сетевой долг (host-auth vs lockstep-наследие NetworkCommandRelay, authority-гейт тика, полный порядок команд (tick,player,seq), float-checksum) — все 5 подтвердили; относится к 4. Главная будущая таска (вынесено отдельно), не трогаем в одиночке.


4. Главная будущая таска (вынесено отдельно)

🎯 Сетевой кооп — host-authoritative (Фаза MP). Репликация состояния юнитов через NGO, клиент рендерит из реплик-состояния, гейт тика на хосте, релей команд для полного ввода, синхрон паузы, late-join/реконнект, сид боя от хоста. Полный состав — 06. Сетевая модель — решение и план §4. Сейчас не трогаем (в одиночке сеть спит).


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

  • Принял техническое решение с развилкой → строка в §1.
  • Починил что-то заметное → строка в §2 (changelog с датой).
  • Заметил долг, который не чинишь сейчас → пункт в §3 с триггером «когда чинить».
  • Не превращай в свалку мелочей: сюда — то, что переживёт текущую сессию и важно будущему-тебе.