Locker and home do not count. A pack without the field keeps vanilla 0.5. Co-authored-by: Cursor <cursoragent@cursor.com>
188 lines
6.9 KiB
C#
188 lines
6.9 KiB
C#
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, and with the assigned teacher standing there. A pupil without today's
|
|
/// textbook in the bag learns at <see cref="BehaviorDef.LessonNoTextbookFactor"/>. The formula
|
|
/// lives in <see cref="HSchool.Ai.LessonLearning"/>.
|
|
/// </summary>
|
|
internal static class LessonLearningSystem
|
|
{
|
|
private static readonly QueryDescription Places =
|
|
new QueryDescription().WithAll<PersonIdentity, Presence, PersonSkills>();
|
|
|
|
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;
|
|
var places = new Dictionary<string, Presence>(StringComparer.Ordinal);
|
|
var teacherSkills = new Dictionary<string, Dictionary<string, float>>(StringComparer.Ordinal);
|
|
world.Query(
|
|
in Places,
|
|
(ref PersonIdentity identity, ref Presence presence, ref PersonSkills personSkills) =>
|
|
{
|
|
places[identity.Id] = presence;
|
|
teacherSkills[identity.Id] = personSkills.Values;
|
|
});
|
|
|
|
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 || !IsStanding(presence))
|
|
{
|
|
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 is null
|
|
|| !presence.NodeId.Equals(lesson.RoomId, StringComparison.Ordinal)
|
|
|| !catalog.Subjects.TryGetValue(lesson.Subject, out var subject))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!TeacherStandingIn(places, lesson.TeacherId, lesson.RoomId))
|
|
{
|
|
school.TryLogLessonOnce(personId, PersonLogTypes.LessonNoTeacher, lesson.Subject);
|
|
return;
|
|
}
|
|
|
|
var hunger = needs.Values.GetValueOrDefault("Hunger", 1f);
|
|
var warmth = needs.Values.GetValueOrDefault("Warmth", 1f);
|
|
if (warmth < rules.NeedThreshold)
|
|
{
|
|
school.TryLogLessonOnce(personId, PersonLogTypes.LessonCold, lesson.Subject);
|
|
}
|
|
|
|
var textbookFactor = 1f;
|
|
if (person.IsStudent && !LessonLearning.HasTextbookInBag(person.Items, lesson.Subject))
|
|
{
|
|
textbookFactor = rules.LessonNoTextbookFactor;
|
|
school.TryLogLessonOnce(personId, PersonLogTypes.LessonNoTextbook, lesson.Subject);
|
|
}
|
|
|
|
IReadOnlyDictionary<string, float> taught = teacherSkills.TryGetValue(lesson.TeacherId, out var found)
|
|
? found
|
|
: new Dictionary<string, float>(StringComparer.Ordinal);
|
|
var teacherSkill = LessonLearning.AverageTeacherSkill(subject, taught, catalog);
|
|
foreach (var share in subject.Skills)
|
|
{
|
|
if (!catalog.Skills.TryGetValue(share.Skill, out var skill) || skill.Abstract)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!skills.Values.TryGetValue(share.Skill, out var current))
|
|
{
|
|
current = skill.Range.Min;
|
|
}
|
|
|
|
skills.Values[share.Skill] = LessonLearning.Gain(
|
|
current,
|
|
skill,
|
|
share.Share,
|
|
rules.LessonSkillPerHour,
|
|
hours,
|
|
hunger,
|
|
TraitOffset(catalog, traits, share.Skill),
|
|
teacherSkill,
|
|
warmth,
|
|
textbookFactor);
|
|
}
|
|
});
|
|
}
|
|
|
|
private static bool IsStanding(Presence presence) =>
|
|
presence.NodeId is not null && presence.Path.Length == 0 && presence.RemainingMinutes <= 0;
|
|
|
|
private static bool TeacherStandingIn(
|
|
Dictionary<string, Presence> places,
|
|
string teacherId,
|
|
string roomId) =>
|
|
places.TryGetValue(teacherId, out var teacher) && IsStanding(teacher)
|
|
&& teacher.NodeId is not null
|
|
&& teacher.NodeId.Equals(roomId, StringComparison.Ordinal);
|
|
|
|
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;
|
|
}
|
|
}
|