b451f83174
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>
131 lines
5.4 KiB
C#
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);
|
|
}
|
|
}
|