Jump to content

Recommended Posts

Original Thread Topic:

I did not want to necro an old thread, but I have included a timer into the ability Holy Light from Paladin spellbook.
However, I do not think that it works too well when you are being attacked while healing and it bumps your cast time
backwards. If I pulled 3 mobs and wanted to check my fight class to heal at or below 50% health, and I am still being
attacked by the mobs, my cast bar gets knocked backwards which increases the cast time. Is there any way to counter
this issue?

Thanks!

Link to comment
https://wrobot.eu/forums/topic/6644-heals-casting-twice-with-timer/
Share on other sites

Hello, you can use the "ObjectManager" and filter it with linq.

I guess it will be used often, so I've made a method for it:

    /// <summary>
    /// Get attacking units from another unit
    /// </summary>
    /// <param name="from">Take hostile units from this unit</param>
    /// <param name="maxRange">Take hostile units within this range</param>
    /// <returns></returns>
    public List<WoWUnit> AttackingUnits(WoWUnit from, float maxRange = 10)
    {
        return ObjectManager.GetWoWUnitHostile().Where(i =>
            from.Guid == i.TargetObject.Guid
            && from.Position.DistanceTo(i.Position) <= maxRange
        ).ToList();
    }

and use it in another method:

            if (condition1
                && ...
                && AttackingUnits(ObjectManager.Me, 15).Count < 3
                )
            {
                //cast spell
            }

 

Thanks Reapler, I already have functions to check hostile units in range, stunned units within range, etc. The issue is
why does WRobot cast the ability twice in a row even though my health percent is above my threshold?
 

if (player.HealthPercent <= 50 && HolyLightTimer.IsReady)
        {
            UseSpell(HolyLight, true, false, 30);
            HolyLightTimer = new Timer(3000); // Holy Light cast time = 2.5 seconds
        }

The problem is, when casting and you take a hit, your cast bar subtracts value thus the timer is rendered useless.
Let's say I pulled 3 hostile units and my health dropped to 48%. I will start to cast Holy Light as intended. However,
the tigers are still attacking me during my heal. If my heal was almost done at 90%, the hostile unit attack from one
unit will knock the 90% back down to say 85%. Then if I get hit by the second hostile unit, it will bump down from 85%,
to 80%. The 3000 millisecond timer most likely is ready at this point, thus I cast Holy Light a second time, even though
my Health percent = 90%.

I think you need to provide me the header of your "UseSpell" method before i can give a well answer. it could be look like this:

public void UseSpell(bool stopMove, bool waitIsCast = true, bool ignoreIfCast = false)

 

protected void UseSpell(Spell spell, bool stopMoving, bool faceTarget = false, float rangeCheck = 5)
    {
        if (
                !player.IsMounted && spell.KnownSpell && spell.IsSpellUsable && spell.IsDistanceGood &&     // Not mounted | Spell Known/Usable/Good Distance
                player.TargetObject.IsAlive && player.TargetObject.IsAttackable &&                          // Target is alive | attackable
                target.GetDistance <= rangeCheck                                                            // Target is within given range
            )
        {

            if (faceTarget)
            {
                MovementManager.Face(player.TargetObject);
            }

            spell.Launch(stopMoving);
        }
    }

 

Personally i wouldn't use a timer like this. It's better to bind a timer on "SPELL_CAST_SUCCESS" with events and add delay on it(the purpose was another).

To your problem:

Your "spell.Launch(stopMoving);" in the method is causing now a own loop, because the parameter "waitIsCast is set to true".

This means while you are casting holylight, and you have no conditions to check your current cast, then the next call of "UseSpell()" will be executed.

If it's called and the character is still casting holy light, it will wait til the current cast is finished. If it's finished the loop in ".Launch();" will break and execute another "SpellManager.CastSpellByNameLUA(this.NameInGame);"

Maybe also add "!ObjectManager.Me.IsCast" otherwise if the character is still casting another unnecessary spell, you may need to add the mentioned timer or reduce tickspeed of your rotation.

If you would like to add the timer, feel free to ask.

 

Edit:

solution would be "spell.Launch(stopMoving, false);" instead of the other launch call

  • 4 weeks later...

Hello, i've created a small example for this, everything else is explained in the comments:

using System;
using System.Collections.Generic;
using System.Threading;
using wManager.Plugin;
using wManager.Wow.Class;
using wManager.Wow.Enums;
using wManager.Wow.Helpers;
using wManager.Wow.ObjectManager;
using static robotManager.Helpful.Logging;

public class Main : IPlugin
{
    #region Variables

    private bool _isLaunched;
    private int _msDelay = 200;
    private DateTime _unlockTime = DateTime.Now;
    private readonly HashSet<string> _noGcdSpells = new HashSet<string>
    {
        "Counterspell",
        //...
        //http://wowwiki.wikia.com/wiki/Cooldown => Abilities noted for not affecting nor being affected by the global cooldown:
    };

    #endregion




    #region Properties

    public bool GcdActive => DateTime.Now < _unlockTime;

    #endregion




    #region WRobot Interface
    
    public void Initialize()
    {
        var pGuid = ToWoWGuid(ObjectManager.Me.Guid);
        EventsLuaWithArgs.OnEventsLuaWithArgs += delegate(LuaEventsId id, List<string> args)
        {
            if (
                id == LuaEventsId.COMBAT_LOG_EVENT_UNFILTERED
                && args[2] == pGuid
                && !_noGcdSpells.Contains(args[9])
                && 
                (
                    args[1] == "SPELL_CAST_SUCCESS"
                    ||
                    args[1] == "SPELL_HEAL"
                    ||
                    args[1] == "SPELL_DAMAGE"
                )
                )
            {
                Write("lock");
                _unlockTime = DateTime.Now.AddMilliseconds(_msDelay);
            }
        };
        _isLaunched = true;
        Write("Loaded");
        while (_isLaunched)
        {
            try
            {
                if (Conditions.ProductIsStartedNotInPause)
                {
                    Pulse();
                }
            }
            catch (Exception e)
            {
                WriteError(e.ToString());
            }
            Thread.Sleep(30);
        }
    }

    public void Dispose()
    {
        _isLaunched = false;
    }
    
    public void Settings()
    {

    }

    #endregion




    #region Pulse

    public void Pulse()
    {
        var spell = new Spell("Lesser Heal");
        if (!GcdActive 
            && spell.IsSpellUsable
            && !ObjectManager.Me.IsCast
            && ObjectManager.Me.TargetObject.HealthPercent <= 90
            )
        {
            spell.Launch(false, false); //will cast the heal if "_msDelay" was passed
        }


/*        if (spell.IsSpellUsable
            && !ObjectManager.Me.IsCast
            && ObjectManager.Me.TargetObject.HealthPercent <= 90)
        {
            spell.Launch(false, false); //.IsSpellUsable is true after gcd was passed but the heal itself can delay
                                        //and cause double heal cast if no additional delay is added like above
                                        //so ObjectManager.Me.TargetObject.HealthPercent <= 90 would be true for a short time
        }*/
    }

    #endregion




    #region Methods

    public string ToWoWGuid(ulong guid)
    {
        var wowGuid = ObjectManager.Me.Guid.ToString("x").ToUpper();
        var c = 16 - wowGuid.Length;
        for (var i = 0; i < c; i++)
        {
            wowGuid = "0" + wowGuid;
        }
        return "0x" + wowGuid;
    }

    #endregion
    
}

if you are going to lower the tickspeed this will rarer happens, but it can still happen. Instant spells aren't included.

That's wild! Thank you for the exuberant example. I will need to look this over a few times to try and wrap my head around it.  Why is this sort of thing not already implemented into WRobot? Also, you defined _msDelay. Where does this number come from? 

Thank you again!

17 minutes ago, Apexx said:

That's wild! Thank you for the exuberant example. I will need to look this over a few times to try and wrap my head around it.  Why is this sort of thing not already implemented into WRobot? Also, you defined _msDelay. Where does this number come from? 

It's the wait time after each cast. You may set this to 50ms - 200ms. But i think 50ms should still be ok on low tick speed.

#region Properties 
     public bool GcdActive => DateTime.Now < _unlockTime; 
#endregion

Error    1    ; expected
Error    2    Invalid token ';' in class, struct, or interface member declaration
Error    3    Syntax error, '>' expected
Error    4    'Main._unlockTime' is a 'field' but is used like a 'type'
Error    5    'System.DateTime.Now' is a 'property' but is used like a 'type'

39 minutes ago, Apexx said:

#region Properties 
     public bool GcdActive => DateTime.Now < _unlockTime; 
#endregion

Error    1    ; expected
Error    2    Invalid token ';' in class, struct, or interface member declaration
Error    3    Syntax error, '>' expected
Error    4    'Main._unlockTime' is a 'field' but is used like a 'type'
Error    5    'System.DateTime.Now' is a 'property' but is used like a 'type'

I forgot to mention that i compiled it with a newer C# version in Vs. For example in C#6 you can import static type members into namespace.

This version should also work with the older version as ".cs" file:

Spoiler

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using wManager.Plugin;
using wManager.Wow.Class;
using wManager.Wow.Enums;
using wManager.Wow.Helpers;
using wManager.Wow.ObjectManager;
using robotManager.Helpful;

public class Main : IPlugin
{
    #region Variables

    private bool _isLaunched;
    private int _msDelay = 150;
    private DateTime _unlockTime = DateTime.Now;
    private readonly HashSet<string> _noGcdSpells = new HashSet<string>
    {
        "Counterspell",
        //...
        //http://wowwiki.wikia.com/wiki/Cooldown => Abilities noted for not affecting nor being affected by the global cooldown:
    };

    #endregion




    #region Properties

        public bool GcdActive
    {
        get
        {
            return DateTime.Now < _unlockTime;
        }
    }

    #endregion




    #region WRobot Interface
    
    public void Initialize()
    {
        var pGuid = ToWoWGuid(ObjectManager.Me.Guid);
        EventsLuaWithArgs.OnEventsLuaWithArgs += delegate(LuaEventsId id, List<string> args)
        {
            if (
                id == LuaEventsId.COMBAT_LOG_EVENT_UNFILTERED
                && args[2] == pGuid
                && !_noGcdSpells.Contains(args[9])
                && 
                (
                    args[1] == "SPELL_CAST_SUCCESS"
                    ||
                    args[1] == "SPELL_HEAL"
                    ||
                    args[1] == "SPELL_DAMAGE"
                )
                )
            {
                Logging.Write("lock");
                _unlockTime = DateTime.Now.AddMilliseconds(_msDelay);
            }
        };
        _isLaunched = true;
        Logging.Write("Loaded");
        while (_isLaunched)
        {
            try
            {
                if (Conditions.ProductIsStartedNotInPause)
                {
                    Pulse();
                }
            }
            catch (Exception e)
            {
                Logging.WriteError(e.ToString());
            }
            Thread.Sleep(30);
        }
    }

    public void Dispose()
    {
        _isLaunched = false;
    }
    
    public void Settings()
    {

    }

    #endregion




    #region Pulse

    public void Pulse()
    {
        var spell = new Spell("Lesser Heal");
        if (!GcdActive 
            && spell.IsSpellUsable
            && !ObjectManager.Me.IsCast
            && ObjectManager.Me.TargetObject.HealthPercent <= 95
            )
        {
            spell.Launch(false, false); //will cast the heal if "_msDelay" was passed
        }

        

/*        if (spell.IsSpellUsable
            && !ObjectManager.Me.IsCast
            && ObjectManager.Me.TargetObject.HealthPercent <= 95)
        {
            spell.Launch(false, false); //.IsSpellUsable is true after gcd was passed but the heal itself can delay
                                        //and cause double heal cast if no additional delay is added like above
                                        //so ObjectManager.Me.TargetObject.HealthPercent <= 90 would be true for a short time
        }*/
    }

    #endregion




    #region Methods

    public string ToWoWGuid(ulong guid)
    {
        var wowGuid = ObjectManager.Me.Guid.ToString("x").ToUpper();
        var c = 16 - wowGuid.Length;
        for (var i = 0; i < c; i++)
        {
            wowGuid = "0" + wowGuid;
        }
        return "0x" + wowGuid;
    }

    #endregion
    
}

 

 

Okay that did the trick! Thank you very much! I was wondering one other thing.

The main reason that I wanted a slight wait time between certain spells (Spammable spells mainly IE: Priest Smite) is to check the target
unit health percentage between casts to determine other abilities based on the target hp % or even the player's hp %.

Is it possible to adjust the msDelay value based on the spell method used?

If you creating healing rotation from my own perspective is always better to use lower than < condition and then make a priority with strongest spell on top, the limits with >= are not so good ;) If you don't know how to make simple priority system ask me.

On 22.08.2017 at 8:34 PM, Apexx said:

It still is casting Lesser Heal twice even though my heath is >= to the determined value set in my settings. It was working fine with my spell Timer before.

The simple conditions like this seems working for me:
 

if (ObjectManager.Me.HealthPercent < 40 && !ObjectManager.Me.IsCast)
{
	Spell.Launch(true, true, false);
}


UPD: No, i lied, not working, still casting twice

I also noticed that the event with gcd lock sometimes react slow. But this works fine for me:

    public void Pulse()//pulse every 50ms
    {
        if (Target.HealthPercent <= 95)
        {
            new Spell("Flash of Light").Launch();//waitIsCast is true => no pulse while casting
            Logging.Write("cast done\nwait 5 seconds");
            Thread.Sleep(5000);
            Logging.Write("let it pulse again");
        }
        Logging.Write("pulse");
    }

or if you don't want to block pulse and only want to lock the spell cast:

    private bool _folCast;
    public void Pulse()
    {
        if (Target.HealthPercent <= 95 && !_folCast)
        {
            _folCast = true;
            Task.Run(delegate
            {
                new Spell("Flash of Light").Launch();//waitIsCast is true => the task have to wait while casting
                Logging.Write("cast done\nset _folCast to false in 5 seconds");
                Thread.Sleep(5000);
                _folCast = false;
                Logging.Write("_folCast is set to false");
            });
        }
        Logging.Write("pulse");
    }

Of course it needs more checks to the cast, but you get the basic idea.

 

The used initialize method:

    public void Initialize()
    {
        _isLaunched = true;
        Logging.Write("Loaded");
        while (_isLaunched)
        {
            try
            {
                if (Conditions.ProductIsStartedNotInPause)
                {
                    Pulse();
                }
            }
            catch (Exception e)
            {
                Logging.WriteError(e.ToString());
            }
            Thread.Sleep(50);//lower values will cause rather double casts
        }
    }

 

If you use Task.Run for other stuff, ensure it won't trigger multiple times in a loop.

If it's still double cast in your fightclass, you should check your routine flow while running.

 

Edited by reapler

Timer Declaration

public static Timer waitTime = new Timer();

Asynchronous Task UseCombatSpell

public static async Task<bool> UseCombatSpell(Spell spell, bool stopMoving, bool faceTarget = false, float rangeCheck = float.MaxValue, int castWait = 0)
{
	// Spell validation
	if (Methods.IsValid(MyTarget, spell))
	{
		// Launch the spell
		if (!Me.IsCast && waitTime.IsReady)
		{
			// Face the target
			if (faceTarget)
				MovementManager.Face(Me.TargetObject);

			Interact.InteractGameObject(MyTarget.GetBaseAddress, stopMoving);
			SpellManager.CastSpellByNameLUA(spell.Name);

			// Wait for cooldown + latency
			await Task.Delay(SpellManager.GetSpellCooldownTimeLeft(spell.Id) + Usefuls.Latency);
			
			// Create a new timer for the desired wait time between casts.
			waitTime = new Timer(castWait);	
			Methods.LogFight("Wait time = " + waitTime.TimeLeft());
			return true;
		}
		return false;
	}
	return false;
}

Please note, that I removed the rangeCheck portion of code from the above method.

Usage:

if (await Abilities.LesserHeal()) return true;	// Lesser Heal


Methods.IsValid is basically making sure that the player knowns the spell, that the spell is usable and in good distance, 
that the player is not eating or drinking, or is mounted..
That the target is attackable and alive, in distance, and in line of sight etc..

Abilities.Spellname is from a custom class of loading and declaring the class spells.

The easier way

    public class WoWSpell : Spell
    {
        private Timer _timer;

        #region Constructor

        /// <summary>
        /// Creates a new instance of the <see cref="WoWSpell"/> class.
        /// </summary>
        /// <param name="spellNameEnglish">The spell name.</param>
        /// <param name="cooldownTimer">The cooldown time.</param>
        public WoWSpell(string spellNameEnglish, double cooldownTimer)
            : base(spellNameEnglish)
        {
            // Set timer
            this._timer = new Timer(cooldownTimer);
        }

        #endregion

        #region Public

        /// <summary>
        /// Gets the flag if the timer is ready or not.
        /// </summary>
        public bool IsReady
        {
            get
            {
                return this._timer.IsReady;
            }
        }

        /// <summary>
        /// Casts the spell if it is ready.
        /// </summary>
        public new void Launch()
        {
            // Is ready?
            if (!this.IsReady)
            {
                // Return
                return;
            }

            // Call launch
            base.Launch();

            // Reset timer
            this._timer.Reset();
        }

        #endregion
    }

Just a sample how it could be done. Hope it helps some people.

If i get it right, in your example

double cooldownTimer

Will be a constant, right?

But the problem is, it may be different, it's not a constant for the same spell. Spell may be different level (lvl 1 heal may be 1,5s cast and the same lvl 6 spell may be 3,5s cast). Spell cast time may be buffed by talants, or may be not buffed, depending on character. Cast time may be hited back by incoming damage or kicked. If it was hitted back, delay should be longer, if it was kicked, it should be shorter. In a word, it's complicated.

 

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
×
×
  • Create New...