Files
AppleHillsProduction/Assets/Scripts/Movement/FollowerController.cs
2025-09-08 15:09:45 +02:00

400 lines
14 KiB
C#

using UnityEngine;
using Pathfinding;
using UnityEngine.SceneManagement;
/// <summary>
/// Controls the follower character, including following the player, handling pickups, and managing held items.
/// </summary>
public class FollowerController : MonoBehaviour
{
[Header("Follower Settings")]
public bool debugDrawTarget = true;
/// <summary>
/// How often to update follow logic.
/// </summary>
public float followUpdateInterval = 0.1f;
/// <summary>
/// Smoothing factor for manual movement.
/// </summary>
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;
[Header("Held Item")]
/// <summary>
/// The item currently held by the follower.
/// </summary>
public PickupItemData currentlyHeldItem;
/// <summary>
/// Renderer for the held item icon.
/// </summary>
public SpriteRenderer heldObjectRenderer;
/// <summary>
/// Desired height for held item icon.
/// </summary>
public float heldIconDisplayHeight = 2.0f;
private bool isReturningToPlayer = false;
// Speed fields for follower
private float playerMaxSpeed = 5f;
private float followerMaxSpeed = 6f;
private float defaultFollowerMaxSpeed = 6f;
// Pickup events
public delegate void FollowerPickupHandler();
/// <summary>
/// Event fired when the follower arrives at a pickup.
/// </summary>
public event FollowerPickupHandler OnPickupArrived;
/// <summary>
/// Event fired when the follower returns to the player after a pickup.
/// </summary>
public event FollowerPickupHandler OnPickupReturned;
private Coroutine pickupCoroutine;
private bool lastInteractionSuccess = true;
/// <summary>
/// Cache for the currently picked-up GameObject (hidden while held).
/// </summary>
private GameObject cachedPickupObject = null;
/// <summary>
/// Set to true if the follower just combined items.
/// </summary>
public bool justCombined = false;
void Awake()
{
aiPath = GetComponent<AIPath>();
// Find art prefab and animator
artTransform = transform.Find("CharacterArt");
if (artTransform != null)
{
animator = artTransform.GetComponent<Animator>();
}
else
{
animator = GetComponentInChildren<Animator>(); // fallback
}
}
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;
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;
}
animator.SetFloat("Speed", Mathf.Clamp01(normalizedSpeed));
}
}
void FindPlayerReference()
{
GameObject playerObj = GameObject.FindGameObjectWithTag("Player");
if (playerObj != null)
{
playerTransform = playerObj.transform;
playerAIPath = playerObj.GetComponent<AIPath>();
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)
/// <summary>
/// Command follower to go to a specific point (pathfinding mode).
/// </summary>
/// <param name="worldPosition">The world position to move to.</param>
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
/// <summary>
/// Command follower to go to a specific point and return to player.
/// </summary>
/// <param name="itemPosition">The position of the item to pick up.</param>
/// <param name="playerTransform">The transform of the player.</param>
public void GoToPointAndReturn(Vector2 itemPosition, Transform playerTransform)
{
if (pickupCoroutine != null)
StopCoroutine(pickupCoroutine);
if (aiPath != null)
aiPath.maxSpeed = followerMaxSpeed;
pickupCoroutine = StartCoroutine(PickupSequence(itemPosition, playerTransform));
}
/// <summary>
/// Set the item held by the follower.
/// </summary>
/// <param name="itemData">The item data to set.</param>
public void SetHeldItem(PickupItemData itemData)
{
currentlyHeldItem = itemData;
if (heldObjectRenderer != null)
{
if (currentlyHeldItem != null && currentlyHeldItem.mapSprite != null)
{
heldObjectRenderer.sprite = currentlyHeldItem.mapSprite;
heldObjectRenderer.enabled = true;
// Scale held icon to fixed height, preserve aspect ratio
var sprite = currentlyHeldItem.mapSprite;
float spriteHeight = sprite.bounds.size.y;
float spriteWidth = sprite.bounds.size.x;
if (spriteHeight > 0f)
{
float scaleY = heldIconDisplayHeight / spriteHeight;
float scaleX = scaleY * (spriteWidth / spriteHeight);
heldObjectRenderer.transform.localScale = new Vector3(scaleX, scaleY, 1f);
}
}
else
{
heldObjectRenderer.sprite = null;
heldObjectRenderer.enabled = false;
}
}
}
/// <summary>
/// Set the result of the last interaction (success or failure).
/// </summary>
/// <param name="success">True if the last interaction was successful, false otherwise.</param>
public void SetInteractionResult(bool success)
{
lastInteractionSuccess = success;
}
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<Pickup>();
if (pickup != null)
{
var slotBehavior = pickup.GetComponent<SlotItemBehavior>();
if (slotBehavior != null)
{
// Slot item: do not destroy or swap, just return to player
break;
}
if (justCombined)
{
// Combination: just destroy the pickup, don't spawn anything
if (cachedPickupObject != null)
{
Destroy(cachedPickupObject);
cachedPickupObject = null;
}
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);
// Cache and hide the picked up object
cachedPickupObject = pickup.gameObject;
cachedPickupObject.SetActive(false);
cachedPickupObject.transform.SetParent(this.transform);
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;
}
void OnDrawGizmos()
{
if (debugDrawTarget && Application.isPlaying)
{
Gizmos.color = Color.cyan;
Gizmos.DrawSphere(targetPoint, 0.2f);
Gizmos.color = Color.yellow;
Gizmos.DrawLine(transform.position, targetPoint);
}
}
}