Demon fixes

This commit is contained in:
Szymon Miś
2025-08-29 11:48:10 +02:00
parent b6336703e7
commit 7cccafbb2b
7 changed files with 10359 additions and 334 deletions

View File

@@ -4998,7 +4998,7 @@ CapsuleCollider:
m_Radius: 2.5
m_Height: 2.5
m_Direction: 1
m_Center: {x: 0, y: 1, z: 0}
m_Center: {x: 0, y: 1.5, z: 0}
--- !u!1 &5185508652979790054
GameObject:
m_ObjectHideFlags: 0

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: f99aa3faf46a5f94985344f44aaf21aa
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 100100000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,472 @@
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
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 0d6ec41ae03923f45a963ca66dbb6c56

View File

@@ -6,8 +6,10 @@ using UnityEngine;
namespace DemonBoss.Magic
{
/// <summary>
/// StateAction for Meteor Strike spell - boss summons meteor falling on player
/// First places decal at player position, after 1.5s checks if path is clear and drops rock
/// FSM Action: Boss calls down a meteor.
/// Shows a decal at the player's position, locks an impact point on ground,
/// then spawns the MeteorProjectile prefab above that point after a delay.
/// Cancels cleanly on state exit.
/// </summary>
[CreateAssetMenu(menuName = "Invector/FSM/Actions/DemonBoss/Call Meteor")]
public class SA_CallMeteor : vStateAction
@@ -15,338 +17,128 @@ namespace DemonBoss.Magic
public override string categoryName => "DemonBoss/Magic";
public override string defaultName => "Call Meteor";
[Header("Meteor Configuration")]
[Tooltip("Meteor rock prefab")]
public GameObject rockPrefab;
[Header("Meteor Setup")]
[Tooltip("Prefab with MeteorProjectile component")]
public GameObject meteorPrefab;
[Tooltip("Decal prefab showing impact location")]
[Tooltip("Visual decal prefab marking impact point")]
public GameObject decalPrefab;
[Tooltip("Height from which meteor falls")]
public float meteorHeight = 30f;
[Tooltip("Height above ground at which meteor spawns")]
public float spawnHeight = 40f;
[Tooltip("Delay between placing decal and spawning meteor")]
[Tooltip("Delay before meteor spawns after decal")]
public float castDelay = 1.5f;
[Tooltip("Obstacle check radius above target")]
public float obstacleCheckRadius = 2f;
[Tooltip("Layer mask for obstacles blocking meteor")]
public LayerMask obstacleLayerMask = -1;
[Tooltip("Layer mask for ground")]
public LayerMask groundLayerMask = -1;
[Tooltip("Animator trigger name for meteor summoning animation")]
public string animatorTrigger = "CastMeteor";
[Header("Targeting")]
[Tooltip("Maximum raycast distance to find ground")]
public float maxGroundDistance = 100f;
[Tooltip("Height above ground for obstacle checking")]
public float airCheckHeight = 5f;
[Header("Ground")]
[Tooltip("Layer mask for ground raycast")]
public LayerMask groundMask = -1;
[Header("Debug")]
[Tooltip("Enable debug logging")]
public bool enableDebug = false;
[Tooltip("Show gizmos in Scene View")]
public bool showGizmos = true;
private Transform player;
private GameObject spawnedDecal;
private Vector3 impactPoint;
private Transform playerTransform;
private Vector3 targetPosition;
private bool meteorCasting = false;
private MonoBehaviour coroutineRunner;
private Coroutine _spawnRoutine;
private CoroutineRunner _runner;
/// <summary>
/// Main action execution method called by FSM
/// Entry point for the FSM action, delegates to OnEnter/OnExit depending on execution type.
/// </summary>
public override void DoAction(vIFSMBehaviourController fsmBehaviour, vFSMComponentExecutionType executionType = vFSMComponentExecutionType.OnStateUpdate)
public override void DoAction(vIFSMBehaviourController fsm, vFSMComponentExecutionType executionType = vFSMComponentExecutionType.OnStateUpdate)
{
if (executionType == vFSMComponentExecutionType.OnStateEnter)
{
OnStateEnter(fsmBehaviour);
}
else if (executionType == vFSMComponentExecutionType.OnStateExit)
{
OnStateExit(fsmBehaviour);
}
OnEnter(fsm);
if (executionType == vFSMComponentExecutionType.OnStateExit)
OnExit();
}
/// <summary>
/// Called when entering state - starts meteor casting
/// Acquires the player, locks the impact point on the ground, shows the decal,
/// and starts the delayed meteor spawn coroutine.
/// </summary>
private void OnStateEnter(vIFSMBehaviourController fsmBehaviour)
private void OnEnter(vIFSMBehaviourController fsm)
{
if (enableDebug) Debug.Log("[SA_CallMeteor] Starting meteor casting");
FindPlayer(fsmBehaviour);
var animator = fsmBehaviour.transform.GetComponent<Animator>();
if (animator != null && !string.IsNullOrEmpty(animatorTrigger))
player = GameObject.FindGameObjectWithTag("Player")?.transform;
if (player == null)
{
animator.SetTrigger(animatorTrigger);
if (enableDebug) Debug.Log($"[SA_CallMeteor] Set trigger: {animatorTrigger}");
}
StartMeteorCast(fsmBehaviour);
}
/// <summary>
/// Called when exiting state - cleanup
/// </summary>
private void OnStateExit(vIFSMBehaviourController fsmBehaviour)
{
if (enableDebug) Debug.Log("[SA_CallMeteor] Exiting meteor state");
meteorCasting = false;
}
/// <summary>
/// Finds player transform
/// </summary>
private void FindPlayer(vIFSMBehaviourController fsmBehaviour)
{
GameObject player = GameObject.FindGameObjectWithTag("Player");
if (player != null)
{
playerTransform = player.transform;
if (enableDebug) Debug.Log("[SA_CallMeteor] Player found by tag");
if (enableDebug) Debug.LogWarning("[SA_CallMeteor] No player found!");
return;
}
var aiController = fsmBehaviour as Invector.vCharacterController.AI.vIControlAI;
if (aiController != null && aiController.currentTarget != null)
{
playerTransform = aiController.currentTarget.transform;
if (enableDebug) Debug.Log("[SA_CallMeteor] Player found through AI target");
return;
}
if (enableDebug) Debug.LogWarning("[SA_CallMeteor] Player not found!");
}
/// <summary>
/// Starts meteor casting process
/// </summary>
private void StartMeteorCast(vIFSMBehaviourController fsmBehaviour)
{
if (playerTransform == null)
{
if (enableDebug) Debug.LogWarning("[SA_CallMeteor] No target - finishing state");
return;
}
if (FindGroundUnderPlayer(out Vector3 groundPos))
{
targetPosition = groundPos;
SpawnDecal(targetPosition);
coroutineRunner = fsmBehaviour.transform.GetComponent<MonoBehaviour>();
if (coroutineRunner != null)
{
coroutineRunner.StartCoroutine(MeteorCastCoroutine(fsmBehaviour));
}
// Raycast down from the player to lock the impact point
Vector3 rayStart = player.position + Vector3.up * 5f;
if (Physics.Raycast(rayStart, Vector3.down, out RaycastHit hit, 100f, groundMask, QueryTriggerInteraction.Ignore))
impactPoint = hit.point;
else
{
CastMeteorImmediate();
}
impactPoint = player.position;
meteorCasting = true;
// Spawn decal
if (decalPrefab != null)
spawnedDecal = LeanPool.Spawn(decalPrefab, impactPoint, Quaternion.identity);
if (enableDebug) Debug.Log($"[SA_CallMeteor] Meteor target: {targetPosition}");
}
else
{
if (enableDebug) Debug.LogWarning("[SA_CallMeteor] Cannot find ground under player");
}
// Get or add a dedicated runner for coroutines
var hostGO = fsm.transform.gameObject;
_runner = hostGO.GetComponent<CoroutineRunner>();
if (_runner == null) _runner = hostGO.AddComponent<CoroutineRunner>();
// Start delayed spawn
_spawnRoutine = _runner.StartCoroutine(SpawnMeteorAfterDelay());
}
/// <summary>
/// Finds ground under player using raycast
/// Cancels the pending spawn and cleans up the decal when exiting the state.
/// </summary>
private bool FindGroundUnderPlayer(out Vector3 groundPosition)
private void OnExit()
{
groundPosition = Vector3.zero;
Vector3 playerPos = playerTransform.position;
Vector3 rayStart = playerPos + Vector3.up * 5f;
Ray groundRay = new Ray(rayStart, Vector3.down);
if (Physics.Raycast(groundRay, out RaycastHit hit, maxGroundDistance, groundLayerMask))
if (_runner != null && _spawnRoutine != null)
{
groundPosition = hit.point;
return true;
_runner.StopCoroutine(_spawnRoutine);
_spawnRoutine = null;
}
groundPosition = playerPos;
return true;
}
/// <summary>
/// Spawns decal showing meteor impact location
/// </summary>
private void SpawnDecal(Vector3 position)
{
if (decalPrefab == null)
{
if (enableDebug) Debug.LogWarning("[SA_CallMeteor] Missing decal prefab");
return;
}
Quaternion decalRotation = Quaternion.identity;
Ray surfaceRay = new Ray(position + Vector3.up * 2f, Vector3.down);
if (Physics.Raycast(surfaceRay, out RaycastHit hit, 5f, groundLayerMask))
{
decalRotation = Quaternion.FromToRotation(Vector3.up, hit.normal);
}
spawnedDecal = LeanPool.Spawn(decalPrefab, position, decalRotation);
if (enableDebug) Debug.Log($"[SA_CallMeteor] Decal spawned at: {position}");
}
/// <summary>
/// Coroutine handling meteor casting process with delay
/// </summary>
private IEnumerator MeteorCastCoroutine(vIFSMBehaviourController fsmBehaviour)
{
yield return new WaitForSeconds(castDelay);
if (enableDebug) Debug.Log("[SA_CallMeteor] Checking if path is clear for meteor");
if (IsPathClearForMeteor(targetPosition))
{
SpawnMeteor(targetPosition);
if (enableDebug) Debug.Log("[SA_CallMeteor] Meteor spawned");
}
else
{
if (enableDebug) Debug.Log("[SA_CallMeteor] Path blocked - meteor not spawned");
}
CleanupDecal();
DEC_CheckCooldown.SetCooldownStatic(fsmBehaviour, "Meteor", 20f);
meteorCasting = false;
}
/// <summary>
/// Immediate meteor cast without coroutine (fallback)
/// </summary>
private void CastMeteorImmediate()
{
if (IsPathClearForMeteor(targetPosition))
{
SpawnMeteor(targetPosition);
}
CleanupDecal();
}
/// <summary>
/// Checks if path above target is clear for meteor
/// </summary>
private bool IsPathClearForMeteor(Vector3 targetPos)
{
Vector3 checkStart = targetPos + Vector3.up * airCheckHeight;
Vector3 checkEnd = targetPos + Vector3.up * meteorHeight;
if (Physics.CheckSphere(checkStart, obstacleCheckRadius, obstacleLayerMask))
{
return false;
}
Ray pathRay = new Ray(checkEnd, Vector3.down);
if (Physics.SphereCast(pathRay, obstacleCheckRadius, meteorHeight - airCheckHeight, obstacleLayerMask))
{
return false;
}
return true;
}
/// <summary>
/// Spawns meteor in air above target
/// </summary>
private void SpawnMeteor(Vector3 targetPos)
{
if (rockPrefab == null)
{
Debug.LogError("[SA_CallMeteor] Missing meteor prefab!");
return;
}
Vector3 meteorSpawnPos = targetPos + Vector3.up * meteorHeight;
GameObject meteor = LeanPool.Spawn(rockPrefab, meteorSpawnPos, Quaternion.identity);
Rigidbody meteorRb = meteor.GetComponent<Rigidbody>();
if (meteorRb != null)
{
meteorRb.linearVelocity = Vector3.down * 5f;
meteorRb.angularVelocity = Random.insideUnitSphere * 2f;
}
if (enableDebug) Debug.Log($"[SA_CallMeteor] Meteor spawned at height: {meteorHeight}m");
}
/// <summary>
/// Removes decal from map
/// </summary>
private void CleanupDecal()
{
if (spawnedDecal != null)
{
LeanPool.Despawn(spawnedDecal);
spawnedDecal = null;
if (enableDebug) Debug.Log("[SA_CallMeteor] Decal removed");
}
}
/// <summary>
/// Checks if meteor is currently being cast
/// Waits for the configured cast delay and then spawns the meteor.
/// </summary>
public bool IsCasting()
private IEnumerator SpawnMeteorAfterDelay()
{
return meteorCasting;
yield return new WaitForSeconds(castDelay);
SpawnMeteor();
}
/// <summary>
/// Returns meteor target position
/// Spawns the meteor prefab above the locked impact point and cleans up the decal.
/// </summary>
public Vector3 GetTargetPosition()
private void SpawnMeteor()
{
return targetPosition;
if (meteorPrefab == null) return;
Vector3 spawnPos = impactPoint + Vector3.up * spawnHeight;
LeanPool.Spawn(meteorPrefab, spawnPos, Quaternion.identity);
if (enableDebug) Debug.Log($"[SA_CallMeteor] Meteor spawned at {spawnPos}, impact={impactPoint}");
if (spawnedDecal != null)
{
LeanPool.Despawn(spawnedDecal);
spawnedDecal = null;
}
}
}
/// <summary>
/// Draws gizmos in Scene View for debugging
/// Lightweight helper component dedicated to running coroutines for ScriptableObject actions.
/// </summary>
private void OnDrawGizmosSelected()
{
if (!showGizmos) return;
if (meteorCasting && targetPosition != Vector3.zero)
{
Gizmos.color = Color.red;
Gizmos.DrawWireSphere(targetPosition, 1f);
Vector3 spawnPos = targetPosition + Vector3.up * meteorHeight;
Gizmos.color = Color.yellow;
Gizmos.DrawWireSphere(spawnPos, 0.5f);
Gizmos.color = Color.yellow;
Gizmos.DrawLine(spawnPos, targetPosition);
Vector3 checkPos = targetPosition + Vector3.up * airCheckHeight;
Gizmos.color = Color.blue;
Gizmos.DrawWireSphere(checkPos, obstacleCheckRadius);
}
}
}
public sealed class CoroutineRunner : MonoBehaviour
{ }
}

View File

@@ -112,8 +112,8 @@ MonoBehaviour:
selectedTrue: 0
selectedFalse: 0
cooldownKey: Turret
cooldownTime: 120
availableAtStart: 0
cooldownTime: 75
availableAtStart: 1
enableDebug: 1
--- !u!114 &-7880340678925941764
MonoBehaviour:
@@ -164,7 +164,7 @@ MonoBehaviour:
selectedTrue: 0
selectedFalse: 0
cooldownKey: Meteor
cooldownTime: 25
cooldownTime: 75
availableAtStart: 1
enableDebug: 0
--- !u!114 &-6568372008305276654
@@ -259,23 +259,15 @@ MonoBehaviour:
parentFSM: {fileID: 11400000}
executionType: -1
editingName: 0
rockPrefab: {fileID: 1947871717301538, guid: 9591667a35466484096c6e63785e136c, type: 3}
decalPrefab: {fileID: 1743301284569550, guid: ad2ab49d0a2a17148bd51c93d977bdbc,
meteorPrefab: {fileID: 1947871717301538, guid: f99aa3faf46a5f94985344f44aaf21aa,
type: 3}
meteorHeight: 30
decalPrefab: {fileID: 0}
spawnHeight: 40
castDelay: 1.5
obstacleCheckRadius: 2
obstacleLayerMask:
groundMask:
serializedVersion: 2
m_Bits: 4294967295
groundLayerMask:
serializedVersion: 2
m_Bits: 4294967295
animatorBlockingBool: IsBlocking
maxGroundDistance: 100
airCheckHeight: 5
enableDebug: 1
showGizmos: 1
--- !u!114 &-6379838510941931433
MonoBehaviour:
m_ObjectHideFlags: 1
@@ -345,11 +337,11 @@ MonoBehaviour:
- decisions:
- trueValue: 1
decision: {fileID: 7927421991537792917}
isValid: 1
isValid: 0
validated: 0
- trueValue: 1
decision: {fileID: -1886887719286938116}
isValid: 1
isValid: 0
validated: 0
trueState: {fileID: -2904979146780567904}
falseState: {fileID: 0}
@@ -409,11 +401,11 @@ MonoBehaviour:
- decisions:
- trueValue: 1
decision: {fileID: 4031404829621142413}
isValid: 0
isValid: 1
validated: 0
- trueValue: 1
decision: {fileID: -2866484833343459521}
isValid: 0
isValid: 1
validated: 0
trueState: {fileID: 2986668563461644515}
falseState: {fileID: 0}
@@ -667,7 +659,7 @@ MonoBehaviour:
- decisions:
- trueValue: 0
decision: {fileID: 7927421991537792917}
isValid: 1
isValid: 0
validated: 0
trueState: {fileID: -312774025800194259}
falseState: {fileID: 0}
@@ -775,8 +767,8 @@ MonoBehaviour:
executionType: -1
editingName: 0
crystalPrefab: {fileID: 115880, guid: b5eabfbed738a224284015c33b62e54b, type: 3}
minSpawnDistance: 6
maxSpawnDistance: 15
minSpawnDistance: 10
maxSpawnDistance: 20
obstacleCheckRadius: 1
groundCheckHeight: 2
obstacleLayerMask:
@@ -1286,10 +1278,10 @@ MonoBehaviour:
nodeRect:
serializedVersion: 2
x: 840
y: 415
y: 410
width: 150
height: 62
positionRect: {x: 840, y: 415}
positionRect: {x: 840, y: 410}
rectWidth: 150
editingName: 1
nodeColor: {r: 1, g: 1, b: 1, a: 1}
@@ -1309,13 +1301,13 @@ MonoBehaviour:
trueRect:
serializedVersion: 2
x: 830
y: 445
y: 440
width: 10
height: 10
falseRect:
serializedVersion: 2
x: 990
y: 455
y: 450
width: 10
height: 10
selectedTrue: 0
@@ -1490,7 +1482,7 @@ MonoBehaviour:
- decisions:
- trueValue: 1
decision: {fileID: 7927421991537792917}
isValid: 0
isValid: 1
validated: 0
trueState: {fileID: -2904979146780567904}
falseState: {fileID: 0}
@@ -1554,7 +1546,7 @@ MonoBehaviour:
- decisions:
- trueValue: 0
decision: {fileID: -6379838510941931433}
isValid: 0
isValid: 1
validated: 0
- trueValue: 1
decision: {fileID: -7938248970223304488}
@@ -1590,7 +1582,7 @@ MonoBehaviour:
- decisions:
- trueValue: 0
decision: {fileID: -6379838510941931433}
isValid: 0
isValid: 1
validated: 0
- trueValue: 1
decision: {fileID: 8113515040269600600}
@@ -1712,14 +1704,14 @@ MonoBehaviour:
canSetAsDefault: 1
canEditName: 1
canEditColor: 1
isOpen: 1
isOpen: 0
isSelected: 1
nodeRect:
serializedVersion: 2
x: 840
y: 625
width: 150
height: 62
height: 30
positionRect: {x: 840, y: 625}
rectWidth: 150
editingName: 1
@@ -1739,16 +1731,16 @@ MonoBehaviour:
parentState: {fileID: 4162026404432437805}
trueRect:
serializedVersion: 2
x: 830
y: 655
width: 10
height: 10
x: 915
y: 640
width: 0
height: 0
falseRect:
serializedVersion: 2
x: 990
y: 665
width: 10
height: 10
x: 915
y: 640
width: 0
height: 0
selectedTrue: 0
selectedFalse: 0
trueSideRight: 0
@@ -1868,7 +1860,7 @@ MonoBehaviour:
selectedTrue: 0
selectedFalse: 0
cooldownKey: Shield
cooldownTime: 25
cooldownTime: 75
availableAtStart: 1
enableDebug: 0
--- !u!114 &8860036500635384459
@@ -1908,15 +1900,15 @@ MonoBehaviour:
canSetAsDefault: 1
canEditName: 1
canEditColor: 1
isOpen: 0
isOpen: 1
isSelected: 0
nodeRect:
serializedVersion: 2
x: 845
y: 225
y: 230
width: 150
height: 30
positionRect: {x: 845, y: 225}
height: 62
positionRect: {x: 845, y: 230}
rectWidth: 150
editingName: 1
nodeColor: {r: 1, g: 1, b: 1, a: 1}
@@ -1935,20 +1927,20 @@ MonoBehaviour:
parentState: {fileID: 9112689765763526057}
trueRect:
serializedVersion: 2
x: 920
y: 240
width: 0
height: 0
x: 835
y: 260
width: 10
height: 10
falseRect:
serializedVersion: 2
x: 920
y: 240
width: 0
height: 0
x: 995
y: 270
width: 10
height: 10
selectedTrue: 0
selectedFalse: 0
trueSideRight: 0
falseSideRight: 0
falseSideRight: 1
decisionEditor: {fileID: 0}
isOpen: 0
scroolView: {x: 0, y: 0, z: 0}