Complete phase 47 romance pack on generic orientation and affinity hooks.

Vanilla catalogs stay without crushes; a content pack ships the overlay so later mods can reuse it.
This commit is contained in:
Leonid Pershin
2026-08-20 10:42:42 +03:00
parent cb869e4977
commit d2911c3daf
43 changed files with 1840 additions and 29 deletions
+195
View File
@@ -0,0 +1,195 @@
namespace HSchool.People.Tests;
public class AffinityTests
{
private static readonly DateTime AsOf = new(2012, 3, 31, 6, 0, 0, DateTimeKind.Utc);
[Fact]
public void SixteenYearOlds_NeverPair_AtAnyOpinion()
{
var catalog = Fixtures.RomanceCatalog();
var boy = Pupil("a", year: 10, age: 16, female: false, "Bisexual");
var girl = Pupil("b", year: 10, age: 16, female: true, "Bisexual");
SetOpinion(boy, girl.Id, 100);
SetOpinion(girl, boy.Id, 100);
var roster = RosterOf(boy, girl, classYear: 10);
Affinity.Refresh(catalog, roster, AsOf);
Assert.Contains("b", boy.Bonds!.Crushes);
Assert.Contains("a", girl.Bonds!.Crushes);
Assert.Null(boy.Bonds.PartnerId);
Assert.Null(girl.Bonds.PartnerId);
Assert.False(Affinity.CanFormPair(catalog, roster, boy, girl, AsOf));
}
[Fact]
public void FirstYear_DoesNotCrushEleventh()
{
var catalog = Fixtures.RomanceCatalog();
var young = Pupil("c1", year: 1, age: 7, female: true, "Bisexual", classId: "y1");
var old = Pupil("c11", year: 11, age: 17, female: false, "Bisexual", classId: "y11");
SetOpinion(young, old.Id, 100);
var roster = new Roster(
[young, old],
[],
[
new SchoolClass("y1", 1, "А", "r1", 16, [young.Id]),
new SchoolClass("y11", 11, "А", "r11", 16, [old.Id]),
]);
Affinity.Refresh(catalog, roster, AsOf);
Assert.DoesNotContain("c11", young.Bonds!.Crushes);
Assert.False(Affinity.CanHaveSympathy(catalog, roster, young, old, AsOf));
}
[Fact]
public void Staff_NeverGetsCrushOnStudent()
{
var catalog = Fixtures.RomanceCatalog();
var teacher = Staff("t", female: false, "Bisexual");
var pupil = Pupil("p", year: 8, age: 14, female: true, "Bisexual");
SetOpinion(teacher, pupil.Id, 100);
SetOpinion(pupil, teacher.Id, 100);
var roster = RosterOf(teacher, pupil, classYear: 8);
Affinity.Refresh(catalog, roster, AsOf);
Assert.DoesNotContain(pupil.Id, teacher.Bonds!.Crushes);
Assert.Contains(teacher.Id, pupil.Bonds!.Crushes);
Assert.Null(teacher.Bonds.PartnerId);
Assert.Null(pupil.Bonds.PartnerId);
Assert.False(Affinity.CanHaveSympathy(catalog, roster, teacher, pupil, AsOf));
Assert.True(Affinity.CanHaveSympathy(catalog, roster, pupil, teacher, AsOf));
}
[Fact]
public void StaffPair_IsAllowedWhenBothAdults()
{
var catalog = Fixtures.RomanceCatalog();
var left = Staff("s1", female: false, "Bisexual");
var right = Staff("s2", female: true, "Bisexual");
SetOpinion(left, right.Id, 80);
SetOpinion(right, left.Id, 80);
var roster = new Roster([left, right], [], []);
var events = Affinity.Refresh(catalog, roster, AsOf);
Assert.Equal(right.Id, left.Bonds!.PartnerId);
Assert.Equal(left.Id, right.Bonds!.PartnerId);
Assert.Contains(events, row => row.Type == Affinity.Pair);
}
[Fact]
public void PairBreak_BlocksNewPairUntilNextDay()
{
var catalog = Fixtures.RomanceCatalog();
var left = Staff("s1", female: false, "Bisexual");
var right = Staff("s2", female: true, "Bisexual");
var third = Staff("s3", female: true, "Bisexual");
SetOpinion(left, right.Id, 80);
SetOpinion(right, left.Id, 80);
var roster = new Roster([left, right, third], [], []);
Affinity.Refresh(catalog, roster, AsOf);
Assert.Equal(right.Id, left.Bonds!.PartnerId);
SetOpinion(left, right.Id, 0);
Affinity.Refresh(catalog, roster, AsOf);
Assert.Null(left.Bonds.PartnerId);
Assert.NotNull(left.Bonds.PairEndedOn);
SetOpinion(left, third.Id, 80);
SetOpinion(third, left.Id, 80);
Affinity.Refresh(catalog, roster, AsOf);
Assert.Null(left.Bonds.PartnerId);
Affinity.Refresh(catalog, roster, AsOf.AddDays(1));
Assert.Equal(third.Id, left.Bonds.PartnerId);
}
[Fact]
public void AdjacentYearStudents_CanHaveSympathy()
{
var catalog = Fixtures.RomanceCatalog();
var younger = Pupil("y", year: 9, age: 15, female: true, "Bisexual", classId: "c9");
var older = Pupil("o", year: 10, age: 18, female: false, "Bisexual", classId: "c10");
SetOpinion(younger, older.Id, 80);
var roster = new Roster(
[younger, older],
[],
[
new SchoolClass("c9", 9, "А", "r9", 16, [younger.Id]),
new SchoolClass("c10", 10, "А", "r10", 16, [older.Id]),
]);
Assert.True(Affinity.CanHaveSympathy(catalog, roster, younger, older, AsOf));
Affinity.Refresh(catalog, roster, AsOf);
Assert.Contains(older.Id, younger.Bonds!.Crushes);
}
private static void SetOpinion(Person person, string targetId, int value)
{
if (value == 0)
{
OpinionStore.Remove(person, targetId);
return;
}
OpinionStore.Set(person, targetId, value);
}
private static Roster RosterOf(Person first, Person second, int classYear)
{
var pupils = new[] { first, second }.Where(person => person.IsStudent).Select(person => person.Id).ToArray();
return new Roster(
[first, second],
[],
[new SchoolClass("c", classYear, "А", "room", 16, pupils)]);
}
private static Person Pupil(string id, int year, int age, bool female, string orientation, string? classId = null)
{
_ = year;
var birth = AsOf.AddYears(-age);
return AdultLike(id, female, orientation, isStudent: true, isStaff: false, birth, classId ?? "c");
}
private static Person Staff(string id, bool female, string orientation) =>
AdultLike(id, female, orientation, isStudent: false, isStaff: true, AsOf.AddYears(-30), classId: null);
private static Person AdultLike(
string id,
bool female,
string orientation,
bool isStudent,
bool isStaff,
DateTime birth,
string? classId)
{
var cases = new CaseTable
{
Nom = id,
Gen = id,
Dat = id,
Acc = id,
Ins = id,
Pre = id,
};
return new Person
{
Id = id,
FamilyId = "f",
Female = female,
BirthDate = DateTime.SpecifyKind(birth, DateTimeKind.Utc),
Name = new PersonName(id, id, id, cases, cases, cases),
IsStudent = isStudent,
IsStaff = isStaff,
IsParent = false,
ClassId = classId,
Numbers = new Dictionary<string, int>(StringComparer.Ordinal),
Choices = new Dictionary<string, string>(StringComparer.Ordinal),
Skills = new Dictionary<string, int>(StringComparer.Ordinal),
Traits = [],
Needs = new Dictionary<string, float>(StringComparer.Ordinal),
Opinions = new Dictionary<string, int>(StringComparer.Ordinal),
Orientation = orientation,
Bonds = new PersonBonds(),
};
}
}
+13
View File
@@ -35,6 +35,19 @@ internal static class Fixtures
PackDocuments.FromDirectory(CatalogLoader.CorePackId, root));
}
public static DefCatalog RomanceCatalog()
{
var vanilla = Path.Combine(AppContext.BaseDirectory, "vanilla");
var romance = Path.Combine(AppContext.BaseDirectory, "romance");
Assert.True(Directory.Exists(romance), $"Romance pack was not copied to {romance}.");
return new CatalogLoader().Load(
[CatalogLoader.CorePackId, "romance"],
[
.. PackDocuments.FromDirectory(CatalogLoader.CorePackId, vanilla),
.. PackDocuments.FromDirectory("romance", romance),
]);
}
public static MapLayout Classrooms(int count, int desks = 16)
{
var rooms = new RoomNode[count];
@@ -30,6 +30,10 @@
<Link>example\%(RecursiveDir)%(Filename)%(Extension)</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="..\..\src\HSchool.Server\mods\romance\**\*">
<Link>romance\%(RecursiveDir)%(Filename)%(Extension)</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="golden\**\*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
@@ -0,0 +1,37 @@
namespace HSchool.People.Tests;
public class OrientationGeneratorTests
{
[Fact]
public void VanillaGenerate_DoesNotWriteOrientation()
{
var roster = Fixtures.Generate(Fixtures.Classrooms(2));
Assert.All(roster.People, person => Assert.Null(person.Orientation));
Assert.All(roster.People, person => Assert.Null(person.Bonds));
}
[Fact]
public void RomanceGenerate_WritesOrientationToEveryone()
{
var catalog = Fixtures.RomanceCatalog();
var roster = RosterGenerator.Generate(catalog, Fixtures.Classrooms(2), Fixtures.SchoolSeed, "Russia", Fixtures.AsOf, "RussianLanguage");
Assert.NotEmpty(roster.People);
Assert.All(roster.People, person =>
{
Assert.False(string.IsNullOrWhiteSpace(person.Orientation));
Assert.True(catalog.Orientations.ContainsKey(person.Orientation!));
Assert.NotNull(person.Bonds);
});
}
[Fact]
public void SameSeed_YieldsTheSameOrientations()
{
var catalog = Fixtures.RomanceCatalog();
var a = RosterGenerator.Generate(catalog, Fixtures.Classrooms(2), Fixtures.SchoolSeed, "Russia", Fixtures.AsOf, "RussianLanguage");
var b = RosterGenerator.Generate(catalog, Fixtures.Classrooms(2), Fixtures.SchoolSeed, "Russia", Fixtures.AsOf, "RussianLanguage");
Assert.Equal(
a.People.Select(person => (person.Id, person.Orientation)),
b.People.Select(person => (person.Id, person.Orientation)));
}
}