Files
h-school/src/HSchool.People/Roster.cs
T
Leonid PershinandCursor 37501c506a Dress people at generation from a separate apparel stream.
Wardrobes live on the person in people.json: everyday layers, textbooks per parallel, and chance-based bags from catalog data. Hauling and the carry cap are defs, not constants. The golden roster gains an items column; later family members shift because Hauling consumes extra appearance rolls. Old saves without items are dressed on load.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-20 03:24:54 +03:00

113 lines
3.7 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; } = [];
public int AgeOn(DateTime asOf) => SchoolYears.AgeYears(BirthDate, asOf);
}
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);