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();
}
}