-
-
Save anonymous/5dd561083d9d465bc08d to your computer and use it in GitHub Desktop.
Modified Exile.cs for flasks that dispel.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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