using System.Collections; using System.Collections.Generic; using UnityEngine; using RPGCreationKit; using RPGCreationKit.BehaviourTree; using System.Linq; namespace RPGCreationKit.AI { /// /// Manages the Behaviour Trees and the interactions with the AI agent from a tree. /// public class AIBehaviourTreed : AICombatSystem { public TreeTickRate tickRate; public int xFrames = 5; public float xSeconds = 1f; public PurposeState purposeState; [Space(10)] [Header("Behaviour Tree Control")] public bool useBT = true; public bool pauseBT = false; public RPGCK_BT currentBehaviour; public bool isUsingPurposeBehaviour = true; public RPGCK_BT purposeBehaviourTree; public RPGCK_BT combatBehaviourTree; [Header("Settings")] public bool keepTicking = true; public override void Start() { base.Start(); if (!isAlive) Die(); else if (useBT) { purposeBehaviourTree = purposeBehaviourTree.RPGCK_BTCopy(this.gameObject, GetComponent()); combatBehaviourTree = combatBehaviourTree.RPGCK_BTCopy(this.gameObject, GetComponent()); // Always start with the NormalBehaviourTree SwitchBehaviourTree(false); StartTicking(); } } public void StartTicking() { keepTicking = true; StopCoroutine(TickBehaviourTree()); StartCoroutine(TickBehaviourTree()); } IEnumerator TickBehaviourTree() { while (keepTicking) { while (pauseBT) yield return null; switch(tickRate) { case TreeTickRate.EveryFrame: yield return new WaitForEndOfFrame(); break; case TreeTickRate.EveryXFrames: int framesPassed = 0; while(framesPassed <= xFrames) { yield return new WaitForEndOfFrame(); framesPassed++; } break; case TreeTickRate.EveryXSeconds_Realtime: yield return new WaitForSecondsRealtime(xSeconds); break; case TreeTickRate.EveryXSeconds_GameTime: yield return new WaitForSeconds(xSeconds); break; } while (pauseBT || currentBehaviour == null || currentBehaviour.nodes.Count <= 0 || currentBehaviour.nodes[0] == null) yield return null; try { (currentBehaviour.nodes[0] as BTNode).Execute(); }catch { } /* if (firstTick == false) { // Tick (currentBehaviour.nodes[0] as BTNode).Execute(); firstTick = true; } else { // Find all the nodes that are still running and execute them ((currentBehaviour.nodes[0] as BTNode).GetOutputPort("firstNode").Connection.node as BTNode).Execute(); } */ yield return null; } } public void ChangeTickRate(TreeTickRate newTickRate) { tickRate = newTickRate; } protected bool anyWaitForSetRunning = false; protected bool anyWaitForSwitchRunning = false; public IEnumerator QueueSwitch(bool _combat) { if (anyWaitForSwitchRunning) yield return null; SwitchBehaviourTree(_combat); } [AIInvokable] public void SwitchBehaviourTree(bool _combat) { if (anyWaitForSwitchRunning) { StartCoroutine(QueueSwitch(_combat)); return; } // Execute OnExit node if present if (currentBehaviour != null && currentBehaviour.nodes.Count > 0 && ((currentBehaviour.nodes[0] as BTNode).GetOutputPort("onExitNode").Connection != null && ((currentBehaviour.nodes[0] as BTNode).GetOutputPort("onExitNode").Connection.node != null))) { anyWaitForSwitchRunning = true; // Execute OnExit before changing the tree StartCoroutine(SwitchBehaviourTree_WaitForExitNodeTask(_combat)); } else { if (_combat) { currentBehaviour = combatBehaviourTree; isUsingPurposeBehaviour = false; } else { currentBehaviour = purposeBehaviourTree; isUsingPurposeBehaviour = true; } } } IEnumerator SwitchBehaviourTree_WaitForExitNodeTask(bool _combat) { pauseBT = true; yield return new WaitForEndOfFrame(); var node = (((currentBehaviour.nodes[0] as BTNode).GetOutputPort("onExitNode").Connection.node) as BTNode); node.ReEvaluate(); while (node.m_NodeState == NodeState.Null || node.m_NodeState == NodeState.Running) { node.Execute(); yield return new WaitForEndOfFrame(); } if (anyWaitForSetRunning) { yield return new WaitForEndOfFrame(); } if (_combat) { currentBehaviour = combatBehaviourTree; isUsingPurposeBehaviour = false; } else { currentBehaviour = purposeBehaviourTree; isUsingPurposeBehaviour = true; } anyWaitForSwitchRunning = false; pauseBT = false; yield break; } [AIInvokable] public void SwitchBehaviourTreeToAI(string _rckID, bool _combat) { // Get the RckAI if possible RckAI sTo = null; CellsSystem.CellInformation.TryToGetAI(_rckID, out sTo); if (sTo != null) { sTo.SwitchBehaviourTree(_combat); } } public override void OnEnterInCombat() { base.OnEnterInCombat(); if (combatBehaviourTree.resetVariablesUponStartResume) combatBehaviourTree.ResetVariables(); if (isUsingActionPoint && !leavingActionPoint) StopUsingNPCActionPoint(); SwitchBehaviourTree(true); } [AIInvokable] public override void LeaveCombat() { base.LeaveCombat(); if (purposeBehaviourTree.resetVariablesUponStartResume) purposeBehaviourTree.ResetVariables(); // reset ishostile isHostile = false; isHostileAgainstPC = false; SwitchBehaviourTree(false); // Reset PurposeState if (purposeState.IsAssigned) { SetTarget(purposeState.Target); purposeState.ResumePurpose(); } } public override void Damage(DamageContext context) { if (context.sender != null && !context.sender.CompareTag("Player")) { // If they're both members of the same factions chances are they didn't meant to attach each other, so ignore for (int i = 0; i < belongsToFactions.Count; i++) { if ((context.sender as EntityFactionable).GetInFaction(belongsToFactions[i].ID)) return; } } base.Damage(context); if (aiSounds != null && isAlive) { if (aiSounds.takeDamageSounds.Length > 0) { int n = Random.Range(0, aiSounds.takeDamageSounds.Length); if (aiSounds.takeDamageSounds[n] != null) { audioSource.clip = aiSounds.takeDamageSounds[n]; audioSource.Play(); } } } if (context.sender != null) { // If they're both members of the same factions chances are they didn't meant to attach each other, so ignore if (isInCombat) { for (int i = 0; i < belongsToFactions.Count; i++) { if ((context.sender as EntityFactionable).GetInFaction(belongsToFactions[i].ID)) return; } } if (!enemyTargets.Any(t => t.m_entity == context.sender)) { if (isInConversation) StopConversation(); if (!m_isInCombat) EnterInCombatAgainst(context.sender); enemyTargets.Add(new VisibleEnemy(context.sender, context.sender.GetComponent(), new AggroInfo(50))); } else // this enemy was already in combat with this agent so just update the aggro { var entTarget = enemyTargets.Find(x => x.m_entity == context.sender); entTarget.m_aggro.AlterAggroValue(AggroSettings.Modifier.Damage, context.amount); float weaponAggroModifier = 0.0f; if (context.weaponItem != null) weaponAggroModifier = context.weaponItem.aggroModifier; else if (context.spell != null) weaponAggroModifier = context.spell.aggroModifier; entTarget.m_aggro.AlterAggroValue(AggroSettings.Modifier.Weapon, weaponAggroModifier); } } } public override void Die() { base.Die(); attributes.CurHealth = -1; attributes.activeEffects.Clear(); keepTicking = false; useBT = false; pauseBT = true; for (int i = 0; i < inventory.Items.Count; i++) { inventory.Items[i].metadata.IsOwnedByNPC = false; } } #region Methods and Functions specifically for Behaviour Trees public bool hasARangedWeapon = false; [AIInvokable] public void HasRangedWeaponCheck() { WeaponItem weaponItem = null; for (int i = 0; i < inventory.subLists[(int)ItemTypes.WeaponItem].Count; i++) { weaponItem = (WeaponItem)inventory.subLists[(int)ItemTypes.WeaponItem][i].item; if (weaponItem.weaponType == WeaponType.Bow || weaponItem.weaponType == WeaponType.Crossbow) { if (HasAmmoForWeapon(weaponItem)) { hasARangedWeapon = true; break; } else { Debug.Log("don't have ammo for " + weaponItem.ItemID); } } } } public bool hasAMeleeWeapon = false; [AIInvokable] public void HasMeleeWeaponCheck() { WeaponItem weaponItem = null; for (int i = 0; i < inventory.subLists[(int)ItemTypes.WeaponItem].Count; i++) { weaponItem = (WeaponItem)inventory.subLists[(int)ItemTypes.WeaponItem][i].item; if (weaponItem.weaponType == WeaponType.BladeOneHand || weaponItem.weaponType == WeaponType.BluntOneHand || weaponItem.weaponType == WeaponType.BladeTwoHands || weaponItem.weaponType == WeaponType.BluntTwoHands || weaponItem.weaponType == WeaponType.DaggerOneHand) { hasAMeleeWeapon = true; break; } } } public float distanceFromMainTarget; [AIInvokable] public void GetDistanceFromMainTarget() { if(mainTarget == null) { Debug.LogWarning("Called GetDistanceFromMainTarget() but no MainTarget has been set"); return; } distanceFromMainTarget = Vector3.Distance(this.transform.position, mainTarget.transform.position); } [AIInvokable] public void Weapon_SwitchToMelee() { isSwitchingWeapon = true; WeaponItem weaponItem = null; for (int i = 0; i < inventory.subLists[(int)ItemTypes.WeaponItem].Count; i++) { weaponItem = (WeaponItem)inventory.subLists[(int)ItemTypes.WeaponItem][i].item; if (weaponItem.weaponType == WeaponType.BladeOneHand || weaponItem.weaponType == WeaponType.BluntOneHand || weaponItem.weaponType == WeaponType.BladeTwoHands || weaponItem.weaponType == WeaponType.BluntTwoHands || weaponItem.weaponType == WeaponType.DaggerOneHand) { ChangeWeapon(weaponItem.ItemID); break; } } } [AIInvokable] public void Weapon_SwitchToRanged() { isSwitchingWeapon = true; WeaponItem weaponItem = null; for (int i = 0; i < inventory.subLists[(int)ItemTypes.WeaponItem].Count; i++) { weaponItem = (WeaponItem)inventory.subLists[(int)ItemTypes.WeaponItem][i].item; if (weaponItem.weaponType == WeaponType.Bow || weaponItem.weaponType == WeaponType.Crossbow) { // We got a weapon, check for ammo if (EquipAmmoForWeapon(weaponItem)) { ChangeWeapon(weaponItem); break; } } } } [AIInvokable] public void Weapon_SwitchToDefault() { ChangeWeapon(defaultWeapon.ItemID); } public float currentWeaponReach = RCKSettings.DEFAULT_WEAPON_REACH; [AIInvokable] public void GetWeaponsReach() { if (equipment.currentWeapon != null) currentWeaponReach = equipment.currentWeapon.Reach; else currentWeaponReach = RCKSettings.DEFAULT_WEAPON_REACH; // 4 is default value } [AIInvokable] public void SpeakToTarget() { if (mainTarget.CompareTag("Player")) RPGCreationKit.Player.RckPlayer.instance.StartDialogue(this.GetComponent()); else { IDialoguable[] targetDialoguable = new IDialoguable[2]; targetDialoguable[0] = mainTarget.GetComponent(); if (targetDialoguable == null) targetDialoguable[0] = mainTarget.GetComponentInParent(); // Set us targetDialoguable[1] = this; if (targetDialoguable[0] != null) { DialogueLogic(targetDialoguable, GetCurrentDialogueGraph()); } } } [AIInvokable] public void SetRckAIAsTarget(string _rckID) { // Get the RckAI if possible RckAI sTo = null; CellsSystem.CellInformation.TryToGetAI(_rckID, out sTo); if (sTo != null) SetTarget(sTo.gameObject); } public override void OnDialogueEnds(GameObject target) { //// PURPOSE_CALLBACK: ClearOnTeleportToCell \\\\\ if (purposeState != null && purposeState.IsAssigned && purposeState.clearsOn == PurposeClearTypes.ClearOnFinishTalkingWith) { if (purposeState.clearsOnData.stringData == speakingTo.GetEntityGameObject().GetComponent().entityID) { // We did it purposeState.CompletePurpose(); } } base.OnDialogueEnds(target); } /// /// Forces the AI to stop the dialogue /// public void StopConversation() { if (entitiesTalkingWith != null) { // Copy before destroying entitiesTalkingWith List dial = new List(); for (int i = 0; i < entitiesTalkingWith.Length; i++) dial.Add(entitiesTalkingWith[i]); for (int i = 0; i < dial.Count; i++) dial[i].ForceToStopDialogue(); } } bool goingToSpeak = false; [AIInvokable] public void SendAIToSpeakToPlayer(bool run) { if (goingToSpeak) return; goingToSpeak = true; StopCoroutine(SendAIToSpeakToPlayerTask()); if ( isReachingActionPoint || isUsingActionPoint) { ResetActionPointAgentState(); StopUsingNPCActionPoint(true); } if (run) Movements_SwitchToRun(); else Movements_SwitchToWalk(); // Set maintarget SetTarget(Player.RckPlayer.instance.gameObject); // Follow until he reaches it StartCoroutine(SendAIToSpeakToPlayerTask()); } public IEnumerator SendAIToSpeakToPlayerTask() { if (!isLoaded) { goingToSpeak = false; yield break; } while (Vector3.Distance(this.gameObject.transform.position, Player.RckPlayer.instance.transform.position) > RCKSettings.NPC_TO_NPC_CONVERSATION_DISTANCE || !CanDialogue() || !Player.RckPlayer.instance.CanDialogue() || !WorldManager.instance.isLoading && !Player.RckPlayer.instance.IsControlledByPlayer() || isInOfflineMode || !isLoaded) yield return new WaitForEndOfFrame(); Player.RckPlayer.instance.StartDialogue(this); goingToSpeak = false; yield break; } [AIInvokable] public void AssignPurpose(RckAI _AI, GameObject _target, PurposeClearTypes _clearsOn, PurposeStateClearsOnData _clearOnData, string _nextPurposeBehaviourTree = null) { purposeState.AssignPurpose(_AI, _target, _clearsOn, _clearOnData, _nextPurposeBehaviourTree); } [AIInvokable] public void Purpose_AssignToFollowCurrentPath() { if(aiPath != null && aiPath.gameObject != null) purposeState.AssignPurpose(GetComponent(), aiPath.gameObject, PurposeClearTypes.ClearOnDeath, null, null); } [AIInvokable] public void Magic_SwitchToHealingSpell() { // Check if the AI already has an healing spell if (spellsKnowledge.spellInUse != null && spellsKnowledge.spellInUse.tag == SpellTag.RestoreHealth) return; for(int i = 0; i < spellsKnowledge.Spells.Count; i++) { if(spellsKnowledge.Spells[i].tag == SpellTag.RestoreHealth) { spellsKnowledge.spellInUse = spellsKnowledge.Spells[i]; return; } } } [AIInvokable] public void Magic_SwitchToDamageSpell() { // Check if the AI already has an healing spell if (spellsKnowledge.spellInUse != null && spellsKnowledge.spellInUse.tag == SpellTag.DamageProjectile || spellsKnowledge.spellInUse.tag == SpellTag.DamageTouch) return; for (int i = 0; i < spellsKnowledge.Spells.Count; i++) { if (spellsKnowledge.Spells[i].tag == SpellTag.DamageProjectile || spellsKnowledge.Spells[i].tag == SpellTag.DamageTouch) { spellsKnowledge.spellInUse = spellsKnowledge.Spells[i]; return; } } } [AIInvokable] public void Magic_SetSpell(string _spellIDToSet) { Spell _spellToSet = SpellsDatabase.GetSpell(_spellIDToSet); if (_spellToSet != null) spellsKnowledge.spellInUse = _spellToSet; } [AIInvokable] public void Magic_AddSpellToKnowledge(string _spellIDToAdd) { Spell _spellToAdd = SpellsDatabase.GetSpell(_spellIDToAdd); if (_spellToAdd != null) spellsKnowledge.Spells.Add(_spellToAdd); } #endregion } }