Wire grade trail notices, dress mark factor, and truancy summons weights.

Lesson multipliers stay explicit in tests; poor marks and unexcused absences toast and may auto-summon, while illness stays excused.
This commit is contained in:
Leonid Pershin
2026-08-21 14:18:41 +03:00
parent 975ca4936e
commit 59a7023bc4
23 changed files with 762 additions and 13 deletions
+90
View File
@@ -0,0 +1,90 @@
using HSchool.Content;
namespace HSchool.People;
/// <summary>
/// Pure trail checks over recent marks and attendance. Simulation decides when to notice or summon.
/// </summary>
public static class GradeTrail
{
public static float? AverageMark(Person person)
{
ArgumentNullException.ThrowIfNull(person);
var list = person.LessonMarks;
if (list is null || list.Count == 0)
{
return null;
}
var total = 0;
foreach (var row in list)
{
total += row.Value;
}
return total / (float)list.Count;
}
public static int TruancyCount(Person person)
{
ArgumentNullException.ThrowIfNull(person);
var list = person.Attendance;
if (list is null || list.Count == 0)
{
return 0;
}
var count = 0;
foreach (var row in list)
{
if (row.Status == AttendanceStatuses.Absent
&& (row.AbsenceReason is null
|| row.AbsenceReason.Equals(AbsenceReasons.Truancy, StringComparison.Ordinal)))
{
count++;
}
}
return count;
}
public static bool HasPoorMarks(Person person, BehaviorDef rules)
{
ArgumentNullException.ThrowIfNull(person);
ArgumentNullException.ThrowIfNull(rules);
if (rules.PoorMarkTrailMinMarks <= 0 || rules.PoorMarkTrailMaxAverage <= 0f)
{
return false;
}
var list = person.LessonMarks;
if (list is null || list.Count < rules.PoorMarkTrailMinMarks)
{
return false;
}
return AverageMark(person) is { } average && average <= rules.PoorMarkTrailMaxAverage;
}
public static bool HasTruancyTrail(Person person, BehaviorDef rules)
{
ArgumentNullException.ThrowIfNull(person);
ArgumentNullException.ThrowIfNull(rules);
if (rules.TruancyTrailMinCount <= 0)
{
return false;
}
return TruancyCount(person) >= rules.TruancyTrailMinCount;
}
public static bool IsPoor(Person person, BehaviorDef rules) =>
HasPoorMarks(person, rules) || HasTruancyTrail(person, rules);
/// <summary>
/// Illness absences are excused for summons / offense memory. Truancy and unknown reasons are not.
/// </summary>
public static bool CountsAsSummonOffense(string? absenceReason) =>
absenceReason is null
|| absenceReason.Equals(AbsenceReasons.Truancy, StringComparison.Ordinal);
}
+4
View File
@@ -12,11 +12,15 @@ public static class OffenseKinds
public const string Reprimand = "reprimand";
/// <summary>Unexcused lesson absence (slice 13). Illness does not use this kind.</summary>
public const string Truancy = "truancy";
public static string LocaleKey(string kind) => kind switch
{
Quarrel => "OffenseQuarrel",
Fight => "OffenseFight",
Reprimand => "OffenseReprimand",
Truancy => "OffenseTruancy",
_ => "Offense" + char.ToUpperInvariant(kind[0]) + kind[1..],
};
}
+1
View File
@@ -27,6 +27,7 @@ public static class Seed
public const int MeetingAttendSalt = 19;
public const int DiseaseSalt = 20;
public const int ContagionSalt = 21;
public const int GradeTrailSalt = 22;
/// <summary>A stream that belongs to the school rather than to one family.</summary>
public static int ForSchool(int schoolSeed, int salt) => Mix(schoolSeed, familyIndex: -1, salt);