From 53f51d6bde019122a1567258d512623ffc369436 Mon Sep 17 00:00:00 2001 From: Leath Cooper Date: Wed, 1 Sep 2021 01:04:48 -0400 Subject: [PATCH 1/5] 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(); } } From 0e84739f2d4ccf1d7f889b3abda1b187526d5519 Mon Sep 17 00:00:00 2001 From: Leath Cooper Date: Fri, 10 Sep 2021 20:58:02 -0400 Subject: [PATCH 2/5] tree stuff --- .../UOContent/Engines/Pathing/MoveResult.cs | 1 - Projects/UOContent/Mobiles/AI/BaseAI.cs | 19 +- .../UOContent/Mobiles/AI/BehaviorTreeAI.cs | 29 +- Projects/UOContent/Mobiles/AI/FireBossAI.cs | 37 ++ .../Mobiles/BT/Actions/CastSpellActionNode.cs | 61 +++ .../Mobiles/BT/Actions/CureSelfNode.cs | 24 ++ .../Mobiles/BT/Actions/EquipItemNode.cs | 28 ++ .../Mobiles/BT/Actions/HealSelfNode.cs | 36 ++ .../Mobiles/BT/Actions/KeepRangeNode.cs | 29 ++ .../Mobiles/BT/Actions/MageComboNode.cs | 140 ++++++ .../Mobiles/BT/Actions/MageDebuffNode.cs | 40 ++ .../Mobiles/BT/Actions/MeditateNode.cs | 20 + .../Mobiles/BT/Actions/PoisonEnemyNode.cs | 34 ++ .../Mobiles/BT/Actions/SetBlackboardData.cs | 33 ++ .../Mobiles/BT/Actions/TargetActionNode.cs | 53 +++ .../Mobiles/BT/Actions/UseSkillNode.cs | 26 ++ .../Mobiles/BT/Actions/WanderNode.cs | 21 + .../Mobiles/BT/BehaviorMageCombatNodeSet.cs | 32 ++ .../Mobiles/BT/BehaviorMagePassiveNodeSet.cs | 25 ++ .../BT/Composite/RandomSelectorNode.cs | 60 +++ .../Mobiles/BT/Composite/SelectorNode.cs | 59 +++ .../Mobiles/BT/Composite/SequenceNode.cs | 59 +++ .../UOContent/Mobiles/BT/Core/BehaviorTree.cs | 400 ++++++++++++++++++ .../Mobiles/BT/Core/BehaviorTreeNode.cs | 27 ++ .../UOContent/Mobiles/BT/Core/Blackboard.cs | 12 + .../Mobiles/BT/Core/CompositeNode.cs | 25 ++ .../Mobiles/BT/Core/DecoratorNode.cs | 28 ++ .../{BehaviorTreeAI => BT/Core}/LeafNode.cs | 2 +- .../Mobiles/BT/Decorator/CheckSkillNode.cs | 32 ++ .../BT/Decorator/ConditionalLoopNode.cs | 36 ++ .../Mobiles/BT/Decorator/CooldownNode.cs | 51 +++ .../Mobiles/BT/Decorator/ForceSuccessNode.cs | 31 ++ .../Mobiles/BT/Decorator/InverterNode.cs | 36 ++ .../Mobiles/BT/Decorator/LoopNode.cs | 42 ++ .../BT/Decorator/OwnerConditionNode.cs | 34 ++ .../BT/Decorator/RandomExecutionNode.cs | 45 ++ .../UOContent/Mobiles/BT/Decorator/TapNode.cs | 29 ++ .../Decorator}/UntilFailNode.cs | 14 +- .../{BehaviorTreeAI => BT/Leaf}/ActionNode.cs | 2 +- .../Mobiles/BT/Leaf/ConditionNode.cs | 26 ++ .../UOContent/Mobiles/BT/MageBehaviorTree.cs | 35 ++ Projects/UOContent/Mobiles/BaseCreature.cs | 1 + Projects/UOContent/Mobiles/BehaviorMage.cs | 91 ++++ .../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/RepeaterNode.cs | 28 -- .../Mobiles/BehaviorTreeAI/SelectorNode.cs | 48 --- .../Mobiles/BehaviorTreeAI/SequenceNode.cs | 48 --- .../Mobiles/BehaviorTreeAI/TapNode.cs | 31 -- Projects/UOContent/Mobiles/FireBoss.cs | 44 ++ .../UOContent/Mobiles/Healers/BaseHealer.cs | 2 +- .../UOContent/Mobiles/Healers/EvilHealer.cs | 4 - .../Mobiles/Vendors/FireBossPriest.cs | 48 +++ Projects/UOContent/Spells/Base/Spell.cs | 6 +- 65 files changed, 1916 insertions(+), 1182 deletions(-) create mode 100644 Projects/UOContent/Mobiles/AI/FireBossAI.cs create mode 100644 Projects/UOContent/Mobiles/BT/Actions/CastSpellActionNode.cs create mode 100644 Projects/UOContent/Mobiles/BT/Actions/CureSelfNode.cs create mode 100644 Projects/UOContent/Mobiles/BT/Actions/EquipItemNode.cs create mode 100644 Projects/UOContent/Mobiles/BT/Actions/HealSelfNode.cs create mode 100644 Projects/UOContent/Mobiles/BT/Actions/KeepRangeNode.cs create mode 100644 Projects/UOContent/Mobiles/BT/Actions/MageComboNode.cs create mode 100644 Projects/UOContent/Mobiles/BT/Actions/MageDebuffNode.cs create mode 100644 Projects/UOContent/Mobiles/BT/Actions/MeditateNode.cs create mode 100644 Projects/UOContent/Mobiles/BT/Actions/PoisonEnemyNode.cs create mode 100644 Projects/UOContent/Mobiles/BT/Actions/SetBlackboardData.cs create mode 100644 Projects/UOContent/Mobiles/BT/Actions/TargetActionNode.cs create mode 100644 Projects/UOContent/Mobiles/BT/Actions/UseSkillNode.cs create mode 100644 Projects/UOContent/Mobiles/BT/Actions/WanderNode.cs create mode 100644 Projects/UOContent/Mobiles/BT/BehaviorMageCombatNodeSet.cs create mode 100644 Projects/UOContent/Mobiles/BT/BehaviorMagePassiveNodeSet.cs create mode 100644 Projects/UOContent/Mobiles/BT/Composite/RandomSelectorNode.cs create mode 100644 Projects/UOContent/Mobiles/BT/Composite/SelectorNode.cs create mode 100644 Projects/UOContent/Mobiles/BT/Composite/SequenceNode.cs create mode 100644 Projects/UOContent/Mobiles/BT/Core/BehaviorTree.cs create mode 100644 Projects/UOContent/Mobiles/BT/Core/BehaviorTreeNode.cs create mode 100644 Projects/UOContent/Mobiles/BT/Core/Blackboard.cs create mode 100644 Projects/UOContent/Mobiles/BT/Core/CompositeNode.cs create mode 100644 Projects/UOContent/Mobiles/BT/Core/DecoratorNode.cs rename Projects/UOContent/Mobiles/{BehaviorTreeAI => BT/Core}/LeafNode.cs (86%) create mode 100644 Projects/UOContent/Mobiles/BT/Decorator/CheckSkillNode.cs create mode 100644 Projects/UOContent/Mobiles/BT/Decorator/ConditionalLoopNode.cs create mode 100644 Projects/UOContent/Mobiles/BT/Decorator/CooldownNode.cs create mode 100644 Projects/UOContent/Mobiles/BT/Decorator/ForceSuccessNode.cs create mode 100644 Projects/UOContent/Mobiles/BT/Decorator/InverterNode.cs create mode 100644 Projects/UOContent/Mobiles/BT/Decorator/LoopNode.cs create mode 100644 Projects/UOContent/Mobiles/BT/Decorator/OwnerConditionNode.cs create mode 100644 Projects/UOContent/Mobiles/BT/Decorator/RandomExecutionNode.cs create mode 100644 Projects/UOContent/Mobiles/BT/Decorator/TapNode.cs rename Projects/UOContent/Mobiles/{BehaviorTreeAI => BT/Decorator}/UntilFailNode.cs (53%) rename Projects/UOContent/Mobiles/{BehaviorTreeAI => BT/Leaf}/ActionNode.cs (86%) create mode 100644 Projects/UOContent/Mobiles/BT/Leaf/ConditionNode.cs create mode 100644 Projects/UOContent/Mobiles/BT/MageBehaviorTree.cs create mode 100644 Projects/UOContent/Mobiles/BehaviorMage.cs delete mode 100644 Projects/UOContent/Mobiles/BehaviorTreeAI/Actions/CastSpellActionNode.cs delete mode 100644 Projects/UOContent/Mobiles/BehaviorTreeAI/Actions/CureSelfNode.cs delete mode 100644 Projects/UOContent/Mobiles/BehaviorTreeAI/Actions/DetectHiddenActionNode.cs delete mode 100644 Projects/UOContent/Mobiles/BehaviorTreeAI/Actions/HealSelfActionNode.cs delete mode 100644 Projects/UOContent/Mobiles/BehaviorTreeAI/Actions/MageComboNode.cs delete mode 100644 Projects/UOContent/Mobiles/BehaviorTreeAI/Actions/SelfHealerNode.cs delete mode 100644 Projects/UOContent/Mobiles/BehaviorTreeAI/Actions/TargetActionNode.cs delete mode 100644 Projects/UOContent/Mobiles/BehaviorTreeAI/AlwaysSucceedNode.cs delete mode 100644 Projects/UOContent/Mobiles/BehaviorTreeAI/BehaviorTree.cs delete mode 100644 Projects/UOContent/Mobiles/BehaviorTreeAI/BehaviorTreeNode.cs delete mode 100644 Projects/UOContent/Mobiles/BehaviorTreeAI/ConditionNode.cs delete mode 100644 Projects/UOContent/Mobiles/BehaviorTreeAI/DecoratorNode.cs delete mode 100644 Projects/UOContent/Mobiles/BehaviorTreeAI/InverterNode.cs delete mode 100644 Projects/UOContent/Mobiles/BehaviorTreeAI/RepeaterNode.cs delete mode 100644 Projects/UOContent/Mobiles/BehaviorTreeAI/SelectorNode.cs delete mode 100644 Projects/UOContent/Mobiles/BehaviorTreeAI/SequenceNode.cs delete mode 100644 Projects/UOContent/Mobiles/BehaviorTreeAI/TapNode.cs create mode 100644 Projects/UOContent/Mobiles/FireBoss.cs create mode 100644 Projects/UOContent/Mobiles/Vendors/FireBossPriest.cs diff --git a/Projects/UOContent/Engines/Pathing/MoveResult.cs b/Projects/UOContent/Engines/Pathing/MoveResult.cs index 643f4f19b..e9cf96963 100644 --- a/Projects/UOContent/Engines/Pathing/MoveResult.cs +++ b/Projects/UOContent/Engines/Pathing/MoveResult.cs @@ -1,7 +1,6 @@ namespace Server { public delegate MoveResult MoveMethod(Direction d); - public enum MoveResult { BadState, diff --git a/Projects/UOContent/Mobiles/AI/BaseAI.cs b/Projects/UOContent/Mobiles/AI/BaseAI.cs index 0ad9e0543..2fd898801 100644 --- a/Projects/UOContent/Mobiles/AI/BaseAI.cs +++ b/Projects/UOContent/Mobiles/AI/BaseAI.cs @@ -13,7 +13,6 @@ using Server.Spells; using Server.Spells.Spellweaving; using Server.Targets; using MoveImpl = Server.Movement.MovementImpl; -using Server.Mobiles.BehaviorTreeAI; namespace Server.Mobiles { @@ -29,7 +28,8 @@ namespace Server.Mobiles AI_Berserk, AI_Predator, AI_Thief, - AI_BehaviorTree + AI_BehaviorTree, + AI_FireBoss } public enum ActionType @@ -138,21 +138,6 @@ 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 diff --git a/Projects/UOContent/Mobiles/AI/BehaviorTreeAI.cs b/Projects/UOContent/Mobiles/AI/BehaviorTreeAI.cs index f9a281bde..0606b97bb 100644 --- a/Projects/UOContent/Mobiles/AI/BehaviorTreeAI.cs +++ b/Projects/UOContent/Mobiles/AI/BehaviorTreeAI.cs @@ -3,31 +3,17 @@ using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; -using Server.Mobiles.BehaviorTreeAI; +using Server.Mobiles.BT; using Server.Targeting; namespace Server.Mobiles.AI { public class BehaviorTreeAI : BaseAI { - private BehaviorTree behaviorTree; + private Blackboard blackboard; 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(); + blackboard = new Blackboard(); } public override bool Think() @@ -37,9 +23,14 @@ namespace Server.Mobiles.AI return false; } - // behaviorTree.RunBehavior(); + MageBehaviorTree.Instance.RunBehavior(m_Mobile, blackboard); - return base.Think(); + return true; + } + + public override bool DoActionWander() + { + return true; } } } diff --git a/Projects/UOContent/Mobiles/AI/FireBossAI.cs b/Projects/UOContent/Mobiles/AI/FireBossAI.cs new file mode 100644 index 000000000..7a7e9a187 --- /dev/null +++ b/Projects/UOContent/Mobiles/AI/FireBossAI.cs @@ -0,0 +1,37 @@ +using Server.Mobiles.BT; + +namespace Server.Mobiles.AI +{ + public class FireBossAI : BaseAI + { + private Blackboard blackboard; + public BehaviorTree Tree { get; private set; } + public FireBossAI(BaseCreature m) : base(m) + { + blackboard = new Blackboard(); + m.PassiveSpeed = 0.05; + m.ActiveSpeed = 0.05; + m.CurrentSpeed = 0.05; + + Tree = new BehaviorTree(); + Tree.TryAddRoot( + new SelectorNode(Tree) + .AddChild( + new SequenceNode(Tree) + .AddChild(new ConditionNode(Tree, (mob, board) => mob.Combatant != null)) + ) + ); + } + public override bool Think() + { + if (m_Mobile.Deleted || !m_Mobile.Alive) + { + return false; + } + + Tree.RunBehavior(m_Mobile, blackboard); + + return base.Think(); + } + } +} diff --git a/Projects/UOContent/Mobiles/BT/Actions/CastSpellActionNode.cs b/Projects/UOContent/Mobiles/BT/Actions/CastSpellActionNode.cs new file mode 100644 index 000000000..232331ccc --- /dev/null +++ b/Projects/UOContent/Mobiles/BT/Actions/CastSpellActionNode.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Server.Spells; + +namespace Server.Mobiles.BT +{ + public class CastSpellActionNode : ActionNode + { + public delegate Spell GetSpellCallback(BaseCreature mob); + private GetSpellCallback getSpellCallback; + public CastSpellActionNode(BehaviorTree tree, GetSpellCallback callback) : base(tree) + { + getSpellCallback = callback; + } + + public override Result Execute(BaseCreature mob, Blackboard blackboard) + { + if (mob.Spell == null) + { + Spell spell = getSpellCallback(mob); + + if (mob.Mana < spell.ScaleMana(spell.GetMana())) + { + mob.DebugSay("Not enough mana..."); + return Result.Failure; + } + + if (Core.TickCount - mob.NextSpellTime < 0) + { + mob.DebugSay("You have not recovered from casting a spell..."); + return Result.Running; + } + + mob.DebugSay("Casting..."); + if (!spell.Cast()) + { + mob.DebugSay("Failed to cast..."); + return Result.Running; + } + + if (!string.IsNullOrEmpty(spell.Mantra)) + mob.PublicOverheadMessage(Network.MessageType.Spell, 0, false, spell.Mantra, false); + + return Result.Running; + } + else if (mob.Spell.IsCasting) + { + mob.DebugSay("Still casting..."); + return Result.Running; + } + else if (!mob.Spell.IsCasting) + { + mob.DebugSay("Finished casting..."); + return Result.Success; + } + + return Result.Failure; + } + } +} diff --git a/Projects/UOContent/Mobiles/BT/Actions/CureSelfNode.cs b/Projects/UOContent/Mobiles/BT/Actions/CureSelfNode.cs new file mode 100644 index 000000000..0d40b09f9 --- /dev/null +++ b/Projects/UOContent/Mobiles/BT/Actions/CureSelfNode.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Server.Spells.Second; + +namespace Server.Mobiles.BT +{ + public class CureSelfNode : OwnerConditionNode + { + public CureSelfNode(BehaviorTree tree, TimeSpan duration) : base(tree, (mob, board) => mob.Poison != null) + { + AddChild( + new CooldownNode(tree, duration) + .AddChild( + new SequenceNode(tree) + .AddChild(new CastSpellActionNode(tree, (mob) => new CureSpell(mob))) + .AddChild(new TargetActionNode(tree)) + ) + ); + } + } +} diff --git a/Projects/UOContent/Mobiles/BT/Actions/EquipItemNode.cs b/Projects/UOContent/Mobiles/BT/Actions/EquipItemNode.cs new file mode 100644 index 000000000..dcc631891 --- /dev/null +++ b/Projects/UOContent/Mobiles/BT/Actions/EquipItemNode.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Server.Mobiles.BT +{ + public class EquipItemNode : ActionNode + { + private Type itemType; + public EquipItemNode(BehaviorTree tree, Type type) : base(tree) + { + itemType = type; + } + public override Result Execute(BaseCreature mob, Blackboard blackboard) + { + Item item = mob.Backpack.FindItemByType(itemType); + + if (item == null || !mob.EquipItem(item)) + { + return Result.Failure; + } + + return Result.Success; + } + } +} diff --git a/Projects/UOContent/Mobiles/BT/Actions/HealSelfNode.cs b/Projects/UOContent/Mobiles/BT/Actions/HealSelfNode.cs new file mode 100644 index 000000000..2c9775671 --- /dev/null +++ b/Projects/UOContent/Mobiles/BT/Actions/HealSelfNode.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Server.Spells.Fourth; +using Server.Spells.First; +using Server.Spells; + +namespace Server.Mobiles.BT +{ + public class HealSelfNode : OwnerConditionNode + { + public HealSelfNode(BehaviorTree tree, TimeSpan duration) : base(tree, (mob, board) => mob.HitsMax - mob.Hits > 0) + { + AddChild( + new CooldownNode(tree, duration) + .AddChild( + new SequenceNode(tree) + .AddChild(new CastSpellActionNode(tree, GetHealSpell)) + .AddChild(new TargetActionNode(tree)) + ) + ); + } + + private Spell GetHealSpell(BaseCreature mob) + { + if (mob.Mana < 10 || mob.HitsMax - mob.Hits < 15 || Utility.RandomDouble() < 0.3) + { + return new HealSpell(mob); + } + + return new GreaterHealSpell(mob); + } + } +} diff --git a/Projects/UOContent/Mobiles/BT/Actions/KeepRangeNode.cs b/Projects/UOContent/Mobiles/BT/Actions/KeepRangeNode.cs new file mode 100644 index 000000000..d6026c79e --- /dev/null +++ b/Projects/UOContent/Mobiles/BT/Actions/KeepRangeNode.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Server.Mobiles.BT +{ + public class KeepRangeNode : ActionNode + { + private int minRange; + private int maxRange; + public KeepRangeNode(BehaviorTree tree, int min, int max) : base(tree) + { + minRange = min; + maxRange = max; + } + public override Result Execute(BaseCreature mob, Blackboard blackboard) + { + if (mob.Combatant != null && !mob.Combatant.Deleted) + { + BehaviorTree.WalkMobileRange(mob, (BaseCreature)mob.Combatant, 4, true, minRange, maxRange); + return Result.Success; + } + + return Result.Failure; + } + } +} diff --git a/Projects/UOContent/Mobiles/BT/Actions/MageComboNode.cs b/Projects/UOContent/Mobiles/BT/Actions/MageComboNode.cs new file mode 100644 index 000000000..b6ff1ec06 --- /dev/null +++ b/Projects/UOContent/Mobiles/BT/Actions/MageComboNode.cs @@ -0,0 +1,140 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Server.Spells.Sixth; +using Server.Spells.First; +using Server.Spells.Second; +using Server.Spells.Fourth; +using Server.Spells.Fifth; +using Server.Spells; +using Server.Spells.Third; + +namespace Server.Mobiles.BT +{ + public class MageComboNode : OwnerConditionNode + { + public MageComboNode(BehaviorTree tree) + : base(tree, canActivate) + { + AddChild( + new SequenceNode(tree) + .AddChild(new ForceSuccessNode(tree, new RandomExecutionNode(tree, 0.5, new PoisonEnemyNode(tree, TimeSpan.FromSeconds(6.0))))) + .AddChild(new ConditionNode(tree, shouldContinueCombo)) + .AddChild( + new ForceSuccessNode(tree) + .AddChild( + new RandomExecutionNode(tree, 0.5) + .AddChild(new SequenceNode(tree) + .AddChild(new CastSpellActionNode(tree, getInterruptSpell)) + .AddChild(new TargetActionNode(tree)) + ) + ) + ) + .AddChild(new ForceSuccessNode(tree, new CureSelfNode(tree, TimeSpan.FromSeconds(5.0)))) + .AddChild(new ConditionNode(tree, shouldContinueCombo)) + .AddChild( + // new ForceSuccessNode(tree) + // .AddChild( + new SequenceNode(tree) + .AddChild(new CastSpellActionNode(tree, getExplosionSpell)) + .AddChild(new TargetActionNode(tree)) + .AddChild(new ConditionNode(tree, shouldContinueCombo)) + .AddChild(new CastSpellActionNode(tree, getEnergyBoltSpell)) + .AddChild(new TargetActionNode(tree)) + // ) + ) + ); + } + private static bool canActivate(BaseCreature mob, Blackboard board) + { + return mob.Combatant != null && !mob.Combatant.Deleted && mob.Mana > 70; + } + private static bool shouldAttemptInterrupt(BaseCreature mob, Blackboard board) + { + if (mob.Combatant == null || mob.Combatant.Deleted) + { + return false; + } + + if (mob.Combatant.Spell != null && mob.Combatant.Spell.IsCasting) + { + switch(mob.Combatant.Spell) + { + case GreaterHealSpell: + case ExplosionSpell: + case EnergyBoltSpell: + case CureSpell: + return true; + default: + return false; + } + } + + if (mob.Combatant.Meditating) + return true; + + return false; + } + private static bool shouldContinueCombo(BaseCreature mob, Blackboard board) + { + if (mob.Combatant == null || mob.Combatant.Deleted) + return false; + + if (mob.Combatant.HitsMax - mob.Combatant.Hits >= 70) + { + return true; + } + + if (mob.HitsMax - mob.Hits <= 50) + { + return true; + } + + if (mob.Mana > 10) + { + return true; + } + + if (mob.Combatant.Spell != null && mob.Combatant.Poison != null) + { + return true; + } + + return false; + } + private static Spell getLightningSpell(BaseCreature mob) + { + return new LightningSpell(mob); + } + + private static Spell getExplosionSpell(BaseCreature mob) + { + return new ExplosionSpell(mob); + } + + private static Spell getEnergyBoltSpell(BaseCreature mob) + { + return new EnergyBoltSpell(mob); + } + + private static Spell getInterruptSpell(BaseCreature mob) + { + var chance = Utility.RandomDouble(); + if (chance < 0.1) + { + new FireballSpell(mob); + } + else if (chance < 0.3) + { + return new LightningSpell(mob); + } + if (chance < 0.5) + { + return new MagicArrowSpell(mob); + } + return new HarmSpell(mob); + } + } +} diff --git a/Projects/UOContent/Mobiles/BT/Actions/MageDebuffNode.cs b/Projects/UOContent/Mobiles/BT/Actions/MageDebuffNode.cs new file mode 100644 index 000000000..b98277ac6 --- /dev/null +++ b/Projects/UOContent/Mobiles/BT/Actions/MageDebuffNode.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Server.Spells.First; + +namespace Server.Mobiles.BT +{ + public class MageDebuffNode : SelectorNode + { + public MageDebuffNode(BehaviorTree tree) : base(tree) + { + AddChild( + new OwnerConditionNode(tree, (mob, board) => mob.Combatant != null && mob.Combatant.Str == mob.Combatant.RawStr) + .AddChild( + new SequenceNode(tree) + .AddChild(new CastSpellActionNode(tree, (mob) => new WeakenSpell(mob))) + .AddChild(new TargetActionNode(tree)) + ) + ); + AddChild( + new OwnerConditionNode(tree, (mob, board) => mob.Combatant != null && mob.Combatant.Dex == mob.Combatant.RawDex) + .AddChild( + new SequenceNode(tree) + .AddChild(new CastSpellActionNode(tree, (mob) => new ClumsySpell(mob))) + .AddChild(new TargetActionNode(tree)) + ) + ); + AddChild( + new OwnerConditionNode(tree, (mob, board) => mob.Combatant != null && mob.Combatant.Int == mob.Combatant.RawInt) + .AddChild( + new SequenceNode(tree) + .AddChild(new CastSpellActionNode(tree, (mob) => new FeeblemindSpell(mob))) + .AddChild(new TargetActionNode(tree)) + ) + ); + } + } +} diff --git a/Projects/UOContent/Mobiles/BT/Actions/MeditateNode.cs b/Projects/UOContent/Mobiles/BT/Actions/MeditateNode.cs new file mode 100644 index 000000000..c82a16ad4 --- /dev/null +++ b/Projects/UOContent/Mobiles/BT/Actions/MeditateNode.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Server.Mobiles.BT +{ + public class MeditateNode : CooldownNode + { + public MeditateNode(BehaviorTree tree, int desiredMana) : base(tree, TimeSpan.FromSeconds(12.0)) + { + AddChild( + new SequenceNode(tree) + .AddChild(new UseSkillNode(tree, SkillName.Meditation)) + .AddChild(new InverterNode(tree, new UntilFailNode(tree, new ConditionNode(tree, (mob, board) => mob.Mana < desiredMana || mob.Meditating)))) + ); + } + } +} diff --git a/Projects/UOContent/Mobiles/BT/Actions/PoisonEnemyNode.cs b/Projects/UOContent/Mobiles/BT/Actions/PoisonEnemyNode.cs new file mode 100644 index 000000000..334b6b7e9 --- /dev/null +++ b/Projects/UOContent/Mobiles/BT/Actions/PoisonEnemyNode.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Server.Spells.Third; + +namespace Server.Mobiles.BT +{ + public class PoisonEnemyNode : OwnerConditionNode + { + public PoisonEnemyNode(BehaviorTree tree, TimeSpan duration) : base(tree, (mob, board) => mob.Combatant != null && !mob.Combatant.Deleted && mob.Combatant.Poison == null) + { + AddChild( + new CooldownNode(tree, duration) + .AddChild( + new SequenceNode(tree) + .AddChild(new CastSpellActionNode(tree, (mob) => new PoisonSpell(mob))) + .AddChild(new TargetActionNode(tree)) + ) + ); + } + + private bool canActivate(BaseCreature mob, Blackboard blackboard) + { + if (mob.Combatant == null || mob.Combatant.Deleted) + { + return false; + } + + return mob.Combatant.Poison == null; + } + } +} diff --git a/Projects/UOContent/Mobiles/BT/Actions/SetBlackboardData.cs b/Projects/UOContent/Mobiles/BT/Actions/SetBlackboardData.cs new file mode 100644 index 000000000..e7f8a4c1e --- /dev/null +++ b/Projects/UOContent/Mobiles/BT/Actions/SetBlackboardData.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Server.Mobiles.BT +{ + public class SetBlackboardData : ActionNode + { + public delegate T BlackboardDataTransformation(BaseCreature mob); + private BlackboardDataTransformation transformation; + private string blackboardKey; + public SetBlackboardData(BehaviorTree tree, string key, BlackboardDataTransformation fn) : base(tree) + { + blackboardKey = key; + transformation = fn; + } + public override Result Execute(BaseCreature mob, Blackboard blackboard) + { + T value = transformation(mob); + if(blackboard.TryGetValue(blackboardKey, out object oldValue) && (T)oldValue != null) + { + blackboard[blackboardKey] = (object)value; + } + else + { + blackboard.Add(blackboardKey, (object)value); + } + return Result.Success; + } + } +} diff --git a/Projects/UOContent/Mobiles/BT/Actions/TargetActionNode.cs b/Projects/UOContent/Mobiles/BT/Actions/TargetActionNode.cs new file mode 100644 index 000000000..c547e562d --- /dev/null +++ b/Projects/UOContent/Mobiles/BT/Actions/TargetActionNode.cs @@ -0,0 +1,53 @@ +using Server.Targeting; + +namespace Server.Mobiles.BT +{ + public class TargetActionNode : ActionNode + { + public TargetActionNode(BehaviorTree tree) : base(tree) + { + } + + public override Result Execute(BaseCreature mob, Blackboard blackboard) + { + Target target = mob.Target; + + if (target == null) + { + mob.DebugSay("Waiting for target..."); + return Result.Running; + } + + Mobile mobTarget = mob.Combatant; + + if ((target.Flags & TargetFlags.Harmful) != 0 && mobTarget != null) + { + if (mobTarget.Deleted || !mobTarget.Alive) + { + mob.DebugSay("Canceling my target because my target is dead or does not exist"); + target.Cancel(mob, TargetCancelType.Canceled); + return Result.Success; + } + + if ((target.Range == -1 || + mob.InRange(mobTarget, target.Range)) && + mob.CanSee(mobTarget) && + mob.InLOS(mobTarget) + ) + { + mob.DebugSay("Targeting my combatant"); + target.Invoke(mob, mobTarget); + return Result.Success; + } + } + else if ((target.Flags & TargetFlags.Beneficial) != 0) + { + mob.DebugSay("Targeting myself"); + target.Invoke(mob, mob); + return Result.Success; + } + + return Result.Failure; + } + } +} diff --git a/Projects/UOContent/Mobiles/BT/Actions/UseSkillNode.cs b/Projects/UOContent/Mobiles/BT/Actions/UseSkillNode.cs new file mode 100644 index 000000000..f7f9cf1c4 --- /dev/null +++ b/Projects/UOContent/Mobiles/BT/Actions/UseSkillNode.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Server.Mobiles.BT +{ + public class UseSkillNode : ActionNode + { + private SkillName skillToUse; + public UseSkillNode(BehaviorTree tree, SkillName skill) : base(tree) + { + skillToUse = skill; + } + public override Result Execute(BaseCreature mob, Blackboard blackboard) + { + if (mob.UseSkill(skillToUse)) + { + mob.Say("{0}", skillToUse.ToString()); + return Result.Success; + } + return Result.Failure; + } + } +} diff --git a/Projects/UOContent/Mobiles/BT/Actions/WanderNode.cs b/Projects/UOContent/Mobiles/BT/Actions/WanderNode.cs new file mode 100644 index 000000000..71630de4f --- /dev/null +++ b/Projects/UOContent/Mobiles/BT/Actions/WanderNode.cs @@ -0,0 +1,21 @@ + +namespace Server.Mobiles.BT +{ + public class WanderNode : ActionNode + { + private int stepsPerWander; + public WanderNode(BehaviorTree tree, int steps) : base(tree) + { + stepsPerWander = steps; + } + public override Result Execute(BaseCreature mob, Blackboard blackboard) + { + if (!mob.CheckIdle()) + { + BehaviorTree.WalkRandomInHome(mob, stepsPerWander); + return Result.Success; + } + return Result.Failure; + } + } +} diff --git a/Projects/UOContent/Mobiles/BT/BehaviorMageCombatNodeSet.cs b/Projects/UOContent/Mobiles/BT/BehaviorMageCombatNodeSet.cs new file mode 100644 index 000000000..1f9433943 --- /dev/null +++ b/Projects/UOContent/Mobiles/BT/BehaviorMageCombatNodeSet.cs @@ -0,0 +1,32 @@ +using System; +using Server.Items; + +namespace Server.Mobiles.BT +{ + public class BehaviorMageCombatNodeSet : OwnerConditionNode + { + public BehaviorMageCombatNodeSet(BehaviorTree tree) + : base(tree, canActivate) + { + AddChild( + new SelectorNode(tree) + .AddChild(new TapNode(tree, (mob, board) => { mob.CurrentSpeed = mob.ActiveSpeed; })) + .AddChild(new CooldownNode(tree, TimeSpan.FromSeconds(0.5), new InverterNode(tree, new KeepRangeNode(tree, 1, 2)))) + .AddChild(new MageDebuffNode(tree)) + .AddChild(new MageComboNode(tree)) + .AddChild(new OwnerConditionNode(tree, (mob, board) => mob.Poison != null, new CureSelfNode(tree, TimeSpan.FromSeconds(6.0)))) + .AddChild(new OwnerConditionNode(tree, (mob, board) => mob.HitsMax - mob.Hits >= 40, new HealSelfNode(tree, TimeSpan.FromSeconds(6.0)))) + .AddChild(new OwnerConditionNode(tree, shouldMeditate, new MeditateNode(tree, 75))) + ); + } + + private static bool canActivate(BaseCreature mob, Blackboard board) + { + return mob.Combatant != null && !mob.Combatant.Deleted; + } + private static bool shouldMeditate(BaseCreature mob, Blackboard board) + { + return mob.Mana <= 30 || (mob.Meditating && mob.Mana < 75); + } + } +} diff --git a/Projects/UOContent/Mobiles/BT/BehaviorMagePassiveNodeSet.cs b/Projects/UOContent/Mobiles/BT/BehaviorMagePassiveNodeSet.cs new file mode 100644 index 000000000..53c1afbcb --- /dev/null +++ b/Projects/UOContent/Mobiles/BT/BehaviorMagePassiveNodeSet.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Server.Mobiles.BT +{ + public class BehaviorMagePassiveNodeSet : OwnerConditionNode + { + public BehaviorMagePassiveNodeSet(BehaviorTree tree) + : base(tree, canActivate) + { + AddChild( + new SelectorNode(tree) + .AddChild(new TapNode(tree, (mob, board) => { mob.CurrentSpeed = mob.PassiveSpeed; })) + .AddChild(new CooldownNode(tree, TimeSpan.FromSeconds(2.0), new WanderNode(tree, 3))) + ); + } + private static bool canActivate(BaseCreature mob, Blackboard board) + { + return mob.Combatant == null; + } + } +} diff --git a/Projects/UOContent/Mobiles/BT/Composite/RandomSelectorNode.cs b/Projects/UOContent/Mobiles/BT/Composite/RandomSelectorNode.cs new file mode 100644 index 000000000..334104d65 --- /dev/null +++ b/Projects/UOContent/Mobiles/BT/Composite/RandomSelectorNode.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Server.Mobiles.BT +{ + public class RandomSelectorNode : CompositeNode + { + private Dictionary retryCache = new Dictionary(); + public RandomSelectorNode(BehaviorTree tree) : base(tree) + { + } + public override Result Execute(BaseCreature mob, Blackboard blackboard) + { + if (!Tree.Claims.TryGetValue(mob, out BehaviorTreeNode node) || node != this) + { + Tree.Claim(mob, this); + } + + if (!currentTaskCache.TryGetValue(mob, out int currentTask)) + { + currentTask = Utility.RandomMinMax(0, Children.Count - 1); + currentTaskCache.Add(mob, currentTask); + } + + if (!retryCache.TryGetValue(mob, out int currentIteration)) + { + currentIteration = 0; + retryCache.Add(mob, currentIteration); + } + + var result = Children[currentTask].Execute(mob, blackboard); + + if (result == Result.Running) + { + return Result.Running; + } + else if (result == Result.Failure) + { + currentIteration++; + currentTaskCache[mob] = Utility.RandomMinMax(0, Children.Count - 1); + if (currentIteration >= Children.Count) + { + retryCache[mob] = 0; + Tree.Release(mob); + return Result.Failure; + } + retryCache[mob] = currentIteration; + return Result.Running; + } + + retryCache[mob] = 0; + currentTaskCache[mob] = 0; + Tree.Release(mob); + return Result.Success; + } + } +} diff --git a/Projects/UOContent/Mobiles/BT/Composite/SelectorNode.cs b/Projects/UOContent/Mobiles/BT/Composite/SelectorNode.cs new file mode 100644 index 000000000..127b504dc --- /dev/null +++ b/Projects/UOContent/Mobiles/BT/Composite/SelectorNode.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Server.Mobiles.BT +{ + public class SelectorNode : CompositeNode + { + public SelectorNode(BehaviorTree tree): base(tree) + { + } + public override Result Execute(BaseCreature mob, Blackboard blackboard) + { + if (!Tree.Claims.TryGetValue(mob, out BehaviorTreeNode node) || node != this) + { + Tree.Claim(mob, this); + } + + if (!currentTaskCache.TryGetValue(mob, out int currentTask)) + { + currentTask = 0; + currentTaskCache.Add(mob, currentTask); + } + + if (currentTask < Children.Count) + { + var result = Children[currentTask].Execute(mob, blackboard); + + if (result == Result.Success) + { + currentTaskCache[mob] = 0; + Tree.Release(mob); + return Result.Success; + } + else if (result == Result.Running) + { + return Result.Running; + } + + currentTask++; + if (currentTask == Children.Count) + { + currentTask = 0; + currentTaskCache[mob] = currentTask; + Tree.Release(mob); + return Result.Failure; + } + + currentTaskCache[mob] = currentTask; + return Result.Running; + } + + Tree.Release(mob); + return Result.Failure; + } + } +} diff --git a/Projects/UOContent/Mobiles/BT/Composite/SequenceNode.cs b/Projects/UOContent/Mobiles/BT/Composite/SequenceNode.cs new file mode 100644 index 000000000..10e14c2d8 --- /dev/null +++ b/Projects/UOContent/Mobiles/BT/Composite/SequenceNode.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Server.Mobiles.BT +{ + public class SequenceNode : CompositeNode + { + public SequenceNode(BehaviorTree tree) : base(tree) + { + } + public override Result Execute(BaseCreature mob, Blackboard blackboard) + { + if (!Tree.Claims.TryGetValue(mob, out BehaviorTreeNode node) || node != this) + { + Tree.Claim(mob, this); + } + + if (!currentTaskCache.TryGetValue(mob, out int currentTask)) + { + currentTask = 0; + currentTaskCache.Add(mob, currentTask); + } + + if (currentTask < Children.Count) + { + var result = Children[currentTask].Execute(mob, blackboard); + + if (result == Result.Running) + { + return Result.Running; + } + else if (result == Result.Failure) + { + currentTaskCache[mob] = 0; + Tree.Release(mob); + return Result.Failure; + } + + currentTask++; + if (currentTask == Children.Count) + { + currentTask = 0; + currentTaskCache[mob] = currentTask; + Tree.Release(mob); + return Result.Success; + } + + currentTaskCache[mob] = currentTask; + return Result.Running; + } + + Tree.Release(mob); + return Result.Failure; + } + } +} diff --git a/Projects/UOContent/Mobiles/BT/Core/BehaviorTree.cs b/Projects/UOContent/Mobiles/BT/Core/BehaviorTree.cs new file mode 100644 index 000000000..06bdb6b45 --- /dev/null +++ b/Projects/UOContent/Mobiles/BT/Core/BehaviorTree.cs @@ -0,0 +1,400 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Server.Items; +using Server.Engines.Spawners; +using MoveImpl = Server.Movement.MovementImpl; + +namespace Server.Mobiles.BT +{ + public class BehaviorTree + { + private BehaviorTreeNode root; + public BehaviorTreeNode Root { get { return root; } } + public Dictionary Claims = new Dictionary(); + public BehaviorTree() + { + } + public bool TryAddRoot(CompositeNode rootNode) + { + if (root == null) + { + root = rootNode; + return true; + } + return false; + } + public virtual void RunBehavior(BaseCreature mob, Blackboard blackboard) + { + if (!Claims.TryGetValue(mob, out BehaviorTreeNode currentNode)) + { + currentNode = null; + Claims.Add(mob, null); + } + + if (Root != null) + { + /* + if (currentNode != null) + { + currentNode.Execute(mob, blackboard); + } + else + { + Root.Execute(mob, blackboard); + } + */ + Root.Execute(mob, blackboard); + } + } + public virtual void Claim(BaseCreature mob, BehaviorTreeNode node) + { + if (!Claims.TryGetValue(mob, out BehaviorTreeNode foundNode)) + { + Claims.Add(mob, node); + } + else + { + Claims[mob] = node; + } + } + public virtual void Release(BaseCreature mob) + { + if (!Claims.TryGetValue(mob, out BehaviorTreeNode foundNode)) + { + Claims.Add(mob, null); + } + else + { + Claims[mob] = null; + } + } + + public static void WalkRandomInHome(BaseCreature mob, int steps) + { + if (mob.Deleted || mob.DisallowAllMoves) + { + return; + } + + if (mob.Home == Point3D.Zero) + { + if (mob.Spawner is RegionSpawner rs) + { + Region region = rs.SpawnRegion; + + if (mob.Region.AcceptsSpawnsFrom(region)) + { + mob.WalkRegion = region; + WalkRandom(mob, steps); + mob.WalkRegion = null; + } + else + { + if (region.GoLocation != Point3D.Zero && Utility.RandomDouble() > 0.5) + { + DoMove(mob, mob.GetDirectionTo(region.GoLocation)); + } + else + { + WalkRandom(mob, steps); + } + } + } + else + { + WalkRandom(mob, steps); + } + } + else + { + for (var i = 0; i < steps; i++) + { + if (mob.RangeHome != 0) + { + var currentDistance = (int)mob.GetDistanceToSqrt(mob.Home); + + if (currentDistance < mob.RangeHome * 2 / 3) + { + WalkRandom(mob, 1); + } + else if (currentDistance > mob.RangeHome) + { + DoMove(mob, mob.GetDirectionTo(mob.Home)); + } + else + { + if (Utility.RandomDouble() > 0.5) + { + DoMove(mob, mob.GetDirectionTo(mob.Home)); + } + else + { + WalkRandom(mob, 1); + } + } + } + else + { + if (mob.Location != mob.Home) + { + DoMove(mob, mob.GetDirectionTo(mob.Home)); + } + } + } + } + } + public static MoveResult WalkRandom(BaseCreature mob, int steps, bool run = false) + { + if (mob.Deleted || mob.DisallowAllMoves) + { + return MoveResult.BadState; + } + + for (var i = 0; i < steps; i++) + { + if (Utility.Random(8) <= 8) + { + var random = Utility.Random(0, 32); + + Direction direction; + + switch (random) + { + case 0: + direction = Direction.Up; + break; + case 1: + direction = Direction.North; + break; + case 2: + direction = Direction.Left; + break; + case 3: + direction = Direction.West; + break; + case 5: + direction = Direction.Down; + break; + case 6: + direction = Direction.South; + break; + case 7: + direction = Direction.Right; + break; + case 8: + direction = Direction.East; + break; + default: + direction = mob.Direction; + break; + } + + DoMove(mob, direction, run); + } + } + return MoveResult.Success; + } + + public static MoveResult DoMove(BaseCreature mob, Direction d, bool run = false) + { + if (mob.Deleted || mob.Frozen || mob.Paralyzed || mob.Spell?.IsCasting == true || mob.DisallowAllMoves) + { + return MoveResult.BadState; + } + + Direction direction = d; + + if (run) + { + direction |= Direction.Running; + } + + mob.Pushing = false; + + MoveImpl.IgnoreMovableImpassables = mob.CanMoveOverObstacles && !mob.CanDestroyObstacles; + + if ((mob.Direction & Direction.Mask) != (direction & Direction.Mask)) + { + bool moved = mob.Move(direction); + + MoveImpl.IgnoreMovableImpassables = false; + return moved ? MoveResult.Success : MoveResult.Blocked; + } + + if (mob.Move(direction)) + { + MoveImpl.IgnoreMovableImpassables = false; + return MoveResult.Success; + } + + bool wasPushing = mob.Pushing; + + bool blocked = true; + + bool canOpenDoors = mob.CanOpenDoors; + bool canDestroyObstacles = mob.CanDestroyObstacles; + + if (canOpenDoors || canDestroyObstacles) + { + Map map = mob.Map; + + if (map != null) + { + int x = mob.X, y = mob.Y; + Movement.Movement.Offset(direction, ref x, ref y); + + int destroyables = 0; + List obstacles = new List(); + + var eable = map.GetItemsInRange(new Point3D(x, y, mob.Location.Z), 1); + + foreach (var item in eable) + { + if (canOpenDoors && item is BaseDoor door && door.Z + door.ItemData.Height > mob.Z && mob.Z + 16 > door.Z) + { + if (door.X != x || door.Y != y) + { + continue; + } + + if (!door.Locked || !door.UseLocks()) + { + obstacles.Add(item); + } + + if (!canDestroyObstacles) + { + break; + } + } + else if (canDestroyObstacles && item.Movable && item.ItemData.Impassable && item.Z + item.ItemData.Height > mob.Z && mob.Z + 16 > item.Z) + { + if (!mob.InRange(item.GetWorldLocation(), 1)) + { + continue; + } + + obstacles.Add(item); + ++destroyables; + } + } + + eable.Free(); + + if (destroyables > 0) + { + Effects.PlaySound(new Point3D(x, y, mob.Z), mob.Map, 0x3B3); + } + + if (obstacles.Count > 0) + blocked = true; + + while (obstacles.Count > 0) + { + Item item = obstacles.First(); + + if (item is BaseDoor door) + { + mob.DebugSay("Opening door..."); + if (!door.Open) + { + door.Use(mob); + } + obstacles.Remove(item); + } + else + { + if (item is Container container) + { + for (var i = 0; i < container.Items.Count; ++i) + { + Item check = container.Items[i]; + + if (check.Movable && check.ItemData.Impassable && container.Z + check.ItemData.Height > mob.Z) + { + obstacles.Add(check); + } + } + + obstacles.Remove(item); + container.Destroy(); + } + else + { + obstacles.Remove(item); + item.Delete(); + } + } + } + + if (!blocked) + { + blocked = !mob.Move(direction); + } + } + } + + if (blocked) + { + int offset = Utility.RandomDouble() >= 0.6 ? 1 : -1; + + for (var i = 0; i < 2; ++i) + { + mob.TurnInternal(offset); + + if (mob.Move(mob.Direction)) + { + MoveImpl.IgnoreMovableImpassables = false; + return MoveResult.SuccessAutoTurn; + } + } + + MoveImpl.IgnoreMovableImpassables = false; + return wasPushing ? MoveResult.BadState : MoveResult.Blocked; + } + + MoveImpl.IgnoreMovableImpassables = false; + return MoveResult.Success; + } + public static bool WalkMobileRange(BaseCreature mob, BaseCreature target, int steps, bool run, int minRange, int maxRange) + { + if (mob.Deleted || mob.DisallowAllMoves) + { + return false; + } + + if (target == null) + { + return false; + } + + for (var i = 0; i < steps; i++) + { + // Get the current distance + var currentDistance = (int)mob.GetDistanceToSqrt(target); + + if (currentDistance < minRange || currentDistance > maxRange) + { + var needCloser = currentDistance > maxRange; + var direction = needCloser ? + mob.GetDirectionTo(target, run) : target.GetDirectionTo(mob, run); + + DoMove(mob, direction, run); + } + else + { + WalkRandom(mob, 2, run); + } + return true; + } + + // Get the current distance + var newDistance = (int)mob.GetDistanceToSqrt(target); + + return newDistance >= minRange && newDistance <= maxRange; + } + } +} diff --git a/Projects/UOContent/Mobiles/BT/Core/BehaviorTreeNode.cs b/Projects/UOContent/Mobiles/BT/Core/BehaviorTreeNode.cs new file mode 100644 index 000000000..c41605a06 --- /dev/null +++ b/Projects/UOContent/Mobiles/BT/Core/BehaviorTreeNode.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Server.Mobiles.BT +{ + public abstract class BehaviorTreeNode + { + public enum Result { Running, Failure, Success } + public BehaviorTreeNode Parent; + public BehaviorTree Tree { get; private set; } + public BehaviorTreeNode(BehaviorTree tree) : this(tree, null) + { + } + public BehaviorTreeNode(BehaviorTree tree, BehaviorTreeNode parent) + { + Parent = parent; + Tree = tree; + } + public virtual Result Execute(BaseCreature mob, Blackboard blackboard) + { + return Result.Failure; + } + } +} diff --git a/Projects/UOContent/Mobiles/BT/Core/Blackboard.cs b/Projects/UOContent/Mobiles/BT/Core/Blackboard.cs new file mode 100644 index 000000000..a75a026f0 --- /dev/null +++ b/Projects/UOContent/Mobiles/BT/Core/Blackboard.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Server.Mobiles.BT +{ + public class Blackboard : Dictionary + { + } +} diff --git a/Projects/UOContent/Mobiles/BT/Core/CompositeNode.cs b/Projects/UOContent/Mobiles/BT/Core/CompositeNode.cs new file mode 100644 index 000000000..ea9acbfbb --- /dev/null +++ b/Projects/UOContent/Mobiles/BT/Core/CompositeNode.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Server.Mobiles.BT +{ + public abstract class CompositeNode : BehaviorTreeNode + { + protected Dictionary currentTaskCache; + public List Children { get; protected set; } + public CompositeNode(BehaviorTree tree) : base(tree) + { + currentTaskCache = new Dictionary(); + Children = new List(); + } + public virtual CompositeNode AddChild(BehaviorTreeNode child) + { + child.Parent = this; + Children.Add(child); + return this; + } + } +} diff --git a/Projects/UOContent/Mobiles/BT/Core/DecoratorNode.cs b/Projects/UOContent/Mobiles/BT/Core/DecoratorNode.cs new file mode 100644 index 000000000..8c9143690 --- /dev/null +++ b/Projects/UOContent/Mobiles/BT/Core/DecoratorNode.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Server.Mobiles.BT +{ + public abstract class DecoratorNode : BehaviorTreeNode + { + public BehaviorTreeNode Child; + public DecoratorNode(BehaviorTree tree) : base(tree) + { + } + public DecoratorNode(BehaviorTree tree, BehaviorTreeNode child) : this(tree) + { + AddChild(child); + } + public virtual DecoratorNode AddChild(BehaviorTreeNode child) + { + if (Child == null) + { + Child = child; + } + return this; + } + } +} diff --git a/Projects/UOContent/Mobiles/BehaviorTreeAI/LeafNode.cs b/Projects/UOContent/Mobiles/BT/Core/LeafNode.cs similarity index 86% rename from Projects/UOContent/Mobiles/BehaviorTreeAI/LeafNode.cs rename to Projects/UOContent/Mobiles/BT/Core/LeafNode.cs index 8c84a0bf6..b8c1f2866 100644 --- a/Projects/UOContent/Mobiles/BehaviorTreeAI/LeafNode.cs +++ b/Projects/UOContent/Mobiles/BT/Core/LeafNode.cs @@ -4,7 +4,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -namespace Server.Mobiles.BehaviorTreeAI +namespace Server.Mobiles.BT { public abstract class LeafNode : BehaviorTreeNode { diff --git a/Projects/UOContent/Mobiles/BT/Decorator/CheckSkillNode.cs b/Projects/UOContent/Mobiles/BT/Decorator/CheckSkillNode.cs new file mode 100644 index 000000000..a2ed01dbb --- /dev/null +++ b/Projects/UOContent/Mobiles/BT/Decorator/CheckSkillNode.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Server.Mobiles.BT +{ + public class CheckSkillNode : DecoratorNode + { + private SkillName checkSkill; + private double minSkillValue; + private double maxSkillValue; + + public CheckSkillNode(BehaviorTree tree, SkillName skill, double minSkill, double maxSkill) : base(tree) + { + checkSkill = skill; + minSkillValue = minSkill; + maxSkillValue = maxSkill; + } + + public override Result Execute(BaseCreature mob, Blackboard blackboard) + { + if(mob.CheckSkill(checkSkill, minSkillValue, maxSkillValue) && Child != null) + { + return Child.Execute(mob, blackboard); + } + + return Result.Failure; + } + } +} diff --git a/Projects/UOContent/Mobiles/BT/Decorator/ConditionalLoopNode.cs b/Projects/UOContent/Mobiles/BT/Decorator/ConditionalLoopNode.cs new file mode 100644 index 000000000..6e3a89381 --- /dev/null +++ b/Projects/UOContent/Mobiles/BT/Decorator/ConditionalLoopNode.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Server.Mobiles.BT +{ + public class ConditionalLoopNode : DecoratorNode + { + public delegate bool ConditionalLoopPredicate(BaseCreature mob, Blackboard blackboard); + private ConditionalLoopPredicate predicate; + public ConditionalLoopNode(BehaviorTree tree, ConditionalLoopPredicate fn) : base(tree) + { + predicate = fn; + } + + public ConditionalLoopNode(BehaviorTree tree, ConditionalLoopPredicate fn, BehaviorTreeNode child) : base(tree, child) + { + predicate = fn; + } + public override Result Execute(BaseCreature mob, Blackboard blackboard) + { + if (predicate(mob, blackboard)) + { + if (Child != null) + { + Child.Execute(mob, blackboard); + return Result.Running; + } + } + + return Result.Failure; + } + } +} diff --git a/Projects/UOContent/Mobiles/BT/Decorator/CooldownNode.cs b/Projects/UOContent/Mobiles/BT/Decorator/CooldownNode.cs new file mode 100644 index 000000000..574450125 --- /dev/null +++ b/Projects/UOContent/Mobiles/BT/Decorator/CooldownNode.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Server.Mobiles.BT +{ + public class CooldownNode : DecoratorNode + { + private TimeSpan cooldownDuration; + private Dictionary cooldowns; + public CooldownNode(BehaviorTree tree, TimeSpan duration) : base(tree) + { + cooldownDuration = duration; + cooldowns = new Dictionary(); + } + public CooldownNode(BehaviorTree tree, TimeSpan duration, BehaviorTreeNode child) : base(tree, child) + { + cooldownDuration = duration; + cooldowns = new Dictionary(); + } + public override Result Execute(BaseCreature mob, Blackboard blackboard) + { + if (Child != null) + { + if (!cooldowns.TryGetValue(mob, out DateTime nextCooldown)) + { + nextCooldown = DateTime.Now; + cooldowns.Add(mob, nextCooldown); + } + + if (DateTime.Now >= nextCooldown) + { + Result result = Child.Execute(mob, blackboard); + + if (result == Result.Running) + { + return Result.Running; + } + else + { + cooldowns[mob] = DateTime.Now + cooldownDuration; + } + } + } + + return Result.Failure; + } + } +} diff --git a/Projects/UOContent/Mobiles/BT/Decorator/ForceSuccessNode.cs b/Projects/UOContent/Mobiles/BT/Decorator/ForceSuccessNode.cs new file mode 100644 index 000000000..4e546328d --- /dev/null +++ b/Projects/UOContent/Mobiles/BT/Decorator/ForceSuccessNode.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Server.Mobiles.BT +{ + public class ForceSuccessNode : DecoratorNode + { + public ForceSuccessNode(BehaviorTree tree) : base(tree) + { + } + public ForceSuccessNode(BehaviorTree tree, BehaviorTreeNode child) : base(tree, child) + { + } + public override Result Execute(BaseCreature mob, Blackboard blackboard) + { + if (Child != null) + { + if (Child.Execute(mob, blackboard) == Result.Running) + { + return Result.Running; + } + + return Result.Success; + } + return Result.Failure; + } + } +} diff --git a/Projects/UOContent/Mobiles/BT/Decorator/InverterNode.cs b/Projects/UOContent/Mobiles/BT/Decorator/InverterNode.cs new file mode 100644 index 000000000..a1f5deb46 --- /dev/null +++ b/Projects/UOContent/Mobiles/BT/Decorator/InverterNode.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Server.Mobiles.BT +{ + public class InverterNode : DecoratorNode + { + public InverterNode(BehaviorTree tree) : base(tree) + { + } + public InverterNode(BehaviorTree tree, BehaviorTreeNode child) : base(tree, child) + { + } + public override Result Execute(BaseCreature mob, Blackboard blackboard) + { + if (Child != null) + { + var result = Child.Execute(mob, blackboard); + switch (result) + { + case Result.Failure: + return Result.Success; + case Result.Success: + return Result.Failure; + default: + return Result.Running; + } + } + + return Result.Failure; + } + } +} diff --git a/Projects/UOContent/Mobiles/BT/Decorator/LoopNode.cs b/Projects/UOContent/Mobiles/BT/Decorator/LoopNode.cs new file mode 100644 index 000000000..c909d267a --- /dev/null +++ b/Projects/UOContent/Mobiles/BT/Decorator/LoopNode.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; + +namespace Server.Mobiles.BT +{ + public class LoopNode : DecoratorNode + { + private int count; + private Dictionary currentCountCache; + public LoopNode(BehaviorTree tree, int n) : base(tree) + { + count = n; + currentCountCache = new Dictionary(); + } + public LoopNode(BehaviorTree tree, int n, BehaviorTreeNode child) : base(tree, child) + { + count = n; + currentCountCache = new Dictionary(); + } + public override Result Execute(BaseCreature mob, Blackboard blackboard) + { + if (!currentCountCache.TryGetValue(mob, out int currentCount)) + { + currentCount = 0; + currentCountCache.Add(mob, currentCount); + } + + if (Child != null && currentCount < count) + { + Child.Execute(mob, blackboard); + currentCountCache[mob]++; + return Result.Running; + } + else if (currentCount >= count) + { + currentCountCache[mob] = 0; + return Result.Success; + } + + return Result.Failure; + } + } +} diff --git a/Projects/UOContent/Mobiles/BT/Decorator/OwnerConditionNode.cs b/Projects/UOContent/Mobiles/BT/Decorator/OwnerConditionNode.cs new file mode 100644 index 000000000..bbb4e7d76 --- /dev/null +++ b/Projects/UOContent/Mobiles/BT/Decorator/OwnerConditionNode.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Server.Mobiles.BT +{ + public class OwnerConditionNode : DecoratorNode + { + public delegate bool OwnerConditionPredicate(BaseCreature mob, Blackboard blackboard); + private OwnerConditionPredicate predicate; + public OwnerConditionNode(BehaviorTree tree, OwnerConditionPredicate fn) : base(tree) + { + predicate = fn; + } + public OwnerConditionNode(BehaviorTree tree, OwnerConditionPredicate fn, BehaviorTreeNode child) : base(tree, child) + { + predicate = fn; + } + public override Result Execute(BaseCreature mob, Blackboard blackboard) + { + if (predicate(mob, blackboard)) + { + if (Child != null) + { + return Child.Execute(mob, blackboard); + } + return Result.Success; + } + return Result.Failure; + } + } +} diff --git a/Projects/UOContent/Mobiles/BT/Decorator/RandomExecutionNode.cs b/Projects/UOContent/Mobiles/BT/Decorator/RandomExecutionNode.cs new file mode 100644 index 000000000..676e3108d --- /dev/null +++ b/Projects/UOContent/Mobiles/BT/Decorator/RandomExecutionNode.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Server.Mobiles.BT +{ + public class RandomExecutionNode : DecoratorNode + { + private double activationChance; + private Dictionary executingCache = new Dictionary(); + public RandomExecutionNode(BehaviorTree tree, double chance) : base(tree) + { + activationChance = chance; + } + public RandomExecutionNode(BehaviorTree tree, double chance, BehaviorTreeNode child) : base(tree, child) + { + activationChance = chance; + } + public override Result Execute(BaseCreature mob, Blackboard blackboard) + { + if (!executingCache.TryGetValue(mob, out bool executing)) + { + executing = false; + executingCache.Add(mob, executing); + } + + if (Child != null && (Utility.RandomDouble() < activationChance || executing)) + { + Result result = Child.Execute(mob, blackboard); + + if (result == Result.Running) + { + executingCache[mob] = true; + return Result.Running; + } + + executingCache[mob] = false; + return result; + } + return Result.Failure; + } + } +} diff --git a/Projects/UOContent/Mobiles/BT/Decorator/TapNode.cs b/Projects/UOContent/Mobiles/BT/Decorator/TapNode.cs new file mode 100644 index 000000000..2a9c563d8 --- /dev/null +++ b/Projects/UOContent/Mobiles/BT/Decorator/TapNode.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Server.Mobiles.BT +{ + public class TapNode : DecoratorNode + { + public delegate void TapNodeAction(BaseCreature mob, Blackboard blackboard); + public TapNodeAction Action { get; private set; } + public TapNode(BehaviorTree tree, TapNodeAction action) : base(tree) + { + Action = action; + } + public override Result Execute(BaseCreature mob, Blackboard blackboard) + { + Action(mob, blackboard); + + if (Child != null) + { + return Child.Execute(mob, blackboard); + } + + return Result.Failure; + } + } +} diff --git a/Projects/UOContent/Mobiles/BehaviorTreeAI/UntilFailNode.cs b/Projects/UOContent/Mobiles/BT/Decorator/UntilFailNode.cs similarity index 53% rename from Projects/UOContent/Mobiles/BehaviorTreeAI/UntilFailNode.cs rename to Projects/UOContent/Mobiles/BT/Decorator/UntilFailNode.cs index 6011f2c50..e5cd471c2 100644 --- a/Projects/UOContent/Mobiles/BehaviorTreeAI/UntilFailNode.cs +++ b/Projects/UOContent/Mobiles/BT/Decorator/UntilFailNode.cs @@ -4,26 +4,28 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -namespace Server.Mobiles.BehaviorTreeAI +namespace Server.Mobiles.BT { public class UntilFailNode : DecoratorNode { public UntilFailNode(BehaviorTree tree) : base(tree) { } + public UntilFailNode(BehaviorTree tree, BehaviorTreeNode child) : base(tree, child) + { + } - public override Result Execute() + public override Result Execute(BaseCreature mob, Blackboard blackboard) { if (Child != null) { - var result = Child.Execute(); - - if (result == Result.Failure) + if(Child.Execute(mob, blackboard) == Result.Failure) { return Result.Failure; } + return Result.Running; } - return Result.Running; + return Result.Failure; } } } diff --git a/Projects/UOContent/Mobiles/BehaviorTreeAI/ActionNode.cs b/Projects/UOContent/Mobiles/BT/Leaf/ActionNode.cs similarity index 86% rename from Projects/UOContent/Mobiles/BehaviorTreeAI/ActionNode.cs rename to Projects/UOContent/Mobiles/BT/Leaf/ActionNode.cs index f1cd3da6e..41d59d46d 100644 --- a/Projects/UOContent/Mobiles/BehaviorTreeAI/ActionNode.cs +++ b/Projects/UOContent/Mobiles/BT/Leaf/ActionNode.cs @@ -4,7 +4,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -namespace Server.Mobiles.BehaviorTreeAI +namespace Server.Mobiles.BT { public abstract class ActionNode : LeafNode { diff --git a/Projects/UOContent/Mobiles/BT/Leaf/ConditionNode.cs b/Projects/UOContent/Mobiles/BT/Leaf/ConditionNode.cs new file mode 100644 index 000000000..fc1c3888a --- /dev/null +++ b/Projects/UOContent/Mobiles/BT/Leaf/ConditionNode.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Server.Mobiles.BT +{ + public delegate bool ConditionNodePredicate(BaseCreature mob, Blackboard blackboard); + public class ConditionNode : LeafNode + { + private ConditionNodePredicate predicate; + public ConditionNode(BehaviorTree tree, ConditionNodePredicate fn) : base(tree) + { + predicate = fn; + } + public override Result Execute(BaseCreature mob, Blackboard blackboard) + { + if (predicate(mob, blackboard)) + { + return Result.Success; + } + return Result.Failure; + } + } +} diff --git a/Projects/UOContent/Mobiles/BT/MageBehaviorTree.cs b/Projects/UOContent/Mobiles/BT/MageBehaviorTree.cs new file mode 100644 index 000000000..6d993d277 --- /dev/null +++ b/Projects/UOContent/Mobiles/BT/MageBehaviorTree.cs @@ -0,0 +1,35 @@ +using System; + +namespace Server.Mobiles.BT +{ + public sealed class MageBehaviorTree : BehaviorTree + { + private static MageBehaviorTree instance; + + private MageBehaviorTree() + { + } + public static MageBehaviorTree Instance + { + get + { + if (instance == null) + { + instance = new MageBehaviorTree(); + instance.InitTree(); + } + return instance; + } + } + private void InitTree() + { + TryAddRoot( + // do the first thing that succeeds of the three children + new SelectorNode(this) + .AddChild(new TapNode(this, (mob, board) => mob.DebugSay("Thinking..."))) + .AddChild(new BehaviorMageCombatNodeSet(this)) + .AddChild(new BehaviorMagePassiveNodeSet(this)) + ); + } + } +} diff --git a/Projects/UOContent/Mobiles/BaseCreature.cs b/Projects/UOContent/Mobiles/BaseCreature.cs index 922fccfee..f7e374687 100644 --- a/Projects/UOContent/Mobiles/BaseCreature.cs +++ b/Projects/UOContent/Mobiles/BaseCreature.cs @@ -2188,6 +2188,7 @@ namespace Server.Mobiles new MeleeAI(this), AIType.AI_Thief => new ThiefAI(this), AIType.AI_BehaviorTree => new AI.BehaviorTreeAI(this), + AIType.AI_FireBoss => new AI.FireBossAI(this), _ => null }; } diff --git a/Projects/UOContent/Mobiles/BehaviorMage.cs b/Projects/UOContent/Mobiles/BehaviorMage.cs new file mode 100644 index 000000000..370b079b6 --- /dev/null +++ b/Projects/UOContent/Mobiles/BehaviorMage.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Server.Mobiles; +using Server.Items; + +namespace Server.Mobiles +{ + public class BehaviorMage : BaseCreature + { + [Constructible] + public BehaviorMage() : base(AIType.AI_BehaviorTree, FightMode.Evil, 2, 0, 0.01, 3) + { + InitBody(); + + SetStr(100, 100); + SetDex(25, 25); + SetInt(100, 100); + + SetDamageType(ResistanceType.Physical, 100); + + SetResistance(ResistanceType.Physical, 40, 50); + SetResistance(ResistanceType.Fire, 40, 50); + SetResistance(ResistanceType.Cold, 40, 50); + SetResistance(ResistanceType.Poison, 40, 50); + SetResistance(ResistanceType.Energy, 40, 50); + + SetSkill(SkillName.EvalInt, 100.0); + SetSkill(SkillName.Magery, 100.0); + SetSkill(SkillName.MagicResist, 100.0); + SetSkill(SkillName.Tactics, 100.0); + SetSkill(SkillName.Meditation, 100.0); + + AddItem(new Backpack() { Movable = false }); + + if (Utility.RandomBool()) + { + SetSkill(SkillName.Swords, 100.0); + RangeFight = 1; + AddToBackpack(new Halberd() { Quality = WeaponQuality.Exceptional, Crafter = this }); + AddToBackpack(new Katana() { Quality = WeaponQuality.Exceptional, Crafter = this }); + } + else + { + SetSkill(SkillName.Archery, 100.0); + RangeFight = 6; + AddToBackpack(new HeavyCrossbow() { Quality = WeaponQuality.Exceptional, Crafter = this }); + AddToBackpack(new Bow() { Quality = WeaponQuality.Exceptional, Crafter = this }); + AddToBackpack(new Bolt() { Amount = 300 }); + AddToBackpack(new Arrow() { Amount = 300 }); + } + + ActiveSpeed = 0.01; + PassiveSpeed = 1; + } + + public BehaviorMage(Serial serial) : base(serial) + { + } + public override bool CanDestroyObstacles => true; + public virtual bool GetGender() => Utility.RandomBool(); + public virtual void InitBody() + { + InitStats(100, 100, 25); + + SpeechHue = Utility.RandomDyedHue(); + Hue = Race.Human.RandomSkinHue(); + + if (Female = GetGender()) + { + Body = 0x191; + Name = NameList.RandomName("female"); + } + else + { + Body = 0x190; + Name = NameList.RandomName("male"); + } + } + public override void Serialize(IGenericWriter writer) + { + base.Serialize(writer); + } + public override void Deserialize(IGenericReader reader) + { + base.Deserialize(reader); + } + } +} diff --git a/Projects/UOContent/Mobiles/BehaviorTreeAI/Actions/CastSpellActionNode.cs b/Projects/UOContent/Mobiles/BehaviorTreeAI/Actions/CastSpellActionNode.cs deleted file mode 100644 index 39b81a6ba..000000000 --- a/Projects/UOContent/Mobiles/BehaviorTreeAI/Actions/CastSpellActionNode.cs +++ /dev/null @@ -1,63 +0,0 @@ -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 deleted file mode 100644 index 3e0b48f60..000000000 --- a/Projects/UOContent/Mobiles/BehaviorTreeAI/Actions/CureSelfNode.cs +++ /dev/null @@ -1,105 +0,0 @@ -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 deleted file mode 100644 index afdf15022..000000000 --- a/Projects/UOContent/Mobiles/BehaviorTreeAI/Actions/DetectHiddenActionNode.cs +++ /dev/null @@ -1,118 +0,0 @@ -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 deleted file mode 100644 index 5e8164a5b..000000000 --- a/Projects/UOContent/Mobiles/BehaviorTreeAI/Actions/HealSelfActionNode.cs +++ /dev/null @@ -1,58 +0,0 @@ -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 deleted file mode 100644 index d1abef6b9..000000000 --- a/Projects/UOContent/Mobiles/BehaviorTreeAI/Actions/MageComboNode.cs +++ /dev/null @@ -1,185 +0,0 @@ -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 deleted file mode 100644 index d71644eaf..000000000 --- a/Projects/UOContent/Mobiles/BehaviorTreeAI/Actions/SelfHealerNode.cs +++ /dev/null @@ -1,50 +0,0 @@ -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 deleted file mode 100644 index 32b194622..000000000 --- a/Projects/UOContent/Mobiles/BehaviorTreeAI/Actions/TargetActionNode.cs +++ /dev/null @@ -1,59 +0,0 @@ -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 deleted file mode 100644 index 6228e081c..000000000 --- a/Projects/UOContent/Mobiles/BehaviorTreeAI/AlwaysSucceedNode.cs +++ /dev/null @@ -1,27 +0,0 @@ -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 deleted file mode 100644 index 20e94994b..000000000 --- a/Projects/UOContent/Mobiles/BehaviorTreeAI/BehaviorTree.cs +++ /dev/null @@ -1,207 +0,0 @@ -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 deleted file mode 100644 index 88256a493..000000000 --- a/Projects/UOContent/Mobiles/BehaviorTreeAI/BehaviorTreeNode.cs +++ /dev/null @@ -1,21 +0,0 @@ -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 deleted file mode 100644 index 4105fe62e..000000000 --- a/Projects/UOContent/Mobiles/BehaviorTreeAI/ConditionNode.cs +++ /dev/null @@ -1,25 +0,0 @@ -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 deleted file mode 100644 index 124eb14c4..000000000 --- a/Projects/UOContent/Mobiles/BehaviorTreeAI/DecoratorNode.cs +++ /dev/null @@ -1,21 +0,0 @@ -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 deleted file mode 100644 index 1862d44e2..000000000 --- a/Projects/UOContent/Mobiles/BehaviorTreeAI/InverterNode.cs +++ /dev/null @@ -1,35 +0,0 @@ -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/RepeaterNode.cs b/Projects/UOContent/Mobiles/BehaviorTreeAI/RepeaterNode.cs deleted file mode 100644 index f8c38aca2..000000000 --- a/Projects/UOContent/Mobiles/BehaviorTreeAI/RepeaterNode.cs +++ /dev/null @@ -1,28 +0,0 @@ -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 deleted file mode 100644 index ce33070b5..000000000 --- a/Projects/UOContent/Mobiles/BehaviorTreeAI/SelectorNode.cs +++ /dev/null @@ -1,48 +0,0 @@ -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 deleted file mode 100644 index 3ed5a4960..000000000 --- a/Projects/UOContent/Mobiles/BehaviorTreeAI/SequenceNode.cs +++ /dev/null @@ -1,48 +0,0 @@ -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 deleted file mode 100644 index fda030d82..000000000 --- a/Projects/UOContent/Mobiles/BehaviorTreeAI/TapNode.cs +++ /dev/null @@ -1,31 +0,0 @@ -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/FireBoss.cs b/Projects/UOContent/Mobiles/FireBoss.cs new file mode 100644 index 000000000..b7083440a --- /dev/null +++ b/Projects/UOContent/Mobiles/FireBoss.cs @@ -0,0 +1,44 @@ +namespace Server.Mobiles +{ + public class FireBoss : BaseCreature + { + [Constructible] + public FireBoss() : base(AIType.AI_FireBoss, FightMode.Closest, 10, 3, 0.05, 0.05) + { + Body = 172; + Hue = 1360; + + SetStr(400); + SetDex(200); + SetInt(1000); + + SetHits(300000); + + SetDamage(50); + + SetDamageType(ResistanceType.Fire, 100); + + SetResistance(ResistanceType.Fire, 50); + + Fame = 25000; + Karma = -25000; + + VirtualArmor = 92; + } + public override string CorpseName => "fire boss corpse"; + public override string DefaultName => "fire boss"; + public FireBoss(Serial serial) : base(serial) + { + } + public override void Serialize(IGenericWriter writer) + { + base.Serialize(writer); + writer.Write(0); + } + public override void Deserialize(IGenericReader reader) + { + base.Deserialize(reader); + var version = reader.ReadInt(); + } + } +} diff --git a/Projects/UOContent/Mobiles/Healers/BaseHealer.cs b/Projects/UOContent/Mobiles/Healers/BaseHealer.cs index d669d5cc5..dc7efe8b3 100644 --- a/Projects/UOContent/Mobiles/Healers/BaseHealer.cs +++ b/Projects/UOContent/Mobiles/Healers/BaseHealer.cs @@ -146,7 +146,7 @@ namespace Server.Mobiles if (!IsInvulnerable) { - AI = AIType.AI_Mage; + AI = AIType.AI_BehaviorTree; ActiveSpeed = 0.2; PassiveSpeed = 0.8; RangePerception = DefaultRangePerception; diff --git a/Projects/UOContent/Mobiles/Healers/EvilHealer.cs b/Projects/UOContent/Mobiles/Healers/EvilHealer.cs index e63990770..6e6fed1b5 100644 --- a/Projects/UOContent/Mobiles/Healers/EvilHealer.cs +++ b/Projects/UOContent/Mobiles/Healers/EvilHealer.cs @@ -7,8 +7,6 @@ namespace Server.Mobiles { AI = AIType.AI_BehaviorTree; - ChangeAIType(AI); - Title = "the healer"; Karma = -10000; @@ -67,8 +65,6 @@ namespace Server.Mobiles { base.Deserialize(reader); - AI = AIType.AI_BehaviorTree; - var version = reader.ReadInt(); } } diff --git a/Projects/UOContent/Mobiles/Vendors/FireBossPriest.cs b/Projects/UOContent/Mobiles/Vendors/FireBossPriest.cs new file mode 100644 index 000000000..ab8c69ef9 --- /dev/null +++ b/Projects/UOContent/Mobiles/Vendors/FireBossPriest.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Server.Mobiles.Vendors +{ + public class FireBossPriest : BaseCreature + { + public FireBoss Owner { get; private set; } + public FireBossPriest(FireBoss owner) : base(AIType.AI_Animal, FightMode.Closest, 10, 1, 2, 2) + { + Owner = owner; + + Body = 770; + Hue = 1358; + + SetStr(1185); + SetDex(255); + SetInt(250); + + SetHits(725); + + SetDamage(25); + + Fame = 24000; + Karma = -24000; + + VirtualArmor = 90; + } + public FireBossPriest(Serial serial) : base(serial) + { + } + public override void Serialize(IGenericWriter writer) + { + base.Serialize(writer); + writer.Write(0); + writer.Write(Owner); + } + public override void Deserialize(IGenericReader reader) + { + base.Deserialize(reader); + var version = reader.ReadInt(); + Owner = reader.ReadEntity(); + } + } +} diff --git a/Projects/UOContent/Spells/Base/Spell.cs b/Projects/UOContent/Spells/Base/Spell.cs index 4be93b9b3..a216561e7 100644 --- a/Projects/UOContent/Spells/Base/Spell.cs +++ b/Projects/UOContent/Spells/Base/Spell.cs @@ -84,7 +84,7 @@ namespace Server.Spells public virtual void OnCasterHurt() { // Confirm: Monsters and pets cannot be disturbed. - if (Caster.Player && IsCasting) + if ((Caster.Player || Caster is BehaviorMage) && IsCasting) { var hasProtection = ProtectionSpell.Registry.TryGetValue(Caster, out var d); if (!hasProtection || d < 1000 && d < Utility.Random(1000)) @@ -393,7 +393,7 @@ namespace Server.Spells m_AnimTimer?.Stop(); - if (Core.AOS && Caster.Player && type == DisturbType.Hurt) + if (Core.AOS && (Caster.Player || Caster is BehaviorMage) && type == DisturbType.Hurt) { DoHurtFizzle(); } @@ -414,7 +414,7 @@ namespace Server.Spells Target.Cancel(Caster); - if (Core.AOS && Caster.Player && type == DisturbType.Hurt) + if (Core.AOS && (Caster.Player || Caster is BehaviorMage) && type == DisturbType.Hurt) { DoHurtFizzle(); } From 3f309edffbe28b9b8abe3717675ec1d8697fce64 Mon Sep 17 00:00:00 2001 From: Leath Cooper Date: Sat, 11 Sep 2021 20:12:32 -0400 Subject: [PATCH 3/5] asdfa --- .../UOContent/Mobiles/AI/BehaviorTreeAI.cs | 24 +++- .../Mobiles/BT/BehaviorMageCombatNodeSet.cs | 4 +- .../Mobiles/BehaviorAI/Composite/Selector.cs | 50 ++++++++ .../Mobiles/BehaviorAI/Composite/Sequence.cs | 50 ++++++++ .../Mobiles/BehaviorAI/Core/Behavior.cs | 52 +++++++++ .../BehaviorAI/Core/BehaviorQueueEntry.cs | 21 ++++ .../Mobiles/BehaviorAI/Core/BehaviorTree.cs | 108 ++++++++++++++++++ .../BehaviorAI/Core/BehaviorTreeContext.cs | 19 +++ .../Mobiles/BehaviorAI/Core/Blackboard.cs | 12 ++ .../Mobiles/BehaviorAI/Core/Composite.cs | 51 +++++++++ .../Mobiles/BehaviorAI/Core/Decorator.cs | 42 +++++++ .../BehaviorAI/Decorator/ConditionalLoop.cs | 41 +++++++ .../Mobiles/BehaviorAI/Decorator/Cooldown.cs | 57 +++++++++ .../BehaviorAI/Decorator/ForceSuccess.cs | 23 ++++ .../Mobiles/BehaviorAI/Decorator/Inverter.cs | 29 +++++ .../Mobiles/BehaviorAI/Decorator/Loop.cs | 57 +++++++++ .../BehaviorAI/Decorator/OwnerCondition.cs | 29 +++++ .../Mobiles/BehaviorAI/Decorator/UntilFail.cs | 30 +++++ .../Mobiles/BehaviorAI/Leaf/AutoTarget.cs | 56 +++++++++ .../Mobiles/BehaviorAI/Leaf/CastSpell.cs | 70 ++++++++++++ .../Mobiles/BehaviorAI/Leaf/Condition.cs | 29 +++++ .../Mobiles/BehaviorAI/Leaf/DynamicTarget.cs | 46 ++++++++ .../UOContent/Mobiles/BehaviorAI/Leaf/Tap.cs | 23 ++++ .../Mobiles/BehaviorAI/Leaf/WaitForTarget.cs | 24 ++++ Projects/UOContent/Mobiles/BehaviorMage.cs | 3 + 25 files changed, 947 insertions(+), 3 deletions(-) create mode 100644 Projects/UOContent/Mobiles/BehaviorAI/Composite/Selector.cs create mode 100644 Projects/UOContent/Mobiles/BehaviorAI/Composite/Sequence.cs create mode 100644 Projects/UOContent/Mobiles/BehaviorAI/Core/Behavior.cs create mode 100644 Projects/UOContent/Mobiles/BehaviorAI/Core/BehaviorQueueEntry.cs create mode 100644 Projects/UOContent/Mobiles/BehaviorAI/Core/BehaviorTree.cs create mode 100644 Projects/UOContent/Mobiles/BehaviorAI/Core/BehaviorTreeContext.cs create mode 100644 Projects/UOContent/Mobiles/BehaviorAI/Core/Blackboard.cs create mode 100644 Projects/UOContent/Mobiles/BehaviorAI/Core/Composite.cs create mode 100644 Projects/UOContent/Mobiles/BehaviorAI/Core/Decorator.cs create mode 100644 Projects/UOContent/Mobiles/BehaviorAI/Decorator/ConditionalLoop.cs create mode 100644 Projects/UOContent/Mobiles/BehaviorAI/Decorator/Cooldown.cs create mode 100644 Projects/UOContent/Mobiles/BehaviorAI/Decorator/ForceSuccess.cs create mode 100644 Projects/UOContent/Mobiles/BehaviorAI/Decorator/Inverter.cs create mode 100644 Projects/UOContent/Mobiles/BehaviorAI/Decorator/Loop.cs create mode 100644 Projects/UOContent/Mobiles/BehaviorAI/Decorator/OwnerCondition.cs create mode 100644 Projects/UOContent/Mobiles/BehaviorAI/Decorator/UntilFail.cs create mode 100644 Projects/UOContent/Mobiles/BehaviorAI/Leaf/AutoTarget.cs create mode 100644 Projects/UOContent/Mobiles/BehaviorAI/Leaf/CastSpell.cs create mode 100644 Projects/UOContent/Mobiles/BehaviorAI/Leaf/Condition.cs create mode 100644 Projects/UOContent/Mobiles/BehaviorAI/Leaf/DynamicTarget.cs create mode 100644 Projects/UOContent/Mobiles/BehaviorAI/Leaf/Tap.cs create mode 100644 Projects/UOContent/Mobiles/BehaviorAI/Leaf/WaitForTarget.cs diff --git a/Projects/UOContent/Mobiles/AI/BehaviorTreeAI.cs b/Projects/UOContent/Mobiles/AI/BehaviorTreeAI.cs index 0606b97bb..945604ce4 100644 --- a/Projects/UOContent/Mobiles/AI/BehaviorTreeAI.cs +++ b/Projects/UOContent/Mobiles/AI/BehaviorTreeAI.cs @@ -3,17 +3,34 @@ using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; -using Server.Mobiles.BT; +// using Server.Mobiles.BT; using Server.Targeting; +using Server.Mobiles.BehaviorAI; +using Server.Spells.Third; namespace Server.Mobiles.AI { public class BehaviorTreeAI : BaseAI { + // private Blackboard blackboard; private Blackboard blackboard; + private BehaviorTree behaviorTree; + private BehaviorTreeContext behaviorTreeContext; public BehaviorTreeAI(BaseCreature m) : base(m) { + // blackboard = new Blackboard(); blackboard = new Blackboard(); + behaviorTreeContext = new BehaviorTreeContext(m, blackboard); + behaviorTree = new BehaviorTree(); + behaviorTree.TryAddRoot( + new Sequence(behaviorTree) + .AddChild(new Condition(behaviorTree, (context) => context.Mobile.RawStr - context.Mobile.Str == 0)) + .AddChild(new CastSpell(behaviorTree, (context) => new BlessSpell(context.Mobile))) + .AddChild(new WaitForTarget(behaviorTree)) + .AddChild(new DynamicTarget(behaviorTree, (context) => context.Mobile)) + ); + + behaviorTree.Start(behaviorTreeContext); } public override bool Think() @@ -23,7 +40,10 @@ namespace Server.Mobiles.AI return false; } - MageBehaviorTree.Instance.RunBehavior(m_Mobile, blackboard); + // MageBehaviorTree.Instance.RunBehavior(m_Mobile, blackboard); + + m_Mobile.DebugSay("Thinking..."); + behaviorTree.Tick(behaviorTreeContext); return true; } diff --git a/Projects/UOContent/Mobiles/BT/BehaviorMageCombatNodeSet.cs b/Projects/UOContent/Mobiles/BT/BehaviorMageCombatNodeSet.cs index 1f9433943..f655226eb 100644 --- a/Projects/UOContent/Mobiles/BT/BehaviorMageCombatNodeSet.cs +++ b/Projects/UOContent/Mobiles/BT/BehaviorMageCombatNodeSet.cs @@ -11,12 +11,14 @@ namespace Server.Mobiles.BT AddChild( new SelectorNode(tree) .AddChild(new TapNode(tree, (mob, board) => { mob.CurrentSpeed = mob.ActiveSpeed; })) - .AddChild(new CooldownNode(tree, TimeSpan.FromSeconds(0.5), new InverterNode(tree, new KeepRangeNode(tree, 1, 2)))) + .AddChild(new CooldownNode(tree, TimeSpan.FromSeconds(0.5), new InverterNode(tree, new KeepRangeNode(tree, 1, 3)))) + /* .AddChild(new MageDebuffNode(tree)) .AddChild(new MageComboNode(tree)) .AddChild(new OwnerConditionNode(tree, (mob, board) => mob.Poison != null, new CureSelfNode(tree, TimeSpan.FromSeconds(6.0)))) .AddChild(new OwnerConditionNode(tree, (mob, board) => mob.HitsMax - mob.Hits >= 40, new HealSelfNode(tree, TimeSpan.FromSeconds(6.0)))) .AddChild(new OwnerConditionNode(tree, shouldMeditate, new MeditateNode(tree, 75))) + */ ); } diff --git a/Projects/UOContent/Mobiles/BehaviorAI/Composite/Selector.cs b/Projects/UOContent/Mobiles/BehaviorAI/Composite/Selector.cs new file mode 100644 index 000000000..b57bfacdd --- /dev/null +++ b/Projects/UOContent/Mobiles/BehaviorAI/Composite/Selector.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Server.Mobiles.BehaviorAI +{ + public class Selector : Composite + { + public Selector(BehaviorTree tree) : base(tree) + { + } + public override void OnChildComplete(BehaviorTreeContext context, Result lastResult) + { + if (!currentChildCache.TryGetValue(context.Mobile, out int currentChild)) + { + currentChild = 0; + currentChildCache.Add(context.Mobile, currentChild); + } + + Behavior child = Children[currentChild]; + + if (lastResult == Result.Success) + { + currentChildCache[context.Mobile] = 0; + SetResult(context, Result.Success); + return; + } + + currentChild++; + if (currentChild >= Children.Count) + { + currentChildCache[context.Mobile] = 0; + SetResult(context, Result.Failure); + return; + } + else + { + currentChildCache[context.Mobile] = currentChild; + SetResult(context, Result.Running); + if (Children[currentChild] == null) + { + throw new NullReferenceException(); + } + Tree.Enqueue(context, Children[currentChild], OnChildComplete); + } + } + } +} diff --git a/Projects/UOContent/Mobiles/BehaviorAI/Composite/Sequence.cs b/Projects/UOContent/Mobiles/BehaviorAI/Composite/Sequence.cs new file mode 100644 index 000000000..aa38262cf --- /dev/null +++ b/Projects/UOContent/Mobiles/BehaviorAI/Composite/Sequence.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Server.Mobiles.BehaviorAI +{ + public class Sequence : Composite + { + public Sequence(BehaviorTree tree) : base(tree) + { + } + public override void OnChildComplete(BehaviorTreeContext context, Result lastResult) + { + if (!currentChildCache.TryGetValue(context.Mobile, out int currentChild)) + { + currentChild = 0; + currentChildCache.Add(context.Mobile, currentChild); + } + + Behavior child = Children[currentChild]; + + if (lastResult == Result.Failure) + { + currentChildCache[context.Mobile] = 0; + SetResult(context, Result.Failure); + return; + } + + currentChild++; + if (currentChild >= Children.Count) + { + currentChildCache[context.Mobile] = 0; + SetResult(context, Result.Success); + return; + } + else + { + SetResult(context, Result.Running); + currentChildCache[context.Mobile] = currentChild; + if (Children[currentChild] == null) + { + throw new NullReferenceException(); + } + Tree.Enqueue(context, Children[currentChild], OnChildComplete); + } + } + } +} diff --git a/Projects/UOContent/Mobiles/BehaviorAI/Core/Behavior.cs b/Projects/UOContent/Mobiles/BehaviorAI/Core/Behavior.cs new file mode 100644 index 000000000..bc6683c3e --- /dev/null +++ b/Projects/UOContent/Mobiles/BehaviorAI/Core/Behavior.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Server.Mobiles.BehaviorAI +{ + public delegate void BehaviorObserver(BehaviorTreeContext context, Result lastResult); + public enum Result + { + Success, + Failure, + Running + } + public abstract class Behavior + { + public BehaviorTree Tree { get; protected set; } + public BehaviorObserver Observer { get; protected set; } + private Dictionary lastResultCache; + public virtual bool IsRunning(BehaviorTreeContext context) => GetResult(context) == Result.Running; + public Behavior(BehaviorTree tree) + { + Tree = tree; + lastResultCache = new Dictionary(); + } + public virtual void Tick(BehaviorTreeContext context) + { + } + public virtual void Execute(BehaviorTreeContext context) + { + } + public Result GetResult(BehaviorTreeContext context) + { + if (!lastResultCache.TryGetValue(context.Mobile, out Result result)) + { + result = Result.Failure; + lastResultCache.Add(context.Mobile, result); + } + return result; + } + public void SetResult(BehaviorTreeContext context, Result result) + { + if (!lastResultCache.TryGetValue(context.Mobile, out Result old)) + { + lastResultCache.Add(context.Mobile, Result.Failure); + } + + lastResultCache[context.Mobile] = result; + } + } +} diff --git a/Projects/UOContent/Mobiles/BehaviorAI/Core/BehaviorQueueEntry.cs b/Projects/UOContent/Mobiles/BehaviorAI/Core/BehaviorQueueEntry.cs new file mode 100644 index 000000000..9b82b562d --- /dev/null +++ b/Projects/UOContent/Mobiles/BehaviorAI/Core/BehaviorQueueEntry.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Server.Mobiles.BehaviorAI +{ + public class BehaviorQueueEntry + { + public BehaviorTreeContext Context { get; } + public BehaviorObserver Observer { get; } + public Behavior Behavior { get; } + public BehaviorQueueEntry(BehaviorTreeContext context, Behavior behavior, BehaviorObserver observer) + { + Context = context; + Behavior = behavior; + Observer = observer; + } + } +} diff --git a/Projects/UOContent/Mobiles/BehaviorAI/Core/BehaviorTree.cs b/Projects/UOContent/Mobiles/BehaviorAI/Core/BehaviorTree.cs new file mode 100644 index 000000000..b9136fa7e --- /dev/null +++ b/Projects/UOContent/Mobiles/BehaviorAI/Core/BehaviorTree.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Server.Mobiles.BehaviorAI +{ + public class BehaviorTree + { + public Behavior Root { get; private set; } + private Dictionary> behaviorQueueCache; + private Dictionary executingCache; + public BehaviorTree() + { + behaviorQueueCache = new Dictionary>(); + executingCache = new Dictionary(); + } + public bool TryAddRoot(Composite behavior) + { + if (Root == null) + { + Root = behavior; + return true; + } + return false; + } + public void Start(BehaviorTreeContext context) + { + if (Root != null) + { + Enqueue(context, Root, ExecutionFinished); + } + } + public void Stop(BehaviorTreeContext context) + { + getQueue(context).Clear(); + } + public virtual void Tick(BehaviorTreeContext context) + { + if (!executingCache.TryGetValue(context.Mobile, out bool executing)) + { + executing = false; + executingCache[context.Mobile] = executing; + } + + if (!executing) + { + executingCache[context.Mobile] = true; + Queue queue = getQueue(context); + queue.Enqueue(null); + while (Step(context)) + { + } + executingCache[context.Mobile] = false; + } + } + public virtual bool Step(BehaviorTreeContext context) + { + Queue queue = getQueue(context); + + BehaviorQueueEntry current = queue.Dequeue(); + + if (current == null || current.Behavior == null || current.Context == null) + { + return false; + } + + current.Behavior.Tick(current.Context); + + if (!current.Behavior.IsRunning(current.Context)) + { + if (current.Observer != null) + { + current.Observer(current.Context, current.Behavior.GetResult(current.Context)); + } + return true; + } + + queue.Enqueue(current); + + return false; + } + public virtual void ExecutionFinished(BehaviorTreeContext context, Result result) + { + } + public void Enqueue(BehaviorTreeContext context, Behavior behavior, BehaviorObserver observer) + { + if (!behaviorQueueCache.TryGetValue(context.Mobile, out Queue queue)) + { + queue = new Queue(); + behaviorQueueCache.Add(context.Mobile, queue); + } + + queue.Enqueue(new BehaviorQueueEntry(context, behavior, observer)); + } + private Queue getQueue(BehaviorTreeContext context) + { + if (!behaviorQueueCache.TryGetValue(context.Mobile, out Queue queue)) + { + queue = new Queue(); + behaviorQueueCache.Add(context.Mobile, queue); + } + + return queue; + } + } +} diff --git a/Projects/UOContent/Mobiles/BehaviorAI/Core/BehaviorTreeContext.cs b/Projects/UOContent/Mobiles/BehaviorAI/Core/BehaviorTreeContext.cs new file mode 100644 index 000000000..440613499 --- /dev/null +++ b/Projects/UOContent/Mobiles/BehaviorAI/Core/BehaviorTreeContext.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Server.Mobiles.BehaviorAI +{ + public class BehaviorTreeContext + { + public BaseCreature Mobile { get; } + public Blackboard Blackboard { get; } + public BehaviorTreeContext(BaseCreature mob, Blackboard blackboard) + { + Mobile = mob; + Blackboard = blackboard; + } + } +} diff --git a/Projects/UOContent/Mobiles/BehaviorAI/Core/Blackboard.cs b/Projects/UOContent/Mobiles/BehaviorAI/Core/Blackboard.cs new file mode 100644 index 000000000..c850af8c4 --- /dev/null +++ b/Projects/UOContent/Mobiles/BehaviorAI/Core/Blackboard.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Server.Mobiles.BehaviorAI +{ + public class Blackboard : Dictionary + { + } +} diff --git a/Projects/UOContent/Mobiles/BehaviorAI/Core/Composite.cs b/Projects/UOContent/Mobiles/BehaviorAI/Core/Composite.cs new file mode 100644 index 000000000..f75dcc14f --- /dev/null +++ b/Projects/UOContent/Mobiles/BehaviorAI/Core/Composite.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Server.Mobiles.BehaviorAI +{ + public class Composite : Behavior + { + public List Children { get; private set; } + protected Dictionary currentChildCache; + public Composite(BehaviorTree tree) : base(tree) + { + Children = new List(); + currentChildCache = new Dictionary(); + } + public Composite AddChild(Behavior child) + { + if (Children != null && child != null) + { + Children.Add(child); + } + return this; + } + public override void Tick(BehaviorTreeContext context) + { + if (!currentChildCache.TryGetValue(context.Mobile, out int currentChild)) + { + currentChild = 0; + currentChildCache.Add(context.Mobile, currentChild); + } + + if (GetResult(context) != Result.Running) + { + currentChild = 0; + currentChildCache[context.Mobile] = currentChild; + SetResult(context, Result.Running); + if (Children[currentChild] == null) + { + throw new NullReferenceException(); + } + Tree.Enqueue(context, Children[currentChild], OnChildComplete); + } + } + public virtual void OnChildComplete(BehaviorTreeContext context, Result result) + { + SetResult(context, Result.Failure); + } + } +} diff --git a/Projects/UOContent/Mobiles/BehaviorAI/Core/Decorator.cs b/Projects/UOContent/Mobiles/BehaviorAI/Core/Decorator.cs new file mode 100644 index 000000000..bedd27e61 --- /dev/null +++ b/Projects/UOContent/Mobiles/BehaviorAI/Core/Decorator.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Server.Mobiles.BehaviorAI +{ + public class Decorator : Behavior + { + public Behavior Child { get; private set; } + public Decorator(BehaviorTree tree) : base(tree) + { + } + public Decorator(BehaviorTree tree, Behavior child) : base(tree) + { + Child = child; + } + public virtual bool AddChild(Behavior child) + { + if (Child == null) + { + Child = child; + return true; + } + return false; + } + public override void Tick(BehaviorTreeContext context) + { + if (Child != null && GetResult(context) != Result.Running) + { + Tree.Enqueue(context, Child, OnChildComplete); + SetResult(context, Result.Running); + return; + } + } + public virtual void OnChildComplete(BehaviorTreeContext context, Result result) + { + SetResult(context, Child.GetResult(context)); + } + } +} diff --git a/Projects/UOContent/Mobiles/BehaviorAI/Decorator/ConditionalLoop.cs b/Projects/UOContent/Mobiles/BehaviorAI/Decorator/ConditionalLoop.cs new file mode 100644 index 000000000..0f92b43ee --- /dev/null +++ b/Projects/UOContent/Mobiles/BehaviorAI/Decorator/ConditionalLoop.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Server.Mobiles.BehaviorAI +{ + public delegate bool ConditionalLoopPredicate(BehaviorTreeContext context); + public class ConditionalLoop : Decorator + { + public ConditionalLoopPredicate Predicate { get; private set; } + public ConditionalLoop(BehaviorTree tree, ConditionalLoopPredicate predicate) : base(tree) + { + Predicate = predicate; + } + public ConditionalLoop(BehaviorTree tree, ConditionalLoopPredicate predicate, Behavior child) : base(tree, child) + { + Predicate = predicate; + } + public override void Tick(BehaviorTreeContext context) + { + if (Predicate(context)) + { + base.Tick(context); + return; + } + SetResult(context, Result.Failure); + return; + } + public override void OnChildComplete(BehaviorTreeContext context, Result result) + { + if (Predicate(context)) + { + Tree.Enqueue(context, Child, OnChildComplete); + return; + } + SetResult(context, Result.Success); + } + } +} diff --git a/Projects/UOContent/Mobiles/BehaviorAI/Decorator/Cooldown.cs b/Projects/UOContent/Mobiles/BehaviorAI/Decorator/Cooldown.cs new file mode 100644 index 000000000..13edfda87 --- /dev/null +++ b/Projects/UOContent/Mobiles/BehaviorAI/Decorator/Cooldown.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Server.Mobiles.BehaviorAI +{ + public class Cooldown : Decorator + { + public TimeSpan Duration { get; } + private Dictionary nextCooldownCache; + public Cooldown(BehaviorTree tree, TimeSpan duration) : base(tree) + { + nextCooldownCache = new Dictionary(); + Duration = duration; + } + public Cooldown(BehaviorTree tree, TimeSpan duration, Behavior child) : base(tree, child) + { + nextCooldownCache = new Dictionary(); + Duration = duration; + } + public override void Tick(BehaviorTreeContext context) + { + if(!nextCooldownCache.TryGetValue(context.Mobile, out long nextCooldown)) + { + nextCooldown = Core.TickCount; + nextCooldownCache.Add(context.Mobile, nextCooldown); + } + + if (Core.TickCount > nextCooldown) + { + base.Tick(context); + } + } + public override void OnChildComplete(BehaviorTreeContext context, Result result) + { + if (!nextCooldownCache.TryGetValue(context.Mobile, out long nextCooldown)) + { + nextCooldown = Core.TickCount; + nextCooldownCache.Add(context.Mobile, nextCooldown); + } + + if (Child != null) + { + if (Child.GetResult(context) == Result.Success) + { + nextCooldownCache[context.Mobile] = Core.TickCount + Duration.Ticks; + } + SetResult(context, Child.GetResult(context)); + return; + } + + SetResult(context, Result.Failure); + } + } +} diff --git a/Projects/UOContent/Mobiles/BehaviorAI/Decorator/ForceSuccess.cs b/Projects/UOContent/Mobiles/BehaviorAI/Decorator/ForceSuccess.cs new file mode 100644 index 000000000..e6a8c9184 --- /dev/null +++ b/Projects/UOContent/Mobiles/BehaviorAI/Decorator/ForceSuccess.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Server.Mobiles.BehaviorAI +{ + public class ForceSuccess : Decorator + { + public ForceSuccess(BehaviorTree tree) : base(tree) + { + } + public ForceSuccess(BehaviorTree tree, Behavior child) : base(tree, child) + { + } + public override void OnChildComplete(BehaviorTreeContext context, Result result) + { + SetResult(context, Result.Success); + } + } +} + diff --git a/Projects/UOContent/Mobiles/BehaviorAI/Decorator/Inverter.cs b/Projects/UOContent/Mobiles/BehaviorAI/Decorator/Inverter.cs new file mode 100644 index 000000000..a3d622e0e --- /dev/null +++ b/Projects/UOContent/Mobiles/BehaviorAI/Decorator/Inverter.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Server.Mobiles.BehaviorAI +{ + public class Inverter : Decorator + { + public Inverter(BehaviorTree tree) : base(tree) + { + } + public Inverter(BehaviorTree tree, Behavior child) : base(tree, child) + { + } + public override void OnChildComplete(BehaviorTreeContext context, Result result) + { + if (result == Result.Success) + { + SetResult(context, Result.Failure); + } + else + { + SetResult(context, Result.Success); + } + } + } +} diff --git a/Projects/UOContent/Mobiles/BehaviorAI/Decorator/Loop.cs b/Projects/UOContent/Mobiles/BehaviorAI/Decorator/Loop.cs new file mode 100644 index 000000000..e93d623e5 --- /dev/null +++ b/Projects/UOContent/Mobiles/BehaviorAI/Decorator/Loop.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Server.Mobiles.BehaviorAI +{ + public class Loop : Decorator + { + private Dictionary currentIterationCache; + private int totalIterations; + public Loop(BehaviorTree tree, int n) : base(tree) + { + currentIterationCache = new Dictionary(); + totalIterations = n; + } + public override void Tick(BehaviorTreeContext context) + { + if (!currentIterationCache.TryGetValue(context.Mobile, out int iteration)) + { + iteration = 0; + currentIterationCache.Add(context.Mobile, iteration); + } + + base.Tick(context); + } + public override void OnChildComplete(BehaviorTreeContext context, Result result) + { + if (!currentIterationCache.TryGetValue(context.Mobile, out int iteration)) + { + iteration = 0; + currentIterationCache.Add(context.Mobile, iteration); + } + + iteration++; + + if (iteration >= totalIterations) + { + currentIterationCache[context.Mobile] = 0; + SetResult(context, Result.Success); + return; + } + + currentIterationCache[context.Mobile] = iteration; + + if (Child != null) + { + Tree.Enqueue(context, Child, OnChildComplete); + return; + } + + currentIterationCache[context.Mobile] = 0; + SetResult(context, Result.Failure); + } + } +} diff --git a/Projects/UOContent/Mobiles/BehaviorAI/Decorator/OwnerCondition.cs b/Projects/UOContent/Mobiles/BehaviorAI/Decorator/OwnerCondition.cs new file mode 100644 index 000000000..2ee6948d2 --- /dev/null +++ b/Projects/UOContent/Mobiles/BehaviorAI/Decorator/OwnerCondition.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Server.Mobiles.BehaviorAI +{ + public delegate bool OwnerConditionPredicate(BehaviorTreeContext context); + public class OwnerCondition : Decorator + { + public OwnerConditionPredicate Predicate { get; } + public OwnerCondition(BehaviorTree tree, OwnerConditionPredicate predicate) : base(tree) + { + Predicate = predicate; + } + public OwnerCondition(BehaviorTree tree, OwnerConditionPredicate predicate, Behavior child) : base(tree, child) + { + Predicate = predicate; + } + public override void Tick(BehaviorTreeContext context) + { + if (Predicate(context)) + { + base.Tick(context); + } + } + } +} diff --git a/Projects/UOContent/Mobiles/BehaviorAI/Decorator/UntilFail.cs b/Projects/UOContent/Mobiles/BehaviorAI/Decorator/UntilFail.cs new file mode 100644 index 000000000..44397f72e --- /dev/null +++ b/Projects/UOContent/Mobiles/BehaviorAI/Decorator/UntilFail.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Server.Mobiles.BehaviorAI +{ + public class UntilFail : Decorator + { + public UntilFail(BehaviorTree tree): base(tree) + { + } + public UntilFail(BehaviorTree tree, Behavior child) : base(tree, child) + { + } + public override void OnChildComplete(BehaviorTreeContext context, Result result) + { + if (Child != null) + { + if (Child.GetResult(context) == Result.Failure) + { + SetResult(context, Result.Success); + return; + } + Tree.Enqueue(context, Child, OnChildComplete); + } + } + } +} diff --git a/Projects/UOContent/Mobiles/BehaviorAI/Leaf/AutoTarget.cs b/Projects/UOContent/Mobiles/BehaviorAI/Leaf/AutoTarget.cs new file mode 100644 index 000000000..d00c84d83 --- /dev/null +++ b/Projects/UOContent/Mobiles/BehaviorAI/Leaf/AutoTarget.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Server.Targeting; + +namespace Server.Mobiles.BehaviorAI +{ + public class AutoTarget : Behavior + { + public AutoTarget(BehaviorTree tree) : base(tree) + { + } + public override void Tick(BehaviorTreeContext context) + { + Target target = context.Mobile.Target; + + if (target == null) + { + SetResult(context, Result.Failure); + return; + } + + Mobile combatant = context.Mobile.Combatant; + + if ((target.Flags & TargetFlags.Harmful) != 0 && combatant != null) + { + if (combatant.Deleted) + { + target.Cancel(context.Mobile, TargetCancelType.Canceled); + SetResult(context, Result.Failure); + return; + } + + if ((target.Range == -1 || context.Mobile.InRange(combatant, target.Range)) && + context.Mobile.CanSee(combatant) && + context.Mobile.InLOS(combatant) + ) + { + target.Invoke(context.Mobile, combatant); + SetResult(context, Result.Success); + return; + } + } + else if ((target.Flags & TargetFlags.Beneficial) != 0) + { + target.Invoke(context.Mobile, context.Mobile); + SetResult(context, Result.Success); + return; + } + + SetResult(context, Result.Failure); + } + } +} diff --git a/Projects/UOContent/Mobiles/BehaviorAI/Leaf/CastSpell.cs b/Projects/UOContent/Mobiles/BehaviorAI/Leaf/CastSpell.cs new file mode 100644 index 000000000..f0a9aba49 --- /dev/null +++ b/Projects/UOContent/Mobiles/BehaviorAI/Leaf/CastSpell.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Server.Spells; + +namespace Server.Mobiles.BehaviorAI +{ + public delegate Spell CastSpellCallback(BehaviorTreeContext context); + public class CastSpell : Behavior + { + public CastSpellCallback Callback { get; } + public CastSpell(BehaviorTree tree, CastSpellCallback callback) : base(tree) + { + Callback = callback; + } + public override void Tick(BehaviorTreeContext context) + { + BaseCreature owner = context.Mobile; + + if (owner.Spell == null) + { + Spell spell = Callback(context); + + if (owner.Mana < spell.GetMana()) + { + SetResult(context, Result.Failure); + owner.DebugSay("Not enough mana..."); + return; + } + + if (Core.TickCount < owner.NextSpellTime) + { + SetResult(context, Result.Running); + owner.DebugSay("On cooldown..."); + return; + } + + if (!spell.Cast()) + { + SetResult(context, Result.Running); + owner.DebugSay("Failed to cast..."); + return; + } + + owner.DebugSay("Casting {0}...", spell.Name); + if (!string.IsNullOrEmpty(spell.Mantra)) + owner.PublicOverheadMessage(Network.MessageType.Spell, owner.SpeechHue, false, spell.Mantra, false); + + SetResult(context, Result.Running); + return; + } + else if (owner.Spell.IsCasting) + { + SetResult(context, Result.Running); + owner.DebugSay("Already casting {0}...", ((Spell)owner.Spell).Name); + return; + } + else if (!owner.Spell.IsCasting) + { + SetResult(context, Result.Success); + owner.DebugSay("Finished casting {0}...", ((Spell)owner.Spell).Name); + return; + } + + SetResult(context, Result.Failure); + } + } +} diff --git a/Projects/UOContent/Mobiles/BehaviorAI/Leaf/Condition.cs b/Projects/UOContent/Mobiles/BehaviorAI/Leaf/Condition.cs new file mode 100644 index 000000000..bc0fea30c --- /dev/null +++ b/Projects/UOContent/Mobiles/BehaviorAI/Leaf/Condition.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Server.Mobiles.BehaviorAI +{ + public delegate bool ConditionPredicate(BehaviorTreeContext context); + public class Condition : Behavior + { + public ConditionPredicate Predicate { get; } + public Condition(BehaviorTree tree, ConditionPredicate predicate) : base(tree) + { + Predicate = predicate; + } + public override void Tick(BehaviorTreeContext context) + { + if(Predicate(context)) + { + SetResult(context, Result.Success); + } + else + { + SetResult(context, Result.Failure); + } + } + } +} diff --git a/Projects/UOContent/Mobiles/BehaviorAI/Leaf/DynamicTarget.cs b/Projects/UOContent/Mobiles/BehaviorAI/Leaf/DynamicTarget.cs new file mode 100644 index 000000000..28560db26 --- /dev/null +++ b/Projects/UOContent/Mobiles/BehaviorAI/Leaf/DynamicTarget.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Server.Targeting; + +namespace Server.Mobiles.BehaviorAI +{ + public delegate object DynamicTargetCallback(BehaviorTreeContext context); + public class DynamicTarget : Behavior + { + public DynamicTargetCallback Transformation { get; private set; } + public DynamicTarget(BehaviorTree tree, DynamicTargetCallback transformation) : base(tree) + { + Transformation = transformation; + } + public override void Tick(BehaviorTreeContext context) + { + if (Transformation != null) + { + SetResult(context, Result.Failure); + return; + } + + Target target = context.Mobile.Target; + + if (target == null) + { + SetResult(context, Result.Failure); + return; + } + + object targeted = Transformation(context); + + if (targeted == null) + { + SetResult(context, Result.Failure); + return; + } + + target.Invoke(context.Mobile, targeted); + SetResult(context, Result.Success); + } + } +} diff --git a/Projects/UOContent/Mobiles/BehaviorAI/Leaf/Tap.cs b/Projects/UOContent/Mobiles/BehaviorAI/Leaf/Tap.cs new file mode 100644 index 000000000..338d4bc28 --- /dev/null +++ b/Projects/UOContent/Mobiles/BehaviorAI/Leaf/Tap.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Server.Mobiles.BehaviorAI +{ + public delegate void TapAction(BehaviorTreeContext context); + public class Tap : Behavior + { + public TapAction Action { get; private set; } + public Tap(BehaviorTree tree, TapAction action) : base(tree) + { + Action = action; + } + public override void Tick(BehaviorTreeContext context) + { + Action(context); + SetResult(context, Result.Failure); + } + } +} diff --git a/Projects/UOContent/Mobiles/BehaviorAI/Leaf/WaitForTarget.cs b/Projects/UOContent/Mobiles/BehaviorAI/Leaf/WaitForTarget.cs new file mode 100644 index 000000000..e07d3a64e --- /dev/null +++ b/Projects/UOContent/Mobiles/BehaviorAI/Leaf/WaitForTarget.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Server.Mobiles.BehaviorAI +{ + public class WaitForTarget : Behavior + { + public WaitForTarget(BehaviorTree tree) : base(tree) + { + } + public override void Tick(BehaviorTreeContext context) + { + if (context.Mobile.Target != null) + { + SetResult(context, Result.Success); + return; + } + SetResult(context, Result.Running); + } + } +} diff --git a/Projects/UOContent/Mobiles/BehaviorMage.cs b/Projects/UOContent/Mobiles/BehaviorMage.cs index 370b079b6..f89566483 100644 --- a/Projects/UOContent/Mobiles/BehaviorMage.cs +++ b/Projects/UOContent/Mobiles/BehaviorMage.cs @@ -13,6 +13,8 @@ namespace Server.Mobiles [Constructible] public BehaviorMage() : base(AIType.AI_BehaviorTree, FightMode.Evil, 2, 0, 0.01, 3) { + Debug = true; + InitBody(); SetStr(100, 100); @@ -32,6 +34,7 @@ namespace Server.Mobiles SetSkill(SkillName.MagicResist, 100.0); SetSkill(SkillName.Tactics, 100.0); SetSkill(SkillName.Meditation, 100.0); + SetSkill(SkillName.Wrestling, 100.0); AddItem(new Backpack() { Movable = false }); From e0874713d17d718037abb45a06e91b8e4c85ca39 Mon Sep 17 00:00:00 2001 From: Leath Cooper Date: Sun, 12 Sep 2021 23:38:56 -0400 Subject: [PATCH 4/5] more event driven behavior tree --- .../UOContent/Mobiles/AI/BehaviorTreeAI.cs | 17 +- .../Mobiles/BehaviorAI/Composite/Selector.cs | 24 +- .../Mobiles/BehaviorAI/Composite/Sequence.cs | 24 +- .../Mobiles/BehaviorAI/Core/Behavior.cs | 4 - .../BehaviorAI/Core/BehaviorQueueEntry.cs | 5 - .../Mobiles/BehaviorAI/Core/BehaviorTree.cs | 339 +++++++++++++++++- .../BehaviorAI/Core/BehaviorTreeContext.cs | 6 - .../Mobiles/BehaviorAI/Core/Blackboard.cs | 4 - .../Mobiles/BehaviorAI/Core/Composite.cs | 8 - .../Mobiles/BehaviorAI/Core/Decorator.cs | 15 +- .../BehaviorAI/Decorator/ConditionalLoop.cs | 6 - .../Mobiles/BehaviorAI/Decorator/Cooldown.cs | 3 - .../BehaviorAI/Decorator/ForceSuccess.cs | 6 - .../Mobiles/BehaviorAI/Decorator/Inverter.cs | 6 - .../Mobiles/BehaviorAI/Decorator/Loop.cs | 4 - .../BehaviorAI/Decorator/OwnerCondition.cs | 7 +- .../Mobiles/BehaviorAI/Decorator/UntilFail.cs | 6 - .../Mobiles/BehaviorAI/Leaf/AutoTarget.cs | 5 - .../Mobiles/BehaviorAI/Leaf/CancelTarget.cs | 22 ++ .../Mobiles/BehaviorAI/Leaf/CastSpell.cs | 9 +- .../Mobiles/BehaviorAI/Leaf/Condition.cs | 6 - .../Mobiles/BehaviorAI/Leaf/DynamicTarget.cs | 13 +- .../UOContent/Mobiles/BehaviorAI/Leaf/Tap.cs | 6 - .../Mobiles/BehaviorAI/Leaf/WaitForTarget.cs | 6 - .../Mobiles/BehaviorAI/Leaf/Wander.cs | 29 ++ .../Mobiles/BehaviorAI/MageCombat.cs | 37 ++ .../Mobiles/BehaviorAI/MagePassive.cs | 14 + Projects/UOContent/Mobiles/BehaviorMage.cs | 4 +- Projects/UOContent/UOContent.csproj | 3 + 29 files changed, 473 insertions(+), 165 deletions(-) create mode 100644 Projects/UOContent/Mobiles/BehaviorAI/Leaf/CancelTarget.cs create mode 100644 Projects/UOContent/Mobiles/BehaviorAI/Leaf/Wander.cs create mode 100644 Projects/UOContent/Mobiles/BehaviorAI/MageCombat.cs create mode 100644 Projects/UOContent/Mobiles/BehaviorAI/MagePassive.cs diff --git a/Projects/UOContent/Mobiles/AI/BehaviorTreeAI.cs b/Projects/UOContent/Mobiles/AI/BehaviorTreeAI.cs index 945604ce4..25426ab0a 100644 --- a/Projects/UOContent/Mobiles/AI/BehaviorTreeAI.cs +++ b/Projects/UOContent/Mobiles/AI/BehaviorTreeAI.cs @@ -12,7 +12,6 @@ namespace Server.Mobiles.AI { public class BehaviorTreeAI : BaseAI { - // private Blackboard blackboard; private Blackboard blackboard; private BehaviorTree behaviorTree; private BehaviorTreeContext behaviorTreeContext; @@ -23,11 +22,16 @@ namespace Server.Mobiles.AI behaviorTreeContext = new BehaviorTreeContext(m, blackboard); behaviorTree = new BehaviorTree(); behaviorTree.TryAddRoot( - new Sequence(behaviorTree) - .AddChild(new Condition(behaviorTree, (context) => context.Mobile.RawStr - context.Mobile.Str == 0)) - .AddChild(new CastSpell(behaviorTree, (context) => new BlessSpell(context.Mobile))) - .AddChild(new WaitForTarget(behaviorTree)) - .AddChild(new DynamicTarget(behaviorTree, (context) => context.Mobile)) + new Selector(behaviorTree) + .AddChild(new MageCombat(behaviorTree)) + .AddChild(new MagePassive(behaviorTree)) + /* + new Sequence(behaviorTree) + .AddChild(new Condition(behaviorTree, (context) => context.Mobile.RawStr - context.Mobile.Str == 0)) + .AddChild(new CastSpell(behaviorTree, (context) => new BlessSpell(context.Mobile))) + .AddChild(new WaitForTarget(behaviorTree)) + .AddChild(new DynamicTarget(behaviorTree, (context) => context.Mobile)) + */ ); behaviorTree.Start(behaviorTreeContext); @@ -42,7 +46,6 @@ namespace Server.Mobiles.AI // MageBehaviorTree.Instance.RunBehavior(m_Mobile, blackboard); - m_Mobile.DebugSay("Thinking..."); behaviorTree.Tick(behaviorTreeContext); return true; diff --git a/Projects/UOContent/Mobiles/BehaviorAI/Composite/Selector.cs b/Projects/UOContent/Mobiles/BehaviorAI/Composite/Selector.cs index b57bfacdd..3d5f253fb 100644 --- a/Projects/UOContent/Mobiles/BehaviorAI/Composite/Selector.cs +++ b/Projects/UOContent/Mobiles/BehaviorAI/Composite/Selector.cs @@ -1,9 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - namespace Server.Mobiles.BehaviorAI { public class Selector : Composite @@ -19,8 +13,7 @@ namespace Server.Mobiles.BehaviorAI currentChildCache.Add(context.Mobile, currentChild); } - Behavior child = Children[currentChild]; - + currentChild++; if (lastResult == Result.Success) { currentChildCache[context.Mobile] = 0; @@ -28,23 +21,16 @@ namespace Server.Mobiles.BehaviorAI return; } - currentChild++; if (currentChild >= Children.Count) { currentChildCache[context.Mobile] = 0; SetResult(context, Result.Failure); return; } - else - { - currentChildCache[context.Mobile] = currentChild; - SetResult(context, Result.Running); - if (Children[currentChild] == null) - { - throw new NullReferenceException(); - } - Tree.Enqueue(context, Children[currentChild], OnChildComplete); - } + + currentChildCache[context.Mobile] = currentChild; + SetResult(context, Result.Running); + Tree.Enqueue(context, Children[currentChild], OnChildComplete); } } } diff --git a/Projects/UOContent/Mobiles/BehaviorAI/Composite/Sequence.cs b/Projects/UOContent/Mobiles/BehaviorAI/Composite/Sequence.cs index aa38262cf..a1bc7473c 100644 --- a/Projects/UOContent/Mobiles/BehaviorAI/Composite/Sequence.cs +++ b/Projects/UOContent/Mobiles/BehaviorAI/Composite/Sequence.cs @@ -1,9 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - namespace Server.Mobiles.BehaviorAI { public class Sequence : Composite @@ -19,8 +13,7 @@ namespace Server.Mobiles.BehaviorAI currentChildCache.Add(context.Mobile, currentChild); } - Behavior child = Children[currentChild]; - + currentChild++; if (lastResult == Result.Failure) { currentChildCache[context.Mobile] = 0; @@ -28,23 +21,16 @@ namespace Server.Mobiles.BehaviorAI return; } - currentChild++; if (currentChild >= Children.Count) { currentChildCache[context.Mobile] = 0; SetResult(context, Result.Success); return; } - else - { - SetResult(context, Result.Running); - currentChildCache[context.Mobile] = currentChild; - if (Children[currentChild] == null) - { - throw new NullReferenceException(); - } - Tree.Enqueue(context, Children[currentChild], OnChildComplete); - } + + currentChildCache[context.Mobile] = currentChild; + SetResult(context, Result.Running); + Tree.Enqueue(context, Children[currentChild], OnChildComplete); } } } diff --git a/Projects/UOContent/Mobiles/BehaviorAI/Core/Behavior.cs b/Projects/UOContent/Mobiles/BehaviorAI/Core/Behavior.cs index bc6683c3e..e0f6a2f21 100644 --- a/Projects/UOContent/Mobiles/BehaviorAI/Core/Behavior.cs +++ b/Projects/UOContent/Mobiles/BehaviorAI/Core/Behavior.cs @@ -1,8 +1,4 @@ -using System; using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Server.Mobiles.BehaviorAI { diff --git a/Projects/UOContent/Mobiles/BehaviorAI/Core/BehaviorQueueEntry.cs b/Projects/UOContent/Mobiles/BehaviorAI/Core/BehaviorQueueEntry.cs index 9b82b562d..90a352e59 100644 --- a/Projects/UOContent/Mobiles/BehaviorAI/Core/BehaviorQueueEntry.cs +++ b/Projects/UOContent/Mobiles/BehaviorAI/Core/BehaviorQueueEntry.cs @@ -1,8 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Server.Mobiles.BehaviorAI { diff --git a/Projects/UOContent/Mobiles/BehaviorAI/Core/BehaviorTree.cs b/Projects/UOContent/Mobiles/BehaviorAI/Core/BehaviorTree.cs index b9136fa7e..b8a1bb08f 100644 --- a/Projects/UOContent/Mobiles/BehaviorAI/Core/BehaviorTree.cs +++ b/Projects/UOContent/Mobiles/BehaviorAI/Core/BehaviorTree.cs @@ -1,8 +1,8 @@ -using System; using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; +using Server.Items; +using Server.Engines.Spawners; +using MoveImpl = Server.Movement.MovementImpl; namespace Server.Mobiles.BehaviorAI { @@ -79,7 +79,7 @@ namespace Server.Mobiles.BehaviorAI queue.Enqueue(current); - return false; + return true; } public virtual void ExecutionFinished(BehaviorTreeContext context, Result result) { @@ -104,5 +104,336 @@ namespace Server.Mobiles.BehaviorAI return queue; } + public static void WalkRandomInHome(BehaviorTreeContext context, int steps) + { + BaseCreature mob = context.Mobile; + + if (mob.Deleted || mob.DisallowAllMoves) + { + return; + } + + if (mob.Home == Point3D.Zero) + { + if (mob.Spawner is RegionSpawner rs) + { + Region region = rs.SpawnRegion; + + if (mob.Region.AcceptsSpawnsFrom(region)) + { + mob.WalkRegion = region; + WalkRandom(context, steps); + mob.WalkRegion = null; + } + else + { + if (region.GoLocation != Point3D.Zero && Utility.RandomDouble() > 0.5) + { + DoMove(context, mob.GetDirectionTo(region.GoLocation)); + } + else + { + WalkRandom(context, steps); + } + } + } + else + { + WalkRandom(context, steps); + } + } + else + { + for (var i = 0; i < steps; i++) + { + if (mob.RangeHome != 0) + { + var currentDistance = (int)mob.GetDistanceToSqrt(mob.Home); + + if (currentDistance < mob.RangeHome * 2 / 3) + { + WalkRandom(context, 1); + } + else if (currentDistance > mob.RangeHome) + { + DoMove(context, mob.GetDirectionTo(mob.Home)); + } + else + { + if (Utility.RandomDouble() > 0.5) + { + DoMove(context, mob.GetDirectionTo(mob.Home)); + } + else + { + WalkRandom(context, 1); + } + } + } + else + { + if (mob.Location != mob.Home) + { + DoMove(context, mob.GetDirectionTo(mob.Home)); + } + } + } + } + } + public static MoveResult WalkRandom(BehaviorTreeContext context, int steps, bool run = false) + { + BaseCreature mob = context.Mobile; + + if (mob.Deleted || mob.DisallowAllMoves) + { + return MoveResult.BadState; + } + + for (var i = 0; i < steps; i++) + { + if (Utility.Random(8) <= 8) + { + var random = Utility.Random(0, 32); + + Direction direction; + + switch (random) + { + case 0: + direction = Direction.Up; + break; + case 1: + direction = Direction.North; + break; + case 2: + direction = Direction.Left; + break; + case 3: + direction = Direction.West; + break; + case 5: + direction = Direction.Down; + break; + case 6: + direction = Direction.South; + break; + case 7: + direction = Direction.Right; + break; + case 8: + direction = Direction.East; + break; + default: + direction = mob.Direction; + break; + } + + DoMove(context, direction, run); + } + } + return MoveResult.Success; + } + public static MoveResult DoMove(BehaviorTreeContext context, Direction d, bool run = false) + { + BaseCreature mob = context.Mobile; + + if (mob.Deleted || mob.Frozen || mob.Paralyzed || mob.Spell?.IsCasting == true || mob.DisallowAllMoves) + { + return MoveResult.BadState; + } + + Direction direction = d; + + if (run) + { + direction |= Direction.Running; + } + + mob.Pushing = false; + + MoveImpl.IgnoreMovableImpassables = mob.CanMoveOverObstacles && !mob.CanDestroyObstacles; + + if ((mob.Direction & Direction.Mask) != (direction & Direction.Mask)) + { + bool moved = mob.Move(direction); + + MoveImpl.IgnoreMovableImpassables = false; + return moved ? MoveResult.Success : MoveResult.Blocked; + } + + if (mob.Move(direction)) + { + MoveImpl.IgnoreMovableImpassables = false; + return MoveResult.Success; + } + + bool wasPushing = mob.Pushing; + + bool blocked = true; + + bool canOpenDoors = mob.CanOpenDoors; + bool canDestroyObstacles = mob.CanDestroyObstacles; + + if (canOpenDoors || canDestroyObstacles) + { + Map map = mob.Map; + + if (map != null) + { + int x = mob.X, y = mob.Y; + Movement.Movement.Offset(direction, ref x, ref y); + + int destroyables = 0; + List obstacles = new List(); + + var eable = map.GetItemsInRange(new Point3D(x, y, mob.Location.Z), 1); + + foreach (var item in eable) + { + if (canOpenDoors && item is BaseDoor door && door.Z + door.ItemData.Height > mob.Z && mob.Z + 16 > door.Z) + { + if (door.X != x || door.Y != y) + { + continue; + } + + if (!door.Locked || !door.UseLocks()) + { + obstacles.Add(item); + } + + if (!canDestroyObstacles) + { + break; + } + } + else if (canDestroyObstacles && item.Movable && item.ItemData.Impassable && item.Z + item.ItemData.Height > mob.Z && mob.Z + 16 > item.Z) + { + if (!mob.InRange(item.GetWorldLocation(), 1)) + { + continue; + } + + obstacles.Add(item); + ++destroyables; + } + } + + eable.Free(); + + if (destroyables > 0) + { + Effects.PlaySound(new Point3D(x, y, mob.Z), mob.Map, 0x3B3); + } + + if (obstacles.Count > 0) + blocked = true; + + while (obstacles.Count > 0) + { + Item item = obstacles.First(); + + if (item is BaseDoor door) + { + mob.DebugSay("Opening door..."); + if (!door.Open) + { + door.Use(mob); + } + obstacles.Remove(item); + } + else + { + if (item is Container container) + { + for (var i = 0; i < container.Items.Count; ++i) + { + Item check = container.Items[i]; + + if (check.Movable && check.ItemData.Impassable && container.Z + check.ItemData.Height > mob.Z) + { + obstacles.Add(check); + } + } + + obstacles.Remove(item); + container.Destroy(); + } + else + { + obstacles.Remove(item); + item.Delete(); + } + } + } + + if (!blocked) + { + blocked = !mob.Move(direction); + } + } + } + + if (blocked) + { + int offset = Utility.RandomDouble() >= 0.6 ? 1 : -1; + + for (var i = 0; i < 2; ++i) + { + mob.TurnInternal(offset); + + if (mob.Move(mob.Direction)) + { + MoveImpl.IgnoreMovableImpassables = false; + return MoveResult.SuccessAutoTurn; + } + } + + MoveImpl.IgnoreMovableImpassables = false; + return wasPushing ? MoveResult.BadState : MoveResult.Blocked; + } + + MoveImpl.IgnoreMovableImpassables = false; + return MoveResult.Success; + } + public static bool WalkMobileRange(BehaviorTreeContext context, BaseCreature target, int steps, bool run, int minRange, int maxRange) + { + BaseCreature mob = context.Mobile; + + if (mob.Deleted || mob.DisallowAllMoves) + { + return false; + } + + if (target == null) + { + return false; + } + + for (var i = 0; i < steps; i++) + { + // Get the current distance + var currentDistance = (int)mob.GetDistanceToSqrt(target); + + if (currentDistance < minRange || currentDistance > maxRange) + { + var needCloser = currentDistance > maxRange; + var direction = needCloser ? + mob.GetDirectionTo(target, run) : target.GetDirectionTo(mob, run); + + DoMove(context, direction, run); + } + else + { + WalkRandom(context, 2, run); + } + return true; + } + + // Get the current distance + var newDistance = (int)mob.GetDistanceToSqrt(target); + + return newDistance >= minRange && newDistance <= maxRange; + } } } diff --git a/Projects/UOContent/Mobiles/BehaviorAI/Core/BehaviorTreeContext.cs b/Projects/UOContent/Mobiles/BehaviorAI/Core/BehaviorTreeContext.cs index 440613499..fd85cf512 100644 --- a/Projects/UOContent/Mobiles/BehaviorAI/Core/BehaviorTreeContext.cs +++ b/Projects/UOContent/Mobiles/BehaviorAI/Core/BehaviorTreeContext.cs @@ -1,9 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - namespace Server.Mobiles.BehaviorAI { public class BehaviorTreeContext diff --git a/Projects/UOContent/Mobiles/BehaviorAI/Core/Blackboard.cs b/Projects/UOContent/Mobiles/BehaviorAI/Core/Blackboard.cs index c850af8c4..567da3600 100644 --- a/Projects/UOContent/Mobiles/BehaviorAI/Core/Blackboard.cs +++ b/Projects/UOContent/Mobiles/BehaviorAI/Core/Blackboard.cs @@ -1,8 +1,4 @@ -using System; using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Server.Mobiles.BehaviorAI { diff --git a/Projects/UOContent/Mobiles/BehaviorAI/Core/Composite.cs b/Projects/UOContent/Mobiles/BehaviorAI/Core/Composite.cs index f75dcc14f..163816218 100644 --- a/Projects/UOContent/Mobiles/BehaviorAI/Core/Composite.cs +++ b/Projects/UOContent/Mobiles/BehaviorAI/Core/Composite.cs @@ -1,8 +1,4 @@ -using System; using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Server.Mobiles.BehaviorAI { @@ -36,10 +32,6 @@ namespace Server.Mobiles.BehaviorAI currentChild = 0; currentChildCache[context.Mobile] = currentChild; SetResult(context, Result.Running); - if (Children[currentChild] == null) - { - throw new NullReferenceException(); - } Tree.Enqueue(context, Children[currentChild], OnChildComplete); } } diff --git a/Projects/UOContent/Mobiles/BehaviorAI/Core/Decorator.cs b/Projects/UOContent/Mobiles/BehaviorAI/Core/Decorator.cs index bedd27e61..feb48dedf 100644 --- a/Projects/UOContent/Mobiles/BehaviorAI/Core/Decorator.cs +++ b/Projects/UOContent/Mobiles/BehaviorAI/Core/Decorator.cs @@ -1,9 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - namespace Server.Mobiles.BehaviorAI { public class Decorator : Behavior @@ -16,14 +10,13 @@ namespace Server.Mobiles.BehaviorAI { Child = child; } - public virtual bool AddChild(Behavior child) + public virtual Decorator AddChild(Behavior child) { if (Child == null) { Child = child; - return true; } - return false; + return this; } public override void Tick(BehaviorTreeContext context) { @@ -34,9 +27,9 @@ namespace Server.Mobiles.BehaviorAI return; } } - public virtual void OnChildComplete(BehaviorTreeContext context, Result result) + public virtual void OnChildComplete(BehaviorTreeContext context, Result lastResult) { - SetResult(context, Child.GetResult(context)); + SetResult(context, lastResult); } } } diff --git a/Projects/UOContent/Mobiles/BehaviorAI/Decorator/ConditionalLoop.cs b/Projects/UOContent/Mobiles/BehaviorAI/Decorator/ConditionalLoop.cs index 0f92b43ee..24fa6e9ad 100644 --- a/Projects/UOContent/Mobiles/BehaviorAI/Decorator/ConditionalLoop.cs +++ b/Projects/UOContent/Mobiles/BehaviorAI/Decorator/ConditionalLoop.cs @@ -1,9 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - namespace Server.Mobiles.BehaviorAI { public delegate bool ConditionalLoopPredicate(BehaviorTreeContext context); diff --git a/Projects/UOContent/Mobiles/BehaviorAI/Decorator/Cooldown.cs b/Projects/UOContent/Mobiles/BehaviorAI/Decorator/Cooldown.cs index 13edfda87..9025210aa 100644 --- a/Projects/UOContent/Mobiles/BehaviorAI/Decorator/Cooldown.cs +++ b/Projects/UOContent/Mobiles/BehaviorAI/Decorator/Cooldown.cs @@ -1,8 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Server.Mobiles.BehaviorAI { diff --git a/Projects/UOContent/Mobiles/BehaviorAI/Decorator/ForceSuccess.cs b/Projects/UOContent/Mobiles/BehaviorAI/Decorator/ForceSuccess.cs index e6a8c9184..f4ad8b733 100644 --- a/Projects/UOContent/Mobiles/BehaviorAI/Decorator/ForceSuccess.cs +++ b/Projects/UOContent/Mobiles/BehaviorAI/Decorator/ForceSuccess.cs @@ -1,9 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - namespace Server.Mobiles.BehaviorAI { public class ForceSuccess : Decorator diff --git a/Projects/UOContent/Mobiles/BehaviorAI/Decorator/Inverter.cs b/Projects/UOContent/Mobiles/BehaviorAI/Decorator/Inverter.cs index a3d622e0e..478c64bb6 100644 --- a/Projects/UOContent/Mobiles/BehaviorAI/Decorator/Inverter.cs +++ b/Projects/UOContent/Mobiles/BehaviorAI/Decorator/Inverter.cs @@ -1,9 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - namespace Server.Mobiles.BehaviorAI { public class Inverter : Decorator diff --git a/Projects/UOContent/Mobiles/BehaviorAI/Decorator/Loop.cs b/Projects/UOContent/Mobiles/BehaviorAI/Decorator/Loop.cs index e93d623e5..304cbf452 100644 --- a/Projects/UOContent/Mobiles/BehaviorAI/Decorator/Loop.cs +++ b/Projects/UOContent/Mobiles/BehaviorAI/Decorator/Loop.cs @@ -1,8 +1,4 @@ -using System; using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Server.Mobiles.BehaviorAI { diff --git a/Projects/UOContent/Mobiles/BehaviorAI/Decorator/OwnerCondition.cs b/Projects/UOContent/Mobiles/BehaviorAI/Decorator/OwnerCondition.cs index 2ee6948d2..5f5b9cd37 100644 --- a/Projects/UOContent/Mobiles/BehaviorAI/Decorator/OwnerCondition.cs +++ b/Projects/UOContent/Mobiles/BehaviorAI/Decorator/OwnerCondition.cs @@ -1,9 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - namespace Server.Mobiles.BehaviorAI { public delegate bool OwnerConditionPredicate(BehaviorTreeContext context); @@ -24,6 +18,7 @@ namespace Server.Mobiles.BehaviorAI { base.Tick(context); } + SetResult(context, Result.Failure); } } } diff --git a/Projects/UOContent/Mobiles/BehaviorAI/Decorator/UntilFail.cs b/Projects/UOContent/Mobiles/BehaviorAI/Decorator/UntilFail.cs index 44397f72e..b89a725f9 100644 --- a/Projects/UOContent/Mobiles/BehaviorAI/Decorator/UntilFail.cs +++ b/Projects/UOContent/Mobiles/BehaviorAI/Decorator/UntilFail.cs @@ -1,9 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - namespace Server.Mobiles.BehaviorAI { public class UntilFail : Decorator diff --git a/Projects/UOContent/Mobiles/BehaviorAI/Leaf/AutoTarget.cs b/Projects/UOContent/Mobiles/BehaviorAI/Leaf/AutoTarget.cs index d00c84d83..515aca0ef 100644 --- a/Projects/UOContent/Mobiles/BehaviorAI/Leaf/AutoTarget.cs +++ b/Projects/UOContent/Mobiles/BehaviorAI/Leaf/AutoTarget.cs @@ -1,8 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using Server.Targeting; namespace Server.Mobiles.BehaviorAI diff --git a/Projects/UOContent/Mobiles/BehaviorAI/Leaf/CancelTarget.cs b/Projects/UOContent/Mobiles/BehaviorAI/Leaf/CancelTarget.cs new file mode 100644 index 000000000..515976197 --- /dev/null +++ b/Projects/UOContent/Mobiles/BehaviorAI/Leaf/CancelTarget.cs @@ -0,0 +1,22 @@ +using Server.Targeting; + +namespace Server.Mobiles.BehaviorAI +{ + public class CancelTarget : Behavior + { + public CancelTarget(BehaviorTree tree) : base(tree) + { + } + public override void Tick(BehaviorTreeContext context) + { + Target target = context.Mobile.Target; + + if (target != null) + { + target.Cancel(context.Mobile, TargetCancelType.Canceled); + } + + SetResult(context, Result.Success); + } + } +} diff --git a/Projects/UOContent/Mobiles/BehaviorAI/Leaf/CastSpell.cs b/Projects/UOContent/Mobiles/BehaviorAI/Leaf/CastSpell.cs index f0a9aba49..29afb006b 100644 --- a/Projects/UOContent/Mobiles/BehaviorAI/Leaf/CastSpell.cs +++ b/Projects/UOContent/Mobiles/BehaviorAI/Leaf/CastSpell.cs @@ -1,8 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using Server.Spells; namespace Server.Mobiles.BehaviorAI @@ -39,7 +34,7 @@ namespace Server.Mobiles.BehaviorAI if (!spell.Cast()) { - SetResult(context, Result.Running); + SetResult(context, Result.Failure); owner.DebugSay("Failed to cast..."); return; } @@ -57,7 +52,7 @@ namespace Server.Mobiles.BehaviorAI owner.DebugSay("Already casting {0}...", ((Spell)owner.Spell).Name); return; } - else if (!owner.Spell.IsCasting) + else if (!owner.Spell.IsCasting && ((Spell)owner.Spell).State == SpellState.Sequencing) { SetResult(context, Result.Success); owner.DebugSay("Finished casting {0}...", ((Spell)owner.Spell).Name); diff --git a/Projects/UOContent/Mobiles/BehaviorAI/Leaf/Condition.cs b/Projects/UOContent/Mobiles/BehaviorAI/Leaf/Condition.cs index bc0fea30c..faaba5283 100644 --- a/Projects/UOContent/Mobiles/BehaviorAI/Leaf/Condition.cs +++ b/Projects/UOContent/Mobiles/BehaviorAI/Leaf/Condition.cs @@ -1,9 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - namespace Server.Mobiles.BehaviorAI { public delegate bool ConditionPredicate(BehaviorTreeContext context); diff --git a/Projects/UOContent/Mobiles/BehaviorAI/Leaf/DynamicTarget.cs b/Projects/UOContent/Mobiles/BehaviorAI/Leaf/DynamicTarget.cs index 28560db26..f640db275 100644 --- a/Projects/UOContent/Mobiles/BehaviorAI/Leaf/DynamicTarget.cs +++ b/Projects/UOContent/Mobiles/BehaviorAI/Leaf/DynamicTarget.cs @@ -1,8 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using Server.Targeting; namespace Server.Mobiles.BehaviorAI @@ -10,14 +5,14 @@ namespace Server.Mobiles.BehaviorAI public delegate object DynamicTargetCallback(BehaviorTreeContext context); public class DynamicTarget : Behavior { - public DynamicTargetCallback Transformation { get; private set; } + public DynamicTargetCallback Callback { get; private set; } public DynamicTarget(BehaviorTree tree, DynamicTargetCallback transformation) : base(tree) { - Transformation = transformation; + Callback = transformation; } public override void Tick(BehaviorTreeContext context) { - if (Transformation != null) + if (Callback == null) { SetResult(context, Result.Failure); return; @@ -31,7 +26,7 @@ namespace Server.Mobiles.BehaviorAI return; } - object targeted = Transformation(context); + object targeted = Callback(context); if (targeted == null) { diff --git a/Projects/UOContent/Mobiles/BehaviorAI/Leaf/Tap.cs b/Projects/UOContent/Mobiles/BehaviorAI/Leaf/Tap.cs index 338d4bc28..8bb3f1c5a 100644 --- a/Projects/UOContent/Mobiles/BehaviorAI/Leaf/Tap.cs +++ b/Projects/UOContent/Mobiles/BehaviorAI/Leaf/Tap.cs @@ -1,9 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - namespace Server.Mobiles.BehaviorAI { public delegate void TapAction(BehaviorTreeContext context); diff --git a/Projects/UOContent/Mobiles/BehaviorAI/Leaf/WaitForTarget.cs b/Projects/UOContent/Mobiles/BehaviorAI/Leaf/WaitForTarget.cs index e07d3a64e..87f767d79 100644 --- a/Projects/UOContent/Mobiles/BehaviorAI/Leaf/WaitForTarget.cs +++ b/Projects/UOContent/Mobiles/BehaviorAI/Leaf/WaitForTarget.cs @@ -1,9 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - namespace Server.Mobiles.BehaviorAI { public class WaitForTarget : Behavior diff --git a/Projects/UOContent/Mobiles/BehaviorAI/Leaf/Wander.cs b/Projects/UOContent/Mobiles/BehaviorAI/Leaf/Wander.cs new file mode 100644 index 000000000..f1660518a --- /dev/null +++ b/Projects/UOContent/Mobiles/BehaviorAI/Leaf/Wander.cs @@ -0,0 +1,29 @@ +namespace Server.Mobiles.BehaviorAI +{ + public class Wander : Behavior + { + public int Range { get; } + public double Chance { get; } + public Wander(BehaviorTree tree) : this(tree, 2) + { + } + public Wander(BehaviorTree tree, int range) : this(tree, range, 1.0) + { + } + public Wander(BehaviorTree tree, int range, double chance) : base(tree) + { + Range = range; + Chance = chance; + } + public override void Tick(BehaviorTreeContext context) + { + if (!context.Mobile.CheckIdle() && Utility.RandomDouble() < Chance) + { + BehaviorTree.WalkRandomInHome(context, Range); + SetResult(context, Result.Success); + return; + } + SetResult(context, Result.Failure); + } + } +} diff --git a/Projects/UOContent/Mobiles/BehaviorAI/MageCombat.cs b/Projects/UOContent/Mobiles/BehaviorAI/MageCombat.cs new file mode 100644 index 000000000..24b9276be --- /dev/null +++ b/Projects/UOContent/Mobiles/BehaviorAI/MageCombat.cs @@ -0,0 +1,37 @@ +using Server.Spells; +using Server.Spells.First; +using Server.Spells.Second; +using Server.Spells.Third; +using Server.Spells.Sixth; + +namespace Server.Mobiles.BehaviorAI +{ + public class MageCombat : OwnerCondition + { + public MageCombat(BehaviorTree tree) : base(tree, canActivate) + { + AddChild( + new Sequence(tree) + .AddChild(new Condition(tree, (context) => context.Mobile.Mana >= 65)) + .AddChild(new CastSpell(tree, (context) => new ExplosionSpell(context.Mobile))) + .AddChild(new WaitForTarget(tree)) + .AddChild(new AutoTarget(tree)) + .AddChild( + new ForceSuccess(tree) + .AddChild( + new Selector(tree) + .AddChild(new OwnerCondition(tree, (context) => Utility.RandomBool(), new CastSpell(tree, (context) => new PoisonSpell(context.Mobile)))) + .AddChild(new OwnerCondition(tree, (context) => Utility.RandomBool(), new CastSpell(tree, (context) => new FireballSpell(context.Mobile)))) + .AddChild(new CastSpell(tree, (context) => new EnergyBoltSpell(context.Mobile))) + ) + ) + .AddChild(new WaitForTarget(tree)) + .AddChild(new AutoTarget(tree)) + ); + } + public static bool canActivate(BehaviorTreeContext context) + { + return context.Mobile.Combatant != null; + } + } +} diff --git a/Projects/UOContent/Mobiles/BehaviorAI/MagePassive.cs b/Projects/UOContent/Mobiles/BehaviorAI/MagePassive.cs new file mode 100644 index 000000000..c76b3b75b --- /dev/null +++ b/Projects/UOContent/Mobiles/BehaviorAI/MagePassive.cs @@ -0,0 +1,14 @@ +namespace Server.Mobiles.BehaviorAI +{ + public class MagePassive : OwnerCondition + { + public MagePassive(BehaviorTree tree) : base(tree, canActivate) + { + AddChild(new Wander(tree, 2, 0.5)); + } + public static bool canActivate(BehaviorTreeContext context) + { + return context.Mobile.Combatant == null; + } + } +} diff --git a/Projects/UOContent/Mobiles/BehaviorMage.cs b/Projects/UOContent/Mobiles/BehaviorMage.cs index f89566483..9236f4d82 100644 --- a/Projects/UOContent/Mobiles/BehaviorMage.cs +++ b/Projects/UOContent/Mobiles/BehaviorMage.cs @@ -11,9 +11,9 @@ namespace Server.Mobiles public class BehaviorMage : BaseCreature { [Constructible] - public BehaviorMage() : base(AIType.AI_BehaviorTree, FightMode.Evil, 2, 0, 0.01, 3) + public BehaviorMage() : base(AIType.AI_BehaviorTree, FightMode.Evil, 2, 0, 0.01, 1) { - Debug = true; + // Debug = true; InitBody(); diff --git a/Projects/UOContent/UOContent.csproj b/Projects/UOContent/UOContent.csproj index 16c2b724f..e0ef6c535 100755 --- a/Projects/UOContent/UOContent.csproj +++ b/Projects/UOContent/UOContent.csproj @@ -52,6 +52,9 @@ + + + true Generated From bb2999d3ab21c6f03bbc8557d55eff0e9fa24837 Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Sat, 16 Oct 2021 19:22:19 -0700 Subject: [PATCH 5/5] WIP-broken --- .../UOContent/Mobiles/AI/BehaviorTreeAI.cs | 18 ++--- .../Mobiles/BT/Actions/CastSpellActionNode.cs | 8 +- .../Mobiles/BT/Actions/MageComboNode.cs | 4 + .../Mobiles/BehaviorAI/Core/Behavior.cs | 11 ++- .../Mobiles/BehaviorAI/Core/BehaviorTree.cs | 77 +++++++------------ .../Mobiles/BehaviorAI/Core/Composite.cs | 2 +- .../Mobiles/BehaviorAI/Core/Decorator.cs | 8 +- .../Mobiles/BehaviorAI/Leaf/CastSpell.cs | 8 +- 8 files changed, 63 insertions(+), 73 deletions(-) diff --git a/Projects/UOContent/Mobiles/AI/BehaviorTreeAI.cs b/Projects/UOContent/Mobiles/AI/BehaviorTreeAI.cs index 25426ab0a..123fd5f60 100644 --- a/Projects/UOContent/Mobiles/AI/BehaviorTreeAI.cs +++ b/Projects/UOContent/Mobiles/AI/BehaviorTreeAI.cs @@ -13,8 +13,8 @@ namespace Server.Mobiles.AI public class BehaviorTreeAI : BaseAI { private Blackboard blackboard; - private BehaviorTree behaviorTree; - private BehaviorTreeContext behaviorTreeContext; + private readonly BehaviorTree behaviorTree; + private readonly BehaviorTreeContext behaviorTreeContext; public BehaviorTreeAI(BaseCreature m) : base(m) { // blackboard = new Blackboard(); @@ -25,13 +25,13 @@ namespace Server.Mobiles.AI new Selector(behaviorTree) .AddChild(new MageCombat(behaviorTree)) .AddChild(new MagePassive(behaviorTree)) - /* - new Sequence(behaviorTree) - .AddChild(new Condition(behaviorTree, (context) => context.Mobile.RawStr - context.Mobile.Str == 0)) - .AddChild(new CastSpell(behaviorTree, (context) => new BlessSpell(context.Mobile))) - .AddChild(new WaitForTarget(behaviorTree)) - .AddChild(new DynamicTarget(behaviorTree, (context) => context.Mobile)) - */ + /* + new Sequence(behaviorTree) + .AddChild(new Condition(behaviorTree, (context) => context.Mobile.RawStr - context.Mobile.Str == 0)) + .AddChild(new CastSpell(behaviorTree, (context) => new BlessSpell(context.Mobile))) + .AddChild(new WaitForTarget(behaviorTree)) + .AddChild(new DynamicTarget(behaviorTree, (context) => context.Mobile)) + */ ); behaviorTree.Start(behaviorTreeContext); diff --git a/Projects/UOContent/Mobiles/BT/Actions/CastSpellActionNode.cs b/Projects/UOContent/Mobiles/BT/Actions/CastSpellActionNode.cs index 232331ccc..831491cbd 100644 --- a/Projects/UOContent/Mobiles/BT/Actions/CastSpellActionNode.cs +++ b/Projects/UOContent/Mobiles/BT/Actions/CastSpellActionNode.cs @@ -40,16 +40,20 @@ namespace Server.Mobiles.BT } if (!string.IsNullOrEmpty(spell.Mantra)) + { mob.PublicOverheadMessage(Network.MessageType.Spell, 0, false, spell.Mantra, false); + } return Result.Running; } - else if (mob.Spell.IsCasting) + + if (mob.Spell.IsCasting) { mob.DebugSay("Still casting..."); return Result.Running; } - else if (!mob.Spell.IsCasting) + + if (!mob.Spell.IsCasting) { mob.DebugSay("Finished casting..."); return Result.Success; diff --git a/Projects/UOContent/Mobiles/BT/Actions/MageComboNode.cs b/Projects/UOContent/Mobiles/BT/Actions/MageComboNode.cs index b6ff1ec06..f55ddb194 100644 --- a/Projects/UOContent/Mobiles/BT/Actions/MageComboNode.cs +++ b/Projects/UOContent/Mobiles/BT/Actions/MageComboNode.cs @@ -73,14 +73,18 @@ namespace Server.Mobiles.BT } if (mob.Combatant.Meditating) + { return true; + } return false; } private static bool shouldContinueCombo(BaseCreature mob, Blackboard board) { if (mob.Combatant == null || mob.Combatant.Deleted) + { return false; + } if (mob.Combatant.HitsMax - mob.Combatant.Hits >= 70) { diff --git a/Projects/UOContent/Mobiles/BehaviorAI/Core/Behavior.cs b/Projects/UOContent/Mobiles/BehaviorAI/Core/Behavior.cs index e0f6a2f21..4e534fcdf 100644 --- a/Projects/UOContent/Mobiles/BehaviorAI/Core/Behavior.cs +++ b/Projects/UOContent/Mobiles/BehaviorAI/Core/Behavior.cs @@ -7,13 +7,15 @@ namespace Server.Mobiles.BehaviorAI { Success, Failure, - Running + Running, + Terminated } public abstract class Behavior { public BehaviorTree Tree { get; protected set; } public BehaviorObserver Observer { get; protected set; } - private Dictionary lastResultCache; + private readonly Dictionary lastResultCache; + public virtual bool IsRunning(BehaviorTreeContext context) => GetResult(context) == Result.Running; public Behavior(BehaviorTree tree) { @@ -30,14 +32,15 @@ namespace Server.Mobiles.BehaviorAI { if (!lastResultCache.TryGetValue(context.Mobile, out Result result)) { - result = Result.Failure; + result = Result.Terminated; lastResultCache.Add(context.Mobile, result); } + return result; } public void SetResult(BehaviorTreeContext context, Result result) { - if (!lastResultCache.TryGetValue(context.Mobile, out Result old)) + if (!lastResultCache.TryGetValue(context.Mobile, out _)) { lastResultCache.Add(context.Mobile, Result.Failure); } diff --git a/Projects/UOContent/Mobiles/BehaviorAI/Core/BehaviorTree.cs b/Projects/UOContent/Mobiles/BehaviorAI/Core/BehaviorTree.cs index b8a1bb08f..b913dc5b0 100644 --- a/Projects/UOContent/Mobiles/BehaviorAI/Core/BehaviorTree.cs +++ b/Projects/UOContent/Mobiles/BehaviorAI/Core/BehaviorTree.cs @@ -9,12 +9,12 @@ namespace Server.Mobiles.BehaviorAI public class BehaviorTree { public Behavior Root { get; private set; } - private Dictionary> behaviorQueueCache; - private Dictionary executingCache; + private readonly Dictionary> _behaviorQueueCache; + private readonly Dictionary _executingCache; public BehaviorTree() { - behaviorQueueCache = new Dictionary>(); - executingCache = new Dictionary(); + _behaviorQueueCache = new Dictionary>(); + _executingCache = new Dictionary(); } public bool TryAddRoot(Composite behavior) { @@ -38,30 +38,29 @@ namespace Server.Mobiles.BehaviorAI } public virtual void Tick(BehaviorTreeContext context) { - if (!executingCache.TryGetValue(context.Mobile, out bool executing)) + if (!_executingCache.TryGetValue(context.Mobile, out bool executing)) { executing = false; - executingCache[context.Mobile] = executing; + _executingCache[context.Mobile] = executing; } if (!executing) { - executingCache[context.Mobile] = true; + _executingCache[context.Mobile] = true; Queue queue = getQueue(context); queue.Enqueue(null); while (Step(context)) { } - executingCache[context.Mobile] = false; + _executingCache[context.Mobile] = false; } } public virtual bool Step(BehaviorTreeContext context) { Queue queue = getQueue(context); - BehaviorQueueEntry current = queue.Dequeue(); - if (current == null || current.Behavior == null || current.Context == null) + if (current?.Behavior == null || current.Context == null) { return false; } @@ -70,10 +69,8 @@ namespace Server.Mobiles.BehaviorAI if (!current.Behavior.IsRunning(current.Context)) { - if (current.Observer != null) - { - current.Observer(current.Context, current.Behavior.GetResult(current.Context)); - } + current.Observer?.Invoke(current.Context, current.Behavior.GetResult(current.Context)); + current.Behavior.SetResult(context, Result.Terminated); return true; } @@ -86,20 +83,20 @@ namespace Server.Mobiles.BehaviorAI } public void Enqueue(BehaviorTreeContext context, Behavior behavior, BehaviorObserver observer) { - if (!behaviorQueueCache.TryGetValue(context.Mobile, out Queue queue)) + if (!_behaviorQueueCache.TryGetValue(context.Mobile, out Queue queue)) { queue = new Queue(); - behaviorQueueCache.Add(context.Mobile, queue); + _behaviorQueueCache.Add(context.Mobile, queue); } queue.Enqueue(new BehaviorQueueEntry(context, behavior, observer)); } private Queue getQueue(BehaviorTreeContext context) { - if (!behaviorQueueCache.TryGetValue(context.Mobile, out Queue queue)) + if (!_behaviorQueueCache.TryGetValue(context.Mobile, out Queue queue)) { queue = new Queue(); - behaviorQueueCache.Add(context.Mobile, queue); + _behaviorQueueCache.Add(context.Mobile, queue); } return queue; @@ -195,38 +192,18 @@ namespace Server.Mobiles.BehaviorAI { var random = Utility.Random(0, 32); - Direction direction; - - switch (random) + Direction direction = random switch { - case 0: - direction = Direction.Up; - break; - case 1: - direction = Direction.North; - break; - case 2: - direction = Direction.Left; - break; - case 3: - direction = Direction.West; - break; - case 5: - direction = Direction.Down; - break; - case 6: - direction = Direction.South; - break; - case 7: - direction = Direction.Right; - break; - case 8: - direction = Direction.East; - break; - default: - direction = mob.Direction; - break; - } + 0 => Direction.Up, + 1 => Direction.North, + 2 => Direction.Left, + 3 => Direction.West, + 5 => Direction.Down, + 6 => Direction.South, + 7 => Direction.Right, + 8 => Direction.East, + _ => mob.Direction + }; DoMove(context, direction, run); } @@ -327,7 +304,9 @@ namespace Server.Mobiles.BehaviorAI } if (obstacles.Count > 0) + { blocked = true; + } while (obstacles.Count > 0) { diff --git a/Projects/UOContent/Mobiles/BehaviorAI/Core/Composite.cs b/Projects/UOContent/Mobiles/BehaviorAI/Core/Composite.cs index 163816218..7965185e6 100644 --- a/Projects/UOContent/Mobiles/BehaviorAI/Core/Composite.cs +++ b/Projects/UOContent/Mobiles/BehaviorAI/Core/Composite.cs @@ -27,7 +27,7 @@ namespace Server.Mobiles.BehaviorAI currentChildCache.Add(context.Mobile, currentChild); } - if (GetResult(context) != Result.Running) + if (GetResult(context) != Result.Terminated) { currentChild = 0; currentChildCache[context.Mobile] = currentChild; diff --git a/Projects/UOContent/Mobiles/BehaviorAI/Core/Decorator.cs b/Projects/UOContent/Mobiles/BehaviorAI/Core/Decorator.cs index feb48dedf..91a36841a 100644 --- a/Projects/UOContent/Mobiles/BehaviorAI/Core/Decorator.cs +++ b/Projects/UOContent/Mobiles/BehaviorAI/Core/Decorator.cs @@ -12,19 +12,15 @@ namespace Server.Mobiles.BehaviorAI } public virtual Decorator AddChild(Behavior child) { - if (Child == null) - { - Child = child; - } + Child ??= child; return this; } public override void Tick(BehaviorTreeContext context) { - if (Child != null && GetResult(context) != Result.Running) + if (Child != null && GetResult(context) != Result.Terminated) { Tree.Enqueue(context, Child, OnChildComplete); SetResult(context, Result.Running); - return; } } public virtual void OnChildComplete(BehaviorTreeContext context, Result lastResult) diff --git a/Projects/UOContent/Mobiles/BehaviorAI/Leaf/CastSpell.cs b/Projects/UOContent/Mobiles/BehaviorAI/Leaf/CastSpell.cs index 29afb006b..5d06eee49 100644 --- a/Projects/UOContent/Mobiles/BehaviorAI/Leaf/CastSpell.cs +++ b/Projects/UOContent/Mobiles/BehaviorAI/Leaf/CastSpell.cs @@ -41,18 +41,22 @@ namespace Server.Mobiles.BehaviorAI owner.DebugSay("Casting {0}...", spell.Name); if (!string.IsNullOrEmpty(spell.Mantra)) + { owner.PublicOverheadMessage(Network.MessageType.Spell, owner.SpeechHue, false, spell.Mantra, false); + } SetResult(context, Result.Running); return; } - else if (owner.Spell.IsCasting) + + if (owner.Spell.IsCasting) { SetResult(context, Result.Running); owner.DebugSay("Already casting {0}...", ((Spell)owner.Spell).Name); return; } - else if (!owner.Spell.IsCasting && ((Spell)owner.Spell).State == SpellState.Sequencing) + + if (!owner.Spell.IsCasting && ((Spell)owner.Spell).State == SpellState.Sequencing) { SetResult(context, Result.Success); owner.DebugSay("Finished casting {0}...", ((Spell)owner.Spell).Name);