using System; using System.Collections.Generic; using Unity.BossRoom.Gameplay.Configuration; using Unity.BossRoom.Gameplay.Actions; using UnityEngine; namespace Unity.BossRoom.Gameplay.GameplayObjects.Character.AI { /// /// Handles enemy AI. Contains AIStateLogics that handle some of the details, /// and has various utility functions that are called by those AIStateLogics /// public class AIBrain { private enum AIStateType { ATTACK, //WANDER, IDLE, } static readonly AIStateType[] k_AIStates = (AIStateType[])Enum.GetValues(typeof(AIStateType)); private ServerCharacter m_ServerCharacter; private ServerActionPlayer m_ServerActionPlayer; private AIStateType m_CurrentState; private Dictionary m_Logics; private List m_HatedEnemies; /// /// If we are created by a spawner, the spawner might override our detection radius /// -1 is a sentinel value meaning "no override" /// private float m_DetectRangeOverride = -1; public AIBrain(ServerCharacter me, ServerActionPlayer myServerActionPlayer) { m_ServerCharacter = me; m_ServerActionPlayer = myServerActionPlayer; m_Logics = new Dictionary { [AIStateType.IDLE] = new IdleAIState(this), //[ AIStateType.WANDER ] = new WanderAIState(this), // not written yet [AIStateType.ATTACK] = new AttackAIState(this, m_ServerActionPlayer), }; m_HatedEnemies = new List(); m_CurrentState = AIStateType.IDLE; } /// /// Should be called by the AIBrain's owner each Update() /// public void Update() { AIStateType newState = FindBestEligibleAIState(); if (m_CurrentState != newState) { m_Logics[newState].Initialize(); } m_CurrentState = newState; m_Logics[m_CurrentState].Update(); } /// /// Called when we received some HP. Positive HP is healing, negative is damage. /// /// The person who hurt or healed us. May be null. /// The amount of HP received. Negative is damage. public void ReceiveHP(ServerCharacter inflicter, int amount) { if (inflicter != null && amount < 0) { Hate(inflicter); } } private AIStateType FindBestEligibleAIState() { // for now we assume the AI states are in order of appropriateness, // which may be nonsensical when there are more states foreach (AIStateType aiStateType in k_AIStates) { if (m_Logics[aiStateType].IsEligible()) { return aiStateType; } } Debug.LogError("No AI states are valid!?!"); return AIStateType.IDLE; } /// /// Returns true if it be appropriate for us to murder this character, starting right now! /// public bool IsAppropriateFoe(ServerCharacter potentialFoe) { if (potentialFoe == null || potentialFoe.IsNpc || potentialFoe.LifeState != LifeState.Alive || potentialFoe.IsStealthy.Value) { return false; } // Also, we could use NavMesh.Raycast() to see if we have line of sight to foe? return true; } /// /// Notify the AIBrain that we should consider this character an enemy. /// /// public void Hate(ServerCharacter character) { if (!m_HatedEnemies.Contains(character)) { m_HatedEnemies.Add(character); } } /// /// Return the raw list of hated enemies -- treat as read-only! /// public List GetHatedEnemies() { // first we clean the list -- remove any enemies that have disappeared (became null), are dead, etc. for (int i = m_HatedEnemies.Count - 1; i >= 0; i--) { if (!IsAppropriateFoe(m_HatedEnemies[i])) { m_HatedEnemies.RemoveAt(i); } } return m_HatedEnemies; } /// /// Retrieve info about who we are. Treat as read-only! /// /// public ServerCharacter GetMyServerCharacter() { return m_ServerCharacter; } /// /// Convenience getter that returns the CharacterData associated with this creature. /// public CharacterClass CharacterData { get { return GameDataSource.Instance.CharacterDataByType[m_ServerCharacter.CharacterType]; } } /// /// The range at which this character can detect enemies, in meters. /// This is usually the same value as is indicated by our game data, but it /// can be dynamically overridden. /// public float DetectRange { get { return (m_DetectRangeOverride == -1) ? CharacterData.DetectRange : m_DetectRangeOverride; } set { m_DetectRangeOverride = value; } } } }