library_once Boomerang uses GroupUtils, xefx, DestructableLib, IsTerrainWalkable
private keyword Data // DO NOT TOUCH; configuration is below
// Credits:
// - Ciebron for the inspiration
// - -JonNny for reporting some bugs
// - Rising_Dusk for his GroupUtils library
// - Anitarf for his IsTerrainWalkable library
// - Vexorian for JassHelper and xe
// - PipeDream for Grimoire
// - PitzerMike for JassNewGenPack and DestructableLib
// - MindWorX for JassNewGenPack
// - SFilip for TESH
globals
private constant real TICK = 1./32 // granulation of movement
private constant integer AID = 'A000' // the ability triggering this spell
private real array DAMAGE // damage dealt by the boomerang to units it hits
private real array DAMAGE_ABSORPTION // the damage dealt is reduced by this much everytime the boomerang hits a unit or a tree
private constant boolean DAMAGE_ABSORPTION_RELATIVE = true // is DAMAGE_ABSORPTION to be treated as an absolute value or a value relative to the current damage
private constant real DAMAGE_BOUNDARY = 10. // once the damage is lower or equal to this, the boomerang stops flying
private constant string BOOMERANG_MODEL = "Abilities\\Weapons\\SentinelMissile\\SentinelMissile.mdl" // this is the model representing the boomerang
private constant real BOOMERANG_COLLSIZE = 96. // the AoE in which units are damaged by the boomerang
private constant real BOOMERANG_SIZE = 1.25 // the scaling of the boomerang
private constant real BOOMERANG_HEIGHT = 64. // the Z height of the boomerang
private constant real BOOMERANG_SPEED = 600. // the speed at which the boomerang moves // due to limitations this is only an approximation
private constant boolean ALLOW_MULTIPLE_HITS = true // if this is true, the boomerang can hit units more than once on his path
private constant boolean USE_RIGHT_BOOMERANG = true // just avoid setting both to false, okay?
private constant boolean USE_LEFT_BOOMERANG = true
private constant boolean COLLIDE_WITH_GROUND = true // do boomerangs collide with unwalkable terrain?
private real array MIN_RANGE // minimum throwing distance for the boomerang
private constant boolean KILL_TREES = false // if true, trees are killed once the boomerang hits one, if this is false, the boomerang stops flying
private constant string HIT_FX = "Objects\\Spawnmodels\\Critters\\Albatross\\CritterBloodAlbatross.mdl" // when the boomerang hits a unit, this effect is spawned on the unit hit
private constant string HIT_FX_ATTPT = "chest" // the beforementioned effect will be attached to this point
private constant attacktype ATTACK_TYPE = ATTACK_TYPE_MAGIC // the attack type of the damage the boomerang deals
private constant damagetype DAMAGE_TYPE = DAMAGE_TYPE_MAGIC // the damage type of the damage the boomerang deals
private constant weapontype WEAPON_TYPE = WEAPON_TYPE_METAL_MEDIUM_SLICE // sound when boomerang hits a unit
endglobals
private function Damage takes integer level, unit u returns real // PROXY
return I2R(GetHeroStr(u, true))
endfunction
private function SetUpDAMAGE_ABSORPTION takes nothing returns nothing
set DAMAGE_ABSORPTION[1]=0.16 // lowers damage dealt by 16% of current damage.
set DAMAGE_ABSORPTION[2]=0.12
set DAMAGE_ABSORPTION[3]=0.08
endfunction
private function Damage_Absorption takes integer level returns real // PROXY
return DAMAGE_ABSORPTION[level]
endfunction
private function SetUpMIN_RANGE takes nothing returns nothing
set MIN_RANGE[1]=200.
set MIN_RANGE[2]=200.
set MIN_RANGE[3]=200.
endfunction
private function Min_Range takes integer level returns real // PROXY
return MIN_RANGE[level]
endfunction
private function ValidTarget takes unit u, Data s returns boolean
return IsUnitType(u, UNIT_TYPE_DEAD)==false /* Delimited Comments FTW
*/ and IsUnitType(u, UNIT_TYPE_STRUCTURE)==false /*
*/ and IsUnitType(u, UNIT_TYPE_MAGIC_IMMUNE)==false /*
*/ and (not IsUnitInGroup(u, s.g)) /*
*/ and IsUnitEnemy(u, GetOwningPlayer(s.c))
endfunction
// This is shit. Don't touch shit.
globals
private location l
private rect R=Rect(0,0,1,1)
private Data tmps
private integer tmpd
endglobals
private struct Data
unit c // caster
xefx dum1 // boomerang dummy 1
xefx dum2 // boomerang dummy 2
boolean d1a // is dummy 1 active
boolean d2a // is dummy 2 active
integer level
real d // planned distance
real a // current angle
real x // x-coord of launch point
real y // y-coord
real tx // targeted x-coord
real ty // targeted y-coord
real f // base-angle
real dam1 // damage dealt to the next unit the boomerang hits
real dam2 // damage dealt to the next unit the boomerang hits
group g // group holding already damaged enemies
static boolexpr DamageFilter
static boolexpr TreeFilter
private integer i
private static thistype array Structs
private static timer T=CreateTimer()
private static integer Count=0
method onDestroy takes nothing returns nothing
set .c=null
if .d1a then
call .dum1.destroy()
endif
if .d2a then
call .dum2.destroy()
endif
call ReleaseGroup(.g)
// clean your struct here
set thistype.Count=thistype.Count-1
set thistype.Structs[.i]=thistype.Structs[thistype.Count]
set thistype.Structs[.i].i=.i
if thistype.Count==0 then
call PauseTimer(thistype.T)
endif
endmethod
private static method UnitDistCheck takes nothing returns nothing
local unit u=GetEnumUnit()
local real x=GetUnitX(u)
local real y=GetUnitY(u)
local real d1x=tmps.dum1.x
local real d1y=tmps.dum1.y
local real d2x=tmps.dum2.x
local real d2y=tmps.dum2.y
if ((not tmps.d1a) or (((x-d1x)*(x-d1x))+((y-d1y)*(y-d1y)))>BOOMERANG_COLLSIZE*BOOMERANG_COLLSIZE) and ((not tmps.d2a) or (((x-d2x)*(x-d2x))+((y-d2y)*(y-d2y)))>BOOMERANG_COLLSIZE*BOOMERANG_COLLSIZE) then // if the unit is not inside any boomerang anymore
// unit is not near any of the active boomerangs
call GroupRemoveUnit(tmps.g, u)
endif
set u=null
endmethod
private static method DamageFilterFunc takes nothing returns boolean
local unit u=GetFilterUnit()
// check if unit is a valid target for damage
if ValidTarget(u, tmps) then
if tmpd==1 then // tmpd hold the current boomerang; 1 for left-wing, 2 for right-wing
// damage the unit; if the unit for some reason cant be damaged, dont continue
if UnitDamageTarget(tmps.c, u, tmps.dam1, false, false, ATTACK_TYPE, DAMAGE_TYPE, WEAPON_TYPE) then
call DestroyEffect(AddSpecialEffectTarget(HIT_FX, u, HIT_FX_ATTPT))
call GroupAddUnit(tmps.g, u)
if DAMAGE_ABSORPTION_RELATIVE then
set tmps.dam1=tmps.dam1*(1-Damage_Absorption(tmps.level))
else
set tmps.dam1=tmps.dam1-Damage_Absorption(tmps.level)
endif
if tmps.dam1<=DAMAGE_BOUNDARY then // damage has become too low
call tmps.dum1.destroy() // destroy the boomerang dummy
set tmps.d1a=false // mark that boomerang as destroyed
if not tmps.d2a then // if the other boomerang is dead as well, destroy the spells instance
call tmps.destroy()
endif
endif
endif
elseif tmpd==2 then // pretty much the same here
if UnitDamageTarget(tmps.c, u, tmps.dam2, false, false, ATTACK_TYPE, DAMAGE_TYPE, WEAPON_TYPE) then
call DestroyEffect(AddSpecialEffectTarget(HIT_FX, u, HIT_FX_ATTPT))
call GroupAddUnit(tmps.g, u)
if DAMAGE_ABSORPTION_RELATIVE then
set tmps.dam2=tmps.dam2*(1-Damage_Absorption(tmps.level))
else
set tmps.dam2=tmps.dam2-Damage_Absorption(tmps.level)
endif
if tmps.dam2<=DAMAGE_BOUNDARY then
call tmps.dum2.destroy()
set tmps.d2a=false
if not tmps.d1a then
call tmps.destroy()
endif
endif
endif
endif
endif
set u=null
return false
endmethod
private static method TreeFilterFunc takes nothing returns boolean
local destructable d=GetFilterDestructable()
local real x
local real y
local real bx
local real by
if (not IsDestructableDead(d)) and IsDestructableTree(d) then // filter out dead and non-tree destructables
set x=GetWidgetX(d)
set y=GetWidgetY(d)
if tmpd==1 then // same as above
set bx=tmps.dum1.x
set by=tmps.dum1.y
if (((x-bx)*(x-bx))+((y-by)*(y-by)))<=BOOMERANG_COLLSIZE*BOOMERANG_COLLSIZE then // tree must be inside the collision radius
if KILL_TREES then // if the boomerang is allowed to kill trees, do so
call KillDestructable(d)
// but adjust the damage as if a unit had been hit
if DAMAGE_ABSORPTION_RELATIVE then
set tmps.dam1=tmps.dam1*(1-Damage_Absorption(tmps.level))
else
set tmps.dam1=tmps.dam1-Damage_Absorption(tmps.level)
endif
if tmps.dam1<=DAMAGE_BOUNDARY then // same as above
call tmps.dum1.destroy()
set tmps.d1a=false
if not tmps.d2a then
call tmps.destroy()
endif
endif
else // if Trees may not be destroyed, destroy the boomerang instantly
call tmps.dum1.destroy()
set tmps.d1a=false
if not tmps.d2a then // and end the spell instance if appropriate
call tmps.destroy()
endif
endif
endif
elseif tmpd==2 then // next section is the same as above
set bx=tmps.dum2.x
set by=tmps.dum2.y
if (((x-bx)*(x-bx))+((y-by)*(y-by)))<=BOOMERANG_COLLSIZE*BOOMERANG_COLLSIZE then
if KILL_TREES then
call KillDestructable(d)
if DAMAGE_ABSORPTION_RELATIVE then
set tmps.dam2=tmps.dam2*(1-Damage_Absorption(tmps.level))
else
set tmps.dam2=tmps.dam2-Damage_Absorption(tmps.level)
endif
if tmps.dam2<=DAMAGE_BOUNDARY then
call tmps.dum2.destroy()
set tmps.d2a=false
if not tmps.d1a then
call tmps.destroy()
endif
endif
else
call tmps.dum2.destroy()
set tmps.d2a=false
if not tmps.d1a then
call tmps.destroy()
endif
endif
endif
endif
endif
set d=null
return false
endmethod
private static method Callback takes nothing returns nothing
local integer i=thistype.Count-1
local thistype s
local real r
local real x
local real y
loop
exitwhen i<0
set s=thistype.Structs[i]
//
// make the boomerang home, even if the caster moves
set s.x=GetUnitX(s.c)
set s.y=GetUnitY(s.c)
set s.d=SquareRoot( ((s.tx-s.x)*(s.tx-s.x))+((s.ty-s.y)*(s.ty-s.y)) )
set s.f=Atan2(s.ty-s.y, s.tx-s.x)-(bj_PI/4)
// functions for moving the boomerang:
// r(a)=distance*Sin(2*a) // a is the angle and goes from 90 to 0 // distance from center point
// x(a)=Cos(a)*r(a) // x and y coordinates in relation to the location it was cast.
// y(a)=Sin(a)*r(a) // note that i inlined some things to allow casting the boomerang in all directions from any point on the map
set r=(s.d*Sin(2*s.a))
set tmps=s
if ALLOW_MULTIPLE_HITS then
call ForGroup(s.g, function Data.UnitDistCheck)
endif
if s.d1a then // is dummy 1 active
set tmpd=1 // indicate were working with dummy 1
set x=s.x+(Cos(s.a+s.f)*r)
set y=s.y+(Sin(s.a+s.f)*r)
set s.dum1.x=x
set s.dum1.y=y
call GroupEnumUnitsInRange(ENUM_GROUP, x, y, BOOMERANG_COLLSIZE, Data.DamageFilter)
call SetRect(R, x-BOOMERANG_COLLSIZE, y-BOOMERANG_COLLSIZE, x+BOOMERANG_COLLSIZE, y+BOOMERANG_COLLSIZE)
call EnumDestructablesInRect(R, Data.TreeFilter, null)
if COLLIDE_WITH_GROUND and (not IsTerrainWalkable(x,y)) then // if boomerangs collide with unwalkable terrain and the terrain is unwalkable
call s.dum1.destroy() // destroy the dummy
set s.d1a=false
if not s.d2a then // if the other dummy is inactive
call s.destroy() // destroy this spell instance
endif
endif
endif
if s.d2a then
set tmpd=2
set x=s.x+(Cos((bj_PI/2-s.a)+s.f)*r)
set y=s.y+(Sin((bj_PI/2-s.a)+s.f)*r)
set s.dum2.x=x
set s.dum2.y=y
call GroupEnumUnitsInRange(ENUM_GROUP, x, y, BOOMERANG_COLLSIZE, Data.DamageFilter)
call SetRect(R, x-BOOMERANG_COLLSIZE, y-BOOMERANG_COLLSIZE, x+BOOMERANG_COLLSIZE, y+BOOMERANG_COLLSIZE)
call EnumDestructablesInRect(R, Data.TreeFilter, null)
if COLLIDE_WITH_GROUND and not IsTerrainWalkable(x,y) then
call s.dum2.destroy()
set s.d2a=false
if not s.d1a then
call s.destroy()
endif
endif
endif
set s.a=s.a-(TICK*((bj_PI*BOOMERANG_SPEED)/(4*s.d)))
if s.a<=0 or IsUnitType(s.c, UNIT_TYPE_DEAD)==true then // stop this spell when caster is dead or the boomerang is at the casters position again.
call s.destroy()
endif
// do your things here, dont forget to call s.destroy() somewhen
//
set i=i-1
endloop
endmethod
static method SpellCond takes nothing returns boolean
return GetSpellAbilityId()==AID
endmethod
static method create takes nothing returns thistype
local thistype s=thistype.allocate()
set l=GetSpellTargetLoc()
set s.tx=GetLocationX(l)
set s.ty=GetLocationY(l)
call RemoveLocation(l)
set l=null
set s.c=GetTriggerUnit()
set s.x=GetUnitX(s.c)
set s.y=GetUnitY(s.c)
set s.level=GetUnitAbilityLevel(s.c, AID)
set s.d=SquareRoot( ((s.tx-s.x)*(s.tx-s.x))+((s.ty-s.y)*(s.ty-s.y)) )
if s.d==0 then
set s.f=(GetUnitFacing(s.c)*bj_DEGTORAD)
else
set s.f=Atan2(s.ty-s.y, s.tx-s.x)
endif
if s.d<Min_Range(s.level) then // enforce the minimum distance
set s.d=Min_Range(s.level)
set s.tx=s.x+s.d*Cos(s.f)
set s.ty=s.y+s.d*Sin(s.f)
endif
set s.f=s.f-(bj_PI/4)
set s.a=(bj_PI/2)
set s.g=NewGroup()
set s.d1a=false
set s.d2a=false
if USE_RIGHT_BOOMERANG then
set s.d1a=true
set s.dam1=Damage(s.level,s.c)
set s.dum1=xefx.create(s.x, s.y, 0)
set s.dum1.fxpath=BOOMERANG_MODEL
set s.dum1.scale=BOOMERANG_SIZE
set s.dum1.z=BOOMERANG_HEIGHT
endif
if USE_LEFT_BOOMERANG then
set s.d2a=true
set s.dam2=Damage(s.level,s.c)
set s.dum2=xefx.create(s.x, s.y, 0)
set s.dum2.fxpath=BOOMERANG_MODEL
set s.dum2.scale=BOOMERANG_SIZE
set s.dum2.z=BOOMERANG_HEIGHT
endif
// initialize the struct here
set thistype.Structs[thistype.Count]=s
set s.i=thistype.Count
if thistype.Count==0 then
call TimerStart(thistype.T, TICK, true, function thistype.Callback)
endif
set thistype.Count=thistype.Count+1
if (not s.d1a) and (not s.d2a) then
call s.destroy()
return 0
endif
return s
endmethod
private 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.SpellCond))
call TriggerAddAction(t, function Data.create)
set Data.DamageFilter=Condition(function Data.DamageFilterFunc)
set Data.TreeFilter=Condition(function Data.TreeFilterFunc)
call SetUpDAMAGE_ABSORPTION()
call SetUpMIN_RANGE()
endmethod
endstruct
endlibrary