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