39117a09ed
Species variants populated against the M6.13 schema: - Lion-Folk sex axis: Mane Guard (male) / Huntress Reflexes (female, +5 ft speed + advantage on initiative). - Elk-Folk sex axis: Antler Combat with 10 ft reach when full rack (male, retains seasonal Antler Drag) / Kick (female, prone on crit). Base traits restored to doc canon: Herd Coordination (Help → +3) + Endurance Runner (40 ft + advantage CON vs forced march); base speed bumped 30 → 40; new base detriment Herd Instinct. Ram-Folk replaced with separate Sheep-Folk + Goat-Folk species rather than a lineage-axis variant on a single Ram entry. Bovidae now has 4 species. The lineage-axis toggle UI in StepSpecies BuildCard rolled back; the schema stays for sex-axis (Lion/Elk) which auto-resolves. ContentLoadTests + HybridCharacterTests updated; Size.cs comment too. Calling lore: ClassDef gains Description; classes.json populated for all 8 callings with the doc's italic blockquote + paragraph profile. StepClass surfaces the description on the card. Card layout uniformity: StepClass / StepSubclass / StepBackground all switched to single-column ExpandFill grids (matching StepClade / StepSpecies). Each card now spans the wizard's content width so the description and feature chips have room to breathe. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
194 lines
7.0 KiB
C#
194 lines
7.0 KiB
C#
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");
|
||
}
|
||
}
|
||
}
|