Compare commits

...
Sign in to create a new pull request.

8 commits

Author SHA1 Message Date
Kamron Batman
62c7acf5d3
Merge branch 'main' into behavior-tree 2022-06-19 10:32:42 -07:00
Kamron Batman
bb2999d3ab
WIP-broken 2021-10-16 19:22:19 -07:00
Kamron Batman
380f40890c
Merge branch 'main' into behavior-tree
# Conflicts:
#	Projects/UOContent/UOContent.csproj
2021-10-16 12:16:18 -07:00
Leath Cooper
e0874713d1 more event driven behavior tree 2021-09-12 23:38:56 -04:00
Kamron Batman
391c49553a
Merge branch 'main' into behavior-tree 2021-09-11 17:16:33 -07:00
Leath Cooper
3f309edffb asdfa 2021-09-11 20:12:32 -04:00
Leath Cooper
0e84739f2d tree stuff 2021-09-10 20:58:02 -04:00
Leath Cooper
53f51d6bde behavior tree initial commit 2021-09-01 01:07:24 -04:00
75 changed files with 3251 additions and 8 deletions

View file

@ -1,7 +1,6 @@
namespace Server namespace Server
{ {
public delegate MoveResult MoveMethod(Direction d); public delegate MoveResult MoveMethod(Direction d);
public enum MoveResult public enum MoveResult
{ {
BadState, BadState,

View file

@ -26,7 +26,9 @@ namespace Server.Mobiles
AI_Mage, AI_Mage,
AI_Berserk, AI_Berserk,
AI_Predator, AI_Predator,
AI_Thief AI_Thief,
AI_BehaviorTree,
AI_FireBoss
} }
public enum ActionType public enum ActionType
@ -2010,7 +2012,7 @@ namespace Server.Mobiles
if (canOpenDoors || canDestroyObstacles) 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; var map = m_Mobile.Map;
@ -2436,7 +2438,7 @@ namespace Server.Mobiles
m_Mobile.NextReacquireTime = Core.TickCount + (int)m_Mobile.ReacquireDelay.TotalMilliseconds; m_Mobile.NextReacquireTime = Core.TickCount + (int)m_Mobile.ReacquireDelay.TotalMilliseconds;
m_Mobile.DebugSay("Acquiring..."); // m_Mobile.DebugSay("Acquiring...");
var map = m_Mobile.Map; var map = m_Mobile.Map;
@ -3069,6 +3071,7 @@ namespace Server.Mobiles
} }
} }
/*
if (m_Owner.CanDetectHidden && Core.TickCount - m_Owner.m_NextDetectHidden >= 0) if (m_Owner.CanDetectHidden && Core.TickCount - m_Owner.m_NextDetectHidden >= 0)
{ {
m_Owner.DetectHidden(); m_Owner.DetectHidden();
@ -3082,6 +3085,7 @@ namespace Server.Mobiles
m_Owner.m_NextDetectHidden = Core.TickCount + m_Owner.m_NextDetectHidden = Core.TickCount +
(int)TimeSpan.FromSeconds(Utility.RandomMinMax(min, max)).TotalMilliseconds; (int)TimeSpan.FromSeconds(Utility.RandomMinMax(min, max)).TotalMilliseconds;
} }
*/
} }
} }
} }

View file

@ -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;
}
}
}

View file

@ -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();
}
}
}

View file

@ -101,12 +101,14 @@ namespace Server.Mobiles
base.DoActionWander(); base.DoActionWander();
/*
if (Utility.RandomDouble() < 0.05) if (Utility.RandomDouble() < 0.05)
{ {
var spell = CheckCastHealingSpell(); var spell = CheckCastHealingSpell();
spell?.Cast(); spell?.Cast();
} }
*/
} }
return true; return true;
@ -115,10 +117,12 @@ namespace Server.Mobiles
private Spell CheckCastHealingSpell() private Spell CheckCastHealingSpell()
{ {
// If I'm poisoned, always attempt to cure. // If I'm poisoned, always attempt to cure.
/*
if (m_Mobile.Poisoned) if (m_Mobile.Poisoned)
{ {
return new CureSpell(m_Mobile); return new CureSpell(m_Mobile);
} }
*/
// Summoned creatures never heal themselves. // Summoned creatures never heal themselves.
if (m_Mobile.Summoned) if (m_Mobile.Summoned)
@ -151,6 +155,7 @@ namespace Server.Mobiles
Spell spell = null; Spell spell = null;
/*
if (m_Mobile.Hits < m_Mobile.HitsMax - 50) if (m_Mobile.Hits < m_Mobile.HitsMax - 50)
{ {
if (UseNecromancy()) if (UseNecromancy())
@ -166,6 +171,7 @@ namespace Server.Mobiles
{ {
spell = new HealSpell(m_Mobile); spell = new HealSpell(m_Mobile);
} }
*/
double delay; double delay;
@ -765,6 +771,7 @@ namespace Server.Mobiles
Spell spell; Spell spell;
var toDispel = FindDispelTarget(true); var toDispel = FindDispelTarget(true);
/*
if (m_Mobile.Poisoned) // Top cast priority is cure if (m_Mobile.Poisoned) // Top cast priority is cure
{ {
m_Mobile.DebugSay("I am going to cure myself"); m_Mobile.DebugSay("I am going to cure myself");
@ -772,6 +779,8 @@ namespace Server.Mobiles
spell = new CureSpell(m_Mobile); spell = new CureSpell(m_Mobile);
} }
else if (toDispel != null) // Something dispellable is attacking us else if (toDispel != null) // Something dispellable is attacking us
*/
if (toDispel != null)
{ {
m_Mobile.DebugSay("I am going to dispel {0}", toDispel); m_Mobile.DebugSay("I am going to dispel {0}", toDispel);
@ -891,10 +900,12 @@ namespace Server.Mobiles
RunFrom(m_Mobile.FocusMob); RunFrom(m_Mobile.FocusMob);
m_Mobile.FocusMob = null; m_Mobile.FocusMob = null;
/*
if (m_Mobile.Poisoned && Utility.Random(0, 5) == 0) if (m_Mobile.Poisoned && Utility.Random(0, 5) == 0)
{ {
new CureSpell(m_Mobile).Cast(); new CureSpell(m_Mobile).Cast();
} }
*/
} }
else else
{ {

View file

@ -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;
}
}
}

View file

@ -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))
)
);
}
}
}

View file

@ -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;
}
}
}

View file

@ -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);
}
}
}

View file

@ -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;
}
}
}

View file

@ -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);
}
}
}

View file

@ -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))
)
);
}
}
}

View file

@ -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))))
);
}
}
}

View file

@ -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;
}
}
}

View file

@ -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<T> : 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;
}
}
}

View file

@ -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;
}
}
}

View file

@ -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;
}
}
}

View file

@ -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;
}
}
}

View file

@ -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);
}
}
}

View file

@ -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;
}
}
}

View file

@ -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<BaseCreature, int> retryCache = new Dictionary<BaseCreature, int>();
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;
}
}
}

View file

@ -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;
}
}
}

View file

@ -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;
}
}
}

View file

@ -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<BaseCreature, BehaviorTreeNode> Claims = new Dictionary<BaseCreature, BehaviorTreeNode>();
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<Item> obstacles = new List<Item>();
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;
}
}
}

View file

@ -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;
}
}
}

View file

@ -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<string, object>
{
}
}

View file

@ -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<BaseCreature, int> currentTaskCache;
public List<BehaviorTreeNode> Children { get; protected set; }
public CompositeNode(BehaviorTree tree) : base(tree)
{
currentTaskCache = new Dictionary<BaseCreature, int>();
Children = new List<BehaviorTreeNode>();
}
public virtual CompositeNode AddChild(BehaviorTreeNode child)
{
child.Parent = this;
Children.Add(child);
return this;
}
}
}

View file

@ -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;
}
}
}

View file

@ -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)
{
}
}
}

View file

@ -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;
}
}
}

View file

@ -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;
}
}
}

View file

@ -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<BaseCreature, DateTime> cooldowns;
public CooldownNode(BehaviorTree tree, TimeSpan duration) : base(tree)
{
cooldownDuration = duration;
cooldowns = new Dictionary<BaseCreature, DateTime>();
}
public CooldownNode(BehaviorTree tree, TimeSpan duration, BehaviorTreeNode child) : base(tree, child)
{
cooldownDuration = duration;
cooldowns = new Dictionary<BaseCreature, DateTime>();
}
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;
}
}
}

View file

@ -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;
}
}
}

View file

@ -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;
}
}
}

View file

@ -0,0 +1,42 @@
using System.Collections.Generic;
namespace Server.Mobiles.BT
{
public class LoopNode : DecoratorNode
{
private int count;
private Dictionary<BaseCreature, int> currentCountCache;
public LoopNode(BehaviorTree tree, int n) : base(tree)
{
count = n;
currentCountCache = new Dictionary<BaseCreature, int>();
}
public LoopNode(BehaviorTree tree, int n, BehaviorTreeNode child) : base(tree, child)
{
count = n;
currentCountCache = new Dictionary<BaseCreature, int>();
}
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;
}
}
}

View file

@ -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;
}
}
}

View file

@ -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<BaseCreature, bool> executingCache = new Dictionary<BaseCreature, bool>();
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;
}
}
}

View file

@ -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;
}
}
}

View file

@ -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;
}
}
}

View file

@ -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)
{
}
}
}

View file

@ -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;
}
}
}

View file

@ -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))
);
}
}
}

View file

@ -2132,6 +2132,8 @@ namespace Server.Mobiles
// m_AI = new PredatorAI(this); // m_AI = new PredatorAI(this);
new MeleeAI(this), new MeleeAI(this),
AIType.AI_Thief => new ThiefAI(this), AIType.AI_Thief => new ThiefAI(this),
AIType.AI_BehaviorTree => new AI.BehaviorTreeAI(this),
AIType.AI_FireBoss => new AI.FireBossAI(this),
_ => null _ => null
}; };
} }

View file

@ -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);
}
}
}

View file

@ -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);
}
}
}

View file

@ -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<BaseCreature, Result> lastResultCache;
public virtual bool IsRunning(BehaviorTreeContext context) => GetResult(context) == Result.Running;
public Behavior(BehaviorTree tree)
{
Tree = tree;
lastResultCache = new Dictionary<BaseCreature, Result>();
}
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;
}
}
}

View file

@ -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;
}
}
}

View file

@ -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<BaseCreature, Queue<BehaviorQueueEntry>> _behaviorQueueCache;
private readonly Dictionary<BaseCreature, bool> _executingCache;
public BehaviorTree()
{
_behaviorQueueCache = new Dictionary<BaseCreature, Queue<BehaviorQueueEntry>>();
_executingCache = new Dictionary<BaseCreature, bool>();
}
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<BehaviorQueueEntry> queue = getQueue(context);
queue.Enqueue(null);
while (Step(context))
{
}
_executingCache[context.Mobile] = false;
}
}
public virtual bool Step(BehaviorTreeContext context)
{
Queue<BehaviorQueueEntry> 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<BehaviorQueueEntry> queue))
{
queue = new Queue<BehaviorQueueEntry>();
_behaviorQueueCache.Add(context.Mobile, queue);
}
queue.Enqueue(new BehaviorQueueEntry(context, behavior, observer));
}
private Queue<BehaviorQueueEntry> getQueue(BehaviorTreeContext context)
{
if (!_behaviorQueueCache.TryGetValue(context.Mobile, out Queue<BehaviorQueueEntry> queue))
{
queue = new Queue<BehaviorQueueEntry>();
_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<Item> obstacles = new List<Item>();
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;
}
}
}

View file

@ -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;
}
}
}

View file

@ -0,0 +1,8 @@
using System.Collections.Generic;
namespace Server.Mobiles.BehaviorAI
{
public class Blackboard : Dictionary<string, object>
{
}
}

View file

@ -0,0 +1,43 @@
using System.Collections.Generic;
namespace Server.Mobiles.BehaviorAI
{
public class Composite : Behavior
{
public List<Behavior> Children { get; private set; }
protected Dictionary<BaseCreature, int> currentChildCache;
public Composite(BehaviorTree tree) : base(tree)
{
Children = new List<Behavior>();
currentChildCache = new Dictionary<BaseCreature, int>();
}
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);
}
}
}

View file

@ -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);
}
}
}

View file

@ -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);
}
}
}

View file

@ -0,0 +1,54 @@
using System;
using System.Collections.Generic;
namespace Server.Mobiles.BehaviorAI
{
public class Cooldown : Decorator
{
public TimeSpan Duration { get; }
private Dictionary<BaseCreature, long> nextCooldownCache;
public Cooldown(BehaviorTree tree, TimeSpan duration) : base(tree)
{
nextCooldownCache = new Dictionary<BaseCreature, long>();
Duration = duration;
}
public Cooldown(BehaviorTree tree, TimeSpan duration, Behavior child) : base(tree, child)
{
nextCooldownCache = new Dictionary<BaseCreature, long>();
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);
}
}
}

View file

@ -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);
}
}
}

View file

@ -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);
}
}
}
}

View file

@ -0,0 +1,53 @@
using System.Collections.Generic;
namespace Server.Mobiles.BehaviorAI
{
public class Loop : Decorator
{
private Dictionary<BaseCreature, int> currentIterationCache;
private int totalIterations;
public Loop(BehaviorTree tree, int n) : base(tree)
{
currentIterationCache = new Dictionary<BaseCreature, int>();
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);
}
}
}

View file

@ -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);
}
}
}

View file

@ -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);
}
}
}
}

View file

@ -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);
}
}
}

View file

@ -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);
}
}
}

View file

@ -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);
}
}
}

View file

@ -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);
}
}
}
}

View file

@ -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);
}
}
}

View file

@ -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);
}
}
}

View file

@ -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);
}
}
}

View file

@ -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);
}
}
}

View file

@ -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;
}
}
}

View file

@ -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;
}
}
}

View file

@ -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);
}
}
}

View file

@ -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();
}
}
}

View file

@ -146,7 +146,7 @@ namespace Server.Mobiles
if (!IsInvulnerable) if (!IsInvulnerable)
{ {
AI = AIType.AI_Mage; AI = AIType.AI_BehaviorTree;
ActiveSpeed = 0.2; ActiveSpeed = 0.2;
PassiveSpeed = 0.8; PassiveSpeed = 0.8;
RangePerception = DefaultRangePerception; RangePerception = DefaultRangePerception;

View file

@ -5,6 +5,8 @@ namespace Server.Mobiles
[Constructible] [Constructible]
public EvilHealer() public EvilHealer()
{ {
AI = AIType.AI_BehaviorTree;
Title = "the healer"; Title = "the healer";
Karma = -10000; Karma = -10000;

View file

@ -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<FireBoss>();
}
}
}

View file

@ -85,7 +85,7 @@ namespace Server.Spells
public virtual void OnCasterHurt() public virtual void OnCasterHurt()
{ {
// Confirm: Monsters and pets cannot be disturbed. // 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); var hasProtection = ProtectionSpell.Registry.TryGetValue(Caster, out var d);
if (!hasProtection || d < 1000 && d < Utility.Random(1000)) if (!hasProtection || d < 1000 && d < Utility.Random(1000))
@ -410,7 +410,7 @@ namespace Server.Spells
_castTimer?.Stop(); _castTimer?.Stop();
_animTimer?.Stop(); _animTimer?.Stop();
if (Core.AOS && Caster.Player && type == DisturbType.Hurt) if (Core.AOS && (Caster.Player || Caster is BehaviorMage) && type == DisturbType.Hurt)
{ {
DoHurtFizzle(); DoHurtFizzle();
} }
@ -423,7 +423,7 @@ namespace Server.Spells
Target.Cancel(Caster); Target.Cancel(Caster);
if (Core.AOS && Caster.Player && type == DisturbType.Hurt) if (Core.AOS && (Caster.Player || Caster is BehaviorMage) && type == DisturbType.Hurt)
{ {
DoHurtFizzle(); DoHurtFizzle();
} }