Files
TheriapolisV3/Theriapolis.Tests/Data/ContentLoadTests.cs
T

194 lines
7.0 KiB
C#
Raw Normal View History

using Theriapolis.Core.Data;
using Theriapolis.Core.Rules.Stats;
using Xunit;
namespace Theriapolis.Tests.Data;
/// <summary>
/// End-to-end content load + cross-file integrity tests for Phase 5 JSON.
/// If a JSON edit breaks any of these, ContentValidate will also fail in CI.
/// </summary>
public sealed class ContentLoadTests
{
private static ContentLoader Loader() => new(TestHelpers.DataDirectory);
[Fact]
public void AllPhase5ContentFiles_LoadCleanly()
{
var loader = Loader();
var clades = loader.LoadClades();
var species = loader.LoadSpecies(clades);
var classes = loader.LoadClasses();
var subs = loader.LoadSubclasses(classes);
var bgs = loader.LoadBackgrounds();
var items = loader.LoadItems();
var npcs = loader.LoadNpcTemplates(items);
Assert.Equal(7, clades.Length);
Assert.True(species.Length >= 19, $"expected ≥19 species, got {species.Length}");
Assert.Equal(8, classes.Length);
Assert.Equal(16, subs.Length); // 8 classes × 2 subclasses
Assert.Equal(12, bgs.Length);
Assert.True(items.Length >= 30, $"expected ≥30 items, got {items.Length}");
Assert.True(npcs.Templates.Length >= 9, $"expected ≥9 NPC templates, got {npcs.Templates.Length}");
}
[Fact]
public void EveryClass_HasLevel1FeaturesDefined()
{
var classes = Loader().LoadClasses();
foreach (var c in classes)
{
var lv1 = Array.Find(c.LevelTable, e => e.Level == 1);
Assert.NotNull(lv1);
Assert.NotEmpty(lv1!.Features);
foreach (var feat in lv1.Features)
Assert.True(c.FeatureDefinitions.ContainsKey(feat),
$"Class '{c.Id}' level 1 references undefined feature '{feat}'");
}
}
[Fact]
public void EveryClass_HasFullLevelTable()
{
var classes = Loader().LoadClasses();
foreach (var c in classes)
{
var levels = c.LevelTable.Select(e => e.Level).OrderBy(x => x).ToArray();
Assert.Equal(20, levels.Length);
for (int lv = 1; lv <= 20; lv++)
Assert.Contains(lv, levels);
}
}
[Fact]
public void EveryClass_LevelTableProficiencyBonusMatchesD20()
{
var classes = Loader().LoadClasses();
foreach (var c in classes)
foreach (var entry in c.LevelTable)
Assert.Equal(ProficiencyBonus.ForLevel(entry.Level), entry.ProficiencyBonus);
}
[Fact]
public void EveryClass_HasTwoSubclasses()
{
var classes = Loader().LoadClasses();
var subs = Loader().LoadSubclasses(classes);
foreach (var c in classes)
{
var matching = subs.Where(s => s.ClassId == c.Id).ToArray();
Assert.Equal(2, matching.Length);
// Subclass ids must match what the class declares
foreach (var sid in c.SubclassIds)
Assert.Contains(sid, matching.Select(s => s.Id));
}
}
[Fact]
public void EverySpecies_ReferencesARealClade()
{
var clades = Loader().LoadClades();
var species = Loader().LoadSpecies(clades);
var cladeIds = clades.Select(c => c.Id).ToHashSet(StringComparer.OrdinalIgnoreCase);
foreach (var sp in species)
Assert.Contains(sp.CladeId, cladeIds);
}
[Theory]
[InlineData("wolf", 1, 0, 1, 0, 1, 0)]
[InlineData("fox", 0, 1, 1, 0, 1, 0)]
[InlineData("coyote", 0, 0, 1, 0, 1, 1)]
[InlineData("lion", 1, 1, 0, 0, 0, 1)]
[InlineData("leopard", 0, 2, 0, 0, 0, 1)]
[InlineData("housecat", 0, 1, 0, 1, 0, 1)]
[InlineData("ferret", 0, 1, 0, 1, 0, 1)]
[InlineData("badger", 0, 1, 1, 1, 0, 0)]
[InlineData("wolverine", 1, 1, 0, 1, 0, 0)]
[InlineData("brown_bear", 1,-1, 2, 0, 0, 0)]
[InlineData("polar_bear", 0,-1, 2, 0, 1, 0)]
[InlineData("elk", 1, 1, 0, 0, 1, 0)]
[InlineData("deer", 0, 2, 0, 0, 1, 0)]
[InlineData("moose", 0, 1, 1, 0, 1, 0)]
[InlineData("rabbit", -1, 2, 0, 0, 1, 0)]
[InlineData("hare", -1, 2, 1, 0, 0, 0)]
[InlineData("bull", 2, 0, 1, 0, 0, 0)]
[InlineData("sheep", 1, 0, 1, 0, 1, 0)]
[InlineData("goat", 1, 0, 1, 0, 1, 0)]
[InlineData("bison", 1, 0, 2, 0, 0, 0)]
public void Clade_Plus_Species_AbilityMods_MatchQuickRefTable(
string speciesId, int str, int dex, int con, int @int, int wis, int cha)
{
var loader = Loader();
var clades = loader.LoadClades();
var species = loader.LoadSpecies(clades);
var sp = species.Single(s => s.Id == speciesId);
var cl = clades.Single(c => c.Id == sp.CladeId);
int Sum(string ability) =>
(cl.AbilityMods.TryGetValue(ability, out var c) ? c : 0) +
(sp.AbilityMods.TryGetValue(ability, out var s) ? s : 0);
Assert.Equal(str, Sum("STR"));
Assert.Equal(dex, Sum("DEX"));
Assert.Equal(con, Sum("CON"));
Assert.Equal(@int, Sum("INT"));
Assert.Equal(wis, Sum("WIS"));
Assert.Equal(cha, Sum("CHA"));
}
[Fact]
public void EveryWeapon_HasDamageAndType()
{
var items = Loader().LoadItems();
foreach (var i in items.Where(i => i.Kind == "weapon"))
{
Assert.False(string.IsNullOrWhiteSpace(i.Damage), $"weapon '{i.Id}' missing damage");
Assert.False(string.IsNullOrWhiteSpace(i.DamageType), $"weapon '{i.Id}' missing damage_type");
}
}
[Fact]
public void EveryArmor_HasPositiveAcBase()
{
var items = Loader().LoadItems();
foreach (var i in items.Where(i => i.Kind == "armor"))
Assert.True(i.AcBase > 0, $"armor '{i.Id}' has non-positive ac_base");
}
[Fact]
public void NpcZoneTable_HasOneEntryPerZone()
{
var items = Loader().LoadItems();
var npcs = Loader().LoadNpcTemplates(items);
int expected = Theriapolis.Core.C.DANGER_ZONE_MAX - Theriapolis.Core.C.DANGER_ZONE_MIN + 1;
foreach (var (kind, byZone) in npcs.SpawnKindToTemplateByZone)
Assert.Equal(expected, byZone.Length);
}
[Fact]
public void NpcZoneTable_AllReferencedTemplatesExist()
{
var items = Loader().LoadItems();
var npcs = Loader().LoadNpcTemplates(items);
var ids = npcs.Templates.Select(t => t.Id).ToHashSet();
foreach (var (_, byZone) in npcs.SpawnKindToTemplateByZone)
foreach (var tid in byZone)
Assert.Contains(tid, ids);
}
[Fact]
public void EveryNpcTemplate_HasPositiveHpAndAc()
{
var items = Loader().LoadItems();
var npcs = Loader().LoadNpcTemplates(items);
foreach (var t in npcs.Templates)
{
Assert.True(t.Hp > 0, $"NPC '{t.Id}' has non-positive HP");
Assert.True(t.Ac > 0, $"NPC '{t.Id}' has non-positive AC");
}
}
}