using Interactions; 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: MonoBehaviour { [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 _currentlyHeldItemData; public PickupItemData CurrentlyHeldItemData => _currentlyHeldItemData; private GameObject _cachedPickupObject = null; public bool justCombined = false; /// /// 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; 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 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; } } #region Movement /// /// Updates the follower's target point to follow the player at a specified distance, /// using the player's current movement direction if available. Disables pathfinding /// when in manual following mode. /// 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; } } } // 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)); } 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(); // 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; } #endregion Movement #region ItemInteractions public void TryPickupItem(GameObject itemObject, PickupItemData itemData) { if (_currentlyHeldItemData != null && _cachedPickupObject != null) { // Drop the currently held item at the current position DropHeldItemAt(transform.position); } // Pick up the new item SetHeldItem(itemData, itemObject.GetComponent()); _cachedPickupObject = itemObject; _cachedPickupObject.SetActive(false); } public enum CombinationResult { Successful, Unsuccessful, NotApplicable } public CombinationResult TryCombineItems(Pickup pickupA, out GameObject newItem) { newItem = null; if (_cachedPickupObject == null) { return CombinationResult.NotApplicable; } Pickup pickupB = _cachedPickupObject.GetComponent(); if (pickupA == null || pickupB == null) { return CombinationResult.NotApplicable; } var rule = GameManager.Instance.GetCombinationRule(pickupA.itemData, pickupB.itemData); Vector3 spawnPos = pickupA.gameObject.transform.position; if (rule != null && rule.resultPrefab != null) { newItem = Instantiate(rule.resultPrefab, spawnPos, Quaternion.identity); PickupItemData itemData = newItem.GetComponent().itemData; Destroy(pickupA.gameObject); Destroy(pickupB.gameObject); TryPickupItem(newItem,itemData); return CombinationResult.Successful; } // If no combination found, return Unsuccessful return CombinationResult.Unsuccessful; } /// /// 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) { _currentlyHeldItemData = itemData; if (heldObjectRenderer != null) { if (_currentlyHeldItemData != null && pickupRenderer != null) { AppleHillsUtils.CopySpriteRendererProperties(pickupRenderer, heldObjectRenderer); } else { heldObjectRenderer.sprite = null; heldObjectRenderer.enabled = false; } } } 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); _cachedPickupObject = obj; } else { ClearHeldItem(); } } public void ClearHeldItem() { _cachedPickupObject = null; _currentlyHeldItemData = null; if (heldObjectRenderer != null) { heldObjectRenderer.sprite = null; heldObjectRenderer.enabled = false; } } public void DropItem(FollowerController follower, Vector3 position) { var item = follower.GetHeldPickupObject(); if (item == null) return; item.transform.position = position; item.transform.SetParent(null); item.SetActive(true); follower.ClearHeldItem(); // Optionally: fire event, update UI, etc. } public void DropHeldItemAt(Vector3 position) { DropItem(this, position); } #endregion ItemInteractions #if UNITY_EDITOR void OnDrawGizmos() { if (debugDrawTarget && Application.isPlaying) { Gizmos.color = Color.cyan; Gizmos.DrawSphere(_targetPoint, 0.2f); Gizmos.color = Color.yellow; Gizmos.DrawLine(transform.position, _targetPoint); } } #endif }