144 lines
4.3 KiB
C#
144 lines
4.3 KiB
C#
using Invector.vCharacterController.AI.FSMBehaviour;
|
|
using Lean.Pool;
|
|
using System.Collections;
|
|
using UnityEngine;
|
|
|
|
namespace DemonBoss.Magic
|
|
{
|
|
/// <summary>
|
|
/// FSM Action: Boss calls down a meteor.
|
|
/// Shows a decal at the player's position, locks an impact point on ground,
|
|
/// then spawns the MeteorProjectile prefab above that point after a delay.
|
|
/// Cancels cleanly on state exit.
|
|
/// </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("Visual decal prefab marking impact point")]
|
|
public GameObject decalPrefab;
|
|
|
|
[Tooltip("Height above ground at which meteor spawns")]
|
|
public float spawnHeight = 40f;
|
|
|
|
[Tooltip("Delay before meteor spawns after decal")]
|
|
public float castDelay = 1.5f;
|
|
|
|
[Header("Ground")]
|
|
[Tooltip("Layer mask for ground raycast")]
|
|
public LayerMask groundMask = -1;
|
|
|
|
[Header("Debug")]
|
|
public bool enableDebug = false;
|
|
|
|
private Transform player;
|
|
private GameObject spawnedDecal;
|
|
private Vector3 impactPoint;
|
|
|
|
private Coroutine _spawnRoutine;
|
|
private CoroutineRunner _runner;
|
|
|
|
/// <summary>
|
|
/// Entry point for the FSM action, delegates to OnEnter/OnExit depending on execution type.
|
|
/// </summary>
|
|
public override void DoAction(vIFSMBehaviourController fsm, vFSMComponentExecutionType executionType = vFSMComponentExecutionType.OnStateUpdate)
|
|
{
|
|
if (executionType == vFSMComponentExecutionType.OnStateEnter)
|
|
OnEnter(fsm);
|
|
|
|
if (executionType == vFSMComponentExecutionType.OnStateExit)
|
|
OnExit();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Acquires the player, locks the impact point on the ground, shows the decal,
|
|
/// and starts the delayed meteor spawn coroutine.
|
|
/// </summary>
|
|
private void OnEnter(vIFSMBehaviourController fsm)
|
|
{
|
|
player = GameObject.FindGameObjectWithTag("Player")?.transform;
|
|
if (player == null)
|
|
{
|
|
if (enableDebug) Debug.LogWarning("[SA_CallMeteor] No player found!");
|
|
return;
|
|
}
|
|
|
|
// Raycast down from the player to lock the impact point
|
|
Vector3 rayStart = player.position + Vector3.up * 5f;
|
|
if (Physics.Raycast(rayStart, Vector3.down, out RaycastHit hit, 100f, groundMask, QueryTriggerInteraction.Ignore))
|
|
impactPoint = hit.point;
|
|
else
|
|
impactPoint = player.position;
|
|
|
|
// Spawn decal
|
|
if (decalPrefab != null)
|
|
spawnedDecal = LeanPool.Spawn(decalPrefab, impactPoint, Quaternion.identity);
|
|
|
|
// Get or add a dedicated runner for coroutines
|
|
var hostGO = fsm.transform.gameObject;
|
|
_runner = hostGO.GetComponent<CoroutineRunner>();
|
|
if (_runner == null) _runner = hostGO.AddComponent<CoroutineRunner>();
|
|
|
|
// Start delayed spawn
|
|
_spawnRoutine = _runner.StartCoroutine(SpawnMeteorAfterDelay());
|
|
}
|
|
|
|
/// <summary>
|
|
/// Cancels the pending spawn and cleans up the decal when exiting the state.
|
|
/// </summary>
|
|
private void OnExit()
|
|
{
|
|
if (_runner != null && _spawnRoutine != null)
|
|
{
|
|
_runner.StopCoroutine(_spawnRoutine);
|
|
_spawnRoutine = null;
|
|
}
|
|
|
|
if (spawnedDecal != null)
|
|
{
|
|
LeanPool.Despawn(spawnedDecal);
|
|
spawnedDecal = null;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Waits for the configured cast delay and then spawns the meteor.
|
|
/// </summary>
|
|
private IEnumerator SpawnMeteorAfterDelay()
|
|
{
|
|
yield return new WaitForSeconds(castDelay);
|
|
SpawnMeteor();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Spawns the meteor prefab above the locked impact point and cleans up the decal.
|
|
/// </summary>
|
|
private void SpawnMeteor()
|
|
{
|
|
if (meteorPrefab == null) return;
|
|
|
|
Vector3 spawnPos = impactPoint + Vector3.up * spawnHeight;
|
|
LeanPool.Spawn(meteorPrefab, spawnPos, Quaternion.identity);
|
|
|
|
if (enableDebug) Debug.Log($"[SA_CallMeteor] Meteor spawned at {spawnPos}, impact={impactPoint}");
|
|
|
|
if (spawnedDecal != null)
|
|
{
|
|
LeanPool.Despawn(spawnedDecal);
|
|
spawnedDecal = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Lightweight helper component dedicated to running coroutines for ScriptableObject actions.
|
|
/// </summary>
|
|
public sealed class CoroutineRunner : MonoBehaviour
|
|
{ }
|
|
} |