Merge branch 'phase/69-offense-memory'
Co-authored-by: Cursor <cursoragent@cursor.com> # Conflicts: # src/HSchool.Server/Api/PeopleModels.cs # src/HSchool.Server/Game/PersonCardReader.cs
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user