Files
beyond/Assets/AI/_Demon/SA_CallMeteor.cs
SzymonMis 06f9c7349d Summoner
2026-02-19 21:34:07 +01:00

337 lines
10 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using DemonBoss.AI;
using Invector.vCharacterController.AI.FSMBehaviour;
using Lean.Pool;
using UnityEngine;
using UnityEngine.Animations;
using UnityEngine.Playables;
namespace DemonBoss.Magic
{
/// <summary>
/// Spawns multiple meteors behind the BOSS and launches them toward the player's position.
/// </summary>
[CreateAssetMenu(menuName = "Invector/FSM/Actions/DemonBoss/Call Meteor")]
public class SA_CallMeteor : vStateAction
{
public override string categoryName => "DemonBoss/Magic";
public override string defaultName => "Call Meteor";
[Header("Meteor Setup")]
[Tooltip("Prefab with MeteorProjectile component")]
public GameObject meteorPrefab;
[Tooltip("Distance behind the BOSS to spawn meteor (meters)")]
public float behindBossDistance = 3f;
[Tooltip("Height above the BOSS to spawn meteor (meters)")]
public float aboveBossHeight = 8f;
[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)")]
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;
// --- Multi-meteor state ---
private int _meteorsSpawned = 0;
private bool _spawningActive = false;
// --- 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)
{
// 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);
}
}
private void OnEnter(vIFSMBehaviourController fsm)
{
_boss = fsm.transform;
_target = GameObject.FindGameObjectWithTag(targetTag)?.transform;
if (_target == null)
{
if (enableDebug) Debug.LogWarning("[SA_CallMeteor] No target found abort");
return;
}
// 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);
// Start the meteor sequence
if (initialCastDelay > 0f)
_boss.gameObject.AddComponent<DelayedInvoker>().Init(initialCastDelay, StartMeteorSequence);
else
StartMeteorSequence();
}
private void OnExit(vIFSMBehaviourController fsm)
{
// Stop any active spawning
_spawningActive = false;
StopOverlayImmediate();
// Clean up any DelayedInvokers attached to the boss
var invokers = _boss?.GetComponents<DelayedInvoker>();
if (invokers != null)
{
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<DelayedInvoker>().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;
// 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);
// Configure the projectile to target the player
var meteorScript = meteorGO.GetComponent<MeteorProjectile>();
if (meteorScript != null)
{
// 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<Rigidbody>();
if (playerRigidbody != null)
{
Vector3 playerVelocity = playerRigidbody.linearVelocity;
float estimatedFlightTime = 2f; // rough estimate
targetPos += playerVelocity * estimatedFlightTime * 0.5f; // partial leading
}
meteorScript.useOverrideImpactPoint = true;
meteorScript.overrideImpactPoint = targetPos;
meteorScript.snapImpactToGround = true;
if (enableDebug) Debug.Log($"[SA_CallMeteor] Meteor #{_meteorsSpawned + 1} configured to target: {targetPos}");
}
else
{
if (enableDebug) Debug.LogError("[SA_CallMeteor] Meteor prefab missing MeteorProjectile component!");
}
}
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<DelayedInvoker>().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;
var animator = owner.GetComponent<Animator>();
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;
// Calculate total sequence duration for overlay
float totalSequenceDuration = initialCastDelay + (meteorCount * meteorSpawnInterval) + 2f; // +2s buffer
float overlayDuration = overlayClip.length / Mathf.Max(0.0001f, overlaySpeed);
// 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()
{
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");
}
// 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;
}
}