Статус: Черновик плана (2026-07-17), согласован с Максом в чате (18 решений + опрос по экономике). Ждёт финального ревью Макса перед реализацией. Детализирует шаг B1 из Planning - Vertical Slice (карта акта) и достраивает всё, что на карте висит: петля забега, магазин, сундук, «?»-узел, элитка, экран исхода, главное меню, модель перезапуска. Опирается на флоу забега (код Assets/_Project/Scripts/Game/Flow/) (§8 карта, §5 автосейв), сейвы (CLAUDE.md §Сохранения), meta-progression (экономика), combat-system (§Перезапуск, реш. №65).

Граница вик (проставлено 2026-07-26 заходом по ГДД; тело плана не менялось)

Дизайн-факты, описанные ниже (структура акта, талия, состав узлов, модель перезапуска), имеют канон-дом в ГДД: Run - Events & Minigames §Место привалов и Combat - System §Перезапуск. Этот док — архив замысла реализации на момент 2026-07-17, поэтому его числа (Columns = 9, состав якорей) намеренно оставлены как след того, что планировалось тогда, и источником правды не являются: фактическая карта живёт в ActConfig (15 колонок), дизайн — в ГДД.

Обновление статуса (2026-07-19): глобальная панель забега РЕАЛИЗОВАНА как RunModeBarView (упоминаемый ниже по тексту RunTopBarView снесён при UI-реворке как легаси). Актуальное устройство UI-слоя — Journal - The World Replicates, The View Does Not. Имена в прозе плана ниже оставлены как след замысла на момент фазы.


Превращает набор разрозненных экранов (бой, награда, текст-ивент, гильдия — уже в едином скине) в играбельный забег от старта до босса: процедурная карта акта (STS-подобная, слева→направо в духе Across the Obelisk), петля обхода узлов, недостающие типы узлов (магазин, сундук, «?»), экономика покупок, модель перезапуска-на-акт, экраны исхода и главное меню. Соло; швы под кооп держим, тела — Фаза 6.


1. Отправная точка (факт на 2026-07-17)

Есть (готовые швы под петлю):

  • IEventFlow.Run(RunContext) → EventResult — полиморфный узел (RunFlow.cs). Реализации: BattleFlow, TextEventFlow.
  • GameFlow (singleton, VContainer) — оркестратор, умеет прогнать один узел за вызов (RunSingleBattleAsync, RunTextEventAsync, PresentRewardAsync). Петли обхода карты нет.
  • RewardService + RewardScreenView — витрина 1-из-3 реликвий, наклон по RewardTier {Battle/Elite/Boss} (uniqueChance 10/20/100%).
  • RunStateService — владелец RunState, вместимость реликвий (RelicCapacityBase=8..Max=16), TryAddRelic/RemoveRelic, AddGold (кламп в ноль), Autosave, HasSave (готово под кнопку «Продолжить»).
  • Экраны — UXML-оверлеи через MenuRouter (стек VisualElement) + MessagePipe (OpenRewardRequest/OpenTextEventRequest); паттерн отлажен на награде и ивенте.
  • Золото: RunState.Gold, показ в RunTopBarView. ItemData уже имеет поля Cost/ShopWeight.

Есть в данных, но мёртвое (никто не читает/не пишет):

  • DTO карты: MapNodeType {Start, Battle, Elite, TextEvent, Shop, Boss}, MapNode {Id, Type, PayloadId, Edges[], Cleared, UiPosition}, MapState {CurrentNodeId, Nodes[]}, RunState.Map, RunState.CurrentActIndex (RunState.cs). Ни генератора, ни траверса, ни экрана.

Нет вообще:

  • Генератор карты, петля обхода узлов (главное), экран карты.
  • ShopFlow + экран магазина (в коде ни строчки), ChestFlow + экран сундука.
  • «?»-узел (случайное событие), экран исхода забега, главное меню.
  • Модель перезапуска-на-акт: в коде до сих пор старый счётчик BattleFlow.DefaultMaxRetries=2 на каждый бой — техдолг, противоречит реш. №65 (см. §5.6).

Вывод: швы под узлы и экраны готовы, половина контракта карты уже в DTO. Этот план строит петлю поверх готовых швов и добирает 3 недостающих типа узла, экономику и обёртку (меню + исход). Риск низкий — greenfield-слой, не переписывание.


2. Ведущая архитектура (одна, не меню вариантов)

RunState.Map = источник истины позиции; MapGenerator строит граф из сида на старте акта; ActRunner (петля акта) читает карту, показывает MapScreen, ждёт выбор игрока, резолвит узел через IEventFlow и продвигает позицию; типы узлов — полиморфные IEventFlow; экраны — UXML-оверлеи через MenuRouter+MessagePipe.

Почему так:

  • Петля вынесена в отдельный ActRunner, а не размазана по GameFlow. GameFlow остаётся верхним оркестратором (Boot → MainMenu → RunSetup → Act loop → Outcome), а ActRunner.RunActAsync(RunContext) крутит именно обход одного акта. SRP + петлю можно юнит-тестить без сцен/UI. GameFlow делегирует.
  • Резолв узла — таблица MapNodeType → IEventFlow через NodeResolver (фабрика). Новый тип узла = новый IEventFlow + строка в резолвере, центральный switch не разрастается. Ровно то, ради чего заведён IEventFlow.
  • Карта — read-only проекция RunState.Map. MapScreen не хранит состояние, только рисует граф и репортит выбранный узел в ActRunner. Позиция/Cleared живут в RunState и автосохраняются — перезаход в забег восстанавливает карту бесплатно.
  • Экономика — расширение RunStateService, единая точка (как вместимость реликвий уже там). Никаких денежных операций мимо неё.

Сетевой-ready как сквозной принцип (тела — Фаза 6): ActRunner концептуально крутится на хосте; выбор узла идёт через IPlayerIntentSource (соло = локальный клик), барьеры «все готовы» — через IReadyGate (соло = мгновенно). Швы уже есть в RunFlowSeams.cs. Никаких NGO/RPC в этом заходе.


3. Модель данных (что заводим)

3.1. Генерация карты — MapGenerator

Строит MapState из RunState.Seed (через IRngService) на старте акта. Параметры (в GameConfig или ActConfig-SO):

  • Columns = 9 (глубина акта; прицел на ~15 в будущем — число параметризовано, не зашито). Колонка 0 = Start, последняя = Boss.
  • Ширина колонки (сколько параллельных узлов) — диапазон, напр. 1–3, чтобы получались развилки и слияния (топология Across the Obelisk: слева→направо, ветвление + схождение).
  • Edges между соседними колонками по правилам (без пересечений «крест-накрест», гарантия связности старт→босс).
  • UiPosition = (колонка, ряд) для раскладки экрана.
  • Веса типов узлов на промежуточных колонках (NodeTypeWeights): доля Battle / Elite / TextEvent / Shop / Chest / «?». Гарантии: ≥1 магазин перед боссом, элитки не в первой колонке.

Вся карта видна заранее (без тумана войны — идея тумана как модификатора забега вынесена в GDD, см. §7).

3.2. Резолв и обход — ActRunner + NodeResolver

  • ActRunner.RunActAsync(ctx): пока текущий узел не Boss-Cleared
    1. показать MapScreen, получить выбранный доступный узел (сосед по Edges из CurrentNodeId, не Cleared);
    2. NodeResolver.Resolve(node) → IEventFlow;
    3. await flow.Run(ctx); по EventResult:
      • Completednode.Cleared = true, Map.CurrentNodeId = node.Id, Autosave;
      • PlayerDefeated → ветка перезапуска/исхода (§5.6);
    4. если node.Type == Boss и Completed → выход из акта (победа, §5.7).
  • NodeResolver: Battle/Elite/Boss → BattleFlow (с нужным пресетом + RewardTier); TextEvent → TextEventFlow; Shop → ShopFlow; Chest → ChestFlow; «?» → RandomEventFlow (§5.4).

3.3. Экономика (расширение RunStateService)

Решения опроса (2026-07-17):

  • Источники золота: старт забега 100; +20 за победу в бою (обычный/элита/босс — можно скейлить позже); плюс капель с текст-событий (Consequence.Gold, уже есть) и сундуков.
  • Цена покупки реликвии: база по редкости (DropRarity/KitPower) + случайный разброс на сиде. Таблица-дефолт (в GameConfig/EconomyConfig-SO): Common ~50, Cursed ~100, Divine 150, разброс ±20%. У RelicData поля Cost нет — цена вычисляется маппером RelicPricer, ассеты не размечаем.
  • Продажа реликвии: 25% от её цены покупки («не выкинуть, а хоть что-то заработать»).
  • Реролл витрины магазина: фикс 50 за перекат всех 4 слотов.
  • Вместимость реликвий: кап 12 (RunState.RelicCapacity; правим дефолт GameConfig.RelicCapacityBase под 12).

3.4. Модель перезапуска (переезд под реш. №65)

  • RunState.RestartsRemaining : intпул на акт, сбрасывается до GameConfig.RestartsPerAct (=2) в начале каждого акта, не копится.
  • Поражение в бою при RestartsRemaining > 0 → трата попытки, переиграть тот же бой (враги/расстановка не меняются). При == 0 → конец забега (экран исхода).
  • Убирает техдолг BattleFlow.DefaultMaxRetries (счётчик на каждый бой) — переносим в RunState/GameConfig.

3.5. Ассеты-контент (SO)

  • Elite-энкаунтеры (BattlePresetData/EncounterData): отдельные ассеты, пока просто больше врагов (без спец-статов — по запросу Макса, не переусложняем).
  • TextEventData — тянутся из существующего пула Events (переиспользуем, «?»-текст и узлы TextEvent берут отсюда).
  • EconomyConfig (или поля в GameConfig): стартовое золото, награда за бой, таблица цен по редкости, %-продажи, цена реролла, кап реликвий, RestartsPerAct.

4. Порядок работ (шаги, каждый — сессия + коммит)

Data-first. Каждый шаг оставляет играбельный/тестируемый результат.

Часть A — Петля и карта [навигация]

A1 — MapGenerator + траверс MapState [фундамент, без UI]

  • Генератор графа из сида (§3.1): 9 колонок, ветвление, связность, UiPosition, веса типов. Хелперы траверса: доступные соседи из CurrentNodeId/Edges/Cleared, продвижение позиции.
  • EditMode-тесты: связность старт→босс, детерминизм по сиду, корректность доступных узлов.
  • Разблокирует: карта существует как данные, обход тестируем headless.

A2 — ActRunner + NodeResolver [оркестратор петли]

  • ActRunner.RunActAsync (§3.2) поверх GameFlow (делегирование). NodeResolver маппит тип→IEventFlow; на этом шаге Shop/Chest/«?» — заглушки, возвращающие Completed. Автосейв на переходах (флоу забега (код Assets/_Project/Scripts/Game/Flow/) §5).
  • Dev-вход «начать акт» (без меню). Забег проходится через существующие бой/ивент-узлы.
  • Разблокирует: последовательность узлов = собственно забег.

A3 — MapScreen (оверлей выбора узла) [навигация видимая]

  • UXML-оверлей через MenuRouter+MessagePipe: рисует граф слева→направо по UiPosition, иконки по типу, «?»-узлы единой иконкой, подсветка доступных, клик → выбор узла в ActRunner.
  • Разблокирует: карту можно играть руками.

A4 — Единая кнопка «Продолжить» + унификация исходов [UX-консистентность]

  • Единый компонент «Продолжить» в правом нижнем углу, всегда одно место; появляется, когда узел разрешён; возвращает на MapScreen к выбору следующего узла. Прогоняем через все исходы (награда/ивент/сундук/бой).
  • Разблокирует: ровный ритм «узел → продолжить → карта».

Часть B — Узлы и экономика [контент-петли]

B1 — Экономика-фундамент [деньги работают]

  • EconomyConfig/GameConfig: стартовое золото 100, +20 за бой, таблица цен, %-продажи 25, реролл 50, кап 12, RestartsPerAct=2. RelicPricer (цена по редкости + разброс на сиде). Расширить RunStateService: списание/начисление, продажа. Начисление +20 после победы в BattleFlow/ActRunner.
  • Разблокирует: магазин и сундук могут оперировать золотом.

B2 — ShopFlow + ShopScreen [магазин]

  • ShopFlow : IEventFlow; ShopScreen (UXML-оверлей): 4 реликвии в ряд (пул тот же, что награда), покупка списывает золото и убирает слот; реролл 50 перекатывает все 4; панель продажи — инвентарь игрока, продажа за 25%; кнопка «Продолжить»/«Уйти».
  • Нет места (кап 12) при покупке: тост «нет места» — красные буквы по центру экрана, красиво тают, устойчивы к спаму кликов (очередь/пул тостов, не ломается при частых кликах). Освобождение — через продажу.
  • Разблокирует: узел «Магазин»; трата золота.

B3 — ChestFlow + ChestScreen [сундук]

  • ChestFlow : IEventFlow; ChestScreen: фасад сундука, ЛКМ по крышке (лёгкая реакция на клик, без сложной анимации) → существующий RewardScreenView (1-из-3 реликвий) → «Продолжить».
  • Разблокирует: узел «Сундук» (и как исход «?»).

B4 — «?»-узел (RandomEventFlow) [случайное событие]

  • Узел на карте показан как «?»; тип роллится на входе (IRngService): текст 60% / бой 15% / сундук 12% / магазин 8% / элитка 5% — затем делегирует соответствующему IEventFlow.
  • Разблокирует: вариативность узлов.

B5 — Элитка (бой + награда ×2) [сложный бой]

  • Отдельные elite-BattlePresetData (больше врагов). NodeResolver для Elite даёт BattleFlow с elite-пресетом + RewardTier.Elite.
  • Награда ×2: после элитки — два выбора реликвии подряд (PresentRewardAsync зовётся дважды), канон meta-progression. Дёшево — переиспользуем существующую витрину, крутим цикл 2 раза.
  • Разблокирует: кривую сложности внутри акта + осмысленный луз за элитку.

Часть C — Перезапуск и исход [замыкание забега]

C1 — Пул перезапусков на акт (реш. №65) + ХП в топ-баре [страховка]

  • RunState.RestartsRemaining + GameConfig.RestartsPerAct=2, сброс в начале акта (в MVP 1 акт = забег, но модель именно «на акт»). ActRunner: поражение при пуле>0 → трата + переигровка того же боя; при пуле=0 → исход. Убрать BattleFlow.DefaultMaxRetries.
  • Топ-бар: ХП забега видны ВСЕГДА, в любом состоянии, у всех игроков (STS-style). Рендер пипсами через RunTopBarView (расширяем существующий), иконки — Honeti PixelArtGUI (Assets/Honeti/PixelArtGUI/Textures/Icons/; кандидат — сердце/череп). Пипсы гаснут по мере траты перезапусков.
  • Разблокирует: осмысленный проигрыш; стратегию «приберечь на босса»; постоянную читаемость «сколько жизней осталось».

C2 — OutcomeScreen (победа/поражение) [итог]

  • UXML-оверлей: Победа (босс Cleared) / Поражение (пустой пул + проигрыш) → «В меню». Минимальный, без пролога.
  • Разблокирует: забег имеет конец.

Часть D — Вход [обёртка]

D1 — MainMenu [игра как игра]

  • UXML/MVVM: Начать забег · Продолжить (RunStateService.HasSave, готово) · Настройки (готово, переиспользуем) · Выход. GameFlow.Boot → меню, а не сразу в бой.
  • Разблокирует: запуск как игры, а не dev-сцены.

5. Уточнения по запросам Макса

5.1. Топология карты

Процедурная генерация из сида (A1), 9 узлов (прицел на ~15), ветвление слева→направо (Across the Obelisk), вся карта видна заранее. Босс — последняя колонка. 1 акт сейчас, структура Act заложена расширяемой (прицел на 3 акта).

5.2. Магазин

4 реликвии в ряд, покупка убирает слот, реролл 50 (все 4), продажа 25%, кап 12, тост «нет места» (красные буквы по центру, спам-устойчивый). Пул — тот же, что у награды (B1–B2).

5.3. Сундук

Отдельный тип узла и исход «?». Фасад + клик → существующий reward 1-из-3 (B3).

5.4. «?»-узел

До входа — «?»; на входе роллится тип (текст 60 / бой 15 / сундук 12 / магазин 8 / элитка 5), делегирует (B4). Совпадает с каноном превью: точный тип скрыт до входа (флоу забега (код Assets/_Project/Scripts/Game/Flow/) §8).

5.5. Кнопка «Продолжить»

Единый компонент, правый нижний угол, одно место на всех исходах, возврат на карту (A4).

5.6. Перезапуск — пул на акт (реш. №65) [подтверждено]

Перезапуск — пул 2 на АКТ, не на бой (combat-system §Перезапуск, journal-adr №65). В MVP один акт = забег, но модель именно «на акт» (сброс на старте каждого акта). Переносим правильно сразу (C1), убираем техдолг BattleFlow.DefaultMaxRetries. ХП забега видны в топ-баре всегда (§5.8).

5.7. Экран исхода + элитка-награда ×2 [подтверждено]

Минимальный OutcomeScreen победа/поражение (C2). Награда элитки — ×2 (два выбора реликвии подряд), канон meta-progression; переиспользуем витрину, цикл на 2 выбора (B5).

5.8. ХП забега в топ-баре (всегда виден)

Остаток перезапусков (ХП забега) отображается постоянно, в любом состоянии, у всех игроков — как в Slay the Spire. Пипсы через RunTopBarView, иконки Honeti PixelArtGUI. Реализация — C1.


6. Граф зависимостей

A1 (генератор + траверс)
 └── A2 (ActRunner + NodeResolver)
       └── A3 (MapScreen)
             └── A4 (Continue + унификация исходов)
                   ├── B1 (экономика) ── B2 (ShopFlow) 
                   │                     B3 (ChestFlow)
                   ├── B4 («?»-узел)   ← зависит от B2/B3/B5 (делегирует им)
                   └── B5 (элитка)
                         └── C1 (пул перезапусков) ── C2 (OutcomeScreen)
                               └── D1 (MainMenu)

Минимальный ощутимый разрез = A1→A2→A3: карта проходится через существующие бой/ивент-узлы, уже чувствуется забег. Магазин/сундук/«?»/элитка — расширение. Перезапуск+исход+меню — замыкание.


7. Развилки (на Макса)

  1. Перезапуск — пул 2 на акт (реш. №65). ✅ Закрыто (2026-07-17): переносим правильно (C1, §5.6), ХП забега всегда в топ-баре (§5.8).
  2. Награда элитки ×2. ✅ Закрыто (2026-07-17): делаем — два выбора подряд (B5, §5.7).
  3. ActRunner отдельным сервисом vs метод в GameFlow. Дефолт: отдельный сервис (SRP + тестируемость петли). Мелочь.
  4. Место/формат файла плана. Этот файл — tech/40-planning/act-map-run-loop.md. Скилл gdd-scribe (для gdd/) в текущей сессии не виден (запущена из My-Nixi-Agent), план оформлен вручную по эталону vertical-slice.md. Ок?
  5. Ветка. Дефолт: новая ветка от актуальной main/dev под этот заход.

Идея в GDD (по твоей просьбе): «туман войны» как модификатор забега (карта не видна целиком) — записать в open когда будем в контексте gdd-scribe.


8. Принципы захода

  • Data-first: генератор/MapState/EconomyConfig — до интерактива поверх.
  • Переиспользуем готовое: RewardService/RewardScreenView (сундук, магазин), MenuRouter+MessagePipe (все экраны), RunStateService (экономика, вместимость), MapNode/MapState DTO (карта), TextEventFlow/BattleFlow (узлы), HasSave (Продолжить), настройки-экран (меню). Не переписываем.
  • Швы под кооп закладываем, реализацию откладываем (IReadyGate/IPlayerIntentSource, RunState по ID, автосейв=снапшот). Никаких NGO/RPC.
  • Каждый шаг = сессия + коммит. Порядок фаз — граф §6.
  • Заглушки честные: элитка = только «больше врагов»; сундук — лёгкая реакция, без сложной анимации; экран исхода — минимальный. Помечены «позже», не спрятаны.