Files
TheriapolisV3/Theriapolis.Tests/Entities/ScentTagTests.cs
T

215 lines
8.2 KiB
C#
Raw Normal View History

using Theriapolis.Core.Data;
using Theriapolis.Core.Entities;
using Xunit;
namespace Theriapolis.Tests.Entities;
/// <summary>
/// Phase 6.5 M6 — per-NPC scent profile data layer. Verifies the
/// ScentTag derivation: faction id → affiliation tag, runtime flags →
/// distress / activity tags, count truncation per the Scent Literacy
/// (top 1) vs Scent Mastery (top 3) tier.
/// </summary>
public sealed class ScentTagTests
{
// ── Faction → tag mapping ─────────────────────────────────────────────
[Theory]
[InlineData("maw", ScentTag.MawAffiliated)]
[InlineData("inheritors", ScentTag.InheritorAffiliated)]
[InlineData("thorn_council", ScentTag.ThornCouncilAffiliated)]
[InlineData("covenant_enforcers", ScentTag.CovenantEnforcerAffiliated)]
[InlineData("hybrid_underground", ScentTag.HybridUndergroundAffiliated)]
[InlineData("unsheathed", ScentTag.UnsheathedAffiliated)]
[InlineData("merchant_guilds", ScentTag.MerchantAffiliated)]
public void FromFactionId_MapsKnownFactions(string factionId, ScentTag expected)
{
Assert.Equal(expected, ScentTagExtensions.FromFactionId(factionId));
}
[Theory]
[InlineData("")]
[InlineData("not_a_real_faction")]
[InlineData("random_string")]
public void FromFactionId_ReturnsNoneForUnknown(string factionId)
{
Assert.Equal(ScentTag.None, ScentTagExtensions.FromFactionId(factionId));
}
[Fact]
public void FromFactionId_IsCaseInsensitive()
{
Assert.Equal(ScentTag.MawAffiliated, ScentTagExtensions.FromFactionId("MAW"));
Assert.Equal(ScentTag.InheritorAffiliated, ScentTagExtensions.FromFactionId("Inheritors"));
}
// ── ComputeScentTags: faction-derived ─────────────────────────────────
[Fact]
public void ComputeScentTags_LacroixSurfacesMawAffiliated()
{
// Lacroix scenario: faction=maw, full HP, no kills.
var npc = MakeResidentNpc("canidae", "coyote", factionId: "maw");
var tags = npc.ComputeScentTags(maxCount: 1);
Assert.Single(tags);
Assert.Equal(ScentTag.MawAffiliated, tags[0]);
}
[Fact]
public void ComputeScentTags_MerchantSurfacesMerchantAffiliated()
{
var npc = MakeResidentNpc("canidae", "fox", factionId: "merchant_guilds");
var tags = npc.ComputeScentTags(maxCount: 1);
Assert.Single(tags);
Assert.Equal(ScentTag.MerchantAffiliated, tags[0]);
}
[Fact]
public void ComputeScentTags_NoFactionEmpty()
{
var npc = MakeResidentNpc("canidae", "wolf", factionId: "");
var tags = npc.ComputeScentTags(maxCount: 1);
Assert.Empty(tags);
}
// ── ComputeScentTags: runtime-derived ─────────────────────────────────
[Fact]
public void ComputeScentTags_RecentlyKilledSurfaces()
{
var npc = MakeResidentNpc("canidae", "wolf", factionId: "");
npc.HasRecentlyKilled = true;
var tags = npc.ComputeScentTags(maxCount: 1);
Assert.Single(tags);
Assert.Equal(ScentTag.RecentlyKilled, tags[0]);
}
[Fact]
public void ComputeScentTags_FrightenedAtLowHp()
{
var npc = MakeResidentNpc("canidae", "wolf", factionId: "");
// Drop HP to 20% (below 25% threshold).
npc.CurrentHp = npc.MaxHp / 5;
var tags = npc.ComputeScentTags(maxCount: 1);
Assert.Single(tags);
Assert.Equal(ScentTag.Frightened, tags[0]);
}
[Fact]
public void ComputeScentTags_WoundedAtHalfHp()
{
var npc = MakeResidentNpc("canidae", "wolf", factionId: "");
// Drop HP to ~40% (below 50% but above 25%).
npc.CurrentHp = (int)(npc.MaxHp * 0.4f);
var tags = npc.ComputeScentTags(maxCount: 1);
Assert.Single(tags);
Assert.Equal(ScentTag.Wounded, tags[0]);
}
[Fact]
public void ComputeScentTags_DeadEmitsNoFrightened()
{
var npc = MakeResidentNpc("canidae", "wolf", factionId: "");
npc.CurrentHp = 0;
var tags = npc.ComputeScentTags(maxCount: 5);
// Dead NPCs don't carry distress markers (they're past distress).
Assert.DoesNotContain(ScentTag.Frightened, tags);
Assert.DoesNotContain(ScentTag.Wounded, tags);
}
[Fact]
public void ComputeScentTags_ContrabandFlagSurfaces()
{
var npc = MakeResidentNpc("canidae", "wolf", factionId: "");
npc.CarriesContrabandFlag = true;
var tags = npc.ComputeScentTags(maxCount: 5);
Assert.Contains(ScentTag.CarriesContraband, tags);
}
// ── ComputeScentTags: priority + truncation ───────────────────────────
[Fact]
public void ComputeScentTags_FactionTagWinsAtMaxCount1()
{
// Faction (priority 18) leads runtime tags (priority 16+) when
// truncating to a single read.
var npc = MakeResidentNpc("canidae", "coyote", factionId: "maw");
npc.HasRecentlyKilled = true;
npc.CarriesContrabandFlag = true;
npc.CurrentHp = npc.MaxHp / 5; // also Frightened
var tags = npc.ComputeScentTags(maxCount: 1);
Assert.Single(tags);
Assert.Equal(ScentTag.MawAffiliated, tags[0]);
}
[Fact]
public void ComputeScentTags_MasteryReadsUpToThree()
{
var npc = MakeResidentNpc("canidae", "coyote", factionId: "maw");
npc.HasRecentlyKilled = true;
npc.CurrentHp = npc.MaxHp / 5; // Frightened
npc.CarriesContrabandFlag = true;
var tags = npc.ComputeScentTags(maxCount: 3);
Assert.Equal(3, tags.Count);
// Order: faction (1), then runtime in declaration order.
Assert.Equal(ScentTag.MawAffiliated, tags[0]);
Assert.Equal(ScentTag.RecentlyKilled, tags[1]);
Assert.Equal(ScentTag.Frightened, tags[2]);
}
[Fact]
public void ComputeScentTags_MaxCountZeroReturnsEmpty()
{
var npc = MakeResidentNpc("canidae", "coyote", factionId: "maw");
npc.HasRecentlyKilled = true;
var tags = npc.ComputeScentTags(maxCount: 0);
Assert.Empty(tags);
}
[Fact]
public void ComputeScentTags_MaxCountFiveCapsAtAvailable()
{
// Only faction tag available; cap at 5 returns just the one.
var npc = MakeResidentNpc("canidae", "coyote", factionId: "maw");
var tags = npc.ComputeScentTags(maxCount: 5);
Assert.Single(tags);
}
// ── DisplayName ───────────────────────────────────────────────────────
[Fact]
public void DisplayName_ProducesReadableText()
{
Assert.Equal("Maw-affiliated", ScentTag.MawAffiliated.DisplayName());
Assert.Equal("Recently killed", ScentTag.RecentlyKilled.DisplayName());
Assert.Equal("Inheritor-affiliated", ScentTag.InheritorAffiliated.DisplayName());
Assert.Equal("Frightened", ScentTag.Frightened.DisplayName());
}
[Fact]
public void IsNarrative_TrueForFactionTags_FalseForRuntimeTags()
{
Assert.True(ScentTag.MawAffiliated.IsNarrative());
Assert.True(ScentTag.MerchantAffiliated.IsNarrative());
Assert.False(ScentTag.RecentlyKilled.IsNarrative());
Assert.False(ScentTag.Frightened.IsNarrative());
Assert.False(ScentTag.None.IsNarrative());
}
// ── Helpers ───────────────────────────────────────────────────────────
private NpcActor MakeResidentNpc(string clade, string species, string factionId)
{
var resident = new ResidentTemplateDef
{
Id = "test_npc",
Name = "Test NPC",
Clade = clade,
Species = species,
Faction = factionId,
Hp = 20,
};
return new NpcActor(resident) { Id = 1 };
}
}