Rework of base interactables and managed behaviors
This commit is contained in:
@@ -5,6 +5,7 @@ using System.Collections.Generic;
|
||||
using UnityEngine.Events;
|
||||
using System.Threading.Tasks;
|
||||
using Core;
|
||||
using Core.Lifecycle;
|
||||
|
||||
namespace Interactions
|
||||
{
|
||||
@@ -20,7 +21,7 @@ namespace Interactions
|
||||
/// Base class for interactable objects that can respond to tap input events.
|
||||
/// Subclasses should override OnCharacterArrived() to implement interaction-specific logic.
|
||||
/// </summary>
|
||||
public class InteractableBase : MonoBehaviour, ITouchInputConsumer
|
||||
public class InteractableBase : ManagedBehaviour, ITouchInputConsumer
|
||||
{
|
||||
[Header("Interaction Settings")]
|
||||
public bool isOneTime;
|
||||
@@ -33,21 +34,16 @@ namespace Interactions
|
||||
public UnityEvent characterArrived;
|
||||
public UnityEvent<bool> interactionComplete;
|
||||
|
||||
// Helpers for managing interaction state
|
||||
private bool _interactionInProgress;
|
||||
protected PlayerTouchController _playerRef;
|
||||
protected FollowerController _followerController;
|
||||
private bool _isActive = true;
|
||||
private InteractionEventType _currentEventType;
|
||||
private PlayerTouchController playerRef;
|
||||
protected FollowerController FollowerController;
|
||||
private bool isActive = true;
|
||||
|
||||
// Action component system
|
||||
private List<InteractionActionBase> _registeredActions = new List<InteractionActionBase>();
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
// Subscribe to interactionComplete event
|
||||
interactionComplete.AddListener(OnInteractionComplete);
|
||||
}
|
||||
// ManagedBehaviour configuration
|
||||
public override int ManagedAwakePriority => 100; // Gameplay base classes
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Register an action component with this interactable
|
||||
@@ -73,14 +69,12 @@ namespace Interactions
|
||||
/// </summary>
|
||||
private async Task DispatchEventAsync(InteractionEventType eventType)
|
||||
{
|
||||
_currentEventType = eventType;
|
||||
|
||||
// Collect all tasks from actions that want to respond
|
||||
List<Task<bool>> tasks = new List<Task<bool>>();
|
||||
|
||||
foreach (var action in _registeredActions)
|
||||
{
|
||||
Task<bool> task = action.OnInteractionEvent(eventType, _playerRef, _followerController);
|
||||
Task<bool> task = action.OnInteractionEvent(eventType, playerRef, FollowerController);
|
||||
if (task != null)
|
||||
{
|
||||
tasks.Add(task);
|
||||
@@ -97,39 +91,178 @@ namespace Interactions
|
||||
|
||||
/// <summary>
|
||||
/// Handles tap input. Triggers interaction logic.
|
||||
/// Can be overridden for fully custom interaction logic.
|
||||
/// </summary>
|
||||
public void OnTap(Vector2 worldPosition)
|
||||
public virtual void OnTap(Vector2 worldPosition)
|
||||
{
|
||||
if (!_isActive)
|
||||
// 1. High-level validation
|
||||
if (!CanBeClicked())
|
||||
{
|
||||
Logging.Debug($"[Interactable] Is disabled!");
|
||||
return;
|
||||
return; // Silent failure
|
||||
}
|
||||
|
||||
Logging.Debug($"[Interactable] OnTap at {worldPosition} on {gameObject.name}");
|
||||
|
||||
// Start the interaction process asynchronously
|
||||
_ = TryInteractAsync();
|
||||
_ = StartInteractionFlowAsync();
|
||||
}
|
||||
|
||||
private async Task TryInteractAsync()
|
||||
/// <summary>
|
||||
/// Template method that orchestrates the entire interaction flow.
|
||||
/// </summary>
|
||||
private async Task StartInteractionFlowAsync()
|
||||
{
|
||||
_interactionInProgress = true;
|
||||
// 2. Find characters
|
||||
playerRef = FindFirstObjectByType<PlayerTouchController>();
|
||||
FollowerController = FindFirstObjectByType<FollowerController>();
|
||||
|
||||
_playerRef = FindFirstObjectByType<PlayerTouchController>();
|
||||
_followerController = FindFirstObjectByType<FollowerController>();
|
||||
// 3. Virtual hook: Setup
|
||||
OnInteractionStarted();
|
||||
|
||||
interactionStarted?.Invoke(_playerRef, _followerController);
|
||||
|
||||
// Dispatch the InteractionStarted event to action components
|
||||
// 4. Fire events
|
||||
interactionStarted?.Invoke(playerRef, FollowerController);
|
||||
await DispatchEventAsync(InteractionEventType.InteractionStarted);
|
||||
|
||||
// After all InteractionStarted actions complete, proceed to player movement
|
||||
await StartPlayerMovementAsync();
|
||||
// 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);
|
||||
}
|
||||
|
||||
private async Task StartPlayerMovementAsync()
|
||||
#region Virtual Lifecycle Methods
|
||||
|
||||
/// <summary>
|
||||
/// High-level clickability check. Called BEFORE interaction starts.
|
||||
/// Override to add custom high-level validation (is active, on cooldown, etc.)
|
||||
/// </summary>
|
||||
/// <returns>True if interaction can start, false for silent rejection</returns>
|
||||
protected virtual bool CanBeClicked()
|
||||
{
|
||||
if (_playerRef == null)
|
||||
if (!isActive) return false;
|
||||
// Note: isOneTime and cooldown handled in FinishInteraction
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called after characters are found but before movement starts.
|
||||
/// Override to perform setup logic.
|
||||
/// </summary>
|
||||
protected virtual void OnInteractionStarted()
|
||||
{
|
||||
// Default: do nothing
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the interacting character reaches destination.
|
||||
/// Override to trigger animations or other arrival reactions.
|
||||
/// </summary>
|
||||
protected virtual void OnInteractingCharacterArrived()
|
||||
{
|
||||
// Default: do nothing
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Main interaction logic. OVERRIDE THIS in child classes.
|
||||
/// </summary>
|
||||
/// <returns>True if interaction succeeded, false otherwise</returns>
|
||||
protected virtual bool DoInteraction()
|
||||
{
|
||||
Debug.LogWarning($"[Interactable] DoInteraction not implemented for {GetType().Name}");
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called after interaction completes. Override to perform cleanup logic.
|
||||
/// </summary>
|
||||
/// <param name="success">Whether the interaction succeeded</param>
|
||||
protected virtual void OnInteractionFinished(bool success)
|
||||
{
|
||||
// Default: do nothing
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Child-specific validation. Override to add interaction-specific validation.
|
||||
/// </summary>
|
||||
/// <returns>Tuple of (canProceed, errorMessage)</returns>
|
||||
protected virtual (bool canProceed, string errorMessage) CanProceedWithInteraction()
|
||||
{
|
||||
return (true, null); // Default: always allow
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Validation
|
||||
|
||||
/// <summary>
|
||||
/// Combines base and child validation.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Base validation that always runs. Checks puzzle step locks and common prerequisites.
|
||||
/// </summary>
|
||||
private (bool canProceed, string errorMessage) ValidateInteractionBase()
|
||||
{
|
||||
// Check if there's an ObjectiveStepBehaviour attached
|
||||
var step = GetComponent<PuzzleS.ObjectiveStepBehaviour>();
|
||||
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
|
||||
|
||||
/// <summary>
|
||||
/// Orchestrates character movement based on characterToInteract setting.
|
||||
/// </summary>
|
||||
private async Task MoveCharactersAsync()
|
||||
{
|
||||
if (playerRef == null)
|
||||
{
|
||||
Logging.Debug($"[Interactable] Player character could not be found. Aborting interaction.");
|
||||
interactionInterrupted.Invoke();
|
||||
@@ -137,350 +270,222 @@ namespace Interactions
|
||||
return;
|
||||
}
|
||||
|
||||
// If characterToInteract is None, immediately trigger the characterArrived event
|
||||
// If characterToInteract is None, skip movement
|
||||
if (characterToInteract == CharacterToInteract.None)
|
||||
{
|
||||
await BroadcastCharacterArrivedAsync();
|
||||
return;
|
||||
return; // Continue to arrival
|
||||
}
|
||||
|
||||
// Check for a CharacterMoveToTarget component for Trafalgar (player) or Both
|
||||
Vector3 stopPoint;
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Moves the player to the interaction point or custom target.
|
||||
/// </summary>
|
||||
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<CharacterMoveToTarget>();
|
||||
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<bool>();
|
||||
|
||||
// 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;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If no custom target was found, use the default behavior
|
||||
// If no custom target, use default distance
|
||||
if (!customTargetFound)
|
||||
{
|
||||
// Compute closest point on the interaction radius
|
||||
Vector3 interactablePos = transform.position;
|
||||
Vector3 playerPos = _playerRef.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<bool>();
|
||||
|
||||
// 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);
|
||||
// Wait for player to arrive
|
||||
var tcs = new TaskCompletionSource<bool>();
|
||||
|
||||
// After all PlayerArrived actions complete, proceed to character interaction
|
||||
await HandleCharacterInteractionAsync();
|
||||
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;
|
||||
}
|
||||
|
||||
private async Task HandleCharacterInteractionAsync()
|
||||
/// <summary>
|
||||
/// Moves the follower to the interaction point or custom target.
|
||||
/// </summary>
|
||||
private async Task MoveFollowerAsync()
|
||||
{
|
||||
if (characterToInteract == CharacterToInteract.Pulver)
|
||||
{
|
||||
// We need to wait for the follower to arrive, so use a TaskCompletionSource
|
||||
var tcs = new TaskCompletionSource<bool>();
|
||||
|
||||
// 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<CharacterMoveToTarget>();
|
||||
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<bool>();
|
||||
|
||||
// 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<CharacterMoveToTarget>();
|
||||
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)
|
||||
if (FollowerController == null)
|
||||
return;
|
||||
|
||||
// Dispatch InteractingCharacterArrived event and WAIT for all actions to complete
|
||||
// This ensures we wait for any timeline animations to finish before proceeding
|
||||
Logging.Debug("[Interactable] Follower arrived, dispatching InteractingCharacterArrived event and waiting for completion");
|
||||
await DispatchEventAsync(InteractionEventType.InteractingCharacterArrived);
|
||||
Logging.Debug("[Interactable] All InteractingCharacterArrived actions completed, proceeding with interaction");
|
||||
|
||||
// Check if we have any components that might have paused the interaction flow
|
||||
foreach (var action in _registeredActions)
|
||||
// Check for a CharacterMoveToTarget component for Pulver or Both
|
||||
Vector3 targetPosition = transform.position;
|
||||
CharacterMoveToTarget[] moveTargets = GetComponentsInChildren<CharacterMoveToTarget>();
|
||||
foreach (var target in moveTargets)
|
||||
{
|
||||
if (action is InteractionTimelineAction timelineAction &&
|
||||
timelineAction.respondToEvents.Contains(InteractionEventType.InteractingCharacterArrived) &&
|
||||
timelineAction.pauseInteractionFlow)
|
||||
if (target.characterType == CharacterToInteract.Pulver || target.characterType == CharacterToInteract.Both)
|
||||
{
|
||||
targetPosition = target.GetTargetPosition();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Tell the follower to return to the player
|
||||
if (_followerController != null && _playerRef != null)
|
||||
// Wait for follower to arrive
|
||||
var tcs = new TaskCompletionSource<bool>();
|
||||
|
||||
void OnFollowerArrivedLocal()
|
||||
{
|
||||
_followerController.ReturnToPlayer(_playerRef.transform);
|
||||
if (FollowerController != null)
|
||||
{
|
||||
FollowerController.OnPickupArrived -= OnFollowerArrivedLocal;
|
||||
}
|
||||
|
||||
// Tell follower to return to player
|
||||
if (FollowerController != null && playerRef != null)
|
||||
{
|
||||
FollowerController.ReturnToPlayer(playerRef.transform);
|
||||
}
|
||||
|
||||
tcs.TrySetResult(true);
|
||||
}
|
||||
|
||||
// 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 Task BroadcastCharacterArrivedAsync()
|
||||
{
|
||||
// Check for ObjectiveStepBehaviour and lock state
|
||||
var step = GetComponent<PuzzleS.ObjectiveStepBehaviour>();
|
||||
var slot = GetComponent<ItemSlot>();
|
||||
if (step != null && !step.IsStepUnlocked() && slot == null)
|
||||
{
|
||||
DebugUIMessage.Show("This step is locked!", Color.yellow);
|
||||
CompleteInteraction(false);
|
||||
// Reset variables for next time
|
||||
_interactionInProgress = false;
|
||||
_playerRef = null;
|
||||
_followerController = null;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
// Dispatch CharacterArrived event
|
||||
// await DispatchEventAsync(InteractionEventType.InteractingCharacterArrived);
|
||||
FollowerController.OnPickupArrived += OnFollowerArrivedLocal;
|
||||
FollowerController.GoToPoint(targetPosition);
|
||||
|
||||
// Broadcast appropriate event
|
||||
characterArrived?.Invoke();
|
||||
|
||||
// Call the virtual method for subclasses to override
|
||||
OnCharacterArrived();
|
||||
|
||||
// Reset variables for next time
|
||||
_interactionInProgress = false;
|
||||
_playerRef = null;
|
||||
_followerController = null;
|
||||
return Task.CompletedTask;
|
||||
await tcs.Task;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the character has arrived at the interaction point.
|
||||
/// Subclasses should override this to implement interaction-specific logic
|
||||
/// and call CompleteInteraction(bool success) when done.
|
||||
/// Handles interaction being cancelled (player stopped moving).
|
||||
/// </summary>
|
||||
protected virtual void OnCharacterArrived()
|
||||
private async Task HandleInteractionCancelledAsync()
|
||||
{
|
||||
// Default implementation does nothing - subclasses should override
|
||||
// and call CompleteInteraction when their logic is complete
|
||||
interactionInterrupted?.Invoke();
|
||||
await DispatchEventAsync(InteractionEventType.InteractionInterrupted);
|
||||
}
|
||||
|
||||
private async void OnInteractionComplete(bool success)
|
||||
#endregion
|
||||
|
||||
#region Finalization
|
||||
|
||||
/// <summary>
|
||||
/// Finalizes the interaction after DoInteraction completes.
|
||||
/// </summary>
|
||||
private async void FinishInteraction(bool success)
|
||||
{
|
||||
// Dispatch InteractionComplete event
|
||||
// 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;
|
||||
isActive = false;
|
||||
}
|
||||
else if (cooldown >= 0f)
|
||||
{
|
||||
StartCoroutine(HandleCooldown());
|
||||
}
|
||||
}
|
||||
|
||||
// Reset state
|
||||
playerRef = null;
|
||||
FollowerController = null;
|
||||
}
|
||||
|
||||
private System.Collections.IEnumerator HandleCooldown()
|
||||
{
|
||||
_isActive = false;
|
||||
isActive = false;
|
||||
yield return new WaitForSeconds(cooldown);
|
||||
_isActive = true;
|
||||
isActive = true;
|
||||
}
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
#region Legacy Methods & Compatibility
|
||||
|
||||
/// <summary>
|
||||
/// DEPRECATED: Override DoInteraction() instead.
|
||||
/// This method is kept temporarily for backward compatibility during migration.
|
||||
/// </summary>
|
||||
[Obsolete("Override DoInteraction() instead")]
|
||||
protected virtual void OnCharacterArrived()
|
||||
{
|
||||
// Default implementation does nothing
|
||||
// Children should override DoInteraction() in the new pattern
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
protected void CompleteInteraction(bool success)
|
||||
{
|
||||
// For now, this manually triggers completion
|
||||
// After migration, DoInteraction() return value will replace this
|
||||
interactionComplete?.Invoke(success);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Legacy method for backward compatibility.
|
||||
/// </summary>
|
||||
[Obsolete("Use CompleteInteraction instead")]
|
||||
public void BroadcastInteractionComplete(bool success)
|
||||
{
|
||||
CompleteInteraction(success);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ITouchInputConsumer Implementation
|
||||
|
||||
public void OnHoldStart(Vector2 position)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
@@ -495,25 +500,8 @@ namespace Interactions
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Call this from subclasses to mark the interaction as complete.
|
||||
/// </summary>
|
||||
/// <param name="success">Whether the interaction was successful</param>
|
||||
protected void CompleteInteraction(bool success)
|
||||
{
|
||||
interactionComplete?.Invoke(success);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Legacy method for backward compatibility. Use CompleteInteraction instead.
|
||||
/// </summary>
|
||||
/// TODO: Remove this method in future versions
|
||||
[Obsolete("Use CompleteInteraction instead")]
|
||||
public void BroadcastInteractionComplete(bool success)
|
||||
{
|
||||
CompleteInteraction(success);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#if UNITY_EDITOR
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user