using UnityEngine; using Pathfinding; namespace Input { /// /// Handles player movement in response to tap and hold input events. /// Supports both direct and pathfinding movement modes, and provides event/callbacks for arrival/cancellation. /// public class PlayerTouchController : MonoBehaviour, ITouchInputConsumer { // --- Movement State --- private Vector3 targetPosition; private Vector3 directMoveVelocity; // default is Vector3.zero private bool isHolding; private Vector2 lastHoldPosition; private Coroutine pathfindingDragCoroutine; private float pathfindingDragUpdateInterval = 0.1f; // Interval in seconds // --- Unity/Component References --- private AIPath aiPath; // Note: String-based property lookup is flagged as inefficient, but is common in Unity for dynamic children. private Animator animator; private Transform artTransform; // --- MoveToAndNotify State --- public delegate void ArrivedAtTargetHandler(); private Coroutine moveToCoroutine; public event ArrivedAtTargetHandler OnArrivedAtTarget; public event System.Action OnMoveToCancelled; private bool interruptMoveTo; void Awake() { aiPath = GetComponent(); artTransform = transform.Find("CharacterArt"); if (artTransform != null) animator = artTransform.GetComponent(); else animator = GetComponentInChildren(); } void Start() { InputManager.Instance?.SetDefaultConsumer(this); } /// /// Handles tap input. Always uses pathfinding to move to the tapped location. /// Cancels any in-progress MoveToAndNotify. /// public void OnTap(Vector2 worldPosition) { InterruptMoveTo(); Debug.Log($"[PlayerTouchController] OnTap at {worldPosition}"); if (aiPath != null) { aiPath.enabled = true; aiPath.canMove = true; aiPath.isStopped = false; SetTargetPosition(worldPosition); directMoveVelocity = Vector3.zero; isHolding = false; } } /// /// Handles the start of a hold input. Begins tracking the finger and uses the correct movement mode. /// Cancels any in-progress MoveToAndNotify. /// public void OnHoldStart(Vector2 worldPosition) { InterruptMoveTo(); Debug.Log($"[PlayerTouchController] OnHoldStart at {worldPosition}"); lastHoldPosition = worldPosition; isHolding = true; if (GameManager.Instance.DefaultHoldMovementMode == GameSettings.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; } } /// /// Handles hold move input. Updates the target position for direct or pathfinding movement. /// public void OnHoldMove(Vector2 worldPosition) { if (!isHolding) return; lastHoldPosition = worldPosition; if (GameManager.Instance.DefaultHoldMovementMode == GameSettings.HoldMovementMode.Direct) { if (aiPath != null && aiPath.enabled) aiPath.enabled = false; MoveDirectlyTo(worldPosition); } // If pathfinding, coroutine will update destination } /// /// Handles the end of a hold input. Stops tracking and disables movement as needed. /// public void OnHoldEnd(Vector2 worldPosition) { Debug.Log($"[PlayerTouchController] OnHoldEnd at {worldPosition}"); isHolding = false; directMoveVelocity = Vector3.zero; if (aiPath != null && GameManager.Instance.DefaultHoldMovementMode == GameSettings.HoldMovementMode.Pathfinding) { if (pathfindingDragCoroutine != null) { StopCoroutine(pathfindingDragCoroutine); pathfindingDragCoroutine = null; } } if (aiPath != null && GameManager.Instance.DefaultHoldMovementMode == GameSettings.HoldMovementMode.Direct) { aiPath.enabled = false; } } /// /// Sets the target position for pathfinding movement. /// private void SetTargetPosition(Vector2 worldPosition) { if (aiPath != null) { aiPath.destination = worldPosition; aiPath.maxSpeed = GameManager.Instance.MoveSpeed; aiPath.canMove = true; aiPath.isStopped = false; } } /// /// Moves the player directly towards the specified world position. /// private 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 = aiPath.maxSpeed; float acceleration = aiPath.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; transform.position += move; } void Update() { if (animator != null && aiPath != null) { float normalizedSpeed = 0f; if (isHolding && GameManager.Instance.DefaultHoldMovementMode == GameSettings.HoldMovementMode.Direct) { normalizedSpeed = directMoveVelocity.magnitude / aiPath.maxSpeed; } else if (aiPath.enabled) { normalizedSpeed = aiPath.velocity.magnitude / aiPath.maxSpeed; } animator.SetFloat("Speed", Mathf.Clamp01(normalizedSpeed)); } } /// /// Coroutine for updating the AIPath destination during pathfinding hold movement. /// private System.Collections.IEnumerator PathfindingDragUpdateCoroutine() { while (isHolding && aiPath != null) { aiPath.destination = new Vector3(lastHoldPosition.x, lastHoldPosition.y, transform.position.z); yield return new WaitForSeconds(pathfindingDragUpdateInterval); } } /// /// Moves the player to a specific target position and notifies via events when arrived or cancelled. /// This is used by systems like Pickup.cs to orchestrate movement. /// public 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 void InterruptMoveTo() { interruptMoveTo = true; isHolding = false; directMoveVelocity = Vector3.zero; if (GameManager.Instance.DefaultHoldMovementMode == GameSettings.HoldMovementMode.Direct && aiPath != null) aiPath.enabled = false; OnMoveToCancelled?.Invoke(); } /// /// Coroutine for moving the player to a target position and firing arrival/cancel events. /// private System.Collections.IEnumerator MoveToTargetCoroutine(Vector3 target) { if (aiPath != null) { aiPath.destination = target; aiPath.maxSpeed = GameManager.Instance.MoveSpeed; } 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 <= GameManager.Instance.StopDistance + 0.2f) { break; } yield return null; } moveToCoroutine = null; if (!interruptMoveTo) { OnArrivedAtTarget?.Invoke(); } } } }