# Люди: генерация, характеристики, просмотр Договорённость на следующий срез, не текущий код. Типы и карта: [`defs.md`](../01-shell/defs.md). Экран игрока: [`near-term.md`](../01-shell/near-term.md). Потоки: [`runtime.md`](../01-shell/runtime.md). Проекты: [`projects.md`](../01-shell/projects.md). Школа перестаёт быть пустой коробкой. Появляются ученики, работники и родители: у каждого имя, тело, навыки, черты и нужды. **Ничего из этого пока ни на что не влияет** — люди не ходят, не работают и не учатся. Срез отвечает на вопрос «кто в этой школе», а не «что они делают». Тот же принцип, что и с картой: **код — системы, содержание — данные**. Новый навык, черта, цвет волос или набор имён появляются файлом в `mods//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 учеников, потом приписать им родителей») не даёт ни братьев, ни учителей-родителей. **Места раздаются вперемешку по всей школе, а не подряд.** Карта отдаёт ученические места сгруппированными по кабинетам, и семья, берущая подряд идущие места, получала бы всех детей в один класс — то есть ровесниками, с одним и тем же окном рождения. Порядок мест перемешивается от школьного сида до раздачи. Цена решения названа прямо: набор мест перестаёт быть «дописываемым с конца». Более крупная карта даёт другую раскладку по классам с самого начала, а не только в хвосте. Что остаётся неизменным — **как разыгрывается сама семья**: её сид зависит только от номера, поэтому фамилия, состав и имена семьи №5 одинаковы в школе на четыре кабинета и в школе на пять. Меняется то, в каком классе сидят её дети, и вместе с классом — год рождения. ### Неполные семьи Примерно **8%** семей с детьми живут с одним родителем. Кто именно остался — отец или мать — решается броском, без перевеса в чью-либо сторону. **Причина не моделируется.** Ростер записывает, кто живёт в доме, а не почему. Развод, вдовство и прочее — это события, а событий в этом срезе нет; появится лента — появится и повод завести признак. Ребёнок **в любом случае носит отцовскую фамилию и отчество**. Значит, отсутствующему отцу всё равно разыгрывается имя — оно записывается на семью вместе с фамилией. Читать их с присутствующего родителя нельзя: у матери-одиночки нет мужской формы фамилии, а сыну нужна именно она. Решение «кто остался» берётся из **отдельного потока**, а не из общего с внешностью. В общем потоке два соседних броска оказались связаны, и все неполные семьи до единой вышли материнскими. **Номер ребёнка в идентификаторе — счётчик, а не длина списка.** Выпускник уходит из ростера; если следующий ребёнок семьи получит номер по числу оставшихся детей, он унаследует идентификатор ушедшего, и все ссылки на него — семейные связи, открытая карточка — молча начнут показывать другого человека. ## Дефы Пять новых видов. Ссылки — строками `defName`, как везде. 1. **SkillDef** — навык. Диапазон, распределение при генерации, зависимость от возраста, ограничения со стороны тела (см. ниже). Не каждый человек получает каждый навык: `always` — у всех, предметы — по параллели ученика, `work` — только взрослым, `adultChance` — шанс у взрослого поверх родного языка. 2. **TraitDef** — черта. Вес (частота), список несовместимых черт, ограничения по роли и возрасту, модификаторы навыков. 3. **BodyAttributeDef** — свойство тела. Либо число с распределением (рост, вес), либо выбор из списка с весами (цвет волос, цвет глаз). Распределение зависит от пола и возраста. 4. **NeedDef** — нужда. Начальное значение и скорость убывания. 5. **NameSetDef** — набор имён (см. отдельный раздел). Телосложение — не отдельный def, а **производное**: считается из роста и веса кодом. Данные не должны дублировать то, что выводится. ### Что кладём в `core` | Вид | Ванильное содержание | | --- | --- | | Навыки | Школьные предметы, языки набора имён, общение, физические (ловкость, сила, выносливость) и рабочие (педагогика, медицина, кухня…) | | Черты | Около десяти для начала: усидчивый, рассеянный, задира, тихоня, лидер, лентяй, любопытный, вспыльчивый, добрый, аккуратный | | Тело | Рост, вес, цвет волос, цвет глаз | | Нужды | Сон, голод, туалет, общение | Слои одинаковы у всех ролей: у родителя те же *виды* данных, что у ученика. Набор ключей навыков — нет. Нет ключа — нет навыка, а не ноль: карточка его не показывает, урок, если предмет всё же начался, стартует с `Range.Min`. Иначе первоклассник носил бы химию «для полноты», а код всё равно спрашивал бы, есть ли она. ### Как тело ограничивает навыки «Толстый не может быть ловким» — правило **генерации**, а не игровой эффект: характеристики пока ни на что не влияют, но между собой согласованы. Ограничение живёт в `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 }, ], } ``` Генератор сначала бросает тело, потом те навыки, которые человеку положены, потом применяет ограничения как зажим. Порядок важен и фиксируется: **тело первично**, навык подстраивается. Иначе один и тот же сид даст разных людей в зависимости от порядка перебора. Выдача навыков тоже фиксирована: `always` → родной язык школы → предметы параллели (ученик) → лишние академические, рабочие и `adultChance` (взрослый) → черты и зажим тела → родственные языки набора. Годовой набор добавляет только новые ключи предметов; уже выданные значения не перебрасываются. Ограничения — не только вниз: «атлет не может быть совсем неловким» так же осмысленно, поэтому `min` есть наравне с `max`. ## Нужды Четыре нужды в `core`: **сон, голод, туалет, общение**. `NeedDef` описывает шкалу и скорость убывания. Машинерия работает с этого среза: значение лежит на человеке, тикает вместе со школой, показывается в карточке. **Но в `core` скорость убывания равна нулю.** Есть тела, которые тратят, и нет ничего, что восполняет: некому поесть, негде поспать. Ненулевая скорость в этом срезе означала бы школу, где через игровые сутки все нужды на нуле, — и это выглядело бы поломкой, а не игрой. Скорость — данные, не код. Когда появится еда и сон, цифра меняется в JSONC, а не в C#. Отменено срезом 5: в `core` ненулевой `decayPerHour`, сон и голод восстанавливаются вне школы, см. [`ai.md`](../05-ai/ai.md). ## Имена `NameSetDef` — набор имён целиком: мужские имена, женские имена, фамилии, правила отчеств и склонений. В `core` один набор — славянский. Другие наборы приходят модами. **Один набор на школу.** Выбирается в диалоге создания рядом с модами и сохраняется в файле школы. Смеси с весами («80% славянских, 20% прочих») — не в этом срезе: они требуют UI для весов. Набор несёт список языков, на которых здесь говорят (`nativeLanguages`). Школа выбирает **один** при создании — это родной язык всех людей. Остальные языки списка часто появляются на низком уровне (`relatedLanguageChance` и соседние поля на том же def): русскоязычный может понимать белорусский. Уже выданный ключ — родной или предмет школы — не перезаписывается, поэтому белорусский ученик сохраняет школьный русский, а не обрезанный «чуть-чуть». Шанс `0` оставляет родственников выключенными (одноязычный пакет). Набор имён **не связан с языком интерфейса**. Английский UI не превращает Иванову в Ivanova: язык интерфейса — про подписи кнопок, набор имён — про то, кто учится в школе. ### Склонения Русскому тексту нужны падежи: «вызвать **Иванову Марию**», «дневник **Ивановой Марии**». Шесть падежей на каждую часть ФИО. Два способа, и нужны оба: - **Правило** — по умолчанию. Код знает небольшой набор моделей склонения (`-ов/-ова`, `-ий/-ая`, `-а/-я`, несклоняемые). В данных — только имя и id модели. - **Явная таблица** — когда правило не работает. Любая запись может задать все шесть форм руками. Это тот же принцип, что и везде: код знает *как*, данные говорят *что*. Словарь из шести форм на каждое из тысячи имён никто не выдержит, а одними правилами русские фамилии не покрываются. Падежи нужны заранее, до появления ленты событий: набор имён с одной формой потом придётся переписывать целиком, а он самый объёмный файл в `core`. ## Отдельная библиотека Новый проект **`HSchool.People`**. ``` Protocol ← Server → Simulation → People → Content ``` - Зависит от `HSchool.Content` (нужны defs и каталог). - **Не** зависит от Arch, ASP.NET и сокетов — как `Content` сейчас. - Отдаёт простые записи. Превращает их в сущности `World` уже `HSchool.Simulation`. Так генератор тестируется без мира и без хоста: «этот сид + эта карта + этот набор имён + этот родной язык → ровно эти люди». Это единственный способ поймать регрессию в генераторе — глазами такое не проверяется. ### Детерминизм Генерация детерминирована от **сида**. Сид — своё поле школы, не её идентификатор: при создании его бросает сервер, либо его передают в `POST /api/schools`. Он лежит в файле людей и виден в списке школ и на экране школы, чтобы им можно было поделиться. Один сид + одна карта + один набор имён + один родной язык = одна и та же школа, всегда. Если родной язык при создании не указан, его выбирает тот же сид — повтор без поля даёт тот же язык. Существующие школы не перебрасываются: они продолжают жить с числом, уже записанным в файле людей. Сид у каждой семьи свой, выведенный из школьного: добавление тринадцатой семьи не должно менять первые двенадцать. Иначе тест на генератор ломается от любой правки порядка. ## Годовой набор Состав живёт: 1 сентября классы переходят в следующую параллель, одиннадцатый выпускается, его кабинет освобождается под новый первый. Вместимость при этом постоянна — она задана картой. Набор не увеличивает школу, а **замещает** выпуск. Школа растёт только стройкой. Выпускается **старшая имеющаяся** параллель: школа из четырёх кабинетов не должна ждать одиннадцатого класса, чтобы кого-то выпустить. Выпускники уходят из ростера. Их родители уходят следом, но только если в школе не осталось других их детей и сами они в ней не работают, — иначе ростер копил бы людей, ни с чем не связанных, до бесконечности. Школа создаётся посреди учебного года (по умолчанию 3 апреля), поэтому при создании классы уже сформированы и возраст ученика соответствует его параллели: в N-й класс ходят те, кому исполнилось N+6 до 1 сентября. Переход в следующую параллель **добавляет** навыки новых предметов (английский с пятого, химия с восьмого) и не перебрасывает уже выданные. ## Просмотр Не отдельный экран. Список людей — **панель в оболочке менеджера**, рядом с картой: уходя в отдельный экран, игрок теряет из виду часы и локацию, а смысл этой оболочки ровно в том, чтобы всё было на одном экране. Ориентир — прогрессивный QSP: текст, списки и ссылки, без сцены. Панель «Люди» — вторая вкладка той же панели, что и карта: фильтрам, колонкам и пейджеру нужна ширина, а держать список третьим рядом под картой значило бы отдать ему высоту, которой на экране школы нет. Вкладка переключается рядом с деревом, выбранная строка открывает карточку справа — там же, где содержимое локации. - **Фильтры**: роль (ученики / работники / родители), параллель и литера, должность, пол, возрастной диапазон. - **Сортировки**: фамилия, возраст, параллель, должность. - **Пейджинг** обязателен: тысяча учеников — это три тысячи человек с семьями. - **Строка списка** — только лёгкое: ФИО, роль, класс или должность, возраст, пол. - **Карточка** одного человека — тело, навыки (только те, что есть), черты, нужды, семья со ссылками на родных. ## Масштаб Школа на 1000 учеников — это около **3000 человек** вместе с родителями, и до шести таких школ живут одновременно. Три решения следуют отсюда напрямую. **Ростер не ходит в снимке карты.** Снимок при открытии школы остаётся про помещения. Люди — отдельный путь, иначе открытие школы означало бы мегабайт по сокету. **Список — HTTP, а не сокет.** Запрос-ответ с фильтром, сортировкой и страницей — это REST по природе, как и меню. Воркер публикует неизменяемый снимок ростера (он меняется редко: создание, набор, наём), обработчик читает опубликованный снимок и не трогает воркер — инвариант 3 цел. **Живые значения — из воркера.** Нужды тикают, поэтому карточка одного человека запрашивается у воркера через мейлбокс и `TaskCompletionSource`, как create и delete. Один человек — дешёвый запрос; три тысячи людей двадцать раз в секунду — нет. **Ростер — отдельный файл.** `saves/{id}.people.json` пишется только когда состав изменился; `saves/{id}.json` остаётся маленьким и частым. Сейчас файл школы переписывается каждые 30 секунд — таскать через это мегабайт ростера, который меняется раз в игровой год, незачем. ## Советы, которые стоит принять сразу - Не делать «родителя» отдельным видом сущности. Иначе учительница-мать станет двумя людьми. - Не хранить телосложение в данных — оно выводится из роста и веса. - Не начинать с падежных таблиц на каждое имя: правило по умолчанию, таблица как исключение. - Не отправлять черты и навыки в списке — только в карточке. Список читают глазами, карточку открывают по одному человеку. - Сид на семью, а не один на школу: иначе любая правка генератора переставит всех. - Ненулевую скорость нужд не включать, пока нечем их восполнять — сделано срезом 5, см. [`ai.md`](../05-ai/ai.md). ## Заведомо не сейчас Срез 2 закрыт. Строки, которые отменили следующие срезы, помечены. - Поведение: расписание, уроки, перемещение по карте — отменено срезами 4–5, см. [`schedule.md`](../04-schedule/schedule.md), [`ai.md`](../05-ai/ai.md). - Влияние характеристик на что-либо. Черты и навыки в этом срезе — текст в карточке — отменено срезом 5 (маршрут, обучение). - Вызов родителя в школу. Задел есть (родитель — сущность), реализации нет. - Наём и увольнение вручную. Должности закрываются генератором — отменено срезом 3, см. [`staffing.md`](../03-staffing/staffing.md). Школа открывается без штата. - Смеси наборов имён с весами. - Новые роли из модов: ученик, работник, родитель — код, не данные. - Портреты и любой арт. - Причина неполной семьи: развод, вдовство и прочее — это события, а ленты событий ещё нет. ## Зафиксировано этим разговором | Тема | Решение | | --- | --- | | Размер школы | Из карты: `PupilSlots` — ученики, `RoomDef.positions` — штат | | Класс | Один кабинет = один класс; класс — сущность ростера, не поле карты | | Параллель и литера | Поля класса, а не разбор подписи комнаты; раскладывает генератор | | Подписи кабинетов | Номера помещений («204»), имя класса живёт на классе | | Ванильная карта | Дорабатывается до 11 кабинетов — по одному на параллель | | Когда генерируются | Полный состав при создании, затем набор каждое 1 сентября | | Годовой набор | Замещает выпуск; вместимость меняет только стройка | | Кого выпускать | Старшую имеющуюся параллель, а не обязательно одиннадцатую | | Уход из ростера | Выпускники уходят; родители — если не осталось детей и они здесь не работают | | Роли | Ученик, работник, родитель; роль — код, должность — `PositionDef` | | Родители | Сущности в `World`, без места на карте; задел на вызов в школу | | Совмещение ролей | Один человек может быть работником и родителем ученика | | Единица генерации | Семья, а не человек | | Неполные семьи | Около 8% семей с детьми; остаётся случайный родитель; причина не моделируется | | Фамилия и отчество | Всегда отцовские; имя отца и фамилия записаны на семье, а не на человеке | | Раздача мест | Вперемешку по школе от сида: иначе братья и сёстры — всегда одноклассники | | Стабильность сида | Гарантируется по семье (фамилия, состав, имена), не по месту в классе | | Идентификатор ребёнка | Счётчик семьи; номер выпустившегося не переиспользуется | | Набор в первый класс | Новые семьи приходят по одному ребёнку — иначе это тройняшки-ровесники | | Порядок ограничений | Черта не может пробить потолок тела: зажим повторяется после модификаторов | | Слои человека | Личность, тело, навыки, черты, нужды, связи | | Новые defs | `SkillDef`, `TraitDef`, `BodyAttributeDef`, `NeedDef`, `NameSetDef` | | Телосложение | Производное от роста и веса, не def | | Тело и навыки | `SkillDef.bodyLimits` зажимают навык; тело бросается первым | | Влияние характеристик | Никакого; только просмотр | | Навыки в `core` | Предметы, языки, общение, физические, рабочие | | Выдача навыков | Не полный каталог: always, родной, предметы года, взрослые лишние, родственные языки | | Черты в `core` | Около десяти | | Нужды в `core` | Сон, голод, туалет, общение | | Нужды | Машинерия есть; убывание в `core` — срез 5, не ноль, см. [`ai.md`](../05-ai/ai.md) | | Слои по ролям | Одинаковые виды данных; ключи навыков зависят от роли и возраста | | Набор имён | Один на школу, выбор в create, в `core` славянский; прочие — моды | | Родной язык | Один из `nativeLanguages` набора, выбор в create; остальные языки списка — часто низкий навык | | Падежи | Правило по умолчанию (код), явная таблица как исключение (данные) | | Имена и язык UI | Независимы | | Библиотека | `HSchool.People`: зависит от Content, не знает Arch и ASP.NET | | Детерминизм | Свой сид школы (не id), в сейве и в API; свой сид на семью; родной язык — часть входа | | Просмотр | Панель в оболочке менеджера, вкладкой рядом с картой; не отдельный экран | | Список | Фильтры, сортировки, пейджинг; в строке — только лёгкие поля | | Транспорт списка | HTTP по опубликованному снимку ростера | | Транспорт карточки | Запрос в воркер, потому что нужды живые | | Сохранение | `saves/{id}.people.json`, пишется при изменении состава |