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; /// /// Phase 6 M5 — patrol/guard NPC allegiance flips when local disposition /// drops to HOSTILE. /// public sealed class FactionAggressionTests : IClassFixture { 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(); 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); } }