Compare commits

...

3 Commits

Author SHA1 Message Date
Michal Pikulski
5b33eb8fbb Finalize work on the dump controller switching 2025-12-15 10:18:43 +01:00
Michal Pikulski
b14fdfbe68 A bit wonky but working switching 2025-12-15 00:57:50 +01:00
Michal Pikulski
8995dd1949 Working item switcher 2025-12-15 00:05:35 +01:00
32 changed files with 2226 additions and 1155 deletions

View File

@@ -57,7 +57,7 @@ Material:
- _Thickness: 0.01
m_Colors:
- White: {r: 1, g: 1, b: 1, a: 1}
- _Color: {r: 1, g: 0.87843144, b: 0.57254905, a: 1}
- _Color: {r: 1, g: 0.8784314, b: 0.57254905, a: 1}
- _Colour: {r: 0.9341286, g: 1, b: 0, a: 1}
m_BuildTextureStacks: []
m_AllowLocking: 1

View File

@@ -0,0 +1,101 @@
using UnityEditor;
using UnityEngine;
using Items;
namespace Editor
{
/// <summary>
/// Custom editor for ControllerSwitchItem that shows only relevant fields based on camera switch mode.
/// </summary>
[CustomEditor(typeof(ControllerSwitchItem))]
public class ControllerSwitchItemEditor : UnityEditor.Editor
{
private SerializedProperty _targetControllerName;
private SerializedProperty _cameraSwitchMode;
private SerializedProperty _targetVirtualCamera;
private SerializedProperty _targetCameraState;
private SerializedProperty _visualRepresentation;
// Base class properties
private SerializedProperty _isOneTime;
private SerializedProperty _cooldown;
private SerializedProperty _characterToInteract;
private void OnEnable()
{
// Controller Switch Settings
_targetControllerName = serializedObject.FindProperty("targetControllerName");
_cameraSwitchMode = serializedObject.FindProperty("cameraSwitchMode");
_targetVirtualCamera = serializedObject.FindProperty("targetVirtualCamera");
_targetCameraState = serializedObject.FindProperty("targetCameraState");
_visualRepresentation = serializedObject.FindProperty("visualRepresentation");
// Base class properties
_isOneTime = serializedObject.FindProperty("isOneTime");
_cooldown = serializedObject.FindProperty("cooldown");
_characterToInteract = serializedObject.FindProperty("characterToInteract");
}
public override void OnInspectorGUI()
{
serializedObject.Update();
// Draw script field (read-only)
EditorGUI.BeginDisabledGroup(true);
EditorGUILayout.ObjectField("Script", MonoScript.FromMonoBehaviour((MonoBehaviour)target), GetType(), false);
EditorGUI.EndDisabledGroup();
EditorGUILayout.Space();
// Interaction Settings (from base class)
EditorGUILayout.LabelField("Interaction Settings", EditorStyles.boldLabel);
EditorGUILayout.PropertyField(_isOneTime);
EditorGUILayout.PropertyField(_cooldown);
EditorGUILayout.PropertyField(_characterToInteract);
EditorGUILayout.Space();
// Controller Switch Settings
EditorGUILayout.LabelField("Controller Switch Settings", EditorStyles.boldLabel);
EditorGUILayout.PropertyField(_targetControllerName);
EditorGUILayout.Space();
// Camera Settings
EditorGUILayout.LabelField("Camera Settings", EditorStyles.boldLabel);
EditorGUILayout.PropertyField(_cameraSwitchMode);
// Show relevant camera fields based on mode
CameraSwitchMode mode = (CameraSwitchMode)_cameraSwitchMode.enumValueIndex;
switch (mode)
{
case CameraSwitchMode.None:
EditorGUILayout.HelpBox("No camera switching will occur. Only the controller will be switched.", MessageType.Info);
break;
case CameraSwitchMode.DirectReference:
EditorGUILayout.PropertyField(_targetVirtualCamera);
if (_targetVirtualCamera.objectReferenceValue == null)
{
EditorGUILayout.HelpBox("Assign a Cinemachine camera to blend to when switching controllers.", MessageType.Warning);
}
break;
case CameraSwitchMode.TrashMazeCameraState:
EditorGUILayout.PropertyField(_targetCameraState);
EditorGUILayout.HelpBox("Uses TrashMazeCameraController to switch camera state. Make sure TrashMazeCameraController is present in the scene.", MessageType.Info);
break;
}
EditorGUILayout.Space();
// Visual Feedback
EditorGUILayout.LabelField("Visual Feedback", EditorStyles.boldLabel);
EditorGUILayout.PropertyField(_visualRepresentation);
serializedObject.ApplyModifiedProperties();
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: e193f0999dad43a4a15ace4245249070
timeCreated: 1765750391

View File

@@ -0,0 +1,173 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1 &1190045438452732069
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1136290793271151494}
- component: {fileID: 5320225058563658919}
- component: {fileID: 7813271480623895155}
- component: {fileID: 6196606079257550}
m_Layer: 0
m_Name: ControllerSwitchItem_To_pulver
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &1136290793271151494
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1190045438452732069}
serializedVersion: 2
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: -56, y: -38.8, z: 0}
m_LocalScale: {x: 10, y: 10, z: 10}
m_ConstrainProportionsScale: 1
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &5320225058563658919
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1190045438452732069}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 915abd653d714ea3ae11bbf14feafb1e, type: 3}
m_Name:
m_EditorClassIdentifier: AppleHillsScripts::Items.ControllerSwitchItem
isOneTime: 0
cooldown: -1
characterToInteract: 2
interactionStarted:
m_PersistentCalls:
m_Calls: []
interactionInterrupted:
m_PersistentCalls:
m_Calls: []
characterArrived:
m_PersistentCalls:
m_Calls: []
interactionComplete:
m_PersistentCalls:
m_Calls: []
targetControllerName: pulver
cameraSwitchMode: 2
targetVirtualCamera: {fileID: 0}
targetCameraState: 1
visualRepresentation: {fileID: 0}
--- !u!212 &7813271480623895155
SpriteRenderer:
serializedVersion: 2
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1190045438452732069}
m_Enabled: 1
m_CastShadows: 0
m_ReceiveShadows: 0
m_DynamicOccludee: 1
m_StaticShadowCaster: 0
m_MotionVectors: 1
m_LightProbeUsage: 1
m_ReflectionProbeUsage: 1
m_RayTracingMode: 0
m_RayTraceProcedural: 0
m_RayTracingAccelStructBuildFlagsOverride: 0
m_RayTracingAccelStructBuildFlags: 1
m_SmallMeshCulling: 1
m_ForceMeshLod: -1
m_MeshLodSelectionBias: 0
m_RenderingLayerMask: 1
m_RendererPriority: 0
m_Materials:
- {fileID: 2100000, guid: 9dfc825aed78fcd4ba02077103263b40, type: 2}
m_StaticBatchInfo:
firstSubMesh: 0
subMeshCount: 0
m_StaticBatchRoot: {fileID: 0}
m_ProbeAnchor: {fileID: 0}
m_LightProbeVolumeOverride: {fileID: 0}
m_ScaleInLightmap: 1
m_ReceiveGI: 1
m_PreserveUVs: 0
m_IgnoreNormalsForChartDetection: 0
m_ImportantGI: 0
m_StitchLightmapSeams: 1
m_SelectedEditorRenderState: 0
m_MinimumChartSize: 4
m_AutoUVMaxDistance: 0.5
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
m_GlobalIlluminationMeshLod: 0
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingOrder: 0
m_MaskInteraction: 0
m_Sprite: {fileID: 2636902231072113825, guid: ee014bd71cac2bc4ab845f435726f383, type: 3}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_FlipX: 0
m_FlipY: 0
m_DrawMode: 0
m_Size: {x: 0.42, y: 0.42}
m_AdaptiveModeThreshold: 0.5
m_SpriteTileMode: 0
m_WasSpriteAssigned: 1
m_SpriteSortPoint: 0
--- !u!61 &6196606079257550
BoxCollider2D:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1190045438452732069}
m_Enabled: 1
serializedVersion: 3
m_Density: 1
m_Material: {fileID: 0}
m_IncludeLayers:
serializedVersion: 2
m_Bits: 0
m_ExcludeLayers:
serializedVersion: 2
m_Bits: 0
m_LayerOverridePriority: 0
m_ForceSendLayers:
serializedVersion: 2
m_Bits: 4294967295
m_ForceReceiveLayers:
serializedVersion: 2
m_Bits: 4294967295
m_ContactCaptureLayers:
serializedVersion: 2
m_Bits: 4294967295
m_CallbackLayers:
serializedVersion: 2
m_Bits: 4294967295
m_IsTrigger: 0
m_UsedByEffector: 0
m_CompositeOperation: 0
m_CompositeOrder: 0
m_Offset: {x: -0.002767995, y: -0.000000022351742}
m_SpriteTilingProperty:
border: {x: 0, y: 0, z: 0, w: 0}
pivot: {x: 0.5, y: 0.5}
oldSize: {x: 0.42, y: 0.42}
newSize: {x: 0.42, y: 0.42}
adaptiveTilingThreshold: 0.5
drawMode: 0
adaptiveTiling: 0
m_AutoTiling: 0
m_Size: {x: 0.44087836, y: 0.440879}
m_EdgeRadius: 0

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 67a60833f9f205940a2308bd74a2863e
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -65,7 +65,8 @@ MonoBehaviour:
itemData: {fileID: 11400000, guid: ba611946a9f3d5d4784bd16d7061c2c6, type: 2}
iconRenderer: {fileID: 0}
slottedItemRenderers:
- {fileID: 853265584245318634}
- renderer: {fileID: 0}
assignedItem: {fileID: 0}
onItemSlotted:
m_PersistentCalls:
m_Calls: []
@@ -173,6 +174,7 @@ Transform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!212 &853265584245318634
SpriteRenderer:
serializedVersion: 2
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
@@ -218,6 +220,7 @@ SpriteRenderer:
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingOrder: 0
m_MaskInteraction: 0
m_Sprite: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_FlipX: 0
@@ -227,5 +230,4 @@ SpriteRenderer:
m_AdaptiveModeThreshold: 0.5
m_SpriteTileMode: 0
m_WasSpriteAssigned: 0
m_MaskInteraction: 0
m_SpriteSortPoint: 0

View File

@@ -37,6 +37,7 @@ Transform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!212 &1054160620432813893
SpriteRenderer:
serializedVersion: 2
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
@@ -82,6 +83,7 @@ SpriteRenderer:
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingOrder: 1
m_MaskInteraction: 0
m_Sprite: {fileID: -5380470264793805610, guid: 9aee36bc2f2ec8248ad3db30306aa7f4, type: 3}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_FlipX: 0
@@ -91,7 +93,6 @@ SpriteRenderer:
m_AdaptiveModeThreshold: 0.5
m_SpriteTileMode: 0
m_WasSpriteAssigned: 1
m_MaskInteraction: 0
m_SpriteSortPoint: 0
--- !u!61 &3660267275524594777
BoxCollider2D:

View File

@@ -36,6 +36,7 @@ Transform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!212 &4945860249160435993
SpriteRenderer:
serializedVersion: 2
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
@@ -81,6 +82,7 @@ SpriteRenderer:
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingOrder: 0
m_MaskInteraction: 0
m_Sprite: {fileID: -6493275343144376112, guid: f3e1e24212ad86445b362335713279bd, type: 3}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_FlipX: 0
@@ -90,7 +92,6 @@ SpriteRenderer:
m_AdaptiveModeThreshold: 0.5
m_SpriteTileMode: 0
m_WasSpriteAssigned: 1
m_MaskInteraction: 0
m_SpriteSortPoint: 0
--- !u!114 &7255725277252707145
MonoBehaviour:

View File

@@ -37,6 +37,7 @@ Transform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!212 &5787940828305859459
SpriteRenderer:
serializedVersion: 2
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
@@ -82,6 +83,7 @@ SpriteRenderer:
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingOrder: 1
m_MaskInteraction: 0
m_Sprite: {fileID: 3212686851042344676, guid: 2c96f5ba929679d47ad402a4eda5bd9f, type: 3}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_FlipX: 0
@@ -91,7 +93,6 @@ SpriteRenderer:
m_AdaptiveModeThreshold: 0.5
m_SpriteTileMode: 0
m_WasSpriteAssigned: 1
m_MaskInteraction: 0
m_SpriteSortPoint: 0
--- !u!61 &1572163134238919128
BoxCollider2D:

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -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
}
}

View 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; }
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 0d1b1be281334b9390cc96d7a8ff3132
timeCreated: 1765754798

View File

@@ -34,6 +34,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.
/// </summary>
@@ -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
}
}

View File

@@ -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");
}
}
public override void OnHoldStart(Vector2 worldPosition)
#region IInteractingCharacter Override
/// <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

View 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
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 915abd653d714ea3ae11bbf14feafb1e
timeCreated: 1765747971

View File

@@ -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;

View File

@@ -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
}
Logging.Debug($"[Interactable] Movement cancelled for {gameObject.name}");
await HandleInteractionCancelledAsync(playerRef);
return false;
}
/// <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;
}
}
// 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;
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 44bf6c911c674dc98cc5a06ad14c7d56
timeCreated: 1765747971

View File

@@ -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
{
@@ -52,6 +53,18 @@ namespace Minigames.TrashMaze.Core
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()
{
base.Update(); // Call base for movement and animation
@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: d058b159d0aa43699eaba263b7b8c5a7
timeCreated: 1765749988

View File

@@ -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>

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: c1c86c04b4dc4dd5add77ba3bb17f95e
timeCreated: 1765749918

View 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
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 4e26aed52b5e4597b3cbba9191fe463b
timeCreated: 1765749918

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 56c21fe8abef4887a819d02c0fbdb5d8
timeCreated: 1765753993

View 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;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 29f4ca2c743f4890aab59e4ccdda2c79
timeCreated: 1765753993

View File

@@ -15,13 +15,13 @@ MonoBehaviour:
defaultPlayerMovement:
moveSpeed: 15
maxAcceleration: 10000
stopDistance: 0.1
stopDistance: 0.5
useRigidbody: 0
defaultHoldMovementMode: 1
trashMazeMovement:
moveSpeed: 15
maxAcceleration: 10000
stopDistance: 0.1
stopDistance: 0.5
useRigidbody: 0
defaultHoldMovementMode: 1
followerMovement: