using Arch.Core; using HSchool.Content; using HSchool.People; using HSchool.Server.Api; using HSchool.Simulation; namespace HSchool.Server.Game; /// /// Builds a person card on the school's worker thread so live need values come from the World. /// internal static class PersonCardReader { private static readonly QueryDescription IdentityAndNeeds = new QueryDescription().WithAll(); private static readonly QueryDescription IdentityAndSkills = new QueryDescription().WithAll(); private static readonly QueryDescription IdentityAndActivity = new QueryDescription().WithAll(); 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)); person ??= school.Applicants?.Applicants .FirstOrDefault(applicant => applicant.Person.Id.Equals(personId, StringComparison.Ordinal)) ?.Person; 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; var skills = LiveSkills(school.World, personId); var activityId = LiveActivity(school.World, personId); string? activityLabel = null; if (activityId is not null && catalog is not null && catalog.Actions.TryGetValue(activityId, out var action)) { activityLabel = catalog.Label(locale, action); } else if (activityId is not null) { activityLabel = activityId; } 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.ClassId, person.Position, PeopleListMapper.PositionLabel(catalog, locale, person.Position), Body(person, catalog, locale), Skills(person, skills, catalog, locale), Traits(person, catalog, locale), Needs(needs, catalog, locale), activityId, activityLabel, Family(roster, person)); } private static IReadOnlyDictionary? LiveNeeds(World world, string personId) { Dictionary? found = null; world.Query(in IdentityAndNeeds, (ref PersonIdentity identity, ref PersonNeeds needs) => { if (identity.Id.Equals(personId, StringComparison.Ordinal)) { found = new Dictionary(needs.Values, StringComparer.Ordinal); } }); return found; } private static IReadOnlyDictionary? LiveSkills(World world, string personId) { Dictionary? found = null; world.Query(in IdentityAndSkills, (ref PersonIdentity identity, ref PersonSkills skills) => { if (identity.Id.Equals(personId, StringComparison.Ordinal)) { found = new Dictionary(skills.Values, StringComparer.Ordinal); } }); return found; } private static string? LiveActivity(World world, string personId) { string? found = null; var query = IdentityAndActivity; world.Query(in query, (ref PersonIdentity identity, ref PersonActivity activity) => { if (identity.Id.Equals(personId, StringComparison.Ordinal) && activity.IsActive) { found = activity.ActionId; } }); return found; } private static IReadOnlyList Body(Person person, DefCatalog? catalog, string locale) { var rows = new List(); 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 Skills( Person person, IReadOnlyDictionary? live, DefCatalog? catalog, string locale) { if (catalog is null) { if (live is not null) { return live .Select(pair => new LabeledStatResponse(pair.Key, pair.Key, FormatSkill(pair.Value))) .ToArray(); } return person.Skills .Select(pair => new LabeledStatResponse(pair.Key, pair.Key, pair.Value.ToString())) .ToArray(); } var rows = new List(); foreach (var def in catalog.Skills.Values) { if (def.Abstract) { continue; } if (live is not null) { if (!live.TryGetValue(def.DefName, out var liveValue)) { continue; } rows.Add(new LabeledStatResponse(def.DefName, catalog.Label(locale, def), FormatSkill(liveValue))); continue; } if (!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 string FormatSkill(float value) => Math.Round(value, 2).ToString("0.##"); private static IReadOnlyList Traits(Person person, DefCatalog? catalog, string locale) { var rows = new List(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 Needs( IReadOnlyDictionary 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(); 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 Relatives( IReadOnlyList ids, IReadOnlyDictionary people, string except) { var rows = new List(); 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; } }