using UnityEngine;
using Pathfinding;
using UnityEngine.SceneManagement;
using Utils;
///
/// Controls the follower character, including following the player, handling pickups, and managing held items.
///
public class FollowerController : Character
{
[Header("Follower Settings")]
public bool debugDrawTarget = true;
///
/// How often to update follow logic.
///
public float followUpdateInterval = 0.1f;
///
/// Smoothing factor for manual movement.
///
public float manualMoveSmooth = 8f;
private Transform _playerTransform;
private AIPath _playerAIPath;
private AIPath _aiPath;
private Vector3 _targetPoint;
private float _timer;
private bool _isManualFollowing = true;
private Vector3 _lastMoveDir = Vector3.right;
private float _currentSpeed = 0f;
private Animator _animator;
private Transform _artTransform;
private SpriteRenderer spriteRenderer;
private PickupItemData _currentlyHeldItem;
public PickupItemData CurrentlyHeldItem => _currentlyHeldItem;
///
/// Renderer for the held item icon.
///
public SpriteRenderer heldObjectRenderer;
private bool _isReturningToPlayer = false;
private float _playerMaxSpeed = 5f;
private float _followerMaxSpeed = 6f;
private float _defaultFollowerMaxSpeed = 6f;
// Pickup events
public delegate void FollowerPickupHandler();
///
/// Event fired when the follower arrives at a pickup.
///
public event FollowerPickupHandler OnPickupArrived;
///
/// Event fired when the follower returns to the player after a pickup.
///
public event FollowerPickupHandler OnPickupReturned;
private Coroutine _pickupCoroutine;
private bool _lastInteractionSuccess = true;
///
/// Cache for the currently picked-up GameObject (hidden while held).
///
private GameObject _cachedPickupObject = null;
public bool justCombined = false;
///
/// Caches the given pickup object as the currently held item, hides it, and parents it to the follower.
///
public void CacheHeldPickupObject(GameObject obj)
{
// Do not destroy the previous object; just replace and hide
_cachedPickupObject = obj;
if (_cachedPickupObject != null)
{
_cachedPickupObject.SetActive(false);
}
}
void Awake()
{
_aiPath = GetComponent();
// Find art prefab and animator
_artTransform = transform.Find("CharacterArt");
if (_artTransform != null)
{
_animator = _artTransform.GetComponent();
spriteRenderer = _artTransform.GetComponent();
}
else
{
_animator = GetComponentInChildren(); // fallback
spriteRenderer = GetComponentInChildren();
}
}
void OnEnable()
{
SceneManager.sceneLoaded += OnSceneLoaded;
FindPlayerReference();
}
void OnDisable()
{
SceneManager.sceneLoaded -= OnSceneLoaded;
}
void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
FindPlayerReference();
}
void UpdateFollowTarget()
{
if (_playerTransform == null)
{
FindPlayerReference();
if (_playerTransform == null)
return;
}
if (_isManualFollowing)
{
Vector3 playerPos = _playerTransform.position;
Vector3 moveDir = Vector3.zero;
if (_playerAIPath != null && _playerAIPath.velocity.magnitude > 0.01f)
{
moveDir = _playerAIPath.velocity.normalized;
_lastMoveDir = moveDir;
}
else
{
moveDir = _lastMoveDir;
}
// Use GameSettings for followDistance
_targetPoint = playerPos - moveDir * GameManager.Instance.FollowDistance;
_targetPoint.z = 0;
if (_aiPath != null)
{
_aiPath.enabled = false;
}
}
}
void Update()
{
if (_playerTransform == null)
{
FindPlayerReference();
if (_playerTransform == null)
return;
}
_timer += Time.deltaTime;
if (_timer >= GameManager.Instance.FollowUpdateInterval)
{
_timer = 0f;
UpdateFollowTarget();
}
if (_isManualFollowing)
{
Vector2 current2D = new Vector2(transform.position.x, transform.position.y);
Vector2 target2D = new Vector2(_targetPoint.x, _targetPoint.y);
float dist = Vector2.Distance(current2D, target2D);
float minSpeed = _followerMaxSpeed * 0.3f;
float lerpFactor = GameManager.Instance.ManualMoveSmooth * Time.deltaTime;
float targetSpeed = 0f;
if (dist > GameManager.Instance.StopThreshold)
{
if (dist > GameManager.Instance.ThresholdFar)
{
targetSpeed = _followerMaxSpeed;
}
else if (dist > GameManager.Instance.ThresholdNear && dist <= GameManager.Instance.ThresholdFar)
{
targetSpeed = _followerMaxSpeed;
}
else if (dist > GameManager.Instance.StopThreshold && dist <= GameManager.Instance.ThresholdNear)
{
targetSpeed = minSpeed;
}
_currentSpeed = Mathf.Lerp(_currentSpeed, targetSpeed, lerpFactor);
if (dist > GameManager.Instance.StopThreshold && dist <= GameManager.Instance.ThresholdNear)
{
_currentSpeed = Mathf.Max(_currentSpeed, minSpeed);
}
Vector3 dir = (_targetPoint - transform.position).normalized;
// Sprite flipping based on movement direction
if (spriteRenderer != null && dir.sqrMagnitude > 0.001f)
{
if (dir.x > 0.01f)
spriteRenderer.flipX = false;
else if (dir.x < -0.01f)
spriteRenderer.flipX = true;
}
transform.position += dir * _currentSpeed * Time.deltaTime;
}
else
{
_currentSpeed = 0f;
}
}
if (_isReturningToPlayer && _aiPath != null && _aiPath.enabled && _playerTransform != null)
{
_aiPath.destination = _playerTransform.position;
}
if (_animator != null)
{
float normalizedSpeed = 0f;
if (_isManualFollowing)
{
normalizedSpeed = _currentSpeed / _followerMaxSpeed;
}
else if (_aiPath != null)
{
normalizedSpeed = _aiPath.velocity.magnitude / _followerMaxSpeed;
// Sprite flipping for pathfinding mode
if (spriteRenderer != null && _aiPath.velocity.sqrMagnitude > 0.001f)
{
if (_aiPath.velocity.x > 0.01f)
spriteRenderer.flipX = false;
else if (_aiPath.velocity.x < -0.01f)
spriteRenderer.flipX = true;
}
}
_animator.SetFloat("Speed", Mathf.Clamp01(normalizedSpeed));
}
}
void FindPlayerReference()
{
GameObject playerObj = GameObject.FindGameObjectWithTag("Player");
if (playerObj != null)
{
_playerTransform = playerObj.transform;
_playerAIPath = playerObj.GetComponent();
if (_playerAIPath != null)
{
_playerMaxSpeed = _playerAIPath.maxSpeed;
_defaultFollowerMaxSpeed = _playerMaxSpeed;
_followerMaxSpeed = _playerMaxSpeed * GameManager.Instance.FollowerSpeedMultiplier;
}
}
else
{
_playerTransform = null;
_playerAIPath = null;
}
}
// Command follower to go to a specific point (pathfinding mode)
///
/// Command follower to go to a specific point (pathfinding mode).
///
/// The world position to move to.
public void GoToPoint(Vector2 worldPosition)
{
_isManualFollowing = false;
if (_aiPath != null)
{
_aiPath.enabled = true;
_aiPath.maxSpeed = _followerMaxSpeed;
_aiPath.destination = new Vector3(worldPosition.x, worldPosition.y, 0);
}
}
// Command follower to go to a specific point and return to player
///
/// Command follower to go to a specific point and return to player.
///
/// The position of the item to pick up.
/// The transform of the player.
public void GoToPointAndReturn(Vector2 itemPosition, Transform playerTransform)
{
if (_pickupCoroutine != null)
StopCoroutine(_pickupCoroutine);
if (_aiPath != null)
_aiPath.maxSpeed = _followerMaxSpeed;
_pickupCoroutine = StartCoroutine(PickupSequence(itemPosition, playerTransform));
}
///
/// Set the item held by the follower, copying all visual properties from the Pickup's SpriteRenderer.
///
/// The item data to set.
/// The SpriteRenderer from the Pickup to copy appearance from.
public void SetHeldItem(PickupItemData itemData, SpriteRenderer pickupRenderer = null)
{
_currentlyHeldItem = itemData;
if (heldObjectRenderer != null)
{
if (_currentlyHeldItem != null && pickupRenderer != null)
{
AppleHillsUtils.CopySpriteRendererProperties(pickupRenderer, heldObjectRenderer);
}
else
{
heldObjectRenderer.sprite = null;
heldObjectRenderer.enabled = false;
}
}
}
///
/// Set the result of the last interaction (success or failure).
///
/// True if the last interaction was successful, false otherwise.
public void SetInteractionResult(bool success)
{
_lastInteractionSuccess = success;
}
public GameObject GetHeldPickupObject()
{
return _cachedPickupObject;
}
public void SetHeldItemFromObject(GameObject obj)
{
if (obj == null)
{
ClearHeldItem();
return;
}
var pickup = obj.GetComponent();
if (pickup != null)
{
SetHeldItem(pickup.itemData, pickup.iconRenderer);
CacheHeldPickupObject(obj);
}
else
{
ClearHeldItem();
}
}
public void ClearHeldItem()
{
if (_cachedPickupObject != null)
{
// Destroy(_cachedPickupObject);
_cachedPickupObject = null;
}
_currentlyHeldItem = null;
if (heldObjectRenderer != null)
{
heldObjectRenderer.sprite = null;
heldObjectRenderer.enabled = false;
}
}
private System.Collections.IEnumerator PickupSequence(Vector2 itemPosition, Transform playerTransform)
{
_isManualFollowing = false;
_isReturningToPlayer = false;
if (_aiPath != null)
{
_aiPath.enabled = true;
_aiPath.maxSpeed = _followerMaxSpeed;
_aiPath.destination = new Vector3(itemPosition.x, itemPosition.y, 0);
}
// Wait until follower reaches item (2D distance)
while (Vector2.Distance(new Vector2(transform.position.x, transform.position.y), new Vector2(itemPosition.x, itemPosition.y)) > GameManager.Instance.StopThreshold)
{
yield return null;
}
OnPickupArrived?.Invoke();
// Only perform pickup/swap logic if interaction succeeded
if (_lastInteractionSuccess && heldObjectRenderer != null)
{
Collider2D[] hits = Physics2D.OverlapCircleAll(itemPosition, 0.2f);
foreach (var hit in hits)
{
var pickup = hit.GetComponent();
if (pickup != null)
{
var slotBehavior = pickup.GetComponent();
if (slotBehavior != null)
{
// Slot item: do not destroy or swap, just return to player
break;
}
if (justCombined)
{
GameObject.Destroy(pickup.gameObject);
justCombined = false;
break;
}
// Swap logic: if holding an item, drop it here
if (_currentlyHeldItem != null && _cachedPickupObject != null)
{
// Drop the cached object at the pickup's position
_cachedPickupObject.transform.position = pickup.transform.position;
_cachedPickupObject.transform.SetParent(null);
_cachedPickupObject.SetActive(true);
_cachedPickupObject = null;
}
SetHeldItem(pickup.itemData, pickup.iconRenderer);
CacheHeldPickupObject(pickup.gameObject);
break;
}
}
}
// Wait briefly, then return to player
yield return new WaitForSeconds(0.2f);
if (_aiPath != null && playerTransform != null)
{
_aiPath.maxSpeed = _followerMaxSpeed;
_aiPath.destination = playerTransform.position;
}
_isReturningToPlayer = true;
// Wait until follower returns to player (2D distance)
while (playerTransform != null && Vector2.Distance(new Vector2(transform.position.x, transform.position.y), new Vector2(playerTransform.position.x, playerTransform.position.y)) > GameManager.Instance.StopThreshold)
{
yield return null;
}
_isReturningToPlayer = false;
OnPickupReturned?.Invoke();
// Reset follower speed to normal after pickup
_followerMaxSpeed = _defaultFollowerMaxSpeed;
if (_aiPath != null)
_aiPath.maxSpeed = _followerMaxSpeed;
_isManualFollowing = true;
if (_aiPath != null)
_aiPath.enabled = false;
_pickupCoroutine = null;
}
///
/// Drop the held item at the specified position, unparenting and activating it.
///
/// The world position to drop the item at.
public void DropHeldItemAt(Vector3 position)
{
if (_cachedPickupObject != null)
{
_cachedPickupObject.transform.position = position;
_cachedPickupObject.transform.SetParent(null);
_cachedPickupObject.SetActive(true);
_cachedPickupObject = null;
_currentlyHeldItem = null;
if (heldObjectRenderer != null)
{
heldObjectRenderer.sprite = null;
heldObjectRenderer.enabled = false;
}
}
}
void OnDrawGizmos()
{
if (debugDrawTarget && Application.isPlaying)
{
Gizmos.color = Color.cyan;
Gizmos.DrawSphere(_targetPoint, 0.2f);
Gizmos.color = Color.yellow;
Gizmos.DrawLine(transform.position, _targetPoint);
}
}
}