• 🏆 Texturing Contest #33 is OPEN! Contestants must re-texture a SD unit model found in-game (Warcraft 3 Classic), recreating the unit into a peaceful NPC version. 🔗Click here to enter!
  • It's time for the first HD Modeling Contest of 2024. Join the theme discussion for Hive's HD Modeling Contest #6! Click here to post your idea!

[Spell] casting single target spell on multiple targets

Status
Not open for further replies.
Level 1
Joined
Jul 20, 2018
Messages
4
Hey guys! I only just started learning JASS following some tutorials on this site (probs to all the creators).
Now I am trying to create a custom spell, it is supposed to be a howl that decreases armor, movement speed and attack speed of all enemy units in a certain radius around the caster. My approach was to create the spell "Intimidating Howl" that spawns a dummy at the caster's location, which is then equipped with two custom spells that I created, based on faeriefire (to reduce the armor) and slow (to reduce movement and attack speed). I am trying to let the dummy cast both these spells on all enemy units around him.
Now the problem is, that they only get cast on a single target, unless I insert a
Code:
call TriggerSleepAction(0.1)
after the spell order. That however delays the spells, I would like them to be simultaneously cast on all targets. How can I fix my problems? The code is as follows:
Code:
function WolfIntimidatingHowl_Conditions takes nothing returns boolean
    return GetSpellAbilityId() == 'A004'
endfunction

function WolfIntimidatingHowl_Actions takes nothing returns nothing

    //call BJDebugMsg("Entered Howl Actions")
    local unit caster = GetSpellAbilityUnit()
    local group targets = CreateGroup()
    local unit current_target
    local location caster_location = GetUnitLoc(caster)
    local unit dummy
    local real radius = 600.0
    local effect castEffect
   
        set castEffect = AddSpecialEffectLoc("Abilities\\Spells\\Other\\HowlOfTerror\\HowlCaster.mdl", caster_location)
        call GroupEnumUnitsInRangeOfLoc(targets, caster_location, radius, null)
        set dummy = CreateUnitAtLoc(GetOwningPlayer(caster), 'h000', caster_location, 0.)
       
        call UnitAddAbility(dummy, 'A002')
        call UnitAddAbility(dummy, 'A003')
       
        loop
            set current_target = FirstOfGroup(targets)   
        exitwhen current_target == null
            if IsUnitEnemy(current_target, GetOwningPlayer(caster)) then
                call IssueTargetOrder(dummy, "faeriefire", current_target)
        call TriggerSleepAction(0.0001)
                call IssueTargetOrder(dummy, "slow", current_target)
        call TriggerSleepAction(0.0001)
   
            endif
            call GroupRemoveUnit(targets, current_target)
        endloop
   
    call DestroyEffect(castEffect)
    call RemoveLocation(caster_location)
    call DestroyGroup(targets)
    call RemoveUnit(dummy)
    set castEffect = null
    set caster = null
    set targets = null
    set current_target = null
    set caster_location = null
    set dummy = null
   
endfunction

function InitTrig_WolfIntimidatingHowl takes nothing returns nothing
    set gg_trg_WolfIntimidatingHowl = CreateTrigger()
    call TriggerRegisterAnyUnitEventBJ(gg_trg_WolfIntimidatingHowl, EVENT_PLAYER_UNIT_SPELL_EFFECT)
    call TriggerAddCondition(gg_trg_WolfIntimidatingHowl, Condition(function WolfIntimidatingHowl_Conditions))
    call TriggerAddAction(gg_trg_WolfIntimidatingHowl, function WolfIntimidatingHowl_Actions)
endfunction
 
Level 39
Joined
Feb 27, 2007
Messages
5,010
TriggerSleepAction() is a really bad function because it polls net traffic, isn't necessarily the same for all players, and actually has a minimum duration. Regarding that last point your TSA(0.0001) is roughly TSA(0.27) if I remember correctly. For precisely timing your code you'll want to use timers. However this code doesn't need it! To fix your problem the dummy spells need to have no animation tags attached to them and the unit needs to have a "cast backswing" of 0; this allows it to cast spells instantaneously!

Also don't use locations, use X/Y coordinates whenever possible. There are native equivalents for X/Y calls for everything except GetUnitZ(), which does require a location. And when you post code here you can use [code=jass] instead of [code] tags.

____

Alternative solution with one downside: one buff icon will show green as if it was a friendly/positive buff rather than red like a debuff like the other. You can have the dummy unit cast 2 modified spells: thunderclap (-ms, -attackspeed) and Item Temporary Area Armor Bonus ('AIda'). The second is from an item that normally gives armor to nearby units so you just give it a negative bonus armor and boom! Thunderclap should be obvious. This removes the need to cast a bunch of spells targeting each unit and you can do your targeting and whatnot directly in the OE.
 
Last edited:
Level 1
Joined
Jul 20, 2018
Messages
4
Hi Pyrogasm, first of all thank you for your reply, that definitely clarified a few things for me :)
My spell however is still not quiet working as intended. I got rid of all uses of "location" and of TriggerSleepAction().
My dummy has a "cast backswing" of 0, and I believe that I have my spells set up correctly (I will post them).

The problem now is, that when I cast the spell, the two dummy spells are either not applied at all, or to just 1-3 units.
Oddly enough though, I use the debug print to check how often the spell was casted, and it I get the message "both abilites cast successfully" once per unit in radius. The slow/faerifire effect however is only applied to 0-3 units in range. Any idea what I am still doing wrong?

I am going to give your alternative solution a try once I gave up on this one :D

JASS:
function WolfIntimidatingHowl_Conditions takes nothing returns boolean
    return GetSpellAbilityId() == 'A004'
endfunction
function WolfIntimidatingHowl_Actions takes nothing returns nothing
    local unit caster = GetSpellAbilityUnit()
    local group targets = CreateGroup()
    local unit current_target
    local real caster_loc_x = GetUnitX(caster)
    local real caster_loc_y = GetUnitY(caster)
    local unit dummy
    local real radius = 300.0
    local effect castEffect
  
        set castEffect = AddSpecialEffect("Abilities\\Spells\\Other\\HowlOfTerror\\HowlCaster.mdl", caster_loc_x, caster_loc_y)
        call GroupEnumUnitsInRange(targets, caster_loc_x, caster_loc_y, radius, null)
        set dummy = CreateUnit(GetOwningPlayer(caster), 'h000', caster_loc_x, caster_loc_y, 0.)
      
        if UnitAddAbility(dummy, 'A002') and  UnitAddAbility(dummy, 'A003') then
            call BJDebugMsg("Both abilities addes successfully!")
        endif
      
        loop
            set current_target = FirstOfGroup(targets)  
        exitwhen current_target == null
            if IsUnitEnemy(current_target, GetOwningPlayer(caster)) and (not IsUnitDeadBJ(current_target)) and (IsUnitType(current_target, UNIT_TYPE_STRUCTURE) == false) then
          
                if IssueTargetOrder(dummy, "faeriefire", current_target) and  IssueTargetOrder(dummy, "slow", current_target) then
                    call BJDebugMsg("Both abilities cast successfully!")  
                endif
                
            endif
            call GroupRemoveUnit(targets, current_target)
            set current_target = null
        endloop
  
    call DestroyEffect(castEffect)
    call DestroyGroup(targets)
    call RemoveUnit(dummy)
    set castEffect = null
    set caster = null
    set targets = null
    set current_target = null
    set dummy = null
  
endfunction
function InitTrig_WolfIntimidatingHowl takes nothing returns nothing
    set gg_trg_WolfIntimidatingHowl = CreateTrigger()
    call TriggerRegisterAnyUnitEventBJ(gg_trg_WolfIntimidatingHowl, EVENT_PLAYER_UNIT_SPELL_EFFECT)
    call TriggerAddCondition(gg_trg_WolfIntimidatingHowl, Condition(function WolfIntimidatingHowl_Conditions))
    call TriggerAddAction(gg_trg_WolfIntimidatingHowl, function WolfIntimidatingHowl_Actions)
endfunction

faeriefire.png
slow.png


Edit: How can I create the JASS tag for code?
 
Level 39
Joined
Feb 27, 2007
Messages
5,010
Use [code=jass][/code] instead of [code] tags and you'll get colors and formatting to show up.

I forgot this in my first post since I just popped back into the mod scene here: to cast the spells instantaneously you'll have to use a ForGroup() call back. With 0 backswing and 0 cast point (which I think I also forgot in my previous post) the unit still can't cast twice instantly in the same thread without some sort of interrupt.

You can achieve splitting off or interrupting the thread by things like .evaluate(), ExecuteFunc(), and TimerStart(with 0.00 timeout), and if TriggerSleepAction(0) worked you could use that, but it doesn't. The easiest way in this case is to run the caster through a ForGroup that orders it to cast on each unit. Like this:
JASS:
globals
    unit dummy
    //ForGroup does not allow you to pass arguments to the enumerated function
    //so we store data in globals to use
endglobals

function groupCall takes nothing returns nothing
    local unit u = GetEnumUnit() //Picked Unit
    call IssueTargetOrder(<dummy cast ff on u>)
endfunction

function yourShit takes nothing returns nothing
    local group g = ...
    call FillGroupWithUnits() //however you do it
    set dummy = yourdummyunit
    call ForGroup(g, function groupCall)
    call CleanUpDummyUnit() //however you do it
    call DeatroyGroup(g)
endfunction


Edit, a few more notes:

1. GetSpellAbilityUnit() and GetTriggerUnit() are functionally equivalent, so you should use the latter for consistency. Also some unit calls like that return null if you call them after TriggerSleepAction() or in another thread, but GetTriggerUnit() always works even after waits.

2. Checking for "is unit dead" can be dicey. Instead see if its life is <0.406: if GetWidgetLife(unit) < 0.406 then

3. Using null for the BoolExpr argument in GroupEnumUnitsInRange actually causes a small memory leak. If you think you need to use null instead use BOOLEXPR_TRUE below. YouYu can run your target filter right there with a few more globals. Store caster, owning player, level of spell and you can do
JASS:
function isTarget takes nothing returns boolean
    local unit t = GetFilterUnit()
    local boolean b1 = GetWidgetLife(u) > 0.405
    local boolean b2 = IsUnitEnemy(t, CASTER_OWNER_GLOBAL)
    local boolean b3 = not IsUnitType(t, UNIT_TYPE_STRUCTURE)
    local boolean b4 = LEVEL_GLOBAL >= 3 or not IsUnitType(t, UNIT_TYPE_MAGIC_IMMUNE)

    return b1 and b2 and b3 and b4
endfunction

//Better null group enum
globals
    boolexpr BOOLEXPR_TRUE = null
endglobals

function trueBool takes nothing returns boolean
    return true
endfunction

//initialize this yourself 
function onInit takes nothing returns nothing
    set BOOLEXPR_TRUE = Filter(function trueBool)
endfunction

4. Instead of using RemoveUnit on your dummies, set them to "can't raise doesn't decay" in the OE and then get rid of them by applying timed lives to the dummies via UnitApplyTimedLife(). Automatic cleanup and lets the casters take as long as they need instead of possibly being removed mid-cast.

5. Creating a special effect and then immediately destroying it as you are doing here will play that effect's death animation. Normally that doesn't matter that much but many buff/effect models don't have death animations so they simply won't show up when you try to use this method. Instead I would recommend something like Vexorian's xe or some more current system that allows for on demand movable effects with easy cleanup.

6. If your dummies are well designed with a proper model they won't be a strain on the system if you create one per target of the dummy spell. That might seem a little excessive but sometimes that's easier than making the ForGroup callback with globals.
 
Last edited:
Level 1
Joined
Jul 20, 2018
Messages
4
Hey man, thanks again for the reply. Based on your tips I did a bunch more research and trial and error .. and at this point nothing works anymore :/
Just as I was about to think "hey, I think I understand how to structure this shit", things went downhill again.
Here's the current version of my code, stripped of all the comments to myself, and all the debug prints.
Is there, structure-wise, anything that is obviously wrong?
Again, your help is very much appreciated!

JASS:
scope WolfIntimidatingHowl initializer Init
    globals
        private constant integer SPELL_ID = 'A004'
        private constant integer DUMMY_ID = 'h000'
        private constant string TARGET_EFFECT = "Abilities\\Spells\\Human\\HolyBolt\\HolyBoltSpecialArt.mdl"
        private constant string CASTER_EFFECT = "Abilities\\Spells\\Other\\HowlOfTerror\\HowlCaster.mdl"
        private constant damagetype D_TYPE = DAMAGE_TYPE_MAGIC
        private constant attacktype A_TYPE = ATTACK_TYPE_MAGIC
        private constant real RADIUS = 500.00
             
        private boolexpr b
        private group targets
        private unit dummy
        private unit caster
    endglobals 
    private function isValidTarget takes unit target returns boolean
        local boolean b1 = GetWidgetLife(target) > 0.405
        local boolean b2 = not IsUnitType(target, UNIT_TYPE_STRUCTURE)
        local boolean b3 = not IsUnitType(target, UNIT_TYPE_MAGIC_IMMUNE)
     
        return b1 and b2 and b3 
    endfunction
 
    private function Pick takes nothing returns boolean
        return isValidTarget(GetFilterUnit())
    endfunction
 
    private function castOnTarget takes nothing returns nothing
        local unit current = GetEnumUnit()
        local real dX
        local real dY
        local player casterOwner = GetOwningPlayer(caster)
        local unit newDummy
     
        if IsUnitEnemy(current, casterOwner) then
     
            set dX = GetUnitX(current)
            set dY = GetUnitY(current)
            set newDummy = CreateUnit(casterOwner, DUMMY_ID, dX, dY, 0.0)     
         
            call UnitAddAbility(dummy, 'A002')
            call IssueTargetOrder(dummy, "slow", current)
         
            call DestroyEffect(AddSpecialEffect(TARGET_EFFECT, dX, dY))
            call UnitApplyTimedLife(dummy, 'BTLF', 1)
     
        else
         
            call BJDebugMsg("Current interation unit is no enemy!")
        endif 
    endfunction
    // ========================= CONDITIONS ======================
    private function Conditions takes nothing returns boolean
        return GetSpellAbilityId() == SPELL_ID
    endfunction
 
    // =========================== ACTIONS =======================
    private function Actions takes nothing returns nothing
     
        local real CasterLocX
        local real CasterLocY
        local unit test = null
        set caster = GetTriggerUnit()
     
        set CasterLocX = GetUnitX(caster)
        set CasterLocY = GetUnitY(caster)
     
                 
            call DestroyEffect(AddSpecialEffect(CASTER_EFFECT, CasterLocX, CasterLocY))
            call GroupEnumUnitsInRange(targets, CasterLocX, CasterLocY, RADIUS, b)
                     
            call ForGroup(targets, function castOnTarget)
                     
            call DestroyGroup(targets)
            set targets = null
                     
    endfunction
    // ============================ INIT =========================
    private function Init takes nothing returns nothing
         local trigger WolfIntimidatingHowl = CreateTrigger()
         call TriggerRegisterAnyUnitEventBJ(WolfIntimidatingHowl, EVENT_PLAYER_UNIT_SPELL_EFFECT)
         call TriggerAddCondition(WolfIntimidatingHowl, Condition(function Conditions))
         call TriggerAddAction(WolfIntimidatingHowl, function Actions)
       
         // INIT GLOBALS
         set b = Condition(function Pick)
         set targets = CreateGroup()
    endfunction
endscope
 
Level 39
Joined
Feb 27, 2007
Messages
5,010
I think you DO understand how to structure it, since you got to some of the things that even I forgot were best practice, like storing the target filterfunc in a global boolexpr. In general your structure looks totally fine and correct to me.

I believe the problem is that you destroy your global enum group the first time the trigger runs. Change the following:
JASS:
call DestroyGroup(targets)
set targets = null
// ...
// To this
call GroupClear(targets)
If that doesn't work, post the version with the debug messages and tell me which messages you ARE seeing.
 
Level 1
Joined
Jul 20, 2018
Messages
4
Holy hell ..
Coming back to it after a couple hours I noticed that I create a dummy called "newDummy", but use a dummy called "dummy".
Guess I'm the dummy after all.

My final code seems to work as I want it to, however per target I am now creating 2 dummies for 2 seconds, which might not be the smartest approach.
I will definitely try to replace the "slow" cast with a thunderclap to at least half the amount of dummies.
Again, thank you very much for your help, your comments taught me quiet a lot!
Final code for now:

JASS:
scope WolfIntimidatingHowl initializer Init
    globals
        private constant integer SPELL_ID = 'A004'
       
        private constant integer SPELL_ID_SLOW = 'A002'
        private constant string ORDER_ID_SLOW = "slow"
        private constant integer SPELL_ID_ARMOR = 'A003'
        private constant string ORDER_ID_ARMOR = "faeriefire"
       
       
        private constant integer DUMMY_ID = 'h000' 
        private constant string TARGET_EFFECT = "Abilities\\Spells\\Human\\HolyBolt\\HolyBoltSpecialArt.mdl"
        private constant string CASTER_EFFECT = "Abilities\\Spells\\Other\\HowlOfTerror\\HowlCaster.mdl"
        private constant damagetype D_TYPE = DAMAGE_TYPE_MAGIC
        private constant attacktype A_TYPE = ATTACK_TYPE_MAGIC
        private constant real RADIUS = 800.00
               
        private boolexpr b
        private group targets
        private unit dummy
        private unit caster
    endglobals   
    private function isValidTarget takes unit target returns boolean
        local boolean b1 = GetWidgetLife(target) > 0.405
        local boolean b2 = not IsUnitType(target, UNIT_TYPE_STRUCTURE)
        local boolean b3 = not IsUnitType(target, UNIT_TYPE_MAGIC_IMMUNE)
       
        return b1 and b2 and b3   
    endfunction
   
    private function Pick takes nothing returns boolean
        return isValidTarget(GetFilterUnit())
    endfunction
   
    private function castSlowOnTarget takes nothing returns nothing
        local unit current = GetEnumUnit()
        local real dX
        local real dY
        local player casterOwner = GetOwningPlayer(caster) 
        local unit newDummy
       
        if IsUnitEnemy(current, casterOwner) then   
            set dX = GetUnitX(current)
            set dY = GetUnitY(current)
           
            set newDummy = CreateUnit(casterOwner, DUMMY_ID, dX, dY, 0.0)       
           
            call UnitAddAbility(newDummy, SPELL_ID_SLOW)           
            call IssueTargetOrder(newDummy, ORDER_ID_SLOW, current)                       
            call UnitApplyTimedLife(newDummy, 'BTLF', 2)
            call DestroyEffect(AddSpecialEffect(TARGET_EFFECT, dX, dY))
       
        else
           
            call BJDebugMsg("Current interation unit is no enemy!")
        endif   
    endfunction
   
    private function castFaeriefireOnTarget takes nothing returns nothing
        local unit current = GetEnumUnit()
        local real dX
        local real dY
        local player casterOwner = GetOwningPlayer(caster) 
        local unit newDummy
       
        if IsUnitEnemy(current, casterOwner) then   
            set dX = GetUnitX(current)
            set dY = GetUnitY(current)
           
            set newDummy = CreateUnit(casterOwner, DUMMY_ID, dX, dY, 0.0)       
           
            call UnitAddAbility(newDummy, SPELL_ID_ARMOR           
            call IssueTargetOrder(newDummy, ORDER_ID_ARMOR, current)                       
            call UnitApplyTimedLife(newDummy, 'BTLF', 2)
            call DestroyEffect(AddSpecialEffect(TARGET_EFFECT, dX, dY))
       
        else
           
            call BJDebugMsg("Current interation unit is no enemy!")
        endif   
    endfunction
    // ========================= CONDITIONS ======================
    private function Conditions takes nothing returns boolean
        return GetSpellAbilityId() == SPELL_ID
    endfunction
   
    // =========================== ACTIONS =======================
    private function Actions takes nothing returns nothing
       
        local real CasterLocX
        local real CasterLocY
        set caster = GetTriggerUnit()
       
        set CasterLocX = GetUnitX(caster)
        set CasterLocY = GetUnitY(caster)
       
                   
            call DestroyEffect(AddSpecialEffect(CASTER_EFFECT, CasterLocX, CasterLocY))
            call GroupEnumUnitsInRange(targets, CasterLocX, CasterLocY, RADIUS, b)
                       
            call ForGroup(targets, function castSlowOnTarget)
            call ForGroup(targets, function castFaeriefireOnTarget)                       
            call GroupClear(targets) 
                       
    endfunction
    // ============================ INIT =========================
    private function Init takes nothing returns nothing
         local trigger WolfIntimidatingHowl = CreateTrigger()
         call TriggerRegisterAnyUnitEventBJ(WolfIntimidatingHowl, EVENT_PLAYER_UNIT_SPELL_EFFECT)
         call TriggerAddCondition(WolfIntimidatingHowl, Condition(function Conditions))
         call TriggerAddAction(WolfIntimidatingHowl, function Actions)
         
         // INIT GLOBALS
         set b = Condition(function Pick)
         set targets = CreateGroup()
         
         // PRELOADS
         call Preload(TARGET_EFFECT)
         call Preload(CASTER_EFFECT)
         
         set bj_lastCreatedUnit = CreateUnit(Player(PLAYER_NEUTRAL_PASSIVE), DUMMY_ID, 0,0,0)
         call UnitAddAbility(bj_lastCreatedUnit, SPELL_ID)
         call KillUnit(bj_lastCreatedUnit)
    endfunction
endscope
 
Status
Not open for further replies.
Top