using Arch.Core; using HSchool.Ai; using HSchool.Content; using HSchool.People; using HSchool.Schedule; namespace HSchool.Simulation.Tests; public class NurseCareSimulationTests { private static readonly DateTime TuesdayMorning = new(2012, 4, 3, 8, 0, 0, DateTimeKind.Utc); [Fact] public void NurseInOffice_SeverityFallsFasterThanUntendedControl() { using var tended = OpenWithNurse(); using var control = OpenWithNurse(); var tendedPupil = MakeSick(tended, severity: 0.6f); var controlPupil = MakeSick(control, severity: 0.6f); SetMedicine(tended, 80); SetMedicine(control, 80); PlaceAt(tended, tendedPupil.Id, "medical-office"); PlaceAt(control, controlPupil.Id, "classroom-101"); PlaceNurseAt(tended, "medical-office"); PlaceNurseAt(control, "classroom-101"); // Enqueue + admit so tend applies to the co-located patient. NurseCareSystem.Apply(tended, gameMinutes: 0); Assert.True(NurseCareSystem.IsVisiting(tended, tendedPupil.Id)); NurseCareSystem.Apply(tended, gameMinutes: 24 * 60); Assert.True(HealthConditions.Tick( controlPupil, control.PeopleSeed, DateOnly.FromDateTime(control.Clock.Time).DayNumber, gameMinutes: 24 * 60, control.Catalog, control.Clock.Time)); Assert.True(tendedPupil.Conditions![0].Severity < controlPupil.Conditions![0].Severity); Assert.True(tendedPupil.Conditions[0].Progress > controlPupil.Conditions[0].Progress); } [Fact] public void SkipEmpty_ClearsNurseVisitQueue() { using var school = OpenWithNurse(); var pupil = MakeSick(school, severity: 0.7f); NurseCareSystem.Apply(school, gameMinutes: 0); Assert.True(NurseCareSystem.IsVisiting(school, pupil.Id)); Assert.NotEmpty(school.NurseVisits); ClearCampus(school); school.Clock.JumpTo(new DateTime(2012, 4, 3, 20, 0, 0, DateTimeKind.Utc)); var result = school.TrySkipEmpty(); Assert.Equal(SkipEmptyError.None, result.Error); Assert.Empty(school.NurseVisits); Assert.False(NurseCareSystem.IsVisiting(school, pupil.Id)); } [Fact] public void WithoutNurse_DoesNotEnqueue() { using var school = OpenStaffedNoNurse(); var pupil = MakeSick(school, severity: 0.7f); NurseCareSystem.Apply(school, gameMinutes: 0); Assert.False(NurseCareSystem.IsVisiting(school, pupil.Id)); Assert.Empty(school.NurseVisits); } private static Person MakeSick(School school, float severity) { var pupil = school.Roster!.People.First(person => person.IsStudent && !person.IsParent); HealthConditions.Add( pupil, new HealthCondition { DefName = "CommonCold", Severity = severity, Progress = 0.1f, Source = "test", StartedAt = TuesdayMorning.AddDays(-2), }); return pupil; } private static void SetMedicine(School school, int medicine) { var nurse = school.Roster!.People.First(person => person.IsStaff && Staffing.NursePosition.Equals(person.Position, StringComparison.Ordinal)); ((Dictionary)nurse.Skills)[NurseCare.MedicineSkill] = medicine; var query = new QueryDescription().WithAll(); school.World.Query(in query, (ref PersonIdentity identity, ref PersonSkills skills) => { if (identity.Id.Equals(nurse.Id, StringComparison.Ordinal)) { skills.Values[NurseCare.MedicineSkill] = medicine; } }); } private static void PlaceNurseAt(School school, string nodeId) { var nurseId = NurseCare.NurseId(school.Roster!)!; PlaceAt(school, nurseId, nodeId); } private static void PlaceAt(School school, string personId, string? nodeId) { TalkCircleSystem.Interrupt(school, personId); var query = new QueryDescription().WithAll(); school.World.Query( in query, (ref PersonIdentity identity, ref Presence presence, ref PersonActivity activity, ref Intent intent) => { if (!identity.Id.Equals(personId, StringComparison.Ordinal)) { return; } presence = nodeId is null ? Presence.OffCampus : new Presence(nodeId, 0f, nodeId, false, []); activity = PersonActivity.Idle; intent = Intent.None; }); } private static void ClearCampus(School school) { var query = new QueryDescription().WithAll(); school.World.Query( in query, (ref PersonIdentity _, ref Presence presence, ref PersonActivity activity, ref Intent intent) => { presence = Presence.OffCampus; activity = PersonActivity.Idle; intent = Intent.None; }); } private static School OpenWithNurse() { var school = OpenStaffedNoNurse(); HireNurse(school); return school; } private static void HireNurse(School school) { const float cap = 100_000f; var pool = school.Applicants!; var applicantId = pool.Applicants[0].Person.Id; var hired = Staffing.Hire( school.Catalog!, school.Map!, school.Roster!, pool, applicantId, Staffing.NursePosition, cap); Assert.Equal(StaffingError.None, hired.Error); school.ApplyStaffing(hired.Roster, hired.Pool); Assert.NotNull(NurseCare.NurseId(school.Roster!)); } private static School OpenStaffedNoNurse() { var (catalog, map) = Vanilla(); var roster = RosterGenerator.Generate(catalog, map, schoolSeed: 80, "Russia", TuesdayMorning); var pool = ApplicantPool.Create(catalog, roster, 80, "Russia", TuesdayMorning); var school = School.Create(80, "NurseCare", TuesdayMorning, catalog, map); school.InstallPeople(roster, 80, "Russia", pool); var schoolClass = roster.Classes.First(row => row.RoomId == "classroom-101"); school.SetTimetable(new Timetable( [ new LessonPlacement(schoolClass.Id, "Mathematics", "t1", schoolClass.RoomId, Day: 1, Period: 1), ], [])); school.Clock.JumpTo(TuesdayMorning); return school; } private static (DefCatalog Catalog, MapLayout Map) Vanilla() { var root = Path.Combine(AppContext.BaseDirectory, "vanilla"); Assert.True(Directory.Exists(root), $"Vanilla pack missing at {root}."); var documents = new List(); foreach (var path in Directory.EnumerateFiles(root, "*.*", SearchOption.AllDirectories)) { if (!path.EndsWith(".jsonc", StringComparison.OrdinalIgnoreCase) && !path.EndsWith(".json", StringComparison.OrdinalIgnoreCase)) { continue; } documents.Add(new ContentDocument( CatalogLoader.CorePackId, Path.GetRelativePath(root, path).Replace('\\', '/'), File.ReadAllText(path))); } var catalog = new CatalogLoader().Load([CatalogLoader.CorePackId], documents); var map = CatalogLoader.LastDefaultMap([CatalogLoader.CorePackId], documents); Assert.NotNull(map); return (catalog, map); } }