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

Stormcyclone [1.0]

This bundle is marked as useful / simple. Simplicity is bliss, low effort and/or may contain minor bugs.
  • Like
Reactions: Geries
Moin moin =)

Well it's me again and I just made spell and I want share it with you and hope for some good feedback.

Spell:
The Druid uses the mighty source of nature to create a storm, which slows enemy units in it.
In addition, small cyclones will spin around the druid, which damage nearby enemies units.

JASS:
library Stormcyclone initializer init

    globals
        private constant integer    DUMMY_ID                = 'dumx'
        private constant integer    DUMMY_CYCLONE_ID        = 'dum1'    

        private constant integer    SPELL_SKILL_ID          = 'A000'
        private constant integer    SPELL_DUMMY_ID          = 'A001'
        private constant integer    SPELL_DUMMY_BUFF        = 'B000'
        private constant integer    SPELL_SKILL_ID_ORDER    = 852183
        private constant string     SPELL_DUMMY_ID_ORDER    = "slow"
        
        private constant attacktype A_TYPE                  = ATTACK_TYPE_NORMAL
        private constant damagetype D_TYPE                  = DAMAGE_TYPE_NORMAL
        private constant weapontype W_TYPE                  = WEAPON_TYPE_WHOKNOWS
        
        private constant real       STORM_AREA              = 800.
        private constant string     STORM_EDGE_SFX          = "Abilities\\Spells\\NightElf\\FaerieFire\\FaerieFireTarget.mdl"
        private constant real       STORM_EDGE_SFX_AMOUNT   = 36
        
        private constant integer    CYCLONE_WAVES_AMOUNT    = 3
        private constant integer    CYCLONE_PER_WAVES       = 4
        private constant real       CYCLONE_DAMAGE_AREA     = 150.
        private constant real       CYCLONE_DAMAGE_INTERVAL = 1.
        private constant real       CYCLONE_MOVESPEED       = 20.
        
        // Don't change \\
        
        private constant real       LOOP_TIME   = 0.03125
        private constant hashtable  HASH        = InitHashtable()
        private          integer    TEMP
    endglobals
    
    private function damage takes integer level returns real
        return 75. * level
    endfunction
    
    struct Stormcyclone
        unit    caster  =   null
        player  owner   =   null
        
        real    dmg     =   0.
        
        group   cgrp    =   CreateGroup()
        group   sgrp    =   CreateGroup()
        group   dgrp    =   CreateGroup()
        
        effect array efc[1]

        // == Slow struct == \\
        
        static method slow_filter takes nothing returns boolean
            local thistype this = TEMP
            local unit u = GetFilterUnit()
            
            if GetWidgetLife(u) > 0.405 and IsUnitEnemy(u,.owner) and IsUnitType(u,UNIT_TYPE_STRUCTURE) == false and IsUnitType(u,UNIT_TYPE_MAGIC_IMMUNE) == false and IsUnitInGroup(u,.sgrp) == false then
                call GroupAddUnit(.sgrp,u)
            endif
            
            set u = null
            return false
        endmethod
        
        static method slow_action takes nothing returns nothing
            local thistype this = TEMP
            local unit u1 = GetEnumUnit()
            local unit u2
            local real x = GetUnitX(u1) - GetUnitX(.caster)
            local real y = GetUnitY(u1) - GetUnitY(.caster)
            
            if GetWidgetLife(u1) < 0.405 then
                call GroupRemoveUnit(.sgrp,u1)
            endif
            
            if GetUnitCurrentOrder(.caster) == SPELL_SKILL_ID_ORDER then
                if SquareRoot(x * x + y * y) <= STORM_AREA then
                    if GetUnitAbilityLevel(u1,SPELL_DUMMY_BUFF) == 0 then
                        set u2 = CreateUnit(.owner,DUMMY_ID,GetUnitX(u1),GetUnitY(u1),0.)
                        call UnitApplyTimedLife(u2,'BTFL',1.)
                        call UnitAddAbility(u2,SPELL_DUMMY_ID)
                        call SetUnitAbilityLevel(u2,SPELL_DUMMY_ID,GetUnitAbilityLevel(.caster,SPELL_SKILL_ID))
                        call IssueTargetOrder(u2,SPELL_DUMMY_ID_ORDER,u1)
                        set u2 = null
                    endif
                else
                    call UnitRemoveAbility(u1,SPELL_DUMMY_BUFF)
                    call GroupRemoveUnit(.sgrp,u1)
                endif
            else
                call UnitRemoveAbility(u1,SPELL_DUMMY_BUFF)
                call GroupRemoveUnit(.sgrp,u1)
            endif
            
            set u1 = null
        endmethod

        // == Damage struct == \\
        
        static method cyclone_damage_remove takes nothing returns nothing
            local thistype this = TEMP
            local timer t = GetExpiredTimer()

            call GroupRemoveUnit(.dgrp,LoadUnitHandle(HASH,GetHandleId(t),1))
            call DestroyTimer(t)
            set t = null
        endmethod

        static method cyclone_damage_add takes nothing returns boolean
            local thistype this = TEMP
            local unit u = GetFilterUnit()
            local timer t = CreateTimer()
            
            if GetWidgetLife(u) > 0.405 and IsUnitEnemy(u,.owner) and IsUnitType(u,UNIT_TYPE_STRUCTURE) == false and IsUnitType(u,UNIT_TYPE_MAGIC_IMMUNE) == false and IsUnitInGroup(u,.dgrp) == false then
                call UnitDamageTarget(.caster,u,.dmg,true,false,A_TYPE,D_TYPE,W_TYPE)
                call GroupAddUnit(.dgrp,u)
                call SaveUnitHandle(HASH,GetHandleId(t),1,u)
                call TimerStart(t,CYCLONE_DAMAGE_INTERVAL,false,function thistype.cyclone_damage_remove)
            endif
            
            set u = null
            set t = null
            return false
        endmethod
        
        // == Base struct == \\
        
        static method cyclone_move takes nothing returns nothing
            local thistype this = TEMP
            local group g = CreateGroup()
            local unit u = GetEnumUnit()
            local integer v = GetUnitUserData(u)
            local real x = GetUnitX(u)
            local real y = GetUnitY(u)
            local real f = GetWidgetLife(u) -1.
            
            call SetWidgetLife(u, f + ((CYCLONE_MOVESPEED * 180.) / (bj_PI * v)) +1.)
            set f = f * bj_DEGTORAD
            set x = x + v *(Cos(f) - Cos(f - (CYCLONE_MOVESPEED / v)))
            set y = y + v *(Sin(f) - Sin(f - (CYCLONE_MOVESPEED / v)))
            call SetUnitX(u,x)
            call SetUnitY(u,y)
            call GroupEnumUnitsInRange(g,GetUnitX(u),GetUnitY(u),CYCLONE_DAMAGE_AREA,Condition(function thistype.cyclone_damage_add))
            
            call DestroyGroup(g)
            set g = null
            set u = null
        endmethod

        static method periodic takes nothing returns nothing
            local timer t = GetExpiredTimer()
            local thistype this = LoadInteger(HASH,GetHandleId(t),0)
            local group g = CreateGroup()
            local unit u
            local integer i = 0
            
            if GetUnitCurrentOrder(.caster) == SPELL_SKILL_ID_ORDER then
                call GroupEnumUnitsInRange(g,GetUnitX(.caster),GetUnitY(.caster),STORM_AREA,Condition(function thistype.slow_filter))
                call ForGroup(.cgrp,function thistype.cyclone_move)
                call ForGroup(.sgrp,function thistype.slow_action)
            else
                call ForGroup(.sgrp,function thistype.slow_action)
                loop
                    exitwhen i == STORM_EDGE_SFX_AMOUNT
                    call DestroyEffect(.efc[i])
                    set i = i + 1
                endloop
                loop
                    set u = FirstOfGroup(.cgrp)
                    exitwhen u == null
                    call RemoveUnit(u)
                    call GroupRemoveUnit(.cgrp,u)
                endloop
                call PauseTimer(t)
                call DestroyTimer(t)
                set t = null
                call DestroyGroup(.cgrp)
                call DestroyGroup(.sgrp)
                call .destroy()
            endif
            
            call DestroyGroup(g)
            set g = null
            set t = null
        endmethod

        static method create takes unit caster returns thistype
            local thistype this = .allocate()
            local timer t = CreateTimer()
            local unit u
            local integer a = 0
            local integer b = 0
            local real x1 = 0.
            local real y1 = 0.
            local real x2 = 0.
            local real y2 = 0.
            
            set .caster = caster
            set .dmg    = damage(GetUnitAbilityLevel(.caster,SPELL_SKILL_ID))
            set .owner  = GetOwningPlayer(caster)
            set TEMP = this

            loop
                exitwhen a == STORM_EDGE_SFX_AMOUNT
                set x1 = GetUnitX(.caster) + STORM_AREA * Cos(360 / STORM_EDGE_SFX_AMOUNT * bj_DEGTORAD * a)
                set y1 = GetUnitY(.caster) + STORM_AREA * Sin(360 / STORM_EDGE_SFX_AMOUNT * bj_DEGTORAD * a)
                set .efc[a] = AddSpecialEffect(STORM_EDGE_SFX,x1,y1)
                set a = a + 1
            endloop
            set a = 0

            loop
                exitwhen a == CYCLONE_WAVES_AMOUNT
                set x1 = STORM_AREA / CYCLONE_WAVES_AMOUNT * a + 120.
                set b = 0
                loop
                    exitwhen b == CYCLONE_PER_WAVES
                    set y1 = 360. / CYCLONE_PER_WAVES * b
                    set x2 = GetUnitX(.caster) + x1 * Cos(y1 * bj_DEGTORAD)
                    set y2 = GetUnitY(.caster) + x1 * Sin(y1 * bj_DEGTORAD)
                    set u = CreateUnit(.owner,DUMMY_CYCLONE_ID,x2,y2,0.)
                    call SetWidgetLife(u,y1 + 1.)
                    call SetUnitUserData(u,R2I(x1))
                    call GroupAddUnit(.cgrp,u)
                    set b = b + 1
                endloop
                set a = a + 1
            endloop

            call SaveInteger(HASH,GetHandleId(t),0,this)
            call TimerStart(t,LOOP_TIME,true,function thistype.periodic)
            set t = null
            return this
        endmethod
    endstruct

    private function condition takes nothing returns boolean
        if GetSpellAbilityId() == SPELL_SKILL_ID then
            call Stormcyclone.create(GetTriggerUnit())
        endif
        return false
    endfunction

    private function init takes nothing returns nothing
        local trigger t = CreateTrigger()
        local integer i = 0
        
        loop
            exitwhen i == 16
            call TriggerRegisterPlayerUnitEvent(t,Player(i),EVENT_PLAYER_UNIT_SPELL_EFFECT,null)
            set i = i + 1
        endloop
        call TriggerAddCondition(t,function condition)
        call Preload(STORM_EDGE_SFX)
        
        set t = null
    endfunction
endlibrary

Imbalanced thanks and greetings to my friend "ap0calypse" - still one of the greatest coder and for helping me with vJass.

Greetings and Peace
Dr. Boom


Keywords:
Storm, Cyclone, Spin
Contents

Stormcyclone 1.0 (Map)

Reviews
12th Dec 2015 IcemanBo: Too long as NeedsFix. Rejected. 00:14, 18th Jul 2012 Magtheridon96: 3 groups per instance is just overkill :/ 1 group or a table of units would suffice. You could attach an integer to each unit so you can separate...

Moderator

M

Moderator

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

00:14, 18th Jul 2012
Magtheridon96:

  • 3 groups per instance is just overkill :/
    1 group or a table of units would suffice.
    You could attach an integer to each unit so you can separate them
    instead of having 3 groups.
  • Instead of using the old fashioned way of iterating through units
    in a group, we now use "First of group" loops.
    JASS:
    /*
    *   First we need a dummy group to use.
    *   Users have the option to use bj_lastCreatedGroup,
    *   but due to certain bugs involved with nesting 
    *   group enumerations, I would highly recommend
    *   that you do not.
    */
    globals
        private group enumGroup = CreateGroup()
    endglobals
    
    /*
    *   This function takes a unit and returns a boolean.
    *   It's used to filter out unwanted units in our
    *   group enumeration.
    */
    function filter takes unit filterUnit returns boolean
        return true
    endfunction
    
    /*
    *   This function takes the unit that we are iterating 
    *   over currently in our group enumeration and does 
    *   stuff to him. Really cool stuff if you ask me.
    */
    function actions takes unit enumUnit returns nothing
        call DoNothing()
    endfunction
    
    function Enumerate takes nothing returns nothing
        /*
        *   This is the unit we are going to use
        *   in our group enumeration loop.
        */
        local unit enumUnit
    
        /*
        *   These are just here so that the code
        *   would compile. They represent the 
        *   coordinates of the point that you want 
        *   to reference when selecting units in
        *   range. The range variable represents
        *   ... range.
        */
        local real x = 0
        local real y = 0
        local real range = 0
        
        /*
        *   This function will clear the group 
        *   and select all units in range of 
        *   point(x,y).
        */
        call GroupEnumUnitsInRange(enumGroup, x, y, range, null)
        
        /*
        *   This is where the actual iteration is done.
        */
        loop
            /*
            *   First we get a unit from the group.
            *   After that, we confirm that this unit is
            *   not null. Getting a null unit indicates 
            *   that a group is empty. Finally, we remove
            *   this unit from the group so that we don't
            *   iterate over him more than once.
            */
            set enumUnit = FirstOfGroup(enumGroup)
            exitwhen enumUnit == null
            call GroupRemoveUnit(enumGroup, enumUnit)
            
            /*
            *   Now that we have our unit, we attempt 
            *   to filter him out. If he passes the filter,
            *   actions will be executed for him.
            */
            if filter(enumUnit) then
                call actions(enumUnit)
            endif
        endloop
    endfunction
  • You don't need to inline TriggerRegisterAnyUnitEventBJ
  • When comparing the distances, you don't need to use the SquareRoot, you can just multiply the constant by itself and compare your expression with that. It's faster because SquareRoot is a pretty slow function.
  • Use TimerUtils instead of pausing and destroying the timers only if you want to.
  • Don't create a local group, then enumerate and destroy it, use a global group instead.
 
Level 14
Joined
Aug 8, 2010
Messages
1,022
The spell is good, i like it! I don't know vJass and i can't say if the code is good but the idea is. Perhaps it will be better if the cyclones appear am.... "smoothly", i think? They just come out of nowhere and disappear from nothing. You should make them kinda "fade in" and "fade out" using transparency. :]

Otherwise the spell is good! I give it 4/5. :)
 
Level 10
Joined
Aug 21, 2010
Messages
316
JASS:
call GroupEnumUnitsInRange(g,GetUnitX(.caster),GetUnitY(.caster),STORM_AREA,Condition(function thistype.slow_filter))
call GroupEnumUnitsInRange(g,GetUnitX(u),GetUnitY(u),CYCLONE_DAMAGE_AREA,Condition(function thistype.cyclone_damage_add))

This is better solution!
JASS:
call GroupEnumUnitsInRange(g,GetUnitX(.caster),GetUnitY(.caster),STORM_AREA,null)
call GroupEnumUnitsInRange(g,GetUnitX(u),GetUnitY(u),CYCLONE_DAMAGE_AREA,null)
 
Level 16
Joined
May 1, 2008
Messages
1,605
Moin moin =)

Thanks so far for the feedback you gave me:

@CoLd Bon3: Well I thought about this too but there is one problem with these cyclones. I only can make the cyclone itself transparent but the "dust" around it, stays at 100% transparency and you wouldn't notice a "fade in - fade out" that good =(

@zv27: In your way how should I execute the group then without any filter? I can use ForGroup(), but the direct filter is bitter. If there is any other thing I don't know, then please explain.

@Camper9514094: Sorry I don't understand what you mean, please describe!

Greetings and Peace
Dr. Boom
 
Level 10
Joined
Aug 21, 2010
Messages
316
oh f***ing jass again ! , is it possible to create this with gui?

Why fucking JASS?
Try to learn JASS
It's not that hard
Otherwise, this is vJASS, not JASS
Differences are evident.

Yes, it is.

edit
One more tip Dr. Boom, WEAPON_TYPE_WHOKNOWS is equal to null.

edit
Also, effect array efc[1] is redundant.
It's the same as having effect efc

true

IsUnitType(u,UNIT_TYPE_DEAD)is a better solution thanGetWidgetLife(u) < 0.405

This will never be destroyed call GroupEnumUnitsInRange(g,GetUnitX(u),GetUnitY(u),CYCLONE_DAMAGE_AREA,Condition(function thistype.cyclone_damage_add))
 
Last edited:
But if destroying timers can cause bugs, then it eventually will :/ (Murphy's Law)
Recycling timers is the safest method which is why it's recommended for public resources.

Personally, I don't use timer recycling in my maps because I never destroy triggers or use TriggerSleepAction, but users who import these spells might be doing so :/

I completely agree with you Nes, but in the Spells section, we have to try to save users from themselves often.
 
Level 31
Joined
Jul 10, 2007
Messages
6,306
I know, and I am too, but we have to do it for the sake of the guests that come to this site who happen to be about 11 times more numerous than the members :/

We don't have to do anything >.>.

Ok, let's stop derailing this thread. It's up to the author what solution they choose to use, so long as that solution is an acceptable one. Not using TimerUtils is an acceptable solution. You've no right to force them to use it. I sure as hell would never use it : |.
 
Level 16
Joined
May 1, 2008
Messages
1,605
Moin moin =)

Okay wasn't here a longer time now, but now I'm back and say this. Before I start the work again on this spell, I have some points to say:

1) Attaching integer to units. My first thought about it was using UnitUserData. But this would mess up since I would use it to non-dummy units. Other way I can think about is dealing with hashtables. Anyway I have no idea how to handle hashtables, actually I hate them but even if, is it really worth it?
- If there is any other good way, please tell me since I don't know any good =(

2) The SquareRoot thing: I don't know if my math skills are wrong but as far as I calculated, the result of "with SquareRoot without constant²" and "without SquareRoot with constant²" is different. Maybe SquareRoot is slow (we should define the word slow here anyway..), but it's more accurate.

3) You are kidding me right? In the past I used "First of group" actions for same situations and so many people flamed me for doing that, so I switched to the other way. Now you show up and say I should use it again?
Actually I don't have a problem with switching back, but then you should tell me why "First of Group" is now so much better as it was for like 2 years.

Okay these are the 3 points I really want to know, because I don't understand all and hope I'll learn something from it.

Greetings and Peace
Dr. Boom
 
Level 28
Joined
Jan 26, 2007
Messages
4,789
About 2) SquareRoot is not more accurate. In fact: not using it may actually be more accurate (though Warcraft can handle 8 digits behind the comma I believe, so a slight accuracy-advantage wouldn't matter that much anyway).

The standard way of doing things is something like this:
JASS:
if SquareRoot( x1*x1 + y1*y1 ) >= SquareRoot( x2*x2 + y2*y2 ) then
One thing you should notice is that "SquareRoot" can be cancelled out on both sides (a valid move).

So it becomes:
JASS:
if ( x1*x1 + y1*y1 ) >= ( x2*x2 + y2*y2 ) then
In your case, you use STORM_AREA instead of the second distance, so you could do STORM_AREA * STORM_AREA to get the same results.
This is done by multiplying the numbers by themselves (as you would to get rid of SquareRoot). This may only be done if the number itself is positive (and distances can't be negative).


Why would it increase accuracy? Because using SquareRoot will make the number smaller and may force it to round (though it won't matter much).
 
Level 16
Joined
May 1, 2008
Messages
1,605
Moin moin =)

Wondering what your example has to do with my calculation for the distance check. I would agree to your example, but since I don't check a SquareRoot result with another SquareRoot result, I still see it a bit different:

c65thp7f623hfz966.jpg


Edit: Even if I don't round up any value, its still more accurate as the non sqrt way.

Greetings and Peace
Dr. Boom
 
Level 28
Joined
Jan 26, 2007
Messages
4,789
Moin moin =)

Wondering what your example has to do with my calculation for the distance check. I would agree to your example, but since I don't check a SquareRoot result with another SquareRoot result, I still see it a bit different:

c65thp7f623hfz966.jpg


Edit: Even if I don't round up any value, its still more accurate as the non sqrt way.

Greetings and Peace
Dr. Boom
First of all: it makes the number smaller because that's just what square root does :p
I'm talking about 3 being smaller than 9 etc.
Making a number smaller also means that you have less digits to represent the number, unless you go far beyond the comma (which Warcraft cannot do).

That image shows exactly why SquareRoot is less accurate...

Without SquareRoot (original number), the number is 7351,276106 and it will not automatically round (because there is nothing to round in this case).
SquareRoot number: 85,7395830757 - warcraft cannot remember the last 2 digits of that number, so it will automatically round to 85,73958308 (thus losing a bit of accuracy there).


Now if you check 85,73958308² (the number which is rounded = 7351,276107. It changes the original number by 0.000001, which is both less accurate and completely useless.
All in all: accuracy gets worse, but doesn't matter at all. Even integers are accurate enough for coordinates / distances (in wc3 at least).
 
Top