Статус: Черновик плана (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→- показать
MapScreen, получить выбранный доступный узел (сосед поEdgesизCurrentNodeId, неCleared); NodeResolver.Resolve(node) → IEventFlow;await flow.Run(ctx); поEventResult:Completed→node.Cleared = true,Map.CurrentNodeId = node.Id,Autosave;PlayerDefeated→ ветка перезапуска/исхода (§5.6);
- если
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, Divine150, разброс ±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. Развилки (на Макса)
- Перезапуск — пул 2 на акт (реш. №65). ✅ Закрыто (2026-07-17): переносим правильно (C1, §5.6), ХП забега всегда в топ-баре (§5.8).
- Награда элитки ×2. ✅ Закрыто (2026-07-17): делаем — два выбора подряд (B5, §5.7).
ActRunnerотдельным сервисом vs метод вGameFlow. Дефолт: отдельный сервис (SRP + тестируемость петли). Мелочь.- Место/формат файла плана. Этот файл —
tech/40-planning/act-map-run-loop.md. Скиллgdd-scribe(дляgdd/) в текущей сессии не виден (запущена изMy-Nixi-Agent), план оформлен вручную по эталонуvertical-slice.md. Ок? - Ветка. Дефолт: новая ветка от актуальной
main/devпод этот заход.
Идея в GDD (по твоей просьбе): «туман войны» как модификатор забега (карта не видна целиком) — записать в open когда будем в контексте gdd-scribe.
8. Принципы захода
- Data-first: генератор/
MapState/EconomyConfig— до интерактива поверх. - Переиспользуем готовое:
RewardService/RewardScreenView(сундук, магазин),MenuRouter+MessagePipe (все экраны),RunStateService(экономика, вместимость),MapNode/MapStateDTO (карта),TextEventFlow/BattleFlow(узлы),HasSave(Продолжить), настройки-экран (меню). Не переписываем. - Швы под кооп закладываем, реализацию откладываем (
IReadyGate/IPlayerIntentSource,RunStateпо ID, автосейв=снапшот). Никаких NGO/RPC. - Каждый шаг = сессия + коммит. Порядок фаз — граф §6.
- Заглушки честные: элитка = только «больше врагов»; сундук — лёгкая реакция, без сложной анимации; экран исхода — минимальный. Помечены «позже», не спрятаны.