117 lines
4.0 KiB
C#
117 lines
4.0 KiB
C#
|
|
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);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|