Статус: План составлен (2026-07-14). Часть A реализована (2026-07-14): A1 090913e3 (+.meta f7ebbe0a), A2 ce6cfb33, A3 699a618d — ветка feat/encounter-data-loader, 281/281 EditMode зелёные. Замкнутый разрез «бой → исход → награда 1-из-3 → рост гильдии». Ждёт play-mode QA + дизайн-полиш экрана награды (собран кодом, без UXML). Следующее — развилка §7.1 + ресёрч B1 (генерация карты vs пролог-template): решение Макса. Прогресс/решения — память vertical-slice-progress. Развилки — §7. Опирается на Planning - Roadmap (Фаза 5 «Флоу забега» + цель Фазы 8), флоу забега (код Assets/_Project/Scripts/Game/Flow/), Planning - Deployment & Encounters, meta-progression.
Достраивает dev-срез боя (Planning - Deployment & Encounters, шаги 1–5 готовы) до вертикального разреза: замкнутая петля забега «меню → пролог-карта → узлы (бой / текст-ивент / магазин) → награды → мини-босс → итог», с растущим состоянием гильдии, предметами/баннерами-заглушками, вариативным AI и лимитом реликов. Всё пишется сетевой-ready (швы под host-authoritative кооп), но кооп-реализация — Фаза 6, здесь только соло.
1. Отправная точка (факт на 2026-07-14)
Есть (одна играбельная клетка боя): data-driven энкаунтеры/пресеты (EncounterData, BattlePresetData), гоблины, расстановка Free (DeploymentController), loadout релика (LoadoutViewModel + MenuRouter), EncounterLoader/ResetBattle как движок запуска, BattleOutcome + детект вайпа команды в CombatSimulation. Настройки-меню на UITK (ui-toolkit-settings-menu).
Есть в данных, но не в петле: ItemData (полный: скоупы Vessel/Party, StatModifier[], активка, цена, вес пула), GameConfig (VesselItemSlots = 3), AIPresetData (по 1 пресету на релик, по 3 на гоблинов).
Нет (макро-петля): GameFlow — заглушка Фазы 1 (Boot → LoadBattle → Unload). Нет RunState, IEventFlow, карты, наград, главного меню, автосейва забега. Между боями — пустота: некуда нести золото, релик-награду, растущий ростер.
Вывод: бой замкнут, забег — нет. Этот план строит макро-петлю вокруг готовой боевой клетки и добирает контент-глубину (предметы, AI-варианты, лимит реликов), которую Макс запросил.
2. Ведущая архитектура (одна, не меню вариантов)
RunState (durable, по строковым ID) = единственный источник истины забега; GameFlow (async UniTask) = оркестратор; узлы карты = полиморфные IEventFlow; бой/награда/сцена — эфемерные слои, собираемые из RunState. Ровно модель флоу забега (код Assets/_Project/Scripts/Game/Flow/) §2–4.
Почему так, а не enum-стейт-машина на верхнем уровне:
- Макро-флоу — это последовательность решений игроков, не покадровая переигровка.
async/awaitчитается как сценарий и не угрожает детерминизму боя (детерминизм живёт внутри tick-loop, отдельно). IEventFlowдаёт новые типы узлов (текст-ивент, магазин, риск) без правки центрального switch — прямое требование «добавь текстовые ивенты».RunState/RuntimeUnitразделены жёстко:RunStateпереживает забег и сохраняется,RuntimeUnitсобирается на каждый бой и отбрасывается. Это убивает старую боль «рантайм протёк в данные».
Сетевой-ready как сквозной принцип (реализация — Фаза 6):
RunState— только строковые ID + сериализуемые POCO → реплицируется хостом как есть.GameFlowкрутится концептуально на хосте; переходы, ждущие игроков, идут через интерфейсIReadyGate.WhenAllReady()— соло-реализация возвращает мгновенно, NGO-реализация ждёт всех. Шов есть, тела нет.IEventFlow.Run(RunContext)— хост исполняет, клиенты шлют интенты черезIPlayerIntentSource(соло = локальный ввод). Интерфейсы вводим сейчас, тела соло.- Автосейв = снапшот
RunState→ он же основа репликации и реконнекта.
Нельзя переусердствовать: никаких NGO-типов, RPC, NetworkVariable в этом заходе. Только чистые интерфейсы-швы с соло-телами. Правило seams-vs-defer.
3. Модель данных петли (что заводим)
3.1. RunState (durable POCO, сохраняется в ES3 через DTO)
Поля (все по строковым ID):
seed,currentActIndex,mapGraph(граф узлов + позиция),difficulty.guild:RosterSlot[]— 4 сосуда, каждый{ vesselId?, relicId, aiPresetId, vesselItemIds[], savedPosition }.relicInventory: string[]— собранные, но не надетые релики (запас для свапа между боями).relicCapacity: int— лимит вместимости коллекции реликов (см. §5.4).partyItemIds: string[]— баннеры (Party-скоуп предметы, действуют на всю команду в бою).gold.- Привязка
player → RosterSlot[](для коопа; соло — все слоты одному).
RunState собирается в боевые RuntimeUnit штатной RuntimeUnitFactory.Create(relic, vessel, …) на каждый бой; изменения (травмы позже) пишутся обратно.
3.2. IEventFlow + RunContext
public interface IEventFlow { UniTask<EventResult> Run(RunContext ctx); }RunContext = { RunState, IRngService, IReadyGate, IPlayerIntentSource, сервисы UI }. Реализации: BattleFlow, TextEventFlow, ShopFlow (см. §5). Новый тип = новый класс, центр не трогаем.
3.3. MapNodeData / граф акта
Узел = { id, NodeType, полезная нагрузка (encounter/preset/textEventId/shopPoolId), рёбра }. Граф либо генерится из сида (IRngService), либо берётся из заготовки пролога (§5.5). Хранится в RunState.mapGraph, доступен оверлеем в бою (read-only).
3.4. Ассеты-контент (SO)
TextEventData : ContentDefinition(доменevent) — StS2-текст: заголовок, тело (лок-ключи), 2–4EventChoice { лок-текст, условие?, список Consequence }.Consequence— уже есть в дата-слое (см. data-layer-part3-plan): дать/убрать релик, золото, предмет, травму. Переиспользуем, не изобретаем.RunTemplateData : ContentDefinition(доменrun_template) — заранее выложенный граф для пролога (§5.5): фиксированные узлы со ссылками наBattlePresetData/TextEventData/ shop-пул.ItemData-ассеты — предметы (Vessel) и баннеры (Party), §5.6.AIPresetData-ассеты — по 2–3 на релик, §5.7.
4. Порядок работ (шаги, каждый — сессия + коммит)
Data-first, как в Planning - Deployment & Encounters. Каждый шаг оставляет играбельный результат.
Часть A — скелет петли (замыкает «бой → исход → награда → следующий бой»)
Шаг A1 — RunState + DTO + автосейв + GameConfig-лимиты [фундамент]
RunStatePOCO (§3.1) вGuildmaster.Guild(илиCore), DTO-слой + ES3-плумбинг (data-layer-principles: SO→POCO→DTO). Три точки автосейва (флоу забега (кодAssets/_Project/Scripts/Game/Flow/) §5).GameConfig: добавитьRelicCapacityBase,RelicCapacityMax(лимит реликов, §5.4).VesselItemSlotsуже есть.- Тесты: сериализация round-trip, валидация лимитов.
- Разблокирует: всё, что растёт между боями.
Шаг A2 — GameFlow (async) + IEventFlow + BattleFlow(Prep→Combat→Outcome) [оркестратор]
- Заменить заглушку
GameFlowнастоящим async-флоу:Boot → MainMenu → RunSetup → Run → Act loop. ВвестиIReadyGate/IPlayerIntentSource(соло-тела). BattleFlow: Prep (готово —DeploymentController+loadout) → Combat (готово) → Outcome (новое): поBattleOutcomeпоказать исход, ретраи (до 2, флоу забега (кодAssets/_Project/Scripts/Game/Flow/) §6), вернутьEventResult.EncounterLoaderвызывается изBattleFlow, а не из dev-панели (dev-панель остаётся как debug-вход).- Разблокирует: узлы карты можно исполнять; появляется понятие «после боя».
Шаг A3 — Экран исхода + Экран наград (выбор 1 из 3 реликов) [сердце рогалика]
- Reward-экран на UITK/MVVM (реюз паттерна
LoadoutViewModel/MenuRouter): 3 релика из пула наград (IRngService, ramp по meta-progression-economy: 1 из 3, шансы по типу боя) → выбор пишется вRunState.relicInventory. - Тут же enforce лимит реликов (§5.4): если
relicInventoryполон — выбрать, что сбросить, или пропустить награду. - Разблокирует: осмысленное золото/прогресс; ростер растёт.
Часть B — карта, пролог и типы узлов
Шаг B1 — Карта акта + оверлей выбора узла [навигация]
MapNodeData/граф (§3.3), генерация из сида ИЛИ загрузкаRunTemplateData. Оверлей-UI (read-only поверх сцены, флоу забега (кодAssets/_Project/Scripts/Game/Flow/) §9) для выбора следующего узла.MapStateвRunState.- Разблокирует: последовательность узлов = собственно забег.
Шаг B2 — Пролог: заготовленная обучающая карта [онбординг]
RunTemplateData-ассетrun_template.prologue: не процедурный, фиксированная короткая цепочка — 2–3 боя-пресета по нарастанию + 1 текст-ивент + магазин + мини-босс. Точки для минимальных обучающих подсказок (тултип-строки, без сложной туториал-системы).- «Новый забег» из меню → сначала пролог (template), потом обычный акт (сид). Флаг «пролог пройден» в сейв-профиле (не в
RunStateзабега). - Разблокирует: управляемый первый опыт; демо-срез за стендом.
Шаг B3 — Типы узлов: TextEventFlow + ShopFlow [контент-петли]
TextEventFlow— StS2-текст: показатьTextEventData, дать выбратьEventChoice, применитьConsequence[]кRunState. UITK-экран (текст + кнопки выборов + результат).ShopFlow— купить релик/предмет/баннер заgold(пул черезShopWeight/Cost, уже вItemData). Здесь же — точка апгрейдаrelicCapacity(§5.4) как товар.- 3–4
TextEventData-ассета для пролога + акта. - Разблокирует: разнообразие узлов; «Бой + Магазин + Текст-ивент» = минимум роадмапа.
Часть C — вход в игру
Шаг C1 — Начальное меню (MainMenu) [обёртка]
- UITK/MVVM-экран: Новый забег (→ пролог, если не пройден, иначе выбор) · Продолжить (загрузить автосейв
RunState) · Настройки (готово — ui-toolkit-settings-menu) · Выход. GameFlow.Bootведёт в меню, а не сразу в бой.- Разблокирует: игра запускается как игра, а не как dev-сцена.
Часть D — контент-глубина (параллелится с A/B/C, чистые данные)
Шаг D1 — Предметы + баннеры (заглушки доп-статов) [контент]
- Предметы =
ItemDataскоупVessel(носителю, слоты ужеVesselItemSlots=3): 2–3 ассета, пока чистыеStatModifier[](+HP / +урон / +скорость). Проводка вRuntimeUnitFactory(надеть моды слота на сборку юнита) + loadout-таб «Предметы» частично оживает. - Баннеры =
ItemDataскоупParty(вся команда/бой): 2 ассета,StatModifier[]на всех союзников. Проводка вBattleFlow/сборку боя (применить party-моды команде) + хранение вRunState.partyItemIds. - Активки/эффекты предметов — поля есть, но пока не реализуем (заглушка = только статы, как просил Макс). Магазин (B3) их продаёт.
- Разблокирует: экономику магазина; слой «Сосуда» отдельно от релика.
Шаг D2 — AI-пресеты: 2–3 варианта на релик [контент + оживление таба]
- Сейчас по 1 пресету на релик. Добавить каждому из 7 реликов ещё 1–2 осмысленно отличающихся пресета (напр. Assassin:
dive_squishy/anti_tank/self_preserve; Defender:hold_line/peel_carry). Отличие — в Filter/Score/Positioning, не косметика. - Loadout-таб «AI» перестаёт быть заглушкой: выбор пресета из доступных релику → пишется в
RosterSlot.aiPresetId. - Разблокирует: агентность игрока в prep помимо позиции/релика; демонстрация AI-движка (главное отличие проекта).
5. Уточнения по запросам Макса
5.1. Текстовые ивенты (StS2-style)
Тип узла TextEventFlow + ассеты TextEventData (§3.4, B3). Заголовок/тело/2–4 выбора, каждый выбор → Consequence[] на RunState (дать релик/золото/предмет/травму). Полиморфны через IEventFlow — новые ивенты без правки центра.
5.2. Начальное меню
MainMenu (C1): Новый забег / Продолжить / Настройки (готово) / Выход. UITK, вход из GameFlow.Boot.
5.3. Пролог
run_template.prologue (B2) — заранее выложенная, не процедурная карта: фикс-цепочка боёв-пресетов + текст-ивент + магазин + мини-босс, с минимальными обучающими подсказками. «Новый забег» ведёт через пролог до первого настоящего (сид-)акта.
5.4. Лимит реликов (трактовка — уточни, если иначе)
Трактую как: вместимость коллекции реликов гильдии (RunState.relicCapacity), а не боевые слоты (их всегда 4 сосуда) и не «копии в уровень» (это отдельная механика meta-progression).
- База
GameConfig.RelicCapacityBase, потолокRelicCapacityMax. - Что работает: при получении релика-награды (A3), если
relicInventory.Count >= relicCapacity→ игрок обязан либо сбросить существующий релик, либо отказаться от награды. Enforce на входе в инвентарь, единая точка. - Увеличение: товар в магазине (B3) или награда узла —
relicCapacity += 1(доRelicCapacityMax). - Если Макс имел в виду лимит копий одного релика или размер боевого пула — механика та же (кап + enforce + апгрейд), меняется лишь на что смотрит счётчик. Развилка §7.
5.5. Предметы + баннеры
На существующем ItemData (D1): предметы = Vessel-скоуп (3 слота на сосуд, есть), баннеры = Party-скоуп (на команду). Пока только StatModifier[]-заглушки; активки/эффекты — поля есть, реализация позже.
5.6. AI: 2–3 пресета на релик
D2: добрать реликам варианты, отличающиеся в Filter/Score/Positioning; таб «AI» в loadout оживает.
5.7. Сетевой-ready, но не реализуем
§2: интерфейсы-швы (IReadyGate, IPlayerIntentSource, RunState по ID, автосейв=снапшот) с соло-телами. Никаких NGO/RPC в этом заходе.
6. Граф зависимостей
A1 (RunState + сейв + лимиты)
├── A2 (GameFlow + IEventFlow + BattleFlow.Outcome)
│ └── A3 (награды + enforce лимита реликов)
│ └── B1 (карта + оверлей)
│ ├── B2 (пролог-template)
│ └── B3 (TextEvent + Shop)
│ └── C1 (MainMenu)
└── D1 (предметы/баннеры) ← параллельно, чистые данные
└── D2 (AI-пресеты) ← параллельно, чистые данные
A-цепочка последовательна (каждый шаг встаёт на предыдущий). D1/D2 — контент, параллелятся с A/B/C; магазин (B3) и loadout-табы подхватывают их, когда готовы.
Минимальный «ощутимый» разрез = A1→A2→A3 на цепочке из 3 боёв: уже чувствуется рогалик. Остальное расширяет.
7. Развилки (на Макса)
- Лимит реликов — что именно? Дефолт: вместимость коллекции запаса (§5.4). Альтернативы: лимит копий одного релика / размер боевого пула. Механика (кап+enforce+апгрейд) одна, меняется объект счёта. → уточнить перед A1.
- Пролог — где живёт флаг «пройден»? Дефолт: в профиле сохранения (мета вне забега), не в
RunState. → мелочь, дефолт разумен. - Соло-first vs coop-ready-first. Дефолт: соло-тела за сетевыми интерфейсами (§2), кооп — Фаза 6. Макс подтвердил направление «держать сетевой-ready, но не реализовывать». ✅ закрыто.
- Ветка. Все шаги 1–5 плана 10 не запушены на
feat/encounter-data-loader. Дефолт: макро-петлю начинать новой веткой от неё (или отdevпосле мержа плана 10). → branch-hygiene-before-work.
8. Принципы захода
- Data-first:
RunState/ассеты (event/template/item/ai) заводятся до интерактива поверх. - Переиспользуем готовое:
EncounterLoader/ResetBattle,DeploymentController+loadout,BattleOutcome,ItemData,AIPresetData,Consequence,MenuRouter/MVVM-паттерн,GameConfig— не переписываем. - Швы под кооп закладываем, реализацию откладываем (netcode-host-authoritative, seams-vs-defer).
- Каждый шаг = сессия + коммит. Ресёрч перед B1 (генерация карты vs template), B3 (структура
Consequence), D2 (осмысленные варианты AI по архетипу). - Заглушки честные: предметы/баннеры = только статы; табы предметов/AI оживают частично; активки предметов и полный туториал — помечены «позже», не спрятаны.