using Invector.vCharacterController.AI.FSMBehaviour; using Lean.Pool; using UnityEngine; using UnityEngine.Animations; using UnityEngine.Playables; namespace DemonBoss.Magic { /// /// Spawns a meteor behind the BOSS and launches it 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; [Tooltip("Delay before meteor spawns (wind-up)")] public float castDelay = 0.4f; [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; // --- 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) { if (_overlayPlaying && Time.time >= _overlayStopAtTime) StopOverlayWithFade(); } } 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; } if (enableDebug) Debug.Log($"[SA_CallMeteor] Boss: {_boss.name}, Target: {_target.name}"); // Fire overlay clip (no Animator params) PlayOverlayOnce(_boss); // Optional: wait for castDelay before spawning (simple timer via Invoke) if (castDelay > 0f) _boss.gameObject.AddComponent().Init(castDelay, SpawnMeteor); else SpawnMeteor(); } private void SpawnMeteor() { if (meteorPrefab == null) { if (enableDebug) Debug.LogError("[SA_CallMeteor] Missing meteorPrefab"); return; } if (_boss == null || _target == null) { if (enableDebug) Debug.LogError("[SA_CallMeteor] Missing boss or target reference"); 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; if (enableDebug) Debug.Log($"[SA_CallMeteor] Spawning meteor at: {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) { // Set it to target the player's current position meteorScript.useOverrideImpactPoint = true; meteorScript.overrideImpactPoint = _target.position; meteorScript.snapImpactToGround = true; if (enableDebug) Debug.Log($"[SA_CallMeteor] Meteor configured to target: {_target.position}"); } else { if (enableDebug) Debug.LogError("[SA_CallMeteor] Meteor prefab missing MeteorProjectile component!"); } } // -------- Playables helpers (no Animator params) -------- 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; float len = overlayClip.length / Mathf.Max(0.0001f, overlaySpeed); _overlayStopAtTime = Time.time + len; if (enableDebug) Debug.Log("[SA_CallMeteor] Overlay clip started via Playables"); } 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"); } /// /// 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); } } } } } }