- Marked tasks as complete in the decision-making phase documentation, indicating readiness for implementation. - Updated the README to reflect the completion status of the decision-making phase. - Enhanced localization strings to include new presence tracking features, improving user experience. - Revised the game screen logic to display real-time presence status, including walking states for individuals. - Added tests to validate the new localization strings and presence functionalities, ensuring robust performance.
300 lines
10 KiB
C#
300 lines
10 KiB
C#
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>();
|
|
|
|
private static readonly QueryDescription IdentityAndSkills =
|
|
new QueryDescription().WithAll<PersonIdentity, PersonSkills>();
|
|
|
|
private static readonly QueryDescription IdentityAndActivity =
|
|
new QueryDescription().WithAll<PersonIdentity, PersonActivity>();
|
|
|
|
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<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 IReadOnlyDictionary<string, float>? LiveSkills(World world, string personId)
|
|
{
|
|
Dictionary<string, float>? found = null;
|
|
world.Query(in IdentityAndSkills, (ref PersonIdentity identity, ref PersonSkills skills) =>
|
|
{
|
|
if (identity.Id.Equals(personId, StringComparison.Ordinal))
|
|
{
|
|
found = new Dictionary<string, float>(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<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,
|
|
IReadOnlyDictionary<string, float>? 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<LabeledStatResponse>();
|
|
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<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;
|
|
}
|
|
}
|