using Arch.Core; using HSchool.Ai; using HSchool.People; namespace HSchool.Simulation; /// /// Sick people seek the medical office; FIFO corridor wait; nurse tend while co-located. /// Only the school worker thread mutates . /// internal static class NurseCareSystem { private static readonly QueryDescription PresenceQuery = new QueryDescription().WithAll(); public static bool IsVisiting(School school, string personId) => school.NurseVisits.Any(row => row.PersonId.Equals(personId, StringComparison.Ordinal)); public static bool TryDutyRoom(School school, string personId, out string roomId) { roomId = null!; var ticket = school.NurseVisits.FirstOrDefault(row => row.PersonId.Equals(personId, StringComparison.Ordinal)); if (ticket is null || school.Map is null) { return false; } var office = NurseCare.OfficeNodeId(school.Map); var wait = NurseCare.WaitNodeId(school.Map, office); if (office is null || wait is null) { return false; } roomId = ticket.Admitted ? office : wait; return true; } public static byte ResolvePresencePhase(School school, string personId) { var ticket = school.NurseVisits.FirstOrDefault(row => row.PersonId.Equals(personId, StringComparison.Ordinal)); if (ticket is null) { return NurseCare.PresenceNone; } var walking = false; var caring = false; string? nodeId = null; school.World.Query( in PresenceQuery, (ref PersonIdentity identity, ref Presence presence, ref PersonActivity activity, ref Intent _) => { if (!identity.Id.Equals(personId, StringComparison.Ordinal)) { return; } nodeId = presence.NodeId; walking = presence.Path.Length > 0 || presence.RemainingMinutes > 0 || (presence.DestinationId is not null && !presence.DestinationId.Equals(presence.NodeId, StringComparison.Ordinal)); caring = activity.IsActive && ActivitySystem.IsNurseCare(activity.ActionId); }); if (caring) { return NurseCare.PresenceCare; } if (walking) { return NurseCare.PresenceGoing; } var office = NurseCare.OfficeNodeId(school.Map!); if (ticket.Admitted && office is not null && nodeId is not null && nodeId.Equals(office, StringComparison.Ordinal)) { return NurseCare.PresenceCare; } return NurseCare.PresenceWaiting; } public static string? PresenceActionId(byte phase) => phase switch { NurseCare.PresenceGoing => NurseCare.GoingAction, NurseCare.PresenceWaiting => NurseCare.WaitingAction, NurseCare.PresenceCare => NurseCare.CareAction, _ => null, }; public static void ClearAll(School school) { if (school.NurseVisits.Count == 0) { return; } var ids = school.NurseVisits.Select(row => row.PersonId).ToArray(); school.NurseVisits.Clear(); foreach (var id in ids) { PresenceSystem.Enqueue(school, id); } } public static void Apply(School school, double gameMinutes) { if (school.Roster is null || school.Map is null || school.Catalog is null) { return; } if (!NurseCare.CanStart(school.Roster, school.Map)) { ClearAll(school); return; } EnqueueNeedingCare(school); DropRecovered(school); AdmitHead(school); ApplyTend(school, gameMinutes); TryStartCareActivity(school); } private static void EnqueueNeedingCare(School school) { var catalog = school.Catalog!; var now = school.Clock.Time; foreach (var person in school.Roster!.People.OrderBy(row => row.Id, StringComparer.Ordinal)) { if (IsVisiting(school, person.Id)) { continue; } if (DiseaseEffects.ConditionNeedingNurse(person, catalog, now) is null) { continue; } // Pupils and staff on campus can seek care; parents off-roster roles skip. if (!person.IsStudent && !person.IsStaff) { continue; } school.NurseVisits.Add(new NurseVisitTicket(person.Id, school.NextNurseVisitOrder++, admitted: false)); PresenceSystem.Enqueue(school, person.Id); } } private static void DropRecovered(School school) { var catalog = school.Catalog!; var now = school.Clock.Time; for (var i = school.NurseVisits.Count - 1; i >= 0; i--) { var ticket = school.NurseVisits[i]; var person = school.Roster!.People.FirstOrDefault(row => row.Id.Equals(ticket.PersonId, StringComparison.Ordinal)); if (person is not null && DiseaseEffects.ConditionNeedingNurse(person, catalog, now) is not null) { continue; } school.NurseVisits.RemoveAt(i); PresenceSystem.Enqueue(school, ticket.PersonId); } } private static void AdmitHead(School school) { if (school.NurseVisits.Any(row => row.Admitted)) { return; } var head = school.NurseVisits.OrderBy(row => row.Order).FirstOrDefault(); if (head is null) { return; } head.Admitted = true; PresenceSystem.Enqueue(school, head.PersonId); } private static void ApplyTend(School school, double gameMinutes) { if (gameMinutes <= 0) { return; } var nurseId = NurseCare.NurseId(school.Roster!); if (nurseId is null) { return; } var office = NurseCare.OfficeNodeId(school.Map!); if (office is null || !IsIdleOrCaringIn(school, nurseId, office)) { return; } var nurse = school.Roster!.People.First(row => row.Id.Equals(nurseId, StringComparison.Ordinal)); var medicine = LiveMedicine(school, nurseId) ?? NurseCare.MedicineOf(nurse); var scale = school.Catalog!.BehaviorRules?.NurseTendMedicineScale ?? 1f; var dayNumber = DateOnly.FromDateTime(school.Clock.Time).DayNumber; var changed = false; foreach (var ticket in school.NurseVisits.Where(row => row.Admitted).ToArray()) { if (!IsInNode(school, ticket.PersonId, office)) { continue; } var patient = school.Roster.People.FirstOrDefault(row => row.Id.Equals(ticket.PersonId, StringComparison.Ordinal)); if (patient is null) { continue; } if (DiseaseEffects.Tend( patient, school.PeopleSeed, dayNumber, gameMinutes, school.Catalog, medicine, school.Clock.Time, scale)) { changed = true; } } if (changed) { school.RosterTalkDirty = true; } } private static void TryStartCareActivity(School school) { var admitted = school.NurseVisits.FirstOrDefault(row => row.Admitted); if (admitted is null || school.Catalog is null) { return; } var office = NurseCare.OfficeNodeId(school.Map!); if (office is null || !IsInNode(school, admitted.PersonId, office) || !school.Catalog.Actions.TryGetValue(NurseCare.CareAction, out var action) || action.Abstract) { return; } var minutes = action.Minutes > 0f ? action.Minutes : 5f; school.World.Query( in PresenceQuery, (ref PersonIdentity identity, ref Presence presence, ref PersonActivity activity, ref Intent intent) => { if (!identity.Id.Equals(admitted.PersonId, StringComparison.Ordinal)) { return; } if (activity.IsActive || presence.Path.Length > 0 || presence.RemainingMinutes > 0) { return; } if (presence.NodeId is null || !presence.NodeId.Equals(office, StringComparison.Ordinal)) { return; } activity = new PersonActivity(NurseCare.CareAction, null, minutes); intent = Intent.None; }); } private static bool IsInNode(School school, string personId, string nodeId) { var found = false; school.World.Query( in PresenceQuery, (ref PersonIdentity identity, ref Presence presence, ref PersonActivity _, ref Intent _) => { if (identity.Id.Equals(personId, StringComparison.Ordinal) && presence.NodeId is not null && presence.NodeId.Equals(nodeId, StringComparison.Ordinal) && presence.Path.Length == 0 && presence.RemainingMinutes <= 0) { found = true; } }); return found; } private static bool IsIdleOrCaringIn(School school, string personId, string nodeId) { var found = false; school.World.Query( in PresenceQuery, (ref PersonIdentity identity, ref Presence presence, ref PersonActivity activity, ref Intent _) => { if (!identity.Id.Equals(personId, StringComparison.Ordinal)) { return; } if (presence.NodeId is null || !presence.NodeId.Equals(nodeId, StringComparison.Ordinal) || presence.Path.Length > 0 || presence.RemainingMinutes > 0) { return; } // Nurse on MedicalDuty stands in the office; any active non-care action blocks tend. if (activity.IsActive && !ActivitySystem.IsNurseCare(activity.ActionId)) { return; } found = true; }); return found; } private static float? LiveMedicine(School school, string personId) { float? found = null; var query = new QueryDescription().WithAll(); school.World.Query(in query, (ref PersonIdentity identity, ref PersonSkills skills) => { if (identity.Id.Equals(personId, StringComparison.Ordinal) && skills.Values.TryGetValue(NurseCare.MedicineSkill, out var level)) { found = level; } }); return found; } } /// One nurse-visit ticket. is FIFO; lower goes first. internal sealed class NurseVisitTicket(string personId, int order, bool admitted) { public string PersonId { get; } = personId; public int Order { get; } = order; public bool Admitted { get; set; } = admitted; }