updated demon

This commit is contained in:
2025-08-20 14:28:49 +02:00
parent 3b07a6f937
commit e79d12ebec
13 changed files with 26080 additions and 17039 deletions

View File

@@ -1,379 +1,379 @@
using Lean.Pool; using Lean.Pool;
using System.Collections; using System.Collections;
using UnityEngine; using UnityEngine;
namespace DemonBoss.Magic namespace DemonBoss.Magic
{ {
/// <summary> /// <summary>
/// AI component for crystal turret spawned by boss /// AI component for crystal turret spawned by boss
/// Rotates towards player and shoots fireballs for specified time /// Rotates towards player and shoots fireballs for specified time
/// </summary> /// </summary>
public class CrystalShooterAI : MonoBehaviour public class CrystalShooterAI : MonoBehaviour
{ {
[Header("Shooting Configuration")] [Header("Shooting Configuration")]
[Tooltip("Transform point from which projectiles are fired")] [Tooltip("Transform point from which projectiles are fired")]
public Transform muzzle; public Transform muzzle;
[Tooltip("Fireball prefab")] [Tooltip("Fireball prefab")]
public GameObject fireballPrefab; public GameObject fireballPrefab;
[Tooltip("Fireball speed in m/s")] [Tooltip("Fireball speed in m/s")]
public float fireballSpeed = 28f; public float fireballSpeed = 28f;
[Tooltip("Time between shots in seconds")] [Tooltip("Time between shots in seconds")]
public float fireRate = 0.7f; public float fireRate = 0.7f;
[Tooltip("Maximum number of shots before auto-despawn")] [Tooltip("Maximum number of shots before auto-despawn")]
public int maxShots = 10; public int maxShots = 10;
[Tooltip("Wait time after shooting before despawn")] [Tooltip("Wait time after shooting before despawn")]
public float despawnDelay = 3f; public float despawnDelay = 3f;
[Header("Rotation Configuration")] [Header("Rotation Configuration")]
[Tooltip("Target rotation speed in degrees/s")] [Tooltip("Target rotation speed in degrees/s")]
public float turnSpeed = 120f; public float turnSpeed = 120f;
[Tooltip("Idle spin speed when no target (degrees/s, 0 = disabled)")] [Tooltip("Idle spin speed when no target (degrees/s, 0 = disabled)")]
public float idleSpinSpeed = 30f; public float idleSpinSpeed = 30f;
[Tooltip("Aiming accuracy in degrees (smaller value = more accurate)")] [Tooltip("Aiming accuracy in degrees (smaller value = more accurate)")]
public float aimTolerance = 5f; public float aimTolerance = 5f;
[Header("Targeting")] [Header("Targeting")]
[Tooltip("Automatically find player on start")] [Tooltip("Automatically find player on start")]
public bool autoFindPlayer = true; public bool autoFindPlayer = true;
[Tooltip("Player tag to search for")] [Tooltip("Player tag to search for")]
public string playerTag = "Player"; public string playerTag = "Player";
[Tooltip("Maximum shooting range")] [Tooltip("Maximum shooting range")]
public float maxShootingRange = 50f; public float maxShootingRange = 50f;
[Header("Effects")] [Header("Effects")]
[Tooltip("Particle effect at shot")] [Tooltip("Particle effect at shot")]
public GameObject muzzleFlashPrefab; public GameObject muzzleFlashPrefab;
[Tooltip("Shoot sound")] [Tooltip("Shoot sound")]
public AudioClip shootSound; public AudioClip shootSound;
[Header("Debug")] [Header("Debug")]
[Tooltip("Enable debug logging")] [Tooltip("Enable debug logging")]
public bool enableDebug = false; public bool enableDebug = false;
[Tooltip("Show gizmos in Scene View")] [Tooltip("Show gizmos in Scene View")]
public bool showGizmos = true; public bool showGizmos = true;
// Private variables // Private variables
private Transform target; private Transform target;
private AudioSource audioSource; private AudioSource audioSource;
private Coroutine shootingCoroutine; private Coroutine shootingCoroutine;
private bool isActive = false; private bool isActive = false;
private int shotsFired = 0; private int shotsFired = 0;
private float lastShotTime = 0f; private float lastShotTime = 0f;
private Transform crystalTransform; private Transform crystalTransform;
/// <summary> /// <summary>
/// Component initialization /// Component initialization
/// </summary> /// </summary>
private void Awake() private void Awake()
{ {
crystalTransform = transform; crystalTransform = transform;
audioSource = GetComponent<AudioSource>(); audioSource = GetComponent<AudioSource>();
if (audioSource == null && shootSound != null) if (audioSource == null && shootSound != null)
{ {
audioSource = gameObject.AddComponent<AudioSource>(); audioSource = gameObject.AddComponent<AudioSource>();
audioSource.playOnAwake = false; audioSource.playOnAwake = false;
audioSource.spatialBlend = 1f; audioSource.spatialBlend = 1f;
} }
if (muzzle == null) if (muzzle == null)
{ {
Transform muzzleChild = crystalTransform.Find("muzzle"); Transform muzzleChild = crystalTransform.Find("muzzle");
if (muzzleChild != null) if (muzzleChild != null)
{ {
muzzle = muzzleChild; muzzle = muzzleChild;
if (enableDebug) Debug.Log("[CrystalShooterAI] Found muzzle child"); if (enableDebug) Debug.Log("[CrystalShooterAI] Found muzzle child");
} }
else else
{ {
muzzle = crystalTransform; muzzle = crystalTransform;
if (enableDebug) Debug.LogWarning("[CrystalShooterAI] Using crystal center as muzzle"); if (enableDebug) Debug.LogWarning("[CrystalShooterAI] Using crystal center as muzzle");
} }
} }
} }
/// <summary> /// <summary>
/// Start - begin crystal operation /// Start - begin crystal operation
/// </summary> /// </summary>
private void Start() private void Start()
{ {
if (enableDebug) Debug.Log("[CrystalShooterAI] Crystal activated"); if (enableDebug) Debug.Log("[CrystalShooterAI] Crystal activated");
if (autoFindPlayer && target == null) if (autoFindPlayer && target == null)
{ {
FindPlayer(); FindPlayer();
} }
StartShooting(); StartShooting();
} }
/// <summary> /// <summary>
/// Update - rotate crystal towards target /// Update - rotate crystal towards target
/// </summary> /// </summary>
private void Update() private void Update()
{ {
if (!isActive) return; if (!isActive) return;
RotateTowardsTarget(); RotateTowardsTarget();
} }
/// <summary> /// <summary>
/// Find player automatically /// Find player automatically
/// </summary> /// </summary>
private void FindPlayer() private void FindPlayer()
{ {
GameObject player = GameObject.FindGameObjectWithTag(playerTag); GameObject player = GameObject.FindGameObjectWithTag(playerTag);
if (player != null) if (player != null)
{ {
SetTarget(player.transform); SetTarget(player.transform);
if (enableDebug) Debug.Log("[CrystalShooterAI] Automatically found player"); if (enableDebug) Debug.Log("[CrystalShooterAI] Automatically found player");
} }
else else
{ {
if (enableDebug) Debug.LogWarning("[CrystalShooterAI] Cannot find player with tag: " + playerTag); if (enableDebug) Debug.LogWarning("[CrystalShooterAI] Cannot find player with tag: " + playerTag);
} }
} }
/// <summary> /// <summary>
/// Set target for crystal /// Set target for crystal
/// </summary> /// </summary>
/// <param name="newTarget">Target transform</param> /// <param name="newTarget">Target transform</param>
public void SetTarget(Transform newTarget) public void SetTarget(Transform newTarget)
{ {
target = newTarget; target = newTarget;
if (enableDebug && target != null) if (enableDebug && target != null)
{ {
Debug.Log($"[CrystalShooterAI] Set target: {target.name}"); Debug.Log($"[CrystalShooterAI] Set target: {target.name}");
} }
} }
/// <summary> /// <summary>
/// Start shooting cycle /// Start shooting cycle
/// </summary> /// </summary>
public void StartShooting() public void StartShooting()
{ {
if (isActive) return; if (isActive) return;
isActive = true; isActive = true;
shotsFired = 0; shotsFired = 0;
shootingCoroutine = StartCoroutine(ShootingCoroutine()); shootingCoroutine = StartCoroutine(ShootingCoroutine());
if (enableDebug) Debug.Log("[CrystalShooterAI] Starting shooting"); if (enableDebug) Debug.Log("[CrystalShooterAI] Starting shooting");
} }
/// <summary> /// <summary>
/// Stop shooting /// Stop shooting
/// </summary> /// </summary>
public void StopShooting() public void StopShooting()
{ {
isActive = false; isActive = false;
if (shootingCoroutine != null) if (shootingCoroutine != null)
{ {
StopCoroutine(shootingCoroutine); StopCoroutine(shootingCoroutine);
shootingCoroutine = null; shootingCoroutine = null;
} }
if (enableDebug) Debug.Log("[CrystalShooterAI] Stopped shooting"); if (enableDebug) Debug.Log("[CrystalShooterAI] Stopped shooting");
} }
/// <summary> /// <summary>
/// Main coroutine handling shooting cycle /// Main coroutine handling shooting cycle
/// </summary> /// </summary>
private IEnumerator ShootingCoroutine() private IEnumerator ShootingCoroutine()
{ {
while (shotsFired < maxShots && isActive) while (shotsFired < maxShots && isActive)
{ {
if (CanShoot()) if (CanShoot())
{ {
FireFireball(); FireFireball();
shotsFired++; shotsFired++;
lastShotTime = Time.time; lastShotTime = Time.time;
} }
yield return new WaitForSeconds(fireRate); yield return new WaitForSeconds(fireRate);
} }
if (enableDebug) Debug.Log($"[CrystalShooterAI] Finished shooting ({shotsFired} shots)"); if (enableDebug) Debug.Log($"[CrystalShooterAI] Finished shooting ({shotsFired} shots)");
yield return new WaitForSeconds(despawnDelay); yield return new WaitForSeconds(despawnDelay);
DespawnCrystal(); DespawnCrystal();
} }
/// <summary> /// <summary>
/// Checks if crystal can shoot /// Checks if crystal can shoot
/// </summary> /// </summary>
private bool CanShoot() private bool CanShoot()
{ {
if (target == null) return false; if (target == null) return false;
float distanceToTarget = Vector3.Distance(crystalTransform.position, target.position); float distanceToTarget = Vector3.Distance(crystalTransform.position, target.position);
if (distanceToTarget > maxShootingRange) return false; if (distanceToTarget > maxShootingRange) return false;
Vector3 directionToTarget = (target.position - crystalTransform.position).normalized; Vector3 directionToTarget = (target.position - crystalTransform.position).normalized;
float angleToTarget = Vector3.Angle(crystalTransform.forward, directionToTarget); float angleToTarget = Vector3.Angle(crystalTransform.forward, directionToTarget);
return angleToTarget <= aimTolerance; return angleToTarget <= aimTolerance;
} }
/// <summary> /// <summary>
/// Fires fireball towards target /// Fires fireball towards target
/// </summary> /// </summary>
private void FireFireball() private void FireFireball()
{ {
if (fireballPrefab == null || muzzle == null) if (fireballPrefab == null || muzzle == null)
{ {
if (enableDebug) Debug.LogWarning("[CrystalShooterAI] Missing fireball prefab or muzzle"); if (enableDebug) Debug.LogWarning("[CrystalShooterAI] Missing fireball prefab or muzzle");
return; return;
} }
Vector3 shootDirection = crystalTransform.forward; Vector3 shootDirection = crystalTransform.forward;
if (target != null) if (target != null)
{ {
Vector3 targetCenter = target.position + Vector3.up * 1f; Vector3 targetCenter = target.position + Vector3.up * 1f;
shootDirection = (targetCenter - muzzle.position).normalized; shootDirection = (targetCenter - muzzle.position).normalized;
} }
GameObject fireball = LeanPool.Spawn(fireballPrefab, muzzle.position, GameObject fireball = LeanPool.Spawn(fireballPrefab, muzzle.position,
Quaternion.LookRotation(shootDirection)); Quaternion.LookRotation(shootDirection));
Rigidbody fireballRb = fireball.GetComponent<Rigidbody>(); Rigidbody fireballRb = fireball.GetComponent<Rigidbody>();
if (fireballRb != null) if (fireballRb != null)
{ {
fireballRb.linearVelocity = shootDirection * fireballSpeed; fireballRb.linearVelocity = shootDirection * fireballSpeed;
} }
PlayShootEffects(); PlayShootEffects();
if (enableDebug) if (enableDebug)
{ {
Debug.Log($"[CrystalShooterAI] Shot #{shotsFired + 1} in direction: {shootDirection}"); Debug.Log($"[CrystalShooterAI] Shot #{shotsFired + 1} in direction: {shootDirection}");
} }
} }
/// <summary> /// <summary>
/// Plays shooting effects /// Plays shooting effects
/// </summary> /// </summary>
private void PlayShootEffects() private void PlayShootEffects()
{ {
if (muzzleFlashPrefab != null && muzzle != null) if (muzzleFlashPrefab != null && muzzle != null)
{ {
GameObject flash = LeanPool.Spawn(muzzleFlashPrefab, muzzle.position, muzzle.rotation); GameObject flash = LeanPool.Spawn(muzzleFlashPrefab, muzzle.position, muzzle.rotation);
LeanPool.Despawn(flash, 2f); LeanPool.Despawn(flash, 2f);
} }
if (audioSource != null && shootSound != null) if (audioSource != null && shootSound != null)
{ {
audioSource.PlayOneShot(shootSound); audioSource.PlayOneShot(shootSound);
} }
} }
/// <summary> /// <summary>
/// Rotates crystal towards target or performs idle spin /// Rotates crystal towards target or performs idle spin
/// </summary> /// </summary>
private void RotateTowardsTarget() private void RotateTowardsTarget()
{ {
if (target != null) if (target != null)
{ {
Vector3 directionToTarget = target.position - crystalTransform.position; Vector3 directionToTarget = target.position - crystalTransform.position;
directionToTarget.y = 0; directionToTarget.y = 0;
if (directionToTarget != Vector3.zero) if (directionToTarget != Vector3.zero)
{ {
Quaternion targetRotation = Quaternion.LookRotation(directionToTarget); Quaternion targetRotation = Quaternion.LookRotation(directionToTarget);
crystalTransform.rotation = Quaternion.RotateTowards( crystalTransform.rotation = Quaternion.RotateTowards(
crystalTransform.rotation, crystalTransform.rotation,
targetRotation, targetRotation,
turnSpeed * Time.deltaTime turnSpeed * Time.deltaTime
); );
} }
} }
else if (idleSpinSpeed > 0f) else if (idleSpinSpeed > 0f)
{ {
crystalTransform.Rotate(Vector3.up, idleSpinSpeed * Time.deltaTime); crystalTransform.Rotate(Vector3.up, idleSpinSpeed * Time.deltaTime);
} }
} }
/// <summary> /// <summary>
/// Despawns crystal from map /// Despawns crystal from map
/// </summary> /// </summary>
public void DespawnCrystal() public void DespawnCrystal()
{ {
if (enableDebug) Debug.Log("[CrystalShooterAI] Despawning crystal"); if (enableDebug) Debug.Log("[CrystalShooterAI] Despawning crystal");
StopShooting(); StopShooting();
LeanPool.Despawn(gameObject); LeanPool.Despawn(gameObject);
} }
/// <summary> /// <summary>
/// Forces immediate despawn (e.g. on boss death) /// Forces immediate despawn (e.g. on boss death)
/// </summary> /// </summary>
public void ForceDespawn() public void ForceDespawn()
{ {
if (enableDebug) Debug.Log("[CrystalShooterAI] Forced despawn"); if (enableDebug) Debug.Log("[CrystalShooterAI] Forced despawn");
DespawnCrystal(); DespawnCrystal();
} }
/// <summary> /// <summary>
/// Returns crystal state information /// Returns crystal state information
/// </summary> /// </summary>
public bool IsActive() => isActive; public bool IsActive() => isActive;
public int GetShotsFired() => shotsFired; public int GetShotsFired() => shotsFired;
public int GetRemainingShots() => Mathf.Max(0, maxShots - shotsFired); public int GetRemainingShots() => Mathf.Max(0, maxShots - shotsFired);
public float GetTimeSinceLastShot() => Time.time - lastShotTime; public float GetTimeSinceLastShot() => Time.time - lastShotTime;
/// <summary> /// <summary>
/// Draws gizmos in Scene View /// Draws gizmos in Scene View
/// </summary> /// </summary>
private void OnDrawGizmosSelected() private void OnDrawGizmosSelected()
{ {
if (!showGizmos) return; if (!showGizmos) return;
Gizmos.color = Color.red; Gizmos.color = Color.red;
Gizmos.DrawWireSphere(transform.position, maxShootingRange); Gizmos.DrawWireSphere(transform.position, maxShootingRange);
if (target != null) if (target != null)
{ {
Gizmos.color = Color.yellow; Gizmos.color = Color.yellow;
Gizmos.DrawLine(transform.position, target.position); Gizmos.DrawLine(transform.position, target.position);
if (muzzle != null) if (muzzle != null)
{ {
Gizmos.color = Color.green; Gizmos.color = Color.green;
Vector3 forward = transform.forward * 5f; Vector3 forward = transform.forward * 5f;
Vector3 right = Quaternion.AngleAxis(aimTolerance, transform.up) * forward; Vector3 right = Quaternion.AngleAxis(aimTolerance, transform.up) * forward;
Vector3 left = 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 + right);
Gizmos.DrawLine(muzzle.position, muzzle.position + left); Gizmos.DrawLine(muzzle.position, muzzle.position + left);
} }
} }
if (muzzle != null) if (muzzle != null)
{ {
Gizmos.color = Color.blue; Gizmos.color = Color.blue;
Gizmos.DrawWireSphere(muzzle.position, 0.2f); Gizmos.DrawWireSphere(muzzle.position, 0.2f);
} }
} }
} }
} }

View File

@@ -1,205 +1,205 @@
using Invector.vCharacterController.AI.FSMBehaviour; using Invector.vCharacterController.AI.FSMBehaviour;
using UnityEngine; using UnityEngine;
namespace DemonBoss.Magic namespace DemonBoss.Magic
{ {
/// <summary> /// <summary>
/// Decision node checking cooldown for different boss abilities /// Decision node checking cooldown for different boss abilities
/// Stores Time.time in FSM timers and checks if required cooldown time has passed /// Stores Time.time in FSM timers and checks if required cooldown time has passed
/// </summary> /// </summary>
[CreateAssetMenu(menuName = "Invector/FSM/Decisions/DemonBoss/Check Cooldown")] [CreateAssetMenu(menuName = "Invector/FSM/Decisions/DemonBoss/Check Cooldown")]
public class DEC_CheckCooldown : vStateDecision public class DEC_CheckCooldown : vStateDecision
{ {
public override string categoryName => "DemonBoss/Magic"; public override string categoryName => "DemonBoss/Magic";
public override string defaultName => "Check Cooldown"; public override string defaultName => "Check Cooldown";
[Header("Cooldown Configuration")] [Header("Cooldown Configuration")]
[Tooltip("Unique key for this ability (e.g. 'Shield', 'Turret', 'Meteor')")] [Tooltip("Unique key for this ability (e.g. 'Shield', 'Turret', 'Meteor')")]
public string cooldownKey = "Shield"; public string cooldownKey = "Shield";
[Tooltip("Cooldown time in seconds")] [Tooltip("Cooldown time in seconds")]
public float cooldownTime = 10f; public float cooldownTime = 10f;
[Tooltip("Whether ability should be available immediately at fight start")] [Tooltip("Whether ability should be available immediately at fight start")]
public bool availableAtStart = true; public bool availableAtStart = true;
[Header("Debug")] [Header("Debug")]
[Tooltip("Enable debug logging")] [Tooltip("Enable debug logging")]
public bool enableDebug = false; public bool enableDebug = false;
/// <summary> /// <summary>
/// Main method checking if ability is available /// Main method checking if ability is available
/// </summary> /// </summary>
/// <returns>True if cooldown has passed and ability can be used</returns> /// <returns>True if cooldown has passed and ability can be used</returns>
public override bool Decide(vIFSMBehaviourController fsmBehaviour) public override bool Decide(vIFSMBehaviourController fsmBehaviour)
{ {
if (fsmBehaviour == null) if (fsmBehaviour == null)
{ {
if (enableDebug) Debug.LogWarning($"[DEC_CheckCooldown] No FSM Behaviour for key: {cooldownKey}"); if (enableDebug) Debug.LogWarning($"[DEC_CheckCooldown] No FSM Behaviour for key: {cooldownKey}");
return false; return false;
} }
string timerKey = "cooldown_" + cooldownKey; string timerKey = "cooldown_" + cooldownKey;
if (!fsmBehaviour.HasTimer(timerKey)) if (!fsmBehaviour.HasTimer(timerKey))
{ {
if (availableAtStart) if (availableAtStart)
{ {
if (enableDebug) Debug.Log($"[DEC_CheckCooldown] First use for {cooldownKey} - available"); if (enableDebug) Debug.Log($"[DEC_CheckCooldown] First use for {cooldownKey} - available");
return true; return true;
} }
else else
{ {
SetCooldown(fsmBehaviour, cooldownTime); SetCooldown(fsmBehaviour, cooldownTime);
if (enableDebug) Debug.Log($"[DEC_CheckCooldown] First use for {cooldownKey} - setting cooldown"); if (enableDebug) Debug.Log($"[DEC_CheckCooldown] First use for {cooldownKey} - setting cooldown");
return false; return false;
} }
} }
float lastUsedTime = fsmBehaviour.GetTimer(timerKey); float lastUsedTime = fsmBehaviour.GetTimer(timerKey);
float timeSinceLastUse = Time.time - lastUsedTime; float timeSinceLastUse = Time.time - lastUsedTime;
bool isAvailable = timeSinceLastUse >= cooldownTime; bool isAvailable = timeSinceLastUse >= cooldownTime;
if (enableDebug) if (enableDebug)
{ {
if (isAvailable) if (isAvailable)
{ {
Debug.Log($"[DEC_CheckCooldown] {cooldownKey} available - {timeSinceLastUse:F1}s passed of required {cooldownTime}s"); Debug.Log($"[DEC_CheckCooldown] {cooldownKey} available - {timeSinceLastUse:F1}s passed of required {cooldownTime}s");
} }
else else
{ {
float remainingTime = cooldownTime - timeSinceLastUse; float remainingTime = cooldownTime - timeSinceLastUse;
Debug.Log($"[DEC_CheckCooldown] {cooldownKey} on cooldown - {remainingTime:F1}s remaining"); Debug.Log($"[DEC_CheckCooldown] {cooldownKey} on cooldown - {remainingTime:F1}s remaining");
} }
} }
return isAvailable; return isAvailable;
} }
/// <summary> /// <summary>
/// Sets cooldown for ability - call this after using ability /// Sets cooldown for ability - call this after using ability
/// </summary> /// </summary>
public void SetCooldown(vIFSMBehaviourController fsmBehaviour) public void SetCooldown(vIFSMBehaviourController fsmBehaviour)
{ {
SetCooldown(fsmBehaviour, cooldownTime); SetCooldown(fsmBehaviour, cooldownTime);
} }
/// <summary> /// <summary>
/// Sets cooldown with custom time /// Sets cooldown with custom time
/// </summary> /// </summary>
/// <param name="fsmBehaviour">FSM behaviour reference</param> /// <param name="fsmBehaviour">FSM behaviour reference</param>
/// <param name="customCooldownTime">Custom cooldown time</param> /// <param name="customCooldownTime">Custom cooldown time</param>
public void SetCooldown(vIFSMBehaviourController fsmBehaviour, float customCooldownTime) public void SetCooldown(vIFSMBehaviourController fsmBehaviour, float customCooldownTime)
{ {
if (fsmBehaviour == null) if (fsmBehaviour == null)
{ {
if (enableDebug) Debug.LogWarning($"[DEC_CheckCooldown] Cannot set cooldown - no FSM Behaviour"); if (enableDebug) Debug.LogWarning($"[DEC_CheckCooldown] Cannot set cooldown - no FSM Behaviour");
return; return;
} }
string timerKey = "cooldown_" + cooldownKey; string timerKey = "cooldown_" + cooldownKey;
fsmBehaviour.SetTimer(timerKey, Time.time); fsmBehaviour.SetTimer(timerKey, Time.time);
if (enableDebug) if (enableDebug)
{ {
Debug.Log($"[DEC_CheckCooldown] Set cooldown for {cooldownKey}: {customCooldownTime}s"); Debug.Log($"[DEC_CheckCooldown] Set cooldown for {cooldownKey}: {customCooldownTime}s");
} }
} }
/// <summary> /// <summary>
/// Resets cooldown - ability becomes immediately available /// Resets cooldown - ability becomes immediately available
/// </summary> /// </summary>
public void ResetCooldown(vIFSMBehaviourController fsmBehaviour) public void ResetCooldown(vIFSMBehaviourController fsmBehaviour)
{ {
if (fsmBehaviour == null) if (fsmBehaviour == null)
{ {
if (enableDebug) Debug.LogWarning($"[DEC_CheckCooldown] Cannot reset cooldown - no FSM Behaviour"); if (enableDebug) Debug.LogWarning($"[DEC_CheckCooldown] Cannot reset cooldown - no FSM Behaviour");
return; return;
} }
string timerKey = "cooldown_" + cooldownKey; string timerKey = "cooldown_" + cooldownKey;
float pastTime = Time.time - cooldownTime - 1f; float pastTime = Time.time - cooldownTime - 1f;
fsmBehaviour.SetTimer(timerKey, pastTime); fsmBehaviour.SetTimer(timerKey, pastTime);
if (enableDebug) Debug.Log($"[DEC_CheckCooldown] Reset cooldown for {cooldownKey}"); if (enableDebug) Debug.Log($"[DEC_CheckCooldown] Reset cooldown for {cooldownKey}");
} }
/// <summary> /// <summary>
/// Returns remaining cooldown time in seconds /// Returns remaining cooldown time in seconds
/// </summary> /// </summary>
/// <returns>Remaining cooldown time (0 if available)</returns> /// <returns>Remaining cooldown time (0 if available)</returns>
public float GetRemainingCooldown(vIFSMBehaviourController fsmBehaviour) public float GetRemainingCooldown(vIFSMBehaviourController fsmBehaviour)
{ {
if (fsmBehaviour == null) return 0f; if (fsmBehaviour == null) return 0f;
string timerKey = "cooldown_" + cooldownKey; string timerKey = "cooldown_" + cooldownKey;
if (!fsmBehaviour.HasTimer(timerKey)) if (!fsmBehaviour.HasTimer(timerKey))
{ {
return availableAtStart ? 0f : cooldownTime; return availableAtStart ? 0f : cooldownTime;
} }
float lastUsedTime = fsmBehaviour.GetTimer(timerKey); float lastUsedTime = fsmBehaviour.GetTimer(timerKey);
float timeSinceLastUse = Time.time - lastUsedTime; float timeSinceLastUse = Time.time - lastUsedTime;
return Mathf.Max(0f, cooldownTime - timeSinceLastUse); return Mathf.Max(0f, cooldownTime - timeSinceLastUse);
} }
/// <summary> /// <summary>
/// Checks if ability is available without running main Decision logic /// Checks if ability is available without running main Decision logic
/// </summary> /// </summary>
/// <returns>True if ability is available</returns> /// <returns>True if ability is available</returns>
public bool IsAvailable(vIFSMBehaviourController fsmBehaviour) public bool IsAvailable(vIFSMBehaviourController fsmBehaviour)
{ {
return GetRemainingCooldown(fsmBehaviour) <= 0f; return GetRemainingCooldown(fsmBehaviour) <= 0f;
} }
/// <summary> /// <summary>
/// Returns cooldown progress percentage (0-1) /// Returns cooldown progress percentage (0-1)
/// </summary> /// </summary>
/// <returns>Progress percentage: 0 = just used, 1 = fully recharged</returns> /// <returns>Progress percentage: 0 = just used, 1 = fully recharged</returns>
public float GetCooldownProgress(vIFSMBehaviourController fsmBehaviour) public float GetCooldownProgress(vIFSMBehaviourController fsmBehaviour)
{ {
if (cooldownTime <= 0f) return 1f; if (cooldownTime <= 0f) return 1f;
float remainingTime = GetRemainingCooldown(fsmBehaviour); float remainingTime = GetRemainingCooldown(fsmBehaviour);
return 1f - (remainingTime / cooldownTime); return 1f - (remainingTime / cooldownTime);
} }
/// <summary> /// <summary>
/// Helper method for setting cooldown from external code (e.g. from StateAction) /// Helper method for setting cooldown from external code (e.g. from StateAction)
/// </summary> /// </summary>
/// <param name="fsmBehaviour">FSM reference</param> /// <param name="fsmBehaviour">FSM reference</param>
/// <param name="key">Ability key</param> /// <param name="key">Ability key</param>
/// <param name="cooldown">Cooldown time</param> /// <param name="cooldown">Cooldown time</param>
public static void SetCooldownStatic(vIFSMBehaviourController fsmBehaviour, string key, float cooldown) public static void SetCooldownStatic(vIFSMBehaviourController fsmBehaviour, string key, float cooldown)
{ {
if (fsmBehaviour == null) return; if (fsmBehaviour == null) return;
string timerKey = "cooldown_" + key; string timerKey = "cooldown_" + key;
fsmBehaviour.SetTimer(timerKey, Time.time); fsmBehaviour.SetTimer(timerKey, Time.time);
} }
/// <summary> /// <summary>
/// Helper method for checking cooldown from external code /// Helper method for checking cooldown from external code
/// </summary> /// </summary>
/// <param name="fsmBehaviour">FSM reference</param> /// <param name="fsmBehaviour">FSM reference</param>
/// <param name="key">Ability key</param> /// <param name="key">Ability key</param>
/// <param name="cooldown">Cooldown time</param> /// <param name="cooldown">Cooldown time</param>
/// <returns>True if available</returns> /// <returns>True if available</returns>
public static bool CheckCooldownStatic(vIFSMBehaviourController fsmBehaviour, string key, float cooldown) public static bool CheckCooldownStatic(vIFSMBehaviourController fsmBehaviour, string key, float cooldown)
{ {
if (fsmBehaviour == null) return false; if (fsmBehaviour == null) return false;
string timerKey = "cooldown_" + key; string timerKey = "cooldown_" + key;
if (!fsmBehaviour.HasTimer(timerKey)) return true; if (!fsmBehaviour.HasTimer(timerKey)) return true;
float lastUsedTime = fsmBehaviour.GetTimer(timerKey); float lastUsedTime = fsmBehaviour.GetTimer(timerKey);
return (Time.time - lastUsedTime) >= cooldown; return (Time.time - lastUsedTime) >= cooldown;
} }
} }
} }

View File

@@ -1,105 +1,105 @@
using Invector.vCharacterController.AI.FSMBehaviour; using Invector.vCharacterController.AI.FSMBehaviour;
using UnityEngine; using UnityEngine;
namespace DemonBoss.Magic namespace DemonBoss.Magic
{ {
/// <summary> /// <summary>
/// Decision checking if target has clear sky above (for meteor) /// Decision checking if target has clear sky above (for meteor)
/// </summary> /// </summary>
[CreateAssetMenu(menuName = "Invector/FSM/Decisions/DemonBoss/Target Clear Sky")] [CreateAssetMenu(menuName = "Invector/FSM/Decisions/DemonBoss/Target Clear Sky")]
public class DEC_TargetClearSky : vStateDecision public class DEC_TargetClearSky : vStateDecision
{ {
public override string categoryName => "DemonBoss/Magic"; public override string categoryName => "DemonBoss/Magic";
public override string defaultName => "Target Clear Sky"; public override string defaultName => "Target Clear Sky";
[Header("Sky Check Configuration")] [Header("Sky Check Configuration")]
[Tooltip("Check height above target")] [Tooltip("Check height above target")]
public float checkHeight = 25f; public float checkHeight = 25f;
[Tooltip("Obstacle check radius")] [Tooltip("Obstacle check radius")]
public float checkRadius = 2f; public float checkRadius = 2f;
[Tooltip("Obstacle layer mask")] [Tooltip("Obstacle layer mask")]
public LayerMask obstacleLayerMask = -1; public LayerMask obstacleLayerMask = -1;
[Header("Debug")] [Header("Debug")]
[Tooltip("Enable debug logging")] [Tooltip("Enable debug logging")]
public bool enableDebug = false; public bool enableDebug = false;
[Tooltip("Show gizmos in Scene View")] [Tooltip("Show gizmos in Scene View")]
public bool showGizmos = true; public bool showGizmos = true;
public override bool Decide(vIFSMBehaviourController fsmBehaviour) public override bool Decide(vIFSMBehaviourController fsmBehaviour)
{ {
Transform target = GetTarget(fsmBehaviour); Transform target = GetTarget(fsmBehaviour);
if (target == null) if (target == null)
{ {
if (enableDebug) Debug.Log("[DEC_TargetClearSky] No target found"); if (enableDebug) Debug.Log("[DEC_TargetClearSky] No target found");
return false; return false;
} }
bool isClear = IsSkyClear(target.position); bool isClear = IsSkyClear(target.position);
if (enableDebug) if (enableDebug)
{ {
Debug.Log($"[DEC_TargetClearSky] Sky above target: {(isClear ? "CLEAR" : "BLOCKED")}"); Debug.Log($"[DEC_TargetClearSky] Sky above target: {(isClear ? "CLEAR" : "BLOCKED")}");
} }
return isClear; return isClear;
} }
private bool IsSkyClear(Vector3 targetPosition) private bool IsSkyClear(Vector3 targetPosition)
{ {
Vector3 skyCheckPoint = targetPosition + Vector3.up * checkHeight; Vector3 skyCheckPoint = targetPosition + Vector3.up * checkHeight;
if (Physics.CheckSphere(skyCheckPoint, checkRadius, obstacleLayerMask)) if (Physics.CheckSphere(skyCheckPoint, checkRadius, obstacleLayerMask))
{ {
return false; return false;
} }
Ray skyRay = new Ray(skyCheckPoint, Vector3.down); Ray skyRay = new Ray(skyCheckPoint, Vector3.down);
RaycastHit[] hits = Physics.RaycastAll(skyRay, checkHeight, obstacleLayerMask); RaycastHit[] hits = Physics.RaycastAll(skyRay, checkHeight, obstacleLayerMask);
foreach (var hit in hits) foreach (var hit in hits)
{ {
if (hit.point.y <= targetPosition.y + 0.5f) continue; if (hit.point.y <= targetPosition.y + 0.5f) continue;
return false; return false;
} }
return true; return true;
} }
private Transform GetTarget(vIFSMBehaviourController fsmBehaviour) private Transform GetTarget(vIFSMBehaviourController fsmBehaviour)
{ {
// Try through AI controller // Try through AI controller
var aiController = fsmBehaviour as Invector.vCharacterController.AI.vIControlAI; var aiController = fsmBehaviour as Invector.vCharacterController.AI.vIControlAI;
if (aiController != null && aiController.currentTarget != null) if (aiController != null && aiController.currentTarget != null)
return aiController.currentTarget.transform; return aiController.currentTarget.transform;
// Fallback - find player // Fallback - find player
GameObject player = GameObject.FindGameObjectWithTag("Player"); GameObject player = GameObject.FindGameObjectWithTag("Player");
return player?.transform; return player?.transform;
} }
private void OnDrawGizmosSelected() private void OnDrawGizmosSelected()
{ {
if (!showGizmos) return; if (!showGizmos) return;
GameObject player = GameObject.FindGameObjectWithTag("Player"); GameObject player = GameObject.FindGameObjectWithTag("Player");
if (player == null) return; if (player == null) return;
Vector3 targetPos = player.transform.position; Vector3 targetPos = player.transform.position;
Vector3 skyCheckPoint = targetPos + Vector3.up * checkHeight; Vector3 skyCheckPoint = targetPos + Vector3.up * checkHeight;
Gizmos.color = Color.cyan; Gizmos.color = Color.cyan;
Gizmos.DrawWireSphere(skyCheckPoint, checkRadius); Gizmos.DrawWireSphere(skyCheckPoint, checkRadius);
Gizmos.color = Color.yellow; Gizmos.color = Color.yellow;
Gizmos.DrawLine(targetPos, skyCheckPoint); Gizmos.DrawLine(targetPos, skyCheckPoint);
Gizmos.color = Color.red; Gizmos.color = Color.red;
Gizmos.DrawWireSphere(targetPos, 0.5f); Gizmos.DrawWireSphere(targetPos, 0.5f);
} }
} }
} }

View File

@@ -1,58 +1,58 @@
using Invector.vCharacterController.AI.FSMBehaviour; using Invector.vCharacterController.AI.FSMBehaviour;
using UnityEngine; using UnityEngine;
namespace DemonBoss.Magic namespace DemonBoss.Magic
{ {
/// <summary> /// <summary>
/// Decision checking if target is far away (for Turret ability) /// Decision checking if target is far away (for Turret ability)
/// </summary> /// </summary>
[CreateAssetMenu(menuName = "Invector/FSM/Decisions/DemonBoss/Target Far")] [CreateAssetMenu(menuName = "Invector/FSM/Decisions/DemonBoss/Target Far")]
public class DEC_TargetFar : vStateDecision public class DEC_TargetFar : vStateDecision
{ {
public override string categoryName => "DemonBoss/Magic"; public override string categoryName => "DemonBoss/Magic";
public override string defaultName => "Target Far"; public override string defaultName => "Target Far";
[Header("Distance Configuration")] [Header("Distance Configuration")]
[Tooltip("Minimum distance for target to be considered far")] [Tooltip("Minimum distance for target to be considered far")]
public float minDistance = 8f; public float minDistance = 8f;
[Tooltip("Maximum distance for checking")] [Tooltip("Maximum distance for checking")]
public float maxDistance = 30f; public float maxDistance = 30f;
[Header("Debug")] [Header("Debug")]
[Tooltip("Enable debug logging")] [Tooltip("Enable debug logging")]
public bool enableDebug = false; public bool enableDebug = false;
public override bool Decide(vIFSMBehaviourController fsmBehaviour) public override bool Decide(vIFSMBehaviourController fsmBehaviour)
{ {
Transform target = GetTarget(fsmBehaviour); Transform target = GetTarget(fsmBehaviour);
if (target == null) if (target == null)
{ {
if (enableDebug) Debug.Log("[DEC_TargetFar] No target found"); if (enableDebug) Debug.Log("[DEC_TargetFar] No target found");
return false; return false;
} }
float distance = Vector3.Distance(fsmBehaviour.transform.position, target.position); float distance = Vector3.Distance(fsmBehaviour.transform.position, target.position);
bool isFar = distance >= minDistance && distance <= maxDistance; bool isFar = distance >= minDistance && distance <= maxDistance;
if (enableDebug) if (enableDebug)
{ {
Debug.Log($"[DEC_TargetFar] Distance to target: {distance:F1}m - {(isFar ? "FAR" : "CLOSE")}"); Debug.Log($"[DEC_TargetFar] Distance to target: {distance:F1}m - {(isFar ? "FAR" : "CLOSE")}");
} }
return isFar; return isFar;
} }
private Transform GetTarget(vIFSMBehaviourController fsmBehaviour) private Transform GetTarget(vIFSMBehaviourController fsmBehaviour)
{ {
// Try through AI controller // Try through AI controller
var aiController = fsmBehaviour as Invector.vCharacterController.AI.vIControlAI; var aiController = fsmBehaviour as Invector.vCharacterController.AI.vIControlAI;
if (aiController != null && aiController.currentTarget != null) if (aiController != null && aiController.currentTarget != null)
return aiController.currentTarget.transform; return aiController.currentTarget.transform;
// Fallback - find player // Fallback - find player
GameObject player = GameObject.FindGameObjectWithTag("Player"); GameObject player = GameObject.FindGameObjectWithTag("Player");
return player?.transform; return player?.transform;
} }
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,386 +1,386 @@
using Invector; using Invector;
using Invector.vCharacterController; using Invector.vCharacterController;
using Lean.Pool; using Lean.Pool;
using UnityEngine; using UnityEngine;
namespace DemonBoss.Magic namespace DemonBoss.Magic
{ {
/// <summary> /// <summary>
/// Component handling damage and collisions for fireball fired by crystals /// Component handling damage and collisions for fireball fired by crystals
/// Deals damage on collision with player and automatically despawns /// Deals damage on collision with player and automatically despawns
/// </summary> /// </summary>
[RequireComponent(typeof(Collider))] [RequireComponent(typeof(Collider))]
public class FireballDamage : MonoBehaviour public class FireballDamage : MonoBehaviour
{ {
[Header("Damage Configuration")] [Header("Damage Configuration")]
[Tooltip("Damage dealt by fireball")] [Tooltip("Damage dealt by fireball")]
public float damage = 25f; public float damage = 25f;
[Tooltip("Whether fireball can deal damage only once")] [Tooltip("Whether fireball can deal damage only once")]
public bool damageOnce = true; public bool damageOnce = true;
[Tooltip("Layer mask for targets that can be hit")] [Tooltip("Layer mask for targets that can be hit")]
public LayerMask targetLayerMask = -1; public LayerMask targetLayerMask = -1;
[Header("Impact Effects")] [Header("Impact Effects")]
[Tooltip("Explosion effect prefab on hit")] [Tooltip("Explosion effect prefab on hit")]
public GameObject impactEffectPrefab; public GameObject impactEffectPrefab;
[Tooltip("Impact sound")] [Tooltip("Impact sound")]
public AudioClip impactSound; public AudioClip impactSound;
[Tooltip("Knockback force on hit")] [Tooltip("Knockback force on hit")]
public float knockbackForce = 5f; public float knockbackForce = 5f;
[Header("Lifetime")] [Header("Lifetime")]
[Tooltip("Maximum fireball lifetime in seconds (failsafe)")] [Tooltip("Maximum fireball lifetime in seconds (failsafe)")]
public float maxLifetime = 5f; public float maxLifetime = 5f;
[Tooltip("Whether to destroy fireball on terrain collision")] [Tooltip("Whether to destroy fireball on terrain collision")]
public bool destroyOnTerrain = true; public bool destroyOnTerrain = true;
[Tooltip("Layer mask for terrain/obstacles")] [Tooltip("Layer mask for terrain/obstacles")]
public LayerMask terrainLayerMask = 1; public LayerMask terrainLayerMask = 1;
[Header("Debug")] [Header("Debug")]
[Tooltip("Enable debug logging")] [Tooltip("Enable debug logging")]
public bool enableDebug = false; public bool enableDebug = false;
// Private variables // Private variables
private bool hasDealtDamage = false; private bool hasDealtDamage = false;
private float spawnTime; private float spawnTime;
private AudioSource audioSource; private AudioSource audioSource;
private Collider fireballCollider; private Collider fireballCollider;
private Rigidbody fireballRigidbody; private Rigidbody fireballRigidbody;
/// <summary> /// <summary>
/// Component initialization /// Component initialization
/// </summary> /// </summary>
private void Awake() private void Awake()
{ {
fireballCollider = GetComponent<Collider>(); fireballCollider = GetComponent<Collider>();
fireballRigidbody = GetComponent<Rigidbody>(); fireballRigidbody = GetComponent<Rigidbody>();
if (fireballCollider != null && !fireballCollider.isTrigger) if (fireballCollider != null && !fireballCollider.isTrigger)
{ {
fireballCollider.isTrigger = true; fireballCollider.isTrigger = true;
if (enableDebug) Debug.Log("[FireballDamage] Set collider as trigger"); if (enableDebug) Debug.Log("[FireballDamage] Set collider as trigger");
} }
audioSource = GetComponent<AudioSource>(); audioSource = GetComponent<AudioSource>();
if (audioSource == null && impactSound != null) if (audioSource == null && impactSound != null)
{ {
audioSource = gameObject.AddComponent<AudioSource>(); audioSource = gameObject.AddComponent<AudioSource>();
audioSource.playOnAwake = false; audioSource.playOnAwake = false;
audioSource.spatialBlend = 1f; audioSource.spatialBlend = 1f;
} }
} }
/// <summary> /// <summary>
/// Start - note spawn time and start failsafe timer /// Start - note spawn time and start failsafe timer
/// </summary> /// </summary>
private void Start() private void Start()
{ {
spawnTime = Time.time; spawnTime = Time.time;
hasDealtDamage = false; hasDealtDamage = false;
Invoke(nameof(FailsafeDespawn), maxLifetime); Invoke(nameof(FailsafeDespawn), maxLifetime);
if (enableDebug) Debug.Log("[FireballDamage] Fireball initialized"); if (enableDebug) Debug.Log("[FireballDamage] Fireball initialized");
} }
/// <summary> /// <summary>
/// Trigger collision handling - dealing damage /// Trigger collision handling - dealing damage
/// </summary> /// </summary>
/// <param name="other">Collider that fireball collides with</param> /// <param name="other">Collider that fireball collides with</param>
private void OnTriggerEnter(Collider other) private void OnTriggerEnter(Collider other)
{ {
if (enableDebug) Debug.Log($"[FireballDamage] Collision with: {other.name}"); if (enableDebug) Debug.Log($"[FireballDamage] Collision with: {other.name}");
if (IsValidTarget(other)) if (IsValidTarget(other))
{ {
DealDamageToTarget(other); DealDamageToTarget(other);
CreateImpactEffect(other.transform.position); CreateImpactEffect(other.transform.position);
DespawnFireball(); DespawnFireball();
return; return;
} }
if (destroyOnTerrain && IsTerrainCollision(other)) if (destroyOnTerrain && IsTerrainCollision(other))
{ {
if (enableDebug) Debug.Log("[FireballDamage] Terrain collision"); if (enableDebug) Debug.Log("[FireballDamage] Terrain collision");
CreateImpactEffect(transform.position); CreateImpactEffect(transform.position);
DespawnFireball(); DespawnFireball();
return; return;
} }
} }
/// <summary> /// <summary>
/// Checks if collider is valid target /// Checks if collider is valid target
/// </summary> /// </summary>
/// <param name="other">Collider to check</param> /// <param name="other">Collider to check</param>
/// <returns>True if it's valid target</returns> /// <returns>True if it's valid target</returns>
private bool IsValidTarget(Collider other) private bool IsValidTarget(Collider other)
{ {
if ((targetLayerMask.value & (1 << other.gameObject.layer)) == 0) if ((targetLayerMask.value & (1 << other.gameObject.layer)) == 0)
return false; return false;
if (damageOnce && hasDealtDamage) if (damageOnce && hasDealtDamage)
return false; return false;
var damageReceiver = other.GetComponent<vIDamageReceiver>(); var damageReceiver = other.GetComponent<vIDamageReceiver>();
var healthController = other.GetComponent<vHealthController>(); var healthController = other.GetComponent<vHealthController>();
var character = other.GetComponent<vCharacter>(); var character = other.GetComponent<vCharacter>();
var thirdPersonController = other.GetComponent<vThirdPersonController>(); var thirdPersonController = other.GetComponent<vThirdPersonController>();
if (damageReceiver == null) damageReceiver = other.GetComponentInParent<vIDamageReceiver>(); if (damageReceiver == null) damageReceiver = other.GetComponentInParent<vIDamageReceiver>();
if (healthController == null) healthController = other.GetComponentInParent<vHealthController>(); if (healthController == null) healthController = other.GetComponentInParent<vHealthController>();
if (character == null) character = other.GetComponentInParent<vCharacter>(); if (character == null) character = other.GetComponentInParent<vCharacter>();
if (thirdPersonController == null) thirdPersonController = other.GetComponentInParent<vThirdPersonController>(); if (thirdPersonController == null) thirdPersonController = other.GetComponentInParent<vThirdPersonController>();
return damageReceiver != null || healthController != null || character != null || thirdPersonController != null; return damageReceiver != null || healthController != null || character != null || thirdPersonController != null;
} }
/// <summary> /// <summary>
/// Checks if it's terrain collision /// Checks if it's terrain collision
/// </summary> /// </summary>
/// <param name="other">Collider to check</param> /// <param name="other">Collider to check</param>
/// <returns>True if it's terrain</returns> /// <returns>True if it's terrain</returns>
private bool IsTerrainCollision(Collider other) private bool IsTerrainCollision(Collider other)
{ {
return (terrainLayerMask.value & (1 << other.gameObject.layer)) != 0; return (terrainLayerMask.value & (1 << other.gameObject.layer)) != 0;
} }
/// <summary> /// <summary>
/// Deals damage to target - ulepszona wersja /// Deals damage to target - ulepszona wersja
/// </summary> /// </summary>
/// <param name="targetCollider">Target collider</param> /// <param name="targetCollider">Target collider</param>
private void DealDamageToTarget(Collider targetCollider) private void DealDamageToTarget(Collider targetCollider)
{ {
if (enableDebug) Debug.Log($"[FireballDamage] Dealing {damage} damage to: {targetCollider.name}"); if (enableDebug) Debug.Log($"[FireballDamage] Dealing {damage} damage to: {targetCollider.name}");
Vector3 hitPoint = GetClosestPointOnCollider(targetCollider); Vector3 hitPoint = GetClosestPointOnCollider(targetCollider);
Vector3 hitDirection = GetHitDirection(targetCollider); Vector3 hitDirection = GetHitDirection(targetCollider);
vDamage damageInfo = new vDamage(Mathf.RoundToInt(damage)); vDamage damageInfo = new vDamage(Mathf.RoundToInt(damage));
damageInfo.sender = transform; damageInfo.sender = transform;
damageInfo.hitPosition = hitPoint; damageInfo.hitPosition = hitPoint;
if (knockbackForce > 0f) if (knockbackForce > 0f)
{ {
damageInfo.force = hitDirection * knockbackForce; damageInfo.force = hitDirection * knockbackForce;
} }
bool damageDealt = false; bool damageDealt = false;
var damageReceiver = targetCollider.GetComponent<vIDamageReceiver>(); var damageReceiver = targetCollider.GetComponent<vIDamageReceiver>();
if (damageReceiver == null) damageReceiver = targetCollider.GetComponentInParent<vIDamageReceiver>(); if (damageReceiver == null) damageReceiver = targetCollider.GetComponentInParent<vIDamageReceiver>();
if (damageReceiver != null && !damageDealt) if (damageReceiver != null && !damageDealt)
{ {
damageReceiver.TakeDamage(damageInfo); damageReceiver.TakeDamage(damageInfo);
damageDealt = true; damageDealt = true;
hasDealtDamage = true; hasDealtDamage = true;
if (enableDebug) Debug.Log("[FireballDamage] Damage dealt through vIDamageReceiver"); if (enableDebug) Debug.Log("[FireballDamage] Damage dealt through vIDamageReceiver");
} }
if (!damageDealt) if (!damageDealt)
{ {
var healthController = targetCollider.GetComponent<vHealthController>(); var healthController = targetCollider.GetComponent<vHealthController>();
if (healthController == null) healthController = targetCollider.GetComponentInParent<vHealthController>(); if (healthController == null) healthController = targetCollider.GetComponentInParent<vHealthController>();
if (healthController != null) if (healthController != null)
{ {
healthController.TakeDamage(damageInfo); healthController.TakeDamage(damageInfo);
damageDealt = true; damageDealt = true;
hasDealtDamage = true; hasDealtDamage = true;
if (enableDebug) Debug.Log("[FireballDamage] Damage dealt through vHealthController"); if (enableDebug) Debug.Log("[FireballDamage] Damage dealt through vHealthController");
} }
} }
if (!damageDealt) if (!damageDealt)
{ {
var thirdPersonController = targetCollider.GetComponent<vThirdPersonController>(); var thirdPersonController = targetCollider.GetComponent<vThirdPersonController>();
if (thirdPersonController == null) thirdPersonController = targetCollider.GetComponentInParent<vThirdPersonController>(); if (thirdPersonController == null) thirdPersonController = targetCollider.GetComponentInParent<vThirdPersonController>();
if (thirdPersonController != null) if (thirdPersonController != null)
{ {
if (thirdPersonController is Beyond.bThirdPersonController beyondController) if (thirdPersonController is Beyond.bThirdPersonController beyondController)
{ {
if (!beyondController.GodMode && !beyondController.isImmortal) if (!beyondController.GodMode && !beyondController.isImmortal)
{ {
thirdPersonController.TakeDamage(damageInfo); thirdPersonController.TakeDamage(damageInfo);
damageDealt = true; damageDealt = true;
hasDealtDamage = true; hasDealtDamage = true;
if (enableDebug) Debug.Log("[FireballDamage] Damage dealt through bThirdPersonController"); if (enableDebug) Debug.Log("[FireballDamage] Damage dealt through bThirdPersonController");
} }
else else
{ {
if (enableDebug) Debug.Log("[FireballDamage] Player is immortal or in God Mode - no damage dealt"); if (enableDebug) Debug.Log("[FireballDamage] Player is immortal or in God Mode - no damage dealt");
} }
} }
else else
{ {
thirdPersonController.TakeDamage(damageInfo); thirdPersonController.TakeDamage(damageInfo);
damageDealt = true; damageDealt = true;
hasDealtDamage = true; hasDealtDamage = true;
if (enableDebug) Debug.Log("[FireballDamage] Damage dealt through vThirdPersonController"); if (enableDebug) Debug.Log("[FireballDamage] Damage dealt through vThirdPersonController");
} }
} }
} }
if (!damageDealt) if (!damageDealt)
{ {
if (enableDebug) Debug.LogWarning("[FireballDamage] Could not deal damage - no valid damage receiver found!"); if (enableDebug) Debug.LogWarning("[FireballDamage] Could not deal damage - no valid damage receiver found!");
} }
} }
/// <summary> /// <summary>
/// Takes the closest point on the collider as the point of impact /// Takes the closest point on the collider as the point of impact
/// </summary> /// </summary>
private Vector3 GetClosestPointOnCollider(Collider targetCollider) private Vector3 GetClosestPointOnCollider(Collider targetCollider)
{ {
return targetCollider.ClosestPoint(transform.position); return targetCollider.ClosestPoint(transform.position);
} }
/// <summary> /// <summary>
/// Calculates the direction of impact for knockback /// Calculates the direction of impact for knockback
/// </summary> /// </summary>
private Vector3 GetHitDirection(Collider targetCollider) private Vector3 GetHitDirection(Collider targetCollider)
{ {
Vector3 direction; Vector3 direction;
if (fireballRigidbody != null && fireballRigidbody.linearVelocity.magnitude > 0.1f) if (fireballRigidbody != null && fireballRigidbody.linearVelocity.magnitude > 0.1f)
{ {
direction = fireballRigidbody.linearVelocity.normalized; direction = fireballRigidbody.linearVelocity.normalized;
} }
else else
{ {
direction = (targetCollider.transform.position - transform.position).normalized; direction = (targetCollider.transform.position - transform.position).normalized;
} }
direction.y = Mathf.Max(0.2f, direction.y); direction.y = Mathf.Max(0.2f, direction.y);
return direction.normalized; return direction.normalized;
} }
/// <summary> /// <summary>
/// Creates impact effect /// Creates impact effect
/// </summary> /// </summary>
/// <param name="impactPosition">Impact position</param> /// <param name="impactPosition">Impact position</param>
private void CreateImpactEffect(Vector3 impactPosition) private void CreateImpactEffect(Vector3 impactPosition)
{ {
if (impactEffectPrefab != null) if (impactEffectPrefab != null)
{ {
GameObject impact = LeanPool.Spawn(impactEffectPrefab, impactPosition, Quaternion.identity); GameObject impact = LeanPool.Spawn(impactEffectPrefab, impactPosition, Quaternion.identity);
LeanPool.Despawn(impact, 3f); LeanPool.Despawn(impact, 3f);
if (enableDebug) Debug.Log("[FireballDamage] Impact effect created"); if (enableDebug) Debug.Log("[FireballDamage] Impact effect created");
} }
if (audioSource != null && impactSound != null) if (audioSource != null && impactSound != null)
{ {
audioSource.PlayOneShot(impactSound); audioSource.PlayOneShot(impactSound);
} }
} }
/// <summary> /// <summary>
/// Despawns fireball from map /// Despawns fireball from map
/// </summary> /// </summary>
private void DespawnFireball() private void DespawnFireball()
{ {
if (enableDebug) Debug.Log("[FireballDamage] Despawning fireball"); if (enableDebug) Debug.Log("[FireballDamage] Despawning fireball");
CancelInvoke(nameof(FailsafeDespawn)); CancelInvoke(nameof(FailsafeDespawn));
LeanPool.Despawn(gameObject); LeanPool.Despawn(gameObject);
} }
/// <summary> /// <summary>
/// Failsafe despawn - removes fireball after maximum lifetime /// Failsafe despawn - removes fireball after maximum lifetime
/// </summary> /// </summary>
private void FailsafeDespawn() private void FailsafeDespawn()
{ {
if (enableDebug) Debug.Log("[FireballDamage] Failsafe despawn - lifetime exceeded"); if (enableDebug) Debug.Log("[FireballDamage] Failsafe despawn - lifetime exceeded");
DespawnFireball(); DespawnFireball();
} }
/// <summary> /// <summary>
/// Forces immediate despawn /// Forces immediate despawn
/// </summary> /// </summary>
public void ForceDespawn() public void ForceDespawn()
{ {
if (enableDebug) Debug.Log("[FireballDamage] Forced despawn"); if (enableDebug) Debug.Log("[FireballDamage] Forced despawn");
DespawnFireball(); DespawnFireball();
} }
/// <summary> /// <summary>
/// Checks if fireball has already dealt damage /// Checks if fireball has already dealt damage
/// </summary> /// </summary>
/// <returns>True if damage was already dealt</returns> /// <returns>True if damage was already dealt</returns>
public bool HasDealtDamage() public bool HasDealtDamage()
{ {
return hasDealtDamage; return hasDealtDamage;
} }
/// <summary> /// <summary>
/// Returns fireball lifetime /// Returns fireball lifetime
/// </summary> /// </summary>
/// <returns>Time in seconds since spawn</returns> /// <returns>Time in seconds since spawn</returns>
public float GetLifetime() public float GetLifetime()
{ {
return Time.time - spawnTime; return Time.time - spawnTime;
} }
/// <summary> /// <summary>
/// Sets new damage (e.g. for different difficulty levels) /// Sets new damage (e.g. for different difficulty levels)
/// </summary> /// </summary>
/// <param name="newDamage">New damage</param> /// <param name="newDamage">New damage</param>
public void SetDamage(float newDamage) public void SetDamage(float newDamage)
{ {
damage = newDamage; damage = newDamage;
if (enableDebug) Debug.Log($"[FireballDamage] Set new damage: {damage}"); if (enableDebug) Debug.Log($"[FireballDamage] Set new damage: {damage}");
} }
/// <summary> /// <summary>
/// Sets new knockback /// Sets new knockback
/// </summary> /// </summary>
/// <param name="newKnockback">New knockback force</param> /// <param name="newKnockback">New knockback force</param>
public void SetKnockback(float newKnockback) public void SetKnockback(float newKnockback)
{ {
knockbackForce = newKnockback; knockbackForce = newKnockback;
if (enableDebug) Debug.Log($"[FireballDamage] Set new knockback: {knockbackForce}"); if (enableDebug) Debug.Log($"[FireballDamage] Set new knockback: {knockbackForce}");
} }
/// <summary> /// <summary>
/// Resets fireball state (useful for pooling) /// Resets fireball state (useful for pooling)
/// </summary> /// </summary>
public void ResetFireball() public void ResetFireball()
{ {
hasDealtDamage = false; hasDealtDamage = false;
spawnTime = Time.time; spawnTime = Time.time;
CancelInvoke(); CancelInvoke();
Invoke(nameof(FailsafeDespawn), maxLifetime); Invoke(nameof(FailsafeDespawn), maxLifetime);
if (enableDebug) Debug.Log("[FireballDamage] Fireball reset"); if (enableDebug) Debug.Log("[FireballDamage] Fireball reset");
} }
/// <summary> /// <summary>
/// Called on spawn by Lean Pool /// Called on spawn by Lean Pool
/// </summary> /// </summary>
private void OnSpawn() private void OnSpawn()
{ {
ResetFireball(); ResetFireball();
} }
/// <summary> /// <summary>
/// Called on despawn by Lean Pool /// Called on despawn by Lean Pool
/// </summary> /// </summary>
private void OnDespawn() private void OnDespawn()
{ {
CancelInvoke(); CancelInvoke();
} }
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 82581d6c5dd5945b89394d83db5f4c8b
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

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

View File

@@ -1,185 +1,185 @@
using Invector.vCharacterController.AI.FSMBehaviour; using Invector.vCharacterController.AI.FSMBehaviour;
using Lean.Pool; using Lean.Pool;
using UnityEngine; using UnityEngine;
namespace DemonBoss.Magic namespace DemonBoss.Magic
{ {
/// <summary> /// <summary>
/// StateAction for Magic Shield spell - boss casts magical shield for 5 seconds /// StateAction for Magic Shield spell - boss casts magical shield for 5 seconds
/// During casting boss stands still, after completion returns to Combat /// During casting boss stands still, after completion returns to Combat
/// </summary> /// </summary>
[CreateAssetMenu(menuName = "Invector/FSM/Decisions/DemonBoss/Cast Shield")] [CreateAssetMenu(menuName = "Invector/FSM/Actions/DemonBoss/Cast Shield")]
public class SA_CastShield : vStateAction public class SA_CastShield : vStateAction
{ {
public override string categoryName => "DemonBoss/Magic"; public override string categoryName => "DemonBoss/Magic";
public override string defaultName => "Cast Shield"; public override string defaultName => "Cast Shield";
[Header("Shield Configuration")] [Header("Shield Configuration")]
[Tooltip("Prefab with magical shield particle effect")] [Tooltip("Prefab with magical shield particle effect")]
public GameObject shieldFXPrefab; public GameObject shieldFXPrefab;
[Tooltip("Transform where shield should appear (usually boss center)")] [Tooltip("Transform where shield should appear (usually boss center)")]
public Transform shieldSpawnPoint; public Transform shieldSpawnPoint;
[Tooltip("Shield duration in seconds")] [Tooltip("Shield duration in seconds")]
public float shieldDuration = 5f; public float shieldDuration = 5f;
[Tooltip("Animator trigger name for shield casting animation")] [Tooltip("Animator trigger name for shield casting animation")]
public string animatorTrigger = "CastShield"; public string animatorTrigger = "CastShield";
[Header("Debug")] [Header("Debug")]
[Tooltip("Enable debug logging")] [Tooltip("Enable debug logging")]
public bool enableDebug = false; public bool enableDebug = false;
private GameObject spawnedShield; private GameObject spawnedShield;
private float shieldStartTime; private float shieldStartTime;
private bool shieldActive = false; private bool shieldActive = false;
/// <summary> /// <summary>
/// Main action execution method called by FSM /// Main action execution method called by FSM
/// </summary> /// </summary>
public override void DoAction(vIFSMBehaviourController fsmBehaviour, vFSMComponentExecutionType executionType = vFSMComponentExecutionType.OnStateUpdate) public override void DoAction(vIFSMBehaviourController fsmBehaviour, vFSMComponentExecutionType executionType = vFSMComponentExecutionType.OnStateUpdate)
{ {
if (executionType == vFSMComponentExecutionType.OnStateEnter) if (executionType == vFSMComponentExecutionType.OnStateEnter)
{ {
OnStateEnter(fsmBehaviour); OnStateEnter(fsmBehaviour);
} }
else if (executionType == vFSMComponentExecutionType.OnStateUpdate) else if (executionType == vFSMComponentExecutionType.OnStateUpdate)
{ {
OnStateUpdate(fsmBehaviour); OnStateUpdate(fsmBehaviour);
} }
else if (executionType == vFSMComponentExecutionType.OnStateExit) else if (executionType == vFSMComponentExecutionType.OnStateExit)
{ {
OnStateExit(fsmBehaviour); OnStateExit(fsmBehaviour);
} }
} }
/// <summary> /// <summary>
/// Called when entering state - starts shield casting /// Called when entering state - starts shield casting
/// </summary> /// </summary>
private void OnStateEnter(vIFSMBehaviourController fsmBehaviour) private void OnStateEnter(vIFSMBehaviourController fsmBehaviour)
{ {
if (enableDebug) Debug.Log("[SA_CastShield] Entering shield casting state"); if (enableDebug) Debug.Log("[SA_CastShield] Entering shield casting state");
var aiController = fsmBehaviour as Invector.vCharacterController.AI.vIControlAI; var aiController = fsmBehaviour as Invector.vCharacterController.AI.vIControlAI;
if (aiController != null) if (aiController != null)
{ {
aiController.Stop(); aiController.Stop();
if (enableDebug) Debug.Log("[SA_CastShield] AI stopped"); if (enableDebug) Debug.Log("[SA_CastShield] AI stopped");
} }
var animator = fsmBehaviour.transform.GetComponent<Animator>(); var animator = fsmBehaviour.transform.GetComponent<Animator>();
if (animator != null && !string.IsNullOrEmpty(animatorTrigger)) if (animator != null && !string.IsNullOrEmpty(animatorTrigger))
{ {
animator.SetTrigger(animatorTrigger); animator.SetTrigger(animatorTrigger);
if (enableDebug) Debug.Log($"[SA_CastShield] Set trigger: {animatorTrigger}"); if (enableDebug) Debug.Log($"[SA_CastShield] Set trigger: {animatorTrigger}");
} }
SpawnShieldEffect(fsmBehaviour); SpawnShieldEffect(fsmBehaviour);
shieldStartTime = Time.time; shieldStartTime = Time.time;
shieldActive = true; shieldActive = true;
} }
/// <summary> /// <summary>
/// Called every frame during state duration /// Called every frame during state duration
/// </summary> /// </summary>
private void OnStateUpdate(vIFSMBehaviourController fsmBehaviour) private void OnStateUpdate(vIFSMBehaviourController fsmBehaviour)
{ {
if (shieldActive && Time.time - shieldStartTime >= shieldDuration) if (shieldActive && Time.time - shieldStartTime >= shieldDuration)
{ {
if (enableDebug) Debug.Log("[SA_CastShield] Shield time passed, finishing state"); if (enableDebug) Debug.Log("[SA_CastShield] Shield time passed, finishing state");
FinishShield(fsmBehaviour); FinishShield(fsmBehaviour);
} }
} }
/// <summary> /// <summary>
/// Called when exiting state - cleanup /// Called when exiting state - cleanup
/// </summary> /// </summary>
private void OnStateExit(vIFSMBehaviourController fsmBehaviour) private void OnStateExit(vIFSMBehaviourController fsmBehaviour)
{ {
if (enableDebug) Debug.Log("[SA_CastShield] Exiting shield state"); if (enableDebug) Debug.Log("[SA_CastShield] Exiting shield state");
if (shieldActive) if (shieldActive)
{ {
CleanupShield(); CleanupShield();
} }
var aiController = fsmBehaviour as Invector.vCharacterController.AI.vIControlAI; var aiController = fsmBehaviour as Invector.vCharacterController.AI.vIControlAI;
if (aiController != null) if (aiController != null)
{ {
if (enableDebug) Debug.Log("[SA_CastShield] AI resumed"); if (enableDebug) Debug.Log("[SA_CastShield] AI resumed");
} }
} }
/// <summary> /// <summary>
/// Spawns magical shield particle effect /// Spawns magical shield particle effect
/// </summary> /// </summary>
private void SpawnShieldEffect(vIFSMBehaviourController fsmBehaviour) private void SpawnShieldEffect(vIFSMBehaviourController fsmBehaviour)
{ {
if (shieldFXPrefab == null) if (shieldFXPrefab == null)
{ {
Debug.LogWarning("[SA_CastShield] Missing shieldFXPrefab!"); Debug.LogWarning("[SA_CastShield] Missing shieldFXPrefab!");
return; return;
} }
Vector3 spawnPosition = shieldSpawnPoint != null ? Vector3 spawnPosition = shieldSpawnPoint != null ?
shieldSpawnPoint.position : fsmBehaviour.transform.position; shieldSpawnPoint.position : fsmBehaviour.transform.position;
spawnedShield = LeanPool.Spawn(shieldFXPrefab, spawnPosition, spawnedShield = LeanPool.Spawn(shieldFXPrefab, spawnPosition,
shieldSpawnPoint != null ? shieldSpawnPoint.rotation : fsmBehaviour.transform.rotation); shieldSpawnPoint != null ? shieldSpawnPoint.rotation : fsmBehaviour.transform.rotation);
if (enableDebug) Debug.Log($"[SA_CastShield] Shield spawned at position: {spawnPosition}"); if (enableDebug) Debug.Log($"[SA_CastShield] Shield spawned at position: {spawnPosition}");
if (spawnedShield != null && shieldSpawnPoint != null) if (spawnedShield != null && shieldSpawnPoint != null)
{ {
spawnedShield.transform.SetParent(shieldSpawnPoint); spawnedShield.transform.SetParent(shieldSpawnPoint);
} }
} }
/// <summary> /// <summary>
/// Finishes shield operation and transitions to next state /// Finishes shield operation and transitions to next state
/// </summary> /// </summary>
private void FinishShield(vIFSMBehaviourController fsmBehaviour) private void FinishShield(vIFSMBehaviourController fsmBehaviour)
{ {
shieldActive = false; shieldActive = false;
CleanupShield(); CleanupShield();
DEC_CheckCooldown.SetCooldownStatic(fsmBehaviour, "Shield", 15f); DEC_CheckCooldown.SetCooldownStatic(fsmBehaviour, "Shield", 15f);
// End state - FSM will transition to next state // End state - FSM will transition to next state
// FYI: In Invector FSM, state completion is handled automatically // FYI: In Invector FSM, state completion is handled automatically
} }
/// <summary> /// <summary>
/// Cleans up spawned shield /// Cleans up spawned shield
/// </summary> /// </summary>
private void CleanupShield() private void CleanupShield()
{ {
if (spawnedShield != null) if (spawnedShield != null)
{ {
LeanPool.Despawn(spawnedShield); LeanPool.Despawn(spawnedShield);
spawnedShield = null; spawnedShield = null;
if (enableDebug) Debug.Log("[SA_CastShield] Shield despawned"); if (enableDebug) Debug.Log("[SA_CastShield] Shield despawned");
} }
} }
/// <summary> /// <summary>
/// Checks if shield is currently active /// Checks if shield is currently active
/// </summary> /// </summary>
public bool IsShieldActive() public bool IsShieldActive()
{ {
return shieldActive; return shieldActive;
} }
/// <summary> /// <summary>
/// Returns remaining shield time /// Returns remaining shield time
/// </summary> /// </summary>
public float GetRemainingShieldTime() public float GetRemainingShieldTime()
{ {
if (!shieldActive) return 0f; if (!shieldActive) return 0f;
return Mathf.Max(0f, shieldDuration - (Time.time - shieldStartTime)); return Mathf.Max(0f, shieldDuration - (Time.time - shieldStartTime));
} }
} }
} }

View File

@@ -1,298 +1,298 @@
using Invector.vCharacterController.AI.FSMBehaviour; using Invector.vCharacterController.AI.FSMBehaviour;
using Lean.Pool; using Lean.Pool;
using UnityEngine; using UnityEngine;
namespace DemonBoss.Magic namespace DemonBoss.Magic
{ {
/// <summary> /// <summary>
/// StateAction for intelligent crystal turret spawning /// StateAction for intelligent crystal turret spawning
/// Searches for optimal position in 2-6m ring from boss, prefers "behind boss" relative to player /// Searches for optimal position in 2-6m ring from boss, prefers "behind boss" relative to player
/// </summary> /// </summary>
[CreateAssetMenu(menuName = "Invector/FSM/Actions/DemonBoss/Spawn Turret Smart")] [CreateAssetMenu(menuName = "Invector/FSM/Actions/DemonBoss/Spawn Turret Smart")]
public class SA_SpawnTurretSmart : vStateAction public class SA_SpawnTurretSmart : vStateAction
{ {
public override string categoryName => "DemonBoss/Magic"; public override string categoryName => "DemonBoss/Magic";
public override string defaultName => "Spawn Turret Smart"; public override string defaultName => "Spawn Turret Smart";
[Header("Turret Configuration")] [Header("Turret Configuration")]
[Tooltip("Crystal prefab with CrystalShooterAI component")] [Tooltip("Crystal prefab with CrystalShooterAI component")]
public GameObject crystalPrefab; public GameObject crystalPrefab;
[Tooltip("Minimum distance from boss for crystal spawn")] [Tooltip("Minimum distance from boss for crystal spawn")]
public float minSpawnDistance = 2f; public float minSpawnDistance = 2f;
[Tooltip("Maximum distance from boss for crystal spawn")] [Tooltip("Maximum distance from boss for crystal spawn")]
public float maxSpawnDistance = 6f; public float maxSpawnDistance = 6f;
[Tooltip("Collision check radius when choosing position")] [Tooltip("Collision check radius when choosing position")]
public float obstacleCheckRadius = 1f; public float obstacleCheckRadius = 1f;
[Tooltip("Height above ground for raycast ground checking")] [Tooltip("Height above ground for raycast ground checking")]
public float groundCheckHeight = 2f; public float groundCheckHeight = 2f;
[Tooltip("Layer mask for obstacles")] [Tooltip("Layer mask for obstacles")]
public LayerMask obstacleLayerMask = -1; public LayerMask obstacleLayerMask = -1;
[Tooltip("Layer mask for ground")] [Tooltip("Layer mask for ground")]
public LayerMask groundLayerMask = -1; public LayerMask groundLayerMask = -1;
[Tooltip("Animator trigger name for crystal casting animation")] [Tooltip("Animator trigger name for crystal casting animation")]
public string animatorTrigger = "CastCrystal"; public string animatorTrigger = "CastCrystal";
[Header("Smart Positioning")] [Header("Smart Positioning")]
[Tooltip("Preference multiplier for positions behind boss (relative to player)")] [Tooltip("Preference multiplier for positions behind boss (relative to player)")]
public float backPreferenceMultiplier = 2f; public float backPreferenceMultiplier = 2f;
[Tooltip("Number of attempts to find valid position")] [Tooltip("Number of attempts to find valid position")]
public int maxSpawnAttempts = 12; public int maxSpawnAttempts = 12;
[Header("Debug")] [Header("Debug")]
[Tooltip("Enable debug logging")] [Tooltip("Enable debug logging")]
public bool enableDebug = false; public bool enableDebug = false;
[Tooltip("Show gizmos in Scene View")] [Tooltip("Show gizmos in Scene View")]
public bool showGizmos = true; public bool showGizmos = true;
private GameObject spawnedCrystal; private GameObject spawnedCrystal;
private Transform playerTransform; private Transform playerTransform;
/// <summary> /// <summary>
/// Main action execution method called by FSM /// Main action execution method called by FSM
/// </summary> /// </summary>
public override void DoAction(vIFSMBehaviourController fsmBehaviour, vFSMComponentExecutionType executionType = vFSMComponentExecutionType.OnStateUpdate) public override void DoAction(vIFSMBehaviourController fsmBehaviour, vFSMComponentExecutionType executionType = vFSMComponentExecutionType.OnStateUpdate)
{ {
if (executionType == vFSMComponentExecutionType.OnStateEnter) if (executionType == vFSMComponentExecutionType.OnStateEnter)
{ {
OnStateEnter(fsmBehaviour); OnStateEnter(fsmBehaviour);
} }
} }
/// <summary> /// <summary>
/// Called when entering state - intelligently spawns crystal /// Called when entering state - intelligently spawns crystal
/// </summary> /// </summary>
private void OnStateEnter(vIFSMBehaviourController fsmBehaviour) private void OnStateEnter(vIFSMBehaviourController fsmBehaviour)
{ {
if (enableDebug) Debug.Log("[SA_SpawnTurretSmart] Starting intelligent crystal spawn"); if (enableDebug) Debug.Log("[SA_SpawnTurretSmart] Starting intelligent crystal spawn");
FindPlayer(fsmBehaviour); FindPlayer(fsmBehaviour);
var animator = fsmBehaviour.transform.GetComponent<Animator>(); var animator = fsmBehaviour.transform.GetComponent<Animator>();
if (animator != null && !string.IsNullOrEmpty(animatorTrigger)) if (animator != null && !string.IsNullOrEmpty(animatorTrigger))
{ {
animator.SetTrigger(animatorTrigger); animator.SetTrigger(animatorTrigger);
if (enableDebug) Debug.Log($"[SA_SpawnTurretSmart] Set trigger: {animatorTrigger}"); if (enableDebug) Debug.Log($"[SA_SpawnTurretSmart] Set trigger: {animatorTrigger}");
} }
SpawnCrystalSmart(fsmBehaviour); SpawnCrystalSmart(fsmBehaviour);
DEC_CheckCooldown.SetCooldownStatic(fsmBehaviour, "Turret", 12f); DEC_CheckCooldown.SetCooldownStatic(fsmBehaviour, "Turret", 12f);
} }
/// <summary> /// <summary>
/// Finds player transform /// Finds player transform
/// </summary> /// </summary>
private void FindPlayer(vIFSMBehaviourController fsmBehaviour) private void FindPlayer(vIFSMBehaviourController fsmBehaviour)
{ {
GameObject player = GameObject.FindGameObjectWithTag("Player"); GameObject player = GameObject.FindGameObjectWithTag("Player");
if (player != null) if (player != null)
{ {
playerTransform = player.transform; playerTransform = player.transform;
if (enableDebug) Debug.Log("[SA_SpawnTurretSmart] Player found by tag"); if (enableDebug) Debug.Log("[SA_SpawnTurretSmart] Player found by tag");
return; return;
} }
var aiController = fsmBehaviour as Invector.vCharacterController.AI.vIControlAI; var aiController = fsmBehaviour as Invector.vCharacterController.AI.vIControlAI;
if (aiController != null && aiController.currentTarget != null) if (aiController != null && aiController.currentTarget != null)
{ {
playerTransform = aiController.currentTarget.transform; playerTransform = aiController.currentTarget.transform;
if (enableDebug) Debug.Log("[SA_SpawnTurretSmart] Player found through AI target"); if (enableDebug) Debug.Log("[SA_SpawnTurretSmart] Player found through AI target");
return; return;
} }
if (enableDebug) Debug.LogWarning("[SA_SpawnTurretSmart] Player not found!"); if (enableDebug) Debug.LogWarning("[SA_SpawnTurretSmart] Player not found!");
} }
/// <summary> /// <summary>
/// Intelligently spawns crystal in optimal position /// Intelligently spawns crystal in optimal position
/// </summary> /// </summary>
private void SpawnCrystalSmart(vIFSMBehaviourController fsmBehaviour) private void SpawnCrystalSmart(vIFSMBehaviourController fsmBehaviour)
{ {
if (crystalPrefab == null) if (crystalPrefab == null)
{ {
Debug.LogError("[SA_SpawnTurretSmart] Missing crystalPrefab!"); Debug.LogError("[SA_SpawnTurretSmart] Missing crystalPrefab!");
return; return;
} }
Vector3 bestPosition = Vector3.zero; Vector3 bestPosition = Vector3.zero;
bool foundValidPosition = false; bool foundValidPosition = false;
float bestScore = float.MinValue; float bestScore = float.MinValue;
Vector3 bossPos = fsmBehaviour.transform.position; Vector3 bossPos = fsmBehaviour.transform.position;
Vector3 playerDirection = Vector3.zero; Vector3 playerDirection = Vector3.zero;
if (playerTransform != null) if (playerTransform != null)
{ {
playerDirection = (playerTransform.position - bossPos).normalized; playerDirection = (playerTransform.position - bossPos).normalized;
} }
for (int i = 0; i < maxSpawnAttempts; i++) for (int i = 0; i < maxSpawnAttempts; i++)
{ {
float angle = (360f / maxSpawnAttempts) * i + Random.Range(-15f, 15f); float angle = (360f / maxSpawnAttempts) * i + Random.Range(-15f, 15f);
Vector3 direction = new Vector3(Mathf.Cos(angle * Mathf.Deg2Rad), 0, Mathf.Sin(angle * Mathf.Deg2Rad)); Vector3 direction = new Vector3(Mathf.Cos(angle * Mathf.Deg2Rad), 0, Mathf.Sin(angle * Mathf.Deg2Rad));
float distance = Random.Range(minSpawnDistance, maxSpawnDistance); float distance = Random.Range(minSpawnDistance, maxSpawnDistance);
Vector3 testPosition = bossPos + direction * distance; Vector3 testPosition = bossPos + direction * distance;
if (IsPositionValid(testPosition, out Vector3 groundPosition)) if (IsPositionValid(testPosition, out Vector3 groundPosition))
{ {
float score = EvaluatePosition(groundPosition, playerDirection, direction); float score = EvaluatePosition(groundPosition, playerDirection, direction);
if (score > bestScore) if (score > bestScore)
{ {
bestScore = score; bestScore = score;
bestPosition = groundPosition; bestPosition = groundPosition;
foundValidPosition = true; foundValidPosition = true;
} }
} }
} }
if (foundValidPosition) if (foundValidPosition)
{ {
SpawnCrystal(bestPosition, fsmBehaviour); SpawnCrystal(bestPosition, fsmBehaviour);
if (enableDebug) Debug.Log($"[SA_SpawnTurretSmart] Crystal spawned at position: {bestPosition} (score: {bestScore:F2})"); if (enableDebug) Debug.Log($"[SA_SpawnTurretSmart] Crystal spawned at position: {bestPosition} (score: {bestScore:F2})");
} }
else else
{ {
Vector3 fallbackPos = bossPos + fsmBehaviour.transform.forward * minSpawnDistance; Vector3 fallbackPos = bossPos + fsmBehaviour.transform.forward * minSpawnDistance;
SpawnCrystal(fallbackPos, fsmBehaviour); SpawnCrystal(fallbackPos, fsmBehaviour);
if (enableDebug) Debug.LogWarning("[SA_SpawnTurretSmart] Using fallback position"); if (enableDebug) Debug.LogWarning("[SA_SpawnTurretSmart] Using fallback position");
} }
} }
/// <summary> /// <summary>
/// Checks if position is valid (no obstacles, has ground) /// Checks if position is valid (no obstacles, has ground)
/// </summary> /// </summary>
private bool IsPositionValid(Vector3 position, out Vector3 groundPosition) private bool IsPositionValid(Vector3 position, out Vector3 groundPosition)
{ {
groundPosition = position; groundPosition = position;
if (Physics.CheckSphere(position + Vector3.up * obstacleCheckRadius, obstacleCheckRadius, obstacleLayerMask)) if (Physics.CheckSphere(position + Vector3.up * obstacleCheckRadius, obstacleCheckRadius, obstacleLayerMask))
{ {
return false; return false;
} }
Ray groundRay = new Ray(position + Vector3.up * groundCheckHeight, Vector3.down); Ray groundRay = new Ray(position + Vector3.up * groundCheckHeight, Vector3.down);
if (Physics.Raycast(groundRay, out RaycastHit hit, groundCheckHeight + 2f, groundLayerMask)) if (Physics.Raycast(groundRay, out RaycastHit hit, groundCheckHeight + 2f, groundLayerMask))
{ {
groundPosition = hit.point; groundPosition = hit.point;
if (Physics.CheckSphere(groundPosition + Vector3.up * obstacleCheckRadius, obstacleCheckRadius, obstacleLayerMask)) if (Physics.CheckSphere(groundPosition + Vector3.up * obstacleCheckRadius, obstacleCheckRadius, obstacleLayerMask))
{ {
return false; return false;
} }
return true; return true;
} }
return false; return false;
} }
/// <summary> /// <summary>
/// Evaluates position quality (higher score = better position) /// Evaluates position quality (higher score = better position)
/// </summary> /// </summary>
private float EvaluatePosition(Vector3 position, Vector3 playerDirection, Vector3 positionDirection) private float EvaluatePosition(Vector3 position, Vector3 playerDirection, Vector3 positionDirection)
{ {
float score = 0f; float score = 0f;
if (playerTransform != null && playerDirection != Vector3.zero) if (playerTransform != null && playerDirection != Vector3.zero)
{ {
float angleToPlayer = Vector3.Angle(-playerDirection, positionDirection); float angleToPlayer = Vector3.Angle(-playerDirection, positionDirection);
// The smaller the angle (closer to "behind"), the better the score // The smaller the angle (closer to "behind"), the better the score
float backScore = (180f - angleToPlayer) / 180f; float backScore = (180f - angleToPlayer) / 180f;
score += backScore * backPreferenceMultiplier; score += backScore * backPreferenceMultiplier;
} }
Vector3 bossPos = new Vector3(); Vector3 bossPos = new Vector3();
float distance = Vector3.Distance(position, bossPos); float distance = Vector3.Distance(position, bossPos);
float optimalDistance = (minSpawnDistance + maxSpawnDistance) * 0.5f; float optimalDistance = (minSpawnDistance + maxSpawnDistance) * 0.5f;
float distanceScore = 1f - Mathf.Abs(distance - optimalDistance) / maxSpawnDistance; float distanceScore = 1f - Mathf.Abs(distance - optimalDistance) / maxSpawnDistance;
score += distanceScore; score += distanceScore;
score += Random.Range(-0.1f, 0.1f); score += Random.Range(-0.1f, 0.1f);
return score; return score;
} }
/// <summary> /// <summary>
/// Spawns crystal at given position /// Spawns crystal at given position
/// </summary> /// </summary>
private void SpawnCrystal(Vector3 position, vIFSMBehaviourController fsmBehaviour) private void SpawnCrystal(Vector3 position, vIFSMBehaviourController fsmBehaviour)
{ {
Quaternion rotation = Quaternion.identity; Quaternion rotation = Quaternion.identity;
if (playerTransform != null) if (playerTransform != null)
{ {
Vector3 lookDirection = (playerTransform.position - position).normalized; Vector3 lookDirection = (playerTransform.position - position).normalized;
lookDirection.y = 0; lookDirection.y = 0;
if (lookDirection != Vector3.zero) if (lookDirection != Vector3.zero)
{ {
rotation = Quaternion.LookRotation(lookDirection); rotation = Quaternion.LookRotation(lookDirection);
} }
} }
spawnedCrystal = LeanPool.Spawn(crystalPrefab, position, rotation); spawnedCrystal = LeanPool.Spawn(crystalPrefab, position, rotation);
var shooterAI = spawnedCrystal.GetComponent<CrystalShooterAI>(); var shooterAI = spawnedCrystal.GetComponent<CrystalShooterAI>();
if (shooterAI == null) if (shooterAI == null)
{ {
Debug.LogError("[SA_SpawnTurretSmart] Crystal prefab doesn't have CrystalShooterAI component!"); Debug.LogError("[SA_SpawnTurretSmart] Crystal prefab doesn't have CrystalShooterAI component!");
} }
else else
{ {
if (playerTransform != null) if (playerTransform != null)
{ {
shooterAI.SetTarget(playerTransform); shooterAI.SetTarget(playerTransform);
} }
} }
} }
/// <summary> /// <summary>
/// Draws gizmos in Scene View for debugging /// Draws gizmos in Scene View for debugging
/// </summary> /// </summary>
private void OnDrawGizmosSelected() private void OnDrawGizmosSelected()
{ {
if (!showGizmos) return; if (!showGizmos) return;
Vector3 pos = new Vector3(); Vector3 pos = new Vector3();
// Spawn ring // Spawn ring
Gizmos.color = Color.green; Gizmos.color = Color.green;
DrawWireCircle(pos, minSpawnDistance); DrawWireCircle(pos, minSpawnDistance);
Gizmos.color = Color.red; Gizmos.color = Color.red;
DrawWireCircle(pos, maxSpawnDistance); DrawWireCircle(pos, maxSpawnDistance);
// Obstacle check radius // Obstacle check radius
Gizmos.color = Color.yellow; Gizmos.color = Color.yellow;
Gizmos.DrawWireSphere(pos + Vector3.up * obstacleCheckRadius, obstacleCheckRadius); Gizmos.DrawWireSphere(pos + Vector3.up * obstacleCheckRadius, obstacleCheckRadius);
} }
/// <summary> /// <summary>
/// Helper method for drawing circles /// Helper method for drawing circles
/// </summary> /// </summary>
private void DrawWireCircle(Vector3 center, float radius) private void DrawWireCircle(Vector3 center, float radius)
{ {
int segments = 32; int segments = 32;
float angle = 0f; float angle = 0f;
Vector3 prevPoint = center + new Vector3(radius, 0, 0); Vector3 prevPoint = center + new Vector3(radius, 0, 0);
for (int i = 1; i <= segments; i++) for (int i = 1; i <= segments; i++)
{ {
angle = (float)i / segments * 360f * Mathf.Deg2Rad; angle = (float)i / segments * 360f * Mathf.Deg2Rad;
Vector3 newPoint = center + new Vector3(Mathf.Cos(angle) * radius, 0, Mathf.Sin(angle) * radius); Vector3 newPoint = center + new Vector3(Mathf.Cos(angle) * radius, 0, Mathf.Sin(angle) * radius);
Gizmos.DrawLine(prevPoint, newPoint); Gizmos.DrawLine(prevPoint, newPoint);
prevPoint = newPoint; prevPoint = newPoint;
} }
} }
} }
} }

View File

@@ -1,38 +1,38 @@
using UnityEngine; using UnityEngine;
public class ShieldScaleUp : MonoBehaviour public class ShieldScaleUp : MonoBehaviour
{ {
[Tooltip("Time it takes to fully grow the shield")] [Tooltip("Time it takes to fully grow the shield")]
public float growDuration = 0.5f; public float growDuration = 0.5f;
[Tooltip("Curve to control growth speed over time")] [Tooltip("Curve to control growth speed over time")]
public AnimationCurve scaleCurve = AnimationCurve.EaseInOut(0, 0, 1, 1); public AnimationCurve scaleCurve = AnimationCurve.EaseInOut(0, 0, 1, 1);
private float timer = 0f; private float timer = 0f;
private Vector3 targetScale; private Vector3 targetScale;
private void Awake() private void Awake()
{ {
targetScale = transform.localScale; targetScale = transform.localScale;
transform.localScale = Vector3.zero; transform.localScale = Vector3.zero;
} }
private void OnEnable() private void OnEnable()
{ {
timer = 0f; timer = 0f;
transform.localScale = Vector3.zero; transform.localScale = Vector3.zero;
} }
private void Update() private void Update()
{ {
if (timer < growDuration) if (timer < growDuration)
{ {
timer += Time.deltaTime; timer += Time.deltaTime;
float t = Mathf.Clamp01(timer / growDuration); float t = Mathf.Clamp01(timer / growDuration);
float scaleValue = scaleCurve.Evaluate(t); float scaleValue = scaleCurve.Evaluate(t);
transform.localScale = targetScale * scaleValue; transform.localScale = targetScale * scaleValue;
} }
} }
} }

File diff suppressed because it is too large Load Diff