Статус: ТЗ к исполнению (функционал обсуждён и утверждён Максом 2026-07-12)
Исполняемое техзадание «Контент-хаба» — единого UI Toolkit
EditorWindowдля работы со всем дата-слоем проекта (контент-SO, визуалы, аудио-каталог, конфиги, палитры). Дизайн-обсуждение — §1 этого файла (отдельного дизайн-файла нет; при неоднозначности решает §1, при конфликте с реальным кодом — спросить Макса, не угадывать). Исполнение: пакеты 0–9 строго по порядку, один пакет = одна ветка = один PR вdev.
0. Общие правила исполнения (читать перед каждым пакетом)
- Ветки: от свежего
origin/dev, имяfeat/content-hub-p{N}. PR вdev, squash-merge, ветку после мержа удалить. Коммиты — английский, conventional (.cursor/rules/git-conventions.mdc). - Тесты: после каждого пакета — полный EditMode-прогон (Unity MCP
run_testsили./scripts/run-tests.ps1). База: все текущие тесты зелёные. Красный тест, который «мешает», — повод остановиться, а не править тест. - Хаб — editor-only и read-mostly. Никакого кода хаба в рантайм-сборках. Запись в ассеты — только через
SerializedObject/Undo(редактирование полей) иContentCrudService(CRUD). Рантайм-SO не получают ссылок на editor-типы. - Не дублировать сим. Любое вычисление, которое уже делает боевой код (бейк статов, тайминг атаки), — реюз того кода, не переписывание формул в хабе. Разошедшиеся формулы = баг класса «таблица врёт».
- Визуальная приёмка каждой страницы — за Максом; агент проверяет только не-визуально (индекс, значения, события, тесты). После каждого пакета — короткая инструкция Максу «что потыкать».
- Запреты из дата-слоя действуют: никакого Odin Serializer; мутаций SO в рантайме;
Resources.Load. Odin — только атрибуты/дровверы (сборки*.Editor). - Порт из Bloodlines-Dark-UI (
C:\My Projects\Bloodlines-Dark-UI\Assets\Alebardium\DesignSystem\Editor\) — это перенос паттерна с упрощением, не копипаст 1:1: выкидываем EditorLoc-локализацию, drag-reorder, JSON IO, WCAG. Файлы переписываются под наш namespace и наши нужды; авторство исходника — сам Макс, лицензионных вопросов нет. - Производительность: страницы работают с закэшированным
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, сетка всех юнитов |
| Audio | sound | матрица покрытия 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-навигацией, готовое принимать страницы.
- Папка/asmdef по §2. Окно
ContentHubWindow, менюTools/Guildmaster/Content Hub, minSize ≈ 960×600. - Pill-бар по образцу
ThemeEditorWindow(BuildTabs/MakePill/OnPillClicked— перенос с зачисткой): группы Content (Browser) / Design (Balance, Coverage, Visual, Audio) / System (Doctor, Configs). Пустые страницы — заглушкиHelpBox. - USS-токены
--gh-*+ базовый chrome (порт подходаThemeEditorTokens.uss, палитру взять нейтрально-тёмную, Максу — свобода перекрасить). HubToasts(портNotifyбез локализации),TwoPane(портBuildTwoPane+ApplyTwoPaneFilter, без drag).- Персистентность:
[SerializeField]текущая страница;OnFocus— мягкий rebuild (как у референса).
Acceptance: окно открывается, переключает страницы, переживает domain reload; тесты проекта зелёные (кода-логики ещё нет — прогон регрессионный).
Пакет 1 — ContentIndex
Цель: единая модель данных хаба. Самый важный пакет — всё остальное вьюхи.
ContentIndexпо контракту §2: скан поContentPaths.CreatableTypes+UnitVisual+AudioCatalog+ конфиги +CombatColorPalette; записи, графUses/UsedBy.ContentIndexPostprocessor— точечная инвалидация (изменённый ассет → перечитать запись и рёбра), полная перестройка только при массовых операциях.StatMath: эффективный стат-блок юнита = тот же путь бейка, что у сима (найти место, гдеRuntimeUnitFactory/сборка юнита накладываетStatModifier[]на дефолтыStatsConfig; если логика не выделена в чистую функцию — выделить её В СИМ-КОДЕ и реюзать, не копировать). Производные: атак/сек и DPS автоатаки — формулы вывести из фактического кода тайминга (AttackTiming/AutoAttackSystem), не изобретать.- Когорты: агрегаты (avg/median/min/max) по эффективным статам отдельно для Relics и Enemies.
- Тесты (EditMode): индекс находит все ассеты folder-per-type; граф симметричен (A.Uses(B) ⟺ B.UsedBy(A)); эффективные статы юнита из индекса == статам собранного
RuntimeUnit(контракт «таблица не врёт»); агрегаты корректны на синтетике.
Acceptance: тесты пакета + полный прогон зелёные.
Пакет 2 — Browser + CRUD + детальная (замена v0)
Цель: полный паритет с ContentManagerWindow и лучше; v0 удаляется.
- Левая панель (
TwoPane): дерево доменов (порядокCreatableTypes) + спутники/конфиги отдельными группами; счётчики в заголовках групп; фильтр-поиск (по id/имени), фильтр поTagData. - Тулбар: Create (меню по
CreatableTypes→ContentCrudService.Create), Duplicate, Delete (с диалогом usages — теперь из индекса, мгновенно), Sync DB, Validate (реюз логикиValidateSelected— перенести вContentValidationServiceуже сейчас, окно зовёт сервис). - Детальная: карточка-шапка (портрет/иконка, id, тип, кнопки Reveal/Ping, Duplicate, Delete, Compare — Compare пока disabled, оживёт в пакете 6) +
InspectorElement(new SerializedObject(asset)). - Stat-context (юниты): сворачиваемая панель под карточкой — таблица «стат → эффективное значение | база StatsConfig | среднее когорты | min–max», live-обновление при правке (подписка на
SerializedObject-изменения/Undo.undoRedoPerformed+ пересчёт через индекс). - Удалить
ContentManagerWindow.cs(логика Validate уже перенесена в сервис; CRUD и так в сервисе). - Тесты:
ContentValidationServiceна правила id (формат/домен/дубль) и null-компоненты эффектов.
Acceptance: все операции v0 доступны в хабе; v0 удалён; Макс подтверждает паритет руками; тесты зелёные.
Пакет 3 — Навигация: чипы, история, Ctrl+K
Цель: «один хаб» становится правдой — между сущностями ходят, а не ищут.
EntityChip: иконка+имя+id, клик → переход, ПКМ-меню (Reveal, Compare, Duplicate). Детальная страница рендерит объект-рефы юнита чипами в карточке (визуал, AI-пресет, эффекты, теги, абилки) — секция «Связи» + «Используется в» из графа (UsedBy).HubNavigation: адрес = (страница, id, контекст); стек Back/Forward; кнопки в тулбаре + мышиные кнопки назад/вперёд; история сериализуется в окне.CommandPalette(Ctrl+K): оверлей-модал (порт паттернаTeModal), fuzzy по displayName/id всего индекса, стрелки+Enter, Esc.
Acceptance: сценарий Макса «юнит → чип UnitVisual → детальная визуала → Back» работает; Ctrl+K находит и переходит; тесты зелёные (навигация — чистые структуры, покрыть стек тестом).
Пакет 4 — Doctor
Цель: «валидно ли ВСЁ» одним экраном.
- Расширить
ContentValidationServiceдо полного прохода по индексу: дубли id; формат/домен id; битые object-рефы (missing); null-компонентыEffectData; юнит безVisual;UnitVisualбез обязательных клипов (idle/run/attack); пустые обязательные поля по типам (список согласовать с Максом при реализации — первый проход по фактическим данным покажет шум). - Страница: список issue (severity Error/Warning, текст, чип сущности → переход), группировка по типу проблемы, кнопка Rescan; авто-инвалидация вместе с индексом.
- Бейдж-счётчик ошибок на pill-кнопке Doctor (стиль — красная точка+число, как save-dot у референса).
- Перевести
ContentCrudService.FindUsagesна граф индекса (медленный сканGetDependenciesостаётся фолбэком, если индекс не построен). - Тесты: каждое правило Doctor на синтетических нарушителях (временные ассеты в тесте).
Acceptance: Doctor находит подсаженные нарушения и ведёт к ним; на реальном контенте список согласован с Максом (что чинится, что глушится); тесты зелёные.
Пакет 5 — Balance: таблица
Цель: первая горячая точка Макса — «все статы всех юнитов, сортировка, сравнение».
MultiColumnListView: строки = Relics+Enemies (фильтр по типу/тегам/поиск сверху), колонки = полный стат-блок (перечень — стат-система файла «03», источник значений —StatMath, т.е. эффективные статы) + производные (минимум: атак/сек, DPS автоатаки; расширяемый реестр колонок — добавление колонки = одна декларация).- Сортировка по любой колонке; хитмап относительно медианы колонки (двухцветная шкала ниже/выше, альфа по отклонению); замороженная первая колонка (имя-чип).
- Pin-to-compare: закрепить до 4 строк сверху; кнопка «только закреплённые».
- Клик по ячейке → детальная юнита (Browser) с раскрытым stat-context.
- Тесты: производные колонки против сим-формул (то же правило «таблица не врёт»); реестр колонок отдаёт значения для всех юнитов без исключений.
Acceptance: Макс видит все статы всех юнитов, сортирует, сравнивает; значения совпадают с фактическими в бою (спот-чек через тесты пакета 1/5); тесты зелёные.
Пакет 6 — Balance: связка с Browser + Coverage + экспорт
- Кнопка Compare в карточке юнита (заложена в пакете 2) → страница Balance с этим юнитом закреплённым и подсвеченным.
- Страница Coverage: счётчики контента по доменам; разрезы юнитов по
AttackType/DamageType/ResourceType/инфо-тегам («каких много/мало» одним экраном); клик по сегменту → Browser с этим фильтром. - Экспорт markdown: текущая таблица Balance (с фильтрами) → markdown-таблица в буфер/файл. Формат согласовать с обсидиановской таблицей персонажей; после приёмки ручная таблица в GDD помечается «генерится хабом».
- Тесты: markdown-экспорт на синтетике (стабильный формат).
Acceptance: сценарий «Сравнить» работает из детальной; Coverage отвечает на «кого мало»; экспорт вставляется в Obsidian без правок; тесты зелёные.
Пакет 7 — Visual
Цель: вторая горячая точка — «посмотреть юнита, его анимации, масштаб».
WorldPreviewSurface:EditorSceneManager.NewPreviewScene+ мировая 2D-камера + RT (переделкаPreviewSurfaceиз Bloodlines: без Canvas, world-space; фон — нейтральная шахматка/цвет).ClipPlayer: инстанс спрайт-объекта юнита в preview-сцене, проигрывание выбранногоAnimationClip(AnimationMode/ручной сэмплингclip.SampleAnimationпо editor-времени — выбрать при реализации то, что стабильнее в preview-сцене). Управление: autoplay при выборе клипа, слайдер скорости (0.1–2×), пауза (решение Макса — только это, без покадрового шага).- Детальная Visual: портрет, кнопки клипов (idle/run/attack/death/hit/skills — подписи и «пусто»-стейт), плеер, линейка-оверлей 1.7u (рекомендуемая высота из конвенции масштаба) поверх RT.
- Сетка всех юнитов (портреты или первый кадр idle, в едином мировом масштабе) — режим «Lineup» для проверки консистентности силуэтов; клик → детальная.
- Чипы-прыжки Unit ↔ UnitVisual в обе стороны (граф уже знает связь).
- Тесты: только не-визуальная логика (маппинг клипов, наличие/пусто); проигрывание — визуальная приёмка Макса.
Acceptance: Макс открывает юнита, смотрит анимации со скоростью/паузой, видит масштаб против линейки, lineup показывает всех; preview-сцена не течёт (закрытие окна освобождает сцену/RT — проверить повторными открытиями).
Пакет 8 — Audio coverage
- Страница Audio: матрица из
AudioCatalog— строки = юниты/сущности, колонки = события каталога; ячейка = назначено/пусто; сортировка «самые дырявые сверху»; клик → детальная каталога (InspectorElement) с фокусом на записи. - Прослушивание — НЕ в этом пакете (§1.5).
- Тесты: построение матрицы из каталога на синтетике.
Acceptance: видно, что озвучено, а что нет; тесты зелёные.
Пакет 9 — Configs & Design
- Раздел Configs:
StatsConfig,SimTuningConfig,GameConfig,CombatColorPalette— детальные черезInspectorElement. - Diff-vs-defaults: временный
CreateInstanceтипа → сравнениеSerializedProperty-обходом → список «поле: значение ≠ дефолт (дефолт)» над инспектором + подсветка. Показывает «что я вообще накрутил» (field-initializers — наша тест-страховка, теперь и визуальная). - Тесты: 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. Открытые вопросы (решить по ходу, не блокируют старт)
- «Рекомендованный» стат в stat-context: v1 = база
StatsConfig. Если Максу нужны отдельные дизайнерские target-значения per-стат/per-роль — это новый маленький SO; решить после того, как панель поживёт. - Точный список «обязательных полей» для Doctor per-тип — согласовать на реальном контенте (пакет 4).
- Формат markdown-экспорта — сверить с фактической обсидиан-таблицей персонажей (пакет 6).
- Палитра/стиль
--gh-*токенов — Макс задаёт при первой визуальной приёмке (P0–P2), дальше только токены.