using UnityEngine; using Pathfinding; using AppleHills.Core.Settings; using Core; using Core.Lifecycle; namespace Input { /// /// Base class for player movement controllers. /// Handles tap-to-move and hold-to-move input with pathfinding or direct movement. /// Derived classes can override to add specialized behavior (e.g., shader updates). /// public abstract class BasePlayerMovementController : ManagedBehaviour, ITouchInputConsumer { [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; // 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(); // Register with InputManager if (InputManager.Instance != null) { InputManager.Instance.SetDefaultConsumer(this); Logging.Debug($"[{GetType().Name}] Registered as default input consumer"); } _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) { 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) { 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 } }