Files
Christopher Wiebe b451f83174 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>
2026-04-30 20:40:51 -07:00

131 lines
5.4 KiB
C#

using Theriapolis.Core;
using Theriapolis.Core.Data;
using Theriapolis.Core.Entities;
using Theriapolis.Core.Rules.Character;
using Theriapolis.Core.Rules.Reputation;
using Theriapolis.Core.Rules.Stats;
using Xunit;
namespace Theriapolis.Tests.Reputation;
/// <summary>
/// Phase 6 M5 — patrol/guard NPC allegiance flips when local disposition
/// drops to HOSTILE.
/// </summary>
public sealed class FactionAggressionTests : IClassFixture<WorldCache>
{
private readonly WorldCache _cache;
public FactionAggressionTests(WorldCache c) => _cache = c;
private static Character WolfPc(ContentResolver content)
{
var b = new CharacterBuilder()
.WithClade(content.Clades["canidae"])
.WithSpecies(content.Species["wolf"])
.WithClass(content.Classes["fangsworn"])
.WithBackground(content.Backgrounds["pack_raised"])
.WithAbilities(new AbilityScores(13, 12, 14, 10, 10, 11));
var classD = content.Classes["fangsworn"];
var added = new HashSet<SkillId>();
for (int i = 0; i < classD.SkillOptions.Length && added.Count < classD.SkillsChoose; i++)
{
try
{
var sk = SkillIdExtensions.FromJson(classD.SkillOptions[i]);
if (added.Add(sk)) b.ChooseSkill(sk);
}
catch (System.ArgumentException) { }
}
return b.Build();
}
[Fact]
public void EnforcerPatrol_FlipsToHostile_WhenStandingTanks()
{
var content = new ContentResolver(new ContentLoader(TestHelpers.DataDirectory));
var pc = WolfPc(content);
var rep = new PlayerReputation();
var actors = new ActorManager();
actors.SpawnPlayer(new Theriapolis.Core.Util.Vec2(50, 50));
var world = _cache.Get(0xCAFEBABEUL).World;
var s = world.Settlements.First();
// Spawn a militia patroller manually using the npc template so it
// carries the covenant_enforcers faction id.
var template = content.Npcs.Templates.First(t => t.Id == "militia_patrol");
var npc = actors.SpawnNpc(template, new Theriapolis.Core.Util.Vec2(s.TileX * C.WORLD_TILE_PIXELS, s.TileY * C.WORLD_TILE_PIXELS));
// Give them a home settlement so propagation has somewhere to land.
// (The init-only HomeSettlementId requires reconstruction; instead
// we re-spawn through the (NpcActor pre) path with the field set.)
actors.RemoveActor(npc.Id);
var pre = new NpcActor(template)
{
Id = -1,
Position = new Theriapolis.Core.Util.Vec2(s.TileX * C.WORLD_TILE_PIXELS, s.TileY * C.WORLD_TILE_PIXELS),
HomeSettlementId = s.Id,
};
var live = actors.SpawnNpc(pre);
Assert.Equal(Allegiance.Neutral, live.Allegiance);
Assert.Equal("covenant_enforcers", live.FactionId);
// Tank the player's Enforcer standing with a single big crime.
rep.Submit(new RepEvent
{
Kind = RepEventKind.Crime,
FactionId = "covenant_enforcers",
Magnitude = -80,
OriginTileX = s.TileX, OriginTileY = s.TileY,
}, content.Factions);
int flipped = FactionAggression.UpdateAllegiances(actors, pc, rep, content, world, 0xCAFEBABEUL);
Assert.True(flipped >= 1, $"expected ≥1 flip; got {flipped}");
Assert.Equal(Allegiance.Hostile, live.Allegiance);
}
[Fact]
public void Resident_WithoutFaction_DoesNotFlip()
{
// A friendly non-faction resident (innkeeper without faction) should
// never flip even with player infamy.
var content = new ContentResolver(new ContentLoader(TestHelpers.DataDirectory));
var pc = WolfPc(content);
var rep = new PlayerReputation();
var actors = new ActorManager();
actors.SpawnPlayer(new Theriapolis.Core.Util.Vec2(50, 50));
var world = _cache.Get(0xCAFEBABEUL).World;
var template = content.Residents["generic_innkeeper"]; // no faction
var pre = new NpcActor(template)
{
Id = -1,
Position = new Theriapolis.Core.Util.Vec2(0, 0),
};
var live = actors.SpawnNpc(pre);
rep.Factions.Set("covenant_enforcers", -100);
FactionAggression.UpdateAllegiances(actors, pc, rep, content, world, 0xCAFEBABEUL);
Assert.NotEqual(Allegiance.Hostile, live.Allegiance);
}
[Fact]
public void HostileNpc_StaysHostile_AndDoesNotChangeAgain()
{
// Sanity: a hostile NPC isn't "promoted" further (no "extra hostile").
var content = new ContentResolver(new ContentLoader(TestHelpers.DataDirectory));
var pc = WolfPc(content);
var rep = new PlayerReputation();
var actors = new ActorManager();
actors.SpawnPlayer(new Theriapolis.Core.Util.Vec2(50, 50));
var world = _cache.Get(0xCAFEBABEUL).World;
// brigand_footpad has default_allegiance = hostile and no faction.
var template = content.Npcs.Templates.First(t => t.Id == "brigand_footpad");
var live = actors.SpawnNpc(template, new Theriapolis.Core.Util.Vec2(0, 0));
Assert.Equal(Allegiance.Hostile, live.Allegiance);
int flipped = FactionAggression.UpdateAllegiances(actors, pc, rep, content, world, 0xCAFEBABEUL);
Assert.Equal(0, flipped);
Assert.Equal(Allegiance.Hostile, live.Allegiance);
}
}