diff --git a/docs/design/defs.md b/docs/design/defs.md new file mode 100644 index 0000000..9dd6360 --- /dev/null +++ b/docs/design/defs.md @@ -0,0 +1,234 @@ +# Defs, карта и моды + +Договорённость на ближайшее планирование, не текущий код. +Экран игрока: [`near-term.md`](near-term.md). Потоки и ECS: [`runtime.md`](runtime.md). +Как устроен хост сейчас: [`../architecture.md`](../architecture.md). + +Идея как у RimWorld: **ядро — код систем**, содержание игры — **данные (defs)**. Новый стул, кабинет +или действие, которое уже умеет код, появляются без пересборки `HSchool.Simulation`. Сборка нужна, +только когда появляется *новый вид поведения*, которого нет ни в одной системе. + +Это не локальная RimWorld: сервер авторитетен, клиент — браузер. Папка мода живёт **у сервера**. +Клиент не грузит defs и не считает ходы; он рисует снимки, которые сервер собрал из defs + инстанса +карты. + +## Два слоя, не один + +| Слой | Что это | Пример | +| --- | --- | --- | +| **Def** | Тип, каталог | «Стул», «Сесть», «Кабинет директора» | +| **Инстанс** | Конкретная школа / карта | *этот* стул директора в *этом* кабинете, дверь в *этот* коридор | + +Дерево в UI — вид на инстанс. Граф проходов (из кабинета в коридор и обратно) тоже **инстанс**: +в def кабинета нельзя честно написать «ведёт в коридор №3», этого коридора ещё нет. В def — +какие предметы и работы *характерны* для типа помещения; на карте — какие узлы реально стоят +и какими рёбрами связаны. + +Должность и вакансия рождаются из инстанса: поставили кабинет директора → в школе появилась +должность и незакрытая вакансия. Снесли кабинет — вакансия уходит. Def помещения говорит +*какую* должность этот тип создаёт, карта говорит *что уже построено*. + +## Виды def (первая нарезка) + +Имена предварительные, смысл — ваш. + +1. **ActionDef** — глагол: сесть, перенести. Код системы знает, как исполнить известный `defName`. +2. **ThingDef** — предмет. Ссылается на действия, которые с ним допустимы (`Chair` → `Sit`). +3. **PositionDef** — должность (директор). Вакансия — не def, а состояние школы. +4. **WorkDef** — работа, которую можно вести в помещении (работа директора, урок, обход школы). +5. **RoomDef** — тип помещения. Слоты предметов, должности, которые он открывает, список WorkDef. +6. **BuildingDef** / группировка этажа — если зданию нужны свои свойства; иначе этаж может быть + только узлом карты без отдельного def. + +Связи «из A можно попасть в B» **не** поле RoomDef, а рёбра карты. Иначе дефолтная и пользовательская +карта не смогут расставить одни и те же типы комнат по-разному. + +## Как выглядит дефолтная карта + +Это файл **раскладки**, не каталог типов. Он ссылается на `defName` и задаёт id узлов. + +Иерархия дерева (она же закрывает вопрос «из чего дерево»): + +``` +территория школы + └── здание (их может быть несколько) + └── этаж (если в здании больше одного; иначе этаж можно не показывать) + └── помещение +``` + +Граф локаций — те же помещения плюс рёбра в обе стороны (кабинет ↔ коридор). Дерево — группировка +для UI, граф — куда можно перейти и откуда берутся «соседние» события. + +В процессе разработки кладём одну ванильную раскладку в Core. При создании школы игрок может +изменить её в редакторе или стереть и собрать свою. Редактор оперирует инстансом и **выбирает +типы из каталога def**, а не рисует свободную геометрию (геометрии в этом горизонте нет). + +## Формат: JSONC, не новый язык + +Рекомендация: **JSON с комментариями и хвостовыми запятыми** (JSONC). `System.Text.Json` это уже +умеет (`ReadCommentHandling.Allow`, `AllowTrailingCommas`). Отдельный YAML/TOML/XML не даёт +выигрыша, а RimWorld-XML здесь ни к чему — патчи можно сделать проще. + +Почему не «чистый JSON»: defs пишут люди, комментарии обязательны. Почему не YAML: пробелы и +два парсера в голове. Почему не свой формат: его придётся документировать сильнее, чем игру. + +Один файл — один def или небольшая пачка одного вида (`things/chair.jsonc`). Ссылки — строки +`defName`, резолв после загрузки всего каталога (как у RimWorld: сначала прочитать, потом связать). + +Наследование по желанию, как `ParentName`: `{ "defName": "OfficeChair", "parent": "Chair" }`. +Не обязательно в первом срезе; без него можно жить, пока стульев мало. + +Патчи модов (добавить действие к ванильному стулу, не копируя файл) — отдельным видом файлов +позже. Сначала: новые defs + замена дефолтной карты целиком. Частичные патчи — когда появится +второй мод, которому это реально нужно. + +Подписи содержания живут **не в def**, а в `localizations/` мода. В def остаётся `defName` +(и при необходимости ключ, если он не совпадает с именем). Черновик без вшитых ru/en: + +```jsonc +// ActionDef — заготовка: каталог знает глагол, исполнения в этом горизонте нет +{ "defName": "Sit" } + +// ThingDef +{ "defName": "Chair", "actions": ["Sit"] } + +// RoomDef: тип, не конкретный кабинет на карте +{ + "defName": "PrincipalsOffice", + "slots": [ + { "key": "directorChair", "thing": "DirectorsChair" }, + { "key": "desk", "thing": "Desk" }, + { "key": "guestChair", "thing": "Chair", "count": 2 }, + ], + "positions": ["Principal"], + "works": ["PrincipalOfficeWork", "TeachLesson", "WalkSchool"], +} + +// Кусок карты (инстанс) +{ + "rooms": [ + { + "id": "principals-office", + "def": "PrincipalsOffice", + "building": "main", + "floor": "1", + "links": ["corridor-1"], + }, + ], +} +``` + +## Что остаётся кодом + +Def говорит «у стула есть Sit». Система `Sit` в C# знает правила: кто может сесть, сколько это +длится, что происходит с персонажем. Новый предмет с тем же `Sit` — только JSON. Новый глагол +«взломать замок», которого нет в коде — либо обобщённый исполнитель (долго и скользко), либо +сборка с новой системой. + +Пока держим правило RimWorld: **данные компонуют известные глаголы; неизвестный глагол = код.** +Скриптовый язык внутри JSON в этом горизонте не заводим. + +В первой живой версии ActionDef и ссылки с предметов — **заготовки**: они есть в каталоге и на +карте, системы их ещё не исполняют, игрок не отдаёт приказ «сесть». Имеет смысл уже показать +список в панели локации (чтобы каркас был правдой), но не симулировать. + +## Карта: пустые комнаты и связность + +Помещение может быть **пустым** (ни одного предмета в слотах). Не может быть **висячим**: каждое +помещение связано хотя бы с одним другим, и карта в целом — один связный неориентированный граф +(рёбра двусторонние). + +Исключение: школа из **одной** комнаты — граф из одной вершины, рёбер нет, это допустимо. +Два здания без перехода между ними — нет: нужен общий узел (коридор, двор, территория как +локация) или явное ребро. + +Сервер отвергает раскладку с изолированной комнатой, ребром в несуществующий id и `defName`, +которого нет в каталоге *выбранных для этой школы* модов. + +## Редактор — только при создании + +В этом горизонте карту меняют **до** того, как школа пошла жить: в диалоге создания. После +create раскладка замораживается вместе с набором модов. Построить/снести в уже идущей школе — +отдельный разговор (это уже игровые приказы, не редактор). + +Порядок в UI: выбрать моды → получить каталог (типы комнат/предметов) → править или стереть +дефолтную карту → имя, дата, создать. Редактор без списка модов не знает, какие RoomDef существуют. + +## Папка мода и локализации + +Моды — подпапки на диске сервера, например `mods//`. Ядро — тоже мод (всегда включён или +выбирается отдельно, см. открытые вопросы), лежит рядом, чтобы путь загрузки был один. + +``` +mods/ + core/ + defs/ # JSONC типов + localizations/ + ru.jsonc + en.jsonc + maps/ + default.jsonc # ванильная раскладка + furniture-pack/ + defs/ + localizations/ + ru.jsonc + en.jsonc +``` + +Ключи локализации — по `defName` (и суффиксам вроде `.label`, если понадобятся несколько строк +на тип). Нет ключа — в UI показывается `defName`, это сразу видно в тесте мода. + +Хром клиента (меню, кнопки) по-прежнему в `src/HSchool.Client/src/i18n/`. Строки содержания +сервер резолвит из `localizations/` включённых модов под язык клиента и кладёт в снимок / в +ответ каталога для редактора. Браузер файлы модов не читает. + +При создании школы в UI показывают список папок из `mods/` (кроме того, что решим всегда +включать). Выбранный набор уходит вместе с раскладкой. Работник школы грузит **этот** набор +и замораживает каталог. Новые файлы на диске влияют только на следующие школы; список модов +для диалога можно сканировать при `GET`, без рестарта процесса. + +## Где это живёт в решении + +- Каталог def и загрузка папок — `HSchool.Simulation` (без ASP.NET, без сокетов). Тесты читают + фикстуры и резолвят ссылки. Каталог **на школу**: зависит от модов, выбранных при create. +- Инстанс карты — часть `School`, рядом с часами и ECS. Свой `World` у каждой школы; свой поток — + см. [`runtime.md`](runtime.md). +- Валидация раскладки с клиента — на сервере: неизвестный `defName`, ребро в никуда, несвязный + граф, изолированная комната (если комнат больше одной). +- Клиент редактора — DOM в диалоге создания; тело create = имя, дата, список модов, раскладка. + +Сканирование `mods/` — при запросе списка и при создании школы, не каждый тик. Горячая подмена +каталога у уже живой школы в этом горизонте не нужна. + +## Советы, которые стоит принять сразу + +- Не класть граф проходов в RoomDef — только в карту. +- Не слать весь каталог и всю карту 20 раз в секунду: дерево и состав комнаты — при изменении + или при открытии школы; часы как сейчас. +- Редактор только в create: шлёт раскладку и список id модов, не пути на диск и не C#. +- Слоты в RoomDef — чертёж; на карте слот можно не заполнять (пустая комната). +- Локали не смешивать с defs: иначе мод на третьем языке правит типы. + +## Зафиксировано этим разговором + +| Тема | Решение | +| --- | --- | +| Содержание игры | Максимально defs, ядро — системы под известные глаголы | +| Формат (предложение) | JSONC, ссылки строками `defName` | +| Карта | Инстанс: территория → здания → этажи → помещения + граф связей | +| Дефолтная карта | Одна ванильная раскладка Core; при создании можно править или стереть | +| Редактор | Только в диалоге создания школы | +| Пустая комната | Можно; граф должен быть связным (одна комната — ок) | +| Действия | Заготовки в def, без исполнения и без приказов | +| Моды | `mods//` на диске сервера, выбор в UI при создании; каталог замораживается | +| Локализация модов | `mods//localizations/{ru,en}.jsonc`, не внутри def | +| Код модов (DLL) | Не в этом горизонте | +| Поток / ECS | Отдельный `World` обязательно; отдельный поток на школу — цель, см. [`runtime.md`](runtime.md) | + +## Ещё не решено + +1. `core` всегда включён и его нельзя снять в UI, или это такой же мод в списке? +2. Территория / двор — отдельная ходибельная локация (чтобы связывать здания), или только группировка в дереве? +3. Этаж без отдельного def — ок, пока у этажа нет своих свойств? +4. Порядок модов в списке при create влияет на коллизии `defName` (последний победил) — так и делаем? +5. Сохранение школы на диск — здесь или после первого живого среза? (набор модов и раскладку всё равно надо будет сериализовать вместе со школой, когда сохранения появятся) diff --git a/docs/design/near-term.md b/docs/design/near-term.md new file mode 100644 index 0000000..efab940 --- /dev/null +++ b/docs/design/near-term.md @@ -0,0 +1,60 @@ +# Ближайший горизонт: что видит игрок + +Это не текущее состояние кода, а договорённости на ближайшее планирование. +Как устроено сейчас — в [`architecture.md`](../architecture.md). + +## Решение: не PixiJS, а менеджер на DOM + +Внутри школы нет 2D-сцены, камеры и спрайтов. Интерфейс ближе к менеджеру, чем к классическому +QSP: не «полотно текста + список действий», а панели. + +Ориентир по экрану школы (после часов и паузы, которые уже есть): + +- **Карта** — дерево локаций (tree view). +- **Общие события** — то, что происходит в школе целиком, не привязано к выбранному узлу. +- **Выбранная локация** — что происходит именно там. + +Картинок локаций и портретов в этом горизонте нет; позже можно добавить, не ломая панели. +Анимация пока только переходы панелей/списков и живые цифры (часы, счётчики). Графики — +когда появятся числа, которые ими стоит показывать, не раньше. + +Клиент остаётся plain DOM + CSS. `pixi.js` для этого не нужен и в этом горизонте его стоит +убрать из зависимостей. + +Симуляция по-прежнему на сервере: ученики и комнаты могут существовать как данные в Arch ECS, +даже если клиент рисует строки списка, а не человечков. + +Типы комнат, предметов и действий описываются **defs** (JSONC), конкретная школа — **картой-инстансом**. +Подробности: [`defs.md`](defs.md). Дерево в UI — это территория → здания → этажи → помещения. + +## Что уже зафиксировано + +| Тема | Решение | +| --- | --- | +| Вид внутри школы | Менеджер: дерево + общие события + содержимое локации | +| Рендер | DOM, не WebGL/Pixi | +| Картинки | Нет в этом горизонте | +| Анимация | Переходы и живые цифры | +| Часы, пауза, скорость | Остаются; это не отменяется панелями | +| Логика игры | Только сервер; клиент шлёт намерения и рисует то, что сказали | +| Данные игры | Defs (типы) + карта (инстанс), см. [`defs.md`](defs.md) | +| Дерево карты | Территория → здание → этаж → помещение; рёбра проходов — граф инстанса | +| Редактор карты | Только при создании школы | +| Моды | Папки на сервере, выбор в диалоге создания | +| Поток школы | Цель: один поток и один ECS world на школу, см. [`runtime.md`](runtime.md) | + +## Ещё не решено + +Эти вопросы закрывают игровой срез. Карту и моды больше не блокируют. + +1. Что такое «общее событие»: лог за смену, только текущие процессы, или новости («урок начался»)? +2. Что видно на выбранной локации сначала: предметы (в т.ч. пусто), заготовки действий, вакансии? +3. Выбор узла в дереве — только фильтр на клиенте или намерение на сервер? + +Открытые вопросы по core-моду, двору и коллизиям имён — в конце [`defs.md`](defs.md). + +## Заведомо не сейчас + +- Портреты, арт локаций, схема этажа в пикселях. +- Управление персонажем от первого/третьего лица. +- Победа/кампания, пока нет даже наблюдаемого мира. diff --git a/docs/design/runtime.md b/docs/design/runtime.md new file mode 100644 index 0000000..a7b2068 --- /dev/null +++ b/docs/design/runtime.md @@ -0,0 +1,61 @@ +# Рантайм школы: поток и ECS + +Договорённость на ближайшее планирование. Сейчас в коде **не так**: один `GameLoopService` +тикает все школы на одном потоке, и это записано в `AGENTS.md` как инвариант. Ниже — целевая +модель. Менять инвариант в рабочих соглашениях имеет смысл только вместе с кодом. + +Экран и defs: [`near-term.md`](near-term.md), [`defs.md`](defs.md). + +## Что уже почти есть + +У каждой `School` уже свой Arch `World`. Это оставляем и делаем жёстким правилом: мир не +разделяется между школами и не отдаётся чужому потоку. Arch не потокобезопасен. + +## Целевая модель + +Каждая школа — **актор**: свой поток (или `TaskCreationOptions.LongRunning`, что для нас то же +самое: выделенный поток, не пул тиков), свой `World`, свои часы, свой **замороженный** каталог +def (набор модов, выбранный при создании). + +Над ними — тонкий супервизор (сегодняшняя роль `GameLoopService` + `SchoolRegistry`): + +- создать / удалить школу, знать лимит; +- держать ящики: `id →` очередь команд этой школы; +- собирать **снимки** для `GET /api/schools` (работник публикует неизменяемое состояние, как + сейчас цикл публикует `SchoolsState`); +- маршрутизировать сокет: `OpenSchool` / `SetRunning` / кадры часов — только в ящик той школы. + +Супервизор **не** вызывает `World`, не тикает часы, не читает defs инстанса. Работник **не** +трогает чужой мир и не ходит в ASP.NET. + +Тик по-прежнему фиксированный (`SimulationOptions.FixedDeltaTime`), у каждого работника свой +таймер. Школы не синхронизируют календарь друг с другом — так и задумано. + +Пока школ максимум шесть, шесть потоков — нормально. Не ставить тик симуляции на `ThreadPool`: +под нагрузкой его заберут запросы и часы начнут плыть. + +## Зачем ломать текущий цикл + +Один поток на все школы дешевле и проще (нет гонок между мирами). Отдельный поток нужен, когда +школы перестанут быть «только часы»: моды, карта, ECS. Тогда одна тяжёлая школа не должна +останавливать остальные, и набор модов у школы A не должен быть глобальным синглтоном процесса. + +Каталог def **на школу**, не на процесс: при создании выбирают моды, работник грузит папки и +больше их не перечитывает. Иначе правка JSON на диске внезапно меняет уже идущую школу. + +## Как это стыкуется с сокетом и меню + +Как сейчас по смыслу, другое только «кто трогает School»: + +- входящее с HTTP/WS → команда в ящик школы (или супервизору, если это create/delete); +- меню читает опубликованный снимок, без блокировки работника; +- исходящие кадры часов — из работника в outbox соединения, не наоборот. + +Инвариант вместо «только поток цикла трогает registry» становится: **только работник школы +трогает её `School` / `World` / каталог; супервизор трогает только таблицу ящиков.** + +## Пока не делаем + +- Потоки на системы внутри одной школы (job system как у RimWorld) — рано. +- Миграция живой школы на другой набор модов. +- Правка `AGENTS.md` до тех пор, пока цикл в коде ещё общий. diff --git a/docs/phases/00-drop-pixi.md b/docs/phases/00-drop-pixi.md new file mode 100644 index 0000000..c9cbe5a --- /dev/null +++ b/docs/phases/00-drop-pixi.md @@ -0,0 +1,21 @@ +# Фаза 0. Убрать PixiJS + +## Зависимости + +Нет. + +## Зачем + +`pixi.js` стоит в клиенте «для вида внутри школы». Ближайший вид — DOM-менеджер, не сцена. +Держать пакет без импорта нельзя: это противоречит бюджету зависимостей в `AGENTS.md`. + +## Задачи + +- [ ] Удалить `pixi.js` из `src/HSchool.Client/package.json` и обновить lockfile +- [ ] Убрать обещание Pixi из `README.md` и `AGENTS.md` (клиент: Vite + TypeScript + DOM) + +## Критерий готовности + +- `npm --prefix src/HSchool.Client run build` проходит +- в клиентском коде нет импорта `pixi.js` +- в рабочих соглашениях больше нет формулировки «Pixi стоит на будущий вид» diff --git a/docs/phases/01-manager-shell.md b/docs/phases/01-manager-shell.md new file mode 100644 index 0000000..8798fb6 --- /dev/null +++ b/docs/phases/01-manager-shell.md @@ -0,0 +1,39 @@ +# Фаза 1. Оболочка менеджера на экране школы + +## Зависимости + +- [Фаза 0](00-drop-pixi.md) — не обязательно технически, но логично закрыть первой + +## Зачем + +Сейчас внутри школы только часы и кнопки скорости. Дальше туда поедут списки, а не холст. +Нужен каркас панелей, чтобы следующий срез (локации, события) садился в готовые места, а не +ломал вёрстку часов. + +Пока нет ответа, **что** лежит в дереве и в ленте, фаза рисует пустые панели с подписями и +сохраняет часы/паузу/скорость. Это не игра, это экран, в который игра въедет. + +## Советы по укладке (не задачи, пока не закрыт дизайн) + +- Выбор узла дерева на этом шаге можно держать **на клиенте**: сервер ещё не шлёт карту. + Когда появится симуляция локаций, решить отдельно, фильтр это или `OpenLocation`. +- Не слать дерево 20 раз в секунду. Часы уже едут по сокету; списки и события — когда меняются + (отдельные кадры или редкий снимок). Иначе меню-карточки повторятся внутри школы. +- Строки UI — через `t(...)`, как остальной клиент. + +## Задачи + +- [ ] На экране школы три области: дерево карты, общие события, выбранная локация — плюс текущие часы и управление временем +- [ ] Пока нет данных с сервера, панели показывают пустое состояние (не ломаются) +- [ ] Переключение языка обновляет подписи панелей + +## Критерий готовности + +- Можно открыть школу и увидеть каркас менеджера рядом с работающими часами +- Нет регрессии паузы и скоростей +- Клиентские тесты и `npm --prefix src/HSchool.Client run build` проходят + +## Стоп + +Не наполнять панели выдуманными учениками и комнатами. Наполнение идёт из defs и карты +([`../design/defs.md`](../design/defs.md)), когда закроются открытые вопросы там. diff --git a/docs/phases/README.md b/docs/phases/README.md new file mode 100644 index 0000000..c9212eb --- /dev/null +++ b/docs/phases/README.md @@ -0,0 +1,15 @@ +# Фазы ближайшего горизонта + +Индекс. Статус: ⬜ не начата, 🔄 в работе, ✅ готова. + +Дизайн игрока: [`../design/near-term.md`](../design/near-term.md). +Defs и карта: [`../design/defs.md`](../design/defs.md). +Потоки: [`../design/runtime.md`](../design/runtime.md). + +| Фаза | Статус | Зачем | +| --- | --- | --- | +| [0. Убрать PixiJS](00-drop-pixi.md) | ⬜ | Зависимость была под сцену, которой не будет | +| [1. Оболочка менеджера](01-manager-shell.md) | ⬜ | Каркас экрана школы под дерево / события / локацию | +| Работник на школу (поток + свой World) | — | Ломает текущий общий цикл; делать до тяжёлой симуляции | +| Каталог def + ванильная карта | — | Загрузчик JSONC, локали мода, связность карты | +| Моды и редактор при создании | — | Список `mods/`, выбор, раскладка в POST create |