• Listen to a special audio message from Bill Roper to the Hive Workshop community (Bill is a former Vice President of Blizzard Entertainment, Producer, Designer, Musician, Voice Actor) 🔗Click here to hear his message!
  • Read Evilhog's interview with Gregory Alper, the original composer of the music for WarCraft: Orcs & Humans 🔗Click here to read the full interview.

[vJASS] Using Functions with Bool Return values as 'ActionType' functions

Status
Not open for further replies.
Level 8
Joined
Feb 3, 2013
Messages
277
Hi all once again, I've been messing around with some more vJass, trying to understand how to use structs; This time I've made a meat hook like spell from DotA. The trigger works completely as intended (this is because I looked at another script to start me off) - but I'm COMPLETELY STUMPED as to how it works. I'm confused because the function Create and Destroy return a boolexpr value - and whether the spell works properly is dependent on the return value. And this is where I need help - can someone explain to me why the boolexpr values are needed in order for this spell to work?

I feel like the problem for me starts when timer's are involved - I understand that I'm using the system to attach struct handles to a timer initially, but the TT system uses only one timer. If i set the Create and Destroy functions so that they don't have any return values, I get an error in game and the hook doesn't work properly. And I know, it shouldn't work because that will lead to stacking struct handles on a single timer. But why do functions that return boolexpr values work?

JASS:
scope MHStruct initializer init 

    globals
        private constant real INC = 30. // Distance between hooks
        private constant real BASE_RANGE = 2000. //Base range of the hook; Formula is BASE_RANGE + (INC_RANGE * LVL)
        private constant real INC_RANGE = 250. // See above
        private constant real BASE_DAMAGE = 100. //Base damage of the hook; Formauls is BASE_DAMAGE + (INC_DAMAGE * LVL)
        private constant real INC_DAMAGE = 50. // See above
        private constant real AOE = 115. // AoE for hook to hit
        private constant real STR_RATIO = 0.1 // Drag damage is based on Heroes strength ratio
        private constant integer DC_ID = 'h000' // Hook Chain Dummy raw ID code
        private constant integer DH_ID = 'h001' // Hook Head Dummy raw ID code
        private constant integer MH_ID = 'A003' // Hook Ability raw ID code
        private constant string FX = "Objects\\Spawnmodels\\Human\\HumanBlood\\BloodElfSpellThiefBlood.mdl" // Effects displayed upon enemy encounter
    endglobals
    
    private struct dat
        unit c // Caster
        unit array d_c[300] // Dummy hookchain - Maximum number of dummy chains per instance is 300
        unit d_h // Dummy hookhead -
        unit t // Target
        location p // Location of caster
        location pp // PolarOffset from Location of Caster towards ang 
        real ang // Angle between location of caster and spell target location
        real rng // Current offset distance of meathook
        real str // 15% of Hero's Str including bonuses
        integer lvl // Level of Meathook for user
        integer i // Count how many hook chains there are 
        boolean boo // Boolean expression deciding whether a unit is hooked or not
    endstruct
    
//=======================================================
// DO NOT TOUCH BELOW THIS LINE  
//========================================================

    private function MHS_Filter takes nothing returns boolean // Filter unit must be a non structure, alive unit; Filter unit can not be a unit type of dummy hookchain or dummy hookhead; Filter Unit can not be the casting unit
        return GetUnitState(GetFilterUnit(), UNIT_STATE_LIFE) > 0 and IsUnitType(GetFilterUnit(), UNIT_TYPE_STRUCTURE) == FALSE and GetUnitTypeId(GetFilterUnit()) != DH_ID and GetUnitTypeId(GetFilterUnit()) != DC_ID and GetFilterUnit() != bj_ghoul[50]
    endfunction
    
    private function Destroy takes nothing returns boolean
        local dat d = TT_GetData()
        set d.rng = d.rng - INC
        set d.pp = PolarProjectionBJ(d.p, d.rng, d.ang)
            if d.boo == TRUE then
                call SetUnitPositionLoc(d.t, d.pp)
                    if IsUnitEnemy(d.t, GetOwningPlayer(d.c)) then
                        call UnitDamageTarget(d.c, d.t, d.str, false, false, ATTACK_TYPE_NORMAL, DAMAGE_TYPE_NORMAL, null)
                    endif
            endif
            
            if d.i == 1 then
                call RemoveUnit(d.d_h)
            else
                call SetUnitPositionLoc(d.d_h, d.pp)
                call RemoveUnit(d.d_c[d.i])
            endif
            
            if d.i == 0 then
                call SetUnitPathing(d.t, true)
                set d.c = null
                set d.t = null
                call d.destroy()
                return TRUE // HERE!!
            endif
            
        set d.i = d.i - 1
        return FALSE // HERE!
    endfunction
    
    private function Create takes nothing returns boolean
        local dat d = TT_GetData()
        local group g = CreateGroup()
        set d.rng = d.rng + INC
        set d.pp = PolarProjectionBJ(d.p, d.rng, d.ang)
        call GroupEnumUnitsInRangeOfLoc(g, d.pp, AOE, Filter(function MHS_Filter))
        set d.t = FirstOfGroup(g)
        
            if d.i == 1 and d.t == null then
                set d.d_h = CreateUnitAtLoc(GetOwningPlayer(d.c), DH_ID, d.pp, d.ang)
                call SetUnitScale(d.d_h, 15., 15., 15.)
            elseif d.i >= 2 and d.t == null then
                call SetUnitPositionLoc(d.d_h, d.pp)
                set d.d_c[d.i] = CreateUnitAtLoc(GetOwningPlayer(d.c), DC_ID,d.pp, d.ang)
            endif
            
            if d.rng >= BASE_RANGE + INC_RANGE * I2R(d.lvl) then
                set d.boo = FALSE
                call TT_Start(function Destroy, d)
                return TRUE /// HERE!!!
            elseif d.t != null then
                set d.boo = TRUE
                call TT_Start(function Destroy, d)
                    if IsUnitEnemy(d.t, GetOwningPlayer(d.c)) == TRUE then
                        call UnitDamageTarget(d.c, d.t, BASE_DAMAGE + INC_DAMAGE * d.lvl, false, false, ATTACK_TYPE_NORMAL, DAMAGE_TYPE_NORMAL, null)
                        call DestroyEffect(AddSpecialEffectTarget(FX, d.t, "chest"))
                    endif
                call SetUnitPathing(d.t, FALSE)
                return TRUE // HERE!!!
            endif
            
        set d.i = d.i + 1
        return FALSE // HERE!!!
    endfunction
        
    private function MHS_Cond takes nothing returns boolean
        return GetSpellAbilityId() == MH_ID
    endfunction
    
    private function MHS_Actions takes nothing returns nothing
        local dat d = dat.create()
        local location loc = GetSpellTargetLoc()
        set bj_ghoul[50] = GetTriggerUnit()
        set d.rng = INC
        set d.i = 1
        set d.c = GetTriggerUnit()
        set d.p = GetUnitLoc(GetTriggerUnit())
        set d.lvl = GetUnitAbilityLevel(d.c, MH_ID)
        set d.ang = AngleBetweenPoints(d.p, loc)
        set d.str = I2R(GetHeroStr(d.c, true)) * STR_RATIO
        call TT_Start(function Create, d)
        call RemoveLocation(loc)
        set loc = null
    endfunction

    private function init takes nothing returns nothing
        local trigger t = CreateTrigger()
        local integer i = 0
            loop
                exitwhen i == 15
                call TriggerRegisterPlayerUnitEvent(t, Player(i), EVENT_PLAYER_UNIT_SPELL_EFFECT, null)
                set i = i + 1
            endloop
        call TriggerAddCondition(t, Condition(function MHS_Cond))
        call TriggerAddAction(t, function MHS_Actions)
    endfunction

endscope

the Time Ticker System by Cohadar.

JASS:
//==============================================================================
//  TT -- TIMER TICKER SYSTEM BY COHADAR -- v3.4
//==============================================================================
//
//  PURPOUSE OF TT:
//       * Passing data to timers
//       * using only one timer for all actions
//
//  PROS:
//       * It is ridiculously easy to use.
//       * It is faster than ABC (and any other attaching system)
//
//  CONS:
//       * It can be used only for high-frequency timers
//         This method fails if PERIOD > 0.1 second
//
//  FUNCTIONS:
//       * TT_Start(userFunc, struct)
//         TT_GetData() -> struct
//
//       * userFunc is a user function that takes nothing and return boolean
//         it will be periodically called by the system until it returns true.
//
//       * TT_GetData() is a function that can be used inside userFunc
//         TT_GetData() will return struct passed to Start function
//
//  DETAILS:
//       * All user functions are stored in an array.
//         Timer will call all those functions each period.
//
//       * While user function returns false timer will continue to call it each period
//         Once user function returns true it will be removed from system
//
//  REQUIREMENTS:
//       * NewGen v4c and above (there might be some problems with older NewGen's)
//
//  THANKS TO:
//       * Vexorian - he was nagging me so much about how attaching to timers is bad
//         that I finally had to do something about it.
//
//  HOW TO IMPORT:
//       * Just create a trigger named TT
//       * convert it to text and replace the whole trigger text with this one
//
//==============================================================================
library TT initializer Init

globals
    // List of recommended periods:
    // 0.04    = 25 calls per second
    // 0.03125 = 32 calls per second
    // 0.025   = 40 calls per second
    // 0.02    = 50 calls per second
    public constant real PERIOD = 0.02
    
    // One Timer to rule them all, One Timer to find them,
    // One Timer to call them all and in the jass bind them
    // In the land of warcraft where the desyncs lie.
    private timer   Timer = CreateTimer()
    
    private integer Data
    private integer Counter = 0
    private trigger array Triggz
    private integer array Dataz
endglobals

//==============================================================================
private function Handler takes nothing returns nothing
    local trigger swap
    local integer i = Counter
    loop
        exitwhen i<=0
        set Data = Dataz[i]
        if TriggerEvaluate(Triggz[i]) then
            set swap = Triggz[i]
            call TriggerClearConditions(swap)
            set Triggz[i] = Triggz[Counter]
            set Triggz[Counter] = swap
            set Dataz[i] = Dataz[Counter]
            set Counter = Counter - 1
        endif
        set i = i - 1
    endloop
    // who can guess why am I not nulling swap here?
endfunction

//==============================================================================
public function Start takes code userFunc, integer data returns nothing
    debug if userFunc == null then
    debug    call BJDebugMsg("ERROR: TT_Start - null userFunc")
    debug    return
    debug endif

    set Counter = Counter + 1    
    
    if Triggz[Counter] == null then
        set Triggz[Counter] = CreateTrigger()
    endif
    
    call TriggerAddCondition(Triggz[Counter], Condition(userFunc))
    set Dataz[Counter] = data
endfunction

//==============================================================================
//  Call this function only inside the userFunc you passed to Start
//==============================================================================
public function GetData takes nothing returns integer
    return Data
endfunction

//==============================================================================
private function Init takes nothing returns nothing
    call TimerStart(Timer, PERIOD, true, function Handler)  
endfunction

endlibrary

//==============================================================================
//  END OF TIMER TICKER SYSTEM
//==============================================================================

once again thanks in advance for all your help <3
 
Level 8
Joined
Feb 3, 2013
Messages
277
Isn't it already MUI? I'm pretty sure it is. Though took me a while to understand this very confusing code, I have to admit. Talking about descriptive variable names...

Sorry.. are my codes are hard to read??

And yes it is MUI, but I don't like the idea of relying on a system that I don't fully understand. I decided to re-make the whole thing using hashtables. Here it is for anyone who wants to see..

EDIT* slightly
JASS:
scope MH initializer init 

    globals
        private constant string FX = "Objects\\Spawnmodels\\Human\\HumanBlood\\BloodElfSpellThiefBlood.mdl"
        private constant real INC = 30
        
        private hashtable hash = InitHashtable()
        private hashtable dude = InitHashtable()
    endglobals
    
    private function MH_Filter takes nothing returns boolean
        return IsUnitAliveBJ(GetFilterUnit()) == TRUE and IsUnitType(GetFilterUnit(), UNIT_TYPE_STRUCTURE) == FALSE and IsUnit(GetFilterUnit(), bj_ghoul[50]) == FALSE and GetUnitTypeId(GetFilterUnit()) != 'h000' and GetUnitTypeId(GetFilterUnit()) != 'h001'
    endfunction
    
    private function MH_Create takes nothing returns nothing
        local timer t_c = GetExpiredTimer()
        local group g = CreateGroup()
        local unit u
        local unit h
        local unit t = LoadUnitHandle(hash, GetHandleId(t_c), 50)
        local unit c = LoadUnitHandle(hash, GetHandleId(t_c), 2)
        local real ang = LoadReal(hash, GetHandleId(t_c), 3)
        local real os_d = LoadReal(hash, GetHandleId(t_c), 4)
        local location pp = PolarProjectionBJ(LoadLocationHandle(hash, GetHandleId(t_c), 5), os_d, ang)
        local location e
        local location f
        local unit array d
        local integer i = LoadInteger(hash, GetHandleId(t_c), 6)
        local boolean b = LoadBoolean(hash, GetHandleId(t_c), 100)
        
        call GroupEnumUnitsInRangeOfLoc(g, pp, 85, Filter(function MH_Filter))
        set u = FirstOfGroup(g)
        
        if os_d >= 2000 and b == FALSE then
            set b = TRUE
            call SaveBoolean(hash, GetHandleId(t_c), 100, b)
        elseif b == TRUE then
            set e = GetUnitLoc(LoadUnitHandle(dude, GetHandleId(t_c), i))
                if i > 1 then
                    call SetUnitPositionLoc(t, e)
                    call SetUnitPathing(t, false)
                endif
            call SetUnitPositionLoc(LoadUnitHandle(hash, GetHandleId(t_c), 20), e)
            call RemoveUnit(LoadUnitHandle(dude, GetHandleId(t_c), i))
            call SaveInteger(hash, GetHandleId(t_c), 6, i - 1)
            call RemoveLocation(e)
                if i == 0 then
                    call RemoveUnit(LoadUnitHandle(hash, GetHandleId(t_c), 20))
                endif
            set e = null
        elseif u != null then
            set b = TRUE
            call SaveBoolean(hash, GetHandleId(t_c), 100, b)
            call SaveUnitHandle(hash, GetHandleId(t_c), 50, u)
            
            if IsUnitEnemy(u, GetOwningPlayer(c)) then
                call UnitDamageTarget(c, u, 100, false, false, ATTACK_TYPE_NORMAL, DAMAGE_TYPE_NORMAL, WEAPON_TYPE_WHOKNOWS)
                call DestroyEffect(AddSpecialEffectTarget(FX, u, "chest"))
            endif 
            
        elseif b == TRUE and i == 0 then
            call PauseTimer(t_c)
            call SetUnitPathing(t, TRUE)
            call RemoveUnit(LoadUnitHandle(hash, GetHandleId(t_c), 20))
            call FlushChildHashtable(dude, GetHandleId(t_c))
            call FlushParentHashtable(dude)
            call FlushChildHashtable(hash, GetHandleId(t_c))
            call FlushParentHashtable(hash)
            call DestroyTimer(t_c)
        else
            if i == 0 then
                set h = CreateUnitAtLoc(GetOwningPlayer(c), 'h001', pp, ang)
                call SetUnitScale(h, 15, 15, 15)
                call SaveUnitHandle(hash, GetHandleId(t_c), 20, h)
                call RemoveLocation(pp)
                call SaveReal(hash, GetHandleId(t_c), 4, os_d + INC)
                call SaveInteger(hash, GetHandleId(t_c), 6, i + 1)
            elseif i > 0 then
                set h = LoadUnitHandle(hash, GetHandleId(t_c), 20)
                set d[i] = CreateUnitAtLoc(GetOwningPlayer(c), 'h000', pp, ang)
                set f = GetUnitLoc(d[i])
                call SetUnitPositionLoc(h, f)
                call SaveUnitHandle(dude, GetHandleId(t_c), i, d[i])
                call SaveReal(hash, GetHandleId(t_c), 4, os_d + INC)
                call SaveInteger(hash, GetHandleId(t_c), 6, i + 1)
                call RemoveLocation(pp)
                call RemoveLocation (f)
            endif
        endif
        
        call DestroyGroup(g)
        call RemoveLocation(pp)
        call RemoveLocation(e)
        call RemoveLocation(f)
        set t_c = null
        set g = null
        set u = null
        set h = null
        set t = null
        set c = null
        set pp = null
        set e = null
        set f = null
    endfunction
        
    private function MH_Start takes nothing returns boolean
        local unit c = GetTriggerUnit()
        local location l_c = GetUnitLoc(c)
        local location l_t = GetSpellTargetLoc()
        local real ang = AngleBetweenPoints(l_c, l_t)
        local real os_d = INC
        local location pp = PolarProjectionBJ(l_c, os_d, ang)
        local timer t_c = CreateTimer()
        local integer i = 0 
        local boolean b = FALSE
        if GetSpellAbilityId() == 'A003' then
            set bj_ghoul[50] = c
            call SaveUnitHandle(hash, GetHandleId(t_c), 2, c)
            call SaveReal(hash, GetHandleId(t_c), 3, ang)
            call SaveReal(hash, GetHandleId(t_c), 4, os_d)
            call SaveLocationHandle(hash, GetHandleId(t_c), 5, l_c)
            call SaveInteger(hash, GetHandleId(t_c), 6, i)
            call SaveBoolean(hash, GetHandleId(t_c), 100, b)
            call TimerStart(t_c, 0.02, true, function MH_Create)
            call TriggerSleepAction (.5)
            call RemoveLocation (l_c)
            call RemoveLocation (l_t)
            call RemoveLocation (pp)
            set l_c = null
            set l_t = null
            set c = null
            set pp = null
            return FALSE
        else
            call TriggerSleepAction (.5)
            call RemoveLocation (l_c)
            call RemoveLocation (l_t)
            call RemoveLocation (pp)
            set l_c = null
            set l_t = null
            set c = null
            set pp = null
            return FALSE
        endif
    endfunction
    
    private function init takes nothing returns nothing
        local trigger t = CreateTrigger()
        call TriggerRegisterAnyUnitEventBJ(t, EVENT_PLAYER_UNIT_SPELL_EFFECT)
        call TriggerAddCondition(t, Condition(function MH_Start))
    endfunction
    
endscope

I have a question... while the first code doesn't lag at all.. (tested up to 350ish hooks)

the second code, the one on this page starts to lag at around 180 hooks, and with each hook after that becomes even slower and slower.. can someone tell me why? at the end of each function run, i destroy and null each handle type and flush both parent and child keys of the hashtables i used once the spell is done running.. ):
 
Last edited:
Your new version is a whole mess and much worse than the old version!

I'd really recommend keeping the old implementation, it's much cleaner and more efficient. Before you start editing or making spells, I really recommend learning the basics of efficient timer attaching.

Just some quick things I saw when checking your code:
1) why do you even need locations? Get rid of them and use X and Y reals for that. Faster, cleaner and less prone to leaks.
2) Do not use TriggerSleepActions to clean your leaks. It's a mess and absolutely not a good practice. Also makes the code harder to read and more vulnerable to unwanted bugs. Besides, you do not even need them at the places you put them. It would work perfectly fine without, except for the removed locations you shouldn't use anyway.
3) You do not need two hashtables. In fact, you do not even need one hashtable for your spell. Use one global hashtable for timer attachment in your whole map and you'll be perfectly fine (or use the table library available on hive).
4) Never flush a parent hashtable for tables used for timer attachment, only flush the child hashtable of your GetHandleId(timer). Flushing the parent table removes ALL data in the hashtable, even those that are not related to your spell instance.
5) I don't get this bj_ghoul[50] thing... remove it and use a variable instead
6) Use descriptive variable names. It's extremely hard to figure out what "u", "h", "t" and "c" mean. Besides, if you really want short variable names, at least take characters that are somehow related to the handle type. Using "t" for a timer or "u" for a unit is a common practice, but "t" and "c" for a unit and "t_c" for a timer is just confusing.
7) Improve the structuring of your spell ... the cases in your MHCreate function are not structured in a logical order. Also, put comments for every case in, for improved readability. Remember: You have to understand your own code at a later point of the mapmaking process. If you come back to this spell in like 5 months, you will not understand what you did there without comments.

All in all, this is an awesome piece of spaghetti programming.


This is a small sample of the easiest execution of timer attachment (can be made faster and easier to read by using a struct and only attach that to the timer instead of all variables involved):

JASS:
globals
 constant hashtable timerhash = CreateHashtable() //this is a global hashtable used for all timer attachments in your map; you should not need more than this single hashtable in your entire map if you do it right!
endglobals

function callback takes nothing returns nothing
  local timer t = GetExpiredTimer()
  local unit sheep = LoadUnitHandle(timerhash, GetHandleId(t), 0)
  local integer case = LoadInteger(timerhash, GetHandleId(t), 1)
  //stuff
  if case == 0 then
    set case = 1
    call SaveInteger(timerhash, GetHandleId(t), 1, case)
    call TimerStart(t, 2.0, false, function callback)
  elseif case < 20 then
    set case = case+1
    call SaveInteger(timerhash, GetHandleId(t), 1, case)
    call TimerStart(t, 2.0, false, function callback)
  elseif ...

  endif
  
  if ... then //condition that terminates or completes the spell
    //destroy everything here that is used only temporary in your spell (sfx, dummy units, groups, etc.)
    call FlushChildHashtable(timerhash, GetHandleId(t))
    call PauseTimer(t) //I recommend doing this before, as destroying a currently running timer might cause bugs
    call DestroyTimer(t)
  endif
  set t = null
  set sheep = null
endfunction

function start takes nothing returns nothing
  local timer t = CreateTimer()
  local unit sheep = GetTriggerUnit()
  local integer case = 0
  call SaveUnitHandle(timerhash, GetHandleId(t), 0, sheep)
  call SaveInteger(timerhash, GetHandleId(t), 1, case)
  call TimerStart(t, 1, false, function callback)
  set t = null
  set sheep = null
endfunction
 
Level 8
Joined
Feb 3, 2013
Messages
277
Okay! Thanks again Zwiebelchen, i re-wrote it, to making it a lot more readable, cleaner, using a single hashtable, and using more efficient objects such as reals x, y coordinates (cause your right, locations are really buggy) and using the Atan2 formula for angles.

Does not lag anymore in game, at least not on my comp;; gotta test it on someone else's...
JASS:
scope Hook initializer init 

//================================================================================
//CONFIGURABLES - CHANGE TO FIT                                
//================================================================================

    globals
        constant hashtable hash                  = InitHashtable()       // Create a hashtable to store values into timers
        
        private constant integer AB_ID           = 'A003'                // Get the rawcode of hook ability
        private constant integer HOOK_HEAD       = 'h001'                // Get the rawcode of hook head dummy
        private constant integer HOOK_CHAIN      = 'h000'                // Get the rawcode of the hook chain dummy
        private constant real    HH_SIZE         = 15.00                 // Hook head size
        private constant real    HC_SIZE         = 4.00                  // Hook chain size
        private constant real    PERIOD          = .0200                 // Time between execution of hook chains created
        private constant real    DBC             = 30.00                 // Real distance between hook chains
        private constant real    BASE_RANGE      = 1000.00               // Base range of spell;; Formula for total range is BASE_RANGE + LVL_RANGE * LVL OF HOOK
        private constant real    LVL_RANGE       = 250.00
        private constant real    BASE_DAMAGE     = 50.00                 // Base damage of spell;; Formula for total range is BASE_DAMAGE + LVL_DAMAGE * LVL OF HOOK
        private constant real    LVL_DAMAGE      = 10 
        private constant real    AOE             = 110                   // AOE in which hook can grab units 
        private constant string  FX              = "Objects\\Spawnmodels\\Human\\HumanBlood\\BloodElfSpellThiefBlood.mdl"  // Effect played upon enemy encounter
        
        private unit HOOKER                                              // Hook casting unit
    endglobals

//================================================================================
//CONDITIONS AND FILTERS
//================================================================================

    private function Hook_Filter takes nothing returns boolean
        return GetUnitTypeId(GetFilterUnit()) != HOOK_HEAD and GetUnitTypeId(GetFilterUnit()) != HOOK_CHAIN and GetFilterUnit() != HOOKER and GetUnitState(GetFilterUnit(), UNIT_STATE_LIFE) > 0 and IsUnitType(GetFilterUnit(), UNIT_TYPE_STRUCTURE) == FALSE
    endfunction
    
//Units filtered: Unit types of HOOK_HEAD and HOOK_CHAIN
//Units filtered: Dead Units
//Units filtered: The Casting Unit of the specific instance
//Units filtered: Structures
    
    private function Hook_Precond takes nothing returns boolean
        return TRUE
    endfunction
    
//'Precondition' for TriggerRegisterPlayerUnitEvent

    private function Hook_Cond takes nothing returns boolean
        return GetSpellAbilityId() == AB_ID
    endfunction

//Return the AbilityID of hook spell

//================================================================================
//BEGINNING OF HOOK ACTIONS
//================================================================================
//  *Function Hook_Start takes place after initial cast ofc...
//
//  The function starts a local repeating timer, and saves initial values 
//  into said timer - values are automatically changed 
//  after each expiration by the Function Hook_Create_Destroy
//
//  *Function Hook_Create_Destroy takes care of all the
//  actions that take place after the initial hook including
//  creation, destruction, and manipulation of the hooked unit,
//  hook chains, and hook head
//
//  It uses only one handle key timer, per instance of the spell
//
//  The function uses data initially stored into the hashtable 'hash'
//  and after each expiration of the key timer, re-saves information 
//  into the handle key timer so that it will match correct values
//  
//  For clarity issues, all the local variable names in the functions
//  have the same variable names, such as timer t, player owner,
//  and etc...

    private function Hook_Create_Destroy takes nothing returns nothing
        local timer t = GetExpiredTimer()                                   // Store expiring local timer from Function Hook_Start
        local player owner = LoadPlayerHandle(hash, GetHandleId(t), 5)      // Get owning player of casting unit
        local integer counter = LoadInteger(hash, GetHandleId(t), 3)        // An indexer for the hook chains and also serves as a counter to check when a hook starts or ends
        local real ang = LoadReal(hash, GetHandleId(t), 2)                  // Get the angle, does not change
        local real t_dist = LoadReal(hash, GetHandleId(t), 4)               // Get the total distance covered by the hook
        local real off_x = (LoadReal(hash, GetHandleId(t), 0)) + t_dist * Cos(ang * bj_DEGTORAD)    // Get the x offset value, locations seem to leak no matter what I do...
        local real off_y = (LoadReal(hash, GetHandleId(t), 1)) + t_dist * Sin(ang * bj_DEGTORAD)    // Get the y offset value,
        local boolean hooked = LoadBoolean(hash, GetHandleId(t), 30)        // Get boolean from the last run, was a unit hooked?
        
        local unit caster = LoadUnitHandle(hash, GetHandleId(t), 15)        // Get caster
        local unit head
        local unit array chain

        local group g = CreateGroup()           
        local unit fog = null
        local unit hooked_unit

        call GroupEnumUnitsInRangeOfLoc(g, Location(off_x, off_y), AOE, Filter(function Hook_Filter))
        set fog = FirstOfGroup(g)
        
//Use a FirstOfGroup to detect, whether a unit is within hook AOE        

        if counter == 0 and hooked == FALSE then            // Very first count of hook creation; creates hook head
            set head = CreateUnit(owner, HOOK_HEAD, off_x, off_y, ang)
            call SetUnitScale(head, HH_SIZE, HH_SIZE, HH_SIZE)
            call SaveUnitHandle(hash, GetHandleId(t), 20, head)
            call SaveReal(hash, GetHandleId(t), 4, t_dist + DBC)
            call SaveInteger(hash, GetHandleId(t), 3, counter + 1)
            
        elseif counter >= 1 and hooked == FALSE then        // Each count after the first; moves the hook head and creates new hook chains
            set head = LoadUnitHandle(hash, GetHandleId(t), 20)
            call SetUnitPosition(head, off_x, off_y)
            set chain[counter] = CreateUnit(owner, HOOK_CHAIN, off_x, off_y, ang)
            call SetUnitScale(chain[counter], HC_SIZE, HC_SIZE, HC_SIZE)
            call SaveUnitHandle(hash, GetHandleId(t), 100 + counter, chain[counter])
            call SaveReal(hash, GetHandleId(t), 4, t_dist + DBC)
            call SaveInteger(hash, GetHandleId(t), 3, counter + 1)
        
//^^If a unit is not hooked, it will run one of the list of calls above

        elseif counter == 0 and hooked == TRUE then         // Last count of hook retraction; destroy hook head and re-enables hooked unit's pathing, flush hashtable 'hash'
            set head = LoadUnitHandle(hash, GetHandleId(t), 20)
            call RemoveUnit(head)
            call PauseTimer(t)
            set hooked_unit = LoadUnitHandle(hash, GetHandleId(t), 6)
            call SetUnitPathing(hooked_unit, TRUE)
            call FlushChildHashtable(hash, GetHandleId(t))
            call DestroyTimer(t)
            
        elseif counter >= 1 and hooked == TRUE then         // Every count except the last; moves hook head and hooked unit, destroys the current hook chain
            set head = LoadUnitHandle(hash, GetHandleId(t), 20)
            call SetUnitPosition(head, off_x, off_y)
            set hooked_unit = LoadUnitHandle(hash, GetHandleId(t), 6)
            call SetUnitPosition(hooked_unit, off_x, off_y)
            set chain[counter] = LoadUnitHandle(hash, GetHandleId(t), 100 + counter)
            call RemoveUnit(chain[counter])
            call SaveReal(hash, GetHandleId(t), 4, t_dist - DBC)
            call SaveInteger(hash, GetHandleId(t), 3, counter - 1)
        endif
        
//^^If a unit is hooked or hook has reached max range, it will run one list of calls above

        if fog != null and hooked == FALSE then             // Detects a unit, using the FirstOfGroup function
            set hooked = TRUE                               // saves it as hooked_unit and disables its pathing
            set hooked_unit = fog
            call SaveBoolean(hash, GetHandleId(t), 30, hooked)
            call SaveUnitHandle(hash, GetHandleId(t), 6, fog)
            call SetUnitPathing(hooked_unit, FALSE)
            
                if IsUnitEnemy(hooked_unit, owner) == TRUE then             // If the unit is an enemy, deals damage and plays effect
                    call UnitDamageTarget(caster, hooked_unit, BASE_DAMAGE + LVL_DAMAGE * I2R(GetUnitAbilityLevel(caster, AB_ID)), false, false, ATTACK_TYPE_NORMAL, DAMAGE_TYPE_NORMAL, WEAPON_TYPE_WHOKNOWS)
                    call DestroyEffect(AddSpecialEffectTarget(FX, fog, "chest"))
                endif
                                                            // If it reaches max range, then begin retracting
        elseif t_dist >= BASE_RANGE + LVL_RANGE * I2R(GetUnitAbilityLevel(caster, AB_ID)) and hooked == FALSE then
            set hooked = TRUE
            call SaveBoolean(hash, GetHandleId(t), 30, hooked)
        endif
        
//^^Detects if a unit is within hook AOE or reaches max range
        
    endfunction

//================================================================================

    private function Hook_Start takes nothing returns nothing
        local unit caster = GetTriggerUnit()
        local location cast_loc = GetUnitLoc(GetTriggerUnit())
        local location targ_loc = GetSpellTargetLoc()
        local real t_dist = 0
        local real ang = bj_RADTODEG * Atan2(GetLocationY(targ_loc) - GetLocationY(cast_loc), GetLocationX(targ_loc) - GetLocationX(cast_loc))
        local integer counter = 0
        local boolean hooked = FALSE
        local timer t = CreateTimer()
        
        set HOOKER = caster
        
        set t_dist = t_dist + DBC
        
        call SaveReal(hash, GetHandleId(t), 0, GetLocationX(cast_loc))  // Saves x of caster's location 
        call SaveReal(hash, GetHandleId(t), 1, GetLocationY(cast_loc))  // Saves y of caster's location
        call SaveReal(hash, GetHandleId(t), 2, ang)                     // Saves angle between caster's location and target location
        call SaveReal(hash, GetHandleId(t), 4, t_dist)                  // Saves initial total distance of hook, which is one count of DBH
        call SaveInteger(hash, GetHandleId(t), 3, counter)              // Sets the initial counter, at 0
        call SavePlayerHandle(hash, GetHandleId(t), 5, GetOwningPlayer(caster)) // Saves player of caster
        call SaveBoolean(hash, GetHandleId(t), 30, hooked)              // Saves initial boolean hooked, as FALSE
        call SaveUnitHandle(hash, GetHandleId(t), 15, caster)           // Saves caster
        call TimerStart(t, PERIOD, true, function Hook_Create_Destroy)  // Run timer
        
        call RemoveLocation(cast_loc)
        call RemoveLocation(targ_loc)
        
        set cast_loc = null
        set targ_loc = null
        set caster = null
    endfunction

//================================================================================

    private function init takes nothing returns nothing
        local trigger t = CreateTrigger()
        local integer i = 0
            loop
            exitwhen i == 15
                call TriggerRegisterPlayerUnitEvent(t, Player(i), EVENT_PLAYER_UNIT_SPELL_EFFECT, Filter(function Hook_Precond))
                set i = i + 1
            endloop
        call TriggerAddCondition(t, Condition(function Hook_Cond))
        call TriggerAddAction(t, function Hook_Start)
    endfunction
    
endscope

(can be made faster and easier to read by using a struct and only attach that to the timer instead of all variables involved)
I'm not sure how to attach structs to timers without using another system.
 
Last edited:
I'm not sure how to attach structs to timers without using another system.
Structs can be attached just like integers (actually struct are just integers in disguise) by using SaveInteger(...)

Btw, you can remove all your temporary locations by using:
JASS:
GetSpellTargetX()
GetSpellTargetY()
GetUnitX()
GetUnitY()
GroupEnumUnitsInRange(group, X, Y, range, filter)
 
Status
Not open for further replies.
Top