diff --git a/src/AcDream.App/Input/PlayerMovementController.cs b/src/AcDream.App/Input/PlayerMovementController.cs index 457760d..0c638a7 100644 --- a/src/AcDream.App/Input/PlayerMovementController.cs +++ b/src/AcDream.App/Input/PlayerMovementController.cs @@ -61,6 +61,7 @@ public sealed class PlayerMovementController private readonly PhysicsEngine _physics; private readonly PhysicsBody _body; private readonly MotionInterpreter _motion; + private readonly PlayerWeenie _weenie; public float MouseTurnSensitivity { get; set; } = 0.003f; @@ -112,7 +113,13 @@ public sealed class PlayerMovementController State = PhysicsStateFlags.Gravity | PhysicsStateFlags.ReportCollisions, }; - _motion = new MotionInterpreter(_body); + _weenie = new PlayerWeenie(runSkill: 200, jumpSkill: 100); + _motion = new MotionInterpreter(_body, _weenie); + } + + public void SetCharacterSkills(int runSkill, int jumpSkill) + { + _weenie.SetSkills(runSkill, jumpSkill); } /// diff --git a/src/AcDream.Core/Physics/PlayerWeenie.cs b/src/AcDream.Core/Physics/PlayerWeenie.cs new file mode 100644 index 0000000..014c96a --- /dev/null +++ b/src/AcDream.Core/Physics/PlayerWeenie.cs @@ -0,0 +1,81 @@ +namespace AcDream.Core.Physics; + +/// +/// IWeenieObject implementation for the local player. Provides skill-based +/// run rate and jump velocity calculations. +/// +/// Formulas from decompiled acclient.exe, cross-referenced against +/// ACE MovementSystem.GetRunRate and MovementSystem.GetJumpHeight. +/// +public sealed class PlayerWeenie : IWeenieObject +{ + private int _runSkill; + private int _jumpSkill; + private float _burden; + + public PlayerWeenie(int runSkill = 0, int jumpSkill = 0, float burden = 0f) + { + _runSkill = runSkill; + _jumpSkill = jumpSkill; + _burden = burden; + } + + public void SetSkills(int runSkill, int jumpSkill) + { + _runSkill = runSkill; + _jumpSkill = jumpSkill; + } + + public void SetBurden(float burden) => _burden = burden; + + public bool InqRunRate(out float rate) + { + rate = GetRunRate(_burden, _runSkill); + return true; + } + + public bool InqJumpVelocity(float extent, out float vz) + { + float height = GetJumpHeight(_burden, _jumpSkill, extent); + vz = MathF.Sqrt(height * 19.6f); + return true; + } + + public bool CanJump(float extent) => true; // burden/stamina checks deferred + + /// + /// RunRate = (burdenMod * (runSkill / (runSkill + 200)) * 11 + 4) / 4 + /// Capped at 4.5 when runSkill >= 800. + /// Source: decompiled + ACE MovementSystem.GetRunRate + /// + public static float GetRunRate(float burden, int runSkill) + { + if (runSkill >= 800) return 18f / 4f; + float loadMod = GetBurdenMod(burden); + return (loadMod * ((float)runSkill / (runSkill + 200) * 11f) + 4f) / 4f; + } + + /// + /// JumpHeight = burdenMod * (jumpSkill / (jumpSkill + 1300) * 22.2 + 0.05) * extent + /// Clamped to minimum 0.35m. + /// Source: decompiled + ACE MovementSystem.GetJumpHeight + /// + public static float GetJumpHeight(float burden, int jumpSkill, float extent) + { + extent = Math.Clamp(extent, 0f, 1f); + float loadMod = GetBurdenMod(burden); + float height = loadMod * ((float)jumpSkill / (jumpSkill + 1300f) * 22.2f + 0.05f) * extent; + return MathF.Max(height, 0.35f); + } + + /// + /// Encumbrance modifier: 1.0 when unloaded, linearly decreasing to 0 at 200%. + /// Source: decompiled + ACE EncumbranceSystem.GetBurdenMod + /// + public static float GetBurdenMod(float burden) + { + if (burden < 1f) return 1f; + if (burden < 2f) return 2f - burden; + return 0f; + } +} diff --git a/tests/AcDream.Core.Tests/Physics/PlayerWeenieTests.cs b/tests/AcDream.Core.Tests/Physics/PlayerWeenieTests.cs new file mode 100644 index 0000000..3430227 --- /dev/null +++ b/tests/AcDream.Core.Tests/Physics/PlayerWeenieTests.cs @@ -0,0 +1,84 @@ +using AcDream.Core.Physics; +using Xunit; + +namespace AcDream.Core.Tests.Physics; + +public class PlayerWeenieTests +{ + [Fact] + public void InqRunRate_Skill200_ReturnsCorrectRate() + { + var pw = new PlayerWeenie(runSkill: 200, jumpSkill: 100); + Assert.True(pw.InqRunRate(out float rate)); + Assert.Equal(2.375f, rate, precision: 3); + } + + [Fact] + public void InqRunRate_Skill800_ReturnsCap() + { + var pw = new PlayerWeenie(runSkill: 800, jumpSkill: 100); + Assert.True(pw.InqRunRate(out float rate)); + Assert.Equal(4.5f, rate, precision: 3); + } + + [Fact] + public void InqRunRate_Skill0_ReturnsBase() + { + var pw = new PlayerWeenie(runSkill: 0, jumpSkill: 100); + Assert.True(pw.InqRunRate(out float rate)); + Assert.Equal(1.0f, rate, precision: 3); + } + + [Fact] + public void InqJumpVelocity_FullExtent_Skill100() + { + var pw = new PlayerWeenie(runSkill: 100, jumpSkill: 100); + Assert.True(pw.InqJumpVelocity(1.0f, out float vz)); + // height = (100/1400) * 22.2 + 0.05 ≈ 1.636 + // vz = sqrt(1.636 * 19.6) ≈ 5.663 + Assert.Equal(5.663f, vz, precision: 1); + } + + [Fact] + public void InqJumpVelocity_HalfExtent_Skill100() + { + var pw = new PlayerWeenie(runSkill: 100, jumpSkill: 100); + Assert.True(pw.InqJumpVelocity(0.5f, out float vz)); + // height = (100/1400) * 22.2 * 0.5 + 0.05 ≈ 0.843 + float expectedHeight = 1.0f * (100f / 1400f * 22.2f + 0.05f) * 0.5f; + float expectedVz = MathF.Sqrt(expectedHeight * 19.6f); + Assert.Equal(expectedVz, vz, precision: 2); + } + + [Fact] + public void InqJumpVelocity_ZeroSkill_ClampsToMinHeight() + { + var pw = new PlayerWeenie(runSkill: 0, jumpSkill: 0); + Assert.True(pw.InqJumpVelocity(1.0f, out float vz)); + // height = max(0.05 * 1.0, 0.35) = 0.35 + // vz = sqrt(0.35 * 19.6) ≈ 2.619 + Assert.Equal(MathF.Sqrt(0.35f * 19.6f), vz, precision: 2); + } + + [Fact] + public void GetBurdenMod_Unencumbered_Returns1() + { + Assert.Equal(1.0f, PlayerWeenie.GetBurdenMod(0f)); + Assert.Equal(1.0f, PlayerWeenie.GetBurdenMod(0.5f)); + Assert.Equal(1.0f, PlayerWeenie.GetBurdenMod(0.99f)); + } + + [Fact] + public void GetBurdenMod_Overloaded_Returns0() + { + Assert.Equal(0.0f, PlayerWeenie.GetBurdenMod(2.0f)); + Assert.Equal(0.0f, PlayerWeenie.GetBurdenMod(3.0f)); + } + + [Fact] + public void GetBurdenMod_PartialBurden_LinearDecrease() + { + Assert.Equal(0.5f, PlayerWeenie.GetBurdenMod(1.5f), precision: 3); + Assert.Equal(0.75f, PlayerWeenie.GetBurdenMod(1.25f), precision: 3); + } +}