Статус: ТЗ к исполнению (дизайн утверждён 2026-07-10)
Исполняемое техзадание Фазы 4 «Контент-каркас». Дизайн-источник — 13. Дата-слой и контент-каркас (SO и конфиги) (ред. 3) — при любой неоднозначности этого ТЗ решает файл 13; при конфликте файла 13 с реальным кодом — спросить Макса, не угадывать. Исполнение: пакеты 0–7 строго по порядку (каждый опирается на предыдущий), один пакет = одна ветка = один PR в
dev.
0. Общие правила исполнения (читать перед каждым пакетом)
- Ветки: от свежего
origin/dev, имяfeat/content-framework-p{N}. PR вdev, squash-merge, ветку после мержа удалить. Коммиты — английский, conventional (.cursor/rules/git-conventions.mdc). - Тесты: после каждого пакета — полный EditMode-прогон (Unity MCP
run_testsили./scripts/run-tests.ps1). База: все текущие тесты зелёные (~196). Красный тест, который «мешает», — повод остановиться, а не править тест. - Детерминизм: пакеты 0–3 не должны менять поведение боя при дефолтных значениях. Существующие checksum/детерминизм-тесты — контракт; если изменились ожидания — это регрессия.
- Сцены и префабы: правки
BattleScene/префабов — только через Unity MCP с проверкой, что префаб не открыт в Prefab Stage (PrefabStageUtility.GetCurrentPrefabStage()== null — жёсткое правило проекта). Ссылочные поля на сцене (например, конфиг-SO наLifetimeScope) — черезmanage_gameobject/execute_code, НЕ ручной правкой YAML. Если редактор занят/недоступен — оставить шаг Максу с точной инструкцией. - Запреты из дизайна: никакого Odin Serializer;
AnimationCurve.Evaluateвне бейка; мутаций SO в рантайме;Resources.Load; статик-доступа к конфигам. Odin — только атрибуты/дровверы/окна (сборки*.Editor). [MovedFrom]: при любом переименовании/переносе типа, сериализуемого через[SerializeReference], — атрибут обязателен.- Сериализация при рефакторинге классов: Unity матчит данные по именам полей. Перенос поля в базовый класс безопасен, если имя не менялось. Переименование поля — только с
[FormerlySerializedAs]. Перед переносом сверить фактические имена в коде и.asset-YAML. - Editor-скрипты миграции ассетов: одноразовые утилиты класть в
Assets/_Project/Scripts/Data/Editor/Migrations/, менюTools/Guildmaster/Migrations/...; после прогона утилита остаётся в репо (история), но помечается[Obsolete]-комментарием «выполнено, дата». - Визуальная приёмка (пакет 4 и любые изменения картинки) — за Максом; агент проверяет только не-визуально (тесты, значения, события).
Целевая раскладка ассетов (наводится в пакете 1, перемещение .asset безопасно — GUID сохраняется):
Assets/_Project/ScriptableObjects/
├── Relics/ ├── Enemies/ ├── Effects/
├── Traits/ ├── Items/ ├── Tags/
├── AiPresets/ ├── Guildmasters/ ├── Consequences/
├── RunModifiers/ ├── Vessels/ ├── Visuals/
├── Configs/ └── Database/ContentDatabase.asset
Пакет 0 — Фикс-лист (чистка дублей и мёртвых конфигов)
Цель: один источник правды для каждого значения; поведение боя не меняется. Дизайн: файл 13 §4.2.
Шаги:
- ArmorK:
CombatLifetimeScopeперестаёт держать_armorK— значение берётся из уже инжектируемогоStatsConfig.ArmorConstantK(оба сейчас 100 — поведение идентично). Поле_armorKудалить, проводкуWithParameter/конструкторов перевести на конфиг. Сцену проверить: лишний serialized-остаток не мешает, но если MCP доступен — почистить. PassiveThresholdPct = 0.2вBulwarkComponent.cs:64иDodgeComponent.cs:79: заменить литерал на именованную константуAIProfile.DefaultPassiveThresholdPct(объявить вAIProfile, использовать и в его собственном дефолте). Итог: значение объявлено ровно один раз.- «Глобальный радиус»
500f(MarkTransferComponent.cs:16,AbilitySystem.cs:144,WhirlDashLandingComponent.cs:80): вынести вSimConstants.GlobalSearchRadius, три места читают её. - Радиус коллизии снаряда
Size × 0.25(AutoAttackSystem.cs:117,148): вынести вSimConstants.ProjectileHitRadiusFactor. - Деспавн снарядов
±200(ProjectileSystem.cs:135): заменить на «границы арены +SimConstants.ProjectileDespawnMargin(предложение: 5)». Выяснить, как границы арены доступны системе (сим уже знает bounds для стен) — привязать к ним, не к мировому нулю. GuildmasterCommands.MakeTestUnit: сверить дефолт-статы болванчика соStatsConfig; где инжект доступен — читать оттуда, где нет — оставить с комментарием-ссылкой на StatsConfig (не плодить тихий дрейф).DamageNumberSpawner/DamageNumber: проверить использование (сцена/префабы/код). Если мертвы — удалить оба файла; если живы — доложить Максу, не удалять.CombatFocusTarget._positionDamping: код-дефолт 3 → 4 (значение сцены; косметика).
Acceptance: полный прогон зелёный; grep не находит литералов 0.2f (в этих трёх местах), 500f, 0.25f (в двух местах), 200f в перечисленных файлах; поведенческие/checksum-тесты без изменений ожиданий.
Пакет 1 — ContentDefinition, id, реестр, валидация
Цель: каркас идентификации и реестра. Дизайн: файл 13 §2, §3.6, §8.
Шаги:
ContentDefinition : ScriptableObject(abstract) вGuildmaster.Data.Definitions:[SerializeField] string _id+public string Id.ContentDomains(static, там же): маппинг тип → префикс домена (RelicData → "relic",EffectData → "effect",VesselData → "vessel", далее пополняется). УтилитаMakeId(Type, assetName)→domain.lower_snake.- Жизненный цикл id (файл 13 §2.3): editor-логика в
Guildmaster.Data.Editor:- автозаполнение пустого id из имени ассета (хук
OnValidateвContentDefinitionпод#if UNITY_EDITOR, сама генерация — в editor-утилите); - Odin-drawer: id read-only + кнопка «Edit Id» (разблокировка с warning);
- авто-уникализация суффиксом
_2при генерации, если занято.
- автозаполнение пустого id из имени ассета (хук
- Миграция существующих типов:
RelicData,VesselData,EffectDataнаследуютContentDefinition. УEffectDataсейчас собственный Id — сверить имя backing-поля; если не_id, перенести с[FormerlySerializedAs]. Проставить ids миграционным скриптом:relic.cryomancerи т.д. (из имён ассетов), эффекты — префиксовать существующие id доeffect.*. ПолеDisplayNameKeyудалить изRelicData/VesselData(ключи теперь выводятся из id — данные в ассетах уже соответствуют шаблону, потери нет). ContentDatabaseSO +IContentDatabase(файл 13 §3.6, сигнатуры там). Реализация словаря — при регистрации в DI. Editor-утилитаTools/Guildmaster/Sync Content Database— сканирует проект и наполняет списки (никакого ручного перетаскивания).- DI:
[SerializeField] ContentDatabaseнаRootLifetimeScope→RegisterInstance<IContentDatabase>. ⚠ Назначение ассета в сцене — правило 0.4 (MCP или Макс). - Раскладка папок (см. §0) — переместить существующие ассеты.
- EditMode-тесты
Tests/EditMode/Content/ContentValidationTests.cs(правила файла 13 §8: 1, 2, 3, 5): формат/уникальность/непустота id по всемContentDefinition-ассетам; полнотаContentDatabase(+ актуальность после Sync); отсутствие null в[SerializeReference]-списках; кросс-инварианты (FleeAtHpPct < ReturnAtHpPct,AttackSpeedMin < Max).
Acceptance: прогон зелёный, включая новые тесты; у всех контент-ассетов id формата domain.name; ContentDatabase.asset создан и полон; спавн через gm_spawn_* работает как раньше.
Пакет 2 — Конфиги: SimTuningConfig, расширение StatsConfig, GameConfig
Цель: все глобальные ручки — в SO; сим получает снапшот. Дизайн: файл 13 §3.4, §4.
Шаги:
SimTuningConfigSO (Configs/): поля и дефолты — таблица файла 13 §3.4 (AiTickRate, MaxAttackAnimTicks, MinWindupTicks, сепарация ×4, ProjectileHitRadiusFactor, ProjectileDespawnMargin, KiteFleeFactor, DefaultPassiveThresholdPct, GlobalSearchRadius).TickRateНЕ переносится.- Бейк: plain-структура
SimTuning(readonly, вGuildmaster.CoreилиCombat);CombatLifetimeScopeстроит её из SO один раз при создании скоупа и инжектит в системы. Системы/SimConstants-потребители переводятся на снапшот; вSimConstantsостаются толькоTickRate/производные и не-балансные константы. Константы из пакета 0 (GlobalSearchRadiusи др.) переезжают изSimConstantsв конфиг. SeparationSystempublic-поля (live-твикgm_sep_*) инициализируются из снапшота; команды продолжают работать.StatsConfig: заполнитьDefaultsбазовыми значениями юнита (согласовать список с Максом по факту: MaxHP, MoveSpeed, AttackRange, AttackSpeed…— свериться с 03. Стат-система (полный список статов) и текущими значениями болванчика/реликвий, чтобы дефолты не изменили баланс семёрки).GameConfigSO: дефолты громкостей (master/music/sfx),DefaultLocale,VesselItemSlots = 3. Регистрация вRootLifetimeScope. Потребителей пока нет — только тип, ассет, DI.- Dev-команда
gm_tuning_rebake(QC): пересобратьSimTuningиз SO и применить к живому бою +Debug.LogWarning("battle tainted"). - Тесты: конфиг-инварианты в
ContentValidationTests(клампы, диапазоны); тест «дефолты SimTuningConfig == прежние SimConstants-значения» (страховка неизменности баланса).
⚠ Сцена: поле SimTuningConfig на CombatLifetimeScope — правило 0.4.
Acceptance: прогон зелёный; checksum/детерминизм-тесты без изменений; gm_sep_* работает; grep: балансных литералов из таблицы §3.4 в коде систем нет.
Пакет 3 — Иерархия UnitData + новые контент-типы
Цель: UnitData → RelicData/EnemyData; все мета-SO Фазы 4. Дизайн: файл 13 §3.0–3.2.
Шаги:
UnitData : ContentDefinition(abstract): перенести боевой кит изRelicData(список — файл 13 §3.1), имена сериализованных полей не менять (правило 0.7).ExtendedZoneAccessостаётся вRelicData.RelicData : UnitData: остаются/добавляютсяRarity(RelicRarity),RunEffects: EffectData[],AltAiPresets: AIPresetData[],ExtendedZoneAccess.EnemyData : UnitData:ThreatPoints: int,GoldBounty: int.- Потребители:
RuntimeUnitFactory,BattleSetupBuilder,GuildmasterCommands— приниматьUnitDataтам, где нужен только кит (vessel-параметр остаётся опциональным как сейчас). AIPresetDataSO + миграция: editor-скрипт создаёт 7 ассетовai_preset.*из inline-Aiреликвий, назначает вUnitData.Ai; после проверки — удалить inline-полеAIProfileизRelicData(отдельным коммитом внутри пакета: сначала перенос+назначение, прогон, потом удаление поля).- Новые SO (поля — таблицы файла 13 §3.0/3.2, без самодеятельности):
TagData(+TagCategory),TraitData(+TraitPolarity),ConsequenceData(+ConsequencePolarity),GuildmasterData,ItemData(+ItemScope),RunModifierData(+RunModTarget). Enum’ы — вGuildmaster.Data.Definitionsрядом сCombatCategories.cs. InfoTags: TagData[]— добавить вUnitData,EffectData,AbilityData, и в новые типы по таблицам.EffectData.Icon,UnitData.Icon— добавить.ContentDomains+ContentDatabase+ Sync + валидация — пополнить всеми новыми типами (правило 7 §8:GuildmasterData.StartingRelicIdsсуществуют, и т.п.).- Глоссарий: в
docs/wiki/gdd/0.4. Локализация.md— Перк→Trait (EN), добавить строку Последствие→Consequence (система: Травма/Закалка — виды последствий). Упоминания в GDD 4/9 — минимальная синхронизация терминов EN.
Acceptance: прогон зелёный; бой семёрки идентичен (checksum-тесты); .asset реликвий не потеряли данных (диф ассетов — только ожидаемые поля); все новые типы создаются через CreateAssetMenu, валидируются, зарегистрированы в БД.
Пакет 4 — Unity Animation: клипы-слоты и маркеры-как-данные
Цель: UnitVisual на AnimationClip-слотах; сим-тайминг из маркеров. Дизайн: файл 13 §3.1 (UnitVisual), решение ред. 3.
Шаги:
UnitVisualv2: поля-слотыAnimationClip(Idle/Run/Attack/Death/Hit/Skills[4]) +Portrait. Старые поля (спрайт-массивы, fps,attackHitFrames) пока пометить[FormerlySerializedAs]-нейтрально/оставить — удалить после успешной конверсии (шаг 5), отдельным коммитом.- Базовый
AnimatorController(один на проект,Visuals/UnitBase.controller): стейты по слотам;UnitViewсобираетAnimatorOverrideControllerв рантайме изUnitVisual. UnitView: заменить кастомный кадровый плеер наAnimator;animator.fireEvents = false; портировать логику подгонки скорости стейта Attack под сим-windup (маркер удара должен лечь на сим-тик удара — логика существует в текущем плеере, переносится).- Ридер маркеров: утилита (Data или Presentation — по месту бейка) читает
clip.events, фильтр поfunctionName == "Marker";event.time→ тики при бейке боя. Источник тайминга удара для сима — как раньшеattackHitFrames, теперь маркеры клипа Attack. - Конверсия 2 ассетов (
FantasyWarrior,MedievalWarrior): editor-скрипт генерируетAnimationClip-ы из спрайт-массивов (keyframe = кадр, длительность из fps),attackHitFrames→AnimationEvent("Marker")в клипе Attack. Клипы — вVisuals/{Name}/. AbilityData.VisualSlot(SkillSlotenum) — поле добавить; каст играет слот (у текущих способностей — дефолт Skill1, назначить осмысленно, где очевидно).- Валидация §8 п.8: обязательные слоты заполнены; клипы Attack/Skill содержат ≥1 маркер; время маркера в пределах клипа;
VisualSlotуказывает на непустой слот. - ⚠ Префаб
UnitViewменяется (Animator-компонент) — правило 0.4 (Prefab Stage) обязательно.
Acceptance: прогон зелёный (включая windup/тайминг-тесты — ожидания не меняются); валидация маркеров зелёная; визуальная приёмка Макса (idle/run/attack/death, момент удара совпадает с ощущением до миграции); после приёмки — коммит с удалением легаси-полей и старого плеера.
Пакет 5 — Контент-менеджер (Odin-окно)
Цель: одно окно для всего контента. Дизайн: файл 13 §9.
Шаги:
ContentCrudService(plain editor-класс, без GUI-вызовов,Guildmaster.Data.Editor): Create (тип → папка по §0, автогенерация id, фокус-переименование), Duplicate (с регенерацией id), Delete (проверка использований черезSearchService/ref=→ список), пути folder-per-type, вызов Sync БД после операций.ContentManagerWindow : OdinMenuEditorWindow(Tools/Guildmaster/Content Manager): дерево доменов черезAddAllAssetsAtPath, search-тулбар, справа Odin-инспектор; тулбар Create/Duplicate/Delete/Validate (Validate — прогон правил §8 по выбранному, вывод в окно).- Инструкция для Макса (в PR-описании): сетап Unity Search — включить dependency indexing, Saved Searches
t:RelicData,t:EffectData…
Acceptance: ручной чеклист (создать/дублировать/удалить/провалидировать каждый тип — id корректны, файлы в правильных папках, БД синхронизирована); рантайм-код не тронут; прогон зелёный.
Пакет 6 — Локализация + Addressables-плумбинг
Цель: таблицы EN/RU живые, ключи валидируются, билд не теряет переводы. Дизайн: файл 13 §5, §6.
Шаги:
- Пакет Addressables 2.3.1 → 2.3.16 (
manifest.json), Localization — Localesen,ru; String Table CollectionContent(+UIзаготовкой). Auto-группыLocalization-*не трогать; Play Mode Script —Use Asset Database. - Build Addressables on Player Build включить в настройках; для CI (GameCI) — проверить, что контент собирается при билде плеера (при необходимости — явный
BuildPlayerContent()в билд-методе; см. грабли game-ci #230). ILocalizationService(Guildmaster.Coreинтерфейс, реализация вGameповерх таблиц):GetString(key), смена локали. UI-потребители подключатся в Фазе 7 — сейчас сервис + DI.- Editor-помощник (файл 13 §6): drawer в инспекторе
ContentDefinition— поля Name/Desc (EN/RU) читают/пишут entries таблицыContentпо ключам{id}.name/{id}.desc; кнопка «Create missing keys» (по ассету и по домену). - Наполнить entries для существующего контента (7 реликвий, 2 визуала не нужны, вессели, эффекты с иконками) — EN-текст черновой (имена из ассетов), RU — по GDD-карточкам реликвий, где есть.
- Валидация §8 п.4: обязательные суффиксы по доменам (relic/trait/item/…:
name+desc; effect:name,desc— только если Icon задан).
Acceptance: прогон зелёный (включая новый лок-тест); в редакторе имя реликвии резолвится из таблицы; смена локали en↔ru меняет строку (проверка через тест сервиса или ручной чек).
Пакет 7 — AudioCatalog
Цель: аудио-шов готов к приходу FMOD-контента. Дизайн: файл 13 §3.5, §7.
Шаги:
AudioCatalogSO — в Presentation-слое (проверить/добавить ссылку asmdef Presentation → FMODUnity):Entries {Key, EventReference}[],Defaults {Action, EventReference}[].- Резолвер (Presentation): ключ
{contentId}.{action}→ точный → дефолт действия → тишина + один лог на ключ за сессию. Подписка на боевые события MessagePipe — по образцу существующих презентеров; вызовы уходят вIAudioService.Play(resolvedKey)(реализация — заглушка, как сейчас). - Валидация: FMOD-сканер (Event References) — инструкция в PR; свой тест — каталог без дубликатов ключей, все
Defaultsпокрывают канонический список действий (attack,hit,death,cast,ui).
Acceptance: компилится, прогон зелёный, резолвер покрыт юнит-тестом с фейк-каталогом; звука нет и не должно быть (FMOD-контент — Фаза 7/9).
Пакет 8 — Ф5-типы (ОТЛОЖЕН)
EncounterData, ActMapConfig, TextEventData, ShopConfig, RewardTableData, DifficultyConfig, DossierPoolsConfig — не делать в этой фазе. Создаются при строительстве флоу забега (Фаза 5) по таблицам файла 13 §3.3; домены уже зарезервированы, валидация готова к расширению.
Сводный Definition of Done Фазы 4
- Пакеты 0–7 смержены в
dev, ветки удалены. - Полный EditMode-прогон зелёный; checksum/детерминизм-тесты не меняли ожиданий ни в одном пакете.
- Все контент-ассеты: id
domain.name, зарегистрированы вContentDatabase, лок-ключиname/descсуществуют. - Глобальные балансные ручки — только в
StatsConfig/SimTuningConfig/GameConfig(grep-чек по таблице §3.4). - Контент-менеджер: create/duplicate/delete/validate работают по всем типам.
- Визуальная приёмка Макса: анимации (пакет 4) и общий смоук боя.
- Файл 13 переведён в статус «Реализовано» (+ отражены отклонения, если были); 07. Техдолг, решения и changelog пополнен записью о фазе.