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

Chilling Gale v 1.0.3

This bundle is marked as useful / simple. Simplicity is bliss, low effort and/or may contain minor bugs.
Description
Creates a Tornado at the target location. Every enemy within its range is pulled into the center. The Tornado lasts 20 seconds but it takes time to build up. Maximum of 20/30/40 dps.


Credits and Systems
T32 by Jesus4Lyf
TerrainPathability by RisingDusk

code
JASS:
library ChillingGale requires T32, TerrainPathability
    // v 1.0.3 by 3yeballz
    
    // Timer32 by Jesus4Lyf             | http://www.thehelper.net/forums/showthread.php/132538
    // TerrainPathability by RisingDusk | http://www.wc3c.net/showthread.php?t=103862

            /////////////////////////////////////
            /*        User Configuration       */
            /////////////////////////////////////
    globals
        // rawcode of spell
        private constant integer SPELLID      = 'A000'
        
        // rawcode of tornado dummy unit
        private constant integer TORNADO      = 'h000'
        
        // number of callbacks to build up
        private constant integer BUILDUP      = 340
        
        // number of callbacks until end
        private constant integer MAXCOUNT     = 640
        
        // initial rotation speed of the tornado
        private constant real    INITSPEED    = 3 * bj_DEGTORAD
        
        // acceleration of the rotation speed
        private constant real    ACCELERATION = 0.94 // must be lower than 1
        private constant real    ACCEL        = 0.15 * bj_DEGTORAD
        
        // amount of damage dealt. damage is equal to rotation speed multiplied by this constant
        private constant real    DMGFACTOR    = 3.4
        
        // pull strength of the tornado
        private constant real    MOVEFACTOR   = 40
        
        // number of callbacks until more dummy units spawn
        private constant integer PHASE1       = 50
        private constant integer PHASE2       = 120
        
        // distance factor for additionally spawning dummy units
        private constant real    DISTFACTOR1  = 0.6
        private constant real    DISTFACTOR2  = 0.3
        
        // radius offset that it looks more realistic
        private constant real    OFFSET       = 40.
        
        // pull direction. must be between 90 and 180
        private constant real    DEGREE       = 110. * bj_DEGTORAD
        
        private constant attacktype atktype = ATTACK_TYPE_NORMAL
        private constant damagetype dmgtype = DAMAGE_TYPE_COLD
        private constant weapontype wpntype = null
    endglobals
    
    private function Damage takes real r, integer level returns real
        return r + r * level
    endfunction
            /////////////////////////////////////
            /*    End of User Configuration    */
            /////////////////////////////////////
    
    private module ChillingGaleInit
        private static method onInit takes nothing returns nothing
            local trigger t = CreateTrigger()
            call TriggerRegisterAnyUnitEventBJ(t, EVENT_PLAYER_UNIT_SPELL_EFFECT)
            call TriggerAddCondition(t, Filter(function thistype.SpellCast))
            set filter = Filter(function thistype.SpellEffect)
        endmethod
    endmodule

    private struct Tornado
        real x
        real y
        real facing
        unit t
        
        static method create takes real setx, real sety, real degree returns thistype
            local thistype this = thistype.allocate()
            set this.x = setx
            set this.y = sety
            set this.facing = degree
            set this.t = CreateUnit(Player(15), TORNADO, this.x, this.y, 0.)
            return this
        endmethod
        
        method destroy takes nothing returns nothing
            call RemoveUnit(this.t)
            call this.deallocate()
        endmethod
    endstruct

    private struct ChillingGale
        private static thistype temp
        private static boolexpr filter
        private static group g = CreateGroup()
        
        private unit caster
        private player owner
        private real midx
        private real midy
        private integer count
        private integer level
        private real speed
        private real dist
        private real damage
        private Tornado array tornado [12]

        private static method SpellEffect takes nothing returns boolean
            local unit u = GetFilterUnit()
            local real facing
            local real locx
            local real locy
            local real dist
            local real x
            local real y
            if not (IsUnitEnemy(u, temp.owner) and GetWidgetLife(u) > 0.405 and GetUnitTypeId(u) != 0) then
                set u = null
                return false
            endif
            set locx = GetUnitX(u)
            set locy = GetUnitY(u)
            set x = locx - temp.midx
            set y = locy - temp.midy
            set dist = SquareRoot(x*x+y*y)
            call UnitDamageTarget(temp.caster, u, temp.damage * (temp.dist - dist) / temp.dist, false, false, atktype, dmgtype, wpntype)
            set facing = Atan2(locy - temp.midy, locx - temp.midx) + DEGREE
            call IsTerrainWalkable(locx + temp.speed * Cos(facing) * MOVEFACTOR, locy + temp.speed * Sin(facing) * MOVEFACTOR)
            call SetUnitX(u, TerrainPathability_X)
            call SetUnitY(u, TerrainPathability_Y)
            set u = null
            return false
        endmethod
        
        private method periodic takes nothing returns nothing
            local integer i = 0
            set this.count = this.count + 1
            if this.count < BUILDUP then
                set this.dist = this.dist + 1. + Pow(0.994, this.count) * 1.2
                set this.speed = this.speed + Pow(ACCELERATION, this.count) * ACCEL
                set this.damage = Damage(this.speed, this.level) * DMGFACTOR
                loop
                    exitwhen i == 4
                    set this.tornado[i].facing = this.tornado[i].facing + this.speed
                    set this.tornado[i].x = this.midx + Cos(this.tornado[i].facing) * this.dist
                    set this.tornado[i].y = this.midy + Sin(this.tornado[i].facing) * this.dist
                    call SetUnitX(this.tornado[i].t, this.tornado[i].x)
                    call SetUnitY(this.tornado[i].t, this.tornado[i].y)
                    set i = i + 1
                endloop
                if this.count > PHASE1 then
                    loop
                        exitwhen i == 8
                        set this.tornado[i].facing = this.tornado[i].facing + this.speed
                        set this.tornado[i].x = this.midx + Cos(this.tornado[i].facing) * this.dist * DISTFACTOR1
                        set this.tornado[i].y = this.midy + Sin(this.tornado[i].facing) * this.dist * DISTFACTOR1
                        call SetUnitX(this.tornado[i].t, this.tornado[i].x)
                        call SetUnitY(this.tornado[i].t, this.tornado[i].y)
                        set i = i + 1
                    endloop
                    if this.count > PHASE2 then
                        loop
                            exitwhen i == 12
                            set this.tornado[i].facing = this.tornado[i].facing + this.speed
                            set this.tornado[i].x = this.midx + Cos(this.tornado[i].facing) * this.dist * DISTFACTOR2
                            set this.tornado[i].y = this.midy + Sin(this.tornado[i].facing) * this.dist * DISTFACTOR2
                            call SetUnitX(this.tornado[i].t, this.tornado[i].x)
                            call SetUnitY(this.tornado[i].t, this.tornado[i].y)
                            set i = i + 1
                        endloop
                    elseif this.count == PHASE2 then
                        loop
                            exitwhen i == 12
                            set this.tornado[i] = Tornado.create(this.midx+Cos(this.tornado[i-8].facing)*this.dist*DISTFACTOR2, this.midy+Sin(this.tornado[i-8].facing)*this.dist*DISTFACTOR2, this.tornado[i-8].facing)
                            set i = i + 1
                        endloop
                    endif
                elseif this.count == PHASE1 then
                    loop
                        exitwhen i == 8
                        set this.tornado[i] = Tornado.create(this.midx+Cos(this.tornado[i-4].facing+bj_PI/4)*this.dist*DISTFACTOR1, this.midy+Sin(this.tornado[i-4].facing+bj_PI/4)*this.dist*DISTFACTOR1, this.tornado[i-4].facing+bj_PI/4)
                        set i = i + 1
                    endloop
                endif
            else
                loop
                    exitwhen i == 4
                    set this.tornado[i].facing = this.tornado[i].facing + this.speed
                    set this.tornado[i].x = this.midx + Cos(this.tornado[i].facing) * this.dist
                    set this.tornado[i].y = this.midy + Sin(this.tornado[i].facing) * this.dist
                    call SetUnitX(this.tornado[i].t, this.tornado[i].x)
                    call SetUnitY(this.tornado[i].t, this.tornado[i].y)
                    set i = i + 1
                endloop
                loop
                    exitwhen i == 8
                    set this.tornado[i].facing = this.tornado[i].facing + this.speed
                    set this.tornado[i].x = this.midx + Cos(this.tornado[i].facing) * this.dist * DISTFACTOR1
                    set this.tornado[i].y = this.midy + Sin(this.tornado[i].facing) * this.dist * DISTFACTOR1
                    call SetUnitX(this.tornado[i].t, this.tornado[i].x)
                    call SetUnitY(this.tornado[i].t, this.tornado[i].y)
                    set i = i + 1
                endloop
                loop
                    exitwhen i == 12
                    set this.tornado[i].facing = this.tornado[i].facing + this.speed
                    set this.tornado[i].x = this.midx + Cos(this.tornado[i].facing) * this.dist * DISTFACTOR2
                    set this.tornado[i].y = this.midy + Sin(this.tornado[i].facing) * this.dist * DISTFACTOR2
                    call SetUnitX(this.tornado[i].t, this.tornado[i].x)
                    call SetUnitY(this.tornado[i].t, this.tornado[i].y)
                    set i = i + 1
                endloop
            endif
            set temp = this
            call GroupEnumUnitsInRange(g, this.midx, this.midy, this.dist + OFFSET, filter)
            if this.count == MAXCOUNT then
                call this.stopPeriodic()
                call this.destroy()
            endif
        endmethod
        
        implement T32x
        
        private static method create takes nothing returns thistype
            local thistype this = thistype.allocate()
            local integer i = 0
            set this.caster = GetTriggerUnit()
            set this.owner = GetOwningPlayer(this.caster)
            set this.midx = GetSpellTargetX()
            set this.midy = GetSpellTargetY()
            set this.count = 0
            set this.level = GetUnitAbilityLevel(this.caster, SPELLID)
            set this.speed = INITSPEED
            set this.dist = 30.
            loop
                exitwhen i == 4
                set this.tornado[i] = Tornado.create(this.midx+Cos(i*bj_PI/2)*this.dist, this.midy+Sin(i*bj_PI/2), i*bj_PI/2)
                set i = i + 1
            endloop
            return this
        endmethod
        
        private method destroy takes nothing returns nothing
            local integer i = 0
            loop
                exitwhen i == 12
                call this.tornado[i].destroy()
                set i = i + 1
            endloop
            call this.deallocate()
        endmethod
        
        private static method SpellCast takes nothing returns boolean
            if GetSpellAbilityId() == SPELLID then
                call thistype.create().startPeriodic()
            endif
            return false
        endmethod
        
        implement ChillingGaleInit
    endstruct

endlibrary
Please give Credits if you use this Spell in your map

Please comment and rate :)

Keywords:
chilling, gale, wind, tornado, vjass
Contents

Just another Warcraft III map (Map)

Reviews
12th Dec 2015 IcemanBo: Too long as NeedsFix. Rejected. 11 Nov 2011 Bribe: You can delete the weapontype constant and just use "null". Use a FirstOfGroup loop with a null filter here. It is loads better than what you currently have...

Moderator

M

Moderator

12th Dec 2015
IcemanBo: Too long as NeedsFix. Rejected.


11 Nov 2011
Bribe: You can delete the weapontype constant and just use "null".

Use a FirstOfGroup loop with a null filter here. It is loads better than what you currently have.

"private static group g" should be set to bj_lastCreatedGroup so that it doesn't create an unnecessary handle.

Unit life check is not safe to detect death, it requires IsUnitType, UNIT_TYPE_DEAD. Also since you can't enumerate removed units you don't need the "GetUnitTypeId" check.

"this.tornado" should be stuck into a local variable before repeat-referencing it.

Things like the "number of tornados" should be configurable. I see a lot of areas in your script which could be configured if you let them be.
 
Level 14
Joined
Nov 18, 2007
Messages
1,084
Code Review:
  • Although it's rather cool that pretty much any other spell can trigger this spell, it's not a good idea. Make a condition in SpellCast to check that the ability being cast is equal to the SPELLID.
  • In Spell Effect, if the unit u fails to meet the conditions, the variable doesn't get nulled. It should be like this:
    JASS:
                if not (IsUnitEnemy(u, temp.owner) and GetWidgetLife(u) > 0.405) then
                   set u = null
                    return false
               endif
  • Some of your struct members in ChillingGale could have initial values.
  • I don't think there's any point for Tornado to have a setowner in the create method if it's always going to be the neutral player (Player 15).
  • You should have more comments detailing what each value does in the user configuration.
 
Level 4
Joined
Feb 28, 2011
Messages
37
*updated*

thanks for your replys and ratings :)

I just dislike how abruptly it disappears. You could make it fade away.
Good idea. Maybe the next version can do that.

Although it's rather cool that pretty much any other spell can trigger this spell, it's not a good idea. Make a condition in SpellCast to check that the ability being cast is equal to the SPELLID.
Sry I forgot that :ugly:

I don't think there's any point for Tornado to have a setowner in the create method if it's always going to be the neutral player (Player 15).
There is one. It should just damage enemys of the owning player.

Which members should have initial values? There are already some.
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
GetWidgetLife(u) > 0.405 or GetUnitTypeId(u) != 0 will fail if the unit's life was raised after it died.

You need to replace GetWidgetLife(u) > 0.405 with IsUnitType(u, UNIT_TYPE_DEAD). The GetUnitTypeId(u) != 0 check is important if the unit was removed (because a removed unit has no unit types).

Also, spell damage should have "null" as a weapontype instead. Weapontypes produce impact-sound effects, like a footman slashing a peon... "null" is the same as WEAPON_TYPE_WHOKNOWS, but shorter to type and doesn't suggest any configuration.
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
Overall is fantastic. But some things to note nonetheless.

  • local handles, even when never destroyed, will keep RAM slightly down to null them.
  • I still think you should be using "null" instead of "wpntype" because it achieves the same effect.
  • set this.t = CreateUnit(Player(15), TORNADO, this.x, this.y, 0.)
    - Why don't you set the facing angle to degrees as well?
  • GetWidgetLife(u) > 0.405 and GetUnitTypeId(u) != 0
    - You should use not IsUnitType(u, UNIT_TYPE_DEAD) instead of GetWidgetLife because the life could have been modified after death and before removal.
  • I would restructure one of your blocks to make it save a couple of local variables, making it more efficient:
    JASS:
                call IsTerrainWalkable(locx + temp.speed * Cos(facing) * MOVEFACTOR, locy + temp.speed * Sin(facing) * MOVEFACTOR)
                call SetUnitX(u, TerrainPathability_X)
                call SetUnitY(u, TerrainPathability_Y)
  • A lot of lookups of "this.tornado". I suggest storing it as a singular variable before repeat-referencing in order to speed the process up.
 
Top