using Invector.vCharacterController.AI.FSMBehaviour; using Lean.Pool; using System.Collections; using UnityEngine; namespace DemonBoss.Magic { /// /// 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. /// [CreateAssetMenu(menuName = "Invector/FSM/Actions/DemonBoss/Call Meteor")] public class SA_CallMeteor : vStateAction { public override string categoryName => "DemonBoss/Magic"; public override string defaultName => "Call Meteor"; [Header("Meteor Setup")] [Tooltip("Prefab with MeteorProjectile component")] public GameObject meteorPrefab; [Tooltip("Visual decal prefab marking impact point")] public GameObject decalPrefab; [Tooltip("Height above ground at which meteor spawns")] public float spawnHeight = 40f; [Tooltip("Delay before meteor spawns after decal")] public float castDelay = 1.5f; [Header("Ground")] [Tooltip("Layer mask for ground raycast")] public LayerMask groundMask = -1; [Header("Debug")] public bool enableDebug = false; private Transform player; private GameObject spawnedDecal; private Vector3 impactPoint; private Coroutine _spawnRoutine; private CoroutineRunner _runner; /// /// Entry point for the FSM action, delegates to OnEnter/OnExit depending on execution type. /// public override void DoAction(vIFSMBehaviourController fsm, vFSMComponentExecutionType executionType = vFSMComponentExecutionType.OnStateUpdate) { if (executionType == vFSMComponentExecutionType.OnStateEnter) OnEnter(fsm); if (executionType == vFSMComponentExecutionType.OnStateExit) OnExit(); } /// /// Acquires the player, locks the impact point on the ground, shows the decal, /// and starts the delayed meteor spawn coroutine. /// private void OnEnter(vIFSMBehaviourController fsm) { player = GameObject.FindGameObjectWithTag("Player")?.transform; if (player == null) { if (enableDebug) Debug.LogWarning("[SA_CallMeteor] No player found!"); return; } // 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 impactPoint = player.position; // Spawn decal if (decalPrefab != null) spawnedDecal = LeanPool.Spawn(decalPrefab, impactPoint, Quaternion.identity); // Get or add a dedicated runner for coroutines var hostGO = fsm.transform.gameObject; _runner = hostGO.GetComponent(); if (_runner == null) _runner = hostGO.AddComponent(); // Start delayed spawn _spawnRoutine = _runner.StartCoroutine(SpawnMeteorAfterDelay()); } /// /// Cancels the pending spawn and cleans up the decal when exiting the state. /// private void OnExit() { if (_runner != null && _spawnRoutine != null) { _runner.StopCoroutine(_spawnRoutine); _spawnRoutine = null; } if (spawnedDecal != null) { LeanPool.Despawn(spawnedDecal); spawnedDecal = null; } } /// /// Waits for the configured cast delay and then spawns the meteor. /// private IEnumerator SpawnMeteorAfterDelay() { yield return new WaitForSeconds(castDelay); SpawnMeteor(); } /// /// Spawns the meteor prefab above the locked impact point and cleans up the decal. /// private void SpawnMeteor() { 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; } } } /// /// Lightweight helper component dedicated to running coroutines for ScriptableObject actions. /// public sealed class CoroutineRunner : MonoBehaviour { } }