407 lines
12 KiB
C#
407 lines
12 KiB
C#
using Invector.vCharacterController.AI.FSMBehaviour;
|
|
using Lean.Pool;
|
|
using UnityEngine;
|
|
using UnityEngine.Animations;
|
|
using UnityEngine.Playables;
|
|
|
|
namespace DemonBoss.Magic
|
|
{
|
|
/// <summary>
|
|
/// Magic Shield
|
|
/// Spawns shield FX, holds for 'shieldDuration', then plays End and cleans up.
|
|
/// </summary>
|
|
[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<Animator>();
|
|
|
|
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<Animator>() : 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);
|
|
|
|
/// <summary>
|
|
/// Fades from whatever is currently audible to the given clip.
|
|
/// Reuses the 2 inputs; swaps playables as needed.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|
|
} |