using System.Collections; using System.Collections.Generic; using System.Linq; using Invector; // Assuming vIHealthController is in this namespace or vCharacterController using Invector.vCharacterController; using UnityEngine; using UnityEngine.UI; namespace Beyond { [vClassHeader("MELEE LOCK-ON")] public class bLockOn : vLockOnBehaviour // Inherits from your provided vLockOnBehaviour { #region variables [System.Serializable] public class LockOnEvent : UnityEngine.Events.UnityEvent { } [Tooltip("Make sure to disable or change the StrafeInput to a different key at the Player Input component")] public bool strafeWhileLockOn = true; [Tooltip("Create a Image inside the UI and assign here")] public RectTransform aimImagePrefab; public Canvas aimImageContainer; public Vector2 aimImageSize = new Vector2(30, 30); [Tooltip("True: Hide the sprite when not Lock On, False: Always show the Sprite")] public bool hideSprite = true; [Tooltip("Create a offset for the sprite based at the center of the target")] [Range(-0.5f, 0.5f)] public float spriteHeight = 0.25f; [Tooltip("Offset for the camera height when locking on")] public float cameraHeightOffset; [Tooltip("Transition Speed for the Camera to rotate towards the target when locking on.")] public float lockSpeed = 0.5f; [Header("LockOn Inputs")] public GenericInput lockOnInput = new GenericInput("Tab", "RightStickClick", "RightStickClick"); // Inputs for Next/Previous target are now handled by calling base.ChangeTarget(1) or base.ChangeTarget(-1) public GenericInput nexTargetInput = new GenericInput("X", false, false, "RightAnalogHorizontal", true, false, "X", false, false); public GenericInput previousTargetInput = new GenericInput("Z", false, false, "RightAnalogHorizontal", true, true, "Z", false, false); public bool isLockingOn { get; private set; } // Keep this for external systems like AutoTargetting public LockOnEvent onLockOnTarget; public LockOnEvent onUnLockOnTarget; private RectTransform _aimImageInstance; // 'inTarget' might be redundant if we rely on base.currentTarget != null and isLockingOn // protected bool inTarget; protected bMeleeCombatInput tpInput; private AutoTargetting autoTargetingSystem; #endregion variables protected void Start() // Override if base has Start, otherwise just 'void Start()' { // If vLockOnBehaviour.Start is not virtual, you might not need base.Start() // but Init() is crucial from the base class. base.Init(); // Call the Init from vLockOnBehaviour tpInput = GetComponent(); if (tpInput) { tpInput.onUpdate -= UpdateLockOn; tpInput.onUpdate += UpdateLockOn; // Player health for unlocking on death (assuming player has vIHealthController) var playerHealth = GetComponent(); if (playerHealth != null) { // This part might need adjustment based on how vIHealthController handles death events // For now, let's assume a simple check or that other systems handle player death state. } } else { Debug.LogError("bLockOn: bMeleeCombatInput not found. Lock-on system disabled."); enabled = false; return; } if (!aimImageContainer) aimImageContainer = GetComponentInChildren(true); if (aimImagePrefab && aimImageContainer && _aimImageInstance == null) { _aimImageInstance = Instantiate(aimImagePrefab, aimImageContainer.transform, false); _aimImageInstance.gameObject.SetActive(false); } // Get AutoTargetting system if (Player.Instance != null) autoTargetingSystem = Player.Instance.GetComponentInChildren(true); if (autoTargetingSystem == null) autoTargetingSystem = FindObjectOfType(); if (autoTargetingSystem != null) { // Set the 'range' field from the base class base.range = autoTargetingSystem.maxTargetingDistance; // Note: findTargetAngle from AutoTargetting is not directly used by this base class. // The base class uses screen-space sorting. We can't easily inject world-space angle here // without modifying vLockOnBehaviour.GetPossibleTargets() or SortTargets(). // For now, switching will respect AutoTargetting's *distance* but use base class's screen-angle logic. } else { Debug.LogWarning("bLockOn: AutoTargetting system not found. Using default vLockOnBehaviour range."); } } public RectTransform AimImageDisplay { get { if (_aimImageInstance == null && aimImagePrefab && aimImageContainer) { _aimImageInstance = Instantiate(aimImagePrefab, aimImageContainer.transform, false); _aimImageInstance.anchoredPosition = Vector2.zero; // ... other setup for _aimImageInstance } return _aimImageInstance; } } protected virtual void UpdateLockOn() { if (this.tpInput == null) return; LockOnInput(); SwitchTargetsInput(); CheckForCharacterAlive(); // Checks base.currentTarget UpdateAimImage(); } protected virtual void LockOnInput() { if (tpInput.tpCamera == null || tpInput.cc == null) return; if (lockOnInput.GetButtonDown() && !tpInput.cc.customAction) { // Toggle lock-on state if (isLockingOn) { UnlockTarget(); } else { AttemptLockOn(null); // Attempt to find a new target } } } // This method is called by base.ChangeTargetRoutine when a target is selected protected override void SetTarget() { // base.currentTarget (which is base.target) has been set by ChangeTargetRoutine // or by our AttemptLockOn/ManuallySetLockOnTarget. // We just need to react to it. if (tpInput.tpCamera != null && base.currentTarget != null) { tpInput.tpCamera.SetLockTarget(base.currentTarget.transform, cameraHeightOffset, lockSpeed); if (isLockingOn) // Only invoke if we are truly in a locking state { onLockOnTarget.Invoke(base.currentTarget.transform); } } else if (base.currentTarget == null && isLockingOn) // Target became null while we thought we were locked { UnlockTarget(false); // Silently unlock if target disappeared } } protected virtual void SwitchTargetsInput() { if (tpInput.tpCamera == null || !isLockingOn) return; // base.currentTarget is the currently locked Transform if (base.currentTarget != null) { if (previousTargetInput.GetButtonDown()) base.ChangeTarget(-1); // Calls base class method else if (nexTargetInput.GetButtonDown()) base.ChangeTarget(1); // Calls base class method } } // 'ChangeTarget' is already defined in the base class and handles index changes. // We override SetTarget() which is called by the base ChangeTargetRoutine. protected virtual void CheckForCharacterAlive() { // base.isCharacterAlive() checks the base.currentTarget if (isLockingOn && base.currentTarget != null && !base.isCharacterAlive()) { // Debug.Log($"bLockOn: Locked target {base.currentTarget.name} died. Attempting to find new one or unlock."); Transform oldDeadTarget = base.currentTarget; AttemptLockOn(null, oldDeadTarget); // Try to find a new target, excluding the dead one for this attempt } } /// /// Attempts to lock onto a target. If preferredTarget is provided and valid, locks that. /// Otherwise, finds the best available target. /// protected void AttemptLockOn(Transform preferredTarget, Transform excludeTarget = null) { if (tpInput == null || tpInput.cc == null || tpInput.tpCamera == null) return; Transform targetToLock = null; if (preferredTarget != null && base.isCharacterAlive(preferredTarget)) { targetToLock = preferredTarget; } else { // Update range from AutoTargetting before finding targets if (autoTargetingSystem != null) base.range = autoTargetingSystem.maxTargetingDistance; List possible = base.GetPossibleTargets(); // This uses base.range if (possible != null && possible.Count > 0) { // Filter out the excludeTarget if provided if (excludeTarget != null) { possible.Remove(excludeTarget); } // Optional: Re-filter 'possible' list here using AutoTargetting's angle // This would require iterating 'possible' and doing Vector3.Angle checks. // For now, we rely on base.SortTargets() which is screen-based. if (possible.Count > 0) { targetToLock = possible.FirstOrDefault(); // Base.GetPossibleTargets already sorts them } } } if (targetToLock != null) { // If we were previously locked on a different target if (isLockingOn && base.currentTarget != null && base.currentTarget != targetToLock) { onUnLockOnTarget.Invoke(base.currentTarget); // Invoke for old target } // This directly sets the protected 'target' field in the base class // Since we can't call a base.SelectTarget(), we mimic what base.UpdateLockOn(true) would do. this.GetType().GetField("target", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)?.SetValue(this, targetToLock); // this.target = targetToLock; // This would create a new field in bLockOn, not set base.target isLockingOn = true; base.UpdateLockOn(true); // This will set base.target if list not empty, and also set _inLockOn // and might call SetTarget if list was empty and now has one. // It's a bit of a dance because the base class design is restrictive. // We need to ensure our 'isLockingOn' and the base class's internal lock state are synced. // And that SetTarget() (our override) is called to update camera & events. if (base.currentTarget == targetToLock) // If base.UpdateLockOn successfully set it { this.SetTarget(); // Manually call our override to ensure camera and events fire UpdateStrafeState(true); UpdateAimImage(); } else // Fallback if base.UpdateLockOn didn't pick our target (e.g. list was empty for it) { // This case should ideally not happen if targetToLock was valid. // If it does, it means base.GetPossibleTargets within UpdateLockOn failed. UnlockTarget(false); // Silently unlock } } else // No target found or preferred target was invalid { UnlockTarget(); } } protected void UnlockTarget(bool invokeEvent = true) { if (tpInput == null || tpInput.tpCamera == null) return; Transform previouslyLocked = base.currentTarget; // Get before clearing base.UpdateLockOn(false); // This sets base.target to null and base._inLockOn to false isLockingOn = false; if (previouslyLocked != null && invokeEvent) { onUnLockOnTarget.Invoke(previouslyLocked); } tpInput.tpCamera.RemoveLockTarget(); UpdateStrafeState(false); UpdateAimImage(); } public void ManuallySetLockOnTarget(Transform newTargetTransform, bool shouldLock) { if (shouldLock) { AttemptLockOn(newTargetTransform); } else { UnlockTarget(); } } public Transform GetCurrentLockOnTarget() { // Relies on base.currentTarget which is the Transform 'target' from base class return isLockingOn ? base.currentTarget : null; } private void UpdateStrafeState(bool shouldStrafe) { if (strafeWhileLockOn && tpInput?.cc != null && !tpInput.cc.locomotionType.Equals(vThirdPersonMotor.LocomotionType.OnlyStrafe)) { bool canStrafe = shouldStrafe && isLockingOn && base.currentTarget != null; tpInput.cc.lockInStrafe = canStrafe; tpInput.cc.isStrafing = canStrafe; } } public void SetLockOnToFalse() => UnlockTarget(); protected virtual void UpdateAimImage() { var displayImage = AimImageDisplay; if (!aimImageContainer || displayImage == null) return; displayImage.sizeDelta = aimImageSize; // base.currentTarget is the Transform, base.isCharacterAlive() checks it bool shouldShowAimImage = isLockingOn && base.currentTarget != null && base.isCharacterAlive(base.currentTarget); if (hideSprite) displayImage.gameObject.SetActive(shouldShowAimImage); else if (!displayImage.gameObject.activeSelf) displayImage.gameObject.SetActive(true); if (shouldShowAimImage && tpCamera != null && base.currentTarget != null) { // Using the extension method from vLockOnHelper displayImage.anchoredPosition = base.currentTarget.GetScreenPointOffBoundsCenter(aimImageContainer, tpCamera.targetCamera, spriteHeight); } else if (!hideSprite) displayImage.anchoredPosition = Vector2.zero; } // StopLockOn is effectively UnlockTarget now // public virtual void StopLockOn() { UnlockTarget(); } public List GetNearbyTargets() // For AutoTargetting/MagicAttacks { if (autoTargetingSystem != null) { base.range = autoTargetingSystem.maxTargetingDistance; // Angle filtering from AutoTargetting would need to be applied manually here // if we want to override the base class's screen-based sorting/filtering for this specific call. } // base.GetPossibleTargets() returns List List allTargetsInBaseRange = base.GetPossibleTargets(); if (allTargetsInBaseRange == null) return new List(); // Optional: Further filter allTargetsInBaseRange by AutoTargetting's angle if (autoTargetingSystem != null && _playerTransform != null) // Assuming _playerTransform is accessible or passed { List angleFilteredTargets = new List(); Vector3 playerPos = _playerTransform.position; // Need player transform Vector3 playerFwd = _playerTransform.forward; foreach (Transform t in allTargetsInBaseRange) { if (t == null) continue; Vector3 dirToTarget = (t.position - playerPos); dirToTarget.y = 0; // Assuming 2D angle check for simplicity if (dirToTarget.sqrMagnitude < 0.001f) // Target is at player's feet { angleFilteredTargets.Add(t); continue; } float angle = Vector3.Angle(playerFwd, dirToTarget.normalized); if (angle <= autoTargetingSystem.targetingAngleThreshold) { angleFilteredTargets.Add(t); } } return angleFilteredTargets; } return allTargetsInBaseRange; } // Need player transform for angle filtering in GetNearbyTargets private Transform _playerTransform; void Awake() // Get player transform { if (Player.Instance != null) _playerTransform = Player.Instance.transform; else _playerTransform = transform; // Fallback if bLockOn is on player } } }