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,163 @@
|
||||
using Theriapolis.Core.Data;
|
||||
using Theriapolis.Core.Rules.Character;
|
||||
using Theriapolis.Core.Rules.Stats;
|
||||
using Theriapolis.Core.Util;
|
||||
using Xunit;
|
||||
|
||||
namespace Theriapolis.Tests.Rules;
|
||||
|
||||
/// <summary>
|
||||
/// CharacterBuilder smoke + integration: every (clade × species) pair,
|
||||
/// when assigned a representative class, produces a valid level-1 character
|
||||
/// with sane HP and ability totals.
|
||||
/// </summary>
|
||||
public sealed class CharacterBuilderTests
|
||||
{
|
||||
private readonly ContentResolver _content = new(new ContentLoader(TestHelpers.DataDirectory));
|
||||
|
||||
[Fact]
|
||||
public void Build_DefaultsProduceValidCharacter()
|
||||
{
|
||||
var c = MinimalBuilder("canidae", "wolf", "fangsworn", "pack_raised").Build();
|
||||
Assert.Equal("canidae", c.Clade.Id);
|
||||
Assert.Equal("wolf", c.Species.Id);
|
||||
Assert.Equal("fangsworn", c.ClassDef.Id);
|
||||
Assert.Equal(1, c.Level);
|
||||
Assert.Equal(0, c.Xp);
|
||||
Assert.True(c.MaxHp > 0);
|
||||
Assert.Equal(c.MaxHp, c.CurrentHp);
|
||||
Assert.True(c.SkillProficiencies.Count >= c.ClassDef.SkillsChoose);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_AppliesCladeAndSpeciesAbilityMods()
|
||||
{
|
||||
// Wolf-Folk: STR+1 (species), CON+1 WIS+1 (canid clade)
|
||||
// Standard Array assigned in class priority — we use a known base for
|
||||
// the test instead of relying on auto-assignment.
|
||||
var b = MinimalBuilder("canidae", "wolf", "fangsworn", "pack_raised");
|
||||
b.BaseAbilities = new AbilityScores(10, 10, 10, 10, 10, 10);
|
||||
var c = b.Build();
|
||||
Assert.Equal(11, c.Abilities.STR); // 10 + species
|
||||
Assert.Equal(10, c.Abilities.DEX);
|
||||
Assert.Equal(11, c.Abilities.CON); // 10 + clade
|
||||
Assert.Equal(10, c.Abilities.INT);
|
||||
Assert.Equal(11, c.Abilities.WIS); // 10 + clade
|
||||
Assert.Equal(10, c.Abilities.CHA);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_HpUsesHitDiePlusConModifier()
|
||||
{
|
||||
var b = MinimalBuilder("canidae", "wolf", "fangsworn", "pack_raised");
|
||||
b.BaseAbilities = new AbilityScores(10, 10, 14, 10, 10, 10); // CON 14 → +2 → +1 after canid (15→+2)
|
||||
var c = b.Build();
|
||||
// Wolf-Folk has no CON mod from species, canid +1 → final CON = 15 → +2.
|
||||
// Fangsworn d10 + 2 = 12.
|
||||
Assert.Equal(12, c.MaxHp);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_RejectsSpeciesNotInChosenClade()
|
||||
{
|
||||
var b = MinimalBuilder("canidae", "wolf", "fangsworn", "pack_raised");
|
||||
b.Species = _content.Species["lion"]; // felid species under canid clade
|
||||
bool ok = b.Validate(out var err);
|
||||
Assert.False(ok);
|
||||
Assert.Contains("clade", err.ToLowerInvariant());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_RequiresExactSkillCount()
|
||||
{
|
||||
var b = MinimalBuilder("canidae", "wolf", "fangsworn", "pack_raised");
|
||||
b.ChosenClassSkills.Clear();
|
||||
Assert.False(b.Validate(out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_RejectsSkillNotOfferedByClass()
|
||||
{
|
||||
var b = MinimalBuilder("canidae", "wolf", "fangsworn", "pack_raised");
|
||||
b.ChosenClassSkills.Clear();
|
||||
b.ChosenClassSkills.Add(SkillId.Athletics);
|
||||
b.ChosenClassSkills.Add(SkillId.Arcana); // Fangsworn does not offer Arcana
|
||||
Assert.False(b.Validate(out var err));
|
||||
Assert.Contains("arcana", err.ToLowerInvariant());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("canidae", "wolf", "fangsworn", "pack_raised")]
|
||||
[InlineData("felidae", "lion", "shadow_pelt", "borderland_stray")]
|
||||
[InlineData("ursidae", "brown_bear", "feral", "coliseum_survivor")]
|
||||
[InlineData("cervidae", "elk", "covenant_keeper", "covenant_enforcer")]
|
||||
[InlineData("bovidae", "bull", "bulwark", "herd_city_born")]
|
||||
[InlineData("leporidae", "rabbit", "muzzle_speaker", "warren_runner")]
|
||||
[InlineData("mustelidae","badger", "claw_wright", "borderland_stray")]
|
||||
[InlineData("felidae", "housecat", "scent_broker", "scent_suppressed")]
|
||||
public void Build_AllRepresentativeCombosValid(
|
||||
string cladeId, string speciesId, string classId, string bgId)
|
||||
{
|
||||
var c = MinimalBuilder(cladeId, speciesId, classId, bgId).Build();
|
||||
Assert.True(c.MaxHp > 0);
|
||||
Assert.True(c.IsAlive);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RollAbilityScores_IsDeterministicGivenSameInputs()
|
||||
{
|
||||
var a = CharacterBuilder.RollAbilityScores(0xCAFE, 1234);
|
||||
var b = CharacterBuilder.RollAbilityScores(0xCAFE, 1234);
|
||||
Assert.Equal(a.STR, b.STR);
|
||||
Assert.Equal(a.DEX, b.DEX);
|
||||
Assert.Equal(a.CON, b.CON);
|
||||
Assert.Equal(a.INT, b.INT);
|
||||
Assert.Equal(a.WIS, b.WIS);
|
||||
Assert.Equal(a.CHA, b.CHA);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RollAbilityScores_DifferentMsProducesDifferentResults()
|
||||
{
|
||||
var a = CharacterBuilder.RollAbilityScores(0xCAFE, 1234);
|
||||
var b = CharacterBuilder.RollAbilityScores(0xCAFE, 5678);
|
||||
// At least one of six should differ across plausibly-different rolls.
|
||||
Assert.True(
|
||||
a.STR != b.STR || a.DEX != b.DEX || a.CON != b.CON ||
|
||||
a.INT != b.INT || a.WIS != b.WIS || a.CHA != b.CHA);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Roll4d6DropLowest_ResultIs3to18()
|
||||
{
|
||||
var rng = new SeededRng(0xDEADBEEFUL);
|
||||
for (int i = 0; i < 1000; i++)
|
||||
{
|
||||
int v = CharacterBuilder.Roll4d6DropLowest(rng);
|
||||
Assert.InRange(v, 3, 18);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────
|
||||
|
||||
private CharacterBuilder MinimalBuilder(string cladeId, string speciesId, string classId, string bgId)
|
||||
{
|
||||
var b = new CharacterBuilder
|
||||
{
|
||||
Clade = _content.Clades[cladeId],
|
||||
Species = _content.Species[speciesId],
|
||||
ClassDef = _content.Classes[classId],
|
||||
Background = _content.Backgrounds[bgId],
|
||||
BaseAbilities = new AbilityScores(15, 14, 13, 12, 10, 8),
|
||||
Name = "Test",
|
||||
};
|
||||
// Auto-pick first N skill options
|
||||
int n = b.ClassDef.SkillsChoose;
|
||||
foreach (var raw in b.ClassDef.SkillOptions)
|
||||
{
|
||||
if (b.ChosenClassSkills.Count >= n) break;
|
||||
try { b.ChosenClassSkills.Add(SkillIdExtensions.FromJson(raw)); } catch { }
|
||||
}
|
||||
return b;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user