using Invector.vCharacterController.AI.FSMBehaviour; using Lean.Pool; using UnityEngine; using UnityEngine.Animations; using UnityEngine.Playables; namespace DemonBoss.Magic { /// /// Spawns multiple meteors behind the BOSS and launches them toward the player's position. /// [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("Distance behind the BOSS to spawn meteor (meters)")] public float behindBossDistance = 3f; [Tooltip("Height above the BOSS to spawn meteor (meters)")] public float aboveBossHeight = 8f; [Header("Multi-Meteor Configuration")] [Tooltip("Number of meteors to spawn in sequence")] public int meteorCount = 5; [Tooltip("Delay before first meteor spawns (wind-up)")] public float initialCastDelay = 0.4f; [Tooltip("Time between each meteor spawn")] public float meteorSpawnInterval = 0.6f; [Header("Muzzle Flash Effect")] [Tooltip("Particle effect prefab for muzzle flash at spawn position")] public GameObject muzzleFlashPrefab; [Tooltip("Duration to keep muzzle flash alive (seconds)")] public float muzzleFlashDuration = 1.5f; [Header("Targeting")] [Tooltip("Tag used to find the target (usually Player)")] public string targetTag = "Player"; [Header("One-off Overlay Clip (No Animator Params)")] public AnimationClip overlayClip; [Tooltip("Playback speed (1 = normal)")] public float overlaySpeed = 1f; [Tooltip("Blend-in seconds (instant in this minimal impl)")] public float overlayFadeIn = 0.10f; [Tooltip("Blend-out seconds (instant in this minimal impl)")] public float overlayFadeOut = 0.10f; [Header("Debug")] public bool enableDebug = false; private Transform _boss; private Transform _target; // --- Multi-meteor state --- private int _meteorsSpawned = 0; private bool _spawningActive = false; // --- Playables runtime --- private PlayableGraph _overlayGraph; private AnimationPlayableOutput _overlayOutput; private AnimationClipPlayable _overlayPlayable; private bool _overlayPlaying; private float _overlayStopAtTime; public override void DoAction(vIFSMBehaviourController fsm, vFSMComponentExecutionType execType = vFSMComponentExecutionType.OnStateUpdate) { if (execType == vFSMComponentExecutionType.OnStateEnter) { OnEnter(fsm); } else if (execType == vFSMComponentExecutionType.OnStateUpdate) { // Keep the state active until all meteors are spawned if (_spawningActive && _meteorsSpawned < meteorCount) { // Don't allow the state to exit while spawning return; } if (_overlayPlaying && Time.time >= _overlayStopAtTime) { StopOverlayWithFade(); } // Only signal completion when all meteors are done AND overlay is finished if (!_spawningActive && !_overlayPlaying && _meteorsSpawned >= meteorCount) { if (enableDebug) Debug.Log($"[SA_CallMeteor] Sequence complete, ready to exit state"); } } else if (execType == vFSMComponentExecutionType.OnStateExit) { OnExit(fsm); } } private void OnEnter(vIFSMBehaviourController fsm) { _boss = fsm.transform; _target = GameObject.FindGameObjectWithTag(targetTag)?.transform; if (_target == null) { if (enableDebug) Debug.LogWarning("[SA_CallMeteor] No target found – abort"); return; } // Reset multi-meteor state _meteorsSpawned = 0; _spawningActive = true; // SET COOLDOWN IMMEDIATELY when meteor ability is used DEC_CheckCooldown.SetCooldownStatic(fsm, "Meteor", 80f); if (enableDebug) Debug.Log($"[SA_CallMeteor] Boss: {_boss.name}, Target: {_target.name}, Count: {meteorCount}"); // Fire overlay clip (no Animator params) PlayOverlayOnce(_boss); // Start the meteor sequence if (initialCastDelay > 0f) _boss.gameObject.AddComponent().Init(initialCastDelay, StartMeteorSequence); else StartMeteorSequence(); } private void OnExit(vIFSMBehaviourController fsm) { // Stop any active spawning _spawningActive = false; StopOverlayImmediate(); // Clean up any DelayedInvokers attached to the boss var invokers = _boss?.GetComponents(); if (invokers != null) { foreach (var invoker in invokers) { if (invoker != null) Object.Destroy(invoker); } } if (enableDebug) Debug.Log($"[SA_CallMeteor] State exited. Spawned {_meteorsSpawned}/{meteorCount} meteors"); } private void StartMeteorSequence() { if (!_spawningActive) return; SpawnNextMeteor(); } private void SpawnNextMeteor() { if (!_spawningActive || _meteorsSpawned >= meteorCount) return; if (_boss == null || _target == null) { if (enableDebug) Debug.LogError("[SA_CallMeteor] Missing boss or target reference"); return; } SpawnSingleMeteor(); _meteorsSpawned++; if (enableDebug) Debug.Log($"[SA_CallMeteor] Spawned meteor {_meteorsSpawned}/{meteorCount}"); // Schedule next meteor if needed if (_meteorsSpawned < meteorCount && _spawningActive) { if (enableDebug) Debug.Log($"[SA_CallMeteor] Scheduling next meteor in {meteorSpawnInterval}s"); _boss.gameObject.AddComponent().Init(meteorSpawnInterval, SpawnNextMeteor); } else { // All meteors spawned _spawningActive = false; if (enableDebug) Debug.Log("[SA_CallMeteor] All meteors spawned, sequence complete"); } } private void SpawnSingleMeteor() { if (meteorPrefab == null) { if (enableDebug) Debug.LogError("[SA_CallMeteor] Missing meteorPrefab"); return; } // Calculate spawn position: behind the BOSS + height Vector3 bossForward = _boss.forward.normalized; Vector3 behindBoss = _boss.position - (bossForward * behindBossDistance); Vector3 spawnPos = behindBoss + Vector3.up * aboveBossHeight; // Add slight randomization to spawn position for multiple meteors if (_meteorsSpawned > 0) { Vector3 randomOffset = new Vector3( Random.Range(-1f, 1f), Random.Range(-0.5f, 0.5f), Random.Range(-1f, 1f) ); spawnPos += randomOffset; } if (enableDebug) Debug.Log($"[SA_CallMeteor] Spawning meteor #{_meteorsSpawned + 1} at: {spawnPos}"); // Spawn muzzle flash effect first SpawnMuzzleFlash(spawnPos); // Spawn the meteor var meteorGO = LeanPool.Spawn(meteorPrefab, spawnPos, Quaternion.identity); // Configure the projectile to target the player var meteorScript = meteorGO.GetComponent(); if (meteorScript != null) { // Update target position for each meteor (player might be moving) Vector3 targetPos = _target.position; // Add slight prediction/leading for moving targets var playerRigidbody = _target.GetComponent(); if (playerRigidbody != null) { Vector3 playerVelocity = playerRigidbody.linearVelocity; float estimatedFlightTime = 2f; // rough estimate targetPos += playerVelocity * estimatedFlightTime * 0.5f; // partial leading } meteorScript.useOverrideImpactPoint = true; meteorScript.overrideImpactPoint = targetPos; meteorScript.snapImpactToGround = true; if (enableDebug) Debug.Log($"[SA_CallMeteor] Meteor #{_meteorsSpawned + 1} configured to target: {targetPos}"); } else { if (enableDebug) Debug.LogError("[SA_CallMeteor] Meteor prefab missing MeteorProjectile component!"); } } private void SpawnMuzzleFlash(Vector3 position) { if (muzzleFlashPrefab == null) return; var muzzleFlash = LeanPool.Spawn(muzzleFlashPrefab, position, Quaternion.identity); if (muzzleFlashDuration > 0f) { // Auto-despawn muzzle flash after duration _boss.gameObject.AddComponent().Init(muzzleFlashDuration, () => { if (muzzleFlash != null) { LeanPool.Despawn(muzzleFlash); } }); } if (enableDebug) Debug.Log($"[SA_CallMeteor] Muzzle flash spawned at: {position}"); } private void PlayOverlayOnce(Transform owner) { if (overlayClip == null) return; var animator = owner.GetComponent(); if (animator == null) return; StopOverlayImmediate(); _overlayGraph = PlayableGraph.Create("ActionOverlay(CallMeteor)"); _overlayGraph.SetTimeUpdateMode(DirectorUpdateMode.GameTime); _overlayPlayable = AnimationClipPlayable.Create(_overlayGraph, overlayClip); _overlayPlayable.SetApplyFootIK(false); _overlayPlayable.SetApplyPlayableIK(false); _overlayPlayable.SetSpeed(Mathf.Max(0.0001f, overlaySpeed)); _overlayOutput = AnimationPlayableOutput.Create(_overlayGraph, "AnimOut", animator); _overlayOutput.SetSourcePlayable(_overlayPlayable); _overlayOutput.SetWeight(1f); _overlayGraph.Play(); _overlayPlaying = true; // Calculate total sequence duration for overlay float totalSequenceDuration = initialCastDelay + (meteorCount * meteorSpawnInterval) + 2f; // +2s buffer float overlayDuration = overlayClip.length / Mathf.Max(0.0001f, overlaySpeed); // Use the longer of the two durations, ensuring overlay covers entire sequence float finalDuration = Mathf.Max(overlayDuration, totalSequenceDuration); _overlayStopAtTime = Time.time + finalDuration; if (enableDebug) Debug.Log($"[SA_CallMeteor] Overlay clip started via Playables, duration: {finalDuration:F1}s (sequence: {totalSequenceDuration:F1}s, clip: {overlayDuration:F1}s)"); } private void StopOverlayImmediate() { if (_overlayGraph.IsValid()) { _overlayGraph.Stop(); _overlayGraph.Destroy(); } _overlayPlaying = false; } private void StopOverlayWithFade() { if (!_overlayPlaying) { StopOverlayImmediate(); return; } if (_overlayOutput.IsOutputNull() == false) _overlayOutput.SetWeight(0f); StopOverlayImmediate(); if (enableDebug) Debug.Log("[SA_CallMeteor] Overlay clip stopped"); } // Public methods for external monitoring public bool IsSequenceActive() => _spawningActive; public int GetMeteorsSpawned() => _meteorsSpawned; public int GetTotalMeteorCount() => meteorCount; public float GetSequenceProgress() => meteorCount > 0 ? (float)_meteorsSpawned / meteorCount : 1f; /// /// Tiny helper MonoBehaviour to delay a callback without coroutines here. /// private sealed class DelayedInvoker : MonoBehaviour { private float _timeLeft; private System.Action _callback; public void Init(float delay, System.Action callback) { _timeLeft = delay; _callback = callback; } private void Update() { _timeLeft -= Time.deltaTime; if (_timeLeft <= 0f) { try { _callback?.Invoke(); } finally { Destroy(this); } } } } } }