314 lines
8.8 KiB
C#
314 lines
8.8 KiB
C#
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
|
||
}
|
||
} |