428 lines
18 KiB
C#
428 lines
18 KiB
C#
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<Transform> { }
|
|
|
|
[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<bMeleeCombatInput>();
|
|
if (tpInput)
|
|
{
|
|
tpInput.onUpdate -= UpdateLockOn;
|
|
tpInput.onUpdate += UpdateLockOn;
|
|
|
|
// Player health for unlocking on death (assuming player has vIHealthController)
|
|
var playerHealth = GetComponent<vIHealthController>();
|
|
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<Canvas>(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<AutoTargetting>(true);
|
|
if (autoTargetingSystem == null) autoTargetingSystem = FindObjectOfType<AutoTargetting>();
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Attempts to lock onto a target. If preferredTarget is provided and valid, locks that.
|
|
/// Otherwise, finds the best available target.
|
|
/// </summary>
|
|
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<Transform> 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<Transform> 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<Transform>
|
|
List<Transform> allTargetsInBaseRange = base.GetPossibleTargets();
|
|
|
|
if (allTargetsInBaseRange == null) return new List<Transform>();
|
|
|
|
// Optional: Further filter allTargetsInBaseRange by AutoTargetting's angle
|
|
if (autoTargetingSystem != null && _playerTransform != null) // Assuming _playerTransform is accessible or passed
|
|
{
|
|
List<Transform> angleFilteredTargets = new List<Transform>();
|
|
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
|
|
}
|
|
|
|
// --- ADD THIS ENTIRE METHOD TO YOUR bLockOn.cs SCRIPT ---
|
|
|
|
/// <summary>
|
|
/// Externally sets the lock-on state.
|
|
/// </summary>
|
|
/// <param name="value">True to turn lock-on ON, False to turn it OFF.</param>
|
|
public void SetLockOn(bool value)
|
|
{
|
|
// If the state is already what we want, do nothing.
|
|
if (isLockingOn == value)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// We don't set isLockingOn directly here, as the methods below handle the state change.
|
|
|
|
if (value)
|
|
{
|
|
// If we are turning lock-on ON, find the best target immediately.
|
|
// This prevents a delay before the first lock.
|
|
// The `null` argument tells it to find any valid target.
|
|
AttemptLockOn(null);
|
|
}
|
|
else
|
|
{
|
|
// If we are turning lock-on OFF, clear the current target.
|
|
UnlockTarget();
|
|
}
|
|
}
|
|
}
|
|
} |