/*
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