diff --git a/src/AcDream.Core/Items/ClientObject.cs b/src/AcDream.Core/Items/ClientObject.cs index 04d9c873..cf08a6b4 100644 --- a/src/AcDream.Core/Items/ClientObject.cs +++ b/src/AcDream.Core/Items/ClientObject.cs @@ -216,6 +216,55 @@ public static class BurdenMath { public const int BurdenPerStrength = 150; + /// Augmentation bonus-burden per aug rank (retail EncumbranceCapacity, 0x1e). + public const int AugBurdenPerRank = 30; + /// Max augmentation bonus-burden contribution per Strength (retail clamp 0x96). + public const int AugBurdenCap = 150; + + /// + /// Retail EncumbranceSystem::EncumbranceCapacity(strength, aug) + /// (named-retail decomp 256393, 0x004fcc00): + /// strength <= 0 ? 0 : strength*150 + clamp(aug*30, 0, 150)*strength. + /// + public static int EncumbranceCapacity(int strength, int aug) + { + if (strength <= 0) return 0; + int bonus = aug * AugBurdenPerRank; + if (bonus < 0) bonus = 0; // decomp: eax_2 < 0 -> base only + if (bonus > AugBurdenCap) bonus = AugBurdenCap; + return strength * BurdenPerStrength + bonus * strength; + } + + /// + /// Retail EncumbranceSystem::Load(capacity, burden) (decomp 256413, + /// 0x004fcc40): the encumbrance ratio burden / capacity + /// (1.0 = at capacity, up to ~3.0). The decompiler mangled the FP divide to + /// return burden; the cap <= 0 guard + single divide is unambiguous. + /// Returns 0 when capacity <= 0 (no-data). + /// + public static float LoadRatio(int capacity, int burden) + => capacity <= 0 ? 0f : (float)burden / capacity; + + /// + /// Retail gmBackpackUI::SetLoadLevel bar fill (decomp 176542, + /// 0x004a6ea6): clamp(load * 0.3333…, 0, 1) — the bar is 1/3 full at + /// 100% capacity, full at 300%. + /// + public static float LoadToFill(float load) + { + float fill = load / 3f; + if (fill < 0f) return 0f; + return fill > 1f ? 1f : fill; + } + + /// + /// Retail gmBackpackUI::SetLoadLevel percent text (decomp 176576): after + /// arg2 = load/3, the text is floor(arg2 * 300) = floor(load * 100), + /// NOT clamped (reads up to 300%+). + /// + public static int LoadToPercent(float load) + => (int)System.MathF.Floor(load * 100f); + public static int ComputeMax(int strength, int bonusBurden) => BurdenPerStrength * strength + strength * bonusBurden; diff --git a/tests/AcDream.Core.Tests/Items/BurdenMathTests.cs b/tests/AcDream.Core.Tests/Items/BurdenMathTests.cs new file mode 100644 index 00000000..25cfa043 --- /dev/null +++ b/tests/AcDream.Core.Tests/Items/BurdenMathTests.cs @@ -0,0 +1,40 @@ +using AcDream.Core.Items; +using Xunit; + +namespace AcDream.Core.Tests.Items; + +public class BurdenMathTests +{ + [Theory] + [InlineData(100, 0, 15000)] // base: str*150 + [InlineData(100, 3, 24000)] // +clamp(3*30,0,150)=90 -> +90*100 + [InlineData(100, 10, 30000)] // 10*30=300 clamped to 150 -> +150*100 + [InlineData(0, 5, 0)] // str<=0 -> 0 + public void EncumbranceCapacity_matches_retail(int str, int aug, int expected) + => Assert.Equal(expected, BurdenMath.EncumbranceCapacity(str, aug)); + + [Theory] + [InlineData(15000, 7500, 0.5f)] + [InlineData(15000, 0, 0f)] + [InlineData(0, 100, 0f)] // cap<=0 guard + public void LoadRatio_is_burden_over_capacity(int cap, int burden, float expected) + => Assert.Equal(expected, BurdenMath.LoadRatio(cap, burden), 4); + + [Theory] + [InlineData(0f, 0f)] + [InlineData(0.5f, 0.16667f)] + [InlineData(1.0f, 0.33333f)] + [InlineData(3.0f, 1.0f)] // full at 300% + [InlineData(4.0f, 1.0f)] // clamped + public void LoadToFill_is_third_clamped(float load, float expected) + => Assert.Equal(expected, BurdenMath.LoadToFill(load), 4); + + [Theory] + [InlineData(0f, 0)] + [InlineData(0.5f, 50)] + [InlineData(1.0f, 100)] + [InlineData(3.0f, 300)] + [InlineData(4.0f, 400)] // percent NOT clamped + public void LoadToPercent_is_floor_load_times_100(float load, int expected) + => Assert.Equal(expected, BurdenMath.LoadToPercent(load)); +}