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 d6d735d57..99881600b 100644 --- a/Projects/UOContent/Mobiles/AI/BaseAI.cs +++ b/Projects/UOContent/Mobiles/AI/BaseAI.cs @@ -26,7 +26,9 @@ namespace Server.Mobiles AI_Mage, AI_Berserk, AI_Predator, - AI_Thief + AI_Thief, + AI_BehaviorTree, + AI_FireBoss } public enum ActionType @@ -2010,7 +2012,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; @@ -2436,7 +2438,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; @@ -3069,6 +3071,7 @@ namespace Server.Mobiles } } + /* if (m_Owner.CanDetectHidden && Core.TickCount - m_Owner.m_NextDetectHidden >= 0) { m_Owner.DetectHidden(); @@ -3082,6 +3085,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..123fd5f60 --- /dev/null +++ b/Projects/UOContent/Mobiles/AI/BehaviorTreeAI.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +// 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 readonly BehaviorTree behaviorTree; + private readonly 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 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); + } + + public override bool Think() + { + if (m_Mobile.Deleted) + { + return false; + } + + // MageBehaviorTree.Instance.RunBehavior(m_Mobile, blackboard); + + behaviorTree.Tick(behaviorTreeContext); + + 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/AI/MageAI.cs b/Projects/UOContent/Mobiles/AI/MageAI.cs index a27240072..159a99d28 100644 --- a/Projects/UOContent/Mobiles/AI/MageAI.cs +++ b/Projects/UOContent/Mobiles/AI/MageAI.cs @@ -101,12 +101,14 @@ namespace Server.Mobiles base.DoActionWander(); + /* if (Utility.RandomDouble() < 0.05) { var spell = CheckCastHealingSpell(); spell?.Cast(); } + */ } return true; @@ -115,10 +117,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) @@ -151,6 +155,7 @@ namespace Server.Mobiles Spell spell = null; + /* if (m_Mobile.Hits < m_Mobile.HitsMax - 50) { if (UseNecromancy()) @@ -166,6 +171,7 @@ namespace Server.Mobiles { spell = new HealSpell(m_Mobile); } + */ double delay; @@ -765,6 +771,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"); @@ -772,6 +779,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); @@ -891,10 +900,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/BT/Actions/CastSpellActionNode.cs b/Projects/UOContent/Mobiles/BT/Actions/CastSpellActionNode.cs new file mode 100644 index 000000000..831491cbd --- /dev/null +++ b/Projects/UOContent/Mobiles/BT/Actions/CastSpellActionNode.cs @@ -0,0 +1,65 @@ +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; + } + + if (mob.Spell.IsCasting) + { + mob.DebugSay("Still casting..."); + return Result.Running; + } + + 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..f55ddb194 --- /dev/null +++ b/Projects/UOContent/Mobiles/BT/Actions/MageComboNode.cs @@ -0,0 +1,144 @@ +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..f655226eb --- /dev/null +++ b/Projects/UOContent/Mobiles/BT/BehaviorMageCombatNodeSet.cs @@ -0,0 +1,34 @@ +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, 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))) + */ + ); + } + + 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/BT/Core/LeafNode.cs b/Projects/UOContent/Mobiles/BT/Core/LeafNode.cs new file mode 100644 index 000000000..b8c1f2866 --- /dev/null +++ b/Projects/UOContent/Mobiles/BT/Core/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.BT +{ + public abstract class LeafNode : BehaviorTreeNode + { + public LeafNode(BehaviorTree tree) : base(tree) + { + } + } +} 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/BT/Decorator/UntilFailNode.cs b/Projects/UOContent/Mobiles/BT/Decorator/UntilFailNode.cs new file mode 100644 index 000000000..e5cd471c2 --- /dev/null +++ b/Projects/UOContent/Mobiles/BT/Decorator/UntilFailNode.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 UntilFailNode : DecoratorNode + { + public UntilFailNode(BehaviorTree tree) : base(tree) + { + } + public UntilFailNode(BehaviorTree tree, BehaviorTreeNode child) : base(tree, child) + { + } + + public override Result Execute(BaseCreature mob, Blackboard blackboard) + { + if (Child != null) + { + if(Child.Execute(mob, blackboard) == Result.Failure) + { + return Result.Failure; + } + return Result.Running; + } + return Result.Failure; + } + } +} diff --git a/Projects/UOContent/Mobiles/BT/Leaf/ActionNode.cs b/Projects/UOContent/Mobiles/BT/Leaf/ActionNode.cs new file mode 100644 index 000000000..41d59d46d --- /dev/null +++ b/Projects/UOContent/Mobiles/BT/Leaf/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.BT +{ + public abstract class ActionNode : LeafNode + { + public ActionNode(BehaviorTree tree) : base(tree) + { + } + } +} 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 d5127a6ab..37dd2cb0f 100644 --- a/Projects/UOContent/Mobiles/BaseCreature.cs +++ b/Projects/UOContent/Mobiles/BaseCreature.cs @@ -2132,6 +2132,8 @@ namespace Server.Mobiles // m_AI = new PredatorAI(this); 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/BehaviorAI/Composite/Selector.cs b/Projects/UOContent/Mobiles/BehaviorAI/Composite/Selector.cs new file mode 100644 index 000000000..3d5f253fb --- /dev/null +++ b/Projects/UOContent/Mobiles/BehaviorAI/Composite/Selector.cs @@ -0,0 +1,36 @@ +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); + } + + currentChild++; + if (lastResult == Result.Success) + { + currentChildCache[context.Mobile] = 0; + SetResult(context, Result.Success); + return; + } + + if (currentChild >= Children.Count) + { + currentChildCache[context.Mobile] = 0; + SetResult(context, Result.Failure); + return; + } + + 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 new file mode 100644 index 000000000..a1bc7473c --- /dev/null +++ b/Projects/UOContent/Mobiles/BehaviorAI/Composite/Sequence.cs @@ -0,0 +1,36 @@ +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); + } + + currentChild++; + if (lastResult == Result.Failure) + { + currentChildCache[context.Mobile] = 0; + SetResult(context, Result.Failure); + return; + } + + if (currentChild >= Children.Count) + { + currentChildCache[context.Mobile] = 0; + SetResult(context, Result.Success); + return; + } + + 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 new file mode 100644 index 000000000..4e534fcdf --- /dev/null +++ b/Projects/UOContent/Mobiles/BehaviorAI/Core/Behavior.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; + +namespace Server.Mobiles.BehaviorAI +{ + public delegate void BehaviorObserver(BehaviorTreeContext context, Result lastResult); + public enum Result + { + Success, + Failure, + Running, + Terminated + } + public abstract class Behavior + { + public BehaviorTree Tree { get; protected set; } + public BehaviorObserver Observer { get; protected set; } + private readonly 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.Terminated; + lastResultCache.Add(context.Mobile, result); + } + + return result; + } + public void SetResult(BehaviorTreeContext context, Result result) + { + if (!lastResultCache.TryGetValue(context.Mobile, out _)) + { + 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..90a352e59 --- /dev/null +++ b/Projects/UOContent/Mobiles/BehaviorAI/Core/BehaviorQueueEntry.cs @@ -0,0 +1,16 @@ + +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..b913dc5b0 --- /dev/null +++ b/Projects/UOContent/Mobiles/BehaviorAI/Core/BehaviorTree.cs @@ -0,0 +1,418 @@ +using System.Collections.Generic; +using System.Linq; +using Server.Items; +using Server.Engines.Spawners; +using MoveImpl = Server.Movement.MovementImpl; + +namespace Server.Mobiles.BehaviorAI +{ + public class BehaviorTree + { + public Behavior Root { get; private set; } + private readonly Dictionary> _behaviorQueueCache; + private readonly 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?.Behavior == null || current.Context == null) + { + return false; + } + + current.Behavior.Tick(current.Context); + + if (!current.Behavior.IsRunning(current.Context)) + { + current.Observer?.Invoke(current.Context, current.Behavior.GetResult(current.Context)); + current.Behavior.SetResult(context, Result.Terminated); + return true; + } + + queue.Enqueue(current); + + return true; + } + 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; + } + 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 = random switch + { + 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); + } + } + 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 new file mode 100644 index 000000000..fd85cf512 --- /dev/null +++ b/Projects/UOContent/Mobiles/BehaviorAI/Core/BehaviorTreeContext.cs @@ -0,0 +1,13 @@ +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..567da3600 --- /dev/null +++ b/Projects/UOContent/Mobiles/BehaviorAI/Core/Blackboard.cs @@ -0,0 +1,8 @@ +using System.Collections.Generic; + +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..7965185e6 --- /dev/null +++ b/Projects/UOContent/Mobiles/BehaviorAI/Core/Composite.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; + +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.Terminated) + { + currentChild = 0; + currentChildCache[context.Mobile] = currentChild; + SetResult(context, Result.Running); + 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..91a36841a --- /dev/null +++ b/Projects/UOContent/Mobiles/BehaviorAI/Core/Decorator.cs @@ -0,0 +1,31 @@ +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 Decorator AddChild(Behavior child) + { + Child ??= child; + return this; + } + public override void Tick(BehaviorTreeContext context) + { + if (Child != null && GetResult(context) != Result.Terminated) + { + Tree.Enqueue(context, Child, OnChildComplete); + SetResult(context, Result.Running); + } + } + public virtual void OnChildComplete(BehaviorTreeContext context, Result lastResult) + { + SetResult(context, lastResult); + } + } +} diff --git a/Projects/UOContent/Mobiles/BehaviorAI/Decorator/ConditionalLoop.cs b/Projects/UOContent/Mobiles/BehaviorAI/Decorator/ConditionalLoop.cs new file mode 100644 index 000000000..24fa6e9ad --- /dev/null +++ b/Projects/UOContent/Mobiles/BehaviorAI/Decorator/ConditionalLoop.cs @@ -0,0 +1,35 @@ +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..9025210aa --- /dev/null +++ b/Projects/UOContent/Mobiles/BehaviorAI/Decorator/Cooldown.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; + +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..f4ad8b733 --- /dev/null +++ b/Projects/UOContent/Mobiles/BehaviorAI/Decorator/ForceSuccess.cs @@ -0,0 +1,17 @@ +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..478c64bb6 --- /dev/null +++ b/Projects/UOContent/Mobiles/BehaviorAI/Decorator/Inverter.cs @@ -0,0 +1,23 @@ +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..304cbf452 --- /dev/null +++ b/Projects/UOContent/Mobiles/BehaviorAI/Decorator/Loop.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; + +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..5f5b9cd37 --- /dev/null +++ b/Projects/UOContent/Mobiles/BehaviorAI/Decorator/OwnerCondition.cs @@ -0,0 +1,24 @@ +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); + } + SetResult(context, Result.Failure); + } + } +} diff --git a/Projects/UOContent/Mobiles/BehaviorAI/Decorator/UntilFail.cs b/Projects/UOContent/Mobiles/BehaviorAI/Decorator/UntilFail.cs new file mode 100644 index 000000000..b89a725f9 --- /dev/null +++ b/Projects/UOContent/Mobiles/BehaviorAI/Decorator/UntilFail.cs @@ -0,0 +1,24 @@ +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..515aca0ef --- /dev/null +++ b/Projects/UOContent/Mobiles/BehaviorAI/Leaf/AutoTarget.cs @@ -0,0 +1,51 @@ +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/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 new file mode 100644 index 000000000..5d06eee49 --- /dev/null +++ b/Projects/UOContent/Mobiles/BehaviorAI/Leaf/CastSpell.cs @@ -0,0 +1,69 @@ +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.Failure); + 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; + } + + if (owner.Spell.IsCasting) + { + SetResult(context, Result.Running); + owner.DebugSay("Already casting {0}...", ((Spell)owner.Spell).Name); + return; + } + + if (!owner.Spell.IsCasting && ((Spell)owner.Spell).State == SpellState.Sequencing) + { + 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..faaba5283 --- /dev/null +++ b/Projects/UOContent/Mobiles/BehaviorAI/Leaf/Condition.cs @@ -0,0 +1,23 @@ +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..f640db275 --- /dev/null +++ b/Projects/UOContent/Mobiles/BehaviorAI/Leaf/DynamicTarget.cs @@ -0,0 +1,41 @@ +using Server.Targeting; + +namespace Server.Mobiles.BehaviorAI +{ + public delegate object DynamicTargetCallback(BehaviorTreeContext context); + public class DynamicTarget : Behavior + { + public DynamicTargetCallback Callback { get; private set; } + public DynamicTarget(BehaviorTree tree, DynamicTargetCallback transformation) : base(tree) + { + Callback = transformation; + } + public override void Tick(BehaviorTreeContext context) + { + if (Callback == null) + { + SetResult(context, Result.Failure); + return; + } + + Target target = context.Mobile.Target; + + if (target == null) + { + SetResult(context, Result.Failure); + return; + } + + object targeted = Callback(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..8bb3f1c5a --- /dev/null +++ b/Projects/UOContent/Mobiles/BehaviorAI/Leaf/Tap.cs @@ -0,0 +1,17 @@ +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..87f767d79 --- /dev/null +++ b/Projects/UOContent/Mobiles/BehaviorAI/Leaf/WaitForTarget.cs @@ -0,0 +1,18 @@ +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/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 new file mode 100644 index 000000000..9236f4d82 --- /dev/null +++ b/Projects/UOContent/Mobiles/BehaviorMage.cs @@ -0,0 +1,94 @@ +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, 1) + { + // Debug = true; + + 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); + SetSkill(SkillName.Wrestling, 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/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 02fcbeb7d..1d99a5455 100644 --- a/Projects/UOContent/Mobiles/Healers/EvilHealer.cs +++ b/Projects/UOContent/Mobiles/Healers/EvilHealer.cs @@ -5,6 +5,8 @@ namespace Server.Mobiles [Constructible] public EvilHealer() { + AI = AIType.AI_BehaviorTree; + Title = "the healer"; Karma = -10000; 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 c7631a7ee..c8d65be66 100644 --- a/Projects/UOContent/Spells/Base/Spell.cs +++ b/Projects/UOContent/Spells/Base/Spell.cs @@ -85,7 +85,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)) @@ -410,7 +410,7 @@ namespace Server.Spells _castTimer?.Stop(); _animTimer?.Stop(); - if (Core.AOS && Caster.Player && type == DisturbType.Hurt) + if (Core.AOS && (Caster.Player || Caster is BehaviorMage) && type == DisturbType.Hurt) { DoHurtFizzle(); } @@ -423,7 +423,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(); }