Rework interactables into a flatter hierarchy, reenable puzzles as well
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using UnityEngine;
|
||||
using Interactions;
|
||||
using UnityEngine;
|
||||
using Pathfinding;
|
||||
using UnityEngine.SceneManagement;
|
||||
using Utils;
|
||||
@@ -6,7 +7,7 @@ using Utils;
|
||||
/// <summary>
|
||||
/// Controls the follower character, including following the player, handling pickups, and managing held items.
|
||||
/// </summary>
|
||||
public class FollowerController : Character
|
||||
public class FollowerController: MonoBehaviour
|
||||
{
|
||||
[Header("Follower Settings")]
|
||||
public bool debugDrawTarget = true;
|
||||
@@ -29,10 +30,13 @@ public class FollowerController : Character
|
||||
private float _currentSpeed = 0f;
|
||||
private Animator _animator;
|
||||
private Transform _artTransform;
|
||||
private SpriteRenderer spriteRenderer;
|
||||
private SpriteRenderer _spriteRenderer;
|
||||
|
||||
private PickupItemData _currentlyHeldItemData;
|
||||
public PickupItemData CurrentlyHeldItemData => _currentlyHeldItemData;
|
||||
private GameObject _cachedPickupObject = null;
|
||||
public bool justCombined = false;
|
||||
|
||||
private PickupItemData _currentlyHeldItem;
|
||||
public PickupItemData CurrentlyHeldItem => _currentlyHeldItem;
|
||||
/// <summary>
|
||||
/// Renderer for the held item icon.
|
||||
/// </summary>
|
||||
@@ -54,27 +58,9 @@ public class FollowerController : Character
|
||||
/// </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;
|
||||
public bool justCombined = false;
|
||||
|
||||
/// <summary>
|
||||
/// Caches the given pickup object as the currently held item, hides it, and parents it to the follower.
|
||||
/// </summary>
|
||||
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()
|
||||
{
|
||||
@@ -84,12 +70,12 @@ public class FollowerController : Character
|
||||
if (_artTransform != null)
|
||||
{
|
||||
_animator = _artTransform.GetComponent<Animator>();
|
||||
spriteRenderer = _artTransform.GetComponent<SpriteRenderer>();
|
||||
_spriteRenderer = _artTransform.GetComponent<SpriteRenderer>();
|
||||
}
|
||||
else
|
||||
{
|
||||
_animator = GetComponentInChildren<Animator>(); // fallback
|
||||
spriteRenderer = GetComponentInChildren<SpriteRenderer>();
|
||||
_spriteRenderer = GetComponentInChildren<SpriteRenderer>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,38 +94,7 @@ public class FollowerController : Character
|
||||
{
|
||||
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)
|
||||
@@ -185,12 +140,12 @@ public class FollowerController : Character
|
||||
}
|
||||
Vector3 dir = (_targetPoint - transform.position).normalized;
|
||||
// Sprite flipping based on movement direction
|
||||
if (spriteRenderer != null && dir.sqrMagnitude > 0.001f)
|
||||
if (_spriteRenderer != null && dir.sqrMagnitude > 0.001f)
|
||||
{
|
||||
if (dir.x > 0.01f)
|
||||
spriteRenderer.flipX = false;
|
||||
_spriteRenderer.flipX = false;
|
||||
else if (dir.x < -0.01f)
|
||||
spriteRenderer.flipX = true;
|
||||
_spriteRenderer.flipX = true;
|
||||
}
|
||||
transform.position += dir * _currentSpeed * Time.deltaTime;
|
||||
}
|
||||
@@ -214,12 +169,12 @@ public class FollowerController : Character
|
||||
{
|
||||
normalizedSpeed = _aiPath.velocity.magnitude / _followerMaxSpeed;
|
||||
// Sprite flipping for pathfinding mode
|
||||
if (spriteRenderer != null && _aiPath.velocity.sqrMagnitude > 0.001f)
|
||||
if (_spriteRenderer != null && _aiPath.velocity.sqrMagnitude > 0.001f)
|
||||
{
|
||||
if (_aiPath.velocity.x > 0.01f)
|
||||
spriteRenderer.flipX = false;
|
||||
_spriteRenderer.flipX = false;
|
||||
else if (_aiPath.velocity.x < -0.01f)
|
||||
spriteRenderer.flipX = true;
|
||||
_spriteRenderer.flipX = true;
|
||||
}
|
||||
}
|
||||
_animator.SetFloat("Speed", Mathf.Clamp01(normalizedSpeed));
|
||||
@@ -246,7 +201,44 @@ public class FollowerController : Character
|
||||
_playerAIPath = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#region Movement
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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)
|
||||
/// <summary>
|
||||
/// Command follower to go to a specific point (pathfinding mode).
|
||||
@@ -277,7 +269,99 @@ public class FollowerController : Character
|
||||
_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<SpriteRenderer>());
|
||||
_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<Pickup>();
|
||||
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<Pickup>().itemData;
|
||||
Destroy(pickupA.gameObject);
|
||||
Destroy(pickupB.gameObject);
|
||||
TryPickupItem(newItem,itemData);
|
||||
return CombinationResult.Successful;
|
||||
}
|
||||
|
||||
// If no combination found, return Unsuccessful
|
||||
return CombinationResult.Unsuccessful;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the item held by the follower, copying all visual properties from the Pickup's SpriteRenderer.
|
||||
/// </summary>
|
||||
@@ -285,10 +369,10 @@ public class FollowerController : Character
|
||||
/// <param name="pickupRenderer">The SpriteRenderer from the Pickup to copy appearance from.</param>
|
||||
public void SetHeldItem(PickupItemData itemData, SpriteRenderer pickupRenderer = null)
|
||||
{
|
||||
_currentlyHeldItem = itemData;
|
||||
_currentlyHeldItemData = itemData;
|
||||
if (heldObjectRenderer != null)
|
||||
{
|
||||
if (_currentlyHeldItem != null && pickupRenderer != null)
|
||||
if (_currentlyHeldItemData != null && pickupRenderer != null)
|
||||
{
|
||||
AppleHillsUtils.CopySpriteRendererProperties(pickupRenderer, heldObjectRenderer);
|
||||
}
|
||||
@@ -299,16 +383,7 @@ public class FollowerController : Character
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <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;
|
||||
}
|
||||
|
||||
|
||||
public GameObject GetHeldPickupObject()
|
||||
{
|
||||
return _cachedPickupObject;
|
||||
@@ -336,7 +411,7 @@ public class FollowerController : Character
|
||||
public void ClearHeldItem()
|
||||
{
|
||||
_cachedPickupObject = null;
|
||||
_currentlyHeldItem = null;
|
||||
_currentlyHeldItemData = null;
|
||||
if (heldObjectRenderer != null)
|
||||
{
|
||||
heldObjectRenderer.sprite = null;
|
||||
@@ -344,83 +419,26 @@ public class FollowerController : Character
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
InteractionOrchestrator.Instance.DropItem(this, position);
|
||||
}
|
||||
|
||||
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: orchestrator handles slotting
|
||||
break;
|
||||
}
|
||||
if (justCombined)
|
||||
{
|
||||
InteractionOrchestrator.Instance.CombineItems(pickup.gameObject, _cachedPickupObject);
|
||||
justCombined = false;
|
||||
break;
|
||||
}
|
||||
// Swap logic: if holding an item, drop it here
|
||||
if (_currentlyHeldItem != null && _cachedPickupObject != null)
|
||||
{
|
||||
InteractionOrchestrator.Instance.DropItem(this, pickup.transform.position);
|
||||
}
|
||||
InteractionOrchestrator.Instance.PickupItem(this, 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;
|
||||
DropItem(this, position);
|
||||
}
|
||||
|
||||
|
||||
#endregion ItemInteractions
|
||||
|
||||
#if UNITY_EDITOR
|
||||
void OnDrawGizmos()
|
||||
{
|
||||
if (debugDrawTarget && Application.isPlaying)
|
||||
@@ -431,4 +449,5 @@ public class FollowerController : Character
|
||||
Gizmos.DrawLine(transform.position, _targetPoint);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user