Demon fixes
This commit is contained in:
@@ -6,8 +6,10 @@ using UnityEngine;
|
||||
namespace DemonBoss.Magic
|
||||
{
|
||||
/// <summary>
|
||||
/// StateAction for Meteor Strike spell - boss summons meteor falling on player
|
||||
/// First places decal at player position, after 1.5s checks if path is clear and drops rock
|
||||
/// 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
|
||||
@@ -15,338 +17,128 @@ namespace DemonBoss.Magic
|
||||
public override string categoryName => "DemonBoss/Magic";
|
||||
public override string defaultName => "Call Meteor";
|
||||
|
||||
[Header("Meteor Configuration")]
|
||||
[Tooltip("Meteor rock prefab")]
|
||||
public GameObject rockPrefab;
|
||||
[Header("Meteor Setup")]
|
||||
[Tooltip("Prefab with MeteorProjectile component")]
|
||||
public GameObject meteorPrefab;
|
||||
|
||||
[Tooltip("Decal prefab showing impact location")]
|
||||
[Tooltip("Visual decal prefab marking impact point")]
|
||||
public GameObject decalPrefab;
|
||||
|
||||
[Tooltip("Height from which meteor falls")]
|
||||
public float meteorHeight = 30f;
|
||||
[Tooltip("Height above ground at which meteor spawns")]
|
||||
public float spawnHeight = 40f;
|
||||
|
||||
[Tooltip("Delay between placing decal and spawning meteor")]
|
||||
[Tooltip("Delay before meteor spawns after decal")]
|
||||
public float castDelay = 1.5f;
|
||||
|
||||
[Tooltip("Obstacle check radius above target")]
|
||||
public float obstacleCheckRadius = 2f;
|
||||
|
||||
[Tooltip("Layer mask for obstacles blocking meteor")]
|
||||
public LayerMask obstacleLayerMask = -1;
|
||||
|
||||
[Tooltip("Layer mask for ground")]
|
||||
public LayerMask groundLayerMask = -1;
|
||||
|
||||
[Tooltip("Animator trigger name for meteor summoning animation")]
|
||||
public string animatorTrigger = "CastMeteor";
|
||||
|
||||
[Header("Targeting")]
|
||||
[Tooltip("Maximum raycast distance to find ground")]
|
||||
public float maxGroundDistance = 100f;
|
||||
|
||||
[Tooltip("Height above ground for obstacle checking")]
|
||||
public float airCheckHeight = 5f;
|
||||
[Header("Ground")]
|
||||
[Tooltip("Layer mask for ground raycast")]
|
||||
public LayerMask groundMask = -1;
|
||||
|
||||
[Header("Debug")]
|
||||
[Tooltip("Enable debug logging")]
|
||||
public bool enableDebug = false;
|
||||
|
||||
[Tooltip("Show gizmos in Scene View")]
|
||||
public bool showGizmos = true;
|
||||
|
||||
private Transform player;
|
||||
private GameObject spawnedDecal;
|
||||
private Vector3 impactPoint;
|
||||
|
||||
private Transform playerTransform;
|
||||
private Vector3 targetPosition;
|
||||
private bool meteorCasting = false;
|
||||
private MonoBehaviour coroutineRunner;
|
||||
private Coroutine _spawnRoutine;
|
||||
private CoroutineRunner _runner;
|
||||
|
||||
/// <summary>
|
||||
/// Main action execution method called by FSM
|
||||
/// Entry point for the FSM action, delegates to OnEnter/OnExit depending on execution type.
|
||||
/// </summary>
|
||||
public override void DoAction(vIFSMBehaviourController fsmBehaviour, vFSMComponentExecutionType executionType = vFSMComponentExecutionType.OnStateUpdate)
|
||||
public override void DoAction(vIFSMBehaviourController fsm, vFSMComponentExecutionType executionType = vFSMComponentExecutionType.OnStateUpdate)
|
||||
{
|
||||
if (executionType == vFSMComponentExecutionType.OnStateEnter)
|
||||
{
|
||||
OnStateEnter(fsmBehaviour);
|
||||
}
|
||||
else if (executionType == vFSMComponentExecutionType.OnStateExit)
|
||||
{
|
||||
OnStateExit(fsmBehaviour);
|
||||
}
|
||||
OnEnter(fsm);
|
||||
|
||||
if (executionType == vFSMComponentExecutionType.OnStateExit)
|
||||
OnExit();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when entering state - starts meteor casting
|
||||
/// Acquires the player, locks the impact point on the ground, shows the decal,
|
||||
/// and starts the delayed meteor spawn coroutine.
|
||||
/// </summary>
|
||||
private void OnStateEnter(vIFSMBehaviourController fsmBehaviour)
|
||||
private void OnEnter(vIFSMBehaviourController fsm)
|
||||
{
|
||||
if (enableDebug) Debug.Log("[SA_CallMeteor] Starting meteor casting");
|
||||
|
||||
FindPlayer(fsmBehaviour);
|
||||
|
||||
var animator = fsmBehaviour.transform.GetComponent<Animator>();
|
||||
if (animator != null && !string.IsNullOrEmpty(animatorTrigger))
|
||||
player = GameObject.FindGameObjectWithTag("Player")?.transform;
|
||||
if (player == null)
|
||||
{
|
||||
animator.SetTrigger(animatorTrigger);
|
||||
if (enableDebug) Debug.Log($"[SA_CallMeteor] Set trigger: {animatorTrigger}");
|
||||
}
|
||||
|
||||
StartMeteorCast(fsmBehaviour);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when exiting state - cleanup
|
||||
/// </summary>
|
||||
private void OnStateExit(vIFSMBehaviourController fsmBehaviour)
|
||||
{
|
||||
if (enableDebug) Debug.Log("[SA_CallMeteor] Exiting meteor state");
|
||||
meteorCasting = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds player transform
|
||||
/// </summary>
|
||||
private void FindPlayer(vIFSMBehaviourController fsmBehaviour)
|
||||
{
|
||||
GameObject player = GameObject.FindGameObjectWithTag("Player");
|
||||
if (player != null)
|
||||
{
|
||||
playerTransform = player.transform;
|
||||
if (enableDebug) Debug.Log("[SA_CallMeteor] Player found by tag");
|
||||
if (enableDebug) Debug.LogWarning("[SA_CallMeteor] No player found!");
|
||||
return;
|
||||
}
|
||||
|
||||
var aiController = fsmBehaviour as Invector.vCharacterController.AI.vIControlAI;
|
||||
if (aiController != null && aiController.currentTarget != null)
|
||||
{
|
||||
playerTransform = aiController.currentTarget.transform;
|
||||
if (enableDebug) Debug.Log("[SA_CallMeteor] Player found through AI target");
|
||||
return;
|
||||
}
|
||||
|
||||
if (enableDebug) Debug.LogWarning("[SA_CallMeteor] Player not found!");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts meteor casting process
|
||||
/// </summary>
|
||||
private void StartMeteorCast(vIFSMBehaviourController fsmBehaviour)
|
||||
{
|
||||
if (playerTransform == null)
|
||||
{
|
||||
if (enableDebug) Debug.LogWarning("[SA_CallMeteor] No target - finishing state");
|
||||
return;
|
||||
}
|
||||
|
||||
if (FindGroundUnderPlayer(out Vector3 groundPos))
|
||||
{
|
||||
targetPosition = groundPos;
|
||||
|
||||
SpawnDecal(targetPosition);
|
||||
|
||||
coroutineRunner = fsmBehaviour.transform.GetComponent<MonoBehaviour>();
|
||||
if (coroutineRunner != null)
|
||||
{
|
||||
coroutineRunner.StartCoroutine(MeteorCastCoroutine(fsmBehaviour));
|
||||
}
|
||||
else
|
||||
{
|
||||
CastMeteorImmediate();
|
||||
}
|
||||
|
||||
meteorCasting = true;
|
||||
|
||||
if (enableDebug) Debug.Log($"[SA_CallMeteor] Meteor target: {targetPosition}");
|
||||
}
|
||||
// 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
|
||||
{
|
||||
if (enableDebug) Debug.LogWarning("[SA_CallMeteor] Cannot find ground under player");
|
||||
}
|
||||
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>
|
||||
/// Finds ground under player using raycast
|
||||
/// Cancels the pending spawn and cleans up the decal when exiting the state.
|
||||
/// </summary>
|
||||
private bool FindGroundUnderPlayer(out Vector3 groundPosition)
|
||||
private void OnExit()
|
||||
{
|
||||
groundPosition = Vector3.zero;
|
||||
|
||||
Vector3 playerPos = playerTransform.position;
|
||||
Vector3 rayStart = playerPos + Vector3.up * 5f;
|
||||
|
||||
Ray groundRay = new Ray(rayStart, Vector3.down);
|
||||
if (Physics.Raycast(groundRay, out RaycastHit hit, maxGroundDistance, groundLayerMask))
|
||||
if (_runner != null && _spawnRoutine != null)
|
||||
{
|
||||
groundPosition = hit.point;
|
||||
return true;
|
||||
_runner.StopCoroutine(_spawnRoutine);
|
||||
_spawnRoutine = null;
|
||||
}
|
||||
|
||||
groundPosition = playerPos;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spawns decal showing meteor impact location
|
||||
/// </summary>
|
||||
private void SpawnDecal(Vector3 position)
|
||||
{
|
||||
if (decalPrefab == null)
|
||||
{
|
||||
if (enableDebug) Debug.LogWarning("[SA_CallMeteor] Missing decal prefab");
|
||||
return;
|
||||
}
|
||||
|
||||
Quaternion decalRotation = Quaternion.identity;
|
||||
|
||||
Ray surfaceRay = new Ray(position + Vector3.up * 2f, Vector3.down);
|
||||
if (Physics.Raycast(surfaceRay, out RaycastHit hit, 5f, groundLayerMask))
|
||||
{
|
||||
decalRotation = Quaternion.FromToRotation(Vector3.up, hit.normal);
|
||||
}
|
||||
|
||||
spawnedDecal = LeanPool.Spawn(decalPrefab, position, decalRotation);
|
||||
|
||||
if (enableDebug) Debug.Log($"[SA_CallMeteor] Decal spawned at: {position}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Coroutine handling meteor casting process with delay
|
||||
/// </summary>
|
||||
private IEnumerator MeteorCastCoroutine(vIFSMBehaviourController fsmBehaviour)
|
||||
{
|
||||
yield return new WaitForSeconds(castDelay);
|
||||
|
||||
if (enableDebug) Debug.Log("[SA_CallMeteor] Checking if path is clear for meteor");
|
||||
|
||||
if (IsPathClearForMeteor(targetPosition))
|
||||
{
|
||||
SpawnMeteor(targetPosition);
|
||||
if (enableDebug) Debug.Log("[SA_CallMeteor] Meteor spawned");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (enableDebug) Debug.Log("[SA_CallMeteor] Path blocked - meteor not spawned");
|
||||
}
|
||||
|
||||
CleanupDecal();
|
||||
|
||||
DEC_CheckCooldown.SetCooldownStatic(fsmBehaviour, "Meteor", 20f);
|
||||
|
||||
meteorCasting = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Immediate meteor cast without coroutine (fallback)
|
||||
/// </summary>
|
||||
private void CastMeteorImmediate()
|
||||
{
|
||||
if (IsPathClearForMeteor(targetPosition))
|
||||
{
|
||||
SpawnMeteor(targetPosition);
|
||||
}
|
||||
CleanupDecal();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if path above target is clear for meteor
|
||||
/// </summary>
|
||||
private bool IsPathClearForMeteor(Vector3 targetPos)
|
||||
{
|
||||
Vector3 checkStart = targetPos + Vector3.up * airCheckHeight;
|
||||
Vector3 checkEnd = targetPos + Vector3.up * meteorHeight;
|
||||
|
||||
if (Physics.CheckSphere(checkStart, obstacleCheckRadius, obstacleLayerMask))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Ray pathRay = new Ray(checkEnd, Vector3.down);
|
||||
if (Physics.SphereCast(pathRay, obstacleCheckRadius, meteorHeight - airCheckHeight, obstacleLayerMask))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spawns meteor in air above target
|
||||
/// </summary>
|
||||
private void SpawnMeteor(Vector3 targetPos)
|
||||
{
|
||||
if (rockPrefab == null)
|
||||
{
|
||||
Debug.LogError("[SA_CallMeteor] Missing meteor prefab!");
|
||||
return;
|
||||
}
|
||||
|
||||
Vector3 meteorSpawnPos = targetPos + Vector3.up * meteorHeight;
|
||||
|
||||
GameObject meteor = LeanPool.Spawn(rockPrefab, meteorSpawnPos, Quaternion.identity);
|
||||
|
||||
Rigidbody meteorRb = meteor.GetComponent<Rigidbody>();
|
||||
if (meteorRb != null)
|
||||
{
|
||||
meteorRb.linearVelocity = Vector3.down * 5f;
|
||||
meteorRb.angularVelocity = Random.insideUnitSphere * 2f;
|
||||
}
|
||||
|
||||
if (enableDebug) Debug.Log($"[SA_CallMeteor] Meteor spawned at height: {meteorHeight}m");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes decal from map
|
||||
/// </summary>
|
||||
private void CleanupDecal()
|
||||
{
|
||||
if (spawnedDecal != null)
|
||||
{
|
||||
LeanPool.Despawn(spawnedDecal);
|
||||
spawnedDecal = null;
|
||||
if (enableDebug) Debug.Log("[SA_CallMeteor] Decal removed");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if meteor is currently being cast
|
||||
/// Waits for the configured cast delay and then spawns the meteor.
|
||||
/// </summary>
|
||||
public bool IsCasting()
|
||||
private IEnumerator SpawnMeteorAfterDelay()
|
||||
{
|
||||
return meteorCasting;
|
||||
yield return new WaitForSeconds(castDelay);
|
||||
SpawnMeteor();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns meteor target position
|
||||
/// Spawns the meteor prefab above the locked impact point and cleans up the decal.
|
||||
/// </summary>
|
||||
public Vector3 GetTargetPosition()
|
||||
private void SpawnMeteor()
|
||||
{
|
||||
return targetPosition;
|
||||
}
|
||||
if (meteorPrefab == null) return;
|
||||
|
||||
/// <summary>
|
||||
/// Draws gizmos in Scene View for debugging
|
||||
/// </summary>
|
||||
private void OnDrawGizmosSelected()
|
||||
{
|
||||
if (!showGizmos) return;
|
||||
Vector3 spawnPos = impactPoint + Vector3.up * spawnHeight;
|
||||
LeanPool.Spawn(meteorPrefab, spawnPos, Quaternion.identity);
|
||||
|
||||
if (meteorCasting && targetPosition != Vector3.zero)
|
||||
if (enableDebug) Debug.Log($"[SA_CallMeteor] Meteor spawned at {spawnPos}, impact={impactPoint}");
|
||||
|
||||
if (spawnedDecal != null)
|
||||
{
|
||||
Gizmos.color = Color.red;
|
||||
Gizmos.DrawWireSphere(targetPosition, 1f);
|
||||
|
||||
Vector3 spawnPos = targetPosition + Vector3.up * meteorHeight;
|
||||
Gizmos.color = Color.yellow;
|
||||
Gizmos.DrawWireSphere(spawnPos, 0.5f);
|
||||
|
||||
Gizmos.color = Color.yellow;
|
||||
Gizmos.DrawLine(spawnPos, targetPosition);
|
||||
|
||||
Vector3 checkPos = targetPosition + Vector3.up * airCheckHeight;
|
||||
Gizmos.color = Color.blue;
|
||||
Gizmos.DrawWireSphere(checkPos, obstacleCheckRadius);
|
||||
LeanPool.Despawn(spawnedDecal);
|
||||
spawnedDecal = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lightweight helper component dedicated to running coroutines for ScriptableObject actions.
|
||||
/// </summary>
|
||||
public sealed class CoroutineRunner : MonoBehaviour
|
||||
{ }
|
||||
}
|
||||
Reference in New Issue
Block a user