using Input; using UnityEngine; using System; using System.Collections.Generic; using UnityEngine.Events; using System.Threading.Tasks; namespace Interactions { public enum CharacterToInteract { None, Trafalgar, Pulver, Both } /// /// Represents an interactable object that can respond to tap input events. /// public class Interactable : MonoBehaviour, ITouchInputConsumer { [Header("Interaction Settings")] public bool isOneTime; public float cooldown = -1f; public CharacterToInteract characterToInteract = CharacterToInteract.Pulver; [Header("Interaction Events")] public UnityEvent interactionStarted; public UnityEvent interactionInterrupted; public UnityEvent characterArrived; public UnityEvent interactionComplete; // Helpers for managing interaction state private bool _interactionInProgress; private PlayerTouchController _playerRef; private FollowerController _followerController; private bool _isActive = true; private InteractionEventType _currentEventType; // Action component system private List _registeredActions = new List(); private void Awake() { // Subscribe to interactionComplete event interactionComplete.AddListener(OnInteractionComplete); } /// /// Register an action component with this interactable /// public void RegisterAction(InteractionActionBase action) { if (!_registeredActions.Contains(action)) { _registeredActions.Add(action); } } /// /// Unregister an action component from this interactable /// public void UnregisterAction(InteractionActionBase action) { _registeredActions.Remove(action); } /// /// Dispatch an interaction event to all registered actions and await their completion /// private async Task DispatchEventAsync(InteractionEventType eventType) { _currentEventType = eventType; // Collect all tasks from actions that want to respond List> tasks = new List>(); foreach (var action in _registeredActions) { Task task = action.OnInteractionEvent(eventType, _playerRef, _followerController); if (task != null) { tasks.Add(task); } } if (tasks.Count > 0) { // Wait for all tasks to complete await Task.WhenAll(tasks); } // If no tasks were added, the method will complete immediately (no need for await) } /// /// Handles tap input. Triggers interaction logic. /// public void OnTap(Vector2 worldPosition) { if (!_isActive) { Debug.Log($"[Interactable] Is disabled!"); return; } Debug.Log($"[Interactable] OnTap at {worldPosition} on {gameObject.name}"); // Start the interaction process asynchronously _ = TryInteractAsync(); } private async Task TryInteractAsync() { _interactionInProgress = true; _playerRef = FindFirstObjectByType(); _followerController = FindFirstObjectByType(); interactionStarted?.Invoke(_playerRef, _followerController); // Dispatch the InteractionStarted event to action components await DispatchEventAsync(InteractionEventType.InteractionStarted); // After all InteractionStarted actions complete, proceed to player movement await StartPlayerMovementAsync(); } private async Task StartPlayerMovementAsync() { if (_playerRef == null) { Debug.Log($"[Interactable] Player character could not be found. Aborting interaction."); interactionInterrupted.Invoke(); await DispatchEventAsync(InteractionEventType.InteractionInterrupted); return; } // If characterToInteract is None, immediately trigger the characterArrived event if (characterToInteract == CharacterToInteract.None) { await BroadcastCharacterArrivedAsync(); return; } // Check for a CharacterMoveToTarget component for Trafalgar (player) or Both Vector3 stopPoint; bool customTargetFound = false; CharacterMoveToTarget[] moveTargets = GetComponentsInChildren(); foreach (var target in moveTargets) { // Target is valid if it matches Trafalgar specifically or is set to Both if (target.characterType == CharacterToInteract.Trafalgar || target.characterType == CharacterToInteract.Both) { stopPoint = target.GetTargetPosition(); customTargetFound = true; // We need to wait for the player to arrive, so use a TaskCompletionSource var tcs = new TaskCompletionSource(); // Use local functions instead of circular lambda references void OnPlayerArrivedLocal() { // First remove both event handlers to prevent memory leaks if (_playerRef != null) { _playerRef.OnArrivedAtTarget -= OnPlayerArrivedLocal; _playerRef.OnMoveToCancelled -= OnPlayerMoveCancelledLocal; } // Then continue with the interaction flow OnPlayerArrivedAsync().ContinueWith(_ => tcs.TrySetResult(true)); } void OnPlayerMoveCancelledLocal() { // First remove both event handlers to prevent memory leaks if (_playerRef != null) { _playerRef.OnArrivedAtTarget -= OnPlayerArrivedLocal; _playerRef.OnMoveToCancelled -= OnPlayerMoveCancelledLocal; } // Then handle the cancellation OnPlayerMoveCancelledAsync().ContinueWith(_ => tcs.TrySetResult(false)); } // Unsubscribe previous handlers (if any) _playerRef.OnArrivedAtTarget -= OnPlayerArrived; _playerRef.OnMoveToCancelled -= OnPlayerMoveCancelled; // Subscribe our new handlers _playerRef.OnArrivedAtTarget += OnPlayerArrivedLocal; _playerRef.OnMoveToCancelled += OnPlayerMoveCancelledLocal; // Start the player movement _playerRef.MoveToAndNotify(stopPoint); // Await player arrival await tcs.Task; return; } } // If no custom target was found, use the default behavior if (!customTargetFound) { // Compute closest point on the interaction radius Vector3 interactablePos = transform.position; Vector3 playerPos = _playerRef.transform.position; float stopDistance = characterToInteract == CharacterToInteract.Pulver ? GameManager.Instance.PlayerStopDistance : GameManager.Instance.PlayerStopDistanceDirectInteraction; Vector3 toPlayer = (playerPos - interactablePos).normalized; stopPoint = interactablePos + toPlayer * stopDistance; // We need to wait for the player to arrive, so use a TaskCompletionSource var tcs = new TaskCompletionSource(); // Use local functions instead of circular lambda references void OnPlayerArrivedLocal() { // First remove both event handlers to prevent memory leaks if (_playerRef != null) { _playerRef.OnArrivedAtTarget -= OnPlayerArrivedLocal; _playerRef.OnMoveToCancelled -= OnPlayerMoveCancelledLocal; } // Then continue with the interaction flow OnPlayerArrivedAsync().ContinueWith(_ => tcs.TrySetResult(true)); } void OnPlayerMoveCancelledLocal() { // First remove both event handlers to prevent memory leaks if (_playerRef != null) { _playerRef.OnArrivedAtTarget -= OnPlayerArrivedLocal; _playerRef.OnMoveToCancelled -= OnPlayerMoveCancelledLocal; } // Then handle the cancellation OnPlayerMoveCancelledAsync().ContinueWith(_ => tcs.TrySetResult(false)); } // Unsubscribe previous handlers (if any) _playerRef.OnArrivedAtTarget -= OnPlayerArrived; _playerRef.OnMoveToCancelled -= OnPlayerMoveCancelled; // Subscribe our new handlers _playerRef.OnArrivedAtTarget += OnPlayerArrivedLocal; _playerRef.OnMoveToCancelled += OnPlayerMoveCancelledLocal; // Start the player movement _playerRef.MoveToAndNotify(stopPoint); // Await player arrival await tcs.Task; } } private async Task OnPlayerMoveCancelledAsync() { _interactionInProgress = false; interactionInterrupted?.Invoke(); await DispatchEventAsync(InteractionEventType.InteractionInterrupted); } private async Task OnPlayerArrivedAsync() { if (!_interactionInProgress) return; // Dispatch PlayerArrived event await DispatchEventAsync(InteractionEventType.PlayerArrived); // After all PlayerArrived actions complete, proceed to character interaction await HandleCharacterInteractionAsync(); } private async Task HandleCharacterInteractionAsync() { if (characterToInteract == CharacterToInteract.Pulver) { // We need to wait for the follower to arrive, so use a TaskCompletionSource var tcs = new TaskCompletionSource(); // Create a proper local function for the event handler void OnFollowerArrivedLocal() { // First remove the event handler to prevent memory leaks if (_followerController != null) { _followerController.OnPickupArrived -= OnFollowerArrivedLocal; } // Then continue with the interaction flow OnFollowerArrivedAsync().ContinueWith(_ => tcs.TrySetResult(true)); } // Register our new local function handler _followerController.OnPickupArrived += OnFollowerArrivedLocal; // Check for a CharacterMoveToTarget component for Pulver or Both Vector3 targetPosition = transform.position; CharacterMoveToTarget[] moveTargets = GetComponentsInChildren(); foreach (var target in moveTargets) { if (target.characterType == CharacterToInteract.Pulver || target.characterType == CharacterToInteract.Both) { targetPosition = target.GetTargetPosition(); break; } } // Use the new GoToPoint method instead of GoToPointAndReturn _followerController.GoToPoint(targetPosition); // Await follower arrival await tcs.Task; } else if (characterToInteract == CharacterToInteract.Trafalgar) { await BroadcastCharacterArrivedAsync(); } else if (characterToInteract == CharacterToInteract.Both) { // We need to wait for the follower to arrive, so use a TaskCompletionSource var tcs = new TaskCompletionSource(); // Create a proper local function for the event handler void OnFollowerArrivedLocal() { // First remove the event handler to prevent memory leaks if (_followerController != null) { _followerController.OnPickupArrived -= OnFollowerArrivedLocal; } // Then continue with the interaction flow OnFollowerArrivedAsync().ContinueWith(_ => tcs.TrySetResult(true)); } // Register our new local function handler _followerController.OnPickupArrived += OnFollowerArrivedLocal; // Check for a CharacterMoveToTarget component for Pulver or Both Vector3 targetPosition = transform.position; CharacterMoveToTarget[] moveTargets = GetComponentsInChildren(); foreach (var target in moveTargets) { if (target.characterType == CharacterToInteract.Pulver || target.characterType == CharacterToInteract.Both) { targetPosition = target.GetTargetPosition(); break; } } // Use the new GoToPoint method instead of GoToPointAndReturn _followerController.GoToPoint(targetPosition); // Await follower arrival await tcs.Task; } } private async Task OnFollowerArrivedAsync() { if (!_interactionInProgress) return; // Dispatch InteractingCharacterArrived event and WAIT for all actions to complete // This ensures we wait for any timeline animations to finish before proceeding Debug.Log("[Interactable] Follower arrived, dispatching InteractingCharacterArrived event and waiting for completion"); await DispatchEventAsync(InteractionEventType.InteractingCharacterArrived); Debug.Log("[Interactable] All InteractingCharacterArrived actions completed, proceeding with interaction"); // Check if we have any components that might have paused the interaction flow bool hasTimelineActions = false; foreach (var action in _registeredActions) { if (action is InteractionTimelineAction timelineAction && timelineAction.respondToEvents.Contains(InteractionEventType.InteractingCharacterArrived) && timelineAction.pauseInteractionFlow) { hasTimelineActions = true; break; } } // Tell the follower to return to the player if (_followerController != null && _playerRef != null) { _followerController.ReturnToPlayer(_playerRef.transform); } // After all InteractingCharacterArrived actions complete, proceed to character arrived await BroadcastCharacterArrivedAsync(); } // Legacy non-async method to maintain compatibility with existing code private void OnPlayerArrived() { // This is now just a wrapper for the async version _ = OnPlayerArrivedAsync(); } // Legacy non-async method to maintain compatibility with existing code private void OnPlayerMoveCancelled() { // This is now just a wrapper for the async version _ = OnPlayerMoveCancelledAsync(); } private async Task BroadcastCharacterArrivedAsync() { // Check for ObjectiveStepBehaviour and lock state var step = GetComponent(); if (step != null && !step.IsStepUnlocked()) { DebugUIMessage.Show("This step is locked!", Color.yellow); BroadcastInteractionComplete(false); // Reset variables for next time _interactionInProgress = false; _playerRef = null; _followerController = null; return; } // Dispatch CharacterArrived event // await DispatchEventAsync(InteractionEventType.InteractingCharacterArrived); // Broadcast appropriate event characterArrived?.Invoke(); // Reset variables for next time _interactionInProgress = false; _playerRef = null; _followerController = null; } private async void OnInteractionComplete(bool success) { // Dispatch InteractionComplete event await DispatchEventAsync(InteractionEventType.InteractionComplete); if (success) { if (isOneTime) { _isActive = false; } else if (cooldown >= 0f) { StartCoroutine(HandleCooldown()); } } } private System.Collections.IEnumerator HandleCooldown() { _isActive = false; yield return new WaitForSeconds(cooldown); _isActive = true; } public void OnHoldStart(Vector2 position) { throw new NotImplementedException(); } public void OnHoldMove(Vector2 position) { throw new NotImplementedException(); } public void OnHoldEnd(Vector2 position) { throw new NotImplementedException(); } public void BroadcastInteractionComplete(bool success) { interactionComplete?.Invoke(success); } #if UNITY_EDITOR /// /// Draws gizmos for pickup interaction range in the editor. /// void OnDrawGizmos() { float playerStopDistance = characterToInteract == CharacterToInteract.Trafalgar ? AppleHills.SettingsAccess.GetPlayerStopDistanceDirectInteraction() : AppleHills.SettingsAccess.GetPlayerStopDistance(); Gizmos.color = Color.yellow; Gizmos.DrawWireSphere(transform.position, playerStopDistance); GameObject playerObj = GameObject.FindGameObjectWithTag("Player"); if (playerObj != null) { Vector3 stopPoint = transform.position + (playerObj.transform.position - transform.position).normalized * playerStopDistance; Gizmos.color = Color.cyan; Gizmos.DrawSphere(stopPoint, 0.15f); } } #endif } }