Initial commit: Theriapolis baseline at port/godot branch point

Captures the pre-Godot-port state of the codebase. This is the rollback
anchor for the Godot port (M0 of theriapolis-rpg-implementation-plan-godot-port.md).
All Phase 0 through Phase 6.5 work is included; Phase 7 is in flight.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Christopher Wiebe
2026-04-30 20:40:51 -07:00
commit b451f83174
525 changed files with 75786 additions and 0 deletions
@@ -0,0 +1,110 @@
using Theriapolis.Core;
using Theriapolis.Core.Data;
using Theriapolis.Core.Rules.Reputation;
using Xunit;
namespace Theriapolis.Tests.Reputation;
/// <summary>
/// Phase 6 M2 — opposition matrix application.
///
/// The plan §I-2 spells out the exact multipliers; this suite verifies
/// the cascade fires in both directions (gains and losses) and stays
/// inside the clamp range.
/// </summary>
public sealed class FactionOppositionTests
{
private static IReadOnlyDictionary<string, FactionDef> LoadFactions()
=> new ContentResolver(new ContentLoader(TestHelpers.DataDirectory)).Factions;
[Fact]
public void GainWithInheritors_CascadesIntoOppositionLosses()
{
var factions = LoadFactions();
var standing = new FactionStanding();
var applied = standing.Apply("inheritors", 10, factions);
// Per the doc: +10 with Inheritors should yield -5 Enforcers,
// -2 Thorn Council, -3 Hybrid Underground, -3 Unsheathed.
Assert.Equal( 10, standing.Get("inheritors"));
Assert.Equal( -5, standing.Get("covenant_enforcers"));
Assert.Equal( -2, standing.Get("thorn_council"));
Assert.Equal( -3, standing.Get("hybrid_underground"));
Assert.Equal( -3, standing.Get("unsheathed"));
Assert.Equal( 0, standing.Get("merchant_guilds")); // multiplier 0
// The applied list reports every actually-changed faction.
Assert.Contains(applied, t => t.FactionId == "inheritors" && t.Delta == 10);
Assert.Contains(applied, t => t.FactionId == "covenant_enforcers" && t.Delta == -5);
}
[Fact]
public void LossWithInheritors_CascadesIntoOppositionGains()
{
var factions = LoadFactions();
var standing = new FactionStanding();
standing.Apply("inheritors", -20, factions);
// -20 × -0.5 = +10 with Enforcers.
Assert.Equal(-20, standing.Get("inheritors"));
Assert.Equal( 10, standing.Get("covenant_enforcers"));
Assert.Equal( 4, standing.Get("thorn_council"));
Assert.Equal( 6, standing.Get("hybrid_underground"));
Assert.Equal( 6, standing.Get("unsheathed"));
}
[Fact]
public void Standing_IsClampedToRepRange()
{
var factions = LoadFactions();
var standing = new FactionStanding();
standing.Apply("inheritors", 200, factions);
Assert.Equal(C.REP_MAX, standing.Get("inheritors"));
// Cascaded -100 is below the floor — verify clamping.
Assert.Equal(C.REP_MIN, standing.Get("covenant_enforcers"));
}
[Fact]
public void ZeroDelta_DoesNothing()
{
var factions = LoadFactions();
var standing = new FactionStanding();
standing.Set("inheritors", 25);
standing.Apply("inheritors", 0, factions);
Assert.Equal(25, standing.Get("inheritors"));
Assert.Equal( 0, standing.Get("covenant_enforcers"));
}
[Fact]
public void UnknownFaction_NoCascade()
{
var factions = LoadFactions();
var standing = new FactionStanding();
// No throw; standing accumulates against the unknown id.
standing.Apply("not_a_real_faction", 10, factions);
Assert.Equal(10, standing.Get("not_a_real_faction"));
Assert.Equal( 0, standing.Get("covenant_enforcers"));
}
[Fact]
public void SubmitEvent_AppliesFactionAndPersonal()
{
var factions = LoadFactions();
var rep = new PlayerReputation();
rep.Submit(new RepEvent
{
Kind = RepEventKind.Quest,
FactionId = "inheritors",
RoleTag = "test.someone",
Magnitude = 10,
Note = "test event",
}, factions);
Assert.Equal(10, rep.Factions.Get("inheritors"));
Assert.Equal(-5, rep.Factions.Get("covenant_enforcers"));
Assert.Equal(10, rep.PersonalFor("test.someone").Score);
Assert.Single(rep.Ledger.Entries);
}
}