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

Water Vortex v1.03

  • Like
Reactions: watermelon_1234
Created for a spell contest :)

Water Vortex

Ability Type: Channeling
Target Type: None
Effect: Area Damage, Disable

Description:
Creates a water vortex at caster point. The vortex sucks some units into it, removing them from existance and damages all other nearby units.

JASS:
/*
    water vortex v1.03 by scorpion182
    requires:
    timer utils, bound sentinel, xedamage, xefx by Vexorian
    grouputils by Rising_Dusk
*/

library WaterVortex requires TimerUtils, GroupUtils, xefx, xedamage, TerrainPathability, BoundSentinel
    private keyword data //don't touch this
//---------------------------------------------------------------------------------------------
//--------------CALIBRATION SECTION------------------------------------------------------------
    globals
        private constant integer SPELL_ID='A000' //ability rawcode
        private constant string ORDER_ID="starfall" //ability order string
        private constant integer MAXMISSCOUNT=50 // keep this value higher than number of GetMissileCount*GetLayerCount
        private constant string PATH="Abilities\\Weapons\\WaterElementalMissile\\WaterElementalMissile.mdl" //missile fx path
        private constant string CRUSH_FX="Objects\\Spawnmodels\\Naga\\NagaDeath\\NagaDeath.mdl" //on-death effect
        private constant string CRUSH_ATTCH="origin" //on-death effect attachment point
        private constant string DAMAGE_FX="" //on-damage effect
        private constant string DAMAGE_ATTCH="origin" //on-damage effect attachment point
        private constant real SCALE=1. //missile scale
        private constant integer RED=255 //vertex coloring in RGB , 
        private constant integer GREEN=255 // alpha is the opacity value
        private constant integer BLUE=255  
        private constant integer ALPHA=255 
        private constant real HEIGHT_INC=50. //missile height increment
        private constant real ANGLE_SPEED=.15 //missile angle speed, it's in radians
        private constant boolean INSTANTKILL=true //instant kill the sucked unit wandering too close if true
        private constant boolean DISABLEPATHING=true // If this is true, the spell will look better but can make units stop in weird places if the channeling is stopped.
                                                     // If it is false, the spell will look a bit worse, but shouldn't be able to create those problems.
    endglobals
    
    private constant function GetTargetCount takes integer lvl returns integer
        return 5+lvl*0 // how many units the maximum can suck into it at the same time
    endfunction
    
    private constant function GetAoE takes integer lvl returns real
        return 600.+lvl*0. // area of effect of the spell
    endfunction
    
    private constant function GetAngleSpeed takes integer lvl returns real
        return 1.309+lvl*0. // how much the targets turn while being sucked in per interval, it's in radians
    endfunction
    
    private constant function GetSpeed takes integer lvl returns real
        return 50.00+lvl*0. // how fast the units are sucked in
    endfunction
    
    private constant function DistanceToAbsorb takes integer lvl returns real
        return 80.00+lvl*0. // how close a sucked unit must be to the center to disappear.
    endfunction
    
    private constant function GetDamage takes integer lvl returns real
        return 30.*lvl //deal damage per interval
    endfunction
    
    private constant function GetMissileCount takes integer lvl returns integer 
        return 5*lvl+0 //number of missiles each layer
    endfunction
    
    private constant function GetLayerCount takes integer lvl returns integer
        return 2*lvl+0 //number of layers
    endfunction
    
    //damage filter
    private function DamageOptions takes xedamage spellDamage returns nothing
        set spellDamage.dtype=DAMAGE_TYPE_UNIVERSAL
        set spellDamage.atype=ATTACK_TYPE_NORMAL
        set spellDamage.exception=UNIT_TYPE_STRUCTURE
        set spellDamage.visibleOnly=true 
        set spellDamage.damageAllies=false //damage allies if true
    endfunction
    
    //filter the targets, should match the value from DamageOptions
    private function IsValidTarget takes unit u, data s returns boolean
        
        return not IsUnitType(u, UNIT_TYPE_DEAD) and GetUnitTypeId(u) != 0 and IsUnitType(u,UNIT_TYPE_STRUCTURE)==false and IsUnitEnemy(u,GetOwningPlayer(s.caster))==true and IsUnitInGroup(u,s.victim)==false and IsUnitVisible(u,GetOwningPlayer(s.caster))==true
        
    endfunction
//------------END OF CALIBRATION----------------------------------------------------------------
    globals
        private group casters=CreateGroup()
        private xedamage xed
    endglobals
    
    private struct data
        unit caster
        timer t
        real x
        real y
        integer lvl
        integer count=0
        group victim
        xefx array fx[MAXMISSCOUNT]
        real array angle[MAXMISSCOUNT]
        private static thistype temp
        
        static method create takes unit c, real x, real y returns data
            local data this=data.allocate()
            local integer i=0
            local integer j=0
            local integer k=0
            local real height=HEIGHT_INC
            local real a=2*bj_PI
            
            call GroupAddUnit(casters,c)
            set .caster=c
            set .x=x
            set .y=y
            set .lvl=GetUnitAbilityLevel(c,SPELL_ID)
            set .victim=NewGroup()
            set .t=NewTimer()
            
            loop
            exitwhen i==GetLayerCount(.lvl)
            
                loop
                exitwhen j==GetMissileCount(.lvl)*(i+1)
                    
                    set .fx[j]=xefx.create(x,y,k*a/GetMissileCount(.lvl))
                    set .fx[j].fxpath=PATH
                    set .fx[j].scale=SCALE
                    set .fx[j].z=height
                    set .angle[j]=k*a/GetMissileCount(.lvl)
                    set height=height+HEIGHT_INC
                    
                    set k=k+1
                    set j=j+1
                endloop
                
                set height=HEIGHT_INC //to synchronize each layer missile' height
                set k=0
                set i=i+1
            endloop
            
            return this
        
        endmethod
        
        static method movecallback takes nothing returns nothing
            local unit f=GetEnumUnit()
            local real x
            local real y
            local real dist
            local real a
            local real d
            
            //move the target unit
            set x=temp.x-GetUnitX(f)
            set y=temp.y-GetUnitY(f)
            set dist=SquareRoot(x*x+y*y)
            
            set a=Atan2(GetUnitY(f)-temp.y, GetUnitX(f)-temp.x)+GetAngleSpeed(temp.lvl)*XE_ANIMATION_PERIOD
            set d=dist-GetSpeed(temp.lvl)*XE_ANIMATION_PERIOD
            set x = temp.x+d*Cos(a)
            set y = temp.y+d*Sin(a)
            
            if (DISABLEPATHING==true) or (DISABLEPATHING==false and (IsTerrainWalkable(x,y) or IsUnitType(f,UNIT_TYPE_FLYING))) then
                call SetUnitX(f, x)
                call SetUnitY(f, y)
            endif
            
            if dist<=DistanceToAbsorb(temp.lvl) and INSTANTKILL==true then
                call xed.useSpecialEffect(CRUSH_FX,CRUSH_ATTCH)
                //instant kill the unit
                call xed.damageTarget(temp.caster,f,99999.)
            endif
            //kick the dead units from the victim group
            if (IsUnitType(f, UNIT_TYPE_DEAD) or GetUnitTypeId(f) == 0) then
                call GroupRemoveUnit(temp.victim,f)
                set temp.count=temp.count-1
            endif
            
            set f=null
        endmethod
        
        //moving the missiles and the targets
        static method move takes nothing returns nothing
            local real a
            local integer i=0
            local integer j=0
            
            loop
            exitwhen i==GetLayerCount(temp.lvl)
                
                loop
                exitwhen j==GetMissileCount(temp.lvl)*(i+1)
                    
                    set a=temp.angle[j]+ANGLE_SPEED
                    
                    set temp.fx[j].x=temp.x+GetAoE(temp.lvl)/(i+1)*Cos(a)
                    set temp.fx[j].y=temp.y+GetAoE(temp.lvl)/(i+1)*Sin(a)
                    
                    set temp.angle[j]=a
                    
                    set j=j+1
                
                endloop
                
                set i=i+1
            endloop
            
            
            call ForGroup(temp.victim,function data.movecallback)
            
        endmethod
        
        private method onDestroy takes nothing returns nothing
            local unit f 
            local integer i=0
            
            call ReleaseTimer(.t)
            call GroupRemoveUnit(casters,.caster)
            //i just hate using ForGroup here :)
            loop
            set f=FirstOfGroup(.victim)
            exitwhen f==null
                call PauseUnit(f,false)
                call GroupRemoveUnit(.victim,f)
            endloop
            //destroy fx
            loop
            exitwhen i==GetLayerCount(.lvl)*GetMissileCount(.lvl)
                call .fx[i].destroy()
                set i=i+1
            endloop
            
            call ReleaseGroup(.victim)
            //unit f will already be null by the end of the FirstOfGroup loop
        endmethod
        
        //filter the targets
        static method VictimFilter takes nothing returns boolean
            local unit u=GetFilterUnit()
            local integer a=GetTargetCount(temp.lvl)
            
            if temp.count==a then
                set u=null
                return false
            endif
            
            if IsValidTarget(u,temp)==true then
                call GroupAddUnit(temp.victim,u)
                call PauseUnit(u,true)
                set temp.count=temp.count+1
            endif
            
            set u=null
            return false
        endmethod
        
        static method Loop takes nothing returns nothing
            local data this=data(GetTimerData(GetExpiredTimer()))
            local integer a=GetTargetCount(.lvl)
            local unit f
            local boolexpr be
            
            if (GetUnitCurrentOrder(.caster)==OrderId(ORDER_ID)) then
                set .temp=this
                //damage nearby units, include the targets
                call GroupEnumUnitsInArea(ENUM_GROUP,.x,.y,GetAoE(.lvl),BOOLEXPR_TRUE)
                call xed.useSpecialEffect(DAMAGE_FX,DAMAGE_ATTCH)
                call xed.damageGroup(.caster,ENUM_GROUP,GetDamage(.lvl)*XE_ANIMATION_PERIOD)
                //search targets
                if .count<a then
                    set be=Condition(function data.VictimFilter)
                    call GroupEnumUnitsInArea(ENUM_GROUP,.x,.y,GetAoE(.lvl),be)
                    call DestroyBoolExpr(be)
                endif
            
                call data.move()
            
                else
                    call .destroy()
            endif
            
            set be=null
            set f=null
        endmethod
    
        //trigger conditions are faster than trigger actions :D
        static method SpellEffect takes nothing returns boolean
            local data this
            local unit caster=GetSpellAbilityUnit()
        
            if GetSpellAbilityId() == SPELL_ID and IsUnitInGroup(caster,casters)==false then
                set this=data.create(caster,GetUnitX(caster),GetUnitY(caster))
                call SetTimerData(.t,this)
                call TimerStart(.t,XE_ANIMATION_PERIOD,true,function data.Loop)
            endif
            set caster=null
            return false
        endmethod
        
        //spell trigger actions
        static method onInit takes nothing returns nothing
            local trigger t=CreateTrigger()
            call TriggerRegisterAnyUnitEventBJ(t,EVENT_PLAYER_UNIT_SPELL_EFFECT)
            call TriggerAddCondition(t,Condition(function data.SpellEffect))
        
            //init xedamage
            set xed=xedamage.create()
            call DamageOptions(xed)
        endmethod
    
    endstruct
    
endlibrary


Requires:
- TimerUtils by Vexorian
- xe by Vexorian
- BoundSentinel by Vexorian
- GroupUtils by Rising_Dusk
- TerrainPathability by Rising_Dusk


Changelog:
~ v1.01 Fixed codes.
~ v1.01b Remove unnecessary codes.
~ v1.01c Fixed minor bugs.
~ v1.01d Make disable unit pathing configurable.
~ v1.01e Get rid UnitAlive native.
~v1.02 Fixed disablepathing issues, number of missiles and number of layers are configurable now.
~v.1.02b Replace GroupEnumUnitsInRange with GroupEnumUnitsInArea
~ v1.03 Added TerrainPathability to requirements.

Keywords:
water, vortex, whirpool, area, splash, blue, naga, wizard
Contents

Water Vortex v1.03 (Map)

Reviews
21:52, 4th Jan 2010 The_Reborn_Devil: The coding looks good and the effects are nice, but please use one timer to loop through all of them instead of having one timer per struct. Status: Approved Rating: Useful

Moderator

M

Moderator

21:52, 4th Jan 2010
The_Reborn_Devil:
The coding looks good and the effects are nice, but please use one timer to loop through all of them instead of having one timer per struct.

Status: Approved
Rating: Useful
 
Level 9
Joined
Dec 12, 2007
Messages
489
ehm.... looks neat in code and idea... but you should use scope, so you don't need that requires. IMO
and you made a damage option using xedamage, but when you pick the victim:
JASS:
            if UnitAlive(u)==true and IsUnitType(u,UNIT_TYPE_STRUCTURE)==false and IsUnitEnemy(u,GetOwningPlayer(temp.caster))==true and IsUnitInGroup(u,temp.victim)==false and IsUnitVisible(u,GetOwningPlayer(temp.caster))==true then
I don't know, but I think this is hardcoded, as even though I change the damage option, it won't affect the unit it pick as victim, am I right or not?
and this part:
from the static method move:
JASS:
...
            local real a
            local integer i=0
            //outer missile
            loop
            exitwhen i==MISSILE_COUNT/2
                set a=temp.angle[i]+ANGLE_SPEED
                set temp.fx[i].x=temp.x+GetAoE(temp.lvl)*Cos(a)
                set temp.fx[i].y=temp.y+GetAoE(temp.lvl)*Sin(a)
                set temp.angle[i]=a
                set i=i+1
            endloop
            //inner missile
            set i=5
            loop
            exitwhen i==MISSILE_COUNT
                set a=temp.angle[i]+.15
                set temp.fx[i].x=temp.x+GetAoE(temp.lvl)/2*Cos(a)
                set temp.fx[i].y=temp.y+GetAoE(temp.lvl)/2*Sin(a)
                set temp.angle[i]=a
                set i=i+1
            endloop
...
I assume this set i = 5 is because MISSILE_COUNT/2 is 5 ? this is hardcoded once again, because if I change the MISSILE_COUNT, that will show unwanted result.
and I don't see any point of adding the caster into casters group, or you intend to make it so when a unit is channeling it, it won't be able to rechannel it again during that period?
 
Last edited:
Level 16
Joined
Jun 9, 2008
Messages
734
ehm.... looks neat in code and idea... but you should use scope, so you don't need that requires. IMO
and you made a damage option using xedamage, but when you pick the victim:
JASS:
            if UnitAlive(u)==true and IsUnitType(u,UNIT_TYPE_STRUCTURE)==false and IsUnitEnemy(u,GetOwningPlayer(temp.caster))==true and IsUnitInGroup(u,temp.victim)==false and IsUnitVisible(u,GetOwningPlayer(temp.caster))==true then
I don't know, but I think this is hardcoded, as even though I change the damage option, it won't affect the unit it pick as victim, am I right or not?
and this part:
from the static method move:
JASS:
...
            local real a
            local integer i=0
            //outer missile
            loop
            exitwhen i==MISSILE_COUNT/2
                set a=temp.angle[i]+ANGLE_SPEED
                set temp.fx[i].x=temp.x+GetAoE(temp.lvl)*Cos(a)
                set temp.fx[i].y=temp.y+GetAoE(temp.lvl)*Sin(a)
                set temp.angle[i]=a
                set i=i+1
            endloop
            //inner missile
            set i=5
            loop
            exitwhen i==MISSILE_COUNT
                set a=temp.angle[i]+.15
                set temp.fx[i].x=temp.x+GetAoE(temp.lvl)/2*Cos(a)
                set temp.fx[i].y=temp.y+GetAoE(temp.lvl)/2*Sin(a)
                set temp.angle[i]=a
                set i=i+1
            endloop
...
I assume this set i = 5 is because MISSILE_COUNT/2 is 5 ? this is hardcoded once again, because if I change the MISSILE_COUNT, that will show unwanted result.
and I don't see any point of adding the caster into casters group, or you intend to make it so when a unit is channeling it, it won't be able to rechannel it again during that period?

oops thanks i forgot that part :grin:. and yes caster group to avoid double casting.

EDIT:

UPDATED!

ok fix it, it's configurable now :D, and sorry can't give you rep "You must spread some Reputation around before giving it to Dark_Axl again."

EDIT EDIT:
Updated Version 1.01d
 
Last edited:
Level 14
Joined
Nov 18, 2007
Messages
1,084
I've only looked at the coding so I have no comments on the execution of the spell.

  • Why did you get rid of UnitAlive?
  • By the way, since you're using GroupUtils, you could use GroupEnumUnitsInArea instead of GroupEnumUnitsInRange.
  • Not a big deal, but I think using GetTriggerUnit() would be better than GetSpellAbilityUnit().
  • Why didn't you just do
    JASS:
    local real x = GetUnitX(caster)
    local real y = GetUnitY(caster)
    Actually you don't even really need those variables since you use them only once. You could have even just did
    JASS:
    local unit caster = GetTriggerUnit()
    local data this = data.create(caster,GetUnitX(caster),GetUnitY(caster)
    Or just modify the create method to do that inside it.
  • I don't like your indentation in some of your methods. =P
  • Why didn't you make "be" a global variable?
  • I think you should initialize your variables instead of doing it afterward.
  • Not to be nitpicky but for the
    JASS:
    if i<MISSILE_COUNT/2 then
         //outer missile
        set temp.fx[i].x=temp.x+GetAoE(temp.lvl)*Cos(a)
        set temp.fx[i].y=temp.y+GetAoE(temp.lvl)*Sin(a)
    else
        //inner missile
        set temp.fx[i].x=temp.x+GetAoE(temp.lvl)/2*Cos(a)
        set temp.fx[i].y=temp.y+GetAoE(temp.lvl)/2*Sin(a)
    endif
    you could have simplified it by using a variable to make it shorter. Example:
    JASS:
    local integer ex = 2
    if i<MISSILE_COUNT/2 then
        set ex = 1
    endif
    set temp.fx[i].x=temp.x+GetAoE(temp.lvl)/ex*Cos(a)
    set temp.fx[i].y=temp.y+GetAoE(temp.lvl)/ex*Sin(a)
  • Like Dark_Axl has mentioned, you should make this a scope instead of a library.
  • Shouldn't BoundSentinel be optional?
 
Last edited:
Level 16
Joined
Jun 9, 2008
Messages
734
meh forgot to upload new update here

~ Why did you get rid of UnitAlive?
like i said at wc3c, the map got screwed when optimized with vex's optimizer (with the unitalive native inside, i don't know why :( , the map won't load. But it's fine before optimized).

~ GroupEnumUnitsInArea
???

~ Why didn't you make "be" a global variable?
to access struct member with enumeration

~ Like Dark_Axl has mentioned, you should make this a scope instead of a library

I use library because you'll get correct errors if you are missing an external library.

~ Shouldn't BoundSentinel be optional?
no. i use setx and sety native, it's for safety.
 
Level 14
Joined
Nov 18, 2007
Messages
1,084
~ GroupEnumUnitsInArea
???
GroupUtils got updated with that function. Unlike GroupEnumUnitsInRange, it accounts for collision size, so it's more accurate for targeting spells.

no. i use setx and sety native, it's for safety.
You may want to mention that it's optional though because it's not required for the spell to actually work but it's useful in making it safer.
 
Level 16
Joined
Jun 9, 2008
Messages
734
~ GroupUtils got updated with that function. Unlike GroupEnumUnitsInRange, it accounts for collision size, so it's more accurate for targeting spells.

i see

~ You may want to mention that it's optional though because it's not required for the spell to actually work but it's useful in making it safer.

With the SetUnitPosition() native you can't move an unit outside the playable map area. With the functions SetUnitX/Y you can move an unit outside the playable map area but not the entire map. if you try to give an order to an unit witch is outside the playable map area, the game will crash. so BoundSentinel is a must.

Updated Version 1.02b
 
Last edited:
Top