Files
beyond/Assets/AI/_Summoner/SummonerAI.cs
2025-11-20 14:34:59 +01:00

359 lines
9.0 KiB
C#

using Invector;
using Invector.vCharacterController.AI;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace DemonBoss.Summoner
{
/// <summary>
/// Main AI controller for Summoner enemy
/// Manages spawned minions and provides combat state tracking
/// Attach to Summoner character along with vControlAI
/// </summary>
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<GameObject> activeMinions = new List<GameObject>();
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<vHealthController>();
audioSource = GetComponent<AudioSource>();
if (audioSource == null && summonSound != null)
{
audioSource = gameObject.AddComponent<AudioSource>();
audioSource.playOnAwake = false;
audioSource.spatialBlend = 1f;
}
}
private void Start()
{
FindPlayer();
}
private void Update()
{
CleanupDeadMinions();
}
/// <summary>
/// Find player by tag
/// </summary>
private void FindPlayer()
{
GameObject player = GameObject.FindGameObjectWithTag(playerTag);
if (player != null)
{
playerTransform = player.transform;
if (enableDebug) Debug.Log("[SummonerAI] Player found: " + player.name);
}
}
/// <summary>
/// Start spawning minions
/// </summary>
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());
}
/// <summary>
/// Stop spawning minions immediately
/// </summary>
public void StopSpawning()
{
if (spawnCoroutine != null)
{
StopCoroutine(spawnCoroutine);
spawnCoroutine = null;
}
isSpawning = false;
}
/// <summary>
/// Coroutine that spawns minions with delays
/// </summary>
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;
}
/// <summary>
/// Spawn a single minion at random position around summoner
/// </summary>
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<vControlAI>();
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}");
}
/// <summary>
/// Remove destroyed/null minions from list
/// </summary>
private void CleanupDeadMinions()
{
activeMinions.RemoveAll(minion => minion == null);
}
/// <summary>
/// Check if summoner should spawn minions based on health
/// </summary>
public bool ShouldSummonByHealth()
{
if (healthController == null) return false;
float healthPercent = healthController.currentHealth / healthController.MaxHealth;
return healthPercent <= healthThresholdForSummon;
}
/// <summary>
/// Get distance to player
/// </summary>
public float GetDistanceToPlayer()
{
if (playerTransform == null)
{
FindPlayer();
if (playerTransform == null) return float.MaxValue;
}
return Vector3.Distance(transform.position, playerTransform.position);
}
/// <summary>
/// Check if player is in melee range
/// </summary>
public bool IsPlayerInMeleeRange()
{
return GetDistanceToPlayer() <= meleeEngageDistance;
}
/// <summary>
/// Check if summoner should engage in melee combat
/// </summary>
public bool ShouldEngageMelee()
{
if (!IsPlayerInMeleeRange()) return false;
if (fightWithMinions) return true;
return !HasActiveMinions;
}
/// <summary>
/// Destroy all active minions (e.g., when summoner dies)
/// </summary>
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;
}
}
}
}