using UnityEngine; using System.Collections; using System.Collections.Generic; 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.")] public float playerRotationSpeed = 10f; [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, will try to find a SkinnedMeshRenderer first, then MeshRenderer. If false, affects all renderers.")] public bool preferSkinnedMeshRenderer = true; public vFSMBehaviourController CurrentTarget { get; private set; } public event Action OnTargetSelected; public event Action OnTargetDeselected; private GameStateManager _gameStateManager; private Coroutine _targetingLoopCoroutine; private Dictionary _originalMaterialColors = new Dictionary(); private Dictionary _materialToFadeCoroutineMap = new Dictionary(); private Transform _playerTransform; void Start() { if (Player.Instance == null) // Player.Instance should be set by its own Awake { 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); // Initialize based on current state } 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 // State is NORMAL or other non-combat { if (_targetingLoopCoroutine != null) { StopCoroutine(_targetingLoopCoroutine); _targetingLoopCoroutine = null; } if (CurrentTarget != null) // If there was a target, deselect it { vFSMBehaviourController oldTarget = CurrentTarget; SetNewTarget(null); // This will handle fade out and event } } } private IEnumerator TargetingLoop() { while (true) { if (_playerTransform == null && Player.Instance != null) // Defensive: re-cache if player was respawned or similar { _playerTransform = Player.Instance.transform; } if (_playerTransform != null) // Only proceed if we have a valid player transform { UpdateTarget(); } yield return new WaitForSeconds(targetingInterval); } } /// /// Checks if the given AI target is within the specified angle from the source transform's forward direction. /// 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; // Consider only horizontal angle // If target is effectively at the same horizontal position, consider it in angle. 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 combatControllers = _gameStateManager.GetActiveCombatcontrollers(); if (combatControllers == null || combatControllers.Count == 0) { if (CurrentTarget != null) SetNewTarget(null); // No enemies, clear current return; } foreach (var controller in combatControllers) { if (controller == null || !controller.gameObject.activeInHierarchy || controller.aiController.currentHealth <= 0) { continue; } // Check 1: Is target within selection angle? if (!IsTargetInAngle(_playerTransform, controller, targetingAngleThreshold)) { continue; } // Check 2: Is target within selection distance? float distSqr = (controller.transform.position - _playerTransform.position).sqrMagnitude; if (distSqr <= minDistanceSqr) // distSqr must also be <= maxTargetingDistance^2 due to minDistanceSqr initialization { // If multiple targets are at similar very close distances, this might pick the "last one" processed. // For more refined "closest", ensure this is strictly '<' for new candidates, // or add a secondary sort criterion if multiple are at exact same minDistanceSqr. // Current logic is fine for most cases. minDistanceSqr = distSqr; bestCandidate = controller; } } if (CurrentTarget != bestCandidate) { SetNewTarget(bestCandidate); } else if (CurrentTarget != null && !IsTargetValid(CurrentTarget)) // Current target became invalid (e.g. died, moved out of range/angle) { SetNewTarget(null); } } /// /// Checks if a given target is still valid according to AutoTargetting's rules. /// private bool IsTargetValid(vFSMBehaviourController target) { if (target == null || !target.gameObject.activeInHierarchy || target.aiController.currentHealth <= 0) return false; if (_playerTransform == null) return false; // Should not happen if script is active // Check 1: Angle (using AutoTargetting's own targetingAngleThreshold) if (!IsTargetInAngle(_playerTransform, target, targetingAngleThreshold)) return false; // Check 2: Distance (using AutoTargetting's own maxTargetingDistance) float distSqr = (target.transform.position - _playerTransform.position).sqrMagnitude; return distSqr <= maxTargetingDistance * maxTargetingDistance; } private void SetNewTarget(vFSMBehaviourController newTarget) { if (CurrentTarget == newTarget) return; // Deselect previous target if (CurrentTarget != null) { ApplyHighlight(CurrentTarget, false); // Fade out OnTargetDeselected?.Invoke(CurrentTarget); } CurrentTarget = newTarget; // Select new target if (CurrentTarget != null) { ApplyHighlight(CurrentTarget, true); // Fade in OnTargetSelected?.Invoke(CurrentTarget); } } /// /// Smoothly rotates the player character towards the CurrentTarget on the horizontal plane. /// 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; // Already at target or too close to get a direction directionToTarget.Normalize(); Quaternion targetRotation = Quaternion.LookRotation(directionToTarget); _playerTransform.rotation = Quaternion.Lerp(_playerTransform.rotation, targetRotation, deltaTime * playerRotationSpeed); } /// /// Gets the health of the CurrentTarget. /// /// The health of the CurrentTarget, or -1f if no target or target has no health component. 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(); // Attempt to find a new one if in combat } } private Renderer[] GetTargetRenderers(vFSMBehaviourController targetController) { if (targetController == null) return new Renderer[0]; if (preferSkinnedMeshRenderer) { SkinnedMeshRenderer smr = targetController.GetComponentInChildren(); if (smr != null) return new Renderer[] { smr }; MeshRenderer mr = targetController.GetComponentInChildren(); if (mr != null) return new Renderer[] { mr }; } return targetController.GetComponentsInChildren(true); // Include inactive renderers if any } 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; // Use rend.materials to get instances for modification foreach (Material mat in rend.materials) { if (mat == null || !mat.HasProperty(materialHighlightPropertyName)) continue; // Stop any existing fade coroutine for this specific material if (_materialToFadeCoroutineMap.TryGetValue(mat, out Coroutine existingCoroutine) && existingCoroutine != null) { StopCoroutine(existingCoroutine); } Color currentColor = mat.GetColor(materialHighlightPropertyName); Color targetColor = fadeIn ? highlightColor : deselectHighlightColor; // Smartly store original color only if not already a highlight/deselect color. if (fadeIn) { if (!_originalMaterialColors.ContainsKey(mat) || (_originalMaterialColors[mat] != currentColor && currentColor != deselectHighlightColor && currentColor != highlightColor) ) { _originalMaterialColors[mat] = currentColor; } } // When fading out, if an original was stored, we could potentially use it instead of always deselectHighlightColor. // However, for a consistent "off" state, deselectHighlightColor (e.g., black) is usually desired. // If fading out and original exists and isn't black: // else if (_originalMaterialColors.TryGetValue(mat, out Color original) && original != deselectHighlightColor) // { // targetColor = original; // Fade back to true original // } 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; // yield return null; // Not strictly necessary here as StopCoroutine handles overlaps. while (timer < duration) { if (material == null) yield break; // Material might have been destroyed timer += Time.deltaTime; float progress = Mathf.Clamp01(timer / duration); material.SetColor(materialHighlightPropertyName, Color.Lerp(fromValue, toValue, progress)); yield return null; } if (material != null) // Final set { material.SetColor(materialHighlightPropertyName, toValue); } // Optional: Remove from map if coroutine completed naturally and is still the one in the map. // if (_materialToFadeCoroutineMap.TryGetValue(material, out Coroutine currentMappedCoroutine) && currentMappedCoroutine == /*this specific instance, tricky to get*/) // { // _materialToFadeCoroutineMap.Remove(material); // } } }