@@ -1,6 +1,7 @@
using UnityEngine ;
using Pathfinding ;
using UnityEngine.SceneManagement ;
using Utils ;
/// <summary>
/// Controls the follower character, including following the player, handling pickups, and managing held items.
@@ -18,38 +19,28 @@ public class FollowerController : MonoBehaviour
/// </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 Animat or animat or;
private Transform artTransform ;
[Header(" Held Item")]
/// <summary>
/// The item currently held by the follower.
/// </summary>
public PickupItemData currentlyHeldItem ;
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 Transf orm _artTransf orm ;
private PickupItemData _currentlyHeldItem ;
public PickupItemData CurrentlyHeldItem = > _currently HeldItem;
/// <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 play erMaxSpeed = 5 f;
private float followerMaxSpeed = 6f ;
private float defaultFollowerMaxSpeed = 6f ;
private bool _ isReturningToPlayer = false ;
private float _playerMaxSpeed = 5f ;
private float _followerMaxSpeed = 6f ;
private float _defaultFollow erMaxSpeed = 6 f;
// Pickup events
public delegate void FollowerPickupHandler ( ) ;
@@ -61,14 +52,14 @@ public class FollowerController : MonoBehaviour
/// Event fired when the follower returns to the player after a pickup.
/// </summary>
public event FollowerPickupHandler OnPickupReturned ;
private Coroutine pickupCoroutine ;
private Coroutine _ pickupCoroutine;
private bool lastInteractionSuccess = true ;
private bool _ lastInteractionSuccess = true ;
/// <summary>
/// Cache for the currently picked-up GameObject (hidden while held).
/// </summary>
private GameObject cachedPickupObject = null ;
private GameObject _ cachedPickupObject = null ;
/// <summary>
/// Set to true if the follower just combined items.
@@ -77,16 +68,16 @@ public class FollowerController : MonoBehaviour
void Awake ( )
{
aiPath = GetComponent < AIPath > ( ) ;
_ aiPath = GetComponent < AIPath > ( ) ;
// Find art prefab and animator
artTransform = transform . Find ( "CharacterArt" ) ;
if ( artTransform ! = null )
_ artTransform = transform . Find ( "CharacterArt" ) ;
if ( _ artTransform ! = null )
{
animator = artTransform . GetComponent < Animator > ( ) ;
_ animator = _ artTransform. GetComponent < Animator > ( ) ;
}
else
{
animator = GetComponentInChildren < Animator > ( ) ; // fallback
_ animator = GetComponentInChildren < Animator > ( ) ; // fallback
}
}
@@ -108,102 +99,102 @@ public class FollowerController : MonoBehaviour
void UpdateFollowTarget ( )
{
if ( playerTransform = = null )
if ( _ playerTransform = = null )
{
FindPlayerReference ( ) ;
if ( playerTransform = = null )
if ( _ playerTransform = = null )
return ;
}
if ( isManualFollowing )
if ( _ isManualFollowing)
{
Vector3 playerPos = playerTransform . position ;
Vector3 playerPos = _ playerTransform. position ;
Vector3 moveDir = Vector3 . zero ;
if ( playerAIPath ! = null & & playerAIPath . velocity . magnitude > 0.01f )
if ( _ playerAIPath ! = null & & _ playerAIPath. velocity . magnitude > 0.01f )
{
moveDir = playerAIPath . velocity . normalized ;
lastMoveDir = moveDir ;
moveDir = _ playerAIPath. velocity . normalized ;
_ lastMoveDir = moveDir ;
}
else
{
moveDir = lastMoveDir ;
moveDir = _ lastMoveDir;
}
// Use GameSettings for followDistance
targetPoint = playerPos - moveDir * GameManager . Instance . FollowDistance ;
targetPoint . z = 0 ;
if ( aiPath ! = null )
_ targetPoint = playerPos - moveDir * GameManager . Instance . FollowDistance ;
_ targetPoint. z = 0 ;
if ( _ aiPath ! = null )
{
aiPath . enabled = false ;
_ aiPath. enabled = false ;
}
}
}
void Update ( )
{
if ( playerTransform = = null )
if ( _ playerTransform = = null )
{
FindPlayerReference ( ) ;
if ( playerTransform = = null )
if ( _ playerTransform = = null )
return ;
}
timer + = Time . deltaTime ;
if ( timer > = GameManager . Instance . FollowUpdateInterval )
_ timer + = Time . deltaTime ;
if ( _ timer > = GameManager . Instance . FollowUpdateInterval )
{
timer = 0f ;
_ timer = 0f ;
UpdateFollowTarget ( ) ;
}
if ( isManualFollowing )
if ( _ isManualFollowing)
{
Vector2 current2D = new Vector2 ( transform . position . x , transform . position . y ) ;
Vector2 target2D = new Vector2 ( targetPoint . x , targetPoint . y ) ;
Vector2 target2D = new Vector2 ( _ targetPoint. x , _ targetPoint. y ) ;
float dist = Vector2 . Distance ( current2D , target2D ) ;
float minSpeed = followerMaxSpeed * 0.3f ;
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 ;
targetSpeed = _ followerMaxSpeed;
}
else if ( dist > GameManager . Instance . ThresholdNear & & dist < = GameManager . Instance . ThresholdFar )
{
targetSpeed = followerMaxSpeed ;
targetSpeed = _ followerMaxSpeed;
}
else if ( dist > GameManager . Instance . StopThreshold & & dist < = GameManager . Instance . ThresholdNear )
{
targetSpeed = minSpeed ;
}
currentSpeed = Mathf . Lerp ( currentSpeed , targetSpeed , lerpFactor ) ;
_ currentSpeed = Mathf . Lerp ( _ currentSpeed, targetSpeed , lerpFactor ) ;
if ( dist > GameManager . Instance . StopThreshold & & dist < = GameManager . Instance . ThresholdNear )
{
currentSpeed = Mathf . Max ( currentSpeed , minSpeed ) ;
_ currentSpeed = Mathf . Max ( _ currentSpeed, minSpeed ) ;
}
Vector3 dir = ( targetPoint - transform . position ) . normalized ;
transform . position + = dir * currentSpeed * Time . deltaTime ;
Vector3 dir = ( _ targetPoint - transform . position ) . normalized ;
transform . position + = dir * _ currentSpeed * Time . deltaTime ;
}
else
{
currentSpeed = 0f ;
_ currentSpeed = 0f ;
}
}
if ( isReturningToPlayer & & aiPath ! = null & & aiPath . enabled & & playerTransform ! = null )
if ( _ isReturningToPlayer & & _ aiPath ! = null & & _ aiPath. enabled & & _ playerTransform ! = null )
{
aiPath . destination = playerTransform . position ;
_ aiPath. destination = _ playerTransform. position ;
}
if ( animator ! = null )
if ( _ animator ! = null )
{
float normalizedSpeed = 0f ;
if ( isManualFollowing )
if ( _ isManualFollowing)
{
normalizedSpeed = currentSpeed / followerMaxSpeed ;
normalizedSpeed = _ currentSpeed / _ followerMaxSpeed;
}
else if ( aiPath ! = null )
else if ( _ aiPath ! = null )
{
normalizedSpeed = aiPath . velocity . magnitude / followerMaxSpeed ;
normalizedSpeed = _ aiPath. velocity . magnitude / _ followerMaxSpeed;
}
animator . SetFloat ( "Speed" , Mathf . Clamp01 ( normalizedSpeed ) ) ;
_ animator. SetFloat ( "Speed" , Mathf . Clamp01 ( normalizedSpeed ) ) ;
}
}
@@ -212,19 +203,19 @@ public class FollowerController : MonoBehaviour
GameObject playerObj = GameObject . FindGameObjectWithTag ( "Player" ) ;
if ( playerObj ! = null )
{
playerTransform = playerObj . transform ;
playerAIPath = playerObj . GetComponent < AIPath > ( ) ;
if ( playerAIPath ! = null )
_ playerTransform = playerObj . transform ;
_ playerAIPath = playerObj . GetComponent < AIPath > ( ) ;
if ( _ playerAIPath ! = null )
{
playerMaxSpeed = playerAIPath . maxSpeed ;
defaultFollowerMaxSpeed = playerMaxSpeed ;
followerMaxSpeed = playerMaxSpeed * GameManager . Instance . FollowerSpeedMultiplier ;
_ playerMaxSpeed = _ playerAIPath. maxSpeed ;
_ defaultFollowerMaxSpeed = _ playerMaxSpeed;
_ followerMaxSpeed = _ playerMaxSpeed * GameManager . Instance . FollowerSpeedMultiplier ;
}
}
else
{
playerTransform = null ;
playerAIPath = null ;
_ playerTransform = null ;
_ playerAIPath = null ;
}
}
@@ -235,12 +226,12 @@ public class FollowerController : MonoBehaviour
/// <param name="worldPosition">The world position to move to.</param>
public void GoToPoint ( Vector2 worldPosition )
{
isManualFollowing = false ;
if ( aiPath ! = null )
_ isManualFollowing = false ;
if ( _ aiPath ! = null )
{
aiPath . enabled = true ;
aiPath . maxSpeed = followerMaxSpeed ;
aiPath . destination = new Vector3 ( worldPosition . x , worldPosition . y , 0 ) ;
_ aiPath. enabled = true ;
_ aiPath. maxSpeed = _ followerMaxSpeed;
_ aiPath. destination = new Vector3 ( worldPosition . x , worldPosition . y , 0 ) ;
}
}
@@ -252,36 +243,26 @@ public class FollowerController : MonoBehaviour
/// <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 ) ) ;
if ( _ pickupCoroutine ! = null )
StopCoroutine ( _ pickupCoroutine) ;
if ( _ aiPath ! = null )
_ aiPath. maxSpeed = _ followerMaxSpeed;
_ pickupCoroutine = StartCoroutine ( PickupSequence ( itemPosition , playerTransform ) ) ;
}
/// <summary>
/// Set the item held by the follower.
/// Set the item held by the follower, copying all visual properties from the Pickup's SpriteRenderer .
/// </summary>
/// <param name="itemData">The item data to set.</param>
public void SetHeldItem ( PickupItemData itemData )
/// <param name="pickupRenderer">The SpriteRenderer from the Pickup to copy appearance from.</param>
public void SetHeldItem ( PickupItemData itemData , SpriteRenderer pickupRenderer = null )
{
currentlyHeldItem = itemData ;
_ currentlyHeldItem = itemData ;
if ( heldObjectRenderer ! = null )
{
if ( currentlyHeldItem ! = null & & currentlyHeldItem . mapSprite ! = null )
if ( _ currentlyHeldItem ! = null & & pickupRenderer ! = 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 ) ;
}
AppleHillsUtils . CopySpriteRendererProperties ( pickupRenderer , heldObjectRenderer ) ;
}
else
{
@@ -297,18 +278,18 @@ public class FollowerController : MonoBehaviour
/// <param name="success">True if the last interaction was successful, false otherwise.</param>
public void SetInteractionResult ( bool success )
{
lastInteractionSuccess = success ;
_ lastInteractionSuccess = success ;
}
private System . Collections . IEnumerator PickupSequence ( Vector2 itemPosition , Transform playerTransform )
{
isManualFollowing = false ;
isReturningToPlayer = false ;
if ( aiPath ! = null )
_ isManualFollowing = false ;
_ isReturningToPlayer = false ;
if ( _ aiPath ! = null )
{
aiPath . enabled = true ;
aiPath . maxSpeed = followerMaxSpeed ;
aiPath . destination = new Vector3 ( itemPosition . x , itemPosition . y , 0 ) ;
_ 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 )
@@ -317,7 +298,7 @@ public class FollowerController : MonoBehaviour
}
OnPickupArrived ? . Invoke ( ) ;
// Only perform pickup/swap logic if interaction succeeded
if ( lastInteractionSuccess & & heldObjectRenderer ! = null )
if ( _ lastInteractionSuccess & & heldObjectRenderer ! = null )
{
Collider2D [ ] hits = Physics2D . OverlapCircleAll ( itemPosition , 0.2f ) ;
foreach ( var hit in hits )
@@ -334,56 +315,56 @@ public class FollowerController : MonoBehaviour
if ( justCombined )
{
// Combination: just destroy the pickup, don't spawn anything
if ( cachedPickupObject ! = null )
if ( _ cachedPickupObject ! = null )
{
Destroy ( cachedPickupObject ) ;
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 )
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 ;
_ cachedPickupObject. transform . position = pickup . transform . position ;
_ cachedPickupObject. transform . SetParent ( null ) ;
_ cachedPickupObject. SetActive ( true ) ;
_ cachedPickupObject = null ;
}
SetHeldItem ( pickup . itemData ) ;
SetHeldItem ( pickup . itemData , pickup . iconRenderer );
// Cache and hide the picked up object
cachedPickupObject = pickup . gameObject ;
cachedPickupObject . SetActive ( false ) ;
cachedPickupObject . transform . SetParent ( this . transform ) ;
_ 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 )
if ( _ aiPath ! = null & & playerTransform ! = null )
{
aiPath . maxSpeed = followerMaxSpeed ;
aiPath . destination = playerTransform . position ;
_ aiPath. maxSpeed = _ followerMaxSpeed;
_ aiPath. destination = playerTransform . position ;
}
isReturningToPlayer = true ;
_ 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 ;
_ 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 ;
_ followerMaxSpeed = _ defaultFollowerMaxSpeed;
if ( _ aiPath ! = null )
_ aiPath. maxSpeed = _ followerMaxSpeed;
_ isManualFollowing = true ;
if ( _ aiPath ! = null )
_ aiPath. enabled = false ;
_ pickupCoroutine = null ;
}
void OnDrawGizmos ( )
@@ -391,9 +372,9 @@ public class FollowerController : MonoBehaviour
if ( debugDrawTarget & & Application . isPlaying )
{
Gizmos . color = Color . cyan ;
Gizmos . DrawSphere ( targetPoint , 0.2f ) ;
Gizmos . DrawSphere ( _ targetPoint, 0.2f ) ;
Gizmos . color = Color . yellow ;
Gizmos . DrawLine ( transform . position , targetPoint ) ;
Gizmos . DrawLine ( transform . position , _ targetPoint) ;
}
}
}