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

Tornado spell request!

Status
Not open for further replies.
Level 14
Joined
Jul 19, 2007
Messages
772
I need some help to make a tornado spell for my map that works abit like shockwave but also knocks back all damaged enemies and slowing their movement speed. I'm very lost at making triggering spells so I can't do this spell, please is someone up for helping me?

Better describiton of the tornado-spell:
The caster summons a powerful wind to attack his enemy targets in the target location. Enemies who get to close to the wind will take damage based on the caster's intelligence and will be knocked back and gets it movement speed slowed for 5 seconds. |n|n|cffffcc00Level 1|r - 1x intelligence damage, slows movement speed by 15%. |n|n|cffffcc00Level 2|r - 2x intelligence damage, slows movement speed by 25%. |n|n|cffffcc00Level 3|r - 3x intelligence damage, slows movement speed by 35%. |n|n|cffffcc00Level 4|r - 4x intelligence damage, slows movement speed by 45%.
 
Level 39
Joined
Feb 27, 2007
Messages
5,010
Map is attached. I used Knockback3D, which has a weird bug with the dust fx model I just posted about here, so if you don't like how that looks you can just change the model. The caster units in this map don't have any intelligence and aren't heroes so the spell can't be leveled up but it will do the requested int scaling damage and scale as requested. I didn't write a tooltip, you can do that yourself. If you want sounds you'll have to do that yourself too or give me something to go on and I'll put some sound in. Choose fx as you so desire.

There is 1 trigger, 3 dependency libraries, 2 abilities, 1 buff, and 1 dummy caster. Copy them all. The map also has a couple other spells I was working on in it, just ignore them unless you like them.
JASS:
library Windblast requires Knockback3D
  globals
    public  integer SPELL_ID   = 'Wbst'
    public  integer DUMMY_ID   = 'dmmy'
    private real    TIMER_TICK = 0.03
    private real MAX_COLLISION = 128.
   
    private real VELOCITY = 900.
    private real DISTANCE = 600.
   
    private real KB_VEL = 500.
    private real KB_ALPHA = 0. //elevation angle to knock back at

    private real    EFFECT_H   = 10.
    private string  EFFECT_1A  = "Doodads\\Cinematic\\GlowingRunes\\GlowingRunes3.mdl"

    private real    E1A_SCALE  = 2.2
    private real    E1A_TSCALE = .1
    private real    E1A_TSKIP  = 1.0 //skip this far ahead in the stand animation

    private integer E1A_COL_R  = 255
    private integer E1A_COL_G  = 255
    private integer E1A_COL_B  = 255

  endglobals
 
  native UnitAlive takes unit u returns boolean
 
  globals
    public  timer   SpellTimer  = CreateTimer()
    public  trigger CastTrigger = CreateTrigger()

    public  keyword SpellData
    private SpellData array GSD
    private integer COUNT = 0
    private integer CURR = 0
  endglobals



  private module spellBehavior
    // these are defined in the struct below for you to use:
    // they do not need to be destroyed or removed, that is handled already
    //
    // static group SEARCH_GROUP  <-- a group you can use to search for targets via GroupEnumUnitsIn...
    // unit DUMMY                 <-- can cast instantly and is moved to (.cx, .cy) every timer step
    //                                add abilities to it with .prepDummy(abilCode) (level is set automatically)
    // group HIT_GROUP            <-- units already hit by this instance of the spell, you are free to clear this as necessary, but you must add units manually
    // integer l                  <-- level of the spell when it was cast
    // unit c                     <-- caster
    // player p                   <-- owner of caster
    // real cx, cy, cz            <-- coordinates of the center of the spell
    // you may end an instance early by calling .endInstance()
    // you may add your own instance or static members here:

    static integer SECONDARY_ID   = 'Wbls'
    static string SECONDARY_ORDER = "slow"
    static real RADIUS = 100.
   
    method dmgIntFactor takes nothing returns real
      return this.l * 1.0
    endmethod

    static method onInitEx takes nothing returns nothing
      //called on map init, so you can set up anything you need to here (most likely arrays or some global dummies)
    endmethod

    method onStart takes nothing returns nothing
      //called when the spell is cast
      call this.prepDummy(SECONDARY_ID)
    endmethod

    method onPeriodic takes nothing returns nothing
      local unit u
      local real x
      local real y
      local real a
      local real dmg = this.dmgIntFactor()*GetHeroInt(this.c, true)

      call GroupEnumUnitsInRange(SEARCH_GROUP, this.cx, this.cy, RADIUS+MAX_COLLISION, null)
      loop
        set u = FirstOfGroup(SEARCH_GROUP)
        exitwhen u == null
        call GroupRemoveUnit(SEARCH_GROUP, u)

        if IsUnitInRangeXY(u, this.cx, this.cy, RADIUS) and /*
        */ IsUnitEnemy(u, this.p) and UnitAlive(u)      and /*
        */ not IsUnitType(u, UNIT_TYPE_MAGIC_IMMUNE)    and /*
        */ not BlzIsUnitInvulnerable(u)                 and /*
        */ not IsUnitInGroup(u, this.HIT_GROUP)         then
       
          set x = GetUnitX(u)
          set y = GetUnitY(u)
          set a = Atan2(y-this.cx, x-this.cx)
          if a > 0. then
            set a = this.kbaL
          else
            set a = this.kbaR
          endif

          call UnitDamageTarget(this.c, u, dmg, false, false, ATTACK_TYPE_NORMAL, DAMAGE_TYPE_FORCE, WEAPON_TYPE_WHOKNOWS)
          call Knockback3D.add(u, KB_VEL, a, KB_ALPHA)
          call IssueTargetOrder(this.DUMMY, SECONDARY_ORDER, u)
          call GroupAddUnit(this.HIT_GROUP, u)
        endif
      endloop

      set u = null
    endmethod

    method onEnd takes nothing returns nothing
      //called when the spell instance ends
    endmethod
  endmodule



  public struct SpellData
    static group SEARCH_GROUP = CreateGroup()
    group HIT_GROUP = null
    unit DUMMY = null

    unit c    = null
    player p  = null
    integer l = 0
    integer d = 0
    effect e = null
    real cx = 0.
    real cy = 0.
    real dx = 0.
    real dy = 0.
    real kbaL = 0.
    real kbaR = 0.


    method prepDummy takes integer abil returns nothing
      call UnitAddAbility(this.DUMMY, abil)
      call SetUnitAbilityLevel(this.DUMMY, abil, this.l)
    endmethod

    implement spellBehavior

    method endInstance takes nothing returns nothing
      call this.onEnd()

      call RemoveUnit(this.DUMMY)
      call DestroyGroup(this.HIT_GROUP)
      //call DestroyEffect(this.e)
      call RemoveEffect(this.e)

      set GSD[CURR] = GSD[COUNT-1]
      set COUNT = COUNT-1
      set CURR = CURR-1

      if COUNT == 0 then
        call PauseTimer(SpellTimer)
      endif

      call this.destroy()
    endmethod

    static method periodic takes nothing returns nothing
      local SpellData sd

      set CURR = 0
      loop
        exitwhen CURR >= COUNT
        set sd = GSD[CURR]

        set sd.cx = sd.cx + sd.dx
        set sd.cy = sd.cy + sd.dy
        call BlzSetSpecialEffectX(sd.e, sd.cx)
        call BlzSetSpecialEffectY(sd.e, sd.cy)
        call BlzSetSpecialEffectHeight(sd.e, EFFECT_H)
        call SetUnitX(sd.DUMMY, sd.cx)
        call SetUnitY(sd.DUMMY, sd.cy)
        call sd.onPeriodic()

        set sd.d = sd.d-1
        if sd.d <= 0 then
          call sd.endInstance()
        endif
       
        set CURR = CURR+1
      endloop
    endmethod

    static method create takes unit c, real x, real y returns thistype
      local thistype sd = thistype.allocate()
      local real a
     
      set sd.c = c
      set sd.p = GetOwningPlayer(sd.c)
      set sd.l = GetUnitAbilityLevel(sd.c, SPELL_ID)
      set sd.cx = GetUnitX(sd.c)
      set sd.cy = GetUnitY(sd.c)
      set a = Atan2(y-sd.cy, x-sd.cx)
      set sd.kbaL = a + bj_PI/2.
      set sd.kbaR = a - bj_PI/2.
      set sd.dx = VELOCITY*Cos(a)*TIMER_TICK
      set sd.dy = VELOCITY*Sin(a)*TIMER_TICK
      set sd.d = R2I(DISTANCE/VELOCITY/TIMER_TICK)

      set sd.e = AddSpecialEffect(EFFECT_1A, sd.cx, sd.cy)
      call BlzSetSpecialEffectColor(sd.e, E1A_COL_R, E1A_COL_G, E1A_COL_B)
      call BlzSetSpecialEffectHeight(sd.e, EFFECT_H)
      call BlzSetSpecialEffectScale(sd.e, E1A_SCALE)
      call BlzSetSpecialEffectTimeScale(sd.e, E1A_TSCALE)
      call BlzSetSpecialEffectTime(sd.e, E1A_TSKIP)
     
      set sd.DUMMY = CreateUnit(sd.p, DUMMY_ID, sd.cx, sd.cy, 0.)
      set sd.HIT_GROUP = CreateGroup()

      if COUNT == 0 then
        call TimerStart(SpellTimer, TIMER_TICK, true, function thistype.periodic)
      endif
      set GSD[COUNT] = sd
      set COUNT = COUNT+1

      return sd
    endmethod

    static method onCast takes nothing returns boolean
      if GetSpellAbilityId() == SPELL_ID then
        call SpellData.create(GetTriggerUnit(), GetSpellTargetX(), GetSpellTargetY())
        call GSD[COUNT-1].onStart()
      endif
      return false
    endmethod

    static method onInit takes nothing returns nothing
      call TriggerRegisterAnyUnitEventBJ(CastTrigger, EVENT_PLAYER_UNIT_SPELL_EFFECT)
      call TriggerAddCondition(CastTrigger, Condition(function thistype.onCast))
      call thistype.onInitEx()
    endmethod
  endstruct

endlibrary
 

Attachments

  • NewTestMap.w3x
    89 KB · Views: 38
Level 14
Joined
Jul 19, 2007
Messages
772
Map is attached. I used Knockback3D, which has a weird bug with the dust fx model I just posted about here, so if you don't like how that looks you can just change the model. The caster units in this map don't have any intelligence and aren't heroes so the spell can't be leveled up but it will do the requested int scaling damage and scale as requested. I didn't write a tooltip, you can do that yourself. If you want sounds you'll have to do that yourself too or give me something to go on and I'll put some sound in. Choose fx as you so desire.

There is 1 trigger, 3 dependency libraries, 2 abilities, 1 buff, and 1 dummy caster. Copy them all. The map also has a couple other spells I was working on in it, just ignore them unless you like them.
JASS:
library Windblast requires Knockback3D
  globals
    public  integer SPELL_ID   = 'Wbst'
    public  integer DUMMY_ID   = 'dmmy'
    private real    TIMER_TICK = 0.03
    private real MAX_COLLISION = 128.
  
    private real VELOCITY = 900.
    private real DISTANCE = 600.
  
    private real KB_VEL = 500.
    private real KB_ALPHA = 0. //elevation angle to knock back at

    private real    EFFECT_H   = 10.
    private string  EFFECT_1A  = "Doodads\\Cinematic\\GlowingRunes\\GlowingRunes3.mdl"

    private real    E1A_SCALE  = 2.2
    private real    E1A_TSCALE = .1
    private real    E1A_TSKIP  = 1.0 //skip this far ahead in the stand animation

    private integer E1A_COL_R  = 255
    private integer E1A_COL_G  = 255
    private integer E1A_COL_B  = 255

  endglobals
 
  native UnitAlive takes unit u returns boolean
 
  globals
    public  timer   SpellTimer  = CreateTimer()
    public  trigger CastTrigger = CreateTrigger()

    public  keyword SpellData
    private SpellData array GSD
    private integer COUNT = 0
    private integer CURR = 0
  endglobals



  private module spellBehavior
    // these are defined in the struct below for you to use:
    // they do not need to be destroyed or removed, that is handled already
    //
    // static group SEARCH_GROUP  <-- a group you can use to search for targets via GroupEnumUnitsIn...
    // unit DUMMY                 <-- can cast instantly and is moved to (.cx, .cy) every timer step
    //                                add abilities to it with .prepDummy(abilCode) (level is set automatically)
    // group HIT_GROUP            <-- units already hit by this instance of the spell, you are free to clear this as necessary, but you must add units manually
    // integer l                  <-- level of the spell when it was cast
    // unit c                     <-- caster
    // player p                   <-- owner of caster
    // real cx, cy, cz            <-- coordinates of the center of the spell
    // you may end an instance early by calling .endInstance()
    // you may add your own instance or static members here:

    static integer SECONDARY_ID   = 'Wbls'
    static string SECONDARY_ORDER = "slow"
    static real RADIUS = 100.
  
    method dmgIntFactor takes nothing returns real
      return this.l * 1.0
    endmethod

    static method onInitEx takes nothing returns nothing
      //called on map init, so you can set up anything you need to here (most likely arrays or some global dummies)
    endmethod

    method onStart takes nothing returns nothing
      //called when the spell is cast
      call this.prepDummy(SECONDARY_ID)
    endmethod

    method onPeriodic takes nothing returns nothing
      local unit u
      local real x
      local real y
      local real a
      local real dmg = this.dmgIntFactor()*GetHeroInt(this.c, true)

      call GroupEnumUnitsInRange(SEARCH_GROUP, this.cx, this.cy, RADIUS+MAX_COLLISION, null)
      loop
        set u = FirstOfGroup(SEARCH_GROUP)
        exitwhen u == null
        call GroupRemoveUnit(SEARCH_GROUP, u)

        if IsUnitInRangeXY(u, this.cx, this.cy, RADIUS) and /*
        */ IsUnitEnemy(u, this.p) and UnitAlive(u)      and /*
        */ not IsUnitType(u, UNIT_TYPE_MAGIC_IMMUNE)    and /*
        */ not BlzIsUnitInvulnerable(u)                 and /*
        */ not IsUnitInGroup(u, this.HIT_GROUP)         then
      
          set x = GetUnitX(u)
          set y = GetUnitY(u)
          set a = Atan2(y-this.cx, x-this.cx)
          if a > 0. then
            set a = this.kbaL
          else
            set a = this.kbaR
          endif

          call UnitDamageTarget(this.c, u, dmg, false, false, ATTACK_TYPE_NORMAL, DAMAGE_TYPE_FORCE, WEAPON_TYPE_WHOKNOWS)
          call Knockback3D.add(u, KB_VEL, a, KB_ALPHA)
          call IssueTargetOrder(this.DUMMY, SECONDARY_ORDER, u)
          call GroupAddUnit(this.HIT_GROUP, u)
        endif
      endloop

      set u = null
    endmethod

    method onEnd takes nothing returns nothing
      //called when the spell instance ends
    endmethod
  endmodule



  public struct SpellData
    static group SEARCH_GROUP = CreateGroup()
    group HIT_GROUP = null
    unit DUMMY = null

    unit c    = null
    player p  = null
    integer l = 0
    integer d = 0
    effect e = null
    real cx = 0.
    real cy = 0.
    real dx = 0.
    real dy = 0.
    real kbaL = 0.
    real kbaR = 0.


    method prepDummy takes integer abil returns nothing
      call UnitAddAbility(this.DUMMY, abil)
      call SetUnitAbilityLevel(this.DUMMY, abil, this.l)
    endmethod

    implement spellBehavior

    method endInstance takes nothing returns nothing
      call this.onEnd()

      call RemoveUnit(this.DUMMY)
      call DestroyGroup(this.HIT_GROUP)
      //call DestroyEffect(this.e)
      call RemoveEffect(this.e)

      set GSD[CURR] = GSD[COUNT-1]
      set COUNT = COUNT-1
      set CURR = CURR-1

      if COUNT == 0 then
        call PauseTimer(SpellTimer)
      endif

      call this.destroy()
    endmethod

    static method periodic takes nothing returns nothing
      local SpellData sd

      set CURR = 0
      loop
        exitwhen CURR >= COUNT
        set sd = GSD[CURR]

        set sd.cx = sd.cx + sd.dx
        set sd.cy = sd.cy + sd.dy
        call BlzSetSpecialEffectX(sd.e, sd.cx)
        call BlzSetSpecialEffectY(sd.e, sd.cy)
        call BlzSetSpecialEffectHeight(sd.e, EFFECT_H)
        call SetUnitX(sd.DUMMY, sd.cx)
        call SetUnitY(sd.DUMMY, sd.cy)
        call sd.onPeriodic()

        set sd.d = sd.d-1
        if sd.d <= 0 then
          call sd.endInstance()
        endif
      
        set CURR = CURR+1
      endloop
    endmethod

    static method create takes unit c, real x, real y returns thistype
      local thistype sd = thistype.allocate()
      local real a
    
      set sd.c = c
      set sd.p = GetOwningPlayer(sd.c)
      set sd.l = GetUnitAbilityLevel(sd.c, SPELL_ID)
      set sd.cx = GetUnitX(sd.c)
      set sd.cy = GetUnitY(sd.c)
      set a = Atan2(y-sd.cy, x-sd.cx)
      set sd.kbaL = a + bj_PI/2.
      set sd.kbaR = a - bj_PI/2.
      set sd.dx = VELOCITY*Cos(a)*TIMER_TICK
      set sd.dy = VELOCITY*Sin(a)*TIMER_TICK
      set sd.d = R2I(DISTANCE/VELOCITY/TIMER_TICK)

      set sd.e = AddSpecialEffect(EFFECT_1A, sd.cx, sd.cy)
      call BlzSetSpecialEffectColor(sd.e, E1A_COL_R, E1A_COL_G, E1A_COL_B)
      call BlzSetSpecialEffectHeight(sd.e, EFFECT_H)
      call BlzSetSpecialEffectScale(sd.e, E1A_SCALE)
      call BlzSetSpecialEffectTimeScale(sd.e, E1A_TSCALE)
      call BlzSetSpecialEffectTime(sd.e, E1A_TSKIP)
    
      set sd.DUMMY = CreateUnit(sd.p, DUMMY_ID, sd.cx, sd.cy, 0.)
      set sd.HIT_GROUP = CreateGroup()

      if COUNT == 0 then
        call TimerStart(SpellTimer, TIMER_TICK, true, function thistype.periodic)
      endif
      set GSD[COUNT] = sd
      set COUNT = COUNT+1

      return sd
    endmethod

    static method onCast takes nothing returns boolean
      if GetSpellAbilityId() == SPELL_ID then
        call SpellData.create(GetTriggerUnit(), GetSpellTargetX(), GetSpellTargetY())
        call GSD[COUNT-1].onStart()
      endif
      return false
    endmethod

    static method onInit takes nothing returns nothing
      call TriggerRegisterAnyUnitEventBJ(CastTrigger, EVENT_PLAYER_UNIT_SPELL_EFFECT)
      call TriggerAddCondition(CastTrigger, Condition(function thistype.onCast))
      call thistype.onInitEx()
    endmethod
  endstruct

endlibrary
Sry but that's not really how I would like the spell to be like... and I prefer it to NOT be created by JASS-triggers because I'm totally lost at it and almost afraid of JASS-triggerings... You don't know the standard Shockwave ability? I would like it to be like that but with tornado-missile instead and knock all enemies that it hits aside from the tornado with the distance of 250 and damage them and then slowing their movement speed for the short duration.
 
Level 39
Joined
Feb 27, 2007
Messages
5,010
The spell literally does everything that you asked. Just change the model of the effect in the configuration. It deals damage, the caster I used just doesn't have any intelligence. It knocks back and it slows them with a dummy slow ability.

If you have very specific requests then you need to include that information in your request post. You did not specify that you are afraid of JASS. You didn't specify which models should be used where. You didn't specify how far to knock back, how long the slow should last, or the radius of the effect, so I chose values for these things myself. Read the spell. Read the configuration. Mess with some of the config variables. If something doesn't work the way you wanted it to, I can suggest how to change it. If you have questions, ask them, but don't refuse someone's hard work because it doesn't conform to a standard you didn't lay out when you asked for it.
 

Uncle

Warcraft Moderator
Level 64
Joined
Aug 10, 2018
Messages
6,537
You don't have to understand JASS to know how to use his spell. Here's what you have to configure:

Code:
library Windblast requires Knockback3D
  globals
    public  integer SPELL_ID   = 'Wbst'
    public  integer DUMMY_ID   = 'dmmy'
    private real    TIMER_TICK = 0.03
    private real MAX_COLLISION = 128. //This ISN'T the Area of Effect
 
    private real VELOCITY = 900. //The speed of the tornado missile
    private real DISTANCE = 600. //The distance the tornado travels
 
    private real KB_VEL = 1000. //The speed of the knockback effect
    private real KB_ALPHA = 0. //elevation angle to knock back at

    private real    EFFECT_H   = 10. //I assume this is the height of the effect
    private string  EFFECT_1A  = "Abilities\\Spells\\Other\\Tornado\\TornadoElementalSmall.mdl" //The model of the effect. Make sure to use \\ instead of a single \ between the words like how I have it set

    private real    E1A_SCALE  = 0.8 //The size of the tornado missile
    private real    E1A_TSCALE = .1
    private real    E1A_TSKIP  = 1.0 //skip this far ahead in the stand animation

    private integer E1A_COL_R  = 255 //The colors of the tornado missile. These are the default values
    private integer E1A_COL_G  = 255
    private integer E1A_COL_B  = 255

  endglobals
So when you click on the Trigger "Windblast", you'll see that he put all of the configurable variables at the top. Simply modify these to match the values that you desire. The names are pretty self-explanatory. If you look at the code I posted I already edited the fields to probably match what you want, I also added some comments like "//The speed of the tornado missile" you don't need to copy that it's not part of the code.

@Pyrogasm
One thing I did notice was that the knockback seems a little funky. Maybe it's intended this way, but it seems to shove units to the side instead of knocking them back based on their angle between the unit and the tornado.
 
Last edited:
Level 39
Joined
Feb 27, 2007
Messages
5,010
//The Area of Effect of the tornado missile
Actually, no, and I should have annotated that specific variable in particular. That's actually the max collision size of a unit in the map, used for range checking with IsUnitInRangeXY. The impact radius is configured in the spellBehavior module below.

One thing I did notice was that the knockback seems a little funky. Maybe it's intended this way, but it seems to shove units to the side instead of knocking them back based on their angle between the unit and the tornado.
I specifically wrote it this way. Otherwise, units mostly along the line of the spell would be knocked back along the spell direction rather than away from it.
 
Status
Not open for further replies.
Top