diff --git a/Assets/AI/Archer.meta b/Assets/AI/_Archer.meta
similarity index 100%
rename from Assets/AI/Archer.meta
rename to Assets/AI/_Archer.meta
diff --git a/Assets/AI/_Archer/ArcherProjectile.cs b/Assets/AI/_Archer/ArcherProjectile.cs
new file mode 100644
index 000000000..318b6a7dc
--- /dev/null
+++ b/Assets/AI/_Archer/ArcherProjectile.cs
@@ -0,0 +1,327 @@
+using Invector;
+using Invector.vCharacterController;
+using Lean.Pool;
+using UnityEngine;
+
+namespace ArcherEnemy
+{
+ ///
+ /// Arrow projectile shot by archer enemies
+ /// Flies in straight line with gravity and deals damage on hit
+ ///
+ public class ArcherProjectile : MonoBehaviour
+ {
+ #region Configuration
+
+ [Header("Movement")]
+ [Tooltip("Initial velocity of the arrow (m/s)")]
+ public float initialSpeed = 30f;
+
+ [Tooltip("Gravity multiplier (higher = more arc)")]
+ public float gravityMultiplier = 1f;
+
+ [Tooltip("Max lifetime before auto-despawn (seconds)")]
+ public float maxLifetime = 10f;
+
+ [Header("Damage")]
+ [Tooltip("Damage dealt on hit")]
+ public int damage = 15;
+
+ [Tooltip("Knockback force")]
+ public float knockbackForce = 5f;
+
+ [Tooltip("Layers that can be hit")]
+ public LayerMask hitLayers = -1;
+
+ [Header("Effects")]
+ [Tooltip("Impact VFX prefab")]
+ public GameObject impactVFXPrefab;
+
+ [Tooltip("Trail renderer (optional)")]
+ public TrailRenderer trail;
+
+ [Header("Audio")]
+ [Tooltip("Impact sound")]
+ public AudioClip impactSound;
+
+ [Header("Debug")]
+ [Tooltip("Enable debug logging")]
+ public bool enableDebug = false;
+
+ [Tooltip("Show trajectory gizmos")]
+ public bool showGizmos = true;
+
+ #endregion Configuration
+
+ #region Runtime State
+
+ private Vector3 velocity;
+ private float lifetime = 0f;
+ private bool hasHit = false;
+ private AudioSource audioSource;
+
+ #endregion Runtime State
+
+ #region Unity Lifecycle
+
+ private void Awake()
+ {
+ if (impactSound != null)
+ {
+ audioSource = GetComponent();
+ if (audioSource == null)
+ {
+ audioSource = gameObject.AddComponent();
+ audioSource.playOnAwake = false;
+ audioSource.spatialBlend = 1f; // 3D sound
+ }
+ }
+ }
+
+ private void OnEnable()
+ {
+ ResetState();
+
+ // Set initial velocity in forward direction
+ velocity = transform.forward * initialSpeed;
+
+ if (enableDebug)
+ Debug.Log($"[ArcherProjectile] Spawned at {transform.position}, velocity: {velocity}");
+ }
+
+ private void Update()
+ {
+ if (hasHit) return;
+
+ lifetime += Time.deltaTime;
+
+ // Check lifetime
+ if (lifetime >= maxLifetime)
+ {
+ if (enableDebug) Debug.Log("[ArcherProjectile] Lifetime expired");
+ Despawn();
+ return;
+ }
+
+ // Apply gravity
+ velocity += Physics.gravity * gravityMultiplier * Time.deltaTime;
+
+ // Calculate movement step
+ Vector3 moveStep = velocity * Time.deltaTime;
+ Vector3 newPosition = transform.position + moveStep;
+
+ // Raycast for collision detection
+ if (Physics.Raycast(transform.position, moveStep.normalized, out RaycastHit hit,
+ moveStep.magnitude, hitLayers, QueryTriggerInteraction.Ignore))
+ {
+ OnHit(hit);
+ return;
+ }
+
+ // Move arrow
+ transform.position = newPosition;
+
+ // Rotate arrow to face movement direction
+ if (velocity.sqrMagnitude > 0.001f)
+ {
+ transform.rotation = Quaternion.LookRotation(velocity.normalized);
+ }
+ }
+
+ #endregion Unity Lifecycle
+
+ #region Collision & Damage
+
+ private void OnHit(RaycastHit hit)
+ {
+ if (hasHit) return;
+ hasHit = true;
+
+ if (enableDebug)
+ Debug.Log($"[ArcherProjectile] Hit: {hit.collider.name} at {hit.point}");
+
+ // Position arrow at impact point
+ transform.position = hit.point;
+ transform.rotation = Quaternion.LookRotation(hit.normal);
+
+ // Try to deal damage
+ DealDamage(hit.collider, hit.point, hit.normal);
+
+ // Spawn impact VFX
+ SpawnImpactVFX(hit.point, hit.normal);
+
+ // Play impact sound
+ PlayImpactSound();
+
+ // Stick arrow to surface or despawn
+ StickToSurface(hit);
+ }
+
+ private void DealDamage(Collider targetCollider, Vector3 hitPoint, Vector3 hitNormal)
+ {
+ // Calculate hit direction (opposite of normal for knockback)
+ Vector3 hitDirection = -hitNormal;
+ if (velocity.sqrMagnitude > 0.001f)
+ {
+ hitDirection = velocity.normalized;
+ }
+
+ // Create damage info
+ vDamage damageInfo = new vDamage(damage)
+ {
+ sender = transform,
+ hitPosition = hitPoint
+ };
+
+ if (knockbackForce > 0f)
+ {
+ damageInfo.force = hitDirection * knockbackForce;
+ }
+
+ bool damageDealt = false;
+
+ // Try vIDamageReceiver
+ var damageReceiver = targetCollider.GetComponent() ??
+ targetCollider.GetComponentInParent();
+
+ if (damageReceiver != null)
+ {
+ damageReceiver.TakeDamage(damageInfo);
+ damageDealt = true;
+ if (enableDebug) Debug.Log("[ArcherProjectile] Damage dealt via vIDamageReceiver");
+ }
+
+ // Fallback to vHealthController
+ if (!damageDealt)
+ {
+ var healthController = targetCollider.GetComponent() ??
+ targetCollider.GetComponentInParent();
+
+ if (healthController != null)
+ {
+ healthController.TakeDamage(damageInfo);
+ damageDealt = true;
+ if (enableDebug) Debug.Log("[ArcherProjectile] Damage dealt via vHealthController");
+ }
+ }
+
+ // Fallback to vThirdPersonController
+ if (!damageDealt)
+ {
+ var tpc = targetCollider.GetComponent() ??
+ targetCollider.GetComponentInParent();
+
+ if (tpc != null)
+ {
+ // Handle Beyond variant
+ if (tpc is Beyond.bThirdPersonController beyond)
+ {
+ if (!beyond.GodMode && !beyond.isImmortal)
+ {
+ tpc.TakeDamage(damageInfo);
+ damageDealt = true;
+ if (enableDebug) Debug.Log("[ArcherProjectile] Damage dealt via bThirdPersonController");
+ }
+ else
+ {
+ if (enableDebug) Debug.Log("[ArcherProjectile] Target is immortal - no damage");
+ }
+ }
+ else
+ {
+ tpc.TakeDamage(damageInfo);
+ damageDealt = true;
+ if (enableDebug) Debug.Log("[ArcherProjectile] Damage dealt via vThirdPersonController");
+ }
+ }
+ }
+
+ if (!damageDealt && enableDebug)
+ {
+ Debug.Log("[ArcherProjectile] No damage dealt - no valid receiver found");
+ }
+ }
+
+ #endregion Collision & Damage
+
+ #region Effects
+
+ private void SpawnImpactVFX(Vector3 position, Vector3 normal)
+ {
+ if (impactVFXPrefab == null) return;
+
+ Quaternion rotation = Quaternion.LookRotation(normal);
+ GameObject vfx = LeanPool.Spawn(impactVFXPrefab, position, rotation);
+ LeanPool.Despawn(vfx, 3f);
+
+ if (enableDebug) Debug.Log("[ArcherProjectile] Impact VFX spawned");
+ }
+
+ private void PlayImpactSound()
+ {
+ if (audioSource != null && impactSound != null)
+ {
+ audioSource.PlayOneShot(impactSound);
+ }
+ }
+
+ #endregion Effects
+
+ #region Arrow Sticking
+
+ private void StickToSurface(RaycastHit hit)
+ {
+ // Option 1: Parent arrow to hit object (if it has rigidbody, it will move with it)
+ // Option 2: Just despawn after short delay
+
+ // For now, despawn after brief delay to show impact
+ LeanPool.Despawn(gameObject, 0.1f);
+ }
+
+ #endregion Arrow Sticking
+
+ #region Pooling
+
+ private void ResetState()
+ {
+ hasHit = false;
+ lifetime = 0f;
+ velocity = Vector3.zero;
+
+ // Reset trail if present
+ if (trail != null)
+ {
+ trail.Clear();
+ }
+ }
+
+ private void Despawn()
+ {
+ if (enableDebug) Debug.Log("[ArcherProjectile] Despawning");
+ LeanPool.Despawn(gameObject);
+ }
+
+ #endregion Pooling
+
+ #region Gizmos
+
+#if UNITY_EDITOR
+
+ private void OnDrawGizmos()
+ {
+ if (!showGizmos || !Application.isPlaying) return;
+
+ // Draw velocity vector
+ Gizmos.color = Color.yellow;
+ Gizmos.DrawRay(transform.position, velocity.normalized * 2f);
+
+ // Draw forward direction
+ Gizmos.color = Color.blue;
+ Gizmos.DrawRay(transform.position, transform.forward * 1f);
+ }
+
+#endif
+
+ #endregion Gizmos
+ }
+}
\ No newline at end of file
diff --git a/Assets/AI/_Archer/ArcherProjectile.cs.meta b/Assets/AI/_Archer/ArcherProjectile.cs.meta
new file mode 100644
index 000000000..e111c5098
--- /dev/null
+++ b/Assets/AI/_Archer/ArcherProjectile.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 9c5e129708014c64981ec3d5665de1b4
\ No newline at end of file
diff --git a/Assets/AI/_Archer/ArcherShootingAI.cs b/Assets/AI/_Archer/ArcherShootingAI.cs
new file mode 100644
index 000000000..b2a8c5dfa
--- /dev/null
+++ b/Assets/AI/_Archer/ArcherShootingAI.cs
@@ -0,0 +1,430 @@
+using Lean.Pool;
+using UnityEngine;
+
+namespace ArcherEnemy
+{
+ ///
+ /// AI component for archer enemy that shoots arrows at target
+ /// Should be attached to archer enemy prefab
+ ///
+ public class ArcherShootingAI : MonoBehaviour
+ {
+ [Header("References")]
+ [Tooltip("Transform point from which arrows are shot (usually hand bone or weapon tip)")]
+ public Transform shootPoint;
+
+ [Tooltip("Arrow prefab with ArcherProjectile component")]
+ public GameObject arrowPrefab;
+
+ [Tooltip("Animator for triggering shoot animation")]
+ public Animator animator;
+
+ [Header("Targeting")]
+ [Tooltip("Auto-find player on start")]
+ public bool autoFindPlayer = true;
+
+ [Tooltip("Player tag to search for")]
+ public string playerTag = "Player";
+
+ [Tooltip("Height offset for aiming (aim at chest/head)")]
+ public float targetHeightOffset = 1.2f;
+
+ [Header("Shooting Parameters")]
+ [Tooltip("Minimum distance to shoot from")]
+ public float minShootDistance = 8f;
+
+ [Tooltip("Maximum distance to shoot from")]
+ public float maxShootDistance = 25f;
+
+ [Tooltip("Time between shots (seconds)")]
+ public float shootCooldown = 2f;
+
+ [Tooltip("Arrow launch speed (m/s)")]
+ public float arrowSpeed = 30f;
+
+ [Tooltip("How much to lead the target (predict movement)")]
+ [Range(0f, 1f)]
+ public float leadTargetAmount = 0.5f;
+
+ [Header("Animation")]
+ [Tooltip("Animator trigger parameter for shooting")]
+ public string shootTriggerName = "Shoot";
+
+ [Tooltip("Delay after animation starts before spawning arrow")]
+ public float shootAnimationDelay = 0.3f;
+
+ [Header("Aiming")]
+ [Tooltip("How quickly archer rotates to face target (degrees/sec)")]
+ public float turnSpeed = 180f;
+
+ [Tooltip("Angle tolerance for shooting (degrees)")]
+ public float aimTolerance = 15f;
+
+ [Header("Effects")]
+ [Tooltip("Muzzle flash effect at shoot point")]
+ public GameObject muzzleFlashPrefab;
+
+ [Tooltip("Shoot sound")]
+ public AudioClip shootSound;
+
+ [Header("Debug")]
+ [Tooltip("Enable debug logging")]
+ public bool enableDebug = false;
+
+ [Tooltip("Show aiming gizmos")]
+ public bool showGizmos = true;
+
+ #region Private Fields
+
+ private Transform target;
+ private float lastShootTime = -999f;
+ private bool isAiming = false;
+ private AudioSource audioSource;
+ private Vector3 lastKnownTargetVelocity;
+ private Vector3 lastTargetPosition;
+
+ #endregion Private Fields
+
+ #region Unity Lifecycle
+
+ private void Awake()
+ {
+ // Setup audio source
+ if (shootSound != null)
+ {
+ audioSource = GetComponent();
+ if (audioSource == null)
+ {
+ audioSource = gameObject.AddComponent();
+ audioSource.playOnAwake = false;
+ audioSource.spatialBlend = 1f;
+ }
+ }
+
+ // Find animator if not assigned
+ if (animator == null)
+ {
+ animator = GetComponent();
+ }
+
+ // Find shoot point if not assigned
+ if (shootPoint == null)
+ {
+ // Try to find "ShootPoint" child transform
+ Transform shootPointChild = transform.Find("ShootPoint");
+ shootPoint = shootPointChild != null ? shootPointChild : transform;
+ }
+ }
+
+ private void Start()
+ {
+ if (autoFindPlayer)
+ {
+ FindPlayer();
+ }
+ }
+
+ private void Update()
+ {
+ if (target != null && isAiming)
+ {
+ RotateTowardsTarget();
+ UpdateTargetVelocity();
+ }
+ }
+
+ #endregion Unity Lifecycle
+
+ #region Target Management
+
+ ///
+ /// Finds player by tag
+ ///
+ private void FindPlayer()
+ {
+ GameObject player = GameObject.FindGameObjectWithTag(playerTag);
+ if (player != null)
+ {
+ SetTarget(player.transform);
+ }
+ }
+
+ ///
+ /// Sets the target to shoot at
+ ///
+ public void SetTarget(Transform newTarget)
+ {
+ target = newTarget;
+ if (target != null)
+ {
+ lastTargetPosition = target.position;
+ }
+ }
+
+ ///
+ /// Updates target velocity for prediction
+ ///
+ private void UpdateTargetVelocity()
+ {
+ if (target == null) return;
+
+ Vector3 currentPosition = target.position;
+ lastKnownTargetVelocity = (currentPosition - lastTargetPosition) / Time.deltaTime;
+ lastTargetPosition = currentPosition;
+ }
+
+ #endregion Target Management
+
+ #region Shooting Logic
+
+ ///
+ /// Checks if archer can shoot at target
+ ///
+ public bool CanShoot()
+ {
+ if (target == null) return false;
+
+ float distance = Vector3.Distance(transform.position, target.position);
+
+ // Check distance range
+ if (distance < minShootDistance || distance > maxShootDistance)
+ {
+ return false;
+ }
+
+ // Check cooldown
+ if (Time.time < lastShootTime + shootCooldown)
+ {
+ return false;
+ }
+
+ // Check if facing target (within tolerance)
+ Vector3 directionToTarget = (target.position - transform.position).normalized;
+ float angleToTarget = Vector3.Angle(transform.forward, directionToTarget);
+
+ if (angleToTarget > aimTolerance)
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ ///
+ /// Initiates shooting sequence
+ ///
+ public void StartShooting()
+ {
+ if (!CanShoot())
+ {
+ if (enableDebug) Debug.Log("[ArcherShootingAI] Cannot shoot - conditions not met");
+ return;
+ }
+
+ isAiming = true;
+
+ // Trigger animation
+ if (animator != null && !string.IsNullOrEmpty(shootTriggerName))
+ {
+ animator.SetTrigger(shootTriggerName);
+ }
+
+ // Schedule arrow spawn after animation delay
+ Invoke(nameof(SpawnArrow), shootAnimationDelay);
+
+ lastShootTime = Time.time;
+
+ if (enableDebug) Debug.Log($"[ArcherShootingAI] Started shooting at {target.name}");
+ }
+
+ ///
+ /// Spawns arrow projectile
+ ///
+ private void SpawnArrow()
+ {
+ if (arrowPrefab == null)
+ {
+ Debug.LogError("[ArcherShootingAI] Arrow prefab not assigned!");
+ return;
+ }
+
+ if (shootPoint == null)
+ {
+ Debug.LogError("[ArcherShootingAI] Shoot point not assigned!");
+ return;
+ }
+
+ // Calculate aim point with prediction
+ Vector3 aimPoint = CalculateAimPoint();
+
+ // Calculate shoot direction
+ Vector3 shootDirection = (aimPoint - shootPoint.position).normalized;
+
+ // Calculate rotation for arrow
+ Quaternion arrowRotation = Quaternion.LookRotation(shootDirection);
+
+ // Spawn arrow
+ GameObject arrow = LeanPool.Spawn(arrowPrefab, shootPoint.position, arrowRotation);
+
+ // Configure arrow projectile
+ var projectile = arrow.GetComponent();
+ if (projectile != null)
+ {
+ projectile.initialSpeed = arrowSpeed;
+ }
+
+ // Play effects
+ PlayShootEffects();
+
+ if (enableDebug)
+ Debug.Log($"[ArcherShootingAI] Arrow spawned, direction: {shootDirection}");
+ }
+
+ ///
+ /// Calculates aim point with target prediction
+ ///
+ private Vector3 CalculateAimPoint()
+ {
+ if (target == null) return shootPoint.position + transform.forward * 10f;
+
+ // Base aim point
+ Vector3 targetPosition = target.position + Vector3.up * targetHeightOffset;
+
+ // Add prediction based on target velocity
+ if (leadTargetAmount > 0f && lastKnownTargetVelocity.sqrMagnitude > 0.1f)
+ {
+ // Estimate time to target
+ float distance = Vector3.Distance(shootPoint.position, targetPosition);
+ float timeToTarget = distance / arrowSpeed;
+
+ // Predict future position
+ Vector3 predictedOffset = lastKnownTargetVelocity * timeToTarget * leadTargetAmount;
+ targetPosition += predictedOffset;
+ }
+
+ return targetPosition;
+ }
+
+ ///
+ /// Stops shooting sequence
+ ///
+ public void StopShooting()
+ {
+ isAiming = false;
+ CancelInvoke(nameof(SpawnArrow));
+ }
+
+ #endregion Shooting Logic
+
+ #region Effects
+
+ private void PlayShootEffects()
+ {
+ // Spawn muzzle flash
+ if (muzzleFlashPrefab != null && shootPoint != null)
+ {
+ GameObject flash = LeanPool.Spawn(muzzleFlashPrefab, shootPoint.position, shootPoint.rotation);
+ LeanPool.Despawn(flash, 2f);
+ }
+
+ // Play shoot sound
+ if (audioSource != null && shootSound != null)
+ {
+ audioSource.PlayOneShot(shootSound);
+ }
+ }
+
+ #endregion Effects
+
+ #region Rotation
+
+ ///
+ /// Smoothly rotates archer to face target
+ ///
+ private void RotateTowardsTarget()
+ {
+ if (target == null) return;
+
+ Vector3 directionToTarget = (target.position - transform.position);
+ directionToTarget.y = 0f; // Keep rotation on Y axis only
+
+ if (directionToTarget.sqrMagnitude > 0.001f)
+ {
+ Quaternion targetRotation = Quaternion.LookRotation(directionToTarget);
+ transform.rotation = Quaternion.RotateTowards(
+ transform.rotation,
+ targetRotation,
+ turnSpeed * Time.deltaTime
+ );
+ }
+ }
+
+ ///
+ /// Checks if archer is facing target within tolerance
+ ///
+ public bool IsFacingTarget()
+ {
+ if (target == null) return false;
+
+ Vector3 directionToTarget = (target.position - transform.position).normalized;
+ float angle = Vector3.Angle(transform.forward, directionToTarget);
+
+ return angle <= aimTolerance;
+ }
+
+ #endregion Rotation
+
+ #region Public Query Methods
+
+ public Transform GetTarget() => target;
+
+ public bool IsAiming() => isAiming;
+
+ public float GetTimeSinceLastShot() => Time.time - lastShootTime;
+
+ public float GetDistanceToTarget() => target != null ? Vector3.Distance(transform.position, target.position) : float.MaxValue;
+
+ public bool IsInShootingRange() => GetDistanceToTarget() >= minShootDistance && GetDistanceToTarget() <= maxShootDistance;
+
+ #endregion Public Query Methods
+
+ #region Gizmos
+
+#if UNITY_EDITOR
+
+ private void OnDrawGizmosSelected()
+ {
+ if (!showGizmos) return;
+
+ // Draw shooting range
+ Gizmos.color = Color.yellow;
+ UnityEditor.Handles.color = new Color(1f, 1f, 0f, 0.1f);
+ UnityEditor.Handles.DrawSolidDisc(transform.position, Vector3.up, minShootDistance);
+
+ Gizmos.color = Color.green;
+ UnityEditor.Handles.color = new Color(0f, 1f, 0f, 0.1f);
+ UnityEditor.Handles.DrawSolidDisc(transform.position, Vector3.up, maxShootDistance);
+
+ // Draw shoot point
+ if (shootPoint != null)
+ {
+ Gizmos.color = Color.red;
+ Gizmos.DrawWireSphere(shootPoint.position, 0.1f);
+ Gizmos.DrawLine(shootPoint.position, shootPoint.position + shootPoint.forward * 2f);
+ }
+
+ // Draw aim line to target
+ if (target != null && Application.isPlaying)
+ {
+ Vector3 aimPoint = CalculateAimPoint();
+ Gizmos.color = Color.cyan;
+ Gizmos.DrawLine(shootPoint != null ? shootPoint.position : transform.position, aimPoint);
+ Gizmos.DrawWireSphere(aimPoint, 0.2f);
+ }
+ }
+
+#endif
+
+ #endregion Gizmos
+ }
+}
\ No newline at end of file
diff --git a/Assets/AI/_Archer/ArcherShootingAI.cs.meta b/Assets/AI/_Archer/ArcherShootingAI.cs.meta
new file mode 100644
index 000000000..bd3f83748
--- /dev/null
+++ b/Assets/AI/_Archer/ArcherShootingAI.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 6b6fc5c257dfe0a42856b4f8169fb925
\ No newline at end of file
diff --git a/Assets/AI/Archer/Archer_BaseModel.prefab b/Assets/AI/_Archer/Archer_BaseModel.prefab
similarity index 100%
rename from Assets/AI/Archer/Archer_BaseModel.prefab
rename to Assets/AI/_Archer/Archer_BaseModel.prefab
diff --git a/Assets/AI/Archer/Archer_BaseModel.prefab.meta b/Assets/AI/_Archer/Archer_BaseModel.prefab.meta
similarity index 100%
rename from Assets/AI/Archer/Archer_BaseModel.prefab.meta
rename to Assets/AI/_Archer/Archer_BaseModel.prefab.meta
diff --git a/Assets/AI/Archer/ArrowProjectile_AI.prefab b/Assets/AI/_Archer/ArrowProjectile_AI.prefab
similarity index 100%
rename from Assets/AI/Archer/ArrowProjectile_AI.prefab
rename to Assets/AI/_Archer/ArrowProjectile_AI.prefab
diff --git a/Assets/AI/Archer/ArrowProjectile_AI.prefab.meta b/Assets/AI/_Archer/ArrowProjectile_AI.prefab.meta
similarity index 100%
rename from Assets/AI/Archer/ArrowProjectile_AI.prefab.meta
rename to Assets/AI/_Archer/ArrowProjectile_AI.prefab.meta
diff --git a/Assets/AI/_Archer/DEC_CanShoot.cs b/Assets/AI/_Archer/DEC_CanShoot.cs
new file mode 100644
index 000000000..f2c8cef6d
--- /dev/null
+++ b/Assets/AI/_Archer/DEC_CanShoot.cs
@@ -0,0 +1,66 @@
+using Invector.vCharacterController.AI.FSMBehaviour;
+using UnityEngine;
+
+namespace ArcherEnemy
+{
+ ///
+ /// Decision checking if archer can shoot
+ /// Checks cooldown, aiming, and射ing AI component readiness
+ ///
+ [CreateAssetMenu(menuName = "Invector/FSM/Decisions/Archer/Can Shoot")]
+ public class DEC_CanShoot : vStateDecision
+ {
+ public override string categoryName => "Archer/Combat";
+ public override string defaultName => "Can Shoot";
+
+ [Header("Configuration")]
+ [Tooltip("Check if archer is facing target within tolerance")]
+ public bool checkFacingTarget = true;
+
+ [Tooltip("Angle tolerance for shooting (degrees)")]
+ public float aimTolerance = 20f;
+
+ [Header("Debug")]
+ [Tooltip("Enable debug logging")]
+ public bool enableDebug = false;
+
+ public override bool Decide(vIFSMBehaviourController fsmBehaviour)
+ {
+ // Get ArcherShootingAI component
+ var shootingAI = fsmBehaviour.gameObject.GetComponent();
+
+ if (shootingAI == null)
+ {
+ if (enableDebug) Debug.LogWarning("[DEC_CanShoot] No ArcherShootingAI component found!");
+ return false;
+ }
+
+ // Use the shooting AI's CanShoot method
+ bool canShoot = shootingAI.CanShoot();
+
+ // Optional: additional facing check
+ if (canShoot && checkFacingTarget)
+ {
+ Transform target = shootingAI.GetTarget();
+ if (target != null)
+ {
+ Vector3 directionToTarget = (target.position - fsmBehaviour.transform.position).normalized;
+ float angle = Vector3.Angle(fsmBehaviour.transform.forward, directionToTarget);
+
+ if (angle > aimTolerance)
+ {
+ canShoot = false;
+ if (enableDebug) Debug.Log($"[DEC_CanShoot] Not facing target: {angle:F1}° (tolerance: {aimTolerance}°)");
+ }
+ }
+ }
+
+ if (enableDebug)
+ {
+ Debug.Log($"[DEC_CanShoot] {(canShoot ? "CAN SHOOT" : "CANNOT SHOOT")}");
+ }
+
+ return canShoot;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Assets/AI/_Archer/DEC_CanShoot.cs.meta b/Assets/AI/_Archer/DEC_CanShoot.cs.meta
new file mode 100644
index 000000000..b9db53996
--- /dev/null
+++ b/Assets/AI/_Archer/DEC_CanShoot.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: dd96521d511252744900d3adb9ef1b2e
\ No newline at end of file
diff --git a/Assets/AI/_Archer/DEC_PlayerInShootRange.cs b/Assets/AI/_Archer/DEC_PlayerInShootRange.cs
new file mode 100644
index 000000000..83ee94747
--- /dev/null
+++ b/Assets/AI/_Archer/DEC_PlayerInShootRange.cs
@@ -0,0 +1,112 @@
+using Invector.vCharacterController.AI.FSMBehaviour;
+using UnityEngine;
+
+namespace ArcherEnemy
+{
+ ///
+ /// Decision checking if player is in optimal shooting range
+ /// Returns true when player is far enough but not too far
+ ///
+ [CreateAssetMenu(menuName = "Invector/FSM/Decisions/Archer/Player In Shoot Range")]
+ public class DEC_PlayerInShootRange : vStateDecision
+ {
+ public override string categoryName => "Archer/Combat";
+ public override string defaultName => "Player In Shoot Range";
+
+ [Header("Range Configuration")]
+ [Tooltip("Minimum safe distance to start shooting")]
+ public float minShootDistance = 8f;
+
+ [Tooltip("Maximum effective shooting distance")]
+ public float maxShootDistance = 25f;
+
+ [Tooltip("Also check if we have clear line of sight")]
+ public bool checkLineOfSight = true;
+
+ [Tooltip("Layers that block line of sight")]
+ public LayerMask obstacleMask = -1;
+
+ [Header("Debug")]
+ [Tooltip("Enable debug logging")]
+ public bool enableDebug = false;
+
+ [Tooltip("Show range gizmos")]
+ public bool showGizmos = true;
+
+ public override bool Decide(vIFSMBehaviourController fsmBehaviour)
+ {
+ Transform target = GetTarget(fsmBehaviour);
+
+ if (target == null)
+ {
+ if (enableDebug) Debug.Log("[DEC_PlayerInShootRange] No target found");
+ return false;
+ }
+
+ Vector3 archerPos = fsmBehaviour.transform.position;
+ Vector3 targetPos = target.position;
+
+ float distance = Vector3.Distance(archerPos, targetPos);
+
+ // Check distance range
+ bool inRange = distance >= minShootDistance && distance <= maxShootDistance;
+
+ if (!inRange)
+ {
+ if (enableDebug)
+ {
+ if (distance < minShootDistance)
+ Debug.Log($"[DEC_PlayerInShootRange] Player too close: {distance:F1}m (min: {minShootDistance})");
+ else
+ Debug.Log($"[DEC_PlayerInShootRange] Player too far: {distance:F1}m (max: {maxShootDistance})");
+ }
+ return false;
+ }
+
+ // Check line of sight if enabled
+ if (checkLineOfSight)
+ {
+ Vector3 shootPoint = archerPos + Vector3.up * 1.5f; // Approximate chest height
+ Vector3 targetPoint = targetPos + Vector3.up * 1f;
+ Vector3 direction = targetPoint - shootPoint;
+
+ if (Physics.Raycast(shootPoint, direction.normalized, distance, obstacleMask, QueryTriggerInteraction.Ignore))
+ {
+ if (enableDebug) Debug.Log("[DEC_PlayerInShootRange] Line of sight blocked");
+ return false;
+ }
+ }
+
+ if (enableDebug)
+ {
+ Debug.Log($"[DEC_PlayerInShootRange] IN RANGE: {distance:F1}m");
+ }
+
+ 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;
+ }
+
+#if UNITY_EDITOR
+
+ private void OnDrawGizmosSelected()
+ {
+ if (!showGizmos) return;
+
+ // This would need to be drawn from the archer's position in-game
+ // For now, just a visual reference
+ }
+
+#endif
+ }
+}
\ No newline at end of file
diff --git a/Assets/AI/_Archer/DEC_PlayerInShootRange.cs.meta b/Assets/AI/_Archer/DEC_PlayerInShootRange.cs.meta
new file mode 100644
index 000000000..d34ff55b1
--- /dev/null
+++ b/Assets/AI/_Archer/DEC_PlayerInShootRange.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 6577855f02c3b2649b6a787e88baea8b
\ No newline at end of file
diff --git a/Assets/AI/_Archer/DEC_PlayerTooClose.cs b/Assets/AI/_Archer/DEC_PlayerTooClose.cs
new file mode 100644
index 000000000..815affaeb
--- /dev/null
+++ b/Assets/AI/_Archer/DEC_PlayerTooClose.cs
@@ -0,0 +1,80 @@
+using Invector.vCharacterController.AI.FSMBehaviour;
+using UnityEngine;
+
+namespace ArcherEnemy
+{
+ ///
+ /// Decision checking if player is too close to the archer
+ /// Used to trigger retreat/flee behavior
+ ///
+ [CreateAssetMenu(menuName = "Invector/FSM/Decisions/Archer/Player Too Close")]
+ public class DEC_PlayerTooClose : vStateDecision
+ {
+ public override string categoryName => "Archer/Combat";
+ public override string defaultName => "Player Too Close";
+
+ [Header("Distance Configuration")]
+ [Tooltip("Distance below which player is considered too close")]
+ public float dangerDistance = 6f;
+
+ [Tooltip("Optional: check only if player is approaching (not retreating)")]
+ public bool checkIfApproaching = false;
+
+ [Header("Debug")]
+ [Tooltip("Enable debug logging")]
+ public bool enableDebug = false;
+
+ private Vector3 lastPlayerPosition;
+ private bool hasLastPosition = false;
+
+ public override bool Decide(vIFSMBehaviourController fsmBehaviour)
+ {
+ Transform target = GetTarget(fsmBehaviour);
+
+ if (target == null)
+ {
+ if (enableDebug) Debug.Log("[DEC_PlayerTooClose] No target found");
+ hasLastPosition = false;
+ return false;
+ }
+
+ float distance = Vector3.Distance(fsmBehaviour.transform.position, target.position);
+ bool tooClose = distance < dangerDistance;
+
+ // Optional: check if player is approaching
+ if (checkIfApproaching && hasLastPosition)
+ {
+ float previousDistance = Vector3.Distance(fsmBehaviour.transform.position, lastPlayerPosition);
+ bool isApproaching = distance < previousDistance;
+
+ if (!isApproaching)
+ {
+ tooClose = false; // Player is moving away, not a threat
+ }
+ }
+
+ // Store current position for next frame
+ lastPlayerPosition = target.position;
+ hasLastPosition = true;
+
+ if (enableDebug)
+ {
+ Debug.Log($"[DEC_PlayerTooClose] Distance: {distance:F1}m - {(tooClose ? "TOO CLOSE" : "SAFE")}");
+ }
+
+ return tooClose;
+ }
+
+ 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/_Archer/DEC_PlayerTooClose.cs.meta b/Assets/AI/_Archer/DEC_PlayerTooClose.cs.meta
new file mode 100644
index 000000000..67764fba4
--- /dev/null
+++ b/Assets/AI/_Archer/DEC_PlayerTooClose.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 3508c9b7b0af0d540a18d406403cff4d
\ No newline at end of file
diff --git a/Assets/AI/_Archer/SA_FleeFromPlayer.cs b/Assets/AI/_Archer/SA_FleeFromPlayer.cs
new file mode 100644
index 000000000..bece5d642
--- /dev/null
+++ b/Assets/AI/_Archer/SA_FleeFromPlayer.cs
@@ -0,0 +1,269 @@
+using Invector.vCharacterController.AI.FSMBehaviour;
+using UnityEngine;
+
+namespace ArcherEnemy
+{
+ ///
+ /// State action that makes archer flee/retreat from player
+ /// Moves archer away from player while trying to maintain shooting distance
+ ///
+ [CreateAssetMenu(menuName = "Invector/FSM/Actions/Archer/Flee From Player")]
+ public class SA_FleeFromPlayer : vStateAction
+ {
+ public override string categoryName => "Archer/Combat";
+ public override string defaultName => "Flee From Player";
+
+ [Header("Flee Configuration")]
+ [Tooltip("Desired safe distance from player")]
+ public float safeDistance = 12f;
+
+ [Tooltip("How far to look ahead when fleeing")]
+ public float fleeDistance = 5f;
+
+ [Tooltip("Check for obstacles when fleeing")]
+ public bool avoidObstacles = true;
+
+ [Tooltip("Layers considered as obstacles")]
+ public LayerMask obstacleMask = -1;
+
+ [Tooltip("Number of directions to try when finding flee path")]
+ public int directionSamples = 8;
+
+ [Header("Movement")]
+ [Tooltip("Movement speed multiplier (uses AI's speed)")]
+ [Range(0.5f, 2f)]
+ public float speedMultiplier = 1.2f;
+
+ [Tooltip("Make archer sprint while fleeing")]
+ public bool useSprint = true;
+
+ [Header("Rotation")]
+ [Tooltip("Keep facing player while backing away")]
+ public bool facePlayer = true;
+
+ [Tooltip("Rotation speed when facing player (degrees/sec)")]
+ public float turnSpeed = 180f;
+
+ [Header("Debug")]
+ [Tooltip("Enable debug logging")]
+ public bool enableDebug = false;
+
+ [Tooltip("Show flee direction gizmos")]
+ public bool showGizmos = true;
+
+ private Vector3 currentFleeDirection;
+ private Transform currentTarget;
+
+ public override void DoAction(vIFSMBehaviourController fsmBehaviour, vFSMComponentExecutionType executionType = vFSMComponentExecutionType.OnStateUpdate)
+ {
+ if (executionType == vFSMComponentExecutionType.OnStateEnter)
+ {
+ OnEnter(fsmBehaviour);
+ }
+ else if (executionType == vFSMComponentExecutionType.OnStateUpdate)
+ {
+ OnUpdate(fsmBehaviour);
+ }
+ else if (executionType == vFSMComponentExecutionType.OnStateExit)
+ {
+ OnExit(fsmBehaviour);
+ }
+ }
+
+ private void OnEnter(vIFSMBehaviourController fsmBehaviour)
+ {
+ currentTarget = GetTarget(fsmBehaviour);
+
+ if (currentTarget == null)
+ {
+ if (enableDebug) Debug.LogWarning("[SA_FleeFromPlayer] No target found!");
+ return;
+ }
+
+ // Calculate initial flee direction
+ currentFleeDirection = CalculateFleeDirection(fsmBehaviour);
+
+ // Set AI to sprint if enabled
+ var aiController = fsmBehaviour as Invector.vCharacterController.AI.vIControlAI;
+ if (aiController != null && useSprint)
+ {
+ }
+
+ if (enableDebug) Debug.Log("[SA_FleeFromPlayer] Started fleeing from player");
+ }
+
+ private void OnUpdate(vIFSMBehaviourController fsmBehaviour)
+ {
+ if (currentTarget == null)
+ {
+ currentTarget = GetTarget(fsmBehaviour);
+ if (currentTarget == null) return;
+ }
+
+ // Recalculate flee direction periodically
+ currentFleeDirection = CalculateFleeDirection(fsmBehaviour);
+
+ // Move in flee direction
+ MoveInDirection(fsmBehaviour, currentFleeDirection);
+
+ // Face player while fleeing if enabled
+ if (facePlayer)
+ {
+ RotateTowardsPlayer(fsmBehaviour);
+ }
+ }
+
+ private void OnExit(vIFSMBehaviourController fsmBehaviour)
+ {
+ // Reset AI speed
+ var aiController = fsmBehaviour as Invector.vCharacterController.AI.vIControlAI;
+ if (aiController != null)
+ {
+ }
+
+ if (enableDebug) Debug.Log("[SA_FleeFromPlayer] Stopped fleeing");
+ }
+
+ ///
+ /// Calculates the best direction to flee
+ ///
+ private Vector3 CalculateFleeDirection(vIFSMBehaviourController fsmBehaviour)
+ {
+ Vector3 archerPos = fsmBehaviour.transform.position;
+ Vector3 playerPos = currentTarget.position;
+
+ // Basic flee direction: away from player
+ Vector3 awayFromPlayer = (archerPos - playerPos).normalized;
+
+ // If not avoiding obstacles, return simple direction
+ if (!avoidObstacles)
+ {
+ return awayFromPlayer;
+ }
+
+ // Try to find clear path
+ Vector3 bestDirection = awayFromPlayer;
+ float bestScore = EvaluateDirection(archerPos, awayFromPlayer, playerPos);
+
+ // Sample multiple directions around the flee vector
+ for (int i = 0; i < directionSamples; i++)
+ {
+ float angle = (360f / directionSamples) * i;
+ Vector3 testDirection = Quaternion.Euler(0f, angle, 0f) * awayFromPlayer;
+ float score = EvaluateDirection(archerPos, testDirection, playerPos);
+
+ if (score > bestScore)
+ {
+ bestScore = score;
+ bestDirection = testDirection;
+ }
+ }
+
+ return bestDirection;
+ }
+
+ ///
+ /// Evaluates how good a flee direction is (higher = better)
+ ///
+ private float EvaluateDirection(Vector3 from, Vector3 direction, Vector3 playerPos)
+ {
+ float score = 0f;
+
+ // Check if path is clear
+ Ray ray = new Ray(from + Vector3.up * 0.5f, direction);
+ bool isBlocked = Physics.Raycast(ray, fleeDistance, obstacleMask, QueryTriggerInteraction.Ignore);
+
+ if (!isBlocked)
+ {
+ score += 10f; // Big bonus for clear path
+ }
+
+ // Prefer directions that move away from player
+ Vector3 awayFromPlayer = (from - playerPos).normalized;
+ float alignment = Vector3.Dot(direction, awayFromPlayer);
+ score += alignment * 5f;
+
+ // Check destination height (avoid running off cliffs)
+ Vector3 destination = from + direction * fleeDistance;
+ if (Physics.Raycast(destination + Vector3.up * 2f, Vector3.down, 5f, obstacleMask))
+ {
+ score += 3f; // Bonus for having ground
+ }
+
+ return score;
+ }
+
+ ///
+ /// Moves archer in specified direction
+ ///
+ private void MoveInDirection(vIFSMBehaviourController fsmBehaviour, Vector3 direction)
+ {
+ var aiController = fsmBehaviour as Invector.vCharacterController.AI.vIControlAI;
+
+ if (aiController == null) return;
+
+ // Calculate destination point
+ Vector3 destination = fsmBehaviour.transform.position + direction * fleeDistance;
+
+ // Use Invector's AI movement
+ aiController.MoveTo(destination);
+
+ // Apply speed multiplier
+ var motor = aiController as Invector.vCharacterController.AI.vSimpleMeleeAI_Controller;
+ if (motor != null)
+ {
+ // This would need to be adapted to your specific Invector version
+ // The goal is to make the archer move faster while fleeing
+ }
+ }
+
+ ///
+ /// Rotates archer to face player while backing away
+ ///
+ private void RotateTowardsPlayer(vIFSMBehaviourController fsmBehaviour)
+ {
+ if (currentTarget == null) return;
+
+ Vector3 directionToPlayer = (currentTarget.position - fsmBehaviour.transform.position);
+ directionToPlayer.y = 0f;
+
+ if (directionToPlayer.sqrMagnitude > 0.001f)
+ {
+ Quaternion targetRotation = Quaternion.LookRotation(directionToPlayer);
+ fsmBehaviour.transform.rotation = Quaternion.RotateTowards(
+ fsmBehaviour.transform.rotation,
+ targetRotation,
+ turnSpeed * Time.deltaTime
+ );
+ }
+ }
+
+ 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;
+ }
+
+#if UNITY_EDITOR
+
+ private void OnDrawGizmosSelected()
+ {
+ if (!showGizmos || !Application.isPlaying) return;
+
+ // Draw current flee direction
+ if (currentFleeDirection != Vector3.zero)
+ {
+ Gizmos.color = Color.red;
+ // This would need the archer's position to draw properly
+ }
+ }
+
+#endif
+ }
+}
\ No newline at end of file
diff --git a/Assets/AI/_Archer/SA_FleeFromPlayer.cs.meta b/Assets/AI/_Archer/SA_FleeFromPlayer.cs.meta
new file mode 100644
index 000000000..28acf3c0d
--- /dev/null
+++ b/Assets/AI/_Archer/SA_FleeFromPlayer.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: e5e9bb52e9254ac4895a3e9192dc2692
\ No newline at end of file
diff --git a/Assets/AI/_Archer/SA_ShootArrow.cs b/Assets/AI/_Archer/SA_ShootArrow.cs
new file mode 100644
index 000000000..b98d6dea6
--- /dev/null
+++ b/Assets/AI/_Archer/SA_ShootArrow.cs
@@ -0,0 +1,105 @@
+using Invector.vCharacterController.AI.FSMBehaviour;
+using UnityEngine;
+
+namespace ArcherEnemy
+{
+ ///
+ /// State action that makes archer shoot an arrow
+ /// Should be called in OnStateEnter or OnStateUpdate
+ ///
+ [CreateAssetMenu(menuName = "Invector/FSM/Actions/Archer/Shoot Arrow")]
+ public class SA_ShootArrow : vStateAction
+ {
+ public override string categoryName => "Archer/Combat";
+ public override string defaultName => "Shoot Arrow";
+
+ [Header("Configuration")]
+ [Tooltip("Shoot once per state enter, or continuously?")]
+ public bool shootOnce = true;
+
+ [Tooltip("Time between shots when shooting continuously")]
+ public float shootInterval = 2f;
+
+ [Header("Debug")]
+ [Tooltip("Enable debug logging")]
+ public bool enableDebug = false;
+
+ private float lastShootTime = -999f;
+ private bool hasShotThisState = false;
+
+ public override void DoAction(vIFSMBehaviourController fsmBehaviour, vFSMComponentExecutionType executionType = vFSMComponentExecutionType.OnStateUpdate)
+ {
+ if (executionType == vFSMComponentExecutionType.OnStateEnter)
+ {
+ OnEnter(fsmBehaviour);
+ }
+ else if (executionType == vFSMComponentExecutionType.OnStateUpdate)
+ {
+ OnUpdate(fsmBehaviour);
+ }
+ else if (executionType == vFSMComponentExecutionType.OnStateExit)
+ {
+ OnExit(fsmBehaviour);
+ }
+ }
+
+ private void OnEnter(vIFSMBehaviourController fsmBehaviour)
+ {
+ hasShotThisState = false;
+
+ if (shootOnce)
+ {
+ TryShoot(fsmBehaviour);
+ }
+ }
+
+ private void OnUpdate(vIFSMBehaviourController fsmBehaviour)
+ {
+ if (shootOnce && hasShotThisState)
+ {
+ return; // Already shot once this state
+ }
+
+ // Check interval for continuous shooting
+ if (!shootOnce && Time.time >= lastShootTime + shootInterval)
+ {
+ TryShoot(fsmBehaviour);
+ }
+ }
+
+ private void OnExit(vIFSMBehaviourController fsmBehaviour)
+ {
+ // Stop any shooting sequence
+ var shootingAI = fsmBehaviour.gameObject.GetComponent();
+ if (shootingAI != null)
+ {
+ shootingAI.StopShooting();
+ }
+ }
+
+ private void TryShoot(vIFSMBehaviourController fsmBehaviour)
+ {
+ var shootingAI = fsmBehaviour.gameObject.GetComponent();
+
+ if (shootingAI == null)
+ {
+ if (enableDebug) Debug.LogError("[SA_ShootArrow] No ArcherShootingAI component found!");
+ return;
+ }
+
+ // Attempt to shoot
+ if (shootingAI.CanShoot())
+ {
+ shootingAI.StartShooting();
+ hasShotThisState = true;
+ lastShootTime = Time.time;
+
+ if (enableDebug) Debug.Log("[SA_ShootArrow] Shooting arrow");
+ }
+ else
+ {
+ if (enableDebug) Debug.Log("[SA_ShootArrow] Cannot shoot - conditions not met");
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Assets/AI/_Archer/SA_ShootArrow.cs.meta b/Assets/AI/_Archer/SA_ShootArrow.cs.meta
new file mode 100644
index 000000000..47dab3d50
--- /dev/null
+++ b/Assets/AI/_Archer/SA_ShootArrow.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: cbef6f8d442ae24408f1132173eca9e0
\ No newline at end of file
diff --git a/Assets/AI/Archer/SM_Skeleton_Archer_Arrow.prefab b/Assets/AI/_Archer/SM_Skeleton_Archer_Arrow.prefab
similarity index 100%
rename from Assets/AI/Archer/SM_Skeleton_Archer_Arrow.prefab
rename to Assets/AI/_Archer/SM_Skeleton_Archer_Arrow.prefab
diff --git a/Assets/AI/Archer/SM_Skeleton_Archer_Arrow.prefab.meta b/Assets/AI/_Archer/SM_Skeleton_Archer_Arrow.prefab.meta
similarity index 100%
rename from Assets/AI/Archer/SM_Skeleton_Archer_Arrow.prefab.meta
rename to Assets/AI/_Archer/SM_Skeleton_Archer_Arrow.prefab.meta
diff --git a/Assets/AI/Demon.meta b/Assets/AI/_Demon.meta
similarity index 100%
rename from Assets/AI/Demon.meta
rename to Assets/AI/_Demon.meta
diff --git a/Assets/AI/Demon/CrystalShooterAI.cs b/Assets/AI/_Demon/CrystalShooterAI.cs
similarity index 96%
rename from Assets/AI/Demon/CrystalShooterAI.cs
rename to Assets/AI/_Demon/CrystalShooterAI.cs
index 5bb05b369..1c85cf9e3 100644
--- a/Assets/AI/Demon/CrystalShooterAI.cs
+++ b/Assets/AI/_Demon/CrystalShooterAI.cs
@@ -1,290 +1,290 @@
-using Lean.Pool;
-using System.Collections;
-using UnityEngine;
-
-namespace DemonBoss.Magic
-{
- public class CrystalShooterAI : MonoBehaviour
- {
- [Header("Shooting Configuration")]
- [Tooltip("Transform point from which projectiles are fired")]
- public Transform muzzle;
-
- [Tooltip("Fireball prefab (projectile with its own targeting logic)")]
- public GameObject fireballPrefab;
-
- [Tooltip("Base seconds between shots")]
- public float fireRate = 0.7f;
-
- [Tooltip("Maximum number of shots before auto-despawn")]
- public int maxShots = 10;
-
- [Tooltip("Wait time after last shot before despawn")]
- public float despawnDelay = 3f;
-
- [Header("Randomization (Desync)")]
- [Tooltip("Random initial delay after spawn to desync turrets (seconds)")]
- public Vector2 initialStaggerRange = new Vector2(0.0f, 0.6f);
-
- [Tooltip("Per-shot random jitter added to fireRate (seconds). Range x means +/- x.")]
- public float fireRateJitter = 0.2f;
-
- [Tooltip("Aim wobble in degrees (0 = disabled). Small value adds natural dispersion.")]
- public float aimJitterDegrees = 0f;
-
- [Header("Rotation Configuration")]
- [Tooltip("Yaw rotation speed in degrees per second")]
- public float turnSpeed = 120f;
-
- [Tooltip("Idle spin speed when no target (degrees/s, 0 = disabled)")]
- public float idleSpinSpeed = 30f;
-
- [Tooltip("Aiming accuracy in degrees (smaller = stricter)")]
- public float aimTolerance = 5f;
-
- [Header("Targeting (Turret-Side Only)")]
- [Tooltip("Auto-find player on start by tag")]
- public bool autoFindPlayer = true;
-
- [Tooltip("Player tag to search for")]
- public string playerTag = "Player";
-
- [Tooltip("Max range for allowing shots")]
- public float maxShootingRange = 50f;
-
- [Header("Effects")]
- [Tooltip("Enable or disable muzzle flash & sound effects when firing")]
- public bool useShootEffects = true;
-
- [Tooltip("Particle effect at shot (pooled)")]
- public GameObject muzzleFlashPrefab;
-
- [Tooltip("Shoot sound (played on AudioSource)")]
- public AudioClip shootSound;
-
- [Header("Debug")]
- [Tooltip("Enable debug logging")]
- public bool enableDebug = false;
-
- [Tooltip("Show gizmos in Scene View")]
- public bool showGizmos = true;
-
- private Transform target;
- private AudioSource audioSource;
- private Coroutine shootingCoroutine;
- private bool isActive = false;
- private int shotsFired = 0;
- private float lastShotTime = 0f;
- private Transform crystalTransform;
-
- private void Awake()
- {
- crystalTransform = transform;
-
- audioSource = GetComponent();
- if (audioSource == null && shootSound != null)
- {
- audioSource = gameObject.AddComponent();
- audioSource.playOnAwake = false;
- audioSource.spatialBlend = 1f;
- }
-
- if (muzzle == null)
- {
- Transform muzzleChild = crystalTransform.Find("muzzle");
- muzzle = muzzleChild != null ? muzzleChild : crystalTransform;
- }
- }
-
- private void Start()
- {
- if (autoFindPlayer && target == null)
- FindPlayer();
-
- StartShooting();
- }
-
- /// Update tick: rotate towards target or idle spin.
- private void Update()
- {
- if (!isActive) return;
- RotateTowardsTarget();
- }
-
- /// Attempts to find the player by tag (for turret-only aiming).
- private void FindPlayer()
- {
- GameObject player = GameObject.FindGameObjectWithTag(playerTag);
- if (player != null) SetTarget(player.transform);
- }
-
- /// Sets the turret's aiming target (does NOT propagate to projectiles).
- public void SetTarget(Transform newTarget) => target = newTarget;
-
- /// Starts the timed shooting routine (fires until maxShots, then despawns).
- public void StartShooting()
- {
- if (isActive) return;
- isActive = true;
- shotsFired = 0;
- shootingCoroutine = StartCoroutine(ShootingCoroutine());
- }
-
- /// Stops the shooting routine immediately.
- public void StopShooting()
- {
- isActive = false;
- if (shootingCoroutine != null)
- {
- StopCoroutine(shootingCoroutine);
- shootingCoroutine = null;
- }
- }
-
- ///
- /// Main shooting loop with initial spawn stagger and per-shot jitter.
- ///
- private IEnumerator ShootingCoroutine()
- {
- // 1) Initial stagger so multiple crystals don't start at the same frame
- if (initialStaggerRange.y > 0f)
- {
- float stagger = Random.Range(initialStaggerRange.x, initialStaggerRange.y);
- if (stagger > 0f) yield return new WaitForSeconds(stagger);
- }
-
- // 2) Normal loop with CanShoot gate and per-shot jittered waits
- while (shotsFired < maxShots && isActive)
- {
- if (CanShoot())
- {
- FireFireball();
- shotsFired++;
- lastShotTime = Time.time;
- }
-
- float wait = fireRate + (fireRateJitter > 0f ? Random.Range(-fireRateJitter, fireRateJitter) : 0f);
- // Clamp wait to something safe so it never becomes non-positive
- if (wait < 0.05f) wait = 0.05f;
- yield return new WaitForSeconds(wait);
- }
-
- yield return new WaitForSeconds(despawnDelay);
- DespawnCrystal();
- }
-
- /// Aiming/range gate for firing.
- private bool CanShoot()
- {
- if (target == null) return false;
-
- float distanceToTarget = Vector3.Distance(crystalTransform.position, target.position);
- if (distanceToTarget > maxShootingRange) return false;
-
- Vector3 directionToTarget = (target.position - crystalTransform.position).normalized;
- float angleToTarget = Vector3.Angle(crystalTransform.forward, directionToTarget);
-
- return angleToTarget <= aimTolerance;
- }
-
- /// Spawns a fireball oriented towards the turret's current aim direction with optional dispersion.
- private void FireFireball()
- {
- if (fireballPrefab == null || muzzle == null) return;
-
- Vector3 shootDirection;
- if (target != null)
- {
- Vector3 targetCenter = target.position + Vector3.up * 1f;
- shootDirection = (targetCenter - muzzle.position).normalized;
- }
- else shootDirection = crystalTransform.forward;
-
- // Apply small aim jitter (random yaw/pitch) to avoid perfect sync volleys
- if (aimJitterDegrees > 0f)
- {
- float yaw = Random.Range(-aimJitterDegrees, aimJitterDegrees);
- float pitch = Random.Range(-aimJitterDegrees * 0.5f, aimJitterDegrees * 0.5f); // usually less pitch dispersion
- shootDirection = Quaternion.Euler(pitch, yaw, 0f) * shootDirection;
- }
-
- Vector3 spawnPosition = muzzle.position;
- Quaternion spawnRotation = Quaternion.LookRotation(shootDirection);
-
- LeanPool.Spawn(fireballPrefab, spawnPosition, spawnRotation);
- PlayShootEffects();
- }
-
- /// Plays muzzle VFX and shoot SFX (if enabled).
- private void PlayShootEffects()
- {
- if (!useShootEffects) return;
-
- if (muzzleFlashPrefab != null && muzzle != null)
- {
- GameObject flash = LeanPool.Spawn(muzzleFlashPrefab, muzzle.position, muzzle.rotation);
- LeanPool.Despawn(flash, 2f);
- }
-
- if (audioSource != null && shootSound != null)
- audioSource.PlayOneShot(shootSound);
- }
-
- /// Smooth yaw rotation towards target; idles by spinning when no target.
- private void RotateTowardsTarget()
- {
- if (target != null)
- {
- Vector3 directionToTarget = target.position - crystalTransform.position;
- directionToTarget.y = 0f;
-
- if (directionToTarget != Vector3.zero)
- {
- Quaternion targetRotation = Quaternion.LookRotation(directionToTarget);
- crystalTransform.rotation = Quaternion.RotateTowards(
- crystalTransform.rotation,
- targetRotation,
- turnSpeed * Time.deltaTime
- );
- }
- }
- else if (idleSpinSpeed > 0f)
- {
- crystalTransform.Rotate(Vector3.up, idleSpinSpeed * Time.deltaTime);
- }
- }
-
- /// Despawns the turret via Lean Pool.
- public void DespawnCrystal()
- {
- StopShooting();
- LeanPool.Despawn(gameObject);
- }
-
- /// Forces immediate despawn (e.g., boss death).
- public void ForceDespawn() => DespawnCrystal();
-
- /// Returns crystal state information.
- public bool IsActive() => isActive;
-
- public int GetShotsFired() => shotsFired;
-
- public int GetRemainingShots() => Mathf.Max(0, maxShots - shotsFired);
-
- public float GetTimeSinceLastShot() => Time.time - lastShotTime;
-
- private void OnDrawGizmosSelected()
- {
- if (!showGizmos) return;
-
- Gizmos.color = Color.red;
- Gizmos.DrawWireSphere(transform.position, maxShootingRange);
-
- if (muzzle != null)
- {
- Gizmos.color = Color.blue;
- Gizmos.DrawWireSphere(muzzle.position, 0.2f);
- }
- }
- }
+using Lean.Pool;
+using System.Collections;
+using UnityEngine;
+
+namespace DemonBoss.Magic
+{
+ public class CrystalShooterAI : MonoBehaviour
+ {
+ [Header("Shooting Configuration")]
+ [Tooltip("Transform point from which projectiles are fired")]
+ public Transform muzzle;
+
+ [Tooltip("Fireball prefab (projectile with its own targeting logic)")]
+ public GameObject fireballPrefab;
+
+ [Tooltip("Base seconds between shots")]
+ public float fireRate = 0.7f;
+
+ [Tooltip("Maximum number of shots before auto-despawn")]
+ public int maxShots = 10;
+
+ [Tooltip("Wait time after last shot before despawn")]
+ public float despawnDelay = 3f;
+
+ [Header("Randomization (Desync)")]
+ [Tooltip("Random initial delay after spawn to desync turrets (seconds)")]
+ public Vector2 initialStaggerRange = new Vector2(0.0f, 0.6f);
+
+ [Tooltip("Per-shot random jitter added to fireRate (seconds). Range x means +/- x.")]
+ public float fireRateJitter = 0.2f;
+
+ [Tooltip("Aim wobble in degrees (0 = disabled). Small value adds natural dispersion.")]
+ public float aimJitterDegrees = 0f;
+
+ [Header("Rotation Configuration")]
+ [Tooltip("Yaw rotation speed in degrees per second")]
+ public float turnSpeed = 120f;
+
+ [Tooltip("Idle spin speed when no target (degrees/s, 0 = disabled)")]
+ public float idleSpinSpeed = 30f;
+
+ [Tooltip("Aiming accuracy in degrees (smaller = stricter)")]
+ public float aimTolerance = 5f;
+
+ [Header("Targeting (Turret-Side Only)")]
+ [Tooltip("Auto-find player on start by tag")]
+ public bool autoFindPlayer = true;
+
+ [Tooltip("Player tag to search for")]
+ public string playerTag = "Player";
+
+ [Tooltip("Max range for allowing shots")]
+ public float maxShootingRange = 50f;
+
+ [Header("Effects")]
+ [Tooltip("Enable or disable muzzle flash & sound effects when firing")]
+ public bool useShootEffects = true;
+
+ [Tooltip("Particle effect at shot (pooled)")]
+ public GameObject muzzleFlashPrefab;
+
+ [Tooltip("Shoot sound (played on AudioSource)")]
+ public AudioClip shootSound;
+
+ [Header("Debug")]
+ [Tooltip("Enable debug logging")]
+ public bool enableDebug = false;
+
+ [Tooltip("Show gizmos in Scene View")]
+ public bool showGizmos = true;
+
+ private Transform target;
+ private AudioSource audioSource;
+ private Coroutine shootingCoroutine;
+ private bool isActive = false;
+ private int shotsFired = 0;
+ private float lastShotTime = 0f;
+ private Transform crystalTransform;
+
+ private void Awake()
+ {
+ crystalTransform = transform;
+
+ audioSource = GetComponent();
+ if (audioSource == null && shootSound != null)
+ {
+ audioSource = gameObject.AddComponent();
+ audioSource.playOnAwake = false;
+ audioSource.spatialBlend = 1f;
+ }
+
+ if (muzzle == null)
+ {
+ Transform muzzleChild = crystalTransform.Find("muzzle");
+ muzzle = muzzleChild != null ? muzzleChild : crystalTransform;
+ }
+ }
+
+ private void Start()
+ {
+ if (autoFindPlayer && target == null)
+ FindPlayer();
+
+ StartShooting();
+ }
+
+ /// Update tick: rotate towards target or idle spin.
+ private void Update()
+ {
+ if (!isActive) return;
+ RotateTowardsTarget();
+ }
+
+ /// Attempts to find the player by tag (for turret-only aiming).
+ private void FindPlayer()
+ {
+ GameObject player = GameObject.FindGameObjectWithTag(playerTag);
+ if (player != null) SetTarget(player.transform);
+ }
+
+ /// Sets the turret's aiming target (does NOT propagate to projectiles).
+ public void SetTarget(Transform newTarget) => target = newTarget;
+
+ /// Starts the timed shooting routine (fires until maxShots, then despawns).
+ public void StartShooting()
+ {
+ if (isActive) return;
+ isActive = true;
+ shotsFired = 0;
+ shootingCoroutine = StartCoroutine(ShootingCoroutine());
+ }
+
+ /// Stops the shooting routine immediately.
+ public void StopShooting()
+ {
+ isActive = false;
+ if (shootingCoroutine != null)
+ {
+ StopCoroutine(shootingCoroutine);
+ shootingCoroutine = null;
+ }
+ }
+
+ ///
+ /// Main shooting loop with initial spawn stagger and per-shot jitter.
+ ///
+ private IEnumerator ShootingCoroutine()
+ {
+ // 1) Initial stagger so multiple crystals don't start at the same frame
+ if (initialStaggerRange.y > 0f)
+ {
+ float stagger = Random.Range(initialStaggerRange.x, initialStaggerRange.y);
+ if (stagger > 0f) yield return new WaitForSeconds(stagger);
+ }
+
+ // 2) Normal loop with CanShoot gate and per-shot jittered waits
+ while (shotsFired < maxShots && isActive)
+ {
+ if (CanShoot())
+ {
+ FireFireball();
+ shotsFired++;
+ lastShotTime = Time.time;
+ }
+
+ float wait = fireRate + (fireRateJitter > 0f ? Random.Range(-fireRateJitter, fireRateJitter) : 0f);
+ // Clamp wait to something safe so it never becomes non-positive
+ if (wait < 0.05f) wait = 0.05f;
+ yield return new WaitForSeconds(wait);
+ }
+
+ yield return new WaitForSeconds(despawnDelay);
+ DespawnCrystal();
+ }
+
+ /// Aiming/range gate for firing.
+ private bool CanShoot()
+ {
+ if (target == null) return false;
+
+ float distanceToTarget = Vector3.Distance(crystalTransform.position, target.position);
+ if (distanceToTarget > maxShootingRange) return false;
+
+ Vector3 directionToTarget = (target.position - crystalTransform.position).normalized;
+ float angleToTarget = Vector3.Angle(crystalTransform.forward, directionToTarget);
+
+ return angleToTarget <= aimTolerance;
+ }
+
+ /// Spawns a fireball oriented towards the turret's current aim direction with optional dispersion.
+ private void FireFireball()
+ {
+ if (fireballPrefab == null || muzzle == null) return;
+
+ Vector3 shootDirection;
+ if (target != null)
+ {
+ Vector3 targetCenter = target.position + Vector3.up * 1f;
+ shootDirection = (targetCenter - muzzle.position).normalized;
+ }
+ else shootDirection = crystalTransform.forward;
+
+ // Apply small aim jitter (random yaw/pitch) to avoid perfect sync volleys
+ if (aimJitterDegrees > 0f)
+ {
+ float yaw = Random.Range(-aimJitterDegrees, aimJitterDegrees);
+ float pitch = Random.Range(-aimJitterDegrees * 0.5f, aimJitterDegrees * 0.5f); // usually less pitch dispersion
+ shootDirection = Quaternion.Euler(pitch, yaw, 0f) * shootDirection;
+ }
+
+ Vector3 spawnPosition = muzzle.position;
+ Quaternion spawnRotation = Quaternion.LookRotation(shootDirection);
+
+ LeanPool.Spawn(fireballPrefab, spawnPosition, spawnRotation);
+ PlayShootEffects();
+ }
+
+ /// Plays muzzle VFX and shoot SFX (if enabled).
+ private void PlayShootEffects()
+ {
+ if (!useShootEffects) return;
+
+ if (muzzleFlashPrefab != null && muzzle != null)
+ {
+ GameObject flash = LeanPool.Spawn(muzzleFlashPrefab, muzzle.position, muzzle.rotation);
+ LeanPool.Despawn(flash, 2f);
+ }
+
+ if (audioSource != null && shootSound != null)
+ audioSource.PlayOneShot(shootSound);
+ }
+
+ /// Smooth yaw rotation towards target; idles by spinning when no target.
+ private void RotateTowardsTarget()
+ {
+ if (target != null)
+ {
+ Vector3 directionToTarget = target.position - crystalTransform.position;
+ directionToTarget.y = 0f;
+
+ if (directionToTarget != Vector3.zero)
+ {
+ Quaternion targetRotation = Quaternion.LookRotation(directionToTarget);
+ crystalTransform.rotation = Quaternion.RotateTowards(
+ crystalTransform.rotation,
+ targetRotation,
+ turnSpeed * Time.deltaTime
+ );
+ }
+ }
+ else if (idleSpinSpeed > 0f)
+ {
+ crystalTransform.Rotate(Vector3.up, idleSpinSpeed * Time.deltaTime);
+ }
+ }
+
+ /// Despawns the turret via Lean Pool.
+ public void DespawnCrystal()
+ {
+ StopShooting();
+ LeanPool.Despawn(gameObject);
+ }
+
+ /// Forces immediate despawn (e.g., boss death).
+ public void ForceDespawn() => DespawnCrystal();
+
+ /// Returns crystal state information.
+ public bool IsActive() => isActive;
+
+ public int GetShotsFired() => shotsFired;
+
+ public int GetRemainingShots() => Mathf.Max(0, maxShots - shotsFired);
+
+ public float GetTimeSinceLastShot() => Time.time - lastShotTime;
+
+ private void OnDrawGizmosSelected()
+ {
+ if (!showGizmos) return;
+
+ Gizmos.color = Color.red;
+ Gizmos.DrawWireSphere(transform.position, maxShootingRange);
+
+ if (muzzle != null)
+ {
+ Gizmos.color = Color.blue;
+ Gizmos.DrawWireSphere(muzzle.position, 0.2f);
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/Assets/AI/Demon/CrystalShooterAI.cs.meta b/Assets/AI/_Demon/CrystalShooterAI.cs.meta
similarity index 100%
rename from Assets/AI/Demon/CrystalShooterAI.cs.meta
rename to Assets/AI/_Demon/CrystalShooterAI.cs.meta
diff --git a/Assets/AI/Demon/Crystals_red 1.mat b/Assets/AI/_Demon/Crystals_red 1.mat
similarity index 100%
rename from Assets/AI/Demon/Crystals_red 1.mat
rename to Assets/AI/_Demon/Crystals_red 1.mat
diff --git a/Assets/AI/Demon/Crystals_red 1.mat.meta b/Assets/AI/_Demon/Crystals_red 1.mat.meta
similarity index 100%
rename from Assets/AI/Demon/Crystals_red 1.mat.meta
rename to Assets/AI/_Demon/Crystals_red 1.mat.meta
diff --git a/Assets/AI/Demon/Crystals_red 2.mat b/Assets/AI/_Demon/Crystals_red 2.mat
similarity index 100%
rename from Assets/AI/Demon/Crystals_red 2.mat
rename to Assets/AI/_Demon/Crystals_red 2.mat
diff --git a/Assets/AI/Demon/Crystals_red 2.mat.meta b/Assets/AI/_Demon/Crystals_red 2.mat.meta
similarity index 100%
rename from Assets/AI/Demon/Crystals_red 2.mat.meta
rename to Assets/AI/_Demon/Crystals_red 2.mat.meta
diff --git a/Assets/AI/Demon/DEC_CheckCooldown.cs b/Assets/AI/_Demon/DEC_CheckCooldown.cs
similarity index 96%
rename from Assets/AI/Demon/DEC_CheckCooldown.cs
rename to Assets/AI/_Demon/DEC_CheckCooldown.cs
index a99fb5fad..bfa3ad2e3 100644
--- a/Assets/AI/Demon/DEC_CheckCooldown.cs
+++ b/Assets/AI/_Demon/DEC_CheckCooldown.cs
@@ -1,205 +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;
- }
- }
+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
similarity index 100%
rename from Assets/AI/Demon/DEC_CheckCooldown.cs.meta
rename to Assets/AI/_Demon/DEC_CheckCooldown.cs.meta
diff --git a/Assets/AI/Demon/DEC_TargetClearSky.cs b/Assets/AI/_Demon/DEC_TargetClearSky.cs
similarity index 96%
rename from Assets/AI/Demon/DEC_TargetClearSky.cs
rename to Assets/AI/_Demon/DEC_TargetClearSky.cs
index 80417519d..95ab638f3 100644
--- a/Assets/AI/Demon/DEC_TargetClearSky.cs
+++ b/Assets/AI/_Demon/DEC_TargetClearSky.cs
@@ -1,105 +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);
- }
- }
+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
similarity index 100%
rename from Assets/AI/Demon/DEC_TargetClearSky.cs.meta
rename to Assets/AI/_Demon/DEC_TargetClearSky.cs.meta
diff --git a/Assets/AI/Demon/DEC_TargetFar.cs b/Assets/AI/_Demon/DEC_TargetFar.cs
similarity index 96%
rename from Assets/AI/Demon/DEC_TargetFar.cs
rename to Assets/AI/_Demon/DEC_TargetFar.cs
index db886eb66..f73103635 100644
--- a/Assets/AI/Demon/DEC_TargetFar.cs
+++ b/Assets/AI/_Demon/DEC_TargetFar.cs
@@ -1,58 +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;
- }
- }
+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
similarity index 100%
rename from Assets/AI/Demon/DEC_TargetFar.cs.meta
rename to Assets/AI/_Demon/DEC_TargetFar.cs.meta
diff --git a/Assets/AI/Demon/DemonShield.prefab b/Assets/AI/_Demon/DemonShield.prefab
similarity index 100%
rename from Assets/AI/Demon/DemonShield.prefab
rename to Assets/AI/_Demon/DemonShield.prefab
diff --git a/Assets/AI/Demon/DemonShield.prefab.meta b/Assets/AI/_Demon/DemonShield.prefab.meta
similarity index 100%
rename from Assets/AI/Demon/DemonShield.prefab.meta
rename to Assets/AI/_Demon/DemonShield.prefab.meta
diff --git a/Assets/AI/Demon/DestroyableTurret.cs b/Assets/AI/_Demon/DestroyableTurret.cs
similarity index 100%
rename from Assets/AI/Demon/DestroyableTurret.cs
rename to Assets/AI/_Demon/DestroyableTurret.cs
diff --git a/Assets/AI/Demon/DestroyableTurret.cs.meta b/Assets/AI/_Demon/DestroyableTurret.cs.meta
similarity index 100%
rename from Assets/AI/Demon/DestroyableTurret.cs.meta
rename to Assets/AI/_Demon/DestroyableTurret.cs.meta
diff --git a/Assets/AI/Demon/FireBall new.prefab b/Assets/AI/_Demon/FireBall new.prefab
similarity index 100%
rename from Assets/AI/Demon/FireBall new.prefab
rename to Assets/AI/_Demon/FireBall new.prefab
diff --git a/Assets/AI/Demon/FireBall new.prefab.meta b/Assets/AI/_Demon/FireBall new.prefab.meta
similarity index 100%
rename from Assets/AI/Demon/FireBall new.prefab.meta
rename to Assets/AI/_Demon/FireBall new.prefab.meta
diff --git a/Assets/AI/Demon/FireBall.prefab b/Assets/AI/_Demon/FireBall.prefab
similarity index 100%
rename from Assets/AI/Demon/FireBall.prefab
rename to Assets/AI/_Demon/FireBall.prefab
diff --git a/Assets/AI/Demon/FireBall.prefab.meta b/Assets/AI/_Demon/FireBall.prefab.meta
similarity index 100%
rename from Assets/AI/Demon/FireBall.prefab.meta
rename to Assets/AI/_Demon/FireBall.prefab.meta
diff --git a/Assets/AI/Demon/FireballProjectile.cs b/Assets/AI/_Demon/FireballProjectile.cs
similarity index 100%
rename from Assets/AI/Demon/FireballProjectile.cs
rename to Assets/AI/_Demon/FireballProjectile.cs
diff --git a/Assets/AI/Demon/FireballProjectile.cs.meta b/Assets/AI/_Demon/FireballProjectile.cs.meta
similarity index 100%
rename from Assets/AI/Demon/FireballProjectile.cs.meta
rename to Assets/AI/_Demon/FireballProjectile.cs.meta
diff --git a/Assets/AI/Demon/GhostDemon.prefab b/Assets/AI/_Demon/GhostDemon.prefab
similarity index 100%
rename from Assets/AI/Demon/GhostDemon.prefab
rename to Assets/AI/_Demon/GhostDemon.prefab
diff --git a/Assets/AI/Demon/GhostDemon.prefab.meta b/Assets/AI/_Demon/GhostDemon.prefab.meta
similarity index 100%
rename from Assets/AI/Demon/GhostDemon.prefab.meta
rename to Assets/AI/_Demon/GhostDemon.prefab.meta
diff --git a/Assets/AI/Demon/Meteor.prefab b/Assets/AI/_Demon/Meteor.prefab
similarity index 100%
rename from Assets/AI/Demon/Meteor.prefab
rename to Assets/AI/_Demon/Meteor.prefab
diff --git a/Assets/AI/Demon/Meteor.prefab.meta b/Assets/AI/_Demon/Meteor.prefab.meta
similarity index 100%
rename from Assets/AI/Demon/Meteor.prefab.meta
rename to Assets/AI/_Demon/Meteor.prefab.meta
diff --git a/Assets/AI/Demon/MeteorProjectile.cs b/Assets/AI/_Demon/MeteorProjectile.cs
similarity index 100%
rename from Assets/AI/Demon/MeteorProjectile.cs
rename to Assets/AI/_Demon/MeteorProjectile.cs
diff --git a/Assets/AI/Demon/MeteorProjectile.cs.meta b/Assets/AI/_Demon/MeteorProjectile.cs.meta
similarity index 100%
rename from Assets/AI/Demon/MeteorProjectile.cs.meta
rename to Assets/AI/_Demon/MeteorProjectile.cs.meta
diff --git a/Assets/AI/Demon/SA_CallMeteor.cs b/Assets/AI/_Demon/SA_CallMeteor.cs
similarity index 96%
rename from Assets/AI/Demon/SA_CallMeteor.cs
rename to Assets/AI/_Demon/SA_CallMeteor.cs
index 1e4dc7e40..b325f409c 100644
--- a/Assets/AI/Demon/SA_CallMeteor.cs
+++ b/Assets/AI/_Demon/SA_CallMeteor.cs
@@ -1,359 +1,359 @@
-using Invector.vCharacterController.AI.FSMBehaviour;
-using Lean.Pool;
-using UnityEngine;
-using UnityEngine.Animations;
-using UnityEngine.Playables;
-
-namespace DemonBoss.Magic
-{
- ///
- /// Spawns multiple meteors behind the BOSS and launches them toward the player's position.
- ///
- [CreateAssetMenu(menuName = "Invector/FSM/Actions/DemonBoss/Call Meteor")]
- public class SA_CallMeteor : vStateAction
- {
- public override string categoryName => "DemonBoss/Magic";
- public override string defaultName => "Call Meteor";
-
- [Header("Meteor Setup")]
- [Tooltip("Prefab with MeteorProjectile component")]
- public GameObject meteorPrefab;
-
- [Tooltip("Distance behind the BOSS to spawn meteor (meters)")]
- public float behindBossDistance = 3f;
-
- [Tooltip("Height above the BOSS to spawn meteor (meters)")]
- public float aboveBossHeight = 8f;
-
- [Header("Multi-Meteor Configuration")]
- [Tooltip("Number of meteors to spawn in sequence")]
- public int meteorCount = 5;
-
- [Tooltip("Delay before first meteor spawns (wind-up)")]
- public float initialCastDelay = 0.4f;
-
- [Tooltip("Time between each meteor spawn")]
- public float meteorSpawnInterval = 0.6f;
-
- [Header("Muzzle Flash Effect")]
- [Tooltip("Particle effect prefab for muzzle flash at spawn position")]
- public GameObject muzzleFlashPrefab;
-
- [Tooltip("Duration to keep muzzle flash alive (seconds)")]
- public float muzzleFlashDuration = 1.5f;
-
- [Header("Targeting")]
- [Tooltip("Tag used to find the target (usually Player)")]
- public string targetTag = "Player";
-
- [Header("One-off Overlay Clip (No Animator Params)")]
- public AnimationClip overlayClip;
-
- [Tooltip("Playback speed (1 = normal)")] public float overlaySpeed = 1f;
- [Tooltip("Blend-in seconds (instant in this minimal impl)")] public float overlayFadeIn = 0.10f;
- [Tooltip("Blend-out seconds (instant in this minimal impl)")] public float overlayFadeOut = 0.10f;
-
- [Header("Debug")]
- public bool enableDebug = false;
-
- private Transform _boss;
- private Transform _target;
-
- // --- Multi-meteor state ---
- private int _meteorsSpawned = 0;
-
- private bool _spawningActive = false;
-
- // --- Playables runtime ---
- private PlayableGraph _overlayGraph;
-
- private AnimationPlayableOutput _overlayOutput;
- private AnimationClipPlayable _overlayPlayable;
- private bool _overlayPlaying;
- private float _overlayStopAtTime;
-
- public override void DoAction(vIFSMBehaviourController fsm, vFSMComponentExecutionType execType = vFSMComponentExecutionType.OnStateUpdate)
- {
- if (execType == vFSMComponentExecutionType.OnStateEnter)
- {
- OnEnter(fsm);
- }
- else if (execType == vFSMComponentExecutionType.OnStateUpdate)
- {
- // Keep the state active until all meteors are spawned
- if (_spawningActive && _meteorsSpawned < meteorCount)
- {
- // Don't allow the state to exit while spawning
- return;
- }
-
- if (_overlayPlaying && Time.time >= _overlayStopAtTime)
- {
- StopOverlayWithFade();
- }
-
- // Only signal completion when all meteors are done AND overlay is finished
- if (!_spawningActive && !_overlayPlaying && _meteorsSpawned >= meteorCount)
- {
- if (enableDebug) Debug.Log($"[SA_CallMeteor] Sequence complete, ready to exit state");
- }
- }
- else if (execType == vFSMComponentExecutionType.OnStateExit)
- {
- OnExit(fsm);
- }
- }
-
- private void OnEnter(vIFSMBehaviourController fsm)
- {
- _boss = fsm.transform;
- _target = GameObject.FindGameObjectWithTag(targetTag)?.transform;
-
- if (_target == null)
- {
- if (enableDebug) Debug.LogWarning("[SA_CallMeteor] No target found – abort");
- return;
- }
-
- // Reset multi-meteor state
- _meteorsSpawned = 0;
- _spawningActive = true;
-
- // SET COOLDOWN IMMEDIATELY when meteor ability is used
- DEC_CheckCooldown.SetCooldownStatic(fsm, "Meteor", 80f);
-
- if (enableDebug) Debug.Log($"[SA_CallMeteor] Boss: {_boss.name}, Target: {_target.name}, Count: {meteorCount}");
-
- // Fire overlay clip (no Animator params)
- PlayOverlayOnce(_boss);
-
- // Start the meteor sequence
- if (initialCastDelay > 0f)
- _boss.gameObject.AddComponent().Init(initialCastDelay, StartMeteorSequence);
- else
- StartMeteorSequence();
- }
-
- private void OnExit(vIFSMBehaviourController fsm)
- {
- // Stop any active spawning
- _spawningActive = false;
- StopOverlayImmediate();
-
- // Clean up any DelayedInvokers attached to the boss
- var invokers = _boss?.GetComponents();
- if (invokers != null)
- {
- foreach (var invoker in invokers)
- {
- if (invoker != null) Object.Destroy(invoker);
- }
- }
-
- if (enableDebug) Debug.Log($"[SA_CallMeteor] State exited. Spawned {_meteorsSpawned}/{meteorCount} meteors");
- }
-
- private void StartMeteorSequence()
- {
- if (!_spawningActive) return;
- SpawnNextMeteor();
- }
-
- private void SpawnNextMeteor()
- {
- if (!_spawningActive || _meteorsSpawned >= meteorCount) return;
-
- if (_boss == null || _target == null)
- {
- if (enableDebug) Debug.LogError("[SA_CallMeteor] Missing boss or target reference");
- return;
- }
-
- SpawnSingleMeteor();
- _meteorsSpawned++;
-
- if (enableDebug) Debug.Log($"[SA_CallMeteor] Spawned meteor {_meteorsSpawned}/{meteorCount}");
-
- // Schedule next meteor if needed
- if (_meteorsSpawned < meteorCount && _spawningActive)
- {
- if (enableDebug) Debug.Log($"[SA_CallMeteor] Scheduling next meteor in {meteorSpawnInterval}s");
- _boss.gameObject.AddComponent().Init(meteorSpawnInterval, SpawnNextMeteor);
- }
- else
- {
- // All meteors spawned
- _spawningActive = false;
- if (enableDebug) Debug.Log("[SA_CallMeteor] All meteors spawned, sequence complete");
- }
- }
-
- private void SpawnSingleMeteor()
- {
- if (meteorPrefab == null)
- {
- if (enableDebug) Debug.LogError("[SA_CallMeteor] Missing meteorPrefab");
- return;
- }
-
- // Calculate spawn position: behind the BOSS + height
- Vector3 bossForward = _boss.forward.normalized;
- Vector3 behindBoss = _boss.position - (bossForward * behindBossDistance);
- Vector3 spawnPos = behindBoss + Vector3.up * aboveBossHeight;
-
- // Add slight randomization to spawn position for multiple meteors
- if (_meteorsSpawned > 0)
- {
- Vector3 randomOffset = new Vector3(
- Random.Range(-1f, 1f),
- Random.Range(-0.5f, 0.5f),
- Random.Range(-1f, 1f)
- );
- spawnPos += randomOffset;
- }
-
- if (enableDebug) Debug.Log($"[SA_CallMeteor] Spawning meteor #{_meteorsSpawned + 1} at: {spawnPos}");
-
- // Spawn muzzle flash effect first
- SpawnMuzzleFlash(spawnPos);
-
- // Spawn the meteor
- var meteorGO = LeanPool.Spawn(meteorPrefab, spawnPos, Quaternion.identity);
-
- // Configure the projectile to target the player
- var meteorScript = meteorGO.GetComponent();
- if (meteorScript != null)
- {
- // Update target position for each meteor (player might be moving)
- Vector3 targetPos = _target.position;
-
- // Add slight prediction/leading for moving targets
- var playerRigidbody = _target.GetComponent();
- if (playerRigidbody != null)
- {
- Vector3 playerVelocity = playerRigidbody.linearVelocity;
- float estimatedFlightTime = 2f; // rough estimate
- targetPos += playerVelocity * estimatedFlightTime * 0.5f; // partial leading
- }
-
- meteorScript.useOverrideImpactPoint = true;
- meteorScript.overrideImpactPoint = targetPos;
- meteorScript.snapImpactToGround = true;
-
- if (enableDebug) Debug.Log($"[SA_CallMeteor] Meteor #{_meteorsSpawned + 1} configured to target: {targetPos}");
- }
- else
- {
- if (enableDebug) Debug.LogError("[SA_CallMeteor] Meteor prefab missing MeteorProjectile component!");
- }
- }
-
- private void SpawnMuzzleFlash(Vector3 position)
- {
- if (muzzleFlashPrefab == null) return;
-
- var muzzleFlash = LeanPool.Spawn(muzzleFlashPrefab, position, Quaternion.identity);
-
- if (muzzleFlashDuration > 0f)
- {
- // Auto-despawn muzzle flash after duration
- _boss.gameObject.AddComponent().Init(muzzleFlashDuration, () =>
- {
- if (muzzleFlash != null)
- {
- LeanPool.Despawn(muzzleFlash);
- }
- });
- }
-
- if (enableDebug) Debug.Log($"[SA_CallMeteor] Muzzle flash spawned at: {position}");
- }
-
- private void PlayOverlayOnce(Transform owner)
- {
- if (overlayClip == null) return;
-
- var animator = owner.GetComponent();
- if (animator == null) return;
-
- StopOverlayImmediate();
-
- _overlayGraph = PlayableGraph.Create("ActionOverlay(CallMeteor)");
- _overlayGraph.SetTimeUpdateMode(DirectorUpdateMode.GameTime);
-
- _overlayPlayable = AnimationClipPlayable.Create(_overlayGraph, overlayClip);
- _overlayPlayable.SetApplyFootIK(false);
- _overlayPlayable.SetApplyPlayableIK(false);
- _overlayPlayable.SetSpeed(Mathf.Max(0.0001f, overlaySpeed));
-
- _overlayOutput = AnimationPlayableOutput.Create(_overlayGraph, "AnimOut", animator);
- _overlayOutput.SetSourcePlayable(_overlayPlayable);
-
- _overlayOutput.SetWeight(1f);
- _overlayGraph.Play();
- _overlayPlaying = true;
-
- // Calculate total sequence duration for overlay
- float totalSequenceDuration = initialCastDelay + (meteorCount * meteorSpawnInterval) + 2f; // +2s buffer
- float overlayDuration = overlayClip.length / Mathf.Max(0.0001f, overlaySpeed);
-
- // Use the longer of the two durations, ensuring overlay covers entire sequence
- float finalDuration = Mathf.Max(overlayDuration, totalSequenceDuration);
- _overlayStopAtTime = Time.time + finalDuration;
-
- if (enableDebug) Debug.Log($"[SA_CallMeteor] Overlay clip started via Playables, duration: {finalDuration:F1}s (sequence: {totalSequenceDuration:F1}s, clip: {overlayDuration:F1}s)");
- }
-
- private void StopOverlayImmediate()
- {
- if (_overlayGraph.IsValid())
- {
- _overlayGraph.Stop();
- _overlayGraph.Destroy();
- }
- _overlayPlaying = false;
- }
-
- private void StopOverlayWithFade()
- {
- if (!_overlayPlaying) { StopOverlayImmediate(); return; }
- if (_overlayOutput.IsOutputNull() == false) _overlayOutput.SetWeight(0f);
- StopOverlayImmediate();
- if (enableDebug) Debug.Log("[SA_CallMeteor] Overlay clip stopped");
- }
-
- // Public methods for external monitoring
- public bool IsSequenceActive() => _spawningActive;
-
- public int GetMeteorsSpawned() => _meteorsSpawned;
-
- public int GetTotalMeteorCount() => meteorCount;
-
- public float GetSequenceProgress() => meteorCount > 0 ? (float)_meteorsSpawned / meteorCount : 1f;
-
- ///
- /// Tiny helper MonoBehaviour to delay a callback without coroutines here.
- ///
- private sealed class DelayedInvoker : MonoBehaviour
- {
- private float _timeLeft;
- private System.Action _callback;
-
- public void Init(float delay, System.Action callback)
- {
- _timeLeft = delay;
- _callback = callback;
- }
-
- private void Update()
- {
- _timeLeft -= Time.deltaTime;
- if (_timeLeft <= 0f)
- {
- try { _callback?.Invoke(); }
- finally { Destroy(this); }
- }
- }
- }
- }
+using Invector.vCharacterController.AI.FSMBehaviour;
+using Lean.Pool;
+using UnityEngine;
+using UnityEngine.Animations;
+using UnityEngine.Playables;
+
+namespace DemonBoss.Magic
+{
+ ///
+ /// Spawns multiple meteors behind the BOSS and launches them toward the player's position.
+ ///
+ [CreateAssetMenu(menuName = "Invector/FSM/Actions/DemonBoss/Call Meteor")]
+ public class SA_CallMeteor : vStateAction
+ {
+ public override string categoryName => "DemonBoss/Magic";
+ public override string defaultName => "Call Meteor";
+
+ [Header("Meteor Setup")]
+ [Tooltip("Prefab with MeteorProjectile component")]
+ public GameObject meteorPrefab;
+
+ [Tooltip("Distance behind the BOSS to spawn meteor (meters)")]
+ public float behindBossDistance = 3f;
+
+ [Tooltip("Height above the BOSS to spawn meteor (meters)")]
+ public float aboveBossHeight = 8f;
+
+ [Header("Multi-Meteor Configuration")]
+ [Tooltip("Number of meteors to spawn in sequence")]
+ public int meteorCount = 5;
+
+ [Tooltip("Delay before first meteor spawns (wind-up)")]
+ public float initialCastDelay = 0.4f;
+
+ [Tooltip("Time between each meteor spawn")]
+ public float meteorSpawnInterval = 0.6f;
+
+ [Header("Muzzle Flash Effect")]
+ [Tooltip("Particle effect prefab for muzzle flash at spawn position")]
+ public GameObject muzzleFlashPrefab;
+
+ [Tooltip("Duration to keep muzzle flash alive (seconds)")]
+ public float muzzleFlashDuration = 1.5f;
+
+ [Header("Targeting")]
+ [Tooltip("Tag used to find the target (usually Player)")]
+ public string targetTag = "Player";
+
+ [Header("One-off Overlay Clip (No Animator Params)")]
+ public AnimationClip overlayClip;
+
+ [Tooltip("Playback speed (1 = normal)")] public float overlaySpeed = 1f;
+ [Tooltip("Blend-in seconds (instant in this minimal impl)")] public float overlayFadeIn = 0.10f;
+ [Tooltip("Blend-out seconds (instant in this minimal impl)")] public float overlayFadeOut = 0.10f;
+
+ [Header("Debug")]
+ public bool enableDebug = false;
+
+ private Transform _boss;
+ private Transform _target;
+
+ // --- Multi-meteor state ---
+ private int _meteorsSpawned = 0;
+
+ private bool _spawningActive = false;
+
+ // --- Playables runtime ---
+ private PlayableGraph _overlayGraph;
+
+ private AnimationPlayableOutput _overlayOutput;
+ private AnimationClipPlayable _overlayPlayable;
+ private bool _overlayPlaying;
+ private float _overlayStopAtTime;
+
+ public override void DoAction(vIFSMBehaviourController fsm, vFSMComponentExecutionType execType = vFSMComponentExecutionType.OnStateUpdate)
+ {
+ if (execType == vFSMComponentExecutionType.OnStateEnter)
+ {
+ OnEnter(fsm);
+ }
+ else if (execType == vFSMComponentExecutionType.OnStateUpdate)
+ {
+ // Keep the state active until all meteors are spawned
+ if (_spawningActive && _meteorsSpawned < meteorCount)
+ {
+ // Don't allow the state to exit while spawning
+ return;
+ }
+
+ if (_overlayPlaying && Time.time >= _overlayStopAtTime)
+ {
+ StopOverlayWithFade();
+ }
+
+ // Only signal completion when all meteors are done AND overlay is finished
+ if (!_spawningActive && !_overlayPlaying && _meteorsSpawned >= meteorCount)
+ {
+ if (enableDebug) Debug.Log($"[SA_CallMeteor] Sequence complete, ready to exit state");
+ }
+ }
+ else if (execType == vFSMComponentExecutionType.OnStateExit)
+ {
+ OnExit(fsm);
+ }
+ }
+
+ private void OnEnter(vIFSMBehaviourController fsm)
+ {
+ _boss = fsm.transform;
+ _target = GameObject.FindGameObjectWithTag(targetTag)?.transform;
+
+ if (_target == null)
+ {
+ if (enableDebug) Debug.LogWarning("[SA_CallMeteor] No target found – abort");
+ return;
+ }
+
+ // Reset multi-meteor state
+ _meteorsSpawned = 0;
+ _spawningActive = true;
+
+ // SET COOLDOWN IMMEDIATELY when meteor ability is used
+ DEC_CheckCooldown.SetCooldownStatic(fsm, "Meteor", 80f);
+
+ if (enableDebug) Debug.Log($"[SA_CallMeteor] Boss: {_boss.name}, Target: {_target.name}, Count: {meteorCount}");
+
+ // Fire overlay clip (no Animator params)
+ PlayOverlayOnce(_boss);
+
+ // Start the meteor sequence
+ if (initialCastDelay > 0f)
+ _boss.gameObject.AddComponent().Init(initialCastDelay, StartMeteorSequence);
+ else
+ StartMeteorSequence();
+ }
+
+ private void OnExit(vIFSMBehaviourController fsm)
+ {
+ // Stop any active spawning
+ _spawningActive = false;
+ StopOverlayImmediate();
+
+ // Clean up any DelayedInvokers attached to the boss
+ var invokers = _boss?.GetComponents();
+ if (invokers != null)
+ {
+ foreach (var invoker in invokers)
+ {
+ if (invoker != null) Object.Destroy(invoker);
+ }
+ }
+
+ if (enableDebug) Debug.Log($"[SA_CallMeteor] State exited. Spawned {_meteorsSpawned}/{meteorCount} meteors");
+ }
+
+ private void StartMeteorSequence()
+ {
+ if (!_spawningActive) return;
+ SpawnNextMeteor();
+ }
+
+ private void SpawnNextMeteor()
+ {
+ if (!_spawningActive || _meteorsSpawned >= meteorCount) return;
+
+ if (_boss == null || _target == null)
+ {
+ if (enableDebug) Debug.LogError("[SA_CallMeteor] Missing boss or target reference");
+ return;
+ }
+
+ SpawnSingleMeteor();
+ _meteorsSpawned++;
+
+ if (enableDebug) Debug.Log($"[SA_CallMeteor] Spawned meteor {_meteorsSpawned}/{meteorCount}");
+
+ // Schedule next meteor if needed
+ if (_meteorsSpawned < meteorCount && _spawningActive)
+ {
+ if (enableDebug) Debug.Log($"[SA_CallMeteor] Scheduling next meteor in {meteorSpawnInterval}s");
+ _boss.gameObject.AddComponent().Init(meteorSpawnInterval, SpawnNextMeteor);
+ }
+ else
+ {
+ // All meteors spawned
+ _spawningActive = false;
+ if (enableDebug) Debug.Log("[SA_CallMeteor] All meteors spawned, sequence complete");
+ }
+ }
+
+ private void SpawnSingleMeteor()
+ {
+ if (meteorPrefab == null)
+ {
+ if (enableDebug) Debug.LogError("[SA_CallMeteor] Missing meteorPrefab");
+ return;
+ }
+
+ // Calculate spawn position: behind the BOSS + height
+ Vector3 bossForward = _boss.forward.normalized;
+ Vector3 behindBoss = _boss.position - (bossForward * behindBossDistance);
+ Vector3 spawnPos = behindBoss + Vector3.up * aboveBossHeight;
+
+ // Add slight randomization to spawn position for multiple meteors
+ if (_meteorsSpawned > 0)
+ {
+ Vector3 randomOffset = new Vector3(
+ Random.Range(-1f, 1f),
+ Random.Range(-0.5f, 0.5f),
+ Random.Range(-1f, 1f)
+ );
+ spawnPos += randomOffset;
+ }
+
+ if (enableDebug) Debug.Log($"[SA_CallMeteor] Spawning meteor #{_meteorsSpawned + 1} at: {spawnPos}");
+
+ // Spawn muzzle flash effect first
+ SpawnMuzzleFlash(spawnPos);
+
+ // Spawn the meteor
+ var meteorGO = LeanPool.Spawn(meteorPrefab, spawnPos, Quaternion.identity);
+
+ // Configure the projectile to target the player
+ var meteorScript = meteorGO.GetComponent();
+ if (meteorScript != null)
+ {
+ // Update target position for each meteor (player might be moving)
+ Vector3 targetPos = _target.position;
+
+ // Add slight prediction/leading for moving targets
+ var playerRigidbody = _target.GetComponent();
+ if (playerRigidbody != null)
+ {
+ Vector3 playerVelocity = playerRigidbody.linearVelocity;
+ float estimatedFlightTime = 2f; // rough estimate
+ targetPos += playerVelocity * estimatedFlightTime * 0.5f; // partial leading
+ }
+
+ meteorScript.useOverrideImpactPoint = true;
+ meteorScript.overrideImpactPoint = targetPos;
+ meteorScript.snapImpactToGround = true;
+
+ if (enableDebug) Debug.Log($"[SA_CallMeteor] Meteor #{_meteorsSpawned + 1} configured to target: {targetPos}");
+ }
+ else
+ {
+ if (enableDebug) Debug.LogError("[SA_CallMeteor] Meteor prefab missing MeteorProjectile component!");
+ }
+ }
+
+ private void SpawnMuzzleFlash(Vector3 position)
+ {
+ if (muzzleFlashPrefab == null) return;
+
+ var muzzleFlash = LeanPool.Spawn(muzzleFlashPrefab, position, Quaternion.identity);
+
+ if (muzzleFlashDuration > 0f)
+ {
+ // Auto-despawn muzzle flash after duration
+ _boss.gameObject.AddComponent().Init(muzzleFlashDuration, () =>
+ {
+ if (muzzleFlash != null)
+ {
+ LeanPool.Despawn(muzzleFlash);
+ }
+ });
+ }
+
+ if (enableDebug) Debug.Log($"[SA_CallMeteor] Muzzle flash spawned at: {position}");
+ }
+
+ private void PlayOverlayOnce(Transform owner)
+ {
+ if (overlayClip == null) return;
+
+ var animator = owner.GetComponent();
+ if (animator == null) return;
+
+ StopOverlayImmediate();
+
+ _overlayGraph = PlayableGraph.Create("ActionOverlay(CallMeteor)");
+ _overlayGraph.SetTimeUpdateMode(DirectorUpdateMode.GameTime);
+
+ _overlayPlayable = AnimationClipPlayable.Create(_overlayGraph, overlayClip);
+ _overlayPlayable.SetApplyFootIK(false);
+ _overlayPlayable.SetApplyPlayableIK(false);
+ _overlayPlayable.SetSpeed(Mathf.Max(0.0001f, overlaySpeed));
+
+ _overlayOutput = AnimationPlayableOutput.Create(_overlayGraph, "AnimOut", animator);
+ _overlayOutput.SetSourcePlayable(_overlayPlayable);
+
+ _overlayOutput.SetWeight(1f);
+ _overlayGraph.Play();
+ _overlayPlaying = true;
+
+ // Calculate total sequence duration for overlay
+ float totalSequenceDuration = initialCastDelay + (meteorCount * meteorSpawnInterval) + 2f; // +2s buffer
+ float overlayDuration = overlayClip.length / Mathf.Max(0.0001f, overlaySpeed);
+
+ // Use the longer of the two durations, ensuring overlay covers entire sequence
+ float finalDuration = Mathf.Max(overlayDuration, totalSequenceDuration);
+ _overlayStopAtTime = Time.time + finalDuration;
+
+ if (enableDebug) Debug.Log($"[SA_CallMeteor] Overlay clip started via Playables, duration: {finalDuration:F1}s (sequence: {totalSequenceDuration:F1}s, clip: {overlayDuration:F1}s)");
+ }
+
+ private void StopOverlayImmediate()
+ {
+ if (_overlayGraph.IsValid())
+ {
+ _overlayGraph.Stop();
+ _overlayGraph.Destroy();
+ }
+ _overlayPlaying = false;
+ }
+
+ private void StopOverlayWithFade()
+ {
+ if (!_overlayPlaying) { StopOverlayImmediate(); return; }
+ if (_overlayOutput.IsOutputNull() == false) _overlayOutput.SetWeight(0f);
+ StopOverlayImmediate();
+ if (enableDebug) Debug.Log("[SA_CallMeteor] Overlay clip stopped");
+ }
+
+ // Public methods for external monitoring
+ public bool IsSequenceActive() => _spawningActive;
+
+ public int GetMeteorsSpawned() => _meteorsSpawned;
+
+ public int GetTotalMeteorCount() => meteorCount;
+
+ public float GetSequenceProgress() => meteorCount > 0 ? (float)_meteorsSpawned / meteorCount : 1f;
+
+ ///
+ /// Tiny helper MonoBehaviour to delay a callback without coroutines here.
+ ///
+ private sealed class DelayedInvoker : MonoBehaviour
+ {
+ private float _timeLeft;
+ private System.Action _callback;
+
+ public void Init(float delay, System.Action callback)
+ {
+ _timeLeft = delay;
+ _callback = callback;
+ }
+
+ private void Update()
+ {
+ _timeLeft -= Time.deltaTime;
+ if (_timeLeft <= 0f)
+ {
+ try { _callback?.Invoke(); }
+ finally { Destroy(this); }
+ }
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/Assets/AI/Demon/SA_CallMeteor.cs.meta b/Assets/AI/_Demon/SA_CallMeteor.cs.meta
similarity index 100%
rename from Assets/AI/Demon/SA_CallMeteor.cs.meta
rename to Assets/AI/_Demon/SA_CallMeteor.cs.meta
diff --git a/Assets/AI/Demon/SA_CastShield.cs b/Assets/AI/_Demon/SA_CastShield.cs
similarity index 100%
rename from Assets/AI/Demon/SA_CastShield.cs
rename to Assets/AI/_Demon/SA_CastShield.cs
diff --git a/Assets/AI/Demon/SA_CastShield.cs.meta b/Assets/AI/_Demon/SA_CastShield.cs.meta
similarity index 100%
rename from Assets/AI/Demon/SA_CastShield.cs.meta
rename to Assets/AI/_Demon/SA_CastShield.cs.meta
diff --git a/Assets/AI/Demon/SA_SpawnTurretSmart.cs b/Assets/AI/_Demon/SA_SpawnTurretSmart.cs
similarity index 100%
rename from Assets/AI/Demon/SA_SpawnTurretSmart.cs
rename to Assets/AI/_Demon/SA_SpawnTurretSmart.cs
diff --git a/Assets/AI/Demon/SA_SpawnTurretSmart.cs.meta b/Assets/AI/_Demon/SA_SpawnTurretSmart.cs.meta
similarity index 100%
rename from Assets/AI/Demon/SA_SpawnTurretSmart.cs.meta
rename to Assets/AI/_Demon/SA_SpawnTurretSmart.cs.meta
diff --git a/Assets/AI/Demon/ShieldDamage.cs b/Assets/AI/_Demon/ShieldDamage.cs
similarity index 100%
rename from Assets/AI/Demon/ShieldDamage.cs
rename to Assets/AI/_Demon/ShieldDamage.cs
diff --git a/Assets/AI/Demon/ShieldDamage.cs.meta b/Assets/AI/_Demon/ShieldDamage.cs.meta
similarity index 100%
rename from Assets/AI/Demon/ShieldDamage.cs.meta
rename to Assets/AI/_Demon/ShieldDamage.cs.meta
diff --git a/Assets/AI/Demon/ShieldScaleUp.cs b/Assets/AI/_Demon/ShieldScaleUp.cs
similarity index 95%
rename from Assets/AI/Demon/ShieldScaleUp.cs
rename to Assets/AI/_Demon/ShieldScaleUp.cs
index 8a09f801a..c8898349d 100644
--- a/Assets/AI/Demon/ShieldScaleUp.cs
+++ b/Assets/AI/_Demon/ShieldScaleUp.cs
@@ -1,38 +1,38 @@
-using UnityEngine;
-
-public class ShieldScaleUp : MonoBehaviour
-{
- [Tooltip("Time it takes to fully grow the shield")]
- public float growDuration = 0.5f;
-
- [Tooltip("Curve to control growth speed over time")]
- public AnimationCurve scaleCurve = AnimationCurve.EaseInOut(0, 0, 1, 1);
-
- private float timer = 0f;
- private Vector3 targetScale;
-
- private void Awake()
- {
- targetScale = transform.localScale;
- transform.localScale = Vector3.zero;
- }
-
- private void OnEnable()
- {
- timer = 0f;
- transform.localScale = Vector3.zero;
- }
-
- private void Update()
- {
- if (timer < growDuration)
- {
- timer += Time.deltaTime;
- float t = Mathf.Clamp01(timer / growDuration);
-
- float scaleValue = scaleCurve.Evaluate(t);
-
- transform.localScale = targetScale * scaleValue;
- }
- }
+using UnityEngine;
+
+public class ShieldScaleUp : MonoBehaviour
+{
+ [Tooltip("Time it takes to fully grow the shield")]
+ public float growDuration = 0.5f;
+
+ [Tooltip("Curve to control growth speed over time")]
+ public AnimationCurve scaleCurve = AnimationCurve.EaseInOut(0, 0, 1, 1);
+
+ private float timer = 0f;
+ private Vector3 targetScale;
+
+ private void Awake()
+ {
+ targetScale = transform.localScale;
+ transform.localScale = Vector3.zero;
+ }
+
+ private void OnEnable()
+ {
+ timer = 0f;
+ transform.localScale = Vector3.zero;
+ }
+
+ private void Update()
+ {
+ if (timer < growDuration)
+ {
+ timer += Time.deltaTime;
+ float t = Mathf.Clamp01(timer / growDuration);
+
+ float scaleValue = scaleCurve.Evaluate(t);
+
+ transform.localScale = targetScale * scaleValue;
+ }
+ }
}
\ No newline at end of file
diff --git a/Assets/AI/Demon/ShieldScaleUp.cs.meta b/Assets/AI/_Demon/ShieldScaleUp.cs.meta
similarity index 100%
rename from Assets/AI/Demon/ShieldScaleUp.cs.meta
rename to Assets/AI/_Demon/ShieldScaleUp.cs.meta
diff --git a/Assets/AI/Demon/Turret.prefab b/Assets/AI/_Demon/Turret.prefab
similarity index 100%
rename from Assets/AI/Demon/Turret.prefab
rename to Assets/AI/_Demon/Turret.prefab
diff --git a/Assets/AI/Demon/Turret.prefab.meta b/Assets/AI/_Demon/Turret.prefab.meta
similarity index 100%
rename from Assets/AI/Demon/Turret.prefab.meta
rename to Assets/AI/_Demon/Turret.prefab.meta
diff --git a/Assets/AI/Demon/Waypoints.prefab b/Assets/AI/_Demon/Waypoints.prefab
similarity index 100%
rename from Assets/AI/Demon/Waypoints.prefab
rename to Assets/AI/_Demon/Waypoints.prefab
diff --git a/Assets/AI/Demon/Waypoints.prefab.meta b/Assets/AI/_Demon/Waypoints.prefab.meta
similarity index 100%
rename from Assets/AI/Demon/Waypoints.prefab.meta
rename to Assets/AI/_Demon/Waypoints.prefab.meta
diff --git a/Assets/AI/Gigant.meta b/Assets/AI/_Gigant.meta
similarity index 100%
rename from Assets/AI/Gigant.meta
rename to Assets/AI/_Gigant.meta
diff --git a/Assets/AI/Gigant/Giant.prefab b/Assets/AI/_Gigant/Giant.prefab
similarity index 100%
rename from Assets/AI/Gigant/Giant.prefab
rename to Assets/AI/_Gigant/Giant.prefab
diff --git a/Assets/AI/Gigant/Giant.prefab.meta b/Assets/AI/_Gigant/Giant.prefab.meta
similarity index 100%
rename from Assets/AI/Gigant/Giant.prefab.meta
rename to Assets/AI/_Gigant/Giant.prefab.meta
diff --git a/Assets/AI/Gigant/LookAtPlayerInitialization.cs b/Assets/AI/_Gigant/LookAtPlayerInitialization.cs
similarity index 100%
rename from Assets/AI/Gigant/LookAtPlayerInitialization.cs
rename to Assets/AI/_Gigant/LookAtPlayerInitialization.cs
diff --git a/Assets/AI/Gigant/LookAtPlayerInitialization.cs.meta b/Assets/AI/_Gigant/LookAtPlayerInitialization.cs.meta
similarity index 100%
rename from Assets/AI/Gigant/LookAtPlayerInitialization.cs.meta
rename to Assets/AI/_Gigant/LookAtPlayerInitialization.cs.meta
diff --git a/Assets/AI/Gigant/RandomizeAnimatorIntOnEnter.cs b/Assets/AI/_Gigant/RandomizeAnimatorIntOnEnter.cs
similarity index 100%
rename from Assets/AI/Gigant/RandomizeAnimatorIntOnEnter.cs
rename to Assets/AI/_Gigant/RandomizeAnimatorIntOnEnter.cs
diff --git a/Assets/AI/Gigant/RandomizeAnimatorIntOnEnter.cs.meta b/Assets/AI/_Gigant/RandomizeAnimatorIntOnEnter.cs.meta
similarity index 100%
rename from Assets/AI/Gigant/RandomizeAnimatorIntOnEnter.cs.meta
rename to Assets/AI/_Gigant/RandomizeAnimatorIntOnEnter.cs.meta
diff --git a/Assets/AI/_Stalker.meta b/Assets/AI/_Stalker.meta
new file mode 100644
index 000000000..47c50b8d1
--- /dev/null
+++ b/Assets/AI/_Stalker.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: f81dc6f089de88d419e022edfdeb3cea
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/AI/_Summoner.meta b/Assets/AI/_Summoner.meta
new file mode 100644
index 000000000..57a249286
--- /dev/null
+++ b/Assets/AI/_Summoner.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 6233c0fbab2ef8f41b45a57062fc585e
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/AI/_Summoner/DEC_CanSpawnMinions.cs b/Assets/AI/_Summoner/DEC_CanSpawnMinions.cs
new file mode 100644
index 000000000..1b8d3b57f
--- /dev/null
+++ b/Assets/AI/_Summoner/DEC_CanSpawnMinions.cs
@@ -0,0 +1,105 @@
+using Invector.vCharacterController.AI.FSMBehaviour;
+using UnityEngine;
+
+namespace DemonBoss.Summoner
+{
+ ///
+ /// FSM Decision checking if summoner can spawn minions
+ /// Checks: not already spawning, minion count below max, cooldown passed
+ ///
+ [CreateAssetMenu(menuName = "Invector/FSM/Decisions/Summoner/Can Spawn Minions")]
+ public class DEC_CanSpawnMinions : vStateDecision
+ {
+ public override string categoryName => "Summoner";
+ public override string defaultName => "Can Spawn Minions";
+
+ [Header("Spawn Conditions")]
+ [Tooltip("Check if health is below threshold")]
+ public bool checkHealthThreshold = true;
+
+ [Tooltip("Check if minions are below max count")]
+ public bool checkMinionCount = true;
+
+ [Tooltip("Check if cooldown has passed")]
+ public bool checkCooldown = true;
+
+ [Tooltip("Cooldown between summon attempts (seconds)")]
+ public float cooldownTime = 15f;
+
+ [Header("Distance Check")]
+ [Tooltip("Only spawn if player is within this distance (0 = disabled)")]
+ public float maxDistanceToPlayer = 0f;
+
+ [Header("Debug")]
+ [Tooltip("Enable debug logging")]
+ public bool enableDebug = false;
+
+ private string cooldownKey = "SummonMinions";
+
+ public override bool Decide(vIFSMBehaviourController fsmBehaviour)
+ {
+ var summoner = fsmBehaviour.gameObject.GetComponent();
+ if (summoner == null)
+ {
+ if (enableDebug) Debug.LogWarning("[DEC_CanSpawnMinions] No SummonerAI component found!");
+ return false;
+ }
+
+ // Check if already spawning
+ if (summoner.IsSpawning)
+ {
+ if (enableDebug) Debug.Log("[DEC_CanSpawnMinions] Already spawning - FALSE");
+ return false;
+ }
+
+ // Check minion count
+ if (checkMinionCount && !summoner.CanSpawnMinions)
+ {
+ if (enableDebug) Debug.Log($"[DEC_CanSpawnMinions] Max minions reached ({summoner.ActiveMinionCount}) - FALSE");
+ return false;
+ }
+
+ // Check health threshold
+ if (checkHealthThreshold && !summoner.ShouldSummonByHealth())
+ {
+ if (enableDebug) Debug.Log("[DEC_CanSpawnMinions] Health threshold not met - FALSE");
+ return false;
+ }
+
+ // Check distance to player
+ if (maxDistanceToPlayer > 0f)
+ {
+ float distance = summoner.GetDistanceToPlayer();
+ if (distance > maxDistanceToPlayer)
+ {
+ if (enableDebug) Debug.Log($"[DEC_CanSpawnMinions] Player too far ({distance:F1}m) - FALSE");
+ return false;
+ }
+ }
+
+ // Check cooldown
+ if (checkCooldown)
+ {
+ string timerKey = "cooldown_" + cooldownKey;
+
+ if (fsmBehaviour.HasTimer(timerKey))
+ {
+ float lastUsedTime = fsmBehaviour.GetTimer(timerKey);
+ float timeSinceLastUse = Time.time - lastUsedTime;
+
+ if (timeSinceLastUse < cooldownTime)
+ {
+ if (enableDebug) Debug.Log($"[DEC_CanSpawnMinions] On cooldown - {cooldownTime - timeSinceLastUse:F1}s remaining - FALSE");
+ return false;
+ }
+ }
+
+ // Set cooldown for next use
+ fsmBehaviour.SetTimer(timerKey, Time.time);
+ }
+
+ if (enableDebug) Debug.Log("[DEC_CanSpawnMinions] All conditions met - TRUE");
+ return true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Assets/AI/_Summoner/DEC_CanSpawnMinions.cs.meta b/Assets/AI/_Summoner/DEC_CanSpawnMinions.cs.meta
new file mode 100644
index 000000000..168f79a1c
--- /dev/null
+++ b/Assets/AI/_Summoner/DEC_CanSpawnMinions.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 6932f86375a52954785f89df47a2d61e
\ No newline at end of file
diff --git a/Assets/AI/_Summoner/DEC_CheckDistanceToPlayer.cs b/Assets/AI/_Summoner/DEC_CheckDistanceToPlayer.cs
new file mode 100644
index 000000000..1d7fcb363
--- /dev/null
+++ b/Assets/AI/_Summoner/DEC_CheckDistanceToPlayer.cs
@@ -0,0 +1,16 @@
+using UnityEngine;
+
+public class DEC_CheckDistanceToPlayer : MonoBehaviour
+{
+ // Start is called once before the first execution of Update after the MonoBehaviour is created
+ void Start()
+ {
+
+ }
+
+ // Update is called once per frame
+ void Update()
+ {
+
+ }
+}
diff --git a/Assets/AI/_Summoner/DEC_CheckDistanceToPlayer.cs.meta b/Assets/AI/_Summoner/DEC_CheckDistanceToPlayer.cs.meta
new file mode 100644
index 000000000..aee1876b7
--- /dev/null
+++ b/Assets/AI/_Summoner/DEC_CheckDistanceToPlayer.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 40326c8e0d34db64d9aec176f7d5bc97
\ No newline at end of file
diff --git a/Assets/AI/_Summoner/DEC_HasActiveMinions.cs b/Assets/AI/_Summoner/DEC_HasActiveMinions.cs
new file mode 100644
index 000000000..06cf3662b
--- /dev/null
+++ b/Assets/AI/_Summoner/DEC_HasActiveMinions.cs
@@ -0,0 +1,16 @@
+using UnityEngine;
+
+public class DEC_HasActiveMinions : MonoBehaviour
+{
+ // Start is called once before the first execution of Update after the MonoBehaviour is created
+ void Start()
+ {
+
+ }
+
+ // Update is called once per frame
+ void Update()
+ {
+
+ }
+}
diff --git a/Assets/AI/_Summoner/DEC_HasActiveMinions.cs.meta b/Assets/AI/_Summoner/DEC_HasActiveMinions.cs.meta
new file mode 100644
index 000000000..c4cea44cc
--- /dev/null
+++ b/Assets/AI/_Summoner/DEC_HasActiveMinions.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 3a537cd142b23fa4b8285f3d22cda409
\ No newline at end of file
diff --git a/Assets/AI/_Summoner/DEC_ShouldMeleeAttack.cs b/Assets/AI/_Summoner/DEC_ShouldMeleeAttack.cs
new file mode 100644
index 000000000..319367ee1
--- /dev/null
+++ b/Assets/AI/_Summoner/DEC_ShouldMeleeAttack.cs
@@ -0,0 +1,74 @@
+using Invector.vCharacterController.AI.FSMBehaviour;
+using UnityEngine;
+
+namespace DemonBoss.Summoner
+{
+ ///
+ /// FSM Decision checking if summoner should engage in melee combat
+ /// Checks: player distance, minion status, combat settings
+ ///
+ [CreateAssetMenu(menuName = "Invector/FSM/Decisions/Summoner/Should Melee Attack")]
+ public class DEC_ShouldMeleeAttack : vStateDecision
+ {
+ public override string categoryName => "Summoner";
+ public override string defaultName => "Should Melee Attack";
+
+ [Header("Distance Configuration")]
+ [Tooltip("Minimum distance to engage melee")]
+ public float minMeleeDistance = 0f;
+
+ [Tooltip("Maximum distance to engage melee")]
+ public float maxMeleeDistance = 3f;
+
+ [Header("Behavior")]
+ [Tooltip("Attack even when minions are alive")]
+ public bool attackWithMinions = false;
+
+ [Header("Debug")]
+ [Tooltip("Enable debug logging")]
+ public bool enableDebug = false;
+
+ public override bool Decide(vIFSMBehaviourController fsmBehaviour)
+ {
+ var summoner = fsmBehaviour.gameObject.GetComponent();
+ if (summoner == null)
+ {
+ if (enableDebug) Debug.LogWarning("[DEC_ShouldMeleeAttack] No SummonerAI component found!");
+ return false;
+ }
+
+ // Don't attack while spawning
+ if (summoner.IsSpawning)
+ {
+ if (enableDebug) Debug.Log("[DEC_ShouldMeleeAttack] Currently spawning - FALSE");
+ return false;
+ }
+
+ // Check if has minions and shouldn't attack with them
+ if (!attackWithMinions && summoner.HasActiveMinions)
+ {
+ if (enableDebug) Debug.Log($"[DEC_ShouldMeleeAttack] Has {summoner.ActiveMinionCount} minions and attackWithMinions=false - FALSE");
+ return false;
+ }
+
+ // Check distance to player
+ float distance = summoner.GetDistanceToPlayer();
+
+ bool inRange = distance >= minMeleeDistance && distance <= maxMeleeDistance;
+
+ if (enableDebug)
+ {
+ if (inRange)
+ {
+ Debug.Log($"[DEC_ShouldMeleeAttack] Player in melee range ({distance:F1}m) - TRUE");
+ }
+ else
+ {
+ Debug.Log($"[DEC_ShouldMeleeAttack] Player not in melee range ({distance:F1}m, need {minMeleeDistance:F1}-{maxMeleeDistance:F1}m) - FALSE");
+ }
+ }
+
+ return inRange;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Assets/AI/_Summoner/DEC_ShouldMeleeAttack.cs.meta b/Assets/AI/_Summoner/DEC_ShouldMeleeAttack.cs.meta
new file mode 100644
index 000000000..5757779a3
--- /dev/null
+++ b/Assets/AI/_Summoner/DEC_ShouldMeleeAttack.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: f8abf79055070ed47a11a57e0cc5aea3
\ No newline at end of file
diff --git a/Assets/AI/_Summoner/SA_SpawnMinions.cs b/Assets/AI/_Summoner/SA_SpawnMinions.cs
new file mode 100644
index 000000000..8d126a3ee
--- /dev/null
+++ b/Assets/AI/_Summoner/SA_SpawnMinions.cs
@@ -0,0 +1,125 @@
+using Invector.vCharacterController.AI.FSMBehaviour;
+using UnityEngine;
+
+namespace DemonBoss.Summoner
+{
+ ///
+ /// FSM State Action that triggers minion spawning
+ /// Calls SummonerAI.StartSpawning() on state enter
+ ///
+ [CreateAssetMenu(menuName = "Invector/FSM/Actions/Summoner/Spawn Minions")]
+ public class SA_SpawnMinions : vStateAction
+ {
+ public override string categoryName => "Summoner";
+ public override string defaultName => "Spawn Minions";
+
+ [Header("Animation")]
+ [Tooltip("Animator trigger parameter for summoning animation")]
+ public string summonTriggerName = "Summon";
+
+ [Tooltip("Animator bool for summoning state")]
+ public string summoningBoolName = "IsSummoning";
+
+ [Header("Behavior")]
+ [Tooltip("Wait for spawning to complete before allowing state exit")]
+ public bool waitForCompletion = true;
+
+ [Header("Debug")]
+ [Tooltip("Enable debug logging")]
+ public bool enableDebug = false;
+
+ private SummonerAI summoner;
+ private Animator animator;
+ private bool hasStartedSpawning = false;
+
+ public override void DoAction(vIFSMBehaviourController fsmBehaviour, vFSMComponentExecutionType executionType = vFSMComponentExecutionType.OnStateUpdate)
+ {
+ if (executionType == vFSMComponentExecutionType.OnStateEnter)
+ {
+ OnEnter(fsmBehaviour);
+ }
+ else if (executionType == vFSMComponentExecutionType.OnStateUpdate)
+ {
+ OnUpdate(fsmBehaviour);
+ }
+ else if (executionType == vFSMComponentExecutionType.OnStateExit)
+ {
+ OnExit(fsmBehaviour);
+ }
+ }
+
+ private void OnEnter(vIFSMBehaviourController fsmBehaviour)
+ {
+ summoner = fsmBehaviour.gameObject.GetComponent();
+ animator = fsmBehaviour.gameObject.GetComponent();
+
+ if (summoner == null)
+ {
+ Debug.LogError("[SA_SpawnMinions] No SummonerAI component found!");
+ return;
+ }
+
+ // Trigger animation
+ if (animator != null)
+ {
+ if (!string.IsNullOrEmpty(summonTriggerName))
+ {
+ animator.SetTrigger(summonTriggerName);
+ }
+
+ if (!string.IsNullOrEmpty(summoningBoolName))
+ {
+ animator.SetBool(summoningBoolName, true);
+ }
+ }
+
+ // Start spawning minions
+ summoner.StartSpawning();
+ hasStartedSpawning = true;
+
+ if (enableDebug) Debug.Log("[SA_SpawnMinions] Started spawning minions");
+ }
+
+ private void OnUpdate(vIFSMBehaviourController fsmBehaviour)
+ {
+ // If waiting for completion, keep state active until spawning is done
+ if (waitForCompletion && summoner != null && summoner.IsSpawning)
+ {
+ // State will continue until spawning is complete
+ if (enableDebug && Time.frameCount % 60 == 0) // Log once per second
+ {
+ Debug.Log("[SA_SpawnMinions] Waiting for spawning to complete...");
+ }
+ }
+ }
+
+ private void OnExit(vIFSMBehaviourController fsmBehaviour)
+ {
+ // Reset animation bool
+ if (animator != null && !string.IsNullOrEmpty(summoningBoolName))
+ {
+ animator.SetBool(summoningBoolName, false);
+ }
+
+ // If spawning was interrupted, stop it
+ if (summoner != null && summoner.IsSpawning)
+ {
+ summoner.StopSpawning();
+ if (enableDebug) Debug.Log("[SA_SpawnMinions] Spawning interrupted on state exit");
+ }
+
+ hasStartedSpawning = false;
+
+ if (enableDebug) Debug.Log("[SA_SpawnMinions] State exited");
+ }
+
+ ///
+ /// Check if spawning is complete (for FSM decision nodes)
+ ///
+ public bool IsSpawningComplete()
+ {
+ if (summoner == null) return true;
+ return hasStartedSpawning && !summoner.IsSpawning;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Assets/AI/_Summoner/SA_SpawnMinions.cs.meta b/Assets/AI/_Summoner/SA_SpawnMinions.cs.meta
new file mode 100644
index 000000000..abc171ae2
--- /dev/null
+++ b/Assets/AI/_Summoner/SA_SpawnMinions.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: ad57378f916739249891d117d50e333b
\ No newline at end of file
diff --git a/Assets/AI/_Summoner/SummonerAI.cs b/Assets/AI/_Summoner/SummonerAI.cs
new file mode 100644
index 000000000..d92087daf
--- /dev/null
+++ b/Assets/AI/_Summoner/SummonerAI.cs
@@ -0,0 +1,359 @@
+using Invector;
+using Invector.vCharacterController.AI;
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace DemonBoss.Summoner
+{
+ ///
+ /// Main AI controller for Summoner enemy
+ /// Manages spawned minions and provides combat state tracking
+ /// Attach to Summoner character along with vControlAI
+ ///
+ public class SummonerAI : MonoBehaviour
+ {
+ [Header("Minion Management")]
+ [Tooltip("Prefab of minion to spawn")]
+ public GameObject minionPrefab;
+
+ [Tooltip("Maximum number of minions alive at once")]
+ public int maxActiveMinions = 3;
+
+ [Tooltip("Distance from summoner to spawn minions")]
+ public float spawnRadius = 5f;
+
+ [Tooltip("Height offset for spawn position")]
+ public float spawnHeightOffset = 0f;
+
+ [Tooltip("Should minions look at summoner after spawn?")]
+ public bool minionsLookAtCenter = false;
+
+ [Header("Spawn Configuration")]
+ [Tooltip("Number of minions to spawn per summon action")]
+ public int minionsPerSummon = 3;
+
+ [Tooltip("Delay before first minion spawns")]
+ public float initialSpawnDelay = 0.5f;
+
+ [Tooltip("Time between spawning each minion")]
+ public float timeBetweenSpawns = 0.3f;
+
+ [Header("Combat Behavior")]
+ [Tooltip("Minimum distance to player before engaging in melee")]
+ public float meleeEngageDistance = 3f;
+
+ [Tooltip("Should summoner fight when minions are alive?")]
+ public bool fightWithMinions = false;
+
+ [Tooltip("Health percentage threshold to spawn minions (0-1)")]
+ [Range(0f, 1f)]
+ public float healthThresholdForSummon = 0.7f;
+
+ [Header("Targeting")]
+ [Tooltip("Tag to find player")]
+ public string playerTag = "Player";
+
+ [Header("Effects")]
+ [Tooltip("Particle effect at spawn location")]
+ public GameObject spawnEffectPrefab;
+
+ [Tooltip("Sound played when spawning minions")]
+ public AudioClip summonSound;
+
+ [Header("Debug")]
+ [Tooltip("Enable debug logging")]
+ public bool enableDebug = false;
+
+ [Tooltip("Show gizmos in Scene View")]
+ public bool showGizmos = true;
+
+ // Runtime state
+ private List activeMinions = new List();
+
+ private Transform playerTransform;
+ private AudioSource audioSource;
+ private vHealthController healthController;
+ private bool isSpawning = false;
+ private Coroutine spawnCoroutine;
+
+ // Public properties for FSM decisions
+ public bool IsSpawning => isSpawning;
+
+ public int ActiveMinionCount => activeMinions.Count;
+ public bool CanSpawnMinions => activeMinions.Count < maxActiveMinions && !isSpawning;
+ public bool HasActiveMinions => activeMinions.Count > 0;
+
+ private void Awake()
+ {
+ healthController = GetComponent();
+
+ audioSource = GetComponent();
+ if (audioSource == null && summonSound != null)
+ {
+ audioSource = gameObject.AddComponent();
+ audioSource.playOnAwake = false;
+ audioSource.spatialBlend = 1f;
+ }
+ }
+
+ private void Start()
+ {
+ FindPlayer();
+ }
+
+ private void Update()
+ {
+ CleanupDeadMinions();
+ }
+
+ ///
+ /// Find player by tag
+ ///
+ private void FindPlayer()
+ {
+ GameObject player = GameObject.FindGameObjectWithTag(playerTag);
+ if (player != null)
+ {
+ playerTransform = player.transform;
+ if (enableDebug) Debug.Log("[SummonerAI] Player found: " + player.name);
+ }
+ }
+
+ ///
+ /// Start spawning minions
+ ///
+ public void StartSpawning()
+ {
+ if (isSpawning)
+ {
+ if (enableDebug) Debug.Log("[SummonerAI] Already spawning minions");
+ return;
+ }
+
+ if (minionPrefab == null)
+ {
+ Debug.LogError("[SummonerAI] No minion prefab assigned!");
+ return;
+ }
+
+ spawnCoroutine = StartCoroutine(SpawnMinionsCoroutine());
+ }
+
+ ///
+ /// Stop spawning minions immediately
+ ///
+ public void StopSpawning()
+ {
+ if (spawnCoroutine != null)
+ {
+ StopCoroutine(spawnCoroutine);
+ spawnCoroutine = null;
+ }
+ isSpawning = false;
+ }
+
+ ///
+ /// Coroutine that spawns minions with delays
+ ///
+ private IEnumerator SpawnMinionsCoroutine()
+ {
+ isSpawning = true;
+
+ if (enableDebug) Debug.Log($"[SummonerAI] Starting to spawn {minionsPerSummon} minions");
+
+ // Initial delay
+ yield return new WaitForSeconds(initialSpawnDelay);
+
+ // Play summon sound
+ if (audioSource != null && summonSound != null)
+ {
+ audioSource.PlayOneShot(summonSound);
+ }
+
+ // Spawn minions
+ int spawned = 0;
+ for (int i = 0; i < minionsPerSummon && activeMinions.Count < maxActiveMinions; i++)
+ {
+ SpawnSingleMinion();
+ spawned++;
+
+ // Wait between spawns (except after last one)
+ if (i < minionsPerSummon - 1)
+ {
+ yield return new WaitForSeconds(timeBetweenSpawns);
+ }
+ }
+
+ if (enableDebug) Debug.Log($"[SummonerAI] Finished spawning {spawned} minions. Total active: {activeMinions.Count}");
+
+ isSpawning = false;
+ }
+
+ ///
+ /// Spawn a single minion at random position around summoner
+ ///
+ private void SpawnSingleMinion()
+ {
+ // Calculate random spawn position
+ float angle = Random.Range(0f, 360f) * Mathf.Deg2Rad;
+ float x = transform.position.x + spawnRadius * Mathf.Cos(angle);
+ float z = transform.position.z + spawnRadius * Mathf.Sin(angle);
+ Vector3 spawnPosition = new Vector3(x, transform.position.y + spawnHeightOffset, z);
+
+ // Spawn minion
+ GameObject minion = Instantiate(minionPrefab, spawnPosition, Quaternion.identity);
+
+ // Set rotation
+ if (minionsLookAtCenter)
+ {
+ minion.transform.LookAt(transform.position);
+ }
+ else if (playerTransform != null)
+ {
+ // Make minion face player
+ Vector3 directionToPlayer = (playerTransform.position - minion.transform.position).normalized;
+ directionToPlayer.y = 0;
+ if (directionToPlayer != Vector3.zero)
+ {
+ minion.transform.rotation = Quaternion.LookRotation(directionToPlayer);
+ }
+ }
+
+ // Configure minion AI to target player
+ var minionAI = minion.GetComponent();
+ if (minionAI != null && playerTransform != null)
+ {
+ // Set player as target through AI system
+ minionAI.SetCurrentTarget(playerTransform);
+ }
+
+ // Add to active minions list
+ activeMinions.Add(minion);
+
+ // Spawn visual effect
+ if (spawnEffectPrefab != null)
+ {
+ GameObject effect = Instantiate(spawnEffectPrefab, spawnPosition, Quaternion.identity);
+ Destroy(effect, 3f);
+ }
+
+ if (enableDebug) Debug.Log($"[SummonerAI] Spawned minion at {spawnPosition}");
+ }
+
+ ///
+ /// Remove destroyed/null minions from list
+ ///
+ private void CleanupDeadMinions()
+ {
+ activeMinions.RemoveAll(minion => minion == null);
+ }
+
+ ///
+ /// Check if summoner should spawn minions based on health
+ ///
+ public bool ShouldSummonByHealth()
+ {
+ if (healthController == null) return false;
+
+ float healthPercent = healthController.currentHealth / healthController.MaxHealth;
+ return healthPercent <= healthThresholdForSummon;
+ }
+
+ ///
+ /// Get distance to player
+ ///
+ public float GetDistanceToPlayer()
+ {
+ if (playerTransform == null)
+ {
+ FindPlayer();
+ if (playerTransform == null) return float.MaxValue;
+ }
+
+ return Vector3.Distance(transform.position, playerTransform.position);
+ }
+
+ ///
+ /// Check if player is in melee range
+ ///
+ public bool IsPlayerInMeleeRange()
+ {
+ return GetDistanceToPlayer() <= meleeEngageDistance;
+ }
+
+ ///
+ /// Check if summoner should engage in melee combat
+ ///
+ public bool ShouldEngageMelee()
+ {
+ if (!IsPlayerInMeleeRange()) return false;
+ if (fightWithMinions) return true;
+ return !HasActiveMinions;
+ }
+
+ ///
+ /// Destroy all active minions (e.g., when summoner dies)
+ ///
+ public void DestroyAllMinions()
+ {
+ foreach (GameObject minion in activeMinions)
+ {
+ if (minion != null)
+ {
+ Destroy(minion);
+ }
+ }
+ activeMinions.Clear();
+
+ if (enableDebug) Debug.Log("[SummonerAI] All minions destroyed");
+ }
+
+ private void OnDestroy()
+ {
+ // Cleanup minions when summoner dies
+ DestroyAllMinions();
+ }
+
+ private void OnDrawGizmosSelected()
+ {
+ if (!showGizmos) return;
+
+ // Draw spawn radius
+ Gizmos.color = Color.cyan;
+ DrawCircle(transform.position, spawnRadius, 32);
+
+ // Draw melee range
+ Gizmos.color = Color.red;
+ DrawCircle(transform.position, meleeEngageDistance, 16);
+
+ // Draw lines to active minions
+ Gizmos.color = Color.green;
+ foreach (GameObject minion in activeMinions)
+ {
+ if (minion != null)
+ {
+ Gizmos.DrawLine(transform.position + Vector3.up, minion.transform.position + Vector3.up);
+ }
+ }
+ }
+
+ private void DrawCircle(Vector3 center, float radius, int segments)
+ {
+ float angleStep = 360f / segments;
+ Vector3 previousPoint = center + new Vector3(radius, 0, 0);
+
+ for (int i = 1; i <= segments; i++)
+ {
+ float angle = i * angleStep * Mathf.Deg2Rad;
+ Vector3 newPoint = center + new Vector3(
+ Mathf.Cos(angle) * radius,
+ 0,
+ Mathf.Sin(angle) * radius
+ );
+ Gizmos.DrawLine(previousPoint, newPoint);
+ previousPoint = newPoint;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Assets/AI/_Summoner/SummonerAI.cs.meta b/Assets/AI/_Summoner/SummonerAI.cs.meta
new file mode 100644
index 000000000..a1dd42ce6
--- /dev/null
+++ b/Assets/AI/_Summoner/SummonerAI.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: ab91807f36ca9204a8515cfaffac091c
\ No newline at end of file
diff --git a/Assets/AI/Summons.meta b/Assets/AI/_Summons.meta
similarity index 100%
rename from Assets/AI/Summons.meta
rename to Assets/AI/_Summons.meta
diff --git a/Assets/AI/Summons/SummonerSpawner.cs b/Assets/AI/_Summons/SummonerSpawner.cs
similarity index 100%
rename from Assets/AI/Summons/SummonerSpawner.cs
rename to Assets/AI/_Summons/SummonerSpawner.cs
diff --git a/Assets/AI/Summons/SummonerSpawner.cs.meta b/Assets/AI/_Summons/SummonerSpawner.cs.meta
similarity index 100%
rename from Assets/AI/Summons/SummonerSpawner.cs.meta
rename to Assets/AI/_Summons/SummonerSpawner.cs.meta
diff --git a/Assets/AI/Summons/Summoner_Barghest.prefab b/Assets/AI/_Summons/Summoner_Barghest.prefab
similarity index 100%
rename from Assets/AI/Summons/Summoner_Barghest.prefab
rename to Assets/AI/_Summons/Summoner_Barghest.prefab
diff --git a/Assets/AI/Summons/Summoner_Barghest.prefab.meta b/Assets/AI/_Summons/Summoner_Barghest.prefab.meta
similarity index 100%
rename from Assets/AI/Summons/Summoner_Barghest.prefab.meta
rename to Assets/AI/_Summons/Summoner_Barghest.prefab.meta
diff --git a/Assets/AI/Summons/Summoner_Bug.prefab b/Assets/AI/_Summons/Summoner_Bug.prefab
similarity index 100%
rename from Assets/AI/Summons/Summoner_Bug.prefab
rename to Assets/AI/_Summons/Summoner_Bug.prefab
diff --git a/Assets/AI/Summons/Summoner_Bug.prefab.meta b/Assets/AI/_Summons/Summoner_Bug.prefab.meta
similarity index 100%
rename from Assets/AI/Summons/Summoner_Bug.prefab.meta
rename to Assets/AI/_Summons/Summoner_Bug.prefab.meta
diff --git a/Assets/AI/Summons/Summoner_Bug_Spider.prefab b/Assets/AI/_Summons/Summoner_Bug_Spider.prefab
similarity index 100%
rename from Assets/AI/Summons/Summoner_Bug_Spider.prefab
rename to Assets/AI/_Summons/Summoner_Bug_Spider.prefab
diff --git a/Assets/AI/Summons/Summoner_Bug_Spider.prefab.meta b/Assets/AI/_Summons/Summoner_Bug_Spider.prefab.meta
similarity index 100%
rename from Assets/AI/Summons/Summoner_Bug_Spider.prefab.meta
rename to Assets/AI/_Summons/Summoner_Bug_Spider.prefab.meta
diff --git a/Assets/AI/Summons/Summoner_Demon_Dog.prefab b/Assets/AI/_Summons/Summoner_Demon_Dog.prefab
similarity index 100%
rename from Assets/AI/Summons/Summoner_Demon_Dog.prefab
rename to Assets/AI/_Summons/Summoner_Demon_Dog.prefab
diff --git a/Assets/AI/Summons/Summoner_Demon_Dog.prefab.meta b/Assets/AI/_Summons/Summoner_Demon_Dog.prefab.meta
similarity index 100%
rename from Assets/AI/Summons/Summoner_Demon_Dog.prefab.meta
rename to Assets/AI/_Summons/Summoner_Demon_Dog.prefab.meta
diff --git a/Assets/AI/Summons/Summoner_Devil_Spider.prefab b/Assets/AI/_Summons/Summoner_Devil_Spider.prefab
similarity index 100%
rename from Assets/AI/Summons/Summoner_Devil_Spider.prefab
rename to Assets/AI/_Summons/Summoner_Devil_Spider.prefab
diff --git a/Assets/AI/Summons/Summoner_Devil_Spider.prefab.meta b/Assets/AI/_Summons/Summoner_Devil_Spider.prefab.meta
similarity index 100%
rename from Assets/AI/Summons/Summoner_Devil_Spider.prefab.meta
rename to Assets/AI/_Summons/Summoner_Devil_Spider.prefab.meta
diff --git a/Assets/AI/Summons/Summoner_Flying_Bug.prefab b/Assets/AI/_Summons/Summoner_Flying_Bug.prefab
similarity index 100%
rename from Assets/AI/Summons/Summoner_Flying_Bug.prefab
rename to Assets/AI/_Summons/Summoner_Flying_Bug.prefab
diff --git a/Assets/AI/Summons/Summoner_Flying_Bug.prefab.meta b/Assets/AI/_Summons/Summoner_Flying_Bug.prefab.meta
similarity index 100%
rename from Assets/AI/Summons/Summoner_Flying_Bug.prefab.meta
rename to Assets/AI/_Summons/Summoner_Flying_Bug.prefab.meta
diff --git a/Assets/AI/Summons/Summoner_Golem.prefab b/Assets/AI/_Summons/Summoner_Golem.prefab
similarity index 100%
rename from Assets/AI/Summons/Summoner_Golem.prefab
rename to Assets/AI/_Summons/Summoner_Golem.prefab
diff --git a/Assets/AI/Summons/Summoner_Golem.prefab.meta b/Assets/AI/_Summons/Summoner_Golem.prefab.meta
similarity index 100%
rename from Assets/AI/Summons/Summoner_Golem.prefab.meta
rename to Assets/AI/_Summons/Summoner_Golem.prefab.meta
diff --git a/Assets/AI/Summons/Summoner_Golem_Spider.prefab b/Assets/AI/_Summons/Summoner_Golem_Spider.prefab
similarity index 100%
rename from Assets/AI/Summons/Summoner_Golem_Spider.prefab
rename to Assets/AI/_Summons/Summoner_Golem_Spider.prefab
diff --git a/Assets/AI/Summons/Summoner_Golem_Spider.prefab.meta b/Assets/AI/_Summons/Summoner_Golem_Spider.prefab.meta
similarity index 100%
rename from Assets/AI/Summons/Summoner_Golem_Spider.prefab.meta
rename to Assets/AI/_Summons/Summoner_Golem_Spider.prefab.meta