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); }
}
}
}
}
}