From eaa10c4c11da9563ed88234bfca08d973a9c287f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Szymon=20Mi=C5=9B?= <>
Date: Mon, 11 Aug 2025 13:34:13 +0200
Subject: [PATCH] Demon boss part (1/3) Invector FSM nodes
---
Assets/AI/Demon.meta | 8 +
Assets/AI/Demon/DEC_CheckCooldown.cs | 205 ++++++++++++
Assets/AI/Demon/DEC_CheckCooldown.cs.meta | 2 +
Assets/AI/Demon/DEC_TargetClearSky.cs | 105 ++++++
Assets/AI/Demon/DEC_TargetClearSky.cs.meta | 2 +
Assets/AI/Demon/DEC_TargetFar.cs | 58 ++++
Assets/AI/Demon/DEC_TargetFar.cs.meta | 2 +
Assets/AI/Demon/SA_CallMeteor.cs | 352 ++++++++++++++++++++
Assets/AI/Demon/SA_CallMeteor.cs.meta | 2 +
Assets/AI/Demon/SA_CastShield.cs | 185 ++++++++++
Assets/AI/Demon/SA_CastShield.cs.meta | 2 +
Assets/AI/Demon/SA_SpawnTurretSmart.cs | 298 +++++++++++++++++
Assets/AI/Demon/SA_SpawnTurretSmart.cs.meta | 2 +
13 files changed, 1223 insertions(+)
create mode 100644 Assets/AI/Demon.meta
create mode 100644 Assets/AI/Demon/DEC_CheckCooldown.cs
create mode 100644 Assets/AI/Demon/DEC_CheckCooldown.cs.meta
create mode 100644 Assets/AI/Demon/DEC_TargetClearSky.cs
create mode 100644 Assets/AI/Demon/DEC_TargetClearSky.cs.meta
create mode 100644 Assets/AI/Demon/DEC_TargetFar.cs
create mode 100644 Assets/AI/Demon/DEC_TargetFar.cs.meta
create mode 100644 Assets/AI/Demon/SA_CallMeteor.cs
create mode 100644 Assets/AI/Demon/SA_CallMeteor.cs.meta
create mode 100644 Assets/AI/Demon/SA_CastShield.cs
create mode 100644 Assets/AI/Demon/SA_CastShield.cs.meta
create mode 100644 Assets/AI/Demon/SA_SpawnTurretSmart.cs
create mode 100644 Assets/AI/Demon/SA_SpawnTurretSmart.cs.meta
diff --git a/Assets/AI/Demon.meta b/Assets/AI/Demon.meta
new file mode 100644
index 000000000..ff0c21357
--- /dev/null
+++ b/Assets/AI/Demon.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 67c253388a8e1594783ff96204c292f8
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/AI/Demon/DEC_CheckCooldown.cs b/Assets/AI/Demon/DEC_CheckCooldown.cs
new file mode 100644
index 000000000..bfa3ad2e3
--- /dev/null
+++ b/Assets/AI/Demon/DEC_CheckCooldown.cs
@@ -0,0 +1,205 @@
+using Invector.vCharacterController.AI.FSMBehaviour;
+using UnityEngine;
+
+namespace DemonBoss.Magic
+{
+ ///
+ /// Decision node checking cooldown for different boss abilities
+ /// Stores Time.time in FSM timers and checks if required cooldown time has passed
+ ///
+ [CreateAssetMenu(menuName = "Invector/FSM/Decisions/DemonBoss/Check Cooldown")]
+ public class DEC_CheckCooldown : vStateDecision
+ {
+ public override string categoryName => "DemonBoss/Magic";
+ public override string defaultName => "Check Cooldown";
+
+ [Header("Cooldown Configuration")]
+ [Tooltip("Unique key for this ability (e.g. 'Shield', 'Turret', 'Meteor')")]
+ public string cooldownKey = "Shield";
+
+ [Tooltip("Cooldown time in seconds")]
+ public float cooldownTime = 10f;
+
+ [Tooltip("Whether ability should be available immediately at fight start")]
+ public bool availableAtStart = true;
+
+ [Header("Debug")]
+ [Tooltip("Enable debug logging")]
+ public bool enableDebug = false;
+
+ ///
+ /// Main method checking if ability is available
+ ///
+ /// True if cooldown has passed and ability can be used
+ public override bool Decide(vIFSMBehaviourController fsmBehaviour)
+ {
+ if (fsmBehaviour == null)
+ {
+ if (enableDebug) Debug.LogWarning($"[DEC_CheckCooldown] No FSM Behaviour for key: {cooldownKey}");
+ return false;
+ }
+
+ string timerKey = "cooldown_" + cooldownKey;
+
+ if (!fsmBehaviour.HasTimer(timerKey))
+ {
+ if (availableAtStart)
+ {
+ if (enableDebug) Debug.Log($"[DEC_CheckCooldown] First use for {cooldownKey} - available");
+ return true;
+ }
+ else
+ {
+ SetCooldown(fsmBehaviour, cooldownTime);
+ if (enableDebug) Debug.Log($"[DEC_CheckCooldown] First use for {cooldownKey} - setting cooldown");
+ return false;
+ }
+ }
+
+ float lastUsedTime = fsmBehaviour.GetTimer(timerKey);
+ float timeSinceLastUse = Time.time - lastUsedTime;
+
+ bool isAvailable = timeSinceLastUse >= cooldownTime;
+
+ if (enableDebug)
+ {
+ if (isAvailable)
+ {
+ Debug.Log($"[DEC_CheckCooldown] {cooldownKey} available - {timeSinceLastUse:F1}s passed of required {cooldownTime}s");
+ }
+ else
+ {
+ float remainingTime = cooldownTime - timeSinceLastUse;
+ Debug.Log($"[DEC_CheckCooldown] {cooldownKey} on cooldown - {remainingTime:F1}s remaining");
+ }
+ }
+
+ return isAvailable;
+ }
+
+ ///
+ /// Sets cooldown for ability - call this after using ability
+ ///
+ public void SetCooldown(vIFSMBehaviourController fsmBehaviour)
+ {
+ SetCooldown(fsmBehaviour, cooldownTime);
+ }
+
+ ///
+ /// Sets cooldown with custom time
+ ///
+ /// FSM behaviour reference
+ /// Custom cooldown time
+ public void SetCooldown(vIFSMBehaviourController fsmBehaviour, float customCooldownTime)
+ {
+ if (fsmBehaviour == null)
+ {
+ if (enableDebug) Debug.LogWarning($"[DEC_CheckCooldown] Cannot set cooldown - no FSM Behaviour");
+ return;
+ }
+
+ string timerKey = "cooldown_" + cooldownKey;
+
+ fsmBehaviour.SetTimer(timerKey, Time.time);
+
+ if (enableDebug)
+ {
+ Debug.Log($"[DEC_CheckCooldown] Set cooldown for {cooldownKey}: {customCooldownTime}s");
+ }
+ }
+
+ ///
+ /// Resets cooldown - ability becomes immediately available
+ ///
+ public void ResetCooldown(vIFSMBehaviourController fsmBehaviour)
+ {
+ if (fsmBehaviour == null)
+ {
+ if (enableDebug) Debug.LogWarning($"[DEC_CheckCooldown] Cannot reset cooldown - no FSM Behaviour");
+ return;
+ }
+
+ string timerKey = "cooldown_" + cooldownKey;
+
+ float pastTime = Time.time - cooldownTime - 1f;
+ fsmBehaviour.SetTimer(timerKey, pastTime);
+
+ if (enableDebug) Debug.Log($"[DEC_CheckCooldown] Reset cooldown for {cooldownKey}");
+ }
+
+ ///
+ /// Returns remaining cooldown time in seconds
+ ///
+ /// Remaining cooldown time (0 if available)
+ public float GetRemainingCooldown(vIFSMBehaviourController fsmBehaviour)
+ {
+ if (fsmBehaviour == null) return 0f;
+
+ string timerKey = "cooldown_" + cooldownKey;
+
+ if (!fsmBehaviour.HasTimer(timerKey))
+ {
+ return availableAtStart ? 0f : cooldownTime;
+ }
+
+ float lastUsedTime = fsmBehaviour.GetTimer(timerKey);
+ float timeSinceLastUse = Time.time - lastUsedTime;
+
+ return Mathf.Max(0f, cooldownTime - timeSinceLastUse);
+ }
+
+ ///
+ /// Checks if ability is available without running main Decision logic
+ ///
+ /// True if ability is available
+ public bool IsAvailable(vIFSMBehaviourController fsmBehaviour)
+ {
+ return GetRemainingCooldown(fsmBehaviour) <= 0f;
+ }
+
+ ///
+ /// Returns cooldown progress percentage (0-1)
+ ///
+ /// Progress percentage: 0 = just used, 1 = fully recharged
+ public float GetCooldownProgress(vIFSMBehaviourController fsmBehaviour)
+ {
+ if (cooldownTime <= 0f) return 1f;
+
+ float remainingTime = GetRemainingCooldown(fsmBehaviour);
+ return 1f - (remainingTime / cooldownTime);
+ }
+
+ ///
+ /// Helper method for setting cooldown from external code (e.g. from StateAction)
+ ///
+ /// FSM reference
+ /// Ability key
+ /// Cooldown time
+ public static void SetCooldownStatic(vIFSMBehaviourController fsmBehaviour, string key, float cooldown)
+ {
+ if (fsmBehaviour == null) return;
+
+ string timerKey = "cooldown_" + key;
+ fsmBehaviour.SetTimer(timerKey, Time.time);
+ }
+
+ ///
+ /// Helper method for checking cooldown from external code
+ ///
+ /// FSM reference
+ /// Ability key
+ /// Cooldown time
+ /// True if available
+ public static bool CheckCooldownStatic(vIFSMBehaviourController fsmBehaviour, string key, float cooldown)
+ {
+ if (fsmBehaviour == null) return false;
+
+ string timerKey = "cooldown_" + key;
+
+ if (!fsmBehaviour.HasTimer(timerKey)) return true;
+
+ float lastUsedTime = fsmBehaviour.GetTimer(timerKey);
+ return (Time.time - lastUsedTime) >= cooldown;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Assets/AI/Demon/DEC_CheckCooldown.cs.meta b/Assets/AI/Demon/DEC_CheckCooldown.cs.meta
new file mode 100644
index 000000000..191fafb3e
--- /dev/null
+++ b/Assets/AI/Demon/DEC_CheckCooldown.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: af82d1d082ce4b9478d420f8ca1e72c2
\ No newline at end of file
diff --git a/Assets/AI/Demon/DEC_TargetClearSky.cs b/Assets/AI/Demon/DEC_TargetClearSky.cs
new file mode 100644
index 000000000..95ab638f3
--- /dev/null
+++ b/Assets/AI/Demon/DEC_TargetClearSky.cs
@@ -0,0 +1,105 @@
+using Invector.vCharacterController.AI.FSMBehaviour;
+using UnityEngine;
+
+namespace DemonBoss.Magic
+{
+ ///
+ /// Decision checking if target has clear sky above (for meteor)
+ ///
+ [CreateAssetMenu(menuName = "Invector/FSM/Decisions/DemonBoss/Target Clear Sky")]
+ public class DEC_TargetClearSky : vStateDecision
+ {
+ public override string categoryName => "DemonBoss/Magic";
+ public override string defaultName => "Target Clear Sky";
+
+ [Header("Sky Check Configuration")]
+ [Tooltip("Check height above target")]
+ public float checkHeight = 25f;
+
+ [Tooltip("Obstacle check radius")]
+ public float checkRadius = 2f;
+
+ [Tooltip("Obstacle layer mask")]
+ public LayerMask obstacleLayerMask = -1;
+
+ [Header("Debug")]
+ [Tooltip("Enable debug logging")]
+ public bool enableDebug = false;
+
+ [Tooltip("Show gizmos in Scene View")]
+ public bool showGizmos = true;
+
+ public override bool Decide(vIFSMBehaviourController fsmBehaviour)
+ {
+ Transform target = GetTarget(fsmBehaviour);
+ if (target == null)
+ {
+ if (enableDebug) Debug.Log("[DEC_TargetClearSky] No target found");
+ return false;
+ }
+
+ bool isClear = IsSkyClear(target.position);
+
+ if (enableDebug)
+ {
+ Debug.Log($"[DEC_TargetClearSky] Sky above target: {(isClear ? "CLEAR" : "BLOCKED")}");
+ }
+
+ return isClear;
+ }
+
+ private bool IsSkyClear(Vector3 targetPosition)
+ {
+ Vector3 skyCheckPoint = targetPosition + Vector3.up * checkHeight;
+
+ if (Physics.CheckSphere(skyCheckPoint, checkRadius, obstacleLayerMask))
+ {
+ return false;
+ }
+
+ Ray skyRay = new Ray(skyCheckPoint, Vector3.down);
+ RaycastHit[] hits = Physics.RaycastAll(skyRay, checkHeight, obstacleLayerMask);
+
+ foreach (var hit in hits)
+ {
+ if (hit.point.y <= targetPosition.y + 0.5f) continue;
+
+ return false;
+ }
+
+ return true;
+ }
+
+ private Transform GetTarget(vIFSMBehaviourController fsmBehaviour)
+ {
+ // Try through AI controller
+ var aiController = fsmBehaviour as Invector.vCharacterController.AI.vIControlAI;
+ if (aiController != null && aiController.currentTarget != null)
+ return aiController.currentTarget.transform;
+
+ // Fallback - find player
+ GameObject player = GameObject.FindGameObjectWithTag("Player");
+ return player?.transform;
+ }
+
+ private void OnDrawGizmosSelected()
+ {
+ if (!showGizmos) return;
+
+ GameObject player = GameObject.FindGameObjectWithTag("Player");
+ if (player == null) return;
+
+ Vector3 targetPos = player.transform.position;
+ Vector3 skyCheckPoint = targetPos + Vector3.up * checkHeight;
+
+ Gizmos.color = Color.cyan;
+ Gizmos.DrawWireSphere(skyCheckPoint, checkRadius);
+
+ Gizmos.color = Color.yellow;
+ Gizmos.DrawLine(targetPos, skyCheckPoint);
+
+ Gizmos.color = Color.red;
+ Gizmos.DrawWireSphere(targetPos, 0.5f);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Assets/AI/Demon/DEC_TargetClearSky.cs.meta b/Assets/AI/Demon/DEC_TargetClearSky.cs.meta
new file mode 100644
index 000000000..9ca0aecfa
--- /dev/null
+++ b/Assets/AI/Demon/DEC_TargetClearSky.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: a60b21a6b7b97264b856431f5eb253b1
\ No newline at end of file
diff --git a/Assets/AI/Demon/DEC_TargetFar.cs b/Assets/AI/Demon/DEC_TargetFar.cs
new file mode 100644
index 000000000..f73103635
--- /dev/null
+++ b/Assets/AI/Demon/DEC_TargetFar.cs
@@ -0,0 +1,58 @@
+using Invector.vCharacterController.AI.FSMBehaviour;
+using UnityEngine;
+
+namespace DemonBoss.Magic
+{
+ ///
+ /// Decision checking if target is far away (for Turret ability)
+ ///
+ [CreateAssetMenu(menuName = "Invector/FSM/Decisions/DemonBoss/Target Far")]
+ public class DEC_TargetFar : vStateDecision
+ {
+ public override string categoryName => "DemonBoss/Magic";
+ public override string defaultName => "Target Far";
+
+ [Header("Distance Configuration")]
+ [Tooltip("Minimum distance for target to be considered far")]
+ public float minDistance = 8f;
+
+ [Tooltip("Maximum distance for checking")]
+ public float maxDistance = 30f;
+
+ [Header("Debug")]
+ [Tooltip("Enable debug logging")]
+ public bool enableDebug = false;
+
+ public override bool Decide(vIFSMBehaviourController fsmBehaviour)
+ {
+ Transform target = GetTarget(fsmBehaviour);
+ if (target == null)
+ {
+ if (enableDebug) Debug.Log("[DEC_TargetFar] No target found");
+ return false;
+ }
+
+ float distance = Vector3.Distance(fsmBehaviour.transform.position, target.position);
+ bool isFar = distance >= minDistance && distance <= maxDistance;
+
+ if (enableDebug)
+ {
+ Debug.Log($"[DEC_TargetFar] Distance to target: {distance:F1}m - {(isFar ? "FAR" : "CLOSE")}");
+ }
+
+ return isFar;
+ }
+
+ private Transform GetTarget(vIFSMBehaviourController fsmBehaviour)
+ {
+ // Try through AI controller
+ var aiController = fsmBehaviour as Invector.vCharacterController.AI.vIControlAI;
+ if (aiController != null && aiController.currentTarget != null)
+ return aiController.currentTarget.transform;
+
+ // Fallback - find player
+ GameObject player = GameObject.FindGameObjectWithTag("Player");
+ return player?.transform;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Assets/AI/Demon/DEC_TargetFar.cs.meta b/Assets/AI/Demon/DEC_TargetFar.cs.meta
new file mode 100644
index 000000000..df2d95120
--- /dev/null
+++ b/Assets/AI/Demon/DEC_TargetFar.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 6df4a5087a0930d479908e8416bc8a2a
\ No newline at end of file
diff --git a/Assets/AI/Demon/SA_CallMeteor.cs b/Assets/AI/Demon/SA_CallMeteor.cs
new file mode 100644
index 000000000..ff9987890
--- /dev/null
+++ b/Assets/AI/Demon/SA_CallMeteor.cs
@@ -0,0 +1,352 @@
+using Invector.vCharacterController.AI.FSMBehaviour;
+using Lean.Pool;
+using System.Collections;
+using UnityEngine;
+
+namespace DemonBoss.Magic
+{
+ ///
+ /// 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
+ ///
+ [CreateAssetMenu(menuName = "Invector/FSM/Actions/DemonBoss/Call Meteor")]
+ public class SA_CallMeteor : vStateAction
+ {
+ public override string categoryName => "DemonBoss/Magic";
+ public override string defaultName => "Call Meteor";
+
+ [Header("Meteor Configuration")]
+ [Tooltip("Meteor rock prefab")]
+ public GameObject rockPrefab;
+
+ [Tooltip("Decal prefab showing impact location")]
+ public GameObject decalPrefab;
+
+ [Tooltip("Height from which meteor falls")]
+ public float meteorHeight = 30f;
+
+ [Tooltip("Delay between placing decal and spawning meteor")]
+ public float castDelay = 1.5f;
+
+ [Tooltip("Obstacle check radius above target")]
+ public float obstacleCheckRadius = 2f;
+
+ [Tooltip("Layer mask for obstacles blocking meteor")]
+ public LayerMask obstacleLayerMask = -1;
+
+ [Tooltip("Layer mask for ground")]
+ public LayerMask groundLayerMask = -1;
+
+ [Tooltip("Animator trigger name for meteor summoning animation")]
+ public string animatorTrigger = "CastMeteor";
+
+ [Header("Targeting")]
+ [Tooltip("Maximum raycast distance to find ground")]
+ public float maxGroundDistance = 100f;
+
+ [Tooltip("Height above ground for obstacle checking")]
+ public float airCheckHeight = 5f;
+
+ [Header("Debug")]
+ [Tooltip("Enable debug logging")]
+ public bool enableDebug = false;
+
+ [Tooltip("Show gizmos in Scene View")]
+ public bool showGizmos = true;
+
+ private GameObject spawnedDecal;
+
+ private Transform playerTransform;
+ private Vector3 targetPosition;
+ private bool meteorCasting = false;
+ private MonoBehaviour coroutineRunner;
+
+ ///
+ /// Main action execution method called by FSM
+ ///
+ public override void DoAction(vIFSMBehaviourController fsmBehaviour, vFSMComponentExecutionType executionType = vFSMComponentExecutionType.OnStateUpdate)
+ {
+ if (executionType == vFSMComponentExecutionType.OnStateEnter)
+ {
+ OnStateEnter(fsmBehaviour);
+ }
+ else if (executionType == vFSMComponentExecutionType.OnStateExit)
+ {
+ OnStateExit(fsmBehaviour);
+ }
+ }
+
+ ///
+ /// Called when entering state - starts meteor casting
+ ///
+ private void OnStateEnter(vIFSMBehaviourController fsmBehaviour)
+ {
+ if (enableDebug) Debug.Log("[SA_CallMeteor] Starting meteor casting");
+
+ FindPlayer(fsmBehaviour);
+
+ var animator = fsmBehaviour.transform.GetComponent();
+ if (animator != null && !string.IsNullOrEmpty(animatorTrigger))
+ {
+ animator.SetTrigger(animatorTrigger);
+ if (enableDebug) Debug.Log($"[SA_CallMeteor] Set trigger: {animatorTrigger}");
+ }
+
+ StartMeteorCast(fsmBehaviour);
+ }
+
+ ///
+ /// Called when exiting state - cleanup
+ ///
+ private void OnStateExit(vIFSMBehaviourController fsmBehaviour)
+ {
+ if (enableDebug) Debug.Log("[SA_CallMeteor] Exiting meteor state");
+ meteorCasting = false;
+ }
+
+ ///
+ /// Finds player transform
+ ///
+ private void FindPlayer(vIFSMBehaviourController fsmBehaviour)
+ {
+ GameObject player = GameObject.FindGameObjectWithTag("Player");
+ if (player != null)
+ {
+ playerTransform = player.transform;
+ if (enableDebug) Debug.Log("[SA_CallMeteor] Player found by tag");
+ return;
+ }
+
+ var aiController = fsmBehaviour as Invector.vCharacterController.AI.vIControlAI;
+ if (aiController != null && aiController.currentTarget != null)
+ {
+ playerTransform = aiController.currentTarget.transform;
+ if (enableDebug) Debug.Log("[SA_CallMeteor] Player found through AI target");
+ return;
+ }
+
+ if (enableDebug) Debug.LogWarning("[SA_CallMeteor] Player not found!");
+ }
+
+ ///
+ /// Starts meteor casting process
+ ///
+ private void StartMeteorCast(vIFSMBehaviourController fsmBehaviour)
+ {
+ if (playerTransform == null)
+ {
+ if (enableDebug) Debug.LogWarning("[SA_CallMeteor] No target - finishing state");
+ return;
+ }
+
+ if (FindGroundUnderPlayer(out Vector3 groundPos))
+ {
+ targetPosition = groundPos;
+
+ SpawnDecal(targetPosition);
+
+ coroutineRunner = fsmBehaviour.transform.GetComponent();
+ if (coroutineRunner != null)
+ {
+ coroutineRunner.StartCoroutine(MeteorCastCoroutine(fsmBehaviour));
+ }
+ else
+ {
+ CastMeteorImmediate();
+ }
+
+ meteorCasting = true;
+
+ if (enableDebug) Debug.Log($"[SA_CallMeteor] Meteor target: {targetPosition}");
+ }
+ else
+ {
+ if (enableDebug) Debug.LogWarning("[SA_CallMeteor] Cannot find ground under player");
+ }
+ }
+
+ ///
+ /// Finds ground under player using raycast
+ ///
+ private bool FindGroundUnderPlayer(out Vector3 groundPosition)
+ {
+ groundPosition = Vector3.zero;
+
+ Vector3 playerPos = playerTransform.position;
+ Vector3 rayStart = playerPos + Vector3.up * 5f;
+
+ Ray groundRay = new Ray(rayStart, Vector3.down);
+ if (Physics.Raycast(groundRay, out RaycastHit hit, maxGroundDistance, groundLayerMask))
+ {
+ groundPosition = hit.point;
+ return true;
+ }
+
+ groundPosition = playerPos;
+ return true;
+ }
+
+ ///
+ /// Spawns decal showing meteor impact location
+ ///
+ private void SpawnDecal(Vector3 position)
+ {
+ if (decalPrefab == null)
+ {
+ if (enableDebug) Debug.LogWarning("[SA_CallMeteor] Missing decal prefab");
+ return;
+ }
+
+ Quaternion decalRotation = Quaternion.identity;
+
+ Ray surfaceRay = new Ray(position + Vector3.up * 2f, Vector3.down);
+ if (Physics.Raycast(surfaceRay, out RaycastHit hit, 5f, groundLayerMask))
+ {
+ decalRotation = Quaternion.FromToRotation(Vector3.up, hit.normal);
+ }
+
+ spawnedDecal = LeanPool.Spawn(decalPrefab, position, decalRotation);
+
+ if (enableDebug) Debug.Log($"[SA_CallMeteor] Decal spawned at: {position}");
+ }
+
+ ///
+ /// Coroutine handling meteor casting process with delay
+ ///
+ private IEnumerator MeteorCastCoroutine(vIFSMBehaviourController fsmBehaviour)
+ {
+ yield return new WaitForSeconds(castDelay);
+
+ if (enableDebug) Debug.Log("[SA_CallMeteor] Checking if path is clear for meteor");
+
+ if (IsPathClearForMeteor(targetPosition))
+ {
+ SpawnMeteor(targetPosition);
+ if (enableDebug) Debug.Log("[SA_CallMeteor] Meteor spawned");
+ }
+ else
+ {
+ if (enableDebug) Debug.Log("[SA_CallMeteor] Path blocked - meteor not spawned");
+ }
+
+ CleanupDecal();
+
+ DEC_CheckCooldown.SetCooldownStatic(fsmBehaviour, "Meteor", 20f);
+
+ meteorCasting = false;
+ }
+
+ ///
+ /// Immediate meteor cast without coroutine (fallback)
+ ///
+ private void CastMeteorImmediate()
+ {
+ if (IsPathClearForMeteor(targetPosition))
+ {
+ SpawnMeteor(targetPosition);
+ }
+ CleanupDecal();
+ }
+
+ ///
+ /// Checks if path above target is clear for meteor
+ ///
+ private bool IsPathClearForMeteor(Vector3 targetPos)
+ {
+ Vector3 checkStart = targetPos + Vector3.up * airCheckHeight;
+ Vector3 checkEnd = targetPos + Vector3.up * meteorHeight;
+
+ if (Physics.CheckSphere(checkStart, obstacleCheckRadius, obstacleLayerMask))
+ {
+ return false;
+ }
+
+ Ray pathRay = new Ray(checkEnd, Vector3.down);
+ if (Physics.SphereCast(pathRay, obstacleCheckRadius, meteorHeight - airCheckHeight, obstacleLayerMask))
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ ///
+ /// Spawns meteor in air above target
+ ///
+ private void SpawnMeteor(Vector3 targetPos)
+ {
+ if (rockPrefab == null)
+ {
+ Debug.LogError("[SA_CallMeteor] Missing meteor prefab!");
+ return;
+ }
+
+ Vector3 meteorSpawnPos = targetPos + Vector3.up * meteorHeight;
+
+ GameObject meteor = LeanPool.Spawn(rockPrefab, meteorSpawnPos, Quaternion.identity);
+
+ Rigidbody meteorRb = meteor.GetComponent();
+ if (meteorRb != null)
+ {
+ meteorRb.linearVelocity = Vector3.down * 5f;
+ meteorRb.angularVelocity = Random.insideUnitSphere * 2f;
+ }
+
+ if (enableDebug) Debug.Log($"[SA_CallMeteor] Meteor spawned at height: {meteorHeight}m");
+ }
+
+ ///
+ /// Removes decal from map
+ ///
+ private void CleanupDecal()
+ {
+ if (spawnedDecal != null)
+ {
+ LeanPool.Despawn(spawnedDecal);
+ spawnedDecal = null;
+ if (enableDebug) Debug.Log("[SA_CallMeteor] Decal removed");
+ }
+ }
+
+ ///
+ /// Checks if meteor is currently being cast
+ ///
+ public bool IsCasting()
+ {
+ return meteorCasting;
+ }
+
+ ///
+ /// Returns meteor target position
+ ///
+ public Vector3 GetTargetPosition()
+ {
+ return targetPosition;
+ }
+
+ ///
+ /// Draws gizmos in Scene View for debugging
+ ///
+ private void OnDrawGizmosSelected()
+ {
+ if (!showGizmos) return;
+
+ if (meteorCasting && targetPosition != Vector3.zero)
+ {
+ Gizmos.color = Color.red;
+ Gizmos.DrawWireSphere(targetPosition, 1f);
+
+ Vector3 spawnPos = targetPosition + Vector3.up * meteorHeight;
+ Gizmos.color = Color.yellow;
+ Gizmos.DrawWireSphere(spawnPos, 0.5f);
+
+ Gizmos.color = Color.yellow;
+ Gizmos.DrawLine(spawnPos, targetPosition);
+
+ Vector3 checkPos = targetPosition + Vector3.up * airCheckHeight;
+ Gizmos.color = Color.blue;
+ Gizmos.DrawWireSphere(checkPos, obstacleCheckRadius);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Assets/AI/Demon/SA_CallMeteor.cs.meta b/Assets/AI/Demon/SA_CallMeteor.cs.meta
new file mode 100644
index 000000000..9ffe57f46
--- /dev/null
+++ b/Assets/AI/Demon/SA_CallMeteor.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 6704e78bcf7b30a43a2d06d91e87d694
\ No newline at end of file
diff --git a/Assets/AI/Demon/SA_CastShield.cs b/Assets/AI/Demon/SA_CastShield.cs
new file mode 100644
index 000000000..7cdfb80be
--- /dev/null
+++ b/Assets/AI/Demon/SA_CastShield.cs
@@ -0,0 +1,185 @@
+using Invector.vCharacterController.AI.FSMBehaviour;
+using Lean.Pool;
+using UnityEngine;
+
+namespace DemonBoss.Magic
+{
+ ///
+ /// StateAction for Magic Shield spell - boss casts magical shield for 5 seconds
+ /// During casting boss stands still, after completion returns to Combat
+ ///
+ [CreateAssetMenu(menuName = "Invector/FSM/Decisions/DemonBoss/Cast Shield")]
+ public class SA_CastShield : vStateAction
+ {
+ public override string categoryName => "DemonBoss/Magic";
+ public override string defaultName => "Cast Shield";
+
+ [Header("Shield Configuration")]
+ [Tooltip("Prefab with magical shield particle effect")]
+ public GameObject shieldFXPrefab;
+
+ [Tooltip("Transform where shield should appear (usually boss center)")]
+ public Transform shieldSpawnPoint;
+
+ [Tooltip("Shield duration in seconds")]
+ public float shieldDuration = 5f;
+
+ [Tooltip("Animator trigger name for shield casting animation")]
+ public string animatorTrigger = "CastShield";
+
+ [Header("Debug")]
+ [Tooltip("Enable debug logging")]
+ public bool enableDebug = false;
+
+ private GameObject spawnedShield;
+
+ private float shieldStartTime;
+ private bool shieldActive = false;
+
+ ///
+ /// Main action execution method called by FSM
+ ///
+ public override void DoAction(vIFSMBehaviourController fsmBehaviour, vFSMComponentExecutionType executionType = vFSMComponentExecutionType.OnStateUpdate)
+ {
+ if (executionType == vFSMComponentExecutionType.OnStateEnter)
+ {
+ OnStateEnter(fsmBehaviour);
+ }
+ else if (executionType == vFSMComponentExecutionType.OnStateUpdate)
+ {
+ OnStateUpdate(fsmBehaviour);
+ }
+ else if (executionType == vFSMComponentExecutionType.OnStateExit)
+ {
+ OnStateExit(fsmBehaviour);
+ }
+ }
+
+ ///
+ /// Called when entering state - starts shield casting
+ ///
+ private void OnStateEnter(vIFSMBehaviourController fsmBehaviour)
+ {
+ if (enableDebug) Debug.Log("[SA_CastShield] Entering shield casting state");
+
+ var aiController = fsmBehaviour as Invector.vCharacterController.AI.vIControlAI;
+ if (aiController != null)
+ {
+ aiController.Stop();
+ if (enableDebug) Debug.Log("[SA_CastShield] AI stopped");
+ }
+
+ var animator = fsmBehaviour.transform.GetComponent();
+ if (animator != null && !string.IsNullOrEmpty(animatorTrigger))
+ {
+ animator.SetTrigger(animatorTrigger);
+ if (enableDebug) Debug.Log($"[SA_CastShield] Set trigger: {animatorTrigger}");
+ }
+
+ SpawnShieldEffect(fsmBehaviour);
+
+ shieldStartTime = Time.time;
+ shieldActive = true;
+ }
+
+ ///
+ /// Called every frame during state duration
+ ///
+ private void OnStateUpdate(vIFSMBehaviourController fsmBehaviour)
+ {
+ if (shieldActive && Time.time - shieldStartTime >= shieldDuration)
+ {
+ if (enableDebug) Debug.Log("[SA_CastShield] Shield time passed, finishing state");
+ FinishShield(fsmBehaviour);
+ }
+ }
+
+ ///
+ /// Called when exiting state - cleanup
+ ///
+ private void OnStateExit(vIFSMBehaviourController fsmBehaviour)
+ {
+ if (enableDebug) Debug.Log("[SA_CastShield] Exiting shield state");
+
+ if (shieldActive)
+ {
+ CleanupShield();
+ }
+
+ var aiController = fsmBehaviour as Invector.vCharacterController.AI.vIControlAI;
+ if (aiController != null)
+ {
+ if (enableDebug) Debug.Log("[SA_CastShield] AI resumed");
+ }
+ }
+
+ ///
+ /// Spawns magical shield particle effect
+ ///
+ private void SpawnShieldEffect(vIFSMBehaviourController fsmBehaviour)
+ {
+ if (shieldFXPrefab == null)
+ {
+ Debug.LogWarning("[SA_CastShield] Missing shieldFXPrefab!");
+ return;
+ }
+
+ Vector3 spawnPosition = shieldSpawnPoint != null ?
+ shieldSpawnPoint.position : fsmBehaviour.transform.position;
+
+ spawnedShield = LeanPool.Spawn(shieldFXPrefab, spawnPosition,
+ shieldSpawnPoint != null ? shieldSpawnPoint.rotation : fsmBehaviour.transform.rotation);
+
+ if (enableDebug) Debug.Log($"[SA_CastShield] Shield spawned at position: {spawnPosition}");
+
+ if (spawnedShield != null && shieldSpawnPoint != null)
+ {
+ spawnedShield.transform.SetParent(shieldSpawnPoint);
+ }
+ }
+
+ ///
+ /// Finishes shield operation and transitions to next state
+ ///
+ private void FinishShield(vIFSMBehaviourController fsmBehaviour)
+ {
+ shieldActive = false;
+ CleanupShield();
+
+ DEC_CheckCooldown.SetCooldownStatic(fsmBehaviour, "Shield", 15f);
+
+ // End state - FSM will transition to next state
+ // FYI: In Invector FSM, state completion is handled automatically
+ }
+
+ ///
+ /// Cleans up spawned shield
+ ///
+ private void CleanupShield()
+ {
+ if (spawnedShield != null)
+ {
+ LeanPool.Despawn(spawnedShield);
+ spawnedShield = null;
+ if (enableDebug) Debug.Log("[SA_CastShield] Shield despawned");
+ }
+ }
+
+ ///
+ /// Checks if shield is currently active
+ ///
+ public bool IsShieldActive()
+ {
+ return shieldActive;
+ }
+
+ ///
+ /// Returns remaining shield time
+ ///
+ public float GetRemainingShieldTime()
+ {
+ if (!shieldActive) return 0f;
+ return Mathf.Max(0f, shieldDuration - (Time.time - shieldStartTime));
+ }
+ }
+}
\ No newline at end of file
diff --git a/Assets/AI/Demon/SA_CastShield.cs.meta b/Assets/AI/Demon/SA_CastShield.cs.meta
new file mode 100644
index 000000000..5897e16c6
--- /dev/null
+++ b/Assets/AI/Demon/SA_CastShield.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: c53af76a8a097a244a3002b8aa7b4ceb
\ No newline at end of file
diff --git a/Assets/AI/Demon/SA_SpawnTurretSmart.cs b/Assets/AI/Demon/SA_SpawnTurretSmart.cs
new file mode 100644
index 000000000..e0b377cc2
--- /dev/null
+++ b/Assets/AI/Demon/SA_SpawnTurretSmart.cs
@@ -0,0 +1,298 @@
+using Invector.vCharacterController.AI.FSMBehaviour;
+using Lean.Pool;
+using UnityEngine;
+
+namespace DemonBoss.Magic
+{
+ ///
+ /// StateAction for intelligent crystal turret spawning
+ /// Searches for optimal position in 2-6m ring from boss, prefers "behind boss" relative to player
+ ///
+ [CreateAssetMenu(menuName = "Invector/FSM/Actions/DemonBoss/Spawn Turret Smart")]
+ public class SA_SpawnTurretSmart : vStateAction
+ {
+ public override string categoryName => "DemonBoss/Magic";
+ public override string defaultName => "Spawn Turret Smart";
+
+ [Header("Turret Configuration")]
+ [Tooltip("Crystal prefab with CrystalShooterAI component")]
+ public GameObject crystalPrefab;
+
+ [Tooltip("Minimum distance from boss for crystal spawn")]
+ public float minSpawnDistance = 2f;
+
+ [Tooltip("Maximum distance from boss for crystal spawn")]
+ public float maxSpawnDistance = 6f;
+
+ [Tooltip("Collision check radius when choosing position")]
+ public float obstacleCheckRadius = 1f;
+
+ [Tooltip("Height above ground for raycast ground checking")]
+ public float groundCheckHeight = 2f;
+
+ [Tooltip("Layer mask for obstacles")]
+ public LayerMask obstacleLayerMask = -1;
+
+ [Tooltip("Layer mask for ground")]
+ public LayerMask groundLayerMask = -1;
+
+ [Tooltip("Animator trigger name for crystal casting animation")]
+ public string animatorTrigger = "CastCrystal";
+
+ [Header("Smart Positioning")]
+ [Tooltip("Preference multiplier for positions behind boss (relative to player)")]
+ public float backPreferenceMultiplier = 2f;
+
+ [Tooltip("Number of attempts to find valid position")]
+ public int maxSpawnAttempts = 12;
+
+ [Header("Debug")]
+ [Tooltip("Enable debug logging")]
+ public bool enableDebug = false;
+
+ [Tooltip("Show gizmos in Scene View")]
+ public bool showGizmos = true;
+
+ private GameObject spawnedCrystal;
+
+ private Transform playerTransform;
+
+ ///
+ /// Main action execution method called by FSM
+ ///
+ public override void DoAction(vIFSMBehaviourController fsmBehaviour, vFSMComponentExecutionType executionType = vFSMComponentExecutionType.OnStateUpdate)
+ {
+ if (executionType == vFSMComponentExecutionType.OnStateEnter)
+ {
+ OnStateEnter(fsmBehaviour);
+ }
+ }
+
+ ///
+ /// Called when entering state - intelligently spawns crystal
+ ///
+ private void OnStateEnter(vIFSMBehaviourController fsmBehaviour)
+ {
+ if (enableDebug) Debug.Log("[SA_SpawnTurretSmart] Starting intelligent crystal spawn");
+
+ FindPlayer(fsmBehaviour);
+
+ var animator = fsmBehaviour.transform.GetComponent();
+ if (animator != null && !string.IsNullOrEmpty(animatorTrigger))
+ {
+ animator.SetTrigger(animatorTrigger);
+ if (enableDebug) Debug.Log($"[SA_SpawnTurretSmart] Set trigger: {animatorTrigger}");
+ }
+
+ SpawnCrystalSmart(fsmBehaviour);
+
+ DEC_CheckCooldown.SetCooldownStatic(fsmBehaviour, "Turret", 12f);
+ }
+
+ ///
+ /// Finds player transform
+ ///
+ private void FindPlayer(vIFSMBehaviourController fsmBehaviour)
+ {
+ GameObject player = GameObject.FindGameObjectWithTag("Player");
+ if (player != null)
+ {
+ playerTransform = player.transform;
+ if (enableDebug) Debug.Log("[SA_SpawnTurretSmart] Player found by tag");
+ return;
+ }
+
+ var aiController = fsmBehaviour as Invector.vCharacterController.AI.vIControlAI;
+ if (aiController != null && aiController.currentTarget != null)
+ {
+ playerTransform = aiController.currentTarget.transform;
+ if (enableDebug) Debug.Log("[SA_SpawnTurretSmart] Player found through AI target");
+ return;
+ }
+
+ if (enableDebug) Debug.LogWarning("[SA_SpawnTurretSmart] Player not found!");
+ }
+
+ ///
+ /// Intelligently spawns crystal in optimal position
+ ///
+ private void SpawnCrystalSmart(vIFSMBehaviourController fsmBehaviour)
+ {
+ if (crystalPrefab == null)
+ {
+ Debug.LogError("[SA_SpawnTurretSmart] Missing crystalPrefab!");
+ return;
+ }
+
+ Vector3 bestPosition = Vector3.zero;
+ bool foundValidPosition = false;
+ float bestScore = float.MinValue;
+
+ Vector3 bossPos = fsmBehaviour.transform.position;
+ Vector3 playerDirection = Vector3.zero;
+
+ if (playerTransform != null)
+ {
+ playerDirection = (playerTransform.position - bossPos).normalized;
+ }
+
+ for (int i = 0; i < maxSpawnAttempts; i++)
+ {
+ float angle = (360f / maxSpawnAttempts) * i + Random.Range(-15f, 15f);
+ Vector3 direction = new Vector3(Mathf.Cos(angle * Mathf.Deg2Rad), 0, Mathf.Sin(angle * Mathf.Deg2Rad));
+
+ float distance = Random.Range(minSpawnDistance, maxSpawnDistance);
+ Vector3 testPosition = bossPos + direction * distance;
+
+ if (IsPositionValid(testPosition, out Vector3 groundPosition))
+ {
+ float score = EvaluatePosition(groundPosition, playerDirection, direction);
+
+ if (score > bestScore)
+ {
+ bestScore = score;
+ bestPosition = groundPosition;
+ foundValidPosition = true;
+ }
+ }
+ }
+
+ if (foundValidPosition)
+ {
+ SpawnCrystal(bestPosition, fsmBehaviour);
+ if (enableDebug) Debug.Log($"[SA_SpawnTurretSmart] Crystal spawned at position: {bestPosition} (score: {bestScore:F2})");
+ }
+ else
+ {
+ Vector3 fallbackPos = bossPos + fsmBehaviour.transform.forward * minSpawnDistance;
+ SpawnCrystal(fallbackPos, fsmBehaviour);
+ if (enableDebug) Debug.LogWarning("[SA_SpawnTurretSmart] Using fallback position");
+ }
+ }
+
+ ///
+ /// Checks if position is valid (no obstacles, has ground)
+ ///
+ private bool IsPositionValid(Vector3 position, out Vector3 groundPosition)
+ {
+ groundPosition = position;
+
+ if (Physics.CheckSphere(position + Vector3.up * obstacleCheckRadius, obstacleCheckRadius, obstacleLayerMask))
+ {
+ return false;
+ }
+
+ Ray groundRay = new Ray(position + Vector3.up * groundCheckHeight, Vector3.down);
+ if (Physics.Raycast(groundRay, out RaycastHit hit, groundCheckHeight + 2f, groundLayerMask))
+ {
+ groundPosition = hit.point;
+
+ if (Physics.CheckSphere(groundPosition + Vector3.up * obstacleCheckRadius, obstacleCheckRadius, obstacleLayerMask))
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ ///
+ /// Evaluates position quality (higher score = better position)
+ ///
+ private float EvaluatePosition(Vector3 position, Vector3 playerDirection, Vector3 positionDirection)
+ {
+ float score = 0f;
+
+ if (playerTransform != null && playerDirection != Vector3.zero)
+ {
+ float angleToPlayer = Vector3.Angle(-playerDirection, positionDirection);
+
+ // The smaller the angle (closer to "behind"), the better the score
+ float backScore = (180f - angleToPlayer) / 180f;
+ score += backScore * backPreferenceMultiplier;
+ }
+
+ Vector3 bossPos = new Vector3();
+ float distance = Vector3.Distance(position, bossPos);
+ float optimalDistance = (minSpawnDistance + maxSpawnDistance) * 0.5f;
+ float distanceScore = 1f - Mathf.Abs(distance - optimalDistance) / maxSpawnDistance;
+ score += distanceScore;
+
+ score += Random.Range(-0.1f, 0.1f);
+
+ return score;
+ }
+
+ ///
+ /// Spawns crystal at given position
+ ///
+ private void SpawnCrystal(Vector3 position, vIFSMBehaviourController fsmBehaviour)
+ {
+ Quaternion rotation = Quaternion.identity;
+ if (playerTransform != null)
+ {
+ Vector3 lookDirection = (playerTransform.position - position).normalized;
+ lookDirection.y = 0;
+ if (lookDirection != Vector3.zero)
+ {
+ rotation = Quaternion.LookRotation(lookDirection);
+ }
+ }
+
+ spawnedCrystal = LeanPool.Spawn(crystalPrefab, position, rotation);
+
+ var shooterAI = spawnedCrystal.GetComponent();
+ if (shooterAI == null)
+ {
+ Debug.LogError("[SA_SpawnTurretSmart] Crystal prefab doesn't have CrystalShooterAI component!");
+ }
+ else
+ {
+ if (playerTransform != null)
+ {
+ shooterAI.SetTarget(playerTransform);
+ }
+ }
+ }
+
+ ///
+ /// Draws gizmos in Scene View for debugging
+ ///
+ private void OnDrawGizmosSelected()
+ {
+ if (!showGizmos) return;
+
+ Vector3 pos = new Vector3();
+
+ // Spawn ring
+ Gizmos.color = Color.green;
+ DrawWireCircle(pos, minSpawnDistance);
+ Gizmos.color = Color.red;
+ DrawWireCircle(pos, maxSpawnDistance);
+
+ // Obstacle check radius
+ Gizmos.color = Color.yellow;
+ Gizmos.DrawWireSphere(pos + Vector3.up * obstacleCheckRadius, obstacleCheckRadius);
+ }
+
+ ///
+ /// Helper method for drawing circles
+ ///
+ private void DrawWireCircle(Vector3 center, float radius)
+ {
+ int segments = 32;
+ float angle = 0f;
+ Vector3 prevPoint = center + new Vector3(radius, 0, 0);
+
+ for (int i = 1; i <= segments; i++)
+ {
+ angle = (float)i / segments * 360f * Mathf.Deg2Rad;
+ Vector3 newPoint = center + new Vector3(Mathf.Cos(angle) * radius, 0, Mathf.Sin(angle) * radius);
+ Gizmos.DrawLine(prevPoint, newPoint);
+ prevPoint = newPoint;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Assets/AI/Demon/SA_SpawnTurretSmart.cs.meta b/Assets/AI/Demon/SA_SpawnTurretSmart.cs.meta
new file mode 100644
index 000000000..93a534a26
--- /dev/null
+++ b/Assets/AI/Demon/SA_SpawnTurretSmart.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 5fcb700ea476bed44becf27143de31bd
\ No newline at end of file