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>
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user