b451f83174
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>
123 lines
4.5 KiB
C#
123 lines
4.5 KiB
C#
using Theriapolis.Core.Data;
|
|
using Theriapolis.Core.Items;
|
|
using Theriapolis.Core.Rules.Character;
|
|
using Theriapolis.Core.Rules.Stats;
|
|
using Xunit;
|
|
|
|
namespace Theriapolis.Tests.Rules;
|
|
|
|
/// <summary>
|
|
/// Verifies that every class's <c>starting_kit</c> in classes.json
|
|
/// references real items, that auto-equipped items land in their declared
|
|
/// slot, and that fresh characters arrive armed and armoured.
|
|
/// </summary>
|
|
public sealed class StartingKitTests
|
|
{
|
|
private readonly ContentResolver _content = new(new ContentLoader(TestHelpers.DataDirectory));
|
|
|
|
[Fact]
|
|
public void EveryClass_HasNonEmptyStartingKit()
|
|
{
|
|
foreach (var c in _content.Classes.Values)
|
|
Assert.NotEmpty(c.StartingKit);
|
|
}
|
|
|
|
[Fact]
|
|
public void EveryStartingKitItem_ReferencesARealItem()
|
|
{
|
|
foreach (var c in _content.Classes.Values)
|
|
foreach (var entry in c.StartingKit)
|
|
Assert.True(
|
|
_content.Items.ContainsKey(entry.ItemId),
|
|
$"Class '{c.Id}' starting_kit references unknown item '{entry.ItemId}'");
|
|
}
|
|
|
|
[Fact]
|
|
public void EveryAutoEquipEntry_HasValidEquipSlot()
|
|
{
|
|
foreach (var c in _content.Classes.Values)
|
|
foreach (var entry in c.StartingKit)
|
|
{
|
|
if (!entry.AutoEquip) continue;
|
|
Assert.False(
|
|
string.IsNullOrEmpty(entry.EquipSlot),
|
|
$"Class '{c.Id}' auto-equip entry '{entry.ItemId}' has empty equip_slot");
|
|
Assert.NotNull(EquipSlotExtensions.FromJson(entry.EquipSlot));
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("fangsworn", "rend_sword", "chain_shirt", "buckler")]
|
|
[InlineData("bulwark", "hoof_club", "chain_mail", "standard_shield")]
|
|
[InlineData("covenant_keeper", "rend_sword", "chain_shirt", "standard_shield")]
|
|
[InlineData("claw_wright", "hoof_club", "studded_leather", "buckler")]
|
|
public void StartingKit_AppliedAndEquipped_FullKit(
|
|
string classId, string mainHand, string body, string offHand)
|
|
{
|
|
var c = BuildWithKit(classId);
|
|
AssertEquipped(c, EquipSlot.MainHand, mainHand);
|
|
AssertEquipped(c, EquipSlot.Body, body);
|
|
AssertEquipped(c, EquipSlot.OffHand, offHand);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("feral", "paw_axe", "hide_vest")]
|
|
[InlineData("shadow_pelt", "thorn_blade", "studded_leather")]
|
|
[InlineData("scent_broker", "fang_knife", "leather_harness")]
|
|
[InlineData("muzzle_speaker", "fang_knife", "studded_leather")]
|
|
public void StartingKit_AppliedAndEquipped_NoShield(string classId, string mainHand, string body)
|
|
{
|
|
var c = BuildWithKit(classId);
|
|
AssertEquipped(c, EquipSlot.MainHand, mainHand);
|
|
AssertEquipped(c, EquipSlot.Body, body);
|
|
Assert.Null(c.Inventory.GetEquipped(EquipSlot.OffHand));
|
|
}
|
|
|
|
[Fact]
|
|
public void StartingKit_Skipped_WhenItemsTableNotPassed()
|
|
{
|
|
var c = MakeBuilder("fangsworn").Build(); // no items dict → no kit applied
|
|
Assert.Empty(c.Inventory.Items);
|
|
}
|
|
|
|
[Fact]
|
|
public void StartingKit_ProducesPositiveAcOverUnarmoredBaseline()
|
|
{
|
|
var c = BuildWithKit("fangsworn");
|
|
int armored = DerivedStats.ArmorClass(c);
|
|
Assert.True(armored >= 14, $"Fangsworn starting kit should produce AC ≥ 14 (chain shirt + buckler), got {armored}");
|
|
}
|
|
|
|
private Character BuildWithKit(string classId)
|
|
=> MakeBuilder(classId).Build(_content.Items);
|
|
|
|
private CharacterBuilder MakeBuilder(string classId)
|
|
{
|
|
var classDef = _content.Classes[classId];
|
|
var b = new CharacterBuilder
|
|
{
|
|
Clade = _content.Clades["canidae"],
|
|
Species = _content.Species["wolf"],
|
|
ClassDef = classDef,
|
|
Background = _content.Backgrounds["pack_raised"],
|
|
BaseAbilities = new AbilityScores(15, 14, 13, 12, 10, 8),
|
|
Name = "KitTest",
|
|
};
|
|
// Pick the right number of skills for this class.
|
|
int n = classDef.SkillsChoose;
|
|
foreach (var raw in classDef.SkillOptions)
|
|
{
|
|
if (b.ChosenClassSkills.Count >= n) break;
|
|
try { b.ChosenClassSkills.Add(SkillIdExtensions.FromJson(raw)); } catch { }
|
|
}
|
|
return b;
|
|
}
|
|
|
|
private static void AssertEquipped(Character c, EquipSlot slot, string expectedItemId)
|
|
{
|
|
var inst = c.Inventory.GetEquipped(slot);
|
|
Assert.NotNull(inst);
|
|
Assert.Equal(expectedItemId, inst!.Def.Id);
|
|
}
|
|
}
|