Merge branch 'phase/47-romance-pack'
# Conflicts: # docs/phases/README.md # src/HSchool.Content/PeopleDefs.cs # src/HSchool.People/Seed.cs
This commit is contained in:
@@ -0,0 +1,494 @@
|
||||
using HSchool.Content;
|
||||
|
||||
namespace HSchool.People;
|
||||
|
||||
/// <summary>One overlay change the simulation can log. Type is a person-log id, not a pack name.</summary>
|
||||
public sealed record AffinityEvent(string PersonId, string Type, string? OtherId);
|
||||
|
||||
/// <summary>
|
||||
/// Crush and pair overlay driven by <see cref="AffinityRulesDef"/>. No-ops when the catalog has
|
||||
/// no rules or no orientations — packs are not named here.
|
||||
/// </summary>
|
||||
public static class Affinity
|
||||
{
|
||||
public const string Crush = "affinity-crush";
|
||||
public const string Pair = "affinity-pair";
|
||||
public const string PairBreak = "affinity-pair-break";
|
||||
public const string Tease = "affinity-tease";
|
||||
public const string Rebuff = "affinity-rebuff";
|
||||
|
||||
public static IReadOnlyList<AffinityEvent> Refresh(DefCatalog catalog, Roster roster, DateTime asOf)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(catalog);
|
||||
ArgumentNullException.ThrowIfNull(roster);
|
||||
|
||||
var rules = catalog.AffinityRules;
|
||||
if (rules is null || catalog.Orientations.Count == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var people = roster.People.ToDictionary(person => person.Id, StringComparer.Ordinal);
|
||||
var years = roster.Classes.ToDictionary(schoolClass => schoolClass.Id, StringComparer.Ordinal);
|
||||
var events = new List<AffinityEvent>();
|
||||
var ordered = roster.People.OrderBy(person => person.Id, StringComparer.Ordinal).ToArray();
|
||||
|
||||
foreach (var person in ordered)
|
||||
{
|
||||
DropInvalid(catalog, rules, roster, people, years, person, asOf, events);
|
||||
}
|
||||
|
||||
foreach (var person in ordered)
|
||||
{
|
||||
GainCrushes(catalog, rules, roster, people, years, person, asOf, events);
|
||||
}
|
||||
|
||||
foreach (var person in ordered)
|
||||
{
|
||||
TryFormPair(catalog, rules, roster, people, years, person, asOf, events);
|
||||
}
|
||||
|
||||
return events;
|
||||
}
|
||||
|
||||
public static IReadOnlyList<AffinityEvent> RebuffTalk(
|
||||
DefCatalog catalog,
|
||||
Roster roster,
|
||||
IReadOnlyList<string> talkedIds,
|
||||
DateTime asOf)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(catalog);
|
||||
ArgumentNullException.ThrowIfNull(roster);
|
||||
ArgumentNullException.ThrowIfNull(talkedIds);
|
||||
|
||||
var rules = catalog.AffinityRules;
|
||||
if (rules is null || catalog.Orientations.Count == 0 || talkedIds.Count < 2)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var people = roster.People.ToDictionary(person => person.Id, StringComparer.Ordinal);
|
||||
var present = talkedIds.Where(people.ContainsKey).Distinct(StringComparer.Ordinal).OrderBy(id => id, StringComparer.Ordinal).ToArray();
|
||||
var events = new List<AffinityEvent>();
|
||||
foreach (var fromId in present)
|
||||
{
|
||||
var from = people[fromId];
|
||||
if (from.Bonds is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var crushId in from.Bonds.Crushes.ToArray())
|
||||
{
|
||||
if (!people.TryGetValue(crushId, out var to) || !present.Contains(crushId, StringComparer.Ordinal))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!from.IsStudent || !to.IsStaff)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (RoleAllows(rules, to, from, pair: false))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var mutable = OpinionGenerator.EnsureMutableOpinions(from);
|
||||
var current = OpinionStore.Get(mutable, to.Id) ?? 0;
|
||||
OpinionStore.Set(mutable, to.Id, current + rules.RebuffOpinionShift);
|
||||
events.Add(new AffinityEvent(from.Id, Rebuff, to.Id));
|
||||
}
|
||||
}
|
||||
|
||||
_ = asOf;
|
||||
return events;
|
||||
}
|
||||
|
||||
public static bool CanHaveSympathy(
|
||||
DefCatalog catalog,
|
||||
Roster roster,
|
||||
Person from,
|
||||
Person to,
|
||||
DateTime asOf)
|
||||
{
|
||||
var rules = catalog.AffinityRules;
|
||||
if (rules is null || from.Id.Equals(to.Id, StringComparison.Ordinal))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var years = roster.Classes.ToDictionary(schoolClass => schoolClass.Id, StringComparer.Ordinal);
|
||||
return CanSympathy(catalog, rules, roster, years, from, to, asOf);
|
||||
}
|
||||
|
||||
public static bool CanFormPair(
|
||||
DefCatalog catalog,
|
||||
Roster roster,
|
||||
Person from,
|
||||
Person to,
|
||||
DateTime asOf)
|
||||
{
|
||||
var rules = catalog.AffinityRules;
|
||||
if (rules is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var years = roster.Classes.ToDictionary(schoolClass => schoolClass.Id, StringComparer.Ordinal);
|
||||
return CanPair(catalog, rules, roster, years, from, to, asOf);
|
||||
}
|
||||
|
||||
private static void DropInvalid(
|
||||
DefCatalog catalog,
|
||||
AffinityRulesDef rules,
|
||||
Roster roster,
|
||||
Dictionary<string, Person> people,
|
||||
IReadOnlyDictionary<string, SchoolClass> years,
|
||||
Person person,
|
||||
DateTime asOf,
|
||||
List<AffinityEvent> events)
|
||||
{
|
||||
var bonds = EnsureBonds(person);
|
||||
if (bonds is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var keep = new List<string>();
|
||||
foreach (var crushId in bonds.Crushes)
|
||||
{
|
||||
if (people.TryGetValue(crushId, out var target)
|
||||
&& CanSympathy(catalog, rules, roster, years, person, target, asOf))
|
||||
{
|
||||
keep.Add(crushId);
|
||||
}
|
||||
}
|
||||
|
||||
if (keep.Count != bonds.Crushes.Count)
|
||||
{
|
||||
bonds.Crushes.Clear();
|
||||
bonds.Crushes.AddRange(keep);
|
||||
}
|
||||
|
||||
if (bonds.PartnerId is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!people.TryGetValue(bonds.PartnerId, out var partner)
|
||||
|| !CanRemainPair(catalog, rules, roster, years, person, partner, asOf))
|
||||
{
|
||||
var otherId = bonds.PartnerId;
|
||||
BreakPair(person, partner, asOf);
|
||||
events.Add(new AffinityEvent(person.Id, PairBreak, otherId));
|
||||
if (partner is not null)
|
||||
{
|
||||
events.Add(new AffinityEvent(partner.Id, PairBreak, person.Id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void GainCrushes(
|
||||
DefCatalog catalog,
|
||||
AffinityRulesDef rules,
|
||||
Roster roster,
|
||||
Dictionary<string, Person> people,
|
||||
IReadOnlyDictionary<string, SchoolClass> years,
|
||||
Person person,
|
||||
DateTime asOf,
|
||||
List<AffinityEvent> events)
|
||||
{
|
||||
var bonds = EnsureBonds(person);
|
||||
if (bonds is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var threshold = rules.SympathyThreshold + TraitOffset(catalog, person, breakOffset: false);
|
||||
foreach (var (targetId, opinion) in person.Opinions)
|
||||
{
|
||||
if (opinion < threshold || !people.TryGetValue(targetId, out var target))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!CanSympathy(catalog, rules, roster, years, person, target, asOf))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (bonds.Crushes.Contains(targetId, StringComparer.Ordinal))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
bonds.Crushes.Add(targetId);
|
||||
events.Add(new AffinityEvent(person.Id, Crush, targetId));
|
||||
if (person.IsStudent)
|
||||
{
|
||||
events.Add(new AffinityEvent(person.Id, Tease, targetId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void TryFormPair(
|
||||
DefCatalog catalog,
|
||||
AffinityRulesDef rules,
|
||||
Roster roster,
|
||||
Dictionary<string, Person> people,
|
||||
IReadOnlyDictionary<string, SchoolClass> years,
|
||||
Person person,
|
||||
DateTime asOf,
|
||||
List<AffinityEvent> events)
|
||||
{
|
||||
var bonds = person.Bonds;
|
||||
if (bonds is null || bonds.PartnerId is not null || !CooldownElapsed(bonds, asOf))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var threshold = rules.PairThreshold;
|
||||
foreach (var crushId in bonds.Crushes.OrderBy(id => id, StringComparer.Ordinal))
|
||||
{
|
||||
if (!people.TryGetValue(crushId, out var other) || other.Bonds is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (other.Bonds.PartnerId is not null || !CooldownElapsed(other.Bonds, asOf))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!other.Bonds.Crushes.Contains(person.Id, StringComparer.Ordinal))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var fromView = OpinionStore.Get(person, other.Id) ?? 0;
|
||||
var toView = OpinionStore.Get(other, person.Id) ?? 0;
|
||||
if (fromView < threshold || toView < threshold)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!CanPair(catalog, rules, roster, years, person, other, asOf))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
bonds.PartnerId = other.Id;
|
||||
other.Bonds.PartnerId = person.Id;
|
||||
events.Add(new AffinityEvent(person.Id, Pair, other.Id));
|
||||
events.Add(new AffinityEvent(other.Id, Pair, person.Id));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool CanSympathy(
|
||||
DefCatalog catalog,
|
||||
AffinityRulesDef rules,
|
||||
Roster roster,
|
||||
IReadOnlyDictionary<string, SchoolClass> years,
|
||||
Person from,
|
||||
Person to,
|
||||
DateTime asOf)
|
||||
{
|
||||
if (from.Orientation is null || !Attracted(catalog, from, to))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (rules.ExcludeFamily && OpinionStore.FamilyMemberIds(roster, from).Contains(to.Id))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!RoleAllows(rules, from, to, pair: false))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (from.IsStudent && to.IsStudent && !StudentsClose(rules, years, from, to, asOf))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool CanPair(
|
||||
DefCatalog catalog,
|
||||
AffinityRulesDef rules,
|
||||
Roster roster,
|
||||
IReadOnlyDictionary<string, SchoolClass> years,
|
||||
Person from,
|
||||
Person to,
|
||||
DateTime asOf)
|
||||
{
|
||||
if (!CanSympathy(catalog, rules, roster, years, from, to, asOf)
|
||||
|| !CanSympathy(catalog, rules, roster, years, to, from, asOf))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (from.AgeOn(asOf) < rules.PairMinAge || to.AgeOn(asOf) < rules.PairMinAge)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return RoleAllows(rules, from, to, pair: true) && RoleAllows(rules, to, from, pair: true);
|
||||
}
|
||||
|
||||
private static bool CanRemainPair(
|
||||
DefCatalog catalog,
|
||||
AffinityRulesDef rules,
|
||||
Roster roster,
|
||||
IReadOnlyDictionary<string, SchoolClass> years,
|
||||
Person from,
|
||||
Person to,
|
||||
DateTime asOf)
|
||||
{
|
||||
if (!CanPair(catalog, rules, roster, years, from, to, asOf))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var floor = rules.PairBreakThreshold + TraitOffset(catalog, from, breakOffset: true);
|
||||
var view = OpinionStore.Get(from, to.Id) ?? 0;
|
||||
return view >= floor;
|
||||
}
|
||||
|
||||
private static bool Attracted(DefCatalog catalog, Person from, Person to)
|
||||
{
|
||||
if (from.Orientation is null || !catalog.Orientations.TryGetValue(from.Orientation, out var def) || def.Abstract)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return from.Female == to.Female ? def.SameGender : def.OppositeGender;
|
||||
}
|
||||
|
||||
private static bool RoleAllows(AffinityRulesDef rules, Person from, Person to, bool pair)
|
||||
{
|
||||
var fromRole = PrimaryRole(from);
|
||||
var toRole = PrimaryRole(to);
|
||||
if (fromRole is null || toRole is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var row in rules.RolePairs)
|
||||
{
|
||||
if (!row.From.Equals(fromRole, StringComparison.OrdinalIgnoreCase)
|
||||
|| !row.To.Equals(toRole, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
return pair ? row.Pair : row.Sympathy;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string? PrimaryRole(Person person)
|
||||
{
|
||||
if (person.IsStudent)
|
||||
{
|
||||
return PersonRoles.Student;
|
||||
}
|
||||
|
||||
if (person.IsStaff)
|
||||
{
|
||||
return PersonRoles.Staff;
|
||||
}
|
||||
|
||||
return person.IsParent ? PersonRoles.Parent : null;
|
||||
}
|
||||
|
||||
private static bool StudentsClose(
|
||||
AffinityRulesDef rules,
|
||||
IReadOnlyDictionary<string, SchoolClass> years,
|
||||
Person from,
|
||||
Person to,
|
||||
DateTime asOf)
|
||||
{
|
||||
if (Math.Abs(from.AgeOn(asOf) - to.AgeOn(asOf)) <= rules.StudentAgeDeltaYears)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!rules.StudentAdjacentYear)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var fromYear = YearOf(years, from);
|
||||
var toYear = YearOf(years, to);
|
||||
return fromYear is { } left && toYear is { } right && Math.Abs(left - right) == 1;
|
||||
}
|
||||
|
||||
private static int? YearOf(IReadOnlyDictionary<string, SchoolClass> years, Person person) =>
|
||||
person.ClassId is { } classId && years.TryGetValue(classId, out var schoolClass) ? schoolClass.Year : null;
|
||||
|
||||
private static int TraitOffset(DefCatalog catalog, Person person, bool breakOffset)
|
||||
{
|
||||
var total = 0;
|
||||
foreach (var traitId in person.Traits)
|
||||
{
|
||||
if (!catalog.Traits.TryGetValue(traitId, out var trait))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
total += breakOffset ? trait.AffinityBreakOffset : trait.AffinityThresholdOffset;
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
|
||||
private static PersonBonds? EnsureBonds(Person person)
|
||||
{
|
||||
if (person.Orientation is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return person.Bonds ??= new PersonBonds();
|
||||
}
|
||||
|
||||
private static bool CooldownElapsed(PersonBonds bonds, DateTime asOf)
|
||||
{
|
||||
if (bonds.PairEndedOn is not { } ended)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return asOf.Date > DateTime.SpecifyKind(ended, DateTimeKind.Utc).Date;
|
||||
}
|
||||
|
||||
private static void BreakPair(Person left, Person? right, DateTime asOf)
|
||||
{
|
||||
var leftBonds = EnsureBonds(left);
|
||||
if (leftBonds is not null)
|
||||
{
|
||||
leftBonds.PartnerId = null;
|
||||
leftBonds.PairEndedOn = asOf;
|
||||
}
|
||||
|
||||
if (right is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var rightBonds = EnsureBonds(right);
|
||||
if (rightBonds is not null)
|
||||
{
|
||||
rightBonds.PartnerId = null;
|
||||
rightBonds.PairEndedOn = asOf;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -179,7 +179,10 @@ public sealed record ApplicantPool(int Week, int NextIndex, IReadOnlyList<Applic
|
||||
applicants.Add(next);
|
||||
}
|
||||
|
||||
return new ApplicantPool(current.Week, nextIndex, applicants);
|
||||
return OrientationGenerator.AssignPool(
|
||||
catalog,
|
||||
new ApplicantPool(current.Week, nextIndex, applicants),
|
||||
schoolSeed);
|
||||
}
|
||||
|
||||
private static StaffingDef RequireRules(DefCatalog catalog) =>
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
using HSchool.Content;
|
||||
|
||||
namespace HSchool.People;
|
||||
|
||||
/// <summary>
|
||||
/// Writes <see cref="Person.Orientation"/> only when the catalog has orientations. Vanilla
|
||||
/// catalogs skip this, so the field stays null and is omitted from people.json.
|
||||
/// </summary>
|
||||
public static class OrientationGenerator
|
||||
{
|
||||
public static Roster Assign(DefCatalog catalog, Roster roster, int schoolSeed)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(catalog);
|
||||
ArgumentNullException.ThrowIfNull(roster);
|
||||
|
||||
if (catalog.Orientations.Count == 0)
|
||||
{
|
||||
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 = AssignIfMissing(catalog, person, schoolSeed);
|
||||
people[i] = updated;
|
||||
changed |= !ReferenceEquals(updated, person);
|
||||
}
|
||||
|
||||
return changed ? roster with { People = people } : roster;
|
||||
}
|
||||
|
||||
public static ApplicantPool AssignPool(DefCatalog catalog, ApplicantPool pool, int schoolSeed)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(catalog);
|
||||
ArgumentNullException.ThrowIfNull(pool);
|
||||
|
||||
if (catalog.Orientations.Count == 0 || pool.Applicants.Count == 0)
|
||||
{
|
||||
return pool;
|
||||
}
|
||||
|
||||
var applicants = new Applicant[pool.Applicants.Count];
|
||||
var changed = false;
|
||||
for (var i = 0; i < pool.Applicants.Count; i++)
|
||||
{
|
||||
var applicant = pool.Applicants[i];
|
||||
var person = AssignIfMissing(catalog, applicant.Person, schoolSeed);
|
||||
changed |= !ReferenceEquals(person, applicant.Person);
|
||||
applicants[i] = ReferenceEquals(person, applicant.Person) ? applicant : applicant with { Person = person };
|
||||
}
|
||||
|
||||
return changed ? pool with { Applicants = applicants } : pool;
|
||||
}
|
||||
|
||||
public static Person AssignIfMissing(DefCatalog catalog, Person person, int schoolSeed)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(catalog);
|
||||
ArgumentNullException.ThrowIfNull(person);
|
||||
|
||||
if (catalog.Orientations.Count == 0 || person.Orientation is not null)
|
||||
{
|
||||
return person;
|
||||
}
|
||||
|
||||
var id = Pick(catalog, person.Id, schoolSeed);
|
||||
if (id is null)
|
||||
{
|
||||
return person;
|
||||
}
|
||||
|
||||
return person with
|
||||
{
|
||||
Orientation = id,
|
||||
Bonds = person.Bonds ?? new PersonBonds(),
|
||||
};
|
||||
}
|
||||
|
||||
public static string? Pick(DefCatalog catalog, string personId, int schoolSeed)
|
||||
{
|
||||
var options = catalog.Orientations.Values
|
||||
.Where(def => !def.Abstract)
|
||||
.OrderBy(def => def.DefName, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
if (options.Length == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var total = 0;
|
||||
foreach (var option in options)
|
||||
{
|
||||
total += option.Weight;
|
||||
}
|
||||
|
||||
if (total <= 0)
|
||||
{
|
||||
return options[0].DefName;
|
||||
}
|
||||
|
||||
var rng = new Random(Seed.Mix(schoolSeed, personId, dayNumber: 0, Seed.OrientationSalt));
|
||||
var roll = rng.Next(total);
|
||||
foreach (var option in options)
|
||||
{
|
||||
roll -= option.Weight;
|
||||
if (roll < 0)
|
||||
{
|
||||
return option.DefName;
|
||||
}
|
||||
}
|
||||
|
||||
return options[^1].DefName;
|
||||
}
|
||||
}
|
||||
@@ -55,9 +55,30 @@ public sealed record Person
|
||||
/// <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);
|
||||
|
||||
/// <summary>
|
||||
/// OrientationDef name. Null when the catalog has no orientations (vanilla). Omitted from JSON.
|
||||
/// </summary>
|
||||
public string? Orientation { get; init; }
|
||||
|
||||
/// <summary>Crushes and the one pair. Null without an orientation pack. Mutable overlay.</summary>
|
||||
public PersonBonds? Bonds { get; set; }
|
||||
|
||||
public int AgeOn(DateTime asOf) => SchoolYears.AgeYears(BirthDate, asOf);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One-way crushes and at most one pair. Lives on the person when a pack shipped orientations.
|
||||
/// Mutated in place the same way opinions are.
|
||||
/// </summary>
|
||||
public sealed class PersonBonds
|
||||
{
|
||||
public List<string> Crushes { get; set; } = [];
|
||||
|
||||
public string? PartnerId { get; set; }
|
||||
|
||||
public DateTime? PairEndedOn { get; set; }
|
||||
}
|
||||
|
||||
public sealed record PersonName(
|
||||
string Given,
|
||||
string Surname,
|
||||
|
||||
@@ -45,7 +45,9 @@ public static class RosterGenerator
|
||||
|
||||
var classes = FillClasses(demand.Classes, people);
|
||||
var roster = LockerAssigner.Apply(catalog, map, new Roster(people, families, classes));
|
||||
return OpinionGenerator.SeedFamily(catalog, roster);
|
||||
roster = OrientationGenerator.Assign(catalog, OpinionGenerator.SeedFamily(catalog, roster), schoolSeed);
|
||||
Affinity.Refresh(catalog, roster, when);
|
||||
return roster;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -18,6 +18,7 @@ public static class Seed
|
||||
public const int ClimatePresetSalt = 10;
|
||||
public const int ApparelSalt = 11;
|
||||
public const int WhisperSalt = 12;
|
||||
public const int OrientationSalt = 13;
|
||||
|
||||
/// <summary>A stream that belongs to the school rather than to one family.</summary>
|
||||
public static int ForSchool(int schoolSeed, int salt) => Mix(schoolSeed, familyIndex: -1, salt);
|
||||
|
||||
@@ -148,7 +148,9 @@ public static class YearlyIntake
|
||||
|
||||
var people = remainingPeople.Values.ToList();
|
||||
var filled = AttachPupils(classes, people);
|
||||
return new Roster(people, families, filled);
|
||||
var next = OrientationGenerator.Assign(catalog, new Roster(people, families, filled), schoolSeed);
|
||||
Affinity.Refresh(catalog, next, when);
|
||||
return next;
|
||||
}
|
||||
|
||||
private static void GrantPromotedSkills(
|
||||
|
||||
Reference in New Issue
Block a user