using AcDream.Core.CharGen;
namespace AcDream.Core.Tests.CharGen;
///
/// Tests for — retail's skill-credit
/// spend port (CharGenState::UpdateRemainingSkillCredits @
/// 0x005C37C0).
///
public sealed class ChargenSkillCreditMathTests
{
private static readonly Dictionary Costs = new()
{
[1u] = new ChargenSkillCost(1u, NormalCost: 4, PrimaryCost: 12), // Axe
[11u] = new ChargenSkillCost(11u, NormalCost: 4, PrimaryCost: 12), // Sword
[24u] = new ChargenSkillCost(24u, NormalCost: 1, PrimaryCost: 3), // Run
// Deliberately no entry for skill id 2 (Bow) — heritage doesn't offer it.
};
[Fact]
public void ComputeSpent_IgnoresInactiveAndUntrainedSkills()
{
var advancement = new ChargenSkillAdvancementSet
{
[1u] = ChargenSkillAdvancementClass.Inactive,
[11u] = ChargenSkillAdvancementClass.Untrained,
};
Assert.Equal(0, ChargenSkillCreditMath.ComputeSpent(advancement, Costs));
}
[Fact]
public void ComputeSpent_ChargesNormalCostForTrainedSkills()
{
var advancement = new ChargenSkillAdvancementSet { [1u] = ChargenSkillAdvancementClass.Trained };
Assert.Equal(4, ChargenSkillCreditMath.ComputeSpent(advancement, Costs));
}
[Fact]
public void ComputeSpent_ChargesPrimaryCostInsteadOfNormalCostForSpecializedSkills()
{
// Retail adds exactly one of NormalCost/PrimaryCost per skill, never
// both — PrimaryCost is the TOTAL cost to reach Specialized.
var advancement = new ChargenSkillAdvancementSet { [1u] = ChargenSkillAdvancementClass.Specialized };
Assert.Equal(12, ChargenSkillCreditMath.ComputeSpent(advancement, Costs));
}
[Fact]
public void ComputeSpent_SumsAcrossMultipleTrainedAndSpecializedSkills()
{
var advancement = new ChargenSkillAdvancementSet
{
[1u] = ChargenSkillAdvancementClass.Trained, // 4
[11u] = ChargenSkillAdvancementClass.Specialized, // 12
[24u] = ChargenSkillAdvancementClass.Trained, // 1
};
Assert.Equal(17, ChargenSkillCreditMath.ComputeSpent(advancement, Costs));
}
[Fact]
public void ComputeSpent_SkillWithNoCostEntryIsSkippedDefensively()
{
var advancement = new ChargenSkillAdvancementSet { [2u] = ChargenSkillAdvancementClass.Trained };
Assert.Equal(0, ChargenSkillCreditMath.ComputeSpent(advancement, Costs));
}
[Fact]
public void RemainingCredits_IsTotalMinusSpent_AndMayGoNegativeUnlikeAttributes()
{
var advancement = new ChargenSkillAdvancementSet
{
[1u] = ChargenSkillAdvancementClass.Trained,
[11u] = ChargenSkillAdvancementClass.Specialized,
};
Assert.Equal(84, ChargenSkillCreditMath.RemainingCredits(100u, advancement, Costs));
// Retail's Finish gate never checks remainingSkillCredits, so
// overspending relative to the (small, synthetic) budget below is a
// representable state, not a thrown exception.
Assert.Equal(-16, ChargenSkillCreditMath.RemainingCredits(0u, advancement, Costs));
}
}