maze_switching (#82)
Co-authored-by: Michal Pikulski <michal.a.pikulski@gmail.com> Reviewed-on: #82
This commit is contained in:
@@ -9,9 +9,10 @@ namespace Input
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
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;
|
||||
@@ -67,13 +74,6 @@ namespace Input
|
||||
{
|
||||
base.OnManagedStart();
|
||||
|
||||
// Register with InputManager
|
||||
if (InputManager.Instance != null)
|
||||
{
|
||||
InputManager.Instance.SetDefaultConsumer(this);
|
||||
Logging.Debug($"[{GetType().Name}] Registered as default input consumer");
|
||||
}
|
||||
|
||||
_logVerbosity = DeveloperSettingsProvider.Instance.GetSettings<DebugSettings>().inputLogVerbosity;
|
||||
}
|
||||
|
||||
@@ -102,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)
|
||||
{
|
||||
@@ -116,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;
|
||||
@@ -325,6 +327,103 @@ namespace Input
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IInteractingCharacter Implementation
|
||||
|
||||
/// <summary>
|
||||
/// Controller-driven interaction movement. Base implementation moves this controller to the interactable.
|
||||
/// Override in derived classes for custom behavior (e.g., PlayerTouchController handles follower dispatch).
|
||||
/// </summary>
|
||||
public virtual async System.Threading.Tasks.Task<bool> MoveToInteractableAsync(Interactions.InteractableBase interactable)
|
||||
{
|
||||
// Default behavior: move self to interactable position
|
||||
Vector3 targetPosition = interactable.transform.position;
|
||||
|
||||
// Check for custom CharacterMoveToTarget
|
||||
var moveTargets = interactable.GetComponentsInChildren<Interactions.CharacterMoveToTarget>();
|
||||
foreach (var target in moveTargets)
|
||||
{
|
||||
if (target.characterType == Interactions.CharacterToInteract.Trafalgar ||
|
||||
target.characterType == Interactions.CharacterToInteract.Both)
|
||||
{
|
||||
targetPosition = target.GetTargetPosition();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Use MovementUtilities to handle movement
|
||||
return await Utils.MovementUtilities.MoveToPositionAsync(this, targetPosition);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cancels any in-progress MoveToAndNotify operation and fires the cancellation event.
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Coroutine for moving the character to a target position and firing arrival/cancel events.
|
||||
/// </summary>
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
47
Assets/Scripts/Input/IInteractingCharacter.cs
Normal file
47
Assets/Scripts/Input/IInteractingCharacter.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Input
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public interface IInteractingCharacter
|
||||
{
|
||||
/// <summary>
|
||||
/// Controller-driven interaction movement. Each controller implements its own behavior
|
||||
/// based on the interactable's settings (characterToInteract, CharacterMoveToTarget, etc.)
|
||||
/// </summary>
|
||||
/// <param name="interactable">The interactable to move to</param>
|
||||
/// <returns>True if movement succeeded and character arrived, false if cancelled/failed</returns>
|
||||
Task<bool> MoveToInteractableAsync(Interactions.InteractableBase interactable);
|
||||
|
||||
/// <summary>
|
||||
/// Moves character to target position and notifies when arrived/cancelled
|
||||
/// </summary>
|
||||
void MoveToAndNotify(Vector3 target);
|
||||
|
||||
/// <summary>
|
||||
/// Interrupts any in-progress MoveToAndNotify operation
|
||||
/// </summary>
|
||||
void InterruptMoveTo();
|
||||
|
||||
/// <summary>
|
||||
/// Fired when character arrives at MoveToAndNotify target
|
||||
/// </summary>
|
||||
event System.Action OnArrivedAtTarget;
|
||||
|
||||
/// <summary>
|
||||
/// Fired when MoveToAndNotify is cancelled/interrupted
|
||||
/// </summary>
|
||||
event System.Action OnMoveToCancelled;
|
||||
|
||||
/// <summary>
|
||||
/// Character's transform (for position queries)
|
||||
/// </summary>
|
||||
Transform transform { get; }
|
||||
}
|
||||
}
|
||||
|
||||
3
Assets/Scripts/Input/IInteractingCharacter.cs.meta
Normal file
3
Assets/Scripts/Input/IInteractingCharacter.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0d1b1be281334b9390cc96d7a8ff3132
|
||||
timeCreated: 1765754798
|
||||
@@ -33,6 +33,9 @@ namespace Input
|
||||
|
||||
// Track which consumer is handling the current hold operation
|
||||
private ITouchInputConsumer _activeHoldConsumer;
|
||||
|
||||
// Controller registration system
|
||||
private readonly Dictionary<string, ITouchInputConsumer> _registeredControllers = new Dictionary<string, ITouchInputConsumer>();
|
||||
|
||||
/// <summary>
|
||||
/// Singleton instance of the InputManager. No longer creates an instance if one doesn't exist.
|
||||
@@ -408,5 +411,126 @@ namespace Input
|
||||
consumer.OnTap(worldPos);
|
||||
return true;
|
||||
}
|
||||
|
||||
#region Controller Registration System
|
||||
|
||||
/// <summary>
|
||||
/// Registers a controller with a unique name for later switching.
|
||||
/// </summary>
|
||||
/// <param name="controllerName">Unique name for the controller</param>
|
||||
/// <param name="controller">The controller instance to register</param>
|
||||
/// <param name="setAsDefaultConsumer">If true, sets this controller as the default input consumer</param>
|
||||
public void RegisterController(string controllerName, ITouchInputConsumer controller, bool setAsDefaultConsumer = false)
|
||||
{
|
||||
if (string.IsNullOrEmpty(controllerName))
|
||||
{
|
||||
Debug.LogError("[InputManager] Cannot register controller with null or empty name.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (controller == null)
|
||||
{
|
||||
Debug.LogError($"[InputManager] Cannot register null controller for name: {controllerName}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (_registeredControllers.ContainsKey(controllerName))
|
||||
{
|
||||
Debug.LogWarning($"[InputManager] Controller with name '{controllerName}' is already registered. Overwriting.");
|
||||
}
|
||||
|
||||
_registeredControllers[controllerName] = controller;
|
||||
Logging.Debug($"Controller registered: {controllerName}");
|
||||
|
||||
if (setAsDefaultConsumer)
|
||||
{
|
||||
SetDefaultConsumer(controller);
|
||||
Logging.Debug($"Controller '{controllerName}' set as default consumer.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unregisters a controller by name.
|
||||
/// </summary>
|
||||
/// <param name="controllerName">Name of the controller to unregister</param>
|
||||
public void UnregisterController(string controllerName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(controllerName))
|
||||
{
|
||||
Debug.LogError("[InputManager] Cannot unregister controller with null or empty name.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (_registeredControllers.Remove(controllerName))
|
||||
{
|
||||
Logging.Debug($"Controller unregistered: {controllerName}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning($"[InputManager] Attempted to unregister non-existent controller: {controllerName}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a registered controller by name.
|
||||
/// </summary>
|
||||
/// <param name="controllerName">Name of the controller to retrieve</param>
|
||||
/// <returns>The controller if found, null otherwise</returns>
|
||||
public ITouchInputConsumer GetController(string controllerName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(controllerName))
|
||||
{
|
||||
Debug.LogError("[InputManager] Cannot get controller with null or empty name.");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (_registeredControllers.TryGetValue(controllerName, out ITouchInputConsumer controller))
|
||||
{
|
||||
return controller;
|
||||
}
|
||||
|
||||
Debug.LogWarning($"[InputManager] Controller not found: {controllerName}");
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Switches to a registered controller by name, setting it as the default consumer.
|
||||
/// </summary>
|
||||
/// <param name="controllerName">Name of the controller to switch to</param>
|
||||
/// <returns>True if the switch was successful, false otherwise</returns>
|
||||
public bool SwitchToController(string controllerName)
|
||||
{
|
||||
ITouchInputConsumer controller = GetController(controllerName);
|
||||
if (controller != null)
|
||||
{
|
||||
SetDefaultConsumer(controller);
|
||||
Logging.Debug($"Switched to controller: {controllerName}");
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a controller with the given name is registered.
|
||||
/// </summary>
|
||||
/// <param name="controllerName">Name to check</param>
|
||||
/// <returns>True if registered, false otherwise</returns>
|
||||
public bool IsControllerRegistered(string controllerName)
|
||||
{
|
||||
return !string.IsNullOrEmpty(controllerName) && _registeredControllers.ContainsKey(controllerName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the currently active controller (the default consumer).
|
||||
/// This is the controller that currently has input control.
|
||||
/// </summary>
|
||||
/// <returns>The active controller, or null if no default consumer is set</returns>
|
||||
public ITouchInputConsumer GetActiveController()
|
||||
{
|
||||
return defaultConsumer;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,17 +16,12 @@ namespace Input
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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;
|
||||
@@ -39,89 +34,126 @@ namespace Input
|
||||
_movementSettings = configs.DefaultPlayerMovement;
|
||||
}
|
||||
|
||||
#region ITouchInputConsumer Overrides (Add InterruptMoveTo)
|
||||
|
||||
public override void OnTap(Vector2 worldPosition)
|
||||
internal override void OnManagedStart()
|
||||
{
|
||||
InterruptMoveTo();
|
||||
base.OnTap(worldPosition);
|
||||
base.OnManagedStart();
|
||||
|
||||
// Register with InputManager as default consumer
|
||||
if (InputManager.Instance != null)
|
||||
{
|
||||
InputManager.Instance.RegisterController("trafalgar", this, setAsDefaultConsumer: true);
|
||||
Logging.Debug($"[PlayerTouchController] Registered controller '{gameObject.name}' as default consumer");
|
||||
}
|
||||
}
|
||||
|
||||
#region IInteractingCharacter Override
|
||||
|
||||
public override void OnHoldStart(Vector2 worldPosition)
|
||||
/// <summary>
|
||||
/// PlayerTouchController-specific interaction movement.
|
||||
/// Handles main character movement + follower dispatch based on interactable.characterToInteract setting.
|
||||
/// </summary>
|
||||
public override async System.Threading.Tasks.Task<bool> MoveToInteractableAsync(Interactions.InteractableBase interactable)
|
||||
{
|
||||
InterruptMoveTo();
|
||||
base.OnHoldStart(worldPosition);
|
||||
var characterToInteract = interactable.characterToInteract;
|
||||
|
||||
// If None, skip movement
|
||||
if (characterToInteract == Interactions.CharacterToInteract.None)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Determine stop distance based on interaction type
|
||||
float stopDistance;
|
||||
if (characterToInteract == Interactions.CharacterToInteract.Trafalgar)
|
||||
{
|
||||
// Move ONLY main character directly to item (close distance)
|
||||
stopDistance = Core.GameManager.Instance.PlayerStopDistanceDirectInteraction;
|
||||
}
|
||||
else // Pulver or Both
|
||||
{
|
||||
// Move main character to radius (far distance)
|
||||
stopDistance = Core.GameManager.Instance.PlayerStopDistance;
|
||||
}
|
||||
|
||||
// Calculate stop position for main character
|
||||
Vector3 stopPoint = interactable.transform.position;
|
||||
bool customTargetFound = false;
|
||||
|
||||
// Check for custom CharacterMoveToTarget for main character
|
||||
var moveTargets = interactable.GetComponentsInChildren<Interactions.CharacterMoveToTarget>();
|
||||
foreach (var target in moveTargets)
|
||||
{
|
||||
if (target.characterType == Interactions.CharacterToInteract.Trafalgar ||
|
||||
target.characterType == Interactions.CharacterToInteract.Both)
|
||||
{
|
||||
stopPoint = target.GetTargetPosition();
|
||||
customTargetFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If no custom target, calculate based on distance
|
||||
if (!customTargetFound)
|
||||
{
|
||||
stopPoint = Utils.MovementUtilities.CalculateStopPosition(
|
||||
interactable.transform.position,
|
||||
transform.position,
|
||||
stopDistance
|
||||
);
|
||||
}
|
||||
|
||||
// Move main character
|
||||
bool mainCharacterArrived = await Utils.MovementUtilities.MoveToPositionAsync(this, stopPoint);
|
||||
|
||||
if (!mainCharacterArrived)
|
||||
{
|
||||
return false; // Movement cancelled
|
||||
}
|
||||
|
||||
// Handle follower dispatch based on interaction type
|
||||
if (characterToInteract == Interactions.CharacterToInteract.Pulver ||
|
||||
characterToInteract == Interactions.CharacterToInteract.Both)
|
||||
{
|
||||
// Find follower and dispatch to interactable
|
||||
var followerController = FindFirstObjectByType<FollowerController>();
|
||||
if (followerController != null)
|
||||
{
|
||||
// Determine follower target position
|
||||
Vector3 followerTarget = interactable.transform.position;
|
||||
|
||||
// Check for custom target for Pulver
|
||||
foreach (var target in moveTargets)
|
||||
{
|
||||
if (target.characterType == Interactions.CharacterToInteract.Pulver ||
|
||||
target.characterType == Interactions.CharacterToInteract.Both)
|
||||
{
|
||||
followerTarget = target.GetTargetPosition();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for follower to arrive
|
||||
var tcs = new System.Threading.Tasks.TaskCompletionSource<bool>();
|
||||
|
||||
void OnFollowerArrived()
|
||||
{
|
||||
followerController.OnPickupArrived -= OnFollowerArrived;
|
||||
followerController.ReturnToPlayer(transform);
|
||||
tcs.TrySetResult(true);
|
||||
}
|
||||
|
||||
followerController.OnPickupArrived += OnFollowerArrived;
|
||||
followerController.GoToPoint(followerTarget);
|
||||
|
||||
await tcs.Task;
|
||||
}
|
||||
}
|
||||
|
||||
return true; // Success
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cancels any in-progress MoveToAndNotify operation and fires the cancellation event.
|
||||
/// </summary>
|
||||
public void InterruptMoveTo()
|
||||
{
|
||||
_interruptMoveTo = true;
|
||||
_isHolding = false;
|
||||
_directMoveVelocity = Vector3.zero;
|
||||
if (Settings.DefaultHoldMovementMode == HoldMovementMode.Direct && _aiPath != null)
|
||||
_aiPath.enabled = false;
|
||||
OnMoveToCancelled?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Coroutine for moving the player to a target position and firing arrival/cancel events.
|
||||
/// </summary>
|
||||
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
|
||||
|
||||
|
||||
318
Assets/Scripts/Interactions/ControllerSwitchItem.cs
Normal file
318
Assets/Scripts/Interactions/ControllerSwitchItem.cs
Normal file
@@ -0,0 +1,318 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using Core;
|
||||
using Input;
|
||||
using Interactions;
|
||||
using Minigames.TrashMaze.Core;
|
||||
using Minigames.TrashMaze.Data;
|
||||
using Unity.Cinemachine;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Items
|
||||
{
|
||||
/// <summary>
|
||||
/// Saveable data for ControllerSwitchItem state
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class ControllerSwitchItemSaveData
|
||||
{
|
||||
public bool hasBeenUsed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Camera switching mode for controller switch items
|
||||
/// </summary>
|
||||
public enum CameraSwitchMode
|
||||
{
|
||||
/// <summary>
|
||||
/// No camera switching - controller switch only
|
||||
/// </summary>
|
||||
None,
|
||||
|
||||
/// <summary>
|
||||
/// Use a direct reference to a Cinemachine camera
|
||||
/// </summary>
|
||||
DirectReference,
|
||||
|
||||
/// <summary>
|
||||
/// Use TrashMazeCameraController state manager API
|
||||
/// </summary>
|
||||
TrashMazeCameraState
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An interactable item that switches control from one character controller to another.
|
||||
/// When clicked:
|
||||
/// 1. The selected character moves to this item's position
|
||||
/// 2. Upon arrival, the current controller is disabled
|
||||
/// 3. Camera blends to the target camera (based on camera mode)
|
||||
/// 4. Once the blend completes, control switches to the target controller
|
||||
/// </summary>
|
||||
public class ControllerSwitchItem : SaveableInteractable
|
||||
{
|
||||
[Header("Controller Switch Settings")]
|
||||
[Tooltip("Name of the controller to switch to (must match GameObject name of the controller)")]
|
||||
[SerializeField] private string targetControllerName;
|
||||
|
||||
[Header("Camera Settings")]
|
||||
[Tooltip("How to switch the camera when changing controllers")]
|
||||
[SerializeField] private CameraSwitchMode cameraSwitchMode = CameraSwitchMode.None;
|
||||
|
||||
[Tooltip("Direct camera reference (only used if Camera Switch Mode is DirectReference)")]
|
||||
[SerializeField] private CinemachineCamera targetVirtualCamera;
|
||||
|
||||
[Tooltip("Target camera state (only used if Camera Switch Mode is TrashMazeCameraState)")]
|
||||
[SerializeField] private TrashMazeCameraState targetCameraState;
|
||||
|
||||
[Header("Visual Feedback")]
|
||||
[Tooltip("Visual representation to hide after use (optional)")]
|
||||
[SerializeField] private GameObject visualRepresentation;
|
||||
|
||||
// State
|
||||
private bool _hasBeenUsed;
|
||||
private PlayerTouchController _currentPlayerController;
|
||||
private bool _isSwitching;
|
||||
|
||||
public override string SaveId => $"{gameObject.scene.name}/ControllerSwitchItem/{gameObject.name}";
|
||||
|
||||
internal override void OnManagedAwake()
|
||||
{
|
||||
base.OnManagedAwake();
|
||||
|
||||
if (string.IsNullOrEmpty(targetControllerName))
|
||||
{
|
||||
Debug.LogError($"[ControllerSwitchItem] {gameObject.name} has no target controller name specified!");
|
||||
}
|
||||
}
|
||||
|
||||
internal override void OnManagedStart()
|
||||
{
|
||||
base.OnManagedStart();
|
||||
|
||||
// Apply state after restoration
|
||||
if (_hasBeenUsed && isOneTime)
|
||||
{
|
||||
DisableVisual();
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool CanBeClicked()
|
||||
{
|
||||
// Cannot be clicked if already used (one-time) or if currently switching
|
||||
if (_isSwitching)
|
||||
return false;
|
||||
|
||||
if (isOneTime && _hasBeenUsed)
|
||||
return false;
|
||||
|
||||
// Check if target controller is registered
|
||||
if (!InputManager.Instance.IsControllerRegistered(targetControllerName))
|
||||
{
|
||||
Debug.LogWarning($"[ControllerSwitchItem] Target controller '{targetControllerName}' is not registered with InputManager.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return base.CanBeClicked();
|
||||
}
|
||||
|
||||
protected override bool DoInteraction()
|
||||
{
|
||||
if (_isSwitching)
|
||||
return false;
|
||||
|
||||
// By the time this is called, the interacting character has already arrived at this item
|
||||
// We just need to perform the controller/camera switch
|
||||
|
||||
Logging.Debug("[ControllerSwitchItem] Starting controller switch sequence");
|
||||
|
||||
// Start the async switch sequence (camera blend + controller switch)
|
||||
StartCoroutine(SwitchControllerSequence());
|
||||
|
||||
// Return true immediately - interaction is considered successful
|
||||
// The coroutine will handle the actual switching asynchronously
|
||||
return true;
|
||||
}
|
||||
|
||||
private IEnumerator SwitchControllerSequence()
|
||||
{
|
||||
_isSwitching = true;
|
||||
|
||||
// Step 1: Get current player controller (the one we're switching FROM)
|
||||
_currentPlayerController = FindFirstObjectByType<PlayerTouchController>();
|
||||
if (_currentPlayerController == null)
|
||||
{
|
||||
Debug.LogError("[ControllerSwitchItem] Could not find PlayerTouchController in scene!");
|
||||
_isSwitching = false;
|
||||
yield break;
|
||||
}
|
||||
|
||||
Logging.Debug("[ControllerSwitchItem] Character has arrived, beginning switch");
|
||||
|
||||
// Step 2: Disable current player controller
|
||||
_currentPlayerController.enabled = false;
|
||||
Logging.Debug("[ControllerSwitchItem] Disabled current player controller");
|
||||
|
||||
// Step 3: Blend to target camera based on mode
|
||||
yield return SwitchCamera();
|
||||
|
||||
// Step 4: Switch to target controller
|
||||
ITouchInputConsumer targetController = InputManager.Instance.GetController(targetControllerName);
|
||||
if (targetController != null)
|
||||
{
|
||||
// Enable the target controller if it's a MonoBehaviour
|
||||
if (targetController is MonoBehaviour targetMono)
|
||||
{
|
||||
targetMono.enabled = true;
|
||||
Logging.Debug($"[ControllerSwitchItem] Enabled target controller: {targetControllerName}");
|
||||
}
|
||||
|
||||
// Switch input control to the target controller
|
||||
bool switchSuccess = InputManager.Instance.SwitchToController(targetControllerName);
|
||||
|
||||
if (switchSuccess)
|
||||
{
|
||||
Logging.Debug($"[ControllerSwitchItem] Successfully switched input to controller: {targetControllerName}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError($"[ControllerSwitchItem] Failed to switch to controller: {targetControllerName}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError($"[ControllerSwitchItem] Target controller '{targetControllerName}' not found!");
|
||||
}
|
||||
|
||||
// Step 5: Mark as used if one-time use
|
||||
if (isOneTime)
|
||||
{
|
||||
_hasBeenUsed = true;
|
||||
DisableVisual();
|
||||
}
|
||||
|
||||
_isSwitching = false;
|
||||
}
|
||||
|
||||
private IEnumerator SwitchCamera()
|
||||
{
|
||||
switch (cameraSwitchMode)
|
||||
{
|
||||
case CameraSwitchMode.None:
|
||||
// No camera switching
|
||||
Logging.Debug("[ControllerSwitchItem] No camera switching configured");
|
||||
break;
|
||||
|
||||
case CameraSwitchMode.DirectReference:
|
||||
if (targetVirtualCamera != null)
|
||||
{
|
||||
Logging.Debug($"[ControllerSwitchItem] Blending to camera: {targetVirtualCamera.name}");
|
||||
|
||||
// Set the target camera as highest priority
|
||||
targetVirtualCamera.Priority = 100;
|
||||
|
||||
// Wait for camera blend to complete
|
||||
yield return WaitForCameraBlend();
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("[ControllerSwitchItem] DirectReference mode selected but no camera assigned!");
|
||||
}
|
||||
break;
|
||||
|
||||
case CameraSwitchMode.TrashMazeCameraState:
|
||||
if (TrashMazeCameraController.Instance != null)
|
||||
{
|
||||
Logging.Debug($"[ControllerSwitchItem] Switching to camera state: {targetCameraState}");
|
||||
|
||||
// Use the state manager API
|
||||
if (targetCameraState == TrashMazeCameraState.Gameplay)
|
||||
{
|
||||
TrashMazeCameraController.Instance.SwitchToGameplay();
|
||||
}
|
||||
else if (targetCameraState == TrashMazeCameraState.Maze)
|
||||
{
|
||||
TrashMazeCameraController.Instance.SwitchToMaze();
|
||||
}
|
||||
|
||||
// Wait for camera blend to complete
|
||||
yield return WaitForCameraBlend();
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("[ControllerSwitchItem] TrashMazeCameraController instance not found in scene!");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator WaitForCameraBlend()
|
||||
{
|
||||
CinemachineBrain brain = Camera.main?.GetComponent<CinemachineBrain>();
|
||||
if (brain != null)
|
||||
{
|
||||
// Wait until blend is not active
|
||||
while (brain.IsBlending)
|
||||
{
|
||||
yield return null;
|
||||
}
|
||||
|
||||
Logging.Debug("[ControllerSwitchItem] Camera blend completed");
|
||||
}
|
||||
else
|
||||
{
|
||||
// If no brain, just wait a brief moment
|
||||
yield return new WaitForSeconds(0.5f);
|
||||
}
|
||||
}
|
||||
|
||||
private void DisableVisual()
|
||||
{
|
||||
if (visualRepresentation != null)
|
||||
{
|
||||
visualRepresentation.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
#region Save/Load
|
||||
|
||||
protected override object GetSerializableState()
|
||||
{
|
||||
return new ControllerSwitchItemSaveData
|
||||
{
|
||||
hasBeenUsed = _hasBeenUsed
|
||||
};
|
||||
}
|
||||
|
||||
protected override void ApplySerializableState(string serializedData)
|
||||
{
|
||||
try
|
||||
{
|
||||
var data = JsonUtility.FromJson<ControllerSwitchItemSaveData>(serializedData);
|
||||
_hasBeenUsed = data.hasBeenUsed;
|
||||
Logging.Debug($"[ControllerSwitchItem] Restored state: hasBeenUsed={_hasBeenUsed}");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogError($"[ControllerSwitchItem] Failed to deserialize save data: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#if UNITY_EDITOR
|
||||
private void OnValidate()
|
||||
{
|
||||
// Visual feedback in editor
|
||||
if (string.IsNullOrEmpty(targetControllerName))
|
||||
{
|
||||
name = "ControllerSwitchItem (UNCONFIGURED)";
|
||||
}
|
||||
else
|
||||
{
|
||||
name = $"ControllerSwitchItem_To_{targetControllerName}";
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
3
Assets/Scripts/Interactions/ControllerSwitchItem.cs.meta
Normal file
3
Assets/Scripts/Interactions/ControllerSwitchItem.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 915abd653d714ea3ae11bbf14feafb1e
|
||||
timeCreated: 1765747971
|
||||
@@ -56,7 +56,10 @@ public class GlowOutline : ManagedBehaviour
|
||||
|
||||
foreach (SpriteRenderer childSprite in childrenSprites)
|
||||
{
|
||||
if (itemSprite.sprite != null)
|
||||
if (!itemSprite)
|
||||
continue;
|
||||
|
||||
if (itemSprite?.sprite != null)
|
||||
{
|
||||
childSprite.sprite = itemSprite.sprite;
|
||||
childSprite.material = outlineMaterial;
|
||||
|
||||
@@ -34,7 +34,7 @@ namespace Interactions
|
||||
public UnityEvent characterArrived;
|
||||
public UnityEvent<bool> interactionComplete;
|
||||
|
||||
private PlayerTouchController playerRef;
|
||||
private IInteractingCharacter _interactingCharacter;
|
||||
protected FollowerController FollowerController;
|
||||
private bool isActive = true;
|
||||
|
||||
@@ -69,7 +69,7 @@ namespace Interactions
|
||||
/// <summary>
|
||||
/// Dispatch an interaction event to all registered actions and await their completion
|
||||
/// </summary>
|
||||
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<Task<bool>> tasks = new List<Task<bool>>();
|
||||
@@ -114,26 +114,52 @@ namespace Interactions
|
||||
/// </summary>
|
||||
private async Task StartInteractionFlowAsync()
|
||||
{
|
||||
// 2. Find characters
|
||||
playerRef = FindFirstObjectByType<PlayerTouchController>();
|
||||
// 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<PlayerTouchController>();
|
||||
Logging.Warning("[Interactable] No active controller from InputManager, falling back to FindFirstObjectByType<PlayerTouchController>");
|
||||
}
|
||||
|
||||
_interactingCharacter = playerController;
|
||||
FollowerController = FindFirstObjectByType<FollowerController>();
|
||||
|
||||
// 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();
|
||||
bool movementSucceeded = await MoveCharactersAsync(playerRef);
|
||||
|
||||
// If movement was cancelled, stop the interaction flow
|
||||
if (!movementSucceeded)
|
||||
{
|
||||
Logging.Debug($"[Interactable] Interaction cancelled due to movement failure on {gameObject.name}");
|
||||
return;
|
||||
}
|
||||
|
||||
// 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 +169,7 @@ namespace Interactions
|
||||
{
|
||||
DebugUIMessage.Show(errorMessage, Color.yellow);
|
||||
}
|
||||
FinishInteraction(false);
|
||||
FinishInteraction(false, playerRef);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -151,7 +177,7 @@ namespace Interactions
|
||||
bool success = DoInteraction();
|
||||
|
||||
// 10. Finish up
|
||||
FinishInteraction(success);
|
||||
FinishInteraction(success, playerRef);
|
||||
}
|
||||
|
||||
#region Virtual Lifecycle Methods
|
||||
@@ -260,151 +286,46 @@ namespace Interactions
|
||||
#region Character Movement Orchestration
|
||||
|
||||
/// <summary>
|
||||
/// Orchestrates character movement based on characterToInteract setting.
|
||||
/// Delegates movement to the interacting character's controller.
|
||||
/// Each controller implements its own movement behavior based on this interactable's settings.
|
||||
/// </summary>
|
||||
private async Task MoveCharactersAsync()
|
||||
/// <returns>True if movement succeeded, false if cancelled or failed</returns>
|
||||
private async Task<bool> 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);
|
||||
return;
|
||||
await DispatchEventAsync(InteractionEventType.InteractionInterrupted, playerRef);
|
||||
return false;
|
||||
}
|
||||
|
||||
// If characterToInteract is None, skip movement
|
||||
if (characterToInteract == CharacterToInteract.None)
|
||||
{
|
||||
return; // Continue to arrival
|
||||
return true; // Continue to arrival
|
||||
}
|
||||
|
||||
// Move player and optionally follower based on characterToInteract setting
|
||||
if (characterToInteract == CharacterToInteract.Trafalgar)
|
||||
// Delegate to controller - let it decide how to handle the interaction
|
||||
bool arrived = await _interactingCharacter.MoveToInteractableAsync(this);
|
||||
|
||||
if (!arrived)
|
||||
{
|
||||
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)
|
||||
{
|
||||
if (target.characterType == CharacterToInteract.Trafalgar || target.characterType == CharacterToInteract.Both)
|
||||
{
|
||||
stopPoint = target.GetTargetPosition();
|
||||
customTargetFound = true;
|
||||
break;
|
||||
}
|
||||
Logging.Debug($"[Interactable] Movement cancelled for {gameObject.name}");
|
||||
await HandleInteractionCancelledAsync(playerRef);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 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<bool>();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Moves the follower to the interaction point or custom target.
|
||||
/// </summary>
|
||||
private async Task MoveFollowerAsync()
|
||||
{
|
||||
if (FollowerController == null)
|
||||
return;
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for follower to arrive
|
||||
var tcs = new TaskCompletionSource<bool>();
|
||||
|
||||
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;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles interaction being cancelled (player stopped moving).
|
||||
/// </summary>
|
||||
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 +335,14 @@ namespace Interactions
|
||||
/// <summary>
|
||||
/// Finalizes the interaction after DoInteraction completes.
|
||||
/// </summary>
|
||||
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 +358,7 @@ namespace Interactions
|
||||
}
|
||||
|
||||
// Reset state
|
||||
playerRef = null;
|
||||
_interactingCharacter = null;
|
||||
FollowerController = null;
|
||||
}
|
||||
|
||||
|
||||
3
Assets/Scripts/Items.meta
Normal file
3
Assets/Scripts/Items.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 44bf6c911c674dc98cc5a06ad14c7d56
|
||||
timeCreated: 1765747971
|
||||
@@ -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.
|
||||
/// </summary>
|
||||
public class PulverController : BasePlayerMovementController
|
||||
{
|
||||
@@ -51,6 +52,18 @@ namespace Minigames.TrashMaze.Core
|
||||
_visionRadius = configs.FollowerMovement.TrashMazeVisionRadius;
|
||||
Logging.Debug($"[PulverController] Loaded vision radius from settings: {_visionRadius}");
|
||||
}
|
||||
|
||||
internal override void OnManagedStart()
|
||||
{
|
||||
base.OnManagedStart();
|
||||
|
||||
// Register with InputManager (not as default consumer)
|
||||
if (InputManager.Instance != null)
|
||||
{
|
||||
InputManager.Instance.RegisterController("pulver", this, setAsDefaultConsumer: false);
|
||||
Logging.Debug($"[PulverController] Registered controller '{gameObject.name}'");
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
@@ -86,6 +99,33 @@ namespace Minigames.TrashMaze.Core
|
||||
{
|
||||
_visionRadius = Mathf.Max(0.1f, radius);
|
||||
}
|
||||
|
||||
#region IInteractingCharacter Override
|
||||
|
||||
/// <summary>
|
||||
/// PulverController-specific interaction movement.
|
||||
/// Moves Pulver to the interactable using the main character's stop distance.
|
||||
/// No follower logic since Pulver is alone in the maze.
|
||||
/// </summary>
|
||||
public override async System.Threading.Tasks.Task<bool> MoveToInteractableAsync(Interactions.InteractableBase interactable)
|
||||
{
|
||||
// Use the same stop distance as main character for consistency
|
||||
float stopDistance = GameManager.Instance.PlayerStopDistance;
|
||||
|
||||
// Calculate stop position
|
||||
Vector3 stopPoint = Utils.MovementUtilities.CalculateStopPosition(
|
||||
interactable.transform.position,
|
||||
transform.position,
|
||||
stopDistance
|
||||
);
|
||||
|
||||
Logging.Debug($"[PulverController] Moving to interactable {interactable.gameObject.name} at stop distance {stopDistance}");
|
||||
|
||||
// Use MovementUtilities to handle movement
|
||||
return await Utils.MovementUtilities.MoveToPositionAsync(this, stopPoint);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
using Common.Camera;
|
||||
using Core;
|
||||
using Minigames.TrashMaze.Data;
|
||||
using Unity.Cinemachine;
|
||||
|
||||
namespace Minigames.TrashMaze.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Manages camera states for the Trash Maze minigame.
|
||||
/// Handles transitions between Gameplay (level exploration) and Maze (inside maze exploration) cameras.
|
||||
/// Provides singleton access for easy camera switching from items and other systems.
|
||||
/// </summary>
|
||||
public class TrashMazeCameraController : CameraStateManager<TrashMazeCameraState>
|
||||
{
|
||||
#region Singleton
|
||||
|
||||
private static TrashMazeCameraController _instance;
|
||||
|
||||
/// <summary>
|
||||
/// Singleton instance of the camera controller
|
||||
/// </summary>
|
||||
public static TrashMazeCameraController Instance => _instance;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Lifecycle
|
||||
|
||||
internal override void OnManagedAwake()
|
||||
{
|
||||
// Base class handles InitializeCameraMap() and ValidateCameras()
|
||||
base.OnManagedAwake();
|
||||
|
||||
// Set singleton
|
||||
if (_instance != null && _instance != this)
|
||||
{
|
||||
Logging.Warning("[TrashMazeCameraController] Multiple instances detected! Destroying duplicate.");
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
_instance = this;
|
||||
}
|
||||
|
||||
internal override void OnManagedStart()
|
||||
{
|
||||
base.OnManagedStart();
|
||||
|
||||
// Start in gameplay camera by default
|
||||
SwitchToGameplay();
|
||||
}
|
||||
|
||||
internal override void OnManagedDestroy()
|
||||
{
|
||||
base.OnManagedDestroy();
|
||||
|
||||
if (_instance == this)
|
||||
{
|
||||
_instance = null;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public API
|
||||
|
||||
/// <summary>
|
||||
/// Switch to the main gameplay camera (level exploration)
|
||||
/// </summary>
|
||||
public void SwitchToGameplay()
|
||||
{
|
||||
SwitchToState(TrashMazeCameraState.Gameplay);
|
||||
|
||||
if (showDebugLogs)
|
||||
Logging.Debug("[TrashMazeCameraController] Switched to Gameplay camera");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Switch to the maze camera (inside maze exploration)
|
||||
/// </summary>
|
||||
public void SwitchToMaze()
|
||||
{
|
||||
SwitchToState(TrashMazeCameraState.Maze);
|
||||
|
||||
if (showDebugLogs)
|
||||
Logging.Debug("[TrashMazeCameraController] Switched to Maze camera");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the gameplay camera
|
||||
/// </summary>
|
||||
public CinemachineCamera GetGameplayCamera()
|
||||
{
|
||||
return GetCamera(TrashMazeCameraState.Gameplay);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the maze camera
|
||||
/// </summary>
|
||||
public CinemachineCamera GetMazeCamera()
|
||||
{
|
||||
return GetCamera(TrashMazeCameraState.Maze);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d058b159d0aa43699eaba263b7b8c5a7
|
||||
timeCreated: 1765749988
|
||||
@@ -13,8 +13,7 @@ namespace Minigames.TrashMaze.Core
|
||||
public static TrashMazeController Instance { get; private set; }
|
||||
|
||||
[Header("Player")]
|
||||
[SerializeField] private PulverController pulverPrefab;
|
||||
[SerializeField] private Transform startPosition;
|
||||
[SerializeField] private PulverController pulverController;
|
||||
|
||||
[Header("Background")]
|
||||
[Tooltip("Background sprite renderer - world size and center are inferred from its bounds")]
|
||||
@@ -27,7 +26,6 @@ namespace Minigames.TrashMaze.Core
|
||||
private static readonly int WorldSizeID = Shader.PropertyToID("_WorldSize");
|
||||
private static readonly int WorldCenterID = Shader.PropertyToID("_WorldCenter");
|
||||
|
||||
private PulverController _pulverInstance;
|
||||
private bool _mazeCompleted;
|
||||
|
||||
internal override void OnManagedAwake()
|
||||
@@ -59,8 +57,8 @@ namespace Minigames.TrashMaze.Core
|
||||
// Infer world bounds from background renderer and set shader globals
|
||||
ApplyBackgroundBoundsToShader();
|
||||
|
||||
// Spawn player
|
||||
SpawnPulver();
|
||||
// Validate player reference
|
||||
InitializePulver();
|
||||
|
||||
Logging.Debug("[TrashMazeController] Trash Maze initialized");
|
||||
}
|
||||
@@ -108,18 +106,15 @@ namespace Minigames.TrashMaze.Core
|
||||
$"Size=({worldSize.x:F2}, {worldSize.y:F2}), Center=({worldCenter.x:F2}, {worldCenter.y:F2})");
|
||||
}
|
||||
|
||||
private void SpawnPulver()
|
||||
private void InitializePulver()
|
||||
{
|
||||
if (pulverPrefab == null)
|
||||
if (pulverController == null)
|
||||
{
|
||||
Logging.Error("[TrashMazeController] Pulver prefab not assigned!");
|
||||
Logging.Error("[TrashMazeController] PulverController reference not assigned! Please assign it in the Inspector.");
|
||||
return;
|
||||
}
|
||||
|
||||
Vector3 spawnPosition = startPosition != null ? startPosition.position : Vector3.zero;
|
||||
_pulverInstance = Instantiate(pulverPrefab, spawnPosition, Quaternion.identity);
|
||||
|
||||
Logging.Debug($"[TrashMazeController] Pulver spawned at {spawnPosition}");
|
||||
Logging.Debug($"[TrashMazeController] Pulver controller initialized at {pulverController.transform.position}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
3
Assets/Scripts/Minigames/TrashMaze/Data.meta
Normal file
3
Assets/Scripts/Minigames/TrashMaze/Data.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c1c86c04b4dc4dd5add77ba3bb17f95e
|
||||
timeCreated: 1765749918
|
||||
19
Assets/Scripts/Minigames/TrashMaze/Data/TrashMazeEnums.cs
Normal file
19
Assets/Scripts/Minigames/TrashMaze/Data/TrashMazeEnums.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
namespace Minigames.TrashMaze.Data
|
||||
{
|
||||
/// <summary>
|
||||
/// Camera states for Trash Maze minigame
|
||||
/// </summary>
|
||||
public enum TrashMazeCameraState
|
||||
{
|
||||
/// <summary>
|
||||
/// Main gameplay camera following Trafalgar around the level
|
||||
/// </summary>
|
||||
Gameplay,
|
||||
|
||||
/// <summary>
|
||||
/// Maze camera following Pulver when exploring the maze alone
|
||||
/// </summary>
|
||||
Maze
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4e26aed52b5e4597b3cbba9191fe463b
|
||||
timeCreated: 1765749918
|
||||
3
Assets/Scripts/Utilities.meta
Normal file
3
Assets/Scripts/Utilities.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 56c21fe8abef4887a819d02c0fbdb5d8
|
||||
timeCreated: 1765753993
|
||||
66
Assets/Scripts/Utils/MovementUtilities.cs
Normal file
66
Assets/Scripts/Utils/MovementUtilities.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
using System.Threading.Tasks;
|
||||
using Core;
|
||||
using Input;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Utils
|
||||
{
|
||||
/// <summary>
|
||||
/// Utility methods for character movement operations.
|
||||
/// Extracted from interaction/controller code for reusability.
|
||||
/// </summary>
|
||||
public static class MovementUtilities
|
||||
{
|
||||
/// <summary>
|
||||
/// Moves a character to a target position and waits for arrival.
|
||||
/// Works with any controller implementing IInteractingCharacter.
|
||||
/// </summary>
|
||||
/// <param name="character">The character to move (must implement IInteractingCharacter)</param>
|
||||
/// <param name="targetPosition">World position to move to</param>
|
||||
/// <returns>Task that completes when the character arrives or movement is cancelled</returns>
|
||||
public static async Task<bool> MoveToPositionAsync(IInteractingCharacter character, Vector3 targetPosition)
|
||||
{
|
||||
if (character == null)
|
||||
{
|
||||
Logging.Warning("[MovementUtilities] Cannot move null character");
|
||||
return false;
|
||||
}
|
||||
|
||||
var tcs = new TaskCompletionSource<bool>();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates a stop position at a given distance from a target position towards a character.
|
||||
/// </summary>
|
||||
/// <param name="targetPosition">The target position</param>
|
||||
/// <param name="characterPosition">The character's current position</param>
|
||||
/// <param name="stopDistance">Distance from target to stop at</param>
|
||||
/// <returns>The calculated stop position</returns>
|
||||
public static Vector3 CalculateStopPosition(Vector3 targetPosition, Vector3 characterPosition, float stopDistance)
|
||||
{
|
||||
Vector3 toCharacter = (characterPosition - targetPosition).normalized;
|
||||
return targetPosition + toCharacter * stopDistance;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
3
Assets/Scripts/Utils/MovementUtilities.cs.meta
Normal file
3
Assets/Scripts/Utils/MovementUtilities.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 29f4ca2c743f4890aab59e4ccdda2c79
|
||||
timeCreated: 1765753993
|
||||
Reference in New Issue
Block a user