From 53f51d6bde019122a1567258d512623ffc369436 Mon Sep 17 00:00:00 2001 From: Leath Cooper Date: Wed, 1 Sep 2021 01:04:48 -0400 Subject: [PATCH] behavior tree initial commit --- Projects/UOContent/Mobiles/AI/BaseAI.cs | 25 ++- .../UOContent/Mobiles/AI/BehaviorTreeAI.cs | 45 ++++ Projects/UOContent/Mobiles/AI/MageAI.cs | 11 + Projects/UOContent/Mobiles/BaseCreature.cs | 1 + .../Mobiles/BehaviorTreeAI/ActionNode.cs | 15 ++ .../Actions/CastSpellActionNode.cs | 63 ++++++ .../BehaviorTreeAI/Actions/CureSelfNode.cs | 105 +++++++++ .../Actions/DetectHiddenActionNode.cs | 118 ++++++++++ .../Actions/HealSelfActionNode.cs | 58 +++++ .../BehaviorTreeAI/Actions/MageComboNode.cs | 185 ++++++++++++++++ .../BehaviorTreeAI/Actions/SelfHealerNode.cs | 50 +++++ .../Actions/TargetActionNode.cs | 59 +++++ .../BehaviorTreeAI/AlwaysSucceedNode.cs | 27 +++ .../Mobiles/BehaviorTreeAI/BehaviorTree.cs | 207 ++++++++++++++++++ .../BehaviorTreeAI/BehaviorTreeNode.cs | 21 ++ .../Mobiles/BehaviorTreeAI/ConditionNode.cs | 25 +++ .../Mobiles/BehaviorTreeAI/DecoratorNode.cs | 21 ++ .../Mobiles/BehaviorTreeAI/InverterNode.cs | 35 +++ .../Mobiles/BehaviorTreeAI/LeafNode.cs | 15 ++ .../Mobiles/BehaviorTreeAI/RepeaterNode.cs | 28 +++ .../Mobiles/BehaviorTreeAI/SelectorNode.cs | 48 ++++ .../Mobiles/BehaviorTreeAI/SequenceNode.cs | 48 ++++ .../Mobiles/BehaviorTreeAI/TapNode.cs | 31 +++ .../Mobiles/BehaviorTreeAI/UntilFailNode.cs | 29 +++ .../UOContent/Mobiles/Healers/EvilHealer.cs | 6 + 25 files changed, 1273 insertions(+), 3 deletions(-) create mode 100644 Projects/UOContent/Mobiles/AI/BehaviorTreeAI.cs create mode 100644 Projects/UOContent/Mobiles/BehaviorTreeAI/ActionNode.cs create mode 100644 Projects/UOContent/Mobiles/BehaviorTreeAI/Actions/CastSpellActionNode.cs create mode 100644 Projects/UOContent/Mobiles/BehaviorTreeAI/Actions/CureSelfNode.cs create mode 100644 Projects/UOContent/Mobiles/BehaviorTreeAI/Actions/DetectHiddenActionNode.cs create mode 100644 Projects/UOContent/Mobiles/BehaviorTreeAI/Actions/HealSelfActionNode.cs create mode 100644 Projects/UOContent/Mobiles/BehaviorTreeAI/Actions/MageComboNode.cs create mode 100644 Projects/UOContent/Mobiles/BehaviorTreeAI/Actions/SelfHealerNode.cs create mode 100644 Projects/UOContent/Mobiles/BehaviorTreeAI/Actions/TargetActionNode.cs create mode 100644 Projects/UOContent/Mobiles/BehaviorTreeAI/AlwaysSucceedNode.cs create mode 100644 Projects/UOContent/Mobiles/BehaviorTreeAI/BehaviorTree.cs create mode 100644 Projects/UOContent/Mobiles/BehaviorTreeAI/BehaviorTreeNode.cs create mode 100644 Projects/UOContent/Mobiles/BehaviorTreeAI/ConditionNode.cs create mode 100644 Projects/UOContent/Mobiles/BehaviorTreeAI/DecoratorNode.cs create mode 100644 Projects/UOContent/Mobiles/BehaviorTreeAI/InverterNode.cs create mode 100644 Projects/UOContent/Mobiles/BehaviorTreeAI/LeafNode.cs create mode 100644 Projects/UOContent/Mobiles/BehaviorTreeAI/RepeaterNode.cs create mode 100644 Projects/UOContent/Mobiles/BehaviorTreeAI/SelectorNode.cs create mode 100644 Projects/UOContent/Mobiles/BehaviorTreeAI/SequenceNode.cs create mode 100644 Projects/UOContent/Mobiles/BehaviorTreeAI/TapNode.cs create mode 100644 Projects/UOContent/Mobiles/BehaviorTreeAI/UntilFailNode.cs diff --git a/Projects/UOContent/Mobiles/AI/BaseAI.cs b/Projects/UOContent/Mobiles/AI/BaseAI.cs index 42f74c24f..0ad9e0543 100644 --- a/Projects/UOContent/Mobiles/AI/BaseAI.cs +++ b/Projects/UOContent/Mobiles/AI/BaseAI.cs @@ -13,6 +13,7 @@ using Server.Spells; using Server.Spells.Spellweaving; using Server.Targets; using MoveImpl = Server.Movement.MovementImpl; +using Server.Mobiles.BehaviorTreeAI; namespace Server.Mobiles { @@ -27,7 +28,8 @@ namespace Server.Mobiles AI_Mage, AI_Berserk, AI_Predator, - AI_Thief + AI_Thief, + AI_BehaviorTree } public enum ActionType @@ -136,6 +138,21 @@ namespace Server.Mobiles } Action = ActionType.Wander; + + /* + behaviorTree = new BehaviorTree(this.m_Mobile); + + behaviorTree.AddRoot( + new RepeaterNode(behaviorTree, + new SelectorNode(behaviorTree, new BehaviorTreeNode[1] + { + new SelfHealerNode(behaviorTree), + }) + ) + ); + + behaviorTree.Start(); + */ } public ActionType Action @@ -2019,7 +2036,7 @@ namespace Server.Mobiles if (canOpenDoors || canDestroyObstacles) { - m_Mobile.DebugSay("My movement was blocked, I will try to clear some obstacles."); + // m_Mobile.DebugSay("My movement was blocked, I will try to clear some obstacles."); var map = m_Mobile.Map; @@ -2445,7 +2462,7 @@ namespace Server.Mobiles m_Mobile.NextReacquireTime = Core.TickCount + (int)m_Mobile.ReacquireDelay.TotalMilliseconds; - m_Mobile.DebugSay("Acquiring..."); + // m_Mobile.DebugSay("Acquiring..."); var map = m_Mobile.Map; @@ -3090,6 +3107,7 @@ namespace Server.Mobiles } } + /* if (m_Owner.CanDetectHidden && Core.TickCount - m_Owner.m_NextDetectHidden >= 0) { m_Owner.DetectHidden(); @@ -3103,6 +3121,7 @@ namespace Server.Mobiles m_Owner.m_NextDetectHidden = Core.TickCount + (int)TimeSpan.FromSeconds(Utility.RandomMinMax(min, max)).TotalMilliseconds; } + */ } } } diff --git a/Projects/UOContent/Mobiles/AI/BehaviorTreeAI.cs b/Projects/UOContent/Mobiles/AI/BehaviorTreeAI.cs new file mode 100644 index 000000000..f9a281bde --- /dev/null +++ b/Projects/UOContent/Mobiles/AI/BehaviorTreeAI.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Server.Mobiles.BehaviorTreeAI; +using Server.Targeting; + +namespace Server.Mobiles.AI +{ + public class BehaviorTreeAI : BaseAI + { + private BehaviorTree behaviorTree; + public BehaviorTreeAI(BaseCreature m) : base(m) + { + behaviorTree = new BehaviorTree(m); + + behaviorTree.AddRoot( + new SelectorNode(behaviorTree, new BehaviorTreeNode[2] + { + new SelfHealerNode(behaviorTree), + // new MageComboNode(behaviorTree), + new TapNode( + behaviorTree, + (mob) => mob.DebugSay("I am taking no action") + ) + }) + ); + + behaviorTree.Start(); + } + + public override bool Think() + { + if (m_Mobile.Deleted) + { + return false; + } + + // behaviorTree.RunBehavior(); + + return base.Think(); + } + } +} diff --git a/Projects/UOContent/Mobiles/AI/MageAI.cs b/Projects/UOContent/Mobiles/AI/MageAI.cs index 93bc51226..8631bfba0 100644 --- a/Projects/UOContent/Mobiles/AI/MageAI.cs +++ b/Projects/UOContent/Mobiles/AI/MageAI.cs @@ -106,12 +106,14 @@ namespace Server.Mobiles base.DoActionWander(); + /* if (Utility.RandomDouble() < 0.05) { var spell = CheckCastHealingSpell(); spell?.Cast(); } + */ } return true; @@ -120,10 +122,12 @@ namespace Server.Mobiles private Spell CheckCastHealingSpell() { // If I'm poisoned, always attempt to cure. + /* if (m_Mobile.Poisoned) { return new CureSpell(m_Mobile); } + */ // Summoned creatures never heal themselves. if (m_Mobile.Summoned) @@ -156,6 +160,7 @@ namespace Server.Mobiles Spell spell = null; + /* if (m_Mobile.Hits < m_Mobile.HitsMax - 50) { if (UseNecromancy()) @@ -171,6 +176,7 @@ namespace Server.Mobiles { spell = new HealSpell(m_Mobile); } + */ double delay; @@ -775,6 +781,7 @@ namespace Server.Mobiles Spell spell; var toDispel = FindDispelTarget(true); + /* if (m_Mobile.Poisoned) // Top cast priority is cure { m_Mobile.DebugSay("I am going to cure myself"); @@ -782,6 +789,8 @@ namespace Server.Mobiles spell = new CureSpell(m_Mobile); } else if (toDispel != null) // Something dispellable is attacking us + */ + if (toDispel != null) { m_Mobile.DebugSay("I am going to dispel {0}", toDispel); @@ -901,10 +910,12 @@ namespace Server.Mobiles RunFrom(m_Mobile.FocusMob); m_Mobile.FocusMob = null; + /* if (m_Mobile.Poisoned && Utility.Random(0, 5) == 0) { new CureSpell(m_Mobile).Cast(); } + */ } else { diff --git a/Projects/UOContent/Mobiles/BaseCreature.cs b/Projects/UOContent/Mobiles/BaseCreature.cs index f846378ac..922fccfee 100644 --- a/Projects/UOContent/Mobiles/BaseCreature.cs +++ b/Projects/UOContent/Mobiles/BaseCreature.cs @@ -2187,6 +2187,7 @@ namespace Server.Mobiles // m_AI = new PredatorAI(this); new MeleeAI(this), AIType.AI_Thief => new ThiefAI(this), + AIType.AI_BehaviorTree => new AI.BehaviorTreeAI(this), _ => null }; } diff --git a/Projects/UOContent/Mobiles/BehaviorTreeAI/ActionNode.cs b/Projects/UOContent/Mobiles/BehaviorTreeAI/ActionNode.cs new file mode 100644 index 000000000..f1cd3da6e --- /dev/null +++ b/Projects/UOContent/Mobiles/BehaviorTreeAI/ActionNode.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Server.Mobiles.BehaviorTreeAI +{ + public abstract class ActionNode : LeafNode + { + public ActionNode(BehaviorTree tree) : base(tree) + { + } + } +} diff --git a/Projects/UOContent/Mobiles/BehaviorTreeAI/Actions/CastSpellActionNode.cs b/Projects/UOContent/Mobiles/BehaviorTreeAI/Actions/CastSpellActionNode.cs new file mode 100644 index 000000000..39b81a6ba --- /dev/null +++ b/Projects/UOContent/Mobiles/BehaviorTreeAI/Actions/CastSpellActionNode.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Server.Spells; + +namespace Server.Mobiles.BehaviorTreeAI +{ + public class CastSpellActionNode : ActionNode + { + private Func getSpellCallback; + public CastSpellActionNode(BehaviorTree tree, Func callback) : base(tree) + { + object nextCastTime; + if (!Tree.Blackboard.TryGetValue("nextCastTime", out nextCastTime)) + { + tree.Blackboard.Add("nextCastTime", DateTime.Now); + } + getSpellCallback = callback; + } + + public override Result Execute() + { + object nextCastTime; + + if (Tree.Blackboard.TryGetValue("nextCastTime", out nextCastTime) && DateTime.Now < (DateTime)nextCastTime) + { + Tree.Mobile.DebugSay("Spells are on cooldown..."); + return Result.Failure; + } + + if (Tree.Mobile.Spell == null) + { + Spell spell = getSpellCallback(Tree.Mobile); + + Tree.Mobile.DebugSay("Casting..."); + if (!spell.Cast()) + { + Tree.Mobile.DebugSay("Failed to cast..."); + return Result.Failure; + } + + if (!string.IsNullOrEmpty(spell.Mantra)) + Tree.Mobile.PublicOverheadMessage(Network.MessageType.Spell, 0, false, spell.Mantra, false); + + return Result.Running; + } + else if (Tree.Mobile.Spell.IsCasting) + { + Tree.Mobile.DebugSay("Still casting..."); + return Result.Running; + } + else if (!Tree.Mobile.Spell.IsCasting) + { + Tree.Mobile.DebugSay("Finished casting..."); + return Result.Success; + } + + return Result.Failure; + } + } +} diff --git a/Projects/UOContent/Mobiles/BehaviorTreeAI/Actions/CureSelfNode.cs b/Projects/UOContent/Mobiles/BehaviorTreeAI/Actions/CureSelfNode.cs new file mode 100644 index 000000000..3e0b48f60 --- /dev/null +++ b/Projects/UOContent/Mobiles/BehaviorTreeAI/Actions/CureSelfNode.cs @@ -0,0 +1,105 @@ +using System; +using Server.Spells; +using Server.Spells.Second; +using System.Collections.Generic; + +namespace Server.Mobiles.BehaviorTreeAI +{ + public class CureSelfNode : SequenceNode + { + public CureSelfNode(BehaviorTree tree) : base(tree) + { + Children = new List(new BehaviorTreeNode[5] + { + new ConditionNode( + tree, + (mob) => + { + if (mob.Poison == null) + { + mob.DebugSay("I am not poisoned"); + return false; + } + + object nextHealTime; + if (Tree.Blackboard.TryGetValue("nextHealTime", out nextHealTime)) + { + if (DateTime.Now >= (DateTime)nextHealTime) + { + return true; + } + mob.DebugSay("My heal cooldown is active"); + } + + mob.DebugSay("My heal cooldown is not set"); + + return false; + } + ), + new ConditionNode( + tree, + (mob) => + { + return mob.Spell == null || !mob.Spell.IsCasting; + } + ), + new CastSpellActionNode( + tree, + (mob) => new CureSpell(mob) + ), + new AlwaysSucceedNode( + tree, + new TapNode( + tree, + (mob) => + { + Tree.Blackboard.Remove("nextHealTime"); + Tree.Blackboard.Add("nextHealTime", DateTime.Now + TimeSpan.FromSeconds(10.0)); + } + ) + ), + new TargetActionNode(tree), + + }); + } + } + /* + public class CureSelfActionNode : ActionNode + { + public TimeSpan Cooldown { get; private set; } + public CureSelfActionNode(BehaviorTree tree, TimeSpan cooldown) : base(tree) + { + object nextHealTime; + if (!Tree.Blackboard.TryGetValue("nextHealTime", out nextHealTime)) + { + tree.Blackboard.Add("nextHealTime", DateTime.Now); + } + Cooldown = cooldown; + } + + public override Result Execute() + { + object nextHealTime; + if (Tree.Mobile.Poison != null && Tree.Blackboard.TryGetValue("nextHealTime", out nextHealTime)) + { + if (DateTime.Now >= (DateTime)nextHealTime) + { + new CureSpell(Tree.Mobile).Cast(); + Tree.Mobile.DebugSay("Casting cure spell for myself"); + Tree.Blackboard.Remove("nextHealTime"); + Tree.Blackboard.Add("nextHealTime", DateTime.Now + Cooldown); + return Result.Success; + } + + Tree.Mobile.DebugSay("I'm poisoned, but my healing is on cooldown"); + + return Result.Failure; + } + + Tree.Mobile.DebugSay("I'm not poisoned"); + + return Result.Failure; + } + } + */ +} diff --git a/Projects/UOContent/Mobiles/BehaviorTreeAI/Actions/DetectHiddenActionNode.cs b/Projects/UOContent/Mobiles/BehaviorTreeAI/Actions/DetectHiddenActionNode.cs new file mode 100644 index 000000000..afdf15022 --- /dev/null +++ b/Projects/UOContent/Mobiles/BehaviorTreeAI/Actions/DetectHiddenActionNode.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Server.Mobiles.BehaviorTreeAI +{ + class DetectHiddenActionNode : BehaviorTreeNode + { + public TimeSpan Cooldown { get; private set; } + public DetectHiddenActionNode(BehaviorTree tree, TimeSpan cooldown) : base(tree) + { + object nextDetectTime; + if (!Tree.Blackboard.TryGetValue("nextDetectTime", out nextDetectTime)) + { + tree.Blackboard.Add("nextDetectTime", DateTime.Now); + } + Cooldown = cooldown; + } + public override Result Execute() + { + /* + if (m_Mobile.Deleted || m_Mobile.Map == null) + { + return; + } + + m_Mobile.DebugSay("Checking for hidden players"); + + var srcSkill = m_Mobile.Skills.DetectHidden.Value; + + if (srcSkill <= 0) + { + return; + } + + var eable = m_Mobile.GetMobilesInRange(m_Mobile.RangePerception); + + foreach (var trg in eable) + { + if (trg != m_Mobile && trg.Player && trg.Alive && trg.Hidden && trg.AccessLevel == AccessLevel.Player && + m_Mobile.InLOS(trg)) + { + m_Mobile.DebugSay("Trying to detect {0}", trg.Name); + + var trgHiding = trg.Skills.Hiding.Value / 2.9; + var trgStealth = trg.Skills.Stealth.Value / 1.8; + + var chance = srcSkill / 1.2 - Math.Min(trgHiding, trgStealth); + + if (chance < srcSkill / 10) + { + chance = srcSkill / 10; + } + + chance /= 100; + + if (chance > Utility.RandomDouble()) + { + trg.RevealingAction(); + trg.SendLocalizedMessage(500814); // You have been revealed! + } + } + } + + eable.Free(); + */ + + Tree.Mobile.DebugSay("DetectHiddenActionNode"); + + object nextDetectTime; + + if (Tree.Blackboard.TryGetValue("nextDetectTime", out nextDetectTime)) + { + if (DateTime.Now >= (DateTime)nextDetectTime) + { + var srcSkill = Tree.Mobile.Skills.DetectHidden.Value; + + var eable = Tree.Mobile.GetMobilesInRange(Tree.Mobile.RangePerception); + + foreach (var trg in eable) + { + if (trg != Tree.Mobile && trg.Player && trg.Alive && trg.Hidden && trg.AccessLevel == AccessLevel.Player && + Tree.Mobile.InLOS(trg)) + { + Tree.Mobile.DebugSay("DetectHiddenActionNode {0}", trg.Name); + + var trgHiding = trg.Skills.Hiding.Value / 2.9; + var trgStealth = trg.Skills.Stealth.Value / 1.8; + + var chance = srcSkill / 1.2 - Math.Min(trgHiding, trgStealth); + + if (chance < srcSkill / 10) + { + chance = srcSkill / 10; + } + + chance /= 100; + + if (chance > Utility.RandomDouble()) + { + trg.RevealingAction(); + trg.SendLocalizedMessage(500814); // You have been revealed! + } + } + } + + return Result.Success; + } + + Tree.Mobile.DebugSay("DetectHiddenActionNode Cooldown"); + } + + return Result.Failure; + } + } +} diff --git a/Projects/UOContent/Mobiles/BehaviorTreeAI/Actions/HealSelfActionNode.cs b/Projects/UOContent/Mobiles/BehaviorTreeAI/Actions/HealSelfActionNode.cs new file mode 100644 index 000000000..5e8164a5b --- /dev/null +++ b/Projects/UOContent/Mobiles/BehaviorTreeAI/Actions/HealSelfActionNode.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Server.Spells.First; +using Server.Spells.Fourth; + +namespace Server.Mobiles.BehaviorTreeAI +{ + + public class HealSelfActionNode : BehaviorTreeNode + { + public TimeSpan Cooldown { get; private set; } + public HealSelfActionNode(BehaviorTree tree, TimeSpan cooldown) : base(tree) + { + object nextHealTime; + if (!Tree.Blackboard.TryGetValue("nextHealTime", out nextHealTime)) + { + tree.Blackboard.Add("nextHealTime", DateTime.Now); + } + Cooldown = cooldown; + } + public override Result Execute() + { + object nextHealTime; + if (Tree.Mobile.Hits < Tree.Mobile.HitsMax && Tree.Blackboard.TryGetValue("nextHealTime", out nextHealTime)) + { + if (DateTime.Now >= (DateTime)nextHealTime) + { + if (Tree.Mobile.Hits < Tree.Mobile.HitsMax - 50) + { + new GreaterHealSpell(Tree.Mobile).Cast(); + Tree.Mobile.DebugSay("Casting greater heal for myself"); + } + else + { + new HealSpell(Tree.Mobile).Cast(); + Tree.Mobile.DebugSay("Casting heal spell for myself"); + } + + Tree.Blackboard.Remove("nextHealTime"); + Tree.Blackboard.Add("nextHealTime", DateTime.Now + Cooldown); + + return Result.Success; + } + + Tree.Mobile.DebugSay("I'm hurt, but my healing is on cooldown {0}s", ((DateTime)nextHealTime - DateTime.Now).TotalSeconds); + + return Result.Failure; + } + + Tree.Mobile.DebugSay("I'm at full health"); + + return Result.Failure; + } + } +} diff --git a/Projects/UOContent/Mobiles/BehaviorTreeAI/Actions/MageComboNode.cs b/Projects/UOContent/Mobiles/BehaviorTreeAI/Actions/MageComboNode.cs new file mode 100644 index 000000000..d1abef6b9 --- /dev/null +++ b/Projects/UOContent/Mobiles/BehaviorTreeAI/Actions/MageComboNode.cs @@ -0,0 +1,185 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Server.Spells.Sixth; +using Server.Spells.Fifth; +using Server.Spells.First; + +namespace Server.Mobiles.BehaviorTreeAI +{ + public class MageComboNode : SequenceNode + { + public MageComboNode(BehaviorTree tree) : base(tree) + { + /* + Children = new List(new BehaviorTreeNode[1] + { + // if we have a combatant + new ConditionNode( + tree, + mob => mob.Combatant != null, + ) + new ConditionNode( + tree, + mob => mob.Combatant != null, + // do the following in sequence + new SelectorNode( + tree, + new BehaviorTreeNode[2] + { + // do the first appropriate debuff until all are done + new SelectorNode( + tree, + new BehaviorTreeNode[3] + { + // weaken if necessary + new ConditionNode( + tree, + mob => mob.Combatant.Str == mob.Combatant.RawStr, + new SequenceNode( + tree, + new BehaviorTreeNode[2] + { + new CastSpellActionNode( + tree, + mob => new WeakenSpell(mob) + ), + new TargetActionNode(tree) + } + ) + ), + // clumbsy if necessary + new ConditionNode( + tree, + mob => mob.Combatant.Dex == mob.Combatant.RawDex, + new SequenceNode( + tree, + new BehaviorTreeNode[2] + { + new CastSpellActionNode( + tree, + mob => new ClumsySpell(mob) + ), + new TargetActionNode(tree) + } + ) + ), + // feeblemind if necessary + new ConditionNode( + tree, + mob => mob.Combatant.Int == mob.Combatant.RawInt, + new SequenceNode( + tree, + new BehaviorTreeNode[2] + { + new CastSpellActionNode( + tree, + mob => new FeeblemindSpell(mob) + ), + new TargetActionNode(tree) + } + ) + ), + } + ), + // do explosion ebolt combo + new SequenceNode( + tree, + new BehaviorTreeNode[2] + { + // explosion + new SequenceNode( + tree, + new BehaviorTreeNode[2] + { + new CastSpellActionNode( + tree, + mob => new ExplosionSpell(mob) + ), + new TargetActionNode(tree) + } + ), + // ebolt + new SequenceNode( + tree, + new BehaviorTreeNode[2] + { + new CastSpellActionNode( + tree, + mob => new EnergyBoltSpell(mob) + ), + new TargetActionNode(tree) + } + ) + } + ), + } + ) + ) + }); + */ + + /* + Children = new List(new BehaviorTreeNode[1] { + new ConditionNode( + tree, + mob => mob.Combatant != null, + new SequenceNode( + tree, + new BehaviorTreeNode[3] { + new AlwaysSucceedNode( + tree, + new ConditionNode( + tree, + mob => mob.Combatant != null && mob.Combatant.Str == mob.Combatant.RawStr, + new SequenceNode( + tree, + new BehaviorTreeNode[2] + { + new CastSpellActionNode( + tree, + mob => new WeakenSpell(mob) + ), + new TargetActionNode(tree) + } + ) + ) + ), + new SequenceNode(tree, new BehaviorTreeNode[2]{ + new CastSpellActionNode( + tree, + mob => new ExplosionSpell(mob) + ), + new TargetActionNode(tree) + }), + new SequenceNode(tree, new BehaviorTreeNode[2]{ + new SelectorNode( + tree, + new BehaviorTreeNode[2] + { + new ConditionNode( + tree, + mob => Utility.RandomDouble() < 0.3, + new CastSpellActionNode( + tree, + mob => new MindBlastSpell(mob) + ) + ), + new CastSpellActionNode( + tree, + mob => new EnergyBoltSpell(mob) + ) + } + ), + new TargetActionNode(tree) + }), + } + ) + ) + }); + */ + } + } +} diff --git a/Projects/UOContent/Mobiles/BehaviorTreeAI/Actions/SelfHealerNode.cs b/Projects/UOContent/Mobiles/BehaviorTreeAI/Actions/SelfHealerNode.cs new file mode 100644 index 000000000..d71644eaf --- /dev/null +++ b/Projects/UOContent/Mobiles/BehaviorTreeAI/Actions/SelfHealerNode.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; + +namespace Server.Mobiles.BehaviorTreeAI +{ + class SelfHealerNode : SelectorNode + { + public SelfHealerNode(BehaviorTree tree) : base(tree) + { + Children = new List(new BehaviorTreeNode[1] + { + new SequenceNode( + tree, + new BehaviorTreeNode[2] + { + new ConditionNode( + tree, + mob => true // Utility.RandomDouble() < 0.5 + ), + new CureSelfNode(tree), + } + ) + /* + new SequenceNode( + tree, + new BehaviorTreeNode[2] + { + new ConditionNode( + tree, + mob => mob.Hits < mob.HitsMax - 20 + ), + new SequenceNode( + tree, + new BehaviorTreeNode[3] + { + new ConditionNode( + tree, + mob => true // Utility.RandomDouble() < 0.2 + ), + new HealSelfActionNode(tree, TimeSpan.FromSeconds(10.0)), + new TargetActionNode(tree) + } + ) + } + ) + */ + }); + } + } +} diff --git a/Projects/UOContent/Mobiles/BehaviorTreeAI/Actions/TargetActionNode.cs b/Projects/UOContent/Mobiles/BehaviorTreeAI/Actions/TargetActionNode.cs new file mode 100644 index 000000000..32b194622 --- /dev/null +++ b/Projects/UOContent/Mobiles/BehaviorTreeAI/Actions/TargetActionNode.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Server.Spells; +using Server.Targeting; + +namespace Server.Mobiles.BehaviorTreeAI +{ + public class TargetActionNode : ActionNode + { + public TargetActionNode(BehaviorTree tree) : base(tree) + { + } + + public override Result Execute() + { + Target target = Tree.Mobile.Target; + + if (target == null) + { + Tree.Mobile.DebugSay("Waiting for target..."); + return Result.Running; + } + + Mobile mobTarget = Tree.Mobile.Combatant; + + if ((target.Flags & TargetFlags.Harmful) != 0 && mobTarget != null) + { + if (mobTarget.Deleted || !mobTarget.Alive) + { + Tree.Mobile.DebugSay("Canceling my target because my target is dead or does not exist"); + target.Cancel(Tree.Mobile, TargetCancelType.Canceled); + return Result.Success; + } + + if ((target.Range == -1 || + Tree.Mobile.InRange(mobTarget, target.Range)) && + Tree.Mobile.CanSee(mobTarget) && + Tree.Mobile.InLOS(mobTarget) + ) + { + Tree.Mobile.DebugSay("Targeting my combatant"); + target.Invoke(Tree.Mobile, mobTarget); + return Result.Success; + } + } + else if ((target.Flags & TargetFlags.Beneficial) != 0) + { + Tree.Mobile.DebugSay("Targeting myself"); + target.Invoke(Tree.Mobile, Tree.Mobile); + return Result.Success; + } + + return Result.Failure; + } + } +} diff --git a/Projects/UOContent/Mobiles/BehaviorTreeAI/AlwaysSucceedNode.cs b/Projects/UOContent/Mobiles/BehaviorTreeAI/AlwaysSucceedNode.cs new file mode 100644 index 000000000..6228e081c --- /dev/null +++ b/Projects/UOContent/Mobiles/BehaviorTreeAI/AlwaysSucceedNode.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Server.Mobiles.BehaviorTreeAI +{ + public class AlwaysSucceedNode : DecoratorNode + { + public AlwaysSucceedNode(BehaviorTree tree, BehaviorTreeNode child) : base(tree, child) + { + } + public AlwaysSucceedNode(BehaviorTree tree) : base(tree) + { + } + + public override Result Execute() + { + if (Child != null) + { + Child.Execute(); + } + return Result.Success; + } + } +} diff --git a/Projects/UOContent/Mobiles/BehaviorTreeAI/BehaviorTree.cs b/Projects/UOContent/Mobiles/BehaviorTreeAI/BehaviorTree.cs new file mode 100644 index 000000000..20e94994b --- /dev/null +++ b/Projects/UOContent/Mobiles/BehaviorTreeAI/BehaviorTree.cs @@ -0,0 +1,207 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Server.Mobiles.BehaviorTreeAI +{ + public class BehaviorTree + { + /* + public static BehaviorTree Instance; + private static BehaviorTreeTimer timer; + public static void Initialize() + { + Instance = new BehaviorTree(); + + var seq = new SequenceNode(Instance, new BehaviorTreeNode[3]{ + new ActionNode(Instance, () => { Console.WriteLine("ActionNode1"); return BehaviorTreeNode.Result.Success; }), + new ActionNode(Instance, () => { Console.WriteLine("ActionNode2"); return BehaviorTreeNode.Result.Success; }), + new SequenceNode(Instance, new BehaviorTreeNode[4]{ + new ActionNode(Instance, () => { Console.WriteLine("ActionNode3"); return BehaviorTreeNode.Result.Success; }), + new ActionNode(Instance, () => { Console.WriteLine("ActionNode4"); return BehaviorTreeNode.Result.Success; }), + new ActionNode(Instance, () => { Console.WriteLine("ActionNode5"); return BehaviorTreeNode.Result.Success; }), + new SelectorNode(Instance, new BehaviorTreeNode[3] + { + new ActionNode(Instance, () => { Console.WriteLine("ActionNode6"); return BehaviorTreeNode.Result.Failure; }), + new ActionNode(Instance, () => { Console.WriteLine("ActionNode7"); return BehaviorTreeNode.Result.Success; }), + new ActionNode(Instance, () => { Console.WriteLine("ActionNode8"); return BehaviorTreeNode.Result.Success; }) + }) + }), + }); + + Instance.AddRoot( + new RepeaterNode(Instance, seq) + ); + + Instance.Start(); + } + */ + + private BehaviorTreeNode root; + public Dictionary Blackboard { get; set; } + public BehaviorTreeNode Root { get { return root; } } + + private BehaviorTreeTimer treeTimer; + + public BaseCreature Mobile { get; private set; } + + public BehaviorTree(BaseCreature mob) + { + Blackboard = new Dictionary(); + treeTimer = new BehaviorTreeTimer(this); + Mobile = mob; + } + + public void AddRoot(BehaviorTreeNode rootNode) + { + if (root == null) + { + root = rootNode; + } + } + + public void Start() + { + treeTimer.Start(); + } + + public void Stop() + { + treeTimer.Stop(); + } + + public virtual void Tick() + { + RunBehavior(); + } + + public virtual void RunBehavior() + { + Root.Execute(); + } + + private class BehaviorTreeTimer : Timer + { + private BehaviorTree behaviorTree; + public BehaviorTreeTimer(BehaviorTree tree) : base(TimeSpan.Zero, TimeSpan.FromSeconds(1.0)) + { + behaviorTree = tree; + } + + protected override void OnTick() + { + if (behaviorTree != null) + { + behaviorTree.Tick(); + } + } + } + } + /* + public enum ControlFlowNodeType + { + Sequence, + Fallback, + Parallel, + Decorator + } + + public enum ExecutionNodeType + { + Action, + Condition + } + + public class BehaviorTreeNode + { + private readonly List _children = new List(); + public bool Root { get { return Parent == null; } } + + public BehaviorTreeNode() { } + public BehaviorTreeNode(BehaviorTreeNode parent) : this() + { + Parent = parent; + } + + public BehaviorTreeNode this[int i] + { + get { return _children[i]; } + } + + public BehaviorTreeNode Parent { get; private set; } + + public ReadOnlyCollection Children + { + get { return _children.AsReadOnly(); } + } + + public virtual BehaviorTreeNode AddChild(BehaviorTreeNode node) + { + _children.Add(node); + return node; + } + + public virtual bool Evaluate(BaseCreature creature) + { + return false; + } + + public virtual bool Traverse(BaseCreature creature) + { + foreach (var child in _children) + child.Traverse(creature); + } + } + + public class SequenceNode : BehaviorTreeNode { + public override bool Evaluate(BaseCreature creature) + { + return Children.All((BehaviorTreeNode node) => node.Evaluate(creature)); + } + + public override bool Traverse(BaseCreature creature) + { + if (Evaluate(creature)) return true; + + foreach (var child in Children) + child.Traverse(creature); + } + } + + public class FallbackNode : BehaviorTreeNode + { + public override bool Evaluate(BaseCreature creature) + { + return Children.Any((BehaviorTreeNode node) => node.Evaluate(creature)); + } + + public override bool Traverse(BaseCreature creature) + { + return Evaluate(creature); + } + } + + public class ParallelNode : BehaviorTreeNode + { + + } + + public class DecoratorNode : BehaviorTreeNode + { + + } + + public class ActionNode : BehaviorTreeNode + { + + } + + public class ConditionNode : BehaviorTreeNode + { + + } + */ +} diff --git a/Projects/UOContent/Mobiles/BehaviorTreeAI/BehaviorTreeNode.cs b/Projects/UOContent/Mobiles/BehaviorTreeAI/BehaviorTreeNode.cs new file mode 100644 index 000000000..88256a493 --- /dev/null +++ b/Projects/UOContent/Mobiles/BehaviorTreeAI/BehaviorTreeNode.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Server.Mobiles.BehaviorTreeAI +{ + public abstract class BehaviorTreeNode + { + public enum Result { Running, Failure, Success } + + public BehaviorTree Tree { get; private set; } + + public BehaviorTreeNode(BehaviorTree tree) + { + Tree = tree; + } + public abstract Result Execute(); + } +} diff --git a/Projects/UOContent/Mobiles/BehaviorTreeAI/ConditionNode.cs b/Projects/UOContent/Mobiles/BehaviorTreeAI/ConditionNode.cs new file mode 100644 index 000000000..4105fe62e --- /dev/null +++ b/Projects/UOContent/Mobiles/BehaviorTreeAI/ConditionNode.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Server.Mobiles.BehaviorTreeAI +{ + public class ConditionNode : LeafNode + { + private Predicate predicate; + public ConditionNode(BehaviorTree tree, Predicate fn) : base(tree) + { + predicate = fn; + } + public override Result Execute() + { + if (predicate(Tree.Mobile)) + { + return Result.Success; + } + return Result.Failure; + } + } +} diff --git a/Projects/UOContent/Mobiles/BehaviorTreeAI/DecoratorNode.cs b/Projects/UOContent/Mobiles/BehaviorTreeAI/DecoratorNode.cs new file mode 100644 index 000000000..124eb14c4 --- /dev/null +++ b/Projects/UOContent/Mobiles/BehaviorTreeAI/DecoratorNode.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Server.Mobiles.BehaviorTreeAI +{ + public abstract class DecoratorNode : BehaviorTreeNode + { + public BehaviorTreeNode Child { get; set; } + + public DecoratorNode(BehaviorTree tree) : this(tree, null) + { + } + public DecoratorNode(BehaviorTree tree, BehaviorTreeNode child) : base(tree) + { + Child = child; + } + } +} diff --git a/Projects/UOContent/Mobiles/BehaviorTreeAI/InverterNode.cs b/Projects/UOContent/Mobiles/BehaviorTreeAI/InverterNode.cs new file mode 100644 index 000000000..1862d44e2 --- /dev/null +++ b/Projects/UOContent/Mobiles/BehaviorTreeAI/InverterNode.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Server.Mobiles.BehaviorTreeAI +{ + public class InverterNode : DecoratorNode + { + public InverterNode(BehaviorTree tree) : base(tree) + { + } + + public override Result Execute() + { + if (Child != null) + { + var result = Child.Execute(); + + if (result == Result.Success) + { + return Result.Failure; + } + else if (result == Result.Failure) + { + return Result.Success; + } + + return Result.Running; + } + return Result.Success; + } + } +} diff --git a/Projects/UOContent/Mobiles/BehaviorTreeAI/LeafNode.cs b/Projects/UOContent/Mobiles/BehaviorTreeAI/LeafNode.cs new file mode 100644 index 000000000..8c84a0bf6 --- /dev/null +++ b/Projects/UOContent/Mobiles/BehaviorTreeAI/LeafNode.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Server.Mobiles.BehaviorTreeAI +{ + public abstract class LeafNode : BehaviorTreeNode + { + public LeafNode(BehaviorTree tree) : base(tree) + { + } + } +} diff --git a/Projects/UOContent/Mobiles/BehaviorTreeAI/RepeaterNode.cs b/Projects/UOContent/Mobiles/BehaviorTreeAI/RepeaterNode.cs new file mode 100644 index 000000000..f8c38aca2 --- /dev/null +++ b/Projects/UOContent/Mobiles/BehaviorTreeAI/RepeaterNode.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Server.Mobiles.BehaviorTreeAI +{ + public class RepeaterNode : DecoratorNode + { + public RepeaterNode(BehaviorTree tree) : base(tree) + { + } + + public RepeaterNode(BehaviorTree tree, BehaviorTreeNode child) : base(tree, child) + { + } + + public override Result Execute() + { + if (Child != null) + { + Child.Execute(); + } + return Result.Running; + } + } +} diff --git a/Projects/UOContent/Mobiles/BehaviorTreeAI/SelectorNode.cs b/Projects/UOContent/Mobiles/BehaviorTreeAI/SelectorNode.cs new file mode 100644 index 000000000..ce33070b5 --- /dev/null +++ b/Projects/UOContent/Mobiles/BehaviorTreeAI/SelectorNode.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Server.Mobiles.BehaviorTreeAI +{ + class SelectorNode : BehaviorTreeNode + { + int currentTask = 0; + public List Children { get; protected set; } + public SelectorNode(BehaviorTree tree) : this(tree, new BehaviorTreeNode[0]) { } + public SelectorNode(BehaviorTree tree, BehaviorTreeNode[] children) : base(tree) + { + Children = new List(children); + } + + public override Result Execute() + { + if (currentTask < Children.Count) + { + var result = Children[currentTask].Execute(); + + if (result == Result.Success) + { + currentTask = 0; + return Result.Success; + } + else if (result == Result.Running) + { + return Result.Running; + } + + currentTask++; + if (currentTask == Children.Count) + { + currentTask = 0; + return Result.Failure; + } + + return Result.Running; + } + + return Result.Success; + } + } +} diff --git a/Projects/UOContent/Mobiles/BehaviorTreeAI/SequenceNode.cs b/Projects/UOContent/Mobiles/BehaviorTreeAI/SequenceNode.cs new file mode 100644 index 000000000..3ed5a4960 --- /dev/null +++ b/Projects/UOContent/Mobiles/BehaviorTreeAI/SequenceNode.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Server.Mobiles.BehaviorTreeAI +{ + public class SequenceNode : BehaviorTreeNode + { + int currentTask = 0; + public List Children { get; protected set; } + public SequenceNode(BehaviorTree tree) : this(tree, new BehaviorTreeNode[0]) { } + public SequenceNode(BehaviorTree tree, BehaviorTreeNode[] children) : base(tree) + { + Children = new List(children); + } + + public override Result Execute() + { + if(currentTask < Children.Count) + { + var result = Children[currentTask].Execute(); + + if (result == Result.Running) + { + return Result.Running; + } + else if (result == Result.Failure) + { + currentTask = 0; + return Result.Failure; + } + + currentTask++; + if(currentTask == Children.Count) + { + currentTask = 0; + return Result.Success; + } + + return Result.Running; + } + + return Result.Success; + } + } +} diff --git a/Projects/UOContent/Mobiles/BehaviorTreeAI/TapNode.cs b/Projects/UOContent/Mobiles/BehaviorTreeAI/TapNode.cs new file mode 100644 index 000000000..fda030d82 --- /dev/null +++ b/Projects/UOContent/Mobiles/BehaviorTreeAI/TapNode.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Server.Mobiles.BehaviorTreeAI +{ + public class TapNode : DecoratorNode + { + public Action Action { get; private set; } + public TapNode(BehaviorTree tree, Action action, BehaviorTreeNode child) : base(tree, child) + { + } + public TapNode(BehaviorTree tree, Action action) : base(tree) + { + Action = action; + } + public override Result Execute() + { + Action(Tree.Mobile); + + if (Child != null) + { + return Child.Execute(); + } + + return Result.Failure; + } + } +} diff --git a/Projects/UOContent/Mobiles/BehaviorTreeAI/UntilFailNode.cs b/Projects/UOContent/Mobiles/BehaviorTreeAI/UntilFailNode.cs new file mode 100644 index 000000000..6011f2c50 --- /dev/null +++ b/Projects/UOContent/Mobiles/BehaviorTreeAI/UntilFailNode.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Server.Mobiles.BehaviorTreeAI +{ + public class UntilFailNode : DecoratorNode + { + public UntilFailNode(BehaviorTree tree) : base(tree) + { + } + + public override Result Execute() + { + if (Child != null) + { + var result = Child.Execute(); + + if (result == Result.Failure) + { + return Result.Failure; + } + } + return Result.Running; + } + } +} diff --git a/Projects/UOContent/Mobiles/Healers/EvilHealer.cs b/Projects/UOContent/Mobiles/Healers/EvilHealer.cs index 0fd51e5e8..e63990770 100644 --- a/Projects/UOContent/Mobiles/Healers/EvilHealer.cs +++ b/Projects/UOContent/Mobiles/Healers/EvilHealer.cs @@ -5,6 +5,10 @@ namespace Server.Mobiles [Constructible] public EvilHealer() { + AI = AIType.AI_BehaviorTree; + + ChangeAIType(AI); + Title = "the healer"; Karma = -10000; @@ -63,6 +67,8 @@ namespace Server.Mobiles { base.Deserialize(reader); + AI = AIType.AI_BehaviorTree; + var version = reader.ReadInt(); } }