'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.
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
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
CreateTargetMissile
Fires a missile at the target.CreatePointMissile
Fires a missile at coordinates targetX/targetYunit caster
- the unit who does the damagingstring sfx
- the model path of the missilereal speed
- how far the missile moves per secondreal damage
- how much damage the missile does to its targetreal 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.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
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
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
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
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
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