Update design documentation to clarify the structure and relationships of game elements. Enhance the definitions of TerritoryDef, BuildingDef, and FloorDef, and introduce new concepts like Patch for modifying definitions. Revise the near-term design to outline the UI structure and interactions, ensuring a clear framework for future development phases.
ci / server (push) Failing after 3m37s
ci / client (push) Successful in 16s

This commit is contained in:
Leonid Pershin
2026-08-18 13:41:35 +03:00
parent d96b420133
commit bc9a33e67f
9 changed files with 337 additions and 105 deletions
+113 -49
View File
@@ -1,8 +1,8 @@
# Defs, карта и моды
Договорённость на ближайшее планирование, не текущий код.
Экран игрока: [`near-term.md`](near-term.md). Потоки и ECS: [`runtime.md`](runtime.md).
Как устроен хост сейчас: [`../architecture.md`](../architecture.md).
Экран игрока: [`near-term.md`](near-term.md). Потоки: [`runtime.md`](runtime.md).
Проекты: [`projects.md`](projects.md). Как устроен хост сейчас: [`../architecture.md`](../architecture.md).
Идея как у RimWorld: **ядро — код систем**, содержание игры — **данные (defs)**. Новый стул, кабинет
или действие, которое уже умеет код, появляются без пересборки `HSchool.Simulation`. Сборка нужна,
@@ -37,8 +37,13 @@
3. **PositionDef** — должность (директор). Вакансия — не def, а состояние школы.
4. **WorkDef** — работа, которую можно вести в помещении (работа директора, урок, обход школы).
5. **RoomDef** — тип помещения. Слоты предметов, должности, которые он открывает, список WorkDef.
6. **BuildingDef** / группировка этажа — если зданию нужны свои свойства; иначе этаж может быть
только узлом карты без отдельного def.
6. **BuildingDef** — тип корпуса на территории (учебный, спортзал). Тонкий: редактор ставит *тип*
здания, моды добавляют новые корпуса.
7. **FloorDef** — тип этажа (типовой, подвал, чердак). Сейчас тонкий: `defName` и локаль, без правил.
Узел карты: id, `def`, родитель-здание, при необходимости номер/подпись («2»). Задел: позже на
def повесятся свойства этажа, не меняя форму ссылки на карте.
8. **TerritoryDef** — тип двора. Корень дерева и узел графа, такой же тонкий задел. В `core` хотя
бы один. Клик по двору — те же секции панели, что у комнаты.
Связи «из A можно попасть в B» **не** поле RoomDef, а рёбра карты. Иначе дефолтная и пользовательская
карта не смогут расставить одни и те же типы комнат по-разному.
@@ -50,14 +55,15 @@
Иерархия дерева (она же закрывает вопрос «из чего дерево»):
```
территория школы
└── здание (их может быть несколько)
└── этаж (если в здании больше одного; иначе этаж можно не показывать)
территория школы ← TerritoryDef, корень дерева и узел графа
└── здание
└── этаж ← FloorDef + id узла
└── помещение
```
Граф локаций — те же помещения плюс рёбра в обе стороны (кабинет ↔ коридор). Дерево — группировка
для UI, граф — куда можно перейти и откуда берутся «соседние» события.
Граф локаций — территория, помещения и любые другие ходибелые узлы плюс рёбра в обе стороны
(кабинет ↔ коридор, крыльцо ↔ двор). Дерево — группировка для UI; граф — куда можно перейти.
Два здания связываются через территорию (или явное ребро), не «висят» рядом в дереве без пути.
В процессе разработки кладём одну ванильную раскладку в Core. При создании школы игрок может
изменить её в редакторе или стереть и собрать свою. Редактор оперирует инстансом и **выбирает
@@ -73,14 +79,55 @@
два парсера в голове. Почему не свой формат: его придётся документировать сильнее, чем игру.
Один файл — один def или небольшая пачка одного вида (`things/chair.jsonc`). Ссылки — строки
`defName`, резолв после загрузки всего каталога (как у RimWorld: сначала прочитать, потом связать).
`defName`. Загрузчик Content идёт так:
Наследование по желанию, как `ParentName`: `{ "defName": "OfficeChair", "parent": "Chair" }`.
Не обязательно в первом срезе; без него можно жить, пока стульев мало.
1. Прочитать defs всех пакетов по порядку (`core`, затем моды). Повтор `defName` — последний
победил, warning.
2. Разрешить **наследование**.
3. Применить **патчи** пакетов в том же порядке.
4. Резолв ссылок (`actions`, слоты). Abstract def на карту ставить нельзя.
Патчи модов (добавить действие к ванильному стулу, не копируя файл) — отдельным видом файлов
позже. Сначала: новые defs + замена дефолтной карты целиком. Частичные патчи — когда появится
второй мод, которому это реально нужно.
Это база: новые поля у def и новые `op` у патча добавляются в Content, папки не меняются.
### Наследование
Как `ParentName` у RimWorld:
```jsonc
{ "defName": "FurnitureBase", "abstract": true }
{ "defName": "Chair", "parent": "FurnitureBase", "actions": ["Sit"] }
{ "defName": "OfficeChair", "parent": "Chair" }
```
- Нет поля у ребёнка — берётся у родителя (цепочка до корня).
- Поле есть — **заменяет** целиком, в том числе массивы. Дописать элемент в массив родителя —
не наследованием, а патчем.
- `abstract: true` — только база, на карте и в редакторе не выбирается.
- Родитель другого вида (Thing → Room) или цикл — ошибка загрузки каталога.
- Локаль: ключ `defName`, если нет — вверх по `parent`.
### Патчи
Чтобы не копировать ванильный стул ради одного действия. Лежат в `patches/`, не в `defs/`.
Цель — `defName` уже после наследования. Указатель поля — JSON Pointer.
```jsonc
{
"target": "Chair",
"ops": [
{ "op": "add", "path": "/actions/-", "value": "Inspect" },
{ "op": "replace", "path": "/slots/0/thing", "value": "DirectorsChair" },
{ "op": "remove", "path": "/works/1" },
],
}
```
В этом срезе три операции: `add` (в массив `-` = в конец), `replace`, `remove`. Неизвестный
`op`, битый pointer или нет `target` — каталог не грузится (школу с этим набором модов не
поднимать). Новые операции потом просто появляются в enum, файлы модов те же.
Патч **не** меняет раскладку карты: дефолтная карта по-прежнему файл `maps/` (last-wins на файл,
если мод положит свой `default.jsonc`). Патчи — про типы.
Подписи содержания живут **не в def**, а в `localizations/` мода. В def остаётся `defName`
(и при необходимости ключ, если он не совпадает с именем). Черновик без вшитых ru/en:
@@ -129,18 +176,30 @@ Def говорит «у стула есть Sit». Система `Sit` в C# з
Скриптовый язык внутри JSON в этом горизонте не заводим.
В первой живой версии ActionDef и ссылки с предметов — **заготовки**: они есть в каталоге и на
карте, системы их ещё не исполняют, игрок не отдаёт приказ «сесть». Имеет смысл уже показать
список в панели локации (чтобы каркас был правдой), но не симулировать.
карте, системы их ещё не исполняют, игрок не отдаёт приказ «сесть». В панели локации показываем
имена действий у стоящих предметов. Персонажи и «А говорит с Б» — секции панели, списки пустые.
Должности берём из RoomDef поставленного помещения (вакансии без людей).
## Снимок карты на клиент
При `OpenSchool` сервер один раз отдаёт дерево и состав всех узлов (двор, корпуса, этажи,
помещения). Подписи — язык из Hello (`ru` | `en`). Каталог для редактора — тот же код языка
в `?lang=` у HTTP. `Accept-Language` не используем: переключатель RU/EN в подвале с ним разъедется.
Клиент сам выбирает узел; секции панели одни и те же. Часы по-прежнему 20 Гц. Карта после
create в этом срезе не меняется, повторно слать не нужно. Hello с locale — смена раскладки
кадра, в том же коммите bump версии протокола.
## Карта: пустые комнаты и связность
Помещение может быть **пустым** (ни одного предмета в слотах). Не может быть **висячим**: каждое
помещение связано хотя бы с одним другим, и карта в целом — один связный неориентированный граф
(рёбра двусторонние).
Помещение может быть **пустым** (ни одного предмета в слотах). Карта в целом — один связный
неориентированный граф (рёбра двусторонние). Территория в нём всегда есть: это и корень дерева,
и двор.
Исключение: школа из **одной** комнаты — граф из одной вершины, рёбер нет, это допустимо.
Два здания без перехода между ними — нет: нужен общий узел (коридор, двор, территория как
локация) или явное ребро.
Изолированный узел запрещён. Практический минимум: двор + хотя бы одно помещение с ребром во двор
(или цепочка помещений, которая в итоге выходит во двор). Школа «одна комната без двора» в этой
модели не собирается — двор не выкидывается. Два здания без пути через двор (или другое ребро) —
тоже нет.
Сервер отвергает раскладку с изолированной комнатой, ребром в несуществующий id и `defName`,
которого нет в каталоге *выбранных для этой школы* модов.
@@ -156,8 +215,8 @@ create раскладка замораживается вместе с набо
## Папка мода и локализации
Моды — подпапки на диске сервера, например `mods/<id>/`. Ядро — тоже мод (всегда включён или
выбирается отдельно, см. открытые вопросы), лежит рядом, чтобы путь загрузки был один.
Моды — подпапки на диске сервера, например `mods/<id>/`. Ядро — мод `core`: тот же формат папки,
но **всегда включён**, в UI создания его нельзя снять. Остальные папки — чекбоксы.
```
mods/
@@ -170,11 +229,16 @@ mods/
default.jsonc # ванильная раскладка
furniture-pack/
defs/
patches/ # правки чужих def, не копии
localizations/
ru.jsonc
en.jsonc
```
Порядок загрузки: сначала `core`, потом выбранные моды в том порядке, в каком они стоят в
запросе create. Одинаковый `defName` или ключ локали — **побеждает последний**. Имеет смысл
писать предупреждение в лог, не молча, чтобы конфликт модов было видно.
Ключи локализации — по `defName` (и суффиксам вроде `.label`, если понадобятся несколько строк
на тип). Нет ключа — в UI показывается `defName`, это сразу видно в тесте мода.
@@ -182,19 +246,19 @@ mods/
сервер резолвит из `localizations/` включённых модов под язык клиента и кладёт в снимок / в
ответ каталога для редактора. Браузер файлы модов не читает.
При создании школы в UI показывают список папок из `mods/` (кроме того, что решим всегда
включать). Выбранный набор уходит вместе с раскладкой. Работник школы грузит **этот** набор
и замораживает каталог. Новые файлы на диске влияют только на следующие школы; список модов
для диалога можно сканировать при `GET`, без рестарта процесса.
В диалоге создания показывают `core` как включённый и заблокированный плюс список остальных
папок. Тело create несёт только *дополнительные* id (сервер всё равно подставит `core` первым).
Работник школы грузит этот набор и замораживает каталог. Новые файлы на диске влияют только на
следующие школы; список модов для диалога можно сканировать при `GET`, без рестарта процесса.
## Где это живёт в решении
- Каталог def и загрузка папок — `HSchool.Simulation` (без ASP.NET, без сокетов). Тесты читают
фикстуры и резолвят ссылки. Каталог **на школу**: зависит от модов, выбранных при create.
- Инстанс карты — часть `School`, рядом с часами и ECS. Свой `World` у каждой школы; свой поток —
см. [`runtime.md`](runtime.md).
- Валидация раскладки с клиента — на сервере: неизвестный `defName`, ребро в никуда, несвязный
граф, изолированная комната (если комнат больше одной).
- Каталог def, раскладка и валидация — **HSchool.Content**, см. [`projects.md`](projects.md).
`HSchool.Simulation` только пользуется уже собранным каталогом. Пути `mods/` и `saves/` — Server.
- Инстанс карты (граф) — тип из Content, лежит в `School` (Simulation) вместе с часами и ECS.
Поток и файл сейва — Server, см. [`runtime.md`](runtime.md) и [`projects.md`](projects.md).
Сейв: раскладка, id модов, часы, имя; каталог снова с диска при загрузке. Не писать каждый тик.
- Валидация раскладки — Content; Server только вызывает её на теле create.
- Клиент редактора — DOM в диалоге создания; тело create = имя, дата, список модов, раскладка.
Сканирование `mods/` — при запросе списка и при создании школы, не каждый тик. Горячая подмена
@@ -203,8 +267,7 @@ mods/
## Советы, которые стоит принять сразу
- Не класть граф проходов в RoomDef — только в карту.
- Не слать весь каталог и всю карту 20 раз в секунду: дерево и состав комнаты — при изменении
или при открытии школы; часы как сейчас.
- Не слать весь каталог и всю карту 20 раз в секунду: дерево — один снимок при открытии.
- Редактор только в create: шлёт раскладку и список id модов, не пути на диск и не C#.
- Слоты в RoomDef — чертёж; на карте слот можно не заполнять (пустая комната).
- Локали не смешивать с defs: иначе мод на третьем языке правит типы.
@@ -218,17 +281,18 @@ mods/
| Карта | Инстанс: территория → здания → этажи → помещения + граф связей |
| Дефолтная карта | Одна ванильная раскладка Core; при создании можно править или стереть |
| Редактор | Только в диалоге создания школы |
| Пустая комната | Можно; граф должен быть связным (одна комната — ок) |
| Пустая комната | Можно; граф связный, двор всегда в графе |
| Действия | Заготовки в def, без исполнения и без приказов |
| Моды | `mods/<id>/` на диске сервера, выбор в UI при создании; каталог замораживается |
| Моды | `mods/<id>/`; `core` всегда первый и не снимается; остальные — UI create |
| Коллизии имён | Последний пакет победил (defs и локали), warning в лог |
| Наследование | `parent`, `abstract`; поле ребёнка заменяет, не мержит массивы |
| Патчи | `patches/` после наследования; ops: add / replace / remove; неизвестный op — ошибка |
| Территория | TerritoryDef: корень дерева и ходибелый двор |
| Панель узла | Одни секции у двора, корпуса, этажа, комнаты |
| Язык | `?lang=` и Hello; не Accept-Language |
| Локализация модов | `mods/<id>/localizations/{ru,en}.jsonc`, не внутри def |
| Код модов (DLL) | Не в этом горизонте |
| Поток / ECS | Отдельный `World` обязательно; отдельный поток на школу — цель, см. [`runtime.md`](runtime.md) |
## Ещё не решено
1. `core` всегда включён и его нельзя снять в UI, или это такой же мод в списке?
2. Территория / двор — отдельная ходибельная локация (чтобы связывать здания), или только группировка в дереве?
3. Этаж без отдельного def — ок, пока у этажа нет своих свойств?
4. Порядок модов в списке при create влияет на коллизии `defName` (последний победил) — так и делаем?
5. Сохранение школы на диск — здесь или после первого живого среза? (набор модов и раскладку всё равно надо будет сериализовать вместе со школой, когда сохранения появятся)
| Этаж | FloorDef, тонкий; правила этажа — поля def позже |
| Здание | BuildingDef — тип корпуса |
| Сохранения | Файл школы: раскладка, id модов, часы, имя, пауза/скорость; каталог с диска при загрузке |
| Поток / ECS | Основа среза: один поток и один `World` на школу, см. [`runtime.md`](runtime.md) |
| Код модов (DLL) | Не в этом срезе |
+33 -15
View File
@@ -25,7 +25,26 @@ QSP: не «полотно текста + список действий», а п
даже если клиент рисует строки списка, а не человечков.
Типы комнат, предметов и действий описываются **defs** (JSONC), конкретная школа — **картой-инстансом**.
Подробности: [`defs.md`](defs.md). Дерево в UI — это территория → здания → этажи → помещения.
Подробности: [`defs.md`](defs.md). Дерево в UI — территория (ещё и ходибелая локация) → здания → этажи → помещения.
## Первый срез (закрыто)
Это и реализуем. Нового геймплея сверх этого не закладываем.
**Экран школы.** Часы, пауза, скорость. Три панели:
- дерево карты;
- общие события — **пусто** (заголовок и пустое состояние);
- выбранная локация (комната, этаж, корпус **или двор**): имя, предметы, персонажи (пусто),
действия на месте (пусто), должности. Секции те же; где данных нет — пустое состояние.
Клик по дереву — **фильтр на клиенте**. Сервер один раз отдаёт снимок. Язык подписей модов:
`?lang=` на HTTP-каталоге (диалог создания) и то же значение в **Hello** на сокете (снимок при
открытии). Не `Accept-Language`: его легко разъехать с переключателем RU/EN.
**Create.** Моды (`core` заблокирован) → каталог → редактор карты или стереть дефолт → имя, дата → школа на диске и в своём потоке.
**Не в этом срезе:** исполнение действий, живые персонажи, записи в ленте событий, смена модов у живой школы, Pixi, DLL-моды.
## Что уже зафиксировано
@@ -38,23 +57,22 @@ QSP: не «полотно текста + список действий», а п
| Часы, пауза, скорость | Остаются; это не отменяется панелями |
| Логика игры | Только сервер; клиент шлёт намерения и рисует то, что сказали |
| Данные игры | Defs (типы) + карта (инстанс), см. [`defs.md`](defs.md) |
| Дерево карты | Территория → здание → этаж → помещение; рёбра проходов — граф инстанса |
| Дерево карты | Территория (= двор, узел графа) → здание → этаж → помещение |
| Редактор карты | Только при создании школы |
| Моды | Папки на сервере, выбор в диалоге создания |
| Поток школы | Цель: один поток и один ECS world на школу, см. [`runtime.md`](runtime.md) |
## Ещё не решено
Эти вопросы закрывают игровой срез. Карту и моды больше не блокируют.
1. Что такое «общее событие»: лог за смену, только текущие процессы, или новости («урок начался»)?
2. Что видно на выбранной локации сначала: предметы (в т.ч. пусто), заготовки действий, вакансии?
3. Выбор узла в дереве — только фильтр на клиенте или намерение на сервер?
Открытые вопросы по core-моду, двору и коллизиям имён — в конце [`defs.md`](defs.md).
| Моды | `core` всегда; остальные — выбор при создании; поздний мод побеждает при споре имён |
| Поток школы | Основа среза: один поток и один ECS world на школу, см. [`runtime.md`](runtime.md) |
| Сохранения | На диск; часы не писать на каждом тике |
| События | Панель есть, лента пустая |
| Локация | Одни секции для двора, корпуса, этажа и комнаты |
| Двор | TerritoryDef, узел графа и корень дерева |
| Язык модов | HTTP `?lang=`, то же в Hello; не Accept-Language |
| Проекты | Каталог не в Server и не в Simulation, см. [`projects.md`](projects.md) |
| Выбор в дереве | Клиентский фильтр |
| Этаж | FloorDef (тонкий тип); узел карты ссылается на def |
## Заведомо не сейчас
- Портреты, арт локаций, схема этажа в пикселях.
- Управление персонажем от первого/третьего лица.
- Победа/кампания, пока нет даже наблюдаемого мира.
- Победа/кампания, живые персонажи, исполнение «сесть», записи событий.
- Смена модов у живой школы, код модов (DLL).
+53
View File
@@ -0,0 +1,53 @@
# Нарезка проектов (целевая)
Сейчас в коде: `Protocol ← Server → Simulation`. Этого мало, когда появятся defs, карта и моды.
Класть каталог в Simulation (рядом с Arch) или в Server (рядом с Kestrel) — оба варианта смешают
слои. Ниже — куда что идёт в этом срезе. `architecture.md` правится, когда код так и станет.
## Зависимости
```
Protocol — ни на кого из игровых проектов (только байты)
Content — ни на Protocol, ни на Simulation, ни на ASP.NET
Simulation → Content (каталог и раскладка, без файловых путей хоста)
Server → Protocol, Simulation, Content
Client — своя сторона Protocol (TS) + HTTP
```
Клиент по-прежнему не ссылается на C#-проекты.
## Кто чем владеет
| Проект | Да | Нет |
| --- | --- | --- |
| **HSchool.Content** (новый) | Типы def, JSONC-загрузчик, наследование, патчи, слияние локалей, граф карты, валидация связности | Arch, часы, HTTP, потоки, пути `mods/` с диска хоста |
| **HSchool.Simulation** | `School`, `GameClock`, Arch `World`, тик, применение раскладки к миру | Kestrel, сокеты, `Directory.Enumerate`, сейв-файлы |
| **HSchool.Server** | Хост, супервизор, работник-поток, `mods/` и `saves/` из конфига, HTTP/WS, DTO запросов | Правила «комната должна быть связана», схема Chair |
| **HSchool.Protocol** | Кадры сокета, в том числе снимок карты и locale в Hello | Имена комнат, JSONC |
| **HSchool.Client** | DOM, `t()` для хрома, рисует снимок | Файлы модов, симуляция |
Content принимает уже открытые потоки или список документов (сервер прочитал папки и отдал
байты/пути через узкий порт вроде «вот корневые каталоги этих id»). Тогда тесты Content кормят
фикстурами из `tests/`, без `appsettings`.
Сейв: **форма** (часы + id модов + раскладка) собирается из типов Simulation/Content; **запись
файла** — Server, и только с потока работника.
## Зачем не сваливать в Simulation
Arch и фиксированный тик — про живую школу. Парсер JSONC, `defName` и проверка графа не знают,
что такое tick. Их гоняют сотни раз в юните без мира. Если загрузчик сидит в Simulation, любой
тест каталога тащит ECS и наоборот.
## Зачем не сваливать в Server
Иначе связность карты и last-wins нельзя проверить без Aspire. Хост должен быть тонким:
найти папки, сериализовать HTTP, крутить потоки.
## Тесты
- `tests/HSchool.Content.Tests` — фикстуры JSONC, коллизии имён, дырявый граф, двор обязателен.
- `HSchool.Simulation.Tests` — часы, тик, «школа с таким каталогом живёт».
- `HSchool.AppHost.Tests` — create с картой, рестарт, сейв на диске.
Ориентир в `AGENTS.md` обновить в фазе, которая заводит проект (сейчас это фаза 3).
+14 -9
View File
@@ -5,28 +5,33 @@
модель. Менять инвариант в рабочих соглашениях имеет смысл только вместе с кодом.
Экран и defs: [`near-term.md`](near-term.md), [`defs.md`](defs.md).
Куда класть код: [`projects.md`](projects.md).
Работник (поток, очередь, сейв-файл) живёт в **Server**. То, что он тикает — `School` в
**Simulation**. Каталог, с которым школа создана — **Content**, замороженный у работника.
## Что уже почти есть
У каждой `School` уже свой Arch `World`. Это оставляем и делаем жёстким правилом: мир не
разделяется между школами и не отдаётся чужому потоку. Arch не потокобезопасен.
## Целевая модель
## Целевая модель — в этом срезе
Каждая школа — **актор**: свой поток (или `TaskCreationOptions.LongRunning`, что для нас то же
самое: выделенный поток, не пул тиков), свой `World`, свои часы, свой **замороженный** каталог
def (набор модов, выбранный при создании).
Каждая школа — **актор**: свой поток (`TaskCreationOptions.LongRunning` / выделенный поток, не
пул тиков), свой `World`, свои часы, свой **замороженный** каталог def, свой файл на диске.
Над ними — тонкий супервизор (сегодняшняя роль `GameLoopService` + `SchoolRegistry`):
Над ними — тонкий супервизор (вместо сегодняшнего общего цикла):
- создать / удалить школу, знать лимит;
- держать ящики: `id →` очередь команд этой школы;
- собирать **снимки** для `GET /api/schools` (работник публикует неизменяемое состояние, как
сейчас цикл публикует `SchoolsState`);
- маршрутизировать сокет: `OpenSchool` / `SetRunning` / кадры часов — только в ящик той школы.
- маршрутизировать сокет: `OpenSchool` / `SetRunning` / кадры часов — только в ящик той школы;
- при старте процесса поднять школы с диска, при остановке — дождаться записи работников.
Супервизор **не** вызывает `World`, не тикает часы, не читает defs инстанса. Работник **не**
трогает чужой мир и не ходит в ASP.NET.
трогает чужой мир и не ходит в ASP.NET. Файл школы пишет **работник** (create/delete через
команду супервизору, снимок часов — редкий, shutdown — обязательный).
Тик по-прежнему фиксированный (`SimulationOptions.FixedDeltaTime`), у каждого работника свой
таймер. Школы не синхронизируют календарь друг с другом — так и задумано.
@@ -56,6 +61,6 @@ def (набор модов, выбранный при создании).
## Пока не делаем
- Потоки на системы внутри одной школы (job system как у RimWorld) — рано.
- Потоки на системы внутри одной школы (job system как у RimWorld).
- Миграция живой школы на другой набор модов.
- Правка `AGENTS.md` до тех пор, пока цикл в коде ещё общий.
- Запись сейва на каждом тике.
+12 -23
View File
@@ -2,38 +2,27 @@
## Зависимости
- [Фаза 0](00-drop-pixi.md) — не обязательно технически, но логично закрыть первой
- [Фаза 0](00-drop-pixi.md) — логично закрыть первой
## Зачем
Сейчас внутри школы только часы и кнопки скорости. Дальше туда поедут списки, а не холст.
Нужен каркас панелей, чтобы следующий срез (локации, события) садился в готовые места, а не
ломал вёрстку часов.
Пока нет ответа, **что** лежит в дереве и в ленте, фаза рисует пустые панели с подписями и
сохраняет часы/паузу/скорость. Это не игра, это экран, в который игра въедет.
## Советы по укладке (не задачи, пока не закрыт дизайн)
- Выбор узла дерева на этом шаге можно держать **на клиенте**: сервер ещё не шлёт карту.
Когда появится симуляция локаций, решить отдельно, фильтр это или `OpenLocation`.
- Не слать дерево 20 раз в секунду. Часы уже едут по сокету; списки и события — когда меняются
(отдельные кадры или редкий снимок). Иначе меню-карточки повторятся внутри школы.
- Строки UI — через `t(...)`, как остальной клиент.
Сейчас внутри школы только часы. Нужны места под дерево, события и локацию — с теми секциями,
которые уже закрыты в срезе, даже если списки пустые.
## Задачи
- [ ] На экране школы три области: дерево карты, общие события, выбранная локация — плюс текущие часы и управление временем
- [ ] Пока нет данных с сервера, панели показывают пустое состояние (не ломаются)
- [ ] Переключение языка обновляет подписи панелей
- [ ] Три области: дерево карты, общие события, выбранная локация — плюс часы и скорость
- [ ] События: заголовок и пустое состояние, без выдуманных записей
- [ ] Локация: секции имя, предметы, персонажи, действия на месте, должности — пустые, не ломаются
- [ ] Клик по дереву переключает панель локации на клиенте (пока узлы-заглушки или пустое дерево)
- [ ] Переключение языка обновляет подписи панелей и пустых состояний (`t(...)`)
## Критерий готовности
- Можно открыть школу и увидеть каркас менеджера рядом с работающими часами
- Нет регрессии паузы и скоростей
- Клиентские тесты и `npm --prefix src/HSchool.Client run build` проходят
- Школу можно открыть, часы и пауза работают как раньше
- Видны пустые панели среза
- `npm --prefix src/HSchool.Client test` и `run build` проходят
## Стоп
Не наполнять панели выдуманными учениками и комнатами. Наполнение идёт из defs и карты
([`../design/defs.md`](../design/defs.md)), когда закроются открытые вопросы там.
Не наполнять персонажами и событиями. Данные — с фазы 4.
+32
View File
@@ -0,0 +1,32 @@
# Фаза 2. Работник школы и диск
## Зависимости
Нет по коду клиента. Ломает текущий общий `GameLoopService`.
## Зачем
Основа среза: каждая школа — свой поток, свой Arch `World`, свой файл. Пока ещё можно обойтись
часами без карты — сейв расширится в фазе 3, формат закладывать расширяемым.
## Задачи
- [ ] Супервизор держит ящики `id →` очередь; не трогает `World`
- [ ] У школы выделенный поток (`LongRunning`), свой таймер фиксированного шага, свой `World`
- [ ] Команды create/delete/open/close/running/speed идут в ящик, не в общий цикл
- [ ] Меню читает опубликованные снимки, как сейчас, без блокировки работника
- [ ] Кадры часов по-прежнему из работника в outbox соединения
- [ ] Сейв на диск: имя, id, часы, running, speedIndex; запись при create/delete, shutdown, редкий снимок часов (не 20 Гц)
- [ ] Старт процесса поднимает школы с диска
- [ ] Обновить инвариант в `AGENTS.md`: трогает школу только её работник
- [ ] Тесты AppHost: create → рестарт хоста (или явный reload) → школа на месте с тем же временем с разумной погрешностью снимка
## Критерий готовности
- `dotnet test` проходит
- Две школы тикают независимо (пауза одной не останавливает часы другой — уже так по симуляции; поток не должен это ломать)
- После перезапуска сервера список школ не пустой, если их создали
## Стоп
Не грузить defs в этой фазе. Не писать файл каждый тик.
+34
View File
@@ -0,0 +1,34 @@
# Фаза 3. Каталог def и карта
## Зависимости
- [Фаза 2](02-school-worker.md) — каталог и раскладка живут у работника школы
## Зачем
Типы в JSONC, инстанс карты в школе, валидация связности. Без этого нечему редактировать в create.
## Задачи
- [ ] Проект `HSchool.Content` (+ тесты): JSONC, defs, локали, граф карты, валидация. Не Arch, не ASP.NET
- [ ] Загрузчик пачки документов (сервер потом подставит папки `mods/`)
- [ ] `core` всегда первый; дальше моды по списку; повтор `defName` / ключа локали — последний победил, warning в лог
- [ ] Резолв ссылок `ThingDef.actions`, слотов RoomDef после чтения всего набора
- [ ] Карта-инстанс: двор (TerritoryDef) всегда есть; здания (BuildingDef), этажи (FloorDef + id), помещения, двусторонние рёбра
- [ ] Валидация: неизвестный def, ребро в никуда, несвязный граф, изолированный узел
- [ ] Пустая комната допустима; должности для панели — из def узла, если они там есть
- [ ] Каталог замораживается на работнике при create/load; сейв хранит id модов и раскладку, не развёрнутый каталог
- [ ] Нет папки мода из сейва — школу не стартовать, файл не удалять, в лог
- [ ] Обновить ориентир в `AGENTS.md` (куда класть defs)
- [ ] Наследование: `parent`, `abstract`, замена полей, запрет цикла и чужого вида, abstract нельзя на карту
- [ ] Патчи из `patches/`: add / replace / remove по JSON Pointer; неизвестный op или нет target — ошибка каталога
- [ ] Тесты Content: фикстуры JSONC, last-wins, parent/abstract, патч add в actions, связность, пустая комната, одна комната без двора — отказ
## Критерий готовности
- `dotnet test` покрывает загрузчик и валидацию без хоста (`HSchool.Content.Tests`)
- Ванильная раскладка `mods/core/maps/default.jsonc` проходит валидацию
## Стоп
Не UI редактора. Не исполнение ActionDef. Не xpath и не новые op сверх add/replace/remove.
+34
View File
@@ -0,0 +1,34 @@
# Фаза 4. Моды и редактор в create
## Зависимости
- [Фаза 1](01-manager-shell.md)
- [Фаза 3](03-defs-map.md)
## Зачем
Игрок выбирает моды, правит или стирает дефолтную карту, создаёт школу. Внутри школы дерево и
панель локации читают снимок, не заглушки.
## Задачи
- [ ] `GET` списка модов: `core` как обязательный, остальные папки `mods/`
- [ ] `GET` каталога с `?lang=ru|en` (типы + локали) для `core` + выбранных id
- [ ] Hello несёт тот же locale; снимок карты при OpenSchool на этом языке
- [ ] `POST /api/schools` принимает доп. моды и раскладку; сервер всегда подставляет `core` первым и валидирует
- [ ] В диалоге создания: чекбоксы модов (`core` нельзя снять), редактор дерева/связей/слотов или сброс к дефолту
- [ ] При `OpenSchool` — один снимок карты (дерево + локации: имя, предметы, пустые персонажи и действия на месте, должности). Не на каждый клик, не 20 Гц
- [ ] Клиент фильтрует выбранный узел; часы как сейчас
- [ ] Протокол/HTTP описать в `docs/protocol.md` в том же коммите, что кодек
- [ ] Тесты API: create с картой, отказ на дырявый граф, открытие отдаёт снимок
## Критерий готовности
- Создать школу с ванильной картой и с упрощённой своей
- Открыть: дерево кликается, локация показывает имя/предметы/должности, персонажи и действия на месте пустые, события пустые
- Перезапуск сервера поднимает эту школу с той же раскладкой
- `dotnet test` и клиентский `npm test` / `build` проходят
## Стоп
Не приказы «сесть». Не лента событий. Не `OpenLocation` на сервер.
+12 -9
View File
@@ -1,15 +1,18 @@
# Фазы ближайшего горизонта
# Фазы первого среза
Индекс. Статус: ⬜ не начата, 🔄 в работе, ✅ готова.
Дизайн игрока: [`../design/near-term.md`](../design/near-term.md).
Defs и карта: [`../design/defs.md`](../design/defs.md).
Потоки: [`../design/runtime.md`](../design/runtime.md).
Дизайн закрыт: [`../design/near-term.md`](../design/near-term.md),
[`../design/defs.md`](../design/defs.md),
[`../design/runtime.md`](../design/runtime.md),
[`../design/projects.md`](../design/projects.md).
Новых игровых фич в эти фазы не добавлять. 1 и 2 можно вести параллельно (клиент / сервер).
| Фаза | Статус | Зачем |
| --- | --- | --- |
| [0. Убрать PixiJS](00-drop-pixi.md) | ⬜ | Зависимость была под сцену, которой не будет |
| [1. Оболочка менеджера](01-manager-shell.md) | ⬜ | Каркас экрана школы под дерево / события / локацию |
| Работник на школу (поток + свой World) | — | Ломает текущий общий цикл; делать до тяжёлой симуляции |
| Каталог def + ванильная карта | | Загрузчик JSONC, локали мода, связность карты |
| Моды и редактор при создании | | Список `mods/`, выбор, раскладка в POST create |
| [0. Убрать PixiJS](00-drop-pixi.md) | ⬜ | Сцена не планируется |
| [1. Оболочка менеджера](01-manager-shell.md) | ⬜ | Панели с секциями среза, пока без данных |
| [2. Работник школы и диск](02-school-worker.md) | ⬜ | Поток + World + сейв — основа |
| [3. Каталог def и карта](03-defs-map.md) | | JSONC, core, валидация раскладки |
| [4. Моды и редактор в create](04-create-editor.md) | | Выбор модов, карта в POST, снимок при открытии |