Files
beyond/Assets/AI/Demon/MeteorProjectile.cs
Szymon Miś 7cccafbb2b Demon fixes
2025-08-29 11:48:10 +02:00

472 lines
14 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Invector;
using Invector.vCharacterController;
using Lean.Pool;
using UnityEngine;
using UnityEngine.Events;
namespace DemonBoss.Magic
{
public class MeteorProjectile : MonoBehaviour
{
#region Configuration
[Header("Targeting")]
[Tooltip("Tag of the target to pick impact point from on spawn")]
public string targetTag = "Player";
[Tooltip("If true, raycasts down from impact point to find ground (better visuals)")]
public bool snapImpactToGround = true;
[Tooltip("LayerMask used when snapping impact point to ground")]
public LayerMask groundMask = ~0;
[Header("Entry Geometry")]
[Tooltip("If > 0, teleports start Y above impact point by this height (keeps current XZ). 0 = keep original height.")]
public float spawnHeightAboveTarget = 12f;
[Tooltip("Entry angle measured DOWN from horizontal (e.g. 15° = shallow, 45° = steeper)")]
[Range(0f, 89f)]
public float entryAngleDownFromHorizontal = 20f;
[Tooltip("How closely to aim towards the target in XZ before tilting down (0..1). 1 = purely towards target, 0 = purely downward.")]
[Range(0f, 1f)]
public float azimuthAimWeight = 0.85f;
[Header("Movement")]
[Tooltip("Initial speed magnitude in units per second")]
public float speed = 30f;
[Tooltip("Extra downward acceleration to add a slight curve (0 = disabled)")]
public float gravityLikeAcceleration = 0f;
[Tooltip("Rotate the meteor mesh to face velocity")]
public bool rotateToVelocity = true;
[Tooltip("Seconds before the meteor auto-despawns")]
public float maxLifeTime = 12f;
[Header("Impact / AOE")]
[Tooltip("Explosion radius at impact (0 = no AOE, only single target)")]
public float explosionRadius = 0f;
[Tooltip("Layers that should receive AOE damage")]
public LayerMask explosionMask = ~0;
[Tooltip("If true, AOE will only damage colliders with the same tag as targetTag")]
public bool aoeOnlyTargetTag = false;
[Tooltip("Sphere radius for continuous collision checks")]
public float castRadius = 0.25f;
[Tooltip("Layers that block the meteor during flight (ground/walls/etc.)")]
public LayerMask collisionMask = ~0;
[Tooltip("Damage that will be dealt to Player")]
public float damage = 40.0f;
[Tooltip("Knockback that will be applied to Player")]
public float knockbackForce = 5.0f;
[Header("Debug")]
[Tooltip("Enable verbose logs and debug rays")]
public bool enableDebug = false;
[Header("Events")]
[Tooltip("Invoked once on any impact (use for VFX/SFX/CameraShake)")]
public UnityEvent onImpact;
#endregion Configuration
#region Runtime
private Vector3 impactPoint;
private Vector3 velocityDir;
private Vector3 velocity;
private float timer = 0f;
private bool hasDealtDamage = false;
private bool hasImpacted = false;
private Vector3 prevPos;
#endregion Runtime
#region Unity
/// <summary>
/// Resets runtime state, locks the impact point, computes entry direction and initial velocity.
/// </summary>
private void OnEnable()
{
timer = 0f;
hasDealtDamage = false;
hasImpacted = false;
// Acquire player and lock impact point
Vector3 targetPos = GetPlayerPosition();
impactPoint = snapImpactToGround ? SnapPointToGround(targetPos) : targetPos;
// Optionally start from above to guarantee top-down feel
if (spawnHeightAboveTarget > 0f)
{
Vector3 p = transform.position;
p.y = impactPoint.y + spawnHeightAboveTarget;
transform.position = p;
}
Vector3 toImpactXZ = new Vector3(impactPoint.x, transform.position.y, impactPoint.z) - transform.position;
Vector3 azimuthDir = toImpactXZ.sqrMagnitude > 0.0001f ? toImpactXZ.normalized : transform.forward;
Vector3 tiltAxis = Vector3.Cross(Vector3.up, azimuthDir);
if (tiltAxis.sqrMagnitude < 0.0001f) tiltAxis = Vector3.right;
Quaternion downTilt = Quaternion.AngleAxis(-entryAngleDownFromHorizontal, tiltAxis);
Vector3 tiltedDir = (downTilt * azimuthDir).normalized;
velocityDir = Vector3.Slerp(Vector3.down, tiltedDir, Mathf.Clamp01(azimuthAimWeight)).normalized;
velocity = velocityDir * Mathf.Max(0f, speed);
if (rotateToVelocity && velocity.sqrMagnitude > 0.0001f)
transform.rotation = Quaternion.LookRotation(velocity.normalized);
if (enableDebug)
{
Debug.Log($"[Meteor] Impact={impactPoint}, start={transform.position}, dir={velocityDir}");
Debug.DrawLine(transform.position, transform.position + velocityDir * 6f, Color.red, 3f);
Debug.DrawLine(transform.position, impactPoint, Color.yellow, 2f);
}
prevPos = transform.position;
}
/// <summary>
/// Integrates motion with optional downward acceleration, performs a SphereCast for continuous collision,
/// rotates to face velocity, and handles lifetime expiry.
/// </summary>
private void Update()
{
timer += Time.deltaTime;
// Downward "gravity-like" acceleration
if (gravityLikeAcceleration > 0f)
velocity += Vector3.down * gravityLikeAcceleration * Time.deltaTime;
Vector3 nextPos = transform.position + velocity * Time.deltaTime;
// Continuous collision: SphereCast from current position toward next
Vector3 castDir = nextPos - transform.position;
float castDist = castDir.magnitude;
if (castDist > 0.0001f)
{
if (Physics.SphereCast(transform.position, castRadius, castDir.normalized,
out RaycastHit hit, castDist, collisionMask, QueryTriggerInteraction.Ignore))
{
transform.position = hit.point;
Impact(hit.collider, hit.point, hit.normal);
return;
}
}
// No hit — apply movement
transform.position = nextPos;
// Face velocity if requested
if (rotateToVelocity && velocity.sqrMagnitude > 0.0001f)
transform.rotation = Quaternion.LookRotation(velocity.normalized);
prevPos = transform.position;
// Auto-despawn on lifetime cap
if (timer >= maxLifeTime)
{
if (enableDebug) Debug.Log("[Meteor] Max lifetime reached → Despawn");
Despawn();
}
}
/// <summary>
/// Trigger-based contact: funnels into unified Impact().
/// </summary>
private void OnTriggerEnter(Collider other)
{
// Even if not the intended target, a meteor typically impacts anything it touches
Impact(other, transform.position, -SafeNormal(velocity));
}
/// <summary>
/// Collision-based contact: funnels into unified Impact().
/// </summary>
private void OnCollisionEnter(Collision collision)
{
var cp = collision.contacts.Length > 0 ? collision.contacts[0] : default;
Impact(collision.collider, cp.point != default ? cp.point : transform.position, cp.normal != default ? cp.normal : Vector3.up);
}
#endregion Unity
#region Helpers: Target / Ground
/// <summary>
/// Gets the target's current position (by tag). If not found, projects forward from current position.
/// </summary>
private Vector3 GetPlayerPosition()
{
GameObject playerObj = GameObject.FindGameObjectWithTag(targetTag);
if (playerObj != null)
return playerObj.transform.position;
if (enableDebug) Debug.LogWarning($"[Meteor] No target with tag '{targetTag}' found, using forward guess.");
return transform.position + transform.forward * 10f;
}
/// <summary>
/// Raycasts down from above the source point to find ground; returns original point if no hit.
/// </summary>
private Vector3 SnapPointToGround(Vector3 source)
{
Vector3 start = source + Vector3.up * 50f;
if (Physics.Raycast(start, Vector3.down, out RaycastHit hit, 200f, groundMask, QueryTriggerInteraction.Ignore))
return hit.point;
return source;
}
/// <summary>
/// Returns a safe normalized version of a vector (falls back to Vector3.down if tiny).
/// </summary>
private static Vector3 SafeNormal(Vector3 v)
{
return v.sqrMagnitude > 0.0001f ? v.normalized : Vector3.down;
}
#endregion Helpers: Target / Ground
#region Impact & Damage
/// <summary>
/// Unified impact handler (idempotent). Deals single-target damage if applicable,
/// optional AOE damage, invokes events, and despawns.
/// </summary>
private void Impact(Collider hitCol, Vector3 hitPoint, Vector3 hitNormal)
{
if (hasImpacted) return;
hasImpacted = true;
if (enableDebug)
Debug.Log($"[Meteor] Impact with {(hitCol ? hitCol.name : "null")} at {hitPoint}");
// Single target (original logic)
if (!hasDealtDamage && hitCol != null && hitCol.CompareTag(targetTag))
DealDamageToTarget(hitCol);
// AOE (if enabled)
if (explosionRadius > 0f)
DealAreaDamage(hitPoint);
// Hook for VFX/SFX/CameraShake
onImpact?.Invoke();
Despawn();
}
/// <summary>
/// Deals damage to the intended single target using Invector interfaces/components.
/// </summary>
private void DealDamageToTarget(Collider targetCollider)
{
if (enableDebug) Debug.Log($"[MeteorDamage] Dealing {damage} damage to: {targetCollider.name}");
Vector3 hitPoint = GetClosestPointOnCollider(targetCollider);
Vector3 hitDirection = GetHitDirection(targetCollider);
vDamage damageInfo = new vDamage(Mathf.RoundToInt(damage))
{
sender = transform,
hitPosition = hitPoint
};
if (knockbackForce > 0f)
damageInfo.force = hitDirection * knockbackForce;
bool damageDealt = false;
// vIDamageReceiver
var receiver = targetCollider.GetComponent<vIDamageReceiver>() ??
targetCollider.GetComponentInParent<vIDamageReceiver>();
if (receiver != null)
{
receiver.TakeDamage(damageInfo);
damageDealt = true;
hasDealtDamage = true;
if (enableDebug) Debug.Log("[MeteorDamage] Damage via vIDamageReceiver");
}
// vHealthController
if (!damageDealt)
{
var health = targetCollider.GetComponent<vHealthController>() ??
targetCollider.GetComponentInParent<vHealthController>();
if (health != null)
{
health.TakeDamage(damageInfo);
damageDealt = true;
hasDealtDamage = true;
if (enableDebug) Debug.Log("[MeteorDamage] Damage via vHealthController");
}
}
// vThirdPersonController (including 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("[MeteorDamage] Damage via bThirdPersonController");
}
else if (enableDebug)
{
Debug.Log("[MeteorDamage] Target is immortal/GodMode no damage");
}
}
else
{
tpc.TakeDamage(damageInfo);
damageDealt = true;
hasDealtDamage = true;
if (enableDebug) Debug.Log("[MeteorDamage] Damage via vThirdPersonController");
}
}
}
if (!damageDealt && enableDebug)
Debug.LogWarning("[MeteorDamage] No valid damage receiver found!");
}
/// <summary>
/// Deals AOE damage to all colliders within explosionRadius using Invector-compatible logic.
/// </summary>
private void DealAreaDamage(Vector3 center)
{
Collider[] hits = Physics.OverlapSphere(center, explosionRadius, explosionMask, QueryTriggerInteraction.Ignore);
foreach (var col in hits)
{
if (col == null) continue;
if (aoeOnlyTargetTag && !col.CompareTag(targetTag)) continue;
// Avoid double-hitting the same single target if already processed
if (col.CompareTag(targetTag) && hasDealtDamage) continue;
Vector3 hp = col.ClosestPoint(center);
Vector3 dir = (col.bounds.center - center).normalized;
vDamage damageInfo = new vDamage(Mathf.RoundToInt(damage))
{
sender = transform,
hitPosition = hp,
force = knockbackForce > 0f ? dir * knockbackForce : Vector3.zero
};
bool dealt = false;
var receiver = col.GetComponent<vIDamageReceiver>() ?? col.GetComponentInParent<vIDamageReceiver>();
if (receiver != null) { receiver.TakeDamage(damageInfo); dealt = true; }
if (!dealt)
{
var health = col.GetComponent<vHealthController>() ?? col.GetComponentInParent<vHealthController>();
if (health != null) { health.TakeDamage(damageInfo); dealt = true; }
}
if (!dealt)
{
var tpc = col.GetComponent<vThirdPersonController>() ?? col.GetComponentInParent<vThirdPersonController>();
if (tpc != null)
{
if (tpc is Beyond.bThirdPersonController beyond)
{
if (!beyond.GodMode && !beyond.isImmortal) { tpc.TakeDamage(damageInfo); dealt = true; }
}
else { tpc.TakeDamage(damageInfo); dealt = true; }
}
}
}
// Consider AOE as the final damage application for this projectile
hasDealtDamage = true;
}
/// <summary>
/// Gets the closest point on a collider to this projectile's position.
/// </summary>
private Vector3 GetClosestPointOnCollider(Collider col)
{
return col.ClosestPoint(transform.position);
}
/// <summary>
/// Computes a hit direction from projectile toward a collider's center; falls back to current velocity.
/// </summary>
private Vector3 GetHitDirection(Collider col)
{
Vector3 dir = (col.bounds.center - transform.position).normalized;
return dir.sqrMagnitude > 0.0001f ? dir : SafeNormal(velocity);
}
#endregion Impact & Damage
#region Pooling
/// <summary>
/// Returns this projectile to the LeanPool.
/// </summary>
private void Despawn()
{
if (enableDebug) Debug.Log("[Meteor] Despawn via LeanPool");
LeanPool.Despawn(gameObject);
}
#endregion Pooling
#if UNITY_EDITOR
/// <summary>
/// Editor-only gizmos to visualize entry direction and impact point when selected.
/// </summary>
private void OnDrawGizmosSelected()
{
Gizmos.color = Color.cyan;
Gizmos.DrawWireSphere(transform.position, 0.25f);
Gizmos.color = Color.red;
Gizmos.DrawLine(transform.position, transform.position + SafeNormal(velocity.sqrMagnitude > 0 ? velocity : velocityDir) * 3f);
Gizmos.color = Color.yellow;
Gizmos.DrawWireSphere(impactPoint, 0.35f);
}
#endif
#region Validation
/// <summary>
/// Clamps and validates configuration values in the inspector.
/// </summary>
private void OnValidate()
{
speed = Mathf.Max(0f, speed);
maxLifeTime = Mathf.Max(0.01f, maxLifeTime);
explosionRadius = Mathf.Max(0f, explosionRadius);
castRadius = Mathf.Clamp(castRadius, 0.01f, 2f);
entryAngleDownFromHorizontal = Mathf.Clamp(entryAngleDownFromHorizontal, 0f, 89f);
azimuthAimWeight = Mathf.Clamp01(azimuthAimWeight);
}
#endregion Validation
}
}