232 lines
6.6 KiB
C#
232 lines
6.6 KiB
C#
using Arch.Core;
|
|
using HSchool.Ai;
|
|
using HSchool.Content;
|
|
using HSchool.People;
|
|
|
|
namespace HSchool.Simulation;
|
|
|
|
/// <summary>Condition captions live in the catalog. The client draws what it is told.</summary>
|
|
public static class ApparelCondition
|
|
{
|
|
public static string BandId(BehaviorDef? rules, float condition)
|
|
{
|
|
var bands = rules?.ApparelConditionBands is { Count: > 0 } listed
|
|
? listed
|
|
: BehaviorDef.DefaultConditionBands;
|
|
ApparelConditionBand? best = null;
|
|
foreach (var band in bands)
|
|
{
|
|
if (condition >= band.Min && (best is null || band.Min > best.Min))
|
|
{
|
|
best = band;
|
|
}
|
|
}
|
|
|
|
return best?.Id ?? "ApparelConditionRags";
|
|
}
|
|
|
|
public static string Label(DefCatalog catalog, string locale, float condition) =>
|
|
catalog.Text(locale, BandId(catalog.BehaviorRules, condition));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Worn apparel loses condition on campus from game time, then morning issues a fresh copy
|
|
/// of anything too ragged to leave home in. Bag and locker are left alone.
|
|
/// </summary>
|
|
internal static class ApparelWear
|
|
{
|
|
private static readonly QueryDescription Identities =
|
|
new QueryDescription().WithAll<PersonIdentity, Presence>();
|
|
|
|
public static bool Apply(School school, double gameMinutes, DateTime before)
|
|
{
|
|
var bandCrossed = Wear(school, gameMinutes);
|
|
if (!CrossedDayStart(before, school.Clock.Time))
|
|
{
|
|
return bandCrossed;
|
|
}
|
|
|
|
school.ResetDayLog();
|
|
if (school.Catalog is null || !SchoolDay.IsWorkday(school.Catalog, school.Clock.Time, school.SchoolWeekDays))
|
|
{
|
|
return bandCrossed;
|
|
}
|
|
|
|
return bandCrossed | ReplaceRags(school);
|
|
}
|
|
|
|
internal static bool CrossedDayStart(DateTime before, DateTime after)
|
|
{
|
|
if (after <= before)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var cursor = DateTime.SpecifyKind(before.Date, DateTimeKind.Utc).Add(SchoolDay.DayStart.ToTimeSpan());
|
|
if (before >= cursor)
|
|
{
|
|
cursor = cursor.AddDays(1);
|
|
}
|
|
|
|
return after >= cursor;
|
|
}
|
|
|
|
private static bool Wear(School school, double gameMinutes)
|
|
{
|
|
if (gameMinutes <= 0 || school.Roster is null || school.Catalog?.BehaviorRules is null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var rate = school.Catalog.BehaviorRules.ApparelWearPerHour;
|
|
if (rate <= 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var onCampus = OnCampusIds(school);
|
|
if (onCampus.Count == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var drop = (float)(rate * (gameMinutes / 60d));
|
|
var crossed = false;
|
|
foreach (var person in school.Roster.People)
|
|
{
|
|
if (!onCampus.Contains(person.Id))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
crossed |= WearPerson(school.Catalog, person, drop);
|
|
}
|
|
|
|
return crossed;
|
|
}
|
|
|
|
private static bool WearPerson(DefCatalog catalog, Person person, float drop)
|
|
{
|
|
var crossed = false;
|
|
for (var i = 0; i < person.Items.Count; i++)
|
|
{
|
|
var item = person.Items[i];
|
|
if (!item.Location.Equals(ItemLocations.Worn, StringComparison.Ordinal)
|
|
|| !IsApparel(catalog, item.Def))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var next = Math.Clamp(item.Condition - drop, 0f, 1f);
|
|
if (next == item.Condition)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (ApparelCondition.BandId(catalog.BehaviorRules, item.Condition)
|
|
!= ApparelCondition.BandId(catalog.BehaviorRules, next))
|
|
{
|
|
crossed = true;
|
|
}
|
|
|
|
SetItem(person, i, item with { Condition = next });
|
|
}
|
|
|
|
return crossed;
|
|
}
|
|
|
|
private static bool ReplaceRags(School school)
|
|
{
|
|
if (school.Roster is null || school.Catalog?.BehaviorRules is null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var onCampus = OnCampusIds(school);
|
|
var threshold = school.Catalog.BehaviorRules.ApparelReplaceBelow;
|
|
var replaced = false;
|
|
foreach (var person in school.Roster.People)
|
|
{
|
|
if (onCampus.Contains(person.Id))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
replaced |= ReplacePerson(school, person, threshold);
|
|
}
|
|
|
|
return replaced;
|
|
}
|
|
|
|
private static bool ReplacePerson(School school, Person person, float threshold)
|
|
{
|
|
var catalog = school.Catalog!;
|
|
var replaced = false;
|
|
for (var i = 0; i < person.Items.Count; i++)
|
|
{
|
|
var item = person.Items[i];
|
|
if (!item.Location.Equals(ItemLocations.Worn, StringComparison.Ordinal)
|
|
|| item.Condition >= threshold
|
|
|| !IsApparel(catalog, item.Def))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var color = NearestColor(catalog, item.Def, item.Color);
|
|
SetItem(person, i, item with { Condition = 1f, Color = color });
|
|
school.AppendDayLog(new PersonLogEvent(
|
|
person.Id,
|
|
school.Clock.Time,
|
|
PersonLogTypes.ApparelReplaced,
|
|
item.Def));
|
|
replaced = true;
|
|
}
|
|
|
|
return replaced;
|
|
}
|
|
|
|
private static HashSet<string> OnCampusIds(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 bool IsApparel(DefCatalog catalog, string defName) =>
|
|
catalog.Things.TryGetValue(defName, out var def) && def.Layers.Count > 0;
|
|
|
|
private static string? NearestColor(DefCatalog catalog, string defName, string? current)
|
|
{
|
|
if (!catalog.Things.TryGetValue(defName, out var def) || def.Colors.Count == 0)
|
|
{
|
|
return current;
|
|
}
|
|
|
|
if (current is not null
|
|
&& def.Colors.Contains(current, StringComparer.Ordinal))
|
|
{
|
|
return current;
|
|
}
|
|
|
|
return def.Colors[0];
|
|
}
|
|
|
|
private static void SetItem(Person person, int index, InventoryItem item)
|
|
{
|
|
if (person.Items is IList<InventoryItem> list && !list.IsReadOnly)
|
|
{
|
|
list[index] = item;
|
|
return;
|
|
}
|
|
|
|
throw new InvalidOperationException($"Cannot mutate items for {person.Id}.");
|
|
}
|
|
}
|