Статус: ТЗ к исполнению (функционал обсуждён и утверждён Максом 2026-07-12)


Исполняемое техзадание «Контент-хаба» — единого UI Toolkit EditorWindow для работы со всем дата-слоем проекта (контент-SO, визуалы, аудио-каталог, конфиги, палитры). Дизайн-обсуждение — §1 этого файла (отдельного дизайн-файла нет; при неоднозначности решает §1, при конфликте с реальным кодом — спросить Макса, не угадывать). Исполнение: пакеты 0–9 строго по порядку, один пакет = одна ветка = один PR в dev.


0. Общие правила исполнения (читать перед каждым пакетом)

  1. Ветки: от свежего origin/dev, имя feat/content-hub-p{N}. PR в dev, squash-merge, ветку после мержа удалить. Коммиты — английский, conventional (.cursor/rules/git-conventions.mdc).
  2. Тесты: после каждого пакета — полный EditMode-прогон (Unity MCP run_tests или ./scripts/run-tests.ps1). База: все текущие тесты зелёные. Красный тест, который «мешает», — повод остановиться, а не править тест.
  3. Хаб — editor-only и read-mostly. Никакого кода хаба в рантайм-сборках. Запись в ассеты — только через SerializedObject/Undo (редактирование полей) и ContentCrudService (CRUD). Рантайм-SO не получают ссылок на editor-типы.
  4. Не дублировать сим. Любое вычисление, которое уже делает боевой код (бейк статов, тайминг атаки), — реюз того кода, не переписывание формул в хабе. Разошедшиеся формулы = баг класса «таблица врёт».
  5. Визуальная приёмка каждой страницы — за Максом; агент проверяет только не-визуально (индекс, значения, события, тесты). После каждого пакета — короткая инструкция Максу «что потыкать».
  6. Запреты из дата-слоя действуют: никакого Odin Serializer; мутаций SO в рантайме; Resources.Load. Odin — только атрибуты/дровверы (сборки *.Editor).
  7. Порт из Bloodlines-Dark-UI (C:\My Projects\Bloodlines-Dark-UI\Assets\Alebardium\DesignSystem\Editor\) — это перенос паттерна с упрощением, не копипаст 1:1: выкидываем EditorLoc-локализацию, drag-reorder, JSON IO, WCAG. Файлы переписываются под наш namespace и наши нужды; авторство исходника — сам Макс, лицензионных вопросов нет.
  8. Производительность: страницы работают с закэшированным ContentIndex; полный скан AssetDatabase — только при построении/инвалидации индекса, никогда в перерисовке UI.

1. Дизайн (утверждённые решения)

1.1 Что это

Одно окно ContentHubWindow (меню Tools/Guildmaster/Content Hub), «страницы» — pill-табы по образцу ThemeEditorWindow из Bloodlines-Dark-UI. Хаб заменяет текущее Odin-окно ContentManagerWindow (удаляется в пакете 2 после паритета).

Главный принцип: хаб не дублирует Unity — он даёт обзор, сравнение, связи и покрытие. Поля одного SO рисует существующий инспектор (Odin) через InspectorElement; файлы — Project window; клипы — Animation window. Хаб — это то, чего нет: «все сразу», «сравнить», «найти дыры», «пройти по ссылке и вернуться».

1.2 Скоуп данных

Весь дата-слой (решение Макса «всё в хабе»):

  • ContentDefinition-семейство: RelicData, EnemyData, VesselData, EffectData, TagData, TraitData, ConsequenceData, AIPresetData, GuildmasterData, ItemData, RunModifierData (список = ContentPaths.CreatableTypes, не хардкодить второй раз);
  • спутники: UnitVisual, AudioCatalog;
  • конфиги-синглтоны: StatsConfig, SimTuningConfig, GameConfig;
  • дизайн-SO: CombatColorPalette (и будущие).

1.3 Страницы (итоговый набор v1)

СтраницаРоль МаксаСодержание
Browserвседерево по доменам, фильтры (домен/теги/поиск), CRUD, детальная: карточка + InspectorElement + stat-context (для юнитов)
Doctorпрогерполная валидация всего контента одним списком, severity, переход по клику, бейдж-счётчик на pill
BalanceГДтаблица юниты×статы (эффективные + производные), сортировка, хитмап, pin-to-compare, «Сравнить» из Browser
CoverageГДсводки «каких много/мало» по доменам/тегам/типам + экспорт markdown (обсидиан-таблица генерится, ручная умирает)
Visualхудожникпревью UnitVisual: портрет, плеер клипов (autoplay/скорость/пауза), линейка 1.7u, сетка всех юнитов
Audiosoundматрица покрытия AudioCatalog (юнит/событие × есть/пусто); прослушивание — отложено
Configsпрогер/UIконфиги и палитры через InspectorElement + подсветка отличий от код-дефолтов

1.4 Сквозная эргономика (утверждено)

  • Чипы-ссылки: любой реф на сущность в любой странице — кликабельный чип (иконка+имя+id) → переход к сущности.
  • Back/Forward с историей переходов (браузерная модель) — сценарий «юнит → UnitVisual → назад».
  • Ctrl+K — командная палитра: fuzzy-поиск по всему индексу, Enter → переход.
  • Stat-context у юнита: при настройке статов рядом видны база (StatsConfig), среднее по когорте, min–max — «средний и рекомендованный рядом». Открытый вопрос §5.1: что считать «рекомендованным» сверх базы.
  • Состояние окна (страница, фильтры, выбор, история) переживает domain reload и перезапуск редактора.
  • Все страницы — вьюхи над одним ContentIndex; никакая страница не сканирует ассеты сама.

1.5 Явно вне скоупа (roadmap)

  • Feel-лаба (dummy prefab + vfx/шейдер/feel-тумблеры) — вероятно, вырастет из Visual-страницы позже.
  • Matchup-симулятор (батчи детерминированных боёв из окна, winrate-матрица) — отдельный будущий инструмент; сим headless и детерминирован, техническая возможность есть.
  • Прослушивание FMOD-эвентов из хаба (помним инцидент с PlayInEditor-оверлеем) — только после Audio-матрицы и отдельным решением.
  • Локализация самого инструмента, drag-reorder, JSON-экспорт тем — не тащить из Bloodlines.

2. Архитектура кода

Assets/_Project/Scripts/EditorTools/ContentHub/
├── Guildmaster.ContentHub.Editor.asmdef   # editor-only
├── ContentHubWindow.cs                    # шелл: pill-нав, тулбар, роутер страниц
├── ContentHubWindow.<Page>.cs             # partial на страницу (как ThemeEditorWindow.Blueprints.cs)
├── Core/
│   ├── ContentIndex.cs                    # модель + граф ссылок + когорты статов
│   ├── ContentIndexPostprocessor.cs       # инвалидация на импорт/удаление/переименование
│   ├── ContentValidationService.cs        # правила Doctor (чистая логика, тестируемая)
│   ├── HubNavigation.cs                   # история Back/Forward + текущий адрес (страница, id)
│   └── StatMath.cs                        # эффективные статы через сим-бейк + производные колонки
├── Widgets/
│   ├── EntityChip.cs                      # кликабельный чип сущности
│   ├── TwoPane.cs                         # порт two-pane+search из ThemeEditorWindow (упрощённый)
│   ├── HubToasts.cs                       # порт Notify
│   └── CommandPalette.cs                  # Ctrl+K оверлей
├── Preview/
│   ├── WorldPreviewSurface.cs             # NewPreviewScene + мировая камера + RT (переделка PreviewSurface)
│   └── ClipPlayer.cs                      # проигрывание AnimationClip (autoplay/speed/pause)
└── Styles/
    ├── ContentHub.uss / ContentHubTokens.uss   # токены --gh-* по образцу --te-*

Ссылки asmdef: Guildmaster.Data (+ Guildmaster.Data.Editor — реюз ContentCrudService/ContentPaths/ContentIdUtility/ContentDatabaseSync), Guildmaster.Combat (бейк статов/тайминг атаки для StatMath), Guildmaster.Presentation (типы AudioCatalog, CombatColorPalette). Точные имена сборок сверить по фактическим asmdef при реализации.

ContentIndex — контракт:

  • строится лениво, кэшируется статически; Changed-событие для страниц;
  • записи: { ContentDefinition | спутник | конфиг } → id, тип, путь, displayName, иконка/портрет;
  • граф ссылок в обе стороны (Uses / UsedBy) — по SerializedObject-обходу object-реф полей, один проход на построение (замена медленному ContentCrudService.FindUsages-скану; сам CRUD-сервис перевести на индекс в пакете 4);
  • когорты статов: для Relic/Enemy — эффективный стат-блок (через StatMath) + агрегаты по когорте (среднее, медиана, min, max) с инвалидацией вместе с индексом.

3. Пакеты

Пакет 0 — Шелл окна + стили + виджеты-основа

Цель: пустое, но живое окно с pill-навигацией, готовое принимать страницы.

  1. Папка/asmdef по §2. Окно ContentHubWindow, меню Tools/Guildmaster/Content Hub, minSize ≈ 960×600.
  2. Pill-бар по образцу ThemeEditorWindow (BuildTabs/MakePill/OnPillClicked — перенос с зачисткой): группы Content (Browser) / Design (Balance, Coverage, Visual, Audio) / System (Doctor, Configs). Пустые страницы — заглушки HelpBox.
  3. USS-токены --gh-* + базовый chrome (порт подхода ThemeEditorTokens.uss, палитру взять нейтрально-тёмную, Максу — свобода перекрасить).
  4. HubToasts (порт Notify без локализации), TwoPane (порт BuildTwoPane+ApplyTwoPaneFilter, без drag).
  5. Персистентность: [SerializeField] текущая страница; OnFocus — мягкий rebuild (как у референса).

Acceptance: окно открывается, переключает страницы, переживает domain reload; тесты проекта зелёные (кода-логики ещё нет — прогон регрессионный).

Пакет 1 — ContentIndex

Цель: единая модель данных хаба. Самый важный пакет — всё остальное вьюхи.

  1. ContentIndex по контракту §2: скан по ContentPaths.CreatableTypes + UnitVisual + AudioCatalog + конфиги + CombatColorPalette; записи, граф Uses/UsedBy.
  2. ContentIndexPostprocessor — точечная инвалидация (изменённый ассет → перечитать запись и рёбра), полная перестройка только при массовых операциях.
  3. StatMath: эффективный стат-блок юнита = тот же путь бейка, что у сима (найти место, где RuntimeUnitFactory/сборка юнита накладывает StatModifier[] на дефолты StatsConfig; если логика не выделена в чистую функцию — выделить её В СИМ-КОДЕ и реюзать, не копировать). Производные: атак/сек и DPS автоатаки — формулы вывести из фактического кода тайминга (AttackTiming/AutoAttackSystem), не изобретать.
  4. Когорты: агрегаты (avg/median/min/max) по эффективным статам отдельно для Relics и Enemies.
  5. Тесты (EditMode): индекс находит все ассеты folder-per-type; граф симметричен (A.Uses(B) ⟺ B.UsedBy(A)); эффективные статы юнита из индекса == статам собранного RuntimeUnit (контракт «таблица не врёт»); агрегаты корректны на синтетике.

Acceptance: тесты пакета + полный прогон зелёные.

Пакет 2 — Browser + CRUD + детальная (замена v0)

Цель: полный паритет с ContentManagerWindow и лучше; v0 удаляется.

  1. Левая панель (TwoPane): дерево доменов (порядок CreatableTypes) + спутники/конфиги отдельными группами; счётчики в заголовках групп; фильтр-поиск (по id/имени), фильтр по TagData.
  2. Тулбар: Create (меню по CreatableTypesContentCrudService.Create), Duplicate, Delete (с диалогом usages — теперь из индекса, мгновенно), Sync DB, Validate (реюз логики ValidateSelected — перенести в ContentValidationService уже сейчас, окно зовёт сервис).
  3. Детальная: карточка-шапка (портрет/иконка, id, тип, кнопки Reveal/Ping, Duplicate, Delete, Compare — Compare пока disabled, оживёт в пакете 6) + InspectorElement(new SerializedObject(asset)).
  4. Stat-context (юниты): сворачиваемая панель под карточкой — таблица «стат → эффективное значение | база StatsConfig | среднее когорты | min–max», live-обновление при правке (подписка на SerializedObject-изменения/Undo.undoRedoPerformed + пересчёт через индекс).
  5. Удалить ContentManagerWindow.cs (логика Validate уже перенесена в сервис; CRUD и так в сервисе).
  6. Тесты: ContentValidationService на правила id (формат/домен/дубль) и null-компоненты эффектов.

Acceptance: все операции v0 доступны в хабе; v0 удалён; Макс подтверждает паритет руками; тесты зелёные.

Пакет 3 — Навигация: чипы, история, Ctrl+K

Цель: «один хаб» становится правдой — между сущностями ходят, а не ищут.

  1. EntityChip: иконка+имя+id, клик → переход, ПКМ-меню (Reveal, Compare, Duplicate). Детальная страница рендерит объект-рефы юнита чипами в карточке (визуал, AI-пресет, эффекты, теги, абилки) — секция «Связи» + «Используется в» из графа (UsedBy).
  2. HubNavigation: адрес = (страница, id, контекст); стек Back/Forward; кнопки в тулбаре + мышиные кнопки назад/вперёд; история сериализуется в окне.
  3. CommandPalette (Ctrl+K): оверлей-модал (порт паттерна TeModal), fuzzy по displayName/id всего индекса, стрелки+Enter, Esc.

Acceptance: сценарий Макса «юнит → чип UnitVisual → детальная визуала → Back» работает; Ctrl+K находит и переходит; тесты зелёные (навигация — чистые структуры, покрыть стек тестом).

Пакет 4 — Doctor

Цель: «валидно ли ВСЁ» одним экраном.

  1. Расширить ContentValidationService до полного прохода по индексу: дубли id; формат/домен id; битые object-рефы (missing); null-компоненты EffectData; юнит без Visual; UnitVisual без обязательных клипов (idle/run/attack); пустые обязательные поля по типам (список согласовать с Максом при реализации — первый проход по фактическим данным покажет шум).
  2. Страница: список issue (severity Error/Warning, текст, чип сущности → переход), группировка по типу проблемы, кнопка Rescan; авто-инвалидация вместе с индексом.
  3. Бейдж-счётчик ошибок на pill-кнопке Doctor (стиль — красная точка+число, как save-dot у референса).
  4. Перевести ContentCrudService.FindUsages на граф индекса (медленный скан GetDependencies остаётся фолбэком, если индекс не построен).
  5. Тесты: каждое правило Doctor на синтетических нарушителях (временные ассеты в тесте).

Acceptance: Doctor находит подсаженные нарушения и ведёт к ним; на реальном контенте список согласован с Максом (что чинится, что глушится); тесты зелёные.

Пакет 5 — Balance: таблица

Цель: первая горячая точка Макса — «все статы всех юнитов, сортировка, сравнение».

  1. MultiColumnListView: строки = Relics+Enemies (фильтр по типу/тегам/поиск сверху), колонки = полный стат-блок (перечень — стат-система файла «03», источник значений — StatMath, т.е. эффективные статы) + производные (минимум: атак/сек, DPS автоатаки; расширяемый реестр колонок — добавление колонки = одна декларация).
  2. Сортировка по любой колонке; хитмап относительно медианы колонки (двухцветная шкала ниже/выше, альфа по отклонению); замороженная первая колонка (имя-чип).
  3. Pin-to-compare: закрепить до 4 строк сверху; кнопка «только закреплённые».
  4. Клик по ячейке → детальная юнита (Browser) с раскрытым stat-context.
  5. Тесты: производные колонки против сим-формул (то же правило «таблица не врёт»); реестр колонок отдаёт значения для всех юнитов без исключений.

Acceptance: Макс видит все статы всех юнитов, сортирует, сравнивает; значения совпадают с фактическими в бою (спот-чек через тесты пакета 1/5); тесты зелёные.

Пакет 6 — Balance: связка с Browser + Coverage + экспорт

  1. Кнопка Compare в карточке юнита (заложена в пакете 2) → страница Balance с этим юнитом закреплённым и подсвеченным.
  2. Страница Coverage: счётчики контента по доменам; разрезы юнитов по AttackType/DamageType/ResourceType/инфо-тегам («каких много/мало» одним экраном); клик по сегменту → Browser с этим фильтром.
  3. Экспорт markdown: текущая таблица Balance (с фильтрами) → markdown-таблица в буфер/файл. Формат согласовать с обсидиановской таблицей персонажей; после приёмки ручная таблица в GDD помечается «генерится хабом».
  4. Тесты: markdown-экспорт на синтетике (стабильный формат).

Acceptance: сценарий «Сравнить» работает из детальной; Coverage отвечает на «кого мало»; экспорт вставляется в Obsidian без правок; тесты зелёные.

Пакет 7 — Visual

Цель: вторая горячая точка — «посмотреть юнита, его анимации, масштаб».

  1. WorldPreviewSurface: EditorSceneManager.NewPreviewScene + мировая 2D-камера + RT (переделка PreviewSurface из Bloodlines: без Canvas, world-space; фон — нейтральная шахматка/цвет).
  2. ClipPlayer: инстанс спрайт-объекта юнита в preview-сцене, проигрывание выбранного AnimationClip (AnimationMode/ручной сэмплинг clip.SampleAnimation по editor-времени — выбрать при реализации то, что стабильнее в preview-сцене). Управление: autoplay при выборе клипа, слайдер скорости (0.1–2×), пауза (решение Макса — только это, без покадрового шага).
  3. Детальная Visual: портрет, кнопки клипов (idle/run/attack/death/hit/skills — подписи и «пусто»-стейт), плеер, линейка-оверлей 1.7u (рекомендуемая высота из конвенции масштаба) поверх RT.
  4. Сетка всех юнитов (портреты или первый кадр idle, в едином мировом масштабе) — режим «Lineup» для проверки консистентности силуэтов; клик → детальная.
  5. Чипы-прыжки Unit ↔ UnitVisual в обе стороны (граф уже знает связь).
  6. Тесты: только не-визуальная логика (маппинг клипов, наличие/пусто); проигрывание — визуальная приёмка Макса.

Acceptance: Макс открывает юнита, смотрит анимации со скоростью/паузой, видит масштаб против линейки, lineup показывает всех; preview-сцена не течёт (закрытие окна освобождает сцену/RT — проверить повторными открытиями).

Пакет 8 — Audio coverage

  1. Страница Audio: матрица из AudioCatalog — строки = юниты/сущности, колонки = события каталога; ячейка = назначено/пусто; сортировка «самые дырявые сверху»; клик → детальная каталога (InspectorElement) с фокусом на записи.
  2. Прослушивание — НЕ в этом пакете (§1.5).
  3. Тесты: построение матрицы из каталога на синтетике.

Acceptance: видно, что озвучено, а что нет; тесты зелёные.

Пакет 9 — Configs & Design

  1. Раздел Configs: StatsConfig, SimTuningConfig, GameConfig, CombatColorPalette — детальные через InspectorElement.
  2. Diff-vs-defaults: временный CreateInstance типа → сравнение SerializedProperty-обходом → список «поле: значение ≠ дефолт (дефолт)» над инспектором + подсветка. Показывает «что я вообще накрутил» (field-initializers — наша тест-страховка, теперь и визуальная).
  3. Тесты: diff-механика на синтетическом SO (изменённые/неизменённые поля).

Acceptance: конфиги редактируются из хаба, отличия от дефолтов видны сразу; тесты зелёные.


4. Порядок и вехи

P0 шелл → P1 индекс → P2 Browser (v0 умирает) → P3 навигация → P4 Doctor
   → P5 Balance-таблица → P6 Compare+Coverage+экспорт → P7 Visual → P8 Audio → P9 Configs

Вехи приёмки Максом: после P2 (жизнь без v0), после P6 (ГД-контур целиком), после P7 (арт-контур). Порядок P5–P7 при желании Макса можно поменять местами (P7 не зависит от P5/P6).

5. Открытые вопросы (решить по ходу, не блокируют старт)

  1. «Рекомендованный» стат в stat-context: v1 = база StatsConfig. Если Максу нужны отдельные дизайнерские target-значения per-стат/per-роль — это новый маленький SO; решить после того, как панель поживёт.
  2. Точный список «обязательных полей» для Doctor per-тип — согласовать на реальном контенте (пакет 4).
  3. Формат markdown-экспорта — сверить с фактической обсидиан-таблицей персонажей (пакет 6).
  4. Палитра/стиль --gh-* токенов — Макс задаёт при первой визуальной приёмке (P0–P2), дальше только токены.