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