diff --git a/Assets/AI/Demon/SA_CallMeteor.cs b/Assets/AI/Demon/SA_CallMeteor.cs
index dc397bd07..d1e358377 100644
--- a/Assets/AI/Demon/SA_CallMeteor.cs
+++ b/Assets/AI/Demon/SA_CallMeteor.cs
@@ -1,12 +1,13 @@
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
- /// Similar mechanics to FireballProjectile but coming from above
+ /// 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
@@ -31,18 +32,37 @@ namespace DemonBoss.Magic
[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)
@@ -58,7 +78,14 @@ namespace DemonBoss.Magic
if (enableDebug) Debug.Log($"[SA_CallMeteor] Boss: {_boss.name}, Target: {_target.name}");
- SpawnMeteor();
+ // 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()
@@ -101,5 +128,79 @@ namespace DemonBoss.Magic
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); }
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/Assets/AI/Demon/SA_CastShield.cs b/Assets/AI/Demon/SA_CastShield.cs
index ea1789c72..3d96a678a 100644
--- a/Assets/AI/Demon/SA_CastShield.cs
+++ b/Assets/AI/Demon/SA_CastShield.cs
@@ -1,187 +1,376 @@
using Invector.vCharacterController.AI.FSMBehaviour;
using Lean.Pool;
using UnityEngine;
+using UnityEngine.Animations;
+using UnityEngine.Playables;
namespace DemonBoss.Magic
{
///
- /// StateAction for Magic Shield spell - boss casts magical shield for 5 seconds
- /// During casting boss stands still, after completion returns to Combat
+ /// Magic Shield
+ /// Spawns shield FX, holds for 'shieldDuration', then plays End and cleans up.
///
- [CreateAssetMenu(menuName = "Invector/FSM/Actions/DemonBoss/Cast Shield")]
+ [CreateAssetMenu(menuName = "Invector/FSM/Actions/DemonBoss/Cast Shield (Start/Keep/End)")]
public class SA_CastShield : vStateAction
{
public override string categoryName => "DemonBoss/Magic";
public override string defaultName => "Cast Shield";
- [Header("Shield Configuration")]
+ [Header("Shield Logic")]
[Tooltip("Prefab with magical shield particle effect")]
public GameObject shieldFXPrefab;
- [Tooltip("Shield duration in seconds")]
+ [Tooltip("Shield duration in seconds (time spent in Keep phase before End)")]
public float shieldDuration = 5f;
- [Tooltip("Animator bool parameter name for blocking state")]
+ [Tooltip("Animator bool parameter name for blocking state (optional)")]
public string animatorBlockingBool = "IsBlocking";
+ [Header("Animation Clips (no Animator params)")]
+ [Tooltip("One-shot intro")]
+ public AnimationClip startClip;
+
+ [Tooltip("Looping hold/maintain")]
+ public AnimationClip keepClip;
+
+ [Tooltip("One-shot outro")]
+ public AnimationClip endClip;
+
+ [Header("Playback & Fades")]
+ [Tooltip("Global playback speed for all clips")]
+ public float clipSpeed = 1f;
+
+ [Tooltip("Seconds to crossfade between phases")]
+ public float crossfadeTime = 0.12f;
+
+ [Tooltip("If the state exits early, still play End quickly before full teardown")]
+ public bool playEndOnEarlyExit = true;
+
[Header("Debug")]
- [Tooltip("Enable debug logging")]
- public bool enableDebug = false;
+ public bool debugLogs = false;
- private GameObject spawnedShield;
- private Animator npcAnimator;
- private Transform npcTransform;
- private float shieldStartTime;
- private bool shieldActive = false;
+ // --- Runtime (shield/FX) ---
+ private Transform _npc;
- ///
- /// Main action execution method called by FSM
- ///
- public override void DoAction(vIFSMBehaviourController fsmBehaviour, vFSMComponentExecutionType executionType = vFSMComponentExecutionType.OnStateUpdate)
+ private Animator _anim;
+ private GameObject _spawnedShieldFX;
+ private float _shieldStartTime;
+ private bool _shieldActive;
+
+ // --- Playables graph state ---
+ private PlayableGraph _graph;
+
+ private AnimationPlayableOutput _output;
+ private AnimationMixerPlayable _mixer; // 2-way mixer for crossfades
+
+ private AnimationClipPlayable _pStart;
+ private AnimationClipPlayable _pKeep;
+ private AnimationClipPlayable _pEnd;
+
+ private enum Phase
+ { None, Start, Keep, End, Done }
+
+ private Phase _phase = Phase.None;
+
+ // crossfade bookkeeping
+ private int _fromInput = -1; // 0 or 1
+
+ private int _toInput = -1; // 0 or 1
+ private float _fadeT0; // Time.time at fade start
+ private float _fadeDur; // seconds
+ private bool _fading;
+
+ // which clip is bound to each input
+ private AnimationClipPlayable _input0;
+
+ private AnimationClipPlayable _input1;
+
+ // timers for auto-advance
+ private float _phaseScheduledEnd = 0f; // absolute Time.time when Start/End should be done
+
+ public override void DoAction(vIFSMBehaviourController fsm, vFSMComponentExecutionType execType = vFSMComponentExecutionType.OnStateUpdate)
{
- if (executionType == vFSMComponentExecutionType.OnStateEnter)
+ if (execType == vFSMComponentExecutionType.OnStateEnter) OnEnter(fsm);
+ else if (execType == vFSMComponentExecutionType.OnStateUpdate) OnUpdate(fsm);
+ else if (execType == vFSMComponentExecutionType.OnStateExit) OnExit(fsm);
+ }
+
+ // ------------------ FSM Hooks ------------------
+
+ private void OnEnter(vIFSMBehaviourController fsm)
+ {
+ _npc = fsm.transform;
+ _anim = _npc.GetComponent();
+
+ if (_anim != null && !string.IsNullOrEmpty(animatorBlockingBool))
+ _anim.SetBool(animatorBlockingBool, true);
+
+ SpawnShieldFX();
+
+ _shieldStartTime = Time.time;
+ _shieldActive = true;
+
+ BuildGraphIfNeeded();
+
+ if (startClip != null)
{
- OnStateEnter(fsmBehaviour);
+ EnterStart();
}
- else if (executionType == vFSMComponentExecutionType.OnStateUpdate)
+ else if (keepClip != null)
{
- OnStateUpdate(fsmBehaviour);
+ EnterKeep();
}
- else if (executionType == vFSMComponentExecutionType.OnStateExit)
+ else
{
- OnStateExit(fsmBehaviour);
+ if (debugLogs) Debug.Log("[SA_CastShield] No Start/Keep clips; waiting to End.");
+ _phase = Phase.Keep; // logical keep (no anim)
}
}
- ///
- /// Called when entering state - starts shield casting
- ///
- private void OnStateEnter(vIFSMBehaviourController fsmBehaviour)
+ private void OnUpdate(vIFSMBehaviourController fsm)
{
- if (enableDebug) Debug.Log("[SA_CastShield] Entering shield casting state");
-
- // Store NPC references
- npcTransform = fsmBehaviour.transform;
- npcAnimator = npcTransform.GetComponent();
-
- var aiController = fsmBehaviour as Invector.vCharacterController.AI.vIControlAI;
- if (aiController != null)
+ // handle crossfade
+ if (_fading)
{
- aiController.Stop();
- if (enableDebug) Debug.Log("[SA_CastShield] AI stopped");
+ float u = Mathf.Clamp01((Time.time - _fadeT0) / Mathf.Max(0.0001f, _fadeDur));
+ if (_fromInput >= 0) _mixer.SetInputWeight(_fromInput, 1f - u);
+ if (_toInput >= 0) _mixer.SetInputWeight(_toInput, u);
+ if (u >= 1f) _fading = false;
}
- if (npcAnimator != null && !string.IsNullOrEmpty(animatorBlockingBool))
+ // auto-advance phases based on timers
+ if (_phase == Phase.Start && Time.time >= _phaseScheduledEnd)
{
- npcAnimator.SetBool(animatorBlockingBool, true);
- if (enableDebug) Debug.Log($"[SA_CastShield] Set bool: {animatorBlockingBool} = true");
+ if (keepClip != null) CrossfadeToKeep();
+ else BeginEnd(); // no keep; go straight to End window
}
-
- SpawnShieldEffect(fsmBehaviour);
-
- shieldStartTime = Time.time;
- shieldActive = true;
- }
-
- ///
- /// Called every frame during state duration
- ///
- private void OnStateUpdate(vIFSMBehaviourController fsmBehaviour)
- {
- if (shieldActive && Time.time - shieldStartTime >= shieldDuration)
+ else if (_phase == Phase.Keep)
{
- if (enableDebug) Debug.Log("[SA_CastShield] Shield time passed, finishing state");
- FinishShield(fsmBehaviour);
+ // When shield timer is up, begin End
+ if (_shieldActive && (Time.time - _shieldStartTime) >= shieldDuration)
+ {
+ BeginEnd();
+ }
+ }
+ else if (_phase == Phase.End && Time.time >= _phaseScheduledEnd)
+ {
+ // End completed
+ _phase = Phase.Done;
+ TeardownGraph();
+ CleanupShieldFX();
+ _shieldActive = false;
}
}
- ///
- /// Called when exiting state - cleanup
- ///
- private void OnStateExit(vIFSMBehaviourController fsmBehaviour)
+ private void OnExit(vIFSMBehaviourController fsm)
{
- if (enableDebug) Debug.Log("[SA_CastShield] Exiting shield state");
+ if (_anim != null && !string.IsNullOrEmpty(animatorBlockingBool))
+ _anim.SetBool(animatorBlockingBool, false);
- if (npcAnimator != null && !string.IsNullOrEmpty(animatorBlockingBool))
+ // If we left early, optionally play End briefly (best-effort)
+ if (playEndOnEarlyExit && _phase != Phase.End && _phase != Phase.Done && endClip != null)
{
- npcAnimator.SetBool(animatorBlockingBool, false);
- if (enableDebug) Debug.Log($"[SA_CastShield] Set bool: {animatorBlockingBool} = false");
+ if (debugLogs) Debug.Log("[SA_CastShield] Early exit: playing End quickly.");
+ // build graph if it was never built (e.g., no Start/Keep)
+ BuildGraphIfNeeded();
+ CrossfadeTo(endClip, out _pEnd, quick: true);
+ _phase = Phase.End;
+ _phaseScheduledEnd = Time.time + Mathf.Min(endClip.length / SafeSpeed(), 0.25f); // quick outro
+ }
+ else
+ {
+ // otherwise normal cleanup
+ TeardownGraph();
}
- if (shieldActive)
- {
- CleanupShield();
- }
-
- var aiController = fsmBehaviour as Invector.vCharacterController.AI.vIControlAI;
- if (aiController != null)
- {
- if (enableDebug) Debug.Log("[SA_CastShield] AI resumed");
- }
+ CleanupShieldFX();
+ _shieldActive = false;
}
- ///
- /// Spawns magical shield particle effect
- ///
- private void SpawnShieldEffect(vIFSMBehaviourController fsmBehaviour)
+ // ------------------ Phase Transitions ------------------
+
+ private void EnterStart()
{
- if (shieldFXPrefab == null)
+ CrossfadeTo(startClip, out _pStart, quick: false);
+ _phase = Phase.Start;
+ _phaseScheduledEnd = Time.time + (startClip.length / SafeSpeed());
+ if (debugLogs) Debug.Log("[SA_CastShield] Start phase.");
+ }
+
+ private void CrossfadeToKeep()
+ {
+ if (keepClip == null)
{
- Debug.LogWarning("[SA_CastShield] Missing shieldFXPrefab!");
+ BeginEnd();
return;
}
-
- // Spawn shield at NPC's position and rotation
- Vector3 spawnPosition = npcTransform.position;
- Quaternion spawnRotation = npcTransform.rotation;
-
- spawnedShield = LeanPool.Spawn(shieldFXPrefab, spawnPosition, spawnRotation);
-
- if (enableDebug) Debug.Log($"[SA_CastShield] Shield spawned at NPC position: {spawnPosition}");
+ CrossfadeTo(keepClip, out _pKeep, quick: false, loop: true);
+ _phase = Phase.Keep;
+ if (debugLogs) Debug.Log("[SA_CastShield] Switched to Keep (loop).");
}
- ///
- /// Finishes shield operation and transitions to next state
- ///
- private void FinishShield(vIFSMBehaviourController fsmBehaviour)
+ private void EnterKeep()
{
- shieldActive = false;
- CleanupShield();
-
- DEC_CheckCooldown.SetCooldownStatic(fsmBehaviour, "Shield", 15f);
-
- // End state - FSM will transition to next state
- // FYI: In Invector FSM, state completion is handled automatically
+ CrossfadeTo(keepClip, out _pKeep, quick: false, loop: true);
+ _phase = Phase.Keep;
+ if (debugLogs) Debug.Log("[SA_CastShield] Entered Keep directly.");
}
- ///
- /// Cleans up spawned shield
- ///
- private void CleanupShield()
+ private void BeginEnd()
{
- if (spawnedShield != null)
+ if (endClip == null)
{
- LeanPool.Despawn(spawnedShield);
- spawnedShield = null;
- if (enableDebug) Debug.Log("[SA_CastShield] Shield despawned");
+ // No end clip; just finish
+ _phase = Phase.Done;
+ TeardownGraph();
+ CleanupShieldFX();
+ _shieldActive = false;
+ if (debugLogs) Debug.Log("[SA_CastShield] No End clip; finished.");
+ return;
+ }
+ CrossfadeTo(endClip, out _pEnd, quick: false);
+ _phase = Phase.End;
+ _phaseScheduledEnd = Time.time + (endClip.length / SafeSpeed());
+ if (debugLogs) Debug.Log("[SA_CastShield] End phase.");
+ }
+
+ // ------------------ Graph Setup / Crossfade ------------------
+
+ private void BuildGraphIfNeeded()
+ {
+ if (_graph.IsValid()) return;
+
+ if (_anim == null)
+ {
+ _anim = _npc ? _npc.GetComponent() : null;
+ if (_anim == null)
+ {
+ if (debugLogs) Debug.LogWarning("[SA_CastShield] No Animator found; animation disabled.");
+ return;
+ }
+ }
+
+ _graph = PlayableGraph.Create("ShieldOverlayGraph");
+ _graph.SetTimeUpdateMode(DirectorUpdateMode.GameTime);
+
+ _mixer = AnimationMixerPlayable.Create(_graph, 2); // 2-way mixer
+ _mixer.SetInputWeight(0, 0f);
+ _mixer.SetInputWeight(1, 0f);
+
+ _output = AnimationPlayableOutput.Create(_graph, "AnimOut", _anim);
+ _output.SetSourcePlayable(_mixer);
+ _output.SetWeight(1f);
+
+ _graph.Play();
+ }
+
+ private void TeardownGraph()
+ {
+ if (_graph.IsValid())
+ {
+ _graph.Stop();
+ _graph.Destroy();
+ }
+ _input0 = default;
+ _input1 = default;
+ _fromInput = _toInput = -1;
+ _fading = false;
+ }
+
+ private float SafeSpeed() => Mathf.Max(0.0001f, clipSpeed);
+
+ ///
+ /// Fades from whatever is currently audible to the given clip.
+ /// Reuses the 2 inputs; swaps playables as needed.
+ ///
+ private void CrossfadeTo(AnimationClip clip,
+ out AnimationClipPlayable playableOut,
+ bool quick,
+ bool loop = false)
+ {
+ playableOut = default;
+
+ if (!_graph.IsValid() || clip == null) return;
+
+ float speed = SafeSpeed();
+
+ // Prepare or reuse a slot (two inputs that we swap between)
+ // Choose the silent input as target; if none is silent, pick the opposite of the currently "from".
+ int targetInput = (_mixer.GetInputWeight(0) < 0.0001f) ? 0 : (_mixer.GetInputWeight(1) < 0.0001f) ? 1 : (_toInput == 0 ? 1 : 0);
+
+ // Destroy existing playable on that input if any
+ var currentPlayableOnTarget = (AnimationClipPlayable)_mixer.GetInput(targetInput);
+ if (currentPlayableOnTarget.IsValid())
+ {
+ _graph.Disconnect(_mixer, targetInput);
+ currentPlayableOnTarget.Destroy();
+ }
+
+ // Create new clip playable
+ var newPlayable = AnimationClipPlayable.Create(_graph, clip);
+ newPlayable.SetApplyFootIK(false);
+ newPlayable.SetApplyPlayableIK(false);
+ newPlayable.SetSpeed(speed);
+
+ // Connect to mixer
+ _graph.Connect(newPlayable, 0, _mixer, targetInput);
+ _mixer.SetInputWeight(targetInput, 0f);
+
+ // Cache which playable is on which slot for optional debug
+ if (targetInput == 0) _input0 = newPlayable; else _input1 = newPlayable;
+
+ // Determine current audible input to fade from
+ int sourceInput = (targetInput == 0) ? 1 : 0;
+ float wSource = _mixer.GetInputWeight(sourceInput);
+ bool hasSource = wSource > 0.0001f && _mixer.GetInput(sourceInput).IsValid();
+
+ // Start from beginning for new phase
+ newPlayable.SetTime(0);
+ playableOut = newPlayable;
+
+ // Configure fade
+ _fromInput = hasSource ? sourceInput : -1;
+ _toInput = targetInput;
+ _fadeDur = Mathf.Max(0f, quick ? Mathf.Min(0.06f, crossfadeTime) : crossfadeTime);
+ _fadeT0 = Time.time;
+ _fading = _fadeDur > 0f && hasSource;
+
+ // Set immediate weights if not fading
+ if (!_fading)
+ {
+ if (_fromInput >= 0) _mixer.SetInputWeight(_fromInput, 0f);
+ _mixer.SetInputWeight(_toInput, 1f);
}
}
- ///
- /// Checks if shield is currently active
- ///
- public bool IsShieldActive()
+ // ------------------ FX Helpers ------------------
+
+ private void SpawnShieldFX()
{
- return shieldActive;
+ if (shieldFXPrefab == null) return;
+ _spawnedShieldFX = LeanPool.Spawn(shieldFXPrefab, _npc.position, _npc.rotation);
+ if (debugLogs) Debug.Log("[SA_CastShield] Shield FX spawned.");
}
- ///
- /// Returns remaining shield time
- ///
+ private void CleanupShieldFX()
+ {
+ if (_spawnedShieldFX != null)
+ {
+ LeanPool.Despawn(_spawnedShieldFX);
+ _spawnedShieldFX = null;
+ if (debugLogs) Debug.Log("[SA_CastShield] Shield FX despawned.");
+ }
+ }
+
+ // ------------------ Public Query ------------------
+
+ public bool IsShieldActive() => _shieldActive;
+
public float GetRemainingShieldTime()
{
- if (!shieldActive) return 0f;
- return Mathf.Max(0f, shieldDuration - (Time.time - shieldStartTime));
+ if (!_shieldActive) return 0f;
+ float t = Mathf.Max(0f, shieldDuration - (Time.time - _shieldStartTime));
+ return (_phase == Phase.End || _phase == Phase.Done) ? 0f : t;
}
}
}
\ No newline at end of file
diff --git a/Assets/AI/Demon/SA_SpawnTurretSmart.cs b/Assets/AI/Demon/SA_SpawnTurretSmart.cs
index c00440bc0..31c8d2f51 100644
--- a/Assets/AI/Demon/SA_SpawnTurretSmart.cs
+++ b/Assets/AI/Demon/SA_SpawnTurretSmart.cs
@@ -1,13 +1,14 @@
using Invector.vCharacterController.AI.FSMBehaviour;
using Lean.Pool;
using UnityEngine;
+using UnityEngine.Animations;
+using UnityEngine.Playables;
namespace DemonBoss.Magic
{
///
/// Spawns exactly 3 crystal turrets around the opponent.
- /// Now with per-spawn randomness: global rotation offset and per-turret angle/radius jitter.
- /// Validates positions (ground/obstacles) and enforces minimum separation.
+ /// Now with per-spawn randomness and position validation.
///
[CreateAssetMenu(menuName = "Invector/FSM/Actions/DemonBoss/Spawn 3 Turrets Radial")]
public class SA_SpawnTurretSmart : vStateAction
@@ -43,6 +44,13 @@ namespace DemonBoss.Magic
[Tooltip("Per-turret radius jitter (meters). 0 = disabled.")]
public float perTurretRadiusJitter = 0.75f;
+ [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;
@@ -52,11 +60,25 @@ namespace DemonBoss.Magic
private Transform npcTransform;
private Transform playerTransform;
- private readonly System.Collections.Generic.List _lastPlanned = new System.Collections.Generic.List(3);
+ private readonly System.Collections.Generic.List _lastPlanned =
+ new System.Collections.Generic.List(3);
+
+ // --- Playables runtime ---
+ private PlayableGraph _overlayGraph;
+
+ private AnimationPlayableOutput _overlayOutput;
+ private AnimationClipPlayable _overlayPlayable;
+ private bool _overlayPlaying;
+ private float _overlayStopAtTime;
public override void DoAction(vIFSMBehaviourController fsmBehaviour, vFSMComponentExecutionType executionType = vFSMComponentExecutionType.OnStateUpdate)
{
if (executionType == vFSMComponentExecutionType.OnStateEnter) OnStateEnter(fsmBehaviour);
+ else if (executionType == vFSMComponentExecutionType.OnStateUpdate)
+ {
+ // Auto-stop overlay when finished
+ if (_overlayPlaying && Time.time >= _overlayStopAtTime) StopOverlayWithFade();
+ }
else if (executionType == vFSMComponentExecutionType.OnStateExit) OnStateExit(fsmBehaviour);
}
@@ -70,6 +92,8 @@ namespace DemonBoss.Magic
if (npcAnimator != null && !string.IsNullOrEmpty(animatorBlockingBool))
npcAnimator.SetBool(animatorBlockingBool, true);
+ PlayOverlayOnce(npcTransform);
+
SpawnThreeTurretsRadial(fsmBehaviour);
DEC_CheckCooldown.SetCooldownStatic(fsmBehaviour, "Turret", 12f);
@@ -79,6 +103,8 @@ namespace DemonBoss.Magic
{
if (npcAnimator != null && !string.IsNullOrEmpty(animatorBlockingBool))
npcAnimator.SetBool(animatorBlockingBool, false);
+
+ StopOverlayWithFade();
}
private void FindPlayer(vIFSMBehaviourController fsmBehaviour)
@@ -270,5 +296,54 @@ namespace DemonBoss.Magic
prevPoint = newPoint;
}
}
+
+ private void PlayOverlayOnce(Transform owner)
+ {
+ if (overlayClip == null) return;
+
+ if (npcAnimator == null)
+ npcAnimator = owner.GetComponent();
+ if (npcAnimator == null) return;
+
+ StopOverlayImmediate(); // safety
+
+ _overlayGraph = PlayableGraph.Create("ActionOverlay(SpawnTurret)");
+ _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", npcAnimator);
+ _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_SpawnTurretSmart] 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_SpawnTurretSmart] Overlay clip stopped");
+ }
}
}
\ No newline at end of file
diff --git a/Assets/AI/FSM/FSM_Demon.asset b/Assets/AI/FSM/FSM_Demon.asset
index 8605f614b..45c8a25e4 100644
--- a/Assets/AI/FSM/FSM_Demon.asset
+++ b/Assets/AI/FSM/FSM_Demon.asset
@@ -134,7 +134,13 @@ MonoBehaviour:
type: 3}
shieldDuration: 10
animatorBlockingBool: IsBlocking
- enableDebug: 1
+ startClip: {fileID: 7400082, guid: 3ef453d7877555243997dba1cdaa2958, type: 3}
+ keepClip: {fileID: 7400084, guid: 3ef453d7877555243997dba1cdaa2958, type: 3}
+ endClip: {fileID: 7400086, guid: 3ef453d7877555243997dba1cdaa2958, type: 3}
+ clipSpeed: 1
+ crossfadeTime: 0.12
+ playEndOnEarlyExit: 1
+ debugLogs: 0
--- !u!114 &-6568372008305276654
MonoBehaviour:
m_ObjectHideFlags: 1
@@ -158,11 +164,11 @@ MonoBehaviour:
isSelected: 0
nodeRect:
serializedVersion: 2
- x: -445
- y: 30
+ x: -595
+ y: 70
width: 150
height: 62
- positionRect: {x: -445, y: 30}
+ positionRect: {x: -595, y: 70}
rectWidth: 150
editingName: 1
nodeColor: {r: 0, g: 1, b: 1, a: 1}
@@ -189,14 +195,14 @@ MonoBehaviour:
parentState: {fileID: -6568372008305276654}
trueRect:
serializedVersion: 2
- x: -295
- y: 60
+ x: -445
+ y: 100
width: 10
height: 10
falseRect:
serializedVersion: 2
- x: -295
- y: 70
+ x: -445
+ y: 110
width: 10
height: 10
selectedTrue: 0
@@ -233,6 +239,10 @@ MonoBehaviour:
aboveBossHeight: 20
castDelay: 1.5
targetTag: Player
+ overlayClip: {fileID: 7400088, guid: 3ef453d7877555243997dba1cdaa2958, type: 3}
+ overlaySpeed: 1
+ overlayFadeIn: 0.1
+ overlayFadeOut: 0.1
enableDebug: 1
--- !u!114 &-6379838510941931433
MonoBehaviour:
@@ -287,11 +297,11 @@ MonoBehaviour:
isSelected: 0
nodeRect:
serializedVersion: 2
- x: -410
- y: 185
+ x: -560
+ y: 220
width: 150
height: 106
- positionRect: {x: -410, y: 185}
+ positionRect: {x: -560, y: 220}
rectWidth: 150
editingName: 1
nodeColor: {r: 1, g: 0, b: 0, a: 1}
@@ -318,14 +328,14 @@ MonoBehaviour:
parentState: {fileID: -6144582714324757854}
trueRect:
serializedVersion: 2
- x: -260
- y: 215
+ x: -410
+ y: 250
width: 10
height: 10
falseRect:
serializedVersion: 2
- x: -260
- y: 225
+ x: -410
+ y: 260
width: 10
height: 10
selectedTrue: 0
@@ -346,14 +356,14 @@ MonoBehaviour:
parentState: {fileID: -6144582714324757854}
trueRect:
serializedVersion: 2
- x: -260
- y: 237
+ x: -410
+ y: 272
width: 10
height: 10
falseRect:
serializedVersion: 2
- x: -260
- y: 247
+ x: -410
+ y: 282
width: 10
height: 10
selectedTrue: 0
@@ -382,14 +392,14 @@ MonoBehaviour:
parentState: {fileID: -6144582714324757854}
trueRect:
serializedVersion: 2
- x: -260
- y: 259
+ x: -410
+ y: 294
width: 10
height: 10
falseRect:
serializedVersion: 2
- x: -260
- y: 269
+ x: -410
+ y: 304
width: 10
height: 10
selectedTrue: 0
@@ -540,11 +550,11 @@ MonoBehaviour:
isSelected: 0
nodeRect:
serializedVersion: 2
- x: -130
- y: 35
+ x: -285
+ y: 75
width: 150
height: 62
- positionRect: {x: -130, y: 35}
+ positionRect: {x: -285, y: 75}
rectWidth: 150
editingName: 1
nodeColor: {r: 0.10323405, g: 1, b: 0, a: 1}
@@ -563,14 +573,14 @@ MonoBehaviour:
parentState: {fileID: -3177478727897100882}
trueRect:
serializedVersion: 2
- x: 20
- y: 65
+ x: -135
+ y: 105
width: 10
height: 10
falseRect:
serializedVersion: 2
- x: 20
- y: 75
+ x: -135
+ y: 115
width: 10
height: 10
selectedTrue: 0
@@ -609,11 +619,11 @@ MonoBehaviour:
isSelected: 0
nodeRect:
serializedVersion: 2
- x: 165
- y: -5
+ x: 0
+ y: 35
width: 150
height: 62
- positionRect: {x: 165, y: -5}
+ positionRect: {x: 0, y: 35}
rectWidth: 150
editingName: 1
nodeColor: {r: 0, g: 1, b: 0.004989147, a: 1}
@@ -625,7 +635,7 @@ MonoBehaviour:
- decisions:
- trueValue: 0
decision: {fileID: 7927421991537792917}
- isValid: 0
+ isValid: 1
validated: 0
trueState: {fileID: -312774025800194259}
falseState: {fileID: 0}
@@ -636,14 +646,14 @@ MonoBehaviour:
parentState: {fileID: -2904979146780567904}
trueRect:
serializedVersion: 2
- x: 155
- y: 25
+ x: -10
+ y: 65
width: 10
height: 10
falseRect:
serializedVersion: 2
- x: 315
- y: 35
+ x: 150
+ y: 75
width: 10
height: 10
selectedTrue: 0
@@ -751,6 +761,10 @@ MonoBehaviour:
globalStartAngleRandom: 1
perTurretAngleJitter: 10
perTurretRadiusJitter: 0.75
+ overlayClip: {fileID: 7400080, guid: 3ef453d7877555243997dba1cdaa2958, type: 3}
+ overlaySpeed: 1
+ overlayFadeIn: 0.1
+ overlayFadeOut: 0.1
enableDebug: 1
showGizmos: 1
--- !u!114 &-712571192746352845
@@ -776,11 +790,11 @@ MonoBehaviour:
isSelected: 0
nodeRect:
serializedVersion: 2
- x: -130
- y: -20
+ x: -285
+ y: 20
width: 150
height: 30
- positionRect: {x: -130, y: -20}
+ positionRect: {x: -285, y: 20}
rectWidth: 150
editingName: 0
nodeColor: {r: 0, g: 1, b: 0, a: 1}
@@ -852,11 +866,11 @@ MonoBehaviour:
isSelected: 0
nodeRect:
serializedVersion: 2
- x: -325
- y: 445
+ x: -480
+ y: 480
width: 150
height: 150
- positionRect: {x: -325, y: 445}
+ positionRect: {x: -480, y: 480}
rectWidth: 150
editingName: 1
nodeColor: {r: 1, g: 0.95132554, b: 0, a: 1}
@@ -883,14 +897,14 @@ MonoBehaviour:
parentState: {fileID: -312774025800194259}
trueRect:
serializedVersion: 2
- x: -175
- y: 475
+ x: -330
+ y: 510
width: 10
height: 10
falseRect:
serializedVersion: 2
- x: -175
- y: 485
+ x: -330
+ y: 520
width: 10
height: 10
selectedTrue: 0
@@ -915,14 +929,14 @@ MonoBehaviour:
parentState: {fileID: -312774025800194259}
trueRect:
serializedVersion: 2
- x: -175
- y: 497
+ x: -330
+ y: 532
width: 10
height: 10
falseRect:
serializedVersion: 2
- x: -175
- y: 507
+ x: -330
+ y: 542
width: 10
height: 10
selectedTrue: 0
@@ -955,14 +969,14 @@ MonoBehaviour:
parentState: {fileID: -312774025800194259}
trueRect:
serializedVersion: 2
- x: -335
- y: 519
+ x: -490
+ y: 554
width: 10
height: 10
falseRect:
serializedVersion: 2
- x: -175
- y: 529
+ x: -330
+ y: 564
width: 10
height: 10
selectedTrue: 0
@@ -991,14 +1005,14 @@ MonoBehaviour:
parentState: {fileID: -312774025800194259}
trueRect:
serializedVersion: 2
- x: -175
- y: 541
+ x: -330
+ y: 576
width: 10
height: 10
falseRect:
serializedVersion: 2
- x: -175
- y: 551
+ x: -330
+ y: 586
width: 10
height: 10
selectedTrue: 0
@@ -1023,14 +1037,14 @@ MonoBehaviour:
parentState: {fileID: -312774025800194259}
trueRect:
serializedVersion: 2
- x: -175
- y: 563
+ x: -330
+ y: 598
width: 10
height: 10
falseRect:
serializedVersion: 2
- x: -175
- y: 573
+ x: -330
+ y: 608
width: 10
height: 10
selectedTrue: 0
@@ -1077,7 +1091,7 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: a5fc604039227434d8b4e63ebc5e74a5, type: 3}
m_Name: FSM_Demon
m_EditorClassIdentifier:
- selectedNode: {fileID: 4162026404432437805}
+ selectedNode: {fileID: 766956384951898899}
wantConnection: 0
connectionNode: {fileID: 0}
showProperties: 1
@@ -1094,7 +1108,7 @@ MonoBehaviour:
- {fileID: 9112689765763526057}
- {fileID: 766956384951898899}
- {fileID: 4162026404432437805}
- panOffset: {x: -1205, y: 130}
+ panOffset: {x: -860, y: -90}
overNode: 0
actions:
- {fileID: 0}
@@ -1178,11 +1192,11 @@ MonoBehaviour:
isSelected: 0
nodeRect:
serializedVersion: 2
- x: 685
- y: 675
+ x: 515
+ y: 710
width: 150
height: 62
- positionRect: {x: 685, y: 675}
+ positionRect: {x: 515, y: 710}
rectWidth: 150
editingName: 1
nodeColor: {r: 1, g: 1, b: 1, a: 1}
@@ -1201,14 +1215,14 @@ MonoBehaviour:
parentState: {fileID: 762670965814380212}
trueRect:
serializedVersion: 2
- x: 675
- y: 705
+ x: 505
+ y: 740
width: 10
height: 10
falseRect:
serializedVersion: 2
- x: 835
- y: 715
+ x: 665
+ y: 750
width: 10
height: 10
selectedTrue: 0
@@ -1245,14 +1259,14 @@ MonoBehaviour:
canEditName: 1
canEditColor: 1
isOpen: 1
- isSelected: 0
+ isSelected: 1
nodeRect:
serializedVersion: 2
- x: 750
- y: 125
+ x: 580
+ y: 165
width: 150
height: 62
- positionRect: {x: 750, y: 125}
+ positionRect: {x: 580, y: 165}
rectWidth: 150
editingName: 1
nodeColor: {r: 1, g: 1, b: 1, a: 1}
@@ -1271,14 +1285,14 @@ MonoBehaviour:
parentState: {fileID: 766956384951898899}
trueRect:
serializedVersion: 2
- x: 740
- y: 155
+ x: 570
+ y: 195
width: 10
height: 10
falseRect:
serializedVersion: 2
- x: 900
- y: 165
+ x: 730
+ y: 205
width: 10
height: 10
selectedTrue: 0
@@ -1367,11 +1381,11 @@ MonoBehaviour:
isSelected: 0
nodeRect:
serializedVersion: 2
- x: 475
- y: 675
+ x: 305
+ y: 710
width: 150
height: 62
- positionRect: {x: 475, y: 675}
+ positionRect: {x: 305, y: 710}
rectWidth: 150
editingName: 1
nodeColor: {r: 1, g: 1, b: 1, a: 1}
@@ -1390,14 +1404,14 @@ MonoBehaviour:
parentState: {fileID: 2691300596403639167}
trueRect:
serializedVersion: 2
- x: 465
- y: 705
+ x: 295
+ y: 740
width: 10
height: 10
falseRect:
serializedVersion: 2
- x: 625
- y: 715
+ x: 455
+ y: 750
width: 10
height: 10
selectedTrue: 0
@@ -1437,11 +1451,11 @@ MonoBehaviour:
isSelected: 0
nodeRect:
serializedVersion: 2
- x: 220
- y: 395
+ x: 50
+ y: 430
width: 150
height: 150
- positionRect: {x: 220, y: 395}
+ positionRect: {x: 50, y: 430}
rectWidth: 150
editingName: 1
nodeColor: {r: 1, g: 0, b: 0, a: 1}
@@ -1464,14 +1478,14 @@ MonoBehaviour:
parentState: {fileID: 2986668563461644515}
trueRect:
serializedVersion: 2
- x: 210
- y: 425
+ x: 40
+ y: 460
width: 10
height: 10
falseRect:
serializedVersion: 2
- x: 370
- y: 435
+ x: 200
+ y: 470
width: 10
height: 10
selectedTrue: 0
@@ -1496,14 +1510,14 @@ MonoBehaviour:
parentState: {fileID: 2986668563461644515}
trueRect:
serializedVersion: 2
- x: 210
- y: 447
+ x: 40
+ y: 482
width: 10
height: 10
falseRect:
serializedVersion: 2
- x: 370
- y: 457
+ x: 200
+ y: 492
width: 10
height: 10
selectedTrue: 0
@@ -1517,11 +1531,11 @@ MonoBehaviour:
- decisions:
- trueValue: 0
decision: {fileID: -6379838510941931433}
- isValid: 0
+ isValid: 1
validated: 0
- trueValue: 1
decision: {fileID: -7938248970223304488}
- isValid: 1
+ isValid: 0
validated: 0
trueState: {fileID: 766956384951898899}
falseState: {fileID: 0}
@@ -1532,14 +1546,14 @@ MonoBehaviour:
parentState: {fileID: 2986668563461644515}
trueRect:
serializedVersion: 2
- x: 370
- y: 469
+ x: 200
+ y: 504
width: 10
height: 10
falseRect:
serializedVersion: 2
- x: 370
- y: 479
+ x: 200
+ y: 514
width: 10
height: 10
selectedTrue: 0
@@ -1553,7 +1567,7 @@ MonoBehaviour:
- decisions:
- trueValue: 0
decision: {fileID: -6379838510941931433}
- isValid: 0
+ isValid: 1
validated: 0
- trueValue: 1
decision: {fileID: 8113515040269600600}
@@ -1568,14 +1582,14 @@ MonoBehaviour:
parentState: {fileID: 2986668563461644515}
trueRect:
serializedVersion: 2
- x: 370
- y: 491
+ x: 200
+ y: 526
width: 10
height: 10
falseRect:
serializedVersion: 2
- x: 370
- y: 501
+ x: 200
+ y: 536
width: 10
height: 10
selectedTrue: 0
@@ -1604,14 +1618,14 @@ MonoBehaviour:
parentState: {fileID: 2986668563461644515}
trueRect:
serializedVersion: 2
- x: 370
- y: 513
+ x: 200
+ y: 548
width: 10
height: 10
falseRect:
serializedVersion: 2
- x: 370
- y: 523
+ x: 200
+ y: 558
width: 10
height: 10
selectedTrue: 0
@@ -1708,14 +1722,14 @@ MonoBehaviour:
canEditName: 1
canEditColor: 1
isOpen: 0
- isSelected: 1
+ isSelected: 0
nodeRect:
serializedVersion: 2
- x: 745
- y: 340
+ x: 575
+ y: 375
width: 150
height: 30
- positionRect: {x: 745, y: 340}
+ positionRect: {x: 575, y: 375}
rectWidth: 150
editingName: 1
nodeColor: {r: 1, g: 1, b: 1, a: 1}
@@ -1734,14 +1748,14 @@ MonoBehaviour:
parentState: {fileID: 4162026404432437805}
trueRect:
serializedVersion: 2
- x: 820
- y: 355
+ x: 650
+ y: 390
width: 0
height: 0
falseRect:
serializedVersion: 2
- x: 820
- y: 355
+ x: 650
+ y: 390
width: 0
height: 0
selectedTrue: 0
@@ -1907,11 +1921,11 @@ MonoBehaviour:
isSelected: 0
nodeRect:
serializedVersion: 2
- x: 755
- y: -50
+ x: 585
+ y: -5
width: 150
height: 62
- positionRect: {x: 755, y: -50}
+ positionRect: {x: 585, y: -5}
rectWidth: 150
editingName: 1
nodeColor: {r: 1, g: 1, b: 1, a: 1}
@@ -1930,14 +1944,14 @@ MonoBehaviour:
parentState: {fileID: 9112689765763526057}
trueRect:
serializedVersion: 2
- x: 745
- y: -20
+ x: 575
+ y: 25
width: 10
height: 10
falseRect:
serializedVersion: 2
- x: 905
- y: -10
+ x: 735
+ y: 35
width: 10
height: 10
selectedTrue: 0
diff --git a/Assets/ThirdParty/Invector-3rdPersonController/Melee Combat/Animator/Invector@Bonel_Warrior.controller b/Assets/ThirdParty/Invector-3rdPersonController/Melee Combat/Animator/Invector@Bonel_Warrior.controller
index 088d33cd5..73c213864 100644
--- a/Assets/ThirdParty/Invector-3rdPersonController/Melee Combat/Animator/Invector@Bonel_Warrior.controller
+++ b/Assets/ThirdParty/Invector-3rdPersonController/Melee Combat/Animator/Invector@Bonel_Warrior.controller
@@ -18027,7 +18027,7 @@ BlendTree:
m_Name: Run
m_Childs:
- serializedVersion: 2
- m_Motion: {fileID: 7400012, guid: 37c6cfe59f56e8a4799011397a870a8b, type: 3}
+ m_Motion: {fileID: 7400106, guid: 3ef453d7877555243997dba1cdaa2958, type: 3}
m_Threshold: 0
m_Position: {x: 0, y: 1}
m_TimeScale: 1
@@ -18153,7 +18153,7 @@ BlendTree:
m_DirectBlendParameter: InputHorizontal
m_Mirror: 0
- serializedVersion: 2
- m_Motion: {fileID: 7400012, guid: 37c6cfe59f56e8a4799011397a870a8b, type: 3}
+ m_Motion: {fileID: 7400106, guid: 3ef453d7877555243997dba1cdaa2958, type: 3}
m_Threshold: 0
m_Position: {x: 0, y: 0}
m_TimeScale: 1
@@ -34993,7 +34993,7 @@ AnimatorStateMachine:
m_ChildStateMachines:
- serializedVersion: 1
m_StateMachine: {fileID: 1107736847571817844}
- m_Position: {x: 540, y: 192, z: 0}
+ m_Position: {x: 540, y: 190, z: 0}
- serializedVersion: 1
m_StateMachine: {fileID: 1107135120639222350}
m_Position: {x: 324, y: 132, z: 0}