359 lines
13 KiB
C#
359 lines
13 KiB
C#
using UnityEngine;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Linq; // Required for List.AddRange and ToArray
|
|
using Invector.vCharacterController.AI.FSMBehaviour; // For vFSMBehaviourController
|
|
using Beyond; // For GameStateManager and Player (ensure Player.cs is in this namespace or adjust)
|
|
using System; // For Action
|
|
|
|
public class AutoTargetting : MonoBehaviour
|
|
{
|
|
[Header("Targeting Parameters")]
|
|
[Tooltip("Maximum distance AutoTargetting will consider an enemy for selection.")]
|
|
public float maxTargetingDistance = 20f;
|
|
[Tooltip("How often (in seconds) to re-evaluate for a new target during combat.")]
|
|
public float targetingInterval = 0.25f;
|
|
[Tooltip("Maximum angle (in degrees from player's forward) within which an enemy can be auto-targeted.")]
|
|
public float targetingAngleThreshold = 90f;
|
|
|
|
[Header("Rotation Parameters")]
|
|
[Tooltip("Speed at which the player rotates towards the current target when rotation is explicitly called (e.g., by MagicAttacks).")]
|
|
public float playerRotationSpeed = 10f; // This will be used by MagicAttacks
|
|
|
|
[Header("Visuals")]
|
|
[Tooltip("Name of the material color property to animate for Fresnel effect.")]
|
|
public string materialHighlightPropertyName = "_FresnelColor";
|
|
[Tooltip("HDR Color to use for Fresnel highlight when a target is selected (fade-in target).")]
|
|
[ColorUsage(true, true)]
|
|
public Color highlightColor = Color.white;
|
|
[Tooltip("HDR Color to use for Fresnel when a target is deselected (fade-out target).")]
|
|
[ColorUsage(true, true)]
|
|
public Color deselectHighlightColor = Color.black;
|
|
[Tooltip("Duration of the fade in/out animation for the highlight.")]
|
|
public float highlightFadeDuration = 0.3f;
|
|
[Tooltip("If true, was previously used to prefer SkinnedMeshRenderer. Now GetTargetRenderers collects both SkinnedMeshRenderers and MeshRenderers regardless of this flag. This flag might be repurposed or removed in the future.")]
|
|
public bool preferSkinnedMeshRenderer = true; // Note: Its effect on GetTargetRenderers is changed.
|
|
|
|
public vFSMBehaviourController CurrentTarget { get; private set; }
|
|
public event Action<vFSMBehaviourController> OnTargetSelected;
|
|
public event Action<vFSMBehaviourController> OnTargetDeselected;
|
|
|
|
private GameStateManager _gameStateManager;
|
|
private Coroutine _targetingLoopCoroutine;
|
|
private Dictionary<Material, Color> _originalMaterialColors = new Dictionary<Material, Color>();
|
|
private Dictionary<Material, Coroutine> _materialToFadeCoroutineMap = new Dictionary<Material, Coroutine>();
|
|
private Transform _playerTransform;
|
|
|
|
void Start()
|
|
{
|
|
if (Player.Instance == null)
|
|
{
|
|
Debug.LogError("AutoTargetting: Player.Instance is not available at Start! Ensure Player script with static Instance exists and runs before AutoTargetting.");
|
|
enabled = false;
|
|
return;
|
|
}
|
|
_playerTransform = Player.Instance.transform;
|
|
|
|
_gameStateManager = GameStateManager.Instance;
|
|
if (_gameStateManager != null)
|
|
{
|
|
_gameStateManager.m_OnStateChanged.AddListener(HandleGameStateChanged);
|
|
HandleGameStateChanged(_gameStateManager.CurrentState);
|
|
}
|
|
else
|
|
{
|
|
Debug.LogError("AutoTargetting: GameStateManager.Instance not found! Disabling script.");
|
|
enabled = false;
|
|
}
|
|
}
|
|
|
|
void OnDestroy()
|
|
{
|
|
if (_gameStateManager != null)
|
|
{
|
|
_gameStateManager.m_OnStateChanged.RemoveListener(HandleGameStateChanged);
|
|
}
|
|
StopAndClearAllFadeCoroutines();
|
|
if (_targetingLoopCoroutine != null)
|
|
{
|
|
StopCoroutine(_targetingLoopCoroutine);
|
|
_targetingLoopCoroutine = null;
|
|
}
|
|
}
|
|
|
|
private void StopAndClearAllFadeCoroutines()
|
|
{
|
|
foreach (var pair in _materialToFadeCoroutineMap)
|
|
{
|
|
if (pair.Value != null) StopCoroutine(pair.Value);
|
|
}
|
|
_materialToFadeCoroutineMap.Clear();
|
|
}
|
|
|
|
private void HandleGameStateChanged(GameStateManager.State newState)
|
|
{
|
|
if (newState == GameStateManager.State.COMBAT)
|
|
{
|
|
if (_targetingLoopCoroutine == null)
|
|
{
|
|
_targetingLoopCoroutine = StartCoroutine(TargetingLoop());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (_targetingLoopCoroutine != null)
|
|
{
|
|
StopCoroutine(_targetingLoopCoroutine);
|
|
_targetingLoopCoroutine = null;
|
|
}
|
|
if (CurrentTarget != null)
|
|
{
|
|
SetNewTarget(null);
|
|
}
|
|
}
|
|
}
|
|
|
|
private IEnumerator TargetingLoop()
|
|
{
|
|
while (true)
|
|
{
|
|
if (_playerTransform == null && Player.Instance != null)
|
|
{
|
|
_playerTransform = Player.Instance.transform;
|
|
}
|
|
|
|
if (_playerTransform != null)
|
|
{
|
|
UpdateTarget();
|
|
}
|
|
yield return new WaitForSeconds(targetingInterval);
|
|
}
|
|
}
|
|
|
|
public bool IsTargetInAngle(Transform sourceTransform, vFSMBehaviourController targetAI, float angleThreshold)
|
|
{
|
|
if (targetAI == null || sourceTransform == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
Vector3 directionToTarget = (targetAI.transform.position - sourceTransform.position);
|
|
directionToTarget.y = 0;
|
|
|
|
if (directionToTarget.sqrMagnitude < 0.0001f) return true;
|
|
|
|
directionToTarget.Normalize();
|
|
float angle = Vector3.Angle(sourceTransform.forward, directionToTarget);
|
|
return angle <= angleThreshold;
|
|
}
|
|
|
|
private void UpdateTarget()
|
|
{
|
|
if (_playerTransform == null || _gameStateManager == null) return;
|
|
|
|
vFSMBehaviourController bestCandidate = null;
|
|
float minDistanceSqr = maxTargetingDistance * maxTargetingDistance;
|
|
|
|
HashSet<vFSMBehaviourController> combatControllers = _gameStateManager.GetActiveCombatcontrollers();
|
|
|
|
if (combatControllers == null || combatControllers.Count == 0)
|
|
{
|
|
if (CurrentTarget != null) SetNewTarget(null);
|
|
return;
|
|
}
|
|
|
|
foreach (var controller in combatControllers)
|
|
{
|
|
if (controller == null || !controller.gameObject.activeInHierarchy || controller.aiController.currentHealth <= 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!IsTargetInAngle(_playerTransform, controller, targetingAngleThreshold))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
float distSqr = (controller.transform.position - _playerTransform.position).sqrMagnitude;
|
|
if (distSqr <= minDistanceSqr)
|
|
{
|
|
minDistanceSqr = distSqr;
|
|
bestCandidate = controller;
|
|
}
|
|
}
|
|
|
|
if (CurrentTarget != bestCandidate)
|
|
{
|
|
SetNewTarget(bestCandidate);
|
|
}
|
|
else if (CurrentTarget != null && !IsTargetValid(CurrentTarget))
|
|
{
|
|
SetNewTarget(null);
|
|
}
|
|
}
|
|
|
|
private bool IsTargetValid(vFSMBehaviourController target)
|
|
{
|
|
if (target == null || !target.gameObject.activeInHierarchy || target.aiController.currentHealth <= 0)
|
|
return false;
|
|
|
|
if (_playerTransform == null) return false;
|
|
|
|
if (!IsTargetInAngle(_playerTransform, target, targetingAngleThreshold))
|
|
return false;
|
|
|
|
float distSqr = (target.transform.position - _playerTransform.position).sqrMagnitude;
|
|
return distSqr <= maxTargetingDistance * maxTargetingDistance;
|
|
}
|
|
|
|
private void SetNewTarget(vFSMBehaviourController newTarget)
|
|
{
|
|
if (CurrentTarget == newTarget) return;
|
|
|
|
if (CurrentTarget != null)
|
|
{
|
|
ApplyHighlight(CurrentTarget, false);
|
|
OnTargetDeselected?.Invoke(CurrentTarget);
|
|
}
|
|
|
|
CurrentTarget = newTarget;
|
|
|
|
if (CurrentTarget != null)
|
|
{
|
|
ApplyHighlight(CurrentTarget, true);
|
|
OnTargetSelected?.Invoke(CurrentTarget);
|
|
}
|
|
}
|
|
|
|
public void ExecuteRotationTowardsCurrentTarget(float deltaTime)
|
|
{
|
|
if (CurrentTarget == null || _playerTransform == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Vector3 directionToTarget = CurrentTarget.transform.position - _playerTransform.position;
|
|
directionToTarget.y = 0f;
|
|
|
|
if (directionToTarget.sqrMagnitude < 0.0001f) return;
|
|
|
|
directionToTarget.Normalize();
|
|
Quaternion targetRotation = Quaternion.LookRotation(directionToTarget);
|
|
_playerTransform.rotation = Quaternion.Lerp(_playerTransform.rotation, targetRotation, deltaTime * playerRotationSpeed);
|
|
}
|
|
|
|
public float GetCurrentTargetHealth()
|
|
{
|
|
if (CurrentTarget != null && CurrentTarget.aiController != null)
|
|
{
|
|
return CurrentTarget.aiController.currentHealth;
|
|
}
|
|
return -1f;
|
|
}
|
|
|
|
public void ClearTarget(bool findNewOneImmediately)
|
|
{
|
|
SetNewTarget(null);
|
|
if (findNewOneImmediately && _gameStateManager != null && _gameStateManager.CurrentState == GameStateManager.State.COMBAT)
|
|
{
|
|
UpdateTarget();
|
|
}
|
|
}
|
|
|
|
// *** MODIFIED GetTargetRenderers METHOD ***
|
|
private Renderer[] GetTargetRenderers(vFSMBehaviourController targetController)
|
|
{
|
|
if (targetController == null) return new Renderer[0];
|
|
|
|
List<Renderer> collectedRenderers = new List<Renderer>();
|
|
|
|
// Collect all SkinnedMeshRenderers
|
|
SkinnedMeshRenderer[] smrs = targetController.GetComponentsInChildren<SkinnedMeshRenderer>(true);
|
|
if (smrs != null && smrs.Length > 0)
|
|
{
|
|
collectedRenderers.AddRange(smrs);
|
|
}
|
|
|
|
// Collect all MeshRenderers
|
|
MeshRenderer[] mrs = targetController.GetComponentsInChildren<MeshRenderer>(true);
|
|
if (mrs != null && mrs.Length > 0)
|
|
{
|
|
collectedRenderers.AddRange(mrs);
|
|
}
|
|
|
|
// If no specific SMRs or MRs were found, fall back to all Renderers (optional, but good for safety)
|
|
// Or, if the goal is *only* SMRs and MRs, this fallback can be removed.
|
|
// For now, let's assume we want to highlight *something* if possible.
|
|
// If you strictly want ONLY SMRs and MRs, and nothing else, remove this 'if' block.
|
|
if (collectedRenderers.Count == 0)
|
|
{
|
|
Renderer[] allRenderers = targetController.GetComponentsInChildren<Renderer>(true);
|
|
if (allRenderers != null && allRenderers.Length > 0)
|
|
{
|
|
collectedRenderers.AddRange(allRenderers);
|
|
}
|
|
}
|
|
|
|
// The preferSkinnedMeshRenderer flag is not directly used here for filtering types anymore.
|
|
// It could potentially be used for ordering or other logic if needed in the future.
|
|
|
|
return collectedRenderers.Distinct().ToArray(); // Distinct() to avoid duplicates if an object somehow has multiple relevant renderer types
|
|
}
|
|
|
|
|
|
private void ApplyHighlight(vFSMBehaviourController targetController, bool fadeIn)
|
|
{
|
|
if (targetController == null || string.IsNullOrEmpty(materialHighlightPropertyName)) return;
|
|
|
|
Renderer[] renderers = GetTargetRenderers(targetController);
|
|
|
|
foreach (Renderer rend in renderers)
|
|
{
|
|
if (rend == null) continue;
|
|
|
|
foreach (Material mat in rend.materials)
|
|
{
|
|
if (mat == null || !mat.HasProperty(materialHighlightPropertyName)) continue;
|
|
|
|
if (_materialToFadeCoroutineMap.TryGetValue(mat, out Coroutine existingCoroutine) && existingCoroutine != null)
|
|
{
|
|
StopCoroutine(existingCoroutine);
|
|
}
|
|
|
|
Color currentColor = mat.GetColor(materialHighlightPropertyName);
|
|
Color targetColor = fadeIn ? highlightColor : deselectHighlightColor;
|
|
|
|
if (fadeIn)
|
|
{
|
|
if (!_originalMaterialColors.ContainsKey(mat) ||
|
|
(_originalMaterialColors[mat] != currentColor && currentColor != deselectHighlightColor && currentColor != highlightColor) )
|
|
{
|
|
_originalMaterialColors[mat] = currentColor;
|
|
}
|
|
}
|
|
Coroutine newFadeCoroutine = StartCoroutine(FadeMaterialPropertyCoroutine(mat, currentColor, targetColor, highlightFadeDuration));
|
|
_materialToFadeCoroutineMap[mat] = newFadeCoroutine;
|
|
}
|
|
}
|
|
}
|
|
|
|
private IEnumerator FadeMaterialPropertyCoroutine(Material material, Color fromValue, Color toValue, float duration)
|
|
{
|
|
float timer = 0f;
|
|
|
|
while (timer < duration)
|
|
{
|
|
if (material == null) yield break;
|
|
|
|
timer += Time.deltaTime;
|
|
float progress = Mathf.Clamp01(timer / duration);
|
|
material.SetColor(materialHighlightPropertyName, Color.Lerp(fromValue, toValue, progress));
|
|
yield return null;
|
|
}
|
|
|
|
if (material != null)
|
|
{
|
|
material.SetColor(materialHighlightPropertyName, toValue);
|
|
}
|
|
}
|
|
} |