library_once Boomerang initializer Init requires GroupUtils, xefx, DestructableLib, IsTerrainWalkable
private keyword Boomerang // DO NOT TOUCH; configuration is below
// Credits:
// - Ciebron for the inspiration
// - -JonNny for reporting some bugs
// - Atideva for reporting a bug
// - 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./40 // granulation of boomerang 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 array BOOMERANG_SPEED // the speed at which the boomerang moves // due to limitations this is only an approximation
private constant real BOOMERANG_FOCUS = 2. // the higher this number the more focused is the path of the boomerang. Any value greater than 0 should work. Values below 2 might not look so good.
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 IGNORE_TREES = false // if true, the boomerang will just fly through the trees without doing anything
private constant boolean KILL_TREES = true // 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 returns real // PROXY
return DAMAGE[level]
endfunction
private function Damage_Absorption takes integer level returns real // PROXY
return DAMAGE_ABSORPTION[level]
endfunction
private function Boomerang_Speed takes integer level returns real // PROXY
return BOOMERANG_SPEED[level]
endfunction
private function Min_Range takes integer level returns real // PROXY
return MIN_RANGE[level]
endfunction
private function SetUpSpellData takes nothing returns nothing
set DAMAGE[1] = 200. // initially deals 200 damage
set DAMAGE[2] = 275.
set DAMAGE[3] = 350.
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
set BOOMERANG_SPEED[1] = 600.
set BOOMERANG_SPEED[2] = 600.
set BOOMERANG_SPEED[3] = 600.
set MIN_RANGE[1] = 200.
set MIN_RANGE[2] = 200.
set MIN_RANGE[3] = 200.
endfunction
private function ValidTarget takes unit target, Boomerang this returns boolean
return IsUnitType(target, UNIT_TYPE_DEAD) == false /*
*/ and IsUnitType(target, UNIT_TYPE_STRUCTURE) == false /*
*/ and IsUnitType(target, UNIT_TYPE_MAGIC_IMMUNE) == false /*
*/ and not IsUnitInGroup(target, this.damagedUnits) /*
*/ and IsUnitEnemy(target, GetOwningPlayer(this.caster))
endfunction
// This is shit. Don't touch shit.
globals
private rect R
private Boomerang tmpBoomerang
private constant boolean ANGLE_DIRECTION_FORWARD = true
private constant boolean ANGLE_DIRECTION_REVERSE = false
endglobals
private struct Boomerang
unit caster
unit target
integer level
real angleCurrent
boolean angleDirection
real targetX
real targetY
xefx dummy
boolean active
real damage
group damagedUnits
boolean overTree
boolean markedForDestruction
static boolexpr DamageFilter
static boolexpr TreeFilter
static boolexpr LightTreeFilter
private integer index
private static thistype array Structs
private static timer T = CreateTimer()
private static integer Count = 0
private static method onInit takes nothing returns nothing
set DamageFilter = Filter(function thistype.DamageFilterFunc)
set TreeFilter = Filter(function thistype.TreeFilterFunc)
set LightTreeFilter = Filter(function thistype.LightTreeFilterFunc)
endmethod
method reduceDamage takes nothing returns nothing
static if DAMAGE_ABSORPTION_RELATIVE then
set damage = damage * (1 - Damage_Absorption(level))
else
set damage = damage - Damage_Absorption(level)
endif
if damage <= DAMAGE_BOUNDARY then
set markedForDestruction = true
endif
endmethod
method onDestroy takes nothing returns nothing
set caster = null
set target = null
call dummy.destroy()
call ReleaseGroup(damagedUnits)
// clean your struct here
set Count = Count - 1
set Structs[index] = Structs[Count]
set Structs[index].index = .index
if Count == 0 then
call PauseTimer(T)
endif
endmethod
static method UnitDistCheck takes nothing returns nothing
local unit u = GetEnumUnit()
local real dx = tmpBoomerang.dummy.x - GetUnitX(u)
local real dy = tmpBoomerang.dummy.y - GetUnitY(u)
if (dx*dx + dy*dy) > (BOOMERANG_COLLSIZE * BOOMERANG_COLLSIZE) then
call GroupRemoveUnit(tmpBoomerang.damagedUnits, u)
endif
set u = null
endmethod
static method DamageFilterFunc takes nothing returns boolean
local unit u = GetFilterUnit()
if tmpBoomerang.markedForDestruction then
return false
endif
// check if unit is a valid target for damage
if ValidTarget(u, tmpBoomerang) then
// damage the unit; if the unit for some reason cant be damaged, dont continue
if UnitDamageTarget(tmpBoomerang.caster, u, tmpBoomerang.damage, false, false, ATTACK_TYPE, DAMAGE_TYPE, WEAPON_TYPE) then
call DestroyEffect(AddSpecialEffectTarget(HIT_FX, u, HIT_FX_ATTPT))
call GroupAddUnit(tmpBoomerang.damagedUnits, u)
call tmpBoomerang.reduceDamage()
endif
endif
set u = null
return false
endmethod
static method TreeFilterFunc takes nothing returns boolean
local destructable d = GetFilterDestructable()
local real x
local real y
local real dx
local real dy
if tmpBoomerang.markedForDestruction then
// short circuit logic after the damage boundary has been reached
return false
endif
// filter out dead and non-tree destructables
if (not IsDestructableDead(d)) and IsDestructableTree(d) then
set tmpBoomerang.overTree = true
set x = GetWidgetX(d)
set y = GetWidgetY(d)
set dx = tmpBoomerang.dummy.x - x
set dy = tmpBoomerang.dummy.y - y
// tree must be inside the collision radius
if (dx*dx + dy*dy) <= (BOOMERANG_COLLSIZE * BOOMERANG_COLLSIZE) then
static if KILL_TREES then
call KillDestructable(d)
call tmpBoomerang.reduceDamage()
else
// if Trees may not be destroyed, destroy the boomerang instead
set tmpBoomerang.markedForDestruction = true
endif
endif
endif
set d = null
return false
endmethod
static method LightTreeFilterFunc takes nothing returns boolean
local destructable d = GetFilterDestructable()
if not IsDestructableDead(d) and IsDestructableTree(d) then
set tmpBoomerang.overTree = true
endif
set d = null
return false
endmethod
private static method Callback takes nothing returns nothing
local integer i = Count - 1
local thistype this
local real x
local real y
local real launchX
local real launchY
local real deltaX
local real deltaY
local real distance
local real angleBase
local real angleIncrement
local real offset
loop
exitwhen i < 0
set this = Structs[i]
set tmpBoomerang = this
//
// make the boomerang home, even if the caster moves
if this.target != null then
set this.targetX = GetUnitX(this.target)
set this.targetY = GetUnitY(this.target)
endif
set launchX = GetUnitX(this.caster)
set launchY = GetUnitY(this.caster)
set deltaX = this.targetX - launchX
set deltaY = this.targetY - launchY
set distance = SquareRoot(deltaX*deltaX + deltaY*deltaY)
set angleBase = Atan2(deltaY, deltaX) - ((bj_PI / BOOMERANG_FOCUS) / 2)
// functions for moving the boomerang:
// r(a)=distance*Sin(BOOMERANG_FOCUS*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 offset = distance * Sin(BOOMERANG_FOCUS * this.angleCurrent)
set x = launchX + (Cos(angleBase + this.angleCurrent) * offset)
set y = launchY + (Sin(angleBase + this.angleCurrent) * offset)
set this.dummy.x = x
set this.dummy.y = y
static if ALLOW_MULTIPLE_HITS then
call ForGroup(this.damagedUnits, function thistype.UnitDistCheck)
endif
call GroupEnumUnitsInRange(ENUM_GROUP, x, y, BOOMERANG_COLLSIZE, DamageFilter)
static if not IGNORE_TREES then
set this.overTree = false
call MoveRectTo(R, x, y)
call EnumDestructablesInRect(R, TreeFilter, null)
endif
static if COLLIDE_WITH_GROUND then
static if IGNORE_TREES then
set this.overTree = false
call MoveRectTo(R, x, y)
call EnumDestructablesInRect(R, LightTreeFilter, null)
endif
if not IsTerrainWalkable(x, y) and not this.overTree then
set this.markedForDestruction = true
endif
endif
set angleIncrement = TICK * ((bj_PI / BOOMERANG_FOCUS) / 2) * (Boomerang_Speed(this.level) / distance)
if this.angleDirection == ANGLE_DIRECTION_FORWARD then
set this.angleCurrent = this.angleCurrent + angleIncrement
else
set this.angleCurrent = this.angleCurrent - angleIncrement
endif
if this.angleCurrent <= 0 or this.angleCurrent >= (bj_PI / BOOMERANG_FOCUS) or IsUnitType(this.caster, UNIT_TYPE_DEAD) == true then
set this.markedForDestruction = true
endif
if this.markedForDestruction then
call this.destroy()
endif
set i = i - 1
endloop
endmethod
static method create takes unit caster, unit target, real targetX, real targetY, boolean direction returns thistype
local thistype this = allocate()
local real dx
local real dy
local real distance
local real angleBase
set this.caster = caster
if target == null or target == caster then
set this.target = null
set this.targetX = targetX
set this.targetY = targetY
else
set this.target = target
set this.targetX = GetUnitX(target)
set this.targetY = GetUnitY(target)
endif
set dx = this.targetX - GetUnitX(caster)
set dy = this.targetY - GetUnitY(caster)
set this.level = GetUnitAbilityLevel(this.caster, AID)
set distance = SquareRoot(dx*dx + dy*dy)
if distance == 0.0 then
set angleBase = (GetUnitFacing(this.caster) * bj_DEGTORAD)
else
set angleBase = Atan2(dy, dx)
endif
if distance < Min_Range(this.level) then
// enforce the minimum distance
set distance = Min_Range(this.level)
set this.targetX = GetUnitX(caster) + (distance * Cos(angleBase))
set this.targetY = GetUnitY(caster) + (distance * Sin(angleBase))
endif
if direction == ANGLE_DIRECTION_FORWARD then
set this.angleCurrent = 0.
else
set this.angleCurrent = (bj_PI / BOOMERANG_FOCUS)
endif
set this.angleDirection = direction
set this.damage = Damage(this.level)
set this.dummy = xefx.create(GetUnitX(caster), GetUnitY(caster), 0)
set this.dummy.fxpath = BOOMERANG_MODEL
set this.dummy.scale = BOOMERANG_SIZE
set this.dummy.z = BOOMERANG_HEIGHT
set this.damagedUnits = NewGroup()
set this.overTree = false
set this.markedForDestruction = false
// initialize the struct here
set Structs[Count] = this
set this.index = Count
if Count == 0 then
call TimerStart(T, TICK, true, function thistype.Callback)
endif
set Count = Count + 1
return this
endmethod
endstruct
private function SpellCond takes nothing returns boolean
return GetSpellAbilityId() == AID
endfunction
private function SpellAction takes nothing returns nothing
local unit caster = GetTriggerUnit()
local unit target = GetSpellTargetUnit()
local real targetX = GetSpellTargetX()
local real targetY = GetSpellTargetY()
static if USE_RIGHT_BOOMERANG then
call Boomerang.create(caster, target, targetX, targetY, ANGLE_DIRECTION_FORWARD)
endif
static if USE_LEFT_BOOMERANG then
call Boomerang.create(caster, target, targetX, targetY, ANGLE_DIRECTION_REVERSE)
endif
endfunction
private function Init takes nothing returns nothing
local trigger t=CreateTrigger()
call TriggerRegisterAnyUnitEventBJ(t, EVENT_PLAYER_UNIT_SPELL_EFFECT)
call TriggerAddCondition(t, Condition(function SpellCond))
call TriggerAddAction(t, function SpellAction)
call SetUpSpellData()
set R = Rect(0, 0, 2 * BOOMERANG_COLLSIZE, 2 * BOOMERANG_COLLSIZE)
endfunction
endlibrary