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
+160
View File
@@ -0,0 +1,160 @@
using Arch.Core;
using HSchool.Ai;
using HSchool.Content;
namespace HSchool.Simulation;
/// <summary>
/// Starts and ticks actions on World entities. Rules live in <see cref="ActionStepper"/>; this
/// adapter copies them onto components and counts occupied things at a node.
/// </summary>
internal static class ActivitySystem
{
private static readonly QueryDescription People =
new QueryDescription().WithAll<PersonIdentity, PersonRoles, PersonNeeds, Presence, PersonActivity>();
public static bool TryStart(School school, string personId, string actionId)
{
if (school.Catalog is null || school.Map is null)
{
return false;
}
if (!school.Catalog.Actions.TryGetValue(actionId, out var action) || action.Abstract)
{
return false;
}
var started = false;
var world = school.World;
world.Query(
in People,
(ref PersonIdentity identity, ref PersonRoles roles, ref Presence presence, ref PersonActivity activity) =>
{
if (started || !identity.Id.Equals(personId, StringComparison.Ordinal))
{
return;
}
if (activity.IsActive || presence.NodeId is null)
{
return;
}
if (!RoleFits(action, roles))
{
return;
}
var location = school.Map.NodeDef(presence.NodeId);
if (location is null || !location.Equals(action.Room, StringComparison.Ordinal))
{
return;
}
if (!string.IsNullOrWhiteSpace(action.Thing))
{
var available = RoomOccupancy.ThingCount(school.Catalog, school.Map, presence.NodeId, action.Thing);
var occupied = Occupied(school, presence.NodeId, action.Thing);
if (!ActionStepper.CanOccupy(available, occupied))
{
return;
}
}
var next = ActionStepper.Begin(action);
activity = new PersonActivity(next.ActionId, next.Thing, next.RemainingMinutes);
started = true;
});
return started;
}
public static void Apply(School school, double gameMinutes)
{
if (school.Catalog is null || gameMinutes <= 0)
{
return;
}
var catalog = school.Catalog;
var minutes = (float)gameMinutes;
var world = school.World;
world.Query(in People, (ref PersonNeeds needs, ref Presence presence, ref PersonActivity activity) =>
{
if (!activity.IsActive)
{
return;
}
if (!presence.IsOnCampus || activity.ActionId is null || !catalog.Actions.TryGetValue(activity.ActionId, out var action))
{
activity = PersonActivity.Idle;
return;
}
var next = ActionStepper.Advance(
new ActivityProgress(activity.ActionId, activity.Thing, activity.RemainingMinutes),
minutes,
out var completed);
if (!completed)
{
activity = new PersonActivity(next.ActionId, next.Thing, next.RemainingMinutes);
return;
}
if (!string.IsNullOrWhiteSpace(action.Need)
&& catalog.Needs.TryGetValue(action.Need, out var need)
&& needs.Values.TryGetValue(action.Need, out var current))
{
needs.Values[action.Need] = ActionStepper.ApplyNeedGain(current, action, need);
}
activity = PersonActivity.Idle;
});
}
private static int Occupied(School school, string nodeId, string thing)
{
var occupied = 0;
var world = school.World;
world.Query(in People, (ref Presence presence, ref PersonActivity activity) =>
{
if (activity.IsActive
&& presence.NodeId is not null
&& presence.NodeId.Equals(nodeId, StringComparison.Ordinal)
&& thing.Equals(activity.Thing, StringComparison.Ordinal))
{
occupied++;
}
});
return occupied;
}
private static bool RoleFits(ActionDef action, PersonRoles roles)
{
if (action.Roles.Count == 0)
{
return true;
}
foreach (var role in action.Roles)
{
if (role.Equals(HSchool.Content.PersonRoles.Student, StringComparison.OrdinalIgnoreCase) && roles.IsStudent)
{
return true;
}
if (role.Equals(HSchool.Content.PersonRoles.Staff, StringComparison.OrdinalIgnoreCase) && roles.IsStaff)
{
return true;
}
if (role.Equals(HSchool.Content.PersonRoles.Parent, StringComparison.OrdinalIgnoreCase) && roles.IsParent)
{
return true;
}
}
return false;
}
}