// Copyright (C) 2023 Nicholas Maltbie
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
// associated documentation files (the "Software"), to deal in the Software without restriction,
// including without limitation the rights to use, copy, modify, merge, publish, distribute,
// sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using nickmaltbie.OpenKCC.Character;
using nickmaltbie.OpenKCC.Character.Config;
using nickmaltbie.OpenKCC.Tests.EditMode.Utils;
using nickmaltbie.OpenKCC.Tests.TestCommon;
using nickmaltbie.OpenKCC.Utils;
using nickmaltbie.OpenKCC.Utils.ColliderCast;
using nickmaltbie.TestUtilsUnity.Tests.TestCommon;
using NUnit.Framework;
using UnityEngine;
namespace nickmaltbie.OpenKCC.Tests.EditMode.Character
{
public class VerifyTeleport : MonoBehaviour, IOnPlayerTeleport
{
public Vector3 teleportPos = Vector3.zero;
public void OnPlayerTeleport(Vector3 destPos, Quaternion destRot)
{
teleportPos = destPos;
}
}
///
/// Basic tests for in edit mode.
///
[TestFixture]
public class KCCMovementEngineTests : TestBase
{
private MockColliderCast colliderCastMock;
private KCCMovementEngine engine;
[SetUp]
public override void Setup()
{
GameObject go = CreateGameObject();
colliderCastMock = go.AddComponent();
engine = go.AddComponent();
engine._colliderCast = colliderCastMock;
engine.Awake();
}
[Test]
public void Verify_KCCMovementEngine_SnapDownOverrideNormal()
{
// Setup the normal to originally be Vector3.up
KCCTestUtils.SetupCastSelf(colliderCastMock, default, default, Vector3.up, KCCUtils.Epsilon, true);
KCCGroundedState groundedState = engine.CheckGrounded(false, true);
Assert.AreEqual(Vector3.up, groundedState.SurfaceNormal);
// Setup the step hit to return a different normal
KCCTestUtils.SetupCastSelf(colliderCastMock, default, default, Vector3.forward, KCCUtils.Epsilon, true);
KCCTestUtils.SetupDoRaycastInDirection(colliderCastMock, default, default, Vector3.left, KCCUtils.Epsilon, true);
groundedState = engine.CheckGrounded(false, true);
Assert.AreEqual(Vector3.left, groundedState.SurfaceNormal);
KCCTestUtils.SetupCastSelf(colliderCastMock, default, default, Vector3.forward, KCCUtils.Epsilon, true);
KCCTestUtils.SetupDoRaycastInDirection(colliderCastMock, default, default, Vector3.left, KCCUtils.Epsilon, false);
groundedState = engine.CheckGrounded(false, true);
Assert.AreEqual(Vector3.forward, groundedState.SurfaceNormal);
}
[Test]
public void Verify_KCCMovementEngine_OnPlayerTeleport([NUnit.Framework.Range(0, 100, 10)] float dist)
{
VerifyTeleport verify = engine.gameObject.AddComponent();
engine.TeleportPlayer(Vector3.forward * dist);
Assert.AreEqual(verify.teleportPos, Vector3.forward * dist);
}
[Test]
public void Verify_KCCMovementEngine_Properties()
{
Assert.AreEqual(Vector3.up, engine.Up);
Assert.AreEqual(colliderCastMock, engine.ColliderCast);
Assert.AreEqual(engine.GroundedDistance, KCCMovementEngine.DefaultGroundedDistance);
Assert.AreEqual(engine.GroundCheckDistance, KCCMovementEngine.DefaultGroundCheckDistance);
Assert.AreEqual(engine.MaxWalkAngle, engine.maxWalkAngle);
Assert.AreEqual(engine.MaxBounces, KCCMovementEngine.DefaultMaxBounces);
Assert.AreEqual(engine.VerticalSnapUp, engine.stepHeight);
Assert.AreEqual(engine.StepUpDepth, KCCMovementEngine.DefaultStepUpDepth);
Assert.AreEqual(engine.AnglePower, KCCMovementEngine.DefaultAnglePower);
Assert.AreEqual(engine.MaxPushSpeed, KCCMovementEngine.DefaultMaxPushSpeed);
Assert.AreEqual(engine.CanSnapUp, engine.GroundedState.OnGround);
Assert.AreEqual(engine.SnapDown, engine.stepHeight * KCCMovementEngine.SnapDownModifier);
Assert.AreEqual(engine.MaxDefaultLaunchVelocity, KCCMovementEngine.DefaultMaxLaunchVelocity);
}
[Test]
public void Validate_KCCMovementEngine_KCCGetGroundVelocity(
[Values] bool movingGround,
[Values] bool avoidTransferMomentum,
[Values] bool rigidbody,
[Values] bool isKinematic,
[Values] bool onGround,
[Values] bool loadVelocity)
{
GameObject floor = CreateGameObject();
BoxCollider box = floor.AddComponent();
MovingGroundComponent ground = null;
Rigidbody rb = null;
Vector3 loadedVelocity = Vector3.left;
// Load values of smoothed vector to simulate movement forward
if (loadVelocity)
{
engine.worldVelocity.AddSample(loadedVelocity);
}
if (movingGround)
{
ground = floor.AddComponent();
ground.avoidTransferMomentum = avoidTransferMomentum;
}
if (rigidbody)
{
rb = floor.AddComponent();
rb.isKinematic = isKinematic;
}
KCCTestUtils.SetupCastSelf(colliderCastMock, box, Vector3.zero, Vector3.up, KCCUtils.Epsilon, true);
engine.CheckGrounded(false, false);
Vector3 velocity = engine.GetGroundVelocity();
if (movingGround)
{
if (avoidTransferMomentum)
{
Assert.AreEqual(Vector3.zero, velocity);
}
else
{
Assert.AreEqual(ground.GetVelocityAtPoint(Vector3.zero), velocity);
}
}
else if (rigidbody && !isKinematic)
{
Assert.AreEqual(rb.GetPointVelocity(Vector3.zero), velocity);
}
else if (onGround)
{
if (loadVelocity)
{
Assert.AreEqual(loadedVelocity, velocity);
}
else
{
Assert.AreEqual(Vector3.zero, velocity);
}
}
else
{
if (loadVelocity)
{
Assert.AreEqual(loadedVelocity, velocity);
}
else
{
Assert.AreEqual(Vector3.zero, velocity);
}
}
}
[Test]
public void Verify_KCCMovementEngine_GroundedWithSnap([Values] bool snapped)
{
GameObject box = CreateGameObject();
BoxCollider collider = box.AddComponent();
Vector3 initialNormal = Vector3.up;
Vector3 newNormal = Vector3.down;
KCCTestUtils.SetupCastSelf(colliderCastMock, collider, Vector3.zero, initialNormal, KCCUtils.Epsilon, true);
engine.CheckGrounded(false, false);
Assert.AreEqual(initialNormal, engine.GroundedState.SurfaceNormal);
KCCTestUtils.SetupCastSelf(colliderCastMock, collider, Vector3.zero, newNormal, KCCUtils.Epsilon, true);
engine.CheckGrounded(snapped, snapped);
if (snapped)
{
Assert.AreEqual(initialNormal, engine.GroundedState.SurfaceNormal);
}
else
{
Assert.AreEqual(newNormal, engine.GroundedState.SurfaceNormal);
}
}
///
/// Basic test to verify that the KCCMovementEngine won't
/// move into the ground when moving into the ground
/// with the relative parent config position.
///
[Test]
public void Verify_KCCMovementEngine_NoMoveIntoGround()
{
// For this test pretend the player is a sphere
// with a radius of 0.5 units.
GameObject ground = CreateGameObject();
BoxCollider box = ground.AddComponent();
// Setup positions of ground and player.
ground.transform.position = new Vector3(0, -0.5f, 0);
engine.transform.position = new Vector3(0, 1.5f, 0) + Vector3.up * KCCUtils.Epsilon;
// There should be three calls to the CastSelf
// First call is for computing movement, simply return no hit
// Second call is for snapping to ground, should also return no hit
// because I don't want to also test that code.
// Third call is for CheckGrounded, this is the important one.
// To make this similar to the real case, we need to return that the player
// is just floating a little bit off the ground (0.001 units) and we should
// be fine.
// We should compute the relative position of the player to be
// 0.5 units above the center of the box.
IRaycastHit noHit = KCCTestUtils.SetupRaycastHitMock(default(Collider), Vector3.zero, Vector3.zero, Mathf.Infinity);
IRaycastHit groundCheckHit = KCCTestUtils.SetupRaycastHitMock(box, Vector3.zero, Vector3.up, KCCUtils.Epsilon);
int hitIdx = 0;
colliderCastMock.OnCastSelf = (Vector3 position, Quaternion rotation, Vector3 direction, float distance, out IRaycastHit hit, int layerMask, QueryTriggerInteraction queryTriggerInteraction, float skinWidth) =>
{
// Only return hit past second cast
if (++hitIdx < 2)
{
hit = noHit;
return false;
}
hit = groundCheckHit;
return true;
};
// Setup a basic collision between the player and the ground
// one meter below the player.
engine.MovePlayer(Vector3.down);
// Now assert that the absolute position of the player
// is about (0, 0.5, 0)
TestUtils.AssertInBounds(engine.transform.position, Vector3.up * 0.5f, 2 * KCCUtils.Epsilon);
// After calling Update this should be true as well.
engine.Update();
TestUtils.AssertInBounds(engine.transform.position, Vector3.up * 0.5f, 2 * KCCUtils.Epsilon);
}
[Test]
public void SerializationValidationTests()
{
GameObject go = CreateGameObject();
go.AddComponent();
KCCMovementEngine movementEngine = go.AddComponent();
movementEngine.serializationVersion = "";
movementEngine.layerMask = 0;
movementEngine.OnBeforeSerialize();
movementEngine.OnAfterDeserialize();
Assert.AreEqual(movementEngine.serializationVersion, KCCMovementEngine.CurrentSerializationVersion);
Assert.AreEqual(movementEngine.layerMask.value, RaycastHelperConstants.DefaultLayerMask);
}
}
}