using RoR2.ConVar; using UnityEngine; namespace RoR2; public class LocalNavigator { private readonly struct BodyComponents { public readonly CharacterBody body; public readonly Transform transform; public readonly CharacterMotor motor; public readonly Collider bodyCollider; public BodyComponents(CharacterBody body) { this = default(BodyComponents); this.body = body; if ((bool)body) { transform = body.transform; motor = body.characterMotor; if ((bool)motor) { bodyCollider = body.characterMotor.Motor.Capsule; } } } } private readonly struct BodySnapshot { public readonly Vector3 position; public readonly Vector3 chestPosition; public readonly Vector3 footPosition; public readonly Vector3 motorMoveDirection; public readonly Vector3 motorVelocity; public readonly float maxMoveSpeed; public readonly float acceleration; public readonly float maxJumpHeight; public readonly float maxJumpSpeed; public readonly bool isGrounded; public readonly float time; public readonly float bodyRadius; public readonly bool isFlying; public readonly bool isJumping; private readonly bool hasBody; private readonly bool hasBodyCollider; private readonly bool hasMotor; public bool isValid => hasBody; public bool canJump { get { if (isGrounded) { return maxJumpSpeed > 0f; } return false; } } public BodySnapshot(in BodyComponents bodyComponents, float time) { this = default(BodySnapshot); this.time = time; hasBody = bodyComponents.body; hasBodyCollider = bodyComponents.bodyCollider; hasMotor = bodyComponents.motor; if ((bool)bodyComponents.body) { position = bodyComponents.transform.position; chestPosition = position; footPosition = position; maxMoveSpeed = bodyComponents.body.moveSpeed; acceleration = bodyComponents.body.acceleration; maxJumpHeight = bodyComponents.body.maxJumpHeight; maxJumpSpeed = bodyComponents.body.jumpPower; bodyRadius = bodyComponents.body.radius; isFlying = bodyComponents.body.isFlying; } if ((bool)bodyComponents.bodyCollider) { Bounds bounds = bodyComponents.bodyCollider.bounds; bounds.center = Vector3.zero; chestPosition.y += bounds.max.y * 0.5f; footPosition.y += bounds.min.y; } if ((bool)bodyComponents.motor) { isGrounded = bodyComponents.motor.isGrounded; motorVelocity = bodyComponents.motor.velocity; isJumping = !isGrounded; } } } private readonly struct SnapshotDelta { public readonly float deltaTime; public readonly Vector3 estimatedVelocity; public readonly bool isValid; public SnapshotDelta(in BodySnapshot oldSnapshot, in BodySnapshot newSnapshot) { this = default(SnapshotDelta); if (oldSnapshot.isValid && newSnapshot.isValid) { deltaTime = newSnapshot.time - oldSnapshot.time; isValid = deltaTime > 0f; if (isValid) { estimatedVelocity = (newSnapshot.position - oldSnapshot.position) / deltaTime; } } } } private struct RaycastResults { public bool forwardObstructed; public bool leftObstructed; public bool rightObstructed; } private Vector3 previousMoveVector; public Vector3 targetPosition; public float avoidanceDuration = 0.5f; private float avoidanceTimer; public bool allowWalkOffCliff; public bool wasObstructedLastUpdate; private float localTime; private float raycastTimer; private static readonly float raycastUpdateInterval = 0.2f; private static readonly float avoidanceAngle = 45f; private const bool enableFrustration = true; private const bool enableWhiskers = true; private const bool enableCliffAvoidance = true; private BodyComponents bodyComponents; private float walkFrustration; private const float frustrationLimit = 1f; private const float frustrationDecayRate = 0.25f; private const float frustrationMinimumSpeed = 0.1f; private float aiStopwatch; private Vector3 velocityDelta; private Vector3 estimatedAcceleration; private float accelerationAccuracy; private float moveDirectionAccuracy; private bool hasMadeSharpTurn; private float speedAsFractionOfTopSpeed; private bool isAlreadyMovingAtSufficientSpeed; private BodySnapshot currentSnapshot; private BodySnapshot previousSnapshot; private SnapshotDelta currentSnapshotDelta; private SnapshotDelta previousSnapshotDelta; private RaycastResults raycastResults; private static readonly BoolConVar cvLocalNavigatorDebugDraw = new BoolConVar("local_navigator_debug_draw", ConVarFlags.None, "0", "Enables debug drawing of LocalNavigator (drawing visible in editor only).\n Orange Line: Current position to target position\n Yellow Line: Raycasts\n Red Point: Raycast hit position\n Green Line: Final chosen move vector"); public Vector3 moveVector { get; private set; } public float jumpSpeed { get; private set; } public void SetBody(CharacterBody newBody) { bodyComponents = new BodyComponents(newBody); PushSnapshot(); PushSnapshot(); } private void PushSnapshot() { previousSnapshot = currentSnapshot; currentSnapshot = new BodySnapshot(in bodyComponents, localTime); previousSnapshotDelta = currentSnapshotDelta; currentSnapshotDelta = new SnapshotDelta(in previousSnapshot, in currentSnapshot); } public void Update(float deltaTime) { localTime += deltaTime; PushSnapshot(); if (cvLocalNavigatorDebugDraw.value) { Debug.DrawLine(currentSnapshot.position, targetPosition, new Color32(byte.MaxValue, 127, 39, 127), deltaTime); } wasObstructedLastUpdate = false; jumpSpeed = 0f; previousMoveVector = moveVector; Vector3 vector = targetPosition - currentSnapshot.position; if (!currentSnapshot.isFlying) { vector.y = 0f; } float magnitude = vector.magnitude; Vector3 positionToTargetNormalized = vector / magnitude; Vector3 vector2 = moveVector; CompensateForVelocityByRefinement(deltaTime, ref vector2, in positionToTargetNormalized); moveVector = vector2; if (magnitude == 0f) { moveVector = Vector3.zero; } else { raycastResults = GetRaycasts(in currentSnapshot, moveVector, deltaTime); if (raycastResults.forwardObstructed) { int num = ((!raycastResults.leftObstructed) ? (-1) : 0) + ((!raycastResults.rightObstructed) ? 1 : 0); if (num == 0) { num = ((Random.Range(0, 1) != 1) ? 1 : (-1)); } moveVector = Quaternion.Euler(0f, (0f - avoidanceAngle) * (float)num, 0f) * moveVector; wasObstructedLastUpdate = raycastResults.leftObstructed || raycastResults.rightObstructed; } float deltaTime2 = deltaTime + 0.4f; ref BodySnapshot reference = ref currentSnapshot; ref SnapshotDelta reference2 = ref currentSnapshotDelta; Vector3 currentMoveVector = moveVector; if (CheckCliffAhead(in reference, in reference2, in currentMoveVector, deltaTime2)) { wasObstructedLastUpdate = true; moveVector = -moveVector; ref BodySnapshot reference3 = ref currentSnapshot; ref SnapshotDelta reference4 = ref currentSnapshotDelta; currentMoveVector = moveVector; if (CheckCliffAhead(in reference3, in reference4, in currentMoveVector, deltaTime2)) { moveVector *= 0.25f; } } CalculateFrustration(deltaTime, ref walkFrustration); if (walkFrustration >= 1f) { jumpSpeed = currentSnapshot.maxJumpSpeed; } } avoidanceTimer -= deltaTime; walkFrustration = Mathf.Clamp(walkFrustration - deltaTime * 0.25f, 0f, 1f); if (cvLocalNavigatorDebugDraw.value) { Debug.DrawRay(currentSnapshot.position, moveVector * 5f, Color.green, deltaTime, depthTest: false); } } private void CompensateForVelocityByRefinement(float nextExpectedDeltaTime, ref Vector3 moveVector, in Vector3 positionToTargetNormalized) { Vector3 estimatedVelocity = currentSnapshotDelta.estimatedVelocity; _ = currentSnapshot.acceleration; float num = Vector3.Dot(estimatedVelocity, positionToTargetNormalized); Vector3 vector = positionToTargetNormalized; Vector3 vector2 = vector; int num2 = 8; if (!currentSnapshot.isJumping) { for (int i = 0; i < num2; i++) { EstimateNextMovement(in currentSnapshot, in currentSnapshotDelta, in vector2, nextExpectedDeltaTime, out var nextPosition, out var nextVelocity); Vector3 vector3 = targetPosition - nextPosition; if (!currentSnapshot.isFlying) { vector3.y = 0f; } Vector3 normalized = vector3.normalized; vector2 = normalized; if (Vector3.Dot(nextVelocity, normalized) > num) { vector = normalized; } } } moveVector = vector; } private void CompensateForVelocityByBruteForce(float nextExpectedDeltaTime, ref Vector3 moveVector, in Vector3 positionToTargetNormalized) { Vector3 estimatedVelocity = currentSnapshotDelta.estimatedVelocity; _ = currentSnapshot; float num = Vector3.Dot(estimatedVelocity.normalized, positionToTargetNormalized); Vector3 vector = moveVector; int num2 = 16; float num3 = 360f / (float)num2; for (int i = 0; i < num2; i++) { Vector3 vector2 = Quaternion.AngleAxis((float)i * num3, Vector3.up) * Vector3.forward; EstimateNextMovement(in currentSnapshot, in currentSnapshotDelta, in vector2, nextExpectedDeltaTime, out var nextPosition, out var nextVelocity); float num4 = Vector3.Dot((targetPosition - nextPosition).normalized, nextVelocity.normalized); if (num4 > num) { num = num4; vector = vector2; } } moveVector = vector; } private static void EstimateNextMovement(in BodySnapshot currentSnapshot, in SnapshotDelta currentSnapshotDelta, in Vector3 moveVector, float nextDeltaTime, out Vector3 nextPosition, out Vector3 nextVelocity) { Vector3 estimatedVelocity = currentSnapshotDelta.estimatedVelocity; float acceleration = currentSnapshot.acceleration; nextVelocity = Vector3.MoveTowards(estimatedVelocity, moveVector * currentSnapshot.maxMoveSpeed, acceleration * nextDeltaTime); float num = Mathf.Min((nextVelocity - estimatedVelocity).magnitude / acceleration, nextDeltaTime); float num2 = nextDeltaTime - num; Vector3 vector = (nextVelocity - estimatedVelocity) / num; Vector3 vector2 = nextVelocity * num + vector * (0.5f * num * num); Vector3 vector3 = nextVelocity * num2; nextPosition = currentSnapshot.position + vector2 + vector3; } private RaycastResults GetRaycasts(in BodySnapshot currentSnapshot, Vector3 positionToTargetNormalized, float lookaheadTime) { RaycastResults result = default(RaycastResults); Vector3 origin = currentSnapshot.chestPosition; LayerMask layerMask = (int)LayerIndex.world.mask | (int)LayerIndex.CommonMasks.characterBodiesOrDefault; float maxDistance = currentSnapshot.bodyRadius + currentSnapshot.maxMoveSpeed * lookaheadTime; result.forwardObstructed = Raycast(in origin, in positionToTargetNormalized, out var hitInfo, maxDistance, layerMask); if (result.forwardObstructed) { Vector3 direction = Quaternion.Euler(0f, avoidanceAngle, 0f) * positionToTargetNormalized; result.leftObstructed = Raycast(in origin, in direction, out hitInfo, maxDistance, layerMask); direction = Quaternion.Euler(0f, 0f - avoidanceAngle, 0f) * positionToTargetNormalized; result.rightObstructed = Raycast(in origin, in direction, out hitInfo, maxDistance, layerMask); } return result; } private bool Raycast(in Vector3 origin, in Vector3 direction, out RaycastHit hitInfo, float maxDistance, LayerMask layerMask) { bool flag = Physics.Raycast(origin, direction, out hitInfo, maxDistance, layerMask); if (cvLocalNavigatorDebugDraw.value) { Debug.DrawRay(origin, direction.normalized * maxDistance, Color.yellow, raycastUpdateInterval); if (flag) { Util.DebugCross(hitInfo.point, 1f, Color.red, raycastUpdateInterval); } } return flag; } private bool Linecast(in Vector3 start, in Vector3 end, out RaycastHit hitInfo, LayerMask layerMask) { bool flag = Physics.Linecast(start, end, out hitInfo, layerMask); if (cvLocalNavigatorDebugDraw.value) { Debug.DrawLine(start, end, Color.yellow, raycastUpdateInterval); if (flag) { Util.DebugCross(hitInfo.point, 1f, Color.red, raycastUpdateInterval); } } return flag; } private bool CheckCliffAhead(in BodySnapshot currentSnapshot, in SnapshotDelta currentSnapshotDelta, in Vector3 currentMoveVector, float deltaTime) { if (!allowWalkOffCliff && currentSnapshot.isGrounded) { EstimateNextMovement(in currentSnapshot, in currentSnapshotDelta, in currentMoveVector, deltaTime, out var nextPosition, out var _); float num = currentSnapshot.chestPosition.y - currentSnapshot.position.y; float num2 = currentSnapshot.footPosition.y - currentSnapshot.position.y; Vector3 start = nextPosition; start.y += num; Vector3 end = start; end.y += num2; end.y -= 4f; RaycastHit hitInfo; return !Linecast(in start, in end, out hitInfo, LayerIndex.world.mask); } return false; } private void CalculateFrustration(float deltaTime, ref float frustration) { if (!currentSnapshot.canJump) { frustration = 0f; } else { if (!currentSnapshot.isValid || !currentSnapshotDelta.isValid || !currentSnapshotDelta.isValid || !previousSnapshotDelta.isValid) { return; } if (true) { Vector3 rhs = FlattenDirection(moveVector); Vector3 vector2 = FlattenDirection(currentSnapshotDelta.estimatedVelocity); Vector3 vector3 = FlattenDirection(previousSnapshotDelta.estimatedVelocity); float num = Vector3.Dot(vector2, rhs); velocityDelta = vector2 - vector3; estimatedAcceleration = velocityDelta / deltaTime; float num2 = Vector3.Dot(estimatedAcceleration, rhs); float num3 = estimatedAcceleration.magnitude - num2; speedAsFractionOfTopSpeed = num / currentSnapshot.maxMoveSpeed; isAlreadyMovingAtSufficientSpeed = speedAsFractionOfTopSpeed >= 0.45f; if (isAlreadyMovingAtSufficientSpeed) { frustration = 0f; } else if (num3 > num2 * 2f) { frustration += 1.25f * deltaTime; } return; } float num4 = currentSnapshot.motorVelocity.magnitude + bodyComponents.motor.rootMotion.magnitude / deltaTime; float magnitude = (targetPosition - currentSnapshot.position).magnitude; if (currentSnapshot.maxMoveSpeed != 0f && num4 != 0f && magnitude > Mathf.Epsilon) { float magnitude2 = currentSnapshotDelta.estimatedVelocity.magnitude; if (magnitude2 <= num4 || magnitude2 <= 0.1f) { frustration += 1.25f * deltaTime; } } } static Vector3 FlattenDirection(Vector3 vector) { if (Mathf.Abs(vector.y) > 0f) { vector.y = 0f; } return vector; } } }