[vJASS] Chain

Level 33
Joined
Apr 24, 2012
Messages
5,115
I was creating an Arena and thought of Slashes and Bouncing abilities

PLEASE GO TO THE FILTERING SYSTEM FIRST BEFORE USING!

http://www.hiveworkshop.com/forums/jass-resources-412/filtering-system-240887/
JASS:
library Chain/* 2.0
************************************************************************************
*
*   Used for building chain/bouncing spells like Omnislash 
*   and Custom Chain Lightning
*
*   Also allows you to do target priorities.
*
*   The system's target prioty requires you to use FilteringSystem
*
************************************************************************************
*
*   */ requires /*
*       
*       */Table /*
*       hiveworkshop.com/forums/jass-resources-412/snippet-new-table-188084/
*       */GetClosestWidget /*
*       hiveworkshop.com/forums/jass-resources-412/filtering-system-240887/
*       */FilteringSystem /*
*       hiveworkshop.com/forums/jass-resources-412/snippet-getclosestwidget-204217/
*
************************************************************************************
*
*   API
*
*   struct ChainType
*       [See the comments in the struct
*
*   struct ChainPriority
*       readonly integer priorities
*           - the current priority size
*       static method create takes nothing returns thistype
*       
*       method operator next= takes integer filter returns nothing
*           - enqueues the priority filter
*           - pushed values must follow the FilteringSystem values
*
*       method operator [] takes integer index returns integer
*           - get the priority filter
*
*       method destroy takes nothing returns nothing
*           - mostly not recommended. Let ChainPriority act as a constant and/or
*             a basis of some of your chains
*
*   struct Chain
*       readonly integer cType
*           - The chain type
*
*       player ownPlayer
*           - the owner of the chain
*           - used for filtering system; default is Player(14)(see DEFAULT_OWNER)
*
*       real x
*       real y
*       real radius
*           - the coordinates and the radius of the chain
*
*       boolean locked
*           - if true, the chain moves to the owner's position whenever
*             it is requested to get the next target (getNext())
*       unit owner
*           - the unit owner of the chain
*
*       boolean limited
*       integer count
*           - the number of counts/picks the chain can do.
*           - if limited is false, the chain can pick infinitely
*
*       boolean waitTargets
*           - if true, the chain can pick another unit for the next cycle
*             if it happens to find none on the previous cycle
*           - if false, the chain gets destroyed(regardless if limited is false)
*             Take note that you don't need to manually destroy the chain
*             if you set this to false
*
*       
*      static method create takes integer chain, real x, real y, real range, 
*                                 integer bounces, boolean limited, boolean waitTargets, 
*                                 ChainPriority priority returns thistype
*
*       static method createEx takes integer chain, unit o, real range, integer bounces, 
*                                    boolean limited, boolean waitTargets, ChainPriority priority 
*                                    returns thistype
*           - "chain" : the ChainType
*           - "o" : the unit owner
* 
*       method getNext takes nothing returns unit
*           - requests for the next target
*
*       method destroy takes nothing returns nothing
*
*
************************************************************************************
*
*   Credits
*
*       Bribe
*       TheWrecker
*       Bannar
*
************************************************************************************/
    globals
        /*
        *   The default owner of all chains.
        */
        private constant player DEFAULT_OWNER = Player(14)
        /*
        *   Destroy the chain if it has empty counts? (if it is limited)
        */
        private constant boolean DESTROY_EMPTY_CHAIN = true
    endglobals
    /*
    *   Chain Types
    */
    struct ChainType extends array
        /*
        *   Type 1 : Picks unit randomly in range
        */
        readonly static constant integer RANDOM = 1
        /*
        *   Type 2 : The previous picked unit won't be picked on the next cycle
        *            Picking method is random
        */
        readonly static constant integer BOUNCE_RANDOM = 2
        /*
        *   Type 3 : The previous picked unit won't be picked on the next cycle.
        *            Picking method is the closest unit.
        */
        readonly static constant integer BOUNCE_CLOSEST = 3
        /*
        *   Type 4: All units picked before won't be picked on future cycles.
        *           Picking method is random
        */
        readonly static constant integer UNIQUE_RANDOM = 4
        /*
        *   Type 5: All units picked before won't be picked on future cycles.
        *           Picking method is the closest unit
        */
        readonly static constant integer UNIQUE_CLOSEST = 5
    endstruct
    /*
    *   Chain Priority
    *       To make chains prioritize unit types.
    *       Priorities are in queue, meaning that it checks from the first
    *       priority to the last.
    *
    *       Values that are pushed in the ChainPriority must follow
    *       The Filtering System method (See FilteringSystem library)
    */
    struct ChainPriority
        readonly integer priorities
        private Table t
        static method create takes nothing returns thistype
            local thistype this = allocate()
            set t = Table.create()
            set priorities = 0
            return this
        endmethod
        /*
        *   add the priority filter to the unit
        */
        method operator next= takes integer filter returns nothing
            set priorities = priorities + 1
            set t[priorities] = filter
        endmethod
        /*
        *   get the priority filter 
        */
        method operator [] takes integer index returns integer
            if priorities >= index and index > 0 then
                return t[priorities]
            endif
            return 0
        endmethod
        
        method destroy takes nothing returns nothing
            set priorities = 0
            call t.destroy()
        endmethod
    endstruct
    
    struct Chain
        readonly integer cType
        /*
        *   The owner of the chain
        */
        player ownPlayer
        /*
        *   The coordinates
        */
        real x
        real y
        real radius 
        /*
        *   the owner (unit)
        *   if it is locked, the Chain always pick the
        *   target from the owning unit's position
        */
        boolean locked
        unit owner
        /*
        *   pick counter
        *   limited = if false, Chain can still pick even if it exceeds the
        *   counter
        */
        boolean limited
        integer count
        /*
        *   used to check if the chain can wait for targets(when it can't pick any)
        *   if false, the chain is destroyed once it fails to find a unit
        */
        boolean waitTargets
        
        private ChainPriority prio
        
        /*
        *   Used for BOUNCE_RANDOM and BOUNCE_CLOSEST
        */
        private unit prevTarget
        
        /*
        *   Used for UNIQUE_RANDOM and UNIQUE_CLOSEST
        */
        private Table hitUnits
        
        static method create takes integer chain, real x, real y, real range, integer bounces, boolean limited, boolean waitTargets, ChainPriority priority returns thistype
            local thistype this = allocate()
            set cType = chain
            
            set this.ownPlayer = DEFAULT_OWNER
            
            set this.x = x
            set this.y = y
            set this.radius = range
            
            set this.count = bounces
            set this.limited = limited
            
            set this.prio = priority
            
            set this.waitTargets = waitTargets
            if chain == ChainType.UNIQUE_RANDOM or chain == ChainType.UNIQUE_CLOSEST then
                set hitUnits = Table.create()
            endif
            return this
        endmethod
        
        static method createEx takes integer chain, unit o, real range, integer bounces, boolean limited, boolean waitTargets, ChainPriority priority returns thistype
            local thistype this = create(chain, GetUnitX(o), GetUnitY(o), range, bounces, waitTargets, limited, priority)
            set ownPlayer = GetOwningPlayer(o)
            set owner = o
            return this
        endmethod
        
        method destroy takes nothing returns nothing
            set x = 0
            set y = 0
            set radius = 0
            set count = 0
            set limited = false
            set waitTargets = false
            if cType == ChainType.UNIQUE_RANDOM or cType == ChainType.UNIQUE_CLOSEST then
                call hitUnits.destroy()
            endif
            
            set cType = 0
            set ownPlayer = null
            set owner = null
        endmethod
        
        private static group g = CreateGroup()
        private static unit array picked
        private static integer pickCount = 0
        
        private static thistype temp
        private static integer currentPrio
        private static unit fUnit
        /*
        *   filter for the bounce types
        */
        private static method bFilter takes nothing returns boolean
            set fUnit = GetFilterUnit()
            return temp.prevTarget != fUnit and UnitFilter(fUnit, temp.ownPlayer, temp.prio[currentPrio])
        endmethod
        /*
        *   filter for the unique types
        */
        private static method uFilter takes nothing returns boolean
            set fUnit = GetFilterUnit()
            return UnitFilter(fUnit, temp.ownPlayer, temp.prio[currentPrio]) and not temp.hitUnits.boolean[GetHandleId(fUnit)]
        endmethod
        /*
        *   to prevent null handle leaks
        */
        private static unit tempU
        /*
        *   the picking method
        */
        method getNext takes nothing returns unit
            local unit enumU
            local integer pr = 0
            if this != 0 then
                /*
                *   Check if the count has reached 0
                */
                if count == 0 then
                    /*
                    *   if the chain is limited, return null
                    */
                    if limited then
                        if DESTROY_EMPTY_CHAIN then
                            call destroy()
                        endif
                        return null
                    endif
                    /*
                    *   Otherwise, don't let it drop below 0
                    */
                    set count = 1
                endif
                /*
                *   if chain is locked, move the chain to the unit's position
                */
                if locked then
                    set x = GetUnitX(owner)
                    set y = GetUnitY(owner)
                endif
                /*
                *   ITERATE ALL PRIORITY FILTERS
                */
                set pickCount = 0
                loop
                    set pr = pr + 1
                    /*
                    *   Stubborn users :V
                    */
                    if prio[pr] == 0 then
                        return null
                    endif
                    if cType == ChainType.RANDOM then
                        /*
                        *   Get all the units that match the filter
                        */
                        call GroupEnumUnitsInRange(g, x, y, radius, null)
                        loop
                            set enumU = FirstOfGroup(g)
                            exitwhen enumU == null
                            if UnitFilter(enumU, ownPlayer, prio[pr]) then
                                set pickCount = pickCount + 1
                                set picked[pickCount] = enumU
                            endif
                            call GroupRemoveUnit(g, enumU)
                        endloop
                        /*
                        *   check if it has picked a unit
                        */
                        if pickCount > 0 then
                            /*
                            *   if true, decrement counter then return a random unit from the array
                            */
                            set count = count - 1
                            return picked[GetRandomInt(1, pickCount)]
                        endif
                        /*
                        *   Otherwise, continue to the next priority
                        */
                        set pickCount = 0
                    elseif cType == ChainType.BOUNCE_RANDOM then
                        /*
                        *   Get all the units
                        */
                        call GroupEnumUnitsInRange(g, x, y, radius, null)
                        loop
                            set enumU = FirstOfGroup(g)
                            exitwhen enumU == null
                            if UnitFilter(enumU, ownPlayer, prio[pr]) and enumU != prevTarget then
                                set pickCount = pickCount + 1
                                set picked[pickCount] = enumU
                            endif
                            call GroupRemoveUnit(g, enumU)
                        endloop
                        /*
                        *   if it has picked any
                        */
                        if pickCount > 0 then
                            /*
                            *   if true, protect the picked target for the next cycle
                            */
                            set prevTarget = picked[GetRandomInt(1, pickCount)]
                            set count = count - 1
                            return prevTarget
                        endif
                        /*
                        *   continue to the next priority
                        */
                        set pickCount = 0
                    elseif cType == ChainType.BOUNCE_CLOSEST then
                        /*
                        *   get the closest unit
                        */
                        set temp = this
                        set currentPrio = pr
                        set tempU = GetClosestUnitInRange(x, y, radius, Filter(function thistype.bFilter))
                        /*
                        *   Check if the pickin succeeded
                        */
                        if tempU != null then
                            /*
                            *   if true, decrement count then protect unit for the next cycle
                            */
                            set count = count - 1
                            set prevTarget = tempU
                            return tempU
                        endif
                        set tempU = null
                    elseif cType == ChainType.UNIQUE_RANDOM then
                        /*
                        *   Get the units that are not yet picked by the previous cycles
                        */
                        call GroupEnumUnitsInRange(g, x, y, radius, null)
                        loop
                            set enumU = FirstOfGroup(g)
                            exitwhen enumU == null
                            if UnitFilter(enumU, ownPlayer, prio[pr]) and not hitUnits.boolean[GetHandleId(enumU)] then
                                set pickCount = pickCount + 1
                                set picked[pickCount] = enumU
                            endif
                            call GroupRemoveUnit(g, enumU)
                        endloop
                        /*
                        *   Check if it has picked any unit
                        */
                        if pickCount > 0 then
                            /*
                            *   Get the random unit, then protect the unit for the future cycles
                            */
                            set tempU = picked[GetRandomInt(1, pickCount)]
                            set hitUnits.boolean[GetHandleId(tempU)] = true
                            set count = count - 1
                            return tempU
                        endif
                        /*
                        *   continue to next priority
                        */
                        set pickCount = 0
                    elseif cType == ChainType.UNIQUE_CLOSEST then
                        /*
                        *   Get the closest unit
                        */
                        set temp = this
                        set currentPrio = pr
                        set tempU = GetClosestUnitInRange(x, y, radius, Filter(function thistype.uFilter))
                        /*
                        *   Check if the picking succeeded
                        */
                        if tempU != null then
                            /*
                            *   if true, protect the unit then return
                            */
                            set count = count - 1
                            set hitUnits.boolean[GetHandleId(tempU)] = true
                            return tempU
                        endif
                        /*
                        *   continue to next priority
                        */
                        set tempU = null
                    endif
                    exitwhen pr >= prio.priorities
                endloop
                /*
                *   The cycle failed, check if it can wait for targets.
                *   if not, destroy
                */
                if not waitTargets then
                    call destroy()
                endif
                
                return null
            endif
            return null
        endmethod
    endstruct
endlibrary

Example code of a simple Sleight of Fist:
JASS:
library SleightOfFist requires Chain
    globals
        private constant integer ABIL_ID = 'fist'
        
        private constant real TIMEOUT = 0.03125
        
        private constant real RADIUS = 600
        
        private constant string SWORD_SFX = "Abilities\\Weapons\\PhoenixMissile\\Phoenix_Missile.mdl"
    endglobals
    
    struct SleightOfFist
        private unit u
        private real x
        private real y
        private Chain c
        
        private static ChainPriority cp
        
        private static thistype array ind
        private static thistype count = 0
        
        private effect sword
        
        private static constant timer t = CreateTimer()
        
        private static method P takes nothing returns nothing
            local thistype this = count
            local unit target
            loop
                exitwhen this == 0
                set target = c.getNext()
                if target != null then
                    call SetUnitX(u, GetUnitX(target))
                    call SetUnitY(u, GetUnitY(target))
                else
                    set u = count.u
                    set x = count.x
                    set y = count.y
                    set c = count.c
                    
                    call DestroyEffect(sword)
                    set sword = count.sword
                    
                    set count = count - 1
                    if count == 0 then
                        call PauseTimer(t)
                    endif
                endif
                set target = null
                set this = this - 1
            endloop
        endmethod
        
        private static method onCast takes nothing returns boolean
            local thistype this 
            if GetSpellAbilityId() == ABIL_ID then
                set this = count + 1
                set count = this
                set u = GetTriggerUnit()
                set x = GetSpellTargetX()
                set y = GetSpellTargetY()
                //set c = Chain.create(ChainType.RANDOM, x, y, RADIUS, 10, true, false, cp)
                //set c = Chain.create(ChainType.BOUNCE_RANDOM, x, y, RADIUS, 10, true, false, cp)
                //set c = Chain.create(ChainType.BOUNCE_CLOSEST, x, y, RADIUS, 10, true, false, cp)
                set c = Chain.create(ChainType.UNIQUE_RANDOM, x, y, RADIUS, 10, true, false, cp)
                //set c = Chain.create(ChainType.UNIQUE_CLOSEST, x, y, RADIUS, 10, true, false, cp)
                set sword = AddSpecialEffectTarget(SWORD_SFX, u, "weapon")
                if count == 1 then
                    call TimerStart(t, TIMEOUT, true, function thistype.P)
                endif
            endif
            return false
        endmethod
        
        private static method onInit takes nothing returns nothing
            local trigger t = CreateTrigger()
            call TriggerRegisterAnyUnitEventBJ(t, EVENT_PLAYER_UNIT_SPELL_EFFECT)
            call TriggerAddCondition(t, Condition(function thistype.onCast))
            set t = null
            
            set cp = ChainPriority.create()
            /*
            *   Sample Target Priority
            *   Heroes are picked first, if the chain can't find any hero, 
            *   it tries to pick non-heroes
            */
            set cp.next = FS_UNIT_ALIVE + FS_UNIT_ENEMY + FS_UNIT_HERO
            set cp.next = FS_UNIT_ALIVE + FS_UNIT_ENEMY + FS_UNIT_NON_HERO
        endmethod
    endstruct
endlibrary

Changelogs:
- 2.0
Major rework:
- rewrote the whole library
- no longer picks destructables and items
- CONSISTENT type removed
- Chains can now have target priorities
- No longer has Event (and events)
- No longer runs on a timer
- 1.1
- Improved Random selection
- Fixed Random stopping suddenly
- Improved Pick algorithm
- added new method : getNextTarget
- Fixed parts of the code (Thanks Bannar)
 

Attachments

  • Chain.w3x
    48.1 KB · Views: 136
Last edited:
Level 33
Joined
Apr 24, 2012
Messages
5,115
I would try to add Filters in Creators, tho the big problem is how will the filters catch your conditions like IsUnitEnemy :3

Atm, you will have to do the comparison yourself just like in the demo.

It is still okay even if I leave the filter feature,that would make the API much lighter :D
 
Last edited:
Dunno about the filtering, but about Chain.CONSISTENT, when you say lock to one target, isn't that the same as like, a DoT? As in the chain repeats itself on the same target until it's out of bounces?

also:
JASS:
//! runtextmacro CHAIN_PICK_FILTERS("Unit", "unit")
//! runtextmacro CHAIN_PICK_FILTERS("Destructable", "destructable")
//! runtextmacro CHAIN_PICK_FILTERS("Item", "item")
it can chain to destructables as well?
 
Level 33
Joined
Apr 24, 2012
Messages
5,115
Dunno about the filtering, but about Chain.CONSISTENT, when you say lock to one target, isn't that the same as like, a DoT? As in the chain repeats itself on the same target until it's out of bounces?

also:
JASS:
//! runtextmacro CHAIN_PICK_FILTERS("Unit", "unit")
//! runtextmacro CHAIN_PICK_FILTERS("Destructable", "destructable")
//! runtextmacro CHAIN_PICK_FILTERS("Item", "item")
it can chain to destructables as well?

like a DoT probably :D tho you are the one to apply what happens when Chain.CONSISTENT runs

"Chain" picks Destructables and Items. All widgets so yes. ofc you can select which kind of widget your Chain should pick via toggleTargets(allowUnit, allowDestructable, allowItem) or just put the values in the create or startOnTarget methods

I request we put an end to the 00000 stuff at the end of decimals. It's fine to do 5.00 as it helps signify that the value is real and not an int, but the trailing zeroes after an already-lengthy value definition is nonsense.

Who started it anyway? I thought reals are lossy
 
Level 24
Joined
Mar 19, 2008
Messages
3,134
I'll give you a propper feedback once I'm back from work, but notice that you don't really need SquareRoot() inside private function GD. Work with direct values instead.

Whatmore, you should modulate this snippet i.e split unit/ destructable/ item parts, put them within macros and reposition those next to globals. Granted there is a comment where "internal" script starts, this gives user an opportunity to include only modules that he/ she finds fitting.

JASS:
struct ChainSearchStyle
    readonly static constant integer CONSISTENT = 1
    readonly static constant integer RANDOM = 2
    readonly static constant integer BOUNCE = 3
    readonly static constant integer UNIQUE = 4
endstruct
Anyone?

Even if you do not want to expose "search Style" in such fashion, please consider starting "styles" from 1, instead of 0. Value of 0 should be reserved for validation checks.
 
Level 33
Joined
Apr 24, 2012
Messages
5,115
but notice that you don't really need SquareRoot() inside private function GD . Work with direct values instead.
Oh right, thank you

Whatmore, you should modulate this snippet i.e split unit/ destructable/ item parts, put them within macros and reposition those next to globals.
That's neat, thank you

JASS:
struct ChainSearchStyle
    readonly static constant integer CONSISTENT = 1
    readonly static constant integer RANDOM = 2
    readonly static constant integer BOUNCE = 3
    readonly static constant integer UNIQUE = 4
endstruct

I think Chain.Type.blah blah is much better :) Tho I think I should stick to what I have currently

ven if you do not want to expose "search Style" in such fashion, please consider starting "styles" from 1, instead of 0. Value of 0 should be reserved for validation checks.
Oh I see, thanks

Updating
 
Level 13
Joined
Nov 7, 2014
Messages
570
It seems that having the pick logic hardcoded is kind of limiting:

possible:

Chain Lightning would be: Chain.UNIQUE ("picks nearest widgets within radius that is not yet been picked", seems badly named, should be Chain.LIGHTNING ? =)),

not possible:

Healing Wave (Shadow Hunter) which would be: Chain.UNIQUE (a unit can be picked only once) + Chain.LOWEST_HEALTH_FRIENDLY_UNIT (a friendly unit with the lowest health)

Random Impale (I just made that up, channeled AOE that lifts unit up/stuns them just once) which would be: Chain.UNIQUE (a unit can be picked only once) + Chain.RANDOM (picks random units within radius)


Also, is the user supposed to deal with linked lists?

from the demo:
JASS:
// on cast
    set c = Chain.startUnits(u, Chain.UNIQUE, 5, GetSpellTargetX(), GetSpellTargetY(), RADIUS, 1.0, false)
    if c > 0 then
        set x = GetUnitX(u)
        set y = GetUnitY(u)
        set sword = AddSpecialEffectTarget("Abilities\\Weapons\\PhoenixMissile\\Phoenix_Missile.mdl", u, "weapon")

        // ???
        set n[this] = 0
        set p[this] = p[0]
        set n[p[0]] = this
        set p[0] = this
    endif

And about the demo spell ("Sleight of Fist"), how do you guarantee that the Blade Master will always hit?
I think dota had a spell like that... it disabled the hero, probably triggered the damage part and the attack animation part or something...
 
Level 33
Joined
Apr 24, 2012
Messages
5,115
It is up to the user which units shall be filtered in the process of iteration. Also, they can make a Chain produce a chain if they wanted, this is just a core. I'm not covering any possible scenarios my system shall be providing (like LOWEST_HEALTH_BLAHBLAH), i repeat, users are the ones to do the filter. My system just picks widgets, you filter it :V (see thread starter)

Users are not enforced to use linked lists, I used it in the demo because it is the only method I know for building a MUI spell :D

They can use Dynamic Indexing for iterations, or Hashtables are actually much better in this case, so that instead of O(n) they could just do O(1) for chain comparison)

I don't guarantee sleight of fist to become an official spell, it is just made for the demo :V please just focus on the system.
 
Level 33
Joined
Apr 24, 2012
Messages
5,115
will rework on this.

the next Chain will never pick the target over time, instead it picks the target whenever the user wants the next target.

the new version will also include priority targets, where every time the user requests the target, it checks first the target priority based on what the struct instance have, for example : Chain A was requested to get the next target, A checks first the nearby heroes, then nearby units.

As of now, I request someone to GY this, because the rework will take me a lot of time.
I don't have much time currently because I am co-developing a language with my friend(for fun :D)
 
There is documention, but I honestly have bit problems with understanding the real nature.
What does it exactly do? I mean, I can pretty easy can get the closest widgets by the GetClosestWidget library, and filter them. I also may use a simple group to ensure unique attacks, and I might go from low distance to far distance. I can define my own timeouts, and I can limit the amount to number "x".
It gives me back the data structure of units, that I pretty much already have, but in form of a doubly linked list.
I honestly have problems to see the usefulness, let's disucss.

edit: Awaiting update until clarified.
 
Last edited:

AGD

AGD

Level 14
Joined
Mar 29, 2016
Messages
678
The goal of this snippet, that I can see can't be done with groups without proper tweaking is that it takes into consideration a moving origin/center. Like for example in chain lightnings, the picking type is CLOSEST_UNIQUE. With a normal group, the lightning will only enumerate the units in an area once in the instance of casting but if let's say the picking has a timeout and within the time, a new unit enters the area. The new unit won't be hit since it is not included in the enumeration which means that the group should be updated just at the instance of picking. This snippet boxes up the constant enumeration per unit picking.
 
In the demo I saw a periodic lookup is wrapped in the getNext function, which I think would be fairly easy to mimic with the used GetNearestWidget + static group (lookup).
The extreme case is maybe really if you use a timeout, so (only?) then you need to code yourself more strucutre that will remove a unit after a duration from the static group. All other cases don't even need this (one) extra function, hm..
 
Level 33
Joined
Apr 24, 2012
Messages
5,115
Wow, I just got reminded that this resource was graveyarded, welp.

The reason that I decided to add that "getNext" was because not only this library was written for creating "chain lightning-like spells, but also to make a chain reaction that awaits for the result before picking a new unit. Similarly, a good example would be the overused spell Omnislash that requires the unit to attack before jumping to another unit. It allows to give that unit enough time to do it's thing before jumping to a next target. There are many use cases, but more over, this is for creating chain spells.

P.S. forgot that this uses TheFilteringSystem, neat, good thing I had this well documented (at least on my perspective)
 
Top