Skip to content

Instantly share code, notes, and snippets.

Created March 24, 2014 22:24
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save anonymous/5dd561083d9d465bc08d to your computer and use it in GitHub Desktop.
Save anonymous/5dd561083d9d465bc08d to your computer and use it in GitHub Desktop.
Modified Exile.cs for flasks that dispel.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Windows;
using System.Windows.Forms.VisualStyles;
using GrindBot7;
using Loki.Bot;
using Loki.Bot.Logic.Behaviors;
using Loki.Bot.Navigation;
using Loki.Bot.Pathfinding;
using Loki.Game;
using Loki.Game.Inventory;
using Loki.Game.Objects;
using Loki.Game.Utilities;
using Loki.TreeSharp;
using Loki.Utilities;
using System.Diagnostics;
using Action = Loki.TreeSharp.Action;
using Loki.Game.GameData;
using log4net;
namespace ExileRoutine
{
public enum ClusterType
{
Radius,
Chained,
//Cone
}
public partial class Exile
{
private static readonly ILog Log = Logger.GetLoggerInstanceForType();
#region Config stuff that I haven't moved to the UI
public static bool IsMeleeBased = false;
#endregion
#region Flask Logic
private Stopwatch _EnduringCryCd = new Stopwatch();
private Stopwatch _MoltenShellCd = new Stopwatch();
private readonly WaitTimer _flaskCd = new WaitTimer(TimeSpan.FromSeconds(0.5));
private IEnumerable<InventoryItem> LifeFlasks
{
get
{
IEnumerable<InventoryItem> inv = LokiPoe.ObjectManager.Me.Inventory.Flasks.Items;
return from item in inv
let flask = item.Flask
where flask != null && flask.HealthRecover > 0 && flask.CanUse
orderby flask.IsInstantRecovery ? flask.HealthRecover : flask.HealthRecoveredPerSecond descending
select item;
}
}
private IEnumerable<InventoryItem> ManaFlasks
{
get
{
IEnumerable<InventoryItem> inv = LokiPoe.ObjectManager.Me.Inventory.Flasks.Items;
return from item in inv
let flask = item.Flask
where flask != null && flask.ManaRecover > 0 && flask.CanUse
orderby flask.IsInstantRecovery ? flask.ManaRecover : flask.ManaRecoveredPerSecond descending
select item;
}
}
private IEnumerable<InventoryItem> GraniteFlasks
{
get
{
IEnumerable<InventoryItem> inv = LokiPoe.ObjectManager.Me.Inventory.Flasks.Items;
return from item in inv
let flask = item.Flask
where flask != null && item.Name == "Granite Flask" && flask.CanUse
select item;
}
}
private IEnumerable<InventoryItem> JadeFlasks
{
get
{
IEnumerable<InventoryItem> inv = LokiPoe.ObjectManager.Me.Inventory.Flasks.Items;
return from item in inv
let flask = item.Flask
where flask != null && item.Name == "Jade Flask" && flask.CanUse
select item;
}
}
private IEnumerable<InventoryItem> QuicksilverFlasks
{
get
{
//InternalName: flask_utility_sprint, BuffType: 24, CasterId: 13848, OwnerId: 0, TimeLeft: 00:00:05.0710000, Charges: 1, Description: You have greatly increased Movement Speed
IEnumerable<InventoryItem> inv = LokiPoe.ObjectManager.Me.Inventory.Flasks.Items;
return from item in inv
let flask = item.Flask
where flask != null && item.Name == "Quicksilver Flask" && flask.CanUse
select item;
}
}
private IEnumerable<InventoryItem> BleedingFlasks
{
get
{
IEnumerable<InventoryItem> inv = LokiPoe.ObjectManager.Me.Inventory.Flasks.Items;
return from item in inv
let flask = item.Flask
where flask != null && item.Item.ExplicitStats.ContainsKey(StatType.LocalFlaskRemoveBleedingOnUse) && flask.CanUse
select item;
}
}
private IEnumerable<InventoryItem> ShockFlasks
{
get
{
IEnumerable<InventoryItem> inv = LokiPoe.ObjectManager.Me.Inventory.Flasks.Items;
return from item in inv
let flask = item.Flask
where flask != null && item.Item.ExplicitStats.ContainsKey(StatType.LocalFlaskRemoveShockOnUse) && flask.CanUse
select item;
}
}
private IEnumerable<InventoryItem> BurningFlasks
{
get
{
IEnumerable<InventoryItem> inv = LokiPoe.ObjectManager.Me.Inventory.Flasks.Items;
return from item in inv
let flask = item.Flask
where flask != null && item.Item.ExplicitStats.ContainsKey(StatType.LocalFlaskDispelsBurning) && flask.CanUse
select item;
}
}
private IEnumerable<InventoryItem> FrozenFlasks
{
get
{
IEnumerable<InventoryItem> inv = LokiPoe.ObjectManager.Me.Inventory.Flasks.Items;
return from item in inv
let flask = item.Flask
where flask != null && item.Item.ExplicitStats.ContainsKey(StatType.LocalFlaskDispelsFreezeAndChill) && flask.CanUse
select item;
}
}
private Composite CreateFlaskLogic()
{
return new PrioritySelector(
// This is an example of how to make the bot logout if combat is triggered when there are no HP flasks left.
// For global checks, a plugin would have to be used instead.
/*new Decorator(ret => LifeFlasks.Count() == 0,
new Action(ret =>
{
Log.Error("Logging out because we have no more health flasks to use! This is not an error, but rather an important debug message.");
// If we don't have any life flasks left, logout. This is to ensure the bot will not fight on
// if flasks run out. Triggering a town run does not take priority of combat, so that's why that
// code isn't placed here.
LokiPoe.Gui.Logout(false);
})),*/
// Uncomment this to use any flask which dispels the effect as soon as you have the effect
/*new Decorator(ret => _flaskCd.IsFinished && Me.HasAura("frozen") && FrozenFlasks.Count() != 0,
new Action(ret =>
{
FrozenFlasks.First().Use();
_flaskCd.Reset();
})),*/
// Uncomment this to use any flask which dispels the effect as soon as you have the effect
/*new Decorator(ret => _flaskCd.IsFinished && Me.HasAura("chilled") && FrozenFlasks.Count() != 0,
new Action(ret =>
{
FrozenFlasks.First().Use();
_flaskCd.Reset();
})),*/
// Uncomment this to use any flask which dispels the effect as soon as you have the effect
/*new Decorator(ret => _flaskCd.IsFinished && Me.HasAura("ignited") && BurningFlasks.Count() != 0,
new Action(ret =>
{
BurningFlasks.First().Use();
_flaskCd.Reset();
})),*/
// Uncomment this to use any flask which dispels the effect as soon as you have the effect
/*new Decorator(ret => _flaskCd.IsFinished && Me.HasAura("shocked") && ShockedFlasks.Count() != 0,
new Action(ret =>
{
ShockedFlasks.First().Use();
_flaskCd.Reset();
})),*/
// Uncomment this to use any flask which dispels the effect as soon as you have the effect
/*new Decorator(ret => _flaskCd.IsFinished && Me.HasAura("puncture") && BleedingFlasks.Count() != 0,
new Action(ret =>
{
BleedingFlasks.First().Use();
_flaskCd.Reset();
})),*/
new Decorator(ret => _flaskCd.IsFinished && Me.HealthPercent < 70 && LifeFlasks.Count() != 0 && !Me.HasAura("flask_effect_life"),
new Action(ret =>
{
LifeFlasks.First().Use();
_flaskCd.Reset();
})),
new Decorator(ret => _flaskCd.IsFinished && Me.ManaPercent < 50 && ManaFlasks.Count() != 0 && !Me.HasAura("flask_effect_mana"),
new Action(ret =>
{
ManaFlasks.First().Use();
_flaskCd.Reset();
})),
new Decorator(ret => _flaskCd.IsFinished && Me.HealthPercent < 58 && GraniteFlasks.Count() != 0 && !Me.HasAura("flask_effect_granite"),
new Action(ret =>
{
GraniteFlasks.First().Use();
_flaskCd.Reset();
})),
new Decorator(ret => _flaskCd.IsFinished && Me.HealthPercent < 66 && JadeFlasks.Count() != 0 && !Me.HasAura("flask_effect_jade"),
new Action(ret =>
{
JadeFlasks.First().Use();
_flaskCd.Reset();
}))
);
}
#endregion
#region Cached Values
private int? _cachedMaxEnduranceCharges;
private int? _cachedMaxFrenzyCharges;
private int? _cachedMaxPowerCharges;
private int? _cachedMaxSkeletons;
private int? _cachedMaxTotems;
public int MaxTotems
{
get
{
if (_cachedMaxTotems == null)
{
_cachedMaxTotems = LokiPoe.ObjectManager.Me.GetStat(StatType.NumberOfTotemsAllowed);
}
return _cachedMaxTotems.Value;
}
}
public int MaxEnduranceCharges
{
get
{
if (_cachedMaxEnduranceCharges == null)
{
_cachedMaxEnduranceCharges = LokiPoe.ObjectManager.Me.GetStat(StatType.MaxEnduranceCharges);
}
return _cachedMaxEnduranceCharges.Value;
}
}
public int MaxPowerCharges
{
get
{
if (_cachedMaxPowerCharges == null)
{
_cachedMaxPowerCharges = LokiPoe.ObjectManager.Me.GetStat(StatType.MaxPowerCharges);
}
return _cachedMaxPowerCharges.Value;
}
}
public int MaxFrenzyCharges
{
get
{
if (_cachedMaxFrenzyCharges == null)
{
_cachedMaxFrenzyCharges = LokiPoe.ObjectManager.Me.GetStat(StatType.MaxFrenzyCharges);
}
return _cachedMaxFrenzyCharges.Value;
}
}
public int MaxSkeletons
{
get
{
if (_cachedMaxSkeletons == null)
{
Spell s = SpellManager.GetSpell("Summon Skeletons");
if (s != null)
{
_cachedMaxSkeletons = s.GetStat(StatType.NumberOfSkeletonsAllowed);
}
else
{
_cachedMaxSkeletons = 0;
}
}
return _cachedMaxSkeletons.Value;
}
}
#endregion
#region Spell Registration
#region Register Spell
private void Register(string spell)
{
Register(spell, ret => true);
}
private void Register(string spell, SpellManager.GetSelection<bool> requirement)
{
Register(spell, requirement, ret => BestTarget);
}
private void Register(string spell, SpellManager.GetSelection<bool> requirement, SpellManager.GetSelection<NetworkObject> on)
{
// If we don't have the spell, or it's a totem, ignore it
if (!SpellManager.HasSpell(spell, true) || SpellManager.GetSpell(spell).GetStat(StatType.IsTotem) != 0)
{
return;
}
Log.Debug("• Registered " + spell);
CombatComposite.AddChild(SpellManager.CreateSpellCastComposite(spell, requirement, on));
}
private void Register(string spell, SpellManager.GetSelection<bool> requirement, SpellManager.GetSelection<Vector2i> on)
{
// If we don't have the spell, or it's a totem, ignore it
if (!SpellManager.HasSpell(spell, true) || SpellManager.GetSpell(spell).GetStat(StatType.IsTotem) != 0)
{
return;
}
Log.Debug("• Registered " + spell);
CombatComposite.AddChild(SpellManager.CreateSpellCastComposite(spell, requirement, on));
}
#endregion
#region Register Buff
private void RegisterBuff(string spell, SpellManager.GetSelection<bool> requirement)
{
// If we don't have the spell, or it's a totem, ignore it
if (!SpellManager.HasSpell(spell, true) || SpellManager.GetSpell(spell).GetStat(StatType.IsTotem) != 0)
{
return;
}
Log.Debug("• Registered Buff " + spell);
BuffComposite.AddChild(SpellManager.CreateSpellCastNoTargetOrLocationComposite(spell,
ret =>
{
bool req = requirement == null || requirement(ret);
if (req)
{
return !LokiPoe.ObjectManager.Me.HasAura(spell);
}
return false;
}));
}
#endregion
#region Register Summon
private IEnumerable<Monster> DeadSummonablesNearby
{
get { return LokiPoe.ObjectManager.Objects.OfType<Monster>().Where(m => m.IsValid && !m.IsFriendly && m.IsDead && m.Distance < 40 && m.CorpseUsable); }
}
private void RegisterSummon(string spell, Func<int> current, Func<int> max, bool deadOnly, SpellManager.GetSelection<NetworkObject> onTarget = null,
SpellManager.GetSelection<bool> extraReqs = null)
{
// If we don't have the spell, or it's a totem, ignore it
if (!SpellManager.HasSpell(spell, true))
{
return;
}
bool isTotem = SpellManager.GetSpell(spell).GetStat(StatType.IsTotem) != 0;
if (isTotem)
{
// This was causing a bug if the user registered the skill themselves!
//RegisterTotem(spell, extraReqs, onTarget);
return;
}
if (onTarget == null)
{
// Cast on dead targets, or our current "best" target.
if (deadOnly)
{
onTarget = o => DeadSummonablesNearby.FirstOrDefault();
}
else
{
onTarget = o => BestTarget;
}
}
SpellManager.GetSelection<bool> requirements = predicate =>
{
if (extraReqs != null && !extraReqs(predicate))
{
return false;
}
bool deads = !deadOnly || DeadSummonablesNearby.Any();
int c = current();
int m = max();
if (c < m)
{
return deads;
}
return false;
};
// NOTE: For summoning, we need to cast "on the location" or our input won't match the target at hand.
// There's a way to get around this (by using the corpse targeting) but it's not currently implemented.
CombatComposite.AddChild(SpellManager.CreateSpellCastComposite(spell, requirements, ctx => onTarget(ctx).Position));
Log.Debug("• Registered Summon " + spell);
}
#endregion
#region Register Trap
private void RegisterTrap(string spell, SpellManager.GetSelection<bool> requirements = null)
{
if (!SpellManager.HasSpell(spell, true) || SpellManager.GetSpell(spell).GetStat(StatType.IsTrap) == 0)
{
return;
}
if (requirements == null)
{
requirements = o => true;
}
Log.Debug("• Registered Trap " + spell);
CombatComposite.Children.Add(SpellManager.CreateSpellCastComposite(spell, requirements, ret => BestTarget));
}
#endregion
#region RegisterCurse
private void RegisterCurse(string spell, string curse)
{
// 5+ mobs near the best target.
RegisterCurse(spell, curse, ret => BestTarget.IsCursable && (BestTarget.Rarity >= Rarity.Rare || NumberOfMobsNear(BestTarget, 10, 4)));
}
private void RegisterCurse(string spell, string curse, SpellManager.GetSelection<bool> requirement)
{
RegisterCurse(spell, curse, requirement, ret => BestTarget);
}
private void RegisterCurse(string spell, string curse, SpellManager.GetSelection<bool> requirement, SpellManager.GetSelection<NetworkObject> on)
{
// If we don't have the spell, or it's a totem, ignore it
if (!SpellManager.HasSpell(spell, true) || SpellManager.GetSpell(spell).GetStat(StatType.IsTotem) != 0)
{
return;
}
Log.Debug("• Registered Curse " + spell);
// Note: we're putting this in the buff composite, since we want it to run before the normal combat stuff.
BuffComposite.AddChild(SpellManager.CreateSpellCastComposite(spell,
ret =>
{
// First, check if there are requirements for this curse.
bool req = requirement == null || requirement(ret);
// If the requirements have been met, ensure the monster doesn't have the aura we're looking for.
if (req)
{
return !(on(ret) as Actor).HasAura(curse);
}
return false;
},
on));
}
#endregion
#region Register Totem
private void RegisterTotem(string spell)
{
RegisterTotem(spell, ret => true);
}
private void RegisterTotem(string spell, SpellManager.GetSelection<bool> requirement)
{
RegisterTotem(spell, requirement, ret => BestTarget);
}
private void RegisterTotem(string spell, SpellManager.GetSelection<bool> requirement, SpellManager.GetSelection<NetworkObject> on)
{
if (!SpellManager.HasSpell(spell, true) || SpellManager.GetSpell(spell).GetStat(StatType.IsTotem) == 0)
{
return;
}
Log.Debug("• Registered Totem " + spell);
CombatComposite.AddChild(CreateCastTotemComposite(spell, 0.75f, requirement, ret => on(ret).Position));
}
private void RegisterTotem(string spell, SpellManager.GetSelection<bool> requirement, SpellManager.GetSelection<Vector2i> on)
{
if (!SpellManager.HasSpell(spell, true))
{
return;
}
Log.Debug("• Registered Totem " + spell);
CombatComposite.AddChild(CreateCastTotemComposite(spell, 0.75f, requirement, on));
}
#endregion
#endregion
#region Summons
private int? _cachedMaxSpectres;
private int? _cachedMaxZombies;
public int MaxZombies
{
get
{
if (_cachedMaxZombies == null)
{
Spell s = SpellManager.GetSpell("Raise Zombie");
if (s != null)
{
_cachedMaxZombies = s.GetStat(StatType.NumberOfZombiesAllowed);
}
else
{
_cachedMaxZombies = 0;
}
}
return _cachedMaxZombies.Value;
}
}
public int MaxSpectres
{
get
{
if (_cachedMaxSpectres == null)
{
Spell s = SpellManager.GetSpell("Raise Spectre");
if (s != null)
{
_cachedMaxSpectres = s.GetStat(StatType.NumberOfSpectresAllowed);
}
else
{
_cachedMaxSpectres = 0;
}
}
return _cachedMaxSpectres.Value;
}
}
/// <summary>
/// Returns a list of zombies.
/// </summary>
public static List<Monster> MyZombies
{
get
{
Spell spell = SpellManager.GetSpell("Raise Zombie");
if (spell == null)
return new List<Monster>();
return spell.DeployedObjects.Select(m => m as Monster).ToList();
}
}
/// <summary>
/// Returns a list of spectres.
/// </summary>
public static List<Monster> MySpectres
{
get
{
Spell spell = SpellManager.GetSpell("Raise Spectre");
if (spell == null)
return new List<Monster>();
return spell.DeployedObjects.Select(m => m as Monster).ToList();
}
}
/// <summary>
/// Returns a list of skeletons.
/// </summary>
public static List<Monster> MySkeletons
{
get
{
Spell spell = SpellManager.GetSpell("Summon Skeletons");
if (spell == null)
return new List<Monster>();
return spell.DeployedObjects.Select(m => m as Monster).ToList();
}
}
// TODO: Will update these later after testing.
private int NumDominatedMonsters
{
get { return LokiPoe.ObjectManager.Objects.OfType<Monster>().Count(m => m.IsValid && m.HasAura("dominated") && m.Reaction == Reaction.Friendly); }
}
private int NumZombies { get { return LokiPoe.ObjectManager.Objects.OfType<Monster>().Count(m => m.IsValid && m.IsFriendly && m.Name == "Raised Zombie" && !m.IsDead); } }
private int NumSkeletons { get { return LokiPoe.ObjectManager.Objects.OfType<Monster>().Count(m => m.IsValid && m.IsFriendly && m.Name == "Summoned Skeleton" && !m.IsDead); } }
private int NumSpectres { get { return LokiPoe.ObjectManager.Objects.OfType<Monster>().Count(m => m.IsValid && m.IsFriendly && m.HasAura("spectre_buff") && !m.IsDead); } }
private static bool ShouldSummonMinion(string spellName, string minionName, StatType maxMinionStat, bool deadOnly, string buffName = null)
{
// Don't let us cast a summon without a minion name, or a buff name (for spectres)
if (minionName == null && buffName == null)
{
return false;
}
int minions =
LokiPoe.ObjectManager.Objects.OfType<Actor>()
.Count(
o =>
o.IsValid &&
o.IsFriendly &&
!o.IsDead &&
// If we need to check by name, do so.
(minionName == null || o.Name == minionName) &&
// Otherwise, check by the buff (spectres)
(buffName == null || o.HasAura(buffName)));
// And calc the number of minions we can have. (TODO: Cache this!!!)
Spell spell = SpellManager.GetSpell(spellName);
int maxMinionsCount = spell.GetStat(maxMinionStat);
bool hasMinionsLeft = minions < maxMinionsCount;
if (deadOnly && hasMinionsLeft)
{
// IsActiveDead checks for dead, unfriendly, memory validity, and excludes tent spawners.
int count = LokiPoe.ObjectManager.Objects.OfType<Monster>().Count(o => o.IsActiveDead && o.Distance < 40);
return count != 0;
}
return hasMinionsLeft;
}
internal static Composite CreateSummonMinionsComposite(string spellName, string minionName, StatType maxMinionStat, bool deadOnly, string buffName = null)
{
return SpellManager.CreateSpellCastComposite(spellName, ctx => ShouldSummonMinion(spellName, minionName, maxMinionStat, deadOnly, buffName), ctx => (ctx as Actor));
}
#endregion
#region LOS/Movement
private readonly Dictionary<string, DateTime> _totemTimers = new Dictionary<string, DateTime>();
internal Composite CreateMoveIntoRange(float range)
{
//// Using some new stuff from the bot!
//return new ActionRunCoroutine(() => GetInRangeCoroutine(range));
return new Decorator(ret => BestTarget.PathDistance() > range /*|| !BestTarget.IsInLineOfSight*/,
CommonBehaviors.MoveTo(ret => BestTarget.Position, ret => "CreateMoveIntoRange"));
}
//private IEnumerator GetInRangeCoroutine(float range)
//{
// if (BestTarget.Distance > range || !BestTarget.IsInLineOfSight)
// {
// Navigator.BeginMoveTo(new MoveCommand(BestTarget.Position, "[Exile CR] MoveIntoRange", null, null));
// }
// Vector2i targetStartLocation = BestTarget.Position;
// while (BestTarget.Distance > range || !BestTarget.IsInLineOfSight)
// {
// // While the best target is outside of whatever defined range, wait until the Navigator moves us into range to attack it.
// yield return LokiCoroutine.EndTick;
// // We started a move command, now, at this point we need to test and see if we need to *switch* the command
// var loc = BestTarget.Position;
// if (loc.DistanceSqr(targetStartLocation) > 5 * 5)
// {
// Navigator.BeginMoveTo(new MoveCommand(BestTarget.Position, "[Exile CR] MoveIntoRange (Target Moved)", null, null));
// // Reset this, so the next "pulse" we'll be checking against the new position.
// targetStartLocation = loc;
// }
// }
// Navigator.Stop();
//}
internal Composite CreateCastTotemComposite(string spellName, float distancePercent, SpellManager.GetSelection<bool> reqs, SpellManager.GetSelection<Vector2i> on)
{
return SpellManager.CreateSpellCastComposite(spellName,
ctx => ShouldCastTotem(spellName, ctx as Actor) && (reqs == null || reqs(ctx)),
ctx => CalculateTotemPlacement(on == null ? BestTarget.Position : on(ctx), distancePercent));
}
private bool ShouldCastTotem(string totemName, NetworkObject target)
{
// IsTotem
// TotemDuration
// TotemRange
//
Spell spell = SpellManager.GetSpell(totemName);
int totemRange = spell.GetStat(StatType.TotemRange);
var spellCastTime = (int) spell.CastTime.TotalMilliseconds;
int maxTotemCount = spell.GetStat(StatType.SkillDisplayNumberOfTotemsAllowed);
/*List<Actor> currentTotems =
LokiPoe.ObjectManager.Objects.OfType<Actor>().Where(
o => o.IsValid && o.Reaction == Reaction.Friendly && o.Name == "Totem" && o.AvailableSpells.Any(s => s.Name == totemName))
.ToList();*/
// New api stuff, yay!
var currentTotems = spell.DeployedObjects;
DateTime lastTime;
_totemTimers.TryGetValue(totemName, out lastTime);
bool shouldcast = lastTime < DateTime.Now && (currentTotems.Count() < maxTotemCount || currentTotems.Any(o => o.Position.Distance(target.Position) > totemRange));
if (shouldcast)
{
DateTime castTime = DateTime.Now.AddMilliseconds(spellCastTime + 500);
if (!_totemTimers.ContainsKey(totemName))
{
_totemTimers.Add(totemName, castTime);
}
else
{
_totemTimers[totemName] = castTime;
}
}
return shouldcast;
}
private Vector2i CalculateTotemPlacement(Vector2i target, float distancePercent)
{
// If we're not normalized to 0-1, normalize it. (People like to use either 0.75 or 75 for values)
if (distancePercent > 1f)
{
distancePercent /= 100f;
}
Vector2 myPos = LokiPoe.ObjectManager.Me.WorldPosition;
Vector2 theirPos = target.MapToWorld();
// 43 * 0.75
float distance = myPos.Distance(theirPos) * distancePercent;
Vector2 direction = theirPos - myPos;
direction.Normalize();
// So the direction is now normalized to 0-1.
// We can apply our distance percentage to this.
direction *= distance;
// Convert from world -> map. We only used the world coordinates for float precision.
return (myPos + direction).WorldToMap();
}
#endregion
#region Utilities
private bool HasAura(Actor actor, string auraName, int minCharges = -1, double minSecondsLeft = -1)
{
Aura aura = actor.Auras.FirstOrDefault(a => a.Name == auraName || a.InternalName == auraName);
// The actor doesn't even have the aura, so we don't need to go messing with it. :)
if (aura == null)
{
return false;
}
// Check if mincharges needs to be ensured
if (minCharges != -1)
{
// This is an exclusive check. So if we pass 3, we want to ensure we have 3 charges up.
// Thus; 2 < 3, we don't have enough charges, and therefore we "don't have the aura" yet
if (aura.Charges < minCharges)
{
return false;
}
}
// Those with R# installed, can ignore the following error.
// ReSharper disable once CompareOfFloatsByEqualityOperator
if (minSecondsLeft != -1)
{
if (aura.TimeLeft.TotalSeconds < minSecondsLeft)
{
return false;
}
}
// We have the aura.
// We have enough charges.
// And the time left is above the min seconds threshold.
return true;
}
/// <summary> Evaluate cluster size. </summary>
/// <remarks> Nesox, 2013-07-27. </remarks>
/// <exception cref="NotImplementedException">
/// Thrown when the requested operation is
/// unimplemented.
/// </exception>
/// <param name="inputPoint"> The input point. </param>
/// <param name="positions"> The positions. </param>
/// <param name="clusterType"> Type of the cluster. </param>
/// <param name="clusterRange"> The cluster range. </param>
/// <returns> . </returns>
internal static int EvaluateClusterSize(Vector2i inputPoint, IEnumerable<Vector2i> positions, ClusterType clusterType,
float clusterRange)
{
switch (clusterType)
{
case ClusterType.Radius:
return
positions.Count(
v => v.DistanceSqr(inputPoint) < clusterRange * clusterRange);
default:
throw new NotImplementedException("Operation currently not implemented.");
}
}
/// <summary> Gets cluster count. </summary>
/// <remarks> Nesox, 2013-07-27. </remarks>
/// <param name="target"> Target for the. </param>
/// <param name="type"> The type. </param>
/// <param name="clusterRange"> The cluster range. </param>
/// <returns> The cluster count. </returns>
public static int GetClusterCount(Actor target, ClusterType type, float clusterRange)
{
IEnumerable<Vector2i> positions = Targeting.Combat.Targets.Select(e => e.Position);
int count = EvaluateClusterSize(target.Position, positions, type, clusterRange);
return count;
}
/// <summary>
/// Returns whether or not the specified count of mobs are near the specified monster, within the defined range.
/// </summary>
/// <param name="monster"></param>
/// <param name="distance"></param>
/// <param name="count"></param>
/// <returns></returns>
private bool NumberOfMobsNear(NetworkObject monster, float distance, int count, bool dead = false)
{
if (monster == null)
{
return false;
}
Vector2i mpos = monster.Position;
int curCount = 0;
foreach (Monster mob in Targeting.Combat.Targets.OfType<Monster>())
{
if (mob.Id == monster.Id)
{
continue;
}
// If we're only checking for dead mobs... then... yeah...
if (dead)
{
if (!mob.IsDead)
continue;
}
else if (mob.IsDead)
continue;
if (mob.Position.Distance(mpos) < distance)
{
curCount++;
}
if (curCount >= count)
{
return true;
}
}
return false;
}
#endregion
}
#region Spell Implementation
public partial class Exile
{
private void RegisterTotems()
{
RegisterTotem("Decoy Totem");
RegisterTotem("Devouring Totem");
RegisterTotem("Flame Totem");
RegisterTotem("Rejuvenation Totem");
RegisterTotem("Shockwave Totem");
RegisterTotem("Searing Bond");
// These are commonly used spell totem skills. Ideally, we'd want to register every
// skill there is, but Exile is already in need of some revamps.
RegisterTotem("Spark");
RegisterTotem("Summon Skeletons");
RegisterTotem("Raise Zombie");
RegisterTotem("Raise Spectre");
RegisterTotem("Lightning Warp");
RegisterTotem("Incinerate");
}
private void RegisterBuffs()
{
RegisterBuff("Molten Shell",
ret => !LokiPoe.ObjectManager.Me.HasAura("fire_shield") && (!_MoltenShellCd.IsRunning || (_MoltenShellCd.IsRunning && _MoltenShellCd.ElapsedMilliseconds > 5000))
);
BuffComposite.AddChild(new Decorator(ret => !_MoltenShellCd.IsRunning,
new Action(ret => { _MoltenShellCd.Start(); })));
BuffComposite.AddChild(
new Decorator(ret => _MoltenShellCd.IsRunning && _MoltenShellCd.ElapsedMilliseconds > 5000,
new Action(ret => { _MoltenShellCd.Restart(); })));
RegisterBuff("Arctic Armour", ret => !LokiPoe.ObjectManager.Me.HasAura("ice_shield"));
//RegisterBuff("Blood Rage", ret => !LokiPoe.ObjectManager.Me.HasAura("blood_rage"));
//RegisterBuff("Righteous Fire", ret => !LokiPoe.ObjectManager.Me.HasAura("righteous_fire"));
// Experimental!
RegisterBuff("Enduring Cry",
ret =>
!HasAura(Me, "endurance_charge", MaxEnduranceCharges, 3) && NumberOfMobsNear(Me, 30, 1) &&
!_EnduringCryCd.IsRunning || (_EnduringCryCd.IsRunning && _EnduringCryCd.ElapsedMilliseconds > 4500));
BuffComposite.AddChild(new Decorator(ret => !_EnduringCryCd.IsRunning,
new Action(ret => { _EnduringCryCd.Start(); })));
BuffComposite.AddChild(
new Decorator(ret => _EnduringCryCd.IsRunning && _EnduringCryCd.ElapsedMilliseconds > 4500,
new Action(ret => { _EnduringCryCd.Restart(); })));
}
private void RegisterCurses()
{
RegisterCurse("Temporal Chains", "curse_temporal_chains");
RegisterCurse("Punishment", "curse_punishment");
RegisterCurse("Warlord's Mark", "curse_drain_essence");
RegisterCurse("Projectile Weakness", "curse_projectile_weakness");
RegisterCurse("Conductivity", "curse_lightning_weakness");
RegisterCurse("Enfeeble", "curse_enfeeble");
RegisterCurse("Frostbite", "curse_cold_weakness");
RegisterCurse("Flammability", "curse_fire_weakness");
RegisterCurse("Elemental Weakness", "curse_elemental_weakness");
RegisterCurse("Critical Weakness", "curse_critical_weakness");
RegisterCurse("Vulnerability", "curse_vulnerability");
}
private void RegisterSummons()
{
RegisterSummon("Raise Zombie", () => NumZombies, () => MaxZombies, true);
RegisterSummon("Raise Spectre", () => NumSpectres, () => MaxSpectres, true); // TODO: add spectre selection logic
RegisterSummon("Summon Skeletons",
() => NumSkeletons,
() => MaxSkeletons,
false,
extraReqs: o => NumberOfMobsNear(BestTarget, 20, 3) || BestTarget.Rarity >= Rarity.Rare);
// Due to the mechanics of this skill, it's not being added automatically.
//Register("Summon Raging Spirit");
}
private void RegisterTraps()
{
RegisterTrap("Bear Trap", ret => BestTarget.Rarity >= Rarity.Rare);
RegisterTrap("Fire Trap");
RegisterTrap("Conversion Trap");
RegisterTrap("Lightning Trap");
RegisterTrap("Freeze Mine");
RegisterTrap("Smoke Mine");
}
private void RegisterMainAbilities()
{
// TODO: Some users might want to use these as Nukes, so you'll have to add the conditions
// for using them on bosses, etc.. For now, the bot will just use them as they are available.
// Please report any oddities with Vaal skill usage, as they are new and more changes might be
// needed to handle them.
Register("Vaal Immortal Call", ret =>
{
if (HasAura(LokiPoe.ObjectManager.Me, "endurance_charge", MaxEnduranceCharges))
{
return true;
}
return false;
});
Register("Vaal Detonate Dead", ret => NumberOfMobsNear(BestTarget, 15, 1, true));
Register("Vaal Arc");
Register("Vaal Fireball");
Register("Vaal Spark");
Register("Vaal Power Siphon");
Register("Vaal Cold Snap");
Register("Vaal Ice Nova");
Register("Vaal Burning Arrow");
Register("Vaal Rain of Arrows");
Register("Vaal Molten Shell");
Register("Vaal Lightning Warp");
Register("Vaal Spectral Throw");
Register("Vaal Cyclone");
Register("Vaal Ground Slam");
Register("Vaal Lightning Strike");
Register("Vaal Heavy Strike");
Register("Vaal Double Strike");
Register("Discharge", ret =>
{
// Since the skill has a range, and the client doesn't move you to the target, don't
// cast it when it's out of range. You might want to lower your combat range in this CR
// to keep the mobs close by. The current code is CreateMoveIntoRange(45) and that's what
// you'd want to lower.
if (BestTarget.Distance > 20)
{
return false;
}
// Use discharge when we have the max amount of any type of charge.
if (HasAura(LokiPoe.ObjectManager.Me, "frenzy_charge", MaxFrenzyCharges))
{
return true;
}
if (HasAura(LokiPoe.ObjectManager.Me, "power_charge", MaxPowerCharges))
{
return true;
}
if (HasAura(LokiPoe.ObjectManager.Me, "endurance_charge", MaxEnduranceCharges))
{
return true;
}
return false;
});
Register("Immortal Call", ret =>
{
if (HasAura(LokiPoe.ObjectManager.Me, "endurance_charge", MaxEnduranceCharges))
{
return true;
}
return false;
});
// adding Frenzy to build up and keep charges after the first batch, make it refresh at 3 seconds left
Register("Frenzy", ret => !HasAura(LokiPoe.ObjectManager.Me, "frenzy_charge", MaxFrenzyCharges, 3));
// replacing leap-slam-damage-deal-stile with movement-style
Register("Leap Slam", ret => BestTarget.Distance > 40 && BestTarget.IsInLineOfSight);
// Power Siphon for insta-kill purposes.
Register("Power Siphon", ret => BestTarget.HealthPercent <= 10);
Register("Sweep", ret => NumberOfMobsNear(BestTarget, 10, 1));
Register("Cyclone", ret => NumberOfMobsNear(BestTarget, 10, 1));
Register("Detonate Dead", ret => NumberOfMobsNear(BestTarget, 15, 1, true));
Register("Lightning Arrow", ret => NumberOfMobsNear(BestTarget, 30, 1));
Register("Rain of Arrows", ret => NumberOfMobsNear(BestTarget, 10, 1));
Register("Split Arrow", ret => NumberOfMobsNear(BestTarget, 10, 1));
Register("Arc", ret => NumberOfMobsNear(BestTarget, 10, 1));
Register("Firestorm", ret => NumberOfMobsNear(BestTarget, 10, 2));
Register("Ice Nova", ret => NumberOfMobsNear(LokiPoe.ObjectManager.Me, 10, 2));
Register("Shock Nova", ret => NumberOfMobsNear(LokiPoe.ObjectManager.Me, 10, 2));
// Dump all our frenzy charges on flicker strike if we can.
Register("Flicker Strike", ret => BestTarget.IsInLineOfSight && HasAura(LokiPoe.ObjectManager.Me, "frenzy_charge", MaxFrenzyCharges));
Register("Incinerate", ret => BestTarget.Distance < 40);
// making use of heavy strike for rare monsters
Register("Heavy Strike", ret => BestTarget.Rarity >= Rarity.Rare);
Register("Viper Strike",
ret =>
BestTarget.Rarity >= Rarity.Rare &&
!HasAura(BestTarget, "viper_strike_orb", BestTarget.GetStat(StatType.MaxViperStrikeOrbs)));
Register("Glacial Hammer", ret => NumberOfMobsNear(BestTarget, 12, 2));
Register("Ground Slam", ret => NumberOfMobsNear(BestTarget, 20, 1));
Register("Storm Call");
// This skill requires some tricky logic to do correctly, so it'll be added later.
//Register("Flameblast");
Register("Summon Raging Spirit");
Register("Barrage");
Register("Burning Arrow");
Register("Explosive Arrow");
Register("Ice Shot");
Register("Poison Arrow");
Register("Arctic Breath");
Register("Fireball");
Register("Freezing Pulse");
Register("Ice Spear");
Register("Spark");
Register("Arc");
Register("Spectral Throw", ret => NumberOfMobsNear(BestTarget, 10, 2));
Register("Ethereal Knives");
// Power Siphon for aoe purposes
Register("Power Siphon",
ret =>
(BestTarget.Rarity <= Rarity.Magic && NumberOfMobsNear(BestTarget, 15, 1)) ||
NumberOfMobsNear(BestTarget, 15, 2));
//Register("Frost Wall"); // TODO: this one is more of an "oh shit" kind of thing. Not sure how to proceed with it.
// And from here down, are melee abilities.
// So we need to ensure we're in range.
//CombatComposite.AddChild(CreateMoveIntoRange(10));
Register("Puncture", ret => !BestTarget.HasAura("puncture"));
Register("Dominating Blow", ret => NumDominatedMonsters == 0);
// TODO: Check if we have a minion turned or not
//Register("Reave");
Register("Reave", ret => NumberOfMobsNear(BestTarget, 10, 1));
Register("Cleave");
Register("Infernal Blow");
Register("Lightning Strike");
Register("Shield Charge");
Register("Double Strike");
Register("Dual Strike");
Register("Elemental Hit");
//Register("Flicker Strike");
Register("Whirling Blades");
// These are being added here to register non-requirement skills so people's bot actually attacks
// if they happen to use only skills that have requirements.
Register("Frenzy");
Register("Spectral Throw");
Register("Sweep");
Register("Cyclone");
Register("Detonate Dead");
Register("Lightning Arrow");
Register("Rain of Arrows");
Register("Split Arrow");
Register("Arc");
Register("Firestorm");
Register("Ice Nova");
Register("Shock Nova");
Register("Incinerate");
Register("Ground Slam");
Register("Glacial Hammer");
Register("Heavy Strike");
Register("Cold Snap");
Register("Power Siphon");
//Register("Leap Slam"); // Taking this out due to the LoS issues.
// And fall back to the default if we can...
Register("Default Attack");
}
}
#endregion
#region CombatRoutine Implementations
public partial class Exile : CombatRoutine
{
private Player Me { get { return LokiPoe.ObjectManager.Me; } }
private PrioritySelector BuffComposite { get; set; }
private PrioritySelector CombatComposite { get; set; }
//private PerFrameCachedValue<Monster> _bestTargetCached;
//private int _bestTargetIdCached;
/// <summary>
/// Returns the first object in the combat targeting list as a Monster object.
/// </summary>
public Monster BestTarget
{
get
{
return Targeting.Combat.Targets.FirstOrDefault() as Monster;
}
}
private int _bestTargetId;
/// <summary>
/// Returns a consistent target from the target list.
/// </summary>
public Monster StableBestTarget
{
get
{
var targets = Targeting.Combat.Targets;
// If we have an id stored, validate it first.
if (_bestTargetId != 0)
{
// The last target is now gone, so reset the id.
if (!targets.Any(t => t.Id == _bestTargetId))
{
_bestTargetId = 0;
}
}
// If no id is set, take the first target's id.
if (_bestTargetId == 0)
{
_bestTargetId = targets.FirstOrDefault().Id;
}
// Return the monster with the consistent id we have.
return targets.FirstOrDefault(t => t.Id == _bestTargetId) as Monster;
}
}
#region Overrides of CombatRoutine
private bool _eventsHooked;
/// <summary> Gets the name. </summary>
/// <value> The name. </value>
public override string Name { get { return "Exile"; } }
/// <summary> Gets the buff behavior. </summary>
/// <value> The buff composite. </value>
public override Composite Buff { get { return BuffComposite; } }
/// <summary> Gets the combat behavior. </summary>
/// <value> The combat composite. </value>
public override Composite Combat { get { return CombatComposite; } }
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
/// <filterpriority>2</filterpriority>
public override void Dispose()
{
}
public void SwapWeapons()
{
LokiPoe.Input.PressKey(LokiPoe.ConfigManager.GetActionKey(ActionKey.WeaponSwap).Item1);
}
/// <summary> Initializes this <see cref="CombatRoutine" />. </summary>
public override void Initialize()
{
if (!_eventsHooked)
{
// When we start the bot, we need to re-register all the available spells. This allows us to swap out skills
// Without restarting the bot.
BotMain.OnStart += bot => DoSpellRegistrations();
//DoSpellRegistrations();
_eventsHooked = true;
// This is code people can refer to for doing some more advanced stuff with the CRs.
// Please see this post:
// http://www.thebuddyforum.com/exilebuddy-forum/140618-exilebuddy-release-beta-revision-guide-10.html#post1398840
#region UseAtYourOwnRisk
// This *has* to be set if you want a bot that works.
//Targeting.Combat.UsingCustomInclusionCalcuation = true;
// Add your custom targeting inclusion filter. Anything that returns false,
// will be ignored, even if our default filter would have included it.
//Targeting.Combat.InclusionCalcuation += MyTargetingInclusionCalculator;
#endregion
}
}
private void DoSpellRegistrations()
{
CombatComposite = new PrioritySelector(context => BestTarget);
BuffComposite = new PrioritySelector(context => BestTarget);
// If the BestTarget is null, we should not do anything.
CombatComposite.AddChild(
new Decorator(ret => BestTarget == null,
new Action(ret => RunStatus.Success))
);
// Ensure we add the flask logic *first*
// We don't want to be trying to do buffs and whatnot, if we have to pop a flask.
BuffComposite.AddChild(CreateFlaskLogic());
CombatComposite.AddChild(CreateMoveIntoRange(45));
// Support for the proximity shield mobs.
// Quite annoying when they have this!
CombatComposite.AddChild(
new Decorator(ret => BestTarget.HasAura("proximity_shield_aura"),
CreateMoveIntoRange(10)));
Log.Debug("Registering buffs.");
RegisterBuffs();
Log.Debug("Registering curses.");
RegisterCurses();
Log.Debug("Registering summons.");
RegisterSummons();
Log.Debug("Registering totems.");
RegisterTotems();
Log.Debug("Registering traps.");
RegisterTraps();
Log.Debug("Registering main abilities.");
RegisterMainAbilities();
}
#endregion
}
#endregion
#region UI Goodness
public class ConfigWindow : Window
{
}
#endregion
// This is code people can refer to for doing some more advanced stuff with the CRs.
#region UseAtYourOwnRisk
public partial class Exile
{
private const int DistanceFromMeSetting = 75;
private const int DistanceFromOthersSetting = 30;
private const int ClusterSizeSetting = 3;
/// <summary>
/// Maps the distance between two objects.
/// </summary>
private readonly static Dictionary<int, Dictionary<int, int>> _objectToObjectDistances = new Dictionary<int, Dictionary<int, int>>();
/// <summary>
/// This holds an area specific cache of objects we should be including. This setup won't be perfect due to random id reuse issues, but it
/// addresses the issue of when you have a cluster of mobs, and don't store which ids you should kill, the re-evaluation will ignore the now
/// broken cluster.
/// </summary>
private readonly static PerAreaCachedValue<Dictionary<int, float>> _objectInclusionWeights = new PerAreaCachedValue<Dictionary<int, float>>(
() =>
{
return new Dictionary<int, float>();
});
/// <summary>
/// To take advantage of caching, so we don't do this logic for every single target, which would be terrible performance, we use a PerFrameCachedValue,
/// and perform the update logic once, and cache the results for the rest of the frame. There might be some better optimizations to do, but this is
/// the core logic of what needs to be done. You could even cache this based on time as well for even better improvements
/// </summary>
private static PerFrameCachedValue<List<Monster>> _targetsToConsider = new PerFrameCachedValue<List<Monster>>(() =>
{
_objectToObjectDistances.Clear();
// Active monster list. Note, we can't use Targeting.Combat.Targets, because this *is*
// going to be called to generate the list of targets for Combat!
var targets = LokiPoe.ObjectManager.Objects.OfType<Monster>().Where(o => o.IsActive && o.Distance <= DistanceFromMeSetting).ToList();
foreach (var target in targets)
{
foreach (var otherTarget in targets)
{
// Don't consider itself.
if (target == otherTarget)
continue;
Dictionary<int, int> destination;
// If we don't have an entry for the id yet, we need to add the second dictionary.
if (!_objectToObjectDistances.TryGetValue(target.Id, out destination))
{
destination = new Dictionary<int, int>();
_objectToObjectDistances.Add(target.Id, destination);
}
// Track the distance.
destination[otherTarget.Id] = target.Position.Distance(otherTarget.Position);
}
}
// Now for the logic processing.
foreach (var kvp1 in _objectToObjectDistances)
{
var ids = new List<int>();
// Create a list of ids of objects close enough to us. This can be done with Linq, but I'm
// coding this in Exile, and don't have R# enabled for it.
foreach (var kvp2 in kvp1.Value)
{
if (kvp2.Value <= DistanceFromOthersSetting)
{
ids.Add(kvp2.Key);
}
}
// Check to see if we have the right cluster size. We have to add + 1 for the original target!
if (ids.Count + 1 >= ClusterSizeSetting)
{
// If so, we can either calculate and cache some weights, or just leave it as 1 so the logic will pull it.
_objectInclusionWeights.Value[kvp1.Key] = 1.0f;
foreach (var id in ids)
{
_objectInclusionWeights.Value[id] = 1.0f;
}
}
}
// Finally, we return a list of objects that match the criteria. For the cached objects from before
// a cluster is dispersed, they are still included since we don't clear _objectInclusionWeights.
return LokiPoe.ObjectManager.Objects.OfType<Monster>().Where(o => _objectInclusionWeights.Value.ContainsKey(o.Id)).ToList();
});
bool MyTargetingInclusionCalculator(NetworkObject obj)
{
var m = obj as Monster;
if (m == null)
{
return false;
}
// Always target magic, rare, uniques!
if (m.Rarity >= Rarity.Magic)
return true;
return _targetsToConsider.Value.Contains(obj);
}
}
#endregion
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment