Files
h-school/src/HSchool.People/Roster.cs
T

172 lines
6.1 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
namespace HSchool.People;
/// <summary>Plain roster data. Simulation turns these into entities; this project does not.</summary>
public sealed record Roster(
IReadOnlyList<Person> People,
IReadOnlyList<Family> Families,
IReadOnlyList<SchoolClass> Classes);
public sealed record Person
{
public required string Id { get; init; }
public required string FamilyId { get; init; }
public required bool Female { get; init; }
public required DateTime BirthDate { get; init; }
public required PersonName Name { get; init; }
public required bool IsStudent { get; init; }
public required bool IsStaff { get; init; }
public required bool IsParent { get; init; }
public string? ClassId { get; init; }
public string? Position { get; init; }
public string? WorkplaceRoomId { get; init; }
public required IReadOnlyDictionary<string, int> Numbers { get; init; }
public required IReadOnlyDictionary<string, string> Choices { get; init; }
public required IReadOnlyDictionary<string, int> Skills { get; init; }
public required IReadOnlyList<string> Traits { get; init; }
public required IReadOnlyDictionary<string, float> Needs { get; init; }
/// <summary>SubjectDef names this staff member is assigned. Empty for everyone else.</summary>
public IReadOnlyList<string> Subjects { get; init; } = [];
/// <summary>Hourly rate frozen at hire. Null until the person is staff.</summary>
public float? HourlyWageAsk { get; init; }
/// <summary>Worn, bag, locker and home. Empty on rosters written before clothes existed.</summary>
public IReadOnlyList<InventoryItem> Items { get; init; } = [];
/// <summary>Changing-room node when this pupil won a locker slot. Null for staff and the rest.</summary>
public string? LockerRoomId { get; init; }
/// <summary>What this person thinks of others, A→B. Sparse; zero is not stored.</summary>
public IReadOnlyDictionary<string, int> Opinions { get; init; } = new Dictionary<string, int>(StringComparer.Ordinal);
/// <summary>
/// OrientationDef name. Null when the catalog has no orientations (vanilla). Omitted from JSON.
/// </summary>
public string? Orientation { get; init; }
/// <summary>Crushes and the one pair. Null without an orientation pack. Mutable overlay.</summary>
public PersonBonds? Bonds { get; set; }
/// <summary>
/// Victim a bully keeps aiming at. Cleared when they leave the roster or opinion hits the floor.
/// Null on everyone without a remembers-victim trait.
/// </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; }
/// <summary>
/// Recent lesson marks (25). Null when empty so people.json stays compact.
/// Ceiling and thresholds live on <c>BehaviorDef</c>.
/// </summary>
public List<LessonMarkRecord>? LessonMarks { get; set; }
/// <summary>
/// Recent lesson attendance (present / late / absent). Null when empty so people.json stays compact.
/// Ceiling and late threshold live on <c>BehaviorDef</c>.
/// </summary>
public List<AttendanceRecord>? Attendance { get; set; }
/// <summary>
/// Active medical conditions. Null when healthy (empty) so people.json stays compact.
/// Needs are not replaced by this list.
/// </summary>
public List<HealthCondition>? Conditions { get; set; }
public int AgeOn(DateTime asOf) => SchoolYears.AgeYears(BirthDate, asOf);
}
/// <summary>
/// One-way crushes and at most one pair. Lives on the person when a pack shipped orientations.
/// Mutated in place the same way opinions are.
/// </summary>
public sealed class PersonBonds
{
public List<string> Crushes { get; set; } = [];
public string? PartnerId { get; set; }
public DateTime? PairEndedOn { get; set; }
}
public sealed record PersonName(
string Given,
string Surname,
string Patronymic,
CaseTable GivenCases,
CaseTable SurnameCases,
CaseTable PatronymicCases)
{
/// <summary>Nominative ФИО as the list and cards show it: surname, given, patronymic.</summary>
public string Full => string.Join(' ', new[] { Surname, Given, Patronymic }.Where(part => part.Length > 0));
}
/// <summary>
/// A household. <see cref="ParentIds"/> holds one adult or two — a child keeps the father's
/// surname and patronymic whether or not the father lives with them, so both are recorded here
/// rather than read off a parent who may not be in the roster.
/// </summary>
public sealed record Family(
string Id,
IReadOnlyList<string> ParentIds,
IReadOnlyList<string> ChildIds,
int NextChild = 0,
string FatherGiven = "",
string Surname = "")
{
/// <summary>
/// Id suffix for the next child of this family. It is a counter and not
/// <c>ChildIds.Count</c>, because a graduate leaves the roster: deriving the suffix from the
/// children still present would hand a newcomer the id of the person who just left, and every
/// family link and open card pointing at that id would silently follow the wrong person.
///
/// Rosters written before the counter existed fall back to the highest suffix still present.
/// </summary>
public int NextChildIndex => Math.Max(NextChild, HighestChildSuffix() + 1);
private int HighestChildSuffix()
{
var highest = -1;
foreach (var id in ChildIds)
{
var marker = id.LastIndexOf(".c", StringComparison.Ordinal);
if (marker >= 0 && int.TryParse(id.AsSpan(marker + 2), out var index) && index > highest)
{
highest = index;
}
}
return highest;
}
}
public sealed record SchoolClass(
string Id,
int Year,
string Letter,
string RoomId,
int Capacity,
IReadOnlyList<string> PupilIds,
/// <summary>Hired teacher who is the class teacher, or null when the slot is empty.</summary>
string? ClassTeacherId = null);