diff --git a/docs/design/defs.md b/docs/design/defs.md index 9dd6360..b4c0317 100644 --- a/docs/design/defs.md +++ b/docs/design/defs.md @@ -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//`. Ядро — тоже мод (всегда включён или -выбирается отдельно, см. открытые вопросы), лежит рядом, чтобы путь загрузки был один. +Моды — подпапки на диске сервера, например `mods//`. Ядро — мод `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//` на диске сервера, выбор в UI при создании; каталог замораживается | +| Моды | `mods//`; `core` всегда первый и не снимается; остальные — UI create | +| Коллизии имён | Последний пакет победил (defs и локали), warning в лог | +| Наследование | `parent`, `abstract`; поле ребёнка заменяет, не мержит массивы | +| Патчи | `patches/` после наследования; ops: add / replace / remove; неизвестный op — ошибка | +| Территория | TerritoryDef: корень дерева и ходибелый двор | +| Панель узла | Одни секции у двора, корпуса, этажа, комнаты | +| Язык | `?lang=` и Hello; не Accept-Language | | Локализация модов | `mods//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) | Не в этом срезе | diff --git a/docs/design/near-term.md b/docs/design/near-term.md index efab940..2cc537c 100644 --- a/docs/design/near-term.md +++ b/docs/design/near-term.md @@ -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). diff --git a/docs/design/projects.md b/docs/design/projects.md new file mode 100644 index 0000000..f78ad19 --- /dev/null +++ b/docs/design/projects.md @@ -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). diff --git a/docs/design/runtime.md b/docs/design/runtime.md index a7b2068..278f63d 100644 --- a/docs/design/runtime.md +++ b/docs/design/runtime.md @@ -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` до тех пор, пока цикл в коде ещё общий. +- Запись сейва на каждом тике. diff --git a/docs/phases/01-manager-shell.md b/docs/phases/01-manager-shell.md index 8798fb6..9eebfc9 100644 --- a/docs/phases/01-manager-shell.md +++ b/docs/phases/01-manager-shell.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 и карты -([`../design/defs.md`](../design/defs.md)), когда закроются открытые вопросы там. +Не наполнять персонажами и событиями. Данные — с фазы 4. diff --git a/docs/phases/02-school-worker.md b/docs/phases/02-school-worker.md new file mode 100644 index 0000000..fde17cc --- /dev/null +++ b/docs/phases/02-school-worker.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 в этой фазе. Не писать файл каждый тик. diff --git a/docs/phases/03-defs-map.md b/docs/phases/03-defs-map.md new file mode 100644 index 0000000..248a976 --- /dev/null +++ b/docs/phases/03-defs-map.md @@ -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. diff --git a/docs/phases/04-create-editor.md b/docs/phases/04-create-editor.md new file mode 100644 index 0000000..f1f6e0e --- /dev/null +++ b/docs/phases/04-create-editor.md @@ -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` на сервер. diff --git a/docs/phases/README.md b/docs/phases/README.md index c9212eb..50b8c25 100644 --- a/docs/phases/README.md +++ b/docs/phases/README.md @@ -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, снимок при открытии |