Merge phase 29 country.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -43,12 +43,12 @@ const ru = {
|
||||
errorMissingMod: 'Не выбран обязательный мод «{id}».',
|
||||
errorModCycle: 'Выбранные моды зависят друг от друга по кругу.',
|
||||
errorInvalidCatalog: 'Не удалось загрузить выбранные моды.',
|
||||
errorUnknownNameSet: 'Выбранный набор имён не найден.',
|
||||
errorUnknownNativeLanguage: 'Выбранный родной язык не входит в этот набор имён.',
|
||||
errorUnknownCountry: 'Выбранная страна не найдена.',
|
||||
errorUnknownNativeLanguage: 'Выбранный родной язык не входит в эту страну.',
|
||||
catalogLoadFailed: 'Не удалось загрузить каталог модов.',
|
||||
|
||||
modsTitle: 'Моды',
|
||||
nameSetTitle: 'Набор имён',
|
||||
countryTitle: 'Страна',
|
||||
nativeLanguageTitle: 'Родной язык',
|
||||
nativeLanguageRandom: 'Случайный',
|
||||
randomNativeTitle: 'Выбрать язык наугад',
|
||||
@@ -260,12 +260,12 @@ const en: Messages = {
|
||||
errorMissingMod: 'Required mod “{id}” is not selected.',
|
||||
errorModCycle: 'The selected mods depend on each other in a cycle.',
|
||||
errorInvalidCatalog: 'The selected packs could not be loaded.',
|
||||
errorUnknownNameSet: 'The selected name set is not in the catalog.',
|
||||
errorUnknownNativeLanguage: 'The selected native language is not in that name set.',
|
||||
errorUnknownCountry: 'The selected country is not in the catalog.',
|
||||
errorUnknownNativeLanguage: 'The selected native language is not in that country.',
|
||||
catalogLoadFailed: 'Could not load the mod catalog.',
|
||||
|
||||
modsTitle: 'Mods',
|
||||
nameSetTitle: 'Name set',
|
||||
countryTitle: 'Country',
|
||||
nativeLanguageTitle: 'Native language',
|
||||
nativeLanguageRandom: 'Random',
|
||||
randomNativeTitle: 'Pick a language at random',
|
||||
|
||||
@@ -53,7 +53,7 @@ export async function fetchRandomName(lang: string): Promise<string> {
|
||||
export interface CreateSchoolOptions {
|
||||
readonly modIds?: readonly string[];
|
||||
readonly map?: MapLayout;
|
||||
readonly nameSetId?: string;
|
||||
readonly countryId?: string;
|
||||
readonly nativeLanguage?: string;
|
||||
readonly seed?: number;
|
||||
}
|
||||
@@ -71,7 +71,7 @@ export async function createSchool(
|
||||
startDate: startDate.toISOString(),
|
||||
modIds: extras.modIds ?? [],
|
||||
map: extras.map ?? null,
|
||||
nameSetId: extras.nameSetId ?? null,
|
||||
countryId: extras.countryId ?? null,
|
||||
nativeLanguage: extras.nativeLanguage ?? null,
|
||||
seed: extras.seed ?? null,
|
||||
}),
|
||||
@@ -92,10 +92,11 @@ export interface DefInfo {
|
||||
readonly pupilSlots?: number;
|
||||
}
|
||||
|
||||
export interface NameSetInfo {
|
||||
export interface CountryInfo {
|
||||
readonly defName: string;
|
||||
readonly label: string;
|
||||
readonly nativeLanguages: readonly DefInfo[];
|
||||
readonly climatePresets: readonly string[];
|
||||
}
|
||||
|
||||
export interface RoomSlotInfo {
|
||||
@@ -170,7 +171,7 @@ export interface CatalogResponse {
|
||||
readonly rooms: readonly RoomInfo[];
|
||||
readonly things: readonly DefInfo[];
|
||||
readonly defaultMap: MapLayout;
|
||||
readonly nameSets: readonly NameSetInfo[];
|
||||
readonly countries: readonly CountryInfo[];
|
||||
readonly subjects: readonly SubjectInfo[];
|
||||
readonly dayFrame: DayFrameInfo | null;
|
||||
readonly holidays: readonly HolidayInfo[];
|
||||
|
||||
@@ -54,11 +54,22 @@ function catalog(): CatalogResponse {
|
||||
],
|
||||
things: [],
|
||||
defaultMap,
|
||||
nameSets: [
|
||||
countries: [
|
||||
{
|
||||
defName: 'Russian',
|
||||
label: 'Russian',
|
||||
nativeLanguages: [{ defName: 'Russian', label: 'Russian' }],
|
||||
defName: 'Russia',
|
||||
label: 'Russia',
|
||||
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: [],
|
||||
@@ -162,6 +173,55 @@ describe('createSchoolDialog', () => {
|
||||
finish(school());
|
||||
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', () => {
|
||||
|
||||
@@ -32,7 +32,7 @@ export function createSchoolDialog(options: CreateSchoolOptions): Promise<School
|
||||
const extraModIds = new Set<string>();
|
||||
let catalog: CatalogResponse | null = null;
|
||||
let currentMap: MapLayout | null = null;
|
||||
let nameSetId: string | null = null;
|
||||
let countryId: string | null = null;
|
||||
let nativeLanguageId: string | null = null;
|
||||
|
||||
const modsField = el('div', { class: 'field' });
|
||||
@@ -40,11 +40,11 @@ export function createSchoolDialog(options: CreateSchoolOptions): Promise<School
|
||||
const modsList = el('div', { class: 'mod-list' });
|
||||
modsField.append(modsLabel, modsList);
|
||||
|
||||
const nameSetLabel = el('span', { class: 'field__label' });
|
||||
const nameSetSelect = el('select', { class: 'input' });
|
||||
const nameSetField = el('label', { class: 'field' }, nameSetLabel, nameSetSelect);
|
||||
nameSetSelect.addEventListener('change', () => {
|
||||
nameSetId = nameSetSelect.value === '' ? null : nameSetSelect.value;
|
||||
const countryLabel = el('span', { class: 'field__label' });
|
||||
const countrySelect = el('select', { class: 'input' });
|
||||
const countryField = el('label', { class: 'field' }, countryLabel, countrySelect);
|
||||
countrySelect.addEventListener('change', () => {
|
||||
countryId = countrySelect.value === '' ? null : countrySelect.value;
|
||||
nativeLanguageId = null;
|
||||
if (catalog !== null) {
|
||||
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('div', { class: 'field' }, startLabel, el('div', { class: 'field__row' }, dateInput, timeInput)),
|
||||
modsField,
|
||||
nameSetField,
|
||||
countryField,
|
||||
nativeField,
|
||||
mapField,
|
||||
seedField,
|
||||
@@ -125,7 +125,7 @@ export function createSchoolDialog(options: CreateSchoolOptions): Promise<School
|
||||
const waiting = value || catalog === null;
|
||||
submitButton.toggleAttribute('disabled', waiting);
|
||||
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);
|
||||
seedInput.toggleAttribute('disabled', value);
|
||||
};
|
||||
@@ -149,31 +149,31 @@ export function createSchoolDialog(options: CreateSchoolOptions): Promise<School
|
||||
const applyCatalog = (next: CatalogResponse): void => {
|
||||
catalog = next;
|
||||
currentMap = structuredClone(next.defaultMap);
|
||||
const stillThere = next.nameSets.some((set) => set.defName === nameSetId);
|
||||
nameSetId = stillThere ? nameSetId : (next.nameSets[0]?.defName ?? null);
|
||||
if (!languagesOf(next, nameSetId).some((language) => language.defName === nativeLanguageId)) {
|
||||
const stillThere = next.countries.some((country) => country.defName === countryId);
|
||||
countryId = stillThere ? countryId : (next.countries[0]?.defName ?? null);
|
||||
if (!languagesOf(next, countryId).some((language) => language.defName === nativeLanguageId)) {
|
||||
nativeLanguageId = null;
|
||||
}
|
||||
paintNameSets(next);
|
||||
paintCountries(next);
|
||||
paintNativeLanguages(next);
|
||||
paintMapHint();
|
||||
setBusy(busy);
|
||||
};
|
||||
|
||||
const paintNameSets = (next: CatalogResponse): void => {
|
||||
nameSetSelect.replaceChildren();
|
||||
for (const set of next.nameSets) {
|
||||
const option = el('option', { text: set.label });
|
||||
option.value = set.defName;
|
||||
nameSetSelect.append(option);
|
||||
const paintCountries = (next: CatalogResponse): void => {
|
||||
countrySelect.replaceChildren();
|
||||
for (const country of next.countries) {
|
||||
const option = el('option', { text: country.label });
|
||||
option.value = country.defName;
|
||||
countrySelect.append(option);
|
||||
}
|
||||
|
||||
nameSetSelect.value = nameSetId ?? '';
|
||||
nameSetSelect.disabled = next.nameSets.length <= 1;
|
||||
countrySelect.value = countryId ?? '';
|
||||
countrySelect.disabled = next.countries.length <= 1;
|
||||
};
|
||||
|
||||
const paintNativeLanguages = (next: CatalogResponse): void => {
|
||||
const languages = languagesOf(next, nameSetId);
|
||||
const languages = languagesOf(next, countryId);
|
||||
nativeSelect.replaceChildren();
|
||||
const random = el('option', { text: t('nativeLanguageRandom') });
|
||||
random.value = '';
|
||||
@@ -187,7 +187,7 @@ export function createSchoolDialog(options: CreateSchoolOptions): Promise<School
|
||||
nativeSelect.value = nativeLanguageId ?? '';
|
||||
nativeSelect.disabled = 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> => {
|
||||
@@ -242,7 +242,7 @@ export function createSchoolDialog(options: CreateSchoolOptions): Promise<School
|
||||
const localize = (): void => {
|
||||
title.textContent = t('newSchool');
|
||||
modsLabel.textContent = t('modsTitle');
|
||||
nameSetLabel.textContent = t('nameSetTitle');
|
||||
countryLabel.textContent = t('countryTitle');
|
||||
nativeLabel.textContent = t('nativeLanguageTitle');
|
||||
nativeRandomButton.textContent = t('randomName');
|
||||
nativeRandomButton.title = t('randomNativeTitle');
|
||||
@@ -260,7 +260,7 @@ export function createSchoolDialog(options: CreateSchoolOptions): Promise<School
|
||||
cancelButton.textContent = t('cancel');
|
||||
paintMapHint();
|
||||
if (catalog !== null) {
|
||||
paintNameSets(catalog);
|
||||
paintCountries(catalog);
|
||||
paintNativeLanguages(catalog);
|
||||
}
|
||||
};
|
||||
@@ -284,7 +284,7 @@ export function createSchoolDialog(options: CreateSchoolOptions): Promise<School
|
||||
return;
|
||||
}
|
||||
|
||||
const languages = languagesOf(catalog, nameSetId);
|
||||
const languages = languagesOf(catalog, countryId);
|
||||
if (languages.length === 0) {
|
||||
return;
|
||||
}
|
||||
@@ -332,7 +332,7 @@ export function createSchoolDialog(options: CreateSchoolOptions): Promise<School
|
||||
.create(nameInput.value.trim(), startDate, {
|
||||
modIds: [...extraModIds],
|
||||
map: currentMap,
|
||||
nameSetId: nameSetId ?? undefined,
|
||||
countryId: countryId ?? undefined,
|
||||
nativeLanguage: nativeLanguageId ?? undefined,
|
||||
seed: seed ?? undefined,
|
||||
})
|
||||
@@ -349,12 +349,12 @@ export function createSchoolDialog(options: CreateSchoolOptions): Promise<School
|
||||
return modal.open(nameInput);
|
||||
}
|
||||
|
||||
function languagesOf(catalog: CatalogResponse | null, nameSetId: string | null): readonly { readonly defName: string; readonly label: string }[] {
|
||||
if (catalog === null || nameSetId === null) {
|
||||
function languagesOf(catalog: CatalogResponse | null, countryId: string | null): readonly { readonly defName: string; readonly label: string }[] {
|
||||
if (catalog === null || countryId === null) {
|
||||
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 {
|
||||
@@ -401,8 +401,8 @@ function describe(reason: unknown): string {
|
||||
return t('errorModCycle');
|
||||
case 'invalid-catalog':
|
||||
return t('errorInvalidCatalog');
|
||||
case 'unknown-name-set':
|
||||
return t('errorUnknownNameSet');
|
||||
case 'unknown-country':
|
||||
return t('errorUnknownCountry');
|
||||
case 'unknown-native-language':
|
||||
return t('errorUnknownNativeLanguage');
|
||||
default:
|
||||
|
||||
@@ -307,7 +307,8 @@ public sealed class CatalogLoader
|
||||
var traits = new Dictionary<string, TraitDef>(StringComparer.Ordinal);
|
||||
var bodyAttributes = new Dictionary<string, BodyAttributeDef>(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 staffing = new Dictionary<string, StaffingDef>(StringComparer.Ordinal);
|
||||
var dayFrames = new Dictionary<string, DayFrameDef>(StringComparer.Ordinal);
|
||||
@@ -355,8 +356,11 @@ public sealed class CatalogLoader
|
||||
case DefKind.Need:
|
||||
needs[key.Name] = Jsonc.Deserialize<NeedDef>(json);
|
||||
break;
|
||||
case DefKind.NameSet:
|
||||
nameSets[key.Name] = Jsonc.Deserialize<NameSetDef>(json);
|
||||
case DefKind.Country:
|
||||
countries[key.Name] = Jsonc.Deserialize<CountryDef>(json);
|
||||
break;
|
||||
case DefKind.ClimatePreset:
|
||||
climatePresets[key.Name] = Jsonc.Deserialize<ClimatePresetDef>(json);
|
||||
break;
|
||||
case DefKind.Subject:
|
||||
subjects[key.Name] = Jsonc.Deserialize<SubjectDef>(json);
|
||||
@@ -393,7 +397,8 @@ public sealed class CatalogLoader
|
||||
traits,
|
||||
bodyAttributes,
|
||||
needs,
|
||||
nameSets,
|
||||
countries,
|
||||
climatePresets,
|
||||
subjects,
|
||||
staffing,
|
||||
dayFrames,
|
||||
@@ -541,7 +546,8 @@ public sealed class CatalogLoader
|
||||
.Concat(Enumerate(catalog.Traits.Values))
|
||||
.Concat(Enumerate(catalog.BodyAttributes.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.Staffing.Values))
|
||||
.Concat(Enumerate(catalog.DayFrames.Values))
|
||||
|
||||
@@ -20,7 +20,8 @@ public sealed class DefCatalog
|
||||
IReadOnlyDictionary<string, TraitDef> traits,
|
||||
IReadOnlyDictionary<string, BodyAttributeDef> bodyAttributes,
|
||||
IReadOnlyDictionary<string, NeedDef> needs,
|
||||
IReadOnlyDictionary<string, NameSetDef> nameSets,
|
||||
IReadOnlyDictionary<string, CountryDef> countries,
|
||||
IReadOnlyDictionary<string, ClimatePresetDef> climatePresets,
|
||||
IReadOnlyDictionary<string, SubjectDef> subjects,
|
||||
IReadOnlyDictionary<string, StaffingDef> staffing,
|
||||
IReadOnlyDictionary<string, DayFrameDef> dayFrames,
|
||||
@@ -43,7 +44,8 @@ public sealed class DefCatalog
|
||||
Traits = traits;
|
||||
BodyAttributes = bodyAttributes;
|
||||
Needs = needs;
|
||||
NameSets = nameSets;
|
||||
Countries = countries;
|
||||
ClimatePresets = climatePresets;
|
||||
Subjects = subjects;
|
||||
Staffing = staffing;
|
||||
DayFrames = dayFrames;
|
||||
@@ -91,7 +93,9 @@ public sealed class DefCatalog
|
||||
|
||||
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; }
|
||||
|
||||
@@ -133,7 +137,8 @@ public sealed class DefCatalog
|
||||
DefKind.Trait => Traits.GetValueOrDefault(defName),
|
||||
DefKind.BodyAttribute => BodyAttributes.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.Staffing => Staffing.GetValueOrDefault(defName),
|
||||
DefKind.DayFrame => DayFrames.GetValueOrDefault(defName),
|
||||
@@ -212,7 +217,8 @@ public sealed class DefCatalog
|
||||
TraitDef => DefKind.Trait,
|
||||
BodyAttributeDef => DefKind.BodyAttribute,
|
||||
NeedDef => DefKind.Need,
|
||||
NameSetDef => DefKind.NameSet,
|
||||
CountryDef => DefKind.Country,
|
||||
ClimatePresetDef => DefKind.ClimatePreset,
|
||||
SubjectDef => DefKind.Subject,
|
||||
StaffingDef => DefKind.Staffing,
|
||||
DayFrameDef => DefKind.DayFrame,
|
||||
@@ -245,4 +251,17 @@ public sealed class DefCatalog
|
||||
set.Remove(defName);
|
||||
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,
|
||||
BodyAttribute,
|
||||
Need,
|
||||
NameSet,
|
||||
Country,
|
||||
ClimatePreset,
|
||||
Subject,
|
||||
Staffing,
|
||||
DayFrame,
|
||||
|
||||
@@ -105,8 +105,11 @@ internal static class PackPaths
|
||||
case "needs":
|
||||
kind = DefKind.Need;
|
||||
return true;
|
||||
case "namesets":
|
||||
kind = DefKind.NameSet;
|
||||
case "countries":
|
||||
kind = DefKind.Country;
|
||||
return true;
|
||||
case "climates":
|
||||
kind = DefKind.ClimatePreset;
|
||||
return true;
|
||||
case "subjects":
|
||||
kind = DefKind.Subject;
|
||||
|
||||
@@ -25,9 +25,9 @@ internal static class PeopleDefValidator
|
||||
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)
|
||||
@@ -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))
|
||||
{
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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))
|
||||
{
|
||||
throw new ContentLoadException($"NameSetDef '{names.DefName}' has unknown defaultGivenDeclension.");
|
||||
throw new ContentLoadException($"CountryDef '{countryDefName}' has unknown defaultGivenDeclension.");
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
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))
|
||||
{
|
||||
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)
|
||||
@@ -611,7 +640,7 @@ internal static class PeopleDefValidator
|
||||
: given.Declension;
|
||||
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))
|
||||
{
|
||||
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)
|
||||
@@ -630,7 +659,7 @@ internal static class PeopleDefValidator
|
||||
: surname.Declension;
|
||||
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 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;
|
||||
|
||||
@@ -368,7 +371,7 @@ public sealed class NameSetDef : Def
|
||||
public string DefaultSurnameDeclension { get; init; } = NameGrammar.Ov;
|
||||
|
||||
/// <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
|
||||
/// accepted in JSONC for a one-language pack.
|
||||
/// </summary>
|
||||
@@ -401,3 +404,20 @@ public sealed class NameSetDef : Def
|
||||
|
||||
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,
|
||||
Roster roster,
|
||||
int schoolSeed,
|
||||
string nameSetId,
|
||||
string countryId,
|
||||
DateTime asOf,
|
||||
string? nativeLanguage = null)
|
||||
{
|
||||
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>
|
||||
@@ -31,7 +31,7 @@ public sealed record ApplicantPool(int Week, int NextIndex, IReadOnlyList<Applic
|
||||
DefCatalog catalog,
|
||||
Roster roster,
|
||||
int schoolSeed,
|
||||
string nameSetId,
|
||||
string countryId,
|
||||
DateTime asOf,
|
||||
string? nativeLanguage = null)
|
||||
{
|
||||
@@ -44,7 +44,7 @@ public sealed record ApplicantPool(int Week, int NextIndex, IReadOnlyList<Applic
|
||||
var pool = this;
|
||||
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;
|
||||
@@ -93,7 +93,7 @@ public sealed record ApplicantPool(int Week, int NextIndex, IReadOnlyList<Applic
|
||||
DefCatalog catalog,
|
||||
Roster roster,
|
||||
int schoolSeed,
|
||||
string nameSetId,
|
||||
string countryId,
|
||||
DateTime asOf,
|
||||
int week,
|
||||
string? nativeLanguage)
|
||||
@@ -113,7 +113,7 @@ public sealed record ApplicantPool(int Week, int NextIndex, IReadOnlyList<Applic
|
||||
catalog,
|
||||
roster,
|
||||
schoolSeed,
|
||||
nameSetId,
|
||||
countryId,
|
||||
asOf,
|
||||
new ApplicantPool(week, current.NextIndex, staying),
|
||||
rng,
|
||||
@@ -124,16 +124,16 @@ public sealed record ApplicantPool(int Week, int NextIndex, IReadOnlyList<Applic
|
||||
DefCatalog catalog,
|
||||
Roster roster,
|
||||
int schoolSeed,
|
||||
string nameSetId,
|
||||
string countryId,
|
||||
DateTime asOf,
|
||||
ApplicantPool current,
|
||||
Random? rng = null,
|
||||
string? nativeLanguage = null)
|
||||
{
|
||||
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);
|
||||
|
||||
@@ -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,
|
||||
MapLayout map,
|
||||
int schoolSeed,
|
||||
string nameSetId,
|
||||
string countryId,
|
||||
DateTime? asOf = null,
|
||||
string? nativeLanguage = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(catalog);
|
||||
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);
|
||||
|
||||
@@ -15,6 +15,7 @@ public static class Seed
|
||||
public const int CommuteSalt = 7;
|
||||
public const int SkillGrantSalt = 8;
|
||||
public const int NativeLanguageSalt = 9;
|
||||
public const int ClimatePresetSalt = 10;
|
||||
|
||||
/// <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);
|
||||
|
||||
@@ -38,17 +38,17 @@ public static class YearlyIntake
|
||||
DefCatalog catalog,
|
||||
Roster roster,
|
||||
int schoolSeed,
|
||||
string nameSetId,
|
||||
string countryId,
|
||||
DateTime asOf,
|
||||
string? nativeLanguage = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(catalog);
|
||||
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);
|
||||
|
||||
@@ -122,7 +122,7 @@ internal sealed record CatalogResponse(
|
||||
IReadOnlyList<DefInfoResponse> Floors,
|
||||
IReadOnlyList<RoomInfoResponse> Rooms,
|
||||
IReadOnlyList<DefInfoResponse> Things,
|
||||
IReadOnlyList<NameSetInfoResponse> NameSets,
|
||||
IReadOnlyList<CountryInfoResponse> Countries,
|
||||
IReadOnlyList<SubjectInfoResponse> Subjects,
|
||||
DayFrameResponse? DayFrame,
|
||||
IReadOnlyList<HolidayInfoResponse> Holidays,
|
||||
@@ -135,7 +135,7 @@ internal sealed record CatalogResponse(
|
||||
Placeable(catalog.Floors.Values, catalog, locale),
|
||||
PlaceableRooms(catalog, locale),
|
||||
PlaceableThings(catalog, locale),
|
||||
PlaceableNameSets(catalog, locale),
|
||||
PlaceableCountries(catalog, locale),
|
||||
PlaceableSubjects(catalog, locale),
|
||||
MapDayFrame(catalog, locale),
|
||||
PlaceableHolidays(catalog, locale),
|
||||
@@ -149,20 +149,21 @@ internal sealed record CatalogResponse(
|
||||
.Select(def => new DefInfoResponse(def.DefName, catalog.Label(locale, def)))
|
||||
.ToArray();
|
||||
|
||||
private static IReadOnlyList<NameSetInfoResponse> PlaceableNameSets(DefCatalog catalog, string locale) =>
|
||||
catalog.NameSets.Values
|
||||
private static IReadOnlyList<CountryInfoResponse> PlaceableCountries(DefCatalog catalog, string locale) =>
|
||||
catalog.Countries.Values
|
||||
.Where(def => !def.Abstract)
|
||||
.OrderBy(def => def.DefName, StringComparer.Ordinal)
|
||||
.Select(def => new NameSetInfoResponse(
|
||||
.Select(def => new CountryInfoResponse(
|
||||
def.DefName,
|
||||
catalog.Label(locale, def),
|
||||
def.Spoken
|
||||
def.Names.Spoken
|
||||
.Select(skill => new DefInfoResponse(
|
||||
skill,
|
||||
catalog.Skills.TryGetValue(skill, out var language)
|
||||
? catalog.Label(locale, language)
|
||||
: skill))
|
||||
.ToArray()))
|
||||
.ToArray(),
|
||||
def.ClimatePresets.ToArray()))
|
||||
.ToArray();
|
||||
|
||||
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 NameSetInfoResponse(
|
||||
internal sealed record CountryInfoResponse(
|
||||
string DefName,
|
||||
string Label,
|
||||
IReadOnlyList<DefInfoResponse> NativeLanguages);
|
||||
IReadOnlyList<DefInfoResponse> NativeLanguages,
|
||||
IReadOnlyList<string> ClimatePresets);
|
||||
|
||||
internal sealed record RoomInfoResponse(
|
||||
string DefName,
|
||||
|
||||
@@ -53,7 +53,7 @@ internal static class SchoolEndpoints
|
||||
DateTime.SpecifyKind(request.StartDate, DateTimeKind.Utc),
|
||||
request.ModIds,
|
||||
request.Map,
|
||||
request.NameSetId,
|
||||
request.CountryId,
|
||||
request.NativeLanguage,
|
||||
request.Seed,
|
||||
NewCompletion<SchoolCreationOutcome>());
|
||||
@@ -77,10 +77,10 @@ internal static class SchoolEndpoints
|
||||
Problem(StatusCodes.Status400BadRequest, "unknown-mod", "A selected mod is missing."),
|
||||
SchoolCreationError.InvalidCatalog =>
|
||||
Problem(StatusCodes.Status400BadRequest, "invalid-catalog", "The selected packs could not be loaded."),
|
||||
SchoolCreationError.UnknownNameSet =>
|
||||
Problem(StatusCodes.Status400BadRequest, "unknown-name-set", "The selected name set is not in the catalog."),
|
||||
SchoolCreationError.UnknownCountry =>
|
||||
Problem(StatusCodes.Status400BadRequest, "unknown-country", "The selected country is not in the catalog."),
|
||||
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 =>
|
||||
Problem(
|
||||
StatusCodes.Status400BadRequest,
|
||||
@@ -508,7 +508,7 @@ internal sealed record CreateSchoolRequest(
|
||||
DateTime StartDate,
|
||||
IReadOnlyList<string>? ModIds,
|
||||
MapLayout? Map,
|
||||
string? NameSetId,
|
||||
string? CountryId,
|
||||
string? NativeLanguage,
|
||||
int? Seed);
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ internal abstract record GameCommand
|
||||
DateTime StartDate,
|
||||
IReadOnlyList<string>? ExtraModIds,
|
||||
MapLayout? Map,
|
||||
string? NameSetId,
|
||||
string? CountryId,
|
||||
string? NativeLanguage,
|
||||
int? Seed,
|
||||
TaskCompletionSource<SchoolCreationOutcome> Result) : GameCommand;
|
||||
|
||||
@@ -357,20 +357,14 @@ internal sealed class GameLoopService(
|
||||
return;
|
||||
}
|
||||
|
||||
var nameSetId = ResolveNameSetId(catalog, command.NameSetId);
|
||||
if (nameSetId is null)
|
||||
var countryId = ResolveCountryId(catalog, command.CountryId);
|
||||
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;
|
||||
}
|
||||
|
||||
if (!catalog.NameSets.TryGetValue(nameSetId, out var names))
|
||||
{
|
||||
command.Result.TrySetResult(new SchoolCreationOutcome(null, SchoolCreationError.UnknownNameSet));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(command.NativeLanguage) && !NativeLanguages.Allows(names, command.NativeLanguage))
|
||||
if (!string.IsNullOrWhiteSpace(command.NativeLanguage) && !NativeLanguages.Allows(country.Names, command.NativeLanguage))
|
||||
{
|
||||
command.Result.TrySetResult(new SchoolCreationOutcome(null, SchoolCreationError.UnknownNativeLanguage));
|
||||
return;
|
||||
@@ -379,9 +373,10 @@ internal sealed class GameLoopService(
|
||||
var id = _nextId++;
|
||||
store.WriteNextId(_nextId);
|
||||
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);
|
||||
worker.Start();
|
||||
|
||||
@@ -548,7 +543,8 @@ internal sealed class GameLoopService(
|
||||
isNew: false,
|
||||
save.ModIds,
|
||||
save.Map,
|
||||
save.NameSetId,
|
||||
save.CountryId,
|
||||
save.ClimatePresetId,
|
||||
save.NativeLanguage,
|
||||
createSeed: null,
|
||||
save.Presence);
|
||||
@@ -598,7 +594,8 @@ internal sealed class GameLoopService(
|
||||
bool isNew,
|
||||
IReadOnlyList<string>? modIds,
|
||||
MapLayout? map,
|
||||
string? nameSetId,
|
||||
string? countryId,
|
||||
string? climatePresetId,
|
||||
string? nativeLanguage,
|
||||
int? createSeed = null,
|
||||
IReadOnlyList<PresenceSnapshot>? presence = null) =>
|
||||
@@ -611,7 +608,8 @@ internal sealed class GameLoopService(
|
||||
isNew,
|
||||
modIds,
|
||||
map,
|
||||
nameSetId,
|
||||
countryId,
|
||||
climatePresetId,
|
||||
nativeLanguage,
|
||||
createSeed,
|
||||
presence,
|
||||
@@ -624,12 +622,12 @@ internal sealed class GameLoopService(
|
||||
loggerFactory.CreateLogger($"HSchool.Server.Game.SchoolWorker.{id}"));
|
||||
|
||||
/// <summary>
|
||||
/// Empty request uses the first placeable set (core's Slavic). A named id must exist in the
|
||||
/// catalog already loaded for this pack list — unknown extras were rejected above.
|
||||
/// Empty request uses the first placeable country (core's Russia). A named id must exist in
|
||||
/// the catalog already loaded for this pack list — unknown extras were rejected above.
|
||||
/// </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)
|
||||
.Select(def => def.DefName)
|
||||
.OrderBy(name => name, StringComparer.Ordinal)
|
||||
|
||||
@@ -26,6 +26,11 @@ internal sealed class SchoolSave
|
||||
|
||||
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? NativeLanguage { get; init; }
|
||||
@@ -42,7 +47,7 @@ internal sealed record SchoolSaveIndex(int NextId);
|
||||
/// </summary>
|
||||
internal sealed class SchoolStore
|
||||
{
|
||||
public const int CurrentFormat = 2;
|
||||
public const int CurrentFormat = 3;
|
||||
|
||||
private const string IndexFileName = "index.json";
|
||||
|
||||
@@ -178,6 +183,8 @@ internal sealed class SchoolStore
|
||||
SpeedIndex = save.SpeedIndex,
|
||||
ModIds = save.ModIds,
|
||||
Map = save.Map,
|
||||
CountryId = save.CountryId,
|
||||
ClimatePresetId = save.ClimatePresetId,
|
||||
NameSetId = save.NameSetId,
|
||||
NativeLanguage = save.NativeLanguage,
|
||||
Presence = save.Presence,
|
||||
@@ -194,11 +201,40 @@ internal sealed class SchoolStore
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Named upgrade seam for saves older than <see cref="CurrentFormat"/>. Empty on purpose:
|
||||
/// missing fields already default and extra fields are ignored. Put a migration here when a
|
||||
/// format bump actually needs one.
|
||||
/// Format 2 stored a name set. Slavic becomes Russia; any other id is tried as a country
|
||||
/// (the example pack kept its defName). Climate is filled on the worker from the country's
|
||||
/// first preset so the file is not rewritten until the school saves itself.
|
||||
/// </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)
|
||||
{
|
||||
|
||||
@@ -32,7 +32,8 @@ internal sealed class SchoolWorker
|
||||
private readonly bool _isNew;
|
||||
private readonly IReadOnlyList<string>? _modIds;
|
||||
private readonly MapLayout? _savedMap;
|
||||
private readonly string? _nameSetId;
|
||||
private readonly string? _countryId;
|
||||
private string? _climatePresetId;
|
||||
private string? _nativeLanguage;
|
||||
private readonly int? _createSeed;
|
||||
private readonly IReadOnlyList<PresenceSnapshot>? _savedPresence;
|
||||
@@ -66,7 +67,8 @@ internal sealed class SchoolWorker
|
||||
bool isNew,
|
||||
IReadOnlyList<string>? modIds,
|
||||
MapLayout? savedMap,
|
||||
string? nameSetId,
|
||||
string? countryId,
|
||||
string? climatePresetId,
|
||||
string? nativeLanguage,
|
||||
int? createSeed,
|
||||
IReadOnlyList<PresenceSnapshot>? savedPresence,
|
||||
@@ -86,7 +88,8 @@ internal sealed class SchoolWorker
|
||||
_isNew = isNew;
|
||||
_modIds = modIds;
|
||||
_savedMap = savedMap;
|
||||
_nameSetId = nameSetId;
|
||||
_countryId = countryId;
|
||||
_climatePresetId = climatePresetId;
|
||||
_nativeLanguage = nativeLanguage;
|
||||
_createSeed = createSeed;
|
||||
_savedPresence = savedPresence;
|
||||
@@ -857,12 +860,19 @@ internal sealed class SchoolWorker
|
||||
|
||||
private bool InstallPeople(School school, DefCatalog catalog, MapLayout map)
|
||||
{
|
||||
var nameSetId = ResolveNameSetId(catalog, _nameSetId);
|
||||
if (nameSetId is null)
|
||||
var countryId = ResolveCountryId(catalog, _countryId);
|
||||
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);
|
||||
Roster roster;
|
||||
ApplicantPool applicants;
|
||||
@@ -878,10 +888,10 @@ internal sealed class SchoolWorker
|
||||
}
|
||||
|
||||
seed = createSeed;
|
||||
native = ResolveNative(catalog, nameSetId, seed, _nativeLanguage, generating: true);
|
||||
native = ResolveNative(country, seed, _nativeLanguage, generating: true);
|
||||
_nativeLanguage = native;
|
||||
roster = RosterGenerator.Generate(catalog, map, seed, nameSetId, school.Clock.Time, native);
|
||||
applicants = ApplicantPool.Create(catalog, roster, seed, nameSetId, school.Clock.Time, native);
|
||||
roster = RosterGenerator.Generate(catalog, map, seed, countryId, school.Clock.Time, native);
|
||||
applicants = ApplicantPool.Create(catalog, roster, seed, countryId, school.Clock.Time, native);
|
||||
generated = true;
|
||||
}
|
||||
else
|
||||
@@ -890,16 +900,16 @@ internal sealed class SchoolWorker
|
||||
if (loaded is null)
|
||||
{
|
||||
seed = school.Id;
|
||||
native = ResolveNative(catalog, nameSetId, seed, _nativeLanguage, generating: true);
|
||||
native = ResolveNative(country, seed, _nativeLanguage, generating: true);
|
||||
_nativeLanguage = native;
|
||||
roster = RosterGenerator.Generate(catalog, map, seed, nameSetId, school.Clock.Time, native);
|
||||
applicants = ApplicantPool.Create(catalog, roster, seed, nameSetId, school.Clock.Time, native);
|
||||
roster = RosterGenerator.Generate(catalog, map, seed, countryId, school.Clock.Time, native);
|
||||
applicants = ApplicantPool.Create(catalog, roster, seed, countryId, school.Clock.Time, native);
|
||||
generated = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
seed = loaded.Seed;
|
||||
native = ResolveNative(catalog, nameSetId, seed, _nativeLanguage, generating: false);
|
||||
native = ResolveNative(country, seed, _nativeLanguage, generating: false);
|
||||
_nativeLanguage = native;
|
||||
roster = loaded.ToRoster();
|
||||
if (loaded.Applicants is { Applicants.Count: > 0 })
|
||||
@@ -908,7 +918,7 @@ internal sealed class SchoolWorker
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -920,47 +930,41 @@ internal sealed class SchoolWorker
|
||||
$"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);
|
||||
school.ConfigurePresence(_options.SchoolWeekDays, _options.MaxDecisionsPerTick);
|
||||
school.RestorePresence(_savedPresence);
|
||||
return generated;
|
||||
}
|
||||
|
||||
private static string? ResolveNameSetId(DefCatalog catalog, string? requested)
|
||||
private static string? ResolveCountryId(DefCatalog catalog, string? requested)
|
||||
{
|
||||
var available = catalog.NameSets.Values
|
||||
.Where(def => !def.Abstract)
|
||||
.Select(def => def.DefName)
|
||||
.OrderBy(name => name, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
if (available.Length == 0)
|
||||
if (string.IsNullOrWhiteSpace(requested))
|
||||
{
|
||||
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(
|
||||
DefCatalog catalog,
|
||||
string nameSetId,
|
||||
CountryDef country,
|
||||
int schoolSeed,
|
||||
string? requested,
|
||||
bool generating)
|
||||
{
|
||||
if (!catalog.NameSets.TryGetValue(nameSetId, out var names))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return NativeLanguages.Pick(names, schoolSeed, requested, rollIfOmitted: generating && string.IsNullOrWhiteSpace(requested));
|
||||
}
|
||||
bool generating) =>
|
||||
NativeLanguages.Pick(country.Names, schoolSeed, requested, rollIfOmitted: generating && string.IsNullOrWhiteSpace(requested));
|
||||
|
||||
private void Persist()
|
||||
{
|
||||
@@ -983,7 +987,8 @@ internal sealed class SchoolWorker
|
||||
SpeedIndex = school.Clock.SpeedIndex,
|
||||
ModIds = school.Catalog?.PackIds,
|
||||
Map = school.Map,
|
||||
NameSetId = _nameSetId,
|
||||
CountryId = school.CountryId,
|
||||
ClimatePresetId = school.ClimatePresetId,
|
||||
NativeLanguage = _nativeLanguage,
|
||||
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",
|
||||
"Toilet": "Toilet",
|
||||
"Social": "Social",
|
||||
"Slavic": "Slavic",
|
||||
"Russia": "Russia",
|
||||
"TemperateContinental": "Temperate continental",
|
||||
"ContinentalCold": "Cold continental",
|
||||
"Build": "Build",
|
||||
"Skinny": "Skinny",
|
||||
"Average": "Average",
|
||||
|
||||
@@ -144,7 +144,9 @@
|
||||
"Hunger": "Голод",
|
||||
"Toilet": "Туалет",
|
||||
"Social": "Общение",
|
||||
"Slavic": "Славянский",
|
||||
"Russia": "Россия",
|
||||
"TemperateContinental": "Умеренно-континентальный",
|
||||
"ContinentalCold": "Континентальный холодный",
|
||||
"Build": "Телосложение",
|
||||
"Skinny": "Худощавое",
|
||||
"Average": "Обычное",
|
||||
|
||||
@@ -10,7 +10,7 @@ example/
|
||||
pack.jsonc # version + requires: ["core"]
|
||||
localizations/{ru,en}.jsonc
|
||||
defs/traits/traits.jsonc # two traits
|
||||
defs/namesets/example.jsonc
|
||||
defs/countries/example.jsonc
|
||||
defs/rooms/store.jsonc # one placeable room
|
||||
defs/things/chair.jsonc # last-wins: same defName as core
|
||||
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; }
|
||||
|
||||
/// <summary>Name pack used to generate this school's people. Needed again on 1 September.</summary>
|
||||
public string? NameSetId { get; private set; }
|
||||
/// <summary>Country used to generate this school's people. Needed again on 1 September.</summary>
|
||||
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>
|
||||
public string? NativeLanguage { get; private set; }
|
||||
@@ -118,14 +121,15 @@ public sealed class School : IDisposable
|
||||
/// <summary>
|
||||
/// Installs a roster that already matches the map. Spawns entities; does not write to disk.
|
||||
/// </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);
|
||||
ArgumentNullException.ThrowIfNull(roster);
|
||||
|
||||
Roster = roster;
|
||||
PeopleSeed = seed;
|
||||
NameSetId = nameSetId;
|
||||
CountryId = countryId;
|
||||
ClimatePresetId = climatePresetId;
|
||||
NativeLanguage = nativeLanguage;
|
||||
Applicants = applicants;
|
||||
RosterSpawner.Spawn(World, roster);
|
||||
@@ -282,7 +286,7 @@ public sealed class School : IDisposable
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -290,7 +294,7 @@ public sealed class School : IDisposable
|
||||
var changed = false;
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -307,12 +311,12 @@ public sealed class School : IDisposable
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
return false;
|
||||
|
||||
@@ -13,7 +13,7 @@ public enum SchoolCreationError
|
||||
InvalidMap,
|
||||
UnknownMod,
|
||||
InvalidCatalog,
|
||||
UnknownNameSet,
|
||||
UnknownCountry,
|
||||
UnknownNativeLanguage,
|
||||
MissingMod,
|
||||
ModCycle,
|
||||
|
||||
Reference in New Issue
Block a user