HomeUser Control Panel (unavailable in archive)ForumsTutorialsArt GalleryResourcesMaps

Mulit-Targeting System

02-09-2009, 07:00 AM#1
Pyrogasm
What is Wc3 missing? An easy way to make spells with multiple targets. Thus is the reason for making this system of mine. I hope to create a simple way to configure spells that, for example, switch the position of two target units on the battlefield, destroy 5 target trees, or even create a death field within 6 target points.

Here is the code I have so far:
Collapse JASS:
library MultiTargeting initializer Init requires LastOrder, TimerUtils
    globals
        private constant string CANCEL_ORDER = "taunt" //In theory this can be any order
        private constant integer CANCEL_ID = 'cncl' //Change it if you want
        private constant string CANCEL_HOTKEY = "l" //Must be lowercase

        private constant real CANCEL_DETECT_PERIOD = 0.04
        private constant real CANCEL_MARGIN = 0.10

        private constant integer MAX_NUMBER_OF_TARGETS = 10 //This indirectly limits the number of available instances of
                                                            //The MultiTargetHolder struct, which shouldn't be an issue anyway

        constant integer MULT_POINT_TARGET = 1        //Constants used in return values
        constant integer MULT_UNIT_TARGET = 2
        constant integer MULT_ITEM_TARGET = 3
        constant integer MULT_DESTRUCTABLE_TARGET = 4

        constant integer NEXT_POINT = 1
        constant integer CANCEL = 2
        constant integer CAST = 3
    endglobals

    //Objectmerger for Cancel


    function interface MultiTargetCondition takes nothing returns integer
    function interface MultiTargetAction takes nothing returns nothing

    globals
        private integer CancelOrderId = 0

        private keyword MultiTargetHolder
        public MultiTargetHolder array MTHs
        private integer N = 0
        private MultiTargetHolder array PeriodicMTHs

        private timer T = null
        public MultiTargetHolder CurrentMTH = 0

        private trigger CancelTrig = null
        private trigger TargetTrig = null

        private SpellInfo array PSpells
        private SpellInfo array USpells
        private SpellInfo array ISpells
        private SpellInfo array DSpells

        private integer PN = 0
        private integer UN = 0
        private integer IN = 0
        private integer DN = 0
    endglobals

    function GetCurrentTargetHolder takes nothing returns MultiTargetHolder
        return CurrentMTH
    endfunction

    private struct SpellInfo
        integer Index = 0
        integer Id = 0
        integer Order = 0
        string HK = ""
        MultiTargetCondition Cond = null
        MultiTargetAction Cancel = null
    endstruct

    struct MultiTargetHolder
        unit O = null
        integer Targets = 0
        integer Index = 0
        integer Type = 0
        player P = null
        timer T = null
        SpellInfo SI = 0

        real array X[MAX_NUMBER_OF_TARGETS]
        real array Y[MAX_NUMBER_OF_TARGETS]
        unit array U[MAX_NUMBER_OF_TARGETS]
        item array I[MAX_NUMBER_OF_TARGETS]
        destructable array D[MAX_NUMBER_OF_TARGETS]

        static method Periodic takes nothing returns nothing
            local integer J = 0
            local MultiTargetHolder M

            loop
                set M = PeriodicMTHs[J]
                if GetLocalPlayer() == M.P then
                    call ForceUIKey(CANCEL_HOTKEY)
                endif                

                set J = J+1
                exitwhen J >= N
            endloop
        endmethod

        method AddToStack takes nothing returns nothing
            call UnitAddAbility(.O, CANCEL_ID)

            set PeriodicMTHs[N] = this
            set .Index = N
            set N = N+1

            if N == 1 then
                call TimerStart(T, CANCEL_DETECT_PERIOD, true, function MultiTargetHolder.Periodic)
            endif
        endmethod

        method RemoveFromStack takes nothing returns nothing
            call UnitRemoveAbility(.O, CANCEL_ID)

            set N = N-1
            if N == 0 then
                call PauseTimer(T)
            else
                set PeriodicMTHs[.Index] = PeriodicMTHs[N]
            endif
        endmethod

        static method Refresh takes nothing returns nothing
            local MultiTargetHolder M = GetTimerData(GetExpiredTimer())
            call ReleaseTimer(M.T)
            call M.AddToStack()
        endmethod

        method NextTarget takes nothing returns nothing
             call DisableTrigger(CancelTrig)
             call DisableTrigger(TargetTrig)
             call PauseUnit(.O, true)
             call IssueImmediateOrder(.O, "stop")
             call PauseUnit(.O, false)
             call EnableTrigger(CancelTrig)
             call EnableTrigger(TargetTrig)

             if GetLocalPlayer() == .P then
                 call ForceUIKey(.SI.HK)
             endif

             set .T = NewTimer()
             call SetTimerData(.T, this)
             call TimerStart(.T, CANCEL_MARGIN, false, function MultiTargetHolder.Refresh)
        endmethod

        static method create takes unit O, integer Type, SpellInfo SI returns MultiTargetHolder
            local MultiTargetHolder M = TargetHolder.allocate()

            set M.O = O
            set M.Type = Type
            set M.P = GetOwningPlayer(O)
            set M.SI = SI

            return M
        endmethod

        method onDestroy takes nothing returns nothing
            set MTHs[GetUnitIndex(.O)] = 0
        endmethod
    endstruct

    private function OnTargetOrder takes nothing returns boolean
        local eventid E = GetTriggerEventId()
        local integer OId = GetIssuedOrderId()
        local integer J = 0
        local SpellInfo SI
        local unit O = GetTriggerUnit()
        local MultiTargetHolder M = 0
        local integer Index
        local unit U
        local item I
        local destructable D

        if E == EVENT_PLAYER_UNIT_ISSUED_POINT_ORDER then
            loop
                set SI = PSpells[J]
                if SI.Order == OId and GetUnitAbilityLevel(O, SI.Id) > 0 then
                    set Index = GetUnitIndex(O)
                    set M = MTHs[Index]
                    if M == 0 then
                        set M = MultiTargetHolder.create(O, MULTI_POINT_TARGET, SI)
                        set MTHs[Index] = M
                    endif

                    set M.X[M.Targets] = GetOrderTargetX()
                    set M.Y[M.Targets] = GetOrderTargetY()
                    set M.Targets = M.Targets+1

                    exitwhen true
                endif

                set J = J+1
                exitwhen J >= PN
            endloop
        else
            set U = GetOrderTargetUnit()
            if U != null then
                loop
                    set SI = USpells[J]
                    if SI.Order == OId and GetUnitAbilityLevel(O, SI.Id) > 0 then
                        set Index = GetUnitIndex(O)
                        set M = MTHs[Index]
                        if M == 0 then
                            set M = MultiTargetHolder.create(O, MULTI_DESTRUCTABLE_TARGET, SI)
                            set MTHs[Index] = M
                        endif

                        set M.U[M.Targets] = U
                        set M.Targets = M.Targets+1

                        set U = null
                        exitwhen true
                    endif

                    set J = J+1
                    exitwhen J >= UN
                endloop
            else
                set I = GetOrderTargetItem()
                if I != null then
                    loop
                        set SI = ISpells[J]
                        if SI.Order == OId and GetUnitAbilityLevel(O, SI.Id) > 0 then
                            set Index = GetUnitIndex(O)
                            set M = MTHs[Index]
                            if M == 0 then
                                set M = MultiTargetHolder.create(O, MULTI_DESTRUCTABLE_TARGET, SI)
                                set MTHs[Index] = M
                            endif

                            set M.I[M.Targets] = I
                            set M.Targets = M.Targets+1

                            set I = null
                            exitwhen true
                        endif

                        set J = J+1
                        exitwhen J >= IN
                    endloop
                else
                    set D = GetOrderTargetDestructable()
                    if D != null then
                        loop
                            set SI = DSpells[J]
                            if SI.Order == OId and GetUnitAbilityLevel(O, SI.Id) > 0 then
                                set Index = GetUnitIndex(O)
                                set M = MTHs[Index]
                                if M == 0 then
                                    set M = MultiTargetHolder.create(O, MULTI_DESTRUCTABLE_TARGET, SI)
                                    set MTHs[Index] = M
                                endif

                                set M.D[M.Targets] = D
                                set M.Targets = M.Targets+1

                                set D = null
                                exitwhen true
                            endif

                            set J = J+1
                            exitwhen J >= DN
                        endloop
                    endif
                endif                
            endif
        endif

        if M != 0 then
            call M.RemoveFromStack()
            set CurrentMTH = M

            set Index = M.SI.Cond.evaluate()
            if Index == NEXT then
                call M.NextTarget()
            elseif Index == CANCEL then
                call M.SI.Cancel.execute()
                call M.destroy()
            endif
        endif

        return false
    endfunction

    private function OnCancelOrder takes nothing returns boolean
        local MultiTargetHolder M
        local unit U = GetTriggerUnit()

        if GetIssuedOrderId() == CancelOrderId and GetUnitAbilityLevel(U, CANCEL_ID) > 0 then
            set M = MTHs[GetUnitIndex(U)]
            set CurrentMTH = M

            call M.SI.Cancel.execute()
            call M.RemoveFromStack()
            call M.destroy()
        endif

        set U = null
        return false
    endfunction


    function RegisterMultiTargetEvent takes integer EventType, integer SpellId, string SpellOrder, string SpellHotKey, MultiTargetCondition Cond, MultiTargetAction Act, MultiTargetAction Cancel returns nothing
        local SpellInfo SI = SpellInfo.create()

        set SI.Index = N
        set SI.Id = SpellId
        set SI.Order = OrderId(SpellOrder)
        set SI.HK = SpellHotKey
        set SI.TN = 1

        set SpellInfos[N] = SI
        set N = N+1

        if EventType == MULT_POINT_TARGET then
            set PSpells[PN] = SI
            set PN = PN+1
        elseif EventType == MULT_UNIT_TARGET then
            set USpells[PN] = SI
            set UN = UN+1
        elseif EventType == MULT_ITEM_TARGET then
            set ISpells[PN] = SI
            set IN = IN+1
        elseif EventType == MULT_DESTRUCTABLE_TARGET then
            set DSpells[PN] = SI
            set DN = DN+1
        endif
    endfunction

    private function Init takes nothing returns nothing
        set SpellInfos[0] = 0
        set PSpells[0] = 0
        set USpells[0] = 0
        set ISpells[0] = 0
        set DSpells[0] = 0

        set CancelOrderId = OrderId(CANCEL_ORDER)
        set T = CreateTimer()

        set CancelTrig = CreateTrigger()
        call TriggerRegisterAnyUnitEvent(CancelTrig, EVENT_PLAYER_UNIT_ISSUED_ORDER)
        call TriggerAddCondition(CancelTrig, Condition(function OnCancelOrder)

        set TargetTrig = CreateTrigger()
        call TriggerRegisterAnyUnitEvent(TargetTrig, EVENT_PLAYER_UNIT_ISSUED_TARGET_ORDER)
        call TriggerRegisterAnyUnitEvent(TargetTrig, EVENT_PLAYER_UNIT_ISSUED_POINT_ORDER)
        call TriggerAddCondition(TargetTrig, Condition(function OnTargetOrder)
    endfunction
endlibrary
I have not yet done any syntax checking, nor do I know if it all works properly yet, but the method I used should work.

At any rate, the way this works is you would call RegisterMultiTargetEvent(<Type of targeting>, <The spell cast that triggers this event>, <The orderstring of said spell>, <The hotkey of said spell>, <A function to evaluate when a new target is 'found;>, <A function to execute when the targeting is cancelled>)
The "type" can be MULTI_POINT_TARGET, MULTI_UNIT_TARGET, MULTI_ITEM_TARGET, or MULTI_DESTRUCTABLE_TARGET.

The first function would have the format function interface MultiTargetCondition takes nothing returns integer, and would return one of three things: NEXT, CANCEL, or CAST.
  • If NEXT, the system puts the targeting instance back into the queue to find another target
  • If CANCEL, the system cancels targeting and then executes the cancel function
  • If CAST, the system does nothing and lets the spell cast continue

The cancel function can neither take nor return anything, but may do whatever it wants. It is not required that it destroy the Targeting instance.

And that's pretty much as best as I can explain it... I'll give an example. This spell would switch the position of two units:
Collapse JASS:
scope Switch initializer Init
    globals
        private constant integer SPELL_ID = 'Swch'
        private constant string SPELL_HOTKEY = "w"
        private constant string SPELL_ORDER = "transmute"
    endglobals

    private function TargetCancel takes nothing returns nothing
        local MultiTargetHolder M = GetCurrentTargetHolder()

        if M.Targets == 1 then
            call SetUnitVertexColor(M.U[0], 255, 255, 255, 0)
    endfunction

    private function TargetConditions takes nothing returns integer
        local MultiTargetHolder M = GetCurrentTargetHolder()

        if M.Targets == 1 then
            if GetLocalPlayer() == M.P then
                call SetUnitVertexColor(M.U[0], 150, 150, 255, 0) //Unless I got alpha mixed up
            endif
            return NEXT
        elseif M.Targets == 2 then
            return CAST
        endif
    endfunction

    private function CastConditions takes nothing returns boolean
        return GetSpellAbilityId() == SPELL_ID
    endfunction

    private function Cast takes nothing returns nothing
        local MultiTargetHolder M = MTHs[GetUnitIndex(GetTriggerUnit())]
        local real X0 = GetUnitX(M.U[0])
        local real Y0 = GetUnitY(M.U[0])
        local real X1 = GetUnitX(M.U[1])
        local real Y1 = GetUnitY(M.U[1])

        call SetUnitX(M.U[0], X1)
        call SetUnitY(M.U[0], Y1)
        call SetUnitX(M.U[1], X0)
        call SetUnitY(M.U[1], Y0)

        call M.destroy()
    endfunction

    private function Init takes nothing returns nothing
        local trigger T = CreateTrigger()

        call RegisterMultiTargetEvent(MULTI_UNIT_TARGET, SPELL_ID, SPELL_ORDER, SPELL_HOTKEY, function TargetConditions, function TargetCancel)
        call TriggerRegisterAnyUnitEventBJ(T, EVENT_PLAYER_UNIT_SPELL_EFFECT)
        call TriggerAddCondition(T, Condition(function CastConditions))
        call TriggerAddAction(T, function Cast)

        set T = null
    endfunction
endscope

Yeah... So I'm looking for comments on the syntax/how things are done. And I guess just showcasing the idea here.

An issue with this system right now is that you can only have multiple targets of the same type; in the future I would like to make it so that you can have any type of targets for each time you register the event. This would require some reworking of things with actually casting the spell, and stuff like that.
02-09-2009, 10:14 AM#2
saw792
Your 'MULTI_..._TARGET' constants actually say 'MULT'. You forgot the I. Just noticed you used 'MULTI' in the example spell.

EDIT: You also forgot an 'endif' in the example code.
02-09-2009, 10:26 AM#3
fX_
I didn't read the script 'coz I am in a hurry to say that I think this is a very good idea.

I think Anitarf or somebody made something like this for 'Tripwire' - a submitted spell. When I tested it I also though "what a good idea". lol
02-10-2009, 05:29 AM#4
Pyrogasm
And the method Anitarft used for Tripwire (with a few changes) is the method I used in Flare for the 10th Spell Session.

What I meant by "suggestions on syntax" is: how would you envision this working? (Not: where are some syntax errors?) I mean; how would you like to access the stored targets when the spell is cast, and that sort of thing? I sort-of have the struct thing going on, but I don't know if that's a very good solution.

Perhaps there's a more elegant way to go about this?
02-10-2009, 05:38 AM#5
Bobo_The_Kodo
Maybe something like this:

call TriggerRegisterMultiSpellEffect( t, spellid ) ( or something similair )

and then in actions

GetOrderPointX1()
GetOrderPointY1()
GetOrderTarget1()

or

Targets.x(1)
Targets.y(1)

or

Targets.x[1]
Targets.y[1]
Targets.widget[1]
Targets.unit[1]

I would probably prefer the last one mentioned
02-10-2009, 06:18 AM#6
Pyrogasm
The problem with doing it through triggers is this: The condition function needs to differentiate between 3 possible scenarios (cancel, get another target, or continue with cast), and you can't do that with just returning a boolean.

Also, it doesn't include re-issuing order functionality yet, but I plan to add that by implementing LastOrder.


Edit: I like the idea of caster:Target1 syntax.
02-10-2009, 11:27 PM#7
Pyrogasm
I don't understand what you're talking about, and I think it's because you don't get what I mean. :P

Anyway, if I were to make my own TriggerRegister event, the user would also add a conditionfunc to that trigger, which I would evaluate to see what should be done when a target is detected. That would need to determine whether to continue finding targets, let the cast happen, or cancel the casting. However, this conditionfunc can only return true or false, which is not sufficient.

When to do all three of those could be different for all spells, so the user needs to supply an appropriate function (I can't just evaluate some default system function), which is why I can't just use the trigger's ConditionFunc. I can't think of a way to set it up so that the user doesn't need to supply the proper function (TargetConditions, in the Switch spell) and can still use TriggerAddCondition() and TriggerAddAction() normally.

Aside from that, there needs to be a cancel function too.
02-11-2009, 12:18 AM#8
fX_
I wouldn't alter how triggers work.
-I'd keep it as:

TriggerRegisterSomeEvent
TriggerAddCondition
TriggerAddAction

Maybe you can just detect when a spell is casted, determine the spell and the configuration of its multi target aspect according to the system.