Enhance school creation functionality by introducing support for name sets in the API and UI. Update the catalog to include skills, traits, body attributes, needs, and name sets, improving character generation capabilities. Revise localization strings for better user guidance and update tests to validate the new name set functionality and ensure robustness in school creation processes.
This commit is contained in:
@@ -0,0 +1,335 @@
|
||||
# Люди: генерация, характеристики, просмотр
|
||||
|
||||
Договорённость на следующий срез, не текущий код.
|
||||
Типы и карта: [`defs.md`](defs.md). Экран игрока: [`near-term.md`](near-term.md).
|
||||
Потоки: [`runtime.md`](runtime.md). Проекты: [`projects.md`](projects.md).
|
||||
|
||||
Школа перестаёт быть пустой коробкой. Появляются ученики, работники и родители: у каждого имя,
|
||||
тело, навыки, черты и нужды. **Ничего из этого пока ни на что не влияет** — люди не ходят, не
|
||||
работают и не учатся. Срез отвечает на вопрос «кто в этой школе», а не «что они делают».
|
||||
|
||||
Тот же принцип, что и с картой: **код — системы, содержание — данные**. Новый навык, черта, цвет
|
||||
волос или набор имён появляются файлом в `mods/<id>/defs/`, без пересборки.
|
||||
|
||||
## Откуда берётся состав
|
||||
|
||||
Карта уже знает вместимость. `ThingDef.PupilSlots × count` даёт ученические места в комнате,
|
||||
`RoomDef.positions` — должности, которые комната открывает. Отдельного «числа учеников» в форме
|
||||
создания **нет**: состав школы — следствие того, что построено.
|
||||
|
||||
| Что построено | Что появляется |
|
||||
| --- | --- |
|
||||
| Кабинет с 16 партами | Класс на 16 учеников |
|
||||
| Кабинет директора | Должность директора и один работник |
|
||||
| Спортзал | Должность физрука |
|
||||
| Библиотека | Должность библиотекаря |
|
||||
|
||||
**Один кабинет — один класс.** Кабинет закреплён за классом, как в обычной школе: класс живёт в
|
||||
своём кабинете, и вместимость кабинета есть размер класса. Хотите одиннадцать параллелей —
|
||||
поставьте одиннадцать кабинетов. Это делает редактор карты осмысленным: игрок строит не декорации,
|
||||
а штатное расписание.
|
||||
|
||||
**Ванильная карта дорабатывается до одиннадцати кабинетов**, по одному на параллель, чтобы школа
|
||||
из коробки была полной. Сейчас их четыре.
|
||||
|
||||
### Класс — сущность ростера, а не поле карты
|
||||
|
||||
Параллель и литера — именно поля, а не разбор подписи комнаты. Подпись разбирать нельзя дважды:
|
||||
кириллическая «А» и латинская «A» на глаз неразличимы (в нынешней ванильной карте кабинеты
|
||||
подписаны латиницей — `1A`, `2B`), а в другой локали литеры вообще другие.
|
||||
|
||||
Но и в **карте** этим полям не место. Параллель растёт каждый год: 5Б становится 6Б, не переезжая
|
||||
из кабинета. Раскладка же после создания школы неизменна — иначе файл карты пришлось бы
|
||||
переписывать каждое игровое 1 сентября.
|
||||
|
||||
Поэтому класс — отдельная сущность ростера: параллель, литера, закреплённый кабинет. Кабинет
|
||||
закреплён за классом на всё время его жизни; одиннадцатый выпускается — кабинет достаётся новому
|
||||
первому. Раскладку по параллелям при создании делает генератор, игрок её не задаёт.
|
||||
|
||||
Из этого следует, что подписи кабинетов в ванильной карте — **номера помещений** («101», «204»),
|
||||
как в настоящей школе, а не имена классов. Имя класса живёт на классе и меняется вместе с ним.
|
||||
|
||||
## Из чего состоит человек
|
||||
|
||||
Шесть слоёв. Первые два есть у всех, остальные — по роли.
|
||||
|
||||
| Слой | Что это | Пример |
|
||||
| --- | --- | --- |
|
||||
| **Личность** | ФИО со склонениями, пол, дата рождения, роль | Иванова Мария Петровна, ж, 2001-03-14 |
|
||||
| **Тело** | Рост, вес, цвет волос, цвет глаз, телосложение | 164 см, 52 кг, русые, серые, худощавое |
|
||||
| **Навыки** | Числовые шкалы | Математика 62, Ловкость 31 |
|
||||
| **Черты** | Дискретные, с весами и несовместимостями | Усидчивый, Задира |
|
||||
| **Нужды** | Числовые шкалы, убывают со временем | Сон 0.7, Голод 0.4 |
|
||||
| **Связи** | Семья: родители, дети, братья и сёстры | дочь Иванова П.С. и Ивановой О.М. |
|
||||
|
||||
Роли три: **ученик**, **работник**, **родитель**. Роль — не def: код различает их по существу,
|
||||
а не по данным. Должность работника — `PositionDef`, он уже есть.
|
||||
|
||||
Один человек может быть в двух ролях сразу: учительница математики, чей сын учится в седьмом
|
||||
классе, — это **один** человек с ролью работника и связью «родитель» к ученику. Отдельной сущности
|
||||
«родитель Иванова» для неё не заводится.
|
||||
|
||||
## Родители
|
||||
|
||||
Родители — **полноценные сущности** в мире школы, но **без места на карте**. Они существуют,
|
||||
потому что у ученика есть семья, а не потому что ходят по коридорам.
|
||||
|
||||
Это задел, а не упрощение: позже родителя можно будет вызвать в школу — тогда у него появится
|
||||
местоположение и цель, но не появится новый тип сущности. Поэтому родитель с самого начала лежит
|
||||
в `World` наравне с учеником, а не отдельным списком «карточек при ученике».
|
||||
|
||||
## Генерация семьями
|
||||
|
||||
Люди генерируются **не по одному**. Единица генерации — семья:
|
||||
|
||||
```
|
||||
Семья Ивановых
|
||||
├── Иванов Пётр Сергеевич родитель, 41 год
|
||||
├── Иванова Ольга Михайловна родитель, 39 лет
|
||||
├── Иванова Мария Петровна ученица, 7Б
|
||||
└── Иванов Кирилл Петрович ученик, 3А
|
||||
```
|
||||
|
||||
Что это даёт бесплатно и чего иначе не получить:
|
||||
|
||||
- **Фамилии повторяются.** Школа без братьев и сестёр читается как список случайных строк.
|
||||
- **Отчества сходятся.** Отчество ребёнка — от имени отца, а не от отдельного словаря.
|
||||
- **Родовые формы согласованы.** Иванов / Иванова в одной семье, а не вразнобой.
|
||||
- **Работник может быть родителем ученика** — фактом семьи, без специального правила.
|
||||
|
||||
Генератор идёт сверху вниз: сначала решает, сколько семей нужно, чтобы заполнить места и
|
||||
должности; потом строит взрослых; потом детей нужных возрастов; и только затем раскладывает детей
|
||||
по классам, а взрослых — по должностям. Обратный порядок («сгенерировать 400 учеников, потом
|
||||
приписать им родителей») не даёт ни братьев, ни учителей-родителей.
|
||||
|
||||
## Дефы
|
||||
|
||||
Пять новых видов. Ссылки — строками `defName`, как везде.
|
||||
|
||||
1. **SkillDef** — навык. Диапазон, распределение при генерации, зависимость от возраста,
|
||||
ограничения со стороны тела (см. ниже).
|
||||
2. **TraitDef** — черта. Вес (частота), список несовместимых черт, ограничения по роли и возрасту,
|
||||
модификаторы навыков.
|
||||
3. **BodyAttributeDef** — свойство тела. Либо число с распределением (рост, вес), либо выбор из
|
||||
списка с весами (цвет волос, цвет глаз). Распределение зависит от пола и возраста.
|
||||
4. **NeedDef** — нужда. Начальное значение и скорость убывания.
|
||||
5. **NameSetDef** — набор имён (см. отдельный раздел).
|
||||
|
||||
Телосложение — не отдельный def, а **производное**: считается из роста и веса кодом. Данные не
|
||||
должны дублировать то, что выводится.
|
||||
|
||||
### Что кладём в `core`
|
||||
|
||||
| Вид | Ванильное содержание |
|
||||
| --- | --- |
|
||||
| Навыки | Школьные предметы (математика, русский, литература, физика, история, физкультура…) и физические (ловкость, сила, выносливость) |
|
||||
| Черты | Около десяти для начала: усидчивый, рассеянный, задира, тихоня, лидер, лентяй, любопытный, вспыльчивый, добрый, аккуратный |
|
||||
| Тело | Рост, вес, цвет волос, цвет глаз |
|
||||
| Нужды | Сон, голод, туалет, общение |
|
||||
|
||||
Слои одинаковы у всех ролей. У неработающего родителя те же навыки и черты, что у ученика: урезать
|
||||
их дешевле по памяти и дороже в коде — иначе каждое место, читающее навык, начинается с вопроса
|
||||
«а есть ли он у этого человека».
|
||||
|
||||
### Как тело ограничивает навыки
|
||||
|
||||
«Толстый не может быть ловким» — правило **генерации**, а не игровой эффект: характеристики пока
|
||||
ни на что не влияют, но между собой согласованы.
|
||||
|
||||
Ограничение живёт в `SkillDef` и ссылается на свойство тела:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"defName": "Agility",
|
||||
"range": { "min": 0, "max": 100 },
|
||||
"bodyLimits": [
|
||||
{ "attribute": "Build", "value": "Obese", "max": 25 },
|
||||
{ "attribute": "Build", "value": "Heavy", "max": 45 },
|
||||
{ "attribute": "Build", "value": "Athletic", "min": 40 },
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
Генератор сначала бросает тело, потом навык, потом применяет ограничения как зажим. Порядок важен
|
||||
и фиксируется: **тело первично**, навык подстраивается. Иначе один и тот же сид даст разных людей
|
||||
в зависимости от порядка перебора.
|
||||
|
||||
Ограничения — не только вниз: «атлет не может быть совсем неловким» так же осмысленно, поэтому
|
||||
`min` есть наравне с `max`.
|
||||
|
||||
## Нужды
|
||||
|
||||
Четыре нужды в `core`: **сон, голод, туалет, общение**. `NeedDef` описывает шкалу и скорость
|
||||
убывания. Машинерия работает с этого среза: значение лежит на человеке, тикает вместе со школой,
|
||||
показывается в карточке.
|
||||
|
||||
**Но в `core` скорость убывания равна нулю.** Есть тела, которые тратят, и нет ничего, что
|
||||
восполняет: некому поесть, негде поспать. Ненулевая скорость в этом срезе означала бы школу, где
|
||||
через игровые сутки все нужды на нуле, — и это выглядело бы поломкой, а не игрой.
|
||||
|
||||
Скорость — данные, не код. Когда появится еда и сон, цифра меняется в JSONC, а не в C#.
|
||||
|
||||
## Имена
|
||||
|
||||
`NameSetDef` — набор имён целиком: мужские имена, женские имена, фамилии, правила отчеств и
|
||||
склонений. В `core` один набор — славянский. Другие наборы приходят модами.
|
||||
|
||||
**Один набор на школу.** Выбирается в диалоге создания рядом с модами и сохраняется в файле школы.
|
||||
Смеси с весами («80% славянских, 20% прочих») — не в этом срезе: они требуют UI для весов.
|
||||
|
||||
Набор имён **не связан с языком интерфейса**. Английский UI не превращает Иванову в Ivanova:
|
||||
язык интерфейса — про подписи кнопок, набор имён — про то, кто учится в школе.
|
||||
|
||||
### Склонения
|
||||
|
||||
Русскому тексту нужны падежи: «вызвать **Иванову Марию**», «дневник **Ивановой Марии**». Шесть
|
||||
падежей на каждую часть ФИО.
|
||||
|
||||
Два способа, и нужны оба:
|
||||
|
||||
- **Правило** — по умолчанию. Код знает небольшой набор моделей склонения (`-ов/-ова`, `-ий/-ая`,
|
||||
`-а/-я`, несклоняемые). В данных — только имя и id модели.
|
||||
- **Явная таблица** — когда правило не работает. Любая запись может задать все шесть форм руками.
|
||||
|
||||
Это тот же принцип, что и везде: код знает *как*, данные говорят *что*. Словарь из шести форм на
|
||||
каждое из тысячи имён никто не выдержит, а одними правилами русские фамилии не покрываются.
|
||||
|
||||
Падежи нужны заранее, до появления ленты событий: набор имён с одной формой потом придётся
|
||||
переписывать целиком, а он самый объёмный файл в `core`.
|
||||
|
||||
## Отдельная библиотека
|
||||
|
||||
Новый проект **`HSchool.People`**.
|
||||
|
||||
```
|
||||
Protocol ← Server → Simulation → People → Content
|
||||
```
|
||||
|
||||
- Зависит от `HSchool.Content` (нужны defs и каталог).
|
||||
- **Не** зависит от Arch, ASP.NET и сокетов — как `Content` сейчас.
|
||||
- Отдаёт простые записи. Превращает их в сущности `World` уже `HSchool.Simulation`.
|
||||
|
||||
Так генератор тестируется без мира и без хоста: «этот сид + эта карта + этот набор имён → ровно
|
||||
эти люди». Это единственный способ поймать регрессию в генераторе — глазами такое не проверяется.
|
||||
|
||||
### Детерминизм
|
||||
|
||||
Генерация детерминирована от **сида**, сид лежит в файле школы. Один сид + одна карта + один набор
|
||||
имён = одна и та же школа, всегда.
|
||||
|
||||
Сид у каждой семьи свой, выведенный из школьного: добавление тринадцатой семьи не должно менять
|
||||
первые двенадцать. Иначе тест на генератор ломается от любой правки порядка.
|
||||
|
||||
## Годовой набор
|
||||
|
||||
Состав живёт: 1 сентября классы переходят в следующую параллель, одиннадцатый выпускается, его
|
||||
кабинет освобождается под новый первый.
|
||||
|
||||
Вместимость при этом постоянна — она задана картой. Набор не увеличивает школу, а **замещает**
|
||||
выпуск. Школа растёт только стройкой.
|
||||
|
||||
Выпускается **старшая имеющаяся** параллель: школа из четырёх кабинетов не должна ждать
|
||||
одиннадцатого класса, чтобы кого-то выпустить.
|
||||
|
||||
Выпускники уходят из ростера. Их родители уходят следом, но только если в школе не осталось
|
||||
других их детей и сами они в ней не работают, — иначе ростер копил бы людей, ни с чем не
|
||||
связанных, до бесконечности.
|
||||
|
||||
Школа создаётся посреди учебного года (по умолчанию 3 апреля), поэтому при создании классы уже
|
||||
сформированы и возраст ученика соответствует его параллели: в N-й класс ходят те, кому
|
||||
исполнилось N+6 до 1 сентября.
|
||||
|
||||
## Просмотр
|
||||
|
||||
Не отдельный экран. Список людей — **панель в оболочке менеджера**, рядом с картой и событиями:
|
||||
уходя в отдельный экран, игрок теряет из виду часы, локацию и события, а смысл этой оболочки
|
||||
ровно в том, чтобы всё было на одном экране. Ориентир — прогрессивный QSP: текст, списки и ссылки,
|
||||
без сцены.
|
||||
|
||||
Панель «Люди» занимает нижний ряд во всю ширину — фильтрам, колонкам и пейджеру нужна ширина,
|
||||
которой нет в колонке шириной в треть экрана.
|
||||
|
||||
- **Фильтры**: роль (ученики / работники / родители), параллель и литера, должность, пол,
|
||||
возрастной диапазон.
|
||||
- **Сортировки**: фамилия, возраст, параллель, должность.
|
||||
- **Пейджинг** обязателен: тысяча учеников — это три тысячи человек с семьями.
|
||||
- **Строка списка** — только лёгкое: ФИО, роль, класс или должность, возраст, пол.
|
||||
- **Карточка** одного человека — тело, навыки, черты, нужды, семья со ссылками на родных.
|
||||
|
||||
## Масштаб
|
||||
|
||||
Школа на 1000 учеников — это около **3000 человек** вместе с родителями, и до шести таких школ
|
||||
живут одновременно. Три решения следуют отсюда напрямую.
|
||||
|
||||
**Ростер не ходит в снимке карты.** Снимок при открытии школы остаётся про помещения. Люди —
|
||||
отдельный путь, иначе открытие школы означало бы мегабайт по сокету.
|
||||
|
||||
**Список — HTTP, а не сокет.** Запрос-ответ с фильтром, сортировкой и страницей — это REST по
|
||||
природе, как и меню. Воркер публикует неизменяемый снимок ростера (он меняется редко: создание,
|
||||
набор, наём), обработчик читает опубликованный снимок и не трогает воркер — инвариант 3 цел.
|
||||
|
||||
**Живые значения — из воркера.** Нужды тикают, поэтому карточка одного человека запрашивается у
|
||||
воркера через мейлбокс и `TaskCompletionSource`, как create и delete. Один человек — дешёвый
|
||||
запрос; три тысячи людей двадцать раз в секунду — нет.
|
||||
|
||||
**Ростер — отдельный файл.** `saves/{id}.people.json` пишется только когда состав изменился;
|
||||
`saves/{id}.json` остаётся маленьким и частым. Сейчас файл школы переписывается каждые 30 секунд —
|
||||
таскать через это мегабайт ростера, который меняется раз в игровой год, незачем.
|
||||
|
||||
## Советы, которые стоит принять сразу
|
||||
|
||||
- Не делать «родителя» отдельным видом сущности. Иначе учительница-мать станет двумя людьми.
|
||||
- Не хранить телосложение в данных — оно выводится из роста и веса.
|
||||
- Не начинать с падежных таблиц на каждое имя: правило по умолчанию, таблица как исключение.
|
||||
- Не отправлять черты и навыки в списке — только в карточке. Список читают глазами, карточку
|
||||
открывают по одному человеку.
|
||||
- Сид на семью, а не один на школу: иначе любая правка генератора переставит всех.
|
||||
- Ненулевую скорость нужд не включать, пока нечем их восполнять.
|
||||
|
||||
## Заведомо не сейчас
|
||||
|
||||
- Поведение: расписание, уроки, перемещение по карте, исполнение действий.
|
||||
- Влияние характеристик на что-либо. Черты и навыки в этом срезе — текст в карточке.
|
||||
- Вызов родителя в школу. Задел есть (родитель — сущность), реализации нет.
|
||||
- Наём и увольнение вручную. Должности закрываются генератором.
|
||||
- Смеси наборов имён с весами.
|
||||
- Новые роли из модов: ученик, работник, родитель — код, не данные.
|
||||
- Портреты и любой арт.
|
||||
|
||||
## Зафиксировано этим разговором
|
||||
|
||||
| Тема | Решение |
|
||||
| --- | --- |
|
||||
| Размер школы | Из карты: `PupilSlots` — ученики, `RoomDef.positions` — штат |
|
||||
| Класс | Один кабинет = один класс; класс — сущность ростера, не поле карты |
|
||||
| Параллель и литера | Поля класса, а не разбор подписи комнаты; раскладывает генератор |
|
||||
| Подписи кабинетов | Номера помещений («204»), имя класса живёт на классе |
|
||||
| Ванильная карта | Дорабатывается до 11 кабинетов — по одному на параллель |
|
||||
| Когда генерируются | Полный состав при создании, затем набор каждое 1 сентября |
|
||||
| Годовой набор | Замещает выпуск; вместимость меняет только стройка |
|
||||
| Кого выпускать | Старшую имеющуюся параллель, а не обязательно одиннадцатую |
|
||||
| Уход из ростера | Выпускники уходят; родители — если не осталось детей и они здесь не работают |
|
||||
| Роли | Ученик, работник, родитель; роль — код, должность — `PositionDef` |
|
||||
| Родители | Сущности в `World`, без места на карте; задел на вызов в школу |
|
||||
| Совмещение ролей | Один человек может быть работником и родителем ученика |
|
||||
| Единица генерации | Семья, а не человек |
|
||||
| Слои человека | Личность, тело, навыки, черты, нужды, связи |
|
||||
| Новые defs | `SkillDef`, `TraitDef`, `BodyAttributeDef`, `NeedDef`, `NameSetDef` |
|
||||
| Телосложение | Производное от роста и веса, не def |
|
||||
| Тело и навыки | `SkillDef.bodyLimits` зажимают навык; тело бросается первым |
|
||||
| Влияние характеристик | Никакого; только просмотр |
|
||||
| Навыки в `core` | Школьные предметы + физические |
|
||||
| Черты в `core` | Около десяти |
|
||||
| Нужды в `core` | Сон, голод, туалет, общение |
|
||||
| Нужды | Машинерия есть, скорость убывания в `core` — ноль |
|
||||
| Слои по ролям | Одинаковые у всех; родителей не урезаем |
|
||||
| Набор имён | Один на школу, выбор в create, в `core` славянский; прочие — моды |
|
||||
| Падежи | Правило по умолчанию (код), явная таблица как исключение (данные) |
|
||||
| Имена и язык UI | Независимы |
|
||||
| Библиотека | `HSchool.People`: зависит от Content, не знает Arch и ASP.NET |
|
||||
| Детерминизм | Сид в сейве, свой сид на семью |
|
||||
| Просмотр | Панель в оболочке менеджера, нижний ряд; не отдельный экран |
|
||||
| Список | Фильтры, сортировки, пейджинг; в строке — только лёгкие поля |
|
||||
| Транспорт списка | HTTP по опубликованному снимку ростера |
|
||||
| Транспорт карточки | Запрос в воркер, потому что нужды живые |
|
||||
| Сохранение | `saves/{id}.people.json`, пишется при изменении состава |
|
||||
@@ -1,6 +1,6 @@
|
||||
# Нарезка проектов (целевая)
|
||||
|
||||
Сейчас в коде: `Protocol ← Server → Simulation → Content`. Каталог не в Simulation (рядом с Arch)
|
||||
Сейчас в коде: `Protocol ← Server → Simulation → People → Content`. Каталог не в Simulation (рядом с Arch)
|
||||
и не в Server (рядом с Kestrel) — парсер JSONC и проверка графа не знают, что такое tick.
|
||||
|
||||
## Зависимости
|
||||
@@ -8,7 +8,8 @@
|
||||
```
|
||||
Protocol — ни на кого из игровых проектов (только байты)
|
||||
Content — ни на Protocol, ни на Simulation, ни на ASP.NET
|
||||
Simulation → Content (каталог и раскладка, без файловых путей хоста)
|
||||
People → Content (defs, карта, склонения; без Arch и хоста)
|
||||
Simulation → Content (каталог и раскладка; People — в фазе 7)
|
||||
Server → Protocol, Simulation, Content
|
||||
Client — своя сторона Protocol (TS) + HTTP
|
||||
```
|
||||
@@ -19,7 +20,8 @@ Client — своя сторона Protocol (TS) + HTTP
|
||||
|
||||
| Проект | Да | Нет |
|
||||
| --- | --- | --- |
|
||||
| **HSchool.Content** (новый) | Типы def, JSONC-загрузчик, наследование, патчи, слияние локалей, граф карты, валидация связности | Arch, часы, HTTP, потоки, пути `mods/` с диска хоста |
|
||||
| **HSchool.Content** | Типы def, JSONC-загрузчик, наследование, патчи, слияние локалей, граф карты, валидация связности | Arch, часы, HTTP, потоки, пути `mods/` с диска хоста |
|
||||
| **HSchool.People** | Генерация ростера: семьи, классы, тело/навыки/черты из каталога и сида | Arch, ASP.NET, сокеты, `DateTime.Now`, файлы сейва |
|
||||
| **HSchool.Simulation** | `School`, `GameClock`, Arch `World`, тик, применение раскладки к миру | Kestrel, сокеты, `Directory.Enumerate`, сейв-файлы |
|
||||
| **HSchool.Server** | Хост, супервизор, работник-поток, `mods/` и `saves/` из конфига, HTTP/WS, DTO запросов | Правила «комната должна быть связана», схема Chair |
|
||||
| **HSchool.Protocol** | Кадры сокета, в том числе снимок карты и locale в Hello | Имена комнат, JSONC |
|
||||
@@ -46,6 +48,7 @@ Arch и фиксированный тик — про живую школу. Па
|
||||
## Тесты
|
||||
|
||||
- `tests/HSchool.Content.Tests` — фикстуры JSONC, коллизии имён, дырявый граф, двор обязателен.
|
||||
- `tests/HSchool.People.Tests` — тот же сид даёт тот же ростер; места и должности заполнены.
|
||||
- `HSchool.Simulation.Tests` — часы, тик, «школа с таким каталогом живёт».
|
||||
- `HSchool.AppHost.Tests` — create с картой, рестарт, сейв на диске.
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# Фаза 5. Дефы человека
|
||||
|
||||
## Зависимости
|
||||
|
||||
- [Фаза 3](03-defs-map.md)
|
||||
|
||||
## Зачем
|
||||
|
||||
Каталог учится описывать людей. Ни одного человека в этой фазе не появляется — только типы,
|
||||
из которых их потом соберут.
|
||||
|
||||
## Задачи
|
||||
|
||||
- [x] `SkillDef`, `TraitDef`, `BodyAttributeDef`, `NeedDef`, `NameSetDef` в `HSchool.Content`:
|
||||
новые `DefKind`, разбор, наследование и патчи на общих правилах
|
||||
- [x] `BodyAttributeDef` двух видов: число с распределением (рост, вес) и выбор из списка с
|
||||
весами (цвет волос, цвет глаз); распределение зависит от пола и возраста
|
||||
- [x] `SkillDef.bodyLimits` — зажимы навыка по свойству тела (`min` / `max`); ссылка на
|
||||
неизвестный атрибут или его значение — ошибка загрузки, а не тихий пропуск
|
||||
- [x] `TraitDef`: вес, несовместимости, ограничения по роли и возрасту, модификаторы навыков;
|
||||
односторонняя несовместимость считается взаимной
|
||||
- [x] `NameSetDef`: мужские и женские имена, фамилии с родовыми формами, правило отчества,
|
||||
модель склонения по умолчанию или явная таблица шести падежей на запись
|
||||
- [x] Ванильное содержание `core`: навыки (предметы + физические), около десяти черт, четыре
|
||||
нужды со скоростью убывания **ноль**, славянский набор имён
|
||||
- [x] Локализация новых defs в `ru` и `en`
|
||||
- [x] `GET /api/catalog` отдаёт список наборов имён — форме создания нужен выбор
|
||||
- [x] Тесты Content: разбор каждого вида, наследование навыка, патч черты, неизвестная ссылка —
|
||||
ошибка, склонение по правилу и перекрытие таблицей
|
||||
|
||||
## Критерий готовности
|
||||
|
||||
- Каталог `core` грузится, все новые defs проходят валидацию
|
||||
- Мод, добавляющий свой набор имён и пару черт, виден в каталоге
|
||||
- `dotnet test` проходит; школы по-прежнему пустые
|
||||
|
||||
## Стоп
|
||||
|
||||
Не генерировать людей. Не трогать ECS, сервер сверх выдачи каталога и клиент сверх выбора набора.
|
||||
@@ -0,0 +1,42 @@
|
||||
# Фаза 6. Библиотека генерации
|
||||
|
||||
## Зависимости
|
||||
|
||||
- [Фаза 5](05-people-defs.md)
|
||||
|
||||
## Зачем
|
||||
|
||||
Из каталога, карты и сида получаются люди — семьями и детерминированно. Библиотека тестируется
|
||||
без мира, без сокета и без хоста, как `HSchool.Content` сейчас.
|
||||
|
||||
## Задачи
|
||||
|
||||
- [x] Новый проект `HSchool.People` и `tests/HSchool.People.Tests`; ссылка только на
|
||||
`HSchool.Content`, ни Arch, ни ASP.NET
|
||||
- [x] Записи `Person`, `Family`, `SchoolClass`, `Roster` — простые данные, не сущности
|
||||
- [x] Ёмкость из карты: `PupilSlots` по кабинетам и `RoomDef.positions` дают, сколько нужно
|
||||
учеников и каких работников
|
||||
- [x] Генерация семьями: взрослые → дети нужных возрастов → раскладка детей по параллелям и
|
||||
взрослых по должностям
|
||||
- [x] Совмещение ролей: работник может оказаться родителем ученика той же школы
|
||||
- [x] Порядок бросков зафиксирован: тело первым, навыки после, `bodyLimits` применяются зажимом
|
||||
- [x] Склонения: модели в коде, явная таблица из def перекрывает модель
|
||||
- [x] Сид школы приходит снаружи; сид каждой семьи выводится из него
|
||||
|
||||
## Тесты, без которых фаза не закрыта
|
||||
|
||||
- [x] Тот же сид, карта и набор имён — ровно тот же ростер
|
||||
- [x] Тринадцатая семья не меняет первые двенадцать
|
||||
- [x] Фамилии, родовые формы и отчества внутри семьи согласованы
|
||||
- [x] Ученик с телосложением «полное» не получает высокую ловкость
|
||||
- [x] Все ученические места заполнены, все должности закрыты
|
||||
- [x] Школа из одного кабинета и школа из одиннадцати — обе собираются
|
||||
|
||||
## Критерий готовности
|
||||
|
||||
- `HSchool.People.Tests` зелёные
|
||||
- В проекте нет упоминаний сокетов, ECS и `DateTime.Now`
|
||||
|
||||
## Стоп
|
||||
|
||||
Не класть людей в `World`. Не трогать сервер, клиент и формат сейва.
|
||||
@@ -0,0 +1,35 @@
|
||||
# Фаза 7. Люди в школе
|
||||
|
||||
## Зависимости
|
||||
|
||||
- [Фаза 6](06-people-generator.md)
|
||||
- [Фаза 2](02-school-worker.md)
|
||||
|
||||
## Зачем
|
||||
|
||||
Школа создаётся с людьми, они живут в её `World` и переживают перезапуск сервера. Экран пока
|
||||
не меняется — это фаза про данные.
|
||||
|
||||
## Задачи
|
||||
|
||||
- [ ] Ванильная карта дорабатывается до одиннадцати кабинетов; подписи кабинетов становятся
|
||||
номерами помещений («204»), а не именами классов
|
||||
- [ ] Компоненты Arch под слои человека; сущности собираются из записей генератора
|
||||
- [ ] Класс как сущность: параллель, литера, закреплённый кабинет
|
||||
- [ ] Нужды тикают вместе со школой — механика есть, скорость в `core` нулевая
|
||||
- [ ] `saves/{id}.people.json`: сид, люди, семьи, классы. Пишется **только** при изменении
|
||||
состава, не по таймеру; `saves/{id}.json` остаётся маленьким и частым
|
||||
- [ ] Загрузка ростера при старте школы; ростер, не сходящийся с картой, оставляет школу
|
||||
незапущенной и файл нетронутым — как сейчас с картой
|
||||
- [ ] Воркер публикует неизменяемый снимок ростера, как публикует `SchoolState`
|
||||
|
||||
## Критерий готовности
|
||||
|
||||
- Создать школу — все ученические места и должности заняты
|
||||
- Перезапустить сервер — те же люди с теми же именами и характеристиками
|
||||
- Файл ростера не переписывается каждые тридцать секунд
|
||||
- `dotnet test` проходит
|
||||
|
||||
## Стоп
|
||||
|
||||
Ни панели, ни фильтров, ни HTTP списка людей. Никакого поведения: люди существуют и стоят.
|
||||
@@ -0,0 +1,39 @@
|
||||
# Фаза 8. Просмотр людей
|
||||
|
||||
## Зависимости
|
||||
|
||||
- [Фаза 7](07-people-in-school.md)
|
||||
- [Фаза 1](01-manager-shell.md)
|
||||
|
||||
## Зачем
|
||||
|
||||
Игрок видит, кто учится и работает в его школе, не теряя из виду часы, карту и события.
|
||||
|
||||
## Задачи
|
||||
|
||||
- [ ] `GET /api/schools/{id}/people`: фильтры (роль, параллель, должность, пол, диапазон
|
||||
возраста), сортировка, страница. Читает **опубликованный снимок** ростера и не трогает
|
||||
воркер — инвариант «только поток школы касается школы» остаётся целым
|
||||
- [ ] `GET /api/schools/{id}/people/{personId}`: карточка через мейлбокс воркера с
|
||||
`TaskCompletionSource`, потому что нужды живые
|
||||
- [ ] Панель «Люди» нижним рядом оболочки менеджера, во всю ширину: фильтры, сортируемые
|
||||
колонки, пейджер
|
||||
- [ ] Строка списка — только лёгкие поля: ФИО, роль, класс или должность, возраст, пол
|
||||
- [ ] Карточка: тело, навыки, черты, нужды, семья со ссылками на родных — по ссылке открывается
|
||||
карточка родственника
|
||||
- [ ] Строки и подписи через `t(...)`, как всё остальное
|
||||
- [ ] `docs/protocol.md` пополняется в том же коммите, что и обработчики
|
||||
- [ ] Тесты API: фильтр по роли и параллели, сортировка, границы страниц, неизвестная школа,
|
||||
неизвестный человек
|
||||
|
||||
## Критерий готовности
|
||||
|
||||
- Открыть школу, отфильтровать учеников пятой параллели, отсортировать по фамилии
|
||||
- Открыть карточку ученика, увидеть родителей, перейти по ссылке к матери
|
||||
- Тысяча строк не роняет и не подвешивает панель
|
||||
- `dotnet test` и клиентские `npm test` / `build` проходят
|
||||
|
||||
## Стоп
|
||||
|
||||
Не редактировать людей. Не нанимать и не увольнять руками. Не показывать характеристики в списке —
|
||||
только в карточке.
|
||||
@@ -0,0 +1,34 @@
|
||||
# Фаза 9. Годовой набор
|
||||
|
||||
## Зависимости
|
||||
|
||||
- [Фаза 7](07-people-in-school.md)
|
||||
|
||||
## Зачем
|
||||
|
||||
Состав перестаёт быть слепком момента создания. Первого сентября классы переходят в следующую
|
||||
параллель, старший выпускается, приходит новый первый.
|
||||
|
||||
## Задачи
|
||||
|
||||
- [ ] Переход по игровому календарю на 1 сентября: у каждого класса параллель +1
|
||||
- [ ] Выпуск **старшей имеющейся** параллели — школа из четырёх кабинетов не ждёт одиннадцатого
|
||||
класса, чтобы кого-то выпустить
|
||||
- [ ] Освободившийся кабинет достаётся новому первому классу; вместимость школы не меняется —
|
||||
её меняет только стройка
|
||||
- [ ] Набор первого класса тем же генератором и теми же семьями: у нового ученика может
|
||||
оказаться брат или сестра в старшей параллели
|
||||
- [ ] Выпускники уходят из ростера; их родители уходят тоже, если не осталось других детей в
|
||||
школе и они в ней не работают
|
||||
- [ ] Ростер переписывается один раз за переход, снимок публикуется заново — не по тику
|
||||
|
||||
## Критерий готовности
|
||||
|
||||
- Перемотать год на ×4: параллели сдвинулись, прежний выпускной класс исчез, появился новый первый
|
||||
- Вместимость школы до и после перехода совпадает
|
||||
- Перезапуск сервера после перехода поднимает новый состав
|
||||
- Тест на школу без одиннадцатой параллели: выпускается старшая имеющаяся
|
||||
|
||||
## Стоп
|
||||
|
||||
Не второгодники. Не переводы между школами. Не увольнения и наём работников.
|
||||
+21
-1
@@ -1,7 +1,9 @@
|
||||
# Фазы первого среза
|
||||
# Фазы
|
||||
|
||||
Индекс. Статус: ⬜ не начата, 🔄 в работе, ✅ готова.
|
||||
|
||||
## Срез 1. Оболочка и карта
|
||||
|
||||
Дизайн закрыт: [`../design/near-term.md`](../design/near-term.md),
|
||||
[`../design/defs.md`](../design/defs.md),
|
||||
[`../design/runtime.md`](../design/runtime.md),
|
||||
@@ -16,3 +18,21 @@
|
||||
| [2. Работник школы и диск](02-school-worker.md) | ✅ | Поток + World + сейв — основа |
|
||||
| [3. Каталог def и карта](03-defs-map.md) | ✅ | JSONC, core, валидация раскладки |
|
||||
| [4. Моды и редактор в create](04-create-editor.md) | ✅ | Выбор модов, карта в POST, снимок при открытии |
|
||||
|
||||
## Срез 2. Люди
|
||||
|
||||
Дизайн: [`../design/people.md`](../design/people.md).
|
||||
|
||||
Школа наполняется людьми. Ученики, работники и родители существуют, показываются и обновляются
|
||||
раз в год — но **ничего не делают**: ни расписания, ни перемещений, ни влияния характеристик.
|
||||
|
||||
Фазы идут по порядку, кроме 8 и 9: обе зависят только от 7 и могут вестись параллельно
|
||||
(клиент / симуляция).
|
||||
|
||||
| Фаза | Статус | Зачем |
|
||||
| --- | --- | --- |
|
||||
| [5. Дефы человека](05-people-defs.md) | ✅ | Навыки, черты, тело, нужды, наборы имён в каталоге |
|
||||
| [6. Библиотека генерации](06-people-generator.md) | ✅ | `HSchool.People`: семьи из карты и сида |
|
||||
| [7. Люди в школе](07-people-in-school.md) | ⬜ | Сущности в `World`, ростер на диске, 11 кабинетов в `core` |
|
||||
| [8. Просмотр людей](08-people-browser.md) | ⬜ | Панель со списком, фильтрами и карточкой |
|
||||
| [9. Годовой набор](09-yearly-intake.md) | ⬜ | Первое сентября: переход, выпуск, набор |
|
||||
|
||||
+8
-1
@@ -69,6 +69,9 @@ Room slots include a `count` (how many of the default thing the def places). Thi
|
||||
`pupilSlots` — how many pupils that thing hosts for lessons. The create editor copies both onto
|
||||
the map instance; the snapshot's per-node pupil-slot total is computed on the server.
|
||||
|
||||
`nameSets` is the list of placeable name packs (`defName` + label). The create dialog picks one;
|
||||
it is independent of the UI language.
|
||||
|
||||
`lang` is the same value Hello carries — not `Accept-Language`. Anything other than `en` is
|
||||
Russian.
|
||||
|
||||
@@ -81,12 +84,15 @@ Body:
|
||||
"name": "Гимназия №14",
|
||||
"startDate": "2012-04-03T06:00:00Z",
|
||||
"modIds": [],
|
||||
"map": null
|
||||
"map": null,
|
||||
"nameSetId": "Slavic"
|
||||
}
|
||||
```
|
||||
|
||||
`modIds` are extras; the server always prepends `core`. Omit `map` (or send `null`) to use that
|
||||
pack set's default layout. A supplied map is validated as a connected yard-and-rooms graph.
|
||||
`nameSetId` is a `NameSetDef`; omit it to use the first placeable set in the catalog (vanilla:
|
||||
`Slavic`). Unknown ids return `400` `unknown-name-set`.
|
||||
|
||||
| Status | Meaning |
|
||||
| --- | --- |
|
||||
@@ -96,6 +102,7 @@ pack set's default layout. A supplied map is validated as a connected yard-and-r
|
||||
| `400` `invalid-map` | Missing yard, no rooms, unknown def, or a disconnected graph. |
|
||||
| `400` `unknown-mod` | An extra pack id is missing under `mods/`. |
|
||||
| `400` `invalid-catalog` | The selected packs could not be loaded. |
|
||||
| `400` `unknown-name-set` | `nameSetId` is not a placeable `NameSetDef` in those packs. |
|
||||
| `409` `school-limit-reached` | `maxSchools` schools already exist. |
|
||||
|
||||
Failures are RFC 7807 problem details with an extra `code` field — that is what the UI switches on.
|
||||
|
||||
Reference in New Issue
Block a user