GUI Spell System v1.8.0.0

This bundle is marked as director's cut. It meets 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...

Moderator

M

Moderator

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:
Very impressive coding. This is highly recommendable to any GUI coder out there, who wants to optimize and structure his/her code.
I think it takes some time to get the hang of the system, but then every goes very fast.

Overall very read-able and good documentated. I like that you even thought about small details like considering sleeping creeps.
I also learned something, while reading your code. Hopefully you get some user feedback.

From moderator to user:
Learning to get along with this system could turn out to be a win sitation for you.
  • Fast spell creating.
  • Less global variables required ( better overview ).
  • Performance gain ( I would guess that at 5+ custom spells the performance sky rockets due to structured event execution and better filtering mechanics )
  • .... Read the description youself.

I looking foreward to review a few spell submissions created with GUI Spell System.
 
Level 32
Joined
Apr 24, 2012
Messages
5,112
this is much better than mine(5 events vs 1 event? LoL come at me bruh). Supports 5 events rather than 1 which SpellEffectEvent does and also does not use Hashtables(gotta go faster)

Afaik as I know, GetSpellTargetX() and GetSpellTargetY() also works for events that support on-target units. I mean instead of getting the target unit and get it's location, you can use the two functions instead.


And also, based on what I have understood, this only supports up to 8192 indeces and multiple spells at a time...which I have done 3 years ago before Mag slapped me that it is inefficient(CURSE YOU MAG), that's why I created a Linked List Table.


Mag has so many flaws years ago. omg I just realized this.
 
Level 35
Joined
Sep 26, 2009
Messages
8,441
8192 spells at the same time! Planning for those "circumstances" is what makes Nestharus' code so unbearable. He uses 10 arrays and 50 lines of code to save 0.0001% of performance. The only "realistic" way this system would go over even 100-200 instances (not counting the individual registered spells) is if the spells aren't getting destroyed when finished.
 
Level 32
Joined
Apr 24, 2012
Messages
5,112
8192 spells at the same time! Planning for those "circumstances" is what makes Nestharus' code so unbearable. He uses 10 arrays and 50 lines of code to save 0.0001% of performance. The only "realistic" way this system would go over even 100-200 instances (not counting the individual registered spells) is if the spells aren't getting destroyed when finished.

I like your perspective. That fact that spell instances will approach 8190(or even half of it) is pretty unrealistic.

I hate Mag.
 
Level 17
Joined
Mar 18, 2012
Messages
1,717
8192 spells at the same time!

I guess most "12 player maps" don't run more than 50 spells during the same time.

With less players we might have more custom mob spells. So we again end up between
50 - 100 instances.

In rare cases we might have 100 - 1000 instance if we trigger spells of a caliber like a priest heal.

Supporting +8910 instances is not a valid cenario, not only because the map will then start lagg,
which no map maker ever wanted to achieve.
 
Level 35
Joined
Sep 26, 2009
Messages
8,441
I've made some big improvements to this system since I released it 9 days ago. I quietly released version 1.1, but then realized that there was still a missed opportunity to make it much better, so I released version 1.2.

Here is the list of key differences from version 1.0:
  • Set the OnLoop trigger from your event trigger instead of the configuration trigger. The reason I changed that is so that you can change the OnLoop trigger dynamically per-spell at any phase of the spell. Just set Spell__Trigger_OnLoop to the trigger you want (from an event, from the OnLoop trigger itself, it all works).
  • Make sure you specify a Spell__Time if you want to use the OnLoop feature. Even if you want to just have the minimum timeout, you still need to specify it by setting the variable Spell__Time. You also must set the Spell__Time from the OnLoop trigger, unless you're done with the spell.
  • You no longer destroy spell indices. They are created and destroyed as-needed, and destroyed automatically when either:
    • A spell finishes and the user didn't set a Spell__Time, the spell expires
    • There is a Spell__Time, but it expired and a new Spell__Time wasn't set by the OnLoop trigger.
    • Special note: If you need the index for reasons beyond the OnLoop trigger or the other event triggers and don't want it to be destroyed, set the Spell__Time to as long as you think you'll need it. You do not need to specify an OnLoop trigger when you do this. Think of it the same way as UnitApplyTimedLife but with the spell index instead of a unit.
  • Changed purpose of variable "Spell__Trigger_OnCast". You must now use Spell__Trigger_OnEffect if you want it to run for an effect event.
  • Instead of using Event_e_ variables, I just made various forms of Spell__Trigger_OnChannel/Cast/Effect/Finish.
  • Merged the events end-cast and spell-finish. If you want to differentiate between the two, use the boolean "Spell__Completed" which lets you know if the channeling went through to its natural stop point or if it was interrupted.
  • Removed excess GUI triggers and variables.
  • Registering a spell involves running "Spell System <gen>" as the GUI trigger "Spell System Register <gen>" no longer exists.

New feature as recommended by Wietlol and made easier to understand by Anitarf in his SpellEvent library:

The OnFinish trigger has access to the spell target and its location. This information is otherwise impossible to get by the game engine because it mysteriously forgets that.

I hope you guys enjoy the resource, and please let me know if you have any additional questions! No new features will be considered unless requested by a user, so this is ready for evaluation once a moderator has time for it.
 
Mod_IcemanBo:

I saw several attempts for this concept, but I
seem to like this one the most. It is pretty powerful.

GUI Spell System can make it cool for the user to create spells,
without caring too much about every code implementation himself.

What is a bit annoying for such system,
is that there are always statements included that
are even checked when it would be not needed for a specific spell.
But it's impossible to go without an overhead.. unfortunately.

The demo spell is done properly and looks very nice.
I could also not spot any flaws within this example. :)

I endup with rating 4/5.
__________________________________

User_IcemanBo:

The Spell Completed boolean is really nice.
Are you planing to add a new example? -
because there is this Whiplash ability which does nothing atm.
And btw - I agree with most things mentioned by Mod_IcemanBo. :d
 
Level 35
Joined
Sep 26, 2009
Messages
8,441
It's interesting that you mention overhead as a caveat of this spell. I disagree with any claim of efficiency loss due to cutting down a trigger evaluation per registered spell. After 5 spells are in the map, the negligible performance disadvantage would start to turn into gain. It gets exponentially better from there. Keep in mind that a single trigger evaluation is more expensive than 30 function calls according to Anitarf in his benchmarking thread here: http://www.wc3c.net/showthread.php?p=1132305. Therefore, it may even be less than 5 spells before you get performance gains.

Without this system or Anitarf's vJass SpellEvent, you wouldn't be able to track the target information from an end-cast event. This system compensating for it means that you can successfully use that event to end a channeling spell instead of relying on a timer or having an iffy end cast event. In fact, since 95% of the extra code won't run for non-registered spells, you'll see performance gains at 2 spells for that kind.

Not to mention the minor detail that memory usage is exponentially better with each new spell registered as the events are stored on one main trigger instead of on each spell's trigger.

My goal for this resource is for it to replace Hanky's Dynamic Indexing (which no one is using the proper version of as they tend to use the old school Paladon version). This will never be a requirement for any spell, but it is a huge benefit for those who can make use of its features. For each spell that uses indexing, you'll save between 4-8 arrays per spell, which means fewer variables clogging up the editor so it's easier to hunt the ones you need.

The best way to describe this system is that it's a combination of Anitarf's SpellEvent and Nestharus' CTL32, with the benefit of being GUI and the indexes being allocated/deallocates invisibly to the user so they can keep their code clean and focus on creativity.

Edit: Whiplash is only conceptually finished. I intend to add up to a dozen spells to the test map so this is a combination of system/spellpack.
 
Level 35
Joined
Sep 26, 2009
Messages
8,441
I took that suggestion to make some simple behavior happen when the Paladin casts Resurrect or Divine Shield. I also took the opportunity to improve the logic in a few places in the JASS script. The code is more structured, a little tighter, and instead of storing the original owning player the system will retrieve the caster's current owner.

Edit: update to 1.2.3 - made additional improvements to the code and now, once again, the user can set an OnLoop trigger from their config trigger if they wish to do so. This change made it possible for me to avoid needing an extra variable when re-coding a demo spell in JASS so it may be beneficial for other users, as well.

BPower, I intend to make a video tutorial on how this works as it is extremely simple to install and use.
 
Last edited:
Level 35
Joined
Sep 26, 2009
Messages
8,441
I am attaching a revised version of my old map, Unit Indexer Spell Template, which now uses Spell System just to show how much more simplified everything becomes.

Here are the triggers:


  • Spell Config
    • Events
      • Map initialization
    • Conditions
    • Actions
      • Set Spell__Ability = TesterSpell
      • Set Spell__Trigger_OnEffect = Spell Effect <gen>
      • Set Spell__Trigger_OnLoop = Spell Loop <gen>
      • Trigger - Run Spell System <gen> (ignoring conditions)
  • Spell Effect
    • Events
    • Conditions
    • Actions
      • -------- --------
      • -------- The variable Spell__Index represents this individual cast of the spell --------
      • -------- --------
      • -------- Use the Spell__Index variable with arrays to store exclusive data --------
      • -------- --------
      • -------- These next variables are just to show how to use the spell for this demo --------
      • -------- --------
      • Special Effect - Create a special effect at Spell__TargetPoint using Abilities\Spells\Other\Tornado\TornadoElemental.mdl
      • Set TesterSpell_Effect[Spell__Index] = (Last created special effect)
      • Set TempReal = (Real(Spell__Level))
      • Set TesterSpell_Damage[Spell__Index] = TempReal
      • Set TesterSpell_Radius[Spell__Index] = (200.00 + (50.00 x TempReal))
      • -------- --------
      • -------- You can change the value of 'duration' to whatever you want --------
      • -------- --------
      • Set TesterSpell_Duration[Spell__Index] = (5.00 x TempReal)
      • -------- --------
      • -------- Set how frequently the loop trigger should run --------
      • -------- --------
      • Set Spell__Time = Spell__Interval
  • Spell Loop
    • Events
    • Conditions
    • Actions
      • Custom script: set bj_wantDestroyGroup = true
      • Unit Group - Pick every unit in (Units within TesterSpell_Radius[Spell__Index] of Spell__TargetPoint) and do (Actions)
        • Loop - Actions
          • Set TargetUnit = (Picked unit)
          • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
            • If - Conditions
              • (TargetUnit belongs to an enemy of Spell__CasterOwner) Equal to True
            • Then - Actions
              • Unit - Cause Spell__Caster to damage TargetUnit, dealing TesterSpell_Damage[Spell__Index] damage of attack type Spells and damage type Normal
            • Else - Actions
      • -------- --------
      • -------- When the duration reaches 0 it means the spell is done --------
      • -------- --------
      • Set TesterSpell_Duration[Spell__Index] = (TesterSpell_Duration[Spell__Index] - Spell__Interval)
      • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
        • If - Conditions
          • TesterSpell_Duration[Spell__Index] Less than or equal to 0.00
        • Then - Actions
          • -------- --------
          • -------- Clean up any data attached to this spell --------
          • -------- --------
          • Special Effect - Destroy TesterSpell_Effect[Spell__Index]
        • Else - Actions
          • -------- --------
          • -------- Set Spell__Time again to keep the loop going --------
          • -------- --------
          • Set Spell__Time = Spell__Interval
 

Attachments

  • MUI Spells with Unit Indexer.w3x
    47 KB · Views: 258
Level 35
Joined
Sep 26, 2009
Messages
8,441
Added a new option for speed freaks: Spell__RestrictVars. Set this to True during the config trigger for the spell if you don't want the system to automatically assign the scalar variables like Spell__Caster/Spell__Target/etc.

You can change it dynamically per-spell by manipulating the boolean array Spell_i_AutoVars, which is True by default and you can set it to False to disable the automatic variable assignment.

With this addition, this is now even more of a one-size-fits-all resource. Hope you guys enjoy!
 
Level 35
Joined
Sep 26, 2009
Messages
8,441
Added a new option for GUI users who, for whatever reason, want to use TriggerSleepAction.

JASS:
call SpellWait()

If that function is called, the calling trigger will wait for Spell__Time amount of time. You can keep calling waits as many times as you need.

The behavior is comparable PolledWait, but automatically sets the Spell__ variables for you after the wait has expired and doesn't leak handle indices.

No new variables were needed to make this functionality happen, so the only thing that would need to be updated is the JASS code.

Note that this is to help people with simple triggering and/or they want the bulk of their code in one trigger. This function can be invoked from a spell event trigger of any kind, but the OnLoop trigger will not work during or after a wait.
 
Last edited:
Level 35
Joined
Sep 26, 2009
Messages
8,441
Version 1.3.0.0:

I have added an advanced feature to Spell System which transforms this:

  • Custom script: set bj_wantDestroyGroup = true
  • Unit Group - Pick every unit in (Units within 300.00 of SomePoint) and do (Actions)
    • Loop - Actions
      • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
        • If - Conditions
          • ((Picked unit) belongs to an enemy of Spell__CasterOwner) Equal to True
          • ((Picked unit) is A ground unit) Equal to True
          • ((Picked unit) is Mechanical) Equal to False
          • ((Picked unit) is Magic Immune) Equal to False
          • ((Picked unit) is A structure) Equal to False
        • Then - Actions
          • -------- Do things --------
        • Else - Actions
Into this:

  • Set Spell__InRangePoint = SomePoint
  • Set Spell__InRange = 300.00
  • Unit Group - Pick every unit in Spell__InRangeGroup and do (Actions)
    • Loop - Actions
      • -------- Do things --------
More details are featured on the top post.

Version 1.2.7.0:

After some testing, I found out that The Big Dipper was actually still running even after the unit stopped channeling. Subsequently, I've made it so that if the user has assigned Spell__Trigger_OnFinish, then the system assumes that you want your OnLoop trigger to stop running at that time. The exception to e rule is if you set Spell__Time from the OnFinish trigger, in which case the system knows you still want things running.
 
Level 35
Joined
Sep 26, 2009
Messages
8,441
Here's a new update I've been working on for some time:

1.4

Added a Spell__Channeling to let the user know if the unit is still casting the spell or has finished. This is useful for an OnLoop trigger which can check if the unit is still casting without needing to have registered an OnFinish trigger.

Added a feature which wakes sleeping creeps if they are enumerated by an InRange trigger. This is to mirror the behavior of Object Editor spells which also wake creeps when they affect them in some way.

Added a feature to the filter to disallow living units, if needed. Previously, the system would add both living and dead if a dead tolerance was added.

Fixed a lot of the logic in the code. Now only uses a stack instead of being augmented by a linked list. Optimized the spell filter to use "exitwhen not boolean" instead of "set result = result and boolean".


I've also removed some extra stuff:

Removed the "wait" stuff as it is really unnecessary when you can do the same thing with an OnLoop trigger.

Removed the "AutoVars/RestrictVars stuff as it's just an unnecessary thing to check for. I will soon make a vJass version of this resource to satisfy speed freaks anyway.

Removed the "Spell__Filter_AllowSelf check. I can add it back if anyone wants, but it's something I've never seen a resource concern itself with before.
 
Level 22
Joined
Feb 6, 2014
Messages
2,468
JASS:
call TriggerRegisterAnyUnitEventBJ(t, EVENT_PLAYER_UNIT_SPELL_CHANNEL)
call TriggerRegisterAnyUnitEventBJ(t, EVENT_PLAYER_UNIT_SPELL_CAST)
call TriggerRegisterAnyUnitEventBJ(t, EVENT_PLAYER_UNIT_SPELL_EFFECT)
call TriggerRegisterAnyUnitEventBJ(t, EVENT_PLAYER_UNIT_SPELL_FINISH)
call TriggerRegisterAnyUnitEventBJ(t, EVENT_PLAYER_UNIT_SPELL_ENDCAST)
Why not inline this so You Only Loop Once (YOLO)?
 
Level 35
Joined
Sep 26, 2009
Messages
8,441
Update 1.4.2.0 -

Renamed Spell_i_Hash to Spell__Hash as users now have access to the parent key of "Spell__Index" inside that hashtable! Yep, now everybody gets their own "Table" (without the vJass syntax) while using this system. This will help to bring down any lingering GUI hashtable usage to a bare minimum. Consequently, I've added Table by Vexorian to the list of inspiration for Spell System.

Changed the way the group filter works for the better. I switched it into a FirstOfGroup loop which adds units to an array called Spell__InRangeUnits and track the number of units in Spell__InRangeCount. Then, I loop through the array to add those units back into Spell__InRangeGroup so that both the array and the group are accessible to users. I like the idea of using an array to avoid needing those costly "(number of units)" and "(random unit from)" operations.

Added GroupUtils by Rising_Dusk to the list of inspiration due to the usage of IsUnitInRange and recycling groups - those were all his ideas and I incorporated it them the resource.

Sidenote: I have been burning through these updates really quickly, I know. I promise to make no more updates unless anyone finds bugs, for at least 1 week! I really wanted to get these updates out as I have a huge passion to make this resource great.

In the meanwhile, I will be releasing a video demonstrating how to convert a spell to use Spell System. I feel that this resource is useful enough now to become the new recommended standard for spell making, by far, but if people don't know how it works it can't get there.

Update 1.4.1.0 -

> Fixed a bug where a spell index would get destroyed early if the spell loop trigger finished before the end-cast event ran.

> Now recycles groups instead of creating/destroying them.


Also, I forgot to mention in the last update that there is no longer a "dummy group". A dummy group is too arbitrary to be part of this system - I might as well have a dummy unit variable, too, but that's not the effect I was looking for.
 
In the meanwhile, I will be releasing a video demonstrating how to convert a spell to use Spell System. I feel that this resource is useful enough now to become the new recommended standard for spell making, by far, but if people don't know how it works it can't get there.

Yes! I can't figure out how to use this, so a video would likely be very helpful. Maybe you could create a bunch of example spells besides the one you have already, like a stackable DoT spell.
 

Chaosy

Tutorial Reviewer
Level 38
Joined
Jun 9, 2011
Messages
13,061
Will watch later and see if you can convince me to give it a try though I have a tons o' doubts.

edit:
Okay watched it. Still somewhat confusing to me.
"
Set Spell__Trigger_OnChannel = (This trigger)
Set Spell__Trigger_OnCast = (This trigger)
Set Spell__Trigger_OnEffect = (This trigger)
Set Spell__Trigger_OnFinish = (This trigger)
"
You do not explain what those do well enough I think.
If I were to judge merely by your words in the video I would be clueless.
However when I read the variable names a few times I think they simply replace the default events that you use.
Starts channeling, casts an ability, starts the effect of an ability and finished casting an ability. If that's correct, then that's an easier way to explain it I think.

Secondly how the loop trigger works is a tad unclear to me.
I got absolutely no idea how to end the spell.

Now, there were good things of course, clear and calm voice which was easy to understand.
If you made video tutorials that would be a great hit I think (at least if you recorded with proper software :D)
 
Last edited:
Level 35
Joined
Sep 26, 2009
Messages
8,441
Update to 1.5!

This is a very nice icing-on-the-cake update. I've made it so you can specify a Spell__Duration in addition to (or instead of) the Spell__Time variable. Say you want to have a trigger run every second for 5 seconds. You specify Spell__Time as 1.00 and Spell__Duration as 5.00. NOTE: If you don't set Spell__Time, Spell__Time will default to Spell__Interval which is 0.03125, making it ideal for animating over time.

Here's a JASS script showcasing a new function to make JASS coding with Spell System easier:

JASS:
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



@Chaosy, thanks for the review. Yes, you're right about them replacing the events. The OnLoop trigger works in tandem with Spell__Time (and now Spell__Interval, as well).

I will make a new video soon showing how to create a spell from scratch. Do you know of a lightweight, free screen capture software for Windows? FRAPS causes too much lag for me.
 
Level 35
Joined
Sep 26, 2009
Messages
8,441
@SpellBound, that's limited to GeForce. I have an Intel GMA 3150 in my netbook, and it is bad. The only other machine I have access to is a Mac, which id using a newer OS that can't install War3. I could use WINE, but World Editor doesn't work with WINE.

OK, I tried using FRAPS and it actually doesn't record the Trigger Editor, just the map in the background. And it's limited to 30 seconds.

I thought I had recorded the process of me making the following spell, but it didn't. It took about 2 minutes to copy the Spell Trigger category, create a new map, paste the category and add a new effect to Storm Bolt. The spell modifies Storm Bolt to deal a little damage over time with an effect on the target:

  • New Spell Config
    • Events
      • Map initialization
    • Conditions
    • Actions
      • Set Spell__Ability = Storm Bolt
      • Set Spell__Trigger_OnEffect = New Spell Effect <gen>
      • Set Spell__Trigger_OnLoop = New Spell DoT <gen>
      • Trigger - Run Spell System <gen> (ignoring conditions)
      • Custom script: call CreateUnit(Player(0), 'Hmkg', 0, 0, 0)
      • Custom script: call CreateUnit(Player(15), 'hkni', 150, 0, 0)
  • New Spell Effect
    • Events
    • Conditions
    • Actions
      • Set Spell__Time = 0.50
      • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
        • If - Conditions
          • (Spell__Target is A Hero) Equal to True
        • Then - Actions
          • Set Spell__Duration = 3.00
        • Else - Actions
          • Set Spell__Duration = 5.00
  • New Spell DoT
    • Events
    • Conditions
    • Actions
      • Unit - Cause Spell__Caster to damage Spell__Target, dealing (5.00 x Spell__LevelMultiplier) damage of attack type Spells and damage type Normal
      • Special Effect - Create a special effect attached to the origin of Spell__Target using Abilities\Weapons\FarseerMissile\FarseerMissile.mdl
      • Special Effect - Destroy (Last created special effect)
It took a bit longer to tune which effect to use, how long the duration should be and how much the damage should be, but that was simply polish. I finished that after the video.
 
Level 35
Joined
Sep 26, 2009
Messages
8,441
Minor Update to 1.5.0.1:

- Changed the default configuration of Spell__DummyOwner to Neutral Extra instead of Neutral Passive. This allows both negative and positive spells to be cast from dummies owned by that player.

- Added the Spell__LevelMultiplier variable to the Config trigger so it gets copied when you bring the system over to a new map without the template triggers.
 
Level 35
Joined
Sep 26, 2009
Messages
8,441
Update thanks to KILLCIDE!

You can set the Spell__Time, Spell__Duration and Spell__DurationPerLevel from the Config trigger. To tell the system when to begin the duration, set the boolean Spell__StartDuration to True.

The purpose of this change is to make the duration more streamlined to configure.
 
Level 7
Joined
Jun 14, 2009
Messages
235
I have a question about Spell Group. If no Loop or Duration value is set, will the spell group be destroyed?

In other words. If I set duration to 3 seconds, will Spell Group be usable for 3 seconds or longer?


What I basically want to do is, create a spell that summons 5 projectiles in a cone. The projectiles will fly in a straight line (thus forming the cone) and add a bleed to any unit they hit. Would spell group be usable so that I can add any unit hit by one of the 5 projectiles to the group, and run a check so a different projectiles doesn't trigger twice?
 
Level 35
Joined
Sep 26, 2009
Messages
8,441
Ok, thank you for waiting. I have found out why your debug messages are so confusing in-game, but it's normal and harmless from what I can deduce.

1) The first "deindex" happens on your second timer expiration. It does not occur after the "OnFinish" event as you had thought, because I ran a debug message from that trigger and found it was running after the "deindex".

2) The first "deindex" is just the system removing the trigger from the timer queue because you didn't set a new spell time from the first OnLoop trigger.

3) If you put a second debug message in the block which actually recycles the index, you'll see it only happens once: at the very end.

All this is to say is that the system tried to destroy the index when your second timer expired, but detected that the OnFinish event had not yet expired so kept things alive until the caster finished its job. It is complicated, but it's the only way to allow automatic indexing.

Also, kudos for maling a GUI/JASS version of RetroFade. It looks very nice with the GUI configurables, and would be cool if you submitted it as a standalone resource.
 
Top