using System.Threading; using Code.Game.Gameplay.Player; using Code.Infrastructure.Camera; using Code.Infrastructure.Input; using Code.OfficeGame.BattleMode.Actions.Player; using Code.OfficeGame.BattleMode.Timeline; using Cysharp.Threading.Tasks; using KinematicCharacterController; using Reflex.Attributes; using UnityEngine; using UnityEngine.AI; using UnityEngine.InputSystem; namespace Code.OfficeGame.BattleMode.Actions { public class BattleActionMove : BattleAction { private CameraStateMachine _cameraStateMachine; private IInputService _inputService; private MovementController _movementController; private NavMeshPath _path; private Vector3[] _corners; private int _currentCornerIndex; private Vector3 _targetPosition; private LayerMask _navMeshMask; private const float StoppingDistance = 0.15f; [Inject] public void Construct( CameraStateMachine cameraStateMachine, IInputService inputService, PlayerDecisionMarker playerDecisionMarker) { _playerDecisionMarker = playerDecisionMarker; _cameraStateMachine = cameraStateMachine; _inputService = inputService; _movementController = GetComponent(); _navMeshMask = LayerMask.GetMask("Environment"); } public override void Tick(float tickDelta) { if (Finished) return; if (_corners == null || _currentCornerIndex >= _corners.Length) { StopMovementAndFinish(); return; } Vector3 currentPos = _movementController.Motor.TransientPosition; while (_currentCornerIndex < _corners.Length) { Vector3 toCorner = _corners[_currentCornerIndex] - currentPos; toCorner.y = 0f; if (toCorner.sqrMagnitude <= StoppingDistance * StoppingDistance) { _currentCornerIndex++; } else { break; } } if (_currentCornerIndex >= _corners.Length) { StopMovementAndFinish(); KinematicCharacterSystem.Simulate( tickDelta, KinematicCharacterSystem.CharacterMotors, KinematicCharacterSystem.PhysicsMovers ); return; } Vector3 targetCorner = _corners[_currentCornerIndex]; Vector3 moveDirection = targetCorner - currentPos; moveDirection.y = 0f; float distance = moveDirection.magnitude; Vector3 moveNormal = distance > 0.0001f ? moveDirection / distance : Vector3.zero; float maxStep = _movementController.MaxStableMoveSpeed * tickDelta; float inputMagnitude = (_currentCornerIndex == _corners.Length - 1 && distance < maxStep && maxStep > 0f) ? distance / maxStep : 1f; // TODO Для последнего кадра должен быть особый Look совпадающий с маркером var aiInputs = new AICharacterInputs { MoveVector = moveNormal * inputMagnitude, Look = moveNormal.sqrMagnitude > 0.0001f ? Quaternion.LookRotation(moveNormal, _movementController.Motor.CharacterUp) : _movementController.Motor.TransientRotation }; _movementController.SetInputs(ref aiInputs); KinematicCharacterSystem.Simulate( tickDelta, KinematicCharacterSystem.CharacterMotors, KinematicCharacterSystem.PhysicsMovers ); Vector3 postMovePos = _movementController.Motor.TransientPosition; Vector3 toFinal = _corners[^1] - postMovePos; toFinal.y = 0f; if (_currentCornerIndex >= _corners.Length - 1 && toFinal.sqrMagnitude <= StoppingDistance * StoppingDistance) { StopMovementAndFinish(); } } private void StopMovementAndFinish() { Debug.Log("MovementFinished"); var emptyInputs = new AICharacterInputs { MoveVector = Vector3.zero, Look = _movementController.Motor.TransientRotation }; _movementController.SetInputs(ref emptyInputs); _movementController.Motor.BaseVelocity = Vector3.zero; Finished = true; } private CancellationTokenSource _waitForEnterTokenSource; private BattleActionDecisionResultCode _decisionResultCode; public override async UniTask DecideAction() { _waitForEnterTokenSource = new CancellationTokenSource(); _decisionResultCode = BattleActionDecisionResultCode.CancelDecision; _targetPosition = _movementController.transform.position; _inputService.BindActionPerformed((int)ActionMappings.BattleMode, (int)BattleModeActions.Select, SelectPosition); _inputService.BindActionPerformed((int)ActionMappings.BattleMode, (int)BattleModeActions.Confirm, ConfirmPosition); _inputService.BindActionPerformed((int)ActionMappings.BattleMode, (int)BattleModeActions.Cancel, CancelPosition); _inputService.BindActionPerformed((int)ActionMappings.BattleMode, (int)BattleModeActions.Watchment, MarkPosition); _playerDecisionMarker.Toggle(true); await UniTask.WaitUntilCanceled(_waitForEnterTokenSource.Token).SuppressCancellationThrow(); _playerDecisionMarker.Toggle(false); _inputService.UnbindActionPerformed((int)ActionMappings.BattleMode, (int)BattleModeActions.Select, SelectPosition); _inputService.UnbindActionPerformed((int)ActionMappings.BattleMode, (int)BattleModeActions.Confirm, ConfirmPosition); _inputService.UnbindActionPerformed((int)ActionMappings.BattleMode, (int)BattleModeActions.Cancel, CancelPosition); _inputService.UnbindActionPerformed((int)ActionMappings.BattleMode, (int)BattleModeActions.Watchment, MarkPosition); _waitForEnterTokenSource.Dispose(); _waitForEnterTokenSource = null; Finished = _decisionResultCode != BattleActionDecisionResultCode.Decided; return new() { ResultCode = _decisionResultCode, FrameDuration = _totalFrames}; } public override BattleActionType ActionType => BattleActionType.Move; private int _totalFrames; private PlayerDecisionMarker _playerDecisionMarker; private void CancelPosition(InputAction.CallbackContext obj) { _decisionResultCode = BattleActionDecisionResultCode.CancelDecision; _waitForEnterTokenSource?.Cancel(); } private void ConfirmPosition(InputAction.CallbackContext input) { Vector3 startPos = _movementController.Motor.TransientPosition; _path = new(); if ((_targetPosition - startPos).sqrMagnitude > StoppingDistance * StoppingDistance && NavMesh.CalculatePath(startPos, _targetPosition, NavMesh.AllAreas, _path) && _path.corners.Length > 0) { _corners = _path.corners; _currentCornerIndex = _corners.Length > 1 ? 1 : 0; _decisionResultCode = BattleActionDecisionResultCode.Decided; _totalFrames = CalculatePathFrames(_corners); } else { _decisionResultCode = BattleActionDecisionResultCode.Deciding; _totalFrames = 0; } _waitForEnterTokenSource?.Cancel(); } private int CalculatePathFrames(Vector3[] corners) { 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; // Округляем вверх, так как последний неполный шаг тоже занимает 1 целый кадр return Mathf.CeilToInt(effectiveDistance / stepPerFrame); } private RaycastHit[] _hitBuffer = new RaycastHit[1]; private Vector3 _lastViewedPosition; private void MarkPosition(InputAction.CallbackContext input) { var position = _cameraStateMachine.Camera.transform.position; var direction = _cameraStateMachine.Camera.transform.forward; var hitCount = Physics.RaycastNonAlloc(position, direction, _hitBuffer, Mathf.Infinity, _navMeshMask); if (hitCount == 0) return; var hit = _hitBuffer[0]; _playerDecisionMarker.MoveMarker(hit.point); _lastViewedPosition = hit.point; } private void SelectPosition(InputAction.CallbackContext input) { // var position = _cameraStateMachine.Camera.transform.position; // var direction = _cameraStateMachine.Camera.transform.forward; // // var didHit = Physics.Raycast(position, direction, out RaycastHit hit, Mathf.Infinity, _navMeshMask); // // if (!didHit) return; Debug.Log($"Hit {_lastViewedPosition}"); _targetPosition = _lastViewedPosition; } } }