using UnityEngine; using System.Collections; using System.Collections.Generic; using System.Linq; using Invector.vCharacterController.AI.FSMBehaviour; using Beyond; using System; namespace Beyond { public class AutoTargetting : MonoBehaviour { #region Fields & Properties [Header("Targeting Parameters")] [Tooltip("The maximum distance to highlight a potential target.")] public float maxTargetingDistance = 20f; [Tooltip("The distance within which the system will automatically lock on to the highlighted target.")] public float autoLockOnDistance = 15f; [Tooltip("The distance at which a locked-on target will be automatically unlocked.")] public float unlockDistanceThreshold = 25f; public float targetingInterval = 0.25f; public float targetingAngleThreshold = 90f; [Header("Rotation Parameters")] public float playerRotationSpeed = 10f; [Header("Visuals")] public string materialHighlightPropertyName = "_FresnelColor"; [ColorUsage(true, true)] public Color highlightColor = Color.white; [ColorUsage(true, true)] public Color deselectHighlightColor = Color.black; public float highlightFadeDuration = 0.3f; public bool preferSkinnedMeshRenderer = true; [Header("Lock-On Integration")] public bool autoLockSelectedTarget = false; public bool alwaysLockOnInCombat = true; public bLockOn targetLockSystem; public float manualSwitchCooldownDuration = 0.75f; 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; private bThirdPersonController _playerController; private bool _manualSwitchCooldownActive = false; private float _manualSwitchCooldownTimer = 0f; #endregion #region Unity Methods void Start() { if (Player.Instance == null) { Debug.LogError("AutoTargetting: Player.Instance is not available at Start!"); enabled = false; return; } _playerTransform = Player.Instance.transform; _playerController = Player.Instance.GetComponent(); if (_playerController == null) { Debug.LogError("AutoTargetting: Could not find bThirdPersonController on Player.Instance! Custom roll rotation may not work correctly."); } _gameStateManager = GameStateManager.Instance; if (_gameStateManager != null) { _gameStateManager.m_OnStateChanged.AddListener(HandleGameStateChanged); HandleGameStateChanged(_gameStateManager.CurrentState); } else { Debug.LogError("AutoTargetting: GameStateManager.Instance not found!"); enabled = false; return; } if (targetLockSystem == null) { targetLockSystem = Player.Instance.GetComponentInChildren(true); if (targetLockSystem == null) { Debug.LogWarning("AutoTargetting: bLockOn system not found. Auto-lock will be disabled."); autoLockSelectedTarget = false; } } if (targetLockSystem != null) { targetLockSystem.onLockOnTarget.AddListener(HandleLockOnSystemTargetChanged); targetLockSystem.onUnLockOnTarget.AddListener(HandleLockOnSystemTargetUnlocked); } } void OnDestroy() { if (_gameStateManager != null) _gameStateManager.m_OnStateChanged.RemoveListener(HandleGameStateChanged); StopAndClearAllFadeCoroutines(); if (_targetingLoopCoroutine != null) StopCoroutine(_targetingLoopCoroutine); if (targetLockSystem != null) { targetLockSystem.onLockOnTarget.RemoveListener(HandleLockOnSystemTargetChanged); targetLockSystem.onUnLockOnTarget.RemoveListener(HandleLockOnSystemTargetUnlocked); } } #endregion #region Core Logic 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, true); _manualSwitchCooldownActive = false; } } private IEnumerator TargetingLoop() { while (true) { if (_manualSwitchCooldownActive) { _manualSwitchCooldownTimer -= targetingInterval; if (_manualSwitchCooldownTimer <= 0) _manualSwitchCooldownActive = false; } if (!_manualSwitchCooldownActive) { if (_playerTransform == null && Player.Instance != null) _playerTransform = Player.Instance.transform; if (_playerTransform != null) UpdateTarget(); } yield return new WaitForSeconds(targetingInterval); } } // REFACTORED: This method now has a clearer, more robust flow. private void UpdateTarget() { if (_playerTransform == null || _gameStateManager == null || _manualSwitchCooldownActive) return; // Step 1: Always find the absolute best candidate in range right now. vFSMBehaviourController bestCandidate = null; float minDistanceSqr = maxTargetingDistance * maxTargetingDistance; HashSet combatControllers = _gameStateManager.GetActiveCombatcontrollers(); if (combatControllers != null && combatControllers.Count > 0) { 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; } } } // Step 2: If the best candidate is different from our current one, switch the highlight. if (CurrentTarget != bestCandidate) { SetNewTarget(bestCandidate); } // Step 3: Every update, evaluate and apply the correct lock-on state for the current target. UpdateLockOnState(); } // NEW: This method exclusively handles the logic for locking on and off. private void UpdateLockOnState() { if (targetLockSystem == null || _playerTransform == null) return; // Determine if the target *should* be locked based on distance rules. bool shouldBeLocked = false; if (CurrentTarget != null && (autoLockSelectedTarget || alwaysLockOnInCombat)) { float distanceToTarget = Vector3.Distance(_playerTransform.position, CurrentTarget.transform.position); // This is a hysteresis check: use different distances for locking and unlocking to prevent flickering. if (targetLockSystem.isLockingOn) { // If already locked, stay locked unless we are beyond the unlock threshold. shouldBeLocked = distanceToTarget <= unlockDistanceThreshold; } else { // If not locked, we only engage the lock if we are within the auto-lock distance. shouldBeLocked = distanceToTarget <= autoLockOnDistance; } } // Synchronize the desired state with the lock-on system. Transform desiredLockTarget = shouldBeLocked ? CurrentTarget.transform : null; // The lock-on system should be smart enough to not do anything if the target hasn't changed. // We send the desired target (or null) every update. targetLockSystem.ManuallySetLockOnTarget(desiredLockTarget, true); if (alwaysLockOnInCombat && desiredLockTarget != null && !targetLockSystem.isLockingOn) { targetLockSystem.SetLockOn(true); } } // SIMPLIFIED: This method now only handles changing the CurrentTarget reference and its visual highlight. private void SetNewTarget(vFSMBehaviourController newTarget, bool forceLockSystemUpdate = false) { 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 (_playerController != null && !_playerController.enabled) { return; } if (CurrentTarget == null || _playerTransform == null) return; Vector3 directionToTarget = CurrentTarget.transform.position - _playerTransform.position; directionToTarget.y = 0f; if (directionToTarget.sqrMagnitude < 0.0001f) return; Quaternion targetRotation = Quaternion.LookRotation(directionToTarget.normalized); _playerTransform.rotation = Quaternion.Slerp(_playerTransform.rotation, targetRotation, deltaTime * playerRotationSpeed); } #endregion #region Lock-On System Sync private void HandleLockOnSystemTargetChanged(Transform lockedTargetTransform) { if (lockedTargetTransform == null || targetLockSystem == null) return; _manualSwitchCooldownActive = true; _manualSwitchCooldownTimer = manualSwitchCooldownDuration; vFSMBehaviourController fsmTarget = lockedTargetTransform.GetComponentInParent(); if (fsmTarget == null) fsmTarget = lockedTargetTransform.GetComponentInChildren(true); if (fsmTarget != null && CurrentTarget != fsmTarget) { SetNewTarget(fsmTarget, true); } else if (fsmTarget == null && CurrentTarget != null) { SetNewTarget(null, true); } } private void HandleLockOnSystemTargetUnlocked(Transform previouslyLockedTargetTransform) { if (targetLockSystem == null) return; _manualSwitchCooldownActive = true; _manualSwitchCooldownTimer = manualSwitchCooldownDuration; // After manually unlocking, we let the main UpdateTarget loop find the next best candidate. // If the previously locked target is still the closest, it will be re-highlighted automatically. } #endregion #region Helper Methods 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; return Vector3.Angle(sourceTransform.forward, directionToTarget.normalized) <= angleThreshold; } 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); } public float GetCurrentTargetHealth() { if (CurrentTarget != null && CurrentTarget.aiController != null) { return CurrentTarget.aiController.currentHealth; } return -1f; } public void ClearTarget(bool findNewOneImmediately) { // --- FIX APPLIED HERE --- // 1. Explicitly tell the lock-on system to unlock immediately. // This is the crucial step that resets the 'isLockingOn' flag to false. if (targetLockSystem != null) { targetLockSystem.SetLockOn(false); } // 2. Deselect the current target and remove its highlight. SetNewTarget(null); // 3. If requested, immediately find a new target. if (findNewOneImmediately && _gameStateManager != null && _gameStateManager.CurrentState == GameStateManager.State.COMBAT) { // Now, when UpdateTarget() runs, 'isLockingOn' will be correctly set to false, // forcing the system to use the stricter 'autoLockOnDistance' for the new candidate. if (!_manualSwitchCooldownActive) { UpdateTarget(); } } } #endregion #region Visuals private Renderer[] GetTargetRenderers(vFSMBehaviourController targetController) { if (targetController == null) return new Renderer[0]; return targetController.GetComponentsInChildren(true).Distinct().ToArray(); } 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; 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; material.SetColor(materialHighlightPropertyName, Color.Lerp(fromValue, toValue, Mathf.Clamp01(timer / duration))); yield return null; } if (material != null) material.SetColor(materialHighlightPropertyName, toValue); } #endregion } }