diff --git a/Assets/Prefabs/Levels/Dump/ControllerSwitchItem.prefab b/Assets/Prefabs/Levels/Dump/ControllerSwitchItem.prefab
index d9469bb9..3897d939 100644
--- a/Assets/Prefabs/Levels/Dump/ControllerSwitchItem.prefab
+++ b/Assets/Prefabs/Levels/Dump/ControllerSwitchItem.prefab
@@ -13,7 +13,7 @@ GameObject:
- component: {fileID: 7813271480623895155}
- component: {fileID: 6196606079257550}
m_Layer: 0
- m_Name: ControllerSwitchItem
+ m_Name: ControllerSwitchItem_To_pulver
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
diff --git a/Assets/Scenes/Levels/Dump.unity b/Assets/Scenes/Levels/Dump.unity
index b32fbf33..f048ac7c 100644
--- a/Assets/Scenes/Levels/Dump.unity
+++ b/Assets/Scenes/Levels/Dump.unity
@@ -935,7 +935,7 @@ Transform:
m_GameObject: {fileID: 258612751}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
- m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalPosition: {x: 0.6, y: -0.6, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
@@ -363427,6 +363427,10 @@ PrefabInstance:
serializedVersion: 3
m_TransformParent: {fileID: 0}
m_Modifications:
+ - target: {fileID: 6196606079257550, guid: 67a60833f9f205940a2308bd74a2863e, type: 3}
+ propertyPath: m_AutoTiling
+ value: 0
+ objectReference: {fileID: 0}
- target: {fileID: 1136290793271151494, guid: 67a60833f9f205940a2308bd74a2863e, type: 3}
propertyPath: m_LocalPosition.x
value: 185.3
diff --git a/Assets/Scripts/Input/BasePlayerMovementController.cs b/Assets/Scripts/Input/BasePlayerMovementController.cs
index bee059fb..bdda0b22 100644
--- a/Assets/Scripts/Input/BasePlayerMovementController.cs
+++ b/Assets/Scripts/Input/BasePlayerMovementController.cs
@@ -9,9 +9,10 @@ namespace Input
///
/// Base class for player movement controllers.
/// Handles tap-to-move and hold-to-move input with pathfinding or direct movement.
+ /// Implements IInteractingCharacter to enable interaction with items.
/// Derived classes can override to add specialized behavior (e.g., shader updates).
///
- public abstract class BasePlayerMovementController : ManagedBehaviour, ITouchInputConsumer
+ public abstract class BasePlayerMovementController : ManagedBehaviour, ITouchInputConsumer, IInteractingCharacter
{
[Header("Movement")]
[SerializeField] protected float moveSpeed = 5f;
@@ -42,6 +43,12 @@ namespace Input
public event System.Action OnMovementStarted;
public event System.Action OnMovementStopped;
+ // IInteractingCharacter implementation - scripted movement for interactions
+ private Coroutine _moveToCoroutine;
+ private bool _interruptMoveTo;
+ public event System.Action OnArrivedAtTarget;
+ public event System.Action OnMoveToCancelled;
+
// Components
protected AIPath _aiPath;
protected Animator _animator;
@@ -95,6 +102,7 @@ namespace Input
public virtual void OnTap(Vector2 worldPosition)
{
+ InterruptMoveTo(); // Cancel any scripted movement
Logging.Debug($"[{GetType().Name}] OnTap at {worldPosition}");
if (_aiPath != null)
{
@@ -109,6 +117,7 @@ namespace Input
public virtual void OnHoldStart(Vector2 worldPosition)
{
+ InterruptMoveTo(); // Cancel any scripted movement
Logging.Debug($"[{GetType().Name}] OnHoldStart at {worldPosition}");
_lastHoldPosition = worldPosition;
_isHolding = true;
@@ -318,6 +327,78 @@ namespace Input
}
#endregion
+
+ #region IInteractingCharacter Implementation
+
+ ///
+ /// Moves the character to a specific target position and notifies via events when arrived or cancelled.
+ /// This is used by systems like interactions to orchestrate scripted movement.
+ ///
+ public virtual 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 virtual void InterruptMoveTo()
+ {
+ _interruptMoveTo = true;
+ _isHolding = false;
+ _directMoveVelocity = Vector3.zero;
+ if (Settings != null && Settings.DefaultHoldMovementMode == HoldMovementMode.Direct && _aiPath != null)
+ _aiPath.enabled = false;
+ OnMoveToCancelled?.Invoke();
+ }
+
+ ///
+ /// Coroutine for moving the character to a target position and firing arrival/cancel events.
+ ///
+ protected virtual System.Collections.IEnumerator MoveToTargetCoroutine(Vector3 target)
+ {
+ if (_aiPath != null)
+ {
+ _aiPath.destination = target;
+ _aiPath.maxSpeed = Settings.MoveSpeed;
+ _aiPath.maxAcceleration = Settings.MaxAcceleration;
+ }
+
+ 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 <= Settings.StopDistance + 0.2f)
+ {
+ break;
+ }
+
+ yield return null;
+ }
+
+ _moveToCoroutine = null;
+ if (!_interruptMoveTo)
+ {
+ OnArrivedAtTarget?.Invoke();
+ }
+ }
+
+ #endregion
}
}
diff --git a/Assets/Scripts/Input/IInteractingCharacter.cs b/Assets/Scripts/Input/IInteractingCharacter.cs
new file mode 100644
index 00000000..3df98aff
--- /dev/null
+++ b/Assets/Scripts/Input/IInteractingCharacter.cs
@@ -0,0 +1,38 @@
+using UnityEngine;
+
+namespace Input
+{
+ ///
+ /// Interface for characters that can participate in scripted interactions.
+ /// Provides movement-to-target with arrival/cancellation notifications.
+ /// Implemented by BasePlayerMovementController to enable all controllers to interact with items.
+ ///
+ public interface IInteractingCharacter
+ {
+ ///
+ /// Moves character to target position and notifies when arrived/cancelled
+ ///
+ void MoveToAndNotify(Vector3 target);
+
+ ///
+ /// Interrupts any in-progress MoveToAndNotify operation
+ ///
+ void InterruptMoveTo();
+
+ ///
+ /// Fired when character arrives at MoveToAndNotify target
+ ///
+ event System.Action OnArrivedAtTarget;
+
+ ///
+ /// Fired when MoveToAndNotify is cancelled/interrupted
+ ///
+ event System.Action OnMoveToCancelled;
+
+ ///
+ /// Character's transform (for position queries)
+ ///
+ Transform transform { get; }
+ }
+}
+
diff --git a/Assets/Scripts/Input/IInteractingCharacter.cs.meta b/Assets/Scripts/Input/IInteractingCharacter.cs.meta
new file mode 100644
index 00000000..6db85f16
--- /dev/null
+++ b/Assets/Scripts/Input/IInteractingCharacter.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 0d1b1be281334b9390cc96d7a8ff3132
+timeCreated: 1765754798
\ No newline at end of file
diff --git a/Assets/Scripts/Input/InputManager.cs b/Assets/Scripts/Input/InputManager.cs
index c4d867bb..5fd61373 100644
--- a/Assets/Scripts/Input/InputManager.cs
+++ b/Assets/Scripts/Input/InputManager.cs
@@ -521,6 +521,16 @@ namespace Input
return !string.IsNullOrEmpty(controllerName) && _registeredControllers.ContainsKey(controllerName);
}
+ ///
+ /// Gets the currently active controller (the default consumer).
+ /// This is the controller that currently has input control.
+ ///
+ /// The active controller, or null if no default consumer is set
+ public ITouchInputConsumer GetActiveController()
+ {
+ return defaultConsumer;
+ }
+
#endregion
}
}
diff --git a/Assets/Scripts/Input/PlayerTouchController.cs b/Assets/Scripts/Input/PlayerTouchController.cs
index 21e2a34e..cb4fff7a 100644
--- a/Assets/Scripts/Input/PlayerTouchController.cs
+++ b/Assets/Scripts/Input/PlayerTouchController.cs
@@ -16,17 +16,12 @@ 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.
- /// Extends BasePlayerMovementController with save/load and MoveToAndNotify functionality.
+ /// Supports both direct and pathfinding movement modes.
+ /// Extends BasePlayerMovementController with save/load functionality.
+ /// Interaction capability (MoveToAndNotify) is provided by base class.
///
public class PlayerTouchController : BasePlayerMovementController
{
- // --- PlayerTouchController-specific features (MoveToAndNotify) ---
- public delegate void ArrivedAtTargetHandler();
- private Coroutine _moveToCoroutine;
- public event ArrivedAtTargetHandler OnArrivedAtTarget;
- public event System.Action OnMoveToCancelled;
- private bool _interruptMoveTo;
// Save system configuration
public override bool AutoRegisterForSave => true;
@@ -51,89 +46,6 @@ namespace Input
}
}
- #region ITouchInputConsumer Overrides (Add InterruptMoveTo)
-
- public override void OnTap(Vector2 worldPosition)
- {
- InterruptMoveTo();
- base.OnTap(worldPosition);
- }
-
- public override void OnHoldStart(Vector2 worldPosition)
- {
- InterruptMoveTo();
- base.OnHoldStart(worldPosition);
- }
-
- #endregion
-
- ///
- /// 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 (Settings.DefaultHoldMovementMode == 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 = Settings.MoveSpeed;
- _aiPath.maxAcceleration = Settings.MaxAcceleration;
- }
-
- 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 <= Settings.StopDistance + 0.2f)
- {
- break;
- }
-
- yield return null;
- }
-
- _moveToCoroutine = null;
- if (!_interruptMoveTo)
- {
- OnArrivedAtTarget?.Invoke();
- }
- }
#region Save/Load Lifecycle Hooks
diff --git a/Assets/Scripts/Interactions/Interactable.cs b/Assets/Scripts/Interactions/Interactable.cs
index 0903a7cd..4babad4e 100644
--- a/Assets/Scripts/Interactions/Interactable.cs
+++ b/Assets/Scripts/Interactions/Interactable.cs
@@ -6,6 +6,7 @@ using UnityEngine.Events;
using System.Threading.Tasks;
using Core;
using Core.Lifecycle;
+using Utils;
namespace Interactions
{
@@ -34,7 +35,7 @@ namespace Interactions
public UnityEvent characterArrived;
public UnityEvent interactionComplete;
- private PlayerTouchController playerRef;
+ private IInteractingCharacter _interactingCharacter;
protected FollowerController FollowerController;
private bool isActive = true;
@@ -69,7 +70,7 @@ namespace Interactions
///
/// Dispatch an interaction event to all registered actions and await their completion
///
- private async Task DispatchEventAsync(InteractionEventType eventType)
+ private async Task DispatchEventAsync(InteractionEventType eventType, PlayerTouchController playerRef = null)
{
// Collect all tasks from actions that want to respond
List> tasks = new List>();
@@ -114,26 +115,45 @@ namespace Interactions
///
private async Task StartInteractionFlowAsync()
{
- // 2. Find characters
- playerRef = FindFirstObjectByType();
+ // 2. Find characters - get the ACTIVE controller from InputManager
+ BasePlayerMovementController playerController = null;
+
+ if (InputManager.Instance != null)
+ {
+ // Get the controller that currently has input control
+ var activeController = InputManager.Instance.GetActiveController();
+ playerController = activeController as BasePlayerMovementController;
+ }
+
+ // Fallback: if InputManager doesn't have an active controller, try to find PlayerTouchController specifically
+ if (playerController == null)
+ {
+ playerController = FindFirstObjectByType();
+ Logging.Warning("[Interactable] No active controller from InputManager, falling back to FindFirstObjectByType");
+ }
+
+ _interactingCharacter = playerController;
FollowerController = FindFirstObjectByType();
+ // For legacy event compatibility, try to get PlayerTouchController reference
+ var playerRef = playerController as PlayerTouchController;
+
// 3. Virtual hook: Setup
OnInteractionStarted();
// 4. Fire events
interactionStarted?.Invoke(playerRef, FollowerController);
- await DispatchEventAsync(InteractionEventType.InteractionStarted);
+ await DispatchEventAsync(InteractionEventType.InteractionStarted, playerRef);
// 5. Orchestrate character movement
- await MoveCharactersAsync();
+ await MoveCharactersAsync(playerRef);
// 6. Virtual hook: Arrival reaction
OnInteractingCharacterArrived();
// 7. Fire arrival events
characterArrived?.Invoke();
- await DispatchEventAsync(InteractionEventType.InteractingCharacterArrived);
+ await DispatchEventAsync(InteractionEventType.InteractingCharacterArrived, playerRef);
// 8. Validation (base + child)
var (canProceed, errorMessage) = ValidateInteraction();
@@ -143,7 +163,7 @@ namespace Interactions
{
DebugUIMessage.Show(errorMessage, Color.yellow);
}
- FinishInteraction(false);
+ FinishInteraction(false, playerRef);
return;
}
@@ -151,7 +171,7 @@ namespace Interactions
bool success = DoInteraction();
// 10. Finish up
- FinishInteraction(success);
+ FinishInteraction(success, playerRef);
}
#region Virtual Lifecycle Methods
@@ -262,13 +282,13 @@ namespace Interactions
///
/// Orchestrates character movement based on characterToInteract setting.
///
- private async Task MoveCharactersAsync()
+ private async Task MoveCharactersAsync(PlayerTouchController playerRef = null)
{
- if (playerRef == null)
+ if (_interactingCharacter == null)
{
- Logging.Debug($"[Interactable] Player character could not be found. Aborting interaction.");
+ Logging.Debug($"[Interactable] No interacting character found. Aborting interaction.");
interactionInterrupted.Invoke();
- await DispatchEventAsync(InteractionEventType.InteractionInterrupted);
+ await DispatchEventAsync(InteractionEventType.InteractionInterrupted, playerRef);
return;
}
@@ -278,31 +298,42 @@ namespace Interactions
return; // Continue to arrival
}
- // Move player and optionally follower based on characterToInteract setting
+ // Move the appropriate character based on characterToInteract setting
if (characterToInteract == CharacterToInteract.Trafalgar)
{
- await MovePlayerAsync();
+ await MoveCharacterAsync(_interactingCharacter, CharacterToInteract.Trafalgar, playerRef);
}
- else if (characterToInteract == CharacterToInteract.Pulver || characterToInteract == CharacterToInteract.Both)
+ else if (characterToInteract == CharacterToInteract.Pulver)
{
- await MovePlayerAsync(); // Move player to range first
- await MoveFollowerAsync(); // Then move follower to interaction point
+ await MoveCharacterAsync(_interactingCharacter, CharacterToInteract.Pulver, playerRef);
+ }
+ else if (characterToInteract == CharacterToInteract.Both)
+ {
+ await MoveCharacterAsync(_interactingCharacter, CharacterToInteract.Trafalgar, playerRef); // Move first character to range
+ await MoveFollowerAsync(playerRef); // Then move follower to interaction point
}
}
///
- /// Moves the player to the interaction point or custom target.
+ /// Moves a character controller to the interaction point or custom target.
+ /// Works with any controller implementing IInteractingCharacter.
///
- private async Task MovePlayerAsync()
+ private async Task MoveCharacterAsync(IInteractingCharacter character, CharacterToInteract targetCharacterType, PlayerTouchController playerRef = null)
{
+ if (character == null)
+ {
+ Logging.Warning("[Interactable] Cannot move null character");
+ return;
+ }
+
Vector3 stopPoint = transform.position; // Default to interactable position
bool customTargetFound = false;
- // Check for a CharacterMoveToTarget component for Trafalgar or Both
+ // Check for a CharacterMoveToTarget component
CharacterMoveToTarget[] moveTargets = GetComponentsInChildren();
foreach (var target in moveTargets)
{
- if (target.characterType == CharacterToInteract.Trafalgar || target.characterType == CharacterToInteract.Both)
+ if (target.characterType == targetCharacterType || target.characterType == CharacterToInteract.Both)
{
stopPoint = target.GetTargetPosition();
customTargetFound = true;
@@ -314,49 +345,26 @@ namespace Interactions
if (!customTargetFound)
{
Vector3 interactablePos = transform.position;
- Vector3 playerPos = playerRef.transform.position;
- float stopDistance = characterToInteract == CharacterToInteract.Pulver
+ Vector3 characterPos = character.transform.position;
+ float stopDistance = targetCharacterType == CharacterToInteract.Pulver
? GameManager.Instance.PlayerStopDistance
: GameManager.Instance.PlayerStopDistanceDirectInteraction;
- Vector3 toPlayer = (playerPos - interactablePos).normalized;
- stopPoint = interactablePos + toPlayer * stopDistance;
+ stopPoint = MovementUtilities.CalculateStopPosition(interactablePos, characterPos, stopDistance);
}
- // Wait for player to arrive
- var tcs = new TaskCompletionSource();
+ // Use MovementUtilities to handle movement
+ bool arrived = await MovementUtilities.MoveToPositionAsync(character, stopPoint);
- void OnPlayerArrivedLocal()
+ if (!arrived)
{
- if (playerRef != null)
- {
- playerRef.OnArrivedAtTarget -= OnPlayerArrivedLocal;
- playerRef.OnMoveToCancelled -= OnPlayerMoveCancelledLocal;
- }
- tcs.TrySetResult(true);
+ _ = HandleInteractionCancelledAsync(playerRef);
}
-
- 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()
+ private async Task MoveFollowerAsync(PlayerTouchController playerRef = null)
{
if (FollowerController == null)
return;
@@ -383,7 +391,7 @@ namespace Interactions
FollowerController.OnPickupArrived -= OnFollowerArrivedLocal;
}
- // Tell follower to return to player
+ // Tell follower to return to player if we have a PlayerTouchController reference
if (FollowerController != null && playerRef != null)
{
FollowerController.ReturnToPlayer(playerRef.transform);
@@ -401,10 +409,10 @@ namespace Interactions
///
/// Handles interaction being cancelled (player stopped moving).
///
- private async Task HandleInteractionCancelledAsync()
+ private async Task HandleInteractionCancelledAsync(PlayerTouchController playerRef = null)
{
interactionInterrupted?.Invoke();
- await DispatchEventAsync(InteractionEventType.InteractionInterrupted);
+ await DispatchEventAsync(InteractionEventType.InteractionInterrupted, playerRef);
}
#endregion
@@ -414,14 +422,14 @@ namespace Interactions
///
/// Finalizes the interaction after DoInteraction completes.
///
- private async void FinishInteraction(bool success)
+ private async void FinishInteraction(bool success, PlayerTouchController playerRef = null)
{
// Virtual hook: Cleanup
OnInteractionFinished(success);
// Fire completion events
interactionComplete?.Invoke(success);
- await DispatchEventAsync(InteractionEventType.InteractionComplete);
+ await DispatchEventAsync(InteractionEventType.InteractionComplete, playerRef);
// Handle one-time / cooldown
if (success)
@@ -437,7 +445,7 @@ namespace Interactions
}
// Reset state
- playerRef = null;
+ _interactingCharacter = null;
FollowerController = null;
}
diff --git a/Assets/Scripts/Minigames/TrashMaze/Core/PulverController.cs b/Assets/Scripts/Minigames/TrashMaze/Core/PulverController.cs
index a8389300..06e69359 100644
--- a/Assets/Scripts/Minigames/TrashMaze/Core/PulverController.cs
+++ b/Assets/Scripts/Minigames/TrashMaze/Core/PulverController.cs
@@ -9,6 +9,7 @@ namespace Minigames.TrashMaze.Core
/// Controls Pulver character movement in the Trash Maze.
/// Inherits from BasePlayerMovementController for tap-to-move and hold-to-move.
/// Updates global shader properties for vision radius system.
+ /// Interaction capability (MoveToAndNotify) is provided by base class.
///
public class PulverController : BasePlayerMovementController
{
diff --git a/Assets/Scripts/Utilities.meta b/Assets/Scripts/Utilities.meta
new file mode 100644
index 00000000..56deed13
--- /dev/null
+++ b/Assets/Scripts/Utilities.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 56c21fe8abef4887a819d02c0fbdb5d8
+timeCreated: 1765753993
\ No newline at end of file
diff --git a/Assets/Scripts/Utils/MovementUtilities.cs b/Assets/Scripts/Utils/MovementUtilities.cs
new file mode 100644
index 00000000..7af1f331
--- /dev/null
+++ b/Assets/Scripts/Utils/MovementUtilities.cs
@@ -0,0 +1,66 @@
+using System.Threading.Tasks;
+using Core;
+using Input;
+using UnityEngine;
+
+namespace Utils
+{
+ ///
+ /// Utility methods for character movement operations.
+ /// Extracted from interaction/controller code for reusability.
+ ///
+ public static class MovementUtilities
+ {
+ ///
+ /// Moves a character to a target position and waits for arrival.
+ /// Works with any controller implementing IInteractingCharacter.
+ ///
+ /// The character to move (must implement IInteractingCharacter)
+ /// World position to move to
+ /// Task that completes when the character arrives or movement is cancelled
+ public static async Task MoveToPositionAsync(IInteractingCharacter character, Vector3 targetPosition)
+ {
+ if (character == null)
+ {
+ Logging.Warning("[MovementUtilities] Cannot move null character");
+ return false;
+ }
+
+ var tcs = new TaskCompletionSource();
+
+ void OnArrivedLocal()
+ {
+ character.OnArrivedAtTarget -= OnArrivedLocal;
+ character.OnMoveToCancelled -= OnCancelledLocal;
+ tcs.TrySetResult(true);
+ }
+
+ void OnCancelledLocal()
+ {
+ character.OnArrivedAtTarget -= OnArrivedLocal;
+ character.OnMoveToCancelled -= OnCancelledLocal;
+ tcs.TrySetResult(false);
+ }
+
+ character.OnArrivedAtTarget += OnArrivedLocal;
+ character.OnMoveToCancelled += OnCancelledLocal;
+ character.MoveToAndNotify(targetPosition);
+
+ return await tcs.Task;
+ }
+
+ ///
+ /// Calculates a stop position at a given distance from a target position towards a character.
+ ///
+ /// The target position
+ /// The character's current position
+ /// Distance from target to stop at
+ /// The calculated stop position
+ public static Vector3 CalculateStopPosition(Vector3 targetPosition, Vector3 characterPosition, float stopDistance)
+ {
+ Vector3 toCharacter = (characterPosition - targetPosition).normalized;
+ return targetPosition + toCharacter * stopDistance;
+ }
+ }
+}
+
diff --git a/Assets/Scripts/Utils/MovementUtilities.cs.meta b/Assets/Scripts/Utils/MovementUtilities.cs.meta
new file mode 100644
index 00000000..4452cfaf
--- /dev/null
+++ b/Assets/Scripts/Utils/MovementUtilities.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 29f4ca2c743f4890aab59e4ccdda2c79
+timeCreated: 1765753993
\ No newline at end of file