using HSchool.Content; namespace HSchool.Ai; /// Why someone should walk to their locker room. public enum ApparelGoalKind { None, Pe, Everyday, Weather, } /// /// Dress-code and weather goals beside duty. Triggered on bells and arrival, not every tick. /// public static class ApparelGoals { public static Intent? Goal( DefCatalog catalog, MapLayout map, WalkGraph walks, ApparelActor apparel, ActorState state, OccupiedCount occupied) { ArgumentNullException.ThrowIfNull(catalog); ArgumentNullException.ThrowIfNull(map); ArgumentNullException.ThrowIfNull(walks); if (apparel.Issues == ApparelIssue.None || apparel.LockerRoomNode is null) { return null; } var weight = catalog.BehaviorRules?.ApparelGoalWeight ?? 5.5f; var actionId = ApparelActions.For(apparel.Female); if (!catalog.Actions.TryGetValue(actionId, out var action) || action.Abstract) { return null; } if (state.BoundToLesson && (apparel.Kind == ApparelGoalKind.Pe || apparel.Kind == ApparelGoalKind.Everyday)) { return null; } var room = RoomFor(catalog, map, walks, state, occupied, action, apparel.LockerRoomNode); if (room is null) { return null; } var id = apparel.Kind switch { ApparelGoalKind.Pe => "pe", ApparelGoalKind.Everyday => "everyday", ApparelGoalKind.Weather => "weather", _ => null, }; if (id is null) { return null; } return new Intent(GoalKind.Apparel, id, weight, actionId); } public static ApparelGoalKind Classify(ApparelIssue issues) { if (issues.HasFlag(ApparelIssue.PeRequired)) { return ApparelGoalKind.Pe; } if (issues.HasFlag(ApparelIssue.PeForbidden)) { return ApparelGoalKind.Everyday; } if (issues.HasFlag(ApparelIssue.OuterRequired) || issues.HasFlag(ApparelIssue.OuterForbidden)) { return ApparelGoalKind.Weather; } if (issues.HasFlag(ApparelIssue.Formality) || issues.HasFlag(ApparelIssue.Color) || issues.HasFlag(ApparelIssue.SkirtLength)) { return ApparelGoalKind.Everyday; } return ApparelGoalKind.None; } private static string? RoomFor( DefCatalog catalog, MapLayout map, WalkGraph walks, ActorState state, OccupiedCount occupied, ActionDef action, string preferredNode) { foreach (var room in map.Rooms) { if (!room.Id.Equals(preferredNode, StringComparison.Ordinal)) { continue; } if (!action.Room!.Equals(room.Def, StringComparison.Ordinal)) { continue; } if (!HasSlot(catalog, map, occupied, room.Id, action.Thing)) { continue; } return room.Id; } if (state.NodeId is not null && state.NodeId.Equals(preferredNode, StringComparison.Ordinal)) { return preferredNode; } var from = state.NodeId ?? walks.TerritoryId; var cost = walks.Minutes(from, preferredNode); return float.IsInfinity(cost) ? null : preferredNode; } private static bool HasSlot(DefCatalog catalog, MapLayout map, OccupiedCount occupied, string nodeId, string? thing) { if (string.IsNullOrWhiteSpace(thing)) { return true; } var available = RoomOccupancy.ThingCount(catalog, map, nodeId, thing); return ActionStepper.CanOccupy(available, occupied(nodeId, thing)); } } /// Apparel inputs the generic planner does not carry. public readonly record struct ApparelActor( bool Female, ApparelIssue Issues, ApparelGoalKind Kind, string? LockerRoomNode);