• 🏆 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!

GUI Spell System v1.8.0.0

This bundle is marked as director's cut. It exceeds all expectations and excels in every regard.
GUI Spell System
version 1.8.0.0

About:

Spell System revolutionizes the spell making process. It handles spell indexing, end-cast events, timed triggers, configurable unit-in-range filters, memory leak management, gives you access to a part of a global hashtable and reduces your variable and handle count drastically per spell. This system unifies all spell events and runs your triggers for you, directly. The result is an efficient, fully-featured and clean spell coding experience, and my personal favorite contribution to the GUI community.

Included in the demo map is the first spell I've made in 5 years: The Big Dipper. I am pleased to present this project to the Hive community!

Features:
  • Automatic event handling of spells. The system runs your triggers for you once you have configured your spell with a couple of variables.
  • Automatic creation and destruction of spell indices.
  • Automatic variable setting: sets the cast ability, the ability level, caster, target, caster position and target position and more into Spell__ variables.
  • Automatically runs a spell instance periodically if you specify an OnLoop trigger from a spell.
  • Automatic memory management of caster/target locations.
  • Unified common variables to reduce user's need to constantly create the same, redudant variables each time per spell.
  • Automatically gets all units in a range matching pre-specified conditions.
  • You can configure which units in range are chosen from the configuration trigger of your spell.


Spell Preview: Spell System GIF - Find & Share on GIPHY


Required variables

Spell__Ability (ability)

Set in each spell's Config trigger to the ability you want a trigger for.​

Spell System <gen> (trigger)

Run this trigger, ignoring conditions, once all Config variables are set.​

At least one event trigger is required

Spell__Trigger_OnEffect (trigger)

Trigger runs when a unit starts the effect of the specified ability.​

Spell__Trigger_OnFinish (trigger)

When a unit is done casting the ability (completed/canceled).​

Spell__Trigger_OnCast/Channel (trigger)

Usually unused. For more information about what these do, see Casting events guide

Event Responses

Spell__Caster (unit)

The unit casting the spell.​

Spell__Target (unit)

The target unit of the spell, if applicable. Will be set to (no unit) if it was a point-target or no-target spell.​

Spell__CastPoint (point)

The position of the casting unit. If the caster moves, this point moves with it.​

Spell__TargetPoint (point)

The target point of the spell. If the target was a unit and the unit moves, this point moves with it.​

Commonly-extracted variables

Spell__Level (integer)

The level of the ability being cast.​

Spell__CasterOwner (player)

The caster owner is also commonly used to determine friend from foe, or to specify the owner of a dummy unit.​

MUI variables

Spell__Index (integer)

The unique index which is used in an array to store specific stuff with your spell. Using this variable as an array index ensures your spell is fully-MUI.​

Spell__Hash (hashtable)

You can store anything extra you want or need for your spell into this hashtable as long as you only use Spell__Index as the parent key.​

Common Time variables

Does your spell do something over time? If not, skip this section.

Spell__Trigger_OnLoop (trigger)

Usually set from the config trigger, but can be set or changed at any point in the spell's lifespan. This trigger runs after a specified period of time.​

Spell__Time (real)

How long (in seconds) to wait before the OnLoop trigger runs (or runs again, if used sequentially). It must be set to a value above 0.00 to be registered with the system.

Spell__Duration (real)

Specified during Config or during any phase of the spell. Will keep repeating the OnLoop trigger every Spell__Time seconds until the duration is expired.

If you've already set a duration and wish to terminate it early, simply set the Duration to 0.00.

If you need to know if the duration has ended, check if it's equal to or less than 0.00. If it will still run, it will be greater than 0.00.​

Units-in-range essential variables

Spell__InRangePoint (point)

Set to the center of where you want to pick units from.​

Spell__InRange (real)

How wide should the radius be?​

Spell__InRangeGroup (unit group)

Filled with all units in range of the given point who matched the spell filter criteria.​

Spell__Filter_AllowX (boolean)

Specify what kind of targets you allow to be added to Spell__InRangeGroup. Specify any of these filters from your spell's Config trigger if you want custom ones. What you leave unspecified defaults to whatever the filters are set to in Spell System Config <gen>​



Spell__DurationPerLevel (real)

You can specify a duration per level from the Config trigger. The formula is Spell__Duration + (Spell__Level x Spell__DurationPerLevel).​

Spell__StartDuration (boolean)

Set to True if you specified a duration from the Config trigger so the system knows when you intend to have the duration start from.​

Spell__LevelMultiplier (real)

Made available as a real so you don't have to convert the spell level into a real yourself.​



Spell__UseTargetGroup (boolean)

Set to True from the Config trigger if you want a group to keep track of your target units.​

Spell__TargetGroup (unit group)

If you specified Spell__UseTargetGroup, this group is available to you throughout the duration of the spell. It is empty until you manually add units to it.​

Spell__InRangeCount (integer)

The number of units added to Spell__InRangeGroup.​

Spell__InRangeUnits[] (unit array)

An array with indices between 1 and Spell__InRangeCount listing the units in Spell__InRangeGroup.​

Spell__Trigger_InRange (integer)

Set from your spell's Config trigger (if desired), It is used to add additional things to the unit filter that couldn't be covered otherwise.​

Spell__InRangeMax (integer)

Set before setting Spell__InRange. This integer limits the number of units added to Spell__InRangeGroup. The extra units are removed at random.​



Want to detect if a caster has finished channeling or a spell was canceled early?

Spell__Channeling (boolean)

True if the caster is still channeling the ability. Useful from an OnLoop trigger if you want to interrupt the loop if the caster stops.​

Spell__Completed (boolean)

True if the caster successfully completed the channeling of the spell without it getting interrupted. Invaluable if you want to do bonus effects upon successful execution of the ability.​



Inspiration:

  • SpellEvent by Anitarf - Without his system, GUI Spell System wouldn't be nearly what it is now. Almost all the fundamentals of this system are encapsulated by SpellEvent.
  • vJass structs by Vexorian - Create a unique integer variable which will act as an array index. Has OnDestroy functionality which is run automatically and is double-free safe.
  • Timer32 by Jesus4Lyf - One timer for all periodic events; handles iteration of instances for you behind-the-scenes.
  • Constant Timer Loop 32 by Nestharus - A combination of Timer32 and vJass struct creation/destruction which pauses timers/removes system triggers when completed.
  • GroupUtils by Rising_Dusk - Factors in a unit's collision size into the InRange check and recycles unit groups.
  • Table by Vexorian - The idea to use one hashtable with unique indexes as parent keys so it can be used for many different things.


  • Spell System Config
    • Events
    • Conditions
    • Actions
      • -------- Only one dummy unit type is needed as you can attach an effect to it of any kind --------
      • -------- --------
      • Set Spell__DummyType = Dummy (Vexorian, Anitarf, Infrane)
      • Set Spell__DummyOwner = Neutral Extra
      • Set Spell__Interval = (1.00 / 32.00)
      • -------- --------
      • -------- Configure default values for the unit filter: --------
      • -------- --------
      • Set Spell__Filter_AllowEnemy = True
      • Set Spell__Filter_AllowLiving = True
      • Set Spell__Filter_AllowHero = True
      • Set Spell__Filter_AllowNonHero = True
      • Set Spell__Filter_AllowAlly = False
      • Set Spell__Filter_AllowDead = False
      • Set Spell__Filter_AllowFlying = False
      • Set Spell__Filter_AllowMechanical = False
      • Set Spell__Filter_AllowStructure = False
      • -------- --------
      • -------- Magic immunity is a great thing to block, as it also discludes invulnerable units from being picked --------
      • -------- --------
      • Set Spell__Filter_AllowMagicImmune = False
      • -------- --------
      • -------- Normal WC3 abilities, like Channel, wake sleeping creeps - even if they don't deal damage or apply buffs. --------
      • -------- Because of this, I provided an option to wake up creeps when they are enumerated by an InRange command. --------
      • -------- --------
      • Set Spell__WakeTargets = True


JASS:
function SpellIndexGetVars takes integer i returns nothing
    set udg_Spell__Ability = udg_Spell_i_Abil[udg_Spell_i_Head[i]]
    set udg_Spell__Index = i
    set udg_Spell__Caster = udg_Spell_i_Caster[i]
    set udg_Spell__CasterOwner = GetOwningPlayer(udg_Spell__Caster)
    set udg_Spell__Level = udg_Spell_i_Level[i]
    set udg_Spell__LevelMultiplier = udg_Spell__Level //Spell__LevelMultiplier is a real variable.
    set udg_Spell__Target = udg_Spell_i_Target[i]
   
    //Magic to ensure the locations never leak.
    call MoveLocation(udg_Spell__CastPoint, GetUnitX(udg_Spell__Caster), GetUnitY(udg_Spell__Caster))
    if udg_Spell__Target == null then
        call MoveLocation(udg_Spell__TargetPoint, udg_Spell_i_TargetX[i], udg_Spell_i_TargetY[i])
    else
        call MoveLocation(udg_Spell__TargetPoint, GetUnitX(udg_Spell__Target), GetUnitY(udg_Spell__Target))
    endif
    set udg_Spell__TargetGroup = udg_Spell_i_TargetGroup[i]
    set udg_Spell__Completed = udg_Spell_i_Completed[i]
    set udg_Spell__Channeling = udg_Spell_i_Channeling[i]
endfunction
function SpellSetFilters takes integer i returns nothing
    set udg_Spell_i_AllowEnemy[i]       = udg_Spell__Filter_AllowEnemy
    set udg_Spell_i_AllowAlly[i]        = udg_Spell__Filter_AllowAlly
    set udg_Spell_i_AllowDead[i]        = udg_Spell__Filter_AllowDead
    set udg_Spell_i_AllowLiving[i]      = udg_Spell__Filter_AllowLiving
    set udg_Spell_i_AllowMagicImmune[i] = udg_Spell__Filter_AllowMagicImmune
    set udg_Spell_i_AllowMechanical[i]  = udg_Spell__Filter_AllowMechanical
    set udg_Spell_i_AllowStructure[i]   = udg_Spell__Filter_AllowStructure
    set udg_Spell_i_AllowFlying[i]      = udg_Spell__Filter_AllowFlying
    set udg_Spell_i_AllowHero[i]        = udg_Spell__Filter_AllowHero
    set udg_Spell_i_AllowNonHero[i]     = udg_Spell__Filter_AllowNonHero
endfunction
function SpellIndexDestroy takes integer i returns nothing
    local integer indexOf
    local integer index
    if udg_Spell_i_RecycleList[i] >= 0 then
        return
    endif
    //If the caster is still channeling on the spell, don't destroy until it's finished:
    if not udg_Spell_i_Channeling[i] then
        set index = udg_Spell_i_Head[i]
        set udg_Spell_i_RecycleList[i] = udg_Spell_i_Recycle
        set udg_Spell_i_Recycle = i
       
        //Reset things to defaults:
        set udg_Spell_i_Time[i] = 0.00
        set udg_Spell_i_LastTime[i] = 0.00
        set udg_Spell_i_Duration[i] = 0.00
        set udg_Spell_i_Completed[i] = false
        set udg_Spell_i_Caster[i] = null
        set udg_Spell_i_Target[i] = null
        set udg_Spell_i_OnLoopStack[i] = null
       
        //Recycle any applicable target unit group.
        if udg_Spell_i_TargetGroup[i] != null then
            call GroupClear(udg_Spell_i_TargetGroup[i])
            set udg_Spell_i_GroupStack[udg_Spell_i_GroupN] = udg_Spell_i_TargetGroup[i]
            set udg_Spell_i_GroupN = udg_Spell_i_GroupN + 1
            set udg_Spell_i_TargetGroup[i] = null
        endif
       
        //Clear any user-specified data in the hashtable:
        call FlushChildHashtable(udg_Spell__Hash, i)
        //call BJDebugMsg("Destroying index: " + I2S(i))
    endif
   
    set indexOf = udg_Spell_i_StackRef[i]
    if indexOf >= 0 then
        set index = udg_Spell_i_StackN - 1
        set udg_Spell_i_StackN = index
       
        set udg_Spell_i_StackRef[udg_Spell_i_Stack[index]] = indexOf
        set udg_Spell_i_Stack[indexOf] = udg_Spell_i_Stack[index]
        if index == 0 then
            //If no more spells require the timer, pause it.
            call PauseTimer(udg_Spell_i_Timer)
        endif
        set udg_Spell_i_StackRef[i] = -1
    endif
endfunction
function SpellTriggerExecute takes integer i, trigger t returns real
    local real d = udg_Spell_i_Duration[i]
    local boolean b = false
    set udg_Spell__Duration = d
    set udg_Spell__Time = 0.00
    if t != null then
        set udg_Spell__Trigger_OnLoop = null
        set udg_Spell__Expired = d <= 0.00 //If the duration is <= 0, the spell has expired.
        call SpellIndexGetVars(i)
        if TriggerEvaluate(t) then
            call TriggerExecute(t)
        endif
        if udg_Spell__Trigger_OnLoop != null then
            set udg_Spell_i_OnLoopStack[i] = udg_Spell__Trigger_OnLoop
        endif
        //The remaining lines in this function process the duration specified by the user.
        if udg_Spell__StartDuration then
            set udg_Spell__StartDuration = false
            set udg_Spell__Duration = udg_Spell_i_Duration[udg_Spell_i_Head[i]] + udg_Spell_i_LastTime[udg_Spell_i_Head[i]]*udg_Spell__LevelMultiplier
        elseif (udg_Spell__Expired and d > 0.00) or (udg_Spell__Duration <= 0.00) then
            set udg_Spell__Duration = 0.00
            return udg_Spell__Time
            //The user manually expired the spell or the spell duration ended on its own.
        endif
        if d != udg_Spell__Duration then
            //A new duration has been assigned
            set d = udg_Spell__Duration
            set b = true
        endif
        set udg_Spell__Duration = 0.00
        if udg_Spell__Time == 0.00 then
            if udg_Spell_i_LastTime[i] == 0.00 then
                if udg_Spell_i_Time[udg_Spell_i_Head[i]] > 0.00 then
                    //The user specified a default interval to follow:
                    set udg_Spell__Time = udg_Spell_i_Time[udg_Spell_i_Head[i]]
                else
                    //Set the spell time to the minimum.
                    set udg_Spell__Time = udg_Spell__Interval
                endif
            else
                //Otherwise, set it to what it was before.
                set udg_Spell__Time = udg_Spell_i_LastTime[i]
            endif
        //else, the user is specifying a new time for the spell.
        endif
        set udg_Spell_i_LastTime[i] = udg_Spell__Time //Whatever the case, remember this time for next time.
        if b then
            //The duration was just assigned
            set udg_Spell_i_Duration[i] = d
        else
            //The duration has been ongoing
            set udg_Spell_i_Duration[i] = d - udg_Spell__Time
        endif
    endif
    return udg_Spell__Time
endfunction
//===========================================================================
// Runs every Spell__Interval seconds and handles all of the timed events.
//
function SpellTimerLoop takes nothing returns nothing
    local integer i = udg_Spell_i_StackN
    local integer node
    local real time
    set udg_Spell__Running = true
   
    //Run stack top to bottom to avoid skipping slots when destroying.
    loop
        set i = i - 1
        exitwhen i < 0
        set node = udg_Spell_i_Stack[i]
        set time = udg_Spell_i_Time[node] - udg_Spell__Interval
        if time <= 0.00 then
            set time = SpellTriggerExecute(node, udg_Spell_i_OnLoopStack[node])
        endif
        if time <= 0.00 then
            call SpellIndexDestroy(node)
        else
            set udg_Spell_i_Time[node] = time
        endif
    endloop
    set udg_Spell__Running = false
endfunction
//===========================================================================
// This is the meat of the system as it handles the event responses.
//
function RunSpellEvent takes nothing returns boolean
    local boolean b
    local integer aid = GetSpellAbilityId()
    local integer head = LoadInteger(udg_Spell__Hash, 0, aid)
    local integer i
    local integer id
    local trigger t
    local playerunitevent eid
    if head == 0 then
        //Nothing for this ability has been registered. Skip the sequence.
        return false
    endif
    set eid = ConvertPlayerUnitEvent(GetHandleId(GetTriggerEventId()))
    set udg_Spell__Caster = GetTriggerUnit()
    set id = GetHandleId(udg_Spell__Caster)
    set i = LoadInteger(udg_Spell__Hash, aid, id)
    if i == 0 then
        //This block will almost always happen with the OnChannel event. In the
        //case of Charge Gold and Lumber, only an OnEffect event will run.
        set i = udg_Spell_i_Recycle
        if i == 0 then
            //Create a new, unique index
            set i = udg_Spell_i_Instances + 1
            set udg_Spell_i_Instances = i
        else
            //Repurpose an existing one
            set udg_Spell_i_Recycle = udg_Spell_i_RecycleList[i]
        endif
        //call BJDebugMsg("Creating index: " + I2S(i))
        set udg_Spell_i_RecycleList[i] = -1
        set udg_Spell_i_StackRef[i] = -1
        set udg_Spell_i_Head[i] = head
       
        if eid == EVENT_PLAYER_UNIT_SPELL_CHANNEL then
            set udg_Spell_i_Channeling[i] = true
            call SaveInteger(udg_Spell__Hash, aid, id, i)
            set t = udg_Spell_i_OnChannelStack[head]
        else //eid == EVENT_PLAYER_UNIT_SPELL_EFFECT
            set t = udg_Spell_i_OnEffectStack[head]
        endif
        set udg_Spell_i_Caster[i] = udg_Spell__Caster
        set udg_Spell_i_Level[i] = GetUnitAbilityLevel(udg_Spell__Caster, aid)
        set udg_Spell_i_Target[i] = GetSpellTargetUnit()
        set udg_Spell_i_TargetX[i] = GetSpellTargetX()
        set udg_Spell_i_TargetY[i] = GetSpellTargetY()
       
        set udg_Spell_i_OnLoopStack[i] = udg_Spell_i_OnLoopStack[head]
        if udg_Spell_i_UseTG[head] then
            //Get a recycled unit group or create a new one.
            set id = udg_Spell_i_GroupN - 1
            if id >= 0 then
                set udg_Spell_i_GroupN = id
                set udg_Spell_i_TargetGroup[i] = udg_Spell_i_GroupStack[id]
            else
                set udg_Spell_i_TargetGroup[i] = CreateGroup()
            endif
        endif
    elseif eid == EVENT_PLAYER_UNIT_SPELL_CAST then
        set t = udg_Spell_i_OnCastStack[head]
    elseif eid == EVENT_PLAYER_UNIT_SPELL_EFFECT then
        set t = udg_Spell_i_OnEffectStack[head]
    elseif eid == EVENT_PLAYER_UNIT_SPELL_FINISH then
        set udg_Spell_i_Completed[i] = true
        return true
    else //eid == EVENT_PLAYER_UNIT_SPELL_ENDCAST
        set udg_Spell_i_Channeling[i] = false
        call RemoveSavedInteger(udg_Spell__Hash, aid, id)
        set t = udg_Spell_i_OnFinishStack[head]
    endif
    if SpellTriggerExecute(i, t) > 0.00 then
        //Set the spell time to the user-specified one.
        set udg_Spell_i_Time[i] = udg_Spell__Time
        if udg_Spell_i_StackRef[i] < 0 then
            //Allocate the spell index onto the loop stack.
            set aid = udg_Spell_i_StackN
            set udg_Spell_i_Stack[aid] = i
            set udg_Spell_i_StackRef[i] = aid
            set udg_Spell_i_StackN = aid + 1
            if aid == 0 then
                //If this is the first spell index using the timer, start it up:
                call TimerStart(udg_Spell_i_Timer, udg_Spell__Interval, true, function SpellTimerLoop)
            endif
        endif
    elseif (not udg_Spell_i_Channeling[i]) and (t != null or udg_Spell_i_Time[i] <= 0.00) then
        call SpellIndexDestroy(i)
    endif
    set t = null
    return true
endfunction
//This function is invoked if an event was launched recursively by another event's callback.
function RunPreSpellEvent takes nothing returns nothing
    local integer i = udg_Spell__Index
    local real time = udg_Spell__Time
    local real d = udg_Spell__Duration
    local boolean expired = udg_Spell__Expired
    if udg_Spell__Trigger_OnLoop != null then
        set udg_Spell_i_OnLoopStack[i] = udg_Spell__Trigger_OnLoop
    endif
    if RunSpellEvent() then
        set udg_Spell__Time = time
        set udg_Spell__Duration = d
        set udg_Spell__Expired = expired
        call SpellIndexGetVars(i)
    endif
endfunction
//===========================================================================
// Base function of the system: runs when an ability event does something.
//
function SpellSystemEvent takes nothing returns boolean
    if udg_Spell__Running then
        call RunPreSpellEvent()
    else
        set udg_Spell__Running = true
        call RunSpellEvent()
        set udg_Spell__Running = false
    endif
    return false
endfunction
//===========================================================================
// Set Spell__Ability to your spell's ability
// Set Spell__Trigger_OnChannel/Cast/Effect/Finish/Loop to any trigger(s) you
// want to automatically run.
//
// GUI-friendly: Run Spell System <gen> (ignoring conditions)
//
function SpellSystemRegister takes nothing returns nothing
    local integer aid = udg_Spell__Ability
    local integer head = udg_Spell_i_Instances + 1
   
    if HaveSavedInteger(udg_Spell__Hash, 0, aid) or aid == 0 then
        //The system rejects duplicate or unassigned abilities.
        return
    endif
    set udg_Spell_i_Instances = head
    set udg_Spell_i_Abil[head] = aid
   
    //Preload the ability on dummy unit to help prevent first-instance lag
    call UnitAddAbility(udg_Spell_i_PreloadDummy, aid)
   
    //Save head index to the spell ability so it be referenced later.
    call SaveInteger(udg_Spell__Hash, 0, aid, head)
   
    //Set any applicable event triggers.
    set udg_Spell_i_OnChannelStack[head]= udg_Spell__Trigger_OnChannel
    set udg_Spell_i_OnCastStack[head]   = udg_Spell__Trigger_OnCast
    set udg_Spell_i_OnEffectStack[head] = udg_Spell__Trigger_OnEffect
    set udg_Spell_i_OnFinishStack[head] = udg_Spell__Trigger_OnFinish
    set udg_Spell_i_OnLoopStack[head]   = udg_Spell__Trigger_OnLoop
    set udg_Spell_i_InRangeFilter[head] = udg_Spell__Trigger_InRangeFilter
   
    //Set any customized filter variables:
    call SpellSetFilters(head)
   
    //Tell the system to automatically create target groups, if needed
    set udg_Spell_i_AutoAddTargets[head] = udg_Spell__AutoAddTargets
    set udg_Spell_i_UseTG[head] = udg_Spell__UseTargetGroup or udg_Spell__AutoAddTargets
   
    //Handle automatic buff assignment
    set udg_Spell_i_BuffAbil[head] = udg_Spell__BuffAbility
    set udg_Spell_i_BuffOrder[head] = udg_Spell__BuffOrder
   
    //Set the default time sequences if a duration is used:
    set udg_Spell_i_Time[head]     = udg_Spell__Time
    set udg_Spell_i_Duration[head] = udg_Spell__Duration
    set udg_Spell_i_LastTime[head] = udg_Spell__DurationPerLevel
   
    //Set variables back to their defaults:
    set udg_Spell__Trigger_OnChannel    = null
    set udg_Spell__Trigger_OnCast       = null
    set udg_Spell__Trigger_OnEffect     = null
    set udg_Spell__Trigger_OnFinish     = null
    set udg_Spell__Trigger_OnLoop       = null
    set udg_Spell__Trigger_InRangeFilter= null
    set udg_Spell__AutoAddTargets       = false
    set udg_Spell__UseTargetGroup       = false
    set udg_Spell__Time                 = 0.00
    set udg_Spell__Duration             = 0.00
    set udg_Spell__DurationPerLevel     = 0.00
    set udg_Spell__BuffAbility          = 0
    set udg_Spell__BuffOrder            = 0
   
    set udg_Spell__Filter_AllowEnemy        = udg_Spell_i_AllowEnemy[0]
    set udg_Spell__Filter_AllowAlly         = udg_Spell_i_AllowAlly[0]
    set udg_Spell__Filter_AllowDead         = udg_Spell_i_AllowDead[0]
    set udg_Spell__Filter_AllowMagicImmune  = udg_Spell_i_AllowMagicImmune[0]
    set udg_Spell__Filter_AllowMechanical   = udg_Spell_i_AllowMechanical[0]
    set udg_Spell__Filter_AllowStructure    = udg_Spell_i_AllowStructure[0]
    set udg_Spell__Filter_AllowFlying       = udg_Spell_i_AllowFlying[0]
    set udg_Spell__Filter_AllowHero         = udg_Spell_i_AllowHero[0]
    set udg_Spell__Filter_AllowNonHero      = udg_Spell_i_AllowNonHero[0]
    set udg_Spell__Filter_AllowLiving       = udg_Spell_i_AllowLiving[0]
endfunction
function SpellFilterCompare takes boolean is, boolean yes, boolean no returns boolean
    return (is and yes) or ((not is) and no)
endfunction
//===========================================================================
// Before calling this function, set Spell__InRangePoint to whatever point
// you need, THEN set Spell__InRange to the radius you need. The system will
// enumerate the units matching the configured filter and fill them into
// Spell_InRangeGroup.
//
function SpellGroupUnitsInRange takes nothing returns boolean
    local integer i = udg_Spell_i_Head[udg_Spell__Index]
    local integer j = 0
    local unit u
    local real padding = 64.00
    if udg_Spell_i_AllowStructure[i] then
        //A normal unit can only have up to size 64.00 collision, but if the
        //user needs to check for structures we need a padding big enough for
        //the "fattest" ones: Tier 3 town halls.
        set padding = 197.00
    endif
    call GroupEnumUnitsInRangeOfLoc(udg_Spell__InRangeGroup, udg_Spell__InRangePoint, udg_Spell__InRange + padding, null)
    loop
        set u = FirstOfGroup(udg_Spell__InRangeGroup)
        exitwhen u == null
        call GroupRemoveUnit(udg_Spell__InRangeGroup, u)
        loop
            exitwhen udg_Spell_i_AutoAddTargets[i] and IsUnitInGroup(u, udg_Spell__TargetGroup)
            exitwhen not IsUnitInRangeLoc(u, udg_Spell__InRangePoint, udg_Spell__InRange)
            exitwhen not SpellFilterCompare(IsUnitType(u, UNIT_TYPE_DEAD), udg_Spell_i_AllowDead[i], udg_Spell_i_AllowLiving[i])
            exitwhen not SpellFilterCompare(IsUnitAlly(u, udg_Spell__CasterOwner), udg_Spell_i_AllowAlly[i], udg_Spell_i_AllowEnemy[i])
            exitwhen not SpellFilterCompare(IsUnitType(u, UNIT_TYPE_HERO) or IsUnitType(u, UNIT_TYPE_RESISTANT), udg_Spell_i_AllowHero[i], udg_Spell_i_AllowNonHero[i])
            exitwhen IsUnitType(u, UNIT_TYPE_STRUCTURE) and not udg_Spell_i_AllowStructure[i]
            exitwhen IsUnitType(u, UNIT_TYPE_FLYING) and not udg_Spell_i_AllowFlying[i]
            exitwhen IsUnitType(u, UNIT_TYPE_MECHANICAL) and not udg_Spell_i_AllowMechanical[i]
            exitwhen IsUnitType(u, UNIT_TYPE_MAGIC_IMMUNE) and not udg_Spell_i_AllowMagicImmune[i]
            set udg_Spell__InRangeUnit = u
            //Run the user's designated filter, if one exists.
            exitwhen udg_Spell_i_InRangeFilter[i] != null and not TriggerEvaluate(udg_Spell_i_InRangeFilter[i])
            set j = j + 1
            set udg_Spell__InRangeUnits[j] = u
            exitwhen true
        endloop
    endloop
    if j > udg_Spell__InRangeMax and udg_Spell__InRangeMax > 0 then
        //The user has defined a maximum number of units allowed in the group.
        //Remove a random unit until the total does not exceed capacity.
        loop
            set i = GetRandomInt(1, j)
            set udg_Spell__InRangeUnits[i] = udg_Spell__InRangeUnits[j]
            set j = j - 1
            exitwhen j == udg_Spell__InRangeMax
        endloop
    endif
    set udg_Spell__InRangeCount = j
    set udg_Spell__InRangeMax = 0
    set udg_Spell__InRange = 0.00
    set i = udg_Spell_i_Head[udg_Spell__Index]
    loop
        exitwhen j == 0
        set u = udg_Spell__InRangeUnits[j]
        call GroupAddUnit(udg_Spell__InRangeGroup, u)
        if udg_Spell_i_AutoAddTargets[i] then
            call GroupAddUnit(udg_Spell__TargetGroup, u)
        endif
        if udg_Spell__WakeTargets and UnitIsSleeping(u) then
            call UnitWakeUp(u)
        endif
        if udg_Spell_i_BuffAbil[i] != 0 and udg_Spell_i_BuffOrder[i] != 0 then
            //Auto-buff units added to group:
            call UnitAddAbility(udg_Spell_i_PreloadDummy, udg_Spell_i_BuffAbil[i])
            call IssueTargetOrderById(udg_Spell_i_PreloadDummy, udg_Spell_i_BuffOrder[i], u)
            call UnitRemoveAbility(udg_Spell_i_PreloadDummy, udg_Spell_i_BuffAbil[i])
        endif
        set j = j - 1
    endloop
    set u = null
    return false
endfunction
function SpellPreloadEnd takes nothing returns nothing
    local integer i = udg_Spell_i_Instances
    loop
        exitwhen i == 0
        //Remove preloaded abilities so they don't interfere with orders
        call UnitRemoveAbility(udg_Spell_i_PreloadDummy, udg_Spell_i_Abil[udg_Spell_i_Head[i]])
        set i = i - 1
    endloop
endfunction
//===========================================================================
function InitTrig_Spell_System takes nothing returns nothing
    local integer i = bj_MAX_PLAYER_SLOTS
    local player p
    local trigger t
   
    if gg_trg_Spell_System != null then
        //A JASS function call already initialized the system.
        return
    endif
   
    //This runs before map init events so the hashtable is ready before then.
    set udg_Spell__Hash = InitHashtable()
   
    //Initialize these two locations which will never get removed
    set udg_Spell__CastPoint = Location(0, 0)
    set udg_Spell__TargetPoint = Location(0, 0)
   
    //Recycle existing unit groups into the recycle stack to avoid needing to destroy any extras.
    set udg_Spell_i_GroupStack[2] = udg_Spell__TargetGroup
    set udg_Spell_i_GroupStack[3] = udg_Spell_i_TargetGroup[0]
    set udg_Spell_i_GroupStack[4] = udg_Spell_i_TargetGroup[1]
    set udg_Spell_i_GroupN = 5 //There are already five valid unit groups thanks to Variable Editor.
   
    set t = CreateTrigger()
    call TriggerRegisterVariableEvent(t, "udg_Spell__InRange", GREATER_THAN, 0.00)
    call TriggerAddCondition(t, Filter(function SpellGroupUnitsInRange))
   
    set t = CreateTrigger()
    call TriggerAddCondition(t, Filter(function SpellSystemEvent))
    loop
        set i = i - 1
        set p = Player(i)
        call TriggerRegisterPlayerUnitEvent(t, p, EVENT_PLAYER_UNIT_SPELL_CHANNEL, null)
        call TriggerRegisterPlayerUnitEvent(t, p, EVENT_PLAYER_UNIT_SPELL_CAST, null)
        call TriggerRegisterPlayerUnitEvent(t, p, EVENT_PLAYER_UNIT_SPELL_EFFECT, null)
        call TriggerRegisterPlayerUnitEvent(t, p, EVENT_PLAYER_UNIT_SPELL_FINISH, null)
        call TriggerRegisterPlayerUnitEvent(t, p, EVENT_PLAYER_UNIT_SPELL_ENDCAST, null)
        exitwhen i == 0
    endloop
    set p = null
    set t = null
   
    //Run the configuration trigger so its variables are ready before the
    //map initialization events run.
    call TriggerExecute(gg_trg_Spell_System_Config)
    call SpellSetFilters(0)
   
    //Create this trigger so it's GUI-friendly.
    set gg_trg_Spell_System = CreateTrigger()
    call TriggerAddAction(gg_trg_Spell_System, function SpellSystemRegister)
    set gg_trg_Spell_System_Config = gg_trg_Spell_System //In case the user accidentally picks this one
   
    //Create a dummy unit for preloading abilities and casting buffs.
    set udg_Spell_i_PreloadDummy = CreateUnit(udg_Spell__DummyOwner, udg_Spell__DummyType, 0, 0, 0)
   
    //Start the timer to remove its abilities:
    call TimerStart(udg_Spell_i_Timer, 0.00, false, function SpellPreloadEnd)
    call UnitRemoveAbility(udg_Spell_i_PreloadDummy, 'Amov') //Force it to never move to cast spells
endfunction


  • SS Holy Light Demo Config
    • Events
      • Map initialization
    • Conditions
    • Actions
      • Set Spell__Ability = Holy Light
      • -------- When holy light is cast on a unit, heal it each second for 4/6/8 seconds. --------
      • Set Spell__Time = 1.00
      • Set Spell__Duration = 2.00
      • Set Spell__DurationPerLevel = 2.00
      • -------- --------
      • Set Spell__Trigger_OnEffect = SS Holy Light Demo Effect <gen>
      • Set Spell__Trigger_OnLoop = SS Holy Light Demo Loop <gen>
      • Trigger - Run Spell System <gen> (ignoring conditions)
  • SS Holy Light Demo Effect
    • Events
    • Conditions
    • Actions
      • Set Spell__StartDuration = True
      • Special Effect - Create a special effect attached to the origin of Spell__Target using Abilities\Spells\NightElf\Rejuvenation\RejuvenationTarget.mdl
      • Set HolyLightBuff[Spell__Index] = (Last created special effect)
  • SS Holy Light Demo Loop
    • Events
    • Conditions
    • Actions
      • -------- Heals for 10/15/20 each second --------
      • Unit - Set life of Spell__Target to ((Life of Spell__Target) + (5.00 + (5.00 x Spell__LevelMultiplier)))
      • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
        • If - Conditions
          • Spell__Duration Less than or equal to 0.00
        • Then - Actions
          • -------- When the heal-over-time has ended, destroy the rejuvenation effect --------
          • Special Effect - Destroy HolyLightBuff[Spell__Index]
        • Else - Actions



JASS:
// This function is run every 1.00 for each active divine shield
function SSDivineShieldDemoLoop takes nothing returns nothing
    local integer i = 1
    set udg_Spell__InRangePoint = udg_Spell__CastPoint
    set udg_Spell__InRange = 600.00
    //Spell__CastPoint will always be the Caster's own location, even if he moves from where he originally casted
    loop
        exitwhen i > udg_Spell__InRangeCount
        call IssuePointOrderLoc(udg_Spell__InRangeUnits[i], "move", udg_Spell__CastPoint)
        set i = i + 1
    endloop
endfunction

// The next function will be run when the effect of Divine Shield starts.
function SSDivineShieldDemoEffect takes nothing returns nothing
    //Set the time until next expiration
    set udg_Spell__Time = 1.00
    //Set how long the buff will last
    set udg_Spell__Duration = 15.00*udg_Spell__Level
endfunction

//===========================================================================
function InitTrig_SS_Divine_Shield_Demo takes nothing returns nothing
    //Use of this spell requires you to copy the SpellSystem_Register function from the
    //map header or use the vJass library which contains it.
    call SpellSystem_Register('AHds', function SSDivineShieldDemoEffect, function SSDivineShieldDemoLoop)
endfunction


Keywords:
Spell, system, gui, anitarf, vexorian, jesus4lyf, timer32, spellevent, nestharus, struct
Contents

GUI Spell System (Map)

Reviews
Approved on the 11th Jan 2016 Last review update on the 4th Apr 2016 This resource has been approved by THW moderators BPower and IcemanBo. In consultation with the spell moderation team we agreed on a rating of 6/5, Director's Cut. Criticism...
Thanks for the positive feedback, you two.

I have updated the demo map to include a "new" (old, actually) spell: Blizzard. It took me about an hour to figure out how to configure everything to line up with the one in Object Editor. The hope is to show how to understand the system by comparing it to a very familiar spell:

  • Blizzard Config
    • Events
      • Map initialization
    • Conditions
    • Actions
      • -------- Set the ability from Object Editor which represents the spell --------
      • Set Spell__Ability = Edited Blizzard
      • -------- Set the duration at least as high as the maximum duration in Object Editor. It will stop as soon as the caster is done, anyway. --------
      • Set Spell__Duration = 10.00
      • -------- The Spell__Time is the time between waves. --------
      • Set Spell__Time = 1.00
      • -------- Configure the types of units to be damaged by Blizzard --------
      • Set Spell__Filter_AllowAlly = True
      • Set Spell__Filter_AllowFlying = True
      • Set Spell__Filter_AllowMechanical = True
      • Set Spell__Filter_AllowStructure = True
      • -------- Set the trigger to run when Blizzard is cast --------
      • Set Spell__Trigger_OnCast = Blizzard Start <gen>
      • -------- Set the trigger to run each interval: --------
      • Set Spell__Trigger_OnLoop = Blizzard Wave <gen>
      • -------- Lock in the settings with Spell System by running the system trigger: --------
      • Trigger - Run Spell System <gen> (ignoring conditions)
      • -------- --------
      • -------- Configure the AoE of Blizzard and the number & type of effects: --------
      • Set Blizzard_AoE = 200.00
      • Set Blizzard_Shards[1] = 6
      • Set Blizzard_Shards[2] = 7
      • Set Blizzard_Shards[3] = 10
      • Set Blizzard_FX = Abilities\Spells\Human\Blizzard\BlizzardTarget.mdl
  • Blizzard Start
    • Events
    • Conditions
    • Actions
      • -------- Tell the system to run the wave trigger each Spell__Time until the caster is done channeling the ability. --------
      • Set Spell__StartDuration = True
      • -------- Create shards in advance so they hit the ground at the same time the damage is dealt --------
      • Trigger - Run Blizzard Shards <gen> (ignoring conditions)
  • Blizzard Wave
    • Events
    • Conditions
    • Actions
      • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
        • If - Conditions
          • Spell__Channeling Equal to False
        • Then - Actions
          • -------- Stop future triggered effects if the caster is no longer channeling the ability --------
          • Set Spell__Expired = True
        • Else - Actions
          • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
            • If - Conditions
              • Spell__Expired Equal to False
            • Then - Actions
              • -------- Only create new shards if the spell will still continue for at least one more wave --------
              • Trigger - Run Blizzard Shards <gen> (ignoring conditions)
            • Else - Actions
      • -------- Pick all units within designated range of the target point --------
      • Set Spell__InRangePoint = Spell__TargetPoint
      • Set Spell__InRange = Blizzard_AoE
      • Unit Group - Pick every unit in Spell__InRangeGroup and do (Actions)
        • Loop - Actions
          • -------- Have the caster damage all units in the area for 30/40/50 per wave --------
          • -------- For simplicity, I did not do things like damage caps nor reduction against buildings --------
          • Unit - Cause Spell__Caster to damage (Picked unit), dealing (20.00 + (10.00 x Spell__LevelMultiplier)) damage of attack type Spells and damage type Normal
  • Blizzard Shards
    • Events
    • Conditions
    • Actions
      • -------- Create shards at random points in the AoE --------
      • For each (Integer A) from 1 to Blizzard_Shards[Spell__Level], do (Actions)
        • Loop - Actions
          • Set TempPoint = (Spell__TargetPoint offset by (Random real number between 0.00 and Blizzard_AoE) towards (Random angle) degrees)
          • Special Effect - Create a special effect at TempPoint using Blizzard_FX
          • Special Effect - Destroy (Last created special effect)
          • Custom script: call RemoveLocation(udg_TempPoint)
 
Last edited:
Level 5
Joined
Mar 6, 2015
Messages
130
Nice System Bribe
kinda delighted me specially for the clean up part
i should use it to understand it better but according to the video thats a nice Tool for GUI users like me the timer interval thing is nice thing though but still it`s a little complicated when you encounter it for the first time
Goodjob
 
Level 3
Joined
Jul 7, 2010
Messages
15
Always adored your resources man, but you've really outdone yourself here. Really looking forward to using this in my map!

LOVE THE EFFICIENCY! :D

Edit: For some reason I feel like Spell_Filter_AllowCaster should be an option in the list of target filters. There are quite a few occasions where the mapmaker might want the spell to target allies but not his own hero. What do you think?
 
Last edited:
UPDATE to 1.6.0.0:

Added Spell__InRangeMax to limit the number of units selected by the group to that number. The system integrates a much faster version of "Random N Units in Unit Group" as any extra units are selected at random to be removed from the group.

Added Spell__Trigger_InRangeFilter so the user can specify their own conditions (if desired) to enhance the power of the InRange filter which was otherwise limited to whether the unit has certain properties. You can specify a trigger with only conditions for this variable to work properly. You'll be able to check for things like a unit having a certain buff or being in a certain unit group.

Deprecated Spell__Expired. For backwards-compatibility, it can still be read and set, but I recommend switching to the cleaner "Spell__Duration" reads and sets. When you previously checked if Spell__Expired is True to assess if the spell is about to end, you can now check if Spell__Duration is less than or equal to 0.00. When you previously wanted to terminate the duration early, set Spell__Duration to 0.00.



@DAT-C3 you have the debug message on a part of the code that only runs when the slash has expired.
 
what do you mean? You only mentioned that one thing.

[trigger=This works]MS Spin Wave
Events
Conditions
Actions
If (All Conditions are True) then do (Then Actions) else do (Else Actions)
If - Conditions
Spell__Channeling Equal to False
Then - Actions
-------- Stop future triggered effects if the caster is no longer channeling the ability --------
Set Spell__InRangePoint = Spell__TargetPoint
Game - Display to (All players) the text: (Name of Spell__Caster)
Set Spell__InRange = 256.00
Animation - Change Spell__Caster flying height to 0.00 at 0.00
Unit Group - Pick every unit in Spell__InRangeGroup and do (Actions)
Loop - Actions
Animation - Change (Picked unit) flying height to 0.00 at 0.00
Set Spell__Expired = True
Else - Actions
If (All Conditions are True) then do (Then Actions) else do (Else Actions)
If - Conditions
Spell__Expired Equal to False
Then - Actions
-------- Only create new shards if the spell will still continue for at least one more wave --------
Else - Actions
-------- Pick all units within designated range of the target point --------
Set Spell__InRangePoint = Spell__TargetPoint
Set Spell__InRange = 256.00
Animation - Change Spell__Caster flying height to ((Current flying height of Spell__Caster) + 10.00) at 0.00
Unit Group - Pick every unit in Spell__InRangeGroup and do (Actions)
Loop - Actions
Animation - Change (Picked unit) flying height to ((Current flying height of (Picked unit)) + 10.00) at 0.00
Unit - Cause Spell__Caster to damage (Picked unit), dealing (0.00 + (1.00 x Spell__LevelMultiplier)) damage of attack type Spells and damage type Normal
[/trigger]

[trigger= This doesn't work]slash run
Events
Conditions
Actions
If (All Conditions are True) then do (Then Actions) else do (Else Actions)
If - Conditions
Spell__Channeling Equal to False
Then - Actions
-------- Stop future triggered effects if the caster is no longer channeling the ability --------
Set Spell__InRangePoint = Spell__TargetPoint
Set Spell__InRange = 128.00
Animation - Change Spell__Caster's animation speed to 100.00% of its original speed
Game - Display to (All players) the text: (Name of Spell__Caster)
Set Spell__Expired = True
Else - Actions
If (All Conditions are True) then do (Then Actions) else do (Else Actions)
If - Conditions
Spell__Expired Equal to False
Then - Actions
-------- Only create new shards if the spell will still continue for at least one more wave --------
Else - Actions
-------- Pick all units within designated range of the target point --------
Set Spell__InRangePoint = Spell__TargetPoint
Set Spell__InRange = 160.00
Unit Group - Pick every unit in Spell__InRangeGroup and do (Actions)
Loop - Actions
Unit - Cause Spell__Caster to damage (Picked unit), dealing (50.00 + (1.00 x Spell__LevelMultiplier)) damage of attack type Spells and damage type Normal
[/trigger]


It doesn't set the casters animation speed back to 100 let alone know the caster from the looks of it.
 
Can the Spell__Interval variable be changed?
Also, I have a spell using this system that needs debugging:

  • Ghost viper config
    • Events
      • Map initialization
    • Conditions
    • Actions
      • Set Spell__Ability = Ghost Viper
      • Set Spell__Trigger_OnEffect = Ghost viper effect <gen>
      • Set Spell__Trigger_OnLoop = Ghost viper loop <gen>
      • Trigger - Run Spell System <gen> (checking conditions)
  • Ghost viper effect
    • Events
    • Conditions
    • Actions
      • Set Spell__Time = 0.02
      • Set Spell__Duration = 9.99
      • -------- --------
      • Set GhostOn[(Custom value of Spell__Caster)] = True
      • Unit - Add Permanent Invisibility to Spell__Caster
      • -------- --------
  • Ghost viper loop
    • Events
    • Conditions
    • Actions
      • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
        • If - Conditions
          • Or - Any (Conditions) are true
            • Conditions
              • UnitMoving[(Custom value of Spell__Caster)] Equal to True
              • Spell__Expired Equal to True
        • Then - Actions
          • Set GhostOn[(Custom value of Spell__Caster)] = False
          • Unit - Remove Permanent Invisibility from Spell__Caster
        • Else - Actions
  • Ghost viper effect break attack
    • Events
      • Game - DamageModifierEvent becomes Equal to 1.00
    • Conditions
      • GhostOn[(Custom value of DamageEventSource)] Equal to True
    • Actions
      • Set DamageEventAmount = (DamageEventAmount + 100.00)
      • Set DamageEventType = DamageTypeCriticalStrike
      • Set GhostOn[(Custom value of DamageEventSource)] = False
      • Unit - Remove Permanent Invisibility from DamageEventSource
  • Ghost viper effect break spell
    • Events
      • Unit - A unit Begins casting an ability
      • Unit - A unit Uses an item
    • Conditions
      • GhostOn[(Custom value of (Triggering unit))] Equal to True
    • Actions
      • Set GhostOn[(Custom value of (Triggering unit))] = False
      • Unit - Remove Permanent Invisibility from (Triggering unit)
Basically, this spell makes the caster invisible but it will lose invisibility if it moves, attacks, so on. I set duration to 10s. However, in-game, the Permanent Invisibility spell always lasts longer than 10 seconds (if the caster doesn't do any action to break invisibility) after activation.
 
Last edited:
Level 18
Joined
Oct 17, 2012
Messages
822
In my test map, I register a spell (FK) with the RegisterSpellSystem function, and its loop function does not run.

When I import this spell and its corresponding objects into your demo map, this spell as well as your spells do not function properly. Dummy units do not spawn, enemy units are not knockbacked, casting might even fail, etc.

Notes:
1. FK is registered in a scope initializer.
2. Other spells will work properly once the trigger for FK is disabled.
3. FK will succeed at OnEffect in your demo map if the code for this spell is not in a scope. The loop function still does not run, however.

What is happening? Corruption? What am I doing wrong? Should I not register the spell in a scope initializer?

I will recode the spell in GUI to see if above issues persist.
 

Attachments

  • GUI Spell System - FK.w3x
    96.9 KB · Views: 50
Last edited:
You have to use the JASS initializer when running this from custom script, as the events might run before Spell System is initialized.

Instead of running the Spell System trigger, you would use something like this script:

JASS:
function InitTrig_SS_Divine_Shield_Demo takes nothing returns nothing
    //Use of this spell requires you to copy the SpellSystem_Register function from the
    //map header or use the vJass library which contains it.
    call SpellSystem_Register('AHds', function SSDivineShieldDemoEffect, function SSDivineShieldDemoLoop)
endfunction
 
Level 18
Joined
Oct 17, 2012
Messages
822
JASS:
//CONFIGURATION
constant function AbilityID takes nothing returns integer
    return 'A003'
endfunction

constant function DummyID takes nothing returns integer
    return 'h000'
endfunction

constant function KnivesCount takes nothing returns integer
    return 18
endfunction

constant function Speed takes nothing returns real
    return 15.
endfunction

constant function Distance takes nothing returns real
    return 500.
endfunction

constant function Origin_Z takes nothing returns real
    return 50.
endfunction

constant function Anim_Speed takes nothing returns real
    return 45.
endfunction

constant function Spiral_Angle takes nothing returns real
    return 4.
endfunction

constant function Damage takes real level returns real
    return 75*level
endfunction

constant function CollisionSize takes nothing returns real
    return 90.
endfunction

constant function AttackType takes nothing returns attacktype
    return ATTACK_TYPE_NORMAL
endfunction

constant function DamageType takes nothing returns damagetype
    return DAMAGE_TYPE_UNIVERSAL
endfunction
//END CONFIGURATION

function OnLoop takes nothing returns nothing
    local integer count = udg_index
    local real angle
    local unit u
   
   
    call BJDebugMsg("Looping!")
   
    loop
        exitwhen count < 0
       
        set u = udg_knives[count]
       
        if udg_dist[count] > 0 then
            set angle = Deg2Rad(GetUnitFacing(u) + Spiral_Angle())
            call SetUnitX(u, GetUnitX(u) + Speed()*Cos(angle))
            call SetUnitY(u, GetUnitY(u) + Speed()*Sin(angle))
           
            set udg_dist[count] = udg_dist[count] - Speed()
        else
            call BJDebugMsg("Done!")
            call KillUnit(u)
            call RemoveUnit(u)
           
            set udg_knives[count] = udg_knives[udg_index]
            set udg_dist[count] = udg_dist[udg_index]
            set count = count - 1
            set udg_index = udg_index - 1
           
            if udg_index < 0 then
                set udg_Spell__Time = 0.
            endif
        endif
       
        set u = null
       
        set count = count - 1
    endloop
endfunction

function OnCast takes nothing returns nothing
    local integer count = KnivesCount()
    local real angle = 360/KnivesCount()
    local real angle2 = 0.
    local unit u = GetTriggerUnit()
   
    call BJDebugMsg("OnCast!")
   
    loop
        exitwhen count == 0

        set udg_knives[udg_index] = CreateUnit(Player(15), DummyID(), GetUnitX(u), GetUnitY(u), angle2)
        set udg_dist[udg_index] = Distance()
       
        if udg_index == 0 then
            call BJDebugMsg("Start Loop!")
            set udg_Spell__Time = 9999.
        endif
       
        set udg_index = udg_index + 1
       
        call SetUnitTimeScale(udg_knives[udg_index], Anim_Speed()*0.01)
        call SetUnitFlyHeight(udg_knives[udg_index], Origin_Z(), 0.)
       
        set angle2 = angle2 + angle
       
        set count = count - 1
    endloop
   
    set u = null
endfunction


function InitTrig_FK_Spell takes nothing returns nothing
    call RegisterSpellSystem(AbilityID(), function OnCast, function OnLoop)
endfunction
 
Regarding my spell, I think setting Spell__Time to Spell__Interval was able to solve the problem described. However, the base spell has to be a "No Target" spell that will stop the unit upon casting. If the unit still moves after casting then it would defeat the purpose of the spell. Does anyone know of an instant spell in the game that stops the caster after casting?
 
Level 18
Joined
Oct 17, 2012
Messages
822
I'm not sure what udg_index is, but it's not MUI. You would need it to be an array variable with Spell__Index as the index.

udg_index is for placing the dummy units into a list (acts as an unit dummy group) for later reference. Anyway, I have fixed this already. However, this is not the major issue.

The loop function does not run in any tests that I have done until today.

This problem persevered until I used an initial duration as shown in your JASS example and made copies of the original triggers. Loop functions ran for the copies.
 
Last edited:
I might ran into another problem. Spell__CastPoint seems to be not working.

I made a 2-phase spell: phase 1, create dummy units in a 72 degree area in front of the caster with Carrion Swarm, phase 2, when these dummy units cast Carrion Swarm move them forward to resemble a sweeping effect. When testing in the game, the dummy units seem to be moved to the center of the map, and the angles are very wonky.

Phase 1:
  • Lance config
    • Events
      • Map initialization
    • Conditions
    • Actions
      • Set Spell__Ability = Sweeping Fangs
      • Set Spell__Trigger_OnEffect = Lance effect <gen>
      • Trigger - Run Spell System <gen> (checking conditions)
  • Lance effect
    • Events
    • Conditions
    • Actions
      • Set LanceStartingAngle[Spell__Index] = ((Facing of Spell__Caster) + 36.00)
      • For each (Integer A) from 1 to (2 + (Spell__Level x 2)), do (Actions)
        • Loop - Actions
          • Set LanceStartingAngle[Spell__Index] = (LanceStartingAngle[Spell__Index] - (72.00 / (2.00 + (2.00 x Spell__LevelMultiplier))))
          • Unit - Create 1 Spell__DummyType for Spell__CasterOwner at Spell__CastPoint facing LanceStartingAngle[Spell__Index] degrees
          • Unit - Add a 2.00 second Generic expiration timer to (Last created unit)
          • Unit - Add Carrion Swarm (Neutral Hostile) to (Last created unit)
          • Unit - Order (Last created unit) to Undead Dreadlord - Carrion Swarm Spell__CastPoint
Phase 2:
  • Dummy lance config
    • Events
      • Map initialization
    • Conditions
    • Actions
      • Set Spell__Ability = Carrion Swarm (Neutral Hostile)
      • Set Spell__Trigger_OnEffect = Dummy lance effect <gen>
      • Set Spell__Trigger_OnLoop = Dummy lance loop <gen>
      • Trigger - Run Spell System <gen> (checking conditions)
  • Dummy lance effect
    • Events
    • Conditions
    • Actions
      • -------- --------
      • Set Spell__Time = Spell__Interval
      • Set Spell__Duration = 1.00
      • -------- --------
      • Set LanceDest[Spell__Index] = Spell__CastPoint
      • Unit - Pause the expiration timer for Spell__Caster
      • Unit - Add Minus Armor Aura (Neutral Hostile) to Spell__Caster
      • Animation - Change Spell__Caster's size to (200.00%, 200.00%, 200.00%) of its original size
      • Animation - Change Spell__Caster flying height to 60.00 at 0.00
      • Special Effect - Create a special effect attached to the origin of Spell__Caster using Abilities\Spells\Undead\DevourMagic\DevourMagicBirthMissile.mdl
      • Set LanceFX[Spell__Index] = (Last created special effect)
      • Special Effect - Create a special effect attached to the origin of Spell__Caster using Abilities\Weapons\WardenMissile\WardenMissile.mdl
      • Set LanceFX2[Spell__Index] = (Last created special effect)
  • Dummy lance loop
    • Events
    • Conditions
    • Actions
      • Set LanceDest[Spell__Index] = (LanceDest[Spell__Index] offset by 27.00 towards (Facing of Spell__Caster) degrees)
      • Unit - Move Spell__Caster instantly to LanceDest[Spell__Index], facing (Facing of Spell__Caster) degrees
      • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
        • If - Conditions
          • Spell__Expired Equal to True
        • Then - Actions
          • Unit - Unpause the expiration timer for Spell__Caster
          • Unit - Remove Minus Armor Aura (Neutral Hostile) from Spell__Caster
          • Special Effect - Destroy LanceFX[Spell__Index]
          • Special Effect - Destroy LanceFX2[Spell__Index]
        • Else - Actions
      • Custom script: call RemoveLocation(udg_LanceDest[udg_Spell__Index])
 
Level 37
Joined
Jul 22, 2015
Messages
3,485
@dtnmang the issue is here:
  • Set LanceDest[Spell__Index] = Spell__CastPoint
Removing either of these points will remove both of them. You need to make sure they both have different reference ids, so something like this will work:
  • Set LanceDest[Spell__Index] = (Target point of ability being cast)
  • Set Spell__CastPoint = (Target point of ability being cast)
I know Spell__CastPoint is being handled in the Spell System, so you wouldn't actually do that, but I just put it down so you understand what i mean by making sure they have difference reference ids.
 
Thanks for the input, @KILLCIDE.
I've tried to debug the spell a bit, here's the new result (I'll just repost them without the "config" triggers)

  • Lance effect
    • Events
    • Conditions
    • Actions
      • Set LanceStartingAngle[Spell__Index] = ((Facing of Spell__Caster) + 36.00)
      • For each (Integer A) from 1 to (2 + (Spell__Level x 2)), do (Actions)
        • Loop - Actions
          • Set LanceStartingAngle[Spell__Index] = (LanceStartingAngle[Spell__Index] - (72.00 / (2.00 + (2.00 x Spell__LevelMultiplier))))
          • Unit - Create 1 Spell__DummyType for Spell__CasterOwner at Spell__CastPoint facing LanceStartingAngle[Spell__Index] degrees
          • Unit - Add a 1.00 second Generic expiration timer to (Last created unit)
          • Unit - Add Carrion Swarm (Neutral Hostile) to (Last created unit)
          • Set LanceTarget[Spell__Index] = (Spell__CastPoint offset by 800.00 towards LanceStartingAngle[Spell__Index] degrees)
          • Unit - Order (Last created unit) to Undead Dreadlord - Carrion Swarm LanceTarget[Spell__Index]
      • Custom script: call RemoveLocation(udg_LanceTarget[udg_Spell__Index])
  • Dummy lance effect
    • Events
    • Conditions
    • Actions
      • -------- --------
      • Set Spell__Time = Spell__Interval
      • Set Spell__Duration = 1.00
      • -------- --------
      • Unit - Create 1 Spell__DummyType for Spell__CasterOwner at Spell__CastPoint facing (Facing of Spell__Caster) degrees
      • Unit - Add a (2.00 + Spell__Duration) second Generic expiration timer to (Last created unit)
      • Animation - Change (Last created unit)'s size to (200.00%, 200.00%, 200.00%) of its original size
      • Animation - Change (Last created unit) flying height to 60.00 at 0.00
      • Special Effect - Create a special effect attached to the origin of (Last created unit) using Abilities\Spells\Undead\DevourMagic\DevourMagicBirthMissile.mdl
      • Set LanceFX[Spell__Index] = (Last created special effect)
      • Special Effect - Create a special effect attached to the origin of (Last created unit) using Abilities\Weapons\WardenMissile\WardenMissile.mdl
      • Set LanceFX2[Spell__Index] = (Last created special effect)
      • Unit - Add Minus Armor Aura (Neutral Hostile) to (Last created unit)
      • Set LanceDest[Spell__Index] = (Position of (Last created unit))
      • Set LanceDummy[Spell__Index] = (Last created unit)
  • Dummy lance loop
    • Events
    • Conditions
    • Actions
      • Set LanceDest[Spell__Index] = (LanceDest[Spell__Index] offset by 25.00 towards (Facing of LanceDummy[Spell__Index]) degrees)
      • Unit - Move LanceDummy[Spell__Index] instantly to LanceDest[Spell__Index], facing (Facing of LanceDummy[Spell__Index]) degrees
      • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
        • If - Conditions
          • Spell__Expired Equal to True
        • Then - Actions
          • Unit - Remove Minus Armor Aura (Neutral Hostile) from LanceDummy[Spell__Index]
          • Special Effect - Destroy LanceFX[Spell__Index]
          • Special Effect - Destroy LanceFX2[Spell__Index]
          • Custom script: call RemoveLocation(udg_LanceDest[udg_Spell__Index])
        • Else - Actions
Do you think there's still any more issues?
 
edit: Which "event trigger" variable (__OnLoop, __OnEffect, __OnFinish etc.) does the Spell__Completed boolean go with?
Also, it's pretty hard to work around the Banshee Possession (Aps2) spell with the event trigger variables. __OnEffect will trigger when the targeted unit becomes invulnerable and stunned; __OnFinish will trigger when the caster completes or cancels channeling. There's no variable to trigger when the targeted unit actually gets Possessed. Have to use the condition "Owner of Spell__Target equals to Spell__CasterOwner"
 
Last edited:
I assume since LanceDest[] is set to a new offset location every periodical loop, it only needs to be cleaned up at the final loop. If I clean up LanceDest[] every loop then it will be reset to the center point of the map. But not removing it in every loop leaks too? Wow. Do I have to clean up LanceTarget[] in every For... Integer A loop as well?
 
Last edited:
Revised the trigger, it looks like this now. Did I do it correctly?
  • Dummy lance loop
    • Events
    • Conditions
    • Actions
      • Set LanceDest[Spell__Index] = (LanceDest[Spell__Index] offset by 25.00 towards (Facing of LanceDummy[Spell__Index]) degrees)
      • Unit - Move LanceDummy[Spell__Index] instantly to LanceDest[Spell__Index], facing (Facing of LanceDummy[Spell__Index]) degrees
      • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
        • If - Conditions
          • Spell__Expired Equal to True
        • Then - Actions
          • Special Effect - Destroy LanceFX[Spell__Index]
          • Special Effect - Destroy LanceFX2[Spell__Index]
          • Custom script: call RemoveLocation(udg_LanceDest[udg_Spell__Index])
        • Else - Actions
      • Custom script: call RemoveLocation(PolarProjectionBJ(udg_LanceDest[udg_Spell__Index], 25.00, GetUnitFacing(udg_LanceDummy[udg_Spell__Index])))
 
Like this?
  • Dummy lance loop
    • Events
    • Conditions
    • Actions
      • Set LanceDest_Next[Spell__Index] = (LanceDest[Spell__Index] offset by 25.00 towards (Facing of LanceDummy[Spell__Index]) degrees)
      • Unit - Move LanceDummy[Spell__Index] instantly to LanceDest_Next[Spell__Index], facing (Facing of LanceDummy[Spell__Index]) degrees
      • Set LanceDest[Spell__Index] = (Position of LanceDummy[Spell__Index])
      • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
        • If - Conditions
          • Spell__Expired Equal to True
        • Then - Actions
          • Special Effect - Destroy LanceFX[Spell__Index]
          • Special Effect - Destroy LanceFX2[Spell__Index]
          • Custom script: call RemoveLocation(udg_LanceDest[udg_Spell__Index])
          • Custom script: call RemoveLocation(udg_LanceDest_Next[udg_Spell__Index])
        • Else - Actions
      • Custom script: call RemoveLocation(PolarProjectionBJ(udg_LanceDest[udg_Spell__Index], 25.00, GetUnitFacing(udg_LanceDummy[udg_Spell__Index])))
      • Custom script: call RemoveLocation(GetUnitLoc(udg_LanceDummy[udg_Spell__Index]))
 
Level 37
Joined
Jul 22, 2015
Messages
3,485
As I mentioned, you only have to use two locations. You're still using three or four for whatever reason. This is all you have to do:

  • Dummy lance loop
    • Events
    • Conditions
    • Actions
      • Set TempLoc = (LanceDest[Spell__Index] offset by 25.00 towards (Facing of LanceDummy[Spell__Index]) degrees)
      • Unit - Move LanceDummy[Spell__Index] instantly to TempLoc, facing (Facing of LanceDummy[Spell__Index]) degrees
      • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
        • If - Conditions
          • Spell__Expired Equal to True
        • Then - Actions
          • Special Effect - Destroy LanceFX[Spell__Index]
          • Special Effect - Destroy LanceFX2[Spell__Index]
          • Custom script: call RemoveLocation(udg_LanceDest[udg_Spell__Index])
        • Else - Actions
      • Custom script: call RemoveLocation(udg_TempLoc)
 
My LanceDest_Next[] variable is the TempLoc that you've said, but MUI. Also I need to set the LanceDest[] variable to the dummy's position so the dummy can have the effect of moving forward. If I make the loop trigger the way you wrote down (which I did test), the dummies only move once because LanceDest[] has only been declared once in the 'Spell Effect' trigger. Those dummies have effects on them so they need to have triggered movement
 
Level 37
Joined
Jul 22, 2015
Messages
3,485
Sorry I misunderstood your trigger. You don't need the location variables to be arrays then. You just need two TempLoc variables.

  • Dummy lance loop
    • Events
    • Conditions
    • Actions
      • Set TempLoc1 = (Position of LanceDummy[Spell__Index])
      • Set TempLoc2 = (LanceDest[Spell__Index] offset by 25.00 towards (Facing of LanceDummy[Spell__Index]) degrees)
      • Unit - Move LanceDummy[Spell__Index] instantly to TempLoc2, facing (Facing of LanceDummy[Spell__Index]) degrees
      • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
        • If - Conditions
          • Spell__Expired Equal to True
        • Then - Actions
          • Special Effect - Destroy LanceFX[Spell__Index]
          • Special Effect - Destroy LanceFX2[Spell__Index]
        • Else - Actions
      • Custom script: call RemoveLocation(udg_TempLoc1)
      • Custom script: call RemoveLocation(udg_TempLoc2)
 
"toggles" as in activating. For example my Demon Hunter is stunned and while he's stunned I order him to activate Immolation. It doesn't activate immediately because he's stunned (but will activate once he is no longer stunned). So, does the trigger fire at the moment I order him or the moment Immolation actually gets activated?
 
Which Spell OnEvent trigger do I use to detect the moment the spell projectile hits its target? Because I want to make a spell that deals bonus damage (and this damage is based on the caster's attribute) once the target is hit by the projectile, however both OnEffect and OnFinish triggers cause the bonus damage to hit as soon as the projectile leaves the caster.
 
Which Spell OnEvent trigger do I use to detect the moment the spell projectile hits its target? Because I want to make a spell that deals bonus damage (and this damage is based on the caster's attribute) once the target is hit by the projectile, however both OnEffect and OnFinish triggers cause the bonus damage to hit as soon as the projectile leaves the caster.
You'd use Damage Engine to catch the moment the spell deals damage, provided it's the unit's only damaging spell.

Otherwise, you have to trigger the projectile of the spell so you know yourself when it hits.
 
Update!

1.6.1.0 fixes an issue where the duration was decreasing on the first assignment instead of only on ticks. This now works correctly for both pre-assigned durations and newly-assigned ones.

In the test map, I've added a Devour ability to the Knight which tests the same values identified by dtnmang below:

Interesting tidbit:
Setting Spell__Duration == 1.00 and Spell__Time == 0.2 won't give 5 loop instances, but only 4. Setting Spell__Duration to something slightly higher ( == 1.00 + Spell__Interval for example) will give 5 loop instances properly.

To apply the update, only the JASS code needs to be updated.

Thanks dtnmang!
 
Last edited:
Top