Animate the player: Rukhanka skeletal locomotion (client-derived)
Replace the capsule with a rigged Synty SciFiSpace soldier driven by Rukhanka 2.9 (netcode replication off; animation derived client-side from replicated state). Adds a slim top-down AnimatorController (idle / 2D-strafe locomotion / death) from Synty clips; client-only PlayerAnimationDriveSystem (local CC-velocity + remote position-delta paths); AnimParamMath (+10 EditMode tests); ServerStripAnimationSystem (disables Rukhanka on the server, zero server-side animation). Client.asmdef gains Rukhanka.Runtime/CharacterController/Physics. EditMode 204/204; Play-validated. See DR-022. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
using NUnit.Framework;
|
||||
using ProjectM.Simulation;
|
||||
using Unity.Mathematics;
|
||||
|
||||
namespace ProjectM.Tests
|
||||
{
|
||||
public class AnimParamMathTests
|
||||
{
|
||||
const float Eps = 1e-3f;
|
||||
static readonly float2 Fwd = new float2(0f, 1f); // facing world +Z
|
||||
const float Max = 5f;
|
||||
|
||||
[Test] public void ZeroVelocity_AllZero()
|
||||
{
|
||||
var r = AnimParamMath.LocomotionParams(float3.zero, Fwd, Max);
|
||||
Assert.AreEqual(0f, r.x, Eps); Assert.AreEqual(0f, r.y, Eps); Assert.AreEqual(0f, r.z, Eps);
|
||||
}
|
||||
|
||||
[Test] public void FullForward_MoveZ1_Speed1()
|
||||
{
|
||||
var r = AnimParamMath.LocomotionParams(new float3(0f, 0f, Max), Fwd, Max);
|
||||
Assert.AreEqual(0f, r.x, Eps); Assert.AreEqual(1f, r.y, Eps); Assert.AreEqual(1f, r.z, Eps);
|
||||
}
|
||||
|
||||
[Test] public void FullBackward_MoveZNeg1()
|
||||
{
|
||||
var r = AnimParamMath.LocomotionParams(new float3(0f, 0f, -Max), Fwd, Max);
|
||||
Assert.AreEqual(0f, r.x, Eps); Assert.AreEqual(-1f, r.y, Eps); Assert.AreEqual(1f, r.z, Eps);
|
||||
}
|
||||
|
||||
[Test] public void StrafeRight_MoveX1()
|
||||
{
|
||||
// facing +Z -> right = (+X). world velocity +X -> MoveX = +1.
|
||||
var r = AnimParamMath.LocomotionParams(new float3(Max, 0f, 0f), Fwd, Max);
|
||||
Assert.AreEqual(1f, r.x, Eps); Assert.AreEqual(0f, r.y, Eps); Assert.AreEqual(1f, r.z, Eps);
|
||||
}
|
||||
|
||||
[Test] public void StrafeLeft_MoveXNeg1()
|
||||
{
|
||||
var r = AnimParamMath.LocomotionParams(new float3(-Max, 0f, 0f), Fwd, Max);
|
||||
Assert.AreEqual(-1f, r.x, Eps);
|
||||
}
|
||||
|
||||
[Test] public void HalfForward_HalfSpeed()
|
||||
{
|
||||
var r = AnimParamMath.LocomotionParams(new float3(0f, 0f, Max * 0.5f), Fwd, Max);
|
||||
Assert.AreEqual(0.5f, r.y, Eps); Assert.AreEqual(0.5f, r.z, Eps);
|
||||
}
|
||||
|
||||
[Test] public void OverMax_SpeedClampsTo1()
|
||||
{
|
||||
var r = AnimParamMath.LocomotionParams(new float3(0f, 0f, Max * 3f), Fwd, Max);
|
||||
Assert.AreEqual(1f, r.z, Eps); Assert.AreEqual(1f, r.y, Eps);
|
||||
}
|
||||
|
||||
[Test] public void RotatedFacing_ProjectsIntoFrame()
|
||||
{
|
||||
// facing world +X; velocity world +X should read as forward (MoveZ+), not strafe.
|
||||
var r = AnimParamMath.LocomotionParams(new float3(Max, 0f, 0f), new float2(1f, 0f), Max);
|
||||
Assert.AreEqual(0f, r.x, Eps); Assert.AreEqual(1f, r.y, Eps);
|
||||
}
|
||||
|
||||
[Test] public void DegenerateFacing_DefaultsToWorldForward()
|
||||
{
|
||||
var r = AnimParamMath.LocomotionParams(new float3(0f, 0f, Max), float2.zero, Max);
|
||||
Assert.AreEqual(1f, r.y, Eps); // treated as facing +Z
|
||||
}
|
||||
|
||||
[Test] public void ZeroMaxSpeed_NoNaN_NoDivByZero()
|
||||
{
|
||||
// guard: safeMax = max(maxSpeed, 1e-4) -> finite output even with maxSpeed 0.
|
||||
var r = AnimParamMath.LocomotionParams(new float3(0f, 0f, 1f), Fwd, 0f);
|
||||
Assert.IsFalse(float.IsNaN(r.x) || float.IsNaN(r.y) || float.IsNaN(r.z));
|
||||
Assert.AreEqual(1f, r.z, Eps); // clamps to 1
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user