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();
}
}
}
}