using UnityEngine; using Pathfinding; using AppleHills.Core.Settings; using Core; using Core.Lifecycle; using Core.Settings; namespace Input { /// /// Base class for player movement controllers. /// Handles tap-to-move and hold-to-move input with pathfinding or direct movement. /// Implements IInteractingCharacter to enable interaction with items. /// Derived classes can override to add specialized behavior (e.g., shader updates). /// public abstract class BasePlayerMovementController : ManagedBehaviour, ITouchInputConsumer, IInteractingCharacter { [Header("Movement")] [SerializeField] protected float moveSpeed = 5f; [Header("Collision Simulation")] [SerializeField] protected LayerMask obstacleMask; [SerializeField] protected float colliderRadius = 0.5f; // Movement state protected Vector3 _targetPosition; protected Vector3 _directMoveVelocity; protected bool _isHolding; protected Vector2 _lastHoldPosition; protected Coroutine _pathfindingDragCoroutine; protected float _pathfindingDragUpdateInterval = 0.1f; // Settings reference (populated by derived classes in LoadSettings) protected IPlayerMovementSettings _movementSettings; protected IPlayerMovementSettings Settings => _movementSettings; // Abstract method for derived classes to load their specific settings protected abstract void LoadSettings(); // Movement tracking protected bool _isMoving; public bool IsMoving => _isMoving; public bool IsHolding => _isHolding; public event System.Action OnMovementStarted; public event System.Action OnMovementStopped; // IInteractingCharacter implementation - scripted movement for interactions private Coroutine _moveToCoroutine; private bool _interruptMoveTo; public event System.Action OnArrivedAtTarget; public event System.Action OnMoveToCancelled; // Components protected AIPath _aiPath; protected Animator _animator; protected Transform _artTransform; protected SpriteRenderer _spriteRenderer; // Animation tracking protected Vector3 _lastDirectMoveDir = Vector3.right; public Vector3 LastDirectMoveDir => _lastDirectMoveDir; protected float _lastDirX; protected float _lastDirY = -1f; protected LogVerbosity _logVerbosity = LogVerbosity.Warning; internal override void OnManagedAwake() { base.OnManagedAwake(); LoadSettings(); // Let derived class load appropriate settings InitializeComponents(); } internal override void OnManagedStart() { base.OnManagedStart(); _logVerbosity = DeveloperSettingsProvider.Instance.GetSettings().inputLogVerbosity; } protected virtual void InitializeComponents() { _aiPath = GetComponent(); _artTransform = transform.Find("CharacterArt"); if (_artTransform != null) _animator = _artTransform.GetComponent(); else _animator = GetComponentInChildren(); if (_artTransform != null) _spriteRenderer = _artTransform.GetComponent(); if (_spriteRenderer == null) _spriteRenderer = GetComponentInChildren(); } protected virtual void Update() { UpdateMovementState(); UpdateAnimation(); } #region ITouchInputConsumer Implementation public virtual void OnTap(Vector2 worldPosition) { InterruptMoveTo(); // Cancel any scripted movement Logging.Debug($"[{GetType().Name}] OnTap at {worldPosition}"); if (_aiPath != null) { _aiPath.enabled = true; _aiPath.canMove = true; _aiPath.isStopped = false; SetTargetPosition(worldPosition); _directMoveVelocity = Vector3.zero; _isHolding = false; } } public virtual void OnHoldStart(Vector2 worldPosition) { InterruptMoveTo(); // Cancel any scripted movement Logging.Debug($"[{GetType().Name}] OnHoldStart at {worldPosition}"); _lastHoldPosition = worldPosition; _isHolding = true; if (Settings.DefaultHoldMovementMode == HoldMovementMode.Pathfinding && _aiPath != null) { _aiPath.enabled = true; if (_pathfindingDragCoroutine != null) StopCoroutine(_pathfindingDragCoroutine); _pathfindingDragCoroutine = StartCoroutine(PathfindingDragUpdateCoroutine()); } else // Direct movement { if (_aiPath != null) _aiPath.enabled = false; _directMoveVelocity = Vector3.zero; } } public virtual void OnHoldUpdate(Vector2 worldPosition) { if (!_isHolding) return; _lastHoldPosition = worldPosition; if (Settings.DefaultHoldMovementMode == HoldMovementMode.Direct) { if (_aiPath != null && _aiPath.enabled) _aiPath.enabled = false; MoveDirectlyTo(worldPosition); } } public virtual void OnHoldMove(Vector2 worldPosition) { // Alias for OnHoldUpdate for interface compatibility OnHoldUpdate(worldPosition); } public virtual void OnHoldEnd(Vector2 worldPosition) { Logging.Debug($"[{GetType().Name}] OnHoldEnd at {worldPosition}"); _isHolding = false; _directMoveVelocity = Vector3.zero; if (_aiPath != null && Settings.DefaultHoldMovementMode == HoldMovementMode.Pathfinding) { if (_pathfindingDragCoroutine != null) { StopCoroutine(_pathfindingDragCoroutine); _pathfindingDragCoroutine = null; } } if (_aiPath != null && Settings.DefaultHoldMovementMode == HoldMovementMode.Direct) { _aiPath.enabled = false; } } #endregion #region Movement Methods protected virtual void SetTargetPosition(Vector2 worldPosition) { if (_aiPath != null) { _aiPath.destination = worldPosition; _aiPath.maxSpeed = Settings.MoveSpeed; _aiPath.maxAcceleration = Settings.MaxAcceleration; _aiPath.canMove = true; _aiPath.isStopped = false; } } protected virtual void MoveDirectlyTo(Vector2 worldPosition) { if (_aiPath == null) return; Vector3 current = transform.position; Vector3 target = new Vector3(worldPosition.x, worldPosition.y, current.z); Vector3 toTarget = (target - current); Vector3 direction = toTarget.normalized; float maxSpeed = Settings.MoveSpeed; float acceleration = Settings.MaxAcceleration; _directMoveVelocity = Vector3.MoveTowards(_directMoveVelocity, direction * maxSpeed, acceleration * Time.deltaTime); if (_directMoveVelocity.magnitude > maxSpeed) { _directMoveVelocity = _directMoveVelocity.normalized * maxSpeed; } Vector3 move = _directMoveVelocity * Time.deltaTime; if (move.magnitude > toTarget.magnitude) { move = toTarget; } // Collision simulation Vector3 adjustedVelocity = AdjustVelocityForObstacles(current, _directMoveVelocity); Vector3 adjustedMove = adjustedVelocity * Time.deltaTime; if (adjustedMove.magnitude > toTarget.magnitude) { adjustedMove = toTarget; } transform.position += adjustedMove; _lastDirectMoveDir = _directMoveVelocity.normalized; } protected virtual Vector3 AdjustVelocityForObstacles(Vector3 position, Vector3 velocity) { if (velocity.sqrMagnitude < 0.0001f) return velocity; float moveDistance = velocity.magnitude * Time.deltaTime; Vector2 origin = new Vector2(position.x, position.y); Vector2 dir = velocity.normalized; float rayLength = colliderRadius + moveDistance; RaycastHit2D hit = Physics2D.Raycast(origin, dir, rayLength, obstacleMask); if (hit.collider != null) { Vector2 tangent = new Vector2(-hit.normal.y, hit.normal.x); float slideAmount = Vector2.Dot(velocity, tangent); Vector3 slideVelocity = tangent * slideAmount; return slideVelocity; } return velocity; } protected virtual System.Collections.IEnumerator PathfindingDragUpdateCoroutine() { while (_isHolding && _aiPath != null) { SetTargetPosition(_lastHoldPosition); yield return new WaitForSeconds(_pathfindingDragUpdateInterval); } } #endregion #region State and Animation protected virtual void UpdateMovementState() { bool isCurrentlyMoving = false; if (_isHolding && Settings.DefaultHoldMovementMode == HoldMovementMode.Direct) { isCurrentlyMoving = _directMoveVelocity.sqrMagnitude > 0.001f; } else if (_aiPath != null && _aiPath.enabled) { isCurrentlyMoving = _aiPath.velocity.sqrMagnitude > 0.001f; } if (isCurrentlyMoving && !_isMoving) { _isMoving = true; OnMovementStarted?.Invoke(); Logging.Debug($"[{GetType().Name}] Movement started"); } else if (!isCurrentlyMoving && _isMoving) { _isMoving = false; OnMovementStopped?.Invoke(); Logging.Debug($"[{GetType().Name}] Movement stopped"); } } protected virtual void UpdateAnimation() { if (_animator == null || _aiPath == null) return; float normalizedSpeed = 0f; Vector3 velocity = Vector3.zero; float maxSpeed = Settings.MoveSpeed; if (_isHolding && Settings.DefaultHoldMovementMode == HoldMovementMode.Direct) { normalizedSpeed = _directMoveVelocity.magnitude / maxSpeed; velocity = _directMoveVelocity; } else if (_aiPath.enabled) { normalizedSpeed = _aiPath.velocity.magnitude / maxSpeed; velocity = _aiPath.velocity; } _animator.SetFloat("Speed", Mathf.Clamp01(normalizedSpeed)); if (velocity.sqrMagnitude > 0.01f) { Vector3 normalizedVelocity = velocity.normalized; _lastDirX = normalizedVelocity.x; _lastDirY = normalizedVelocity.y; _animator.SetFloat("DirX", _lastDirX); _animator.SetFloat("DirY", _lastDirY); } else { _animator.SetFloat("DirX", _lastDirX); _animator.SetFloat("DirY", _lastDirY); } } #endregion #region IInteractingCharacter Implementation /// /// Controller-driven interaction movement. Base implementation moves this controller to the interactable. /// Override in derived classes for custom behavior (e.g., PlayerTouchController handles follower dispatch). /// public virtual async System.Threading.Tasks.Task MoveToInteractableAsync(Interactions.InteractableBase interactable) { // Default behavior: move self to interactable position Vector3 targetPosition = interactable.transform.position; // Check for custom CharacterMoveToTarget var moveTargets = interactable.GetComponentsInChildren(); foreach (var target in moveTargets) { if (target.characterType == Interactions.CharacterToInteract.Trafalgar || target.characterType == Interactions.CharacterToInteract.Both) { targetPosition = target.GetTargetPosition(); break; } } // Use MovementUtilities to handle movement return await Utils.MovementUtilities.MoveToPositionAsync(this, targetPosition); } /// /// Moves the character to a specific target position and notifies via events when arrived or cancelled. /// This is used by systems like interactions to orchestrate scripted movement. /// public virtual void MoveToAndNotify(Vector3 target) { // Cancel any previous move-to coroutine if (_moveToCoroutine != null) { StopCoroutine(_moveToCoroutine); } _interruptMoveTo = false; // Ensure pathfinding is enabled for MoveToAndNotify if (_aiPath != null) { _aiPath.enabled = true; _aiPath.canMove = true; _aiPath.isStopped = false; } _moveToCoroutine = StartCoroutine(MoveToTargetCoroutine(target)); } /// /// Cancels any in-progress MoveToAndNotify operation and fires the cancellation event. /// public virtual void InterruptMoveTo() { _interruptMoveTo = true; _isHolding = false; _directMoveVelocity = Vector3.zero; if (Settings != null && Settings.DefaultHoldMovementMode == HoldMovementMode.Direct && _aiPath != null) _aiPath.enabled = false; OnMoveToCancelled?.Invoke(); } /// /// Coroutine for moving the character to a target position and firing arrival/cancel events. /// protected virtual System.Collections.IEnumerator MoveToTargetCoroutine(Vector3 target) { if (_aiPath != null) { _aiPath.destination = target; _aiPath.maxSpeed = Settings.MoveSpeed; _aiPath.maxAcceleration = Settings.MaxAcceleration; } while (!_interruptMoveTo) { Vector2 current2D = new Vector2(transform.position.x, transform.position.y); Vector2 target2D = new Vector2(target.x, target.y); float dist = Vector2.Distance(current2D, target2D); if (dist <= Settings.StopDistance + 0.2f) { break; } yield return null; } _moveToCoroutine = null; if (!_interruptMoveTo) { OnArrivedAtTarget?.Invoke(); } } #endregion } }