Implement phase 35 dress appropriateness and school rules.
Adds outfit evaluation, morning home dressing, locker-room ChangeClothes with day-log events, PE/weather AI goals, dress-rules API, and tests.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
using Arch.Core;
|
||||
using HSchool.Ai;
|
||||
using HSchool.Content;
|
||||
using HSchool.People;
|
||||
|
||||
namespace HSchool.Simulation;
|
||||
|
||||
@@ -25,6 +26,21 @@ internal static class ActivitySystem
|
||||
return false;
|
||||
}
|
||||
|
||||
Person? person = null;
|
||||
if (IsChangeClothes(action.DefName))
|
||||
{
|
||||
if (school.Roster is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
person = school.Roster.People.FirstOrDefault(row => row.Id.Equals(personId, StringComparison.Ordinal));
|
||||
if (person is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
var started = false;
|
||||
var world = school.World;
|
||||
world.Query(
|
||||
@@ -46,6 +62,11 @@ internal static class ActivitySystem
|
||||
return;
|
||||
}
|
||||
|
||||
if (person is not null && !SexFitsChange(action, person))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var location = school.Map.NodeDef(presence.NodeId);
|
||||
if (location is null || !location.Equals(action.Room, StringComparison.Ordinal))
|
||||
{
|
||||
@@ -117,6 +138,11 @@ internal static class ActivitySystem
|
||||
needs.Values[action.Need] = ActionStepper.ApplyNeedGain(current, action, need);
|
||||
}
|
||||
|
||||
if (IsChangeClothes(action.DefName))
|
||||
{
|
||||
FinishChangeClothes(school, identity.Id);
|
||||
}
|
||||
|
||||
activity = PersonActivity.Idle;
|
||||
completed.Add(identity.Id);
|
||||
});
|
||||
@@ -124,6 +150,26 @@ internal static class ActivitySystem
|
||||
return completed;
|
||||
}
|
||||
|
||||
private static void FinishChangeClothes(School school, string personId)
|
||||
{
|
||||
var person = school.Roster?.People.FirstOrDefault(row => row.Id.Equals(personId, StringComparison.Ordinal));
|
||||
if (person is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var schoolClass = person.ClassId is { } classId
|
||||
? school.Roster!.Classes.FirstOrDefault(row => row.Id.Equals(classId, StringComparison.Ordinal))
|
||||
: null;
|
||||
var mode = ApparelPresence.ModeAt(school, person, schoolClass);
|
||||
var subjects = ApparelDresser.TodaySubjects(school, person, schoolClass);
|
||||
ApparelDresser.RedressPerson(school, person, mode, includeHome: false, todaySubjects: subjects);
|
||||
}
|
||||
|
||||
private static bool IsChangeClothes(string actionId) =>
|
||||
actionId.Equals(ApparelActions.ChangeMale, StringComparison.Ordinal)
|
||||
|| actionId.Equals(ApparelActions.ChangeFemale, StringComparison.Ordinal);
|
||||
|
||||
private static int Occupied(School school, string nodeId, string thing)
|
||||
{
|
||||
var occupied = 0;
|
||||
@@ -168,4 +214,19 @@ internal static class ActivitySystem
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool SexFitsChange(ActionDef action, Person person)
|
||||
{
|
||||
if (action.DefName.Equals(ApparelActions.ChangeMale, StringComparison.Ordinal))
|
||||
{
|
||||
return !person.Female;
|
||||
}
|
||||
|
||||
if (action.DefName.Equals(ApparelActions.ChangeFemale, StringComparison.Ordinal))
|
||||
{
|
||||
return person.Female;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,499 @@
|
||||
using HSchool.Ai;
|
||||
using HSchool.Content;
|
||||
using HSchool.People;
|
||||
using HSchool.Schedule;
|
||||
|
||||
namespace HSchool.Simulation;
|
||||
|
||||
/// <summary>
|
||||
/// Picks and applies outfits from worn, bag, locker and home. Used at home instantly and after
|
||||
/// <c>ChangeClothes</c> in a locker room.
|
||||
/// </summary>
|
||||
public static class ApparelDresser
|
||||
{
|
||||
private static readonly string[] LayerOrder =
|
||||
[
|
||||
ApparelLayers.Underwear,
|
||||
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<string>? 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<InventoryItem> SelectOutfit(
|
||||
DefCatalog catalog,
|
||||
ApparelContext context,
|
||||
List<(InventoryItem Item, int Index)> pool,
|
||||
ApparelMode mode)
|
||||
{
|
||||
var chosen = new List<InventoryItem>();
|
||||
var occupied = new HashSet<string>(StringComparer.Ordinal);
|
||||
var candidates = pool
|
||||
.Where(entry => catalog.Things.TryGetValue(entry.Item.Def, out var def) && 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<string> 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<InventoryItem> target,
|
||||
IReadOnlyList<string>? 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<InventoryItem> items,
|
||||
IReadOnlyList<string> 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<InventoryItem> items,
|
||||
IReadOnlyList<string> 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<PersonIdentity, PersonInsulation>();
|
||||
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<InventoryItem> items)
|
||||
{
|
||||
if (person.Items is IList<InventoryItem> 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);
|
||||
|
||||
/// <summary>Subjects this pupil needs in the bag today.</summary>
|
||||
public static IReadOnlyList<string> 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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
using HSchool.Ai;
|
||||
using HSchool.Content;
|
||||
using HSchool.People;
|
||||
using HSchool.Schedule;
|
||||
|
||||
namespace HSchool.Simulation;
|
||||
|
||||
/// <summary>Builds apparel goal inputs for the decision planner.</summary>
|
||||
internal static class ApparelPresence
|
||||
{
|
||||
public static ApparelActor Build(School school, Person person, SchoolClass? schoolClass)
|
||||
{
|
||||
if (school.Catalog is null || !person.IsStudent && !person.IsStaff)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
var mode = ModeAt(school, person, schoolClass);
|
||||
var issues = ApparelDresser.CurrentIssues(school, person, mode);
|
||||
var kind = ApparelGoals.Classify(issues);
|
||||
var room = ChangingRoomNode(school, person);
|
||||
return new ApparelActor(person.Female, issues, kind, room);
|
||||
}
|
||||
|
||||
public static void EnqueueOnWeatherChange(School school, OutdoorWeather before, OutdoorWeather after)
|
||||
{
|
||||
if (before.Tenths == after.Tenths && before.Precipitation == after.Precipitation)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var person in school.Roster!.People.OrderBy(row => row.Id, StringComparer.Ordinal))
|
||||
{
|
||||
school.DecisionQueue.Enqueue(person.Id);
|
||||
}
|
||||
}
|
||||
|
||||
internal static ApparelMode ModeAt(School school, Person person, SchoolClass? schoolClass)
|
||||
{
|
||||
if (school.Catalog is null || school.Timetable is null)
|
||||
{
|
||||
return ApparelMode.Everyday;
|
||||
}
|
||||
|
||||
var slot = SchoolDay.At(school.Catalog, school.Clock.Time, school.SchoolWeekDays);
|
||||
var day = SchoolDay.WeekdayIndex(school.Clock.Time);
|
||||
var lessons = Duty.LessonsToday(person, schoolClass, school.Timetable, day);
|
||||
if (lessons.Count == 0)
|
||||
{
|
||||
return ApparelMode.Everyday;
|
||||
}
|
||||
|
||||
if (slot.Kind == DaySlotKind.Lesson)
|
||||
{
|
||||
var current = lessons.FirstOrDefault(lesson => lesson.Period == slot.Index);
|
||||
return ApparelDresser.ModeForLesson(current?.Subject);
|
||||
}
|
||||
|
||||
var next = lessons.Where(lesson => lesson.Period > slot.Index).OrderBy(lesson => lesson.Period).FirstOrDefault();
|
||||
if (next is not null)
|
||||
{
|
||||
return ApparelDresser.ModeForLesson(next.Subject);
|
||||
}
|
||||
|
||||
var previous = lessons.Where(lesson => lesson.Period < slot.Index).OrderByDescending(lesson => lesson.Period).FirstOrDefault();
|
||||
if (previous is not null && ApparelDresser.ModeForLesson(previous.Subject) == ApparelMode.Pe)
|
||||
{
|
||||
return ApparelMode.Everyday;
|
||||
}
|
||||
|
||||
return ApparelMode.Everyday;
|
||||
}
|
||||
|
||||
internal static string? ChangingRoomNode(School school, Person person)
|
||||
{
|
||||
if (person.LockerRoomId is { } assigned)
|
||||
{
|
||||
return assigned;
|
||||
}
|
||||
|
||||
if (school.Map is null || school.Catalog is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var want = ApparelDresser.ChangingRoomDef(person.Female);
|
||||
if (want is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return school.Map.Rooms
|
||||
.FirstOrDefault(room => room.Def.Equals(want, StringComparison.Ordinal))
|
||||
?.Id;
|
||||
}
|
||||
}
|
||||
@@ -47,12 +47,15 @@ internal static class ApparelWear
|
||||
}
|
||||
|
||||
school.ResetDayLog();
|
||||
if (school.Catalog is null || !SchoolDay.IsWorkday(school.Catalog, school.Clock.Time, school.SchoolWeekDays))
|
||||
if (school.Catalog is null)
|
||||
{
|
||||
return bandCrossed;
|
||||
}
|
||||
|
||||
return bandCrossed | ReplaceRags(school);
|
||||
var morningChanged = MorningDress.Apply(school);
|
||||
var ragsReplaced = SchoolDay.IsWorkday(school.Catalog, school.Clock.Time, school.SchoolWeekDays)
|
||||
&& ReplaceRags(school);
|
||||
return bandCrossed | morningChanged | ragsReplaced;
|
||||
}
|
||||
|
||||
internal static bool CrossedDayStart(DateTime before, DateTime after)
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
using HSchool.Ai;
|
||||
using HSchool.Content;
|
||||
using HSchool.People;
|
||||
using HSchool.Schedule;
|
||||
|
||||
namespace HSchool.Simulation;
|
||||
|
||||
/// <summary>Applies pending dress rules and home morning outfits at six in the morning.</summary>
|
||||
internal static class MorningDress
|
||||
{
|
||||
private static readonly Arch.Core.QueryDescription Identities =
|
||||
new Arch.Core.QueryDescription().WithAll<PersonIdentity, Presence>();
|
||||
|
||||
public static bool Apply(School school)
|
||||
{
|
||||
if (school.Catalog is null || school.Roster is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var isWorkday = SchoolDay.IsWorkday(school.Catalog, school.Clock.Time, school.SchoolWeekDays);
|
||||
var changed = false;
|
||||
if (isWorkday && school.DressRules.HasPending)
|
||||
{
|
||||
school.DressRules = school.DressRules.ApplyPending();
|
||||
changed = true;
|
||||
}
|
||||
|
||||
var offCampus = OffCampusIds(school);
|
||||
foreach (var person in school.Roster.People)
|
||||
{
|
||||
if (!offCampus.Contains(person.Id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var schoolClass = ClassOf(school, person);
|
||||
var subjects = isWorkday
|
||||
? ApparelDresser.TodaySubjects(school, person, schoolClass)
|
||||
: [];
|
||||
changed |= ApparelDresser.RedressPerson(
|
||||
school,
|
||||
person,
|
||||
ApparelMode.Everyday,
|
||||
includeHome: true,
|
||||
todaySubjects: subjects,
|
||||
logChanges: false);
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
private static HashSet<string> OffCampusIds(School school)
|
||||
{
|
||||
var ids = new HashSet<string>(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 SchoolClass? ClassOf(School school, Person person)
|
||||
{
|
||||
if (person.ClassId is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return school.Roster?.Classes.FirstOrDefault(row => row.Id.Equals(person.ClassId, StringComparison.Ordinal));
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,14 @@ public sealed record PersonLogEvent(string PersonId, DateTime Time, string Type,
|
||||
return string.Format(CultureInfo.InvariantCulture, catalog.Text(locale, "ApparelReplaced"), name);
|
||||
}
|
||||
|
||||
if (Type.Equals(PersonLogTypes.ApparelChanged, StringComparison.Ordinal))
|
||||
{
|
||||
var name = catalog.Things.TryGetValue(ThingDef, out var def)
|
||||
? catalog.Label(locale, def)
|
||||
: ThingDef;
|
||||
return string.Format(CultureInfo.InvariantCulture, catalog.Text(locale, "ApparelChanged"), name);
|
||||
}
|
||||
|
||||
if (Type.Equals(PersonLogTypes.ActionStarted, StringComparison.Ordinal)
|
||||
|| Type.Equals(PersonLogTypes.ActionEnded, StringComparison.Ordinal))
|
||||
{
|
||||
|
||||
@@ -378,6 +378,7 @@ internal static class PresenceSystem
|
||||
var frame = school.Catalog!.DayFrame;
|
||||
var lunchOpen = frame is not null
|
||||
&& SchoolDay.IsLunchWindow(frame, slot, ClassOf(school, person)?.Year);
|
||||
var apparel = ApparelPresence.Build(school, person, ClassOf(school, person));
|
||||
var state = new ActorState(
|
||||
presence.NodeId,
|
||||
presence.DestinationId,
|
||||
@@ -390,7 +391,8 @@ internal static class PresenceSystem
|
||||
duty,
|
||||
needs.Values,
|
||||
intent,
|
||||
lunchOpen);
|
||||
lunchOpen,
|
||||
apparel);
|
||||
var decision = DecisionPlanner.Decide(
|
||||
school.Catalog!,
|
||||
school.Map!,
|
||||
|
||||
@@ -85,6 +85,9 @@ public sealed class School : IDisposable
|
||||
/// <summary>Street temperature and precipitation last committed for the clock and warmth.</summary>
|
||||
public OutdoorWeather Weather { get; private set; } = OutdoorWeather.None;
|
||||
|
||||
/// <summary>Student and staff dress rules. Pending pair applies on the next work morning.</summary>
|
||||
public SchoolDressRules DressRules { get; set; } = new();
|
||||
|
||||
/// <summary>Skill everyone generated for this school speaks natively.</summary>
|
||||
public string? NativeLanguage { get; private set; }
|
||||
|
||||
@@ -339,7 +342,12 @@ public sealed class School : IDisposable
|
||||
var next = EvaluateWeather();
|
||||
if (force || next.Tenths != Weather.Tenths || next.Precipitation != Weather.Precipitation)
|
||||
{
|
||||
var before = Weather;
|
||||
Weather = next;
|
||||
if (Roster is not null && Catalog is not null)
|
||||
{
|
||||
ApparelPresence.EnqueueOnWeatherChange(this, before, next);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user