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 { // ... (other headers and variables) [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; [Header("Visuals")] // ... (visual parameters) 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")] [Tooltip("If true, automatically locks onto the target selected by AutoTargetting.")] public bool autoLockSelectedTarget = false; [Tooltip("Reference to the bLockOn script, usually on the player. Will try to find if not set.")] public bLockOn targetLockSystem; [Tooltip("How long (in seconds) AutoTargetting will pause after a manual target switch before re-evaluating.")] public float manualSwitchCooldownDuration = 0.75f; // Cooldown duration 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; // Cooldown variables private bool _manualSwitchCooldownActive = false; private float _manualSwitchCooldownTimer = 0f; void Start() { // ... (Start logic mostly the same) if (Player.Instance == null) { Debug.LogError("AutoTargetting: Player.Instance is not available at Start!"); 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!"); enabled = false; return; } if (targetLockSystem == null) { if (Player.Instance != null) targetLockSystem = Player.Instance.GetComponentInChildren(true); if (targetLockSystem == null) targetLockSystem = GetComponent(); // Fallback if (targetLockSystem == null) { Debug.LogWarning("AutoTargetting: bLockOn system not found. Auto-lock and target sync will be disabled."); autoLockSelectedTarget = false; } } if (targetLockSystem != null) { targetLockSystem.onLockOnTarget.AddListener(HandleLockOnSystemTargetChanged); targetLockSystem.onUnLockOnTarget.AddListener(HandleLockOnSystemTargetUnlocked); } } void OnDestroy() { // ... (Cleanup logic) if (_gameStateManager != null) { _gameStateManager.m_OnStateChanged.RemoveListener(HandleGameStateChanged); } StopAndClearAllFadeCoroutines(); if (_targetingLoopCoroutine != null) { StopCoroutine(_targetingLoopCoroutine); _targetingLoopCoroutine = null; } if (targetLockSystem != null) { targetLockSystem.onLockOnTarget.RemoveListener(HandleLockOnSystemTargetChanged); targetLockSystem.onUnLockOnTarget.RemoveListener(HandleLockOnSystemTargetUnlocked); } } 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); // Force immediate update if leaving combat } _manualSwitchCooldownActive = false; // Reset cooldown when leaving combat } } private IEnumerator TargetingLoop() { while (true) { if (_manualSwitchCooldownActive) { _manualSwitchCooldownTimer -= targetingInterval; // Or Time.deltaTime if loop is faster if (_manualSwitchCooldownTimer <= 0) { _manualSwitchCooldownActive = false; // Debug.Log("AutoTargetting: Manual switch cooldown ended."); } } if (!_manualSwitchCooldownActive) // Only update target if cooldown is not active { 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) { // ... (IsTargetInAngle logic - unchanged) 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; if (_manualSwitchCooldownActive) return; // Double check, though TargetingLoop should handle it vFSMBehaviourController bestCandidate = null; float minDistanceSqr = maxTargetingDistance * maxTargetingDistance; HashSet 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) { // ... (IsTargetValid logic - unchanged) 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); } /// /// Sets a new target for AutoTargetting. /// /// The new target. Can be null. /// If true, ManuallySetLockOnTarget will be called even if newTarget is same as CurrentTarget (used for re-assertion). private void SetNewTarget(vFSMBehaviourController newTarget, bool forceLockSystemUpdate = false) { if (_manualSwitchCooldownActive && !forceLockSystemUpdate) // Don't change target if cooldown is active, unless forced { // If cooldown is active and this SetNewTarget call is NOT from a HandleLockOnSystem... event, // it means AutoTargeting's own loop tried to change target. We prevent this. // If it IS from HandleLockOnSystem... it means bLockOn initiated, and we need to sync. // The 'isManualSwitch' parameter in HandleLockOnSystemTargetChanged handles activating cooldown. return; } if (CurrentTarget == newTarget && !forceLockSystemUpdate) { // If auto-lock is enabled, ensure the bLockOn system is actually locked on this target. if (autoLockSelectedTarget && targetLockSystem != null && CurrentTarget != null) { if (targetLockSystem.GetCurrentLockOnTarget() != CurrentTarget.transform) { targetLockSystem.ManuallySetLockOnTarget(CurrentTarget.transform, true); } } return; } if (CurrentTarget != null) { ApplyHighlight(CurrentTarget, false); OnTargetDeselected?.Invoke(CurrentTarget); if (autoLockSelectedTarget && targetLockSystem != null && targetLockSystem.GetCurrentLockOnTarget() == CurrentTarget.transform && (newTarget == null || newTarget.transform != CurrentTarget.transform)) { targetLockSystem.ManuallySetLockOnTarget(null, false); } } CurrentTarget = newTarget; if (CurrentTarget != null) { ApplyHighlight(CurrentTarget, true); OnTargetSelected?.Invoke(CurrentTarget); if (autoLockSelectedTarget && targetLockSystem != null) { targetLockSystem.ManuallySetLockOnTarget(CurrentTarget.transform, true); } } else // CurrentTarget is now null { // Deselection logic above handles unlocking if autoLockSelectedTarget was true // and if bLockOn was on the CurrentTarget that just became null. } } private void HandleLockOnSystemTargetChanged(Transform lockedTargetTransform) { if (lockedTargetTransform == null || targetLockSystem == null) return; // This event means bLockOn changed target (e.g., player pressed Next/Previous) // Activate the cooldown for AutoTargetting. _manualSwitchCooldownActive = true; _manualSwitchCooldownTimer = manualSwitchCooldownDuration; // Debug.Log($"AutoTargetting: Manual switch detected via bLockOn. Cooldown activated for {manualSwitchCooldownDuration}s."); vFSMBehaviourController fsmTarget = lockedTargetTransform.GetComponentInParent(); if (fsmTarget == null) fsmTarget = lockedTargetTransform.GetComponentInChildren(true); if (CurrentTarget != null && CurrentTarget.transform == lockedTargetTransform) { // AutoTargetting was already on this target, but bLockOn re-confirmed. // Cooldown is still active due to bLockOn's event. return; } if (fsmTarget != null) { // Debug.Log($"AutoTargeting: Syncing to bLockOn's new target: {fsmTarget.name}."); // We need to update AutoTargetting's CurrentTarget to match bLockOn, // even during cooldown, because bLockOn IS the source of truth now. // The 'forceLockSystemUpdate = true' ensures highlights are updated correctly. SetNewTarget(fsmTarget, true); } else { // bLockOn locked something without an FSM, or unlocked. // If AutoTargeting had a target, it should now clear it to sync. // Debug.LogWarning($"AutoTargeting: bLockOn locked {lockedTargetTransform.name} (no FSM) or unlocked. Clearing AutoTarget."); SetNewTarget(null, true); } } private void HandleLockOnSystemTargetUnlocked(Transform previouslyLockedTargetTransform) { if (targetLockSystem == null) return; // This event means bLockOn explicitly unlocked (e.g., player pressed Tab again). // If player manually unlocks, we should respect that and also stop AutoTargetting's autoLock behavior for a bit. // Or, if autoLockSelectedTarget is false, this is just a notification. _manualSwitchCooldownActive = true; // Treat manual unlock like a manual switch _manualSwitchCooldownTimer = manualSwitchCooldownDuration; // Debug.Log($"AutoTargeting: Manual unlock detected via bLockOn. Cooldown activated."); string targetName = previouslyLockedTargetTransform ? previouslyLockedTargetTransform.name : "an unknown target"; // Debug.Log($"AutoTargeting: bLockOn unlocked from '{targetName}'."); if (CurrentTarget != null && previouslyLockedTargetTransform == CurrentTarget.transform) { // AutoTargeting's current target was the one that bLockOn just unlocked. // We should clear AutoTargeting's target to be in sync. // The cooldown will prevent immediate re-auto-targeting. SetNewTarget(null, true); } // If previouslyLockedTargetTransform was something else, AutoTargeting might already be on null or a different target. // The cooldown ensures AutoTargeting doesn't fight this unlock. } // ... (ExecuteRotationTowardsCurrentTarget, GetCurrentTargetHealth, ClearTarget, Renderer methods, Fade coroutine remain the same) 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, true); // Force update if clearing target if (findNewOneImmediately && _gameStateManager != null && _gameStateManager.CurrentState == GameStateManager.State.COMBAT) { // UpdateTarget will respect cooldown, so immediate find might be delayed // If you need it to bypass cooldown here, more logic is needed. // For now, let's assume standard behavior. if (!_manualSwitchCooldownActive) UpdateTarget(); } } private Renderer[] GetTargetRenderers(vFSMBehaviourController targetController) { if (targetController == null) return new Renderer[0]; List collectedRenderers = new List(); SkinnedMeshRenderer[] smrs = targetController.GetComponentsInChildren(true); if (smrs != null && smrs.Length > 0) collectedRenderers.AddRange(smrs); MeshRenderer[] mrs = targetController.GetComponentsInChildren(true); if (mrs != null && mrs.Length > 0) collectedRenderers.AddRange(mrs); if (collectedRenderers.Count == 0) { Renderer[] allRenderers = targetController.GetComponentsInChildren(true); if (allRenderers != null && allRenderers.Length > 0) collectedRenderers.AddRange(allRenderers); } return collectedRenderers.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; 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); } } } }