Update decision-making phase and enhance localization for presence tracking
ci / server (push) Failing after 3m40s
ci / client (push) Successful in 14s

- 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.
This commit is contained in:
Leonid Pershin
2026-08-19 21:00:12 +03:00
parent b135a9caad
commit 5cd5a6d258
21 changed files with 1839 additions and 72 deletions
@@ -0,0 +1,131 @@
using Arch.Core;
using HSchool.Ai;
using HSchool.Content;
using HSchool.People;
using HSchool.Schedule;
namespace HSchool.Simulation;
/// <summary>
/// Grows skills for people who are actually in the lesson: at the room, not walking, not off
/// doing something else. The formula lives in <see cref="HSchool.Ai.LessonLearning"/>.
/// </summary>
internal static class LessonLearningSystem
{
private static readonly QueryDescription People =
new QueryDescription().WithAll<PersonIdentity, PersonSkills, PersonTraits, PersonNeeds, PersonRoles, Presence, PersonActivity>();
public static void Apply(School school, double gameMinutes)
{
if (school.Catalog is null || school.Timetable is null || school.Roster is null || gameMinutes <= 0)
{
return;
}
var rules = school.Catalog.BehaviorRules;
if (rules is null || rules.LessonSkillPerHour <= 0)
{
return;
}
var slot = SchoolDay.At(school.Catalog, school.Clock.Time, school.SchoolWeekDays);
if (slot.Kind != DaySlotKind.Lesson)
{
return;
}
var hours = (float)(gameMinutes / 60d);
var weekday = SchoolDay.WeekdayIndex(school.Clock.Time);
var catalog = school.Catalog;
var world = school.World;
world.Query(
in People,
(ref PersonIdentity identity, ref PersonSkills skills, ref PersonTraits traits, ref PersonNeeds needs, ref PersonRoles roles, ref Presence presence, ref PersonActivity activity) =>
{
if (activity.IsActive || presence.NodeId is null || presence.Path.Length > 0 || presence.RemainingMinutes > 0)
{
return;
}
var personId = identity.Id;
var person = school.Roster.People.FirstOrDefault(row => row.Id.Equals(personId, StringComparison.Ordinal));
if (person is null)
{
return;
}
var lesson = CurrentLesson(school, person, weekday, slot.Index);
if (lesson is null
|| !presence.NodeId.Equals(lesson.RoomId, StringComparison.Ordinal)
|| !catalog.Subjects.TryGetValue(lesson.Subject, out var subject))
{
return;
}
var hunger = needs.Values.GetValueOrDefault("Hunger", 1f);
foreach (var share in subject.Skills)
{
if (!catalog.Skills.TryGetValue(share.Skill, out var skill)
|| skill.Abstract
|| !skills.Values.TryGetValue(share.Skill, out var current))
{
continue;
}
skills.Values[share.Skill] = LessonLearning.Gain(
current,
skill,
share.Share,
rules.LessonSkillPerHour,
hours,
hunger,
TraitOffset(catalog, traits, share.Skill));
}
});
}
private static LessonPlacement? CurrentLesson(School school, Person person, int weekday, int period)
{
foreach (var lesson in Duty.LessonsToday(person, ClassOf(school, person), school.Timetable, weekday))
{
if (lesson.Period == period)
{
return lesson;
}
}
return null;
}
private static SchoolClass? ClassOf(School school, Person person)
{
if (person.ClassId is null)
{
return null;
}
return school.Roster?.Classes.FirstOrDefault(row => row.Id.Equals(person.ClassId, StringComparison.Ordinal));
}
private static int TraitOffset(DefCatalog catalog, PersonTraits traits, string skill)
{
var offset = 0;
foreach (var name in traits.Ids)
{
if (!catalog.Traits.TryGetValue(name, out var trait))
{
continue;
}
foreach (var modifier in trait.SkillModifiers)
{
if (modifier.Skill.Equals(skill, StringComparison.Ordinal))
{
offset += modifier.Offset;
}
}
}
return offset;
}
}