Статус: План составлен (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–4 EventChoice { лок-текст, условие?, список 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-лимиты [фундамент]

  • RunState POCO (§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. Развилки (на Макса)

  1. Лимит реликов — что именно? Дефолт: вместимость коллекции запаса (§5.4). Альтернативы: лимит копий одного релика / размер боевого пула. Механика (кап+enforce+апгрейд) одна, меняется объект счёта. → уточнить перед A1.
  2. Пролог — где живёт флаг «пройден»? Дефолт: в профиле сохранения (мета вне забега), не в RunState. → мелочь, дефолт разумен.
  3. Соло-first vs coop-ready-first. Дефолт: соло-тела за сетевыми интерфейсами (§2), кооп — Фаза 6. Макс подтвердил направление «держать сетевой-ready, но не реализовывать». ✅ закрыто.
  4. Ветка. Все шаги 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 оживают частично; активки предметов и полный туториал — помечены «позже», не спрятаны.