172 lines
6.1 KiB
C#
172 lines
6.1 KiB
C#
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 (2–5). 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);
|