Missile System v1.11 + Assorted Spells

This missile system was designed to be fairly easy to use while retaining the high flexibility required by missile systems these days. I provide the basic framework for the missile, you, as the user, provide the onHit and onLoop.

JASS:
'Missile System

'By Element of Water (EoW)

'What is it?

This missile system is quite a basic system. It differentiates from other missile systems in
one way - it uses function interfaces to allow the user to define their own custom actions
for the system to perform whenever a missile moves or when it hits a unit.

'How do I use it?

To fire a missile, you must first create it using either one of the following methods

static method create takes string sfx, real x, real y, real radius, MissileActions actions returns Missile
static method createFromUnit takes unit u, real radius, MissileActions actions returns Missile

The first one creates a new missile at a point with the model from "sfx" at coordinates x, y.
The second turns an existing unit into a missile.
The real radius how close a unit must be to the missile for it to be "hit".
For what the variable MissileActions actions is, see section 'onHit and onLoop

Now you have created the missile, but it is not moving! You need to set it off using one of
the following methods...

method fireFixed takes unit caster, real targetX, real targetY, real speed returns nothing
method fireTarget takes unit caster, unit target, real speed returns nothing

The first fires the missile from the caster towards the coordinates targetX, targetY.
The second fires the missile towards the unit target. The missile will then follow the
target until it hits, or until it hits another unit in its path.
The real speed is how far (in whatever the measurements the game uses are) the missile will
travel per second.

'onHit and onLoop

Great! Now you have a moving missile. There is only one problem - the missile does not do
anything! To make it do something whenever it hits a unit or its target, or just whenever
it moves, you need to create a struct which extends MissileActions. This is the mystery
variable MissileActions actions we saw in the 'How do I use it' section. Your struct should
contain two methods - onHit and onLoop. The onHit method runs when the missile hits its
target, or when a unit comes within its radius. The onLoop method basically runs whenever
the missile moves - every 1/FPS seconds.

You can do whatever actions you wish within the methods. If you return true, the missile
keeps moving. If you return false, the missile is destroyed. By default onHit returns false,
and onLoop returns true.

It is possible to reference the missile in an onHit or onLoop method using
GetEventMissile. There are several variables accessible by the functions through
GetEventMissile. These are as follows:

unit    GetEventMissile.Caster        - the unit which cast the missile
unit    GetEventMissile.Target        - the target unit of the missile, if it is homing.
unit    GetEventMissile.Dummy         - the dummy unit for the missile
effect  GetEventMissile.SFX           - the current effects on the dummy unit
real    GetEventMissile.StartX        - the x coordinate of where the missile originated
real    GetEventMissile.StartY        - the x coordinate of where the missile originated
real    GetEventMissile.X             - the current x coordinate of the missile
real    GetEventMissile.Y             - the current y coordinate of the missile
real    GetEventMissile.TargetX       - the original target x coordinate of the missile
real    GetEventMissile.TargetY       - the original target y coordinate of the missile
real    GetEventMissile.Speed         - the speed (see above) of the missile
real    GetEventMissile.Radius        - the radius (see above) of the missile
real    GetEventMissile.StartDist     - how far the missile originally was from its target
real    GetEventMissile.CurrentDist   - how far the missile has travelled
boolean GetEventMissile.Homing        - is the missile a homing missile or not?
boolean GetEventMissile.TargetReached - has the missile reached its target?
boolean GetEventMissile.OnTarget      - is the missile on the target point now?
group   GetEventMissile.HitUnits      - the unit group containing the units hit by the
                                      missile which caused the onHit function to be called

All of these variables can be used through any variable of type Missile, but beware that
a lot of them will be null or 0 if not used in conjunction with an onHit or onLoop method.
Some of these variables may also be set, allowing you to change the course of the missile
in onLoop or onHit methods.

There is one extra variable for advanced users, and that is the following:

set MyMissile.data = MyStruct
//it can be retrieved for use too...
set MyStructVar = MyMissile.data

This feature allows you to attach a struct to the missile, which is rather useful in some
circumstances. Of course, you may also do something like:

set GetEventMissile.data = MyStruct
                                     
Look at the example spells which use this system to make complicated spells rather a lot
easier. They are all heavily commented for maximum learning.

JASS:
library Missile initializer Init
   
    //CONSTANTS - change these suitably.
   
    globals
        public constant real FPS = 60.00
        private constant integer DUMMY_ID = 'MSdm'
        private constant player OWNER = Player(15)
    endglobals
   
    //END CONSTANTS
   
    //MAIN SYSTEM CODE - do NOT alter unless you know what you're doing.
   
    globals
        private Missile array Dat
        private integer Index = 0
        private timer Tim = CreateTimer()
       
        private boolexpr Conds
        private group Missiles = CreateGroup()
       
        Missile GetEventMissile
    endglobals
   
    struct MissileActions
        stub method onHit takes nothing returns boolean
            return false
        endmethod
       
        stub method onLoop takes nothing returns boolean
            return true
        endmethod
    endstruct
   
    //kinda transfers all the units in a group into a different group.
    private function AddGroup takes group a, group b returns nothing
        local unit u
        loop
            set u = FirstOfGroup(b)
            exitwhen u == null
            call GroupAddUnit(a, u)
            call GroupRemoveUnit(b, u)
        endloop
    endfunction
   
    private function CondsFunc takes nothing returns boolean
        return (not IsUnitInGroup(GetFilterUnit(), GetEventMissile.hg)) and GetEventMissile.Caster != GetFilterUnit() and GetUnitTypeId(GetFilterUnit()) != DUMMY_ID and GetWidgetLife(GetFilterUnit()) > 0.405
    endfunction
   
    private function Execute takes nothing returns nothing
        local integer i = 0
       
        local real angle
        local real dx
        local real dy
        local real dist
       
        loop
            exitwhen i >= Index
           
            set GetEventMissile = Dat[i]
           
            //calculate how far the missile the missile has travelled
            set GetEventMissile.CurrentDist = GetEventMissile.CurrentDist + GetEventMissile.sp
           
            //If the missile is homing onto a target, recalculate a few values
            if GetEventMissile.Homing then
                //get the new target x/y
                set GetEventMissile.TargetX = GetUnitX(GetEventMissile.Target)
                set GetEventMissile.TargetY = GetUnitY(GetEventMissile.Target)
               
                //get the angle between the missile and the target
                set angle = Atan2(GetEventMissile.TargetY - GetEventMissile.Y, GetEventMissile.TargetX - GetEventMissile.X)
               
                //calculate the sin/cos of the angle
                set GetEventMissile.sn = Sin(angle)
                set GetEventMissile.cs = Cos(angle)
               
                //make the missile face towards the target
                call SetUnitFacing(GetEventMissile.Dummy, angle * bj_RADTODEG)
               
                //cacluclate the distance between the missile and its target
                set dx = GetEventMissile.TargetX - GetEventMissile.X
                set dy = GetEventMissile.TargetY - GetEventMissile.Y
                set dist = SquareRoot(dx * dx + dy * dy)
            else
                set dist = GetEventMissile.StartDist - GetEventMissile.CurrentDist
            endif
           
            //calculate the new x/y coordinates
            set GetEventMissile.X = GetEventMissile.X + GetEventMissile.sp * GetEventMissile.cs // set x = x + speed * cos
            set GetEventMissile.Y = GetEventMissile.Y + GetEventMissile.sp * GetEventMissile.sn // set y = y + speed * sin
           
            //run the onLoop actions, destroying the missile if they return false
            if GetEventMissile.ma != 0 then
                if not GetEventMissile.ma.onLoop() then
                    call GetEventMissile.destroy()
                endif
            endif
           
            //if the missile is collideable, check for collision
            if GetEventMissile.Radius > 0.00 then
                //enumerate the units within range of the missile, which aren't missiles
                call GroupEnumUnitsInRange(GetEventMissile.hu, GetEventMissile.X, GetEventMissile.Y, GetEventMissile.Radius, Conds)
                //if the enumeration picked up any units, then...
                if FirstOfGroup(GetEventMissile.hu) != null then
                    //run the onHit actions, and if they return false then...
                    if not GetEventMissile.ma.onHit() and GetEventMissile.ma != 0 then
                        //destroy the missile
                        call GetEventMissile.destroy()
                    endif
                    //add the hit units to the hit units group so the system doesn't falsely
                    //think they're hit more than once
                    call AddGroup(GetEventMissile.hg, GetEventMissile.hu)
                endif
            endif
           
            //calculate the absolute value of the distance remaining
            if dist < 0 then
                set dist = -dist
            endif
           
            //if the distance remaining is less than the speed of the missile then...
            if dist < GetEventMissile.sp then
                //tell the user the missile has reached its target and...
                set GetEventMissile.TargetReached = true
                set GetEventMissile.OnTarget = true
                //run the onHit actions, destroying the missile if they return false
                if not GetEventMissile.ma.onHit() and GetEventMissile.ma != 0 then
                    call GetEventMissile.destroy()
                endif
                set GetEventMissile.OnTarget = false
            endif
           
            //and finally move the missile.
            call SetUnitX(GetEventMissile.Dummy, GetEventMissile.X)
            call SetUnitY(GetEventMissile.Dummy, GetEventMissile.Y)
           
            set i = i + 1
        endloop
    endfunction
   
    struct Missile
        unit                Dummy = null            //the actual missile unit
        string              fs = ""                 //the model path of the missile
        effect              SFX = null              //the actual model
       
        unit                Caster = null           //missile caster or jumper
        unit                Target = null           //homing target
       
        boolean             dm = false              //dummy, or preplaced unit?
        boolean             Homing = false          //homing?
        real                Radius = 0.             //radius for collision
       
        real                StartX = 0.             //start x
        real                StartY = 0.             //start y
        real                X = 0.                  //current x
        real                Y = 0.                  //current y
        real                TargetX = 0.            //target x
        real                TargetY = 0.            //target y
       
        real                sn = 0.                 //sin
        real                cs = 0.                 //cos
        real                StartDist = 0           //starting distance from the target
        real                CurrentDist = 0         //distance from target
       
        real                sp = 0.                 //speed
       
        group               hu = null               //the units hit this loop
        group               hg = null               //units which have already been hit
       
        boolean             TargetReached = false   //target reached?
        boolean             OnTarget = false        //on target now?
       
        MissileActions      ma = 0                  //the onHit and onLoop actions
       
        integer             data = 0                //attached data
        integer             id = 0                  //the array index of the missile
       
        //internal create method
        private static method coreCreate takes real radius, MissileActions ma returns Missile
            local Missile d = Missile.allocate()
           
            set d.hg = CreateGroup()
            set d.hu = CreateGroup()
            set d.Radius = radius
            set d.ma = ma
           
            return d
        endmethod
       
        //creates a new missile with the given model
        static method create takes string sfx, real x, real y, real radius, MissileActions actions returns Missile
            local Missile d = Missile.coreCreate(radius, actions)
           
            set d.fs = sfx
            set d.StartX = x
            set d.StartY = y
            set d.X = x
            set d.Y = y
            set d.dm = true
           
            return d
        endmethod
       
        //creates a missile from an existing unit
        static method createFromUnit takes unit u, real radius, MissileActions actions returns Missile
            local Missile d = Missile.coreCreate(radius, actions)
           
            set d.Dummy = u
            set d.StartX = GetUnitX(u)
            set d.StartY = GetUnitY(u)
            set d.X = d.StartX
            set d.Y = d.StartY
           
            return d
        endmethod
       
        method operator HitUnits takes nothing returns group
            local group g = CreateGroup()
            set bj_groupAddGroupDest = g
            call ForGroup(.hu, function GroupAddGroupEnum)
            return g
        endmethod
       
        method operator Speed takes nothing returns real
            return .sp * FPS
        endmethod
       
        method operator Speed= takes real speed returns nothing
            set .sp = speed / FPS
        endmethod
       
        //Internal function to fire the missile
        private method fire takes unit caster, real speed returns nothing
            local real angle = Atan2(.TargetY - .Y, .TargetX - .X)
            local real dx = .TargetX - .X
            local real dy = .TargetY - .Y
           
            set .sn = Sin(angle)
            set .cs = Cos(angle)
            set .StartDist = SquareRoot(dx * dx + dy * dy)
           
            if .Dummy == null then
                set .Dummy = CreateUnit(OWNER, DUMMY_ID, .X, .Y, angle * bj_RADTODEG)
                set .SFX = AddSpecialEffectTarget(.fs, .Dummy, "origin")
            endif
           
            set .Caster = caster
            set .sp = speed / FPS
           
            call GroupAddUnit(Missiles, .Dummy)
           
            set Dat[Index] = this
            set .id = Index
            set Index = Index + 1
           
            if Index == 1 then
                call TimerStart(Tim, 1./FPS, true, function Execute)
            endif
        endmethod
       
        //fires the missile at a point!
        method fireFixed takes unit caster, real targetX, real targetY, real speed returns nothing
            set .Homing = false
           
            set .TargetX = targetX
            set .TargetY = targetY
           
            call .fire(caster, speed)
        endmethod
       
        //fires the missile at a unit!
        method fireTarget takes unit caster, unit target, real speed returns nothing
            set .Homing = true
            set .Target = target
           
            set .TargetX = GetUnitX(.Target)
            set .TargetY = GetUnitY(.Target)
           
            call .fire(caster, speed)
        endmethod
       
        method onDestroy takes nothing returns nothing
            set Index = Index - 1
            set Dat[.id] = Dat[Index]
            set Dat[.id].id = .id
           
            call .ma.destroy()
           
            call GroupClear(.hg)
            call DestroyGroup(.hg)
            call DestroyGroup(.hu)
           
            if .SFX != null then
                call DestroyEffect(.SFX)
            endif
           
            call GroupRemoveUnit(Missiles, .Dummy)
           
            if .dm then
                call KillUnit(.Dummy)
            endif
        endmethod
    endstruct
   
    private function Init takes nothing returns nothing
        set Conds = Filter(function CondsFunc)
    endfunction
endlibrary

NEW: The MissileAPI

This is basically a couple of functions to make it much easier to create basic missiles, created through Deuterium's suggestion. There are 2 new functions:

JASS:
function CreateTargetMissile takes unit caster, unit target, string sfx, real speed, real damage, real radius, real aoe, real arc returns nothing
function CreatePointMissile takes unit caster, real targetX, real targetY, string sfx, real speed, real damage, real radius, real aoe, real arc returns nothing

CreateTargetMissileFires a missile at the target.
CreatePointMissileFires a missile at coordinates targetX/targetY

unit caster - the unit who does the damaging
string sfx - the model path of the missile
real speed - how far the missile moves per second
real damage - how much damage the missile does to its target
real radius - the collision radius of the missile. It won't collide if this is 0. Not that this only applies if arc is 0.
real aoe - the aoe of the damage. If this is 0, the missile will only damage the unit hits.
real arc - the height arc value of the missile, similar to the object editor value "Art - Missile Arc". This must be between 0 and 2.

JASS:
library MissileAPI initializer Init needs Missile
    globals
        private boolexpr filter
       
        private group ENUM_GROUP = CreateGroup()
        private real Damage = 0.00
    endglobals
   
    private function TheFilter takes nothing returns boolean
        //if the unit is an enemy of the caster and it isn't immune to magic then...
        if IsUnitEnemy(GetFilterUnit(), GetOwningPlayer(GetEventMissile.Caster)) and not IsUnitType(GetFilterUnit(), UNIT_TYPE_MAGIC_IMMUNE) then
            //make the caster damage it
            call UnitDamageTarget(GetEventMissile.Caster, GetFilterUnit(), Damage, false, true, ATTACK_TYPE_NORMAL, DAMAGE_TYPE_UNIVERSAL, null)
        endif
       
        //return false - don't add the unit to the group
        return false
    endfunction
   
    private struct DamageData extends MissileActions
        real damage
        real aoe
       
        method onHit takes nothing returns boolean
            local unit target = null
           
            //if the missile is designed for a single target then...
            if .aoe <= 0 then
                //if the missile is on target then...
                if GetEventMissile.OnTarget then
                    //if the missile is homing, the hit unit will be its original target
                    if GetEventMissile.Homing then
                        set target = GetEventMissile.Target
                    endif
                    //otherwise, nothing has been hit
                //if the missile isn't on target then it has hit something so...
                else
                    //set the target to a random one of the units it has hit (the group
                    //very probably contains only 1 unit anyway)
                    set target = FirstOfGroup(GetEventMissile.HitUnits)
                endif
               
                //if the missile has actually hit a target then...
                if target != null then
                    //damage the target
                    call UnitDamageTarget(GetEventMissile.Caster, target, .damage, false, true, ATTACK_TYPE_NORMAL, DAMAGE_TYPE_UNIVERSAL, null)
                endif
               
                //clean leaks
                set target = null
                //return false to destroy the missile
                return false
            endif
           
            //if the missile is designed for more than one target then...
            //store the damage in a global variable and...
            set Damage = .damage
            //loop through all the units in range with the filter.
            call GroupEnumUnitsInRange(ENUM_GROUP, GetEventMissile.X, GetEventMissile.Y, .aoe, filter)
           
            //return false to destroy the missile.
            return false
        endmethod
    endstruct
   
    private struct ParabolaData extends MissileActions
        real arc
        unit target = null
        real damage
        real aoe
       
        //Thanks to Shadow1500 for this function
        method parabola takes nothing returns real
            local real t = (GetEventMissile.CurrentDist*2)/GetEventMissile.StartDist-1
            return (-t*t+1)*(GetEventMissile.StartDist/.arc)
        endmethod
       
        method onHit takes nothing returns boolean
            if .aoe <= 0 and .target != null then
                call UnitDamageTarget(GetEventMissile.Caster, .target, .damage, false, true, ATTACK_TYPE_NORMAL, DAMAGE_TYPE_UNIVERSAL, null)
                return false
            endif
           
            set Damage = .damage
            call GroupEnumUnitsInRange(ENUM_GROUP, GetEventMissile.X, GetEventMissile.Y, .aoe, filter)
           
            return false
        endmethod
       
        method onLoop takes nothing returns boolean
            call SetUnitFlyHeight(GetEventMissile.Dummy, .parabola(), 0.00)
            return true
        endmethod
    endstruct
       
    function CreateHomingMissile takes unit caster, unit target, string sfx, real speed, real damage, real radius, real aoe, real arc returns nothing
        local ParabolaData d0
        local DamageData   d1
        local MissileActions d
        local Missile m
        //if there is no arc then...
        if arc <= 0. then
            //create the actions
            set d1 = DamageData.create()
            //store the damage and aoe in the data
            set d1.damage = damage
            set d1.aoe = aoe
           
            set d = d1
        //otherwise...
        else
            //if the arc is over the limit, reduce it to the limit
            if arc > 2. then
                set arc = 2.
            endif
           
            //arc missiles can't have a "radius"
            set radius = 0.
            //create the actions
            set d0 = ParabolaData.create()
            //store the arc, target, damage and aoe in the data
            set d0.arc = arc
            set d0.target = target
            set d0.damage = damage
            set d0.aoe = aoe
           
            set d = d0
        endif
        //create the missile with the actions
        set m = Missile.create(sfx, GetUnitX(caster), GetUnitY(caster), radius, d)
        //fire the missile at the target unit
        if arc <= 0 then
            call m.fireTarget(caster, target, speed)
        else
            call m.fireFixed(caster, GetUnitX(target), GetUnitY(target), speed)
        endif
    endfunction
   
    function CreatePointMissile takes unit caster, real targetX, real targetY, string sfx, real speed, real damage, real radius, real aoe, real arc returns nothing
        local ParabolaData d0
        local DamageData   d1
        local MissileActions d
        local Missile m
        //if there is no arc then...
        if arc <= 0. then
            //create the actions
            set d1 = DamageData.create()
            //store the damage and aoe in the data
            set d1.damage = damage
            set d1.aoe = aoe
           
            set d = d1
        //otherwise...
        else
            //if the arc is over the limit, reduce it to the limit
            if arc > 2. then
                set arc = 2.
            endif
           
            //arc missiles can't have a "radius"
            set radius = 0.
            //create the actions
            set d0 = ParabolaData.create()
            //store the arc, damage and aoe in the data
            set d0.arc = arc
            set d0.damage = damage
            set d0.aoe = aoe
           
            set d = d0
        endif
        //create the missile with the actions
        set m = Missile.create(sfx, GetUnitX(caster), GetUnitY(caster), radius, d)
        //fire the missile at the target point
        call m.fireFixed(caster, targetX, targetY, speed)
    endfunction
   
    private function Init takes nothing returns nothing
        //initialise the filter
        set filter = Filter(function TheFilter)
    endfunction
endlibrary

JASS:
scope Jump initializer Init

globals
    private constant real ARC           = 1.8 //the jump arc
    private constant integer SPELL_ID   = 'A001' //the dummy ability id
    private constant integer FLY_HACK   = 'Amrf' //the ability id of crow form
    private constant real MISSILE_SPEED = 500.00 //the speed of the missile
endglobals

//data struct, containing a single boolean
private struct Data
    boolean paused = false
endstruct

function Conditions takes nothing returns boolean
    return GetSpellAbilityId() == SPELL_ID
endfunction

//Thanks to Shadow1500 for this function
private function JumpParabola takes real dist, real maxdist returns real
    local real t = (dist*2)/maxdist-1
    return (-t*t+1)*(maxdist/ARC)
endfunction

private struct Actions extends MissileActions
    //The onHit function, this stops the unit from moving as a missile and unpauses it. It also
    //destroys the attached data
    method onHit takes nothing returns boolean
        local Data d = GetEventMissile.data
        //unpause the unit
        call PauseUnit(GetEventMissile.Dummy, false)
        //destroy the attached data
        call d.destroy()
        //return false to stop the missile
        return false
    endmethod

    //The onLoop function, this sets the unit's fly height to the appropriate value for the
    //parabola
    method onLoop takes nothing returns boolean
        //use Shadow1500's function for a jump parabola to get the current fly height
        local real parabola = JumpParabola(GetEventMissile.CurrentDist, GetEventMissile.StartDist)
        //store the attached data in a usable variable
        local Data d = GetEventMissile.data
        //set the unit's fly height to the height defined by the parabola
        call SetUnitFlyHeight(GetEventMissile.Dummy, parabola, 0.00)
        //The following is necessary to do after the initial cast action because not doing so
        //can cause the spell's cooldown and mana cost to function
        //incorrectly. Also, if it is not done, the unit will repeatedly try to cast the spell
        //after it has landed, until another order is issued.
        if not d.paused then //if the attached data says the unit is not already paused then..
            call PauseUnit(GetEventMissile.Dummy, true) //pause the unit
            set d.paused = true //and tell the function it is paused
        endif
        return true //returning true means the missile keeps moving
    endmethod
endstruct

private function TrigActions takes nothing returns nothing
    local location target = GetSpellTargetLoc()
    //get the target x and y values from the location variable
    local real x = GetLocationX(target)
    local real y = GetLocationY(target)
    //create the missile from the trigger unit, with a radius 0 - we don't want any collision
    //also defines the onHit and onLoop actions.
    local Missile m = Missile.createFromUnit(GetTriggerUnit(), 0.00, Actions.create())
    //attaches data to the missile
    set m.data = Data.create()
    //add, then remove the crow form ability to give the unit the ability to fly
    call UnitAddAbility(GetTriggerUnit(), FLY_HACK)
    call UnitRemoveAbility(GetTriggerUnit(), FLY_HACK)
    //finally, fire the missile (make the unit jump) at speed 500.00
    call m.fireFixed(GetTriggerUnit(), x, y, MISSILE_SPEED)
   
    call RemoveLocation(target)
    set target = null
endfunction

//===========================================================================
private function Init takes nothing returns nothing
    //create the trigger and register the conditions / actions
    local trigger t = CreateTrigger()
    call TriggerRegisterAnyUnitEventBJ( t, EVENT_PLAYER_UNIT_SPELL_EFFECT )
    call TriggerAddCondition( t, Condition( function Conditions ) )
    call TriggerAddAction( t, function TrigActions )
endfunction

endscope
JASS:
scope Fireball initializer Init

globals
    private constant integer SPELL_ID       = 'A000' //the dummy ability id
    //the missile model
    private constant string SFX             = "Abilities\\Weapons\\FireBallMissile\\FireBallMissile.mdl"
    private constant real AOE               = 250.00 //the explosion radius
    private constant real MISSILE_RADIUS    = 50.00 //the collision radius
    private constant real MISSILE_SPEED     = 500.00 //the speed of the missile
    private constant real DAMAGE            = 100.00 //the damage of the missile
    private constant real MAX_RANGE         = 600.00 //the maximum distance a firebal may
                                                     //travel before it automatically
                                                     //explodes
   
    private group ENUM_GROUP = CreateGroup() //a group used for instant enumerations
    private boolexpr filter //the filter to decide which units are damaged by the fireball
endglobals

private function Conditions takes nothing returns boolean
    return GetSpellAbilityId() == SPELL_ID
endfunction

//the filter function - checks if the enumerated unit is an enemy of the caster, and that
//it's not immune to magic.
private function TheFilter takes nothing returns boolean
    return IsUnitEnemy(GetFilterUnit(), GetOwningPlayer(GetEventMissile.Caster)) and not IsUnitType(GetFilterUnit(), UNIT_TYPE_MAGIC_IMMUNE)
endfunction

//the actions struct, containing onHit and onLoop actions
private struct Actions extends MissileActions
    method onHit takes nothing returns boolean
        local unit u //unit used in the group loop
        local unit caster = GetEventMissile.Caster //it's easier to type "caster" than
                                                   //"GetEventMissile.Caster"
       
        //if this onHit call was caused by the missile reaching it's target, we do not
        //want it to explode.
        if GetEventMissile.OnTarget then
            set caster = null
            return true
        endif
       
        //enumerate the units within range which match the filter
        call GroupEnumUnitsInRange(ENUM_GROUP, GetEventMissile.X, GetEventMissile.Y, AOE, filter)
       
        //if no units matched the enumeration, don't explode the missile
        if FirstOfGroup(ENUM_GROUP) == null then
            set caster = null
            return true
        endif
       
        //loop through the enumerated units, damaging them
        loop
            set u = FirstOfGroup(ENUM_GROUP)
            exitwhen u == null
           
            //make the caster damage the unit with fire damage
            call UnitDamageTarget(caster, u, DAMAGE, false, true, ATTACK_TYPE_MAGIC, DAMAGE_TYPE_FIRE, null)
           
            call GroupRemoveUnit(ENUM_GROUP, u)
        endloop
       
        //clean leaks
        set caster = null
       
        //return false to stop the missile
        return false
    endmethod
   
    method onLoop takes nothing returns boolean
        //if the missile has travelled further than the maximum range, explode it
        if GetEventMissile.CurrentDist >= MAX_RANGE then
            call .onHit()
            return false
        endif
        return true
    endmethod
endstruct

private function TrigActions takes nothing returns nothing
    local location target = GetSpellTargetLoc()
    //find the start x/y and target x/y
    local real x0 = GetUnitX(GetTriggerUnit())
    local real y0 = GetUnitY(GetTriggerUnit())
    local real x1 = GetLocationX(target)
    local real y1 = GetLocationY(target)
    //create the missile at the start x/y
    local Missile m = Missile.create(SFX, x0, y0, MISSILE_RADIUS, Actions.create())
    //fire the missile towards the target x/y
    call m.fireFixed(GetTriggerUnit(), x1, y1, MISSILE_SPEED)
   
    call RemoveLocation(target)
    set target = null
endfunction

//===========================================================================
private function Init takes nothing returns nothing
    //create the trigger / register actions, conditions and events
    local trigger t = CreateTrigger()
    call TriggerRegisterAnyUnitEventBJ(t, EVENT_PLAYER_UNIT_SPELL_EFFECT)
    call TriggerAddCondition(t, Condition(function Conditions ))
    call TriggerAddAction(t, function TrigActions)
   
    //preload the effects to prevent lag
    call Preload(SFX)
   
    //create the filter
    set filter = Filter(function TheFilter)
endfunction

endscope
JASS:
scope LightningHail initializer Init

globals
    private constant integer SPELL_ID       = 'A002' //the dummy ability id
    private constant integer DUMMY_ID       = 'LSdm' //the dummy unit
    private constant player OWNER           = Player(15) //the player to own the dummy unit
    private constant real MISSILE_SPEED     = 200.00 //the speed of the missile
    private constant real SMALL_SPEED       = 50.00 //the speed of the small balls which
                                                    //spawn when the missile explodes
    private constant real STEP_SIZE         = 200.00 //how far the missile moves for each of
                                                     //the below variables to take effect
    private constant real AREA_PER_STEP     = 50.00 //how much wider the area grows per step
    private constant real AOE_PER_STEP      = 25.00 //how much more AOE the smaller balls
                                                    //have per step
    private constant real DAMAGE_PER_STEP   = 50.00 //how much more damage the balls do per
                                                    //step
    private constant real HEIGHT_PER_STEP   = 100.00//how high the ball rises per step
    private constant real SIZE_PER_STEP     = 1.00  //how much the ball grows per step
    private constant integer NUM_BALLS      = 6     //how many smaller balls spawn when the
                                                    //large ball explodes
    private constant real 2PI               = 2 * bj_PI //constant to speed up things with
                                                        //the radians - equivalent to 360
                                                        //degress
   
    private group ENUM_GROUP = CreateGroup() //group used for instant enumerations
    private boolexpr filter //filter for the units hittable by the spell
endglobals

private function Conditions takes nothing returns boolean
    return GetSpellAbilityId() == SPELL_ID
endfunction

//the filter checks if the unit is an enemy of the caster and that the unit isn't immune to
//magic
private function TheFilter takes nothing returns boolean
    return IsUnitEnemy(GetFilterUnit(), GetOwningPlayer(GetEventMissile.Caster)) and not IsUnitType(GetFilterUnit(), UNIT_TYPE_MAGIC_IMMUNE)
endfunction

//Data struct for the small balls
private struct Data2
    real damage //how much damage they will deal
    real aoe    //the area of effect in which they will deal the damage
    real z      //the current height of the balls
    real zfall  //the amount the balls fall by in each onLoop call
endstruct

private struct Actions2 extends MissileActions
    method onHit takes nothing returns boolean
        local Data2 d = GetEventMissile.data //store the missile data in a usable variable
        local unit u //used for the group loop
       
        //enumerate all the damageable units within the aoe
        call GroupEnumUnitsInRange(ENUM_GROUP, GetEventMissile.X, GetEventMissile.Y, d.aoe, filter)
       
        //loopp through the enumerated units, damaging them respectively
        loop
            set u = FirstOfGroup(ENUM_GROUP)
            exitwhen u == null
           
            //cause the caster to damage the unit with lightning damage
            call UnitDamageTarget(GetEventMissile.Caster, u, d.damage, false, true, ATTACK_TYPE_MAGIC, DAMAGE_TYPE_LIGHTNING, null)
           
            call GroupRemoveUnit(ENUM_GROUP, u)
        endloop
       
        //destroy the attached data
        call d.destroy()
        //kill the dummy unit
        call KillUnit(GetEventMissile.Dummy)
       
        //return false so the missile is destroyed
        return false
    endmethod
   
    method onLoop takes nothing returns boolean
        local Data2 d = GetEventMissile.data //store the missile data in a usable variable
       
        set d.z = d.z - d.zfall //reduce the current height variable
        call SetUnitFlyHeight(GetEventMissile.Dummy, d.z, 0.00) //set the unit's height to
                                                                //the appropriate value
       
        return true //return true so the missile keeps moving
    endmethod
endstruct

//Data struct for the original missile
private struct Data1
    real z = 0 //the current height of the missile
    real size = 1.00 //the current size of the missile
endstruct

//The Actions struct for the original missile
private struct Actions1 extends MissileActions
    method onHit takes nothing returns boolean
        local Data1 d = GetEventMissile.data //store the missile data in a usable variable
       
        //calculate the damage for each of the balls
        local real damage= (DAMAGE_PER_STEP / STEP_SIZE) * GetEventMissile.CurrentDist
        //calculate the area over which to spread the balls
        local real area  = (AREA_PER_STEP   / STEP_SIZE) * GetEventMissile.CurrentDist
        //calculate the aoe of the balls
        local real aoe   = (AOE_PER_STEP    / STEP_SIZE) * GetEventMissile.CurrentDist
       
        //the current ball's angle
        local real angle = 0.00
        //the amount to increase the angle by for each ball
        local real step  = 2PI / NUM_BALLS
        //the x/y coordinates of the current ball's target point
        local real x
        local real y
       
        //the current ball's dummy unit
        local unit u
        //the current ball
        local Missile m
        //the current ball's data
        local Data2 d2
       
        loop
            //exit the loop when the angle reaches 360 degrees
            exitwhen angle >= 2PI
           
            //calculate the x/y coordinates of the target point
            set x = GetEventMissile.X + area * Cos(angle)
            set y = GetEventMissile.Y + area * Sin(angle)
           
            //create the dummy unit / set the correct scale and fly height for it
            set u = CreateUnit(OWNER, DUMMY_ID, GetEventMissile.X, GetEventMissile.Y, 0)
            call SetUnitFlyHeight(u, d.z, 0.00)
            call SetUnitScale(u, d.size / NUM_BALLS, 0.00, 0.00)
           
            //create the missile using the newly created dummy unit
            set m = Missile.createFromUnit(u, 0.00, Actions2.create())
            //fire the missile at the target x/y
            call m.fireFixed(GetEventMissile.Caster, x, y, SMALL_SPEED)
            //create the data
            set d2 = Data2.create()
            //store the damage
            set d2.damage = damage
            //store the aoe
            set d2.aoe = aoe
            //store the current height
            set d2.z = d.z
            //store the amount to reduce the height by each onLoop call
            set d2.zfall = (d2.z / (m.StartDist / SMALL_SPEED)) / Missile_FPS
            //store the data on the missile
            set m.data = d2
           
            //increase the angle ready for the next missile
            set angle = angle + step
        endloop
       
        //destroy the attached data
        call d.destroy()
        //kill the dummy unit
        call KillUnit(GetEventMissile.Dummy)
       
        //clean leaks
        set u = null
       
        //return false to destroy the missile
        return false
    endmethod
   
    method onLoop takes nothing returns boolean
        local Data1 d = GetEventMissile.data //store the missile data in a usable variable
       
        //calculate the new size and make the dummy unit that size
        set d.size = 1.00 + (SIZE_PER_STEP   / STEP_SIZE) * GetEventMissile.CurrentDist
        call SetUnitScale(GetEventMissile.Dummy, d.size, 0., 0.)
       
        //calculate the new fly height and set the height of the dummy to that value
        set d.z    =        (HEIGHT_PER_STEP / STEP_SIZE) * GetEventMissile.CurrentDist
        call SetUnitFlyHeight(GetEventMissile.Dummy, d.z, 0.00)
       
        return true
    endmethod
endstruct

private function TrigActions takes nothing returns nothing
    local location target = GetSpellTargetLoc()
    //find the start x/y and target x/y
    local real x0 = GetUnitX(GetTriggerUnit())
    local real y0 = GetUnitY(GetTriggerUnit())
    local real x1 = GetLocationX(target)
    local real y1 = GetLocationY(target)
   
    //create the dummy unit for the missile
    local unit u = CreateUnit(OWNER, DUMMY_ID, x0, y0, 0)
    //create the missile using the dummy unit
    local Missile m = Missile.createFromUnit(u, 0.00, Actions1.create())
    //create and attach a data struct
    set m.data = Data1.create()
    //fire the missile at the target x/y
    call m.fireFixed(GetTriggerUnit(), x1, y1, MISSILE_SPEED)
   
    //clean leaks
    set u = null
    call RemoveLocation(target)
    set target = null
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 Conditions ))
    call TriggerAddAction(t, function TrigActions)
   
    set filter = Filter(function TheFilter)
endfunction

endscope
JASS:
scope ChainDeath initializer Init

globals
    private constant integer SPELL_ID       = 'A003' //the dummy ability id
   
    //the model for the missile
    private constant string MISSILE_ART     = "Abilities\\Spells\\Undead\\DeathCoil\\DeathCoilMissile.mdl"
    //the model for the effects on the target when the missile hits
    private constant string TARGET_ART      = "Abilities\\Spells\\Undead\\DeathCoil\\DeathCoilSpecialArt.mdl"
   
    //the damage the first target recieves
    private constant real DAMAGE            = 300.00
    //the percentage of the previous damage done to the next target
    private constant real DAMAGE_MULTIPLIER = 0.75
    //the percentage of the damage done to heal the caster by
    private constant real PCT_LIFE_STEAL    = 0.25
    //the speed of the missile
    private constant real MISSILE_SPEED     = 750.00
    //the maximum range between units for the spell to chain
    private constant real JUMP_RANGE        = 600.00
   
    //the maximum number of times the spell chains
    private constant integer NUM_JUMPS      = 6
   
    //group used in instant enumerations
    private group ENUM_GROUP                = CreateGroup()
    //filter for the closest unit function
    private boolexpr filter
    //values for the closest unit function
    private real CurX
    private real CurY
    private real CurDist
    private unit CurUnit
endglobals

private function Conditions takes nothing returns boolean
    return GetSpellAbilityId() == SPELL_ID
endfunction

//necessary for the structs to be used above where they are declared
private keyword Data
private keyword Actions

//the filter function
private function TheFilter takes nothing returns boolean
    local real dx
    local real dy
    local real dist
    local Data d = GetEventMissile.data //store the missile data in a usable variable
   
    //check if the unit is a valid target
    if IsUnitEnemy(GetFilterUnit(), GetOwningPlayer(GetEventMissile.Caster)) and not IsUnitType(GetFilterUnit(), UNIT_TYPE_MAGIC_IMMUNE) and not IsUnitInGroup(GetFilterUnit(), d.hit) and GetWidgetLife(GetFilterUnit()) > 0.405 then
        //calculate the distance between the point and the unit
        set dx = GetUnitX(GetFilterUnit()) - CurX
        set dy = GetUnitY(GetFilterUnit()) - CurY
        set dist = SquareRoot(dx * dx + dy * dy)
       
        //calculate the absolute value of the distance
        if dist < 0 then
            set dist = -dist
        endif
       
        //if the distance between this unit and the point is less than the previous distance
        //then update the closest unit
        if dist <= CurDist then
            set CurDist = dist
            set CurUnit = GetFilterUnit()
        endif
    endif
   
    //don't bother adding the unit to the group
    return false
endfunction

//The closest unit function...
private function GetClosestUnit takes nothing returns unit
    set CurDist = JUMP_RANGE //initialise the current distance variable
    set CurUnit = null //initialise the current unit variable
    //loop through the units in range with the filter
    call GroupEnumUnitsInRange(ENUM_GROUP, CurX, CurY, JUMP_RANGE, filter)
    //return the closest unit
    return CurUnit
endfunction

//function to add more modularity to the spell, this bascially makes the missiles
private function MakeDeathMissile takes real x, real y, unit caster, unit target returns Missile
    //create the missile at the specified x/y coordinates
    local Missile m = Missile.create(MISSILE_ART, x, y, 0.00, Actions.create())
    //fire the missile at the target
    call m.fireTarget(caster, target, MISSILE_SPEED)
    //return the new missile
    return m
endfunction

//Data struct for the missile
private struct Data
    real damage = DAMAGE //the amount of damage the missile will do when it hits
    integer num = 0 //the number of jumps already made
    group hit = CreateGroup() //the units already jumped to
endstruct

private struct Actions extends MissileActions
    method onHit takes nothing returns boolean
        local Data d = GetEventMissile.data //store the missile data in a usable variable
        //the new target unit of the missile
        local unit target
        //the new missile
        local Missile m
        //the life of the caster before adding the health absorbed
        local real life = GetUnitState(GetEventMissile.Caster, UNIT_STATE_LIFE)
       
        //the x/y coordinates of where the missile is
        set CurX = GetEventMissile.X
        set CurY = GetEventMissile.Y
       
        //create a special effect using the TARGET_ART model on the hit unit
        call DestroyEffect(AddSpecialEffectTarget(TARGET_ART, GetEventMissile.Target, "origin"))
        //damage the hit unit
        call UnitDamageTarget(GetEventMissile.Caster, GetEventMissile.Target, d.damage, false, true, ATTACK_TYPE_MAGIC, DAMAGE_TYPE_UNIVERSAL, null)
        //increase the caster's health by a percentage of the damage dealt
        call SetUnitState(GetEventMissile.Caster, UNIT_STATE_LIFE, life + d.damage * PCT_LIFE_STEAL)
       
        //if the missile has jumped the maximum number of times, stop the spell
        if d.num >= NUM_JUMPS then
            //clean the group leak
            call GroupClear(d.hit)
            call DestroyGroup(d.hit)
            //destroy the data
            call d.destroy()
            //return false to destroy the missile
            return false
        endif
       
        //add the hit unit to the group of units already hit
        call GroupAddUnit(d.hit, GetEventMissile.Target)
       
        //acquire a new target
        set target = GetClosestUnit()
       
        //if there is no valid unit within the range, stop the spell
        if target == null then
            //clean the group leak
            call GroupClear(d.hit)
            call DestroyGroup(d.hit)
            //destroy the data
            call d.destroy()
            //return false to destroy the missile
            return false
        endif
       
        //make the new missile and fire it at the new target
        set m = MakeDeathMissile(CurX, CurY, GetEventMissile.Caster, target)
        //calculate the damage for the next missile
        set d.damage = d.damage * DAMAGE_MULTIPLIER
        //increment the num variable
        set d.num = d.num + 1
        //store the data in the new missile
        set m.data = d
       
        //return false to destroy the missile
        return false
    endmethod
endstruct

private function TrigActions takes nothing returns nothing
    //the x/y coordinates of the starting point of the missile
    local real x = GetUnitX(GetTriggerUnit())
    local real y = GetUnitY(GetTriggerUnit())
    //create the missile and fire it at the spell target unit
    local Missile m = MakeDeathMissile(x, y, GetTriggerUnit(), GetSpellTargetUnit())
    //create the data and store it on the missile
    set m.data = Data.create()
endfunction

//===========================================================================
private function Init takes nothing returns nothing
    //create the trigger, register events, actions and conditions
    local trigger t = CreateTrigger()
    call TriggerRegisterAnyUnitEventBJ(t, EVENT_PLAYER_UNIT_SPELL_EFFECT)
    call TriggerAddCondition(t, Condition(function Conditions ))
    call TriggerAddAction(t, function TrigActions)
   
    //preload the effects
    call Preload(MISSILE_ART)
    call Preload(TARGET_ART)
   
    //initialise the filter
    set filter = Filter(function TheFilter)
endfunction

Code:
v1.00 - Initial Release
v1.10 - Added the API for ease of use for creating basic missiles
v1.11 - Fixed a few location leaks in the spells


Keywords:
missile, fireball, lightning, ball, jump, chain, death, system, ElementOfWater, Element, of, Water, EoW
Contents

Just another Warcraft III map (Map)

Reviews
BPower: 16:26, 24th Feb 2016 Reason for re-review: Nowadays the spell section is packed full of missile systems from different authors, therefore a more qualified moderator comment than "this is neat stuff" is required. There is keen...
Level 3
Joined
Mar 13, 2009
Messages
62
Lets see,
-Jump is little useless, sorry
-Fireball is same like from warlocks RPG, but ok
-Chain death is cool
-Lighting ball is cool too

[APPROVED] :)
 
Level 7
Joined
Mar 8, 2009
Messages
360
When you jump ontop of another unit you are moved next to the unit but i can't find where this is done in your code. Can you tell me how you do this because my toss spell doesn't land the unit when there is another unit on the landing location.
Thx.
 
Level 17
Joined
Mar 17, 2009
Messages
1,349
Vulcano said:
When you jump ontop of another unit you are moved next to the unit but i can't find where this is done in your code. Can you tell me how you do this because my toss spell doesn't land the unit when there is another unit on the landing location.
Thx.
Well it's been a long time since I checked the code of this system, but usually that is done by turning off the collision of the target unit before tossing, and then turning it on again once the toss is over.
Hope that helps :)
 
@Vulcano/Deuterium

That's not how it was done, Deuterium - the function SetUnitX/Y ignore units' pathing, so they move the unit to the specified location no matter what's in it's way. When the spell finishes (in the onHit method) I use SetUnitPosition, which checks pathing, and places the unit at the nearest pathable point to where you specifiy.
 
Level 7
Joined
Mar 8, 2009
Messages
360
I tried it but it's not working, the unit model stays at start position , the health bars moves up in the air and when the health bar lands the model is moved too to the health bar location, on top of the unit if there is one :s. Now gonna try deut's method.

EDIT: With deut's method i have the same effect as with my old method.
 
Level 7
Joined
Mar 8, 2009
Messages
360
At the end of the toss I do this but it's not working:
JASS:
call SetUnitPathing(toss.launchUnit, true)
call PauseUnit(toss.launchUnit, false)
call SetUnitPosition( toss.launchUnit, GetUnitX(toss.launchUnit), GetUnitY(toss.launchUnit))
 
Level 4
Joined
Feb 20, 2010
Messages
97
Cool system, but how can I make the fireball to explore on obstacles like cliffs?

I also found out, that if you shoot too many fast missiles (speed of 9k, decent enough for a rail gun) then they tend to skip the targets (tested on a massive amount of units standing in a line) and also tends to crash the game. Seems to be only useful for slow missiles.
 
Last edited:
Top