using AcDream.App.Net; using AcDream.Core.CharGen; using DatReaderWriter.DBObjs; using DatReaderWriter.Enums; using DatReaderWriter.Types; namespace AcDream.App.Tests.Net; public sealed class RetailSkillFormulaTests { [Fact] public void ZeroDivisorIsTheOnlyFormulaFailureGate() { SkillFormula invalid = Formula(w: 7, x: 1, y: 1, z: 0); Assert.False(RetailSkillFormula.TryCalculate(invalid, 10u, 20u, out uint invalidResult)); Assert.Equal(0u, invalidResult); SkillFormula zeroX = Formula(w: 7, x: 0, y: 2, z: 3); Assert.True(RetailSkillFormula.TryCalculate(zeroX, 10u, 4u, out uint validResult)); Assert.Equal(5u, validResult); } [Theory] [InlineData(9u, 4u, 2u)] [InlineData(10u, 4u, 3u)] [InlineData(11u, 4u, 3u)] [InlineData(12u, 4u, 3u)] public void DivisionRoundsToNearestWithExactHalvesUp( uint numerator, uint divisor, uint expected) { SkillFormula formula = Formula(w: 0, x: 1, y: 0, z: divisor); Assert.True(RetailSkillFormula.TryCalculate( formula, numerator, 0u, out uint result)); Assert.Equal(expected, result); } [Fact] public void AdditiveWordIsInsideTheNumerator() { SkillFormula formula = Formula(w: 3, x: 1, y: 1, z: 4); Assert.True(RetailSkillFormula.TryCalculate(formula, 4u, 2u, out uint result)); Assert.Equal(2u, result); } [Fact] public void NumeratorUsesUnsignedThirtyTwoBitWrap() { SkillFormula formula = Formula(w: 1, x: 2, y: 0, z: 2); Assert.True(RetailSkillFormula.TryCalculate( formula, uint.MaxValue, 0u, out uint result)); Assert.Equal(0x80000000u, result); } [Fact] public void SignedDatStorageIsReinterpretedAsRetailUnsignedWords() { SkillFormula formula = Formula(w: -1, x: 0, y: 0, z: 1); Assert.True(RetailSkillFormula.TryCalculate(formula, 0u, 0u, out uint result)); Assert.Equal(uint.MaxValue, result); } [Fact] public void LiveResolverLooksUpTheDatFormulaAndTreatsMissingAttributesAsZero() { const uint skillId = 0x345u; const uint firstAttributeId = 1u; const uint secondAttributeId = 2u; var skillTable = new SkillTable(); skillTable.Skills.Add((SkillId)skillId, new SkillBase { Formula = new SkillFormula { AdditiveBonus = 1, Attribute1Multiplier = 1, Attribute2Multiplier = 2, Divisor = 3, Attribute1 = (AttributeId)firstAttributeId, Attribute2 = (AttributeId)secondAttributeId, }, }); var resolver = new LiveSkillCreditResolver(skillTable); uint result = resolver.Resolve( skillId, new Dictionary { [firstAttributeId] = 8u }); Assert.Equal(3u, result); Assert.Equal(0u, resolver.Resolve(0x999u, new Dictionary())); } private static SkillFormula Formula(int w, int x, int y, uint z) => new() { AdditiveBonus = w, Attribute1Multiplier = x, Attribute2Multiplier = y, Divisor = unchecked((int)z), }; // ── CC5 re-review residual round, R2 (2026-08-16): RetailSkillFormula. // ── CalculateChargenScore / ChargenSkillScoreResolver had zero direct // ── coverage — the F12(d) test substitutes `skillId * 10` instead of // ── exercising the real formula. ───────────────────────────────────── /// /// 's level bonus: /// Untrained adds nothing, Trained adds 5, Specialized adds 10, on top /// of the SAME base result /// (formula here: w=0, x=1, y=0, z=1 against /// attribute1=5 -> base 5). /// [Theory] [InlineData(ChargenSkillAdvancementClass.Untrained, 5u)] [InlineData(ChargenSkillAdvancementClass.Trained, 10u)] [InlineData(ChargenSkillAdvancementClass.Specialized, 15u)] public void CalculateChargenScore_AddsTheLevelBonusOnTopOfTheBaseFormula( ChargenSkillAdvancementClass level, uint expected) { var skillBase = new SkillBase { Formula = Formula(w: 0, x: 1, y: 0, z: 1) }; uint result = RetailSkillFormula.CalculateChargenScore(skillBase, attribute1: 5u, attribute2: 0u, level); Assert.Equal(expected, result); } /// /// The divisor-zero skip path: 's /// own failure gate short-circuits /// to a flat 0 BEFORE the level bonus is ever added — even for /// Specialized, which would otherwise add 10. /// [Fact] public void CalculateChargenScore_ZeroDivisor_ReturnsZero_RegardlessOfLevel() { var skillBase = new SkillBase { Formula = Formula(w: 7, x: 1, y: 1, z: 0) }; Assert.Equal(0u, RetailSkillFormula.CalculateChargenScore( skillBase, 10u, 20u, ChargenSkillAdvancementClass.Untrained)); Assert.Equal(0u, RetailSkillFormula.CalculateChargenScore( skillBase, 10u, 20u, ChargenSkillAdvancementClass.Trained)); Assert.Equal(0u, RetailSkillFormula.CalculateChargenScore( skillBase, 10u, 20u, ChargenSkillAdvancementClass.Specialized)); } /// /// 's six-way /// ResolveAttribute switch — one case per /// (Strength=1..Self=6) — resolving through a /// formula that reads ONLY Attribute1 (x=1, y=0) so the /// result unambiguously reports which of the six /// fields the switch actually read. /// [Theory] [InlineData(AttributeId.Strength)] [InlineData(AttributeId.Endurance)] [InlineData(AttributeId.Coordination)] [InlineData(AttributeId.Quickness)] [InlineData(AttributeId.Focus)] [InlineData(AttributeId.Self)] public void ChargenSkillScoreResolver_ResolvesEachAttributeIdThroughTheSixWaySwitch( AttributeId attributeId) { const uint skillId = 0x10u; var skillTable = new SkillTable(); skillTable.Skills.Add((SkillId)skillId, new SkillBase { Formula = new SkillFormula { AdditiveBonus = 0, Attribute1Multiplier = 1, Attribute2Multiplier = 0, Divisor = 1, Attribute1 = attributeId, // Attribute2 deliberately left at its zero default (Strength) — // Attribute2Multiplier=0 means whatever it reads contributes // nothing, so it cannot mask a wrong Attribute1 case. }, }); var resolver = new ChargenSkillScoreResolver(skillTable); ChargenAttributeValues attributes = AttributeValuesWith(attributeId, 42); uint result = resolver.Resolve(skillId, attributes, ChargenSkillAdvancementClass.Untrained); Assert.Equal(42u, result); } private static ChargenAttributeValues AttributeValuesWith(AttributeId attributeId, int value) => attributeId switch { AttributeId.Strength => new ChargenAttributeValues(value, 0, 0, 0, 0, 0), AttributeId.Endurance => new ChargenAttributeValues(0, value, 0, 0, 0, 0), AttributeId.Coordination => new ChargenAttributeValues(0, 0, value, 0, 0, 0), AttributeId.Quickness => new ChargenAttributeValues(0, 0, 0, value, 0, 0), AttributeId.Focus => new ChargenAttributeValues(0, 0, 0, 0, value, 0), AttributeId.Self => new ChargenAttributeValues(0, 0, 0, 0, 0, value), _ => default, }; }