Implement people management features by adding API endpoints for retrieving school rosters and individual person cards. Enhance the UI to support a people browser with filtering and pagination capabilities. Update localization strings for improved user experience and ensure robust handling of person data. Revise documentation to reflect new API functionalities and update tests to validate the new features.
ci / server (push) Failing after 11s
ci / client (push) Successful in 17s

This commit is contained in:
Leonid Pershin
2026-08-18 19:44:42 +03:00
parent 52c5082418
commit 533bd80f5e
23 changed files with 2134 additions and 47 deletions
+219
View File
@@ -0,0 +1,219 @@
using Arch.Core;
using HSchool.Content;
using HSchool.People;
using HSchool.Server.Api;
using HSchool.Simulation;
namespace HSchool.Server.Game;
/// <summary>
/// Builds a person card on the school's worker thread so live need values come from the World.
/// </summary>
internal static class PersonCardReader
{
private static readonly QueryDescription IdentityAndNeeds =
new QueryDescription().WithAll<PersonIdentity, PersonNeeds>();
public static PersonCardResponse? Read(School school, string personId, string locale)
{
var roster = school.Roster;
if (roster is null)
{
return null;
}
var person = roster.People.FirstOrDefault(candidate => candidate.Id.Equals(personId, StringComparison.Ordinal));
if (person is null)
{
return null;
}
var catalog = school.Catalog;
var classes = roster.Classes.ToDictionary(schoolClass => schoolClass.Id, StringComparer.Ordinal);
int? year = null;
string? letter = null;
if (person.ClassId is { } classId && classes.TryGetValue(classId, out var schoolClass))
{
year = schoolClass.Year;
letter = schoolClass.Letter;
}
var needs = LiveNeeds(school.World, personId) ?? person.Needs;
return new PersonCardResponse(
person.Id,
person.Name.Full,
person.Name.Surname,
person.Name.Given,
person.Name.Patronymic,
person.Female,
person.AgeOn(school.Clock.Time),
person.BirthDate,
PeopleListMapper.RolesOf(person),
year,
letter,
person.Position,
PeopleListMapper.PositionLabel(catalog, locale, person.Position),
Body(person, catalog, locale),
Skills(person, catalog, locale),
Traits(person, catalog, locale),
Needs(needs, catalog, locale),
Family(roster, person));
}
private static IReadOnlyDictionary<string, float>? LiveNeeds(World world, string personId)
{
Dictionary<string, float>? found = null;
world.Query(in IdentityAndNeeds, (ref PersonIdentity identity, ref PersonNeeds needs) =>
{
if (identity.Id.Equals(personId, StringComparison.Ordinal))
{
found = new Dictionary<string, float>(needs.Values, StringComparer.Ordinal);
}
});
return found;
}
private static IReadOnlyList<LabeledStatResponse> Body(Person person, DefCatalog? catalog, string locale)
{
var rows = new List<LabeledStatResponse>();
if (catalog is not null)
{
foreach (var def in catalog.BodyAttributes.Values)
{
if (def.Abstract)
{
continue;
}
if (def.Kind == BodyAttributeKind.Number && person.Numbers.TryGetValue(def.DefName, out var number))
{
rows.Add(new LabeledStatResponse(def.DefName, catalog.Label(locale, def), number.ToString()));
}
else if (def.Kind == BodyAttributeKind.Choice && person.Choices.TryGetValue(def.DefName, out var choice))
{
rows.Add(new LabeledStatResponse(def.DefName, catalog.Label(locale, def), catalog.Text(locale, choice)));
}
}
}
else
{
foreach (var (id, number) in person.Numbers)
{
rows.Add(new LabeledStatResponse(id, id, number.ToString()));
}
foreach (var (id, choice) in person.Choices)
{
rows.Add(new LabeledStatResponse(id, id, choice));
}
}
if (person.Choices.TryGetValue(BodyBuilds.Attribute, out var build)
&& rows.TrueForAll(row => row.Id != BodyBuilds.Attribute))
{
var label = catalog?.Text(locale, BodyBuilds.Attribute) ?? BodyBuilds.Attribute;
var value = catalog?.Text(locale, build) ?? build;
rows.Add(new LabeledStatResponse(BodyBuilds.Attribute, label, value));
}
return rows;
}
private static IReadOnlyList<LabeledStatResponse> Skills(Person person, DefCatalog? catalog, string locale)
{
if (catalog is null)
{
return person.Skills
.Select(pair => new LabeledStatResponse(pair.Key, pair.Key, pair.Value.ToString()))
.ToArray();
}
var rows = new List<LabeledStatResponse>();
foreach (var def in catalog.Skills.Values)
{
if (def.Abstract || !person.Skills.TryGetValue(def.DefName, out var value))
{
continue;
}
rows.Add(new LabeledStatResponse(def.DefName, catalog.Label(locale, def), value.ToString()));
}
return rows;
}
private static IReadOnlyList<DefLabelResponse> Traits(Person person, DefCatalog? catalog, string locale)
{
var rows = new List<DefLabelResponse>(person.Traits.Count);
foreach (var id in person.Traits)
{
var label = catalog is not null && catalog.Traits.TryGetValue(id, out var def)
? catalog.Label(locale, def)
: id;
rows.Add(new DefLabelResponse(id, label));
}
return rows;
}
private static IReadOnlyList<NeedStatResponse> Needs(
IReadOnlyDictionary<string, float> values,
DefCatalog? catalog,
string locale)
{
if (catalog is null)
{
return values.Select(pair => new NeedStatResponse(pair.Key, pair.Key, pair.Value)).ToArray();
}
var rows = new List<NeedStatResponse>();
foreach (var def in catalog.Needs.Values)
{
if (def.Abstract || !values.TryGetValue(def.DefName, out var value))
{
continue;
}
rows.Add(new NeedStatResponse(def.DefName, catalog.Label(locale, def), value));
}
return rows;
}
private static PersonFamilyResponse Family(Roster roster, Person person)
{
var family = roster.Families.FirstOrDefault(candidate => candidate.Id.Equals(person.FamilyId, StringComparison.Ordinal));
if (family is null)
{
return new PersonFamilyResponse([], [], [], []);
}
var people = roster.People.ToDictionary(member => member.Id, StringComparer.Ordinal);
var inParents = family.ParentIds.Contains(person.Id, StringComparer.Ordinal);
var inChildren = family.ChildIds.Contains(person.Id, StringComparer.Ordinal);
return new PersonFamilyResponse(
inChildren ? Relatives(family.ParentIds, people, except: person.Id) : [],
inParents ? Relatives(family.ChildIds, people, except: person.Id) : [],
inChildren ? Relatives(family.ChildIds, people, except: person.Id) : [],
inParents ? Relatives(family.ParentIds, people, except: person.Id) : []);
}
private static IReadOnlyList<PersonRelResponse> Relatives(
IReadOnlyList<string> ids,
IReadOnlyDictionary<string, Person> people,
string except)
{
var rows = new List<PersonRelResponse>();
foreach (var id in ids)
{
if (id.Equals(except, StringComparison.Ordinal) || !people.TryGetValue(id, out var relative))
{
continue;
}
rows.Add(new PersonRelResponse(relative.Id, relative.Name.Full, relative.Female));
}
return rows;
}
}