diff --git a/Assets/AI/Demon/SA_CallMeteor.cs b/Assets/AI/Demon/SA_CallMeteor.cs index a363e2cf5..1e4dc7e40 100644 --- a/Assets/AI/Demon/SA_CallMeteor.cs +++ b/Assets/AI/Demon/SA_CallMeteor.cs @@ -7,7 +7,7 @@ using UnityEngine.Playables; namespace DemonBoss.Magic { /// - /// Spawns a meteor behind the BOSS and launches it toward the player's position. + /// 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 @@ -25,8 +25,22 @@ namespace DemonBoss.Magic [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("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)")] @@ -45,6 +59,11 @@ namespace DemonBoss.Magic private Transform _boss; private Transform _target; + // --- Multi-meteor state --- + private int _meteorsSpawned = 0; + + private bool _spawningActive = false; + // --- Playables runtime --- private PlayableGraph _overlayGraph; @@ -61,7 +80,27 @@ namespace DemonBoss.Magic } else if (execType == vFSMComponentExecutionType.OnStateUpdate) { - if (_overlayPlaying && Time.time >= _overlayStopAtTime) StopOverlayWithFade(); + // 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); } } @@ -72,42 +111,111 @@ namespace DemonBoss.Magic if (_target == null) { - if (enableDebug) Debug.LogWarning("[SA_CallMeteor] No target found – abort"); + if (enableDebug) Debug.LogWarning("[SA_CallMeteor] No target found – abort"); return; } - if (enableDebug) Debug.Log($"[SA_CallMeteor] Boss: {_boss.name}, Target: {_target.name}"); + // 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); - // Optional: wait for castDelay before spawning (simple timer via Invoke) - if (castDelay > 0f) - _boss.gameObject.AddComponent().Init(castDelay, SpawnMeteor); + // Start the meteor sequence + if (initialCastDelay > 0f) + _boss.gameObject.AddComponent().Init(initialCastDelay, StartMeteorSequence); else - SpawnMeteor(); + StartMeteorSequence(); } - private void SpawnMeteor() + private void OnExit(vIFSMBehaviourController fsm) { - if (meteorPrefab == null) + // Stop any active spawning + _spawningActive = false; + StopOverlayImmediate(); + + // Clean up any DelayedInvokers attached to the boss + var invokers = _boss?.GetComponents(); + if (invokers != null) { - if (enableDebug) Debug.LogError("[SA_CallMeteor] Missing meteorPrefab"); - return; + 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; - if (enableDebug) Debug.Log($"[SA_CallMeteor] Spawning meteor at: {spawnPos}"); + // 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); @@ -116,12 +224,23 @@ namespace DemonBoss.Magic var meteorScript = meteorGO.GetComponent(); if (meteorScript != null) { - // Set it to target the player's current position + // 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 = _target.position; + meteorScript.overrideImpactPoint = targetPos; meteorScript.snapImpactToGround = true; - if (enableDebug) Debug.Log($"[SA_CallMeteor] Meteor configured to target: {_target.position}"); + if (enableDebug) Debug.Log($"[SA_CallMeteor] Meteor #{_meteorsSpawned + 1} configured to target: {targetPos}"); } else { @@ -129,6 +248,27 @@ namespace DemonBoss.Magic } } + 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; @@ -153,10 +293,15 @@ namespace DemonBoss.Magic _overlayGraph.Play(); _overlayPlaying = true; - float len = overlayClip.length / Mathf.Max(0.0001f, overlaySpeed); - _overlayStopAtTime = Time.time + len; + // Calculate total sequence duration for overlay + float totalSequenceDuration = initialCastDelay + (meteorCount * meteorSpawnInterval) + 2f; // +2s buffer + float overlayDuration = overlayClip.length / Mathf.Max(0.0001f, overlaySpeed); - if (enableDebug) Debug.Log("[SA_CallMeteor] Overlay clip started via Playables"); + // 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() @@ -177,6 +322,15 @@ namespace DemonBoss.Magic 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. /// diff --git a/Assets/AI/FSM/FSM_Demon.asset b/Assets/AI/FSM/FSM_Demon.asset index d78a0bc6c..9878f2e10 100644 --- a/Assets/AI/FSM/FSM_Demon.asset +++ b/Assets/AI/FSM/FSM_Demon.asset @@ -235,9 +235,13 @@ MonoBehaviour: editingName: 0 meteorPrefab: {fileID: 1947871717301538, guid: f99aa3faf46a5f94985344f44aaf21aa, type: 3} - behindBossDistance: 6 + behindBossDistance: 8 aboveBossHeight: 6 - castDelay: 1.5 + meteorCount: 5 + initialCastDelay: 0.4 + meteorSpawnInterval: 1 + muzzleFlashPrefab: {fileID: 0} + muzzleFlashDuration: 1.5 targetTag: Player overlayClip: {fileID: 7400088, guid: 3ef453d7877555243997dba1cdaa2958, type: 3} overlaySpeed: 1 @@ -635,7 +639,7 @@ MonoBehaviour: - decisions: - trueValue: 0 decision: {fileID: 7927421991537792917} - isValid: 0 + isValid: 1 validated: 0 trueState: {fileID: -312774025800194259} falseState: {fileID: 0} @@ -1092,7 +1096,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: a5fc604039227434d8b4e63ebc5e74a5, type: 3} m_Name: FSM_Demon m_EditorClassIdentifier: - selectedNode: {fileID: 9112689765763526057} + selectedNode: {fileID: 4162026404432437805} wantConnection: 0 connectionNode: {fileID: 0} showProperties: 1 @@ -1635,11 +1639,11 @@ MonoBehaviour: - decisions: - trueValue: 0 decision: {fileID: -6379838510941931433} - isValid: 0 + isValid: 1 validated: 0 - trueValue: 1 decision: {fileID: 2998305265418220943} - isValid: 1 + isValid: 0 validated: 0 trueState: {fileID: 4162026404432437805} falseState: {fileID: 0} @@ -1754,14 +1758,14 @@ MonoBehaviour: canEditName: 1 canEditColor: 1 isOpen: 0 - isSelected: 0 + isSelected: 1 nodeRect: serializedVersion: 2 - x: 790 + x: 785 y: 395 width: 150 height: 30 - positionRect: {x: 790, y: 395} + positionRect: {x: 785, y: 395} rectWidth: 150 editingName: 1 nodeColor: {r: 1, g: 1, b: 1, a: 1} @@ -1776,17 +1780,17 @@ MonoBehaviour: muteTrue: 0 muteFalse: 0 transitionType: 0 - transitionDelay: 0.5 + transitionDelay: 5 parentState: {fileID: 4162026404432437805} trueRect: serializedVersion: 2 - x: 865 + x: 860 y: 410 width: 0 height: 0 falseRect: serializedVersion: 2 - x: 865 + x: 860 y: 410 width: 0 height: 0 @@ -1950,7 +1954,7 @@ MonoBehaviour: canEditName: 1 canEditColor: 1 isOpen: 1 - isSelected: 1 + isSelected: 0 nodeRect: serializedVersion: 2 x: 790