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