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