Files

117 lines
4.0 KiB
C#
Raw Permalink Normal View History

using Theriapolis.Core.Data;
using Theriapolis.Core.Rules.Character;
using Xunit;
namespace Theriapolis.Tests.Rules;
/// <summary>
/// Phase 6.5 M2 — SubclassResolver covers the pure look-up surface
/// (subclass id → unlocked feature ids per level, feature def lookup).
/// All subclass mechanics are JSON-driven; tests run against the live
/// content set so authoring drift is caught here.
/// </summary>
public sealed class SubclassResolverTests
{
private readonly ContentResolver _content = new(new ContentLoader(TestHelpers.DataDirectory));
[Fact]
public void TryFindSubclass_ReturnsDefForKnownId()
{
var def = SubclassResolver.TryFindSubclass(_content.Subclasses, "pack_forged");
Assert.NotNull(def);
Assert.Equal("fangsworn", def!.ClassId);
}
[Fact]
public void TryFindSubclass_NullForEmptyId()
{
Assert.Null(SubclassResolver.TryFindSubclass(_content.Subclasses, ""));
Assert.Null(SubclassResolver.TryFindSubclass(_content.Subclasses, null));
}
[Fact]
public void TryFindSubclass_NullForUnknownId()
{
Assert.Null(SubclassResolver.TryFindSubclass(_content.Subclasses, "definitely_not_a_subclass"));
}
[Fact]
public void UnlockedFeaturesAt_Level3_ReturnsFirstFeature()
{
var features = SubclassResolver.UnlockedFeaturesAt(_content.Subclasses, "pack_forged", 3);
Assert.NotEmpty(features);
Assert.Contains("packmates_howl", features);
}
[Fact]
public void UnlockedFeaturesAt_LevelWithoutEntry_ReturnsEmpty()
{
// Pack-Forged has entries at L3, L7, L10, L15, L18 — L4 is empty.
var features = SubclassResolver.UnlockedFeaturesAt(_content.Subclasses, "pack_forged", 4);
Assert.Empty(features);
}
[Fact]
public void UnlockedFeaturesAt_NullSubclassId_ReturnsEmpty()
{
Assert.Empty(SubclassResolver.UnlockedFeaturesAt(_content.Subclasses, null, 3));
Assert.Empty(SubclassResolver.UnlockedFeaturesAt(_content.Subclasses, "", 3));
}
[Fact]
public void ResolveFeatureDef_FindsSubclassFeature()
{
var subclass = _content.Subclasses["pack_forged"];
var classDef = _content.Classes["fangsworn"];
var fdef = SubclassResolver.ResolveFeatureDef(classDef, subclass, "packmates_howl");
Assert.NotNull(fdef);
Assert.Equal("Packmate's Howl", fdef!.Name);
}
[Fact]
public void ResolveFeatureDef_FallsThroughToClassDefForSharedIds()
{
var subclass = _content.Subclasses["pack_forged"];
var classDef = _content.Classes["fangsworn"];
// 'asi' is in the class feature_definitions (shared across subclasses).
var fdef = SubclassResolver.ResolveFeatureDef(classDef, subclass, "asi");
Assert.NotNull(fdef);
}
[Fact]
public void ResolveFeatureDef_NullForUnknownId()
{
var subclass = _content.Subclasses["pack_forged"];
var classDef = _content.Classes["fangsworn"];
Assert.Null(SubclassResolver.ResolveFeatureDef(classDef, subclass, "completely_made_up_feature"));
}
[Fact]
public void EveryClass_HasAtLeastOneSubclass()
{
// Smoke: every class declared in classes.json should resolve to at
// least one entry in subclasses.json.
foreach (var cls in _content.Classes.Values)
{
Assert.NotEmpty(cls.SubclassIds);
foreach (var sid in cls.SubclassIds)
{
Assert.True(_content.Subclasses.ContainsKey(sid),
$"class '{cls.Id}' references unknown subclass '{sid}'");
}
}
}
[Fact]
public void EverySubclass_HasLevel3Features()
{
// M2 ship-point: every authored subclass should have at least one
// level-3 feature so the L3 unlock fires meaningfully.
foreach (var sub in _content.Subclasses.Values)
{
var l3 = SubclassResolver.UnlockedFeaturesAt(_content.Subclasses, sub.Id, 3);
Assert.NotEmpty(l3);
}
}
}