Enhance school creation functionality by introducing support for name sets in the API and UI. Update the catalog to include skills, traits, body attributes, needs, and name sets, improving character generation capabilities. Revise localization strings for better user guidance and update tests to validate the new name set functionality and ensure robustness in school creation processes.
ci / server (push) Failing after 3m45s
ci / client (push) Successful in 17s

This commit is contained in:
Leonid Pershin
2026-08-18 18:57:01 +03:00
parent 38afbcad36
commit e6182e0e45
56 changed files with 3706 additions and 9 deletions
+298
View File
@@ -0,0 +1,298 @@
namespace HSchool.People;
internal static class FamilyFactory
{
public static (Family Family, List<Person> Members) Create(
DefCatalog catalog,
NameSetDef names,
int schoolSeed,
FamilyPlan plan,
DateTime yearStart,
DateTime asOf)
{
var rng = new Random(Seed.Mix(schoolSeed, plan.FamilyIndex, Seed.AppearanceSalt));
var familyId = $"f{plan.FamilyIndex}";
var surname = names.Surnames[rng.Next(names.Surnames.Count)];
var childDrafts = new ChildDraft[plan.Seats.Count];
var usedGiven = new HashSet<string>(StringComparer.Ordinal);
for (var i = 0; i < plan.Seats.Count; i++)
{
var seat = plan.Seats[i];
var female = rng.Next(2) == 0;
var given = PickGiven(female ? names.FemaleGiven : names.MaleGiven, rng, usedGiven);
var (first, last) = SchoolYears.BirthWindow(yearStart, seat.Year);
childDrafts[i] = new ChildDraft(seat, female, given, SchoolYears.RandomInRange(rng, first, last));
}
var fatherGiven = PickGiven(names.MaleGiven, rng);
var motherGiven = PickGiven(names.FemaleGiven, rng);
var fatherPatronymicSource = PickGiven(names.MaleGiven, rng);
var motherPatronymicSource = PickGiven(names.MaleGiven, rng);
DateTime motherBirth;
DateTime fatherBirth;
if (childDrafts.Length == 0)
{
motherBirth = asOf.AddYears(-(28 + rng.Next(28))).AddDays(-rng.Next(365));
fatherBirth = motherBirth.AddDays(rng.Next(-5 * 365, (5 * 365) + 1));
}
else
{
var oldestBirth = childDrafts[0].Birth;
foreach (var child in childDrafts)
{
if (child.Birth < oldestBirth)
{
oldestBirth = child.Birth;
}
}
var motherYearsOlder = 22 + rng.Next(17);
motherBirth = oldestBirth.AddYears(-motherYearsOlder).AddDays(-rng.Next(30));
fatherBirth = motherBirth.AddDays(rng.Next(-5 * 365, (5 * 365) + 1));
}
var members = new List<Person>();
var fatherId = $"{familyId}.p0";
var motherId = $"{familyId}.p1";
members.Add(
RollAdult(
catalog,
names,
rng,
fatherId,
familyId,
female: false,
fatherBirth,
asOf,
surname,
fatherGiven,
NameGrammar.Patronymic(fatherPatronymicSource.Form, female: false, names.PatronymicRule),
isParent: childDrafts.Length > 0));
members.Add(
RollAdult(
catalog,
names,
rng,
motherId,
familyId,
female: true,
motherBirth,
asOf,
surname,
motherGiven,
NameGrammar.Patronymic(motherPatronymicSource.Form, female: true, names.PatronymicRule),
isParent: childDrafts.Length > 0));
var childIds = new List<string>(childDrafts.Length);
for (var i = 0; i < childDrafts.Length; i++)
{
var draft = childDrafts[i];
var id = $"{familyId}.c{i}";
childIds.Add(id);
members.Add(
RollChild(
catalog,
names,
rng,
id,
familyId,
draft,
asOf,
surname,
fatherGiven.Form));
}
var family = new Family(familyId, [fatherId, motherId], childIds);
return (family, members);
}
public static (Family Family, List<Person> Members) CreateStaffOnly(
DefCatalog catalog,
NameSetDef names,
int schoolSeed,
int familyIndex,
DateTime asOf)
{
var empty = new FamilyPlan(familyIndex, []);
return Create(catalog, names, schoolSeed, empty, SchoolYears.StartOn(asOf), asOf);
}
private static Person RollAdult(
DefCatalog catalog,
NameSetDef names,
Random rng,
string id,
string familyId,
bool female,
DateTime birth,
DateTime asOf,
SurnameEntry surname,
GivenNameEntry given,
string patronymic,
bool isParent)
{
var age = SchoolYears.AgeYears(birth, asOf);
var roles = isParent ? new[] { PersonRoles.Parent } : new[] { PersonRoles.Staff };
return FinishPerson(
catalog,
names,
rng,
id,
familyId,
female,
birth,
asOf,
age,
surname,
given,
patronymic,
roles,
isStudent: false,
isParent,
classId: null);
}
private static Person RollChild(
DefCatalog catalog,
NameSetDef names,
Random rng,
string id,
string familyId,
ChildDraft draft,
DateTime asOf,
SurnameEntry surname,
string fatherGiven)
{
var age = SchoolYears.AgeYears(draft.Birth, asOf);
var patronymic = NameGrammar.Patronymic(fatherGiven, draft.Female, names.PatronymicRule);
return FinishPerson(
catalog,
names,
rng,
id,
familyId,
draft.Female,
draft.Birth,
asOf,
age,
surname,
draft.Given,
patronymic,
[PersonRoles.Student],
isStudent: true,
isParent: false,
draft.Seat.ClassId);
}
private static Person FinishPerson(
DefCatalog catalog,
NameSetDef names,
Random rng,
string id,
string familyId,
bool female,
DateTime birth,
DateTime asOf,
int age,
SurnameEntry surname,
GivenNameEntry given,
string patronymic,
IReadOnlyList<string> roles,
bool isStudent,
bool isParent,
string? classId)
{
var (numbers, choices) = PersonSampler.Body(catalog, rng, female, age);
var traits = PersonSampler.Traits(catalog, rng, roles, age);
var skills = PersonSampler.Skills(catalog, rng, age, choices, traits);
var needs = PersonSampler.Needs(catalog);
var name = new PersonName(
given.Form,
female ? surname.Female : surname.Male,
patronymic,
GivenTable(given, names.DefaultGivenDeclension),
SurnameTable(surname, female, names.DefaultSurnameDeclension),
PatronymicTable(patronymic, female));
return new Person
{
Id = id,
FamilyId = familyId,
Female = female,
BirthDate = DateTime.SpecifyKind(birth, DateTimeKind.Utc),
Name = name,
IsStudent = isStudent,
IsStaff = false,
IsParent = isParent,
ClassId = classId,
Numbers = numbers,
Choices = choices,
Skills = skills,
Traits = traits,
Needs = needs,
};
}
private static GivenNameEntry PickGiven(
IReadOnlyList<GivenNameEntry> pool,
Random rng,
HashSet<string>? used = null)
{
if (pool.Count == 0)
{
throw new InvalidOperationException("Name set has no given names for this sex.");
}
for (var attempt = 0; attempt < 16; attempt++)
{
var pick = pool[rng.Next(pool.Count)];
if (used is null || used.Add(pick.Form) || used.Count >= pool.Count)
{
return pick;
}
}
return pool[rng.Next(pool.Count)];
}
private static CaseTable GivenTable(GivenNameEntry entry, string defaultModel) =>
new()
{
Nom = NameGrammar.InflectGiven(entry, GrammaticalCase.Nominative, defaultModel),
Gen = NameGrammar.InflectGiven(entry, GrammaticalCase.Genitive, defaultModel),
Dat = NameGrammar.InflectGiven(entry, GrammaticalCase.Dative, defaultModel),
Acc = NameGrammar.InflectGiven(entry, GrammaticalCase.Accusative, defaultModel),
Ins = NameGrammar.InflectGiven(entry, GrammaticalCase.Instrumental, defaultModel),
Pre = NameGrammar.InflectGiven(entry, GrammaticalCase.Prepositional, defaultModel),
};
private static CaseTable SurnameTable(SurnameEntry entry, bool female, string defaultModel) =>
new()
{
Nom = NameGrammar.InflectSurname(entry, female, GrammaticalCase.Nominative, defaultModel),
Gen = NameGrammar.InflectSurname(entry, female, GrammaticalCase.Genitive, defaultModel),
Dat = NameGrammar.InflectSurname(entry, female, GrammaticalCase.Dative, defaultModel),
Acc = NameGrammar.InflectSurname(entry, female, GrammaticalCase.Accusative, defaultModel),
Ins = NameGrammar.InflectSurname(entry, female, GrammaticalCase.Instrumental, defaultModel),
Pre = NameGrammar.InflectSurname(entry, female, GrammaticalCase.Prepositional, defaultModel),
};
private static CaseTable PatronymicTable(string nominative, bool female) =>
new()
{
Nom = nominative,
Gen = NameGrammar.InflectPatronymic(nominative, female, GrammaticalCase.Genitive),
Dat = NameGrammar.InflectPatronymic(nominative, female, GrammaticalCase.Dative),
Acc = NameGrammar.InflectPatronymic(nominative, female, GrammaticalCase.Accusative),
Ins = NameGrammar.InflectPatronymic(nominative, female, GrammaticalCase.Instrumental),
Pre = NameGrammar.InflectPatronymic(nominative, female, GrammaticalCase.Prepositional),
};
private readonly record struct ChildDraft(
PupilSeat Seat,
bool Female,
GivenNameEntry Given,
DateTime Birth);
}