using HSchool.Ai; using HSchool.Content; using HSchool.People; using HSchool.Schedule; namespace HSchool.Simulation; /// /// Picks and applies outfits from worn, bag, locker and home. Used at home instantly and after /// ChangeClothes in a locker room. /// public static class ApparelDresser { private static readonly string[] LayerOrder = [ ApparelLayers.Underwear, ApparelLayers.UpperUnderwear, ApparelLayers.Socks, ApparelLayers.Bottom, ApparelLayers.Top, ApparelLayers.OverTop, ApparelLayers.Outer, ApparelLayers.Shoes, ApparelLayers.Head, ApparelLayers.Accessory, ]; public static bool RedressPerson( School school, Person person, ApparelMode mode, bool includeHome, IReadOnlyList? todaySubjects = null, bool logChanges = true) { if (school.Catalog is null || school.Roster is null) { return false; } var catalog = school.Catalog; var rules = RuleFor(school, person); var age = person.AgeOn(school.Clock.Time); var context = new ApparelContext( person.Female, age, person.IsStudent, rules, mode, school.Weather.TemperatureC, catalog.BehaviorRules); var pool = Pool(person, includeHome); var target = SelectOutfit(catalog, context, pool, mode); if (target.Count == 0) { return false; } var changed = ApplyOutfit(school, person, pool, target, todaySubjects, logChanges); if (changed) { var updated = school.Roster!.People.First(row => row.Id.Equals(person.Id, StringComparison.Ordinal)); SyncInsulation(school, updated); } return changed; } public static ApparelIssue CurrentIssues(School school, Person person, ApparelMode mode) { if (school.Catalog is null) { return ApparelIssue.None; } var catalog = school.Catalog; var rules = RuleFor(school, person); var context = new ApparelContext( person.Female, person.AgeOn(school.Clock.Time), person.IsStudent, rules, mode, school.Weather.TemperatureC, catalog.BehaviorRules); var worn = WornEntries(catalog, person); return Appropriateness.Issues(catalog, context, worn); } public static ApparelMode ModeForLesson(string? subject) => subject is not null && subject.Equals("PhysicalEducation", StringComparison.Ordinal) ? ApparelMode.Pe : ApparelMode.Everyday; public static string ChangeActionId(bool female) => ApparelActions.For(female); public static string? ChangingRoomDef(bool female) => female ? "FemaleChangingRoom" : "MaleChangingRoom"; internal static DressRulePair RuleFor(School school, Person person) => person.IsStudent ? school.DressRules.Students : school.DressRules.Staff; private static List<(InventoryItem Item, int Index)> Pool(Person person, bool includeHome) { var pool = new List<(InventoryItem, int)>(); for (var i = 0; i < person.Items.Count; i++) { var item = person.Items[i]; if (item.Location.Equals(ItemLocations.Worn, StringComparison.Ordinal) || item.Location.Equals(ItemLocations.Bag, StringComparison.Ordinal) || item.Location.Equals(ItemLocations.Locker, StringComparison.Ordinal) || (includeHome && item.Location.Equals(ItemLocations.Home, StringComparison.Ordinal))) { pool.Add((item, i)); } } return pool; } private static List SelectOutfit( DefCatalog catalog, ApparelContext context, List<(InventoryItem Item, int Index)> pool, ApparelMode mode) { var chosen = new List(); var occupied = new HashSet(StringComparer.Ordinal); var candidates = pool .Where(entry => catalog.Things.TryGetValue(entry.Item.Def, out var def) && !def.Abstract && def.Layers.Count > 0) .Select(entry => (Item: entry.Item, Def: catalog.Things[entry.Item.Def])) .Where(entry => SexFits(entry.Def, context.Female) && AgeFits(entry.Def, context.Age)) .ToList(); foreach (var layer in LayerOrder) { if (occupied.Contains(layer)) { continue; } var pick = PickLayer(catalog, context, candidates, layer, mode, occupied); if (pick is null) { continue; } chosen.Add(pick.Value.Item); foreach (var taken in pick.Value.Def.Layers) { occupied.Add(taken); } } return chosen; } private static (InventoryItem Item, ThingDef Def)? PickLayer( DefCatalog catalog, ApparelContext context, List<(InventoryItem Item, ThingDef Def)> candidates, string layer, ApparelMode mode, HashSet occupied) { (InventoryItem Item, ThingDef Def)? best = null; foreach (var entry in candidates) { if (!entry.Def.Layers.Any(candidate => candidate.Equals(layer, StringComparison.Ordinal)) || !entry.Def.Layers.All(candidate => !occupied.Contains(candidate))) { continue; } if (mode == ApparelMode.Pe && !entry.Def.Pe) { continue; } if (mode == ApparelMode.Everyday && entry.Def.Pe) { continue; } if (!LayerAllowed(catalog, context, entry.Def, entry.Item.Color, mode)) { continue; } if (best is null || Score(entry.Def) > Score(best.Value.Def)) { best = entry; } } return best; } private static bool LayerAllowed( DefCatalog catalog, ApparelContext context, ThingDef def, string? color, ApparelMode mode) { if (mode == ApparelMode.Pe) { return def.Pe; } if (def.Layers.Any(layer => layer.Equals(ApparelLayers.Outer, StringComparison.Ordinal))) { var outerBelow = context.Behavior?.OuterBelowC ?? 10f; var heavyAbove = context.Behavior?.HeavyOuterAboveC ?? 15f; if (context.OutdoorTemperatureC >= outerBelow) { return false; } if (context.OutdoorTemperatureC > heavyAbove && def.DefName.Equals("FurCoat", StringComparison.Ordinal)) { return false; } } return Appropriateness.FormalityFits(context, def) && Appropriateness.SkirtLengthFits(context, def) && Appropriateness.ColorAllowed(catalog, context.Rules, def, color); } private static int Score(ThingDef def) { var score = def.Formality; if (def.Layers.Any(layer => layer.Equals(ApparelLayers.Outer, StringComparison.Ordinal))) { score += (int)def.Insulation; } return score; } private static bool ApplyOutfit( School school, Person person, List<(InventoryItem Item, int Index)> pool, List target, IReadOnlyList? todaySubjects, bool logChanges) { var targetKeys = target .Select(item => Key(item)) .ToHashSet(StringComparer.Ordinal); var hasLocker = person.LockerRoomId is not null; var changed = false; var items = person.Items.ToList(); for (var i = 0; i < items.Count; i++) { var item = items[i]; if (!item.Location.Equals(ItemLocations.Worn, StringComparison.Ordinal)) { continue; } if (targetKeys.Contains(Key(item))) { continue; } items[i] = item with { Location = StashLocation(hasLocker) }; if (logChanges) { school.AppendDayLog(new PersonLogEvent( person.Id, school.Clock.Time, PersonLogTypes.ApparelChanged, item.Def)); } changed = true; } foreach (var want in target) { var index = items.FindIndex(candidate => Key(candidate).Equals(Key(want), StringComparison.Ordinal)); if (index < 0) { continue; } if (items[index].Location.Equals(ItemLocations.Worn, StringComparison.Ordinal)) { continue; } items[index] = items[index] with { Location = ItemLocations.Worn }; if (logChanges) { school.AppendDayLog(new PersonLogEvent( person.Id, school.Clock.Time, PersonLogTypes.ApparelChanged, want.Def)); } changed = true; } if (todaySubjects is not null && school.Catalog is { } catalog) { changed |= RepackBag(school, person, items, todaySubjects); changed |= StagePeKit(catalog, person, items, todaySubjects); } if (changed) { ReplacePerson(person, items); } return changed; } private static bool StagePeKit( DefCatalog catalog, Person person, List items, IReadOnlyList todaySubjects) { if (!todaySubjects.Contains("PhysicalEducation", StringComparer.Ordinal)) { return false; } var hasLocker = person.LockerRoomId is not null; var changed = false; for (var i = 0; i < items.Count; i++) { var item = items[i]; if (!item.Location.Equals(ItemLocations.Home, StringComparison.Ordinal) || !catalog.Things.TryGetValue(item.Def, out var def) || !def.Pe) { continue; } items[i] = item with { Location = StashLocation(hasLocker) }; changed = true; } return changed; } private static bool RepackBag( School school, Person person, List items, IReadOnlyList todaySubjects) { if (school.Catalog is null) { return false; } var catalog = school.Catalog; var haveLocker = person.LockerRoomId is not null; var changed = false; var want = todaySubjects.ToHashSet(StringComparer.Ordinal); var capacity = CarryMass.Capacity(catalog, person.Skills); var held = 0f; foreach (var item in items.Where(row => row.Location.Equals(ItemLocations.Bag, StringComparison.Ordinal))) { if (catalog.Things.TryGetValue(item.Def, out var def)) { held += def.Mass; } } for (var i = 0; i < items.Count; i++) { var item = items[i]; if (item.Subject is null) { continue; } if (want.Contains(item.Subject)) { if (!item.Location.Equals(ItemLocations.Bag, StringComparison.Ordinal) && catalog.Things.TryGetValue(item.Def, out var def) && held + def.Mass <= capacity) { items[i] = item with { Location = ItemLocations.Bag }; held += def.Mass; changed = true; } continue; } if (item.Location.Equals(ItemLocations.Bag, StringComparison.Ordinal)) { items[i] = item with { Location = StashLocation(haveLocker) }; if (catalog.Things.TryGetValue(item.Def, out var def)) { held = Math.Max(0, held - def.Mass); } changed = true; } } return changed; } private static string StashLocation(bool hasLocker) => hasLocker ? ItemLocations.Locker : ItemLocations.Bag; private static string Key(InventoryItem item) => item.Subject is null ? item.Def : $"{item.Def}:{item.Subject}"; private static IEnumerable<(ThingDef Def, string? Color)> WornEntries(DefCatalog catalog, Person person) { foreach (var item in person.Items) { if (!item.Location.Equals(ItemLocations.Worn, StringComparison.Ordinal)) { continue; } if (catalog.Things.TryGetValue(item.Def, out var def)) { yield return (def, item.Color); } } } private static void SyncInsulation(School school, Person person) { var insulation = PersonInsulation.FromPerson(person, school.Catalog); var world = school.World; var query = new Arch.Core.QueryDescription().WithAll(); world.Query(in query, (ref PersonIdentity identity, ref PersonInsulation worn) => { if (identity.Id.Equals(person.Id, StringComparison.Ordinal)) { worn = insulation; } }); } private static void ReplacePerson(Person person, List items) { if (person.Items is IList list && !list.IsReadOnly) { list.Clear(); foreach (var item in items) { list.Add(item); } return; } throw new InvalidOperationException($"Cannot mutate items for {person.Id}."); } private static bool SexFits(ThingDef def, bool female) { if (def.Sex is null) { return true; } var want = female ? "female" : "male"; return def.Sex.Equals(want, StringComparison.OrdinalIgnoreCase); } private static bool AgeFits(ThingDef def, int age) => def.Age is not { } range || (age >= range.Min && age <= range.Max); /// Subjects this pupil needs in the bag today. public static IReadOnlyList TodaySubjects( School school, Person person, SchoolClass? schoolClass) { if (school.Catalog is null || school.Timetable is null || !person.IsStudent) { return []; } var day = SchoolDay.WeekdayIndex(school.Clock.Time); return Duty.LessonsToday(person, schoolClass, school.Timetable, day) .Select(lesson => lesson.Subject) .Distinct(StringComparer.Ordinal) .ToArray(); } }