Files
beyond/Assets/AI/Demon/SA_CastShield.cs
2025-09-19 16:27:25 +02:00

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;
}
}
}