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
@@ -136,6 +136,7 @@ internal static class AttendanceSystem
reason))
{
school.RosterTalkDirty = true;
GradeTrailSystem.AfterAbsent(school, person, reason);
}
}
}
@@ -0,0 +1,78 @@
using HSchool.Content;
using HSchool.People;
namespace HSchool.Simulation;
/// <summary>
/// After marks or absences: info toast, class-teacher log, and optional auto summons weight.
/// Illness absences do not become summons offenses.
/// </summary>
internal static class GradeTrailSystem
{
public static void AfterMark(School school, Person person)
{
if (school.Catalog?.BehaviorRules is not { } rules)
{
return;
}
TryReact(school, person, rules);
}
public static void AfterAbsent(School school, Person person, string? absenceReason)
{
if (school.Catalog?.BehaviorRules is not { } rules)
{
return;
}
if (GradeTrail.CountsAsSummonOffense(absenceReason)
&& OffenseMemory.Record(person, OffenseKinds.Truancy, school.Clock.Time, otherPersonId: null, rules))
{
school.RosterTalkDirty = true;
}
TryReact(school, person, rules);
}
private static void TryReact(School school, Person person, BehaviorDef rules)
{
if (!GradeTrail.IsPoor(person, rules))
{
return;
}
if (!school.TryClaimGradeTrailNotice(person.Id, school.Clock.Time.Date))
{
return;
}
school.RaiseWorldEvent(new WorldEvent(EventTriggers.PoorGradeTrail, person.Id));
var schoolClass = ClassOf(school, person);
if (schoolClass?.ClassTeacherId is { } teacherId)
{
school.AppendDayLog(new PersonLogEvent(
person.Id,
school.Clock.Time,
PersonLogTypes.ClassTeacherGradeNote,
teacherId));
}
if (rules.GradeTrailSummonChance > 0f)
{
DirectorSummonSystem.TryEnqueue(school, person.Id, rules.GradeTrailSummonChance);
}
}
private static SchoolClass? ClassOf(School school, Person person)
{
if (person.ClassId is null || school.Roster is null)
{
return null;
}
return school.Roster.Classes.FirstOrDefault(row =>
row.Id.Equals(person.ClassId, StringComparison.Ordinal));
}
}
@@ -112,6 +112,13 @@ internal static class LessonLearningSystem
textbookFactor *= TalkCircles.LessonWhisperFactor(activity.ActionId, rules);
textbookFactor *= DiseaseEffects.LessonLearningFactor(person, catalog, school.Clock.Time);
var mode = ApparelDresser.ModeForLesson(lesson.Subject);
if (ApparelDresser.CurrentIssues(school, person, mode) != ApparelIssue.None)
{
school.TryLogLessonOnce(personId, PersonLogTypes.LessonDress, lesson.Subject);
textbookFactor *= rules.LessonDressMarkFactor;
}
IReadOnlyDictionary<string, float> taught = teacherSkills.TryGetValue(lesson.TeacherId, out var found)
? found
: new Dictionary<string, float>(StringComparer.Ordinal);
@@ -165,6 +172,7 @@ internal static class LessonLearningSystem
if (LessonMarkMemory.Record(person, lesson.Subject, mark, school.Clock.Time, lesson.Period, rules))
{
school.RosterTalkDirty = true;
GradeTrailSystem.AfterMark(school, person);
}
}
@@ -194,6 +202,7 @@ internal static class LessonLearningSystem
if (LessonMarkMemory.Record(person, lesson.Subject, mark, school.Clock.Time, lesson.Period, rules))
{
school.RosterTalkDirty = true;
GradeTrailSystem.AfterMark(school, person);
}
}
+12 -2
View File
@@ -25,6 +25,8 @@ public static class PersonLogTypes
public const string LessonNoTeacher = "lesson-no-teacher";
public const string LessonCold = "lesson-cold";
public const string LessonNoTextbook = "lesson-no-textbook";
public const string LessonDress = "lesson-dress";
public const string ClassTeacherGradeNote = "class-teacher-grade-note";
}
/// <summary>
@@ -102,7 +104,8 @@ public sealed record PersonLogEvent(string PersonId, DateTime Time, string Type,
if (Type.Equals(PersonLogTypes.LessonNoTeacher, StringComparison.Ordinal)
|| Type.Equals(PersonLogTypes.LessonCold, StringComparison.Ordinal)
|| Type.Equals(PersonLogTypes.LessonNoTextbook, StringComparison.Ordinal))
|| Type.Equals(PersonLogTypes.LessonNoTextbook, StringComparison.Ordinal)
|| Type.Equals(PersonLogTypes.LessonDress, StringComparison.Ordinal))
{
var name = catalog.Subjects.TryGetValue(ThingDef, out var subject)
? catalog.Label(locale, subject)
@@ -111,10 +114,17 @@ public sealed record PersonLogEvent(string PersonId, DateTime Time, string Type,
? "LessonCold"
: Type.Equals(PersonLogTypes.LessonNoTextbook, StringComparison.Ordinal)
? "LessonNoTextbook"
: "LessonNoTeacher";
: Type.Equals(PersonLogTypes.LessonDress, StringComparison.Ordinal)
? "LessonDress"
: "LessonNoTeacher";
return string.Format(CultureInfo.InvariantCulture, catalog.Text(locale, key), name);
}
if (Type.Equals(PersonLogTypes.ClassTeacherGradeNote, StringComparison.Ordinal))
{
return catalog.Text(locale, "ClassTeacherGradeNote");
}
if (Type.Equals(PersonLogTypes.TalkEnded, StringComparison.Ordinal)
|| Type.Equals(PersonLogTypes.HomeTalk, StringComparison.Ordinal))
{
+11
View File
@@ -19,6 +19,7 @@ public sealed class School : IDisposable
private readonly List<PersonLogEvent> _dayLog = [];
private readonly HashSet<string> _lessonLogOnce = new(StringComparer.Ordinal);
private readonly HashSet<string> _lessonMarkOnce = new(StringComparer.Ordinal);
private readonly HashSet<string> _gradeTrailOnce = new(StringComparer.Ordinal);
private readonly List<WorldEvent> _worldEvents = [];
internal School(int id, string name, DateTime startDate, DefCatalog? catalog, MapLayout? map)
@@ -176,6 +177,7 @@ public sealed class School : IDisposable
LoggedActivity.Clear();
_lessonLogOnce.Clear();
_lessonMarkOnce.Clear();
_gradeTrailOnce.Clear();
LastAttendanceSlot = null;
}
@@ -205,6 +207,15 @@ public sealed class School : IDisposable
return _lessonMarkOnce.Add(key);
}
/// <summary>
/// One poor-grade-trail notice per person per calendar day (toast + class-teacher note + summon roll).
/// </summary>
internal bool TryClaimGradeTrailNotice(string personId, DateTime day)
{
var key = string.Concat(personId, "\0", day.ToString("yyyy-MM-dd"));
return _gradeTrailOnce.Add(key);
}
public void QueueDecision(string personId)
{
ObjectDisposedException.ThrowIf(_disposed, this);