• 🏆 Texturing Contest #33 is OPEN! Contestants must re-texture a SD unit model found in-game (Warcraft 3 Classic), recreating the unit into a peaceful NPC version. 🔗Click here to enter!
  • It's time for the first HD Modeling Contest of 2024. Join the theme discussion for Hive's HD Modeling Contest #6! Click here to post your idea!

Hunter's Instinct - v1.2 (FINAL VERISON)

  • Like
Reactions: Imadori
Hello everyone.
This is my entry for Zephyr#9 contest. Hope you enjoy it.

-------------------------------------------------------
The Warden uses her unnatural instincts to locate wounded enemies.
Then, she gains bonus movement speed and the ability to become invisible.
She inflicts bonus damage upon her victim based on her distance from her
farthest target at the time of casting.

Level 1 - 30% speed bonus. 100 damage bonus.
Level 2 - 40% speed bonus. 200 damage bonus.
Level 3 - 50% speed bonus. 300 damage bonus.

Manacost: 90
Cooldown: 30/25/20
-------------------------------------------------------

-------------------------------------------------------
JASS:
library HunterInstinct requires UnitIndexer, TimerUtils

//-------------------------------------------------------------------------------------------
//      Hunter's Instinct
//
//        by Dr.Killer
//
// The Warden uses her unatural instincts to locate wounded enemies.
// Then, she gains bonus movement speed and the ability to become invisible.
// She inflicts bonus damage upon her victim based on her distance from her 
// farthest target at the time of casting.
//-------------------------------------------------------------------------------------------


//-------------------------------------------------------------------------------------------
//      Configuration
//-------------------------------------------------------------------------------------------
globals
    private constant integer MAIN_SPELL_ID              =   'A001'          //Raw code of spell
    private constant integer INVIS_ID_1                 =   'A003'          //Raw codes for wind walk abilities (3 levels)
    private constant integer INVIS_ID_2                 =   'A004'
    private constant integer INVIS_ID_3                 =   'A005'
    private constant integer INVIS_BUFF_ID              =   'B001'          //Wind walk buff raw code
    
    private constant real    RANGE                      =   5100.           //The radius in which targets are picked
    private constant real    DIST_1                     =   1700.           //If Warden's distance to the farthest target is lower than this, a level 1 windwalk will be added
    private constant real    DIST_2                     =   2400.           //Same as above, just a level 2 windwalk will be added.
    private constant real    DURATION                   =   15.             //Spell duration
    private constant integer ARRAY_SIZE                 =   90              //Determines the number of spells which can be run simultaneously by different units
    private constant real    UNIT_MAX_SPEED             =   522.            //Unit's maximum speed in your map (neccessary)
    private constant real    WINDWALK_REMOVE_INTERVAL   =   1.              //The delay before Windwalk is removed from Warden's abilities, after a Windwalk attack 
    private          group   tempGroup                  =   CreateGroup()   //A group used for enumerationa and stuff.
    
endglobals

private function HpThreshold takes integer lvl returns real                 //Enemies whose hit points are below this percent, will be added to targets group
    return (10.+10.*I2R(lvl))/100.
endfunction

private function SpeedBonus takes integer lvl returns real                  //Amount of bonus speed given to Warden upon casting (in percent)
    return (20.+10.*I2R(lvl))/100.
endfunction
//-------------------------------------------------------------------------------------------
//      End of Configuration
//-------------------------------------------------------------------------------------------

globals
    private boolean array flag [ARRAY_SIZE][ARRAY_SIZE]                     //Each target unit has a flag to see if the corresponding spell' duration cast on him has ended. Makes it possible for this spell to be MUI.
    private boolean array invis_flag                                        //Checks whether the wind walk ability has been added to warden or not. Useful for preventing the Warden from getting more than 1 wind walk ability
endglobals

//-------------------------------------------------------------------------------------------
//      Contains the onInit method
//-------------------------------------------------------------------------------------------
private module Init 
    private static method onInit takes nothing returns nothing
        local trigger t        = CreateTrigger()
        local trigger t2       = CreateTrigger()
        
        call TriggerRegisterAnyUnitEventBJ(t, EVENT_PLAYER_UNIT_SPELL_EFFECT)
        call TriggerAddCondition(t, Condition(function thistype.onCast))
        
        call TriggerRegisterAnyUnitEventBJ(t2, EVENT_PLAYER_UNIT_ATTACKED)
        call TriggerAddCondition(t2, Condition(function thistype.onAttack))
        call BJDebugMsg("asD")
        
        set t = null
        set t2 = null
    endmethod
endmodule

//-------------------------------------------------------------------------------------------
//      Spell's main struct, holds all of spell's properties for later use
//-------------------------------------------------------------------------------------------

private struct SpellData                                                    //Struct variable names should be pretty self-explanatory
    private unit        caster
    private integer     casterId
    private player      owner
    private integer     lvl
    private group       targets                                             //A group which holds all the targets of the spell.                                  
    private timer       main_timer    
    private real        casterX
    private real        casterY
    private real        maxDist                                             //To determine wind walk ability level, I store Warden's distance of the farthest target in this
    private real        speedBonus                                          //Amount of speed bonus given to Warden (the real value which is actually added to her speed)
    private boolean     noTarget

//-------------------------------------------------------------------------------------------
//      This method runs when spell duration ends; does cleanups, effect removal, etc.
//-------------------------------------------------------------------------------------------
    private static method onFinish takes nothing returns nothing
        local unit      u            = null                                 //A temp unit used in some loops.
        local SpellData object       = GetTimerData(GetExpiredTimer())      //An instance of SpellData struct which holds all of spell's variables, such as caster, targets, etc.
        local SpellData temp                                                //Used in the Vision Management section. More info below.
        local integer   i            = 0                                    //I & J are simple counters used in loops.
        local integer   j            = 0
        local integer   target_id    = 0                                    //UnitId of the currently picked target (More info below)
        local boolean   grant_vision = false                                //Determines whether shared vision of the currently picked should be denied or granted. Used in making this MUI.
        local integer   owner_id     = GetPlayerId(object.owner)
        
        //----------------------------------------------------------------------------------
        //      Falsing flag related to this spell. Pretty obvious. Prevents this spell
        //      from granting vision of targets any longer.
        //----------------------------------------------------------------------------------
        loop
            set i=i+1
            exitwhen i>ARRAY_SIZE
            set flag[object][i] = false
        endloop
        set i=0
        
        //----------------------------------------------------------------------------------
        //      Vision Management section. Here, I pick the targets one by one, check if
        //      they are under the effect of a same spell (of another source). If that's
        //      true, I see if the owning player of that spell is the same as owner of this
        //      one. If that's also true, vision of that target is granted to the corresponding
        //      player and the grant_vision boolean is set to true.
        //      If not, vision is denied because with no Hunter's Instinct active, 
        //      there is no reason for them to have vision of enemy heroes.
        //      The 'temp' instance, is used to convey info of the other active spell to this
        //      method, so I can have access to its caster, owning player, etc.
        //----------------------------------------------------------------------------------
        loop
            set u = FirstOfGroup(object.targets)
            exitwhen u == null
            set target_id = GetUnitId(u)
            
            loop
                set j=j+1
                exitwhen (j>ARRAY_SIZE or grant_vision == true)
                set temp = SpellData.create()
                if (flag[j][target_id] == true) then
                    set temp = j
                    if (temp.owner == object.owner) then
                        set grant_vision = true
                        call UnitShareVision(u, object.owner, true)
                    endif
                endif
                call temp.destroy()
            endloop
            
            if (not grant_vision) then
                call UnitShareVision(u, object.owner, false)
            endif
            call GroupRemoveUnit(object.targets, u)
            set j=0
        endloop
        
        //----------------------------------------------------------------------------------
        //      Cleanup section. Removes leaks, nullifies handles, etc.
        //      I also set the invis_flag of the caster to false, cause she no longer
        //      possesses the wind walk ability and even if she is in the middle of one,
        //      the buff is removed, so she becomes visible.
        //      I also remove the speed bonus here.
        //----------------------------------------------------------------------------------
        set invis_flag[object.casterId] = false
        call UnitRemoveAbility(object.caster, INVIS_ID_1)
        call UnitRemoveAbility(object.caster, INVIS_ID_2)
        call UnitRemoveAbility(object.caster, INVIS_ID_3)
        call UnitRemoveAbility(object.caster, INVIS_BUFF_ID)
        call SetUnitMoveSpeed(object.caster, GetUnitMoveSpeed(object.caster)-object.speedBonus)    
        call DestroyGroup(object.targets)
        call PauseTimer(GetExpiredTimer())
        call ReleaseTimer(GetExpiredTimer())
        set object.caster = null
    endmethod

//-------------------------------------------------------------------------------------------
//      Core method; sets needed variables, runs the spell, etc.
//-------------------------------------------------------------------------------------------
    private static method onCast takes nothing returns boolean
        local SpellData object                                //Same as above, holds all needed variables for a spell.
        local unit      u               = null                                              //A temp unit used in loops.
        local real      dist            = 0.                                                //Warden's distance form the picked target. Mre info below.
        local real      finalSpeed      = 0.                                                //Warden's final speed, after applying the bonuses.
        local real      originalSpeed   = 0.                                                //Warden's original speed, before applying the bonuses. 
        local real      x               = 0.
        local real      y               = 0.
        
    if (GetSpellAbilityId() == MAIN_SPELL_ID) then
        set object                      = SpellData.create()
        set object.targets              = CreateGroup()
        set object.caster               = GetTriggerUnit()
        set object.casterId             = GetUnitId(object.caster)
        set object.owner                = GetTriggerPlayer()
        set object.casterX              = GetUnitX(object.caster)
        set object.casterY              = GetUnitY(object.caster)
        set object.lvl                  = GetUnitAbilityLevel(object.caster, MAIN_SPELL_ID)

        call GroupEnumUnitsInRange(tempGroup, object.casterX, object.casterY, RANGE, null)  //Aquires all potential targets. Invalid ones are filtered out below.
        
        //----------------------------------------------------------------------------------
        //      Target Validation section. Throws non-hero and non-enemy units out. Also 
        //      dissmissed units whose HP is more than the required threshold which differs
        //      for each level. Also sets required 'flag' slots to true. If a unit's flag
        //      is true, it means than they are under the effect of Hunter's Instinct.
        //      Besides, I store the farthest target's distance in maxDist variable, using
        //      some kind of Bubble Sort method to find it in the first place.
        //----------------------------------------------------------------------------------
        set object.noTarget = true
        loop
            set u = FirstOfGroup(tempGroup)
            exitwhen u == null
            if (IsUnitType(u, UNIT_TYPE_HERO) and (GetWidgetLife(u) <= (GetUnitState(u, UNIT_STATE_MAX_LIFE)*HpThreshold(object.lvl))) and IsUnitEnemy(u, object.owner)) then
                call GroupAddUnit(object.targets, u)
                set object.noTarget = false
                //Finding the distance between caster and current target
                set x = GetUnitX(u)
                set y = GetUnitY(u)
                set dist = (object.casterX-x)*(object.casterX-x) + (object.casterY-y)*(object.casterY-y)
                
                if dist>(object.maxDist*object.maxDist) then
                    set object.maxDist = dist
                endif
                set dist = 0.
                set flag[object][GetUnitId(u)] = true
            else
                set flag[object][GetUnitId(u)] = false
            endif
            call GroupRemoveUnit(tempGroup, u)
        endloop
        
        //----------------------------------------------------------------------------------
        //      This part adds the speed bonus. Also if the bonus causes Warden's speed
        //      to go beyond the maximum allowed speed, this corrects the bonus amount
        //      and removes the excess value.
        //----------------------------------------------------------------------------------
        if not object.noTarget then 
            set originalSpeed     = GetUnitMoveSpeed(object.caster)
            set object.speedBonus = originalSpeed*SpeedBonus(object.lvl)
            set finalSpeed = object.speedBonus+originalSpeed
            if (finalSpeed > UNIT_MAX_SPEED) then 
                set object.speedBonus = object.speedBonus - finalSpeed + UNIT_MAX_SPEED
                set finalSpeed = UNIT_MAX_SPEED
                call SetUnitMoveSpeed(object.caster, UNIT_MAX_SPEED)
            else
                call SetUnitMoveSpeed(object.caster, finalSpeed)
            endif
        endif
        
        //----------------------------------------------------------------------------------
        //      Vision Management section, again. Picks each individual unit present
        //      in the 'targets' group and grants shared vision of that unit to 
        //      the owning player of caster.
        //----------------------------------------------------------------------------------
        call GroupClear(tempGroup)
        call GroupAddGroup(object.targets, tempGroup)
        
        loop
            set u = FirstOfGroup(tempGroup)
            exitwhen u == null
            call UnitShareVision(u, object.owner, true)
            call GroupRemoveUnit(tempGroup, u)
        endloop
        
        //----------------------------------------------------------------------------------
        //      Here, I add the windwalk ability based on Warden's distance from her 
        //      farthest target, which is stored in 'maxDist' variable.
        //----------------------------------------------------------------------------------
        if not invis_flag[object.casterId] then
            if (object.maxDist <= DIST_1*DIST_1) then
                call UnitAddAbility(object.caster, INVIS_ID_1)
            elseif (object.maxDist <= DIST_2*DIST_2) then
                call UnitAddAbility(object.caster, INVIS_ID_2)
            else
                call UnitAddAbility(object.caster, INVIS_ID_3)
            endif
        endif
        set invis_flag[object.casterId] = true
        
        //----------------------------------------------------------------------------------
        //      Fires a timer to call the onFinish method at the end of spell duration.
        //----------------------------------------------------------------------------------
        set  object.main_timer = NewTimer()
        call SetTimerData(object.main_timer, object)
        call TimerStart(object.main_timer, DURATION, true, function thistype.onFinish)
        set u = null
    endif
    
    return false
    endmethod

//-------------------------------------------------------------------------------------------
//      Removes the Windwalk ability after an attack
//-------------------------------------------------------------------------------------------
    private static method removeWindwalk takes nothing returns nothing
        local timer t  = GetExpiredTimer()
        local unit  u  = GetUnitById(GetTimerData(t))
        call UnitRemoveAbility(u, INVIS_ID_1)
        call UnitRemoveAbility(u, INVIS_ID_2)
        call UnitRemoveAbility(u, INVIS_ID_3)
        set u = null
        call PauseTimer(t)
        call ReleaseTimer(t)
        set t = null
    endmethod
    
//-------------------------------------------------------------------------------------------
//      Fires a timer at the end of which Windwalk is removed
//-------------------------------------------------------------------------------------------
    private static method onAttack takes nothing returns boolean
        local timer t
        local unit attacker = GetAttacker()
        
    if (GetUnitAbilityLevel(attacker, INVIS_BUFF_ID)>0) then
        set t = NewTimer()
        call SetTimerData(t, GetUnitId(attacker))
        call TimerStart(t, WINDWALK_REMOVE_INTERVAL, false, function thistype.removeWindwalk)
    endif
    set t = null
    set attacker = null
    return false
    endmethod
    
//-------------------------------------------------------------------------------------------
//      Self-explanatory
//-------------------------------------------------------------------------------------------
    implement Init
endstruct

endlibrary
-------------------------------------------------------

Credits:
- grim001 for AutoIndex library.
- Rising_Dusk for Timer_Utils.
- Magtheridon96 for pointing out some efficiency tips (in fact a lot of tips...)

Keywords:
Assassin,Spell,Instinct,Hunter
Contents

Zephyr #9 - Dr.Killer (Map)

Reviews
19:10, 11th Aug 2012 Magtheridon96: Approved. Tips: - You don't need to pause a timer before you release it because ReleaseTimer(t) already does that. - In the onAttack method, you can get rid of the locals and shorten the function by doing...

Moderator

M

Moderator

19:10, 11th Aug 2012
Magtheridon96:

Approved.

Tips:
- You don't need to pause a timer before you release it because ReleaseTimer(t) already does that.
- In the onAttack method, you can get rid of the locals and shorten the function by doing this:
call TimerStart(NewTimerEx(GetUnitId(GetAttacker())), ...)
You don't have to cache the attacker.
If you're going to repeat a handle-returning call 3 or more times, then caching it into a local is worth it.
If you're going to repeat a scalar-type-returning call (something that returns an integer, boolean, string, or anything that doesn't need to be nulled at the end) 2 or more times, then caching it into a local is also worth it.

That's a golden rule we came up with almost a year ago :D

- onAttack leaks a timer and a unit.
attacker and t should be nulled regardless of whether the unit has the buff or not.
Also, NewTimer() should only be called after you make sure the unit has the buff, else, you're just creating a NewTimer() that does nothing :p

Also, you're leaking struct instances.
Instead of setting object to thistype.create() BEFORE you check if the right spell was casted, you should do it AFTER you check if the right spell was casted xD

Finally, the distances should be configurable.

By the way, you should look into the new NewTimerEx function :p

  • I would totally recommend looking into UnitIndexer by Nestharus. It has a GetUnitById function. A fast one too. Your method of enumerating over every unit on the map to get the unit given the id is very inefficient. His function is just an array lookup that inlines. I /can/ approve this while using AutoIndex, but the cost is just too much :/
  • The GetDistance library's functions should just be in your spell resource. In fact, you don't need them because you can do them in the actual code directly and optimize them by removing the square root =o. You can remove the square root and just compare the retrieved distance value with your maxDist thing multiplied by itself.
  • You can merge the actions and conditions into one function that has an if block to determine whether the spell should execute or not, and you'd just register that function as a condition and return false at the end since conditions run faster than actions. In fact, you can even use a system like SpellEffectEvent by Bribe for this.
  • In the onInit function, you should null the triggers.
  • In the attackCondition function, you should null the units. You don't even need to store them into variables, just use GetAttacker() instead of 'attacker', and don't declare victim because you aren't using it.
  • I would totally recommend handling all the targets differently.
    Use a hashtable or a Table to store the targets and an integer to store the number of targets. The IsUnitGroupEmptyBJ functions enumerates over all units in the group, thus, it's slow. And you don't need to the set the movement of the unit to UNIT_MAX_SPEED inside the if block in this block of code since you're setting it at the end anyways:
    JASS:
            if not IsUnitGroupEmptyBJ(object.targets) then 
                set originalSpeed     = GetUnitMoveSpeed(object.caster)
                set object.speedBonus = originalSpeed*SpeedBonus(object.lvl)
                set finalSpeed = object.speedBonus+originalSpeed
                if (finalSpeed > UNIT_MAX_SPEED) then 
                    set object.speedBonus = object.speedBonus - finalSpeed + UNIT_MAX_SPEED
                    set finalSpeed = UNIT_MAX_SPEED
                    call SetUnitMoveSpeed(object.caster, UNIT_MAX_SPEED)
                endif
                call SetUnitMoveSpeed(object.caster, finalSpeed)
            endif
  • Modifying movement speed is not a very good thing to do. It could conflict with other systems. A good solution would be to use some standard movement speed system. I think there are a few in the JASS section on this site, but I never really use any of them, I just code my own.
  • You don't need to initialize tempGroup inside the onInit function, you can do that in the declaration of the group.
  • GetWidgetLife is faster than GetUnitState when wanting to retrieve unit HP values.
  • Why are you repeating GetOwningPlayer(caster) if you just stored it into a struct variable? And it would be faster to set the owner variable to the triggering player since it points to the same thing but takes less arguments.
  • When I mentioned how you should handle the group-emptiness checking differently, I forgot to say that you can simply set a boolean to true inside the loop after you add units to your target group. Then, instead of checking if the group isn't empty, just use that boolean in the if block.
  • == true is the most redundant thing ever. You can remove it and get the same results.
  • GetOwningPlayer(temp.caster) -> temp.owner

There are probably a ton of other things that could be done too, but I'll give you the rest as soon as you fix all the above so it would be easier for you to manage with all the changes :/
 
Level 5
Joined
Aug 16, 2010
Messages
97
Actually, it's a dota-like spell with the invisible stuff
Here: http://www.playdota.com/heroes/bloodseeker
Not i am saying your spell is bad, it still has its own original, but should there be some "balance" here? :p
Vote for 3/5 (since it lost the point in my mind at the idea, don't mad at me ok? :D)
Mad? Of course not. I always appreciate constructive criticism. But I don't get your point. Where does it lack balance? The Windwalk damage, movespeed buff or ...?
 
Level 9
Joined
Aug 7, 2009
Messages
380
Ok, since this is a spell, not a map, i don't really think the balance job here is very needed :p
Anyway, good job in mixing the existed idea with a new way to turns the idea up to new :)
I can see that's the most well-done of this spell in overall
Since i'm a GUI-er (sad), can't judge on your vJass stuff :) - Goodluck with the contest
Rating now 4/5 (I wasn't test the map when I 1st post the reply)
 
Top