using Invector; using Invector.vCharacterController.AI; using System.Collections; using System.Collections.Generic; using Lean.Pool; 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; [Header("Melee Cooldown")] [Tooltip("Minimum seconds between melee attacks")] public float meleeCooldownMin = 5f; [Tooltip("Maximum seconds between melee attacks")] public float meleeCooldownMax = 8f; [Tooltip("Preferred safe distance from player (used for flee/spacing)")] public float safeDistance = 8f; [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("Summon Restrictions")] [Tooltip("If true, summoner will only summon when no minions are alive")] public bool requireZeroMinionsToSummon = true; [Header("Spell Casting")] [Tooltip("Fireball prefab (projectile handles its own targeting)")] public GameObject fireballPrefab; [Tooltip("Optional spawn pivot for spells (e.g. staff tip). If null, uses summoner transform.")] public Transform spellSpawnPivot; [Tooltip("Cooldown between spell casts (seconds)")] public float spellCooldown = 6f; [Tooltip("Minimum distance required to cast spell")] public float spellMinDistance = 5f; [Tooltip("Maximum distance allowed to cast spell (0 = no limit)")] public float spellMaxDistance = 20f; [Tooltip("Only cast spell when minions are alive")] public bool spellRequiresMinionsAlive = true; [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; private float lastSpellTime = -999f; private bool spawnRequested = false; private float nextMeleeTime = 0f; // Public properties for FSM decisions public bool IsSpawning => isSpawning; public int ActiveMinionCount => activeMinions.Count; public bool CanSpawnMinions { get { if (isSpawning) return false; if (requireZeroMinionsToSummon && activeMinions.Count > 0) return false; return activeMinions.Count < maxActiveMinions; } } 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() { spawnRequested = false; if (isSpawning) { if (enableDebug) Debug.Log("[SummonerAI] Already spawning minions"); return; } if (minionPrefab == null) { Debug.LogError("[SummonerAI] No minion prefab assigned!"); return; } if (requireZeroMinionsToSummon && HasActiveMinions) { if (enableDebug) Debug.Log("[SummonerAI] Minions alive - summon blocked"); return; } spawnCoroutine = StartCoroutine(SpawnMinionsCoroutine()); } /// /// Request spawning to begin on an animation event. /// public void RequestSpawn() { spawnRequested = true; } /// /// Cancel a pending spawn request (e.g., on state exit). /// public void CancelSpawnRequest() { spawnRequested = false; } /// /// Animation event hook. Call from the summon animation. /// public void OnSummonAnimationEvent() { if (!spawnRequested) { return; } StartSpawning(); } /// /// 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 player is within the desired safe distance /// public bool IsPlayerTooCloseForSafeDistance() { return GetDistanceToPlayer() < safeDistance; } /// /// Check if summoner has reached/maintains safe distance /// public bool IsAtSafeDistance() { return GetDistanceToPlayer() >= safeDistance; } /// /// Should summoner try to flee to reach safe distance /// public bool ShouldFleeToSafeDistance() { float distance = GetDistanceToPlayer(); if (distance <= meleeEngageDistance) return false; if (CanSpawnMinions || CanCastSpell()) return false; return distance < safeDistance; } /// /// Check if summoner should engage in melee combat /// public bool ShouldEngageMelee() { if (!IsPlayerInMeleeRange()) return false; if (!CanMeleeNow()) return false; return !CanSpawnMinions && !CanCastSpell(); } public bool CanMeleeNow() { return Time.time >= nextMeleeTime; } public void NotifyMeleeAttack() { float min = Mathf.Min(meleeCooldownMin, meleeCooldownMax); float max = Mathf.Max(meleeCooldownMin, meleeCooldownMax); if (max <= 0f) { nextMeleeTime = 0f; return; } nextMeleeTime = Time.time + Random.Range(min, max); } /// /// Check if summoner can cast a ranged spell now /// public bool CanCastSpell() { if (isSpawning) return false; if (fireballPrefab == null) return false; if (spellRequiresMinionsAlive && !HasActiveMinions) return false; float distance = GetDistanceToPlayer(); if (distance < spellMinDistance) return false; if (spellMaxDistance > 0f && distance > spellMaxDistance) return false; return (Time.time - lastSpellTime) >= spellCooldown; } /// /// Notify that a spell was cast (updates cooldown timer) /// public void NotifySpellCast() { lastSpellTime = Time.time; } /// /// Spawn a fireball from pivot towards player /// public void CastFireball() { if (!CanCastSpell()) return; Transform pivot = spellSpawnPivot != null ? spellSpawnPivot : transform; Vector3 targetPos = playerTransform != null ? playerTransform.position + Vector3.up * 1f : (pivot.position + pivot.forward); Vector3 dir = (targetPos - pivot.position).normalized; if (dir == Vector3.zero) dir = pivot.forward; Quaternion rot = Quaternion.LookRotation(dir); LeanPool.Spawn(fireballPrefab, pivot.position, rot); NotifySpellCast(); if (enableDebug) Debug.Log("[SummonerAI] Cast fireball"); } /// /// 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 safe distance Gizmos.color = Color.yellow; DrawCircle(transform.position, safeDistance, 24); // 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; } } } }