updated demon
This commit is contained in:
@@ -1,379 +1,379 @@
|
||||
using Lean.Pool;
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
|
||||
namespace DemonBoss.Magic
|
||||
{
|
||||
/// <summary>
|
||||
/// AI component for crystal turret spawned by boss
|
||||
/// Rotates towards player and shoots fireballs for specified time
|
||||
/// </summary>
|
||||
public class CrystalShooterAI : MonoBehaviour
|
||||
{
|
||||
[Header("Shooting Configuration")]
|
||||
[Tooltip("Transform point from which projectiles are fired")]
|
||||
public Transform muzzle;
|
||||
|
||||
[Tooltip("Fireball prefab")]
|
||||
public GameObject fireballPrefab;
|
||||
|
||||
[Tooltip("Fireball speed in m/s")]
|
||||
public float fireballSpeed = 28f;
|
||||
|
||||
[Tooltip("Time between shots in seconds")]
|
||||
public float fireRate = 0.7f;
|
||||
|
||||
[Tooltip("Maximum number of shots before auto-despawn")]
|
||||
public int maxShots = 10;
|
||||
|
||||
[Tooltip("Wait time after shooting before despawn")]
|
||||
public float despawnDelay = 3f;
|
||||
|
||||
[Header("Rotation Configuration")]
|
||||
[Tooltip("Target rotation speed in degrees/s")]
|
||||
public float turnSpeed = 120f;
|
||||
|
||||
[Tooltip("Idle spin speed when no target (degrees/s, 0 = disabled)")]
|
||||
public float idleSpinSpeed = 30f;
|
||||
|
||||
[Tooltip("Aiming accuracy in degrees (smaller value = more accurate)")]
|
||||
public float aimTolerance = 5f;
|
||||
|
||||
[Header("Targeting")]
|
||||
[Tooltip("Automatically find player on start")]
|
||||
public bool autoFindPlayer = true;
|
||||
|
||||
[Tooltip("Player tag to search for")]
|
||||
public string playerTag = "Player";
|
||||
|
||||
[Tooltip("Maximum shooting range")]
|
||||
public float maxShootingRange = 50f;
|
||||
|
||||
[Header("Effects")]
|
||||
[Tooltip("Particle effect at shot")]
|
||||
public GameObject muzzleFlashPrefab;
|
||||
|
||||
[Tooltip("Shoot sound")]
|
||||
public AudioClip shootSound;
|
||||
|
||||
[Header("Debug")]
|
||||
[Tooltip("Enable debug logging")]
|
||||
public bool enableDebug = false;
|
||||
|
||||
[Tooltip("Show gizmos in Scene View")]
|
||||
public bool showGizmos = true;
|
||||
|
||||
// Private variables
|
||||
private Transform target;
|
||||
|
||||
private AudioSource audioSource;
|
||||
private Coroutine shootingCoroutine;
|
||||
private bool isActive = false;
|
||||
private int shotsFired = 0;
|
||||
private float lastShotTime = 0f;
|
||||
|
||||
private Transform crystalTransform;
|
||||
|
||||
/// <summary>
|
||||
/// Component initialization
|
||||
/// </summary>
|
||||
private void Awake()
|
||||
{
|
||||
crystalTransform = transform;
|
||||
|
||||
audioSource = GetComponent<AudioSource>();
|
||||
if (audioSource == null && shootSound != null)
|
||||
{
|
||||
audioSource = gameObject.AddComponent<AudioSource>();
|
||||
audioSource.playOnAwake = false;
|
||||
audioSource.spatialBlend = 1f;
|
||||
}
|
||||
|
||||
if (muzzle == null)
|
||||
{
|
||||
Transform muzzleChild = crystalTransform.Find("muzzle");
|
||||
if (muzzleChild != null)
|
||||
{
|
||||
muzzle = muzzleChild;
|
||||
if (enableDebug) Debug.Log("[CrystalShooterAI] Found muzzle child");
|
||||
}
|
||||
else
|
||||
{
|
||||
muzzle = crystalTransform;
|
||||
if (enableDebug) Debug.LogWarning("[CrystalShooterAI] Using crystal center as muzzle");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Start - begin crystal operation
|
||||
/// </summary>
|
||||
private void Start()
|
||||
{
|
||||
if (enableDebug) Debug.Log("[CrystalShooterAI] Crystal activated");
|
||||
|
||||
if (autoFindPlayer && target == null)
|
||||
{
|
||||
FindPlayer();
|
||||
}
|
||||
|
||||
StartShooting();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update - rotate crystal towards target
|
||||
/// </summary>
|
||||
private void Update()
|
||||
{
|
||||
if (!isActive) return;
|
||||
|
||||
RotateTowardsTarget();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find player automatically
|
||||
/// </summary>
|
||||
private void FindPlayer()
|
||||
{
|
||||
GameObject player = GameObject.FindGameObjectWithTag(playerTag);
|
||||
if (player != null)
|
||||
{
|
||||
SetTarget(player.transform);
|
||||
if (enableDebug) Debug.Log("[CrystalShooterAI] Automatically found player");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (enableDebug) Debug.LogWarning("[CrystalShooterAI] Cannot find player with tag: " + playerTag);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set target for crystal
|
||||
/// </summary>
|
||||
/// <param name="newTarget">Target transform</param>
|
||||
public void SetTarget(Transform newTarget)
|
||||
{
|
||||
target = newTarget;
|
||||
if (enableDebug && target != null)
|
||||
{
|
||||
Debug.Log($"[CrystalShooterAI] Set target: {target.name}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Start shooting cycle
|
||||
/// </summary>
|
||||
public void StartShooting()
|
||||
{
|
||||
if (isActive) return;
|
||||
|
||||
isActive = true;
|
||||
shotsFired = 0;
|
||||
|
||||
shootingCoroutine = StartCoroutine(ShootingCoroutine());
|
||||
|
||||
if (enableDebug) Debug.Log("[CrystalShooterAI] Starting shooting");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stop shooting
|
||||
/// </summary>
|
||||
public void StopShooting()
|
||||
{
|
||||
isActive = false;
|
||||
|
||||
if (shootingCoroutine != null)
|
||||
{
|
||||
StopCoroutine(shootingCoroutine);
|
||||
shootingCoroutine = null;
|
||||
}
|
||||
|
||||
if (enableDebug) Debug.Log("[CrystalShooterAI] Stopped shooting");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Main coroutine handling shooting cycle
|
||||
/// </summary>
|
||||
private IEnumerator ShootingCoroutine()
|
||||
{
|
||||
while (shotsFired < maxShots && isActive)
|
||||
{
|
||||
if (CanShoot())
|
||||
{
|
||||
FireFireball();
|
||||
shotsFired++;
|
||||
lastShotTime = Time.time;
|
||||
}
|
||||
|
||||
yield return new WaitForSeconds(fireRate);
|
||||
}
|
||||
|
||||
if (enableDebug) Debug.Log($"[CrystalShooterAI] Finished shooting ({shotsFired} shots)");
|
||||
|
||||
yield return new WaitForSeconds(despawnDelay);
|
||||
|
||||
DespawnCrystal();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if crystal can shoot
|
||||
/// </summary>
|
||||
private bool CanShoot()
|
||||
{
|
||||
if (target == null) return false;
|
||||
|
||||
float distanceToTarget = Vector3.Distance(crystalTransform.position, target.position);
|
||||
if (distanceToTarget > maxShootingRange) return false;
|
||||
|
||||
Vector3 directionToTarget = (target.position - crystalTransform.position).normalized;
|
||||
float angleToTarget = Vector3.Angle(crystalTransform.forward, directionToTarget);
|
||||
|
||||
return angleToTarget <= aimTolerance;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fires fireball towards target
|
||||
/// </summary>
|
||||
private void FireFireball()
|
||||
{
|
||||
if (fireballPrefab == null || muzzle == null)
|
||||
{
|
||||
if (enableDebug) Debug.LogWarning("[CrystalShooterAI] Missing fireball prefab or muzzle");
|
||||
return;
|
||||
}
|
||||
|
||||
Vector3 shootDirection = crystalTransform.forward;
|
||||
if (target != null)
|
||||
{
|
||||
Vector3 targetCenter = target.position + Vector3.up * 1f;
|
||||
shootDirection = (targetCenter - muzzle.position).normalized;
|
||||
}
|
||||
|
||||
GameObject fireball = LeanPool.Spawn(fireballPrefab, muzzle.position,
|
||||
Quaternion.LookRotation(shootDirection));
|
||||
|
||||
Rigidbody fireballRb = fireball.GetComponent<Rigidbody>();
|
||||
if (fireballRb != null)
|
||||
{
|
||||
fireballRb.linearVelocity = shootDirection * fireballSpeed;
|
||||
}
|
||||
|
||||
PlayShootEffects();
|
||||
|
||||
if (enableDebug)
|
||||
{
|
||||
Debug.Log($"[CrystalShooterAI] Shot #{shotsFired + 1} in direction: {shootDirection}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plays shooting effects
|
||||
/// </summary>
|
||||
private void PlayShootEffects()
|
||||
{
|
||||
if (muzzleFlashPrefab != null && muzzle != null)
|
||||
{
|
||||
GameObject flash = LeanPool.Spawn(muzzleFlashPrefab, muzzle.position, muzzle.rotation);
|
||||
|
||||
LeanPool.Despawn(flash, 2f);
|
||||
}
|
||||
|
||||
if (audioSource != null && shootSound != null)
|
||||
{
|
||||
audioSource.PlayOneShot(shootSound);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rotates crystal towards target or performs idle spin
|
||||
/// </summary>
|
||||
private void RotateTowardsTarget()
|
||||
{
|
||||
if (target != null)
|
||||
{
|
||||
Vector3 directionToTarget = target.position - crystalTransform.position;
|
||||
directionToTarget.y = 0;
|
||||
|
||||
if (directionToTarget != Vector3.zero)
|
||||
{
|
||||
Quaternion targetRotation = Quaternion.LookRotation(directionToTarget);
|
||||
crystalTransform.rotation = Quaternion.RotateTowards(
|
||||
crystalTransform.rotation,
|
||||
targetRotation,
|
||||
turnSpeed * Time.deltaTime
|
||||
);
|
||||
}
|
||||
}
|
||||
else if (idleSpinSpeed > 0f)
|
||||
{
|
||||
crystalTransform.Rotate(Vector3.up, idleSpinSpeed * Time.deltaTime);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Despawns crystal from map
|
||||
/// </summary>
|
||||
public void DespawnCrystal()
|
||||
{
|
||||
if (enableDebug) Debug.Log("[CrystalShooterAI] Despawning crystal");
|
||||
|
||||
StopShooting();
|
||||
|
||||
LeanPool.Despawn(gameObject);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Forces immediate despawn (e.g. on boss death)
|
||||
/// </summary>
|
||||
public void ForceDespawn()
|
||||
{
|
||||
if (enableDebug) Debug.Log("[CrystalShooterAI] Forced despawn");
|
||||
DespawnCrystal();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns crystal state information
|
||||
/// </summary>
|
||||
public bool IsActive() => isActive;
|
||||
|
||||
public int GetShotsFired() => shotsFired;
|
||||
|
||||
public int GetRemainingShots() => Mathf.Max(0, maxShots - shotsFired);
|
||||
|
||||
public float GetTimeSinceLastShot() => Time.time - lastShotTime;
|
||||
|
||||
/// <summary>
|
||||
/// Draws gizmos in Scene View
|
||||
/// </summary>
|
||||
private void OnDrawGizmosSelected()
|
||||
{
|
||||
if (!showGizmos) return;
|
||||
|
||||
Gizmos.color = Color.red;
|
||||
Gizmos.DrawWireSphere(transform.position, maxShootingRange);
|
||||
|
||||
if (target != null)
|
||||
{
|
||||
Gizmos.color = Color.yellow;
|
||||
Gizmos.DrawLine(transform.position, target.position);
|
||||
|
||||
if (muzzle != null)
|
||||
{
|
||||
Gizmos.color = Color.green;
|
||||
Vector3 forward = transform.forward * 5f;
|
||||
Vector3 right = Quaternion.AngleAxis(aimTolerance, transform.up) * forward;
|
||||
Vector3 left = Quaternion.AngleAxis(-aimTolerance, transform.up) * forward;
|
||||
|
||||
Gizmos.DrawLine(muzzle.position, muzzle.position + right);
|
||||
Gizmos.DrawLine(muzzle.position, muzzle.position + left);
|
||||
}
|
||||
}
|
||||
|
||||
if (muzzle != null)
|
||||
{
|
||||
Gizmos.color = Color.blue;
|
||||
Gizmos.DrawWireSphere(muzzle.position, 0.2f);
|
||||
}
|
||||
}
|
||||
}
|
||||
using Lean.Pool;
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
|
||||
namespace DemonBoss.Magic
|
||||
{
|
||||
/// <summary>
|
||||
/// AI component for crystal turret spawned by boss
|
||||
/// Rotates towards player and shoots fireballs for specified time
|
||||
/// </summary>
|
||||
public class CrystalShooterAI : MonoBehaviour
|
||||
{
|
||||
[Header("Shooting Configuration")]
|
||||
[Tooltip("Transform point from which projectiles are fired")]
|
||||
public Transform muzzle;
|
||||
|
||||
[Tooltip("Fireball prefab")]
|
||||
public GameObject fireballPrefab;
|
||||
|
||||
[Tooltip("Fireball speed in m/s")]
|
||||
public float fireballSpeed = 28f;
|
||||
|
||||
[Tooltip("Time between shots in seconds")]
|
||||
public float fireRate = 0.7f;
|
||||
|
||||
[Tooltip("Maximum number of shots before auto-despawn")]
|
||||
public int maxShots = 10;
|
||||
|
||||
[Tooltip("Wait time after shooting before despawn")]
|
||||
public float despawnDelay = 3f;
|
||||
|
||||
[Header("Rotation Configuration")]
|
||||
[Tooltip("Target rotation speed in degrees/s")]
|
||||
public float turnSpeed = 120f;
|
||||
|
||||
[Tooltip("Idle spin speed when no target (degrees/s, 0 = disabled)")]
|
||||
public float idleSpinSpeed = 30f;
|
||||
|
||||
[Tooltip("Aiming accuracy in degrees (smaller value = more accurate)")]
|
||||
public float aimTolerance = 5f;
|
||||
|
||||
[Header("Targeting")]
|
||||
[Tooltip("Automatically find player on start")]
|
||||
public bool autoFindPlayer = true;
|
||||
|
||||
[Tooltip("Player tag to search for")]
|
||||
public string playerTag = "Player";
|
||||
|
||||
[Tooltip("Maximum shooting range")]
|
||||
public float maxShootingRange = 50f;
|
||||
|
||||
[Header("Effects")]
|
||||
[Tooltip("Particle effect at shot")]
|
||||
public GameObject muzzleFlashPrefab;
|
||||
|
||||
[Tooltip("Shoot sound")]
|
||||
public AudioClip shootSound;
|
||||
|
||||
[Header("Debug")]
|
||||
[Tooltip("Enable debug logging")]
|
||||
public bool enableDebug = false;
|
||||
|
||||
[Tooltip("Show gizmos in Scene View")]
|
||||
public bool showGizmos = true;
|
||||
|
||||
// Private variables
|
||||
private Transform target;
|
||||
|
||||
private AudioSource audioSource;
|
||||
private Coroutine shootingCoroutine;
|
||||
private bool isActive = false;
|
||||
private int shotsFired = 0;
|
||||
private float lastShotTime = 0f;
|
||||
|
||||
private Transform crystalTransform;
|
||||
|
||||
/// <summary>
|
||||
/// Component initialization
|
||||
/// </summary>
|
||||
private void Awake()
|
||||
{
|
||||
crystalTransform = transform;
|
||||
|
||||
audioSource = GetComponent<AudioSource>();
|
||||
if (audioSource == null && shootSound != null)
|
||||
{
|
||||
audioSource = gameObject.AddComponent<AudioSource>();
|
||||
audioSource.playOnAwake = false;
|
||||
audioSource.spatialBlend = 1f;
|
||||
}
|
||||
|
||||
if (muzzle == null)
|
||||
{
|
||||
Transform muzzleChild = crystalTransform.Find("muzzle");
|
||||
if (muzzleChild != null)
|
||||
{
|
||||
muzzle = muzzleChild;
|
||||
if (enableDebug) Debug.Log("[CrystalShooterAI] Found muzzle child");
|
||||
}
|
||||
else
|
||||
{
|
||||
muzzle = crystalTransform;
|
||||
if (enableDebug) Debug.LogWarning("[CrystalShooterAI] Using crystal center as muzzle");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Start - begin crystal operation
|
||||
/// </summary>
|
||||
private void Start()
|
||||
{
|
||||
if (enableDebug) Debug.Log("[CrystalShooterAI] Crystal activated");
|
||||
|
||||
if (autoFindPlayer && target == null)
|
||||
{
|
||||
FindPlayer();
|
||||
}
|
||||
|
||||
StartShooting();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update - rotate crystal towards target
|
||||
/// </summary>
|
||||
private void Update()
|
||||
{
|
||||
if (!isActive) return;
|
||||
|
||||
RotateTowardsTarget();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find player automatically
|
||||
/// </summary>
|
||||
private void FindPlayer()
|
||||
{
|
||||
GameObject player = GameObject.FindGameObjectWithTag(playerTag);
|
||||
if (player != null)
|
||||
{
|
||||
SetTarget(player.transform);
|
||||
if (enableDebug) Debug.Log("[CrystalShooterAI] Automatically found player");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (enableDebug) Debug.LogWarning("[CrystalShooterAI] Cannot find player with tag: " + playerTag);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set target for crystal
|
||||
/// </summary>
|
||||
/// <param name="newTarget">Target transform</param>
|
||||
public void SetTarget(Transform newTarget)
|
||||
{
|
||||
target = newTarget;
|
||||
if (enableDebug && target != null)
|
||||
{
|
||||
Debug.Log($"[CrystalShooterAI] Set target: {target.name}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Start shooting cycle
|
||||
/// </summary>
|
||||
public void StartShooting()
|
||||
{
|
||||
if (isActive) return;
|
||||
|
||||
isActive = true;
|
||||
shotsFired = 0;
|
||||
|
||||
shootingCoroutine = StartCoroutine(ShootingCoroutine());
|
||||
|
||||
if (enableDebug) Debug.Log("[CrystalShooterAI] Starting shooting");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stop shooting
|
||||
/// </summary>
|
||||
public void StopShooting()
|
||||
{
|
||||
isActive = false;
|
||||
|
||||
if (shootingCoroutine != null)
|
||||
{
|
||||
StopCoroutine(shootingCoroutine);
|
||||
shootingCoroutine = null;
|
||||
}
|
||||
|
||||
if (enableDebug) Debug.Log("[CrystalShooterAI] Stopped shooting");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Main coroutine handling shooting cycle
|
||||
/// </summary>
|
||||
private IEnumerator ShootingCoroutine()
|
||||
{
|
||||
while (shotsFired < maxShots && isActive)
|
||||
{
|
||||
if (CanShoot())
|
||||
{
|
||||
FireFireball();
|
||||
shotsFired++;
|
||||
lastShotTime = Time.time;
|
||||
}
|
||||
|
||||
yield return new WaitForSeconds(fireRate);
|
||||
}
|
||||
|
||||
if (enableDebug) Debug.Log($"[CrystalShooterAI] Finished shooting ({shotsFired} shots)");
|
||||
|
||||
yield return new WaitForSeconds(despawnDelay);
|
||||
|
||||
DespawnCrystal();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if crystal can shoot
|
||||
/// </summary>
|
||||
private bool CanShoot()
|
||||
{
|
||||
if (target == null) return false;
|
||||
|
||||
float distanceToTarget = Vector3.Distance(crystalTransform.position, target.position);
|
||||
if (distanceToTarget > maxShootingRange) return false;
|
||||
|
||||
Vector3 directionToTarget = (target.position - crystalTransform.position).normalized;
|
||||
float angleToTarget = Vector3.Angle(crystalTransform.forward, directionToTarget);
|
||||
|
||||
return angleToTarget <= aimTolerance;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fires fireball towards target
|
||||
/// </summary>
|
||||
private void FireFireball()
|
||||
{
|
||||
if (fireballPrefab == null || muzzle == null)
|
||||
{
|
||||
if (enableDebug) Debug.LogWarning("[CrystalShooterAI] Missing fireball prefab or muzzle");
|
||||
return;
|
||||
}
|
||||
|
||||
Vector3 shootDirection = crystalTransform.forward;
|
||||
if (target != null)
|
||||
{
|
||||
Vector3 targetCenter = target.position + Vector3.up * 1f;
|
||||
shootDirection = (targetCenter - muzzle.position).normalized;
|
||||
}
|
||||
|
||||
GameObject fireball = LeanPool.Spawn(fireballPrefab, muzzle.position,
|
||||
Quaternion.LookRotation(shootDirection));
|
||||
|
||||
Rigidbody fireballRb = fireball.GetComponent<Rigidbody>();
|
||||
if (fireballRb != null)
|
||||
{
|
||||
fireballRb.linearVelocity = shootDirection * fireballSpeed;
|
||||
}
|
||||
|
||||
PlayShootEffects();
|
||||
|
||||
if (enableDebug)
|
||||
{
|
||||
Debug.Log($"[CrystalShooterAI] Shot #{shotsFired + 1} in direction: {shootDirection}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plays shooting effects
|
||||
/// </summary>
|
||||
private void PlayShootEffects()
|
||||
{
|
||||
if (muzzleFlashPrefab != null && muzzle != null)
|
||||
{
|
||||
GameObject flash = LeanPool.Spawn(muzzleFlashPrefab, muzzle.position, muzzle.rotation);
|
||||
|
||||
LeanPool.Despawn(flash, 2f);
|
||||
}
|
||||
|
||||
if (audioSource != null && shootSound != null)
|
||||
{
|
||||
audioSource.PlayOneShot(shootSound);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rotates crystal towards target or performs idle spin
|
||||
/// </summary>
|
||||
private void RotateTowardsTarget()
|
||||
{
|
||||
if (target != null)
|
||||
{
|
||||
Vector3 directionToTarget = target.position - crystalTransform.position;
|
||||
directionToTarget.y = 0;
|
||||
|
||||
if (directionToTarget != Vector3.zero)
|
||||
{
|
||||
Quaternion targetRotation = Quaternion.LookRotation(directionToTarget);
|
||||
crystalTransform.rotation = Quaternion.RotateTowards(
|
||||
crystalTransform.rotation,
|
||||
targetRotation,
|
||||
turnSpeed * Time.deltaTime
|
||||
);
|
||||
}
|
||||
}
|
||||
else if (idleSpinSpeed > 0f)
|
||||
{
|
||||
crystalTransform.Rotate(Vector3.up, idleSpinSpeed * Time.deltaTime);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Despawns crystal from map
|
||||
/// </summary>
|
||||
public void DespawnCrystal()
|
||||
{
|
||||
if (enableDebug) Debug.Log("[CrystalShooterAI] Despawning crystal");
|
||||
|
||||
StopShooting();
|
||||
|
||||
LeanPool.Despawn(gameObject);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Forces immediate despawn (e.g. on boss death)
|
||||
/// </summary>
|
||||
public void ForceDespawn()
|
||||
{
|
||||
if (enableDebug) Debug.Log("[CrystalShooterAI] Forced despawn");
|
||||
DespawnCrystal();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns crystal state information
|
||||
/// </summary>
|
||||
public bool IsActive() => isActive;
|
||||
|
||||
public int GetShotsFired() => shotsFired;
|
||||
|
||||
public int GetRemainingShots() => Mathf.Max(0, maxShots - shotsFired);
|
||||
|
||||
public float GetTimeSinceLastShot() => Time.time - lastShotTime;
|
||||
|
||||
/// <summary>
|
||||
/// Draws gizmos in Scene View
|
||||
/// </summary>
|
||||
private void OnDrawGizmosSelected()
|
||||
{
|
||||
if (!showGizmos) return;
|
||||
|
||||
Gizmos.color = Color.red;
|
||||
Gizmos.DrawWireSphere(transform.position, maxShootingRange);
|
||||
|
||||
if (target != null)
|
||||
{
|
||||
Gizmos.color = Color.yellow;
|
||||
Gizmos.DrawLine(transform.position, target.position);
|
||||
|
||||
if (muzzle != null)
|
||||
{
|
||||
Gizmos.color = Color.green;
|
||||
Vector3 forward = transform.forward * 5f;
|
||||
Vector3 right = Quaternion.AngleAxis(aimTolerance, transform.up) * forward;
|
||||
Vector3 left = Quaternion.AngleAxis(-aimTolerance, transform.up) * forward;
|
||||
|
||||
Gizmos.DrawLine(muzzle.position, muzzle.position + right);
|
||||
Gizmos.DrawLine(muzzle.position, muzzle.position + left);
|
||||
}
|
||||
}
|
||||
|
||||
if (muzzle != null)
|
||||
{
|
||||
Gizmos.color = Color.blue;
|
||||
Gizmos.DrawWireSphere(muzzle.position, 0.2f);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,205 +1,205 @@
|
||||
using Invector.vCharacterController.AI.FSMBehaviour;
|
||||
using UnityEngine;
|
||||
|
||||
namespace DemonBoss.Magic
|
||||
{
|
||||
/// <summary>
|
||||
/// Decision node checking cooldown for different boss abilities
|
||||
/// Stores Time.time in FSM timers and checks if required cooldown time has passed
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "Invector/FSM/Decisions/DemonBoss/Check Cooldown")]
|
||||
public class DEC_CheckCooldown : vStateDecision
|
||||
{
|
||||
public override string categoryName => "DemonBoss/Magic";
|
||||
public override string defaultName => "Check Cooldown";
|
||||
|
||||
[Header("Cooldown Configuration")]
|
||||
[Tooltip("Unique key for this ability (e.g. 'Shield', 'Turret', 'Meteor')")]
|
||||
public string cooldownKey = "Shield";
|
||||
|
||||
[Tooltip("Cooldown time in seconds")]
|
||||
public float cooldownTime = 10f;
|
||||
|
||||
[Tooltip("Whether ability should be available immediately at fight start")]
|
||||
public bool availableAtStart = true;
|
||||
|
||||
[Header("Debug")]
|
||||
[Tooltip("Enable debug logging")]
|
||||
public bool enableDebug = false;
|
||||
|
||||
/// <summary>
|
||||
/// Main method checking if ability is available
|
||||
/// </summary>
|
||||
/// <returns>True if cooldown has passed and ability can be used</returns>
|
||||
public override bool Decide(vIFSMBehaviourController fsmBehaviour)
|
||||
{
|
||||
if (fsmBehaviour == null)
|
||||
{
|
||||
if (enableDebug) Debug.LogWarning($"[DEC_CheckCooldown] No FSM Behaviour for key: {cooldownKey}");
|
||||
return false;
|
||||
}
|
||||
|
||||
string timerKey = "cooldown_" + cooldownKey;
|
||||
|
||||
if (!fsmBehaviour.HasTimer(timerKey))
|
||||
{
|
||||
if (availableAtStart)
|
||||
{
|
||||
if (enableDebug) Debug.Log($"[DEC_CheckCooldown] First use for {cooldownKey} - available");
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
SetCooldown(fsmBehaviour, cooldownTime);
|
||||
if (enableDebug) Debug.Log($"[DEC_CheckCooldown] First use for {cooldownKey} - setting cooldown");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
float lastUsedTime = fsmBehaviour.GetTimer(timerKey);
|
||||
float timeSinceLastUse = Time.time - lastUsedTime;
|
||||
|
||||
bool isAvailable = timeSinceLastUse >= cooldownTime;
|
||||
|
||||
if (enableDebug)
|
||||
{
|
||||
if (isAvailable)
|
||||
{
|
||||
Debug.Log($"[DEC_CheckCooldown] {cooldownKey} available - {timeSinceLastUse:F1}s passed of required {cooldownTime}s");
|
||||
}
|
||||
else
|
||||
{
|
||||
float remainingTime = cooldownTime - timeSinceLastUse;
|
||||
Debug.Log($"[DEC_CheckCooldown] {cooldownKey} on cooldown - {remainingTime:F1}s remaining");
|
||||
}
|
||||
}
|
||||
|
||||
return isAvailable;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets cooldown for ability - call this after using ability
|
||||
/// </summary>
|
||||
public void SetCooldown(vIFSMBehaviourController fsmBehaviour)
|
||||
{
|
||||
SetCooldown(fsmBehaviour, cooldownTime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets cooldown with custom time
|
||||
/// </summary>
|
||||
/// <param name="fsmBehaviour">FSM behaviour reference</param>
|
||||
/// <param name="customCooldownTime">Custom cooldown time</param>
|
||||
public void SetCooldown(vIFSMBehaviourController fsmBehaviour, float customCooldownTime)
|
||||
{
|
||||
if (fsmBehaviour == null)
|
||||
{
|
||||
if (enableDebug) Debug.LogWarning($"[DEC_CheckCooldown] Cannot set cooldown - no FSM Behaviour");
|
||||
return;
|
||||
}
|
||||
|
||||
string timerKey = "cooldown_" + cooldownKey;
|
||||
|
||||
fsmBehaviour.SetTimer(timerKey, Time.time);
|
||||
|
||||
if (enableDebug)
|
||||
{
|
||||
Debug.Log($"[DEC_CheckCooldown] Set cooldown for {cooldownKey}: {customCooldownTime}s");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets cooldown - ability becomes immediately available
|
||||
/// </summary>
|
||||
public void ResetCooldown(vIFSMBehaviourController fsmBehaviour)
|
||||
{
|
||||
if (fsmBehaviour == null)
|
||||
{
|
||||
if (enableDebug) Debug.LogWarning($"[DEC_CheckCooldown] Cannot reset cooldown - no FSM Behaviour");
|
||||
return;
|
||||
}
|
||||
|
||||
string timerKey = "cooldown_" + cooldownKey;
|
||||
|
||||
float pastTime = Time.time - cooldownTime - 1f;
|
||||
fsmBehaviour.SetTimer(timerKey, pastTime);
|
||||
|
||||
if (enableDebug) Debug.Log($"[DEC_CheckCooldown] Reset cooldown for {cooldownKey}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns remaining cooldown time in seconds
|
||||
/// </summary>
|
||||
/// <returns>Remaining cooldown time (0 if available)</returns>
|
||||
public float GetRemainingCooldown(vIFSMBehaviourController fsmBehaviour)
|
||||
{
|
||||
if (fsmBehaviour == null) return 0f;
|
||||
|
||||
string timerKey = "cooldown_" + cooldownKey;
|
||||
|
||||
if (!fsmBehaviour.HasTimer(timerKey))
|
||||
{
|
||||
return availableAtStart ? 0f : cooldownTime;
|
||||
}
|
||||
|
||||
float lastUsedTime = fsmBehaviour.GetTimer(timerKey);
|
||||
float timeSinceLastUse = Time.time - lastUsedTime;
|
||||
|
||||
return Mathf.Max(0f, cooldownTime - timeSinceLastUse);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if ability is available without running main Decision logic
|
||||
/// </summary>
|
||||
/// <returns>True if ability is available</returns>
|
||||
public bool IsAvailable(vIFSMBehaviourController fsmBehaviour)
|
||||
{
|
||||
return GetRemainingCooldown(fsmBehaviour) <= 0f;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns cooldown progress percentage (0-1)
|
||||
/// </summary>
|
||||
/// <returns>Progress percentage: 0 = just used, 1 = fully recharged</returns>
|
||||
public float GetCooldownProgress(vIFSMBehaviourController fsmBehaviour)
|
||||
{
|
||||
if (cooldownTime <= 0f) return 1f;
|
||||
|
||||
float remainingTime = GetRemainingCooldown(fsmBehaviour);
|
||||
return 1f - (remainingTime / cooldownTime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method for setting cooldown from external code (e.g. from StateAction)
|
||||
/// </summary>
|
||||
/// <param name="fsmBehaviour">FSM reference</param>
|
||||
/// <param name="key">Ability key</param>
|
||||
/// <param name="cooldown">Cooldown time</param>
|
||||
public static void SetCooldownStatic(vIFSMBehaviourController fsmBehaviour, string key, float cooldown)
|
||||
{
|
||||
if (fsmBehaviour == null) return;
|
||||
|
||||
string timerKey = "cooldown_" + key;
|
||||
fsmBehaviour.SetTimer(timerKey, Time.time);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method for checking cooldown from external code
|
||||
/// </summary>
|
||||
/// <param name="fsmBehaviour">FSM reference</param>
|
||||
/// <param name="key">Ability key</param>
|
||||
/// <param name="cooldown">Cooldown time</param>
|
||||
/// <returns>True if available</returns>
|
||||
public static bool CheckCooldownStatic(vIFSMBehaviourController fsmBehaviour, string key, float cooldown)
|
||||
{
|
||||
if (fsmBehaviour == null) return false;
|
||||
|
||||
string timerKey = "cooldown_" + key;
|
||||
|
||||
if (!fsmBehaviour.HasTimer(timerKey)) return true;
|
||||
|
||||
float lastUsedTime = fsmBehaviour.GetTimer(timerKey);
|
||||
return (Time.time - lastUsedTime) >= cooldown;
|
||||
}
|
||||
}
|
||||
using Invector.vCharacterController.AI.FSMBehaviour;
|
||||
using UnityEngine;
|
||||
|
||||
namespace DemonBoss.Magic
|
||||
{
|
||||
/// <summary>
|
||||
/// Decision node checking cooldown for different boss abilities
|
||||
/// Stores Time.time in FSM timers and checks if required cooldown time has passed
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "Invector/FSM/Decisions/DemonBoss/Check Cooldown")]
|
||||
public class DEC_CheckCooldown : vStateDecision
|
||||
{
|
||||
public override string categoryName => "DemonBoss/Magic";
|
||||
public override string defaultName => "Check Cooldown";
|
||||
|
||||
[Header("Cooldown Configuration")]
|
||||
[Tooltip("Unique key for this ability (e.g. 'Shield', 'Turret', 'Meteor')")]
|
||||
public string cooldownKey = "Shield";
|
||||
|
||||
[Tooltip("Cooldown time in seconds")]
|
||||
public float cooldownTime = 10f;
|
||||
|
||||
[Tooltip("Whether ability should be available immediately at fight start")]
|
||||
public bool availableAtStart = true;
|
||||
|
||||
[Header("Debug")]
|
||||
[Tooltip("Enable debug logging")]
|
||||
public bool enableDebug = false;
|
||||
|
||||
/// <summary>
|
||||
/// Main method checking if ability is available
|
||||
/// </summary>
|
||||
/// <returns>True if cooldown has passed and ability can be used</returns>
|
||||
public override bool Decide(vIFSMBehaviourController fsmBehaviour)
|
||||
{
|
||||
if (fsmBehaviour == null)
|
||||
{
|
||||
if (enableDebug) Debug.LogWarning($"[DEC_CheckCooldown] No FSM Behaviour for key: {cooldownKey}");
|
||||
return false;
|
||||
}
|
||||
|
||||
string timerKey = "cooldown_" + cooldownKey;
|
||||
|
||||
if (!fsmBehaviour.HasTimer(timerKey))
|
||||
{
|
||||
if (availableAtStart)
|
||||
{
|
||||
if (enableDebug) Debug.Log($"[DEC_CheckCooldown] First use for {cooldownKey} - available");
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
SetCooldown(fsmBehaviour, cooldownTime);
|
||||
if (enableDebug) Debug.Log($"[DEC_CheckCooldown] First use for {cooldownKey} - setting cooldown");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
float lastUsedTime = fsmBehaviour.GetTimer(timerKey);
|
||||
float timeSinceLastUse = Time.time - lastUsedTime;
|
||||
|
||||
bool isAvailable = timeSinceLastUse >= cooldownTime;
|
||||
|
||||
if (enableDebug)
|
||||
{
|
||||
if (isAvailable)
|
||||
{
|
||||
Debug.Log($"[DEC_CheckCooldown] {cooldownKey} available - {timeSinceLastUse:F1}s passed of required {cooldownTime}s");
|
||||
}
|
||||
else
|
||||
{
|
||||
float remainingTime = cooldownTime - timeSinceLastUse;
|
||||
Debug.Log($"[DEC_CheckCooldown] {cooldownKey} on cooldown - {remainingTime:F1}s remaining");
|
||||
}
|
||||
}
|
||||
|
||||
return isAvailable;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets cooldown for ability - call this after using ability
|
||||
/// </summary>
|
||||
public void SetCooldown(vIFSMBehaviourController fsmBehaviour)
|
||||
{
|
||||
SetCooldown(fsmBehaviour, cooldownTime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets cooldown with custom time
|
||||
/// </summary>
|
||||
/// <param name="fsmBehaviour">FSM behaviour reference</param>
|
||||
/// <param name="customCooldownTime">Custom cooldown time</param>
|
||||
public void SetCooldown(vIFSMBehaviourController fsmBehaviour, float customCooldownTime)
|
||||
{
|
||||
if (fsmBehaviour == null)
|
||||
{
|
||||
if (enableDebug) Debug.LogWarning($"[DEC_CheckCooldown] Cannot set cooldown - no FSM Behaviour");
|
||||
return;
|
||||
}
|
||||
|
||||
string timerKey = "cooldown_" + cooldownKey;
|
||||
|
||||
fsmBehaviour.SetTimer(timerKey, Time.time);
|
||||
|
||||
if (enableDebug)
|
||||
{
|
||||
Debug.Log($"[DEC_CheckCooldown] Set cooldown for {cooldownKey}: {customCooldownTime}s");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets cooldown - ability becomes immediately available
|
||||
/// </summary>
|
||||
public void ResetCooldown(vIFSMBehaviourController fsmBehaviour)
|
||||
{
|
||||
if (fsmBehaviour == null)
|
||||
{
|
||||
if (enableDebug) Debug.LogWarning($"[DEC_CheckCooldown] Cannot reset cooldown - no FSM Behaviour");
|
||||
return;
|
||||
}
|
||||
|
||||
string timerKey = "cooldown_" + cooldownKey;
|
||||
|
||||
float pastTime = Time.time - cooldownTime - 1f;
|
||||
fsmBehaviour.SetTimer(timerKey, pastTime);
|
||||
|
||||
if (enableDebug) Debug.Log($"[DEC_CheckCooldown] Reset cooldown for {cooldownKey}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns remaining cooldown time in seconds
|
||||
/// </summary>
|
||||
/// <returns>Remaining cooldown time (0 if available)</returns>
|
||||
public float GetRemainingCooldown(vIFSMBehaviourController fsmBehaviour)
|
||||
{
|
||||
if (fsmBehaviour == null) return 0f;
|
||||
|
||||
string timerKey = "cooldown_" + cooldownKey;
|
||||
|
||||
if (!fsmBehaviour.HasTimer(timerKey))
|
||||
{
|
||||
return availableAtStart ? 0f : cooldownTime;
|
||||
}
|
||||
|
||||
float lastUsedTime = fsmBehaviour.GetTimer(timerKey);
|
||||
float timeSinceLastUse = Time.time - lastUsedTime;
|
||||
|
||||
return Mathf.Max(0f, cooldownTime - timeSinceLastUse);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if ability is available without running main Decision logic
|
||||
/// </summary>
|
||||
/// <returns>True if ability is available</returns>
|
||||
public bool IsAvailable(vIFSMBehaviourController fsmBehaviour)
|
||||
{
|
||||
return GetRemainingCooldown(fsmBehaviour) <= 0f;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns cooldown progress percentage (0-1)
|
||||
/// </summary>
|
||||
/// <returns>Progress percentage: 0 = just used, 1 = fully recharged</returns>
|
||||
public float GetCooldownProgress(vIFSMBehaviourController fsmBehaviour)
|
||||
{
|
||||
if (cooldownTime <= 0f) return 1f;
|
||||
|
||||
float remainingTime = GetRemainingCooldown(fsmBehaviour);
|
||||
return 1f - (remainingTime / cooldownTime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method for setting cooldown from external code (e.g. from StateAction)
|
||||
/// </summary>
|
||||
/// <param name="fsmBehaviour">FSM reference</param>
|
||||
/// <param name="key">Ability key</param>
|
||||
/// <param name="cooldown">Cooldown time</param>
|
||||
public static void SetCooldownStatic(vIFSMBehaviourController fsmBehaviour, string key, float cooldown)
|
||||
{
|
||||
if (fsmBehaviour == null) return;
|
||||
|
||||
string timerKey = "cooldown_" + key;
|
||||
fsmBehaviour.SetTimer(timerKey, Time.time);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method for checking cooldown from external code
|
||||
/// </summary>
|
||||
/// <param name="fsmBehaviour">FSM reference</param>
|
||||
/// <param name="key">Ability key</param>
|
||||
/// <param name="cooldown">Cooldown time</param>
|
||||
/// <returns>True if available</returns>
|
||||
public static bool CheckCooldownStatic(vIFSMBehaviourController fsmBehaviour, string key, float cooldown)
|
||||
{
|
||||
if (fsmBehaviour == null) return false;
|
||||
|
||||
string timerKey = "cooldown_" + key;
|
||||
|
||||
if (!fsmBehaviour.HasTimer(timerKey)) return true;
|
||||
|
||||
float lastUsedTime = fsmBehaviour.GetTimer(timerKey);
|
||||
return (Time.time - lastUsedTime) >= cooldown;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,105 +1,105 @@
|
||||
using Invector.vCharacterController.AI.FSMBehaviour;
|
||||
using UnityEngine;
|
||||
|
||||
namespace DemonBoss.Magic
|
||||
{
|
||||
/// <summary>
|
||||
/// Decision checking if target has clear sky above (for meteor)
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "Invector/FSM/Decisions/DemonBoss/Target Clear Sky")]
|
||||
public class DEC_TargetClearSky : vStateDecision
|
||||
{
|
||||
public override string categoryName => "DemonBoss/Magic";
|
||||
public override string defaultName => "Target Clear Sky";
|
||||
|
||||
[Header("Sky Check Configuration")]
|
||||
[Tooltip("Check height above target")]
|
||||
public float checkHeight = 25f;
|
||||
|
||||
[Tooltip("Obstacle check radius")]
|
||||
public float checkRadius = 2f;
|
||||
|
||||
[Tooltip("Obstacle layer mask")]
|
||||
public LayerMask obstacleLayerMask = -1;
|
||||
|
||||
[Header("Debug")]
|
||||
[Tooltip("Enable debug logging")]
|
||||
public bool enableDebug = false;
|
||||
|
||||
[Tooltip("Show gizmos in Scene View")]
|
||||
public bool showGizmos = true;
|
||||
|
||||
public override bool Decide(vIFSMBehaviourController fsmBehaviour)
|
||||
{
|
||||
Transform target = GetTarget(fsmBehaviour);
|
||||
if (target == null)
|
||||
{
|
||||
if (enableDebug) Debug.Log("[DEC_TargetClearSky] No target found");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool isClear = IsSkyClear(target.position);
|
||||
|
||||
if (enableDebug)
|
||||
{
|
||||
Debug.Log($"[DEC_TargetClearSky] Sky above target: {(isClear ? "CLEAR" : "BLOCKED")}");
|
||||
}
|
||||
|
||||
return isClear;
|
||||
}
|
||||
|
||||
private bool IsSkyClear(Vector3 targetPosition)
|
||||
{
|
||||
Vector3 skyCheckPoint = targetPosition + Vector3.up * checkHeight;
|
||||
|
||||
if (Physics.CheckSphere(skyCheckPoint, checkRadius, obstacleLayerMask))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Ray skyRay = new Ray(skyCheckPoint, Vector3.down);
|
||||
RaycastHit[] hits = Physics.RaycastAll(skyRay, checkHeight, obstacleLayerMask);
|
||||
|
||||
foreach (var hit in hits)
|
||||
{
|
||||
if (hit.point.y <= targetPosition.y + 0.5f) continue;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private Transform GetTarget(vIFSMBehaviourController fsmBehaviour)
|
||||
{
|
||||
// Try through AI controller
|
||||
var aiController = fsmBehaviour as Invector.vCharacterController.AI.vIControlAI;
|
||||
if (aiController != null && aiController.currentTarget != null)
|
||||
return aiController.currentTarget.transform;
|
||||
|
||||
// Fallback - find player
|
||||
GameObject player = GameObject.FindGameObjectWithTag("Player");
|
||||
return player?.transform;
|
||||
}
|
||||
|
||||
private void OnDrawGizmosSelected()
|
||||
{
|
||||
if (!showGizmos) return;
|
||||
|
||||
GameObject player = GameObject.FindGameObjectWithTag("Player");
|
||||
if (player == null) return;
|
||||
|
||||
Vector3 targetPos = player.transform.position;
|
||||
Vector3 skyCheckPoint = targetPos + Vector3.up * checkHeight;
|
||||
|
||||
Gizmos.color = Color.cyan;
|
||||
Gizmos.DrawWireSphere(skyCheckPoint, checkRadius);
|
||||
|
||||
Gizmos.color = Color.yellow;
|
||||
Gizmos.DrawLine(targetPos, skyCheckPoint);
|
||||
|
||||
Gizmos.color = Color.red;
|
||||
Gizmos.DrawWireSphere(targetPos, 0.5f);
|
||||
}
|
||||
}
|
||||
using Invector.vCharacterController.AI.FSMBehaviour;
|
||||
using UnityEngine;
|
||||
|
||||
namespace DemonBoss.Magic
|
||||
{
|
||||
/// <summary>
|
||||
/// Decision checking if target has clear sky above (for meteor)
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "Invector/FSM/Decisions/DemonBoss/Target Clear Sky")]
|
||||
public class DEC_TargetClearSky : vStateDecision
|
||||
{
|
||||
public override string categoryName => "DemonBoss/Magic";
|
||||
public override string defaultName => "Target Clear Sky";
|
||||
|
||||
[Header("Sky Check Configuration")]
|
||||
[Tooltip("Check height above target")]
|
||||
public float checkHeight = 25f;
|
||||
|
||||
[Tooltip("Obstacle check radius")]
|
||||
public float checkRadius = 2f;
|
||||
|
||||
[Tooltip("Obstacle layer mask")]
|
||||
public LayerMask obstacleLayerMask = -1;
|
||||
|
||||
[Header("Debug")]
|
||||
[Tooltip("Enable debug logging")]
|
||||
public bool enableDebug = false;
|
||||
|
||||
[Tooltip("Show gizmos in Scene View")]
|
||||
public bool showGizmos = true;
|
||||
|
||||
public override bool Decide(vIFSMBehaviourController fsmBehaviour)
|
||||
{
|
||||
Transform target = GetTarget(fsmBehaviour);
|
||||
if (target == null)
|
||||
{
|
||||
if (enableDebug) Debug.Log("[DEC_TargetClearSky] No target found");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool isClear = IsSkyClear(target.position);
|
||||
|
||||
if (enableDebug)
|
||||
{
|
||||
Debug.Log($"[DEC_TargetClearSky] Sky above target: {(isClear ? "CLEAR" : "BLOCKED")}");
|
||||
}
|
||||
|
||||
return isClear;
|
||||
}
|
||||
|
||||
private bool IsSkyClear(Vector3 targetPosition)
|
||||
{
|
||||
Vector3 skyCheckPoint = targetPosition + Vector3.up * checkHeight;
|
||||
|
||||
if (Physics.CheckSphere(skyCheckPoint, checkRadius, obstacleLayerMask))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Ray skyRay = new Ray(skyCheckPoint, Vector3.down);
|
||||
RaycastHit[] hits = Physics.RaycastAll(skyRay, checkHeight, obstacleLayerMask);
|
||||
|
||||
foreach (var hit in hits)
|
||||
{
|
||||
if (hit.point.y <= targetPosition.y + 0.5f) continue;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private Transform GetTarget(vIFSMBehaviourController fsmBehaviour)
|
||||
{
|
||||
// Try through AI controller
|
||||
var aiController = fsmBehaviour as Invector.vCharacterController.AI.vIControlAI;
|
||||
if (aiController != null && aiController.currentTarget != null)
|
||||
return aiController.currentTarget.transform;
|
||||
|
||||
// Fallback - find player
|
||||
GameObject player = GameObject.FindGameObjectWithTag("Player");
|
||||
return player?.transform;
|
||||
}
|
||||
|
||||
private void OnDrawGizmosSelected()
|
||||
{
|
||||
if (!showGizmos) return;
|
||||
|
||||
GameObject player = GameObject.FindGameObjectWithTag("Player");
|
||||
if (player == null) return;
|
||||
|
||||
Vector3 targetPos = player.transform.position;
|
||||
Vector3 skyCheckPoint = targetPos + Vector3.up * checkHeight;
|
||||
|
||||
Gizmos.color = Color.cyan;
|
||||
Gizmos.DrawWireSphere(skyCheckPoint, checkRadius);
|
||||
|
||||
Gizmos.color = Color.yellow;
|
||||
Gizmos.DrawLine(targetPos, skyCheckPoint);
|
||||
|
||||
Gizmos.color = Color.red;
|
||||
Gizmos.DrawWireSphere(targetPos, 0.5f);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,58 +1,58 @@
|
||||
using Invector.vCharacterController.AI.FSMBehaviour;
|
||||
using UnityEngine;
|
||||
|
||||
namespace DemonBoss.Magic
|
||||
{
|
||||
/// <summary>
|
||||
/// Decision checking if target is far away (for Turret ability)
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "Invector/FSM/Decisions/DemonBoss/Target Far")]
|
||||
public class DEC_TargetFar : vStateDecision
|
||||
{
|
||||
public override string categoryName => "DemonBoss/Magic";
|
||||
public override string defaultName => "Target Far";
|
||||
|
||||
[Header("Distance Configuration")]
|
||||
[Tooltip("Minimum distance for target to be considered far")]
|
||||
public float minDistance = 8f;
|
||||
|
||||
[Tooltip("Maximum distance for checking")]
|
||||
public float maxDistance = 30f;
|
||||
|
||||
[Header("Debug")]
|
||||
[Tooltip("Enable debug logging")]
|
||||
public bool enableDebug = false;
|
||||
|
||||
public override bool Decide(vIFSMBehaviourController fsmBehaviour)
|
||||
{
|
||||
Transform target = GetTarget(fsmBehaviour);
|
||||
if (target == null)
|
||||
{
|
||||
if (enableDebug) Debug.Log("[DEC_TargetFar] No target found");
|
||||
return false;
|
||||
}
|
||||
|
||||
float distance = Vector3.Distance(fsmBehaviour.transform.position, target.position);
|
||||
bool isFar = distance >= minDistance && distance <= maxDistance;
|
||||
|
||||
if (enableDebug)
|
||||
{
|
||||
Debug.Log($"[DEC_TargetFar] Distance to target: {distance:F1}m - {(isFar ? "FAR" : "CLOSE")}");
|
||||
}
|
||||
|
||||
return isFar;
|
||||
}
|
||||
|
||||
private Transform GetTarget(vIFSMBehaviourController fsmBehaviour)
|
||||
{
|
||||
// Try through AI controller
|
||||
var aiController = fsmBehaviour as Invector.vCharacterController.AI.vIControlAI;
|
||||
if (aiController != null && aiController.currentTarget != null)
|
||||
return aiController.currentTarget.transform;
|
||||
|
||||
// Fallback - find player
|
||||
GameObject player = GameObject.FindGameObjectWithTag("Player");
|
||||
return player?.transform;
|
||||
}
|
||||
}
|
||||
using Invector.vCharacterController.AI.FSMBehaviour;
|
||||
using UnityEngine;
|
||||
|
||||
namespace DemonBoss.Magic
|
||||
{
|
||||
/// <summary>
|
||||
/// Decision checking if target is far away (for Turret ability)
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "Invector/FSM/Decisions/DemonBoss/Target Far")]
|
||||
public class DEC_TargetFar : vStateDecision
|
||||
{
|
||||
public override string categoryName => "DemonBoss/Magic";
|
||||
public override string defaultName => "Target Far";
|
||||
|
||||
[Header("Distance Configuration")]
|
||||
[Tooltip("Minimum distance for target to be considered far")]
|
||||
public float minDistance = 8f;
|
||||
|
||||
[Tooltip("Maximum distance for checking")]
|
||||
public float maxDistance = 30f;
|
||||
|
||||
[Header("Debug")]
|
||||
[Tooltip("Enable debug logging")]
|
||||
public bool enableDebug = false;
|
||||
|
||||
public override bool Decide(vIFSMBehaviourController fsmBehaviour)
|
||||
{
|
||||
Transform target = GetTarget(fsmBehaviour);
|
||||
if (target == null)
|
||||
{
|
||||
if (enableDebug) Debug.Log("[DEC_TargetFar] No target found");
|
||||
return false;
|
||||
}
|
||||
|
||||
float distance = Vector3.Distance(fsmBehaviour.transform.position, target.position);
|
||||
bool isFar = distance >= minDistance && distance <= maxDistance;
|
||||
|
||||
if (enableDebug)
|
||||
{
|
||||
Debug.Log($"[DEC_TargetFar] Distance to target: {distance:F1}m - {(isFar ? "FAR" : "CLOSE")}");
|
||||
}
|
||||
|
||||
return isFar;
|
||||
}
|
||||
|
||||
private Transform GetTarget(vIFSMBehaviourController fsmBehaviour)
|
||||
{
|
||||
// Try through AI controller
|
||||
var aiController = fsmBehaviour as Invector.vCharacterController.AI.vIControlAI;
|
||||
if (aiController != null && aiController.currentTarget != null)
|
||||
return aiController.currentTarget.transform;
|
||||
|
||||
// Fallback - find player
|
||||
GameObject player = GameObject.FindGameObjectWithTag("Player");
|
||||
return player?.transform;
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,386 +1,386 @@
|
||||
using Invector;
|
||||
using Invector.vCharacterController;
|
||||
using Lean.Pool;
|
||||
using UnityEngine;
|
||||
|
||||
namespace DemonBoss.Magic
|
||||
{
|
||||
/// <summary>
|
||||
/// Component handling damage and collisions for fireball fired by crystals
|
||||
/// Deals damage on collision with player and automatically despawns
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(Collider))]
|
||||
public class FireballDamage : MonoBehaviour
|
||||
{
|
||||
[Header("Damage Configuration")]
|
||||
[Tooltip("Damage dealt by fireball")]
|
||||
public float damage = 25f;
|
||||
|
||||
[Tooltip("Whether fireball can deal damage only once")]
|
||||
public bool damageOnce = true;
|
||||
|
||||
[Tooltip("Layer mask for targets that can be hit")]
|
||||
public LayerMask targetLayerMask = -1;
|
||||
|
||||
[Header("Impact Effects")]
|
||||
[Tooltip("Explosion effect prefab on hit")]
|
||||
public GameObject impactEffectPrefab;
|
||||
|
||||
[Tooltip("Impact sound")]
|
||||
public AudioClip impactSound;
|
||||
|
||||
[Tooltip("Knockback force on hit")]
|
||||
public float knockbackForce = 5f;
|
||||
|
||||
[Header("Lifetime")]
|
||||
[Tooltip("Maximum fireball lifetime in seconds (failsafe)")]
|
||||
public float maxLifetime = 5f;
|
||||
|
||||
[Tooltip("Whether to destroy fireball on terrain collision")]
|
||||
public bool destroyOnTerrain = true;
|
||||
|
||||
[Tooltip("Layer mask for terrain/obstacles")]
|
||||
public LayerMask terrainLayerMask = 1;
|
||||
|
||||
[Header("Debug")]
|
||||
[Tooltip("Enable debug logging")]
|
||||
public bool enableDebug = false;
|
||||
|
||||
// Private variables
|
||||
private bool hasDealtDamage = false;
|
||||
|
||||
private float spawnTime;
|
||||
private AudioSource audioSource;
|
||||
private Collider fireballCollider;
|
||||
private Rigidbody fireballRigidbody;
|
||||
|
||||
/// <summary>
|
||||
/// Component initialization
|
||||
/// </summary>
|
||||
private void Awake()
|
||||
{
|
||||
fireballCollider = GetComponent<Collider>();
|
||||
fireballRigidbody = GetComponent<Rigidbody>();
|
||||
|
||||
if (fireballCollider != null && !fireballCollider.isTrigger)
|
||||
{
|
||||
fireballCollider.isTrigger = true;
|
||||
if (enableDebug) Debug.Log("[FireballDamage] Set collider as trigger");
|
||||
}
|
||||
|
||||
audioSource = GetComponent<AudioSource>();
|
||||
if (audioSource == null && impactSound != null)
|
||||
{
|
||||
audioSource = gameObject.AddComponent<AudioSource>();
|
||||
audioSource.playOnAwake = false;
|
||||
audioSource.spatialBlend = 1f;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Start - note spawn time and start failsafe timer
|
||||
/// </summary>
|
||||
private void Start()
|
||||
{
|
||||
spawnTime = Time.time;
|
||||
hasDealtDamage = false;
|
||||
|
||||
Invoke(nameof(FailsafeDespawn), maxLifetime);
|
||||
|
||||
if (enableDebug) Debug.Log("[FireballDamage] Fireball initialized");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Trigger collision handling - dealing damage
|
||||
/// </summary>
|
||||
/// <param name="other">Collider that fireball collides with</param>
|
||||
private void OnTriggerEnter(Collider other)
|
||||
{
|
||||
if (enableDebug) Debug.Log($"[FireballDamage] Collision with: {other.name}");
|
||||
|
||||
if (IsValidTarget(other))
|
||||
{
|
||||
DealDamageToTarget(other);
|
||||
CreateImpactEffect(other.transform.position);
|
||||
DespawnFireball();
|
||||
return;
|
||||
}
|
||||
|
||||
if (destroyOnTerrain && IsTerrainCollision(other))
|
||||
{
|
||||
if (enableDebug) Debug.Log("[FireballDamage] Terrain collision");
|
||||
CreateImpactEffect(transform.position);
|
||||
DespawnFireball();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if collider is valid target
|
||||
/// </summary>
|
||||
/// <param name="other">Collider to check</param>
|
||||
/// <returns>True if it's valid target</returns>
|
||||
private bool IsValidTarget(Collider other)
|
||||
{
|
||||
if ((targetLayerMask.value & (1 << other.gameObject.layer)) == 0)
|
||||
return false;
|
||||
|
||||
if (damageOnce && hasDealtDamage)
|
||||
return false;
|
||||
|
||||
var damageReceiver = other.GetComponent<vIDamageReceiver>();
|
||||
var healthController = other.GetComponent<vHealthController>();
|
||||
var character = other.GetComponent<vCharacter>();
|
||||
var thirdPersonController = other.GetComponent<vThirdPersonController>();
|
||||
|
||||
if (damageReceiver == null) damageReceiver = other.GetComponentInParent<vIDamageReceiver>();
|
||||
if (healthController == null) healthController = other.GetComponentInParent<vHealthController>();
|
||||
if (character == null) character = other.GetComponentInParent<vCharacter>();
|
||||
if (thirdPersonController == null) thirdPersonController = other.GetComponentInParent<vThirdPersonController>();
|
||||
|
||||
return damageReceiver != null || healthController != null || character != null || thirdPersonController != null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if it's terrain collision
|
||||
/// </summary>
|
||||
/// <param name="other">Collider to check</param>
|
||||
/// <returns>True if it's terrain</returns>
|
||||
private bool IsTerrainCollision(Collider other)
|
||||
{
|
||||
return (terrainLayerMask.value & (1 << other.gameObject.layer)) != 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deals damage to target - ulepszona wersja
|
||||
/// </summary>
|
||||
/// <param name="targetCollider">Target collider</param>
|
||||
private void DealDamageToTarget(Collider targetCollider)
|
||||
{
|
||||
if (enableDebug) Debug.Log($"[FireballDamage] Dealing {damage} damage to: {targetCollider.name}");
|
||||
|
||||
Vector3 hitPoint = GetClosestPointOnCollider(targetCollider);
|
||||
Vector3 hitDirection = GetHitDirection(targetCollider);
|
||||
|
||||
vDamage damageInfo = new vDamage(Mathf.RoundToInt(damage));
|
||||
damageInfo.sender = transform;
|
||||
damageInfo.hitPosition = hitPoint;
|
||||
|
||||
if (knockbackForce > 0f)
|
||||
{
|
||||
damageInfo.force = hitDirection * knockbackForce;
|
||||
}
|
||||
|
||||
bool damageDealt = false;
|
||||
|
||||
var damageReceiver = targetCollider.GetComponent<vIDamageReceiver>();
|
||||
if (damageReceiver == null) damageReceiver = targetCollider.GetComponentInParent<vIDamageReceiver>();
|
||||
|
||||
if (damageReceiver != null && !damageDealt)
|
||||
{
|
||||
damageReceiver.TakeDamage(damageInfo);
|
||||
damageDealt = true;
|
||||
hasDealtDamage = true;
|
||||
if (enableDebug) Debug.Log("[FireballDamage] Damage dealt through vIDamageReceiver");
|
||||
}
|
||||
|
||||
if (!damageDealt)
|
||||
{
|
||||
var healthController = targetCollider.GetComponent<vHealthController>();
|
||||
if (healthController == null) healthController = targetCollider.GetComponentInParent<vHealthController>();
|
||||
|
||||
if (healthController != null)
|
||||
{
|
||||
healthController.TakeDamage(damageInfo);
|
||||
damageDealt = true;
|
||||
hasDealtDamage = true;
|
||||
if (enableDebug) Debug.Log("[FireballDamage] Damage dealt through vHealthController");
|
||||
}
|
||||
}
|
||||
|
||||
if (!damageDealt)
|
||||
{
|
||||
var thirdPersonController = targetCollider.GetComponent<vThirdPersonController>();
|
||||
if (thirdPersonController == null) thirdPersonController = targetCollider.GetComponentInParent<vThirdPersonController>();
|
||||
|
||||
if (thirdPersonController != null)
|
||||
{
|
||||
if (thirdPersonController is Beyond.bThirdPersonController beyondController)
|
||||
{
|
||||
if (!beyondController.GodMode && !beyondController.isImmortal)
|
||||
{
|
||||
thirdPersonController.TakeDamage(damageInfo);
|
||||
damageDealt = true;
|
||||
hasDealtDamage = true;
|
||||
if (enableDebug) Debug.Log("[FireballDamage] Damage dealt through bThirdPersonController");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (enableDebug) Debug.Log("[FireballDamage] Player is immortal or in God Mode - no damage dealt");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
thirdPersonController.TakeDamage(damageInfo);
|
||||
damageDealt = true;
|
||||
hasDealtDamage = true;
|
||||
if (enableDebug) Debug.Log("[FireballDamage] Damage dealt through vThirdPersonController");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!damageDealt)
|
||||
{
|
||||
if (enableDebug) Debug.LogWarning("[FireballDamage] Could not deal damage - no valid damage receiver found!");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Takes the closest point on the collider as the point of impact
|
||||
/// </summary>
|
||||
private Vector3 GetClosestPointOnCollider(Collider targetCollider)
|
||||
{
|
||||
return targetCollider.ClosestPoint(transform.position);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the direction of impact for knockback
|
||||
/// </summary>
|
||||
private Vector3 GetHitDirection(Collider targetCollider)
|
||||
{
|
||||
Vector3 direction;
|
||||
|
||||
if (fireballRigidbody != null && fireballRigidbody.linearVelocity.magnitude > 0.1f)
|
||||
{
|
||||
direction = fireballRigidbody.linearVelocity.normalized;
|
||||
}
|
||||
else
|
||||
{
|
||||
direction = (targetCollider.transform.position - transform.position).normalized;
|
||||
}
|
||||
|
||||
direction.y = Mathf.Max(0.2f, direction.y);
|
||||
return direction.normalized;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates impact effect
|
||||
/// </summary>
|
||||
/// <param name="impactPosition">Impact position</param>
|
||||
private void CreateImpactEffect(Vector3 impactPosition)
|
||||
{
|
||||
if (impactEffectPrefab != null)
|
||||
{
|
||||
GameObject impact = LeanPool.Spawn(impactEffectPrefab, impactPosition, Quaternion.identity);
|
||||
|
||||
LeanPool.Despawn(impact, 3f);
|
||||
|
||||
if (enableDebug) Debug.Log("[FireballDamage] Impact effect created");
|
||||
}
|
||||
|
||||
if (audioSource != null && impactSound != null)
|
||||
{
|
||||
audioSource.PlayOneShot(impactSound);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Despawns fireball from map
|
||||
/// </summary>
|
||||
private void DespawnFireball()
|
||||
{
|
||||
if (enableDebug) Debug.Log("[FireballDamage] Despawning fireball");
|
||||
|
||||
CancelInvoke(nameof(FailsafeDespawn));
|
||||
|
||||
LeanPool.Despawn(gameObject);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Failsafe despawn - removes fireball after maximum lifetime
|
||||
/// </summary>
|
||||
private void FailsafeDespawn()
|
||||
{
|
||||
if (enableDebug) Debug.Log("[FireballDamage] Failsafe despawn - lifetime exceeded");
|
||||
DespawnFireball();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Forces immediate despawn
|
||||
/// </summary>
|
||||
public void ForceDespawn()
|
||||
{
|
||||
if (enableDebug) Debug.Log("[FireballDamage] Forced despawn");
|
||||
DespawnFireball();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if fireball has already dealt damage
|
||||
/// </summary>
|
||||
/// <returns>True if damage was already dealt</returns>
|
||||
public bool HasDealtDamage()
|
||||
{
|
||||
return hasDealtDamage;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns fireball lifetime
|
||||
/// </summary>
|
||||
/// <returns>Time in seconds since spawn</returns>
|
||||
public float GetLifetime()
|
||||
{
|
||||
return Time.time - spawnTime;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets new damage (e.g. for different difficulty levels)
|
||||
/// </summary>
|
||||
/// <param name="newDamage">New damage</param>
|
||||
public void SetDamage(float newDamage)
|
||||
{
|
||||
damage = newDamage;
|
||||
if (enableDebug) Debug.Log($"[FireballDamage] Set new damage: {damage}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets new knockback
|
||||
/// </summary>
|
||||
/// <param name="newKnockback">New knockback force</param>
|
||||
public void SetKnockback(float newKnockback)
|
||||
{
|
||||
knockbackForce = newKnockback;
|
||||
if (enableDebug) Debug.Log($"[FireballDamage] Set new knockback: {knockbackForce}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets fireball state (useful for pooling)
|
||||
/// </summary>
|
||||
public void ResetFireball()
|
||||
{
|
||||
hasDealtDamage = false;
|
||||
spawnTime = Time.time;
|
||||
|
||||
CancelInvoke();
|
||||
|
||||
Invoke(nameof(FailsafeDespawn), maxLifetime);
|
||||
|
||||
if (enableDebug) Debug.Log("[FireballDamage] Fireball reset");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called on spawn by Lean Pool
|
||||
/// </summary>
|
||||
private void OnSpawn()
|
||||
{
|
||||
ResetFireball();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called on despawn by Lean Pool
|
||||
/// </summary>
|
||||
private void OnDespawn()
|
||||
{
|
||||
CancelInvoke();
|
||||
}
|
||||
}
|
||||
using Invector;
|
||||
using Invector.vCharacterController;
|
||||
using Lean.Pool;
|
||||
using UnityEngine;
|
||||
|
||||
namespace DemonBoss.Magic
|
||||
{
|
||||
/// <summary>
|
||||
/// Component handling damage and collisions for fireball fired by crystals
|
||||
/// Deals damage on collision with player and automatically despawns
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(Collider))]
|
||||
public class FireballDamage : MonoBehaviour
|
||||
{
|
||||
[Header("Damage Configuration")]
|
||||
[Tooltip("Damage dealt by fireball")]
|
||||
public float damage = 25f;
|
||||
|
||||
[Tooltip("Whether fireball can deal damage only once")]
|
||||
public bool damageOnce = true;
|
||||
|
||||
[Tooltip("Layer mask for targets that can be hit")]
|
||||
public LayerMask targetLayerMask = -1;
|
||||
|
||||
[Header("Impact Effects")]
|
||||
[Tooltip("Explosion effect prefab on hit")]
|
||||
public GameObject impactEffectPrefab;
|
||||
|
||||
[Tooltip("Impact sound")]
|
||||
public AudioClip impactSound;
|
||||
|
||||
[Tooltip("Knockback force on hit")]
|
||||
public float knockbackForce = 5f;
|
||||
|
||||
[Header("Lifetime")]
|
||||
[Tooltip("Maximum fireball lifetime in seconds (failsafe)")]
|
||||
public float maxLifetime = 5f;
|
||||
|
||||
[Tooltip("Whether to destroy fireball on terrain collision")]
|
||||
public bool destroyOnTerrain = true;
|
||||
|
||||
[Tooltip("Layer mask for terrain/obstacles")]
|
||||
public LayerMask terrainLayerMask = 1;
|
||||
|
||||
[Header("Debug")]
|
||||
[Tooltip("Enable debug logging")]
|
||||
public bool enableDebug = false;
|
||||
|
||||
// Private variables
|
||||
private bool hasDealtDamage = false;
|
||||
|
||||
private float spawnTime;
|
||||
private AudioSource audioSource;
|
||||
private Collider fireballCollider;
|
||||
private Rigidbody fireballRigidbody;
|
||||
|
||||
/// <summary>
|
||||
/// Component initialization
|
||||
/// </summary>
|
||||
private void Awake()
|
||||
{
|
||||
fireballCollider = GetComponent<Collider>();
|
||||
fireballRigidbody = GetComponent<Rigidbody>();
|
||||
|
||||
if (fireballCollider != null && !fireballCollider.isTrigger)
|
||||
{
|
||||
fireballCollider.isTrigger = true;
|
||||
if (enableDebug) Debug.Log("[FireballDamage] Set collider as trigger");
|
||||
}
|
||||
|
||||
audioSource = GetComponent<AudioSource>();
|
||||
if (audioSource == null && impactSound != null)
|
||||
{
|
||||
audioSource = gameObject.AddComponent<AudioSource>();
|
||||
audioSource.playOnAwake = false;
|
||||
audioSource.spatialBlend = 1f;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Start - note spawn time and start failsafe timer
|
||||
/// </summary>
|
||||
private void Start()
|
||||
{
|
||||
spawnTime = Time.time;
|
||||
hasDealtDamage = false;
|
||||
|
||||
Invoke(nameof(FailsafeDespawn), maxLifetime);
|
||||
|
||||
if (enableDebug) Debug.Log("[FireballDamage] Fireball initialized");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Trigger collision handling - dealing damage
|
||||
/// </summary>
|
||||
/// <param name="other">Collider that fireball collides with</param>
|
||||
private void OnTriggerEnter(Collider other)
|
||||
{
|
||||
if (enableDebug) Debug.Log($"[FireballDamage] Collision with: {other.name}");
|
||||
|
||||
if (IsValidTarget(other))
|
||||
{
|
||||
DealDamageToTarget(other);
|
||||
CreateImpactEffect(other.transform.position);
|
||||
DespawnFireball();
|
||||
return;
|
||||
}
|
||||
|
||||
if (destroyOnTerrain && IsTerrainCollision(other))
|
||||
{
|
||||
if (enableDebug) Debug.Log("[FireballDamage] Terrain collision");
|
||||
CreateImpactEffect(transform.position);
|
||||
DespawnFireball();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if collider is valid target
|
||||
/// </summary>
|
||||
/// <param name="other">Collider to check</param>
|
||||
/// <returns>True if it's valid target</returns>
|
||||
private bool IsValidTarget(Collider other)
|
||||
{
|
||||
if ((targetLayerMask.value & (1 << other.gameObject.layer)) == 0)
|
||||
return false;
|
||||
|
||||
if (damageOnce && hasDealtDamage)
|
||||
return false;
|
||||
|
||||
var damageReceiver = other.GetComponent<vIDamageReceiver>();
|
||||
var healthController = other.GetComponent<vHealthController>();
|
||||
var character = other.GetComponent<vCharacter>();
|
||||
var thirdPersonController = other.GetComponent<vThirdPersonController>();
|
||||
|
||||
if (damageReceiver == null) damageReceiver = other.GetComponentInParent<vIDamageReceiver>();
|
||||
if (healthController == null) healthController = other.GetComponentInParent<vHealthController>();
|
||||
if (character == null) character = other.GetComponentInParent<vCharacter>();
|
||||
if (thirdPersonController == null) thirdPersonController = other.GetComponentInParent<vThirdPersonController>();
|
||||
|
||||
return damageReceiver != null || healthController != null || character != null || thirdPersonController != null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if it's terrain collision
|
||||
/// </summary>
|
||||
/// <param name="other">Collider to check</param>
|
||||
/// <returns>True if it's terrain</returns>
|
||||
private bool IsTerrainCollision(Collider other)
|
||||
{
|
||||
return (terrainLayerMask.value & (1 << other.gameObject.layer)) != 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deals damage to target - ulepszona wersja
|
||||
/// </summary>
|
||||
/// <param name="targetCollider">Target collider</param>
|
||||
private void DealDamageToTarget(Collider targetCollider)
|
||||
{
|
||||
if (enableDebug) Debug.Log($"[FireballDamage] Dealing {damage} damage to: {targetCollider.name}");
|
||||
|
||||
Vector3 hitPoint = GetClosestPointOnCollider(targetCollider);
|
||||
Vector3 hitDirection = GetHitDirection(targetCollider);
|
||||
|
||||
vDamage damageInfo = new vDamage(Mathf.RoundToInt(damage));
|
||||
damageInfo.sender = transform;
|
||||
damageInfo.hitPosition = hitPoint;
|
||||
|
||||
if (knockbackForce > 0f)
|
||||
{
|
||||
damageInfo.force = hitDirection * knockbackForce;
|
||||
}
|
||||
|
||||
bool damageDealt = false;
|
||||
|
||||
var damageReceiver = targetCollider.GetComponent<vIDamageReceiver>();
|
||||
if (damageReceiver == null) damageReceiver = targetCollider.GetComponentInParent<vIDamageReceiver>();
|
||||
|
||||
if (damageReceiver != null && !damageDealt)
|
||||
{
|
||||
damageReceiver.TakeDamage(damageInfo);
|
||||
damageDealt = true;
|
||||
hasDealtDamage = true;
|
||||
if (enableDebug) Debug.Log("[FireballDamage] Damage dealt through vIDamageReceiver");
|
||||
}
|
||||
|
||||
if (!damageDealt)
|
||||
{
|
||||
var healthController = targetCollider.GetComponent<vHealthController>();
|
||||
if (healthController == null) healthController = targetCollider.GetComponentInParent<vHealthController>();
|
||||
|
||||
if (healthController != null)
|
||||
{
|
||||
healthController.TakeDamage(damageInfo);
|
||||
damageDealt = true;
|
||||
hasDealtDamage = true;
|
||||
if (enableDebug) Debug.Log("[FireballDamage] Damage dealt through vHealthController");
|
||||
}
|
||||
}
|
||||
|
||||
if (!damageDealt)
|
||||
{
|
||||
var thirdPersonController = targetCollider.GetComponent<vThirdPersonController>();
|
||||
if (thirdPersonController == null) thirdPersonController = targetCollider.GetComponentInParent<vThirdPersonController>();
|
||||
|
||||
if (thirdPersonController != null)
|
||||
{
|
||||
if (thirdPersonController is Beyond.bThirdPersonController beyondController)
|
||||
{
|
||||
if (!beyondController.GodMode && !beyondController.isImmortal)
|
||||
{
|
||||
thirdPersonController.TakeDamage(damageInfo);
|
||||
damageDealt = true;
|
||||
hasDealtDamage = true;
|
||||
if (enableDebug) Debug.Log("[FireballDamage] Damage dealt through bThirdPersonController");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (enableDebug) Debug.Log("[FireballDamage] Player is immortal or in God Mode - no damage dealt");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
thirdPersonController.TakeDamage(damageInfo);
|
||||
damageDealt = true;
|
||||
hasDealtDamage = true;
|
||||
if (enableDebug) Debug.Log("[FireballDamage] Damage dealt through vThirdPersonController");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!damageDealt)
|
||||
{
|
||||
if (enableDebug) Debug.LogWarning("[FireballDamage] Could not deal damage - no valid damage receiver found!");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Takes the closest point on the collider as the point of impact
|
||||
/// </summary>
|
||||
private Vector3 GetClosestPointOnCollider(Collider targetCollider)
|
||||
{
|
||||
return targetCollider.ClosestPoint(transform.position);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the direction of impact for knockback
|
||||
/// </summary>
|
||||
private Vector3 GetHitDirection(Collider targetCollider)
|
||||
{
|
||||
Vector3 direction;
|
||||
|
||||
if (fireballRigidbody != null && fireballRigidbody.linearVelocity.magnitude > 0.1f)
|
||||
{
|
||||
direction = fireballRigidbody.linearVelocity.normalized;
|
||||
}
|
||||
else
|
||||
{
|
||||
direction = (targetCollider.transform.position - transform.position).normalized;
|
||||
}
|
||||
|
||||
direction.y = Mathf.Max(0.2f, direction.y);
|
||||
return direction.normalized;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates impact effect
|
||||
/// </summary>
|
||||
/// <param name="impactPosition">Impact position</param>
|
||||
private void CreateImpactEffect(Vector3 impactPosition)
|
||||
{
|
||||
if (impactEffectPrefab != null)
|
||||
{
|
||||
GameObject impact = LeanPool.Spawn(impactEffectPrefab, impactPosition, Quaternion.identity);
|
||||
|
||||
LeanPool.Despawn(impact, 3f);
|
||||
|
||||
if (enableDebug) Debug.Log("[FireballDamage] Impact effect created");
|
||||
}
|
||||
|
||||
if (audioSource != null && impactSound != null)
|
||||
{
|
||||
audioSource.PlayOneShot(impactSound);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Despawns fireball from map
|
||||
/// </summary>
|
||||
private void DespawnFireball()
|
||||
{
|
||||
if (enableDebug) Debug.Log("[FireballDamage] Despawning fireball");
|
||||
|
||||
CancelInvoke(nameof(FailsafeDespawn));
|
||||
|
||||
LeanPool.Despawn(gameObject);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Failsafe despawn - removes fireball after maximum lifetime
|
||||
/// </summary>
|
||||
private void FailsafeDespawn()
|
||||
{
|
||||
if (enableDebug) Debug.Log("[FireballDamage] Failsafe despawn - lifetime exceeded");
|
||||
DespawnFireball();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Forces immediate despawn
|
||||
/// </summary>
|
||||
public void ForceDespawn()
|
||||
{
|
||||
if (enableDebug) Debug.Log("[FireballDamage] Forced despawn");
|
||||
DespawnFireball();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if fireball has already dealt damage
|
||||
/// </summary>
|
||||
/// <returns>True if damage was already dealt</returns>
|
||||
public bool HasDealtDamage()
|
||||
{
|
||||
return hasDealtDamage;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns fireball lifetime
|
||||
/// </summary>
|
||||
/// <returns>Time in seconds since spawn</returns>
|
||||
public float GetLifetime()
|
||||
{
|
||||
return Time.time - spawnTime;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets new damage (e.g. for different difficulty levels)
|
||||
/// </summary>
|
||||
/// <param name="newDamage">New damage</param>
|
||||
public void SetDamage(float newDamage)
|
||||
{
|
||||
damage = newDamage;
|
||||
if (enableDebug) Debug.Log($"[FireballDamage] Set new damage: {damage}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets new knockback
|
||||
/// </summary>
|
||||
/// <param name="newKnockback">New knockback force</param>
|
||||
public void SetKnockback(float newKnockback)
|
||||
{
|
||||
knockbackForce = newKnockback;
|
||||
if (enableDebug) Debug.Log($"[FireballDamage] Set new knockback: {knockbackForce}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets fireball state (useful for pooling)
|
||||
/// </summary>
|
||||
public void ResetFireball()
|
||||
{
|
||||
hasDealtDamage = false;
|
||||
spawnTime = Time.time;
|
||||
|
||||
CancelInvoke();
|
||||
|
||||
Invoke(nameof(FailsafeDespawn), maxLifetime);
|
||||
|
||||
if (enableDebug) Debug.Log("[FireballDamage] Fireball reset");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called on spawn by Lean Pool
|
||||
/// </summary>
|
||||
private void OnSpawn()
|
||||
{
|
||||
ResetFireball();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called on despawn by Lean Pool
|
||||
/// </summary>
|
||||
private void OnDespawn()
|
||||
{
|
||||
CancelInvoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
8334
Assets/AI/Demon/GhostDemon.prefab
Normal file
8334
Assets/AI/Demon/GhostDemon.prefab
Normal file
File diff suppressed because it is too large
Load Diff
7
Assets/AI/Demon/GhostDemon.prefab.meta
Normal file
7
Assets/AI/Demon/GhostDemon.prefab.meta
Normal file
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 82581d6c5dd5945b89394d83db5f4c8b
|
||||
PrefabImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,352 +1,352 @@
|
||||
using Invector.vCharacterController.AI.FSMBehaviour;
|
||||
using Lean.Pool;
|
||||
using System.Collections;
|
||||
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
|
||||
/// </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 Configuration")]
|
||||
[Tooltip("Meteor rock prefab")]
|
||||
public GameObject rockPrefab;
|
||||
|
||||
[Tooltip("Decal prefab showing impact location")]
|
||||
public GameObject decalPrefab;
|
||||
|
||||
[Tooltip("Height from which meteor falls")]
|
||||
public float meteorHeight = 30f;
|
||||
|
||||
[Tooltip("Delay between placing decal and spawning meteor")]
|
||||
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("Debug")]
|
||||
[Tooltip("Enable debug logging")]
|
||||
public bool enableDebug = false;
|
||||
|
||||
[Tooltip("Show gizmos in Scene View")]
|
||||
public bool showGizmos = true;
|
||||
|
||||
private GameObject spawnedDecal;
|
||||
|
||||
private Transform playerTransform;
|
||||
private Vector3 targetPosition;
|
||||
private bool meteorCasting = false;
|
||||
private MonoBehaviour coroutineRunner;
|
||||
|
||||
/// <summary>
|
||||
/// Main action execution method called by FSM
|
||||
/// </summary>
|
||||
public override void DoAction(vIFSMBehaviourController fsmBehaviour, vFSMComponentExecutionType executionType = vFSMComponentExecutionType.OnStateUpdate)
|
||||
{
|
||||
if (executionType == vFSMComponentExecutionType.OnStateEnter)
|
||||
{
|
||||
OnStateEnter(fsmBehaviour);
|
||||
}
|
||||
else if (executionType == vFSMComponentExecutionType.OnStateExit)
|
||||
{
|
||||
OnStateExit(fsmBehaviour);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when entering state - starts meteor casting
|
||||
/// </summary>
|
||||
private void OnStateEnter(vIFSMBehaviourController fsmBehaviour)
|
||||
{
|
||||
if (enableDebug) Debug.Log("[SA_CallMeteor] Starting meteor casting");
|
||||
|
||||
FindPlayer(fsmBehaviour);
|
||||
|
||||
var animator = fsmBehaviour.transform.GetComponent<Animator>();
|
||||
if (animator != null && !string.IsNullOrEmpty(animatorTrigger))
|
||||
{
|
||||
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");
|
||||
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}");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (enableDebug) Debug.LogWarning("[SA_CallMeteor] Cannot find ground under player");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds ground under player using raycast
|
||||
/// </summary>
|
||||
private bool FindGroundUnderPlayer(out Vector3 groundPosition)
|
||||
{
|
||||
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))
|
||||
{
|
||||
groundPosition = hit.point;
|
||||
return true;
|
||||
}
|
||||
|
||||
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
|
||||
/// </summary>
|
||||
public bool IsCasting()
|
||||
{
|
||||
return meteorCasting;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns meteor target position
|
||||
/// </summary>
|
||||
public Vector3 GetTargetPosition()
|
||||
{
|
||||
return targetPosition;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws gizmos in Scene View for debugging
|
||||
/// </summary>
|
||||
private void OnDrawGizmosSelected()
|
||||
{
|
||||
if (!showGizmos) return;
|
||||
|
||||
if (meteorCasting && targetPosition != Vector3.zero)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
using Invector.vCharacterController.AI.FSMBehaviour;
|
||||
using Lean.Pool;
|
||||
using System.Collections;
|
||||
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
|
||||
/// </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 Configuration")]
|
||||
[Tooltip("Meteor rock prefab")]
|
||||
public GameObject rockPrefab;
|
||||
|
||||
[Tooltip("Decal prefab showing impact location")]
|
||||
public GameObject decalPrefab;
|
||||
|
||||
[Tooltip("Height from which meteor falls")]
|
||||
public float meteorHeight = 30f;
|
||||
|
||||
[Tooltip("Delay between placing decal and spawning meteor")]
|
||||
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("Debug")]
|
||||
[Tooltip("Enable debug logging")]
|
||||
public bool enableDebug = false;
|
||||
|
||||
[Tooltip("Show gizmos in Scene View")]
|
||||
public bool showGizmos = true;
|
||||
|
||||
private GameObject spawnedDecal;
|
||||
|
||||
private Transform playerTransform;
|
||||
private Vector3 targetPosition;
|
||||
private bool meteorCasting = false;
|
||||
private MonoBehaviour coroutineRunner;
|
||||
|
||||
/// <summary>
|
||||
/// Main action execution method called by FSM
|
||||
/// </summary>
|
||||
public override void DoAction(vIFSMBehaviourController fsmBehaviour, vFSMComponentExecutionType executionType = vFSMComponentExecutionType.OnStateUpdate)
|
||||
{
|
||||
if (executionType == vFSMComponentExecutionType.OnStateEnter)
|
||||
{
|
||||
OnStateEnter(fsmBehaviour);
|
||||
}
|
||||
else if (executionType == vFSMComponentExecutionType.OnStateExit)
|
||||
{
|
||||
OnStateExit(fsmBehaviour);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when entering state - starts meteor casting
|
||||
/// </summary>
|
||||
private void OnStateEnter(vIFSMBehaviourController fsmBehaviour)
|
||||
{
|
||||
if (enableDebug) Debug.Log("[SA_CallMeteor] Starting meteor casting");
|
||||
|
||||
FindPlayer(fsmBehaviour);
|
||||
|
||||
var animator = fsmBehaviour.transform.GetComponent<Animator>();
|
||||
if (animator != null && !string.IsNullOrEmpty(animatorTrigger))
|
||||
{
|
||||
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");
|
||||
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}");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (enableDebug) Debug.LogWarning("[SA_CallMeteor] Cannot find ground under player");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds ground under player using raycast
|
||||
/// </summary>
|
||||
private bool FindGroundUnderPlayer(out Vector3 groundPosition)
|
||||
{
|
||||
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))
|
||||
{
|
||||
groundPosition = hit.point;
|
||||
return true;
|
||||
}
|
||||
|
||||
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
|
||||
/// </summary>
|
||||
public bool IsCasting()
|
||||
{
|
||||
return meteorCasting;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns meteor target position
|
||||
/// </summary>
|
||||
public Vector3 GetTargetPosition()
|
||||
{
|
||||
return targetPosition;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws gizmos in Scene View for debugging
|
||||
/// </summary>
|
||||
private void OnDrawGizmosSelected()
|
||||
{
|
||||
if (!showGizmos) return;
|
||||
|
||||
if (meteorCasting && targetPosition != Vector3.zero)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,185 +1,185 @@
|
||||
using Invector.vCharacterController.AI.FSMBehaviour;
|
||||
using Lean.Pool;
|
||||
using UnityEngine;
|
||||
|
||||
namespace DemonBoss.Magic
|
||||
{
|
||||
/// <summary>
|
||||
/// StateAction for Magic Shield spell - boss casts magical shield for 5 seconds
|
||||
/// During casting boss stands still, after completion returns to Combat
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "Invector/FSM/Decisions/DemonBoss/Cast Shield")]
|
||||
public class SA_CastShield : vStateAction
|
||||
{
|
||||
public override string categoryName => "DemonBoss/Magic";
|
||||
public override string defaultName => "Cast Shield";
|
||||
|
||||
[Header("Shield Configuration")]
|
||||
[Tooltip("Prefab with magical shield particle effect")]
|
||||
public GameObject shieldFXPrefab;
|
||||
|
||||
[Tooltip("Transform where shield should appear (usually boss center)")]
|
||||
public Transform shieldSpawnPoint;
|
||||
|
||||
[Tooltip("Shield duration in seconds")]
|
||||
public float shieldDuration = 5f;
|
||||
|
||||
[Tooltip("Animator trigger name for shield casting animation")]
|
||||
public string animatorTrigger = "CastShield";
|
||||
|
||||
[Header("Debug")]
|
||||
[Tooltip("Enable debug logging")]
|
||||
public bool enableDebug = false;
|
||||
|
||||
private GameObject spawnedShield;
|
||||
|
||||
private float shieldStartTime;
|
||||
private bool shieldActive = false;
|
||||
|
||||
/// <summary>
|
||||
/// Main action execution method called by FSM
|
||||
/// </summary>
|
||||
public override void DoAction(vIFSMBehaviourController fsmBehaviour, vFSMComponentExecutionType executionType = vFSMComponentExecutionType.OnStateUpdate)
|
||||
{
|
||||
if (executionType == vFSMComponentExecutionType.OnStateEnter)
|
||||
{
|
||||
OnStateEnter(fsmBehaviour);
|
||||
}
|
||||
else if (executionType == vFSMComponentExecutionType.OnStateUpdate)
|
||||
{
|
||||
OnStateUpdate(fsmBehaviour);
|
||||
}
|
||||
else if (executionType == vFSMComponentExecutionType.OnStateExit)
|
||||
{
|
||||
OnStateExit(fsmBehaviour);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when entering state - starts shield casting
|
||||
/// </summary>
|
||||
private void OnStateEnter(vIFSMBehaviourController fsmBehaviour)
|
||||
{
|
||||
if (enableDebug) Debug.Log("[SA_CastShield] Entering shield casting state");
|
||||
|
||||
var aiController = fsmBehaviour as Invector.vCharacterController.AI.vIControlAI;
|
||||
if (aiController != null)
|
||||
{
|
||||
aiController.Stop();
|
||||
if (enableDebug) Debug.Log("[SA_CastShield] AI stopped");
|
||||
}
|
||||
|
||||
var animator = fsmBehaviour.transform.GetComponent<Animator>();
|
||||
if (animator != null && !string.IsNullOrEmpty(animatorTrigger))
|
||||
{
|
||||
animator.SetTrigger(animatorTrigger);
|
||||
if (enableDebug) Debug.Log($"[SA_CastShield] Set trigger: {animatorTrigger}");
|
||||
}
|
||||
|
||||
SpawnShieldEffect(fsmBehaviour);
|
||||
|
||||
shieldStartTime = Time.time;
|
||||
shieldActive = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called every frame during state duration
|
||||
/// </summary>
|
||||
private void OnStateUpdate(vIFSMBehaviourController fsmBehaviour)
|
||||
{
|
||||
if (shieldActive && Time.time - shieldStartTime >= shieldDuration)
|
||||
{
|
||||
if (enableDebug) Debug.Log("[SA_CastShield] Shield time passed, finishing state");
|
||||
FinishShield(fsmBehaviour);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when exiting state - cleanup
|
||||
/// </summary>
|
||||
private void OnStateExit(vIFSMBehaviourController fsmBehaviour)
|
||||
{
|
||||
if (enableDebug) Debug.Log("[SA_CastShield] Exiting shield state");
|
||||
|
||||
if (shieldActive)
|
||||
{
|
||||
CleanupShield();
|
||||
}
|
||||
|
||||
var aiController = fsmBehaviour as Invector.vCharacterController.AI.vIControlAI;
|
||||
if (aiController != null)
|
||||
{
|
||||
if (enableDebug) Debug.Log("[SA_CastShield] AI resumed");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spawns magical shield particle effect
|
||||
/// </summary>
|
||||
private void SpawnShieldEffect(vIFSMBehaviourController fsmBehaviour)
|
||||
{
|
||||
if (shieldFXPrefab == null)
|
||||
{
|
||||
Debug.LogWarning("[SA_CastShield] Missing shieldFXPrefab!");
|
||||
return;
|
||||
}
|
||||
|
||||
Vector3 spawnPosition = shieldSpawnPoint != null ?
|
||||
shieldSpawnPoint.position : fsmBehaviour.transform.position;
|
||||
|
||||
spawnedShield = LeanPool.Spawn(shieldFXPrefab, spawnPosition,
|
||||
shieldSpawnPoint != null ? shieldSpawnPoint.rotation : fsmBehaviour.transform.rotation);
|
||||
|
||||
if (enableDebug) Debug.Log($"[SA_CastShield] Shield spawned at position: {spawnPosition}");
|
||||
|
||||
if (spawnedShield != null && shieldSpawnPoint != null)
|
||||
{
|
||||
spawnedShield.transform.SetParent(shieldSpawnPoint);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finishes shield operation and transitions to next state
|
||||
/// </summary>
|
||||
private void FinishShield(vIFSMBehaviourController fsmBehaviour)
|
||||
{
|
||||
shieldActive = false;
|
||||
CleanupShield();
|
||||
|
||||
DEC_CheckCooldown.SetCooldownStatic(fsmBehaviour, "Shield", 15f);
|
||||
|
||||
// End state - FSM will transition to next state
|
||||
// FYI: In Invector FSM, state completion is handled automatically
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cleans up spawned shield
|
||||
/// </summary>
|
||||
private void CleanupShield()
|
||||
{
|
||||
if (spawnedShield != null)
|
||||
{
|
||||
LeanPool.Despawn(spawnedShield);
|
||||
spawnedShield = null;
|
||||
if (enableDebug) Debug.Log("[SA_CastShield] Shield despawned");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if shield is currently active
|
||||
/// </summary>
|
||||
public bool IsShieldActive()
|
||||
{
|
||||
return shieldActive;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns remaining shield time
|
||||
/// </summary>
|
||||
public float GetRemainingShieldTime()
|
||||
{
|
||||
if (!shieldActive) return 0f;
|
||||
return Mathf.Max(0f, shieldDuration - (Time.time - shieldStartTime));
|
||||
}
|
||||
}
|
||||
using Invector.vCharacterController.AI.FSMBehaviour;
|
||||
using Lean.Pool;
|
||||
using UnityEngine;
|
||||
|
||||
namespace DemonBoss.Magic
|
||||
{
|
||||
/// <summary>
|
||||
/// StateAction for Magic Shield spell - boss casts magical shield for 5 seconds
|
||||
/// During casting boss stands still, after completion returns to Combat
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "Invector/FSM/Actions/DemonBoss/Cast Shield")]
|
||||
public class SA_CastShield : vStateAction
|
||||
{
|
||||
public override string categoryName => "DemonBoss/Magic";
|
||||
public override string defaultName => "Cast Shield";
|
||||
|
||||
[Header("Shield Configuration")]
|
||||
[Tooltip("Prefab with magical shield particle effect")]
|
||||
public GameObject shieldFXPrefab;
|
||||
|
||||
[Tooltip("Transform where shield should appear (usually boss center)")]
|
||||
public Transform shieldSpawnPoint;
|
||||
|
||||
[Tooltip("Shield duration in seconds")]
|
||||
public float shieldDuration = 5f;
|
||||
|
||||
[Tooltip("Animator trigger name for shield casting animation")]
|
||||
public string animatorTrigger = "CastShield";
|
||||
|
||||
[Header("Debug")]
|
||||
[Tooltip("Enable debug logging")]
|
||||
public bool enableDebug = false;
|
||||
|
||||
private GameObject spawnedShield;
|
||||
|
||||
private float shieldStartTime;
|
||||
private bool shieldActive = false;
|
||||
|
||||
/// <summary>
|
||||
/// Main action execution method called by FSM
|
||||
/// </summary>
|
||||
public override void DoAction(vIFSMBehaviourController fsmBehaviour, vFSMComponentExecutionType executionType = vFSMComponentExecutionType.OnStateUpdate)
|
||||
{
|
||||
if (executionType == vFSMComponentExecutionType.OnStateEnter)
|
||||
{
|
||||
OnStateEnter(fsmBehaviour);
|
||||
}
|
||||
else if (executionType == vFSMComponentExecutionType.OnStateUpdate)
|
||||
{
|
||||
OnStateUpdate(fsmBehaviour);
|
||||
}
|
||||
else if (executionType == vFSMComponentExecutionType.OnStateExit)
|
||||
{
|
||||
OnStateExit(fsmBehaviour);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when entering state - starts shield casting
|
||||
/// </summary>
|
||||
private void OnStateEnter(vIFSMBehaviourController fsmBehaviour)
|
||||
{
|
||||
if (enableDebug) Debug.Log("[SA_CastShield] Entering shield casting state");
|
||||
|
||||
var aiController = fsmBehaviour as Invector.vCharacterController.AI.vIControlAI;
|
||||
if (aiController != null)
|
||||
{
|
||||
aiController.Stop();
|
||||
if (enableDebug) Debug.Log("[SA_CastShield] AI stopped");
|
||||
}
|
||||
|
||||
var animator = fsmBehaviour.transform.GetComponent<Animator>();
|
||||
if (animator != null && !string.IsNullOrEmpty(animatorTrigger))
|
||||
{
|
||||
animator.SetTrigger(animatorTrigger);
|
||||
if (enableDebug) Debug.Log($"[SA_CastShield] Set trigger: {animatorTrigger}");
|
||||
}
|
||||
|
||||
SpawnShieldEffect(fsmBehaviour);
|
||||
|
||||
shieldStartTime = Time.time;
|
||||
shieldActive = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called every frame during state duration
|
||||
/// </summary>
|
||||
private void OnStateUpdate(vIFSMBehaviourController fsmBehaviour)
|
||||
{
|
||||
if (shieldActive && Time.time - shieldStartTime >= shieldDuration)
|
||||
{
|
||||
if (enableDebug) Debug.Log("[SA_CastShield] Shield time passed, finishing state");
|
||||
FinishShield(fsmBehaviour);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when exiting state - cleanup
|
||||
/// </summary>
|
||||
private void OnStateExit(vIFSMBehaviourController fsmBehaviour)
|
||||
{
|
||||
if (enableDebug) Debug.Log("[SA_CastShield] Exiting shield state");
|
||||
|
||||
if (shieldActive)
|
||||
{
|
||||
CleanupShield();
|
||||
}
|
||||
|
||||
var aiController = fsmBehaviour as Invector.vCharacterController.AI.vIControlAI;
|
||||
if (aiController != null)
|
||||
{
|
||||
if (enableDebug) Debug.Log("[SA_CastShield] AI resumed");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spawns magical shield particle effect
|
||||
/// </summary>
|
||||
private void SpawnShieldEffect(vIFSMBehaviourController fsmBehaviour)
|
||||
{
|
||||
if (shieldFXPrefab == null)
|
||||
{
|
||||
Debug.LogWarning("[SA_CastShield] Missing shieldFXPrefab!");
|
||||
return;
|
||||
}
|
||||
|
||||
Vector3 spawnPosition = shieldSpawnPoint != null ?
|
||||
shieldSpawnPoint.position : fsmBehaviour.transform.position;
|
||||
|
||||
spawnedShield = LeanPool.Spawn(shieldFXPrefab, spawnPosition,
|
||||
shieldSpawnPoint != null ? shieldSpawnPoint.rotation : fsmBehaviour.transform.rotation);
|
||||
|
||||
if (enableDebug) Debug.Log($"[SA_CastShield] Shield spawned at position: {spawnPosition}");
|
||||
|
||||
if (spawnedShield != null && shieldSpawnPoint != null)
|
||||
{
|
||||
spawnedShield.transform.SetParent(shieldSpawnPoint);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finishes shield operation and transitions to next state
|
||||
/// </summary>
|
||||
private void FinishShield(vIFSMBehaviourController fsmBehaviour)
|
||||
{
|
||||
shieldActive = false;
|
||||
CleanupShield();
|
||||
|
||||
DEC_CheckCooldown.SetCooldownStatic(fsmBehaviour, "Shield", 15f);
|
||||
|
||||
// End state - FSM will transition to next state
|
||||
// FYI: In Invector FSM, state completion is handled automatically
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cleans up spawned shield
|
||||
/// </summary>
|
||||
private void CleanupShield()
|
||||
{
|
||||
if (spawnedShield != null)
|
||||
{
|
||||
LeanPool.Despawn(spawnedShield);
|
||||
spawnedShield = null;
|
||||
if (enableDebug) Debug.Log("[SA_CastShield] Shield despawned");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if shield is currently active
|
||||
/// </summary>
|
||||
public bool IsShieldActive()
|
||||
{
|
||||
return shieldActive;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns remaining shield time
|
||||
/// </summary>
|
||||
public float GetRemainingShieldTime()
|
||||
{
|
||||
if (!shieldActive) return 0f;
|
||||
return Mathf.Max(0f, shieldDuration - (Time.time - shieldStartTime));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,298 +1,298 @@
|
||||
using Invector.vCharacterController.AI.FSMBehaviour;
|
||||
using Lean.Pool;
|
||||
using UnityEngine;
|
||||
|
||||
namespace DemonBoss.Magic
|
||||
{
|
||||
/// <summary>
|
||||
/// StateAction for intelligent crystal turret spawning
|
||||
/// Searches for optimal position in 2-6m ring from boss, prefers "behind boss" relative to player
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "Invector/FSM/Actions/DemonBoss/Spawn Turret Smart")]
|
||||
public class SA_SpawnTurretSmart : vStateAction
|
||||
{
|
||||
public override string categoryName => "DemonBoss/Magic";
|
||||
public override string defaultName => "Spawn Turret Smart";
|
||||
|
||||
[Header("Turret Configuration")]
|
||||
[Tooltip("Crystal prefab with CrystalShooterAI component")]
|
||||
public GameObject crystalPrefab;
|
||||
|
||||
[Tooltip("Minimum distance from boss for crystal spawn")]
|
||||
public float minSpawnDistance = 2f;
|
||||
|
||||
[Tooltip("Maximum distance from boss for crystal spawn")]
|
||||
public float maxSpawnDistance = 6f;
|
||||
|
||||
[Tooltip("Collision check radius when choosing position")]
|
||||
public float obstacleCheckRadius = 1f;
|
||||
|
||||
[Tooltip("Height above ground for raycast ground checking")]
|
||||
public float groundCheckHeight = 2f;
|
||||
|
||||
[Tooltip("Layer mask for obstacles")]
|
||||
public LayerMask obstacleLayerMask = -1;
|
||||
|
||||
[Tooltip("Layer mask for ground")]
|
||||
public LayerMask groundLayerMask = -1;
|
||||
|
||||
[Tooltip("Animator trigger name for crystal casting animation")]
|
||||
public string animatorTrigger = "CastCrystal";
|
||||
|
||||
[Header("Smart Positioning")]
|
||||
[Tooltip("Preference multiplier for positions behind boss (relative to player)")]
|
||||
public float backPreferenceMultiplier = 2f;
|
||||
|
||||
[Tooltip("Number of attempts to find valid position")]
|
||||
public int maxSpawnAttempts = 12;
|
||||
|
||||
[Header("Debug")]
|
||||
[Tooltip("Enable debug logging")]
|
||||
public bool enableDebug = false;
|
||||
|
||||
[Tooltip("Show gizmos in Scene View")]
|
||||
public bool showGizmos = true;
|
||||
|
||||
private GameObject spawnedCrystal;
|
||||
|
||||
private Transform playerTransform;
|
||||
|
||||
/// <summary>
|
||||
/// Main action execution method called by FSM
|
||||
/// </summary>
|
||||
public override void DoAction(vIFSMBehaviourController fsmBehaviour, vFSMComponentExecutionType executionType = vFSMComponentExecutionType.OnStateUpdate)
|
||||
{
|
||||
if (executionType == vFSMComponentExecutionType.OnStateEnter)
|
||||
{
|
||||
OnStateEnter(fsmBehaviour);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when entering state - intelligently spawns crystal
|
||||
/// </summary>
|
||||
private void OnStateEnter(vIFSMBehaviourController fsmBehaviour)
|
||||
{
|
||||
if (enableDebug) Debug.Log("[SA_SpawnTurretSmart] Starting intelligent crystal spawn");
|
||||
|
||||
FindPlayer(fsmBehaviour);
|
||||
|
||||
var animator = fsmBehaviour.transform.GetComponent<Animator>();
|
||||
if (animator != null && !string.IsNullOrEmpty(animatorTrigger))
|
||||
{
|
||||
animator.SetTrigger(animatorTrigger);
|
||||
if (enableDebug) Debug.Log($"[SA_SpawnTurretSmart] Set trigger: {animatorTrigger}");
|
||||
}
|
||||
|
||||
SpawnCrystalSmart(fsmBehaviour);
|
||||
|
||||
DEC_CheckCooldown.SetCooldownStatic(fsmBehaviour, "Turret", 12f);
|
||||
}
|
||||
|
||||
/// <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_SpawnTurretSmart] Player found by tag");
|
||||
return;
|
||||
}
|
||||
|
||||
var aiController = fsmBehaviour as Invector.vCharacterController.AI.vIControlAI;
|
||||
if (aiController != null && aiController.currentTarget != null)
|
||||
{
|
||||
playerTransform = aiController.currentTarget.transform;
|
||||
if (enableDebug) Debug.Log("[SA_SpawnTurretSmart] Player found through AI target");
|
||||
return;
|
||||
}
|
||||
|
||||
if (enableDebug) Debug.LogWarning("[SA_SpawnTurretSmart] Player not found!");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Intelligently spawns crystal in optimal position
|
||||
/// </summary>
|
||||
private void SpawnCrystalSmart(vIFSMBehaviourController fsmBehaviour)
|
||||
{
|
||||
if (crystalPrefab == null)
|
||||
{
|
||||
Debug.LogError("[SA_SpawnTurretSmart] Missing crystalPrefab!");
|
||||
return;
|
||||
}
|
||||
|
||||
Vector3 bestPosition = Vector3.zero;
|
||||
bool foundValidPosition = false;
|
||||
float bestScore = float.MinValue;
|
||||
|
||||
Vector3 bossPos = fsmBehaviour.transform.position;
|
||||
Vector3 playerDirection = Vector3.zero;
|
||||
|
||||
if (playerTransform != null)
|
||||
{
|
||||
playerDirection = (playerTransform.position - bossPos).normalized;
|
||||
}
|
||||
|
||||
for (int i = 0; i < maxSpawnAttempts; i++)
|
||||
{
|
||||
float angle = (360f / maxSpawnAttempts) * i + Random.Range(-15f, 15f);
|
||||
Vector3 direction = new Vector3(Mathf.Cos(angle * Mathf.Deg2Rad), 0, Mathf.Sin(angle * Mathf.Deg2Rad));
|
||||
|
||||
float distance = Random.Range(minSpawnDistance, maxSpawnDistance);
|
||||
Vector3 testPosition = bossPos + direction * distance;
|
||||
|
||||
if (IsPositionValid(testPosition, out Vector3 groundPosition))
|
||||
{
|
||||
float score = EvaluatePosition(groundPosition, playerDirection, direction);
|
||||
|
||||
if (score > bestScore)
|
||||
{
|
||||
bestScore = score;
|
||||
bestPosition = groundPosition;
|
||||
foundValidPosition = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (foundValidPosition)
|
||||
{
|
||||
SpawnCrystal(bestPosition, fsmBehaviour);
|
||||
if (enableDebug) Debug.Log($"[SA_SpawnTurretSmart] Crystal spawned at position: {bestPosition} (score: {bestScore:F2})");
|
||||
}
|
||||
else
|
||||
{
|
||||
Vector3 fallbackPos = bossPos + fsmBehaviour.transform.forward * minSpawnDistance;
|
||||
SpawnCrystal(fallbackPos, fsmBehaviour);
|
||||
if (enableDebug) Debug.LogWarning("[SA_SpawnTurretSmart] Using fallback position");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if position is valid (no obstacles, has ground)
|
||||
/// </summary>
|
||||
private bool IsPositionValid(Vector3 position, out Vector3 groundPosition)
|
||||
{
|
||||
groundPosition = position;
|
||||
|
||||
if (Physics.CheckSphere(position + Vector3.up * obstacleCheckRadius, obstacleCheckRadius, obstacleLayerMask))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Ray groundRay = new Ray(position + Vector3.up * groundCheckHeight, Vector3.down);
|
||||
if (Physics.Raycast(groundRay, out RaycastHit hit, groundCheckHeight + 2f, groundLayerMask))
|
||||
{
|
||||
groundPosition = hit.point;
|
||||
|
||||
if (Physics.CheckSphere(groundPosition + Vector3.up * obstacleCheckRadius, obstacleCheckRadius, obstacleLayerMask))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates position quality (higher score = better position)
|
||||
/// </summary>
|
||||
private float EvaluatePosition(Vector3 position, Vector3 playerDirection, Vector3 positionDirection)
|
||||
{
|
||||
float score = 0f;
|
||||
|
||||
if (playerTransform != null && playerDirection != Vector3.zero)
|
||||
{
|
||||
float angleToPlayer = Vector3.Angle(-playerDirection, positionDirection);
|
||||
|
||||
// The smaller the angle (closer to "behind"), the better the score
|
||||
float backScore = (180f - angleToPlayer) / 180f;
|
||||
score += backScore * backPreferenceMultiplier;
|
||||
}
|
||||
|
||||
Vector3 bossPos = new Vector3();
|
||||
float distance = Vector3.Distance(position, bossPos);
|
||||
float optimalDistance = (minSpawnDistance + maxSpawnDistance) * 0.5f;
|
||||
float distanceScore = 1f - Mathf.Abs(distance - optimalDistance) / maxSpawnDistance;
|
||||
score += distanceScore;
|
||||
|
||||
score += Random.Range(-0.1f, 0.1f);
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spawns crystal at given position
|
||||
/// </summary>
|
||||
private void SpawnCrystal(Vector3 position, vIFSMBehaviourController fsmBehaviour)
|
||||
{
|
||||
Quaternion rotation = Quaternion.identity;
|
||||
if (playerTransform != null)
|
||||
{
|
||||
Vector3 lookDirection = (playerTransform.position - position).normalized;
|
||||
lookDirection.y = 0;
|
||||
if (lookDirection != Vector3.zero)
|
||||
{
|
||||
rotation = Quaternion.LookRotation(lookDirection);
|
||||
}
|
||||
}
|
||||
|
||||
spawnedCrystal = LeanPool.Spawn(crystalPrefab, position, rotation);
|
||||
|
||||
var shooterAI = spawnedCrystal.GetComponent<CrystalShooterAI>();
|
||||
if (shooterAI == null)
|
||||
{
|
||||
Debug.LogError("[SA_SpawnTurretSmart] Crystal prefab doesn't have CrystalShooterAI component!");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (playerTransform != null)
|
||||
{
|
||||
shooterAI.SetTarget(playerTransform);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws gizmos in Scene View for debugging
|
||||
/// </summary>
|
||||
private void OnDrawGizmosSelected()
|
||||
{
|
||||
if (!showGizmos) return;
|
||||
|
||||
Vector3 pos = new Vector3();
|
||||
|
||||
// Spawn ring
|
||||
Gizmos.color = Color.green;
|
||||
DrawWireCircle(pos, minSpawnDistance);
|
||||
Gizmos.color = Color.red;
|
||||
DrawWireCircle(pos, maxSpawnDistance);
|
||||
|
||||
// Obstacle check radius
|
||||
Gizmos.color = Color.yellow;
|
||||
Gizmos.DrawWireSphere(pos + Vector3.up * obstacleCheckRadius, obstacleCheckRadius);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method for drawing circles
|
||||
/// </summary>
|
||||
private void DrawWireCircle(Vector3 center, float radius)
|
||||
{
|
||||
int segments = 32;
|
||||
float angle = 0f;
|
||||
Vector3 prevPoint = center + new Vector3(radius, 0, 0);
|
||||
|
||||
for (int i = 1; i <= segments; i++)
|
||||
{
|
||||
angle = (float)i / segments * 360f * Mathf.Deg2Rad;
|
||||
Vector3 newPoint = center + new Vector3(Mathf.Cos(angle) * radius, 0, Mathf.Sin(angle) * radius);
|
||||
Gizmos.DrawLine(prevPoint, newPoint);
|
||||
prevPoint = newPoint;
|
||||
}
|
||||
}
|
||||
}
|
||||
using Invector.vCharacterController.AI.FSMBehaviour;
|
||||
using Lean.Pool;
|
||||
using UnityEngine;
|
||||
|
||||
namespace DemonBoss.Magic
|
||||
{
|
||||
/// <summary>
|
||||
/// StateAction for intelligent crystal turret spawning
|
||||
/// Searches for optimal position in 2-6m ring from boss, prefers "behind boss" relative to player
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "Invector/FSM/Actions/DemonBoss/Spawn Turret Smart")]
|
||||
public class SA_SpawnTurretSmart : vStateAction
|
||||
{
|
||||
public override string categoryName => "DemonBoss/Magic";
|
||||
public override string defaultName => "Spawn Turret Smart";
|
||||
|
||||
[Header("Turret Configuration")]
|
||||
[Tooltip("Crystal prefab with CrystalShooterAI component")]
|
||||
public GameObject crystalPrefab;
|
||||
|
||||
[Tooltip("Minimum distance from boss for crystal spawn")]
|
||||
public float minSpawnDistance = 2f;
|
||||
|
||||
[Tooltip("Maximum distance from boss for crystal spawn")]
|
||||
public float maxSpawnDistance = 6f;
|
||||
|
||||
[Tooltip("Collision check radius when choosing position")]
|
||||
public float obstacleCheckRadius = 1f;
|
||||
|
||||
[Tooltip("Height above ground for raycast ground checking")]
|
||||
public float groundCheckHeight = 2f;
|
||||
|
||||
[Tooltip("Layer mask for obstacles")]
|
||||
public LayerMask obstacleLayerMask = -1;
|
||||
|
||||
[Tooltip("Layer mask for ground")]
|
||||
public LayerMask groundLayerMask = -1;
|
||||
|
||||
[Tooltip("Animator trigger name for crystal casting animation")]
|
||||
public string animatorTrigger = "CastCrystal";
|
||||
|
||||
[Header("Smart Positioning")]
|
||||
[Tooltip("Preference multiplier for positions behind boss (relative to player)")]
|
||||
public float backPreferenceMultiplier = 2f;
|
||||
|
||||
[Tooltip("Number of attempts to find valid position")]
|
||||
public int maxSpawnAttempts = 12;
|
||||
|
||||
[Header("Debug")]
|
||||
[Tooltip("Enable debug logging")]
|
||||
public bool enableDebug = false;
|
||||
|
||||
[Tooltip("Show gizmos in Scene View")]
|
||||
public bool showGizmos = true;
|
||||
|
||||
private GameObject spawnedCrystal;
|
||||
|
||||
private Transform playerTransform;
|
||||
|
||||
/// <summary>
|
||||
/// Main action execution method called by FSM
|
||||
/// </summary>
|
||||
public override void DoAction(vIFSMBehaviourController fsmBehaviour, vFSMComponentExecutionType executionType = vFSMComponentExecutionType.OnStateUpdate)
|
||||
{
|
||||
if (executionType == vFSMComponentExecutionType.OnStateEnter)
|
||||
{
|
||||
OnStateEnter(fsmBehaviour);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when entering state - intelligently spawns crystal
|
||||
/// </summary>
|
||||
private void OnStateEnter(vIFSMBehaviourController fsmBehaviour)
|
||||
{
|
||||
if (enableDebug) Debug.Log("[SA_SpawnTurretSmart] Starting intelligent crystal spawn");
|
||||
|
||||
FindPlayer(fsmBehaviour);
|
||||
|
||||
var animator = fsmBehaviour.transform.GetComponent<Animator>();
|
||||
if (animator != null && !string.IsNullOrEmpty(animatorTrigger))
|
||||
{
|
||||
animator.SetTrigger(animatorTrigger);
|
||||
if (enableDebug) Debug.Log($"[SA_SpawnTurretSmart] Set trigger: {animatorTrigger}");
|
||||
}
|
||||
|
||||
SpawnCrystalSmart(fsmBehaviour);
|
||||
|
||||
DEC_CheckCooldown.SetCooldownStatic(fsmBehaviour, "Turret", 12f);
|
||||
}
|
||||
|
||||
/// <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_SpawnTurretSmart] Player found by tag");
|
||||
return;
|
||||
}
|
||||
|
||||
var aiController = fsmBehaviour as Invector.vCharacterController.AI.vIControlAI;
|
||||
if (aiController != null && aiController.currentTarget != null)
|
||||
{
|
||||
playerTransform = aiController.currentTarget.transform;
|
||||
if (enableDebug) Debug.Log("[SA_SpawnTurretSmart] Player found through AI target");
|
||||
return;
|
||||
}
|
||||
|
||||
if (enableDebug) Debug.LogWarning("[SA_SpawnTurretSmart] Player not found!");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Intelligently spawns crystal in optimal position
|
||||
/// </summary>
|
||||
private void SpawnCrystalSmart(vIFSMBehaviourController fsmBehaviour)
|
||||
{
|
||||
if (crystalPrefab == null)
|
||||
{
|
||||
Debug.LogError("[SA_SpawnTurretSmart] Missing crystalPrefab!");
|
||||
return;
|
||||
}
|
||||
|
||||
Vector3 bestPosition = Vector3.zero;
|
||||
bool foundValidPosition = false;
|
||||
float bestScore = float.MinValue;
|
||||
|
||||
Vector3 bossPos = fsmBehaviour.transform.position;
|
||||
Vector3 playerDirection = Vector3.zero;
|
||||
|
||||
if (playerTransform != null)
|
||||
{
|
||||
playerDirection = (playerTransform.position - bossPos).normalized;
|
||||
}
|
||||
|
||||
for (int i = 0; i < maxSpawnAttempts; i++)
|
||||
{
|
||||
float angle = (360f / maxSpawnAttempts) * i + Random.Range(-15f, 15f);
|
||||
Vector3 direction = new Vector3(Mathf.Cos(angle * Mathf.Deg2Rad), 0, Mathf.Sin(angle * Mathf.Deg2Rad));
|
||||
|
||||
float distance = Random.Range(minSpawnDistance, maxSpawnDistance);
|
||||
Vector3 testPosition = bossPos + direction * distance;
|
||||
|
||||
if (IsPositionValid(testPosition, out Vector3 groundPosition))
|
||||
{
|
||||
float score = EvaluatePosition(groundPosition, playerDirection, direction);
|
||||
|
||||
if (score > bestScore)
|
||||
{
|
||||
bestScore = score;
|
||||
bestPosition = groundPosition;
|
||||
foundValidPosition = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (foundValidPosition)
|
||||
{
|
||||
SpawnCrystal(bestPosition, fsmBehaviour);
|
||||
if (enableDebug) Debug.Log($"[SA_SpawnTurretSmart] Crystal spawned at position: {bestPosition} (score: {bestScore:F2})");
|
||||
}
|
||||
else
|
||||
{
|
||||
Vector3 fallbackPos = bossPos + fsmBehaviour.transform.forward * minSpawnDistance;
|
||||
SpawnCrystal(fallbackPos, fsmBehaviour);
|
||||
if (enableDebug) Debug.LogWarning("[SA_SpawnTurretSmart] Using fallback position");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if position is valid (no obstacles, has ground)
|
||||
/// </summary>
|
||||
private bool IsPositionValid(Vector3 position, out Vector3 groundPosition)
|
||||
{
|
||||
groundPosition = position;
|
||||
|
||||
if (Physics.CheckSphere(position + Vector3.up * obstacleCheckRadius, obstacleCheckRadius, obstacleLayerMask))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Ray groundRay = new Ray(position + Vector3.up * groundCheckHeight, Vector3.down);
|
||||
if (Physics.Raycast(groundRay, out RaycastHit hit, groundCheckHeight + 2f, groundLayerMask))
|
||||
{
|
||||
groundPosition = hit.point;
|
||||
|
||||
if (Physics.CheckSphere(groundPosition + Vector3.up * obstacleCheckRadius, obstacleCheckRadius, obstacleLayerMask))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates position quality (higher score = better position)
|
||||
/// </summary>
|
||||
private float EvaluatePosition(Vector3 position, Vector3 playerDirection, Vector3 positionDirection)
|
||||
{
|
||||
float score = 0f;
|
||||
|
||||
if (playerTransform != null && playerDirection != Vector3.zero)
|
||||
{
|
||||
float angleToPlayer = Vector3.Angle(-playerDirection, positionDirection);
|
||||
|
||||
// The smaller the angle (closer to "behind"), the better the score
|
||||
float backScore = (180f - angleToPlayer) / 180f;
|
||||
score += backScore * backPreferenceMultiplier;
|
||||
}
|
||||
|
||||
Vector3 bossPos = new Vector3();
|
||||
float distance = Vector3.Distance(position, bossPos);
|
||||
float optimalDistance = (minSpawnDistance + maxSpawnDistance) * 0.5f;
|
||||
float distanceScore = 1f - Mathf.Abs(distance - optimalDistance) / maxSpawnDistance;
|
||||
score += distanceScore;
|
||||
|
||||
score += Random.Range(-0.1f, 0.1f);
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spawns crystal at given position
|
||||
/// </summary>
|
||||
private void SpawnCrystal(Vector3 position, vIFSMBehaviourController fsmBehaviour)
|
||||
{
|
||||
Quaternion rotation = Quaternion.identity;
|
||||
if (playerTransform != null)
|
||||
{
|
||||
Vector3 lookDirection = (playerTransform.position - position).normalized;
|
||||
lookDirection.y = 0;
|
||||
if (lookDirection != Vector3.zero)
|
||||
{
|
||||
rotation = Quaternion.LookRotation(lookDirection);
|
||||
}
|
||||
}
|
||||
|
||||
spawnedCrystal = LeanPool.Spawn(crystalPrefab, position, rotation);
|
||||
|
||||
var shooterAI = spawnedCrystal.GetComponent<CrystalShooterAI>();
|
||||
if (shooterAI == null)
|
||||
{
|
||||
Debug.LogError("[SA_SpawnTurretSmart] Crystal prefab doesn't have CrystalShooterAI component!");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (playerTransform != null)
|
||||
{
|
||||
shooterAI.SetTarget(playerTransform);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws gizmos in Scene View for debugging
|
||||
/// </summary>
|
||||
private void OnDrawGizmosSelected()
|
||||
{
|
||||
if (!showGizmos) return;
|
||||
|
||||
Vector3 pos = new Vector3();
|
||||
|
||||
// Spawn ring
|
||||
Gizmos.color = Color.green;
|
||||
DrawWireCircle(pos, minSpawnDistance);
|
||||
Gizmos.color = Color.red;
|
||||
DrawWireCircle(pos, maxSpawnDistance);
|
||||
|
||||
// Obstacle check radius
|
||||
Gizmos.color = Color.yellow;
|
||||
Gizmos.DrawWireSphere(pos + Vector3.up * obstacleCheckRadius, obstacleCheckRadius);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method for drawing circles
|
||||
/// </summary>
|
||||
private void DrawWireCircle(Vector3 center, float radius)
|
||||
{
|
||||
int segments = 32;
|
||||
float angle = 0f;
|
||||
Vector3 prevPoint = center + new Vector3(radius, 0, 0);
|
||||
|
||||
for (int i = 1; i <= segments; i++)
|
||||
{
|
||||
angle = (float)i / segments * 360f * Mathf.Deg2Rad;
|
||||
Vector3 newPoint = center + new Vector3(Mathf.Cos(angle) * radius, 0, Mathf.Sin(angle) * radius);
|
||||
Gizmos.DrawLine(prevPoint, newPoint);
|
||||
prevPoint = newPoint;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,38 +1,38 @@
|
||||
using UnityEngine;
|
||||
|
||||
public class ShieldScaleUp : MonoBehaviour
|
||||
{
|
||||
[Tooltip("Time it takes to fully grow the shield")]
|
||||
public float growDuration = 0.5f;
|
||||
|
||||
[Tooltip("Curve to control growth speed over time")]
|
||||
public AnimationCurve scaleCurve = AnimationCurve.EaseInOut(0, 0, 1, 1);
|
||||
|
||||
private float timer = 0f;
|
||||
private Vector3 targetScale;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
targetScale = transform.localScale;
|
||||
transform.localScale = Vector3.zero;
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
timer = 0f;
|
||||
transform.localScale = Vector3.zero;
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (timer < growDuration)
|
||||
{
|
||||
timer += Time.deltaTime;
|
||||
float t = Mathf.Clamp01(timer / growDuration);
|
||||
|
||||
float scaleValue = scaleCurve.Evaluate(t);
|
||||
|
||||
transform.localScale = targetScale * scaleValue;
|
||||
}
|
||||
}
|
||||
using UnityEngine;
|
||||
|
||||
public class ShieldScaleUp : MonoBehaviour
|
||||
{
|
||||
[Tooltip("Time it takes to fully grow the shield")]
|
||||
public float growDuration = 0.5f;
|
||||
|
||||
[Tooltip("Curve to control growth speed over time")]
|
||||
public AnimationCurve scaleCurve = AnimationCurve.EaseInOut(0, 0, 1, 1);
|
||||
|
||||
private float timer = 0f;
|
||||
private Vector3 targetScale;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
targetScale = transform.localScale;
|
||||
transform.localScale = Vector3.zero;
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
timer = 0f;
|
||||
transform.localScale = Vector3.zero;
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (timer < growDuration)
|
||||
{
|
||||
timer += Time.deltaTime;
|
||||
float t = Mathf.Clamp01(timer / growDuration);
|
||||
|
||||
float scaleValue = scaleCurve.Evaluate(t);
|
||||
|
||||
transform.localScale = targetScale * scaleValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user