Add opinions storage, morning drift, and Connections card tab.
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
using HSchool.Content;
|
||||
|
||||
namespace HSchool.People;
|
||||
|
||||
/// <summary>Seeds family opinions at generation or when an old save lacks them.</summary>
|
||||
public static class OpinionGenerator
|
||||
{
|
||||
public static Roster SeedFamily(DefCatalog catalog, Roster roster)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(catalog);
|
||||
ArgumentNullException.ThrowIfNull(roster);
|
||||
|
||||
var rules = catalog.BehaviorRules;
|
||||
if (rules is null)
|
||||
{
|
||||
return roster;
|
||||
}
|
||||
|
||||
var people = new Person[roster.People.Count];
|
||||
var changed = false;
|
||||
for (var i = 0; i < roster.People.Count; i++)
|
||||
{
|
||||
var person = roster.People[i];
|
||||
var updated = SeedPersonFamilyOpinions(roster, rules, roster.People, person);
|
||||
people[i] = updated;
|
||||
changed |= !ReferenceEquals(updated, person);
|
||||
}
|
||||
|
||||
return changed ? roster with { People = people } : roster;
|
||||
}
|
||||
|
||||
public static bool NeedsFamilyOpinions(Roster roster)
|
||||
{
|
||||
var people = roster.People.ToDictionary(person => person.Id, StringComparer.Ordinal);
|
||||
foreach (var family in roster.Families)
|
||||
{
|
||||
foreach (var childId in family.ChildIds)
|
||||
{
|
||||
if (!people.TryGetValue(childId, out var child))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var parentId in family.ParentIds)
|
||||
{
|
||||
if (!child.Opinions.ContainsKey(parentId))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static Person EnsureMutableOpinions(Person person)
|
||||
{
|
||||
if (person.Opinions is Dictionary<string, int>)
|
||||
{
|
||||
return person;
|
||||
}
|
||||
|
||||
return person with
|
||||
{
|
||||
Opinions = new Dictionary<string, int>(person.Opinions, StringComparer.Ordinal),
|
||||
};
|
||||
}
|
||||
|
||||
private static Person SeedPersonFamilyOpinions(
|
||||
Roster roster,
|
||||
BehaviorDef rules,
|
||||
IReadOnlyList<Person> rosterPeople,
|
||||
Person person)
|
||||
{
|
||||
var mutable = EnsureMutableOpinions(person);
|
||||
var changed = !ReferenceEquals(mutable, person);
|
||||
var peopleById = rosterPeople.ToDictionary(row => row.Id, StringComparer.Ordinal);
|
||||
foreach (var targetId in OpinionStore.FamilyMemberIds(roster, person))
|
||||
{
|
||||
if (!peopleById.TryGetValue(targetId, out var target))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var basis = OpinionStore.FamilyBasis(roster, rules, mutable, target);
|
||||
if (basis == 0 || mutable.Opinions.ContainsKey(targetId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
changed |= OpinionStore.Set(mutable, targetId, basis);
|
||||
}
|
||||
|
||||
return changed ? mutable : person;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using HSchool.Content;
|
||||
|
||||
namespace HSchool.People;
|
||||
|
||||
/// <summary>Threshold captions live in the catalog. The client draws what it is told.</summary>
|
||||
public static class OpinionLabels
|
||||
{
|
||||
public const int MinValue = -100;
|
||||
public const int MaxValue = 100;
|
||||
|
||||
public static string BandId(BehaviorDef? rules, int value)
|
||||
{
|
||||
var bands = rules?.OpinionBands is { Count: > 0 } listed
|
||||
? listed
|
||||
: BehaviorDef.DefaultOpinionBands;
|
||||
OpinionBand? best = null;
|
||||
foreach (var band in bands)
|
||||
{
|
||||
if (value >= band.Min && (best is null || band.Min > best.Min))
|
||||
{
|
||||
best = band;
|
||||
}
|
||||
}
|
||||
|
||||
return best?.Id ?? "OpinionAcquaintance";
|
||||
}
|
||||
|
||||
public static string Label(DefCatalog catalog, string locale, int value) =>
|
||||
catalog.Text(locale, BandId(catalog.BehaviorRules, value));
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
using HSchool.Content;
|
||||
|
||||
namespace HSchool.People;
|
||||
|
||||
/// <summary>Sparse A→B opinions on a person. Zero is not stored.</summary>
|
||||
public static class OpinionStore
|
||||
{
|
||||
public static int? Get(Person person, string targetId) =>
|
||||
person.Opinions.TryGetValue(targetId, out var value) ? value : null;
|
||||
|
||||
public static bool Set(Person person, string targetId, int value)
|
||||
{
|
||||
value = Math.Clamp(value, OpinionLabels.MinValue, OpinionLabels.MaxValue);
|
||||
if (value == 0)
|
||||
{
|
||||
return Remove(person, targetId);
|
||||
}
|
||||
|
||||
if (person.Opinions is not IDictionary<string, int> dict)
|
||||
{
|
||||
throw new InvalidOperationException($"Cannot mutate opinions for {person.Id}.");
|
||||
}
|
||||
|
||||
if (dict.IsReadOnly)
|
||||
{
|
||||
throw new InvalidOperationException($"Cannot mutate opinions for {person.Id}.");
|
||||
}
|
||||
|
||||
if (dict.TryGetValue(targetId, out var existing) && existing == value)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
dict[targetId] = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool Remove(Person person, string targetId)
|
||||
{
|
||||
if (person.Opinions is not IDictionary<string, int> dict)
|
||||
{
|
||||
throw new InvalidOperationException($"Cannot mutate opinions for {person.Id}.");
|
||||
}
|
||||
|
||||
if (dict.IsReadOnly)
|
||||
{
|
||||
throw new InvalidOperationException($"Cannot mutate opinions for {person.Id}.");
|
||||
}
|
||||
|
||||
return dict.Remove(targetId);
|
||||
}
|
||||
|
||||
public static int FamilyBasis(Roster roster, BehaviorDef rules, Person from, Person to)
|
||||
{
|
||||
var family = roster.Families.FirstOrDefault(candidate =>
|
||||
candidate.Id.Equals(from.FamilyId, StringComparison.Ordinal)
|
||||
&& candidate.Id.Equals(to.FamilyId, StringComparison.Ordinal));
|
||||
if (family is null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var fromParent = InFamily(family.ParentIds, from.Id);
|
||||
var toParent = InFamily(family.ParentIds, to.Id);
|
||||
var fromChild = InFamily(family.ChildIds, from.Id);
|
||||
var toChild = InFamily(family.ChildIds, to.Id);
|
||||
|
||||
if (fromParent && toChild)
|
||||
{
|
||||
return rules.OpinionParentToChildStart;
|
||||
}
|
||||
|
||||
if (fromChild && toParent)
|
||||
{
|
||||
return rules.OpinionChildToParentStart;
|
||||
}
|
||||
|
||||
if (fromChild && toChild && !from.Id.Equals(to.Id, StringComparison.Ordinal))
|
||||
{
|
||||
return rules.OpinionSiblingStart;
|
||||
}
|
||||
|
||||
if (fromParent && toParent && !from.Id.Equals(to.Id, StringComparison.Ordinal))
|
||||
{
|
||||
return rules.OpinionPartnerStart;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static int MoveToward(int current, int target, int step)
|
||||
{
|
||||
if (current == target)
|
||||
{
|
||||
return current;
|
||||
}
|
||||
|
||||
if (current < target)
|
||||
{
|
||||
return Math.Min(current + step, target);
|
||||
}
|
||||
|
||||
return Math.Max(current - step, target);
|
||||
}
|
||||
|
||||
public static HashSet<string> FamilyMemberIds(Roster roster, Person person)
|
||||
{
|
||||
var family = roster.Families.FirstOrDefault(candidate =>
|
||||
candidate.Id.Equals(person.FamilyId, StringComparison.Ordinal));
|
||||
if (family is null)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var ids = new HashSet<string>(StringComparer.Ordinal);
|
||||
foreach (var id in family.ParentIds)
|
||||
{
|
||||
if (!id.Equals(person.Id, StringComparison.Ordinal))
|
||||
{
|
||||
ids.Add(id);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var id in family.ChildIds)
|
||||
{
|
||||
if (!id.Equals(person.Id, StringComparison.Ordinal))
|
||||
{
|
||||
ids.Add(id);
|
||||
}
|
||||
}
|
||||
|
||||
return ids;
|
||||
}
|
||||
|
||||
private static bool InFamily(IReadOnlyList<string> ids, string id)
|
||||
{
|
||||
foreach (var candidate in ids)
|
||||
{
|
||||
if (candidate.Equals(id, StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -52,6 +52,9 @@ public sealed record Person
|
||||
/// <summary>Changing-room node when this pupil won a locker slot. Null for staff and the rest.</summary>
|
||||
public string? LockerRoomId { get; init; }
|
||||
|
||||
/// <summary>What this person thinks of others, A→B. Sparse; zero is not stored.</summary>
|
||||
public IReadOnlyDictionary<string, int> Opinions { get; init; } = new Dictionary<string, int>(StringComparer.Ordinal);
|
||||
|
||||
public int AgeOn(DateTime asOf) => SchoolYears.AgeYears(BirthDate, asOf);
|
||||
}
|
||||
|
||||
|
||||
@@ -44,7 +44,8 @@ public static class RosterGenerator
|
||||
}
|
||||
|
||||
var classes = FillClasses(demand.Classes, people);
|
||||
return LockerAssigner.Apply(catalog, map, new Roster(people, families, classes));
|
||||
var roster = LockerAssigner.Apply(catalog, map, new Roster(people, families, classes));
|
||||
return OpinionGenerator.SeedFamily(catalog, roster);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user