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.
This commit is contained in:
+113
-49
@@ -1,8 +1,8 @@
|
|||||||
# Defs, карта и моды
|
# Defs, карта и моды
|
||||||
|
|
||||||
Договорённость на ближайшее планирование, не текущий код.
|
Договорённость на ближайшее планирование, не текущий код.
|
||||||
Экран игрока: [`near-term.md`](near-term.md). Потоки и ECS: [`runtime.md`](runtime.md).
|
Экран игрока: [`near-term.md`](near-term.md). Потоки: [`runtime.md`](runtime.md).
|
||||||
Как устроен хост сейчас: [`../architecture.md`](../architecture.md).
|
Проекты: [`projects.md`](projects.md). Как устроен хост сейчас: [`../architecture.md`](../architecture.md).
|
||||||
|
|
||||||
Идея как у RimWorld: **ядро — код систем**, содержание игры — **данные (defs)**. Новый стул, кабинет
|
Идея как у RimWorld: **ядро — код систем**, содержание игры — **данные (defs)**. Новый стул, кабинет
|
||||||
или действие, которое уже умеет код, появляются без пересборки `HSchool.Simulation`. Сборка нужна,
|
или действие, которое уже умеет код, появляются без пересборки `HSchool.Simulation`. Сборка нужна,
|
||||||
@@ -37,8 +37,13 @@
|
|||||||
3. **PositionDef** — должность (директор). Вакансия — не def, а состояние школы.
|
3. **PositionDef** — должность (директор). Вакансия — не def, а состояние школы.
|
||||||
4. **WorkDef** — работа, которую можно вести в помещении (работа директора, урок, обход школы).
|
4. **WorkDef** — работа, которую можно вести в помещении (работа директора, урок, обход школы).
|
||||||
5. **RoomDef** — тип помещения. Слоты предметов, должности, которые он открывает, список WorkDef.
|
5. **RoomDef** — тип помещения. Слоты предметов, должности, которые он открывает, список WorkDef.
|
||||||
6. **BuildingDef** / группировка этажа — если зданию нужны свои свойства; иначе этаж может быть
|
6. **BuildingDef** — тип корпуса на территории (учебный, спортзал). Тонкий: редактор ставит *тип*
|
||||||
только узлом карты без отдельного def.
|
здания, моды добавляют новые корпуса.
|
||||||
|
7. **FloorDef** — тип этажа (типовой, подвал, чердак). Сейчас тонкий: `defName` и локаль, без правил.
|
||||||
|
Узел карты: id, `def`, родитель-здание, при необходимости номер/подпись («2»). Задел: позже на
|
||||||
|
def повесятся свойства этажа, не меняя форму ссылки на карте.
|
||||||
|
8. **TerritoryDef** — тип двора. Корень дерева и узел графа, такой же тонкий задел. В `core` хотя
|
||||||
|
бы один. Клик по двору — те же секции панели, что у комнаты.
|
||||||
|
|
||||||
Связи «из A можно попасть в B» **не** поле RoomDef, а рёбра карты. Иначе дефолтная и пользовательская
|
Связи «из A можно попасть в B» **не** поле RoomDef, а рёбра карты. Иначе дефолтная и пользовательская
|
||||||
карта не смогут расставить одни и те же типы комнат по-разному.
|
карта не смогут расставить одни и те же типы комнат по-разному.
|
||||||
@@ -50,14 +55,15 @@
|
|||||||
Иерархия дерева (она же закрывает вопрос «из чего дерево»):
|
Иерархия дерева (она же закрывает вопрос «из чего дерево»):
|
||||||
|
|
||||||
```
|
```
|
||||||
территория школы
|
территория школы ← TerritoryDef, корень дерева и узел графа
|
||||||
└── здание (их может быть несколько)
|
└── здание
|
||||||
└── этаж (если в здании больше одного; иначе этаж можно не показывать)
|
└── этаж ← FloorDef + id узла
|
||||||
└── помещение
|
└── помещение
|
||||||
```
|
```
|
||||||
|
|
||||||
Граф локаций — те же помещения плюс рёбра в обе стороны (кабинет ↔ коридор). Дерево — группировка
|
Граф локаций — территория, помещения и любые другие ходибелые узлы плюс рёбра в обе стороны
|
||||||
для UI, граф — куда можно перейти и откуда берутся «соседние» события.
|
(кабинет ↔ коридор, крыльцо ↔ двор). Дерево — группировка для UI; граф — куда можно перейти.
|
||||||
|
Два здания связываются через территорию (или явное ребро), не «висят» рядом в дереве без пути.
|
||||||
|
|
||||||
В процессе разработки кладём одну ванильную раскладку в Core. При создании школы игрок может
|
В процессе разработки кладём одну ванильную раскладку в Core. При создании школы игрок может
|
||||||
изменить её в редакторе или стереть и собрать свою. Редактор оперирует инстансом и **выбирает
|
изменить её в редакторе или стереть и собрать свою. Редактор оперирует инстансом и **выбирает
|
||||||
@@ -73,14 +79,55 @@
|
|||||||
два парсера в голове. Почему не свой формат: его придётся документировать сильнее, чем игру.
|
два парсера в голове. Почему не свой формат: его придётся документировать сильнее, чем игру.
|
||||||
|
|
||||||
Один файл — один def или небольшая пачка одного вида (`things/chair.jsonc`). Ссылки — строки
|
Один файл — один def или небольшая пачка одного вида (`things/chair.jsonc`). Ссылки — строки
|
||||||
`defName`, резолв после загрузки всего каталога (как у RimWorld: сначала прочитать, потом связать).
|
`defName`. Загрузчик Content идёт так:
|
||||||
|
|
||||||
Наследование по желанию, как `ParentName`: `{ "defName": "OfficeChair", "parent": "Chair" }`.
|
1. Прочитать defs всех пакетов по порядку (`core`, затем моды). Повтор `defName` — последний
|
||||||
Не обязательно в первом срезе; без него можно жить, пока стульев мало.
|
победил, warning.
|
||||||
|
2. Разрешить **наследование**.
|
||||||
|
3. Применить **патчи** пакетов в том же порядке.
|
||||||
|
4. Резолв ссылок (`actions`, слоты). Abstract def на карту ставить нельзя.
|
||||||
|
|
||||||
Патчи модов (добавить действие к ванильному стулу, не копируя файл) — отдельным видом файлов
|
Это база: новые поля у def и новые `op` у патча добавляются в Content, папки не меняются.
|
||||||
позже. Сначала: новые defs + замена дефолтной карты целиком. Частичные патчи — когда появится
|
|
||||||
второй мод, которому это реально нужно.
|
### Наследование
|
||||||
|
|
||||||
|
Как `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`
|
Подписи содержания живут **не в def**, а в `localizations/` мода. В def остаётся `defName`
|
||||||
(и при необходимости ключ, если он не совпадает с именем). Черновик без вшитых ru/en:
|
(и при необходимости ключ, если он не совпадает с именем). Черновик без вшитых ru/en:
|
||||||
@@ -129,18 +176,30 @@ Def говорит «у стула есть Sit». Система `Sit` в C# з
|
|||||||
Скриптовый язык внутри JSON в этом горизонте не заводим.
|
Скриптовый язык внутри JSON в этом горизонте не заводим.
|
||||||
|
|
||||||
В первой живой версии ActionDef и ссылки с предметов — **заготовки**: они есть в каталоге и на
|
В первой живой версии ActionDef и ссылки с предметов — **заготовки**: они есть в каталоге и на
|
||||||
карте, системы их ещё не исполняют, игрок не отдаёт приказ «сесть». Имеет смысл уже показать
|
карте, системы их ещё не исполняют, игрок не отдаёт приказ «сесть». В панели локации показываем
|
||||||
список в панели локации (чтобы каркас был правдой), но не симулировать.
|
имена действий у стоящих предметов. Персонажи и «А говорит с Б» — секции панели, списки пустые.
|
||||||
|
Должности берём из RoomDef поставленного помещения (вакансии без людей).
|
||||||
|
|
||||||
|
## Снимок карты на клиент
|
||||||
|
|
||||||
|
При `OpenSchool` сервер один раз отдаёт дерево и состав всех узлов (двор, корпуса, этажи,
|
||||||
|
помещения). Подписи — язык из Hello (`ru` | `en`). Каталог для редактора — тот же код языка
|
||||||
|
в `?lang=` у HTTP. `Accept-Language` не используем: переключатель RU/EN в подвале с ним разъедется.
|
||||||
|
|
||||||
|
Клиент сам выбирает узел; секции панели одни и те же. Часы по-прежнему 20 Гц. Карта после
|
||||||
|
create в этом срезе не меняется, повторно слать не нужно. Hello с locale — смена раскладки
|
||||||
|
кадра, в том же коммите bump версии протокола.
|
||||||
|
|
||||||
## Карта: пустые комнаты и связность
|
## Карта: пустые комнаты и связность
|
||||||
|
|
||||||
Помещение может быть **пустым** (ни одного предмета в слотах). Не может быть **висячим**: каждое
|
Помещение может быть **пустым** (ни одного предмета в слотах). Карта в целом — один связный
|
||||||
помещение связано хотя бы с одним другим, и карта в целом — один связный неориентированный граф
|
неориентированный граф (рёбра двусторонние). Территория в нём всегда есть: это и корень дерева,
|
||||||
(рёбра двусторонние).
|
и двор.
|
||||||
|
|
||||||
Исключение: школа из **одной** комнаты — граф из одной вершины, рёбер нет, это допустимо.
|
Изолированный узел запрещён. Практический минимум: двор + хотя бы одно помещение с ребром во двор
|
||||||
Два здания без перехода между ними — нет: нужен общий узел (коридор, двор, территория как
|
(или цепочка помещений, которая в итоге выходит во двор). Школа «одна комната без двора» в этой
|
||||||
локация) или явное ребро.
|
модели не собирается — двор не выкидывается. Два здания без пути через двор (или другое ребро) —
|
||||||
|
тоже нет.
|
||||||
|
|
||||||
Сервер отвергает раскладку с изолированной комнатой, ребром в несуществующий id и `defName`,
|
Сервер отвергает раскладку с изолированной комнатой, ребром в несуществующий id и `defName`,
|
||||||
которого нет в каталоге *выбранных для этой школы* модов.
|
которого нет в каталоге *выбранных для этой школы* модов.
|
||||||
@@ -156,8 +215,8 @@ create раскладка замораживается вместе с набо
|
|||||||
|
|
||||||
## Папка мода и локализации
|
## Папка мода и локализации
|
||||||
|
|
||||||
Моды — подпапки на диске сервера, например `mods/<id>/`. Ядро — тоже мод (всегда включён или
|
Моды — подпапки на диске сервера, например `mods/<id>/`. Ядро — мод `core`: тот же формат папки,
|
||||||
выбирается отдельно, см. открытые вопросы), лежит рядом, чтобы путь загрузки был один.
|
но **всегда включён**, в UI создания его нельзя снять. Остальные папки — чекбоксы.
|
||||||
|
|
||||||
```
|
```
|
||||||
mods/
|
mods/
|
||||||
@@ -170,11 +229,16 @@ mods/
|
|||||||
default.jsonc # ванильная раскладка
|
default.jsonc # ванильная раскладка
|
||||||
furniture-pack/
|
furniture-pack/
|
||||||
defs/
|
defs/
|
||||||
|
patches/ # правки чужих def, не копии
|
||||||
localizations/
|
localizations/
|
||||||
ru.jsonc
|
ru.jsonc
|
||||||
en.jsonc
|
en.jsonc
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Порядок загрузки: сначала `core`, потом выбранные моды в том порядке, в каком они стоят в
|
||||||
|
запросе create. Одинаковый `defName` или ключ локали — **побеждает последний**. Имеет смысл
|
||||||
|
писать предупреждение в лог, не молча, чтобы конфликт модов было видно.
|
||||||
|
|
||||||
Ключи локализации — по `defName` (и суффиксам вроде `.label`, если понадобятся несколько строк
|
Ключи локализации — по `defName` (и суффиксам вроде `.label`, если понадобятся несколько строк
|
||||||
на тип). Нет ключа — в UI показывается `defName`, это сразу видно в тесте мода.
|
на тип). Нет ключа — в UI показывается `defName`, это сразу видно в тесте мода.
|
||||||
|
|
||||||
@@ -182,19 +246,19 @@ mods/
|
|||||||
сервер резолвит из `localizations/` включённых модов под язык клиента и кладёт в снимок / в
|
сервер резолвит из `localizations/` включённых модов под язык клиента и кладёт в снимок / в
|
||||||
ответ каталога для редактора. Браузер файлы модов не читает.
|
ответ каталога для редактора. Браузер файлы модов не читает.
|
||||||
|
|
||||||
При создании школы в UI показывают список папок из `mods/` (кроме того, что решим всегда
|
В диалоге создания показывают `core` как включённый и заблокированный плюс список остальных
|
||||||
включать). Выбранный набор уходит вместе с раскладкой. Работник школы грузит **этот** набор
|
папок. Тело create несёт только *дополнительные* id (сервер всё равно подставит `core` первым).
|
||||||
и замораживает каталог. Новые файлы на диске влияют только на следующие школы; список модов
|
Работник школы грузит этот набор и замораживает каталог. Новые файлы на диске влияют только на
|
||||||
для диалога можно сканировать при `GET`, без рестарта процесса.
|
следующие школы; список модов для диалога можно сканировать при `GET`, без рестарта процесса.
|
||||||
|
|
||||||
## Где это живёт в решении
|
## Где это живёт в решении
|
||||||
|
|
||||||
- Каталог def и загрузка папок — `HSchool.Simulation` (без ASP.NET, без сокетов). Тесты читают
|
- Каталог def, раскладка и валидация — **HSchool.Content**, см. [`projects.md`](projects.md).
|
||||||
фикстуры и резолвят ссылки. Каталог **на школу**: зависит от модов, выбранных при create.
|
`HSchool.Simulation` только пользуется уже собранным каталогом. Пути `mods/` и `saves/` — Server.
|
||||||
- Инстанс карты — часть `School`, рядом с часами и ECS. Свой `World` у каждой школы; свой поток —
|
- Инстанс карты (граф) — тип из Content, лежит в `School` (Simulation) вместе с часами и ECS.
|
||||||
см. [`runtime.md`](runtime.md).
|
Поток и файл сейва — Server, см. [`runtime.md`](runtime.md) и [`projects.md`](projects.md).
|
||||||
- Валидация раскладки с клиента — на сервере: неизвестный `defName`, ребро в никуда, несвязный
|
Сейв: раскладка, id модов, часы, имя; каталог снова с диска при загрузке. Не писать каждый тик.
|
||||||
граф, изолированная комната (если комнат больше одной).
|
- Валидация раскладки — Content; Server только вызывает её на теле create.
|
||||||
- Клиент редактора — DOM в диалоге создания; тело create = имя, дата, список модов, раскладка.
|
- Клиент редактора — DOM в диалоге создания; тело create = имя, дата, список модов, раскладка.
|
||||||
|
|
||||||
Сканирование `mods/` — при запросе списка и при создании школы, не каждый тик. Горячая подмена
|
Сканирование `mods/` — при запросе списка и при создании школы, не каждый тик. Горячая подмена
|
||||||
@@ -203,8 +267,7 @@ mods/
|
|||||||
## Советы, которые стоит принять сразу
|
## Советы, которые стоит принять сразу
|
||||||
|
|
||||||
- Не класть граф проходов в RoomDef — только в карту.
|
- Не класть граф проходов в RoomDef — только в карту.
|
||||||
- Не слать весь каталог и всю карту 20 раз в секунду: дерево и состав комнаты — при изменении
|
- Не слать весь каталог и всю карту 20 раз в секунду: дерево — один снимок при открытии.
|
||||||
или при открытии школы; часы как сейчас.
|
|
||||||
- Редактор только в create: шлёт раскладку и список id модов, не пути на диск и не C#.
|
- Редактор только в create: шлёт раскладку и список id модов, не пути на диск и не C#.
|
||||||
- Слоты в RoomDef — чертёж; на карте слот можно не заполнять (пустая комната).
|
- Слоты в RoomDef — чертёж; на карте слот можно не заполнять (пустая комната).
|
||||||
- Локали не смешивать с defs: иначе мод на третьем языке правит типы.
|
- Локали не смешивать с defs: иначе мод на третьем языке правит типы.
|
||||||
@@ -218,17 +281,18 @@ mods/
|
|||||||
| Карта | Инстанс: территория → здания → этажи → помещения + граф связей |
|
| Карта | Инстанс: территория → здания → этажи → помещения + граф связей |
|
||||||
| Дефолтная карта | Одна ванильная раскладка Core; при создании можно править или стереть |
|
| Дефолтная карта | Одна ванильная раскладка Core; при создании можно править или стереть |
|
||||||
| Редактор | Только в диалоге создания школы |
|
| Редактор | Только в диалоге создания школы |
|
||||||
| Пустая комната | Можно; граф должен быть связным (одна комната — ок) |
|
| Пустая комната | Можно; граф связный, двор всегда в графе |
|
||||||
| Действия | Заготовки в def, без исполнения и без приказов |
|
| Действия | Заготовки в 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 |
|
| Локализация модов | `mods/<id>/localizations/{ru,en}.jsonc`, не внутри def |
|
||||||
| Код модов (DLL) | Не в этом горизонте |
|
| Этаж | FloorDef, тонкий; правила этажа — поля def позже |
|
||||||
| Поток / ECS | Отдельный `World` обязательно; отдельный поток на школу — цель, см. [`runtime.md`](runtime.md) |
|
| Здание | BuildingDef — тип корпуса |
|
||||||
|
| Сохранения | Файл школы: раскладка, id модов, часы, имя, пауза/скорость; каталог с диска при загрузке |
|
||||||
## Ещё не решено
|
| Поток / ECS | Основа среза: один поток и один `World` на школу, см. [`runtime.md`](runtime.md) |
|
||||||
|
| Код модов (DLL) | Не в этом срезе |
|
||||||
1. `core` всегда включён и его нельзя снять в UI, или это такой же мод в списке?
|
|
||||||
2. Территория / двор — отдельная ходибельная локация (чтобы связывать здания), или только группировка в дереве?
|
|
||||||
3. Этаж без отдельного def — ок, пока у этажа нет своих свойств?
|
|
||||||
4. Порядок модов в списке при create влияет на коллизии `defName` (последний победил) — так и делаем?
|
|
||||||
5. Сохранение школы на диск — здесь или после первого живого среза? (набор модов и раскладку всё равно надо будет сериализовать вместе со школой, когда сохранения появятся)
|
|
||||||
|
|||||||
+33
-15
@@ -25,7 +25,26 @@ QSP: не «полотно текста + список действий», а п
|
|||||||
даже если клиент рисует строки списка, а не человечков.
|
даже если клиент рисует строки списка, а не человечков.
|
||||||
|
|
||||||
Типы комнат, предметов и действий описываются **defs** (JSONC), конкретная школа — **картой-инстансом**.
|
Типы комнат, предметов и действий описываются **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) |
|
| Данные игры | Defs (типы) + карта (инстанс), см. [`defs.md`](defs.md) |
|
||||||
| Дерево карты | Территория → здание → этаж → помещение; рёбра проходов — граф инстанса |
|
| Дерево карты | Территория (= двор, узел графа) → здание → этаж → помещение |
|
||||||
| Редактор карты | Только при создании школы |
|
| Редактор карты | Только при создании школы |
|
||||||
| Моды | Папки на сервере, выбор в диалоге создания |
|
| Моды | `core` всегда; остальные — выбор при создании; поздний мод побеждает при споре имён |
|
||||||
| Поток школы | Цель: один поток и один ECS world на школу, см. [`runtime.md`](runtime.md) |
|
| Поток школы | Основа среза: один поток и один ECS world на школу, см. [`runtime.md`](runtime.md) |
|
||||||
|
| Сохранения | На диск; часы не писать на каждом тике |
|
||||||
## Ещё не решено
|
| События | Панель есть, лента пустая |
|
||||||
|
| Локация | Одни секции для двора, корпуса, этажа и комнаты |
|
||||||
Эти вопросы закрывают игровой срез. Карту и моды больше не блокируют.
|
| Двор | TerritoryDef, узел графа и корень дерева |
|
||||||
|
| Язык модов | HTTP `?lang=`, то же в Hello; не Accept-Language |
|
||||||
1. Что такое «общее событие»: лог за смену, только текущие процессы, или новости («урок начался»)?
|
| Проекты | Каталог не в Server и не в Simulation, см. [`projects.md`](projects.md) |
|
||||||
2. Что видно на выбранной локации сначала: предметы (в т.ч. пусто), заготовки действий, вакансии?
|
| Выбор в дереве | Клиентский фильтр |
|
||||||
3. Выбор узла в дереве — только фильтр на клиенте или намерение на сервер?
|
| Этаж | FloorDef (тонкий тип); узел карты ссылается на def |
|
||||||
|
|
||||||
Открытые вопросы по core-моду, двору и коллизиям имён — в конце [`defs.md`](defs.md).
|
|
||||||
|
|
||||||
## Заведомо не сейчас
|
## Заведомо не сейчас
|
||||||
|
|
||||||
- Портреты, арт локаций, схема этажа в пикселях.
|
- Портреты, арт локаций, схема этажа в пикселях.
|
||||||
- Управление персонажем от первого/третьего лица.
|
- Управление персонажем от первого/третьего лица.
|
||||||
- Победа/кампания, пока нет даже наблюдаемого мира.
|
- Победа/кампания, живые персонажи, исполнение «сесть», записи событий.
|
||||||
|
- Смена модов у живой школы, код модов (DLL).
|
||||||
|
|||||||
@@ -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
@@ -5,28 +5,33 @@
|
|||||||
модель. Менять инвариант в рабочих соглашениях имеет смысл только вместе с кодом.
|
модель. Менять инвариант в рабочих соглашениях имеет смысл только вместе с кодом.
|
||||||
|
|
||||||
Экран и defs: [`near-term.md`](near-term.md), [`defs.md`](defs.md).
|
Экран и defs: [`near-term.md`](near-term.md), [`defs.md`](defs.md).
|
||||||
|
Куда класть код: [`projects.md`](projects.md).
|
||||||
|
|
||||||
|
Работник (поток, очередь, сейв-файл) живёт в **Server**. То, что он тикает — `School` в
|
||||||
|
**Simulation**. Каталог, с которым школа создана — **Content**, замороженный у работника.
|
||||||
|
|
||||||
## Что уже почти есть
|
## Что уже почти есть
|
||||||
|
|
||||||
У каждой `School` уже свой Arch `World`. Это оставляем и делаем жёстким правилом: мир не
|
У каждой `School` уже свой Arch `World`. Это оставляем и делаем жёстким правилом: мир не
|
||||||
разделяется между школами и не отдаётся чужому потоку. Arch не потокобезопасен.
|
разделяется между школами и не отдаётся чужому потоку. Arch не потокобезопасен.
|
||||||
|
|
||||||
## Целевая модель
|
## Целевая модель — в этом срезе
|
||||||
|
|
||||||
Каждая школа — **актор**: свой поток (или `TaskCreationOptions.LongRunning`, что для нас то же
|
Каждая школа — **актор**: свой поток (`TaskCreationOptions.LongRunning` / выделенный поток, не
|
||||||
самое: выделенный поток, не пул тиков), свой `World`, свои часы, свой **замороженный** каталог
|
пул тиков), свой `World`, свои часы, свой **замороженный** каталог def, свой файл на диске.
|
||||||
def (набор модов, выбранный при создании).
|
|
||||||
|
|
||||||
Над ними — тонкий супервизор (сегодняшняя роль `GameLoopService` + `SchoolRegistry`):
|
Над ними — тонкий супервизор (вместо сегодняшнего общего цикла):
|
||||||
|
|
||||||
- создать / удалить школу, знать лимит;
|
- создать / удалить школу, знать лимит;
|
||||||
- держать ящики: `id →` очередь команд этой школы;
|
- держать ящики: `id →` очередь команд этой школы;
|
||||||
- собирать **снимки** для `GET /api/schools` (работник публикует неизменяемое состояние, как
|
- собирать **снимки** для `GET /api/schools` (работник публикует неизменяемое состояние, как
|
||||||
сейчас цикл публикует `SchoolsState`);
|
сейчас цикл публикует `SchoolsState`);
|
||||||
- маршрутизировать сокет: `OpenSchool` / `SetRunning` / кадры часов — только в ящик той школы.
|
- маршрутизировать сокет: `OpenSchool` / `SetRunning` / кадры часов — только в ящик той школы;
|
||||||
|
- при старте процесса поднять школы с диска, при остановке — дождаться записи работников.
|
||||||
|
|
||||||
Супервизор **не** вызывает `World`, не тикает часы, не читает defs инстанса. Работник **не**
|
Супервизор **не** вызывает `World`, не тикает часы, не читает defs инстанса. Работник **не**
|
||||||
трогает чужой мир и не ходит в ASP.NET.
|
трогает чужой мир и не ходит в ASP.NET. Файл школы пишет **работник** (create/delete через
|
||||||
|
команду супервизору, снимок часов — редкий, shutdown — обязательный).
|
||||||
|
|
||||||
Тик по-прежнему фиксированный (`SimulationOptions.FixedDeltaTime`), у каждого работника свой
|
Тик по-прежнему фиксированный (`SimulationOptions.FixedDeltaTime`), у каждого работника свой
|
||||||
таймер. Школы не синхронизируют календарь друг с другом — так и задумано.
|
таймер. Школы не синхронизируют календарь друг с другом — так и задумано.
|
||||||
@@ -56,6 +61,6 @@ def (набор модов, выбранный при создании).
|
|||||||
|
|
||||||
## Пока не делаем
|
## Пока не делаем
|
||||||
|
|
||||||
- Потоки на системы внутри одной школы (job system как у RimWorld) — рано.
|
- Потоки на системы внутри одной школы (job system как у RimWorld).
|
||||||
- Миграция живой школы на другой набор модов.
|
- Миграция живой школы на другой набор модов.
|
||||||
- Правка `AGENTS.md` до тех пор, пока цикл в коде ещё общий.
|
- Запись сейва на каждом тике.
|
||||||
|
|||||||
@@ -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 и карты
|
Не наполнять персонажами и событиями. Данные — с фазы 4.
|
||||||
([`../design/defs.md`](../design/defs.md)), когда закроются открытые вопросы там.
|
|
||||||
|
|||||||
@@ -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 в этой фазе. Не писать файл каждый тик.
|
||||||
@@ -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.
|
||||||
@@ -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
@@ -1,15 +1,18 @@
|
|||||||
# Фазы ближайшего горизонта
|
# Фазы первого среза
|
||||||
|
|
||||||
Индекс. Статус: ⬜ не начата, 🔄 в работе, ✅ готова.
|
Индекс. Статус: ⬜ не начата, 🔄 в работе, ✅ готова.
|
||||||
|
|
||||||
Дизайн игрока: [`../design/near-term.md`](../design/near-term.md).
|
Дизайн закрыт: [`../design/near-term.md`](../design/near-term.md),
|
||||||
Defs и карта: [`../design/defs.md`](../design/defs.md).
|
[`../design/defs.md`](../design/defs.md),
|
||||||
Потоки: [`../design/runtime.md`](../design/runtime.md).
|
[`../design/runtime.md`](../design/runtime.md),
|
||||||
|
[`../design/projects.md`](../design/projects.md).
|
||||||
|
|
||||||
|
Новых игровых фич в эти фазы не добавлять. 1 и 2 можно вести параллельно (клиент / сервер).
|
||||||
|
|
||||||
| Фаза | Статус | Зачем |
|
| Фаза | Статус | Зачем |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| [0. Убрать PixiJS](00-drop-pixi.md) | ⬜ | Зависимость была под сцену, которой не будет |
|
| [0. Убрать PixiJS](00-drop-pixi.md) | ⬜ | Сцена не планируется |
|
||||||
| [1. Оболочка менеджера](01-manager-shell.md) | ⬜ | Каркас экрана школы под дерево / события / локацию |
|
| [1. Оболочка менеджера](01-manager-shell.md) | ⬜ | Панели с секциями среза, пока без данных |
|
||||||
| Работник на школу (поток + свой World) | — | Ломает текущий общий цикл; делать до тяжёлой симуляции |
|
| [2. Работник школы и диск](02-school-worker.md) | ⬜ | Поток + World + сейв — основа |
|
||||||
| Каталог def + ванильная карта | — | Загрузчик JSONC, локали мода, связность карты |
|
| [3. Каталог def и карта](03-defs-map.md) | ⬜ | JSONC, core, валидация раскладки |
|
||||||
| Моды и редактор при создании | — | Список `mods/`, выбор, раскладка в POST create |
|
| [4. Моды и редактор в create](04-create-editor.md) | ⬜ | Выбор модов, карта в POST, снимок при открытии |
|
||||||
|
|||||||
Reference in New Issue
Block a user