using Code.Game.Gameplay.Player; using Code.OfficeGame.BattleMode.Actions.Deciders; using Code.OfficeGame.BattleMode.Timeline; using KinematicCharacterController; using UnityEngine; using UnityEngine.AI; namespace Code.OfficeGame.BattleMode.Actions { public class BattleActionMove : BattleAction { private const float StoppingDistance = 0.15f; private const float RotateDistance = 1f; private const float NavMeshSampleRadius = 1.5f; private const float TurnSpeedDegPerSec = 720f; private int MoveStateHash => Animator.StringToHash("WalkBlend"); public override BattleActionType ActionType => BattleActionType.Move; public override DeciderType DeciderType => DeciderType.Position; // Ультра супер мега конструкция для проверки и утверждения точной позиции. Но не работающая. public override BattleActionData MountAndGetFrames(EntityProperties entity, BattleActionDecisionResult result) { MovementBattleActionData data = new(); var positionDecisionResult = (PositionBattleActionDeciderResult)result; Vector3 startPos = entity.KinematicCharacterMotor.TransientPosition; Vector3 targetPos = positionDecisionResult.Position; Vector3 toTargetXZ = targetPos - startPos; Debug.DrawLine(startPos, targetPos, Color.red, 5f); float distanceXZ = toTargetXZ.magnitude; if (distanceXZ <= StoppingDistance) { data.IsRotateOnly = true; data.Corners = null; float angle = Quaternion.Angle(entity.KinematicCharacterMotor.TransientRotation, positionDecisionResult.Rotation); if (angle < 1f) { data.TotalFrames = 0; } else { float stepPerFrame = TurnSpeedDegPerSec * BattleTimeline.TickDelta; data.TotalFrames = Mathf.Max(2, Mathf.CeilToInt(angle / stepPerFrame)); } return data; } bool hasStart = NavMesh.SamplePosition(startPos, out NavMeshHit startHit, NavMeshSampleRadius, NavMesh.AllAreas); bool hasTarget = NavMesh.SamplePosition(targetPos, out NavMeshHit targetHit, NavMeshSampleRadius, NavMesh.AllAreas); if (!hasStart || !hasTarget) { data.TotalFrames = 0; return data; } Vector3 validStart = startHit.position; Vector3 validTarget = targetHit.position; if (!NavMesh.Raycast(validStart, validTarget, out _, NavMesh.AllAreas)) { data.Corners = new[] { startPos, validTarget }; data.CurrentCornerIndex = 1; data.TotalFrames = CalculatePathFrames(data.Corners, entity.MovementController); return data; } NavMeshPath path = new(); if (NavMesh.CalculatePath(validStart, validTarget, NavMesh.AllAreas, path) && path.status == NavMeshPathStatus.PathComplete && path.corners.Length > 1) { path.corners[0] = startPos; path.corners[^1] = validTarget; data.Corners = path.corners; data.CurrentCornerIndex = 1; data.TotalFrames = CalculatePathFrames(data.Corners, entity.MovementController); } else { data.TotalFrames = 0; } return data; } public override void Tick( float tickDelta, EntityProperties entity, BattleActionDecisionResult battleActionDecisionResult, BattleActionData battleActionData) { if (battleActionData is not MovementBattleActionData movementActionData || battleActionDecisionResult is not PositionBattleActionDeciderResult positionDecisionResult) { battleActionData.Finished = true; return; } var movementController = entity.MovementController; if (movementActionData.IsRotateOnly) { movementActionData.CurrentFrame++; float angle = Quaternion.Angle(movementController.Motor.TransientRotation, positionDecisionResult.Rotation); int framesLeft = Mathf.Max(1, movementActionData.TotalFrames - movementActionData.CurrentFrame + 1); float stepAngle = angle / framesLeft; Quaternion nextRot = Quaternion.RotateTowards( movementController.Motor.TransientRotation, positionDecisionResult.Rotation, stepAngle ); entity.KinematicCharacterMotor.SetRotation(nextRot); StopMovementAndFinish(movementController); KinematicCharacterSystem.Simulate( tickDelta, KinematicCharacterSystem.CharacterMotors, KinematicCharacterSystem.PhysicsMovers ); if (movementActionData.CurrentFrame >= movementActionData.TotalFrames || angle <= 0.5f) { entity.KinematicCharacterMotor.SetRotation(positionDecisionResult.Rotation); battleActionData.Finished = true; Debug.Log("Rotated"); } return; } if (movementActionData.Corners == null || movementActionData.CurrentCornerIndex >= movementActionData.Corners.Length) { entity.KinematicCharacterMotor.SetRotation(positionDecisionResult.Rotation); StopMovementAndFinish(movementController); battleActionData.Finished = true; Debug.Log("Wrong index"); return; } if (!movementActionData.WasStarted) { movementActionData.WasStarted = true; float transitionDurationSeconds = 10 * BattleTimeline.TickDelta; entity.animator.CrossFadeInFixedTime(MoveStateHash, transitionDurationSeconds); } Vector3 currentPos = entity.KinematicCharacterMotor.TransientPosition; while (movementActionData.CurrentCornerIndex < movementActionData.Corners.Length) { Vector3 toCorner = movementActionData.Corners[movementActionData.CurrentCornerIndex] - currentPos; toCorner.y = 0f; if (toCorner.sqrMagnitude <= StoppingDistance * StoppingDistance) movementActionData.CurrentCornerIndex++; else break; } if (movementActionData.CurrentCornerIndex >= movementActionData.Corners.Length) { entity.KinematicCharacterMotor.SetRotation(positionDecisionResult.Rotation); StopMovementAndFinish(movementController); KinematicCharacterSystem.Simulate( tickDelta, KinematicCharacterSystem.CharacterMotors, KinematicCharacterSystem.PhysicsMovers ); battleActionData.Finished = true; Debug.Log("Last corner"); return; } Vector3 targetCorner = movementActionData.Corners[movementActionData.CurrentCornerIndex]; Vector3 moveDirection = targetCorner - currentPos; moveDirection.y = 0f; float distance = moveDirection.magnitude; Vector3 moveNormal = distance > 0.0001f ? moveDirection / distance : Vector3.zero; bool isLastCorner = movementActionData.CurrentCornerIndex == movementActionData.Corners.Length - 1; bool preLastPoint = isLastCorner && distance < RotateDistance; float maxStep = movementController.MaxStableMoveSpeed * tickDelta; float inputMagnitude = (isLastCorner && distance < maxStep && maxStep > 0f) ? distance / maxStep : 1f; Quaternion lookRotation = preLastPoint ? positionDecisionResult.Rotation : (moveNormal.sqrMagnitude > 0.0001f ? Quaternion.LookRotation(moveNormal, movementController.Motor.CharacterUp) : movementController.Motor.TransientRotation); var aiInputs = new AICharacterInputs { MoveVector = moveNormal * inputMagnitude, Look = lookRotation }; movementController.SetInputs(ref aiInputs); KinematicCharacterSystem.Simulate( tickDelta, KinematicCharacterSystem.CharacterMotors, KinematicCharacterSystem.PhysicsMovers ); Vector3 postMovePos = movementController.Motor.TransientPosition; Vector3 toFinal = movementActionData.Corners[^1] - postMovePos; toFinal.y = 0f; if (isLastCorner && toFinal.sqrMagnitude <= StoppingDistance * StoppingDistance) { entity.KinematicCharacterMotor.SetRotation(positionDecisionResult.Rotation); StopMovementAndFinish(movementController); Debug.Log("Very Last corner"); battleActionData.Finished = true; } } private void StopMovementAndFinish(MovementController movementController) { var emptyInputs = new AICharacterInputs { MoveVector = Vector3.zero, Look = movementController.Motor.TransientRotation }; movementController.SetInputs(ref emptyInputs); movementController.Motor.BaseVelocity = Vector3.zero; } private int CalculatePathFrames(Vector3[] corners, MovementController movementController) { if (corners == null || corners.Length < 2) return 0; float totalDistanceXZ = 0f; for (int i = 1; i < corners.Length; i++) { Vector3 segment = corners[i] - corners[i - 1]; segment.y = 0f; totalDistanceXZ += segment.magnitude; } float effectiveDistance = Mathf.Max(0f, totalDistanceXZ - StoppingDistance); float stepPerFrame = movementController.MaxStableMoveSpeed * BattleTimeline.TickDelta; if (stepPerFrame <= 0.0001f) return 0; return Mathf.Max(1, Mathf.CeilToInt(effectiveDistance / stepPerFrame)); } } }