Статус: принято Максом 2026-08-02. Иерархия уровней жизни задана им, разбор и границы — совместные. Решение и цитаты — журнал «The Levels Of Life Are Named».
Зачем этот документ. Боевой скоуп оброс миром, флоу и показом, и это перестало быть вопросом вкуса: у него нет границы боя, поэтому границу объявляют вручную, и каждый новый участник обязан на неё подписаться. Кто не подписался — ломается молча. Здесь записана целевая иерархия, список легаси на вырывание и порядок фаз, чтобы заход не пришлось изобретать заново.
1. Чем уровень отличается от состояния
Скоуп заводится, когда есть все четыре признака. Три — хватит объекта состояния, и скоуп будет церемонией.
- Граница — наблюдаемые в коде начало и конец.
- Состояние, обязанное умереть на границе — иначе течёт в следующий раз.
- Свои участники — сервисы, бессмысленные вне уровня.
- Своя композиция — набор участников зависит от того, каким уровень родился.
Четвёртый признак — тот, из-за которого заход и случился: роль в коопе меняет состав, а не поведение, и ветвление по ней в рантайме было ошибкой.
2. Целевая иерархия
Приложение (Root) процесс
└─ Мир persist-сцены: арена, камера, показ, время
└─ Сессия ЧЬЁ состояние играем: Owner | Guest
└─ Занятие Двор | Забег | Ристалище | PvP-матч | Дев-арена
└─ Узел только внутри Забега: бой | магазин | событие | привал
└─ Бой заказчиков четыре: узел, Ристалище, PvP, тест
| Уровень | Граница | Что умирает на ней | Композиция зависит от |
|---|---|---|---|
| Мир | процесс | ничего | — |
| Сессия | вход в хаб → выход в главное меню | RunState, сеть, право писать сейв | Owner / Guest |
| Занятие | вход в забег/ристалище/матч → выход | карта акта, счёт матча, скин арены | вида занятия |
| Узел | вход в клетку → результат | предложенное, купленное, счётчик ретраев | типа узла |
| Бой | launch → outcome | лента, RNG боя, реестр юнитов, расстановка | роли сессии |
Бой НЕ дочерний Забегу. Заказчиков четыре, и один из них — тест: боевой скоуп обязан
собираться без RunState, без сети и без карты. Это и есть проверка, что рез прошёл по правильному
месту.
Двор, Ристалище и боевая арена — три скина одной персист-арены и взаимоисключающи по построению (ГД: Двор гильдии). Поэтому Двор — не «место между занятиями», а одно из Занятий, дефолтное.
3. Что решает каждый уровень
Мир
Арена (один экземпляр ArenaLayoutData), камера-риг, TimeScaleService, презентеры мира, ввод,
держатель тел. Живёт весь процесс — как сейчас WorldLifetimeScope, только теперь без дублей
снизу.
Держатель тел — решение Макса 02.08.2026 (журнал
«The Simulation Belongs To The Battle»).
CombatSimulation рождается и умирает вместе с боем; вне боя тела на арене (двор, Ристалище,
ростер между забегами) держит отдельный мировой слой, а не замороженная симуляция.
Подъёмным это делает шов источника кадра: CombatPresenter рисует только из кадра — состав
видов, снаряды, интерполяцию, события, — а живая симуляция нужна ему лишь для dev-оверлеев. Значит
поставщиков может быть два, и показ не заметит разницы:
| Когда | Кто поставляет кадр |
|---|---|
| бой | BattleTapePlayback — лента, с лагом показа |
| вне боя | статичный кадр мира: тела стоят, позы не меняются |
Отвергнуто при выборе: мировая симуляция с RNG боя за швом (дешевле на два дня, но граница боя остаётся объявляемой вручную) и мировая симуляция с пересевом при рождении скоупа (полдня, но «пересев вырван с корнем» тогда не выполняется честно).
Сессия — самый недооценённый уровень
Здесь решается чьё состояние мы играем, и отсюда растут две композиции:
| Owner | Guest | |
|---|---|---|
RunState | грузит из своего сейва, владеет | получает по сети |
| Автосейв | пишет | не существует вовсе |
| Гильдия и мета | своя | чужая, только на просмотр |
| Роль в бою | считает и раздаёт | смотрит ленту |
У гостя нет своей гильдии — он играет в чужом сейве. Значит RunStateService, который сегодня
один на всех и лежит в Root, у гостя не должен существовать: он пишет на диск. Сегодня не выстрелило
только потому, что гость никуда ещё не заходил.
Роль сети живёт здесь и только здесь. В бою она не спрашивается — она выбирает инсталлер.
Занятие
Двор, Забег, Ристалище, PvP-матч, Дев-арена. Общее: конечное мероприятие со своим состоянием и выходом в хаб. Взаимоисключающи — включён ровно один.
Узел
Уже существует, но мимо DI: BattleFlow создаётся через new внутри GameFlow (в
RootLifetimeScope это записано прямым текстом). Уровень есть, скоупа нет, зависимости прокинуты
руками — тот же симптом, что у боя, в другой форме.
Бой
Лента, RNG боя, BattleTapePlayback, реестр юнитов, телеграфы, диагностика, расстановка, драйвер
тика симуляции, кооп-половина по роли сессии.
4. Где PvP
Отдельный вид Занятия, не вариация Забега. У забега карта, мементо, экономика узлов, прогрессия Сосуда; PvP-матчу не нужно ничего из этого, зато нужно своё — счёт, раунды, приватность расстановки, общий гейт «оба готовы». Наследовать забег значило бы втащить пол-игры, чтобы половину выключить.
Следствие приятное: первая версия («расставились друг против друга, оба жмут готов») — это Занятие с одним Боем внутри, поверх готового Ристалища. Из хаба открывается соседней дверью с Ристалищем.
В ГДД механики PvP нет — в дизайн-доке он сегодня упомянут ровно один раз и вскользь. Когда
дойдём до содержания матча (счёт, составы, что ставится на кон), это заводится через
gdd-scribe/content-design, а не здесь: тут только его место в иерархии.
5. Рассмотрено и отвергнуто
| Кандидат в уровни | Вердикт |
|---|---|
| Профиль | данные, не скоуп: своих сервисов нет, переключение идёт через меню с перезагрузкой. Станет скоупом, если появится смена профиля на лету |
| Гильдия | у Owner совпадает с Сессией один в один. Нужен, только если дом можно сменить не выходя в меню — сейчас нельзя |
| Акт | внутри Забега граница есть (карта, пул перезапусков), своих участников почти нет. Именован, не введён |
| Соединение | «хост ушёл = конец сессии», реконнекта нет → соединение совпадает с Сессией |
| Игрок / раздача Сосудов | состояние, не уровень. Хост меняет распределение Сосудов в любой момент забега (ГД: Coop Overview) — скоуп на игрока сломался бы на первой перетасовке |
| Роль в бою | не уровень: приходит из Сессии и лишь выбирает инсталлер |
6. Легаси на вырывание
HARD: чинится корнем, а не заплаткой. Всё перечисленное существует потому, что уровня не было, и исчезает вместе с появлением уровня. Ни один пункт не переезжает «пока так».
| Что | Где | Почему исчезнет само |
|---|---|---|
Второй экземпляр ArenaLayoutData | CombatLifetimeScope.BuildArenaLayout | арена принадлежит Миру; комментарий в WorldLifetimeScope уже утверждает это, а код делал наоборот |
| Ручной пересев RNG | BattleBootstrap.ReseedForBattle | новый боевой скоуп — новый генератор с суб-сидом узла |
_time.SetPaused(false) в трёх местах | BattleBootstrap | TimeScaleService.Dispose оживает вместе с границей боя |
ResetBattle как «граница боя» | CombatSimulation + подписчики | граница становится созданием и уничтожением скоупа |
Reset() у ленты, приёмника, стримера | BattleTape, TapeIntake, TapeStreamer | то же: нечего сбрасывать у объекта, который родился с боем |
| Флаг роли и четыре ветвления по ней | IBattleAuthority и его читатели | роль выбирает инсталлер, а не проверяется в Tick |
Гейт Enabled у раздачи | BattleTapeBroadcast | остаётся только честный вопрос «есть ли слушатели», и он про пиров, а не про роль |
BattleFlow через new мимо DI | GameFlow | узел получает свой скоуп |
WorldStageController, BattleBootstrap в боевом скоупе | CombatLifetimeScope | оба принадлежат Миру и Узлу соответственно |
| Очередь команд симуляции | EnqueueCommand, ICombatCommand, PauseCommand, ResumeCommand | пользователей ноль (техдолг §3.21); сносится в фазе 1, вместе с оговоркой про счётчик тиков на паузе |
| Сброс приёмника у гостя | техдолг §3.22 | заводился как заплатка к отсутствию границы; с границей не нужен |
7. Фазы
Точки коммита — по фазам; красное дерево между фазами не оставляем.
| Фаза | Что | Критерий приёмки |
|---|---|---|
| 1а. Источник кадра | шов «кто поставляет кадр показу»; мировой держатель тел; показ вне боя перестаёт зависеть от ленты | арена вне боя показывает отряд, когда симуляции нет вовсе |
| 1б. Боевой скоуп | симуляция, системы, RNG боя, лента, расстановка рождаются с боем и умирают с ним | боевой скоуп поднимается в тесте без RunState, сети и карты |
| 1в. Вырывание легаси | §6 целиком: пересев, ручные сбросы, флаг роли, очередь команд | ни одного ручного «объявления границы боя» не осталось; полный EditMode зелёный |
| 2. Сессия | Owner / Guest; сюда переезжают RunStateService и сеть | у гостя нет сервиса, пишущего сейв — проверяется тестом; роль коопа больше нигде не спрашивается в рантайме |
| 3. Занятие | Двор, Забег, Ристалище, PvP-матч, Дев-арена | вход в Ристалище и в дев-арену не проходит через петлю забега |
| 4. Узел | узел получает скоуп, BattleFlow уходит из new | зависимости узла разрешаются контейнером |
Мультиплеер продолжается после фазы 2: до неё роль в бою жить негде, а после — она уже готова.
8. Риски
- Заход трогает сердце игры, а не край: боевой скоуп сегодня держит и мир, и флоу, и показ. Отсюда правило про точки коммита и про полный EditMode на каждой фазе, а не в конце.
- Соблазн развести уровни глубже, чем нужно. Профиль, Гильдия и Акт намеренно не вводятся — §5 существует, чтобы через месяц никто не «дозавёл» их из общих соображений.
- Пересоздание боевого скоупа во время боя (хост вышел) сносит показ посреди кадра. Поведение придётся определить явно: доигрывает накопленную ленту или замирает с сообщением. Сегодня это тоже не определено, но молча.