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;
///
/// Gets whether this interactable is currently active (can be clicked)
///
public bool IsActive => isActive;
// Action component system
private List _registeredActions = new List();
///
/// 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;
}
///
/// Enable or disable this interactable
///
public void SetActive(bool active)
{
isActive = active;
}
#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
}
}