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 internal bool isHolding; private Vector2 lastHoldPosition; private Coroutine pathfindingDragCoroutine; private float pathfindingDragUpdateInterval = 0.1f; // Interval in seconds [Header("Collision Simulation")] public LayerMask obstacleMask; public float colliderRadius = 0.5f; // --- 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; private SpriteRenderer spriteRenderer; // --- Last direct movement direction --- private Vector3 _lastDirectMoveDir = Vector3.right; public Vector3 LastDirectMoveDir => _lastDirectMoveDir; // --- 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(); // Cache SpriteRenderer for flipping if (artTransform != null) spriteRenderer = artTransform.GetComponent(); if (spriteRenderer == null) spriteRenderer = 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; } // --- Collision simulation --- Vector3 adjustedVelocity = AdjustVelocityForObstacles(current, directMoveVelocity); Vector3 adjustedMove = adjustedVelocity * Time.deltaTime; if (adjustedMove.magnitude > toTarget.magnitude) { adjustedMove = toTarget; } transform.position += adjustedMove; // Cache the last direct movement direction _lastDirectMoveDir = directMoveVelocity.normalized; } /// /// Simulates collision with obstacles by raycasting in the direction of velocity and projecting the velocity if a collision is detected. /// /// Player's current position. /// Intended velocity for this frame. /// Adjusted velocity after collision simulation. private 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); Debug.DrawLine(origin, origin + dir * rayLength, Color.red, 0.1f); if (hit.collider != null) { // Draw normal and tangent for debug Debug.DrawLine(hit.point, hit.point + hit.normal, Color.green, 0.2f); Vector2 tangent = new Vector2(-hit.normal.y, hit.normal.x); Debug.DrawLine(hit.point, hit.point + tangent, Color.blue, 0.2f); // Project velocity onto tangent to simulate sliding float slideAmount = Vector2.Dot(velocity, tangent); Vector3 slideVelocity = tangent * slideAmount; return slideVelocity; } return velocity; } void Update() { if (animator != null && aiPath != null) { float normalizedSpeed = 0f; Vector3 velocity = Vector3.zero; if (isHolding && GameManager.Instance.DefaultHoldMovementMode == GameSettings.HoldMovementMode.Direct) { normalizedSpeed = directMoveVelocity.magnitude / aiPath.maxSpeed; velocity = directMoveVelocity; } else if (aiPath.enabled) { normalizedSpeed = aiPath.velocity.magnitude / aiPath.maxSpeed; velocity = aiPath.velocity; } animator.SetFloat("Speed", Mathf.Clamp01(normalizedSpeed)); SetSpriteFlip(velocity); } } private void SetSpriteFlip(Vector3 velocity) { if (spriteRenderer != null && velocity.sqrMagnitude > 0.001f) { if (velocity.x > 0.01f) spriteRenderer.flipX = false; else if (velocity.x < -0.01f) spriteRenderer.flipX = true; } } /// /// 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(); } } } }