Files cleanup, added summoner code
This commit is contained in:
400
Assets/AI/_Demon/MeteorProjectile.cs
Normal file
400
Assets/AI/_Demon/MeteorProjectile.cs
Normal file
@@ -0,0 +1,400 @@
|
||||
using Invector;
|
||||
using Invector.vCharacterController;
|
||||
using Lean.Pool;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
|
||||
namespace DemonBoss.Magic
|
||||
{
|
||||
/// <summary>
|
||||
/// Enhanced meteor projectile with fireball-like tracking mechanics
|
||||
/// Can either track player or fly to a locked impact point
|
||||
/// </summary>
|
||||
public class MeteorProjectile : MonoBehaviour
|
||||
{
|
||||
#region Inspector: Targeting
|
||||
|
||||
[Header("Targeting")]
|
||||
[Tooltip("If true, use 'overrideImpactPoint' instead of tracking player")]
|
||||
public bool useOverrideImpactPoint = false;
|
||||
|
||||
[Tooltip("Externally provided locked impact point")]
|
||||
public Vector3 overrideImpactPoint;
|
||||
|
||||
[Tooltip("Tag to find and track if not using override point")]
|
||||
public string targetTag = "Player";
|
||||
|
||||
[Tooltip("Height offset for targeting (aim at chest/head)")]
|
||||
public float targetHeightOffset = 1.0f;
|
||||
|
||||
[Tooltip("If true, raycast to ground from impact point")]
|
||||
public bool snapImpactToGround = true;
|
||||
|
||||
[Tooltip("Layers considered 'ground'")]
|
||||
public LayerMask groundMask = ~0;
|
||||
|
||||
[ReadOnlyInInspector] public Vector3 currentTargetPoint;
|
||||
|
||||
#endregion Inspector: Targeting
|
||||
|
||||
#region Inspector: Flight
|
||||
|
||||
[Header("Flight")]
|
||||
[Tooltip("Movement speed in m/s")]
|
||||
public float speed = 25f;
|
||||
|
||||
[Tooltip("Time to track player before locking to position")]
|
||||
public float trackingTime = 1.5f;
|
||||
|
||||
[Tooltip("Distance threshold to consider arrived")]
|
||||
public float arriveEpsilon = 0.5f;
|
||||
|
||||
[Tooltip("Max lifetime in seconds")]
|
||||
public float maxLifetime = 15f;
|
||||
|
||||
#endregion Inspector: Flight
|
||||
|
||||
#region Inspector: Collision & Damage
|
||||
|
||||
[Header("Collision & Damage")]
|
||||
[Tooltip("Collision detection radius")]
|
||||
public float collisionRadius = 0.8f;
|
||||
|
||||
[Tooltip("Layers that stop the meteor")]
|
||||
public LayerMask stopOnLayers = ~0;
|
||||
|
||||
[Tooltip("Layers that take damage")]
|
||||
public LayerMask damageLayers = ~0;
|
||||
|
||||
[Tooltip("Explosion damage radius")]
|
||||
public float explosionRadius = 4f;
|
||||
|
||||
[Tooltip("Base damage")]
|
||||
public int damage = 35;
|
||||
|
||||
[Tooltip("Knockback force")]
|
||||
public float knockbackForce = 12f;
|
||||
|
||||
#endregion Inspector: Collision & Damage
|
||||
|
||||
#region Inspector: Effects
|
||||
|
||||
[Header("Effects & Events")]
|
||||
public GameObject impactVfxPrefab;
|
||||
|
||||
public UnityEvent onSpawn;
|
||||
public UnityEvent onImpact;
|
||||
|
||||
[Header("Debug")]
|
||||
public bool enableDebug = false;
|
||||
|
||||
#endregion Inspector: Effects
|
||||
|
||||
#region Runtime
|
||||
|
||||
private Transform _player;
|
||||
private Vector3 _lockedTarget;
|
||||
private bool _isLocked = false;
|
||||
private bool _hasImpacted = false;
|
||||
private float _lifetime = 0f;
|
||||
private readonly Collider[] _overlapCache = new Collider[32];
|
||||
|
||||
#endregion Runtime
|
||||
|
||||
#region Unity
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
ResetState();
|
||||
InitializeTargeting();
|
||||
onSpawn?.Invoke();
|
||||
|
||||
if (enableDebug) Debug.Log($"[MeteorProjectile] Spawned at {transform.position}");
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (_hasImpacted) return;
|
||||
|
||||
_lifetime += Time.deltaTime;
|
||||
|
||||
// Check lifetime limit
|
||||
if (_lifetime >= maxLifetime)
|
||||
{
|
||||
if (enableDebug) Debug.Log("[MeteorProjectile] Lifetime expired");
|
||||
DoImpact(transform.position);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle tracking to lock transition
|
||||
if (!_isLocked && _lifetime >= trackingTime)
|
||||
{
|
||||
LockTarget();
|
||||
}
|
||||
|
||||
// Update target position and move
|
||||
UpdateTargetPosition();
|
||||
MoveTowardsTarget();
|
||||
CheckCollisions();
|
||||
}
|
||||
|
||||
#endregion Unity
|
||||
|
||||
#region Targeting & Movement
|
||||
|
||||
private void ResetState()
|
||||
{
|
||||
_hasImpacted = false;
|
||||
_isLocked = false;
|
||||
_lifetime = 0f;
|
||||
_lockedTarget = Vector3.zero;
|
||||
}
|
||||
|
||||
private void InitializeTargeting()
|
||||
{
|
||||
if (useOverrideImpactPoint)
|
||||
{
|
||||
_lockedTarget = snapImpactToGround ? SnapToGround(overrideImpactPoint) : overrideImpactPoint;
|
||||
_isLocked = true;
|
||||
currentTargetPoint = _lockedTarget;
|
||||
|
||||
if (enableDebug) Debug.Log($"[MeteorProjectile] Using override target: {_lockedTarget}");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Find player for tracking
|
||||
var playerGO = GameObject.FindGameObjectWithTag(targetTag);
|
||||
_player = playerGO ? playerGO.transform : null;
|
||||
|
||||
if (enableDebug)
|
||||
{
|
||||
if (_player) Debug.Log($"[MeteorProjectile] Found player: {_player.name}");
|
||||
else Debug.LogWarning($"[MeteorProjectile] No player found with tag: {targetTag}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void LockTarget()
|
||||
{
|
||||
if (_isLocked) return;
|
||||
|
||||
if (_player != null)
|
||||
{
|
||||
Vector3 targetPos = _player.position + Vector3.up * targetHeightOffset;
|
||||
_lockedTarget = snapImpactToGround ? SnapToGround(targetPos) : targetPos;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback: lock to current forward direction
|
||||
_lockedTarget = transform.position + transform.forward * 50f;
|
||||
}
|
||||
|
||||
_isLocked = true;
|
||||
|
||||
if (enableDebug) Debug.Log($"[MeteorProjectile] Target locked to: {_lockedTarget}");
|
||||
}
|
||||
|
||||
private void UpdateTargetPosition()
|
||||
{
|
||||
if (_isLocked)
|
||||
{
|
||||
currentTargetPoint = _lockedTarget;
|
||||
}
|
||||
else if (_player != null)
|
||||
{
|
||||
currentTargetPoint = _player.position + Vector3.up * targetHeightOffset;
|
||||
}
|
||||
else
|
||||
{
|
||||
// No player, keep going forward
|
||||
currentTargetPoint = transform.position + transform.forward * 100f;
|
||||
}
|
||||
}
|
||||
|
||||
private void MoveTowardsTarget()
|
||||
{
|
||||
Vector3 direction = (currentTargetPoint - transform.position).normalized;
|
||||
Vector3 movement = direction * speed * Time.deltaTime;
|
||||
|
||||
transform.position += movement;
|
||||
|
||||
// Face movement direction
|
||||
if (movement.sqrMagnitude > 0.0001f)
|
||||
{
|
||||
transform.rotation = Quaternion.LookRotation(movement.normalized);
|
||||
}
|
||||
|
||||
// Check if we've arrived (only when locked)
|
||||
if (_isLocked && Vector3.Distance(transform.position, currentTargetPoint) <= arriveEpsilon)
|
||||
{
|
||||
if (enableDebug) Debug.Log("[MeteorProjectile] Arrived at target");
|
||||
DoImpact(transform.position);
|
||||
}
|
||||
}
|
||||
|
||||
private void CheckCollisions()
|
||||
{
|
||||
// Use OverlapSphere for collision detection
|
||||
int hitCount = Physics.OverlapSphereNonAlloc(transform.position, collisionRadius, _overlapCache, stopOnLayers, QueryTriggerInteraction.Ignore);
|
||||
|
||||
if (hitCount > 0)
|
||||
{
|
||||
if (enableDebug) Debug.Log($"[MeteorProjectile] Collision detected with {_overlapCache[0].name}");
|
||||
DoImpact(transform.position);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Targeting & Movement
|
||||
|
||||
#region Impact & Damage
|
||||
|
||||
private void DoImpact(Vector3 impactPos)
|
||||
{
|
||||
if (_hasImpacted) return;
|
||||
_hasImpacted = true;
|
||||
|
||||
if (enableDebug) Debug.Log($"[MeteorProjectile] Impact at {impactPos}");
|
||||
|
||||
// Spawn VFX
|
||||
if (impactVfxPrefab != null)
|
||||
{
|
||||
var vfx = LeanPool.Spawn(impactVfxPrefab, impactPos, Quaternion.identity);
|
||||
// Auto-despawn VFX after 5 seconds
|
||||
LeanPool.Despawn(vfx, 5f);
|
||||
}
|
||||
|
||||
onImpact?.Invoke();
|
||||
|
||||
// Deal area damage
|
||||
int damageTargets = Physics.OverlapSphereNonAlloc(impactPos, explosionRadius, _overlapCache, damageLayers, QueryTriggerInteraction.Ignore);
|
||||
|
||||
for (int i = 0; i < damageTargets; i++)
|
||||
{
|
||||
var col = _overlapCache[i];
|
||||
if (col != null)
|
||||
{
|
||||
DealDamageToTarget(col, impactPos);
|
||||
}
|
||||
}
|
||||
|
||||
// Despawn meteor
|
||||
LeanPool.Despawn(gameObject);
|
||||
}
|
||||
|
||||
private void DealDamageToTarget(Collider targetCollider, Vector3 hitPoint)
|
||||
{
|
||||
Vector3 hitDirection = (targetCollider.bounds.center - hitPoint).normalized;
|
||||
if (hitDirection.sqrMagnitude < 0.0001f) hitDirection = Vector3.up;
|
||||
|
||||
vDamage damageInfo = new vDamage(damage)
|
||||
{
|
||||
sender = transform,
|
||||
hitPosition = hitPoint
|
||||
};
|
||||
|
||||
if (knockbackForce > 0f)
|
||||
damageInfo.force = hitDirection * knockbackForce;
|
||||
|
||||
bool damageDealt = false;
|
||||
|
||||
// Try vIDamageReceiver first
|
||||
var receiver = targetCollider.GetComponent<vIDamageReceiver>() ??
|
||||
targetCollider.GetComponentInParent<vIDamageReceiver>();
|
||||
if (receiver != null && !damageDealt)
|
||||
{
|
||||
receiver.TakeDamage(damageInfo);
|
||||
damageDealt = true;
|
||||
}
|
||||
|
||||
// Fallback to vHealthController
|
||||
if (!damageDealt)
|
||||
{
|
||||
var hc = targetCollider.GetComponent<vHealthController>() ??
|
||||
targetCollider.GetComponentInParent<vHealthController>();
|
||||
if (hc != null)
|
||||
{
|
||||
hc.TakeDamage(damageInfo);
|
||||
damageDealt = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to vThirdPersonController
|
||||
if (!damageDealt)
|
||||
{
|
||||
var tpc = targetCollider.GetComponent<vThirdPersonController>() ??
|
||||
targetCollider.GetComponentInParent<vThirdPersonController>();
|
||||
if (tpc != null)
|
||||
{
|
||||
// Handle Beyond variant
|
||||
if (tpc is Beyond.bThirdPersonController beyond)
|
||||
{
|
||||
if (!beyond.GodMode && !beyond.isImmortal)
|
||||
{
|
||||
tpc.TakeDamage(damageInfo);
|
||||
damageDealt = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
tpc.TakeDamage(damageInfo);
|
||||
damageDealt = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply physics force
|
||||
var rb = targetCollider.attachedRigidbody;
|
||||
if (rb != null && knockbackForce > 0f)
|
||||
{
|
||||
rb.AddForce(hitDirection * knockbackForce, ForceMode.Impulse);
|
||||
}
|
||||
|
||||
if (enableDebug && damageDealt)
|
||||
{
|
||||
Debug.Log($"[MeteorProjectile] Dealt {damage} damage to {targetCollider.name}");
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Impact & Damage
|
||||
|
||||
#region Helpers
|
||||
|
||||
private Vector3 SnapToGround(Vector3 point)
|
||||
{
|
||||
Vector3 rayStart = point + Vector3.up * 10f;
|
||||
if (Physics.Raycast(rayStart, Vector3.down, out RaycastHit hit, 50f, groundMask, QueryTriggerInteraction.Ignore))
|
||||
{
|
||||
return hit.point;
|
||||
}
|
||||
return point;
|
||||
}
|
||||
|
||||
#endregion Helpers
|
||||
|
||||
#if UNITY_EDITOR
|
||||
|
||||
private void OnDrawGizmosSelected()
|
||||
{
|
||||
// Draw collision sphere
|
||||
Gizmos.color = Color.red;
|
||||
Gizmos.DrawWireSphere(transform.position, collisionRadius);
|
||||
|
||||
// Draw explosion radius
|
||||
Gizmos.color = Color.red;
|
||||
Gizmos.DrawWireSphere(transform.position, explosionRadius);
|
||||
|
||||
// Draw target point
|
||||
if (currentTargetPoint != Vector3.zero)
|
||||
{
|
||||
Gizmos.color = Color.yellow;
|
||||
Gizmos.DrawWireSphere(currentTargetPoint, 0.5f);
|
||||
Gizmos.DrawLine(transform.position, currentTargetPoint);
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
}
|
||||
|
||||
public sealed class ReadOnlyInInspectorAttribute : PropertyAttribute
|
||||
{ }
|
||||
}
|
||||
Reference in New Issue
Block a user