420 lines
17 KiB
C#
420 lines
17 KiB
C#
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<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;
|
|
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<bThirdPersonController>();
|
|
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<bLockOn>(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<vFSMBehaviourController> 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<vFSMBehaviourController>();
|
|
if (fsmTarget == null) fsmTarget = lockedTargetTransform.GetComponentInChildren<vFSMBehaviourController>(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<Renderer>(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
|
|
}
|
|
} |