Files
TheriapolisV3/Theriapolis.Tests/Entities/ScentTagTests.cs
T
Christopher Wiebe b451f83174 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>
2026-04-30 20:40:51 -07:00

215 lines
8.2 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 };
}
}