215 lines
8.2 KiB
C#
215 lines
8.2 KiB
C#
|
|
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 1–8) 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 };
|
|||
|
|
}
|
|||
|
|
}
|