Pulver trash maze sequence

This commit is contained in:
Michal Pikulski
2025-12-19 15:26:13 +01:00
parent 15c9ba0127
commit f0905f92d3
36 changed files with 2372 additions and 842 deletions

View File

@@ -406,6 +406,13 @@ namespace Input
while (!_interruptMoveTo)
{
// Use AIPath's built-in destination check if available
if (_aiPath != null && _aiPath.reachedDestination)
{
break;
}
// Fallback to distance check
Vector2 current2D = new Vector2(transform.position.x, transform.position.y);
Vector2 target2D = new Vector2(target.x, target.y);
float dist = Vector2.Distance(current2D, target2D);
@@ -425,6 +432,74 @@ namespace Input
}
#endregion
#region Controller Lifecycle
/// <summary>
/// Called when this controller is given input control.
/// Default implementation cleans up any active movement state.
/// Override to add controller-specific activation logic.
/// </summary>
public virtual void ActivateController()
{
// Stop any in-progress movement coroutines
if (_moveToCoroutine != null)
{
StopCoroutine(_moveToCoroutine);
_moveToCoroutine = null;
}
if (_pathfindingDragCoroutine != null)
{
StopCoroutine(_pathfindingDragCoroutine);
_pathfindingDragCoroutine = null;
}
// Reset movement state
_isHolding = false;
_directMoveVelocity = Vector3.zero;
_interruptMoveTo = false;
Logging.Debug($"[{GetType().Name}] Controller activated");
}
/// <summary>
/// Called when this controller loses input control.
/// Default implementation stops all movement and cleans up state.
/// Override to add controller-specific deactivation logic.
/// </summary>
public virtual void DeactivateController()
{
// Stop all movement coroutines
if (_moveToCoroutine != null)
{
StopCoroutine(_moveToCoroutine);
_moveToCoroutine = null;
}
if (_pathfindingDragCoroutine != null)
{
StopCoroutine(_pathfindingDragCoroutine);
_pathfindingDragCoroutine = null;
}
// Reset all movement state
_isHolding = false;
_directMoveVelocity = Vector3.zero;
_interruptMoveTo = false;
_isMoving = false;
// Stop AIPath movement
if (_aiPath != null)
{
_aiPath.enabled = false;
_aiPath.isStopped = true;
}
Logging.Debug($"[{GetType().Name}] Controller deactivated");
}
#endregion
}
}

View File

@@ -1,5 +1,4 @@
using UnityEngine;
using AppleHills.Core.Settings;
using Core;
using Core.Settings;
@@ -45,6 +44,10 @@ namespace Input
InputManager.Instance.RegisterController("trafalgar", this, setAsDefaultConsumer: true);
Logging.Debug($"[PlayerTouchController] Registered controller '{gameObject.name}' as default consumer");
}
// Auto-activate as the default player controller
ActivateController();
Logging.Debug("[PlayerTouchController] Auto-activated as default player controller");
}
#region IInteractingCharacter Override

View File

@@ -3,9 +3,6 @@ using System.Collections;
using Core;
using Input;
using Interactions;
using Minigames.TrashMaze.Core;
using Minigames.TrashMaze.Data;
using Unity.Cinemachine;
using UnityEngine;
using UnityEngine.Events;
@@ -20,61 +17,27 @@ namespace Items
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
/// Base interactable item that switches control from one character controller to another.
/// Level-agnostic - handles only controller switching logic.
/// Derive from this class for level-specific behavior (camera switching, teleportation, etc.)
/// </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;
[Tooltip("Name of the controller to switch to (must match registration name in InputManager)")]
[SerializeField] protected string targetControllerName;
[Header("Visual Feedback")]
[Tooltip("Visual representation to hide after use (optional)")]
[SerializeField] private GameObject visualRepresentation;
[SerializeField] protected GameObject visualRepresentation;
public UnityEvent OnCharacterSwitch;
// State
private bool _hasBeenUsed;
private PlayerTouchController _currentPlayerController;
private bool _isSwitching;
protected bool _hasBeenUsed;
protected bool _isSwitching;
public override string SaveId => $"{gameObject.scene.name}/ControllerSwitchItem/{gameObject.name}";
@@ -123,68 +86,50 @@ namespace Items
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()
protected virtual IEnumerator SwitchControllerSequence()
{
_isSwitching = true;
// Step 1: Get current player controller (the one we're switching FROM)
_currentPlayerController = FindFirstObjectByType<PlayerTouchController>();
if (_currentPlayerController == null)
// Step 1: Get controllers
var currentController = InputManager.Instance.GetActiveController();
var targetController = InputManager.Instance.GetController(targetControllerName);
if (currentController == null || targetController == null)
{
Debug.LogError("[ControllerSwitchItem] Could not find PlayerTouchController in scene!");
Debug.LogError($"[ControllerSwitchItem] Failed to get controllers! Current: {currentController}, Target: {targetController} (name: {targetControllerName})");
_isSwitching = false;
yield break;
}
GameObject currentGameObject = (currentController as MonoBehaviour)?.gameObject;
GameObject targetGameObject = (targetController as MonoBehaviour)?.gameObject;
Logging.Debug($"[ControllerSwitchItem] Switching from {currentGameObject?.name} to {targetGameObject?.name}");
Logging.Debug("[ControllerSwitchItem] Character has arrived, beginning switch");
// Step 2: Deactivate current controller
DeactivateCurrentController(currentController, currentGameObject);
// Step 2: Disable current player controller
_currentPlayerController.enabled = false;
Logging.Debug("[ControllerSwitchItem] Disabled current player controller");
// Step 3: Activate target controller
ActivateTargetController(targetController, targetGameObject);
// 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)
// Step 4: Switch InputManager to target controller
bool switchSuccess = InputManager.Instance.SwitchToController(targetControllerName);
if (switchSuccess)
{
// 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}");
OnCharacterSwitch.Invoke();
}
else
{
Debug.LogError($"[ControllerSwitchItem] Failed to switch to controller: {targetControllerName}");
}
Logging.Debug($"[ControllerSwitchItem] Successfully switched input to controller: {targetControllerName}");
OnCharacterSwitch.Invoke();
}
else
{
Debug.LogError($"[ControllerSwitchItem] Target controller '{targetControllerName}' not found!");
Debug.LogError($"[ControllerSwitchItem] Failed to switch to controller: {targetControllerName}");
}
// Step 5: Mark as used if one-time use
@@ -197,79 +142,65 @@ namespace Items
_isSwitching = false;
}
private IEnumerator SwitchCamera()
protected virtual void DeactivateCurrentController(ITouchInputConsumer currentController, GameObject currentGameObject)
{
switch (cameraSwitchMode)
// If current is a player controller, deactivate it
if (currentController is BasePlayerMovementController currentPlayerController)
{
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;
currentPlayerController.DeactivateController();
}
// If switching FROM follower mode, deactivate follower
if (currentGameObject != null)
{
var currentFollower = currentGameObject.GetComponent<FollowerController>();
if (currentFollower != null && currentFollower.IsFollowerActive)
{
currentFollower.DeactivateFollower();
}
}
}
private IEnumerator WaitForCameraBlend()
protected virtual void ActivateTargetController(ITouchInputConsumer targetController, GameObject targetGameObject)
{
CinemachineBrain brain = Camera.main?.GetComponent<CinemachineBrain>();
if (brain != null)
// Check if target GameObject has FollowerController component
FollowerController targetFollower = null;
if (targetGameObject != null)
{
// Wait until blend is not active
while (brain.IsBlending)
{
yield return null;
}
targetFollower = targetGameObject.GetComponent<FollowerController>();
}
// If switching TO a GameObject with FollowerController, we need special handling
if (targetFollower != null)
{
// Switching TO Pulver player control (Pulver has both FollowerController and PulverController)
// Deactivate follower mode, activate player control
targetFollower.DeactivateFollower();
Logging.Debug("[ControllerSwitchItem] Camera blend completed");
if (targetController is BasePlayerMovementController targetPlayerController)
{
targetPlayerController.ActivateController();
}
}
else
{
// If no brain, just wait a brief moment
yield return new WaitForSeconds(0.5f);
// Switching TO Trafalgar (no FollowerController on Trafalgar)
// If there's a Pulver in the scene, activate its follower mode
var pulverFollower = FindFirstObjectByType<FollowerController>();
if (pulverFollower != null)
{
pulverFollower.ActivateFollower();
}
// Activate the target player controller
if (targetController is BasePlayerMovementController targetPlayerController)
{
targetPlayerController.ActivateController();
}
}
}
private void DisableVisual()
protected void DisableVisual()
{
if (visualRepresentation != null)
{

View File

@@ -0,0 +1,136 @@
using Core;
using UnityEngine;
namespace Interactions
{
/// <summary>
/// Triggers an animation when an interactable interaction completes successfully.
/// Auto-discovers Interactable and Animator components on the same GameObject or children.
/// </summary>
public class TriggerAnimationOnInteraction : MonoBehaviour
{
[Header("Component References (Optional)")]
[Tooltip("Interactable to listen to. Leave empty to auto-discover on this GameObject or children.")]
[SerializeField] private InteractableBase interactable;
[Tooltip("Animator to trigger. Leave empty to auto-discover on this GameObject or children.")]
[SerializeField] private Animator animator;
[Header("Animation Settings")]
[Tooltip("Name of the animation trigger to fire when interaction completes")]
[SerializeField] private string triggerName;
private InteractableBase _interactable;
private Animator _animator;
private bool _isInitialized;
private void Awake()
{
DiscoverComponents();
}
private void OnEnable()
{
if (_isInitialized && _interactable != null)
{
_interactable.interactionComplete.AddListener(OnInteractionComplete);
}
}
private void OnDisable()
{
if (_interactable != null)
{
_interactable.interactionComplete.RemoveListener(OnInteractionComplete);
}
}
/// <summary>
/// Auto-discover Interactable and Animator components if not manually assigned
/// </summary>
private void DiscoverComponents()
{
// Use assigned Interactable or try to find it
_interactable = interactable;
if (_interactable == null)
{
_interactable = GetComponent<InteractableBase>();
}
if (_interactable == null)
{
_interactable = GetComponentInChildren<InteractableBase>();
}
if (_interactable == null)
{
Debug.LogError($"[TriggerAnimationOnInteraction] No InteractableBase found on {gameObject.name} or its children!");
return;
}
// Use assigned Animator or try to find it
_animator = animator;
if (_animator == null)
{
_animator = GetComponent<Animator>();
}
if (_animator == null)
{
_animator = GetComponentInChildren<Animator>();
}
if (_animator == null)
{
Debug.LogError($"[TriggerAnimationOnInteraction] No Animator found on {gameObject.name} or its children!");
return;
}
if (string.IsNullOrEmpty(triggerName))
{
Debug.LogWarning($"[TriggerAnimationOnInteraction] Trigger name is empty on {gameObject.name}!");
}
_isInitialized = true;
string interactableSource = interactable != null ? "assigned" : "auto-discovered";
string animatorSource = animator != null ? "assigned" : "auto-discovered";
Logging.Debug($"[TriggerAnimationOnInteraction] Initialized on {gameObject.name} - Interactable: {_interactable.gameObject.name} ({interactableSource}), Animator: {_animator.gameObject.name} ({animatorSource})");
}
/// <summary>
/// Called when interaction completes
/// </summary>
/// <param name="success">Whether the interaction was successful</param>
private void OnInteractionComplete(bool success)
{
if (!success)
{
Logging.Debug($"[TriggerAnimationOnInteraction] Interaction failed, not triggering animation");
return;
}
if (_animator == null || string.IsNullOrEmpty(triggerName))
{
return;
}
_animator.SetTrigger(triggerName);
Logging.Debug($"[TriggerAnimationOnInteraction] Triggered animation: {triggerName}");
}
#if UNITY_EDITOR
private void OnValidate()
{
// Validate in editor
if (string.IsNullOrEmpty(triggerName))
{
name = "TriggerAnimationOnInteraction (NO TRIGGER)";
}
else
{
name = $"TriggerAnimationOnInteraction ({triggerName})";
}
}
#endif
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: f7da1b8703214c4e813e672ba8fc3b8e
timeCreated: 1766153752

View File

@@ -23,6 +23,9 @@ namespace Minigames.TrashMaze.Core
// Vision radius loaded from settings
private float _visionRadius;
// Controller active state
private bool _isControllerActive = false;
// Public accessors for other systems
public static Vector2 PlayerPosition => Instance != null ? Instance.transform.position : Vector2.zero;
public static float VisionRadius => Instance != null ? Instance._visionRadius : 8f;
@@ -64,10 +67,18 @@ namespace Minigames.TrashMaze.Core
InputManager.Instance.RegisterController("pulver", this, setAsDefaultConsumer: false);
Logging.Debug($"[PulverController] Registered controller '{gameObject.name}'");
}
// Start deactivated - PulverController only activates when explicitly switched to
// This allows FollowerController to be the default mode for Pulver
DeactivateController();
Logging.Debug("[PulverController] Auto-deactivated - waiting for controller switch");
}
protected override void Update()
{
// Only process if this controller is active
if (!_isControllerActive) return;
base.Update(); // Call base for movement and animation
// Update global shader properties for vision system
@@ -127,6 +138,37 @@ namespace Minigames.TrashMaze.Core
}
#endregion
#region Controller Lifecycle Overrides
public override void ActivateController()
{
base.ActivateController();
_isControllerActive = true;
// Enable AIPath for player control
if (_aiPath != null)
{
_aiPath.enabled = true;
_aiPath.maxSpeed = Settings.MoveSpeed;
}
Logging.Debug("[PulverController] Controller activated - shader updates enabled");
}
public override void DeactivateController()
{
// DO NOT call base.DeactivateController() - it disables the shared AIPath
// that FollowerController needs for pickup dispatch!
// Just manage our own internal state instead
_isControllerActive = false;
Logging.Debug("[PulverController] Controller deactivated - shader updates paused (AIPath untouched for FollowerController)");
}
#endregion
}
}

View File

@@ -84,6 +84,17 @@ namespace Minigames.TrashMaze.Core
Logging.Debug("[TrashMazeCameraController] Switched to Maze camera");
}
/// <summary>
/// Switch to Pulver gameplay camera (Pulver in main level, player-controlled)
/// </summary>
public void SwitchToPulverGameplay()
{
SwitchToState(TrashMazeCameraState.PulverGameplay);
if (showDebugLogs)
Logging.Debug("[TrashMazeCameraController] Switched to PulverGameplay camera");
}
/// <summary>
/// Get the gameplay camera
/// </summary>
@@ -100,6 +111,14 @@ namespace Minigames.TrashMaze.Core
return GetCamera(TrashMazeCameraState.Maze);
}
/// <summary>
/// Get the Pulver gameplay camera
/// </summary>
public CinemachineCamera GetPulverGameplayCamera()
{
return GetCamera(TrashMazeCameraState.PulverGameplay);
}
#endregion
}
}

View File

@@ -15,9 +15,6 @@ namespace Minigames.TrashMaze.Core
{
public static TrashMazeController Instance { get; private set; }
[Header("Player")]
[SerializeField] private PulverController pulverController;
[Header("Background")]
[Tooltip("Background sprite renderer - world size and center are inferred from its bounds")]
[SerializeField] private SpriteRenderer backgroundRenderer;
@@ -118,15 +115,6 @@ namespace Minigames.TrashMaze.Core
private void InitializePulver()
{
if (pulverController == null)
{
Logging.Error("[TrashMazeController] PulverController reference not assigned! Please assign it in the Inspector.");
return;
}
Logging.Debug($"[TrashMazeController] Pulver controller initialized at {pulverController.transform.position}");
// TODO: Implement proper events for maze start and finish
pulverControllerSwitch.OnCharacterSwitch.AddListener(SwitchedToPulver);
trafalgarControllerSwitch.OnCharacterSwitch.AddListener(SwitchedToTrafalgar);
}

View File

@@ -13,7 +13,12 @@
/// <summary>
/// Maze camera following Pulver when exploring the maze alone
/// </summary>
Maze
Maze,
/// <summary>
/// Gameplay camera following Pulver (after exiting maze while still player-controlled)
/// </summary>
PulverGameplay
}
}

View File

@@ -0,0 +1,92 @@
using System.Collections;
using Core;
using Interactions;
using Minigames.TrashMaze.Core;
using Minigames.TrashMaze.Data;
using UnityEngine;
namespace Minigames.TrashMaze.Objects
{
/// <summary>
/// Maze exit interactable that teleports Pulver out of the maze.
/// Uses camera blend with midway teleportation to create seamless transition.
/// Does NOT switch controllers - Pulver remains player-controlled.
/// </summary>
public class MazeExit : SaveableInteractable
{
[Header("Maze Exit Settings")]
[Tooltip("Transform where Pulver should be teleported to (maze exit position)")]
[SerializeField] private Transform teleportTarget;
[Tooltip("Camera state to blend to (typically PulverGameplay)")]
[SerializeField] private TrashMazeCameraState targetCameraState = TrashMazeCameraState.PulverGameplay;
public override string SaveId => $"{gameObject.scene.name}/MazeExit/{gameObject.name}";
protected override object GetSerializableState()
{
// MazeExit doesn't need to save state - it's a simple trigger
return null;
}
protected override void ApplySerializableState(string state)
{
// MazeExit doesn't need to restore state
}
protected override bool DoInteraction()
{
Logging.Debug("[MazeExit] Starting maze exit sequence");
StartCoroutine(ExitMazeSequence());
return true;
}
private IEnumerator ExitMazeSequence()
{
// Step 1: Find Pulver (should be the active player-controlled character)
var pulverController = FindFirstObjectByType<PulverController>();
if (pulverController == null)
{
Debug.LogError("[MazeExit] PulverController not found in scene!");
yield break;
}
GameObject pulverGameObject = pulverController.gameObject;
Logging.Debug($"[MazeExit] Found Pulver at {pulverGameObject.transform.position}");
// Step 2: Start camera blend to target state (PulverGameplay)
TeleportationHelper.StartCameraBlend(targetCameraState);
// Step 3: Wait for halfway through blend, teleport Pulver, and set tracking target
yield return TeleportationHelper.TeleportMidBlendAndSetTracking(pulverGameObject, teleportTarget, targetCameraState);
// Step 4: Wait for camera blend to complete
yield return TeleportationHelper.WaitForCameraBlend();
// Step 5: Stop Pulver movement to prevent it from continuing with cached input
if (pulverController != null)
{
pulverController.InterruptMoveTo();
Logging.Debug("[MazeExit] Stopped Pulver movement after teleportation");
}
Logging.Debug("[MazeExit] Maze exit sequence completed");
}
#if UNITY_EDITOR
private void OnValidate()
{
name = "MazeExit";
// Default to PulverGameplay camera
if (targetCameraState == TrashMazeCameraState.Gameplay)
{
targetCameraState = TrashMazeCameraState.PulverGameplay;
}
}
#endif
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 4543937f547b49ce8e506aac52442f73
timeCreated: 1766151078

View File

@@ -0,0 +1,158 @@
using System.Collections;
using Core;
using Minigames.TrashMaze.Core;
using Minigames.TrashMaze.Data;
using Unity.Cinemachine;
using UnityEngine;
namespace Minigames.TrashMaze.Objects
{
/// <summary>
/// Static helper class for trash maze teleportation logic.
/// Provides reusable methods for:
/// - Starting camera blend to a target state
/// - Teleporting a character midway through blend
/// - Repositioning camera to teleported character
/// - Setting up camera tracking after blend completes
/// </summary>
public static class TeleportationHelper
{
/// <summary>
/// Get the camera for the target camera state
/// </summary>
private static CinemachineCamera GetTargetCamera(TrashMazeCameraState cameraState)
{
if (TrashMazeCameraController.Instance == null)
return null;
return cameraState switch
{
TrashMazeCameraState.Gameplay => TrashMazeCameraController.Instance.GetGameplayCamera(),
TrashMazeCameraState.Maze => TrashMazeCameraController.Instance.GetMazeCamera(),
TrashMazeCameraState.PulverGameplay => TrashMazeCameraController.Instance.GetPulverGameplayCamera(),
_ => null
};
}
/// <summary>
/// Start the camera blend to the target state
/// </summary>
public static void StartCameraBlend(TrashMazeCameraState targetState)
{
if (TrashMazeCameraController.Instance != null)
{
Logging.Debug($"[TeleportationHelper] Starting camera blend to {targetState}");
switch (targetState)
{
case TrashMazeCameraState.Gameplay:
TrashMazeCameraController.Instance.SwitchToGameplay();
break;
case TrashMazeCameraState.Maze:
TrashMazeCameraController.Instance.SwitchToMaze();
break;
case TrashMazeCameraState.PulverGameplay:
TrashMazeCameraController.Instance.SwitchToPulverGameplay();
break;
}
}
else
{
Debug.LogError($"[TeleportationHelper] TrashMazeCameraController instance not found!");
}
}
/// <summary>
/// Coroutine that waits for halfway through camera blend, then teleports the character
/// and immediately sets it as the tracking target for the camera
/// </summary>
public static IEnumerator TeleportMidBlendAndSetTracking(GameObject character, Transform teleportTarget, TrashMazeCameraState targetCameraState)
{
CinemachineBrain brain = Camera.main?.GetComponent<CinemachineBrain>();
if (brain != null && brain.IsBlending)
{
// Get blend duration from brain
float blendDuration = brain.ActiveBlend != null ? brain.ActiveBlend.Duration : 1f;
float halfBlendTime = blendDuration / 2f;
Logging.Debug($"[TeleportationHelper] Waiting {halfBlendTime:F2}s (half of {blendDuration:F2}s blend) before teleport");
// Wait for halfway through the blend
yield return new WaitForSeconds(halfBlendTime);
}
else
{
// Fallback: wait a short moment if no blend is detected
yield return new WaitForSeconds(0.25f);
}
// Teleport character
if (character != null && teleportTarget != null)
{
character.transform.position = teleportTarget.position;
character.transform.rotation = teleportTarget.rotation;
Logging.Debug($"[TeleportationHelper] Teleported {character.name} to {teleportTarget.position}");
// Immediately set as tracking target - let the blend finish naturally
SetCameraTrackingTarget(character, targetCameraState);
}
else
{
if (character == null)
Debug.LogError($"[TeleportationHelper] Character GameObject is null!");
if (teleportTarget == null)
Debug.LogError($"[TeleportationHelper] Teleport target not assigned!");
}
}
/// <summary>
/// Wait for camera blend to complete
/// </summary>
public static IEnumerator WaitForCameraBlend()
{
CinemachineBrain brain = Camera.main?.GetComponent<CinemachineBrain>();
if (brain != null)
{
// Wait until blend is complete
while (brain.IsBlending)
{
yield return null;
}
Logging.Debug($"[TeleportationHelper] Camera blend completed");
}
else
{
// Fallback: wait a brief moment
yield return new WaitForSeconds(0.5f);
}
}
/// <summary>
/// Set a character as the tracking target for the target camera
/// </summary>
public static void SetCameraTrackingTarget(GameObject character, TrashMazeCameraState targetCameraState)
{
if (character == null)
{
Debug.LogError($"[TeleportationHelper] Cannot set tracking target - character is null");
return;
}
var targetCamera = GetTargetCamera(targetCameraState);
if (targetCamera != null)
{
targetCamera.Follow = character.transform;
targetCamera.LookAt = character.transform;
Logging.Debug($"[TeleportationHelper] Set {character.name} as tracking target for {targetCameraState} camera");
}
else
{
Debug.LogError($"[TeleportationHelper] Target camera for state {targetCameraState} not found!");
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 1cf42e4c3a1e3c345aaef352bf84c762

View File

@@ -0,0 +1,109 @@
using System.Collections;
using Core;
using Input;
using Items;
using Minigames.TrashMaze.Core;
using Minigames.TrashMaze.Data;
using Unity.Cinemachine;
using UnityEngine;
namespace Minigames.TrashMaze.Objects
{
/// <summary>
/// Trash Maze specific controller switch that transitions TO Pulver (maze entrance).
/// Handles camera blend with midway teleportation to create the illusion of entering the maze.
/// </summary>
public class TrashMazeSwitchToPulver : ControllerSwitchItem
{
[Header("Trash Maze - To Pulver Settings")]
[Tooltip("Transform where Pulver should be teleported to (maze entrance)")]
[SerializeField] private Transform teleportTarget;
[Tooltip("Camera state to blend to (Maze camera)")]
[SerializeField] private TrashMazeCameraState targetCameraState = TrashMazeCameraState.Maze;
protected override IEnumerator SwitchControllerSequence()
{
_isSwitching = true;
// Step 1: Get controllers
var currentController = InputManager.Instance.GetActiveController();
var targetController = InputManager.Instance.GetController("pulver");
if (currentController == null || targetController == null)
{
Debug.LogError($"[TrashMazeSwitchToPulver] Failed to get controllers!");
_isSwitching = false;
yield break;
}
GameObject currentGameObject = (currentController as MonoBehaviour)?.gameObject;
GameObject targetGameObject = (targetController as MonoBehaviour)?.gameObject;
Logging.Debug($"[TrashMazeSwitchToPulver] Switching from {currentGameObject?.name} to Pulver");
// Step 2: Deactivate current controller (Trafalgar)
DeactivateCurrentController(currentController, currentGameObject);
// Step 3: Deactivate Pulver's follower controller (will be teleported)
if (targetGameObject != null)
{
var pulverFollower = targetGameObject.GetComponent<FollowerController>();
if (pulverFollower != null)
{
pulverFollower.DeactivateFollower();
}
}
// Step 4: Start camera blend to maze
TeleportationHelper.StartCameraBlend(targetCameraState);
// Step 5: Wait for halfway through the blend, teleport Pulver, and set tracking target
yield return TeleportationHelper.TeleportMidBlendAndSetTracking(targetGameObject, teleportTarget, targetCameraState);
// Step 6: Wait for camera blend to complete
yield return TeleportationHelper.WaitForCameraBlend();
// Step 7: Activate Pulver controller
if (targetController is BasePlayerMovementController targetPlayerController)
{
targetPlayerController.ActivateController();
}
// Step 8: Switch InputManager to Pulver controller
bool switchSuccess = InputManager.Instance.SwitchToController("pulver");
if (switchSuccess)
{
Logging.Debug($"[TrashMazeSwitchToPulver] Successfully switched to Pulver controller");
OnCharacterSwitch.Invoke();
}
else
{
Debug.LogError($"[TrashMazeSwitchToPulver] Failed to switch to Pulver controller");
}
// Step 9: Mark as used if one-time use
if (isOneTime)
{
DisableVisual();
}
_isSwitching = false;
}
#if UNITY_EDITOR
private void OnValidate()
{
name = "TrashMazeSwitch_ToPulver";
// Default to Maze camera
if (targetCameraState != TrashMazeCameraState.Maze)
{
targetCameraState = TrashMazeCameraState.Maze;
}
}
#endif
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: fe2d6200d0d54638adb61befd932228f
timeCreated: 1766149622

View File

@@ -0,0 +1,180 @@
using System.Collections;
using Core;
using Input;
using Items;
using Minigames.TrashMaze.Core;
using Minigames.TrashMaze.Data;
using Unity.Cinemachine;
using UnityEngine;
namespace Minigames.TrashMaze.Objects
{
/// <summary>
/// Trash Maze specific controller switch that transitions FROM Pulver back TO Trafalgar (maze exit).
/// Handles camera blend back to gameplay view and re-enables follower mode.
/// </summary>
public class TrashMazeSwitchToTrafalgar : ControllerSwitchItem
{
[Header("Trash Maze - To Trafalgar Settings")]
[Tooltip("Camera state to blend to (Gameplay camera)")]
[SerializeField] private TrashMazeCameraState targetCameraState = TrashMazeCameraState.Gameplay;
protected override IEnumerator SwitchControllerSequence()
{
_isSwitching = true;
// Step 1: Get controllers
var currentController = InputManager.Instance.GetActiveController();
var targetController = InputManager.Instance.GetController("trafalgar");
if (currentController == null || targetController == null)
{
Debug.LogError($"[TrashMazeSwitchToTrafalgar] Failed to get controllers!");
_isSwitching = false;
yield break;
}
GameObject currentGameObject = (currentController as MonoBehaviour)?.gameObject;
GameObject targetGameObject = (targetController as MonoBehaviour)?.gameObject;
Logging.Debug($"[TrashMazeSwitchToTrafalgar] Switching from Pulver to {targetGameObject?.name}");
// Step 2: Deactivate current controller (Pulver)
DeactivateCurrentController(currentController, currentGameObject);
// Explicitly deactivate PulverController to ensure it stops receiving input
if (currentGameObject != null)
{
var pulverController = currentGameObject.GetComponent<PulverController>();
if (pulverController != null)
{
pulverController.DeactivateController();
Logging.Debug("[TrashMazeSwitchToTrafalgar] Explicitly deactivated PulverController");
}
}
// Step 3: Start camera blend back to gameplay
StartCameraBlend();
// Step 4: Wait for camera blend to complete
yield return WaitForCameraBlend();
// Step 5: Unset Pulver as tracking target and set Trafalgar for gameplay camera
SetTrafalgarAsTrackingTarget(targetGameObject);
// Step 6: Activate Pulver's follower mode (so it follows Trafalgar)
if (currentGameObject != null)
{
var pulverFollower = currentGameObject.GetComponent<FollowerController>();
if (pulverFollower != null)
{
pulverFollower.ActivateFollower();
}
}
// Step 7: Activate Trafalgar controller
if (targetController is BasePlayerMovementController targetPlayerController)
{
targetPlayerController.ActivateController();
}
// Step 8: Switch InputManager to Trafalgar controller
bool switchSuccess = InputManager.Instance.SwitchToController("trafalgar");
if (switchSuccess)
{
Logging.Debug($"[TrashMazeSwitchToTrafalgar] Successfully switched to Trafalgar controller");
OnCharacterSwitch.Invoke();
}
else
{
Debug.LogError($"[TrashMazeSwitchToTrafalgar] Failed to switch to Trafalgar controller");
}
// Step 8: Mark as used if one-time use
if (isOneTime)
{
DisableVisual();
}
_isSwitching = false;
}
private void StartCameraBlend()
{
if (TrashMazeCameraController.Instance != null)
{
Logging.Debug($"[TrashMazeSwitchToTrafalgar] Starting camera blend to {targetCameraState}");
if (targetCameraState == TrashMazeCameraState.Gameplay)
{
TrashMazeCameraController.Instance.SwitchToGameplay();
}
else
{
Logging.Warning($"[TrashMazeSwitchToTrafalgar] Unexpected camera state: {targetCameraState}");
}
}
else
{
Debug.LogError("[TrashMazeSwitchToTrafalgar] TrashMazeCameraController instance not found!");
}
}
private IEnumerator WaitForCameraBlend()
{
CinemachineBrain brain = Camera.main?.GetComponent<CinemachineBrain>();
if (brain != null)
{
// Wait until blend is complete
while (brain.IsBlending)
{
yield return null;
}
Logging.Debug("[TrashMazeSwitchToTrafalgar] Camera blend completed");
}
else
{
// Fallback: wait a brief moment
yield return new WaitForSeconds(0.5f);
}
}
private void SetTrafalgarAsTrackingTarget(GameObject trafalgarGameObject)
{
if (TrashMazeCameraController.Instance != null)
{
// Clear maze camera tracking target
var mazeCamera = TrashMazeCameraController.Instance.GetMazeCamera();
if (mazeCamera != null)
{
mazeCamera.Follow = null;
mazeCamera.LookAt = null;
Logging.Debug($"[TrashMazeSwitchToTrafalgar] Cleared Pulver as tracking target from maze camera");
}
// Set Trafalgar as tracking target for gameplay camera
if (trafalgarGameObject != null)
{
var gameplayCamera = TrashMazeCameraController.Instance.GetGameplayCamera();
if (gameplayCamera != null)
{
gameplayCamera.Follow = trafalgarGameObject.transform;
gameplayCamera.LookAt = trafalgarGameObject.transform;
Logging.Debug($"[TrashMazeSwitchToTrafalgar] Set Trafalgar as tracking target for gameplay camera");
}
}
}
}
#if UNITY_EDITOR
private void OnValidate()
{
name = "TrashMazeSwitch_ToTrafalgar";
}
#endif
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 60a323cce5144fe9bae5dd3b313315a1
timeCreated: 1766149639

View File

@@ -42,6 +42,9 @@ public class FollowerController : ManagedBehaviour
private IFollowerSettings _settings;
private IInteractionSettings _interactionSettings;
// Follower active state
private bool _isFollowerActive = false;
private GameObject _playerRef;
private Transform _playerTransform;
private AIPath _playerAIPath;
@@ -133,10 +136,18 @@ public class FollowerController : ManagedBehaviour
{
// Find player reference when scene is ready (called for every scene load)
FindPlayerReference();
// Auto-activate follower mode when scene is ready
// This ensures Pulver automatically follows Trafalgar in any scene by default
ActivateFollower();
Logging.Debug("[FollowerController] Auto-activated follower mode on scene ready");
}
void Update()
{
// Only process if follower is active
if (!_isFollowerActive) return;
if (_playerTransform == null)
{
return;
@@ -274,6 +285,118 @@ public class FollowerController : ManagedBehaviour
}
}
#region Follower Lifecycle
/// <summary>
/// Activate follower behavior - starts following the player.
/// </summary>
public void ActivateFollower()
{
_isFollowerActive = true;
// Find/refresh player reference
FindPlayerReference();
// Check if a pickup is currently in progress
bool pickupInProgress = _pickupCoroutine != null;
if (!pickupInProgress)
{
// Only reset to manual following mode if no pickup is active
_isManualFollowing = true;
_isReturningToPlayer = false;
_isPlayingStationaryAnimation = false;
_currentSpeed = 0f;
_timer = 0f;
// Stop stationary animation coroutine if active
if (_stationaryAnimationCoroutine != null)
{
StopCoroutine(_stationaryAnimationCoroutine);
_stationaryAnimationCoroutine = null;
}
// Disable AIPath for manual following
if (_aiPath != null)
{
_aiPath.enabled = false;
}
// Initialize follow target position
UpdateFollowTarget();
}
else
{
// Pickup in progress - don't interfere, just mark as active
Logging.Debug("[FollowerController] Follower activated but pickup in progress - not resetting state");
}
// Always enable TrackableTarget when follower is active
var trackableTarget = GetComponent<UI.Tracking.TrackableTarget>();
if (trackableTarget != null)
{
trackableTarget.enabled = true;
}
Logging.Debug("[FollowerController] Follower activated");
}
/// <summary>
/// Deactivate follower behavior - stops all following and movement.
/// </summary>
public void DeactivateFollower()
{
_isFollowerActive = false;
// Stop all coroutines
if (_pickupCoroutine != null)
{
StopCoroutine(_pickupCoroutine);
_pickupCoroutine = null;
}
if (_stationaryAnimationCoroutine != null)
{
StopCoroutine(_stationaryAnimationCoroutine);
_stationaryAnimationCoroutine = null;
}
// Reset movement state
_isManualFollowing = false;
_isReturningToPlayer = false;
_isPlayingStationaryAnimation = false;
_currentSpeed = 0f;
// Disable AIPath
if (_aiPath != null)
{
_aiPath.enabled = false;
_aiPath.isStopped = true;
}
// Disable TrackableTarget component if present
var trackableTarget = GetComponent<UI.Tracking.TrackableTarget>();
if (trackableTarget != null)
{
trackableTarget.enabled = false;
}
// Set animator to idle
if (_animator != null)
{
_animator.SetFloat("Speed", 0f);
}
Logging.Debug("[FollowerController] Follower deactivated");
}
/// <summary>
/// Check if follower is currently active.
/// </summary>
public bool IsFollowerActive => _isFollowerActive;
#endregion
#region Movement
/// <summary>
/// Updates the follower's target point to follow the player at a specified distance,