using Arch.Core; using HSchool.Ai; using HSchool.Content; using HSchool.People; namespace HSchool.Simulation; /// Condition captions live in the catalog. The client draws what it is told. public static class ApparelCondition { public static string BandId(BehaviorDef? rules, float condition) { var bands = rules?.ApparelConditionBands is { Count: > 0 } listed ? listed : BehaviorDef.DefaultConditionBands; ApparelConditionBand? best = null; foreach (var band in bands) { if (condition >= band.Min && (best is null || band.Min > best.Min)) { best = band; } } return best?.Id ?? "ApparelConditionRags"; } public static string Label(DefCatalog catalog, string locale, float condition) => catalog.Text(locale, BandId(catalog.BehaviorRules, condition)); } /// /// Worn apparel loses condition on campus from game time, then morning issues a fresh copy /// of anything too ragged to leave home in. Bag and locker are left alone. /// internal static class ApparelWear { private static readonly QueryDescription Identities = new QueryDescription().WithAll(); public static bool Apply(School school, double gameMinutes, DateTime before) { var bandCrossed = Wear(school, gameMinutes); if (!CrossedDayStart(before, school.Clock.Time)) { return bandCrossed; } school.ResetDayLog(); if (school.Catalog is null) { return bandCrossed; } var morningChanged = MorningDress.Apply(school); var opinionsChanged = MorningOpinions.Apply(school); var ragsReplaced = SchoolDay.IsWorkday(school.Catalog, school.Clock.Time, school.SchoolWeekDays) && ReplaceRags(school); return bandCrossed | morningChanged | opinionsChanged | ragsReplaced; } internal static bool CrossedDayStart(DateTime before, DateTime after) { if (after <= before) { return false; } var cursor = DateTime.SpecifyKind(before.Date, DateTimeKind.Utc).Add(SchoolDay.DayStart.ToTimeSpan()); if (before >= cursor) { cursor = cursor.AddDays(1); } return after >= cursor; } private static bool Wear(School school, double gameMinutes) { if (gameMinutes <= 0 || school.Roster is null || school.Catalog?.BehaviorRules is null) { return false; } var rate = school.Catalog.BehaviorRules.ApparelWearPerHour; if (rate <= 0) { return false; } var onCampus = OnCampusIds(school); if (onCampus.Count == 0) { return false; } var drop = (float)(rate * (gameMinutes / 60d)); var crossed = false; foreach (var person in school.Roster.People) { if (!onCampus.Contains(person.Id)) { continue; } crossed |= WearPerson(school.Catalog, person, drop); } return crossed; } private static bool WearPerson(DefCatalog catalog, Person person, float drop) { var crossed = false; for (var i = 0; i < person.Items.Count; i++) { var item = person.Items[i]; if (!item.Location.Equals(ItemLocations.Worn, StringComparison.Ordinal) || !IsApparel(catalog, item.Def)) { continue; } var next = Math.Clamp(item.Condition - drop, 0f, 1f); if (next == item.Condition) { continue; } if (ApparelCondition.BandId(catalog.BehaviorRules, item.Condition) != ApparelCondition.BandId(catalog.BehaviorRules, next)) { crossed = true; } SetItem(person, i, item with { Condition = next }); } return crossed; } private static bool ReplaceRags(School school) { if (school.Roster is null || school.Catalog?.BehaviorRules is null) { return false; } var onCampus = OnCampusIds(school); var threshold = school.Catalog.BehaviorRules.ApparelReplaceBelow; var replaced = false; foreach (var person in school.Roster.People) { if (onCampus.Contains(person.Id)) { continue; } replaced |= ReplacePerson(school, person, threshold); } return replaced; } private static bool ReplacePerson(School school, Person person, float threshold) { var catalog = school.Catalog!; var replaced = false; for (var i = 0; i < person.Items.Count; i++) { var item = person.Items[i]; if (!item.Location.Equals(ItemLocations.Worn, StringComparison.Ordinal) || item.Condition >= threshold || !IsApparel(catalog, item.Def)) { continue; } var color = NearestColor(catalog, item.Def, item.Color); SetItem(person, i, item with { Condition = 1f, Color = color }); school.AppendDayLog(new PersonLogEvent( person.Id, school.Clock.Time, PersonLogTypes.ApparelReplaced, item.Def)); replaced = true; } return replaced; } private static HashSet OnCampusIds(School school) { var ids = new HashSet(StringComparer.Ordinal); var world = school.World; world.Query(in Identities, (ref PersonIdentity identity, ref Presence presence) => { if (presence.IsOnCampus) { ids.Add(identity.Id); } }); return ids; } private static bool IsApparel(DefCatalog catalog, string defName) => catalog.Things.TryGetValue(defName, out var def) && !def.Abstract && def.Layers.Count > 0; private static string? NearestColor(DefCatalog catalog, string defName, string? current) { if (!catalog.Things.TryGetValue(defName, out var def) || def.Colors.Count == 0) { return current; } if (current is not null && def.Colors.Contains(current, StringComparer.Ordinal)) { return current; } return def.Colors[0]; } private static void SetItem(Person person, int index, InventoryItem item) { if (person.Items is IList list && !list.IsReadOnly) { list[index] = item; return; } throw new InvalidOperationException($"Cannot mutate items for {person.Id}."); } }