feat(physics): PlayerWeenie with retail Run/Jump formulas
Implements IWeenieObject with GetRunRate and GetJumpHeight from decompiled client, cross-referenced against ACE MovementSystem. Default skills (Run=200, Jump=100) used until skill parsing ships. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
c7fa1d36fb
commit
5cb14da714
3 changed files with 173 additions and 1 deletions
|
|
@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
81
src/AcDream.Core/Physics/PlayerWeenie.cs
Normal file
81
src/AcDream.Core/Physics/PlayerWeenie.cs
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
namespace AcDream.Core.Physics;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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
|
||||
|
||||
/// <summary>
|
||||
/// RunRate = (burdenMod * (runSkill / (runSkill + 200)) * 11 + 4) / 4
|
||||
/// Capped at 4.5 when runSkill >= 800.
|
||||
/// Source: decompiled + ACE MovementSystem.GetRunRate
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// JumpHeight = burdenMod * (jumpSkill / (jumpSkill + 1300) * 22.2 + 0.05) * extent
|
||||
/// Clamped to minimum 0.35m.
|
||||
/// Source: decompiled + ACE MovementSystem.GetJumpHeight
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Encumbrance modifier: 1.0 when unloaded, linearly decreasing to 0 at 200%.
|
||||
/// Source: decompiled + ACE EncumbranceSystem.GetBurdenMod
|
||||
/// </summary>
|
||||
public static float GetBurdenMod(float burden)
|
||||
{
|
||||
if (burden < 1f) return 1f;
|
||||
if (burden < 2f) return 2f - burden;
|
||||
return 0f;
|
||||
}
|
||||
}
|
||||
84
tests/AcDream.Core.Tests/Physics/PlayerWeenieTests.cs
Normal file
84
tests/AcDream.Core.Tests/Physics/PlayerWeenieTests.cs
Normal file
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue