Merge phase 29 country.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -10,7 +10,7 @@ way; this file is *how to work in them*.
|
|||||||
| --- | --- |
|
| --- | --- |
|
||||||
| schools, the game clock, game rules | `src/HSchool.Simulation` |
|
| schools, the game clock, game rules | `src/HSchool.Simulation` |
|
||||||
| defs, JSONC catalog, map validation | `src/HSchool.Content` |
|
| defs, JSONC catalog, map validation | `src/HSchool.Content` |
|
||||||
| skills, traits, body, needs, name sets | `src/HSchool.Content` |
|
| skills, traits, body, needs, countries | `src/HSchool.Content` |
|
||||||
| people generation, families, roster records, yearly intake | `src/HSchool.People` |
|
| people generation, families, roster records, yearly intake | `src/HSchool.People` |
|
||||||
| timetable planning | `src/HSchool.Schedule` |
|
| timetable planning | `src/HSchool.Schedule` |
|
||||||
| routes, day plans, who comes today | `src/HSchool.Ai` |
|
| routes, day plans, who comes today | `src/HSchool.Ai` |
|
||||||
@@ -20,7 +20,7 @@ way; this file is *how to work in them*.
|
|||||||
| connection handling, workers, saves | `src/HSchool.Server` |
|
| connection handling, workers, saves | `src/HSchool.Server` |
|
||||||
| what runs locally | `src/HSchool.AppHost/AppHost.cs` |
|
| what runs locally | `src/HSchool.AppHost/AppHost.cs` |
|
||||||
| screens, dialogs, formatting, UI language | `src/HSchool.Client/src` |
|
| screens, dialogs, formatting, UI language | `src/HSchool.Client/src` |
|
||||||
| clothing, inventory, climate, dress codes | [`docs/design/inventory.md`](docs/design/inventory.md) — slice 7, phases 29–37; no code yet |
|
| clothing, inventory, climate, dress codes | [`docs/design/inventory.md`](docs/design/inventory.md) — slice 7, phases 29–37; country is live, clothing is not |
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
@@ -109,7 +109,7 @@ say so explicitly in the change description.
|
|||||||
|
|
||||||
- Simulation changes need a `GameClock` or `SchoolRegistry` test. They are fast and need no host.
|
- Simulation changes need a `GameClock` or `SchoolRegistry` test. They are fast and need no host.
|
||||||
Putting a roster into `World` and ticking needs belongs there too.
|
Putting a roster into `World` and ticking needs belongs there too.
|
||||||
- People generation belongs in `tests/HSchool.People.Tests`. Same seed, map, name set and
|
- People generation belongs in `tests/HSchool.People.Tests`. Same seed, map, country and
|
||||||
native language must produce the same roster; the suite does not boot a host.
|
native language must produce the same roster; the suite does not boot a host.
|
||||||
- Timetable planning belongs in `tests/HSchool.Schedule.Tests`. Same staff, map and locks must
|
- Timetable planning belongs in `tests/HSchool.Schedule.Tests`. Same staff, map and locks must
|
||||||
produce the same table; the suite does not boot a host.
|
produce the same table; the suite does not boot a host.
|
||||||
|
|||||||
+16
-16
@@ -12,32 +12,32 @@ Create выбирает страну, а не набор имён. Имена и
|
|||||||
|
|
||||||
## Задачи
|
## Задачи
|
||||||
|
|
||||||
- [ ] `CountryDef` и `ClimatePresetDef` в `HSchool.Content`: страна вкладывает нынешние поля
|
- [x] `CountryDef` и `ClimatePresetDef` в `HSchool.Content`: страна вкладывает нынешние поля
|
||||||
набора имён в `names`, ссылается на пресеты списками id
|
набора имён в `names`, ссылается на пресеты списками id
|
||||||
- [ ] Набор имён больше не выбираемый вид; папка `namesets/` в `core` уезжает в страну
|
- [x] Набор имён больше не выбираемый вид; папка `namesets/` в `core` уезжает в страну
|
||||||
- [ ] Ваниль: одна страна `Russia` с нынешним славянским набором и хотя бы одним пресетом
|
- [x] Ваниль: одна страна `Russia` с нынешним славянским набором и хотя бы одним пресетом
|
||||||
(числа погоды можно заглушить — живая улица в фазе 32)
|
(числа погоды можно заглушить — живая улица в фазе 32)
|
||||||
- [ ] `GET /api/catalog` отдаёт `countries` (`defName`, подпись, `nativeLanguages`,
|
- [x] `GET /api/catalog` отдаёт `countries` (`defName`, подпись, `nativeLanguages`,
|
||||||
`climatePresets`) вместо `nameSets`
|
`climatePresets`) вместо `nameSets`
|
||||||
- [ ] `POST /api/schools` принимает `countryId`; неизвестный — `400` `unknown-country`.
|
- [x] `POST /api/schools` принимает `countryId`; неизвестный — `400` `unknown-country`.
|
||||||
`nameSetId` не принимается
|
`nameSetId` не принимается
|
||||||
- [ ] Пресет климата записывается на школу: случайный из списка страны, от сида. Живой школе
|
- [x] Пресет климата записывается на школу: случайный из списка страны, от сида. Живой школе
|
||||||
его не сменить
|
его не сменить
|
||||||
- [ ] Форма создания: подпись «Страна»; язык — если у страны их несколько
|
- [x] Форма создания: подпись «Страна»; язык — если у страны их несколько
|
||||||
- [ ] Старый сейв с `nameSetId: Slavic` поднимается как `Russia` + первый пресет; формат
|
- [x] Старый сейв с `nameSetId: Slavic` поднимается как `Russia` + первый пресет; формат
|
||||||
новее без `countryId` — как с прочими полями сейва, школа не стартует
|
новее без `countryId` — как с прочими полями сейва, школа не стартует
|
||||||
- [ ] `docs/protocol.md` и `AGENTS.md` (ориентир create / каталог) в том же коммите
|
- [x] `docs/protocol.md` и `AGENTS.md` (ориентир create / каталог) в том же коммите
|
||||||
- [ ] Золотой отпечаток ростера обновляется, если вход генератора сменил имя поля
|
- [x] Золотой отпечаток ростера обновляется, если вход генератора сменил имя поля
|
||||||
|
|
||||||
## Тесты, без которых фаза не закрыта
|
## Тесты, без которых фаза не закрыта
|
||||||
|
|
||||||
- [ ] Каталог `core` грузится; страна без имён или с неизвестным пресетом — ошибка загрузки
|
- [x] Каталог `core` грузится; страна без имён или с неизвестным пресетом — ошибка загрузки
|
||||||
- [ ] Create с `countryId: Russia` и без языка даёт тот же родной язык, что прежний сид без
|
- [x] Create с `countryId: Russia` и без языка даёт тот же родной язык, что прежний сид без
|
||||||
`nativeLanguage`
|
`nativeLanguage`
|
||||||
- [ ] Create с неизвестной страной — `400` `unknown-country`
|
- [x] Create с неизвестной страной — `400` `unknown-country`
|
||||||
- [ ] Два create с одним сидом получают один и тот же пресет
|
- [x] Два create с одним сидом получают один и тот же пресет
|
||||||
- [ ] Старый сейв без страны поднимается
|
- [x] Старый сейв без страны поднимается
|
||||||
- [ ] Диалог создания показывает страны, не наборы имён; смена страны обновляет список языков
|
- [x] Диалог создания показывает страны, не наборы имён; смена страны обновляет список языков
|
||||||
|
|
||||||
## Критерий готовности
|
## Критерий готовности
|
||||||
|
|
||||||
|
|||||||
@@ -127,7 +127,7 @@
|
|||||||
|
|
||||||
| Фаза | Статус | Зачем |
|
| Фаза | Статус | Зачем |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| [29. Страна](29-country.md) | 🔄 | `CountryDef`, create выбирает страну, климат-пресет от сида |
|
| [29. Страна](29-country.md) | ✅ | `CountryDef`, create выбирает страну, климат-пресет от сида |
|
||||||
| [30. Слои и палитра](30-apparel-defs.md) | ✅ | Поля `ThingDef`, `ColorDef`, ванильная одежда и ноша |
|
| [30. Слои и палитра](30-apparel-defs.md) | ✅ | Поля `ThingDef`, `ColorDef`, ванильная одежда и ноша |
|
||||||
| [31. Генератор одевает](31-dress-generator.md) | ⬜ | Гардероб на человеке, ноша-число, `people.json` |
|
| [31. Генератор одевает](31-dress-generator.md) | ⬜ | Гардероб на человеке, ноша-число, `people.json` |
|
||||||
|
|
||||||
|
|||||||
+16
-14
@@ -89,13 +89,15 @@ single seats field on a homeroom and the old slot rows on everything else.
|
|||||||
Things carry `pupilSlots` — how many pupils that thing hosts. The snapshot's per-node pupil-slot
|
Things carry `pupilSlots` — how many pupils that thing hosts. The snapshot's per-node pupil-slot
|
||||||
total is computed on the server.
|
total is computed on the server.
|
||||||
|
|
||||||
`nameSets` is the list of placeable name packs (`defName` + label + `nativeLanguages`). Each
|
`countries` is the list of placeable countries (`defName` + label + `nativeLanguages` +
|
||||||
`nativeLanguages` entry is a skill (`defName` + label). The create dialog picks a name set and a
|
`climatePresets`). Each `nativeLanguages` entry is a skill (`defName` + label). The create
|
||||||
native language; the language list comes from that set (vanilla Slavic: Russian, Belarusian,
|
dialog picks a country and, when that country lists more than one tongue, a native language;
|
||||||
Ukrainian). It is independent of the UI language. Everyone in the school is generated with that
|
the language list comes from that country (vanilla Russia: Russian, Belarusian, Ukrainian).
|
||||||
native. Other languages of the same set often appear at a low skill — a Russian speaker who
|
It is independent of the UI language. Everyone in the school is generated with that native.
|
||||||
understands Belarusian. Those rolls live on the `NameSetDef` (`relatedLanguageChance` and
|
Other languages of the same country often appear at a low skill — a Russian speaker who
|
||||||
neighbours), not on this catalog payload.
|
understands Belarusian. Those rolls live on the nested names of the `CountryDef`
|
||||||
|
(`relatedLanguageChance` and neighbours), not on this catalog payload. `climatePresets` are
|
||||||
|
ids; the school rolls one at create from its seed and keeps it.
|
||||||
|
|
||||||
`subjects` is the list of placeable subjects (`defName`, label, `gradeMin`/`gradeMax`,
|
`subjects` is the list of placeable subjects (`defName`, label, `gradeMin`/`gradeMax`,
|
||||||
`hoursPerWeek`, `skills` with shares, and optional `room`). `room` is the RoomDef the lesson
|
`hoursPerWeek`, `skills` with shares, and optional `room`). `room` is the RoomDef the lesson
|
||||||
@@ -121,7 +123,7 @@ Body:
|
|||||||
"startDate": "2012-03-31T06:00:00Z",
|
"startDate": "2012-03-31T06:00:00Z",
|
||||||
"modIds": [],
|
"modIds": [],
|
||||||
"map": null,
|
"map": null,
|
||||||
"nameSetId": "Slavic",
|
"countryId": "Russia",
|
||||||
"nativeLanguage": null,
|
"nativeLanguage": null,
|
||||||
"seed": null
|
"seed": null
|
||||||
}
|
}
|
||||||
@@ -131,10 +133,10 @@ Body:
|
|||||||
pack's `requires` load first (stable topological sort over the player's order). The resolved
|
pack's `requires` load first (stable topological sort over the player's order). The resolved
|
||||||
order is returned as `modIds` on the created school and written to the save, so a restart loads
|
order is returned as `modIds` on the created school and written to the save, so a restart loads
|
||||||
the same catalog. 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.
|
the same catalog. 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:
|
`countryId` is a `CountryDef`; omit it to use the first placeable country in the catalog (vanilla:
|
||||||
`Slavic`). Unknown ids return `400` `unknown-name-set`.
|
`Russia`). Unknown ids return `400` `unknown-country`. The old `nameSetId` field is not accepted.
|
||||||
`nativeLanguage` is a skill from that set's `nativeLanguages`. Omit it (or send `null`) to pick
|
`nativeLanguage` is a skill from that country's `nativeLanguages`. Omit it (or send `null`) to pick
|
||||||
one from the school seed. An id that is not in the set returns `400` `unknown-native-language`.
|
one from the school seed. An id that is not in the country returns `400` `unknown-native-language`.
|
||||||
`seed` is an optional integer. Send it to reproduce a known school; omit it (or send `null`) and
|
`seed` is an optional integer. Send it to reproduce a known school; omit it (or send `null`) and
|
||||||
the server rolls one. Existing saves keep the seed already stored in the people file.
|
the server rolls one. Existing saves keep the seed already stored in the people file.
|
||||||
|
|
||||||
@@ -148,8 +150,8 @@ the server rolls one. Existing saves keep the seed already stored in the people
|
|||||||
| `400` `missing-mod` | A selected pack `requires` an id that was not selected. `missing` is that id. |
|
| `400` `missing-mod` | A selected pack `requires` an id that was not selected. `missing` is that id. |
|
||||||
| `400` `mod-cycle` | Selected packs require each other in a cycle. |
|
| `400` `mod-cycle` | Selected packs require each other in a cycle. |
|
||||||
| `400` `invalid-catalog` | The selected packs could not be loaded. |
|
| `400` `invalid-catalog` | The selected packs could not be loaded. |
|
||||||
| `400` `unknown-name-set` | `nameSetId` is not a placeable `NameSetDef` in those packs. |
|
| `400` `unknown-country` | `countryId` is not a placeable `CountryDef` in those packs. |
|
||||||
| `400` `unknown-native-language` | `nativeLanguage` is not in that name set's `nativeLanguages`. |
|
| `400` `unknown-native-language` | `nativeLanguage` is not in that country's `nativeLanguages`. |
|
||||||
| `409` `school-limit-reached` | `maxSchools` schools already exist. |
|
| `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.
|
Failures are RFC 7807 problem details with an extra `code` field — that is what the UI switches on.
|
||||||
|
|||||||
@@ -43,12 +43,12 @@ const ru = {
|
|||||||
errorMissingMod: 'Не выбран обязательный мод «{id}».',
|
errorMissingMod: 'Не выбран обязательный мод «{id}».',
|
||||||
errorModCycle: 'Выбранные моды зависят друг от друга по кругу.',
|
errorModCycle: 'Выбранные моды зависят друг от друга по кругу.',
|
||||||
errorInvalidCatalog: 'Не удалось загрузить выбранные моды.',
|
errorInvalidCatalog: 'Не удалось загрузить выбранные моды.',
|
||||||
errorUnknownNameSet: 'Выбранный набор имён не найден.',
|
errorUnknownCountry: 'Выбранная страна не найдена.',
|
||||||
errorUnknownNativeLanguage: 'Выбранный родной язык не входит в этот набор имён.',
|
errorUnknownNativeLanguage: 'Выбранный родной язык не входит в эту страну.',
|
||||||
catalogLoadFailed: 'Не удалось загрузить каталог модов.',
|
catalogLoadFailed: 'Не удалось загрузить каталог модов.',
|
||||||
|
|
||||||
modsTitle: 'Моды',
|
modsTitle: 'Моды',
|
||||||
nameSetTitle: 'Набор имён',
|
countryTitle: 'Страна',
|
||||||
nativeLanguageTitle: 'Родной язык',
|
nativeLanguageTitle: 'Родной язык',
|
||||||
nativeLanguageRandom: 'Случайный',
|
nativeLanguageRandom: 'Случайный',
|
||||||
randomNativeTitle: 'Выбрать язык наугад',
|
randomNativeTitle: 'Выбрать язык наугад',
|
||||||
@@ -260,12 +260,12 @@ const en: Messages = {
|
|||||||
errorMissingMod: 'Required mod “{id}” is not selected.',
|
errorMissingMod: 'Required mod “{id}” is not selected.',
|
||||||
errorModCycle: 'The selected mods depend on each other in a cycle.',
|
errorModCycle: 'The selected mods depend on each other in a cycle.',
|
||||||
errorInvalidCatalog: 'The selected packs could not be loaded.',
|
errorInvalidCatalog: 'The selected packs could not be loaded.',
|
||||||
errorUnknownNameSet: 'The selected name set is not in the catalog.',
|
errorUnknownCountry: 'The selected country is not in the catalog.',
|
||||||
errorUnknownNativeLanguage: 'The selected native language is not in that name set.',
|
errorUnknownNativeLanguage: 'The selected native language is not in that country.',
|
||||||
catalogLoadFailed: 'Could not load the mod catalog.',
|
catalogLoadFailed: 'Could not load the mod catalog.',
|
||||||
|
|
||||||
modsTitle: 'Mods',
|
modsTitle: 'Mods',
|
||||||
nameSetTitle: 'Name set',
|
countryTitle: 'Country',
|
||||||
nativeLanguageTitle: 'Native language',
|
nativeLanguageTitle: 'Native language',
|
||||||
nativeLanguageRandom: 'Random',
|
nativeLanguageRandom: 'Random',
|
||||||
randomNativeTitle: 'Pick a language at random',
|
randomNativeTitle: 'Pick a language at random',
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export async function fetchRandomName(lang: string): Promise<string> {
|
|||||||
export interface CreateSchoolOptions {
|
export interface CreateSchoolOptions {
|
||||||
readonly modIds?: readonly string[];
|
readonly modIds?: readonly string[];
|
||||||
readonly map?: MapLayout;
|
readonly map?: MapLayout;
|
||||||
readonly nameSetId?: string;
|
readonly countryId?: string;
|
||||||
readonly nativeLanguage?: string;
|
readonly nativeLanguage?: string;
|
||||||
readonly seed?: number;
|
readonly seed?: number;
|
||||||
}
|
}
|
||||||
@@ -71,7 +71,7 @@ export async function createSchool(
|
|||||||
startDate: startDate.toISOString(),
|
startDate: startDate.toISOString(),
|
||||||
modIds: extras.modIds ?? [],
|
modIds: extras.modIds ?? [],
|
||||||
map: extras.map ?? null,
|
map: extras.map ?? null,
|
||||||
nameSetId: extras.nameSetId ?? null,
|
countryId: extras.countryId ?? null,
|
||||||
nativeLanguage: extras.nativeLanguage ?? null,
|
nativeLanguage: extras.nativeLanguage ?? null,
|
||||||
seed: extras.seed ?? null,
|
seed: extras.seed ?? null,
|
||||||
}),
|
}),
|
||||||
@@ -92,10 +92,11 @@ export interface DefInfo {
|
|||||||
readonly pupilSlots?: number;
|
readonly pupilSlots?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NameSetInfo {
|
export interface CountryInfo {
|
||||||
readonly defName: string;
|
readonly defName: string;
|
||||||
readonly label: string;
|
readonly label: string;
|
||||||
readonly nativeLanguages: readonly DefInfo[];
|
readonly nativeLanguages: readonly DefInfo[];
|
||||||
|
readonly climatePresets: readonly string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RoomSlotInfo {
|
export interface RoomSlotInfo {
|
||||||
@@ -170,7 +171,7 @@ export interface CatalogResponse {
|
|||||||
readonly rooms: readonly RoomInfo[];
|
readonly rooms: readonly RoomInfo[];
|
||||||
readonly things: readonly DefInfo[];
|
readonly things: readonly DefInfo[];
|
||||||
readonly defaultMap: MapLayout;
|
readonly defaultMap: MapLayout;
|
||||||
readonly nameSets: readonly NameSetInfo[];
|
readonly countries: readonly CountryInfo[];
|
||||||
readonly subjects: readonly SubjectInfo[];
|
readonly subjects: readonly SubjectInfo[];
|
||||||
readonly dayFrame: DayFrameInfo | null;
|
readonly dayFrame: DayFrameInfo | null;
|
||||||
readonly holidays: readonly HolidayInfo[];
|
readonly holidays: readonly HolidayInfo[];
|
||||||
|
|||||||
@@ -54,11 +54,22 @@ function catalog(): CatalogResponse {
|
|||||||
],
|
],
|
||||||
things: [],
|
things: [],
|
||||||
defaultMap,
|
defaultMap,
|
||||||
nameSets: [
|
countries: [
|
||||||
{
|
{
|
||||||
defName: 'Russian',
|
defName: 'Russia',
|
||||||
label: 'Russian',
|
label: 'Russia',
|
||||||
nativeLanguages: [{ defName: 'Russian', label: 'Russian' }],
|
nativeLanguages: [
|
||||||
|
{ defName: 'RussianLanguage', label: 'Russian' },
|
||||||
|
{ defName: 'BelarusianLanguage', label: 'Belarusian' },
|
||||||
|
{ defName: 'UkrainianLanguage', label: 'Ukrainian' },
|
||||||
|
],
|
||||||
|
climatePresets: ['TemperateContinental'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
defName: 'Nordic',
|
||||||
|
label: 'Nordic',
|
||||||
|
nativeLanguages: [{ defName: 'NorwegianLanguage', label: 'Norwegian' }],
|
||||||
|
climatePresets: ['TemperateContinental'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
subjects: [],
|
subjects: [],
|
||||||
@@ -162,6 +173,55 @@ describe('createSchoolDialog', () => {
|
|||||||
finish(school());
|
finish(school());
|
||||||
await expect(opened).resolves.toMatchObject({ name: 'North' });
|
await expect(opened).resolves.toMatchObject({ name: 'North' });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('shows countries and refreshes languages when the country changes', async () => {
|
||||||
|
const opened = createSchoolDialog({
|
||||||
|
defaultStartDate: new Date('2024-09-01T00:00:00.000Z'),
|
||||||
|
suggestName: async () => 'North',
|
||||||
|
create: async () => school(),
|
||||||
|
});
|
||||||
|
await vi.waitFor(() => expect(fetchCatalog).toHaveBeenCalled());
|
||||||
|
const dialog = document.querySelector('dialog');
|
||||||
|
if (dialog === null) {
|
||||||
|
throw new Error('create dialog is missing');
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(byText(dialog, '.field__label', t('countryTitle'))).toBeTruthy();
|
||||||
|
expect([...dialog.querySelectorAll('.field__label')].some((node) => node.textContent === 'Name set')).toBe(false);
|
||||||
|
|
||||||
|
const countrySelect = dialog.querySelector('select');
|
||||||
|
if (!(countrySelect instanceof HTMLSelectElement)) {
|
||||||
|
throw new Error('country select is missing');
|
||||||
|
}
|
||||||
|
|
||||||
|
expect([...countrySelect.options].map((option) => option.value)).toEqual(['Russia', 'Nordic']);
|
||||||
|
const nativeField = byText(dialog, '.field__label', t('nativeLanguageTitle')).closest('.field');
|
||||||
|
if (!(nativeField instanceof HTMLElement)) {
|
||||||
|
throw new Error('native language field is missing');
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(nativeField.hidden).toBe(false);
|
||||||
|
|
||||||
|
countrySelect.value = 'Nordic';
|
||||||
|
countrySelect.dispatchEvent(new Event('change'));
|
||||||
|
expect(nativeField.hidden).toBe(true);
|
||||||
|
|
||||||
|
countrySelect.value = 'Russia';
|
||||||
|
countrySelect.dispatchEvent(new Event('change'));
|
||||||
|
const nativeSelect = nativeField.querySelector('select');
|
||||||
|
if (!(nativeSelect instanceof HTMLSelectElement)) {
|
||||||
|
throw new Error('native language select is missing');
|
||||||
|
}
|
||||||
|
|
||||||
|
expect([...nativeSelect.options].map((option) => option.value)).toEqual([
|
||||||
|
'',
|
||||||
|
'RussianLanguage',
|
||||||
|
'BelarusianLanguage',
|
||||||
|
'UkrainianLanguage',
|
||||||
|
]);
|
||||||
|
expect(nativeField.hidden).toBe(false);
|
||||||
|
void opened;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('map reset from the create editor', () => {
|
describe('map reset from the create editor', () => {
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export function createSchoolDialog(options: CreateSchoolOptions): Promise<School
|
|||||||
const extraModIds = new Set<string>();
|
const extraModIds = new Set<string>();
|
||||||
let catalog: CatalogResponse | null = null;
|
let catalog: CatalogResponse | null = null;
|
||||||
let currentMap: MapLayout | null = null;
|
let currentMap: MapLayout | null = null;
|
||||||
let nameSetId: string | null = null;
|
let countryId: string | null = null;
|
||||||
let nativeLanguageId: string | null = null;
|
let nativeLanguageId: string | null = null;
|
||||||
|
|
||||||
const modsField = el('div', { class: 'field' });
|
const modsField = el('div', { class: 'field' });
|
||||||
@@ -40,11 +40,11 @@ export function createSchoolDialog(options: CreateSchoolOptions): Promise<School
|
|||||||
const modsList = el('div', { class: 'mod-list' });
|
const modsList = el('div', { class: 'mod-list' });
|
||||||
modsField.append(modsLabel, modsList);
|
modsField.append(modsLabel, modsList);
|
||||||
|
|
||||||
const nameSetLabel = el('span', { class: 'field__label' });
|
const countryLabel = el('span', { class: 'field__label' });
|
||||||
const nameSetSelect = el('select', { class: 'input' });
|
const countrySelect = el('select', { class: 'input' });
|
||||||
const nameSetField = el('label', { class: 'field' }, nameSetLabel, nameSetSelect);
|
const countryField = el('label', { class: 'field' }, countryLabel, countrySelect);
|
||||||
nameSetSelect.addEventListener('change', () => {
|
countrySelect.addEventListener('change', () => {
|
||||||
nameSetId = nameSetSelect.value === '' ? null : nameSetSelect.value;
|
countryId = countrySelect.value === '' ? null : countrySelect.value;
|
||||||
nativeLanguageId = null;
|
nativeLanguageId = null;
|
||||||
if (catalog !== null) {
|
if (catalog !== null) {
|
||||||
paintNativeLanguages(catalog);
|
paintNativeLanguages(catalog);
|
||||||
@@ -110,7 +110,7 @@ export function createSchoolDialog(options: CreateSchoolOptions): Promise<School
|
|||||||
el('label', { class: 'field' }, nameLabel, el('div', { class: 'field__row' }, nameInput, randomButton)),
|
el('label', { class: 'field' }, nameLabel, el('div', { class: 'field__row' }, nameInput, randomButton)),
|
||||||
el('div', { class: 'field' }, startLabel, el('div', { class: 'field__row' }, dateInput, timeInput)),
|
el('div', { class: 'field' }, startLabel, el('div', { class: 'field__row' }, dateInput, timeInput)),
|
||||||
modsField,
|
modsField,
|
||||||
nameSetField,
|
countryField,
|
||||||
nativeField,
|
nativeField,
|
||||||
mapField,
|
mapField,
|
||||||
seedField,
|
seedField,
|
||||||
@@ -125,7 +125,7 @@ export function createSchoolDialog(options: CreateSchoolOptions): Promise<School
|
|||||||
const waiting = value || catalog === null;
|
const waiting = value || catalog === null;
|
||||||
submitButton.toggleAttribute('disabled', waiting);
|
submitButton.toggleAttribute('disabled', waiting);
|
||||||
randomButton.toggleAttribute('disabled', value);
|
randomButton.toggleAttribute('disabled', value);
|
||||||
nativeRandomButton.toggleAttribute('disabled', value || languagesOf(catalog, nameSetId).length <= 1);
|
nativeRandomButton.toggleAttribute('disabled', value || languagesOf(catalog, countryId).length <= 1);
|
||||||
editMapButton.toggleAttribute('disabled', waiting);
|
editMapButton.toggleAttribute('disabled', waiting);
|
||||||
seedInput.toggleAttribute('disabled', value);
|
seedInput.toggleAttribute('disabled', value);
|
||||||
};
|
};
|
||||||
@@ -149,31 +149,31 @@ export function createSchoolDialog(options: CreateSchoolOptions): Promise<School
|
|||||||
const applyCatalog = (next: CatalogResponse): void => {
|
const applyCatalog = (next: CatalogResponse): void => {
|
||||||
catalog = next;
|
catalog = next;
|
||||||
currentMap = structuredClone(next.defaultMap);
|
currentMap = structuredClone(next.defaultMap);
|
||||||
const stillThere = next.nameSets.some((set) => set.defName === nameSetId);
|
const stillThere = next.countries.some((country) => country.defName === countryId);
|
||||||
nameSetId = stillThere ? nameSetId : (next.nameSets[0]?.defName ?? null);
|
countryId = stillThere ? countryId : (next.countries[0]?.defName ?? null);
|
||||||
if (!languagesOf(next, nameSetId).some((language) => language.defName === nativeLanguageId)) {
|
if (!languagesOf(next, countryId).some((language) => language.defName === nativeLanguageId)) {
|
||||||
nativeLanguageId = null;
|
nativeLanguageId = null;
|
||||||
}
|
}
|
||||||
paintNameSets(next);
|
paintCountries(next);
|
||||||
paintNativeLanguages(next);
|
paintNativeLanguages(next);
|
||||||
paintMapHint();
|
paintMapHint();
|
||||||
setBusy(busy);
|
setBusy(busy);
|
||||||
};
|
};
|
||||||
|
|
||||||
const paintNameSets = (next: CatalogResponse): void => {
|
const paintCountries = (next: CatalogResponse): void => {
|
||||||
nameSetSelect.replaceChildren();
|
countrySelect.replaceChildren();
|
||||||
for (const set of next.nameSets) {
|
for (const country of next.countries) {
|
||||||
const option = el('option', { text: set.label });
|
const option = el('option', { text: country.label });
|
||||||
option.value = set.defName;
|
option.value = country.defName;
|
||||||
nameSetSelect.append(option);
|
countrySelect.append(option);
|
||||||
}
|
}
|
||||||
|
|
||||||
nameSetSelect.value = nameSetId ?? '';
|
countrySelect.value = countryId ?? '';
|
||||||
nameSetSelect.disabled = next.nameSets.length <= 1;
|
countrySelect.disabled = next.countries.length <= 1;
|
||||||
};
|
};
|
||||||
|
|
||||||
const paintNativeLanguages = (next: CatalogResponse): void => {
|
const paintNativeLanguages = (next: CatalogResponse): void => {
|
||||||
const languages = languagesOf(next, nameSetId);
|
const languages = languagesOf(next, countryId);
|
||||||
nativeSelect.replaceChildren();
|
nativeSelect.replaceChildren();
|
||||||
const random = el('option', { text: t('nativeLanguageRandom') });
|
const random = el('option', { text: t('nativeLanguageRandom') });
|
||||||
random.value = '';
|
random.value = '';
|
||||||
@@ -187,7 +187,7 @@ export function createSchoolDialog(options: CreateSchoolOptions): Promise<School
|
|||||||
nativeSelect.value = nativeLanguageId ?? '';
|
nativeSelect.value = nativeLanguageId ?? '';
|
||||||
nativeSelect.disabled = languages.length <= 1;
|
nativeSelect.disabled = languages.length <= 1;
|
||||||
nativeRandomButton.toggleAttribute('disabled', busy || languages.length <= 1);
|
nativeRandomButton.toggleAttribute('disabled', busy || languages.length <= 1);
|
||||||
nativeField.hidden = languages.length === 0;
|
nativeField.hidden = languages.length <= 1;
|
||||||
};
|
};
|
||||||
|
|
||||||
const reloadCatalog = async (): Promise<void> => {
|
const reloadCatalog = async (): Promise<void> => {
|
||||||
@@ -242,7 +242,7 @@ export function createSchoolDialog(options: CreateSchoolOptions): Promise<School
|
|||||||
const localize = (): void => {
|
const localize = (): void => {
|
||||||
title.textContent = t('newSchool');
|
title.textContent = t('newSchool');
|
||||||
modsLabel.textContent = t('modsTitle');
|
modsLabel.textContent = t('modsTitle');
|
||||||
nameSetLabel.textContent = t('nameSetTitle');
|
countryLabel.textContent = t('countryTitle');
|
||||||
nativeLabel.textContent = t('nativeLanguageTitle');
|
nativeLabel.textContent = t('nativeLanguageTitle');
|
||||||
nativeRandomButton.textContent = t('randomName');
|
nativeRandomButton.textContent = t('randomName');
|
||||||
nativeRandomButton.title = t('randomNativeTitle');
|
nativeRandomButton.title = t('randomNativeTitle');
|
||||||
@@ -260,7 +260,7 @@ export function createSchoolDialog(options: CreateSchoolOptions): Promise<School
|
|||||||
cancelButton.textContent = t('cancel');
|
cancelButton.textContent = t('cancel');
|
||||||
paintMapHint();
|
paintMapHint();
|
||||||
if (catalog !== null) {
|
if (catalog !== null) {
|
||||||
paintNameSets(catalog);
|
paintCountries(catalog);
|
||||||
paintNativeLanguages(catalog);
|
paintNativeLanguages(catalog);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -284,7 +284,7 @@ export function createSchoolDialog(options: CreateSchoolOptions): Promise<School
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const languages = languagesOf(catalog, nameSetId);
|
const languages = languagesOf(catalog, countryId);
|
||||||
if (languages.length === 0) {
|
if (languages.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -332,7 +332,7 @@ export function createSchoolDialog(options: CreateSchoolOptions): Promise<School
|
|||||||
.create(nameInput.value.trim(), startDate, {
|
.create(nameInput.value.trim(), startDate, {
|
||||||
modIds: [...extraModIds],
|
modIds: [...extraModIds],
|
||||||
map: currentMap,
|
map: currentMap,
|
||||||
nameSetId: nameSetId ?? undefined,
|
countryId: countryId ?? undefined,
|
||||||
nativeLanguage: nativeLanguageId ?? undefined,
|
nativeLanguage: nativeLanguageId ?? undefined,
|
||||||
seed: seed ?? undefined,
|
seed: seed ?? undefined,
|
||||||
})
|
})
|
||||||
@@ -349,12 +349,12 @@ export function createSchoolDialog(options: CreateSchoolOptions): Promise<School
|
|||||||
return modal.open(nameInput);
|
return modal.open(nameInput);
|
||||||
}
|
}
|
||||||
|
|
||||||
function languagesOf(catalog: CatalogResponse | null, nameSetId: string | null): readonly { readonly defName: string; readonly label: string }[] {
|
function languagesOf(catalog: CatalogResponse | null, countryId: string | null): readonly { readonly defName: string; readonly label: string }[] {
|
||||||
if (catalog === null || nameSetId === null) {
|
if (catalog === null || countryId === null) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return catalog.nameSets.find((set) => set.defName === nameSetId)?.nativeLanguages ?? [];
|
return catalog.countries.find((country) => country.defName === countryId)?.nativeLanguages ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapsEqual(left: MapLayout, right: MapLayout): boolean {
|
function mapsEqual(left: MapLayout, right: MapLayout): boolean {
|
||||||
@@ -401,8 +401,8 @@ function describe(reason: unknown): string {
|
|||||||
return t('errorModCycle');
|
return t('errorModCycle');
|
||||||
case 'invalid-catalog':
|
case 'invalid-catalog':
|
||||||
return t('errorInvalidCatalog');
|
return t('errorInvalidCatalog');
|
||||||
case 'unknown-name-set':
|
case 'unknown-country':
|
||||||
return t('errorUnknownNameSet');
|
return t('errorUnknownCountry');
|
||||||
case 'unknown-native-language':
|
case 'unknown-native-language':
|
||||||
return t('errorUnknownNativeLanguage');
|
return t('errorUnknownNativeLanguage');
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -307,7 +307,8 @@ public sealed class CatalogLoader
|
|||||||
var traits = new Dictionary<string, TraitDef>(StringComparer.Ordinal);
|
var traits = new Dictionary<string, TraitDef>(StringComparer.Ordinal);
|
||||||
var bodyAttributes = new Dictionary<string, BodyAttributeDef>(StringComparer.Ordinal);
|
var bodyAttributes = new Dictionary<string, BodyAttributeDef>(StringComparer.Ordinal);
|
||||||
var needs = new Dictionary<string, NeedDef>(StringComparer.Ordinal);
|
var needs = new Dictionary<string, NeedDef>(StringComparer.Ordinal);
|
||||||
var nameSets = new Dictionary<string, NameSetDef>(StringComparer.Ordinal);
|
var countries = new Dictionary<string, CountryDef>(StringComparer.Ordinal);
|
||||||
|
var climatePresets = new Dictionary<string, ClimatePresetDef>(StringComparer.Ordinal);
|
||||||
var subjects = new Dictionary<string, SubjectDef>(StringComparer.Ordinal);
|
var subjects = new Dictionary<string, SubjectDef>(StringComparer.Ordinal);
|
||||||
var staffing = new Dictionary<string, StaffingDef>(StringComparer.Ordinal);
|
var staffing = new Dictionary<string, StaffingDef>(StringComparer.Ordinal);
|
||||||
var dayFrames = new Dictionary<string, DayFrameDef>(StringComparer.Ordinal);
|
var dayFrames = new Dictionary<string, DayFrameDef>(StringComparer.Ordinal);
|
||||||
@@ -355,8 +356,11 @@ public sealed class CatalogLoader
|
|||||||
case DefKind.Need:
|
case DefKind.Need:
|
||||||
needs[key.Name] = Jsonc.Deserialize<NeedDef>(json);
|
needs[key.Name] = Jsonc.Deserialize<NeedDef>(json);
|
||||||
break;
|
break;
|
||||||
case DefKind.NameSet:
|
case DefKind.Country:
|
||||||
nameSets[key.Name] = Jsonc.Deserialize<NameSetDef>(json);
|
countries[key.Name] = Jsonc.Deserialize<CountryDef>(json);
|
||||||
|
break;
|
||||||
|
case DefKind.ClimatePreset:
|
||||||
|
climatePresets[key.Name] = Jsonc.Deserialize<ClimatePresetDef>(json);
|
||||||
break;
|
break;
|
||||||
case DefKind.Subject:
|
case DefKind.Subject:
|
||||||
subjects[key.Name] = Jsonc.Deserialize<SubjectDef>(json);
|
subjects[key.Name] = Jsonc.Deserialize<SubjectDef>(json);
|
||||||
@@ -393,7 +397,8 @@ public sealed class CatalogLoader
|
|||||||
traits,
|
traits,
|
||||||
bodyAttributes,
|
bodyAttributes,
|
||||||
needs,
|
needs,
|
||||||
nameSets,
|
countries,
|
||||||
|
climatePresets,
|
||||||
subjects,
|
subjects,
|
||||||
staffing,
|
staffing,
|
||||||
dayFrames,
|
dayFrames,
|
||||||
@@ -541,7 +546,8 @@ public sealed class CatalogLoader
|
|||||||
.Concat(Enumerate(catalog.Traits.Values))
|
.Concat(Enumerate(catalog.Traits.Values))
|
||||||
.Concat(Enumerate(catalog.BodyAttributes.Values))
|
.Concat(Enumerate(catalog.BodyAttributes.Values))
|
||||||
.Concat(Enumerate(catalog.Needs.Values))
|
.Concat(Enumerate(catalog.Needs.Values))
|
||||||
.Concat(Enumerate(catalog.NameSets.Values))
|
.Concat(Enumerate(catalog.Countries.Values))
|
||||||
|
.Concat(Enumerate(catalog.ClimatePresets.Values))
|
||||||
.Concat(Enumerate(catalog.Subjects.Values))
|
.Concat(Enumerate(catalog.Subjects.Values))
|
||||||
.Concat(Enumerate(catalog.Staffing.Values))
|
.Concat(Enumerate(catalog.Staffing.Values))
|
||||||
.Concat(Enumerate(catalog.DayFrames.Values))
|
.Concat(Enumerate(catalog.DayFrames.Values))
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ public sealed class DefCatalog
|
|||||||
IReadOnlyDictionary<string, TraitDef> traits,
|
IReadOnlyDictionary<string, TraitDef> traits,
|
||||||
IReadOnlyDictionary<string, BodyAttributeDef> bodyAttributes,
|
IReadOnlyDictionary<string, BodyAttributeDef> bodyAttributes,
|
||||||
IReadOnlyDictionary<string, NeedDef> needs,
|
IReadOnlyDictionary<string, NeedDef> needs,
|
||||||
IReadOnlyDictionary<string, NameSetDef> nameSets,
|
IReadOnlyDictionary<string, CountryDef> countries,
|
||||||
|
IReadOnlyDictionary<string, ClimatePresetDef> climatePresets,
|
||||||
IReadOnlyDictionary<string, SubjectDef> subjects,
|
IReadOnlyDictionary<string, SubjectDef> subjects,
|
||||||
IReadOnlyDictionary<string, StaffingDef> staffing,
|
IReadOnlyDictionary<string, StaffingDef> staffing,
|
||||||
IReadOnlyDictionary<string, DayFrameDef> dayFrames,
|
IReadOnlyDictionary<string, DayFrameDef> dayFrames,
|
||||||
@@ -43,7 +44,8 @@ public sealed class DefCatalog
|
|||||||
Traits = traits;
|
Traits = traits;
|
||||||
BodyAttributes = bodyAttributes;
|
BodyAttributes = bodyAttributes;
|
||||||
Needs = needs;
|
Needs = needs;
|
||||||
NameSets = nameSets;
|
Countries = countries;
|
||||||
|
ClimatePresets = climatePresets;
|
||||||
Subjects = subjects;
|
Subjects = subjects;
|
||||||
Staffing = staffing;
|
Staffing = staffing;
|
||||||
DayFrames = dayFrames;
|
DayFrames = dayFrames;
|
||||||
@@ -91,7 +93,9 @@ public sealed class DefCatalog
|
|||||||
|
|
||||||
public IReadOnlyDictionary<string, NeedDef> Needs { get; }
|
public IReadOnlyDictionary<string, NeedDef> Needs { get; }
|
||||||
|
|
||||||
public IReadOnlyDictionary<string, NameSetDef> NameSets { get; }
|
public IReadOnlyDictionary<string, CountryDef> Countries { get; }
|
||||||
|
|
||||||
|
public IReadOnlyDictionary<string, ClimatePresetDef> ClimatePresets { get; }
|
||||||
|
|
||||||
public IReadOnlyDictionary<string, SubjectDef> Subjects { get; }
|
public IReadOnlyDictionary<string, SubjectDef> Subjects { get; }
|
||||||
|
|
||||||
@@ -133,7 +137,8 @@ public sealed class DefCatalog
|
|||||||
DefKind.Trait => Traits.GetValueOrDefault(defName),
|
DefKind.Trait => Traits.GetValueOrDefault(defName),
|
||||||
DefKind.BodyAttribute => BodyAttributes.GetValueOrDefault(defName),
|
DefKind.BodyAttribute => BodyAttributes.GetValueOrDefault(defName),
|
||||||
DefKind.Need => Needs.GetValueOrDefault(defName),
|
DefKind.Need => Needs.GetValueOrDefault(defName),
|
||||||
DefKind.NameSet => NameSets.GetValueOrDefault(defName),
|
DefKind.Country => Countries.GetValueOrDefault(defName),
|
||||||
|
DefKind.ClimatePreset => ClimatePresets.GetValueOrDefault(defName),
|
||||||
DefKind.Subject => Subjects.GetValueOrDefault(defName),
|
DefKind.Subject => Subjects.GetValueOrDefault(defName),
|
||||||
DefKind.Staffing => Staffing.GetValueOrDefault(defName),
|
DefKind.Staffing => Staffing.GetValueOrDefault(defName),
|
||||||
DefKind.DayFrame => DayFrames.GetValueOrDefault(defName),
|
DefKind.DayFrame => DayFrames.GetValueOrDefault(defName),
|
||||||
@@ -212,7 +217,8 @@ public sealed class DefCatalog
|
|||||||
TraitDef => DefKind.Trait,
|
TraitDef => DefKind.Trait,
|
||||||
BodyAttributeDef => DefKind.BodyAttribute,
|
BodyAttributeDef => DefKind.BodyAttribute,
|
||||||
NeedDef => DefKind.Need,
|
NeedDef => DefKind.Need,
|
||||||
NameSetDef => DefKind.NameSet,
|
CountryDef => DefKind.Country,
|
||||||
|
ClimatePresetDef => DefKind.ClimatePreset,
|
||||||
SubjectDef => DefKind.Subject,
|
SubjectDef => DefKind.Subject,
|
||||||
StaffingDef => DefKind.Staffing,
|
StaffingDef => DefKind.Staffing,
|
||||||
DayFrameDef => DefKind.DayFrame,
|
DayFrameDef => DefKind.DayFrame,
|
||||||
@@ -245,4 +251,17 @@ public sealed class DefCatalog
|
|||||||
set.Remove(defName);
|
set.Remove(defName);
|
||||||
return set;
|
return set;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Nested names of a concrete country, or false when the id is missing or abstract.</summary>
|
||||||
|
public bool TryGetCountryNames(string countryId, out NameSetDef names)
|
||||||
|
{
|
||||||
|
if (Countries.TryGetValue(countryId, out var country) && !country.Abstract)
|
||||||
|
{
|
||||||
|
names = country.Names;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
names = null!;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ public enum DefKind
|
|||||||
Trait,
|
Trait,
|
||||||
BodyAttribute,
|
BodyAttribute,
|
||||||
Need,
|
Need,
|
||||||
NameSet,
|
Country,
|
||||||
|
ClimatePreset,
|
||||||
Subject,
|
Subject,
|
||||||
Staffing,
|
Staffing,
|
||||||
DayFrame,
|
DayFrame,
|
||||||
|
|||||||
@@ -105,8 +105,11 @@ internal static class PackPaths
|
|||||||
case "needs":
|
case "needs":
|
||||||
kind = DefKind.Need;
|
kind = DefKind.Need;
|
||||||
return true;
|
return true;
|
||||||
case "namesets":
|
case "countries":
|
||||||
kind = DefKind.NameSet;
|
kind = DefKind.Country;
|
||||||
|
return true;
|
||||||
|
case "climates":
|
||||||
|
kind = DefKind.ClimatePreset;
|
||||||
return true;
|
return true;
|
||||||
case "subjects":
|
case "subjects":
|
||||||
kind = DefKind.Subject;
|
kind = DefKind.Subject;
|
||||||
|
|||||||
@@ -25,9 +25,9 @@ internal static class PeopleDefValidator
|
|||||||
ValidateNeed(need);
|
ValidateNeed(need);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var names in catalog.NameSets.Values)
|
foreach (var country in catalog.Countries.Values)
|
||||||
{
|
{
|
||||||
ValidateNameSet(names, catalog);
|
ValidateCountry(country, catalog);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var subject in catalog.Subjects.Values)
|
foreach (var subject in catalog.Subjects.Values)
|
||||||
@@ -547,61 +547,90 @@ internal static class PeopleDefValidator
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ValidateNameSet(NameSetDef names, DefCatalog catalog)
|
private static void ValidateCountry(CountryDef country, DefCatalog catalog)
|
||||||
|
{
|
||||||
|
if (country.Abstract)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (country.ClimatePresets.Count == 0)
|
||||||
|
{
|
||||||
|
throw new ContentLoadException($"CountryDef '{country.DefName}' needs at least one climate preset.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var seen = new HashSet<string>(StringComparer.Ordinal);
|
||||||
|
foreach (var presetId in country.ClimatePresets)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(presetId) || !seen.Add(presetId))
|
||||||
|
{
|
||||||
|
throw new ContentLoadException($"CountryDef '{country.DefName}' has a missing or duplicate climate preset.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!catalog.ClimatePresets.TryGetValue(presetId, out var preset) || preset.Abstract)
|
||||||
|
{
|
||||||
|
throw new ContentLoadException($"CountryDef '{country.DefName}' references unknown ClimatePresetDef '{presetId}'.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidateNameSet(country.Names ?? new NameSetDef(), catalog, country.DefName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateNameSet(NameSetDef names, DefCatalog catalog, string countryDefName)
|
||||||
{
|
{
|
||||||
if (!NameGrammar.IsKnownPatronymic(names.PatronymicRule))
|
if (!NameGrammar.IsKnownPatronymic(names.PatronymicRule))
|
||||||
{
|
{
|
||||||
throw new ContentLoadException($"NameSetDef '{names.DefName}' has unknown patronymicRule '{names.PatronymicRule}'.");
|
throw new ContentLoadException($"CountryDef '{countryDefName}' has unknown patronymicRule '{names.PatronymicRule}'.");
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var native in names.Spoken)
|
foreach (var native in names.Spoken)
|
||||||
{
|
{
|
||||||
if (!catalog.Skills.TryGetValue(native, out var language) || language.Abstract)
|
if (!catalog.Skills.TryGetValue(native, out var language) || language.Abstract)
|
||||||
{
|
{
|
||||||
throw new ContentLoadException($"NameSetDef '{names.DefName}' native language '{native}' is not a SkillDef.");
|
throw new ContentLoadException($"CountryDef '{countryDefName}' native language '{native}' is not a SkillDef.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (names.RelatedLanguageChance is < 0 or > 1)
|
if (names.RelatedLanguageChance is < 0 or > 1)
|
||||||
{
|
{
|
||||||
throw new ContentLoadException($"NameSetDef '{names.DefName}' relatedLanguageChance must be 0–1.");
|
throw new ContentLoadException($"CountryDef '{countryDefName}' relatedLanguageChance must be 0–1.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (names.RelatedLanguageStdDev < 0)
|
if (names.RelatedLanguageStdDev < 0)
|
||||||
{
|
{
|
||||||
throw new ContentLoadException($"NameSetDef '{names.DefName}' relatedLanguageStdDev must not be negative.");
|
throw new ContentLoadException($"CountryDef '{countryDefName}' relatedLanguageStdDev must not be negative.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (names.RelatedLanguageMean < 0)
|
if (names.RelatedLanguageMean < 0)
|
||||||
{
|
{
|
||||||
throw new ContentLoadException($"NameSetDef '{names.DefName}' relatedLanguageMean must not be negative.");
|
throw new ContentLoadException($"CountryDef '{countryDefName}' relatedLanguageMean must not be negative.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (names.RelatedLanguageMax < 0)
|
if (names.RelatedLanguageMax < 0)
|
||||||
{
|
{
|
||||||
throw new ContentLoadException($"NameSetDef '{names.DefName}' relatedLanguageMax must not be negative.");
|
throw new ContentLoadException($"CountryDef '{countryDefName}' relatedLanguageMax must not be negative.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!NameGrammar.IsKnownGiven(names.DefaultGivenDeclension))
|
if (!NameGrammar.IsKnownGiven(names.DefaultGivenDeclension))
|
||||||
{
|
{
|
||||||
throw new ContentLoadException($"NameSetDef '{names.DefName}' has unknown defaultGivenDeclension.");
|
throw new ContentLoadException($"CountryDef '{countryDefName}' has unknown defaultGivenDeclension.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!NameGrammar.IsKnownSurname(names.DefaultSurnameDeclension))
|
if (!NameGrammar.IsKnownSurname(names.DefaultSurnameDeclension))
|
||||||
{
|
{
|
||||||
throw new ContentLoadException($"NameSetDef '{names.DefName}' has unknown defaultSurnameDeclension.");
|
throw new ContentLoadException($"CountryDef '{countryDefName}' has unknown defaultSurnameDeclension.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (names.MaleGiven.Count == 0 || names.FemaleGiven.Count == 0 || names.Surnames.Count == 0)
|
if (names.MaleGiven.Count == 0 || names.FemaleGiven.Count == 0 || names.Surnames.Count == 0)
|
||||||
{
|
{
|
||||||
throw new ContentLoadException($"NameSetDef '{names.DefName}' needs male names, female names and surnames.");
|
throw new ContentLoadException($"CountryDef '{countryDefName}' needs male names, female names and surnames.");
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var given in names.MaleGiven.Concat(names.FemaleGiven))
|
foreach (var given in names.MaleGiven.Concat(names.FemaleGiven))
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(given.Form))
|
if (string.IsNullOrWhiteSpace(given.Form))
|
||||||
{
|
{
|
||||||
throw new ContentLoadException($"NameSetDef '{names.DefName}' has an empty given name.");
|
throw new ContentLoadException($"CountryDef '{countryDefName}' has an empty given name.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (given.Cases is null)
|
if (given.Cases is null)
|
||||||
@@ -611,7 +640,7 @@ internal static class PeopleDefValidator
|
|||||||
: given.Declension;
|
: given.Declension;
|
||||||
if (!NameGrammar.IsKnownGiven(model))
|
if (!NameGrammar.IsKnownGiven(model))
|
||||||
{
|
{
|
||||||
throw new ContentLoadException($"NameSetDef '{names.DefName}' given '{given.Form}' has unknown declension '{model}'.");
|
throw new ContentLoadException($"CountryDef '{countryDefName}' given '{given.Form}' has unknown declension '{model}'.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -620,7 +649,7 @@ internal static class PeopleDefValidator
|
|||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(surname.Male) || string.IsNullOrWhiteSpace(surname.Female))
|
if (string.IsNullOrWhiteSpace(surname.Male) || string.IsNullOrWhiteSpace(surname.Female))
|
||||||
{
|
{
|
||||||
throw new ContentLoadException($"NameSetDef '{names.DefName}' has a surname missing a gendered form.");
|
throw new ContentLoadException($"CountryDef '{countryDefName}' has a surname missing a gendered form.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (surname.MaleCases is null && surname.FemaleCases is null)
|
if (surname.MaleCases is null && surname.FemaleCases is null)
|
||||||
@@ -630,7 +659,7 @@ internal static class PeopleDefValidator
|
|||||||
: surname.Declension;
|
: surname.Declension;
|
||||||
if (!NameGrammar.IsKnownSurname(model))
|
if (!NameGrammar.IsKnownSurname(model))
|
||||||
{
|
{
|
||||||
throw new ContentLoadException($"NameSetDef '{names.DefName}' surname '{surname.Male}' has unknown declension '{model}'.");
|
throw new ContentLoadException($"CountryDef '{countryDefName}' surname '{surname.Male}' has unknown declension '{model}'.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -359,7 +359,10 @@ public sealed class SurnameEntry
|
|||||||
public CaseTable? FemaleCases { get; init; }
|
public CaseTable? FemaleCases { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class NameSetDef : Def
|
/// <summary>
|
||||||
|
/// Nested name grammar of a <see cref="CountryDef"/>. Not a selectable catalog kind of its own.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class NameSetDef
|
||||||
{
|
{
|
||||||
public string PatronymicRule { get; init; } = NameGrammar.SlavicPatronymic;
|
public string PatronymicRule { get; init; } = NameGrammar.SlavicPatronymic;
|
||||||
|
|
||||||
@@ -368,7 +371,7 @@ public sealed class NameSetDef : Def
|
|||||||
public string DefaultSurnameDeclension { get; init; } = NameGrammar.Ov;
|
public string DefaultSurnameDeclension { get; init; } = NameGrammar.Ov;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Languages this set can speak natively. Slavic names cover Russian, Belarusian and
|
/// Languages this country can speak natively. Slavic names cover Russian, Belarusian and
|
||||||
/// Ukrainian; the school picks one at create. Singular <see cref="NativeLanguage"/> is still
|
/// Ukrainian; the school picks one at create. Singular <see cref="NativeLanguage"/> is still
|
||||||
/// accepted in JSONC for a one-language pack.
|
/// accepted in JSONC for a one-language pack.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -401,3 +404,20 @@ public sealed class NameSetDef : Def
|
|||||||
|
|
||||||
public IReadOnlyList<SurnameEntry> Surnames { get; init; } = [];
|
public IReadOnlyList<SurnameEntry> Surnames { get; init; } = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// What the player picks at create: nested names plus climate-preset ids. Weather numbers live
|
||||||
|
/// on <see cref="ClimatePresetDef"/> and stay unused until phase 32.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class CountryDef : Def
|
||||||
|
{
|
||||||
|
public IReadOnlyList<string> ClimatePresets { get; init; } = [];
|
||||||
|
|
||||||
|
public NameSetDef Names { get; init; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Outdoor climate a country may roll. Monthly temperatures land in phase 32; the id is enough
|
||||||
|
/// to persist which preset a school was born with.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ClimatePresetDef : Def;
|
||||||
|
|||||||
@@ -15,12 +15,12 @@ public sealed record ApplicantPool(int Week, int NextIndex, IReadOnlyList<Applic
|
|||||||
DefCatalog catalog,
|
DefCatalog catalog,
|
||||||
Roster roster,
|
Roster roster,
|
||||||
int schoolSeed,
|
int schoolSeed,
|
||||||
string nameSetId,
|
string countryId,
|
||||||
DateTime asOf,
|
DateTime asOf,
|
||||||
string? nativeLanguage = null)
|
string? nativeLanguage = null)
|
||||||
{
|
{
|
||||||
var week = ApplicantWeeks.Id(asOf);
|
var week = ApplicantWeeks.Id(asOf);
|
||||||
return Fill(catalog, roster, schoolSeed, nameSetId, asOf, new ApplicantPool(week, 0, []), nativeLanguage: nativeLanguage);
|
return Fill(catalog, roster, schoolSeed, countryId, asOf, new ApplicantPool(week, 0, []), nativeLanguage: nativeLanguage);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -31,7 +31,7 @@ public sealed record ApplicantPool(int Week, int NextIndex, IReadOnlyList<Applic
|
|||||||
DefCatalog catalog,
|
DefCatalog catalog,
|
||||||
Roster roster,
|
Roster roster,
|
||||||
int schoolSeed,
|
int schoolSeed,
|
||||||
string nameSetId,
|
string countryId,
|
||||||
DateTime asOf,
|
DateTime asOf,
|
||||||
string? nativeLanguage = null)
|
string? nativeLanguage = null)
|
||||||
{
|
{
|
||||||
@@ -44,7 +44,7 @@ public sealed record ApplicantPool(int Week, int NextIndex, IReadOnlyList<Applic
|
|||||||
var pool = this;
|
var pool = this;
|
||||||
for (var week = Week + 1; week <= target; week++)
|
for (var week = Week + 1; week <= target; week++)
|
||||||
{
|
{
|
||||||
pool = Step(pool, catalog, roster, schoolSeed, nameSetId, asOf, week, nativeLanguage);
|
pool = Step(pool, catalog, roster, schoolSeed, countryId, asOf, week, nativeLanguage);
|
||||||
}
|
}
|
||||||
|
|
||||||
return pool;
|
return pool;
|
||||||
@@ -93,7 +93,7 @@ public sealed record ApplicantPool(int Week, int NextIndex, IReadOnlyList<Applic
|
|||||||
DefCatalog catalog,
|
DefCatalog catalog,
|
||||||
Roster roster,
|
Roster roster,
|
||||||
int schoolSeed,
|
int schoolSeed,
|
||||||
string nameSetId,
|
string countryId,
|
||||||
DateTime asOf,
|
DateTime asOf,
|
||||||
int week,
|
int week,
|
||||||
string? nativeLanguage)
|
string? nativeLanguage)
|
||||||
@@ -113,7 +113,7 @@ public sealed record ApplicantPool(int Week, int NextIndex, IReadOnlyList<Applic
|
|||||||
catalog,
|
catalog,
|
||||||
roster,
|
roster,
|
||||||
schoolSeed,
|
schoolSeed,
|
||||||
nameSetId,
|
countryId,
|
||||||
asOf,
|
asOf,
|
||||||
new ApplicantPool(week, current.NextIndex, staying),
|
new ApplicantPool(week, current.NextIndex, staying),
|
||||||
rng,
|
rng,
|
||||||
@@ -124,16 +124,16 @@ public sealed record ApplicantPool(int Week, int NextIndex, IReadOnlyList<Applic
|
|||||||
DefCatalog catalog,
|
DefCatalog catalog,
|
||||||
Roster roster,
|
Roster roster,
|
||||||
int schoolSeed,
|
int schoolSeed,
|
||||||
string nameSetId,
|
string countryId,
|
||||||
DateTime asOf,
|
DateTime asOf,
|
||||||
ApplicantPool current,
|
ApplicantPool current,
|
||||||
Random? rng = null,
|
Random? rng = null,
|
||||||
string? nativeLanguage = null)
|
string? nativeLanguage = null)
|
||||||
{
|
{
|
||||||
var rules = RequireRules(catalog);
|
var rules = RequireRules(catalog);
|
||||||
if (!catalog.NameSets.TryGetValue(nameSetId, out var names))
|
if (!catalog.TryGetCountryNames(countryId, out var names))
|
||||||
{
|
{
|
||||||
throw new ArgumentException($"Unknown name set '{nameSetId}'.", nameof(nameSetId));
|
throw new ArgumentException($"Unknown country '{countryId}'.", nameof(countryId));
|
||||||
}
|
}
|
||||||
|
|
||||||
var native = NativeLanguages.Pick(names, schoolSeed, nativeLanguage, rollIfOmitted: false);
|
var native = NativeLanguages.Pick(names, schoolSeed, nativeLanguage, rollIfOmitted: false);
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
namespace HSchool.People;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A country lists climate presets; the school rolls one at birth from the school seed.
|
||||||
|
/// Reloads without a stored pick take the first listed preset so old saves stay put.
|
||||||
|
/// </summary>
|
||||||
|
public static class CountryClimate
|
||||||
|
{
|
||||||
|
public static string? Pick(CountryDef country, int schoolSeed, bool rollIfOmitted)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(country);
|
||||||
|
var presets = country.ClimatePresets;
|
||||||
|
if (presets.Count == 0)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rollIfOmitted || presets.Count == 1)
|
||||||
|
{
|
||||||
|
return presets[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
var rng = new Random(Seed.ForSchool(schoolSeed, Seed.ClimatePresetSalt));
|
||||||
|
return presets[rng.Next(presets.Count)];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,17 +15,17 @@ public static class RosterGenerator
|
|||||||
DefCatalog catalog,
|
DefCatalog catalog,
|
||||||
MapLayout map,
|
MapLayout map,
|
||||||
int schoolSeed,
|
int schoolSeed,
|
||||||
string nameSetId,
|
string countryId,
|
||||||
DateTime? asOf = null,
|
DateTime? asOf = null,
|
||||||
string? nativeLanguage = null)
|
string? nativeLanguage = null)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(catalog);
|
ArgumentNullException.ThrowIfNull(catalog);
|
||||||
ArgumentNullException.ThrowIfNull(map);
|
ArgumentNullException.ThrowIfNull(map);
|
||||||
ArgumentException.ThrowIfNullOrWhiteSpace(nameSetId);
|
ArgumentException.ThrowIfNullOrWhiteSpace(countryId);
|
||||||
|
|
||||||
if (!catalog.NameSets.TryGetValue(nameSetId, out var names))
|
if (!catalog.TryGetCountryNames(countryId, out var names))
|
||||||
{
|
{
|
||||||
throw new ArgumentException($"Unknown name set '{nameSetId}'.", nameof(nameSetId));
|
throw new ArgumentException($"Unknown country '{countryId}'.", nameof(countryId));
|
||||||
}
|
}
|
||||||
|
|
||||||
var native = NativeLanguages.Pick(names, schoolSeed, nativeLanguage, rollIfOmitted: true);
|
var native = NativeLanguages.Pick(names, schoolSeed, nativeLanguage, rollIfOmitted: true);
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ public static class Seed
|
|||||||
public const int CommuteSalt = 7;
|
public const int CommuteSalt = 7;
|
||||||
public const int SkillGrantSalt = 8;
|
public const int SkillGrantSalt = 8;
|
||||||
public const int NativeLanguageSalt = 9;
|
public const int NativeLanguageSalt = 9;
|
||||||
|
public const int ClimatePresetSalt = 10;
|
||||||
|
|
||||||
/// <summary>A stream that belongs to the school rather than to one family.</summary>
|
/// <summary>A stream that belongs to the school rather than to one family.</summary>
|
||||||
public static int ForSchool(int schoolSeed, int salt) => Mix(schoolSeed, familyIndex: -1, salt);
|
public static int ForSchool(int schoolSeed, int salt) => Mix(schoolSeed, familyIndex: -1, salt);
|
||||||
|
|||||||
@@ -38,17 +38,17 @@ public static class YearlyIntake
|
|||||||
DefCatalog catalog,
|
DefCatalog catalog,
|
||||||
Roster roster,
|
Roster roster,
|
||||||
int schoolSeed,
|
int schoolSeed,
|
||||||
string nameSetId,
|
string countryId,
|
||||||
DateTime asOf,
|
DateTime asOf,
|
||||||
string? nativeLanguage = null)
|
string? nativeLanguage = null)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(catalog);
|
ArgumentNullException.ThrowIfNull(catalog);
|
||||||
ArgumentNullException.ThrowIfNull(roster);
|
ArgumentNullException.ThrowIfNull(roster);
|
||||||
ArgumentException.ThrowIfNullOrWhiteSpace(nameSetId);
|
ArgumentException.ThrowIfNullOrWhiteSpace(countryId);
|
||||||
|
|
||||||
if (!catalog.NameSets.TryGetValue(nameSetId, out var names))
|
if (!catalog.TryGetCountryNames(countryId, out var names))
|
||||||
{
|
{
|
||||||
throw new ArgumentException($"Unknown name set '{nameSetId}'.", nameof(nameSetId));
|
throw new ArgumentException($"Unknown country '{countryId}'.", nameof(countryId));
|
||||||
}
|
}
|
||||||
|
|
||||||
var native = NativeLanguages.Pick(names, schoolSeed, nativeLanguage, rollIfOmitted: false);
|
var native = NativeLanguages.Pick(names, schoolSeed, nativeLanguage, rollIfOmitted: false);
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ internal sealed record CatalogResponse(
|
|||||||
IReadOnlyList<DefInfoResponse> Floors,
|
IReadOnlyList<DefInfoResponse> Floors,
|
||||||
IReadOnlyList<RoomInfoResponse> Rooms,
|
IReadOnlyList<RoomInfoResponse> Rooms,
|
||||||
IReadOnlyList<DefInfoResponse> Things,
|
IReadOnlyList<DefInfoResponse> Things,
|
||||||
IReadOnlyList<NameSetInfoResponse> NameSets,
|
IReadOnlyList<CountryInfoResponse> Countries,
|
||||||
IReadOnlyList<SubjectInfoResponse> Subjects,
|
IReadOnlyList<SubjectInfoResponse> Subjects,
|
||||||
DayFrameResponse? DayFrame,
|
DayFrameResponse? DayFrame,
|
||||||
IReadOnlyList<HolidayInfoResponse> Holidays,
|
IReadOnlyList<HolidayInfoResponse> Holidays,
|
||||||
@@ -135,7 +135,7 @@ internal sealed record CatalogResponse(
|
|||||||
Placeable(catalog.Floors.Values, catalog, locale),
|
Placeable(catalog.Floors.Values, catalog, locale),
|
||||||
PlaceableRooms(catalog, locale),
|
PlaceableRooms(catalog, locale),
|
||||||
PlaceableThings(catalog, locale),
|
PlaceableThings(catalog, locale),
|
||||||
PlaceableNameSets(catalog, locale),
|
PlaceableCountries(catalog, locale),
|
||||||
PlaceableSubjects(catalog, locale),
|
PlaceableSubjects(catalog, locale),
|
||||||
MapDayFrame(catalog, locale),
|
MapDayFrame(catalog, locale),
|
||||||
PlaceableHolidays(catalog, locale),
|
PlaceableHolidays(catalog, locale),
|
||||||
@@ -149,20 +149,21 @@ internal sealed record CatalogResponse(
|
|||||||
.Select(def => new DefInfoResponse(def.DefName, catalog.Label(locale, def)))
|
.Select(def => new DefInfoResponse(def.DefName, catalog.Label(locale, def)))
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
private static IReadOnlyList<NameSetInfoResponse> PlaceableNameSets(DefCatalog catalog, string locale) =>
|
private static IReadOnlyList<CountryInfoResponse> PlaceableCountries(DefCatalog catalog, string locale) =>
|
||||||
catalog.NameSets.Values
|
catalog.Countries.Values
|
||||||
.Where(def => !def.Abstract)
|
.Where(def => !def.Abstract)
|
||||||
.OrderBy(def => def.DefName, StringComparer.Ordinal)
|
.OrderBy(def => def.DefName, StringComparer.Ordinal)
|
||||||
.Select(def => new NameSetInfoResponse(
|
.Select(def => new CountryInfoResponse(
|
||||||
def.DefName,
|
def.DefName,
|
||||||
catalog.Label(locale, def),
|
catalog.Label(locale, def),
|
||||||
def.Spoken
|
def.Names.Spoken
|
||||||
.Select(skill => new DefInfoResponse(
|
.Select(skill => new DefInfoResponse(
|
||||||
skill,
|
skill,
|
||||||
catalog.Skills.TryGetValue(skill, out var language)
|
catalog.Skills.TryGetValue(skill, out var language)
|
||||||
? catalog.Label(locale, language)
|
? catalog.Label(locale, language)
|
||||||
: skill))
|
: skill))
|
||||||
.ToArray()))
|
.ToArray(),
|
||||||
|
def.ClimatePresets.ToArray()))
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
private static IReadOnlyList<DefInfoResponse> PlaceableThings(DefCatalog catalog, string locale) =>
|
private static IReadOnlyList<DefInfoResponse> PlaceableThings(DefCatalog catalog, string locale) =>
|
||||||
@@ -230,10 +231,11 @@ internal sealed record CatalogResponse(
|
|||||||
|
|
||||||
internal sealed record DefInfoResponse(string DefName, string Label, int PupilSlots = 0);
|
internal sealed record DefInfoResponse(string DefName, string Label, int PupilSlots = 0);
|
||||||
|
|
||||||
internal sealed record NameSetInfoResponse(
|
internal sealed record CountryInfoResponse(
|
||||||
string DefName,
|
string DefName,
|
||||||
string Label,
|
string Label,
|
||||||
IReadOnlyList<DefInfoResponse> NativeLanguages);
|
IReadOnlyList<DefInfoResponse> NativeLanguages,
|
||||||
|
IReadOnlyList<string> ClimatePresets);
|
||||||
|
|
||||||
internal sealed record RoomInfoResponse(
|
internal sealed record RoomInfoResponse(
|
||||||
string DefName,
|
string DefName,
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ internal static class SchoolEndpoints
|
|||||||
DateTime.SpecifyKind(request.StartDate, DateTimeKind.Utc),
|
DateTime.SpecifyKind(request.StartDate, DateTimeKind.Utc),
|
||||||
request.ModIds,
|
request.ModIds,
|
||||||
request.Map,
|
request.Map,
|
||||||
request.NameSetId,
|
request.CountryId,
|
||||||
request.NativeLanguage,
|
request.NativeLanguage,
|
||||||
request.Seed,
|
request.Seed,
|
||||||
NewCompletion<SchoolCreationOutcome>());
|
NewCompletion<SchoolCreationOutcome>());
|
||||||
@@ -77,10 +77,10 @@ internal static class SchoolEndpoints
|
|||||||
Problem(StatusCodes.Status400BadRequest, "unknown-mod", "A selected mod is missing."),
|
Problem(StatusCodes.Status400BadRequest, "unknown-mod", "A selected mod is missing."),
|
||||||
SchoolCreationError.InvalidCatalog =>
|
SchoolCreationError.InvalidCatalog =>
|
||||||
Problem(StatusCodes.Status400BadRequest, "invalid-catalog", "The selected packs could not be loaded."),
|
Problem(StatusCodes.Status400BadRequest, "invalid-catalog", "The selected packs could not be loaded."),
|
||||||
SchoolCreationError.UnknownNameSet =>
|
SchoolCreationError.UnknownCountry =>
|
||||||
Problem(StatusCodes.Status400BadRequest, "unknown-name-set", "The selected name set is not in the catalog."),
|
Problem(StatusCodes.Status400BadRequest, "unknown-country", "The selected country is not in the catalog."),
|
||||||
SchoolCreationError.UnknownNativeLanguage =>
|
SchoolCreationError.UnknownNativeLanguage =>
|
||||||
Problem(StatusCodes.Status400BadRequest, "unknown-native-language", "The selected native language is not in that name set."),
|
Problem(StatusCodes.Status400BadRequest, "unknown-native-language", "The selected native language is not in that country."),
|
||||||
SchoolCreationError.MissingMod =>
|
SchoolCreationError.MissingMod =>
|
||||||
Problem(
|
Problem(
|
||||||
StatusCodes.Status400BadRequest,
|
StatusCodes.Status400BadRequest,
|
||||||
@@ -508,7 +508,7 @@ internal sealed record CreateSchoolRequest(
|
|||||||
DateTime StartDate,
|
DateTime StartDate,
|
||||||
IReadOnlyList<string>? ModIds,
|
IReadOnlyList<string>? ModIds,
|
||||||
MapLayout? Map,
|
MapLayout? Map,
|
||||||
string? NameSetId,
|
string? CountryId,
|
||||||
string? NativeLanguage,
|
string? NativeLanguage,
|
||||||
int? Seed);
|
int? Seed);
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ internal abstract record GameCommand
|
|||||||
DateTime StartDate,
|
DateTime StartDate,
|
||||||
IReadOnlyList<string>? ExtraModIds,
|
IReadOnlyList<string>? ExtraModIds,
|
||||||
MapLayout? Map,
|
MapLayout? Map,
|
||||||
string? NameSetId,
|
string? CountryId,
|
||||||
string? NativeLanguage,
|
string? NativeLanguage,
|
||||||
int? Seed,
|
int? Seed,
|
||||||
TaskCompletionSource<SchoolCreationOutcome> Result) : GameCommand;
|
TaskCompletionSource<SchoolCreationOutcome> Result) : GameCommand;
|
||||||
|
|||||||
@@ -357,20 +357,14 @@ internal sealed class GameLoopService(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var nameSetId = ResolveNameSetId(catalog, command.NameSetId);
|
var countryId = ResolveCountryId(catalog, command.CountryId);
|
||||||
if (nameSetId is null)
|
if (countryId is null || !catalog.Countries.TryGetValue(countryId, out var country) || country.Abstract)
|
||||||
{
|
{
|
||||||
command.Result.TrySetResult(new SchoolCreationOutcome(null, SchoolCreationError.UnknownNameSet));
|
command.Result.TrySetResult(new SchoolCreationOutcome(null, SchoolCreationError.UnknownCountry));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!catalog.NameSets.TryGetValue(nameSetId, out var names))
|
if (!string.IsNullOrWhiteSpace(command.NativeLanguage) && !NativeLanguages.Allows(country.Names, command.NativeLanguage))
|
||||||
{
|
|
||||||
command.Result.TrySetResult(new SchoolCreationOutcome(null, SchoolCreationError.UnknownNameSet));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(command.NativeLanguage) && !NativeLanguages.Allows(names, command.NativeLanguage))
|
|
||||||
{
|
{
|
||||||
command.Result.TrySetResult(new SchoolCreationOutcome(null, SchoolCreationError.UnknownNativeLanguage));
|
command.Result.TrySetResult(new SchoolCreationOutcome(null, SchoolCreationError.UnknownNativeLanguage));
|
||||||
return;
|
return;
|
||||||
@@ -379,9 +373,10 @@ internal sealed class GameLoopService(
|
|||||||
var id = _nextId++;
|
var id = _nextId++;
|
||||||
store.WriteNextId(_nextId);
|
store.WriteNextId(_nextId);
|
||||||
var seed = command.Seed ?? Random.Shared.Next();
|
var seed = command.Seed ?? Random.Shared.Next();
|
||||||
var nativeLanguage = NativeLanguages.Pick(names, seed, command.NativeLanguage, rollIfOmitted: true);
|
var nativeLanguage = NativeLanguages.Pick(country.Names, seed, command.NativeLanguage, rollIfOmitted: true);
|
||||||
|
var climatePresetId = CountryClimate.Pick(country, seed, rollIfOmitted: true);
|
||||||
|
|
||||||
var worker = SpawnWorker(id, normalized, command.StartDate, running: true, ClockSpeed.DefaultIndex, isNew: true, packIds, command.Map, nameSetId, nativeLanguage, seed);
|
var worker = SpawnWorker(id, normalized, command.StartDate, running: true, ClockSpeed.DefaultIndex, isNew: true, packIds, command.Map, countryId, climatePresetId, nativeLanguage, seed);
|
||||||
Track(worker);
|
Track(worker);
|
||||||
worker.Start();
|
worker.Start();
|
||||||
|
|
||||||
@@ -548,7 +543,8 @@ internal sealed class GameLoopService(
|
|||||||
isNew: false,
|
isNew: false,
|
||||||
save.ModIds,
|
save.ModIds,
|
||||||
save.Map,
|
save.Map,
|
||||||
save.NameSetId,
|
save.CountryId,
|
||||||
|
save.ClimatePresetId,
|
||||||
save.NativeLanguage,
|
save.NativeLanguage,
|
||||||
createSeed: null,
|
createSeed: null,
|
||||||
save.Presence);
|
save.Presence);
|
||||||
@@ -598,7 +594,8 @@ internal sealed class GameLoopService(
|
|||||||
bool isNew,
|
bool isNew,
|
||||||
IReadOnlyList<string>? modIds,
|
IReadOnlyList<string>? modIds,
|
||||||
MapLayout? map,
|
MapLayout? map,
|
||||||
string? nameSetId,
|
string? countryId,
|
||||||
|
string? climatePresetId,
|
||||||
string? nativeLanguage,
|
string? nativeLanguage,
|
||||||
int? createSeed = null,
|
int? createSeed = null,
|
||||||
IReadOnlyList<PresenceSnapshot>? presence = null) =>
|
IReadOnlyList<PresenceSnapshot>? presence = null) =>
|
||||||
@@ -611,7 +608,8 @@ internal sealed class GameLoopService(
|
|||||||
isNew,
|
isNew,
|
||||||
modIds,
|
modIds,
|
||||||
map,
|
map,
|
||||||
nameSetId,
|
countryId,
|
||||||
|
climatePresetId,
|
||||||
nativeLanguage,
|
nativeLanguage,
|
||||||
createSeed,
|
createSeed,
|
||||||
presence,
|
presence,
|
||||||
@@ -624,12 +622,12 @@ internal sealed class GameLoopService(
|
|||||||
loggerFactory.CreateLogger($"HSchool.Server.Game.SchoolWorker.{id}"));
|
loggerFactory.CreateLogger($"HSchool.Server.Game.SchoolWorker.{id}"));
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Empty request uses the first placeable set (core's Slavic). A named id must exist in the
|
/// Empty request uses the first placeable country (core's Russia). A named id must exist in
|
||||||
/// catalog already loaded for this pack list — unknown extras were rejected above.
|
/// the catalog already loaded for this pack list — unknown extras were rejected above.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal static string? ResolveNameSetId(DefCatalog catalog, string? requested)
|
internal static string? ResolveCountryId(DefCatalog catalog, string? requested)
|
||||||
{
|
{
|
||||||
var available = catalog.NameSets.Values
|
var available = catalog.Countries.Values
|
||||||
.Where(def => !def.Abstract)
|
.Where(def => !def.Abstract)
|
||||||
.Select(def => def.DefName)
|
.Select(def => def.DefName)
|
||||||
.OrderBy(name => name, StringComparer.Ordinal)
|
.OrderBy(name => name, StringComparer.Ordinal)
|
||||||
|
|||||||
@@ -26,6 +26,11 @@ internal sealed class SchoolSave
|
|||||||
|
|
||||||
public MapLayout? Map { get; init; }
|
public MapLayout? Map { get; init; }
|
||||||
|
|
||||||
|
public string? CountryId { get; init; }
|
||||||
|
|
||||||
|
public string? ClimatePresetId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Legacy field. Format 2 and older; mapped to <see cref="CountryId"/> on load.</summary>
|
||||||
public string? NameSetId { get; init; }
|
public string? NameSetId { get; init; }
|
||||||
|
|
||||||
public string? NativeLanguage { get; init; }
|
public string? NativeLanguage { get; init; }
|
||||||
@@ -42,7 +47,7 @@ internal sealed record SchoolSaveIndex(int NextId);
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class SchoolStore
|
internal sealed class SchoolStore
|
||||||
{
|
{
|
||||||
public const int CurrentFormat = 2;
|
public const int CurrentFormat = 3;
|
||||||
|
|
||||||
private const string IndexFileName = "index.json";
|
private const string IndexFileName = "index.json";
|
||||||
|
|
||||||
@@ -178,6 +183,8 @@ internal sealed class SchoolStore
|
|||||||
SpeedIndex = save.SpeedIndex,
|
SpeedIndex = save.SpeedIndex,
|
||||||
ModIds = save.ModIds,
|
ModIds = save.ModIds,
|
||||||
Map = save.Map,
|
Map = save.Map,
|
||||||
|
CountryId = save.CountryId,
|
||||||
|
ClimatePresetId = save.ClimatePresetId,
|
||||||
NameSetId = save.NameSetId,
|
NameSetId = save.NameSetId,
|
||||||
NativeLanguage = save.NativeLanguage,
|
NativeLanguage = save.NativeLanguage,
|
||||||
Presence = save.Presence,
|
Presence = save.Presence,
|
||||||
@@ -194,11 +201,40 @@ internal sealed class SchoolStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Named upgrade seam for saves older than <see cref="CurrentFormat"/>. Empty on purpose:
|
/// Format 2 stored a name set. Slavic becomes Russia; any other id is tried as a country
|
||||||
/// missing fields already default and extra fields are ignored. Put a migration here when a
|
/// (the example pack kept its defName). Climate is filled on the worker from the country's
|
||||||
/// format bump actually needs one.
|
/// first preset so the file is not rewritten until the school saves itself.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal static SchoolSave UpgradeOlderSave(SchoolSave save) => save;
|
internal static SchoolSave UpgradeOlderSave(SchoolSave save)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(save.CountryId))
|
||||||
|
{
|
||||||
|
return save;
|
||||||
|
}
|
||||||
|
|
||||||
|
var countryId = save.NameSetId switch
|
||||||
|
{
|
||||||
|
"Slavic" or null or "" => "Russia",
|
||||||
|
_ => save.NameSetId,
|
||||||
|
};
|
||||||
|
|
||||||
|
return new SchoolSave
|
||||||
|
{
|
||||||
|
Format = save.Format,
|
||||||
|
Id = save.Id,
|
||||||
|
Name = save.Name,
|
||||||
|
GameTime = save.GameTime,
|
||||||
|
Running = save.Running,
|
||||||
|
SpeedIndex = save.SpeedIndex,
|
||||||
|
ModIds = save.ModIds,
|
||||||
|
Map = save.Map,
|
||||||
|
CountryId = countryId,
|
||||||
|
ClimatePresetId = save.ClimatePresetId,
|
||||||
|
NameSetId = save.NameSetId,
|
||||||
|
NativeLanguage = save.NativeLanguage,
|
||||||
|
Presence = save.Presence,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public void Save(SchoolSave save)
|
public void Save(SchoolSave save)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ internal sealed class SchoolWorker
|
|||||||
private readonly bool _isNew;
|
private readonly bool _isNew;
|
||||||
private readonly IReadOnlyList<string>? _modIds;
|
private readonly IReadOnlyList<string>? _modIds;
|
||||||
private readonly MapLayout? _savedMap;
|
private readonly MapLayout? _savedMap;
|
||||||
private readonly string? _nameSetId;
|
private readonly string? _countryId;
|
||||||
|
private string? _climatePresetId;
|
||||||
private string? _nativeLanguage;
|
private string? _nativeLanguage;
|
||||||
private readonly int? _createSeed;
|
private readonly int? _createSeed;
|
||||||
private readonly IReadOnlyList<PresenceSnapshot>? _savedPresence;
|
private readonly IReadOnlyList<PresenceSnapshot>? _savedPresence;
|
||||||
@@ -66,7 +67,8 @@ internal sealed class SchoolWorker
|
|||||||
bool isNew,
|
bool isNew,
|
||||||
IReadOnlyList<string>? modIds,
|
IReadOnlyList<string>? modIds,
|
||||||
MapLayout? savedMap,
|
MapLayout? savedMap,
|
||||||
string? nameSetId,
|
string? countryId,
|
||||||
|
string? climatePresetId,
|
||||||
string? nativeLanguage,
|
string? nativeLanguage,
|
||||||
int? createSeed,
|
int? createSeed,
|
||||||
IReadOnlyList<PresenceSnapshot>? savedPresence,
|
IReadOnlyList<PresenceSnapshot>? savedPresence,
|
||||||
@@ -86,7 +88,8 @@ internal sealed class SchoolWorker
|
|||||||
_isNew = isNew;
|
_isNew = isNew;
|
||||||
_modIds = modIds;
|
_modIds = modIds;
|
||||||
_savedMap = savedMap;
|
_savedMap = savedMap;
|
||||||
_nameSetId = nameSetId;
|
_countryId = countryId;
|
||||||
|
_climatePresetId = climatePresetId;
|
||||||
_nativeLanguage = nativeLanguage;
|
_nativeLanguage = nativeLanguage;
|
||||||
_createSeed = createSeed;
|
_createSeed = createSeed;
|
||||||
_savedPresence = savedPresence;
|
_savedPresence = savedPresence;
|
||||||
@@ -857,12 +860,19 @@ internal sealed class SchoolWorker
|
|||||||
|
|
||||||
private bool InstallPeople(School school, DefCatalog catalog, MapLayout map)
|
private bool InstallPeople(School school, DefCatalog catalog, MapLayout map)
|
||||||
{
|
{
|
||||||
var nameSetId = ResolveNameSetId(catalog, _nameSetId);
|
var countryId = ResolveCountryId(catalog, _countryId);
|
||||||
if (nameSetId is null)
|
if (countryId is null)
|
||||||
{
|
{
|
||||||
throw new SchoolContentUnavailableException($"School {_id} has no name set in its catalog.");
|
throw new SchoolContentUnavailableException($"School {_id} has no country in its catalog.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!catalog.Countries.TryGetValue(countryId, out var country) || country.Abstract)
|
||||||
|
{
|
||||||
|
throw new SchoolContentUnavailableException($"School {_id} has no country in its catalog.");
|
||||||
|
}
|
||||||
|
|
||||||
|
_climatePresetId = ResolveClimatePreset(country, _climatePresetId);
|
||||||
|
|
||||||
var demand = SchoolDemand.From(catalog, map);
|
var demand = SchoolDemand.From(catalog, map);
|
||||||
Roster roster;
|
Roster roster;
|
||||||
ApplicantPool applicants;
|
ApplicantPool applicants;
|
||||||
@@ -878,10 +888,10 @@ internal sealed class SchoolWorker
|
|||||||
}
|
}
|
||||||
|
|
||||||
seed = createSeed;
|
seed = createSeed;
|
||||||
native = ResolveNative(catalog, nameSetId, seed, _nativeLanguage, generating: true);
|
native = ResolveNative(country, seed, _nativeLanguage, generating: true);
|
||||||
_nativeLanguage = native;
|
_nativeLanguage = native;
|
||||||
roster = RosterGenerator.Generate(catalog, map, seed, nameSetId, school.Clock.Time, native);
|
roster = RosterGenerator.Generate(catalog, map, seed, countryId, school.Clock.Time, native);
|
||||||
applicants = ApplicantPool.Create(catalog, roster, seed, nameSetId, school.Clock.Time, native);
|
applicants = ApplicantPool.Create(catalog, roster, seed, countryId, school.Clock.Time, native);
|
||||||
generated = true;
|
generated = true;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -890,16 +900,16 @@ internal sealed class SchoolWorker
|
|||||||
if (loaded is null)
|
if (loaded is null)
|
||||||
{
|
{
|
||||||
seed = school.Id;
|
seed = school.Id;
|
||||||
native = ResolveNative(catalog, nameSetId, seed, _nativeLanguage, generating: true);
|
native = ResolveNative(country, seed, _nativeLanguage, generating: true);
|
||||||
_nativeLanguage = native;
|
_nativeLanguage = native;
|
||||||
roster = RosterGenerator.Generate(catalog, map, seed, nameSetId, school.Clock.Time, native);
|
roster = RosterGenerator.Generate(catalog, map, seed, countryId, school.Clock.Time, native);
|
||||||
applicants = ApplicantPool.Create(catalog, roster, seed, nameSetId, school.Clock.Time, native);
|
applicants = ApplicantPool.Create(catalog, roster, seed, countryId, school.Clock.Time, native);
|
||||||
generated = true;
|
generated = true;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
seed = loaded.Seed;
|
seed = loaded.Seed;
|
||||||
native = ResolveNative(catalog, nameSetId, seed, _nativeLanguage, generating: false);
|
native = ResolveNative(country, seed, _nativeLanguage, generating: false);
|
||||||
_nativeLanguage = native;
|
_nativeLanguage = native;
|
||||||
roster = loaded.ToRoster();
|
roster = loaded.ToRoster();
|
||||||
if (loaded.Applicants is { Applicants.Count: > 0 })
|
if (loaded.Applicants is { Applicants.Count: > 0 })
|
||||||
@@ -908,7 +918,7 @@ internal sealed class SchoolWorker
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
applicants = ApplicantPool.Create(catalog, roster, seed, nameSetId, school.Clock.Time, native);
|
applicants = ApplicantPool.Create(catalog, roster, seed, countryId, school.Clock.Time, native);
|
||||||
generated = true;
|
generated = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -920,47 +930,41 @@ internal sealed class SchoolWorker
|
|||||||
$"School {_id} roster does not match its map; the people file was left untouched.");
|
$"School {_id} roster does not match its map; the people file was left untouched.");
|
||||||
}
|
}
|
||||||
|
|
||||||
school.InstallPeople(roster, seed, nameSetId, applicants, _nativeLanguage);
|
school.InstallPeople(roster, seed, countryId, applicants, _nativeLanguage, _climatePresetId);
|
||||||
InstallTimetable(school);
|
InstallTimetable(school);
|
||||||
school.ConfigurePresence(_options.SchoolWeekDays, _options.MaxDecisionsPerTick);
|
school.ConfigurePresence(_options.SchoolWeekDays, _options.MaxDecisionsPerTick);
|
||||||
school.RestorePresence(_savedPresence);
|
school.RestorePresence(_savedPresence);
|
||||||
return generated;
|
return generated;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string? ResolveNameSetId(DefCatalog catalog, string? requested)
|
private static string? ResolveCountryId(DefCatalog catalog, string? requested)
|
||||||
{
|
{
|
||||||
var available = catalog.NameSets.Values
|
if (string.IsNullOrWhiteSpace(requested))
|
||||||
.Where(def => !def.Abstract)
|
|
||||||
.Select(def => def.DefName)
|
|
||||||
.OrderBy(name => name, StringComparer.Ordinal)
|
|
||||||
.ToArray();
|
|
||||||
if (available.Length == 0)
|
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(requested))
|
return catalog.Countries.TryGetValue(requested, out var country) && !country.Abstract
|
||||||
|
? requested
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? ResolveClimatePreset(CountryDef country, string? requested)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(requested) && country.ClimatePresets.Contains(requested, StringComparer.Ordinal))
|
||||||
{
|
{
|
||||||
return available[0];
|
return requested;
|
||||||
}
|
}
|
||||||
|
|
||||||
return available.Contains(requested, StringComparer.Ordinal) ? requested : null;
|
return CountryClimate.Pick(country, schoolSeed: 0, rollIfOmitted: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string? ResolveNative(
|
private static string? ResolveNative(
|
||||||
DefCatalog catalog,
|
CountryDef country,
|
||||||
string nameSetId,
|
|
||||||
int schoolSeed,
|
int schoolSeed,
|
||||||
string? requested,
|
string? requested,
|
||||||
bool generating)
|
bool generating) =>
|
||||||
{
|
NativeLanguages.Pick(country.Names, schoolSeed, requested, rollIfOmitted: generating && string.IsNullOrWhiteSpace(requested));
|
||||||
if (!catalog.NameSets.TryGetValue(nameSetId, out var names))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return NativeLanguages.Pick(names, schoolSeed, requested, rollIfOmitted: generating && string.IsNullOrWhiteSpace(requested));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Persist()
|
private void Persist()
|
||||||
{
|
{
|
||||||
@@ -983,7 +987,8 @@ internal sealed class SchoolWorker
|
|||||||
SpeedIndex = school.Clock.SpeedIndex,
|
SpeedIndex = school.Clock.SpeedIndex,
|
||||||
ModIds = school.Catalog?.PackIds,
|
ModIds = school.Catalog?.PackIds,
|
||||||
Map = school.Map,
|
Map = school.Map,
|
||||||
NameSetId = _nameSetId,
|
CountryId = school.CountryId,
|
||||||
|
ClimatePresetId = school.ClimatePresetId,
|
||||||
NativeLanguage = _nativeLanguage,
|
NativeLanguage = _nativeLanguage,
|
||||||
Presence = school.CapturePresence(),
|
Presence = school.CapturePresence(),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"defName": "ContinentalCold",
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"defName": "TemperateContinental",
|
||||||
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
{
|
||||||
|
"defName": "Russia",
|
||||||
|
"climatePresets": ["TemperateContinental", "ContinentalCold"],
|
||||||
|
"names": {
|
||||||
|
"patronymicRule": "slavic",
|
||||||
|
"defaultGivenDeclension": "hard",
|
||||||
|
"defaultSurnameDeclension": "ov",
|
||||||
|
"nativeLanguages": ["RussianLanguage", "BelarusianLanguage", "UkrainianLanguage"],
|
||||||
|
"relatedLanguageChance": 0.6,
|
||||||
|
"relatedLanguageMean": 22,
|
||||||
|
"relatedLanguageStdDev": 8,
|
||||||
|
"relatedLanguageMax": 40,
|
||||||
|
"maleGiven": [
|
||||||
|
{ "form": "Александр" },
|
||||||
|
{ "form": "Алексей" },
|
||||||
|
{ "form": "Андрей" },
|
||||||
|
{ "form": "Антон" },
|
||||||
|
{ "form": "Артём" },
|
||||||
|
{ "form": "Борис" },
|
||||||
|
{ "form": "Вадим" },
|
||||||
|
{ "form": "Валерий" },
|
||||||
|
{ "form": "Виктор" },
|
||||||
|
{ "form": "Виталий" },
|
||||||
|
{ "form": "Владимир" },
|
||||||
|
{ "form": "Владислав" },
|
||||||
|
{ "form": "Григорий" },
|
||||||
|
{ "form": "Денис" },
|
||||||
|
{ "form": "Дмитрий" },
|
||||||
|
{ "form": "Евгений" },
|
||||||
|
{ "form": "Егор" },
|
||||||
|
{ "form": "Иван" },
|
||||||
|
{ "form": "Игорь", "declension": "soft" },
|
||||||
|
{ "form": "Илья", "declension": "ya" },
|
||||||
|
{ "form": "Кирилл" },
|
||||||
|
{ "form": "Константин" },
|
||||||
|
{ "form": "Максим" },
|
||||||
|
{ "form": "Михаил" },
|
||||||
|
{ "form": "Никита", "declension": "a" },
|
||||||
|
{ "form": "Николай" },
|
||||||
|
{ "form": "Олег" },
|
||||||
|
{ "form": "Павел" },
|
||||||
|
{ "form": "Пётр" },
|
||||||
|
{ "form": "Роман" },
|
||||||
|
{ "form": "Сергей" },
|
||||||
|
{ "form": "Станислав" },
|
||||||
|
{ "form": "Степан" },
|
||||||
|
{ "form": "Фёдор" },
|
||||||
|
{ "form": "Юрий" },
|
||||||
|
],
|
||||||
|
"femaleGiven": [
|
||||||
|
{ "form": "Александра", "declension": "a" },
|
||||||
|
{ "form": "Алина", "declension": "a" },
|
||||||
|
{ "form": "Анастасия", "declension": "iya" },
|
||||||
|
{ "form": "Анна", "declension": "a" },
|
||||||
|
{ "form": "Валентина", "declension": "a" },
|
||||||
|
{ "form": "Валерия", "declension": "iya" },
|
||||||
|
{ "form": "Вера", "declension": "a" },
|
||||||
|
{ "form": "Виктория", "declension": "iya" },
|
||||||
|
{ "form": "Дарья", "declension": "ya" },
|
||||||
|
{ "form": "Екатерина", "declension": "a" },
|
||||||
|
{ "form": "Елена", "declension": "a" },
|
||||||
|
{ "form": "Елизавета", "declension": "a" },
|
||||||
|
{ "form": "Ирина", "declension": "a" },
|
||||||
|
{ "form": "Ксения", "declension": "iya" },
|
||||||
|
{
|
||||||
|
"form": "Любовь",
|
||||||
|
"cases": {
|
||||||
|
"nom": "Любовь",
|
||||||
|
"gen": "Любови",
|
||||||
|
"dat": "Любови",
|
||||||
|
"acc": "Любовь",
|
||||||
|
"ins": "Любовью",
|
||||||
|
"pre": "Любови",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ "form": "Людмила", "declension": "a" },
|
||||||
|
{ "form": "Маргарита", "declension": "a" },
|
||||||
|
{ "form": "Мария", "declension": "iya" },
|
||||||
|
{ "form": "Надежда", "declension": "a" },
|
||||||
|
{ "form": "Наталья", "declension": "ya" },
|
||||||
|
{ "form": "Нина", "declension": "a" },
|
||||||
|
{ "form": "Оксана", "declension": "a" },
|
||||||
|
{ "form": "Ольга", "declension": "a" },
|
||||||
|
{ "form": "Полина", "declension": "a" },
|
||||||
|
{ "form": "Светлана", "declension": "a" },
|
||||||
|
{ "form": "София", "declension": "iya" },
|
||||||
|
{ "form": "Татьяна", "declension": "a" },
|
||||||
|
{ "form": "Юлия", "declension": "iya" },
|
||||||
|
{ "form": "Яна", "declension": "a" },
|
||||||
|
{ "form": "Вероника", "declension": "a" },
|
||||||
|
{ "form": "Диана", "declension": "a" },
|
||||||
|
{ "form": "Марина", "declension": "a" },
|
||||||
|
],
|
||||||
|
"surnames": [
|
||||||
|
{ "male": "Иванов", "female": "Иванова" },
|
||||||
|
{ "male": "Петров", "female": "Петрова" },
|
||||||
|
{ "male": "Смирнов", "female": "Смирнова" },
|
||||||
|
{ "male": "Кузнецов", "female": "Кузнецова" },
|
||||||
|
{ "male": "Попов", "female": "Попова" },
|
||||||
|
{ "male": "Васильев", "female": "Васильева" },
|
||||||
|
{ "male": "Соколов", "female": "Соколова" },
|
||||||
|
{ "male": "Михайлов", "female": "Михайлова" },
|
||||||
|
{ "male": "Новиков", "female": "Новикова" },
|
||||||
|
{ "male": "Фёдоров", "female": "Фёдорова" },
|
||||||
|
{ "male": "Морозов", "female": "Морозова" },
|
||||||
|
{ "male": "Волков", "female": "Волкова" },
|
||||||
|
{ "male": "Алексеев", "female": "Алексеева" },
|
||||||
|
{ "male": "Лебедев", "female": "Лебедева" },
|
||||||
|
{ "male": "Семёнов", "female": "Семёнова" },
|
||||||
|
{ "male": "Егоров", "female": "Егорова" },
|
||||||
|
{ "male": "Павлов", "female": "Павлова" },
|
||||||
|
{ "male": "Козлов", "female": "Козлова" },
|
||||||
|
{ "male": "Степанов", "female": "Степанова" },
|
||||||
|
{ "male": "Николаев", "female": "Николаева" },
|
||||||
|
{ "male": "Орлов", "female": "Орлова" },
|
||||||
|
{ "male": "Андреев", "female": "Андреева" },
|
||||||
|
{ "male": "Макаров", "female": "Макарова" },
|
||||||
|
{ "male": "Никитин", "female": "Никитина", "declension": "in" },
|
||||||
|
{ "male": "Захаров", "female": "Захарова" },
|
||||||
|
{ "male": "Зайцев", "female": "Зайцева" },
|
||||||
|
{ "male": "Соловьёв", "female": "Соловьёва" },
|
||||||
|
{ "male": "Борисов", "female": "Борисова" },
|
||||||
|
{ "male": "Яковлев", "female": "Яковлева" },
|
||||||
|
{ "male": "Григорьев", "female": "Григорьева" },
|
||||||
|
{ "male": "Романов", "female": "Романова" },
|
||||||
|
{ "male": "Воробьёв", "female": "Воробьёва" },
|
||||||
|
{ "male": "Сергеев", "female": "Сергеева" },
|
||||||
|
{ "male": "Кузьмин", "female": "Кузьмина", "declension": "in" },
|
||||||
|
{ "male": "Фролов", "female": "Фролова" },
|
||||||
|
{ "male": "Александров", "female": "Александрова" },
|
||||||
|
{ "male": "Дмитриев", "female": "Дмитриева" },
|
||||||
|
{ "male": "Королёв", "female": "Королёва" },
|
||||||
|
{ "male": "Громов", "female": "Громова" },
|
||||||
|
{ "male": "Ильин", "female": "Ильина", "declension": "in" },
|
||||||
|
{ "male": "Козловский", "female": "Козловская", "declension": "sky" },
|
||||||
|
{ "male": "Орловский", "female": "Орловская", "declension": "sky" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
{
|
|
||||||
"defName": "Slavic",
|
|
||||||
"patronymicRule": "slavic",
|
|
||||||
"defaultGivenDeclension": "hard",
|
|
||||||
"defaultSurnameDeclension": "ov",
|
|
||||||
"nativeLanguages": ["RussianLanguage", "BelarusianLanguage", "UkrainianLanguage"],
|
|
||||||
"relatedLanguageChance": 0.6,
|
|
||||||
"relatedLanguageMean": 22,
|
|
||||||
"relatedLanguageStdDev": 8,
|
|
||||||
"relatedLanguageMax": 40,
|
|
||||||
"maleGiven": [
|
|
||||||
{ "form": "Александр" },
|
|
||||||
{ "form": "Алексей" },
|
|
||||||
{ "form": "Андрей" },
|
|
||||||
{ "form": "Антон" },
|
|
||||||
{ "form": "Артём" },
|
|
||||||
{ "form": "Борис" },
|
|
||||||
{ "form": "Вадим" },
|
|
||||||
{ "form": "Валерий" },
|
|
||||||
{ "form": "Виктор" },
|
|
||||||
{ "form": "Виталий" },
|
|
||||||
{ "form": "Владимир" },
|
|
||||||
{ "form": "Владислав" },
|
|
||||||
{ "form": "Григорий" },
|
|
||||||
{ "form": "Денис" },
|
|
||||||
{ "form": "Дмитрий" },
|
|
||||||
{ "form": "Евгений" },
|
|
||||||
{ "form": "Егор" },
|
|
||||||
{ "form": "Иван" },
|
|
||||||
{ "form": "Игорь", "declension": "soft" },
|
|
||||||
{ "form": "Илья", "declension": "ya" },
|
|
||||||
{ "form": "Кирилл" },
|
|
||||||
{ "form": "Константин" },
|
|
||||||
{ "form": "Максим" },
|
|
||||||
{ "form": "Михаил" },
|
|
||||||
{ "form": "Никита", "declension": "a" },
|
|
||||||
{ "form": "Николай" },
|
|
||||||
{ "form": "Олег" },
|
|
||||||
{ "form": "Павел" },
|
|
||||||
{ "form": "Пётр" },
|
|
||||||
{ "form": "Роман" },
|
|
||||||
{ "form": "Сергей" },
|
|
||||||
{ "form": "Станислав" },
|
|
||||||
{ "form": "Степан" },
|
|
||||||
{ "form": "Фёдор" },
|
|
||||||
{ "form": "Юрий" },
|
|
||||||
],
|
|
||||||
"femaleGiven": [
|
|
||||||
{ "form": "Александра", "declension": "a" },
|
|
||||||
{ "form": "Алина", "declension": "a" },
|
|
||||||
{ "form": "Анастасия", "declension": "iya" },
|
|
||||||
{ "form": "Анна", "declension": "a" },
|
|
||||||
{ "form": "Валентина", "declension": "a" },
|
|
||||||
{ "form": "Валерия", "declension": "iya" },
|
|
||||||
{ "form": "Вера", "declension": "a" },
|
|
||||||
{ "form": "Виктория", "declension": "iya" },
|
|
||||||
{ "form": "Дарья", "declension": "ya" },
|
|
||||||
{ "form": "Екатерина", "declension": "a" },
|
|
||||||
{ "form": "Елена", "declension": "a" },
|
|
||||||
{ "form": "Елизавета", "declension": "a" },
|
|
||||||
{ "form": "Ирина", "declension": "a" },
|
|
||||||
{ "form": "Ксения", "declension": "iya" },
|
|
||||||
{
|
|
||||||
"form": "Любовь",
|
|
||||||
"cases": {
|
|
||||||
"nom": "Любовь",
|
|
||||||
"gen": "Любови",
|
|
||||||
"dat": "Любови",
|
|
||||||
"acc": "Любовь",
|
|
||||||
"ins": "Любовью",
|
|
||||||
"pre": "Любови",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ "form": "Людмила", "declension": "a" },
|
|
||||||
{ "form": "Маргарита", "declension": "a" },
|
|
||||||
{ "form": "Мария", "declension": "iya" },
|
|
||||||
{ "form": "Надежда", "declension": "a" },
|
|
||||||
{ "form": "Наталья", "declension": "ya" },
|
|
||||||
{ "form": "Нина", "declension": "a" },
|
|
||||||
{ "form": "Оксана", "declension": "a" },
|
|
||||||
{ "form": "Ольга", "declension": "a" },
|
|
||||||
{ "form": "Полина", "declension": "a" },
|
|
||||||
{ "form": "Светлана", "declension": "a" },
|
|
||||||
{ "form": "София", "declension": "iya" },
|
|
||||||
{ "form": "Татьяна", "declension": "a" },
|
|
||||||
{ "form": "Юлия", "declension": "iya" },
|
|
||||||
{ "form": "Яна", "declension": "a" },
|
|
||||||
{ "form": "Вероника", "declension": "a" },
|
|
||||||
{ "form": "Диана", "declension": "a" },
|
|
||||||
{ "form": "Марина", "declension": "a" },
|
|
||||||
],
|
|
||||||
"surnames": [
|
|
||||||
{ "male": "Иванов", "female": "Иванова" },
|
|
||||||
{ "male": "Петров", "female": "Петрова" },
|
|
||||||
{ "male": "Смирнов", "female": "Смирнова" },
|
|
||||||
{ "male": "Кузнецов", "female": "Кузнецова" },
|
|
||||||
{ "male": "Попов", "female": "Попова" },
|
|
||||||
{ "male": "Васильев", "female": "Васильева" },
|
|
||||||
{ "male": "Соколов", "female": "Соколова" },
|
|
||||||
{ "male": "Михайлов", "female": "Михайлова" },
|
|
||||||
{ "male": "Новиков", "female": "Новикова" },
|
|
||||||
{ "male": "Фёдоров", "female": "Фёдорова" },
|
|
||||||
{ "male": "Морозов", "female": "Морозова" },
|
|
||||||
{ "male": "Волков", "female": "Волкова" },
|
|
||||||
{ "male": "Алексеев", "female": "Алексеева" },
|
|
||||||
{ "male": "Лебедев", "female": "Лебедева" },
|
|
||||||
{ "male": "Семёнов", "female": "Семёнова" },
|
|
||||||
{ "male": "Егоров", "female": "Егорова" },
|
|
||||||
{ "male": "Павлов", "female": "Павлова" },
|
|
||||||
{ "male": "Козлов", "female": "Козлова" },
|
|
||||||
{ "male": "Степанов", "female": "Степанова" },
|
|
||||||
{ "male": "Николаев", "female": "Николаева" },
|
|
||||||
{ "male": "Орлов", "female": "Орлова" },
|
|
||||||
{ "male": "Андреев", "female": "Андреева" },
|
|
||||||
{ "male": "Макаров", "female": "Макарова" },
|
|
||||||
{ "male": "Никитин", "female": "Никитина", "declension": "in" },
|
|
||||||
{ "male": "Захаров", "female": "Захарова" },
|
|
||||||
{ "male": "Зайцев", "female": "Зайцева" },
|
|
||||||
{ "male": "Соловьёв", "female": "Соловьёва" },
|
|
||||||
{ "male": "Борисов", "female": "Борисова" },
|
|
||||||
{ "male": "Яковлев", "female": "Яковлева" },
|
|
||||||
{ "male": "Григорьев", "female": "Григорьева" },
|
|
||||||
{ "male": "Романов", "female": "Романова" },
|
|
||||||
{ "male": "Воробьёв", "female": "Воробьёва" },
|
|
||||||
{ "male": "Сергеев", "female": "Сергеева" },
|
|
||||||
{ "male": "Кузьмин", "female": "Кузьмина", "declension": "in" },
|
|
||||||
{ "male": "Фролов", "female": "Фролова" },
|
|
||||||
{ "male": "Александров", "female": "Александрова" },
|
|
||||||
{ "male": "Дмитриев", "female": "Дмитриева" },
|
|
||||||
{ "male": "Королёв", "female": "Королёва" },
|
|
||||||
{ "male": "Громов", "female": "Громова" },
|
|
||||||
{ "male": "Ильин", "female": "Ильина", "declension": "in" },
|
|
||||||
{ "male": "Козловский", "female": "Козловская", "declension": "sky" },
|
|
||||||
{ "male": "Орловский", "female": "Орловская", "declension": "sky" },
|
|
||||||
],
|
|
||||||
}
|
|
||||||
@@ -144,7 +144,9 @@
|
|||||||
"Hunger": "Hunger",
|
"Hunger": "Hunger",
|
||||||
"Toilet": "Toilet",
|
"Toilet": "Toilet",
|
||||||
"Social": "Social",
|
"Social": "Social",
|
||||||
"Slavic": "Slavic",
|
"Russia": "Russia",
|
||||||
|
"TemperateContinental": "Temperate continental",
|
||||||
|
"ContinentalCold": "Cold continental",
|
||||||
"Build": "Build",
|
"Build": "Build",
|
||||||
"Skinny": "Skinny",
|
"Skinny": "Skinny",
|
||||||
"Average": "Average",
|
"Average": "Average",
|
||||||
|
|||||||
@@ -144,7 +144,9 @@
|
|||||||
"Hunger": "Голод",
|
"Hunger": "Голод",
|
||||||
"Toilet": "Туалет",
|
"Toilet": "Туалет",
|
||||||
"Social": "Общение",
|
"Social": "Общение",
|
||||||
"Slavic": "Славянский",
|
"Russia": "Россия",
|
||||||
|
"TemperateContinental": "Умеренно-континентальный",
|
||||||
|
"ContinentalCold": "Континентальный холодный",
|
||||||
"Build": "Телосложение",
|
"Build": "Телосложение",
|
||||||
"Skinny": "Худощавое",
|
"Skinny": "Худощавое",
|
||||||
"Average": "Обычное",
|
"Average": "Обычное",
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ example/
|
|||||||
pack.jsonc # version + requires: ["core"]
|
pack.jsonc # version + requires: ["core"]
|
||||||
localizations/{ru,en}.jsonc
|
localizations/{ru,en}.jsonc
|
||||||
defs/traits/traits.jsonc # two traits
|
defs/traits/traits.jsonc # two traits
|
||||||
defs/namesets/example.jsonc
|
defs/countries/example.jsonc
|
||||||
defs/rooms/store.jsonc # one placeable room
|
defs/rooms/store.jsonc # one placeable room
|
||||||
defs/things/chair.jsonc # last-wins: same defName as core
|
defs/things/chair.jsonc # last-wins: same defName as core
|
||||||
patches/principals-office.jsonc
|
patches/principals-office.jsonc
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"defName": "ExampleNames",
|
||||||
|
"climatePresets": ["TemperateContinental"],
|
||||||
|
"names": {
|
||||||
|
"patronymicRule": "slavic",
|
||||||
|
"defaultGivenDeclension": "hard",
|
||||||
|
"defaultSurnameDeclension": "ov",
|
||||||
|
"nativeLanguages": ["RussianLanguage"],
|
||||||
|
"relatedLanguageChance": 0,
|
||||||
|
"maleGiven": [
|
||||||
|
{ "form": "Иван" },
|
||||||
|
],
|
||||||
|
"femaleGiven": [
|
||||||
|
{ "form": "Анна", "declension": "a" },
|
||||||
|
],
|
||||||
|
"surnames": [
|
||||||
|
{ "male": "Тестов", "female": "Тестова" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
{
|
|
||||||
"defName": "ExampleNames",
|
|
||||||
"patronymicRule": "slavic",
|
|
||||||
"defaultGivenDeclension": "hard",
|
|
||||||
"defaultSurnameDeclension": "ov",
|
|
||||||
"nativeLanguages": ["RussianLanguage"],
|
|
||||||
"relatedLanguageChance": 0,
|
|
||||||
"maleGiven": [
|
|
||||||
{ "form": "Иван" },
|
|
||||||
],
|
|
||||||
"femaleGiven": [
|
|
||||||
{ "form": "Анна", "declension": "a" },
|
|
||||||
],
|
|
||||||
"surnames": [
|
|
||||||
{ "male": "Тестов", "female": "Тестова" },
|
|
||||||
],
|
|
||||||
}
|
|
||||||
@@ -75,8 +75,11 @@ public sealed class School : IDisposable
|
|||||||
|
|
||||||
public int PeopleSeed { get; private set; }
|
public int PeopleSeed { get; private set; }
|
||||||
|
|
||||||
/// <summary>Name pack used to generate this school's people. Needed again on 1 September.</summary>
|
/// <summary>Country used to generate this school's people. Needed again on 1 September.</summary>
|
||||||
public string? NameSetId { get; private set; }
|
public string? CountryId { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>Climate preset rolled at birth. Phase 32 reads it; it cannot change on a live school.</summary>
|
||||||
|
public string? ClimatePresetId { get; private set; }
|
||||||
|
|
||||||
/// <summary>Skill everyone generated for this school speaks natively.</summary>
|
/// <summary>Skill everyone generated for this school speaks natively.</summary>
|
||||||
public string? NativeLanguage { get; private set; }
|
public string? NativeLanguage { get; private set; }
|
||||||
@@ -118,14 +121,15 @@ public sealed class School : IDisposable
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Installs a roster that already matches the map. Spawns entities; does not write to disk.
|
/// Installs a roster that already matches the map. Spawns entities; does not write to disk.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void InstallPeople(Roster roster, int seed, string? nameSetId = null, ApplicantPool? applicants = null, string? nativeLanguage = null)
|
public void InstallPeople(Roster roster, int seed, string? countryId = null, ApplicantPool? applicants = null, string? nativeLanguage = null, string? climatePresetId = null)
|
||||||
{
|
{
|
||||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||||
ArgumentNullException.ThrowIfNull(roster);
|
ArgumentNullException.ThrowIfNull(roster);
|
||||||
|
|
||||||
Roster = roster;
|
Roster = roster;
|
||||||
PeopleSeed = seed;
|
PeopleSeed = seed;
|
||||||
NameSetId = nameSetId;
|
CountryId = countryId;
|
||||||
|
ClimatePresetId = climatePresetId;
|
||||||
NativeLanguage = nativeLanguage;
|
NativeLanguage = nativeLanguage;
|
||||||
Applicants = applicants;
|
Applicants = applicants;
|
||||||
RosterSpawner.Spawn(World, roster);
|
RosterSpawner.Spawn(World, roster);
|
||||||
@@ -282,7 +286,7 @@ public sealed class School : IDisposable
|
|||||||
|
|
||||||
private bool TryYearlyIntake(DateTime before, DateTime after)
|
private bool TryYearlyIntake(DateTime before, DateTime after)
|
||||||
{
|
{
|
||||||
if (Roster is null || Catalog is null || NameSetId is null)
|
if (Roster is null || Catalog is null || CountryId is null)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -290,7 +294,7 @@ public sealed class School : IDisposable
|
|||||||
var changed = false;
|
var changed = false;
|
||||||
foreach (var date in YearlyIntake.DatesBetween(before, after))
|
foreach (var date in YearlyIntake.DatesBetween(before, after))
|
||||||
{
|
{
|
||||||
Roster = YearlyIntake.Apply(Catalog, Roster, PeopleSeed, NameSetId, date, NativeLanguage);
|
Roster = YearlyIntake.Apply(Catalog, Roster, PeopleSeed, CountryId, date, NativeLanguage);
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -307,12 +311,12 @@ public sealed class School : IDisposable
|
|||||||
|
|
||||||
private bool TryApplicantRefresh()
|
private bool TryApplicantRefresh()
|
||||||
{
|
{
|
||||||
if (Applicants is null || Roster is null || Catalog is null || NameSetId is null || Catalog.StaffingRules is null)
|
if (Applicants is null || Roster is null || Catalog is null || CountryId is null || Catalog.StaffingRules is null)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var next = Applicants.Advance(Catalog, Roster, PeopleSeed, NameSetId, Clock.Time, NativeLanguage);
|
var next = Applicants.Advance(Catalog, Roster, PeopleSeed, CountryId, Clock.Time, NativeLanguage);
|
||||||
if (next.Week == Applicants.Week)
|
if (next.Week == Applicants.Week)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ public enum SchoolCreationError
|
|||||||
InvalidMap,
|
InvalidMap,
|
||||||
UnknownMod,
|
UnknownMod,
|
||||||
InvalidCatalog,
|
InvalidCatalog,
|
||||||
UnknownNameSet,
|
UnknownCountry,
|
||||||
UnknownNativeLanguage,
|
UnknownNativeLanguage,
|
||||||
MissingMod,
|
MissingMod,
|
||||||
ModCycle,
|
ModCycle,
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ public class WalkingTests
|
|||||||
public void ComesToday_IsFalseOnSundayAndHolidays()
|
public void ComesToday_IsFalseOnSundayAndHolidays()
|
||||||
{
|
{
|
||||||
var (catalog, map) = Fixtures.Vanilla();
|
var (catalog, map) = Fixtures.Vanilla();
|
||||||
var roster = RosterGenerator.Generate(catalog, map, schoolSeed: 1, "Slavic", TuesdayLesson);
|
var roster = RosterGenerator.Generate(catalog, map, schoolSeed: 1, "Russia", TuesdayLesson);
|
||||||
var pupil = roster.People.First(person => person.IsStudent);
|
var pupil = roster.People.First(person => person.IsStudent);
|
||||||
var schoolClass = roster.Classes.First(row => row.Id == pupil.ClassId);
|
var schoolClass = roster.Classes.First(row => row.Id == pupil.ClassId);
|
||||||
var table = new Timetable(
|
var table = new Timetable(
|
||||||
@@ -90,7 +90,7 @@ public class WalkingTests
|
|||||||
public void ClassWithoutLessons_StaysAway_TeacherWithAnotherClassComes()
|
public void ClassWithoutLessons_StaysAway_TeacherWithAnotherClassComes()
|
||||||
{
|
{
|
||||||
var (catalog, map) = Fixtures.Vanilla();
|
var (catalog, map) = Fixtures.Vanilla();
|
||||||
var roster = RosterGenerator.Generate(catalog, map, schoolSeed: 1, "Slavic", TuesdayLesson);
|
var roster = RosterGenerator.Generate(catalog, map, schoolSeed: 1, "Russia", TuesdayLesson);
|
||||||
var idle = roster.Classes[0];
|
var idle = roster.Classes[0];
|
||||||
var busy = roster.Classes[1];
|
var busy = roster.Classes[1];
|
||||||
var idlePupil = roster.People.First(person => person.Id == idle.PupilIds[0]);
|
var idlePupil = roster.People.First(person => person.Id == idle.PupilIds[0]);
|
||||||
@@ -117,7 +117,7 @@ public class WalkingTests
|
|||||||
{
|
{
|
||||||
var (catalog, map) = Fixtures.Vanilla();
|
var (catalog, map) = Fixtures.Vanilla();
|
||||||
var walks = WalkGraph.Build(catalog, map);
|
var walks = WalkGraph.Build(catalog, map);
|
||||||
var roster = RosterGenerator.Generate(catalog, map, schoolSeed: 9, "Slavic", TuesdayLesson);
|
var roster = RosterGenerator.Generate(catalog, map, schoolSeed: 9, "Russia", TuesdayLesson);
|
||||||
var pupil = roster.People.First(person => person.IsStudent);
|
var pupil = roster.People.First(person => person.IsStudent);
|
||||||
var schoolClass = roster.Classes.First(row => row.Id == pupil.ClassId);
|
var schoolClass = roster.Classes.First(row => row.Id == pupil.ClassId);
|
||||||
var table = new Timetable(
|
var table = new Timetable(
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ public class ExamplePackTests(AppHostFixture fixture)
|
|||||||
Assert.Equal("Кабинет директора", Assert.Single(ru.Rooms, room => room.DefName == "PrincipalsOffice").Label);
|
Assert.Equal("Кабинет директора", Assert.Single(ru.Rooms, room => room.DefName == "PrincipalsOffice").Label);
|
||||||
Assert.Equal("Кладовая", Assert.Single(ru.Rooms, room => room.DefName == "ExampleStore").Label);
|
Assert.Equal("Кладовая", Assert.Single(ru.Rooms, room => room.DefName == "ExampleStore").Label);
|
||||||
Assert.Equal("Storeroom", Assert.Single(en.Rooms, room => room.DefName == "ExampleStore").Label);
|
Assert.Equal("Storeroom", Assert.Single(en.Rooms, room => room.DefName == "ExampleStore").Label);
|
||||||
Assert.Equal("Примерные имена", Assert.Single(ru.NameSets, set => set.DefName == "ExampleNames").Label);
|
Assert.Equal("Примерные имена", Assert.Single(ru.Countries, set => set.DefName == "ExampleNames").Label);
|
||||||
Assert.Equal("Example names", Assert.Single(en.NameSets, set => set.DefName == "ExampleNames").Label);
|
Assert.Equal("Example names", Assert.Single(en.Countries, set => set.DefName == "ExampleNames").Label);
|
||||||
Assert.Equal("yard", ru.DefaultMap.Territory?.Id);
|
Assert.Equal("yard", ru.DefaultMap.Territory?.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,14 +139,14 @@ public class ExamplePackTests(AppHostFixture fixture)
|
|||||||
IReadOnlyList<DefInfoResponse> Floors,
|
IReadOnlyList<DefInfoResponse> Floors,
|
||||||
IReadOnlyList<RoomInfoResponse> Rooms,
|
IReadOnlyList<RoomInfoResponse> Rooms,
|
||||||
IReadOnlyList<DefInfoResponse> Things,
|
IReadOnlyList<DefInfoResponse> Things,
|
||||||
IReadOnlyList<NameSetInfoResponse> NameSets,
|
IReadOnlyList<CountryInfoResponse> Countries,
|
||||||
DayFrameResponse? DayFrame,
|
DayFrameResponse? DayFrame,
|
||||||
IReadOnlyList<HolidayInfoResponse> Holidays,
|
IReadOnlyList<HolidayInfoResponse> Holidays,
|
||||||
MapLayoutResponse DefaultMap);
|
MapLayoutResponse DefaultMap);
|
||||||
|
|
||||||
private sealed record DefInfoResponse(string DefName, string Label);
|
private sealed record DefInfoResponse(string DefName, string Label);
|
||||||
|
|
||||||
private sealed record NameSetInfoResponse(string DefName, string Label);
|
private sealed record CountryInfoResponse(string DefName, string Label);
|
||||||
|
|
||||||
private sealed record RoomInfoResponse(
|
private sealed record RoomInfoResponse(
|
||||||
string DefName,
|
string DefName,
|
||||||
|
|||||||
@@ -278,13 +278,14 @@ public class SchoolApiTests(AppHostFixture fixture)
|
|||||||
Assert.Equal("Кабинет директора", Assert.Single(ru.Rooms, room => room.DefName == "PrincipalsOffice").Label);
|
Assert.Equal("Кабинет директора", Assert.Single(ru.Rooms, room => room.DefName == "PrincipalsOffice").Label);
|
||||||
Assert.Equal("Principal's office", Assert.Single(en.Rooms, room => room.DefName == "PrincipalsOffice").Label);
|
Assert.Equal("Principal's office", Assert.Single(en.Rooms, room => room.DefName == "PrincipalsOffice").Label);
|
||||||
Assert.Equal("yard", ru.DefaultMap.Territory?.Id);
|
Assert.Equal("yard", ru.DefaultMap.Territory?.Id);
|
||||||
Assert.Equal("Славянский", Assert.Single(ru.NameSets, set => set.DefName == "Slavic").Label);
|
Assert.Equal("Россия", Assert.Single(ru.Countries, country => country.DefName == "Russia").Label);
|
||||||
Assert.Equal("Slavic", Assert.Single(en.NameSets, set => set.DefName == "Slavic").Label);
|
Assert.Equal("Russia", Assert.Single(en.Countries, country => country.DefName == "Russia").Label);
|
||||||
var slavic = Assert.Single(ru.NameSets, set => set.DefName == "Slavic");
|
var russia = Assert.Single(ru.Countries, country => country.DefName == "Russia");
|
||||||
Assert.Equal(
|
Assert.Equal(
|
||||||
["RussianLanguage", "BelarusianLanguage", "UkrainianLanguage"],
|
["RussianLanguage", "BelarusianLanguage", "UkrainianLanguage"],
|
||||||
slavic.NativeLanguages.Select(language => language.DefName).ToArray());
|
russia.NativeLanguages.Select(language => language.DefName).ToArray());
|
||||||
Assert.Equal("Белорусский", Assert.Single(slavic.NativeLanguages, language => language.DefName == "BelarusianLanguage").Label);
|
Assert.Equal("Белорусский", Assert.Single(russia.NativeLanguages, language => language.DefName == "BelarusianLanguage").Label);
|
||||||
|
Assert.Equal(["TemperateContinental", "ContinentalCold"], russia.ClimatePresets);
|
||||||
Assert.Equal("Начальные классы", Assert.Single(ru.Subjects, subject => subject.DefName == "PrimarySchool").Label);
|
Assert.Equal("Начальные классы", Assert.Single(ru.Subjects, subject => subject.DefName == "PrimarySchool").Label);
|
||||||
Assert.Equal("Primary", Assert.Single(en.Subjects, subject => subject.DefName == "PrimarySchool").Label);
|
Assert.Equal("Primary", Assert.Single(en.Subjects, subject => subject.DefName == "PrimarySchool").Label);
|
||||||
var classroom = Assert.Single(ru.Rooms, room => room.DefName == "Classroom");
|
var classroom = Assert.Single(ru.Rooms, room => room.DefName == "Classroom");
|
||||||
@@ -396,7 +397,7 @@ public class SchoolApiTests(AppHostFixture fixture)
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task CreateSchool_WithAnUnknownNameSet_IsRejected()
|
public async Task CreateSchool_WithAnUnknownCountry_IsRejected()
|
||||||
{
|
{
|
||||||
using var client = fixture.App.CreateHttpClient("server");
|
using var client = fixture.App.CreateHttpClient("server");
|
||||||
await ResetAsync(client);
|
await ResetAsync(client);
|
||||||
@@ -405,14 +406,47 @@ public class SchoolApiTests(AppHostFixture fixture)
|
|||||||
"/api/schools",
|
"/api/schools",
|
||||||
new
|
new
|
||||||
{
|
{
|
||||||
name = "Чужие имена",
|
name = "Чужая страна",
|
||||||
startDate = ExpectedDefaultStart,
|
startDate = ExpectedDefaultStart,
|
||||||
nameSetId = "NoSuchNames",
|
countryId = "NoSuchCountry",
|
||||||
},
|
},
|
||||||
TestContext.Current.CancellationToken);
|
TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||||
Assert.Equal("unknown-name-set", await ProblemCodeAsync(response));
|
Assert.Equal("unknown-country", await ProblemCodeAsync(response));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CreateSchool_WithRussiaAndNoLanguage_PicksTheSameNativeAsTheSeed()
|
||||||
|
{
|
||||||
|
using var client = fixture.App.CreateHttpClient("server");
|
||||||
|
await ResetAsync(client);
|
||||||
|
const int seed = 42;
|
||||||
|
|
||||||
|
var first = await CreateWithCountryAsync(client, "Страна А", ExpectedDefaultStart, "Russia", seed);
|
||||||
|
var second = await CreateWithCountryAsync(client, "Страна Б", ExpectedDefaultStart, "Russia", seed);
|
||||||
|
|
||||||
|
var firstSave = await ReadSchoolSaveAsync(client, first.Id);
|
||||||
|
var secondSave = await ReadSchoolSaveAsync(client, second.Id);
|
||||||
|
Assert.Equal("Russia", firstSave.CountryId);
|
||||||
|
Assert.Equal(firstSave.NativeLanguage, secondSave.NativeLanguage);
|
||||||
|
Assert.Contains(firstSave.NativeLanguage, new[] { "RussianLanguage", "BelarusianLanguage", "UkrainianLanguage" });
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CreateSchool_WithTheSameSeed_GetsTheSameClimatePreset()
|
||||||
|
{
|
||||||
|
using var client = fixture.App.CreateHttpClient("server");
|
||||||
|
await ResetAsync(client);
|
||||||
|
const int seed = 20260818;
|
||||||
|
|
||||||
|
var first = await CreateWithCountryAsync(client, "Климат А", ExpectedDefaultStart, "Russia", seed);
|
||||||
|
var second = await CreateWithCountryAsync(client, "Климат Б", ExpectedDefaultStart, "Russia", seed);
|
||||||
|
|
||||||
|
var firstSave = await ReadSchoolSaveAsync(client, first.Id);
|
||||||
|
var secondSave = await ReadSchoolSaveAsync(client, second.Id);
|
||||||
|
Assert.Equal(firstSave.ClimatePresetId, secondSave.ClimatePresetId);
|
||||||
|
Assert.Contains(firstSave.ClimatePresetId, new[] { "TemperateContinental", "ContinentalCold" });
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -427,7 +461,7 @@ public class SchoolApiTests(AppHostFixture fixture)
|
|||||||
{
|
{
|
||||||
name = "Чужой язык",
|
name = "Чужой язык",
|
||||||
startDate = ExpectedDefaultStart,
|
startDate = ExpectedDefaultStart,
|
||||||
nameSetId = "Slavic",
|
countryId = "Russia",
|
||||||
nativeLanguage = "Klingon",
|
nativeLanguage = "Klingon",
|
||||||
},
|
},
|
||||||
TestContext.Current.CancellationToken);
|
TestContext.Current.CancellationToken);
|
||||||
@@ -521,6 +555,46 @@ public class SchoolApiTests(AppHostFixture fixture)
|
|||||||
return created;
|
return created;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static async Task<SchoolResponse> CreateWithCountryAsync(
|
||||||
|
HttpClient client,
|
||||||
|
string name,
|
||||||
|
DateTime startDate,
|
||||||
|
string countryId,
|
||||||
|
int seed)
|
||||||
|
{
|
||||||
|
using var response = await client.PostAsJsonAsync(
|
||||||
|
"/api/schools",
|
||||||
|
new { name, startDate, countryId, seed },
|
||||||
|
TestContext.Current.CancellationToken);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
var created = await response.Content.ReadFromJsonAsync<SchoolResponse>(TestContext.Current.CancellationToken);
|
||||||
|
Assert.NotNull(created);
|
||||||
|
return created;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<SchoolSaveFile> ReadSchoolSaveAsync(HttpClient client, int id)
|
||||||
|
{
|
||||||
|
var directory = await SavesDirectoryAsync(client);
|
||||||
|
var json = await File.ReadAllTextAsync(Path.Combine(directory, $"{id}.json"), TestContext.Current.CancellationToken);
|
||||||
|
var save = System.Text.Json.JsonSerializer.Deserialize<SchoolSaveFile>(
|
||||||
|
json,
|
||||||
|
new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
||||||
|
Assert.NotNull(save);
|
||||||
|
return save;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<string> SavesDirectoryAsync(HttpClient client)
|
||||||
|
{
|
||||||
|
var payload = await client.GetFromJsonAsync<PathResponse>(
|
||||||
|
"/api/dev/saves-directory",
|
||||||
|
TestContext.Current.CancellationToken);
|
||||||
|
Assert.NotNull(payload);
|
||||||
|
Assert.False(string.IsNullOrWhiteSpace(payload.Path));
|
||||||
|
return payload.Path;
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed record SchoolSaveFile(string? CountryId, string? ClimatePresetId, string? NativeLanguage);
|
||||||
|
|
||||||
internal static readonly object SimpleCustomMap = new
|
internal static readonly object SimpleCustomMap = new
|
||||||
{
|
{
|
||||||
territory = new { id = "yard", def = "SchoolYard" },
|
territory = new { id = "yard", def = "SchoolYard" },
|
||||||
@@ -626,7 +700,7 @@ public class SchoolApiTests(AppHostFixture fixture)
|
|||||||
IReadOnlyList<DefInfoResponse> Floors,
|
IReadOnlyList<DefInfoResponse> Floors,
|
||||||
IReadOnlyList<RoomInfoResponse> Rooms,
|
IReadOnlyList<RoomInfoResponse> Rooms,
|
||||||
IReadOnlyList<DefInfoResponse> Things,
|
IReadOnlyList<DefInfoResponse> Things,
|
||||||
IReadOnlyList<NameSetInfoResponse> NameSets,
|
IReadOnlyList<CountryInfoResponse> Countries,
|
||||||
IReadOnlyList<SubjectInfoResponse> Subjects,
|
IReadOnlyList<SubjectInfoResponse> Subjects,
|
||||||
DayFrameResponse? DayFrame,
|
DayFrameResponse? DayFrame,
|
||||||
IReadOnlyList<HolidayInfoResponse> Holidays,
|
IReadOnlyList<HolidayInfoResponse> Holidays,
|
||||||
@@ -634,10 +708,11 @@ public class SchoolApiTests(AppHostFixture fixture)
|
|||||||
|
|
||||||
private sealed record DefInfoResponse(string DefName, string Label);
|
private sealed record DefInfoResponse(string DefName, string Label);
|
||||||
|
|
||||||
private sealed record NameSetInfoResponse(
|
private sealed record CountryInfoResponse(
|
||||||
string DefName,
|
string DefName,
|
||||||
string Label,
|
string Label,
|
||||||
IReadOnlyList<DefInfoResponse> NativeLanguages);
|
IReadOnlyList<DefInfoResponse> NativeLanguages,
|
||||||
|
IReadOnlyList<string> ClimatePresets);
|
||||||
|
|
||||||
private sealed record RoomInfoResponse(
|
private sealed record RoomInfoResponse(
|
||||||
string DefName,
|
string DefName,
|
||||||
|
|||||||
@@ -37,6 +37,34 @@ public class ServiceabilityTests(AppHostFixture fixture)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CurrentFormatSave_WithoutCountryId_LeavesTheSchoolUnstartedAndTheFileUntouched()
|
||||||
|
{
|
||||||
|
using var client = fixture.App.CreateHttpClient("server");
|
||||||
|
await SchoolApiTests.ResetAsync(client);
|
||||||
|
var directory = await SavesDirectoryAsync(client);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
InstallGolden(directory, "current");
|
||||||
|
var path = Path.Combine(directory, "1.json");
|
||||||
|
BumpFormat(path, 3);
|
||||||
|
var before = File.ReadAllBytes(path);
|
||||||
|
|
||||||
|
using var reload = await client.PostAsync("/api/dev/reload-schools", content: null, TestContext.Current.CancellationToken);
|
||||||
|
reload.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
Assert.Empty((await SchoolApiTests.GetSchoolsAsync(client)).Schools);
|
||||||
|
Assert.Equal(before, File.ReadAllBytes(path));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
DeleteSchoolFiles(directory, 1);
|
||||||
|
using var cleanup = await client.PostAsync("/api/dev/reload-schools", content: null, TestContext.Current.CancellationToken);
|
||||||
|
cleanup.EnsureSuccessStatusCode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task CurrentFormatSave_LoadsAsBefore()
|
public async Task CurrentFormatSave_LoadsAsBefore()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
namespace HSchool.Content.Tests;
|
||||||
|
|
||||||
|
public class CountryDefTests
|
||||||
|
{
|
||||||
|
private readonly CatalogLoader _loader = new();
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void VanillaCore_LoadsRussiaAndClimatePresets()
|
||||||
|
{
|
||||||
|
var root = Path.Combine(AppContext.BaseDirectory, "vanilla");
|
||||||
|
var catalog = _loader.Load([CatalogLoader.CorePackId], PackDocuments.FromDirectory(CatalogLoader.CorePackId, root));
|
||||||
|
|
||||||
|
Assert.True(catalog.Countries.ContainsKey("Russia"));
|
||||||
|
Assert.False(catalog.Countries.ContainsKey("Slavic"));
|
||||||
|
var russia = catalog.Countries["Russia"];
|
||||||
|
Assert.Equal(
|
||||||
|
["RussianLanguage", "BelarusianLanguage", "UkrainianLanguage"],
|
||||||
|
russia.Names.Spoken);
|
||||||
|
Assert.Equal(0.6f, russia.Names.RelatedLanguageChance);
|
||||||
|
Assert.Equal(40, russia.Names.RelatedLanguageMax);
|
||||||
|
Assert.True(russia.Names.MaleGiven.Count >= 20);
|
||||||
|
Assert.Equal(["TemperateContinental", "ContinentalCold"], russia.ClimatePresets);
|
||||||
|
Assert.True(catalog.ClimatePresets.ContainsKey("TemperateContinental"));
|
||||||
|
Assert.True(catalog.ClimatePresets.ContainsKey("ContinentalCold"));
|
||||||
|
Assert.Equal("Россия", catalog.Label("ru", russia));
|
||||||
|
Assert.Equal("Russia", catalog.Label("en", russia));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CountryWithoutNames_FailsTheCatalog()
|
||||||
|
{
|
||||||
|
var ex = Assert.Throws<ContentLoadException>(() => _loader.Load(
|
||||||
|
[CatalogLoader.CorePackId],
|
||||||
|
[
|
||||||
|
PackDocuments.Def(
|
||||||
|
CatalogLoader.CorePackId,
|
||||||
|
"climates",
|
||||||
|
"temperate",
|
||||||
|
"""{ "defName": "TemperateContinental" }"""),
|
||||||
|
PackDocuments.Def(
|
||||||
|
CatalogLoader.CorePackId,
|
||||||
|
"countries",
|
||||||
|
"ghost",
|
||||||
|
"""{ "defName": "GhostLand", "climatePresets": ["TemperateContinental"] }"""),
|
||||||
|
]));
|
||||||
|
|
||||||
|
Assert.Contains("GhostLand", ex.Message, StringComparison.Ordinal);
|
||||||
|
Assert.Contains("names", ex.Message, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CountryWithUnknownPreset_FailsTheCatalog()
|
||||||
|
{
|
||||||
|
var ex = Assert.Throws<ContentLoadException>(() => _loader.Load(
|
||||||
|
[CatalogLoader.CorePackId],
|
||||||
|
[
|
||||||
|
PackDocuments.Def(
|
||||||
|
CatalogLoader.CorePackId,
|
||||||
|
"climates",
|
||||||
|
"temperate",
|
||||||
|
"""{ "defName": "TemperateContinental" }"""),
|
||||||
|
PackDocuments.Def(
|
||||||
|
CatalogLoader.CorePackId,
|
||||||
|
"countries",
|
||||||
|
"ghost",
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"defName": "GhostLand",
|
||||||
|
"climatePresets": ["NoSuchClimate"],
|
||||||
|
"names": {
|
||||||
|
"maleGiven": [{ "form": "Иван" }],
|
||||||
|
"femaleGiven": [{ "form": "Анна", "declension": "a" }],
|
||||||
|
"surnames": [{ "male": "Иванов", "female": "Иванова" }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
]));
|
||||||
|
|
||||||
|
Assert.Contains("NoSuchClimate", ex.Message, StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -40,12 +40,12 @@ public class ExamplePackTests
|
|||||||
|
|
||||||
Assert.True(catalog.Traits.ContainsKey("ExampleEarlyRiser"));
|
Assert.True(catalog.Traits.ContainsKey("ExampleEarlyRiser"));
|
||||||
Assert.True(catalog.Traits.ContainsKey("ExampleNightOwl"));
|
Assert.True(catalog.Traits.ContainsKey("ExampleNightOwl"));
|
||||||
Assert.True(catalog.NameSets.ContainsKey("ExampleNames"));
|
Assert.True(catalog.Countries.ContainsKey("ExampleNames"));
|
||||||
Assert.True(catalog.Rooms.ContainsKey("ExampleStore"));
|
Assert.True(catalog.Rooms.ContainsKey("ExampleStore"));
|
||||||
Assert.Equal("Кладовая", catalog.Label("ru", catalog.Rooms["ExampleStore"]));
|
Assert.Equal("Кладовая", catalog.Label("ru", catalog.Rooms["ExampleStore"]));
|
||||||
Assert.Equal("Storeroom", catalog.Label("en", catalog.Rooms["ExampleStore"]));
|
Assert.Equal("Storeroom", catalog.Label("en", catalog.Rooms["ExampleStore"]));
|
||||||
Assert.Equal("Примерные имена", catalog.Label("ru", catalog.NameSets["ExampleNames"]));
|
Assert.Equal("Примерные имена", catalog.Label("ru", catalog.Countries["ExampleNames"]));
|
||||||
Assert.Equal(["RussianLanguage"], catalog.NameSets["ExampleNames"].Spoken.ToArray());
|
Assert.Equal(["RussianLanguage"], catalog.Countries["ExampleNames"].Names.Spoken.ToArray());
|
||||||
Assert.True(catalog.Rooms.ContainsKey("PrincipalsOffice"));
|
Assert.True(catalog.Rooms.ContainsKey("PrincipalsOffice"));
|
||||||
Assert.Equal("Кабинет директора", catalog.Label("ru", catalog.Rooms["PrincipalsOffice"]));
|
Assert.Equal("Кабинет директора", catalog.Label("ru", catalog.Rooms["PrincipalsOffice"]));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,15 +25,15 @@ public class PeopleDefTests
|
|||||||
Assert.Equal(0.4f, catalog.Skills["English"].AdultChance);
|
Assert.Equal(0.4f, catalog.Skills["English"].AdultChance);
|
||||||
Assert.Equal(
|
Assert.Equal(
|
||||||
["RussianLanguage", "BelarusianLanguage", "UkrainianLanguage"],
|
["RussianLanguage", "BelarusianLanguage", "UkrainianLanguage"],
|
||||||
catalog.NameSets["Slavic"].Spoken);
|
catalog.Countries["Russia"].Names.Spoken);
|
||||||
Assert.Equal(0.6f, catalog.NameSets["Slavic"].RelatedLanguageChance);
|
Assert.Equal(0.6f, catalog.Countries["Russia"].Names.RelatedLanguageChance);
|
||||||
Assert.Equal(40, catalog.NameSets["Slavic"].RelatedLanguageMax);
|
Assert.Equal(40, catalog.Countries["Russia"].Names.RelatedLanguageMax);
|
||||||
Assert.Contains(catalog.Subjects["ForeignLanguage"].Skills, share => share.Skill == "English");
|
Assert.Contains(catalog.Subjects["ForeignLanguage"].Skills, share => share.Skill == "English");
|
||||||
Assert.False(catalog.Skills.ContainsKey("ForeignLanguage"));
|
Assert.False(catalog.Skills.ContainsKey("ForeignLanguage"));
|
||||||
Assert.True(catalog.NameSets.ContainsKey("Slavic"));
|
Assert.True(catalog.Countries.ContainsKey("Russia"));
|
||||||
Assert.True(catalog.NameSets["Slavic"].MaleGiven.Count >= 20);
|
Assert.True(catalog.Countries["Russia"].Names.MaleGiven.Count >= 20);
|
||||||
Assert.Equal("Славянский", catalog.Label("ru", catalog.NameSets["Slavic"]));
|
Assert.Equal("Россия", catalog.Label("ru", catalog.Countries["Russia"]));
|
||||||
Assert.Equal("Slavic", catalog.Label("en", catalog.NameSets["Slavic"]));
|
Assert.Equal("Russia", catalog.Label("en", catalog.Countries["Russia"]));
|
||||||
Assert.Equal("Усидчивый", catalog.Label("ru", catalog.Traits["Diligent"]));
|
Assert.Equal("Усидчивый", catalog.Label("ru", catalog.Traits["Diligent"]));
|
||||||
Assert.True(catalog.Subjects.ContainsKey("PrimarySchool"));
|
Assert.True(catalog.Subjects.ContainsKey("PrimarySchool"));
|
||||||
Assert.NotNull(catalog.StaffingRules);
|
Assert.NotNull(catalog.StaffingRules);
|
||||||
@@ -213,7 +213,7 @@ public class PeopleDefTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ExtraPack_AddsANameSetAndTrait()
|
public void ExtraPack_AddsACountryAndTrait()
|
||||||
{
|
{
|
||||||
var catalog = _loader.Load(
|
var catalog = _loader.Load(
|
||||||
[CatalogLoader.CorePackId, "addon"],
|
[CatalogLoader.CorePackId, "addon"],
|
||||||
@@ -221,37 +221,48 @@ public class PeopleDefTests
|
|||||||
PackDocuments.Def(CatalogLoader.CorePackId, "traits", "kind", """{ "defName": "Kind", "weight": 1 }"""),
|
PackDocuments.Def(CatalogLoader.CorePackId, "traits", "kind", """{ "defName": "Kind", "weight": 1 }"""),
|
||||||
PackDocuments.Def(
|
PackDocuments.Def(
|
||||||
CatalogLoader.CorePackId,
|
CatalogLoader.CorePackId,
|
||||||
"namesets",
|
"climates",
|
||||||
"slavic",
|
"temperate",
|
||||||
|
"""{ "defName": "TemperateContinental" }"""),
|
||||||
|
PackDocuments.Def(
|
||||||
|
CatalogLoader.CorePackId,
|
||||||
|
"countries",
|
||||||
|
"russia",
|
||||||
"""
|
"""
|
||||||
{
|
{
|
||||||
"defName": "Slavic",
|
"defName": "Russia",
|
||||||
"maleGiven": [{ "form": "Иван" }],
|
"climatePresets": ["TemperateContinental"],
|
||||||
"femaleGiven": [{ "form": "Анна", "declension": "a" }],
|
"names": {
|
||||||
"surnames": [{ "male": "Иванов", "female": "Иванова" }]
|
"maleGiven": [{ "form": "Иван" }],
|
||||||
|
"femaleGiven": [{ "form": "Анна", "declension": "a" }],
|
||||||
|
"surnames": [{ "male": "Иванов", "female": "Иванова" }]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
"""),
|
"""),
|
||||||
PackDocuments.Def("addon", "traits", "stoic", """{ "defName": "Stoic", "weight": 3 }"""),
|
PackDocuments.Def("addon", "traits", "stoic", """{ "defName": "Stoic", "weight": 3 }"""),
|
||||||
PackDocuments.Def("addon", "traits", "cheerful", """{ "defName": "Cheerful", "weight": 2 }"""),
|
PackDocuments.Def("addon", "traits", "cheerful", """{ "defName": "Cheerful", "weight": 2 }"""),
|
||||||
PackDocuments.Def(
|
PackDocuments.Def(
|
||||||
"addon",
|
"addon",
|
||||||
"namesets",
|
"countries",
|
||||||
"nordic",
|
"nordic",
|
||||||
"""
|
"""
|
||||||
{
|
{
|
||||||
"defName": "Nordic",
|
"defName": "Nordic",
|
||||||
"maleGiven": [{ "form": "Lars", "declension": "indeclinable" }],
|
"climatePresets": ["TemperateContinental"],
|
||||||
"femaleGiven": [{ "form": "Ingrid", "declension": "indeclinable" }],
|
"names": {
|
||||||
"surnames": [{ "male": "Berg", "female": "Berg", "declension": "indeclinable" }]
|
"maleGiven": [{ "form": "Lars", "declension": "indeclinable" }],
|
||||||
|
"femaleGiven": [{ "form": "Ingrid", "declension": "indeclinable" }],
|
||||||
|
"surnames": [{ "male": "Berg", "female": "Berg", "declension": "indeclinable" }]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
"""),
|
"""),
|
||||||
PackDocuments.Locale("addon", "ru", """{ "Nordic": "Северный", "Stoic": "Стоик", "Cheerful": "Жизнерадостный" }"""),
|
PackDocuments.Locale("addon", "ru", """{ "Nordic": "Северный", "Stoic": "Стоик", "Cheerful": "Жизнерадостный" }"""),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
Assert.True(catalog.NameSets.ContainsKey("Nordic"));
|
Assert.True(catalog.Countries.ContainsKey("Nordic"));
|
||||||
Assert.True(catalog.Traits.ContainsKey("Stoic"));
|
Assert.True(catalog.Traits.ContainsKey("Stoic"));
|
||||||
Assert.True(catalog.Traits.ContainsKey("Cheerful"));
|
Assert.True(catalog.Traits.ContainsKey("Cheerful"));
|
||||||
Assert.Equal("Северный", catalog.Label("ru", catalog.NameSets["Nordic"]));
|
Assert.Equal("Северный", catalog.Label("ru", catalog.Countries["Nordic"]));
|
||||||
Assert.Equal("Стоик", catalog.Label("ru", catalog.Traits["Stoic"]));
|
Assert.Equal("Стоик", catalog.Label("ru", catalog.Traits["Stoic"]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -128,7 +128,8 @@ public class VanillaCoreTests
|
|||||||
keys.AddRange(Names(catalog.Traits.Values));
|
keys.AddRange(Names(catalog.Traits.Values));
|
||||||
keys.AddRange(Names(catalog.BodyAttributes.Values));
|
keys.AddRange(Names(catalog.BodyAttributes.Values));
|
||||||
keys.AddRange(Names(catalog.Needs.Values));
|
keys.AddRange(Names(catalog.Needs.Values));
|
||||||
keys.AddRange(Names(catalog.NameSets.Values));
|
keys.AddRange(Names(catalog.Countries.Values));
|
||||||
|
keys.AddRange(Names(catalog.ClimatePresets.Values));
|
||||||
keys.AddRange(Names(catalog.Subjects.Values));
|
keys.AddRange(Names(catalog.Subjects.Values));
|
||||||
keys.AddRange(Names(catalog.Staffing.Values));
|
keys.AddRange(Names(catalog.Staffing.Values));
|
||||||
keys.AddRange(Names(catalog.DayFrames.Values));
|
keys.AddRange(Names(catalog.DayFrames.Values));
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ public class ApplicantPoolTests
|
|||||||
{
|
{
|
||||||
var roster = Fixtures.Generate(Fixtures.VanillaMap());
|
var roster = Fixtures.Generate(Fixtures.VanillaMap());
|
||||||
var first = Create(roster);
|
var first = Create(roster);
|
||||||
var next = first.Advance(Fixtures.Catalog(), roster, Fixtures.SchoolSeed, "Slavic", Fixtures.AsOf.AddDays(7));
|
var next = first.Advance(Fixtures.Catalog(), roster, Fixtures.SchoolSeed, "Russia", Fixtures.AsOf.AddDays(7));
|
||||||
|
|
||||||
Assert.NotEqual(first.Week, next.Week);
|
Assert.NotEqual(first.Week, next.Week);
|
||||||
var stayed = first.Applicants
|
var stayed = first.Applicants
|
||||||
@@ -45,12 +45,12 @@ public class ApplicantPoolTests
|
|||||||
var roster = Fixtures.Generate(Fixtures.VanillaMap());
|
var roster = Fixtures.Generate(Fixtures.VanillaMap());
|
||||||
var size = catalog.StaffingRules!.PoolSize;
|
var size = catalog.StaffingRules!.PoolSize;
|
||||||
var asOf = Fixtures.AsOf;
|
var asOf = Fixtures.AsOf;
|
||||||
var pool = ApplicantPool.Create(catalog, roster, Fixtures.SchoolSeed, "Slavic", asOf);
|
var pool = ApplicantPool.Create(catalog, roster, Fixtures.SchoolSeed, "Russia", asOf);
|
||||||
|
|
||||||
for (var week = 0; week < 50; week++)
|
for (var week = 0; week < 50; week++)
|
||||||
{
|
{
|
||||||
asOf = asOf.AddDays(7);
|
asOf = asOf.AddDays(7);
|
||||||
pool = pool.Advance(catalog, roster, Fixtures.SchoolSeed, "Slavic", asOf);
|
pool = pool.Advance(catalog, roster, Fixtures.SchoolSeed, "Russia", asOf);
|
||||||
Assert.Equal(size, pool.Applicants.Count);
|
Assert.Equal(size, pool.Applicants.Count);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -94,7 +94,7 @@ public class ApplicantPoolTests
|
|||||||
for (var seed = 1; seed <= 20 && pool is null; seed++)
|
for (var seed = 1; seed <= 20 && pool is null; seed++)
|
||||||
{
|
{
|
||||||
var candidate = Fixtures.Generate(Fixtures.VanillaMap(), seed);
|
var candidate = Fixtures.Generate(Fixtures.VanillaMap(), seed);
|
||||||
var created = ApplicantPool.Create(Fixtures.Catalog(), candidate, seed, "Slavic", Fixtures.AsOf);
|
var created = ApplicantPool.Create(Fixtures.Catalog(), candidate, seed, "Russia", Fixtures.AsOf);
|
||||||
if (created.Applicants.Any(applicant => candidate.People.Any(person => person.Id == applicant.Person.Id)))
|
if (created.Applicants.Any(applicant => candidate.People.Any(person => person.Id == applicant.Person.Id)))
|
||||||
{
|
{
|
||||||
roster = candidate;
|
roster = candidate;
|
||||||
@@ -132,7 +132,7 @@ public class ApplicantPoolTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static ApplicantPool Create(Roster roster) =>
|
private static ApplicantPool Create(Roster roster) =>
|
||||||
ApplicantPool.Create(Fixtures.Catalog(), roster, Fixtures.SchoolSeed, "Slavic", Fixtures.AsOf);
|
ApplicantPool.Create(Fixtures.Catalog(), roster, Fixtures.SchoolSeed, "Russia", Fixtures.AsOf);
|
||||||
|
|
||||||
private static string Snapshot(ApplicantPool pool) =>
|
private static string Snapshot(ApplicantPool pool) =>
|
||||||
string.Join('\n', pool.Applicants.Select(applicant =>
|
string.Join('\n', pool.Applicants.Select(applicant =>
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ internal static class Fixtures
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static Roster Generate(MapLayout map, int seed = SchoolSeed) =>
|
public static Roster Generate(MapLayout map, int seed = SchoolSeed) =>
|
||||||
RosterGenerator.Generate(Catalog(), map, seed, "Slavic", AsOf, "RussianLanguage");
|
RosterGenerator.Generate(Catalog(), map, seed, "Russia", AsOf, "RussianLanguage");
|
||||||
|
|
||||||
public static string RepoRoot()
|
public static string RepoRoot()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ public class GoldenRosterTests
|
|||||||
public void GoldenRoster_MatchesFixture()
|
public void GoldenRoster_MatchesFixture()
|
||||||
{
|
{
|
||||||
var roster = Fixtures.Generate(Fixtures.Classrooms(4));
|
var roster = Fixtures.Generate(Fixtures.Classrooms(4));
|
||||||
var actual = RosterFingerprint.Format(roster, Fixtures.SchoolSeed, "Slavic", Native);
|
var actual = RosterFingerprint.Format(roster, Fixtures.SchoolSeed, "Russia", Native);
|
||||||
var relative = Path.Combine("golden", "roster.txt");
|
var relative = Path.Combine("golden", "roster.txt");
|
||||||
var output = Path.Combine(AppContext.BaseDirectory, relative);
|
var output = Path.Combine(AppContext.BaseDirectory, relative);
|
||||||
var source = Path.Combine(Fixtures.RepoRoot(), "tests", "HSchool.People.Tests", relative);
|
var source = Path.Combine(Fixtures.RepoRoot(), "tests", "HSchool.People.Tests", relative);
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ public class GoldenSaveDump
|
|||||||
|
|
||||||
var catalog = Fixtures.Catalog();
|
var catalog = Fixtures.Catalog();
|
||||||
var map = TwoClassrooms();
|
var map = TwoClassrooms();
|
||||||
var roster = RosterGenerator.Generate(catalog, map, schoolSeed: 1, "Slavic", Fixtures.AsOf, "RussianLanguage");
|
var roster = RosterGenerator.Generate(catalog, map, schoolSeed: 1, "Russia", Fixtures.AsOf, "RussianLanguage");
|
||||||
var pool = ApplicantPool.Create(catalog, roster, schoolSeed: 1, "Slavic", Fixtures.AsOf, "RussianLanguage");
|
var pool = ApplicantPool.Create(catalog, roster, schoolSeed: 1, "Russia", Fixtures.AsOf, "RussianLanguage");
|
||||||
var people = RosterJson.Serialize(RosterDocument.From(1, roster, pool));
|
var people = RosterJson.Serialize(RosterDocument.From(1, roster, pool));
|
||||||
var mapNode = JsonNode.Parse(JsonSerializer.Serialize(map, Jsonc.SerializerOptions));
|
var mapNode = JsonNode.Parse(JsonSerializer.Serialize(map, Jsonc.SerializerOptions));
|
||||||
var current = SchoolJson(mapNode, includeNative: true);
|
var current = SchoolJson(mapNode, includeNative: true);
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ public class ReviewFixTests
|
|||||||
for (var year = 0; year < 4; year++)
|
for (var year = 0; year < 4; year++)
|
||||||
{
|
{
|
||||||
var before = roster.People.ToDictionary(person => person.Id, StringComparer.Ordinal);
|
var before = roster.People.ToDictionary(person => person.Id, StringComparer.Ordinal);
|
||||||
roster = YearlyIntake.Apply(catalog, roster, Fixtures.SchoolSeed, "Slavic", date);
|
roster = YearlyIntake.Apply(catalog, roster, Fixtures.SchoolSeed, "Russia", date);
|
||||||
date = date.AddYears(1);
|
date = date.AddYears(1);
|
||||||
|
|
||||||
foreach (var person in roster.People)
|
foreach (var person in roster.People)
|
||||||
@@ -98,7 +98,7 @@ public class ReviewFixTests
|
|||||||
catalog,
|
catalog,
|
||||||
roster,
|
roster,
|
||||||
Fixtures.SchoolSeed,
|
Fixtures.SchoolSeed,
|
||||||
"Slavic",
|
"Russia",
|
||||||
new DateTime(2012, 9, 1, 0, 0, 0, DateTimeKind.Utc));
|
new DateTime(2012, 9, 1, 0, 0, 0, DateTimeKind.Utc));
|
||||||
|
|
||||||
// Every seat an intake fills is a first-year seat, so a brand-new family that took two or
|
// Every seat an intake fills is a first-year seat, so a brand-new family that took two or
|
||||||
@@ -153,7 +153,7 @@ public class ReviewFixTests
|
|||||||
public void AChildOfASingleMother_StillCarriesTheFathersSurnameAndPatronymic()
|
public void AChildOfASingleMother_StillCarriesTheFathersSurnameAndPatronymic()
|
||||||
{
|
{
|
||||||
var catalog = Fixtures.Catalog();
|
var catalog = Fixtures.Catalog();
|
||||||
var names = catalog.NameSets["Slavic"];
|
var names = catalog.Countries["Russia"].Names;
|
||||||
var roster = Fixtures.Generate(Fixtures.Classrooms(11));
|
var roster = Fixtures.Generate(Fixtures.Classrooms(11));
|
||||||
var people = roster.People.ToDictionary(person => person.Id, StringComparer.Ordinal);
|
var people = roster.People.ToDictionary(person => person.Id, StringComparer.Ordinal);
|
||||||
|
|
||||||
@@ -180,7 +180,7 @@ public class ReviewFixTests
|
|||||||
public void ASingleMotherFamily_CanStillTakeInAYoungerSibling()
|
public void ASingleMotherFamily_CanStillTakeInAYoungerSibling()
|
||||||
{
|
{
|
||||||
var catalog = Fixtures.Catalog();
|
var catalog = Fixtures.Catalog();
|
||||||
var names = catalog.NameSets["Slavic"];
|
var names = catalog.Countries["Russia"].Names;
|
||||||
var roster = Fixtures.Generate(Fixtures.Classrooms(11));
|
var roster = Fixtures.Generate(Fixtures.Classrooms(11));
|
||||||
var people = roster.People.ToDictionary(person => person.Id, StringComparer.Ordinal);
|
var people = roster.People.ToDictionary(person => person.Id, StringComparer.Ordinal);
|
||||||
var motherOnly = roster.Families
|
var motherOnly = roster.Families
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ namespace HSchool.People.Tests;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
internal static class RosterFingerprint
|
internal static class RosterFingerprint
|
||||||
{
|
{
|
||||||
public static string Format(Roster roster, int seed, string nameSet, string native)
|
public static string Format(Roster roster, int seed, string country, string native)
|
||||||
{
|
{
|
||||||
var lines = new List<string>
|
var lines = new List<string>
|
||||||
{
|
{
|
||||||
$"# seed={seed} nameset={nameSet} native={native} people={roster.People.Count} classes={roster.Classes.Count}",
|
$"# seed={seed} country={country} native={native} people={roster.People.Count} classes={roster.Classes.Count}",
|
||||||
};
|
};
|
||||||
|
|
||||||
foreach (var schoolClass in roster.Classes.OrderBy(row => row.Id, StringComparer.Ordinal))
|
foreach (var schoolClass in roster.Classes.OrderBy(row => row.Id, StringComparer.Ordinal))
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ namespace HSchool.People.Tests;
|
|||||||
public class RosterGeneratorTests
|
public class RosterGeneratorTests
|
||||||
{
|
{
|
||||||
[Fact]
|
[Fact]
|
||||||
public void SameSeedMapAndNameSet_YieldTheSameRoster()
|
public void SameSeedMapAndCountry_YieldTheSameRoster()
|
||||||
{
|
{
|
||||||
var map = Fixtures.Classrooms(4);
|
var map = Fixtures.Classrooms(4);
|
||||||
var a = Fixtures.Generate(map);
|
var a = Fixtures.Generate(map);
|
||||||
|
|||||||
@@ -70,11 +70,11 @@ public class SkillGrantTests
|
|||||||
catalog,
|
catalog,
|
||||||
Fixtures.Classrooms(11),
|
Fixtures.Classrooms(11),
|
||||||
Fixtures.SchoolSeed,
|
Fixtures.SchoolSeed,
|
||||||
"Slavic",
|
"Russia",
|
||||||
Fixtures.AsOf,
|
Fixtures.AsOf,
|
||||||
"BelarusianLanguage");
|
"BelarusianLanguage");
|
||||||
|
|
||||||
var max = catalog.NameSets["Slavic"].RelatedLanguageMax;
|
var max = catalog.Countries["Russia"].Names.RelatedLanguageMax;
|
||||||
Assert.All(
|
Assert.All(
|
||||||
roster.People,
|
roster.People,
|
||||||
person =>
|
person =>
|
||||||
@@ -97,7 +97,7 @@ public class SkillGrantTests
|
|||||||
public void RelatedLanguages_AreCommonAndStayLow()
|
public void RelatedLanguages_AreCommonAndStayLow()
|
||||||
{
|
{
|
||||||
var catalog = Fixtures.Catalog();
|
var catalog = Fixtures.Catalog();
|
||||||
var max = catalog.NameSets["Slavic"].RelatedLanguageMax;
|
var max = catalog.Countries["Russia"].Names.RelatedLanguageMax;
|
||||||
var roster = Fixtures.Generate(Fixtures.Classrooms(11));
|
var roster = Fixtures.Generate(Fixtures.Classrooms(11));
|
||||||
Assert.Contains(
|
Assert.Contains(
|
||||||
roster.People,
|
roster.People,
|
||||||
@@ -138,7 +138,7 @@ public class SkillGrantTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void ReloadWithoutASavedPick_TakesTheFirstLanguageInsteadOfRolling()
|
public void ReloadWithoutASavedPick_TakesTheFirstLanguageInsteadOfRolling()
|
||||||
{
|
{
|
||||||
var names = Fixtures.Catalog().NameSets["Slavic"];
|
var names = Fixtures.Catalog().Countries["Russia"].Names;
|
||||||
Assert.True(names.Spoken.Count > 1, "this pins behaviour that only matters for a multi-language set");
|
Assert.True(names.Spoken.Count > 1, "this pins behaviour that only matters for a multi-language set");
|
||||||
|
|
||||||
foreach (var seed in new[] { 1, 2, 7, 20260818 })
|
foreach (var seed in new[] { 1, 2, 7, 20260818 })
|
||||||
@@ -164,8 +164,8 @@ public class SkillGrantTests
|
|||||||
public void OmittedNativeLanguage_IsStableForTheSameSeed()
|
public void OmittedNativeLanguage_IsStableForTheSameSeed()
|
||||||
{
|
{
|
||||||
var map = Fixtures.Classrooms(2);
|
var map = Fixtures.Classrooms(2);
|
||||||
var a = RosterGenerator.Generate(Fixtures.Catalog(), map, 7, "Slavic", Fixtures.AsOf);
|
var a = RosterGenerator.Generate(Fixtures.Catalog(), map, 7, "Russia", Fixtures.AsOf);
|
||||||
var b = RosterGenerator.Generate(Fixtures.Catalog(), map, 7, "Slavic", Fixtures.AsOf);
|
var b = RosterGenerator.Generate(Fixtures.Catalog(), map, 7, "Russia", Fixtures.AsOf);
|
||||||
Assert.Equal(
|
Assert.Equal(
|
||||||
a.People.Select(person => string.Join(',', person.Skills.Keys.Order(StringComparer.Ordinal))),
|
a.People.Select(person => string.Join(',', person.Skills.Keys.Order(StringComparer.Ordinal))),
|
||||||
b.People.Select(person => string.Join(',', person.Skills.Keys.Order(StringComparer.Ordinal))));
|
b.People.Select(person => string.Join(',', person.Skills.Keys.Order(StringComparer.Ordinal))));
|
||||||
@@ -195,7 +195,7 @@ public class SkillGrantTests
|
|||||||
catalog,
|
catalog,
|
||||||
before,
|
before,
|
||||||
Fixtures.SchoolSeed,
|
Fixtures.SchoolSeed,
|
||||||
"Slavic",
|
"Russia",
|
||||||
new DateTime(2012, 9, 1, 0, 0, 0, DateTimeKind.Utc));
|
new DateTime(2012, 9, 1, 0, 0, 0, DateTimeKind.Utc));
|
||||||
var grown = after.People.First(person => person.Id == pupil.Id);
|
var grown = after.People.First(person => person.Id == pupil.Id);
|
||||||
Assert.True(grown.ClassId is { } classId && after.Classes.Any(schoolClass =>
|
Assert.True(grown.ClassId is { } classId && after.Classes.Any(schoolClass =>
|
||||||
|
|||||||
@@ -178,7 +178,7 @@ public class StaffingTests
|
|||||||
for (var seed = 1; seed <= 30 && pool is null; seed++)
|
for (var seed = 1; seed <= 30 && pool is null; seed++)
|
||||||
{
|
{
|
||||||
var candidate = Fixtures.Generate(map, seed);
|
var candidate = Fixtures.Generate(map, seed);
|
||||||
var created = ApplicantPool.Create(catalog, candidate, seed, "Slavic", Fixtures.AsOf);
|
var created = ApplicantPool.Create(catalog, candidate, seed, "Russia", Fixtures.AsOf);
|
||||||
var rosterIds = candidate.People.Select(person => person.Id).ToHashSet(StringComparer.Ordinal);
|
var rosterIds = candidate.People.Select(person => person.Id).ToHashSet(StringComparer.Ordinal);
|
||||||
var parentHere = created.Applicants.FirstOrDefault(applicant => rosterIds.Contains(applicant.Person.Id));
|
var parentHere = created.Applicants.FirstOrDefault(applicant => rosterIds.Contains(applicant.Person.Id));
|
||||||
var generatedHere = created.Applicants.FirstOrDefault(applicant => applicant.Person.Id.StartsWith('a'));
|
var generatedHere = created.Applicants.FirstOrDefault(applicant => applicant.Person.Id.StartsWith('a'));
|
||||||
@@ -292,7 +292,7 @@ public class StaffingTests
|
|||||||
var catalog = Fixtures.Catalog();
|
var catalog = Fixtures.Catalog();
|
||||||
var map = Fixtures.VanillaMap();
|
var map = Fixtures.VanillaMap();
|
||||||
var roster = Fixtures.Generate(map);
|
var roster = Fixtures.Generate(map);
|
||||||
var pool = ApplicantPool.Create(catalog, roster, Fixtures.SchoolSeed, "Slavic", Fixtures.AsOf);
|
var pool = ApplicantPool.Create(catalog, roster, Fixtures.SchoolSeed, "Russia", Fixtures.AsOf);
|
||||||
return (catalog, map, roster, pool);
|
return (catalog, map, roster, pool);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ public class YearlyIntakeTests
|
|||||||
Assert.Equal(4, before.Classes.Max(schoolClass => schoolClass.Year));
|
Assert.Equal(4, before.Classes.Max(schoolClass => schoolClass.Year));
|
||||||
Assert.DoesNotContain(before.Classes, schoolClass => schoolClass.Year == 11);
|
Assert.DoesNotContain(before.Classes, schoolClass => schoolClass.Year == 11);
|
||||||
|
|
||||||
var after = YearlyIntake.Apply(catalog, before, Fixtures.SchoolSeed, "Slavic", September);
|
var after = YearlyIntake.Apply(catalog, before, Fixtures.SchoolSeed, "Russia", September);
|
||||||
var demand = SchoolDemand.From(catalog, map);
|
var demand = SchoolDemand.From(catalog, map);
|
||||||
|
|
||||||
Assert.True(RosterFit.Matches(after, demand));
|
Assert.True(RosterFit.Matches(after, demand));
|
||||||
@@ -46,8 +46,8 @@ public class YearlyIntakeTests
|
|||||||
var map = Fixtures.Classrooms(4);
|
var map = Fixtures.Classrooms(4);
|
||||||
var catalog = Fixtures.Catalog();
|
var catalog = Fixtures.Catalog();
|
||||||
var before = Fixtures.Generate(map);
|
var before = Fixtures.Generate(map);
|
||||||
var a = Snapshot(YearlyIntake.Apply(catalog, before, Fixtures.SchoolSeed, "Slavic", September));
|
var a = Snapshot(YearlyIntake.Apply(catalog, before, Fixtures.SchoolSeed, "Russia", September));
|
||||||
var b = Snapshot(YearlyIntake.Apply(catalog, before, Fixtures.SchoolSeed, "Slavic", September));
|
var b = Snapshot(YearlyIntake.Apply(catalog, before, Fixtures.SchoolSeed, "Russia", September));
|
||||||
Assert.Equal(a, b);
|
Assert.Equal(a, b);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,7 +57,7 @@ public class YearlyIntakeTests
|
|||||||
var map = Fixtures.Classrooms(4);
|
var map = Fixtures.Classrooms(4);
|
||||||
var catalog = Fixtures.Catalog();
|
var catalog = Fixtures.Catalog();
|
||||||
var before = Fixtures.Generate(map);
|
var before = Fixtures.Generate(map);
|
||||||
var after = YearlyIntake.Apply(catalog, before, Fixtures.SchoolSeed, "Slavic", September);
|
var after = YearlyIntake.Apply(catalog, before, Fixtures.SchoolSeed, "Russia", September);
|
||||||
var people = after.People.ToDictionary(person => person.Id, StringComparer.Ordinal);
|
var people = after.People.ToDictionary(person => person.Id, StringComparer.Ordinal);
|
||||||
var yearOf = after.Classes.ToDictionary(schoolClass => schoolClass.Id, schoolClass => schoolClass.Year, StringComparer.Ordinal);
|
var yearOf = after.Classes.ToDictionary(schoolClass => schoolClass.Id, schoolClass => schoolClass.Year, StringComparer.Ordinal);
|
||||||
|
|
||||||
@@ -106,7 +106,7 @@ public class YearlyIntakeTests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var after = YearlyIntake.Apply(catalog, before, Fixtures.SchoolSeed, "Slavic", September);
|
var after = YearlyIntake.Apply(catalog, before, Fixtures.SchoolSeed, "Russia", September);
|
||||||
var afterIds = after.People.Select(person => person.Id).ToHashSet(StringComparer.Ordinal);
|
var afterIds = after.People.Select(person => person.Id).ToHashSet(StringComparer.Ordinal);
|
||||||
Assert.NotEmpty(shouldLeave);
|
Assert.NotEmpty(shouldLeave);
|
||||||
Assert.All(shouldLeave, id => Assert.DoesNotContain(id, afterIds));
|
Assert.All(shouldLeave, id => Assert.DoesNotContain(id, afterIds));
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# seed=20260818 nameset=Slavic native=RussianLanguage people=130 classes=4
|
# seed=20260818 country=Russia native=RussianLanguage people=130 classes=4
|
||||||
class class-classroom-00 year=1 letter=А room=classroom-00 capacity=16 pupils=f1.c1,f1.c2,f13.c0,f18.c0,f19.c0,f24.c1,f26.c1,f27.c1,f27.c2,f28.c2,f32.c1,f33.c0,f34.c0,f6.c0,f6.c2,f8.c0
|
class class-classroom-00 year=1 letter=А room=classroom-00 capacity=16 pupils=f1.c1,f1.c2,f13.c0,f18.c0,f19.c0,f24.c1,f26.c1,f27.c1,f27.c2,f28.c2,f32.c1,f33.c0,f34.c0,f6.c0,f6.c2,f8.c0
|
||||||
class class-classroom-01 year=2 letter=А room=classroom-01 capacity=16 pupils=f0.c1,f12.c0,f12.c1,f12.c2,f13.c1,f13.c2,f14.c2,f20.c0,f23.c1,f26.c0,f27.c0,f29.c0,f31.c0,f4.c0,f7.c0,f9.c0
|
class class-classroom-01 year=2 letter=А room=classroom-01 capacity=16 pupils=f0.c1,f12.c0,f12.c1,f12.c2,f13.c1,f13.c2,f14.c2,f20.c0,f23.c1,f26.c0,f27.c0,f29.c0,f31.c0,f4.c0,f7.c0,f9.c0
|
||||||
class class-classroom-02 year=3 letter=А room=classroom-02 capacity=16 pupils=f10.c0,f14.c0,f16.c0,f17.c1,f2.c0,f21.c0,f22.c1,f23.c0,f25.c0,f28.c0,f3.c0,f3.c1,f32.c0,f32.c2,f5.c0,f6.c1
|
class class-classroom-02 year=3 letter=А room=classroom-02 capacity=16 pupils=f10.c0,f14.c0,f16.c0,f17.c1,f2.c0,f21.c0,f22.c1,f23.c0,f25.c0,f28.c0,f3.c0,f3.c1,f32.c0,f32.c2,f5.c0,f6.c1
|
||||||
|
|||||||
@@ -172,12 +172,12 @@ public class DecisionTests
|
|||||||
private static (School School, string Homeroom, string PupilId) TwoHomeroomLessons()
|
private static (School School, string Homeroom, string PupilId) TwoHomeroomLessons()
|
||||||
{
|
{
|
||||||
var (catalog, map) = Vanilla();
|
var (catalog, map) = Vanilla();
|
||||||
var roster = RosterGenerator.Generate(catalog, map, schoolSeed: 1, "Slavic", TuesdayMorning);
|
var roster = RosterGenerator.Generate(catalog, map, schoolSeed: 1, "Russia", TuesdayMorning);
|
||||||
var pool = ApplicantPool.Create(catalog, roster, schoolSeed: 1, "Slavic", TuesdayMorning);
|
var pool = ApplicantPool.Create(catalog, roster, schoolSeed: 1, "Russia", TuesdayMorning);
|
||||||
var schoolClass = roster.Classes.First(row =>
|
var schoolClass = roster.Classes.First(row =>
|
||||||
row.RoomId is "classroom-101" or "classroom-102" or "classroom-103" or "classroom-104");
|
row.RoomId is "classroom-101" or "classroom-102" or "classroom-103" or "classroom-104");
|
||||||
var school = School.Create(1, "Два урока", TuesdayMorning, catalog, map);
|
var school = School.Create(1, "Два урока", TuesdayMorning, catalog, map);
|
||||||
school.InstallPeople(roster, seed: 1, "Slavic", pool);
|
school.InstallPeople(roster, seed: 1, "Russia", pool);
|
||||||
school.SetTimetable(new Timetable(
|
school.SetTimetable(new Timetable(
|
||||||
[
|
[
|
||||||
new LessonPlacement(schoolClass.Id, "Mathematics", "t1", schoolClass.RoomId, Day: 1, Period: 1),
|
new LessonPlacement(schoolClass.Id, "Mathematics", "t1", schoolClass.RoomId, Day: 1, Period: 1),
|
||||||
@@ -193,12 +193,12 @@ public class DecisionTests
|
|||||||
|
|
||||||
private static School OpenStaffed(DefCatalog catalog, MapLayout map, int seed)
|
private static School OpenStaffed(DefCatalog catalog, MapLayout map, int seed)
|
||||||
{
|
{
|
||||||
var roster = RosterGenerator.Generate(catalog, map, seed, "Slavic", TuesdayMorning);
|
var roster = RosterGenerator.Generate(catalog, map, seed, "Russia", TuesdayMorning);
|
||||||
var pool = ApplicantPool.Create(catalog, roster, seed, "Slavic", TuesdayMorning);
|
var pool = ApplicantPool.Create(catalog, roster, seed, "Russia", TuesdayMorning);
|
||||||
var schoolClass = roster.Classes.First(row =>
|
var schoolClass = roster.Classes.First(row =>
|
||||||
row.RoomId is "classroom-101" or "classroom-102" or "classroom-103" or "classroom-104");
|
row.RoomId is "classroom-101" or "classroom-102" or "classroom-103" or "classroom-104");
|
||||||
var school = School.Create(seed, "Решения", TuesdayMorning, catalog, map);
|
var school = School.Create(seed, "Решения", TuesdayMorning, catalog, map);
|
||||||
school.InstallPeople(roster, seed, "Slavic", pool);
|
school.InstallPeople(roster, seed, "Russia", pool);
|
||||||
school.SetTimetable(new Timetable(
|
school.SetTimetable(new Timetable(
|
||||||
[
|
[
|
||||||
new LessonPlacement(schoolClass.Id, "Mathematics", "t1", schoolClass.RoomId, Day: 1, Period: 1),
|
new LessonPlacement(schoolClass.Id, "Mathematics", "t1", schoolClass.RoomId, Day: 1, Period: 1),
|
||||||
|
|||||||
@@ -93,8 +93,8 @@ public class LunchTests
|
|||||||
private static School StaffedSchool()
|
private static School StaffedSchool()
|
||||||
{
|
{
|
||||||
var (catalog, map) = Vanilla();
|
var (catalog, map) = Vanilla();
|
||||||
var roster = RosterGenerator.Generate(catalog, map, schoolSeed: 1, "Slavic", Tuesday6);
|
var roster = RosterGenerator.Generate(catalog, map, schoolSeed: 1, "Russia", Tuesday6);
|
||||||
var pool = ApplicantPool.Create(catalog, roster, schoolSeed: 1, "Slavic", Tuesday6);
|
var pool = ApplicantPool.Create(catalog, roster, schoolSeed: 1, "Russia", Tuesday6);
|
||||||
|
|
||||||
foreach (var subject in catalog.Subjects.Values.Where(def => !def.Abstract).Select(def => def.DefName))
|
foreach (var subject in catalog.Subjects.Values.Where(def => !def.Abstract).Select(def => def.DefName))
|
||||||
{
|
{
|
||||||
@@ -120,7 +120,7 @@ public class LunchTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
var school = School.Create(1, "Столовая", Tuesday6, catalog, map);
|
var school = School.Create(1, "Столовая", Tuesday6, catalog, map);
|
||||||
school.InstallPeople(roster, seed: 1, "Slavic", pool);
|
school.InstallPeople(roster, seed: 1, "Russia", pool);
|
||||||
school.SetTimetable(SchoolTimetables.Build(catalog, map, roster, null, weekDays: 5));
|
school.SetTimetable(SchoolTimetables.Build(catalog, map, roster, null, weekDays: 5));
|
||||||
school.ConfigurePresence(weekDays: 5, maxDecisionsPerTick: 64);
|
school.ConfigurePresence(weekDays: 5, maxDecisionsPerTick: 64);
|
||||||
return school;
|
return school;
|
||||||
|
|||||||
@@ -14,11 +14,11 @@ public class PeopleInSchoolTests
|
|||||||
{
|
{
|
||||||
var (catalog, map) = Vanilla();
|
var (catalog, map) = Vanilla();
|
||||||
var demand = SchoolDemand.From(catalog, map);
|
var demand = SchoolDemand.From(catalog, map);
|
||||||
var roster = RosterGenerator.Generate(catalog, map, schoolSeed: 1, "Slavic", Start);
|
var roster = RosterGenerator.Generate(catalog, map, schoolSeed: 1, "Russia", Start);
|
||||||
var pool = ApplicantPool.Create(catalog, roster, schoolSeed: 1, "Slavic", Start);
|
var pool = ApplicantPool.Create(catalog, roster, schoolSeed: 1, "Russia", Start);
|
||||||
|
|
||||||
using var school = School.Create(1, "Полная", Start, catalog, map);
|
using var school = School.Create(1, "Полная", Start, catalog, map);
|
||||||
school.InstallPeople(roster, seed: 1, "Slavic", pool);
|
school.InstallPeople(roster, seed: 1, "Russia", pool);
|
||||||
school.Tick(1d / 20d, 5d);
|
school.Tick(1d / 20d, 5d);
|
||||||
|
|
||||||
Assert.Same(roster, school.Roster);
|
Assert.Same(roster, school.Roster);
|
||||||
@@ -39,11 +39,11 @@ public class PeopleInSchoolTests
|
|||||||
{
|
{
|
||||||
var start = new DateTime(2012, 3, 31, 6, 0, 0, DateTimeKind.Utc);
|
var start = new DateTime(2012, 3, 31, 6, 0, 0, DateTimeKind.Utc);
|
||||||
var (catalog, map) = Vanilla();
|
var (catalog, map) = Vanilla();
|
||||||
var roster = RosterGenerator.Generate(catalog, map, schoolSeed: 1, "Slavic", start);
|
var roster = RosterGenerator.Generate(catalog, map, schoolSeed: 1, "Russia", start);
|
||||||
var pool = ApplicantPool.Create(catalog, roster, schoolSeed: 1, "Slavic", start);
|
var pool = ApplicantPool.Create(catalog, roster, schoolSeed: 1, "Russia", start);
|
||||||
|
|
||||||
using var school = School.Create(1, "Неделя", start, catalog, map);
|
using var school = School.Create(1, "Неделя", start, catalog, map);
|
||||||
school.InstallPeople(roster, seed: 1, "Slavic", pool);
|
school.InstallPeople(roster, seed: 1, "Russia", pool);
|
||||||
|
|
||||||
var monday = new DateTime(2012, 4, 2, 6, 0, 0, DateTimeKind.Utc);
|
var monday = new DateTime(2012, 4, 2, 6, 0, 0, DateTimeKind.Utc);
|
||||||
var changed = school.Tick((monday - start).TotalMinutes / 5d, 5d);
|
var changed = school.Tick((monday - start).TotalMinutes / 5d, 5d);
|
||||||
@@ -60,7 +60,7 @@ public class PeopleInSchoolTests
|
|||||||
public void OffCampusNeeds_DoNotMoveOnTick()
|
public void OffCampusNeeds_DoNotMoveOnTick()
|
||||||
{
|
{
|
||||||
var (catalog, map) = Vanilla();
|
var (catalog, map) = Vanilla();
|
||||||
var roster = RosterGenerator.Generate(catalog, map, schoolSeed: 2, "Slavic", Start);
|
var roster = RosterGenerator.Generate(catalog, map, schoolSeed: 2, "Russia", Start);
|
||||||
using var school = School.Create(2, "Нужды", Start, catalog, map);
|
using var school = School.Create(2, "Нужды", Start, catalog, map);
|
||||||
school.InstallPeople(roster, seed: 2);
|
school.InstallPeople(roster, seed: 2);
|
||||||
|
|
||||||
@@ -108,8 +108,8 @@ public class PeopleInSchoolTests
|
|||||||
public void SameSeed_InstallsTheSameNames()
|
public void SameSeed_InstallsTheSameNames()
|
||||||
{
|
{
|
||||||
var (catalog, map) = Vanilla();
|
var (catalog, map) = Vanilla();
|
||||||
var first = RosterGenerator.Generate(catalog, map, schoolSeed: 9, "Slavic", Start);
|
var first = RosterGenerator.Generate(catalog, map, schoolSeed: 9, "Russia", Start);
|
||||||
var second = RosterGenerator.Generate(catalog, map, schoolSeed: 9, "Slavic", Start);
|
var second = RosterGenerator.Generate(catalog, map, schoolSeed: 9, "Russia", Start);
|
||||||
|
|
||||||
using var a = School.Create(9, "А", Start, catalog, map);
|
using var a = School.Create(9, "А", Start, catalog, map);
|
||||||
using var b = School.Create(9, "Б", Start, catalog, map);
|
using var b = School.Create(9, "Б", Start, catalog, map);
|
||||||
@@ -126,7 +126,7 @@ public class PeopleInSchoolTests
|
|||||||
{
|
{
|
||||||
var catalog = Catalog();
|
var catalog = Catalog();
|
||||||
var map = Classrooms(4);
|
var map = Classrooms(4);
|
||||||
var roster = RosterGenerator.Generate(catalog, map, schoolSeed: 1, "Slavic", Start);
|
var roster = RosterGenerator.Generate(catalog, map, schoolSeed: 1, "Russia", Start);
|
||||||
var demand = SchoolDemand.From(catalog, map);
|
var demand = SchoolDemand.From(catalog, map);
|
||||||
var oldest = roster.Classes.Max(schoolClass => schoolClass.Year);
|
var oldest = roster.Classes.Max(schoolClass => schoolClass.Year);
|
||||||
var graduated = roster.Classes
|
var graduated = roster.Classes
|
||||||
@@ -135,7 +135,7 @@ public class PeopleInSchoolTests
|
|||||||
.ToHashSet(StringComparer.Ordinal);
|
.ToHashSet(StringComparer.Ordinal);
|
||||||
|
|
||||||
using var school = School.Create(1, "Четыре кабинета", Start, catalog, map);
|
using var school = School.Create(1, "Четыре кабинета", Start, catalog, map);
|
||||||
school.InstallPeople(roster, seed: 1, "Slavic");
|
school.InstallPeople(roster, seed: 1, "Russia");
|
||||||
|
|
||||||
var sept = new DateTime(2012, 9, 1, 0, 0, 0, DateTimeKind.Utc);
|
var sept = new DateTime(2012, 9, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||||
var changed = school.Tick((sept - Start).TotalMinutes / 5d, 5d);
|
var changed = school.Tick((sept - Start).TotalMinutes / 5d, 5d);
|
||||||
@@ -160,9 +160,9 @@ public class PeopleInSchoolTests
|
|||||||
var start = new DateTime(2012, 9, 1, 6, 0, 0, DateTimeKind.Utc);
|
var start = new DateTime(2012, 9, 1, 6, 0, 0, DateTimeKind.Utc);
|
||||||
var catalog = Catalog();
|
var catalog = Catalog();
|
||||||
var map = Classrooms(4);
|
var map = Classrooms(4);
|
||||||
var roster = RosterGenerator.Generate(catalog, map, schoolSeed: 3, "Slavic", start);
|
var roster = RosterGenerator.Generate(catalog, map, schoolSeed: 3, "Russia", start);
|
||||||
using var school = School.Create(3, "Первое сентября", start, catalog, map);
|
using var school = School.Create(3, "Первое сентября", start, catalog, map);
|
||||||
school.InstallPeople(roster, seed: 3, "Slavic");
|
school.InstallPeople(roster, seed: 3, "Russia");
|
||||||
|
|
||||||
var years = roster.Classes.Select(schoolClass => schoolClass.Year).OrderBy(year => year).ToArray();
|
var years = roster.Classes.Select(schoolClass => schoolClass.Year).OrderBy(year => year).ToArray();
|
||||||
var changed = school.Tick(1d / 20d, 5d);
|
var changed = school.Tick(1d / 20d, 5d);
|
||||||
|
|||||||
@@ -30,15 +30,15 @@ public class PresenceTests
|
|||||||
public void ClassWithoutLessons_StaysAway_TeacherWithAnotherClassComes()
|
public void ClassWithoutLessons_StaysAway_TeacherWithAnotherClassComes()
|
||||||
{
|
{
|
||||||
var (catalog, map) = Vanilla();
|
var (catalog, map) = Vanilla();
|
||||||
var roster = RosterGenerator.Generate(catalog, map, schoolSeed: 1, "Slavic", TuesdayMorning);
|
var roster = RosterGenerator.Generate(catalog, map, schoolSeed: 1, "Russia", TuesdayMorning);
|
||||||
var pool = ApplicantPool.Create(catalog, roster, schoolSeed: 1, "Slavic", TuesdayMorning);
|
var pool = ApplicantPool.Create(catalog, roster, schoolSeed: 1, "Russia", TuesdayMorning);
|
||||||
var idle = roster.Classes[0];
|
var idle = roster.Classes[0];
|
||||||
var busy = roster.Classes[1];
|
var busy = roster.Classes[1];
|
||||||
var hired = Staffing.Hire(catalog, map, roster, pool, pool.Applicants[0].Person.Id, Staffing.TeacherPosition, Cap);
|
var hired = Staffing.Hire(catalog, map, roster, pool, pool.Applicants[0].Person.Id, Staffing.TeacherPosition, Cap);
|
||||||
Assert.Equal(StaffingError.None, hired.Error);
|
Assert.Equal(StaffingError.None, hired.Error);
|
||||||
|
|
||||||
using var school = School.Create(1, "Два класса", TuesdayMorning, catalog, map);
|
using var school = School.Create(1, "Два класса", TuesdayMorning, catalog, map);
|
||||||
school.InstallPeople(hired.Roster, seed: 1, "Slavic", hired.Pool);
|
school.InstallPeople(hired.Roster, seed: 1, "Russia", hired.Pool);
|
||||||
school.SetTimetable(new Timetable(
|
school.SetTimetable(new Timetable(
|
||||||
[new LessonPlacement(busy.Id, "Mathematics", hired.Roster.People.First(person => person.IsStaff).Id, busy.RoomId, Day: 1, Period: 1)],
|
[new LessonPlacement(busy.Id, "Mathematics", hired.Roster.People.First(person => person.IsStaff).Id, busy.RoomId, Day: 1, Period: 1)],
|
||||||
[]));
|
[]));
|
||||||
@@ -108,7 +108,7 @@ public class PresenceTests
|
|||||||
Assert.Contains(snapshot, row => row.NodeId is not null && (row.Path.Count > 0 || row.RemainingMinutes > 0));
|
Assert.Contains(snapshot, row => row.NodeId is not null && (row.Path.Count > 0 || row.RemainingMinutes > 0));
|
||||||
|
|
||||||
using var loaded = School.Load(2, "Сейв", live.Clock.Time, running: true, speedIndex: 0, catalog, map);
|
using var loaded = School.Load(2, "Сейв", live.Clock.Time, running: true, speedIndex: 0, catalog, map);
|
||||||
loaded.InstallPeople(live.Roster!, seed: 4, "Slavic", live.Applicants);
|
loaded.InstallPeople(live.Roster!, seed: 4, "Russia", live.Applicants);
|
||||||
loaded.SetTimetable(live.Timetable!);
|
loaded.SetTimetable(live.Timetable!);
|
||||||
loaded.ConfigurePresence(weekDays: 5, maxDecisionsPerTick: 10_000);
|
loaded.ConfigurePresence(weekDays: 5, maxDecisionsPerTick: 10_000);
|
||||||
loaded.RestorePresence(snapshot);
|
loaded.RestorePresence(snapshot);
|
||||||
@@ -359,12 +359,12 @@ public class PresenceTests
|
|||||||
|
|
||||||
private static School OpenStaffed(DefCatalog catalog, MapLayout map, int seed)
|
private static School OpenStaffed(DefCatalog catalog, MapLayout map, int seed)
|
||||||
{
|
{
|
||||||
var roster = RosterGenerator.Generate(catalog, map, seed, "Slavic", TuesdayMorning);
|
var roster = RosterGenerator.Generate(catalog, map, seed, "Russia", TuesdayMorning);
|
||||||
var pool = ApplicantPool.Create(catalog, roster, seed, "Slavic", TuesdayMorning);
|
var pool = ApplicantPool.Create(catalog, roster, seed, "Russia", TuesdayMorning);
|
||||||
var schoolClass = roster.Classes.First(row =>
|
var schoolClass = roster.Classes.First(row =>
|
||||||
row.RoomId is "classroom-101" or "classroom-102" or "classroom-103" or "classroom-104");
|
row.RoomId is "classroom-101" or "classroom-102" or "classroom-103" or "classroom-104");
|
||||||
var school = School.Create(seed, "Присутствие", TuesdayMorning, catalog, map);
|
var school = School.Create(seed, "Присутствие", TuesdayMorning, catalog, map);
|
||||||
school.InstallPeople(roster, seed, "Slavic", pool);
|
school.InstallPeople(roster, seed, "Russia", pool);
|
||||||
school.SetTimetable(new Timetable(
|
school.SetTimetable(new Timetable(
|
||||||
[
|
[
|
||||||
new LessonPlacement(schoolClass.Id, "Mathematics", "t1", schoolClass.RoomId, Day: 1, Period: 1),
|
new LessonPlacement(schoolClass.Id, "Mathematics", "t1", schoolClass.RoomId, Day: 1, Period: 1),
|
||||||
@@ -377,11 +377,11 @@ public class PresenceTests
|
|||||||
|
|
||||||
private static School Open(DefCatalog catalog, MapLayout map, DateTime start, int seed)
|
private static School Open(DefCatalog catalog, MapLayout map, DateTime start, int seed)
|
||||||
{
|
{
|
||||||
var roster = RosterGenerator.Generate(catalog, map, seed, "Slavic", start);
|
var roster = RosterGenerator.Generate(catalog, map, seed, "Russia", start);
|
||||||
var pool = ApplicantPool.Create(catalog, roster, seed, "Slavic", start);
|
var pool = ApplicantPool.Create(catalog, roster, seed, "Russia", start);
|
||||||
var schoolClass = roster.Classes[0];
|
var schoolClass = roster.Classes[0];
|
||||||
var school = School.Create(seed, "Присутствие", start, catalog, map);
|
var school = School.Create(seed, "Присутствие", start, catalog, map);
|
||||||
school.InstallPeople(roster, seed, "Slavic", pool);
|
school.InstallPeople(roster, seed, "Russia", pool);
|
||||||
school.SetTimetable(new Timetable(
|
school.SetTimetable(new Timetable(
|
||||||
[new LessonPlacement(schoolClass.Id, "Mathematics", "t1", schoolClass.RoomId, Day: 0, Period: 1)],
|
[new LessonPlacement(schoolClass.Id, "Mathematics", "t1", schoolClass.RoomId, Day: 0, Period: 1)],
|
||||||
[]));
|
[]));
|
||||||
@@ -392,10 +392,10 @@ public class PresenceTests
|
|||||||
private static School OpenEmpty(DateTime start)
|
private static School OpenEmpty(DateTime start)
|
||||||
{
|
{
|
||||||
var (catalog, map) = Vanilla();
|
var (catalog, map) = Vanilla();
|
||||||
var roster = RosterGenerator.Generate(catalog, map, schoolSeed: 1, "Slavic", start);
|
var roster = RosterGenerator.Generate(catalog, map, schoolSeed: 1, "Russia", start);
|
||||||
var pool = ApplicantPool.Create(catalog, roster, schoolSeed: 1, "Slavic", start);
|
var pool = ApplicantPool.Create(catalog, roster, schoolSeed: 1, "Russia", start);
|
||||||
var school = School.Create(1, "Пустая", start, catalog, map);
|
var school = School.Create(1, "Пустая", start, catalog, map);
|
||||||
school.InstallPeople(roster, seed: 1, "Slavic", pool);
|
school.InstallPeople(roster, seed: 1, "Russia", pool);
|
||||||
school.ConfigurePresence(weekDays: 5, maxDecisionsPerTick: 10_000);
|
school.ConfigurePresence(weekDays: 5, maxDecisionsPerTick: 10_000);
|
||||||
return school;
|
return school;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user