Enhance protocol and simulation features with activity tracking and behavior definitions

- Updated protocol documentation to include new `activity` and `activityLabel` fields in the person card response, reflecting real-time activity status.
- Introduced `BehaviorDef` to define behavior rules, including need thresholds and lesson skill gains, enhancing AI decision-making.
- Revised the `DefCatalog` to incorporate behavior definitions and updated validation logic to ensure proper behavior handling.
- Enhanced the simulation to manage presence and activity states, allowing for more dynamic interactions within the school environment.
- Updated tests to validate the new activity tracking and behavior functionalities, ensuring robust performance and reliability.
- Improved localization strings to support new activity and behavior features, enhancing user experience.
This commit is contained in:
Leonid Pershin
2026-08-19 19:49:44 +03:00
parent 3c54f981b7
commit b135a9caad
42 changed files with 1134 additions and 59 deletions
+23 -10
View File
@@ -13,7 +13,7 @@ namespace HSchool.Simulation;
internal static class PresenceSystem
{
private static readonly QueryDescription People =
new QueryDescription().WithAll<PersonIdentity, PersonRoles, PersonTraits, Presence>();
new QueryDescription().WithAll<PersonIdentity, PersonRoles, PersonTraits, Presence, PersonActivity>();
public static void Apply(School school, double gameMinutes)
{
@@ -48,7 +48,7 @@ internal static class PresenceSystem
{
var rows = new List<PresenceSnapshot>();
var world = school.World;
world.Query(in People, (ref PersonIdentity identity, ref Presence presence) =>
world.Query(in People, (ref PersonIdentity identity, ref Presence presence, ref PersonActivity activity) =>
{
rows.Add(new PresenceSnapshot(
identity.Id,
@@ -56,7 +56,10 @@ internal static class PresenceSystem
presence.RemainingMinutes,
presence.DestinationId,
presence.HeadingHome,
presence.Path));
presence.Path,
activity.ActionId,
activity.Thing,
activity.RemainingMinutes));
});
rows.Sort((left, right) => StringComparer.Ordinal.Compare(left.PersonId, right.PersonId));
return rows;
@@ -74,17 +77,19 @@ internal static class PresenceSystem
var byId = saved
.Where(row => !string.IsNullOrWhiteSpace(row.PersonId))
.ToDictionary(row => row.PersonId, StringComparer.Ordinal);
ForEachPerson(school, (person, _, ref presence) =>
ForEachPerson(school, (person, _, ref presence, ref activity) =>
{
if (!byId.TryGetValue(person.Id, out var row))
{
presence = PlaceByDuty(school, person);
activity = PersonActivity.Idle;
return;
}
if (row.NodeId is null)
{
presence = Presence.OffCampus;
activity = PersonActivity.Idle;
return;
}
@@ -94,16 +99,20 @@ internal static class PresenceSystem
row.DestinationId,
row.HeadingHome,
row.Path?.ToArray() ?? []);
activity = string.IsNullOrWhiteSpace(row.ActionId)
? PersonActivity.Idle
: new PersonActivity(row.ActionId, row.ActionThing, row.ActionRemaining);
});
}
public static void PlaceMissingByDuty(School school)
{
ForEachPerson(school, (person, _, ref presence) =>
ForEachPerson(school, (person, _, ref presence, ref activity) =>
{
if (!presence.IsOnCampus)
{
presence = PlaceByDuty(school, person);
activity = PersonActivity.Idle;
}
});
}
@@ -323,7 +332,7 @@ internal static class PresenceSystem
}
var world = school.World;
world.Query(in People, (ref Presence presence) =>
world.Query(in People, (ref Presence presence, ref PersonActivity activity) =>
{
if (presence.HeadingHome
&& presence.NodeId is not null
@@ -332,6 +341,7 @@ internal static class PresenceSystem
&& presence.NodeId.Equals(yard, StringComparison.Ordinal))
{
presence = Presence.OffCampus;
activity = PersonActivity.Idle;
}
});
}
@@ -349,17 +359,17 @@ internal static class PresenceSystem
private static IReadOnlyList<Person> OrderedPeople(School school) =>
school.Roster!.People.OrderBy(person => person.Id, StringComparer.Ordinal).ToArray();
private delegate void PersonAction(Person person, PersonIdentity identity, ref Presence presence);
private delegate void PersonAction(Person person, PersonIdentity identity, ref Presence presence, ref PersonActivity activity);
private static void ForEachPerson(School school, PersonAction action)
{
var roster = school.Roster!.People.ToDictionary(person => person.Id, StringComparer.Ordinal);
var world = school.World;
world.Query(in People, (ref PersonIdentity identity, ref Presence presence) =>
world.Query(in People, (ref PersonIdentity identity, ref Presence presence, ref PersonActivity activity) =>
{
if (roster.TryGetValue(identity.Id, out var person))
{
action(person, identity, ref presence);
action(person, identity, ref presence, ref activity);
}
});
}
@@ -371,4 +381,7 @@ public sealed record PresenceSnapshot(
float RemainingMinutes,
string? DestinationId,
bool HeadingHome,
IReadOnlyList<string> Path);
IReadOnlyList<string> Path,
string? ActionId = null,
string? ActionThing = null,
float ActionRemaining = 0f);