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));
+}