Turret refactor

This commit is contained in:
Szymon Miś
2025-08-25 12:57:15 +02:00
parent c6110c2023
commit 081c0a0e27
6 changed files with 370 additions and 477 deletions

View 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
}
}