// Behavior originally contributed by Chinajade.
//
// LICENSE:
// This work is licensed under the
// Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License.
// also known as CC-BY-NC-SA. To view a copy of this license, visit
// http://creativecommons.org/licenses/by-nc-sa/3.0/
// or send a letter to
// Creative Commons // 171 Second Street, Suite 300 // San Francisco, California, 94105, USA.
//
// DOCUMENTATION:
// http://www.thebuddyforum.com/mediawiki/index.php?title=Honorbuddy_Custom_Behavior:_CollectThings
//
// QUICK DOX:
// Collects items from mobs or objects when (right-click) 'interaction' is required.
// Most useful for those type of quests where you blow something up,
// then you have to collect the pieces.
//
// Parameters (required, then optional--both listed alphabetically):
// (***One or more of the following two attributes must be specified***)
// MobIdN [REQUIRED if ObjectId is omitted]: Defines the mobs that drop the Items we're after.
// N may be omitted, or any numeric value--multiple mobs are supported.
// ObjectIdN [REQUIRED if MobId is omitted]: Defines the objects that drop the Items we're after.
// N may be omitted, or any numeric value--mulitple objects are supported.
//
// (This attribute is optional, but governs what other attributes are optional)
// CollectUntil [Default: RequiredCountReached]: Defines the terminating condition for
// this behavior. Available options include: NoTargetsInArea, RequiredCountReached, QuestComplete.
// "Targets" means mobs or objects--whatever is dropping the items we're after.
//
// (***These attributes may/may not be optional based on value of CollectUntil attribute***)
// CollectItemCount [REQUIRED if CollectUntil=RequiredCountReached; Default: 1]:
// represents the number of items we must collect for the behavior to terminate.
// CollectItemId [REQUIRED if CollectUntil=NoTargetsInArea or RequiredCountReached; Default:none]:
// Identifies the item we are collecting. The only time this attribute may be omitted
// is when we're collecting intangibles such as 'attitudes' or 'liberations' that
// will complete the quest.
// QuestId [REQUIRED if CollectUntil=QuestComplete; Default:none]:
//
// (***These attibutes are completely optional***)
// HuntingGroundRadius [Default: 120]: The range from the anchor location (i.e., X/Y/Z) location at which
// targets (mobs or objects) will be sought.
// IgnoreMobsInBlackspots [Default: false]: If true, mobs sitting in blackspotted areas will not be
// considered as targets.
// MobState [Default: DontCare]: Identifies the state in which the Mob must be to be considered
// as a target. The MobState only applies if the target is some form of NPC. The MobState
// Valid values are Alive/Dead/DontCare.
// NonCompeteDistance [Default: 25]: If a player is within this distance of a target that looks
// interesting to us, we'll ignore the target. The assumption is that the player may
// be going for the same target, and we don't want to draw attention.
// PostInteractDelay [Default: 1500ms]: The number of milliseconds to wait after each interaction.
// This is useful if the target requires time for the interaction to complete.
// This value must be on the closed interval [0..61000].
// QuestCompleteRequirement [Default:NotComplete]:
// QuestInLogRequirement [Default:InLog]:
// A full discussion of how the Quest* attributes operate is described in
// http://www.thebuddyforum.com/mediawiki/index.php?title=Honorbuddy_Programming_Cookbook:_QuestId_for_Custom_Behaviors
// X/Y/Z [Default: Toon's initial position]: Defines the anchor of a search area for
// which targets (mobs or objects) will be sought. The hunting ground is defined by
// this value coupled with the CollectionDistance.
//
// Exmaples:
//
//
//
//
//
//
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Linq;
using System.Xml;
using System.Xml.Linq;
using CommonBehaviors.Actions;
using Styx;
using Styx.Combat.CombatRoutine;
using Styx.Helpers;
using Styx.Logic;
using Styx.Logic.BehaviorTree;
using Styx.Logic.Combat;
using Styx.Logic.Inventory.Frames.LootFrame;
using Styx.Logic.Pathing;
using Styx.Logic.Profiles;
using Styx.Logic.Questing;
using Styx.Plugins;
using Styx.WoWInternals;
using Styx.WoWInternals.World;
using Styx.WoWInternals.WoWObjects;
using TreeSharp;
using Action = TreeSharp.Action;
namespace BuddyWiki.CustomBehavior.CollectThings
{
public class CollectThings : CustomForcedBehavior
{
public enum CollectUntilType
{
NoTargetsInArea,
RequiredCountReached,
QuestComplete,
}
public enum MobStateType
{
Alive,
Dead,
DontCare,
}
public CollectThings(Dictionary args)
: base(args)
{
try
{
bool isCollectItemCountRequired = false;
bool isCollectItemIdRequired = false;
bool isQuestIdRequired = false;
CollectUntil = GetAttributeAsNullable("CollectUntil", false, null, null) ?? CollectUntilType.RequiredCountReached;
if ((CollectUntil == CollectUntilType.NoTargetsInArea)
|| (CollectUntil == CollectUntilType.RequiredCountReached))
{
isCollectItemCountRequired = true;
isCollectItemIdRequired = true;
}
else if (CollectUntil == CollectUntilType.QuestComplete)
{ isQuestIdRequired = true; }
CollectItemCount = GetAttributeAsNullable("CollectItemCount", isCollectItemCountRequired, ConstrainAs.CollectionCount, null) ?? 1;
CollectItemId = GetAttributeAsNullable("CollectItemId", isCollectItemIdRequired, ConstrainAs.ItemId, null) ?? 0;
HuntingGroundAnchor = GetAttributeAsNullable("", false, ConstrainAs.WoWPointNonEmpty, null) ?? Me.Location;
HuntingGroundRadius = GetAttributeAsNullable("HuntingGroundRadius", false, new ConstrainTo.Domain(1.0, 200.0), new [] {"CollectionDistance"}) ?? 120.0;
IgnoreMobsInBlackspots = GetAttributeAsNullable("IgnoreMobsInBlackspots", false, null, null) ?? false;
MobIds = GetNumberedAttributesAsArray("MobId", 0, ConstrainAs.MobId, null);
MobState = GetAttributeAsNullable("MobState", false, null, null) ?? MobStateType.DontCare;
NonCompeteDistance = GetAttributeAsNullable("NonCompeteDistance", false, new ConstrainTo.Domain(1.0, 150.0), null) ?? 25.0;
ObjectIds = GetNumberedAttributesAsArray("ObjectId", 0, ConstrainAs.ObjectId, null);
PostInteractDelay = TimeSpan.FromMilliseconds(GetAttributeAsNullable("PostInteractDelay", false, new ConstrainTo.Domain(0, 61000), null) ?? 1500);
RandomizeStartingHotspot= GetAttributeAsNullable("RandomizeStartingHotspot", false, null, null) ?? false;
QuestId = GetAttributeAsNullable("QuestId", isQuestIdRequired, ConstrainAs.QuestId(this), null) ?? 0;
QuestRequirementComplete = GetAttributeAsNullable("QuestCompleteRequirement", false, null, null) ?? QuestCompleteRequirement.NotComplete;
QuestRequirementInLog = GetAttributeAsNullable("QuestInLogRequirement", false, null, null) ?? QuestInLogRequirement.InLog;
// Semantic coherency --
if ((MobIds.Count() <= 0) && (ObjectIds.Count() <= 0))
{
LogMessage("error", "You must specify one or more MobId(s) or ObjectId(s)");
IsAttributeProblem = true;
}
if (HuntingGroundRadius < (NonCompeteDistance * 2))
{
LogMessage("error", "The CollectionDistance (saw '{0}') must be at least twice the size"
+ " of the NonCompeteDistance (saw '{1}').",
HuntingGroundRadius,
NonCompeteDistance);
IsAttributeProblem = true;
}
// Find the item name --
ItemInfo itemInfo = ItemInfo.FromId((uint)CollectItemId);
ItemName = (itemInfo != null) ? itemInfo.Name : string.Format("Item({0})", CollectItemId);
// Sub-behaviors...
_behavior_SwimBreath = new SwimBreathBehavior((messageType, format, argObjects) => LogMessage(messageType, format, argObjects));
_behavior_HuntingGround = new HuntingGroundBehavior((messageType, format, argObjects) => LogMessage(messageType, format, argObjects),
IsViableTarget,
HuntingGroundAnchor,
HuntingGroundRadius);
_behavior_UnderwaterLooting = new UnderwaterLootingBehavior((messageType, format, argObjects) => LogMessage(messageType, format, argObjects));
}
catch (Exception except)
{
// Maintenance problems occur for a number of reasons. The primary two are...
// * Changes were made to the behavior, and boundary conditions weren't properly tested.
// * The Honorbuddy core was changed, and the behavior wasn't adjusted for the new changes.
// In any case, we pinpoint the source of the problem area here, and hopefully it can be quickly
// resolved.
LogMessage("error", "BEHAVIOR MAINTENANCE PROBLEM: " + except.Message
+ "\nFROM HERE:\n"
+ except.StackTrace + "\n");
IsAttributeProblem = true;
}
}
// Attributes provided by caller
public int CollectItemCount { get; private set; }
public int CollectItemId { get; private set; }
public CollectUntilType CollectUntil { get; private set; }
public WoWPoint HuntingGroundAnchor { get; private set; }
public double HuntingGroundRadius { get; private set; }
public bool IgnoreMobsInBlackspots { get; private set; }
public int[] MobIds { get; private set; }
public MobStateType MobState { get; private set; }
public double NonCompeteDistance { get; private set; }
public int[] ObjectIds { get; private set; }
public TimeSpan PostInteractDelay { get; private set; }
public bool RandomizeStartingHotspot { get; private set; }
public int QuestId { get; private set; }
public QuestCompleteRequirement QuestRequirementComplete { get; private set; }
public QuestInLogRequirement QuestRequirementInLog { get; private set; }
// Private properties and data...
private HuntingGroundBehavior _behavior_HuntingGround;
private SwimBreathBehavior _behavior_SwimBreath;
private UnderwaterLootingBehavior _behavior_UnderwaterLooting;
private bool _isBehaviorDone = false;
private bool _isDisposed;
private PluginContainer _pluginAntiDrown;
private bool _pluginAntiDrownWasEnabled;
private WoWObject CurrentTarget { get { return (_behavior_HuntingGround.CurrentTarget); }}
private readonly TimeSpan Delay_MobConsumedExpiry = TimeSpan.FromMinutes(7);
private readonly TimeSpan Delay_BlacklistPlayerTooClose = TimeSpan.FromSeconds(90);
private TimeSpan Delay_WowClientLagTime { get { return (TimeSpan.FromMilliseconds((StyxWoW.WoWClient.Latency * 2) + 150)); } }
private readonly TimeSpan Delay_WoWClientMovementThrottle = TimeSpan.FromMilliseconds(500);
private string ItemName { get; set; }
private static LocalPlayer Me { get { return (ObjectManager.Me); } }
// Private LINQ queries..
private int CollectedItemCount { get {
return ((int)Me.BagItems
.Where(item => (item.ItemInfo.Id == CollectItemId))
.Sum(item => item.StackCount));
}}
// DON'T EDIT THESE--they are auto-populated by Subversion
public override string SubversionId { get { return ("$Id: CollectThings.cs 212 2011-12-07 20:40:52Z raphus $"); } }
public override string SubversionRevision { get { return ("$Revision: 212 $"); } }
~CollectThings()
{
Dispose(false);
}
public void Dispose(bool isExplicitlyInitiatedDispose)
{
if (!_isDisposed)
{
// NOTE: we should call any Dispose() method for any managed or unmanaged
// resource, if that resource provides a Dispose() method.
// Clean up managed resources, if explicit disposal...
if (isExplicitlyInitiatedDispose)
{
// empty, for now
}
// Clean up unmanaged resources (if any) here...
if (_pluginAntiDrown != null)
{
_pluginAntiDrown.Enabled = _pluginAntiDrownWasEnabled;
_pluginAntiDrown = null;
}
TreeRoot.GoalText = string.Empty;
TreeRoot.StatusText = string.Empty;
// Call parent Dispose() (if it exists) here ...
base.Dispose();
}
_isDisposed = true;
}
// If player is close to a target that is interesting to us, ignore the target...
// The player may be going for the same mob, and we don't want to draw attention.
// We'll blacklist the mob for a bit, in case the player is running around, or following
// us. The excaption is ithe player is in our party, then we can freely kill any target
// close to him.
private bool BlacklistIfPlayerNearby(WoWObject target)
{
WoWUnit nearestCompetingPlayer = ObjectManager.GetObjectsOfType(true, false)
.OrderBy(player => player.Location.Distance(target.Location))
.FirstOrDefault(player => player.IsPlayer
&& player.IsAlive
&& !player.IsInOurParty());
// If player is too close to the target, ignore target for a bit...
if ((nearestCompetingPlayer != null)
&& (nearestCompetingPlayer.Location.Distance(target.Location) <= NonCompeteDistance))
{
target.LocallyBlacklist(Delay_BlacklistPlayerTooClose);
return (true);
}
return (false);
}
private void GuiShowProgress(string completionReason)
{
TreeRoot.GoalText = string.Format("{0}: {1}/{2} {3}", this.GetType().Name, CollectedItemCount, CollectItemCount, ItemName);
if (completionReason != null)
{
LogMessage("debug", "Behavior done (" + completionReason + ")");
TreeRoot.GoalText = string.Empty;
TreeRoot.StatusText = string.Empty;
}
}
public bool IsViableTarget(WoWObject target)
{
bool isViable;
if (target == null)
{ return (false); }
isViable = (target.IsValid
&& (MobIds.Contains((int)target.Entry) || ObjectIds.Contains((int)target.Entry))
&& !target.IsLocallyBlacklisted()
&& !BlacklistIfPlayerNearby(target)
&& (IgnoreMobsInBlackspots
? Targeting.IsTooNearBlackspot(ProfileManager.CurrentProfile.Blackspots,
target.Location)
: true));
if (isViable && (target is WoWUnit))
{
WoWUnit wowUnit = target.ToUnit();
isViable = ((wowUnit.IsAlive && (MobState == MobStateType.Alive))
|| (wowUnit.Dead && (MobState == MobStateType.Dead))
|| (MobState == MobStateType.DontCare));
}
return (isViable);
}
private void ParseHuntingGroundHotspots(bool randomizeStartingHotspot)
{
List huntingGroundHotspots = new List();
foreach (XElement element in Element.Elements().Where(elem => (elem.Name == "Hotspot")))
{
double? x = ParseXmlElementDouble(element, "X", true);
double? y = ParseXmlElementDouble(element, "Y", true);
double? z = ParseXmlElementDouble(element, "Z", true);
if (!x.HasValue || !y.HasValue || !z.HasValue)
{ continue; }
bool isStarting = ParseXmlElementBool(element, "StartPoint", false) ?? false;
string name = ParseXmlElementString(element, "Name", false);
huntingGroundHotspots.Add(new WoWPointNamed(new WoWPoint(x.Value, y.Value, z.Value), name, isStarting));
}
_behavior_HuntingGround.UseHotspots(huntingGroundHotspots, randomizeStartingHotspot);
}
private bool? ParseXmlElementBool(XElement element,
string attributeName,
bool isRequired)
{
string location = (((IXmlLineInfo)element).HasLineInfo()
? (" @line " + ((IXmlLineInfo)element).LineNumber.ToString())
: string.Empty);
bool tmpBool;
if (element.Attribute(attributeName) == null)
{
if (isRequired)
{
LogMessage("error", "Hotspot{0} is missing the '{1}' attribute (required)", location, attributeName);
IsAttributeProblem = true;
}
return (null);
}
if (!bool.TryParse(element.Attribute(attributeName).Value, out tmpBool))
{
LogMessage("error", "Hotspot{0} '{1}' attribute is malformed", location, attributeName);
IsAttributeProblem = true;
return (null);
}
return (tmpBool);
}
private double? ParseXmlElementDouble(XElement element,
string attributeName,
bool isRequired)
{
string location = (((IXmlLineInfo)element).HasLineInfo()
? (" @line " + ((IXmlLineInfo)element).LineNumber.ToString())
: string.Empty);
double tmpDouble;
if (element.Attribute(attributeName) == null)
{
if (isRequired)
{
LogMessage("error", "Hotspot{0} is missing the '{1}' attribute (required)", location, attributeName);
IsAttributeProblem = true;
}
return (null);
}
if (!double.TryParse(element.Attribute(attributeName).Value, out tmpDouble))
{
LogMessage("error", "Hotspot{0} '{1}' attribute is malformed", location, attributeName);
IsAttributeProblem = true;
return (null);
}
return (tmpDouble);
}
private string ParseXmlElementString(XElement element,
string attributeName,
bool isRequired)
{
string location = (((IXmlLineInfo)element).HasLineInfo()
? (" @line " + ((IXmlLineInfo)element).LineNumber.ToString())
: string.Empty);
if (element.Attribute(attributeName) == null)
{
if (isRequired)
{
LogMessage("error", "Hotspot{0} is missing the '{1}' attribute (required)", location, attributeName);
IsAttributeProblem = true;
}
return (null);
}
return (element.Attribute(attributeName).Value);
}
#region Overrides of CustomForcedBehavior
protected override Composite CreateBehavior()
{
return (
new PrioritySelector(
// If behavior done, bail...
// Note that this is also an implicit "is quest complete" exit criteria, also.
new Decorator(ret => IsDone,
new Action(delegate { GuiShowProgress("quest complete"); })),
// If we've filled our inventory quota, we're done...
new Decorator(
ret => (CollectedItemCount >= CollectItemCount),
new Action(delegate
{
GuiShowProgress(string.Format("{0}/{1} items collected", CollectedItemCount, CollectItemCount));
_isBehaviorDone = true;
})),
// If we're dead, the behavior can't function so bail until alive...
new Decorator(ret => Me.Dead, new ActionAlwaysSucceed()),
// If swimming, check if we need breath...
_behavior_SwimBreath.CreateBehavior(),
// If there is loot to clean up...
_behavior_UnderwaterLooting.CreateBehavior(() => true),
// Find next target...
_behavior_HuntingGround.CreateBehavior_SelectTarget(() => (CollectUntil == CollectUntilType.NoTargetsInArea)),
// If no target and that's our exit criteria, we're done...
new Decorator(ret => ((CurrentTarget == null)
&& (CollectUntil == CollectUntilType.NoTargetsInArea)),
new Action(delegate
{
GuiShowProgress("No more objects/mobs in area");
_isBehaviorDone = true;
})),
// Otherwise, keep the unit of interest targeted...
new Decorator(ret => ((Me.CurrentTarget != CurrentTarget) && (CurrentTarget is WoWUnit)),
new Action(delegate
{
CurrentTarget.ToUnit().Target();
return (RunStatus.Failure); // Fall through
})),
// Keep progress updated...
new Action(delegate
{
GuiShowProgress(null);
return (RunStatus.Failure); // Fall through
}),
// If we're not at target, move to it...
_behavior_HuntingGround.CreateBehavior_MoveToTarget(),
// We're within interact range, collect the object...
new Sequence(
new Action(delegate { WoWMovement.MoveStop(); }),
new Action(delegate { _behavior_HuntingGround.MobEngaged(CurrentTarget); }),
new WaitContinue(Delay_WowClientLagTime, ret => false, new ActionAlwaysSucceed()),
new Action(delegate { CurrentTarget.Interact(); }),
new WaitContinue(Delay_WowClientLagTime, ret => false, new ActionAlwaysSucceed()),
new WaitContinue(PostInteractDelay, ret => false, new ActionAlwaysSucceed()),
new Action(delegate { CurrentTarget.LocallyBlacklist(Delay_MobConsumedExpiry); }),
new Action(delegate { Me.ClearTarget(); })
)
)
);
}
public override void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
public override bool IsDone
{
get
{
return (_isBehaviorDone // normal completion
|| !UtilIsProgressRequirementsMet(QuestId, QuestRequirementInLog, QuestRequirementComplete));
}
}
public override void OnStart()
{
// The XML element didn't exist when the constructor was called...
// So we had to defer some final parsing that really should've happened in the constructor
// to the OnStart() method. This will parse the final arguments, and set IsAttributeProblem
// correctly, for normal processing.
ParseHuntingGroundHotspots(RandomizeStartingHotspot);
// This reports problems, and stops BT processing if there was a problem with attributes...
// We had to defer this action, as the 'profile line number' is not available during the element's
// constructor call.
OnStart_HandleAttributeProblem();
// If the quest is complete, this behavior is already done...
// So we don't want to falsely inform the user of things that will be skipped.
if (!IsDone)
{
// Disable the AntiDrown plugin if present, as it interferes with our anti-drown prevention...
_pluginAntiDrown = PluginManager.Plugins.FirstOrDefault(plugin => (plugin.Name == "Anti-Drown"));
if (_pluginAntiDrown != null)
{
_pluginAntiDrownWasEnabled = _pluginAntiDrown.Enabled;
_pluginAntiDrown.Enabled = false;
}
PlayerQuest quest = StyxWoW.Me.QuestLog.GetQuestById((uint)QuestId);
TreeRoot.GoalText = this.GetType().Name + ": " + ((quest != null) ? ("\"" + quest.Name + "\"") : "In Progress");
GuiShowProgress(null);
}
}
#endregion
}
public class WoWPointNamed
{
public WoWPointNamed(WoWPoint location,
string name,
bool isStarting)
{
Location = location;
Name = (!string.IsNullOrEmpty(name) ? name : location.ToString());
IsStarting = isStarting;
}
public WoWPointNamed(WoWPoint location,
string name)
: this(location, name, false)
{
// empty
}
public WoWPointNamed(WoWPoint location)
: this(location, null, false)
{
// empty
}
public WoWPointNamed()
: this(WoWPoint.Empty, null, false)
{
// empty
}
public WoWPoint Location { get; set; }
public string Name { get; set; }
public bool IsStarting { get; set; }
}
#region Reusable behaviors
// The behaviors in this section were designed to be efficient and robust.
// The robustness results in some larger-than-normal, but swift code.
// We also designed them to be reused in other behaviors--just copy, paste,
// and call them as-needed.
public class HuntingGroundBehavior
{
public delegate bool BehaviorFailIfNoTargetsDelegate();
public delegate double DistanceDelegate();
public delegate bool IsViableTargetDelegate(WoWObject target);
public delegate WoWPointNamed LocationDelegate();
public delegate void LoggerDelegate(string messageType, string format, params object[] args);
public delegate WoWObject WoWObjectDelegate();
public HuntingGroundBehavior(LoggerDelegate loggerDelegate,
IsViableTargetDelegate isViableTarget,
WoWPoint huntingGroundAnchor,
double collectionDistance)
{
CollectionDistance = collectionDistance;
HuntingGroundAnchor = new WoWPointNamed(huntingGroundAnchor, "Hunting Ground Anchor", true);
IsViableTarget = isViableTarget;
Logger = loggerDelegate;
// UseHotspots(null, false);
}
public void MobEngaged(WoWObject wowObject)
{
if (wowObject == CurrentTarget)
{ _currentTargetAutoBlacklistTimer.Stop(); }
}
// Public properties...
public double CollectionDistance { get; private set; }
public WoWObject CurrentTarget { get; private set; }
public Queue Hotspots { get; set; }
public WoWPointNamed HuntingGroundAnchor { get; private set; }
public IsViableTargetDelegate IsViableTarget { get; private set; }
// Private properties & data...
private const string AuraName_DruidAquaticForm = "Aquatic Form";
private readonly TimeSpan Delay_AutoBlacklist = TimeSpan.FromMinutes(7);
private readonly TimeSpan Delay_RepopWait = TimeSpan.FromMilliseconds(500);
private readonly TimeSpan Delay_StatusUpdateThrottle = TimeSpan.FromMilliseconds(1000);
private readonly TimeSpan Delay_WoWClientMovementThrottle = TimeSpan.FromMilliseconds(0);
private TimeSpan Delay_WowClientLagTime { get { return (TimeSpan.FromMilliseconds((StyxWoW.WoWClient.Latency * 2) + 150)); } }
private readonly LoggerDelegate Logger;
private static LocalPlayer Me { get { return (ObjectManager.Me); } }
private const double MinDistanceToUse_DruidAquaticForm = 27.0;
private int SpellId_DruidAquaticForm = 1066;
private IEnumerable ViableTargets() {
return (ObjectManager.GetObjectsOfType(true, false)
.Where(target => IsViableTarget(target)
&& (target.Distance < CollectionDistance))
.OrderBy(target => (Me.IsSwimming
? target.Distance
: Me.Location.SurfacePathDistance(target.Location))));
}
private TimeSpan _currentTargetAutoBlacklistTime = TimeSpan.FromSeconds(1);
private readonly Stopwatch _currentTargetAutoBlacklistTimer = new Stopwatch();
private Queue _hotSpots = new Queue();
private WoWPointNamed _huntingGroundWaitPoint = new WoWPointNamed(WoWPoint.Empty,
"Hunting Ground Wait Point");
private readonly Stopwatch _repopWaitingTime = new Stopwatch();
///
/// The created behavior was meant to be used in a PrioritySelector.
/// It may also have uses inside other TreeSharp Composites.
///
///
///
/// * RunStatus.Failure, if current target is viable.
/// It will also return Failure if no targets could be located and failIfNoTargets is true
/// * RunStatus.Success, if acquiring a target (or waiting for them to repop)
///
///
public Composite CreateBehavior_SelectTarget()
{
return (CreateBehavior_SelectTarget(() => false));
}
public Composite CreateBehavior_SelectTarget(BehaviorFailIfNoTargetsDelegate failIfNoTargets)
{
return (
new PrioritySelector(
// If we haven't engaged the mob when the auto-blacklist timer expires, give up on it and move on...
new Decorator(ret => ((CurrentTarget != null)
&& (_currentTargetAutoBlacklistTimer.Elapsed > _currentTargetAutoBlacklistTime)),
new Action(delegate
{
Logger("warning", "Taking too long to engage '{0}'--blacklisting", CurrentTarget.Name);
CurrentTarget.LocallyBlacklist(Delay_AutoBlacklist);
CurrentTarget = null;
})),
// If we don't have a current target, select a new one...
// Once we select a target, its 'locked in' (unless it gets blacklisted). This prevents us
// from running back and forth between two equidistant targets.
new Decorator(ret => ((CurrentTarget == null)
|| !CurrentTarget.IsValid
|| CurrentTarget.IsLocallyBlacklisted()
|| !IsViableTarget(CurrentTarget)),
new PrioritySelector(context => CurrentTarget = ViableTargets().FirstOrDefault(),
// If we found next target, we're done...
new Decorator(ret => (CurrentTarget != null),
new Action(delegate
{
_huntingGroundWaitPoint.Location = WoWPoint.Empty;
if (CurrentTarget is WoWUnit)
{ CurrentTarget.ToUnit().Target(); }
_currentTargetAutoBlacklistTime = CalculateAutoBlacklistTime(CurrentTarget);
_currentTargetAutoBlacklistTimer.Reset();
_currentTargetAutoBlacklistTimer.Start();
})),
// If we've exhausted mob/object supply in area, and we need to wait, do so...
new Decorator(ret => !failIfNoTargets(),
// Move back to hunting ground anchor --
new PrioritySelector(
// If we've more than one hotspot, head to the next one...
new Decorator(ret => (_hotSpots.Count() > 1),
new Sequence(context => FindNextHotspot(),
new Action(nextHotspot => TreeRoot.StatusText = "No targets--moving to "
+ ((WoWPointNamed)nextHotspot).Name),
CreateBehavior_InternalMoveTo(() => FindNextHotspot())
)),
// We find a point 'near' our anchor at which to wait...
// This way, if multiple people are using the same profile at the same time,
// they won't be standing on top of each other.
new Decorator(ret => (_huntingGroundWaitPoint.Location == WoWPoint.Empty),
new Action(delegate
{
_huntingGroundWaitPoint.Location = HuntingGroundAnchor.Location.FanOutRandom(CollectionDistance * 0.25);
TreeRoot.StatusText = "No targets--moving to wait near " + _huntingGroundWaitPoint.Name;
_repopWaitingTime.Reset();
_repopWaitingTime.Start();
})),
// Move to our selected random point...
new Decorator(ret => (Me.Location.Distance(_huntingGroundWaitPoint.Location) > Navigator.PathPrecision),
CreateBehavior_InternalMoveTo(() => _huntingGroundWaitPoint)),
// Tell user what's going on...
new Sequence(
new Action(delegate
{
TreeRoot.GoalText = this.GetType().Name + ": Waiting for Repops";
TreeRoot.StatusText = "No targets in area--waiting for repops. " + BuildTimeAsString(_repopWaitingTime.Elapsed);
}),
new WaitContinue(Delay_RepopWait, ret => false, new ActionAlwaysSucceed()))
))
)),
// Re-select target, if it was lost (perhaps, due to combat)...
new Decorator(ret => ((CurrentTarget is WoWUnit) && (Me.CurrentTarget != CurrentTarget)),
new Action(delegate { CurrentTarget.ToUnit().Target();}))
));
}
public Composite CreateBehavior_MoveNearTarget(WoWObjectDelegate target,
DistanceDelegate minRange,
DistanceDelegate maxRange)
{
return (
new PrioritySelector(context => target(),
// If we're too far from target, move closer...
new Decorator(wowObject => (((WoWObject)wowObject).Distance > maxRange()),
new Sequence(
new DecoratorThrottled(Delay_StatusUpdateThrottle, ret => true,
new Action(wowObject =>
{
TreeRoot.StatusText = string.Format("Moving to {0} (distance: {1:0.0}) ",
((WoWObject)wowObject).Name,
((WoWObject)wowObject).Distance);
})),
CreateBehavior_InternalMoveTo(() => new WoWPointNamed(target().Location))
)),
// If we're too close to target, back up...
new Decorator(wowObject => (((WoWObject)wowObject).Distance < minRange()),
new PrioritySelector(
// If backing up, make sure we're facing the target...
new Decorator(ret => Me.MovementInfo.MovingBackward,
new Action(wowObject => WoWMovement.Face(((WoWObject)wowObject).Guid))),
// Start backing up...
new Action(wowObject =>
{
TreeRoot.StatusText = "Too close to \"" + ((WoWObject)wowObject).Name + "\"--backing up";
WoWMovement.MoveStop();
WoWMovement.Face(((WoWObject)wowObject).Guid);
WoWMovement.Move(WoWMovement.MovementDirection.Backwards);
})
)),
// We're between MinRange and MaxRange, stop movement and face the target...
new Decorator(ret => Me.IsMoving,
new Sequence(
new Action(delegate { WoWMovement.MoveStop(); }),
new Action(wowObject => WoWMovement.Face(((WoWObject)wowObject).Guid)),
new WaitContinue(Delay_WowClientLagTime, ret => false, new ActionAlwaysSucceed()),
new ActionAlwaysFail() // fall through to next element
))
));
}
public Composite CreateBehavior_MoveToLocation(LocationDelegate location)
{
return (
new PrioritySelector(
// If we're not at location, move to it...
new Decorator(wowPoint => (Me.Location.Distance((WoWPoint)wowPoint) > Navigator.PathPrecision),
new Sequence(
new DecoratorContinueThrottled(Delay_StatusUpdateThrottle, ret => true,
new Action(wowPoint => TreeRoot.StatusText = "Moving to " + location().Name)),
CreateBehavior_InternalMoveTo(() => location())
))
));
}
public Composite CreateBehavior_MoveToTarget()
{
return (CreateBehavior_MoveToTarget(() => CurrentTarget));
}
public Composite CreateBehavior_MoveToTarget(WoWObjectDelegate target)
{
return (
new PrioritySelector(context => target(),
// If we 'pass by' the current hotspot on the way to the target, advance to next hotspot...
// This prevents bot-like 'back tracking'.
new Decorator(ret => (Me.Location.Distance(_hotSpots.Peek().Location) < (CollectionDistance / 2)),
new Action(delegate
{
// Rotate to the next hotspot in the list...
_hotSpots.Enqueue(_hotSpots.Peek());
_hotSpots.Dequeue();
return (RunStatus.Failure); // Fall through to next
})),
// If we're not at target, move to it...
new Decorator(wowObject => (((WoWObject)wowObject).Distance > ((WoWObject)wowObject).InteractRange),
new Sequence(
new DecoratorContinueThrottled(Delay_StatusUpdateThrottle, ret => true,
new Action(wowObject =>
{
TreeRoot.StatusText = string.Format("Moving to {0} (distance: {1:0.0}) ",
((WoWObject)wowObject).Name,
((WoWObject)wowObject).Distance);
})),
CreateBehavior_InternalMoveTo(() => new WoWPointNamed(target().Location))
)),
// Update status...
new Action(wowObject =>
{
TreeRoot.StatusText = string.Format("Moving to {0} (distance: {1:0.0}) ",
((WoWObject)wowObject).Name,
((WoWObject)wowObject).Distance);
return (RunStatus.Failure);
})
));
}
private static string BuildTimeAsString(TimeSpan timeSpan)
{
string formatString = string.Empty;
if (timeSpan.Hours > 0)
{ formatString = "{0:D2}h:{1:D2}m:{2:D2}s"; }
else if (timeSpan.Minutes > 0)
{ formatString = "{1:D2}m:{2:D2}s"; }
else
{ formatString = "{2:D2}s"; }
return (string.Format(formatString, timeSpan.Hours, timeSpan.Minutes, timeSpan.Seconds));
}
private TimeSpan CalculateAutoBlacklistTime(WoWObject wowObject)
{
double timeToWowObject;
if (Me.IsSwimming)
{ timeToWowObject = Me.Location.Distance(wowObject.Location) / Me.MovementInfo.SwimmingForwardSpeed ; }
else
{ timeToWowObject = Me.Location.SurfacePathDistance(wowObject.Location) / Me.MovementInfo.RunSpeed; }
timeToWowObject *= 2.5; // factor of safety
timeToWowObject = Math.Max(timeToWowObject, 20.0); // 20sec hard lower-limit
return (TimeSpan.FromSeconds(timeToWowObject));
}
private Composite CreateBehavior_InternalMoveTo(LocationDelegate locationDelegate)
{
return (
new Sequence(context => locationDelegate(),
// Druids, switch to Aquatic Form if swimming and distance dictates...
new DecoratorContinue(ret => (SpellManager.CanCast(SpellId_DruidAquaticForm)
&& !Me.HasAura(AuraName_DruidAquaticForm)
&& (Me.Location.Distance(locationDelegate().Location) > MinDistanceToUse_DruidAquaticForm)),
new Action(delegate { SpellManager.Cast(SpellId_DruidAquaticForm); })),
// Move...
new Action(delegate
{
// Try to use Navigator to get there...
WoWPointNamed destination = locationDelegate();
MoveResult moveResult = Navigator.MoveTo(destination.Location);
// If Navigator fails, fall back to click-to-move...
if ((moveResult == MoveResult.Failed) || (moveResult == MoveResult.PathGenerationFailed))
{ WoWMovement.ClickToMove(destination.Location); }
}),
new WaitContinue(Delay_WoWClientMovementThrottle, ret => false, new ActionAlwaysSucceed())
)
);
}
private WoWPointNamed FindStartingHotspot(bool randomStartingHotspot)
{
IEnumerable hotspotsByDistance;
IEnumerable hotspotsStarting;
Random random = new Random((int)DateTime.Now.Ticks);
hotspotsByDistance = (Me.IsSwimming
? _hotSpots.OrderBy(hotspot => hotspot.Location.Distance(Me.Location))
: _hotSpots.OrderBy(hotspot => hotspot.Location.SurfacePathDistance(Me.Location)));
hotspotsStarting = hotspotsByDistance.Where(hotspot => (hotspot.IsStarting == true));
if (hotspotsStarting.Count() <= 0)
{
hotspotsStarting = hotspotsByDistance;
Logger("debug", "No explicit starting hotspot(s)--considering all");
}
Logger("debug", "Hotspot count: {0} ({1})", hotspotsByDistance.Count(),
(randomStartingHotspot ? "randomized" : "starting at nearest"));
WoWPoint startingLocation = (randomStartingHotspot
? hotspotsStarting.OrderBy(ret => random.Next()).FirstOrDefault().Location
: hotspotsStarting.FirstOrDefault().Location);
// Rotate the hotspot queue such that the nearest hotspot is on top...
while (_hotSpots.Peek().Location != startingLocation)
{ _hotSpots.Enqueue(_hotSpots.Dequeue()); }
Logger("debug", "Starting hotspot is {0}", _hotSpots.Peek().Name);
return (_hotSpots.Peek());
}
private WoWPointNamed FindNextHotspot()
{
WoWPointNamed currentHotspot = _hotSpots.Peek();
// If we haven't reached the current hotspot, it is still the 'next' one...
if (Me.Location.Distance(currentHotspot.Location) > Navigator.PathPrecision)
{ return (currentHotspot); }
// Otherwise, rotate to the next hotspot in the list...
_hotSpots.Enqueue(_hotSpots.Dequeue());
return (_hotSpots.Peek());
}
public void UseHotspots(IEnumerable hotspots,
bool randomizeStartingHotspot)
{
_hotSpots = new Queue(hotspots ?? new WoWPointNamed[0]);
if (_hotSpots.Count() <= 0)
{ _hotSpots.Enqueue(HuntingGroundAnchor); }
FindStartingHotspot(randomizeStartingHotspot);
}
}
///
/// This behavior moves to fill a toon's lungs when needed. It utilizes skills available
/// such as a Warlock's Unending Breath, or a Druid's Aquatic Form in its calculations.
/// The behavior looks for nearby air sources (underwater vents, water's surface, etc), and
/// bases its time to move upon the distance that needs to be traveled to get to the air source
/// and the toon's movement speed in the water.
/// This behavior will *not* use engineering and other devices that may allow you to
/// breath--as this could interfere with AutoEquip-type plugins. This behavior will also
/// *not* use potions to help you breathe, as that is a 'profile-level' decision that should
/// be made. In other words, you wouldn't want this behavior to burn a potion to collect
/// a handful of object from shallow, but swimmable water. The profile would know the expected
/// duration underwater, but there is no way for this behavior to know such information.
///
//
// Usage:
// private SwimBreathBehavior _swimBreathBehavior = new SwimBreathBehavior((msgType, fmt, args) => LogMessage(msgType, fmt, args));
// ...
// new PrioritySelector(
// _swimBreathBehavior.CreateBehavior(),
//
public class SwimBreathBehavior
{
public delegate void LoggerDelegate(string messageType, string format, params object[] args);
public SwimBreathBehavior(LoggerDelegate loggerDelegate)
{
Logger = loggerDelegate;
}
// Private properites & data...
private const string AuraName_DruidAquaticForm = "Aquatic Form";
private const string AuraName_WarlockUnendingBreath = "Unending Breath";
private int BreathTimeRemaining { get { return((Timer_SwimBreath.IsVisible)
? (int)Timer_SwimBreath.CurrentTime
: int.MaxValue);
}}
private readonly TimeSpan Delay_StatusUpdateThrottle = TimeSpan.FromMilliseconds(3000);
private readonly LoggerDelegate Logger;
private int MinTime_DruidBreath = 30000; // in milliseconds
private int MinTime_WarlockBreath = 30000; // in milliseconds
private LocalPlayer Me { get { return (ObjectManager.Me); } }
private int SpellId_DruidAquaticForm = 1066;
private int SpellId_WarlockUnendingBreath = 5697;
private readonly TimeSpan ThrottleTimer_BreathCheck = TimeSpan.FromSeconds(5);
private readonly TimeSpan ThrottleTimer_WarlockBreath = TimeSpan.FromSeconds(30);
private readonly TimeSpan Timer_AuraRefresh_EnduringBreath = TimeSpan.FromMilliseconds(180000);
private MirrorTimerInfo Timer_SwimBreath { get { return (Me.GetMirrorTimerInfo(MirrorTimerType.Breath)); } }
private int[] UnderwaterAirSourceObjectIds = { 177524 /* bubbly fissure */
};
private Composite _behaviorRoot;
private bool _isSwimBreathNeeded;
private AirSource _nearestAirSource;
// Private structures...
private struct AirSource
{
public WoWPoint Location;
public string Name;
public AirSource(WoWPoint location, string name) { Location = location; Name = name; }
public double Distance { get { return (Location.Distance(ObjectManager.Me.Location)); }}
public static AirSource Empty = new AirSource(WoWPoint.Empty, "NONE");
}
// Private LINQs
private IEnumerable UnderwaterAirSources { get { return (
ObjectManager.GetObjectsOfType(true, false)
.OrderBy(target => Me.Location.Distance(target.Location))
.Where(target => UnderwaterAirSourceObjectIds.Contains((int)target.Entry))
); }}
private TimeSpan AuraTimeLeft(string auraName)
{
WoWAura wowAura = Me.GetAuraByName(auraName);
return ((wowAura != null) ? wowAura.TimeLeft : TimeSpan.Zero);
}
///
/// The created behavior was meant to be used in a PrioritySelector.
/// It may also have uses inside other TreeSharp Composites.
///
///
///
/// * RunStatus.Failure, if swim breath is not needed
/// * RunStatus.Success, if we're catching our breath, or moving for it
///
///
public Composite CreateBehavior()
{
return (_behaviorRoot ?? (_behaviorRoot =
new Decorator(ret => Me.IsSwimming,
new PrioritySelector(
// Moving to, or fetching breath...
new Decorator(ret => _isSwimBreathNeeded,
new PrioritySelector(
// If toon is filling lungs, stay put until full...
new Decorator(ret => ((Timer_SwimBreath.ChangePerMillisecond > 0)
&& (Timer_SwimBreath.CurrentTime < Timer_SwimBreath.MaxValue)),
new Action(delegate
{
WoWMovement.MoveStop();
TreeRoot.StatusText = "Waiting for full breath";
})),
// If lungs are full, back to work...
new Decorator(ret => (Timer_SwimBreath.CurrentTime >= Timer_SwimBreath.MaxValue),
new Action(delegate { _isSwimBreathNeeded = false; })),
// Move toon to air source, if needed...
new Decorator(ret =>
{
_nearestAirSource = GetNearestAirSource();
return (_nearestAirSource.Distance > Navigator.PathPrecision);
},
new Sequence(
new DecoratorContinueThrottled(Delay_StatusUpdateThrottle, ret => true,
new Action(delegate
{
TreeRoot.StatusText = string.Format("Moving to {0} for breath. (distance {1:0.0})",
_nearestAirSource.Name,
_nearestAirSource.Distance);
})),
new Action(delegate { UnderwaterMoveTo(_nearestAirSource.Location); })
)
)
)),
// If we're a Warlock, refresh Unending Breath as needed...
new DecoratorThrottled(ThrottleTimer_WarlockBreath,
ret => (SpellManager.CanCast(SpellId_WarlockUnendingBreath)
&& (AuraTimeLeft(AuraName_WarlockUnendingBreath) <= Timer_AuraRefresh_EnduringBreath)),
new Action(delegate { SpellManager.Cast(SpellId_WarlockUnendingBreath); })),
// If time to breathe, do something about it...
new DecoratorThrottled(ThrottleTimer_BreathCheck,
ret => IsBreathNeeded(),
new PrioritySelector(
// If we're a Druid, switch to Aquatic form for breath...
new Decorator(ret => (SpellManager.CanCast(SpellId_DruidAquaticForm)
&& !Me.HasAura(AuraName_DruidAquaticForm)),
new Action(delegate
{
SpellManager.Cast(SpellId_DruidAquaticForm);
TreeRoot.StatusText = "Switching to Aquatic Form for breath";
_isSwimBreathNeeded = true;
})),
// Otherwise, we need to deal with 'normal' way to catch breath...
new Action(delegate
{
_nearestAirSource = GetNearestAirSource();
Logger("info", "Moving to {0} for breath. (distance {1:0.0})",
_nearestAirSource.Name, _nearestAirSource.Distance);
_isSwimBreathNeeded = true;
})
))
))));
}
private AirSource GetNearestAirSource()
{
// Assume water's surface is nearest breath...
AirSource nearestAirSource = new AirSource(Me.Location.WaterSurface(), "water's surface");
WoWObject underwaterAirSource = UnderwaterAirSources.FirstOrDefault();
// If underwater air source exists, and is closer that the water's surface...
if ((underwaterAirSource != null)
&& (Me.Location.Distance(underwaterAirSource.Location) <= nearestAirSource.Distance))
{
nearestAirSource.Location = underwaterAirSource.Location;
nearestAirSource.Name = underwaterAirSource.Name;
}
return (nearestAirSource);
}
private bool IsBreathNeeded()
{
int breathTimeRemaining = BreathTimeRemaining;
if (Me.Class == WoWClass.Druid)
{ return (breathTimeRemaining < MinTime_DruidBreath); }
else if (Me.Class == WoWClass.Warlock)
{ return (breathTimeRemaining < MinTime_WarlockBreath); }
// Calculate time needed to get to an air source...
AirSource airSource = GetNearestAirSource();
double travelTime;
travelTime = (((airSource.Location.Distance(Me.Location) / Me.MovementInfo.SwimmingForwardSpeed )
* 2.75) // factor of safety
+ (3 * ThrottleTimer_BreathCheck.TotalSeconds));
travelTime = Math.Min(travelTime, 30.0); // Hard-minimum of 30secs
travelTime *= 1000; // to milliseconds
return (breathTimeRemaining <= travelTime);
}
private void UnderwaterMoveTo(WoWPoint location)
{
// Try to use Navigator to get there...
MoveResult moveResult = Navigator.MoveTo(location);
// If Navigator fails, resort to click-to-move...
if ((moveResult == MoveResult.Failed) || (moveResult == MoveResult.PathGenerationFailed))
{ WoWMovement.ClickToMove(location); }
}
}
///
/// This behavior is necessary since Honorbuddy is incapable of moving underwater.
///
//
// Usage:
// private UnderwaterLootingBehavior _underwaterLootingBehavior = new UnderwaterLootingBehavior((msgType, fmt, args) => LogMessage(msgType, fmt, args));
// ...
// new PrioritySelector(
// _underwaterLootingBehavior.CreateBehavior(),
//
public class UnderwaterLootingBehavior
{
public delegate bool ForceLootDelegate();
public delegate void LoggerDelegate(string messageType, string format, params object[] args);
public UnderwaterLootingBehavior(LoggerDelegate loggerDelegate)
{
Logger = loggerDelegate;
}
// Private properties & data...
private const string AuraName_DruidAquaticForm = "Aquatic Form";
private readonly TimeSpan Delay_BlacklistLootedMob = TimeSpan.FromMinutes(7);
private readonly TimeSpan Delay_WaitForLootCleanup = TimeSpan.FromMilliseconds(5000);
private TimeSpan Delay_WowClientLagTime { get { return (TimeSpan.FromMilliseconds((StyxWoW.WoWClient.Latency * 2) + 150)); } }
private readonly TimeSpan Delay_WowClientWaitForLootFrame = TimeSpan.FromSeconds(10);
private readonly LoggerDelegate Logger;
private static LocalPlayer Me { get { return (ObjectManager.Me); } }
private int SpellId_DruidAquaticForm = 1066;
private Composite _behaviorRoot;
private WoWObject _currentTarget;
// Private LINQ...
private IEnumerable LootList { get {
return (ObjectManager.GetObjectsOfType(true, false)
.Where(target => (target.Dead && target.Lootable
&& !target.IsLootingBlacklisted())));
}}
///
/// The created behavior was meant to be used in a PrioritySelector.
/// It may also have uses inside other TreeSharp Composites.
///
///
///
/// * RunStatus.Failure, if looting is not needed
/// * RunStatus.Success, if we're in the process of looting things
///
///
public Composite CreateBehavior(ForceLootDelegate forceLoot)
{
return (_behaviorRoot ?? (_behaviorRoot =
new Decorator(ret => ((CharacterSettings.Instance.LootMobs || forceLoot()) && (LootList.Count() > 0)),
new PrioritySelector(
// If we're swimming, we need to do loot cleanup for ourselves...
new Decorator(ret => (Me.IsSwimming || forceLoot()),
new PrioritySelector(context => _currentTarget = LootList.FirstOrDefault(),
// If not at nearest target, move to it...
new Decorator(ret => (_currentTarget.Distance > _currentTarget.InteractRange),
new Action(delegate
{
TreeRoot.StatusText = string.Format("Moving to loot target '{0}' (distance {1})...",
_currentTarget.Name,
_currentTarget.Distance);
UnderwaterMoveTo(_currentTarget.Location);
})),
// Within interact range, so loot it...
// NOTE: that we have to locally blacklist looted targets. They sometimes
// have unique (e.g., quest-starting type) items on them. If we already have
// have such an item in our inventory, the target remains lootable, but there
// is nothing we can pick up from it. The blacklist prevents us from getting
// into silly loops because of such mechanics.
new Sequence(
new Action(delegate { WoWMovement.MoveStop(); }),
new WaitContinue(Delay_WowClientLagTime, ret => false, new ActionAlwaysSucceed()),
new Action(delegate { _currentTarget.Interact(); }),
new WaitContinue(Delay_WowClientWaitForLootFrame,
ret => LootFrame.Instance.IsVisible,
new ActionAlwaysSucceed()),
new DecoratorContinue(ret => LootFrame.Instance.IsVisible,
new Action(ret => LootFrame.Instance.LootAll())),
new Action(delegate { _currentTarget.LootingBlacklist(Delay_BlacklistLootedMob); })
)
)),
// We're not swimming, so we want to wait for the 'normal' looting behavior
// to scoop up the loot before allowing other behaviors to continue...
// This keeps it from taking a few steps towards next mob, only to go back to
// previous mob and loot it. This technique can still fail if Honorbddy is slow to update
// infomation. However, it shuts a lot of back-tracking.
new WaitContinue(Delay_WaitForLootCleanup, ret => false, new ActionAlwaysSucceed())
)
)));
}
private void UnderwaterMoveTo(WoWPoint location)
{
// If we're a Druid, use Aquatic Form...
if (SpellManager.CanCast(SpellId_DruidAquaticForm) && !Me.HasAura(AuraName_DruidAquaticForm))
{ SpellManager.Cast(SpellId_DruidAquaticForm); }
// Try to use Navigator to get there...
MoveResult moveResult = Navigator.MoveTo(location);
if ((moveResult == MoveResult.Failed) || (moveResult == MoveResult.PathGenerationFailed))
{ WoWMovement.ClickToMove(location); }
}
}
#endregion // Reusable behaviors
#region Extensions to HBcore
public class DecoratorContinueThrottled : DecoratorContinue
{
public DecoratorContinueThrottled(TimeSpan throttleTime,
CanRunDecoratorDelegate canRun,
Composite composite)
:base(canRun, composite)
{
_throttleTime = throttleTime;
_throttle = new Stopwatch();
_throttle.Reset();
_throttle.Start();
}
protected override bool CanRun(object context)
{
if (_throttle.Elapsed < _throttleTime)
{ return (false); }
_throttle.Reset();
_throttle.Start();
return (base.CanRun(context));
}
private Stopwatch _throttle;
private TimeSpan _throttleTime;
}
public class DecoratorThrottled : Decorator
{
public DecoratorThrottled(TimeSpan throttleTime,
CanRunDecoratorDelegate canRun,
Composite composite)
:base(canRun, composite)
{
_throttleTime = throttleTime;
_throttle = new Stopwatch();
_throttle.Reset();
_throttle.Start();
}
protected override bool CanRun(object context)
{
if (_throttle.Elapsed < _throttleTime)
{ return (false); }
_throttle.Reset();
_throttle.Start();
return (base.CanRun(context));
}
private Stopwatch _throttle;
private TimeSpan _throttleTime;
}
// The HBcore 'global' blacklist will also prevent looting. We don't want that.
// Since the HBcore blacklist is not built to instantiate, we have to roll our
// own.
public class LocalBlackList
{
public LocalBlackList(TimeSpan maxSweepTime)
{
_maxSweepTime = maxSweepTime;
_stopWatchForSweeping.Start();
}
private Dictionary _blackList = new Dictionary();
private TimeSpan _maxSweepTime;
private Stopwatch _stopWatchForSweeping = new Stopwatch();
public void Add(ulong guid, TimeSpan timeSpan)
{
if (_stopWatchForSweeping.Elapsed > _maxSweepTime)
{ RemoveExpired(); }
_blackList[guid] = DateTime.Now.Add(timeSpan);
}
public bool Contains(ulong guid)
{
if (_stopWatchForSweeping.Elapsed > _maxSweepTime)
{ RemoveExpired(); }
return (_blackList.ContainsKey(guid));
}
public void RemoveExpired()
{
DateTime now = DateTime.Now;
List expiredEntries = (from key in _blackList.Keys
where (_blackList[key] < now)
select key).ToList();
foreach (ulong entry in expiredEntries)
{ _blackList.Remove(entry); }
_stopWatchForSweeping.Reset();
_stopWatchForSweeping.Start();
}
}
public static class WoWObject_Extensions
{
private static LocalPlayer Me { get { return (ObjectManager.Me); } }
// We provide our own 'local' blacklist. If we use the global one maintained by HBcore,
// that will prevent us from looting also.
private static LocalBlackList _blackList = new LocalBlackList(TimeSpan.FromSeconds(30));
private static LocalBlackList _blackListLooting = new LocalBlackList(TimeSpan.FromSeconds(30));
public static void LocallyBlacklist(this WoWObject wowObject,
TimeSpan timeSpan)
{
_blackList.Add(wowObject.Guid, timeSpan);
}
public static void LootingBlacklist(this WoWObject wowObject,
TimeSpan timeSpan)
{
_blackListLooting.Add(wowObject.Guid, timeSpan);
}
public static bool IsLocallyBlacklisted(this WoWObject wowObject)
{
return (_blackList.Contains(wowObject.Guid));
}
public static bool IsLootingBlacklisted(this WoWObject wowObject)
{
return (_blackListLooting.Contains(wowObject.Guid));
}
}
public static class WoWUnit_Extensions
{
private static LocalPlayer Me { get { return (ObjectManager.Me); } }
public static bool IsInOurParty(this WoWUnit wowUnit)
{
return ((Me.PartyMembers.FirstOrDefault(partyMember => (partyMember.Guid == wowUnit.Guid))) != null);
}
}
public static class WoWPoint_Extensions
{
public static Random _random = new Random((int)DateTime.Now.Ticks);
private static LocalPlayer Me { get { return (ObjectManager.Me); } }
public const double TAU = (2 * Math.PI); // See http://tauday.com/
public static WoWPoint Add(this WoWPoint wowPoint,
double x,
double y,
double z)
{
return (new WoWPoint((wowPoint.X + x), (wowPoint.Y + y), (wowPoint.Z + z)));
}
public static WoWPoint AddPolarXY(this WoWPoint wowPoint,
double xyHeadingInRadians,
double distance,
double zModifier)
{
return (wowPoint.Add((Math.Cos(xyHeadingInRadians) * distance),
(Math.Sin(xyHeadingInRadians) * distance),
zModifier));
}
// Finds another point near the destination. Useful when toon is 'waiting' for something
// (e.g., boat, mob repops, etc). This allows multiple people running
// the same profile to not stand on top of each other while waiting for
// something.
public static WoWPoint FanOutRandom(this WoWPoint location,
double maxRadius)
{
const int CYLINDER_LINE_COUNT = 12;
const int MAX_TRIES = 50;
const double SAFE_DISTANCE_BUFFER = 1.75;
WoWPoint candidateDestination = location;
int tryCount;
// Most of the time we'll find a viable spot in less than 2 tries...
// However, if you're standing on a pier, or small platform a
// viable alternative may take 10-15 tries--its all up to the
// random number generator.
for (tryCount = MAX_TRIES; tryCount > 0; --tryCount)
{
WoWPoint circlePoint;
bool[] hitResults;
WoWPoint[] hitPoints;
int index;
WorldLine[] traceLines = new WorldLine[CYLINDER_LINE_COUNT +1];
candidateDestination = location.AddPolarXY((TAU * _random.NextDouble()), (maxRadius * _random.NextDouble()), 0.0);
// Build set of tracelines that can evaluate the candidate destination --
// We build a cone of lines with the cone's base at the destination's 'feet',
// and the cone's point at maxRadius over the destination's 'head'. We also
// include the cone 'normal' as the first entry.
// 'Normal' vector
index = 0;
traceLines[index].Start = candidateDestination.Add(0.0, 0.0, maxRadius);
traceLines[index].End = candidateDestination.Add(0.0, 0.0, -maxRadius);
// Cylinder vectors
for (double turnFraction = 0.0; turnFraction < TAU; turnFraction += (TAU / CYLINDER_LINE_COUNT))
{
++index;
circlePoint = candidateDestination.AddPolarXY(turnFraction, SAFE_DISTANCE_BUFFER, 0.0);
traceLines[index].Start = circlePoint.Add(0.0, 0.0, maxRadius);
traceLines[index].End = circlePoint.Add(0.0, 0.0, -maxRadius);
}
// Evaluate the cylinder...
// The result for the 'normal' vector (first one) will be the location where the
// destination meets the ground. Before this MassTrace, only the candidateDestination's
// X/Y values were valid.
GameWorld.MassTraceLine(traceLines.ToArray(),
GameWorld.CGWorldFrameHitFlags.HitTestGroundAndStructures,
out hitResults,
out hitPoints);
candidateDestination = hitPoints[0]; // From 'normal', Destination with valid Z coordinate
// Sanity check...
// We don't want to be standing right on the edge of a drop-off (say we'e on
// a plaform or pier). If there is not solid ground all around us, we reject
// the candidate. Our test for validity is that the walking distance must
// not be more than 20% greater than the straight-line distance to the point.
int viableVectorCount = hitPoints.Sum(point => ((Me.Location.SurfacePathDistance(point) < (Me.Location.Distance(point) * 1.20))
? 1
: 0));
if (viableVectorCount < (CYLINDER_LINE_COUNT +1))
{ continue; }
// If new destination is 'too close' to our current position, try again...
if (Me.Location.Distance(candidateDestination) <= SAFE_DISTANCE_BUFFER)
{ continue; }
break;
}
// If we exhausted our tries, just go with simple destination --
if (tryCount <= 0)
{ candidateDestination = location; }
return (candidateDestination);
}
public static double SurfacePathDistance(this WoWPoint start,
WoWPoint destination)
{
WoWPoint[] groundPath = Navigator.GeneratePath(start, destination) ?? new WoWPoint[0];
// We define an invalid path to be of 'infinite' length
if (groundPath.Length <= 0)
{ return (double.MaxValue); }
double pathDistance = start.Distance(groundPath[0]);
for (int i = 0; i < (groundPath.Length - 1); ++i)
{ pathDistance += groundPath[i].Distance(groundPath[i+1]); }
return (pathDistance);
}
// Returns WoWPoint.Empty if unable to locate water's surface
public static WoWPoint WaterSurface(this WoWPoint location)
{
WoWPoint hitLocation;
bool hitResult;
WoWPoint locationUpper = location.Add(0.0, 0.0, 2000.0);
WoWPoint locationLower = location.Add(0.0, 0.0, -2000.0);
hitResult = (GameWorld.TraceLine(locationUpper,
locationLower,
GameWorld.CGWorldFrameHitFlags.HitTestLiquid,
out hitLocation)
|| GameWorld.TraceLine(locationUpper,
locationLower,
GameWorld.CGWorldFrameHitFlags.HitTestLiquid2,
out hitLocation));
return (hitResult ? hitLocation : WoWPoint.Empty);
}
}
#endregion // Extensions to HBcore
}