using Input; using UnityEngine; using System; using System.Collections.Generic; using UnityEngine.Events; using System.Threading.Tasks; using Core; using Core.Lifecycle; namespace Interactions { public enum CharacterToInteract { None, Trafalgar, Pulver, Both } /// /// Base class for interactable objects that can respond to tap input events. /// Subclasses should override OnCharacterArrived() to implement interaction-specific logic. /// public class InteractableBase : ManagedBehaviour, 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; private PlayerTouchController playerRef; protected FollowerController FollowerController; private bool isActive = true; // Action component system private List _registeredActions = new List(); // ManagedBehaviour configuration public override int ManagedAwakePriority => 100; // Gameplay base classes /// /// 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) { // 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. /// Can be overridden for fully custom interaction logic. /// public virtual void OnTap(Vector2 worldPosition) { // 1. High-level validation if (!CanBeClicked()) { return; // Silent failure } Logging.Debug($"[Interactable] OnTap at {worldPosition} on {gameObject.name}"); // Start the interaction process asynchronously _ = StartInteractionFlowAsync(); } /// /// Template method that orchestrates the entire interaction flow. /// private async Task StartInteractionFlowAsync() { // 2. Find characters playerRef = FindFirstObjectByType(); FollowerController = FindFirstObjectByType(); // 3. Virtual hook: Setup OnInteractionStarted(); // 4. Fire events interactionStarted?.Invoke(playerRef, FollowerController); await DispatchEventAsync(InteractionEventType.InteractionStarted); // 5. Orchestrate character movement await MoveCharactersAsync(); // 6. Virtual hook: Arrival reaction OnInteractingCharacterArrived(); // 7. Fire arrival events characterArrived?.Invoke(); await DispatchEventAsync(InteractionEventType.InteractingCharacterArrived); // 8. Validation (base + child) var (canProceed, errorMessage) = ValidateInteraction(); if (!canProceed) { if (!string.IsNullOrEmpty(errorMessage)) { DebugUIMessage.Show(errorMessage, Color.yellow); } FinishInteraction(false); return; } // 9. Virtual main logic: Do the thing! bool success = DoInteraction(); // 10. Finish up FinishInteraction(success); } #region Virtual Lifecycle Methods /// /// High-level clickability check. Called BEFORE interaction starts. /// Override to add custom high-level validation (is active, on cooldown, etc.) /// /// True if interaction can start, false for silent rejection protected virtual bool CanBeClicked() { if (!isActive) return false; // Note: isOneTime and cooldown handled in FinishInteraction return true; } /// /// Called after characters are found but before movement starts. /// Override to perform setup logic. /// protected virtual void OnInteractionStarted() { // Default: do nothing } /// /// Called when the interacting character reaches destination. /// Override to trigger animations or other arrival reactions. /// protected virtual void OnInteractingCharacterArrived() { // Default: do nothing } /// /// Main interaction logic. OVERRIDE THIS in child classes. /// /// True if interaction succeeded, false otherwise protected virtual bool DoInteraction() { Logging.Warning($"[Interactable] DoInteraction not implemented for {GetType().Name}"); return false; } /// /// Called after interaction completes. Override to perform cleanup logic. /// /// Whether the interaction succeeded protected virtual void OnInteractionFinished(bool success) { // Default: do nothing } /// /// Child-specific validation. Override to add interaction-specific validation. /// /// Tuple of (canProceed, errorMessage) protected virtual (bool canProceed, string errorMessage) CanProceedWithInteraction() { return (true, null); // Default: always allow } #endregion #region Validation /// /// Combines base and child validation. /// private (bool, string) ValidateInteraction() { // Base validation (always runs) var (baseValid, baseError) = ValidateInteractionBase(); if (!baseValid) return (false, baseError); // Child validation (optional override) var (childValid, childError) = CanProceedWithInteraction(); if (!childValid) return (false, childError); return (true, null); } /// /// Base validation that always runs. Checks puzzle step locks and common prerequisites. /// private (bool canProceed, string errorMessage) ValidateInteractionBase() { // Check if there's an ObjectiveStepBehaviour attached var step = GetComponent(); if (step != null && !step.IsStepUnlocked()) { // Special case: ItemSlots can still be interacted with when locked (to swap items) if (!(this is ItemSlot)) { return (false, "This step is locked!"); } } return (true, null); } #endregion #region Character Movement Orchestration /// /// Orchestrates character movement based on characterToInteract setting. /// private async Task MoveCharactersAsync() { if (playerRef == null) { Logging.Debug($"[Interactable] Player character could not be found. Aborting interaction."); interactionInterrupted.Invoke(); await DispatchEventAsync(InteractionEventType.InteractionInterrupted); return; } // If characterToInteract is None, skip movement if (characterToInteract == CharacterToInteract.None) { return; // Continue to arrival } // Move player and optionally follower based on characterToInteract setting if (characterToInteract == CharacterToInteract.Trafalgar) { await MovePlayerAsync(); } else if (characterToInteract == CharacterToInteract.Pulver || characterToInteract == CharacterToInteract.Both) { await MovePlayerAsync(); // Move player to range first await MoveFollowerAsync(); // Then move follower to interaction point } } /// /// Moves the player to the interaction point or custom target. /// private async Task MovePlayerAsync() { Vector3 stopPoint = transform.position; // Default to interactable position bool customTargetFound = false; // Check for a CharacterMoveToTarget component for Trafalgar or Both CharacterMoveToTarget[] moveTargets = GetComponentsInChildren(); foreach (var target in moveTargets) { if (target.characterType == CharacterToInteract.Trafalgar || target.characterType == CharacterToInteract.Both) { stopPoint = target.GetTargetPosition(); customTargetFound = true; break; } } // If no custom target, use default distance if (!customTargetFound) { 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; } // Wait for player to arrive var tcs = new TaskCompletionSource(); void OnPlayerArrivedLocal() { if (playerRef != null) { playerRef.OnArrivedAtTarget -= OnPlayerArrivedLocal; playerRef.OnMoveToCancelled -= OnPlayerMoveCancelledLocal; } tcs.TrySetResult(true); } void OnPlayerMoveCancelledLocal() { if (playerRef != null) { playerRef.OnArrivedAtTarget -= OnPlayerArrivedLocal; playerRef.OnMoveToCancelled -= OnPlayerMoveCancelledLocal; } _ = HandleInteractionCancelledAsync(); tcs.TrySetResult(false); } playerRef.OnArrivedAtTarget += OnPlayerArrivedLocal; playerRef.OnMoveToCancelled += OnPlayerMoveCancelledLocal; playerRef.MoveToAndNotify(stopPoint); await tcs.Task; } /// /// Moves the follower to the interaction point or custom target. /// private async Task MoveFollowerAsync() { if (FollowerController == null) return; // 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; } } // Wait for follower to arrive var tcs = new TaskCompletionSource(); void OnFollowerArrivedLocal() { if (FollowerController != null) { FollowerController.OnPickupArrived -= OnFollowerArrivedLocal; } // Tell follower to return to player if (FollowerController != null && playerRef != null) { FollowerController.ReturnToPlayer(playerRef.transform); } tcs.TrySetResult(true); } FollowerController.OnPickupArrived += OnFollowerArrivedLocal; FollowerController.GoToPoint(targetPosition); await tcs.Task; } /// /// Handles interaction being cancelled (player stopped moving). /// private async Task HandleInteractionCancelledAsync() { interactionInterrupted?.Invoke(); await DispatchEventAsync(InteractionEventType.InteractionInterrupted); } #endregion #region Finalization /// /// Finalizes the interaction after DoInteraction completes. /// private async void FinishInteraction(bool success) { // Virtual hook: Cleanup OnInteractionFinished(success); // Fire completion events interactionComplete?.Invoke(success); await DispatchEventAsync(InteractionEventType.InteractionComplete); // Handle one-time / cooldown if (success) { if (isOneTime) { isActive = false; } else if (cooldown >= 0f) { StartCoroutine(HandleCooldown()); } } // Reset state playerRef = null; FollowerController = null; } private System.Collections.IEnumerator HandleCooldown() { isActive = false; yield return new WaitForSeconds(cooldown); isActive = true; } #endregion #region Legacy Methods & Compatibility /// /// DEPRECATED: Override DoInteraction() instead. /// This method is kept temporarily for backward compatibility during migration. /// [Obsolete("Override DoInteraction() instead")] protected virtual void OnCharacterArrived() { // Default implementation does nothing // Children should override DoInteraction() in the new pattern } /// /// Call this from subclasses to mark the interaction as complete. /// NOTE: In the new pattern, just return true/false from DoInteraction(). /// This is kept for backward compatibility during migration. /// protected void CompleteInteraction(bool success) { // For now, this manually triggers completion // After migration, DoInteraction() return value will replace this interactionComplete?.Invoke(success); } /// /// Legacy method for backward compatibility. /// [Obsolete("Use CompleteInteraction instead")] public void BroadcastInteractionComplete(bool success) { CompleteInteraction(success); } #endregion #region ITouchInputConsumer Implementation public void OnHoldStart(Vector2 position) { throw new NotImplementedException(); } public void OnHoldMove(Vector2 position) { throw new NotImplementedException(); } public void OnHoldEnd(Vector2 position) { throw new NotImplementedException(); } #endregion #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 } }