using Invector.vCharacterController.AI.FSMBehaviour; using Lean.Pool; using UnityEngine; using UnityEngine.Animations; using UnityEngine.Playables; namespace DemonBoss.Magic { /// /// Magic Shield /// Spawns shield FX, holds for 'shieldDuration', then plays End and cleans up. /// [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 Logic")] [Tooltip("Prefab with magical shield particle effect")] public GameObject shieldFXPrefab; [Tooltip("Shield duration in seconds (time spent in Keep phase before End)")] public float shieldDuration = 5f; [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")] public bool debugLogs = false; // --- Runtime (shield/FX) --- private Transform _npc; 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 // Flag to prevent multiple endings private bool _hasEnded = false; public override void DoAction(vIFSMBehaviourController fsm, vFSMComponentExecutionType execType = vFSMComponentExecutionType.OnStateUpdate) { 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) { // Reset all state _hasEnded = false; _phase = Phase.None; _shieldActive = false; _npc = fsm.transform; _anim = _npc.GetComponent(); if (_anim != null && !string.IsNullOrEmpty(animatorBlockingBool)) _anim.SetBool(animatorBlockingBool, true); // SET COOLDOWN IMMEDIATELY when shield is used DEC_CheckCooldown.SetCooldownStatic(fsm, "Shield", 60f); SpawnShieldFX(); _shieldStartTime = Time.time; _shieldActive = true; BuildGraphIfNeeded(); if (startClip != null) { EnterStart(); } else if (keepClip != null) { EnterKeep(); } else { if (debugLogs) Debug.Log("[SA_CastShield] No Start/Keep clips; waiting to End."); _phase = Phase.Keep; // logical keep (no anim) } if (debugLogs) Debug.Log($"[SA_CastShield] Shield started - duration: {shieldDuration}s, cooldown set to 60s"); } private void OnUpdate(vIFSMBehaviourController fsm) { // Don't process if we're already done if (_hasEnded || _phase == Phase.Done) return; // handle crossfade if (_fading) { 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; } // auto-advance phases based on timers if (_phase == Phase.Start && Time.time >= _phaseScheduledEnd) { if (keepClip != null) CrossfadeToKeep(); else BeginEnd(); // no keep; go straight to End window } else if (_phase == Phase.Keep) { // When shield timer is up, begin End if (_shieldActive && (Time.time - _shieldStartTime) >= shieldDuration) { if (debugLogs) Debug.Log("[SA_CastShield] Shield duration expired, beginning end phase"); BeginEnd(); } } else if (_phase == Phase.End && Time.time >= _phaseScheduledEnd) { // End completed if (debugLogs) Debug.Log("[SA_CastShield] End phase completed, finishing shield"); FinishShield(); } } private void OnExit(vIFSMBehaviourController fsm) { if (_anim != null && !string.IsNullOrEmpty(animatorBlockingBool)) _anim.SetBool(animatorBlockingBool, false); // If we left early and haven't ended yet, optionally play End briefly if (playEndOnEarlyExit && !_hasEnded && _phase != Phase.End && _phase != Phase.Done && endClip != null) { 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 } // Always cleanup when exiting FinishShield(); if (debugLogs) Debug.Log("[SA_CastShield] State exited and cleaned up"); } // ------------------ Phase Transitions ------------------ private void EnterStart() { 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) { BeginEnd(); return; } CrossfadeTo(keepClip, out _pKeep, quick: false, loop: true); _phase = Phase.Keep; if (debugLogs) Debug.Log("[SA_CastShield] Switched to Keep (loop)."); } private void EnterKeep() { CrossfadeTo(keepClip, out _pKeep, quick: false, loop: true); _phase = Phase.Keep; if (debugLogs) Debug.Log("[SA_CastShield] Entered Keep directly."); } private void BeginEnd() { // Prevent multiple end calls if (_hasEnded) return; if (endClip == null) { // No end clip; just finish FinishShield(); return; } CrossfadeTo(endClip, out _pEnd, quick: false); _phase = Phase.End; _phaseScheduledEnd = Time.time + (endClip.length / SafeSpeed()); if (debugLogs) Debug.Log("[SA_CastShield] End phase."); } private void FinishShield() { // Prevent multiple finish calls if (_hasEnded) return; _hasEnded = true; _phase = Phase.Done; TeardownGraph(); CleanupShieldFX(); _shieldActive = false; if (debugLogs) Debug.Log("[SA_CastShield] Shield finished and cleaned up"); } // ------------------ 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); // Set looping if requested if (loop) { newPlayable.SetDuration(double.PositiveInfinity); } // 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); } } // ------------------ FX Helpers ------------------ private void SpawnShieldFX() { if (shieldFXPrefab == null) return; _spawnedShieldFX = LeanPool.Spawn(shieldFXPrefab, _npc.position, _npc.rotation); if (debugLogs) Debug.Log("[SA_CastShield] Shield FX spawned."); } 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 && !_hasEnded; public float GetRemainingShieldTime() { if (!_shieldActive || _hasEnded) return 0f; float t = Mathf.Max(0f, shieldDuration - (Time.time - _shieldStartTime)); return (_phase == Phase.End || _phase == Phase.Done) ? 0f : t; } } }