Remember recent quarrels, fights and reprimands on the person.

Short offense list for the pupil card and save, capped by BehaviorDef — not a discipline score.
This commit is contained in:
Leonid Pershin
2026-08-21 09:37:12 +03:00
parent 910ba4da2d
commit fdcaed0f74
22 changed files with 760 additions and 16 deletions
+103
View File
@@ -0,0 +1,103 @@
using System.Text.Json.Serialization;
using HSchool.Content;
namespace HSchool.People;
/// <summary>Stable kind ids written into <see cref="OffenseRecord.Kind"/> and listed on <see cref="BehaviorDef.OffenseMemoryKinds"/>.</summary>
public static class OffenseKinds
{
public const string Quarrel = "quarrel";
public const string Fight = "fight";
public const string Reprimand = "reprimand";
public static string LocaleKey(string kind) => kind switch
{
Quarrel => "OffenseQuarrel",
Fight => "OffenseFight",
Reprimand => "OffenseReprimand",
_ => "Offense" + char.ToUpperInvariant(kind[0]) + kind[1..],
};
}
/// <summary>One remembered misconduct. Sparse list on the person — not a discipline score.</summary>
public sealed class OffenseRecord
{
public required string Kind { get; init; }
public required DateTime Time { get; init; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? OtherPersonId { get; init; }
}
/// <summary>Appends recent offenses and drops the oldest when over the BehaviorDef ceiling.</summary>
public static class OffenseMemory
{
public static bool IsEnabled(string kind, BehaviorDef rules)
{
foreach (var allowed in rules.OffenseMemoryKinds)
{
if (allowed.Equals(kind, StringComparison.Ordinal))
{
return true;
}
}
return false;
}
public static bool Record(Person person, string kind, DateTime time, string? otherPersonId, BehaviorDef rules)
{
ArgumentNullException.ThrowIfNull(person);
ArgumentNullException.ThrowIfNull(rules);
ArgumentException.ThrowIfNullOrWhiteSpace(kind);
if (rules.OffenseMemoryMax <= 0 || !IsEnabled(kind, rules))
{
return false;
}
var list = person.Offenses;
if (list is null)
{
list = [];
person.Offenses = list;
}
list.Add(new OffenseRecord
{
Kind = kind,
Time = DateTime.SpecifyKind(time, DateTimeKind.Utc),
OtherPersonId = string.IsNullOrWhiteSpace(otherPersonId) ? null : otherPersonId,
});
while (list.Count > rules.OffenseMemoryMax)
{
list.RemoveAt(0);
}
return true;
}
public static string? OtherParticipant(string personId, string? victimId, IReadOnlyList<string> members)
{
if (victimId is not null
&& !victimId.Equals(personId, StringComparison.Ordinal)
&& members.Contains(victimId, StringComparer.Ordinal))
{
return victimId;
}
foreach (var member in members)
{
if (!member.Equals(personId, StringComparison.Ordinal))
{
return member;
}
}
return null;
}
}
+6
View File
@@ -69,6 +69,12 @@ public sealed record Person
/// </summary>
public string? BullyVictimId { get; set; }
/// <summary>
/// Recent misconduct (quarrel / fight / reprimand). Null when empty so people.json stays compact.
/// Ceiling and which kinds write live on <c>BehaviorDef</c>.
/// </summary>
public List<OffenseRecord>? Offenses { get; set; }
public int AgeOn(DateTime asOf) => SchoolYears.AgeYears(BirthDate, asOf);
}