Turret refactor
This commit is contained in:
314
Assets/AI/Demon/FireballProjectile.cs
Normal file
314
Assets/AI/Demon/FireballProjectile.cs
Normal file
@@ -0,0 +1,314 @@
|
||||
using Invector;
|
||||
using Invector.vCharacterController;
|
||||
using Lean.Pool;
|
||||
using UnityEngine;
|
||||
|
||||
namespace DemonBoss.Magic
|
||||
{
|
||||
public class FireballProjectile : MonoBehaviour
|
||||
{
|
||||
#region Configuration
|
||||
|
||||
[Header("Targeting")]
|
||||
[Tooltip("Tag of the target to chase while tracking phase is active.")]
|
||||
public string targetTag = "Player";
|
||||
|
||||
[Tooltip("Vertical offset added to the target position (e.g., aim at chest/head instead of feet).")]
|
||||
public float targetHeightOffset = 1.0f;
|
||||
|
||||
[Header("Movement")]
|
||||
[Tooltip("Units per second.")]
|
||||
public float speed = 6f;
|
||||
|
||||
[Tooltip("Seconds to track the player before locking to last known position.")]
|
||||
public float lockTime = 15f;
|
||||
|
||||
[Tooltip("Seconds before the projectile auto-despawns regardless of state.")]
|
||||
public float maxLifeTime = 30f;
|
||||
|
||||
[Tooltip("Distance threshold to consider we arrived at the locked position.")]
|
||||
public float arrivalTolerance = 0.25f;
|
||||
|
||||
[Header("Damage")]
|
||||
[Tooltip("Base damage dealt to the target.")]
|
||||
public int damage = 20;
|
||||
|
||||
[Tooltip("Optional knockback impulse magnitude applied along hit direction.")]
|
||||
public float knockbackForce = 0f;
|
||||
|
||||
[Header("Debug")]
|
||||
[Tooltip("Enable verbose debug logs.")]
|
||||
public bool enableDebug = false;
|
||||
|
||||
#endregion Configuration
|
||||
|
||||
#region Runtime State
|
||||
|
||||
private Transform player;
|
||||
private Vector3 lockedTargetPos;
|
||||
private bool isLocked = false;
|
||||
private bool hasDealtDamage = false;
|
||||
private float timer = 0f;
|
||||
|
||||
#endregion Runtime State
|
||||
|
||||
#region Unity Lifecycle
|
||||
|
||||
/// <summary>
|
||||
/// Called when the object becomes enabled (including when spawned from a pool).
|
||||
/// Resets runtime state and acquires the target by tag.
|
||||
/// </summary>
|
||||
private void OnEnable()
|
||||
{
|
||||
ResetRuntimeState();
|
||||
AcquireTargetByTag();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Per-frame update: handle tracking/lock movement and lifetime/despawn conditions.
|
||||
/// </summary>
|
||||
private void Update()
|
||||
{
|
||||
timer += Time.deltaTime;
|
||||
|
||||
// Transition to 'locked to last position' after lockTime
|
||||
if (!isLocked && timer >= lockTime && player != null)
|
||||
{
|
||||
lockedTargetPos = player.position + Vector3.up * targetHeightOffset;
|
||||
isLocked = true;
|
||||
if (enableDebug) Debug.Log($"[Fireball] Locked to last known player position: {lockedTargetPos}");
|
||||
}
|
||||
|
||||
// Determine current aim point
|
||||
Vector3 aimPoint;
|
||||
if (!isLocked && player != null)
|
||||
{
|
||||
// Tracking phase: follow live player position
|
||||
aimPoint = player.position + Vector3.up * targetHeightOffset;
|
||||
}
|
||||
else if (isLocked)
|
||||
{
|
||||
// Locked phase: fly to memorized position
|
||||
aimPoint = lockedTargetPos;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback: no player found, keep moving forward
|
||||
aimPoint = transform.position + transform.forward * 1000f;
|
||||
}
|
||||
|
||||
// Move towards aim point
|
||||
Vector3 toTarget = aimPoint - transform.position;
|
||||
Vector3 step = toTarget.normalized * speed * Time.deltaTime;
|
||||
transform.position += step;
|
||||
|
||||
// Face movement direction (optional but nice for VFX)
|
||||
if (step.sqrMagnitude > 0.0001f)
|
||||
transform.forward = step.normalized;
|
||||
|
||||
// If locked and close enough to the locked point → despawn
|
||||
if (isLocked && toTarget.magnitude <= arrivalTolerance)
|
||||
{
|
||||
if (enableDebug) Debug.Log("[Fireball] Arrived at locked position → Despawn");
|
||||
Despawn();
|
||||
return;
|
||||
}
|
||||
|
||||
// Hard cap lifetime
|
||||
if (timer >= maxLifeTime)
|
||||
{
|
||||
if (enableDebug) Debug.Log("[Fireball] Max lifetime reached → Despawn");
|
||||
Despawn();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Trigger hit handler – applies damage when colliding with the intended target tag and then despawns.
|
||||
/// </summary>
|
||||
private void OnTriggerEnter(Collider other)
|
||||
{
|
||||
if (hasDealtDamage) return; // guard against multiple triggers in a single frame
|
||||
|
||||
if (other.CompareTag(targetTag))
|
||||
{
|
||||
DealDamageToTarget(other);
|
||||
Despawn();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Unity Lifecycle
|
||||
|
||||
#region Target Acquisition
|
||||
|
||||
/// <summary>
|
||||
/// Finds the target by tag (e.g., "Player"). Called on enable and can be retried if needed.
|
||||
/// </summary>
|
||||
private void AcquireTargetByTag()
|
||||
{
|
||||
var playerObj = GameObject.FindGameObjectWithTag(targetTag);
|
||||
player = playerObj ? playerObj.transform : null;
|
||||
|
||||
if (enableDebug)
|
||||
{
|
||||
if (player != null) Debug.Log($"[Fireball] Target found by tag '{targetTag}'.");
|
||||
else Debug.LogWarning($"[Fireball] No target with tag '{targetTag}' found – moving forward fallback.");
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Target Acquisition
|
||||
|
||||
#region Damage Pipeline (Invector-style)
|
||||
|
||||
/// <summary>
|
||||
/// Applies damage using available receivers on the hit collider or its parents:
|
||||
/// vIDamageReceiver → vHealthController → vThirdPersonController (with Beyond variant checks).
|
||||
/// </summary>
|
||||
private void DealDamageToTarget(Collider targetCollider)
|
||||
{
|
||||
if (enableDebug) Debug.Log($"[FireballDamage] Dealing {damage} damage to: {targetCollider.name}");
|
||||
|
||||
Vector3 hitPoint = GetClosestPointOnCollider(targetCollider);
|
||||
Vector3 hitDirection = GetHitDirection(targetCollider);
|
||||
|
||||
// Build vDamage payload (Invector)
|
||||
vDamage damageInfo = new vDamage(Mathf.RoundToInt(damage))
|
||||
{
|
||||
sender = transform,
|
||||
hitPosition = hitPoint
|
||||
};
|
||||
|
||||
if (knockbackForce > 0f)
|
||||
damageInfo.force = hitDirection * knockbackForce;
|
||||
|
||||
bool damageDealt = false;
|
||||
|
||||
// 1) Try generic vIDamageReceiver (collider or parent)
|
||||
var damageReceiver = targetCollider.GetComponent<vIDamageReceiver>() ??
|
||||
targetCollider.GetComponentInParent<vIDamageReceiver>();
|
||||
|
||||
if (damageReceiver != null && !damageDealt)
|
||||
{
|
||||
damageReceiver.TakeDamage(damageInfo);
|
||||
damageDealt = true;
|
||||
hasDealtDamage = true;
|
||||
if (enableDebug) Debug.Log("[FireballDamage] Damage dealt through vIDamageReceiver");
|
||||
}
|
||||
|
||||
// 2) Fallback to vHealthController
|
||||
if (!damageDealt)
|
||||
{
|
||||
var healthController = targetCollider.GetComponent<vHealthController>() ??
|
||||
targetCollider.GetComponentInParent<vHealthController>();
|
||||
|
||||
if (healthController != null)
|
||||
{
|
||||
healthController.TakeDamage(damageInfo);
|
||||
damageDealt = true;
|
||||
hasDealtDamage = true;
|
||||
if (enableDebug) Debug.Log("[FireballDamage] Damage dealt through vHealthController");
|
||||
}
|
||||
}
|
||||
|
||||
// 3) Fallback to vThirdPersonController (handle Beyond variant)
|
||||
if (!damageDealt)
|
||||
{
|
||||
var tpc = targetCollider.GetComponent<vThirdPersonController>() ??
|
||||
targetCollider.GetComponentInParent<vThirdPersonController>();
|
||||
|
||||
if (tpc != null)
|
||||
{
|
||||
if (tpc is Beyond.bThirdPersonController beyond)
|
||||
{
|
||||
if (!beyond.GodMode && !beyond.isImmortal)
|
||||
{
|
||||
tpc.TakeDamage(damageInfo);
|
||||
damageDealt = true;
|
||||
hasDealtDamage = true;
|
||||
if (enableDebug) Debug.Log("[FireballDamage] Damage dealt through bThirdPersonController");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (enableDebug) Debug.Log("[FireballDamage] Target is immortal / GodMode – no damage dealt");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
tpc.TakeDamage(damageInfo);
|
||||
damageDealt = true;
|
||||
hasDealtDamage = true;
|
||||
if (enableDebug) Debug.Log("[FireballDamage] Damage dealt through vThirdPersonController");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!damageDealt && enableDebug)
|
||||
Debug.LogWarning("[FireballDamage] Could not deal damage – no valid damage receiver found!");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the closest point on the target collider to this projectile position.
|
||||
/// </summary>
|
||||
private Vector3 GetClosestPointOnCollider(Collider col)
|
||||
{
|
||||
return col.ClosestPoint(transform.position);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes a reasonable hit direction from projectile to target center (fallbacks to forward).
|
||||
/// </summary>
|
||||
private Vector3 GetHitDirection(Collider col)
|
||||
{
|
||||
Vector3 dir = (col.bounds.center - transform.position).normalized;
|
||||
return dir.sqrMagnitude > 0.0001f ? dir : transform.forward;
|
||||
}
|
||||
|
||||
#endregion Damage Pipeline (Invector-style)
|
||||
|
||||
#region Pooling & Utilities
|
||||
|
||||
/// <summary>
|
||||
/// Returns this projectile to the Lean Pool (safe to call on non-pooled too).
|
||||
/// </summary>
|
||||
private void Despawn()
|
||||
{
|
||||
if (enableDebug) Debug.Log("[Fireball] Despawn via LeanPool");
|
||||
LeanPool.Despawn(gameObject);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears/initializes runtime variables so pooled instances behave like fresh spawns.
|
||||
/// </summary>
|
||||
private void ResetRuntimeState()
|
||||
{
|
||||
timer = 0f;
|
||||
isLocked = false;
|
||||
hasDealtDamage = false;
|
||||
lockedTargetPos = Vector3.zero;
|
||||
}
|
||||
|
||||
#endregion Pooling & Utilities
|
||||
|
||||
#if UNITY_EDITOR
|
||||
|
||||
/// <summary>
|
||||
/// Optional gizmo: shows lock radius and direction for quick debugging.
|
||||
/// </summary>
|
||||
private void OnDrawGizmosSelected()
|
||||
{
|
||||
Gizmos.color = Color.cyan;
|
||||
Gizmos.DrawWireSphere(transform.position, 0.25f);
|
||||
|
||||
// Draw a small forward arrow
|
||||
Gizmos.DrawLine(transform.position, transform.position + transform.forward * 1.0f);
|
||||
|
||||
// Arrival tolerance sphere (only meaningful when locked)
|
||||
if (isLocked)
|
||||
{
|
||||
Gizmos.color = Color.yellow;
|
||||
Gizmos.DrawWireSphere(lockedTargetPos, arrivalTolerance);
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user