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

Boomerang! 1.4.2

  • Like
Reactions: Arvedui and -JonNny
Throw a deadly boomerang at your enemies.

This uses vJass (JassHelper 0.9.H.0 and up), GroupUtils, xefx, DestructableLib and IsTerrainWalkable.

- "-reset": spawns some footmen
- "-level <level>": Set the heros level to the level you specified. Note that you cannot decrease it that way.
- "-handleid": creates a location, displays its handlid-0x100001, and then destroys it.
- "-credits": displays credits
- "-commands": displays this list
- "-clear": removes all messages ingame


06/03/2009 - Version 1.0.0
- initial release

06/04/2009 - Version 1.1.0
- stopped the spell from triggering when using unrelated abilities
- added a minimum range
- added support for trees
- added damage reduction when hitting multiple targets
- added a hit sound
- switched from direct dummy unit usage to xe

06/05/2009 - Version 1.2.0
- fixed two bugs reported by -JonNny
- enabled damaging units more than once

06/08/2009 - Version 1.2.1
- some optimizations and the final touches (i hope this really is the final version)

06/15/2009 - Version 1.3.0
- added an option to launch a boomerang to the left side, either additionally or exclusively
- made the boomerang collide with unwalkable terrain (after checking for trees)

07/12/2009 - Version 1.3.1
- more and better comments
- made configuring valid targets easier
- boomerangs colliding with unwalkable terrain is now optional
- made an additional version where the calibration section uses formulae (for those who cant/dont want to configure arrays)

08/07/2009 - Version 1.3.3
- Compatibility with 1.24
- 1.3.2 was a private build for a request

06/10/2011 - Version 1.3.4
- fixed a few double frees

07/11/2011 - Version 1.4.0
- you can now change the width of the path of the boomerang (BOOMERANG_FOCUS)
- you can now configure the spell to simply ignore trees.
- a few optimizations in the background (mainly using static ifs) and a bit of cleanup

01/28/2012 - Version 1.4.1
- boomerangs now follow unit targets
- each boomerang now has its own group of units it has already hit
- cleaned up the code with more meaningful names

01/07/2015 - Version 1.4.2
- rewrote the entire spell, no functionality was changed
- fixed a bug involving bad logic which excluded all units from being dealt damage


Code (Array version) + Credits:
JASS:
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

Code (Formula version) + Credits:
JASS:
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 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_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 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
    return 125. + level * 75
endfunction

private function Damage_Absorption takes integer level returns real
    return 0.2 - level * 0.04
endfunction

private function Boomerang_Speed takes integer level returns real
    return 600.
endfunction

private function Min_Range takes integer level returns real
    return 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)
    
    set R = Rect(0, 0, 2 * BOOMERANG_COLLSIZE, 2 * BOOMERANG_COLLSIZE)
endfunction

endlibrary
Contents

Boomerang! 1.4.2 (Map)

Reviews
17:17, 4th Jun 2009 hvo-busterkomo: The effect looks great, and the scripting is done well. My only complaint is (more of a personal preference) is that you use arrays for the range and damage. It seems easier to do it the standard way (return level...

Moderator

M

Moderator

17:17, 4th Jun 2009
hvo-busterkomo:
The effect looks great, and the scripting is done well. My only complaint is (more of a personal preference) is that you use arrays for the range and damage. It seems easier to do it the standard way (return level * 0.25 + 1, etc..).

If you have any questions regarding this review, feel free to send a PM to hvo-busterkomo.
 
Level 11
Joined
Jul 2, 2008
Messages
601
In-Game commands are shown too late, I've already wanted to leave the game, but decided to look for commands here, I typed some in and only after that the system shown me them ;(

Nicely done, performs well, has good settings. Can't anything to you, cause working just with GUI.

In case there are already many boomerangs, 4/5
 
Level 16
Joined
Feb 22, 2006
Messages
960
code looks good, only thing I would change is the TriggerRegisterAnyUnitEventBJ, because it leaks 14 boolexpr
hehe what I see in your create method is that you use thistype for the local var but not for the returned value ;) (but realy, who cares)

I give you 4/5
 
Level 15
Joined
Jan 31, 2007
Messages
502
I love the movement of the boomerang , awesome <3
May add some units to bash in the testmap

-But if its casted into trees or any other position near there it seems to malfunction due the boomerang flies into another dirrection with another range (may it ha sth to do with the min. castrange)

-If casted on the caster directly (e.g. hero icon) it buggs (missile does not move / is not destroyed )

When trying to save the testmap Jasshelpes displays an error which makes it not possible for me to test any changes (i guess its in XE, dunno - never used the dummy system)

Line 504 , Unexpected : "ARGBrecolor"
implement optional ARGBrecolor


As mention by hvo-busterkomo i would also prefer calculations instead of that array for the levels

Else, great work ! [4,5/5]
 
Level 14
Joined
Nov 18, 2007
Messages
816
Thanks, -JonNny. I fixed both bugs.

as for the problem when saving: Im using JH 0.9.G.3, try updating JNGP (v5c) and JH (0.9.G.3).

If you prefer calculations, change the PROXY functions to your formula. You can then delete the arrays and the SetUp functions (dont forget to delete the calls to the SetUp function in the onInit method).

I also made the command list show directly after starting the map.

I dont really care about those 16 leaked null boolexprs. You should use an event stack for your map anyway.


Version 1.2.0
 
Level 5
Joined
May 25, 2009
Messages
103
Hi and thx for programming such a nice spell,
but i am too stupid to import it into my map - and i want it, i need it *g* it fits perfectly into it.

i have importet the DUMMY, importet the ability boomerang! and importet all three trigger-trees.
then i added bommerang! to my hero,

but he only throws a piece of air... :(

thx
 
Level 14
Joined
Nov 18, 2007
Messages
816
okay, let me see:

  1. Copy over the Boomerang trigger, and all Libraries under External Libraries if you dont already have those in your map
  2. Either copy the Boomerang! ability over or recreate it yourself
  3. add the boomerang ability to your hero
  4. adjust the AID constant in the Boomerang trigger to the rawcode of your Boomerang! ability (Press CTRL+D in the Object Editor to display raw values; The Boomerang! ability should look like this: "A000:ANcl (Boomerang!)". The four characters before the double point (here: "A000") are the rawcode of the ability).
  5. Additionally make sure you adjusted the rawcodes in the xebasic library, especially the one for the dummy unit.
 
Last edited:
Level 14
Joined
Nov 18, 2007
Messages
816
  1. Copy over the Boomerang trigger, and all Libraries under External Libraries if you dont already have those in your map
  2. Either copy the Boomerang! ability over or recreate it yourself
  3. add the boomerang ability to your hero
  4. adjust the AID constant in the Boomerang trigger to the rawcode of your Boomerang! ability (Press CTRL+D in the Object Editor to display raw values; The Boomerang! ability should look like this: "A000:ANcl (Boomerang!)". The four characters before the double point (here: "A000") are the rawcode of the ability).
  5. Additionally make sure you adjusted the rawcodes in the xebasic library, expecially the one for the dummy unit.

Version 1.3.1

Comiled with JNGP v5d (that means you need to update).
 
Level 1
Joined
Dec 5, 2010
Messages
2
Hello, i really like this spell and i was looking something like this long time ago. But have problem with copy to my hero in my map... I make all step by step, but still not working. Pls help me i will really appreciative... Thank you
 
Level 13
Joined
May 11, 2008
Messages
1,198
should allow boomerang go through trees(not damaging them)...is that possible?

edit: i can let it go through trees but now it will go up and down cliffs i guess.

oh well it looks nice like how it is so whatever. now we ought to get a better model...

ok nevermind what i asked about before...how do you edit the width of the course? i think it's too wide for my taste...kinda used to link's boomerang so the current width is really wide :D

i do like it a lot though so far.
 
Level 6
Joined
Jan 22, 2012
Messages
198
I get error message when i try to save the map, saying that the different spells are disabled due to errors....and then ofc it doesn't work when i try to play it :p
 
Level 1
Joined
Feb 17, 2015
Messages
1
Which version of NewGen and JassHelper are you using?

Opening your map runs fine, but saving it with neither NewGen 207 (set to Vexorian's), or NewGen 5d you linked 2 years ago saves in a format that will run. I'm using Vexorian's latest JassHelper.

Thanks!

Edit: I duplicated the spell effect into another map, but the boomerang model isn't showing. Will update after some more tinkering...
Edit 2: Still can't get the boomerang model to show. The dummy units (projectiles) are definitely created and causing damage. Same issue as this guy. I've tried different default model paths as well, and double checked for typos. Hmm...
but he only throws a piece of air... :(
Edit 3: All Working! Turns out it matters if you don't import the dummy.mdx model in the import manager. Didn't think it would, and I actually was using my own dummy model previously and it wasn't working then. But, it's all good now.

Kudos Deaod! Cool Ability!
 
Last edited:
Level 8
Joined
Jul 14, 2010
Messages
235
Smooth spell! Good thing you updated with level* for the damage as some people go outside the usual 3 level ups per ability.
Just my personal opinion, I think the damage should be Normal instead of Magic, as they are starblades
 
Level 11
Joined
Jul 4, 2016
Messages
626
It would had been nice if you could put an option to make so that the saw doesn't come back to you if you are beyond a set distance from the saw. Additionally, an option to modify the speed of return differently from the speed of conception would had also been a nice addition.

Nevertheless, even without these options, its a great replacement for the magnetic chain spell.
 
Level 6
Joined
Jul 23, 2018
Messages
243
One problem though, when you have two triggers of the same codes, the second one will not work except the first one. Meaning only one ability will activate the code and other one will not, which is weird and should have worked
 
Level 14
Joined
Nov 18, 2007
Messages
816
Getting two instances of this ability working will require some code changes. If you want different parameters for the two instances i suggest renaming one (change Boomerang in the line containing library_once Boomerang initializer Init requires to something other than Boomerang, like idk. Boomerang2).

If you dont need two instances with different settings you can add an alternative AbilityID to function SpellCond like so
JASS:
private function SpellCond takes nothing returns boolean
    return GetSpellAbilityId() == AID or GetSpellAbilityId() == ALTERNATIVE_AID
endfunction
With ALTERNATIVE_AID standing in for whatever ability youve chosen.
 
Top