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:
@@ -0,0 +1,130 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user