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, карта и моды
|
||||
|
||||
Договорённость на ближайшее планирование, не текущий код.
|
||||
Экран игрока: [`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) | Не в этом срезе |
|
||||
|
||||
Reference in New Issue
Block a user