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:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user