Adds outfit evaluation, morning home dressing, locker-room ChangeClothes with day-log events, PE/weather AI goals, dress-rules API, and tests.
153 lines
4.1 KiB
C#
153 lines
4.1 KiB
C#
using HSchool.Content;
|
|
|
|
namespace HSchool.Ai;
|
|
|
|
/// <summary>Why someone should walk to their locker room.</summary>
|
|
public enum ApparelGoalKind
|
|
{
|
|
None,
|
|
Pe,
|
|
Everyday,
|
|
Weather,
|
|
}
|
|
|
|
/// <summary>
|
|
/// Dress-code and weather goals beside duty. Triggered on bells and arrival, not every tick.
|
|
/// </summary>
|
|
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));
|
|
}
|
|
}
|
|
|
|
/// <summary>Apparel inputs the generic planner does not carry.</summary>
|
|
public readonly record struct ApparelActor(
|
|
bool Female,
|
|
ApparelIssue Issues,
|
|
ApparelGoalKind Kind,
|
|
string? LockerRoomNode);
|