scope AerialAid/*
*************************** Aerial Aid v1.01 *****************************
by Flux
SPELL DESCRIPTION:
Calls for an air support containing goblin paratrooper soldiers. The Plane
is equipped with 2/3/4 guns which fires at enemy units in its field of sight
targeting heroes and units with lower hitpoints. Upon activation, 'Aerial Aid'
is replaced by the sub-skill 'Deploy Paratroopers' which commands your backup
soldiers to parachute from the Plane.
REQUIRES:
(nothing)
OPTIONALLY REQUIRES:
- BonusMod and SetUnitMaxState
If not found, it will generate it's own object editor data to manipulate the
stats per level.
Recommended if your other spells uses BonusMod or SetUnitMaxState as optional
requirement, avoiding redundant functions and object editor abilities.
- DummyRecycler/MissileRecycler
If not found, it will create new units for all the dummy units needed
everytime the spell is cast. Dummy units are used as Parachute, Visual Effects
and Bullet Projectile.
Strongly recommended because this spell creates a lot of units.
- SpellEffectEvent
If not found, it will create another trigger with 'A unit Starts the effect of an
ability' event.
Strongly recommended for maps with huge number of spells.
- Table
If not found, this spell will require 1 hashtable. Hashtable is limited to 256
per map.
Recommended if you used a lot of hashtable in your map to avoid reaching the
hashtable limit.
- WorldBounds
If not found, this spell will create 4 more variables for the map boundary parameters.
Recommended if your other spells uses WorldBounds as optional requirement, avoiding
redundant variables.
---------------------------------- CREDITS: --------------------------------------------
OPTIONAL SYSTEMS:
Bribe - MissileRecycler, SpellEffectEvent, Table
Earth-Fury - BonusMod, SetUnitMaxState
Flux - DummyRecycler
Magtheridon96 - RegisterPlayerUnitEvent
Nestharus - WorldBounds
MODELS:
Anitarf, iNfraNe, Vexorian - Attachable Dummy Model with Pitch Animation
WILL THE ALMIGHTY - Goblin Scout, Goblin Soldier
NatDis - Dwarven Air Force DF 35 Fighter
Fingolfin - Parachute
Grey Knight - Bullet
Tranquil - Snipe Target Effect (edited)
ICONS:
Sin'dorei300 - BTNGoblinRifleman, BTNGoblinFireworker
Marcos DAB - BTNGoblinHeroAviator
SKINS:
Erkki2 - Blue Laser
-----------------------------------------------------------------------------------------*/
native UnitAlive takes unit u returns boolean
//====================================================================================
//============================== CONFIGURATION =======================================
//====================================================================================
globals
//Rawcodes
private constant integer SPELL_ID = 'Aaid'
private constant integer SPELL_ID_DEPLOY = 'Adpy'
private constant integer TOGGLE_ID = 'AAtg'
//Dummy Unit Properties
private constant integer DUMMY_ID = 'dumi'
private constant player DUMMY_OWNER = Player(14)
//Summoned Units
private constant integer PARATROOPER_AIR_ID = 'pair'
private constant integer PLANE_ID = 'plne'
private constant integer TRANSFORM_ID = 'AaTF'
//Periodic Timing of the Spell (Plane and Paratrooper timing)
private constant real TIMEOUT = 0.03125
//Visual Effects
private constant string PARACHUTE_MODEL = "war3mapImported\\Parachute.mdl"
private constant string TARGET_MODEL = "war3mapImported\\Target.mdl"
private constant string TARGET_ATTACHMENT = "overhead"
private constant string LASER_CODE = "LASR" //Laser string code as configured in LightningData.slk
private constant real LASER_DISTANCE = 50 //Max Distance of straight laser lightning in the curve
private constant string BULLET_MODEL = "war3mapImported\\Bullet.mdl"
//Attack and Damage types of Plane Gun
private constant attacktype ATTACK_TYPE = ATTACK_TYPE_NORMAL
private constant damagetype DAMAGE_TYPE = DAMAGE_TYPE_NORMAL
private constant weapontype WEAPON_TYPE = WEAPON_TYPE_WHOKNOWS
//Preload abilities at Map Loading Screen to avoid lag spike the first time it is cast?
private constant boolean PRELOAD = true
//************************************************************************************
//**************************** PLANE CONFIGURATION ***********************************
//************************************************************************************
private constant real PLANE_HEIGHT_INITIAL = 1200
private constant real PLANE_HEIGHT = 600
private constant real PLANE_FALL_SPEED = 700
//************************************************************************************
//***************************** GUN CONFIGURATION ************************************
//************************************************************************************
//When the absolute z difference between the plane and target is PLANE_HEIGHT, the
//plane can't hit units under this range.
//When the absolute z difference between the plane and target is 0, the
//plane can hit units directly in front of it even at close range.
private constant real MIN_HIT_DISTANCE = 500
//Conic Radius for the Plane Gun (degrees)
private constant real PLANE_SIGHT_ANGLE = 40
//Plane Gun will prioritize targeting hero
private constant boolean PRIORITIZE_HERO = true
//Speed of Gun bullets (displacement per second)
private constant real BULLET_SPEED = 3200
//Is the BULLET_SPEED considered a 3D speed? If false, it will be considered as a 2D speed
//meaning the horizontal (ground) speed is constant.
private constant boolean BULLET_SPEED_3D = true
//An optional different TIMEOUT for the Bullet projectiles just in case
private constant real BULLET_TIMEOUT = TIMEOUT
//Collision Size of Bullet
private constant real BULLET_COLLISION = 110
//Gun Field of Sight visible to enemy? Recommended to be set to false
private constant boolean VISIBLE_LASER = false
//************************************************************************************
//************************ PARATROOPER CONFIGURATION *********************************
//************************************************************************************
//When the Paratrooper's height is below GND_HEIGHT, it is now targeted
//as a ground unit
private constant real GND_HEIGHT = 100
//Does the Paratroopers have a Timed Life?
private constant boolean APPLY_TIMED_LIFE = false
//Is the Paratrooper vulnerable while on air?
private constant boolean CAN_BE_HIT = true
//When the current height of the Paratrooper is less than PARACHUTE_ACTIVATE, it will
//activate its parachute
private constant real PARACHUTE_ACTIVATE = 400
//How fast Paratroopers accelerates when their parachute is not yet used (speed per second)
private constant real PARATROOPER_GRAVITY = 150
//How fast Paratroopers will fall upon using their parachute (displacement per second)
private constant real PARACHUTE_FALL_SPEED = 180
//When the Paratrooper hits the ground at this z-speed (displacement per second), it will die!
private constant real SPEED_WITH_DAMAGE = 400
//The lower the values, the greater the friction (for calculation simplicity)
private constant real DRAG_COEFFICIENT = 0.3
endglobals
//************************************************************************************
//*************************** PLANE CONFIGURATION ************************************
//************************************************************************************
//Plane Hitpoints per level
private function PlaneLife takes integer lvl returns real
return 200.0 + 100.0*lvl //300, 400, 500
endfunction
//Plane Bonus armor per level.
private function PlaneArmor takes integer lvl returns integer
return 1 + 1*lvl //2, 3, 4
endfunction
//How fast the Plane moves (displacement per second)
private function PlaneSpeed takes integer lvl returns real
return 350.0 + 0.0*lvl
endfunction
//How long the Plane will last
private function Duration takes integer lvl returns real
return 8.0 + 0.0*lvl //0.0*lvl exist to make the function inline
endfunction
//************************************************************************************
//***************************** GUN CONFIGURATION ************************************
//************************************************************************************
//Search radius for gun target
private function GunRadius takes integer lvl returns real
return 1400 + 0.0*lvl
endfunction
//How fast the gun fires
private function GunFiringRate takes integer lvl returns real
return 0.1 + 0.0*lvl //Every 0.1 second, each Plane gun will fire
endfunction
//Damage per gun bullet
private function GunDamage takes integer lvl returns real
return 4.0 + 1.0*lvl //5, 6, 7
endfunction
//Number of Plane's gun per level
private function GunCount takes integer lvl returns integer
return 1 + lvl //2, 3, 4
endfunction
//What gets hit by the Gun
private function TargetFilter takes unit u, player owner returns boolean
return UnitAlive(u) and IsUnitEnemy(u, owner)
endfunction
//Position of Guns with reference to the Plane's center when the Plane is facing 0 degrees.
//gunNumber starts at 1
private function GunOffsetX takes integer gunNumber, integer totalGunNumber returns real
return 0. + 0*gunNumber + 0*totalGunNumber //All guns will belong to the same x-axis of the Plane
endfunction
private function GunOffsetY takes integer gunNumber, integer totalGunNumber returns real
if totalGunNumber == 2 then
return -150. + gunNumber*100 //-50, 50
elseif totalGunNumber == 3 then
return -100. + gunNumber*50 //-50, 0, 50
elseif totalGunNumber == 4 then
return -100. + gunNumber*40 //-60, -20, 20, 60
endif
return 0.
endfunction
//************************************************************************************
//************************** PARATROOPER CONFIGURATION *******************************
//************************************************************************************
//Paratrooper Hitpoints per level
private function ParatrooperLife takes integer lvl returns real
return 100.0 + 50.0*lvl //150, 200, 250
endfunction
//Paratrooper Bonus Damage per level (See also Attack Cooldown of Paratrooper to balance)
private function ParatrooperAttackDamage takes integer lvl returns integer
return 2 + lvl //3, 4, 5
endfunction
//Paratrooper Bonus Armor per level
private function ParatrooperArmor takes integer lvl returns integer
return lvl //1, 2, 3
endfunction
//Paratrooper Spawnrate (second)
private function SpawnRate takes integer lvl returns real
return 1.0 - 0.2*lvl //Every 0.8/0.6/0.4 second, a Paratrooper will spawn
endfunction
//The maximum number of paratroopers the spell can spawn
private function MaxSpawnCount takes integer lvl returns integer
return 3 + lvl //4, 5, 6
endfunction
static if APPLY_TIMED_LIFE then
//Only applicable if APPLY_TIMED_LIFE = true
//Determines how long each Paratrooper last.
private function TimedLife takes integer lvl returns real
return 7.0 + 1.0*lvl
endfunction
endif
//====================================================================================
//============================ END CONFIGURATION =====================================
//====================================================================================
static if not LIBRARY_BonusMod then
//For the static if to work, the variables must be a static member of a struct
private struct S1 extends array
static constant integer BONUS_ARMOR = 0
static constant integer BONUS_DAMAGE = 1
static integer array pow2
static integer array bonusAbility
endstruct
private function UnitSetBonus2 takes unit u, integer bonusType, integer amount returns boolean
local integer i = 7
loop
if amount >= S1.pow2[i] then
call UnitAddAbility(u, S1.bonusAbility[bonusType*8 + i])
call UnitMakeAbilityPermanent(u, true, S1.bonusAbility[bonusType*7 + i])
set amount = amount - S1.pow2[i]
endif
set i = i - 1
exitwhen i < 0 or amount == 0
endloop
return true
endfunction
private function BonusMod_Init takes nothing returns nothing
local integer i = 0
static if PRELOAD then
local unit u = CreateUnit(DUMMY_OWNER, PARATROOPER_AIR_ID, 0, 0, 0)
endif
// Bonus Mod - Armor abilitys
set S1.bonusAbility[i + 0] = 'ZxA0' // +1
set S1.bonusAbility[i + 1] = 'ZxA1' // +2
set S1.bonusAbility[i + 2] = 'ZxA2' // +4
set S1.bonusAbility[i + 3] = 'ZxA3' // +8
set S1.bonusAbility[i + 4] = 'ZxA4' // +16
set S1.bonusAbility[i + 5] = 'ZxA5' // +32
set S1.bonusAbility[i + 6] = 'ZxA6' // +64
set S1.bonusAbility[i + 7] = 'ZxA7' // +128
// Bonus Mod - Damage abilitys
set i = 8
set S1.bonusAbility[i + 0] = 'ZxB0' // +1
set S1.bonusAbility[i + 1] = 'ZxB1' // +2
set S1.bonusAbility[i + 2] = 'ZxB2' // +4
set S1.bonusAbility[i + 3] = 'ZxB3' // +8
set S1.bonusAbility[i + 4] = 'ZxB4' // +16
set S1.bonusAbility[i + 5] = 'ZxB5' // +32
set S1.bonusAbility[i + 6] = 'ZxB6' // +64
set S1.bonusAbility[i + 7] = 'ZxB7' // +128
//Initialize cached power of 2
set S1.pow2[0] = 1
set i = 1
loop
set S1.pow2[i] = 2*S1.pow2[i - 1]
set i = i + 1
exitwhen i == 8
endloop
//Preload
static if PRELOAD then
set i = 0
loop
exitwhen i > 15
call UnitAddAbility(u, S1.bonusAbility[i])
set i = i + 1
endloop
call RemoveUnit(u)
set u = null
endif
endfunction
endif
static if not LIBRARY_SetUnitMaxState then
private struct S2 extends array
static integer array pow2
endstruct
private function SetUnitMaxState2 takes unit u, unitstate state, real newValue returns nothing
local integer newVal = R2I(newValue)
local integer i = 8
local integer offset
set newVal = newVal - R2I(GetUnitState(u, state))
if newVal > 0 then
set offset = 11
elseif newVal < 0 then
set offset = 2
set newVal = -newVal
else
return
endif
loop
exitwhen newVal == 0 or i < 0
if newVal >= S2.pow2[i] then
call UnitAddAbility(u, 'Zx01')
call SetUnitAbilityLevel(u, 'Zx01', offset + i)
call UnitRemoveAbility(u, 'Zx01')
set newVal = newVal - S2.pow2[i]
else
set i = i - 1
endif
endloop
endfunction
private function SetUnitMaxState_Init takes nothing returns nothing
local integer i = 1
static if PRELOAD then
local unit u = CreateUnit(DUMMY_OWNER, PARATROOPER_AIR_ID, 0, 0, 0)
endif
set S2.pow2[0] = 1
loop
set S2.pow2[i] = 2*S2.pow2[i - 1]
set i = i + 1
exitwhen i == 11
endloop
static if PRELOAD then
call UnitAddAbility(u, 'Zx01')
call RemoveUnit(u)
set u = null
endif
endfunction
endif
static if not LIBRARY_WorldBounds then
//Uses a private struct WorldBounds2
private struct WorldBounds2 extends array
readonly static real maxX
readonly static real maxY
readonly static real minX
readonly static real minY
private static method onInit takes nothing returns nothing
local rect map = GetWorldBounds()
set thistype.maxX = GetRectMaxX(map)
set thistype.maxY = GetRectMaxY(map)
set thistype.minX = GetRectMinX(map)
set thistype.minY = GetRectMinY(map)
call RemoveRect(map)
set map = null
endmethod
endstruct
endif
globals
private location l = Location(0, 0)
private real playMinX
private real playMaxX
private real playMinY
private real playMaxY
endglobals
private struct Paratrooper
private unit soldier
private unit parachute
private effect parachuteModel
private boolean activated
private boolean transformed
private integer damage //bonus damage is lost upon transformation
//that's why this needs to be stored
//Movement
private real z
private real dz
private thistype next
private thistype prev
private static timer t = CreateTimer()
//constants to avoid repeating calculations
//constants to fill in the TIMEOUT
private static constant real GRAVITY = -PARATROOPER_GRAVITY*TIMEOUT
private static constant real SPEED_DAMAGE = SPEED_WITH_DAMAGE*TIMEOUT
private static constant real TERMINAL_VELOCITY = -PARACHUTE_FALL_SPEED*TIMEOUT
static if not LIBRARY_BonusMod then
private static constant integer BONUS_ARMOR = 0
private static constant integer BONUS_DAMAGE = 1
endif
//Calls when the soldier landed
private method destroy takes nothing returns nothing
//Remove from List
set .prev.next = .next
set .next.prev = .prev
if thistype(0).next == 0 then
call PauseTimer(t)
endif
//Remove Parachute
call DestroyEffect(.parachuteModel)
static if LIBRARY_DummyRecycler then
call DummyAddRecycleTimer(.parachute, 3.0)
elseif LIBRARY_MissileRecycler then
call RecycleMissile(.parachute)
else
call UnitApplyTimedLife(.parachute, 'BTLF', 3.0)
endif
set .parachuteModel = null
set .soldier = null
set .parachute = null
call .deallocate()
endmethod
//! textmacro AERIAL_AID_PARATROOPER_UPDATE
set .z = .z + .dz
if .activated then
if .dz < TERMINAL_VELOCITY then
//Apply drag (air-resistance)
set .dz = .dz*DRAG_COEFFICIENT
if .dz > TERMINAL_VELOCITY then
set .dz = TERMINAL_VELOCITY
endif
else
//increase speed due to gravity until it reaches terminal velocity
set .dz = .dz + GRAVITY
endif
call SetUnitX(.parachute, GetUnitX(.soldier))
call SetUnitY(.parachute, GetUnitY(.soldier))
else
set .dz = .dz + GRAVITY
if .z < PARACHUTE_ACTIVATE then
set .activated = true
//Create the Parachute
static if LIBRARY_DummyRecycler then
set .parachute = GetRecycledDummy(GetUnitX(.soldier), GetUnitY(.soldier), .z, GetRandomReal(0, 360))
elseif LIBRARY_MissileRecycler then
set .parachute = GetRecycledMissile(GetUnitX(.soldier), GetUnitY(.soldier), .z, GetRandomReal(0, 360))
else
set .parachute = CreateUnit(DUMMY_OWNER, DUMMY_ID, GetUnitX(.soldier), GetUnitY(.soldier), GetRandomReal(0, 360))
call SetUnitFlyHeight(.parachute, .z, 0)
endif
set .parachuteModel = AddSpecialEffectTarget(PARACHUTE_MODEL, .parachute, "origin")
endif
endif
if .z <= 0 then
call SetUnitFlyHeight(.parachute, .z, 0)
//For realism effect
if .dz < -SPEED_DAMAGE then
call KillUnit(.soldier)
endif
call .destroy()
else
if .activated then
call SetUnitFlyHeight(.parachute, .z, 0)
endif
if .z < GND_HEIGHT and not .transformed then
call UnitAddAbility(.soldier, TRANSFORM_ID)
call UnitRemoveAbility(.soldier, TRANSFORM_ID)
set .transformed = true
static if LIBRARY_BonusMod then
call UnitSetBonus(.soldier, BONUS_DAMAGE, .damage)
else
call UnitSetBonus2(.soldier, BONUS_DAMAGE, .damage)
endif
endif
call SetUnitFlyHeight(.soldier, .z, 0)
endif
//! endtextmacro
private static method pickAll takes nothing returns nothing
local thistype this = thistype(0).next
local real x
local real y
loop
exitwhen this == 0
//Using a textmacro to save a function call per paratrooper instance
//! runtextmacro AERIAL_AID_PARATROOPER_UPDATE()
set this = .next
endloop
endmethod
static method create takes player owner, integer lvl, real x, real y, real z returns thistype
local thistype this = .allocate()
local real spd = 0
//Paratrooper Initialization
set .transformed = false
set .activated = false
set .z = z
set .dz = 0
//Create the soldier
set .soldier = CreateUnit(owner, PARATROOPER_AIR_ID, x, y, GetRandomReal(0, 360))
static if APPLY_TIMED_LIFE then
call UnitApplyTimedLife(.soldier, 'BTLF', TimedLife(lvl))
endif
call MoveLocation(l, x, y)
call SetUnitFlyHeight(.soldier, .z - GetLocationZ(l), 0)
static if not CAN_BE_HIT then
call UnitAddAbility(.soldier, 'Avul')
endif
//Stats
set .damage = ParatrooperAttackDamage(lvl)
static if LIBRARY_BonusMod then
call UnitSetBonus(.soldier, BONUS_DAMAGE, .damage)
call UnitSetBonus(.soldier, BONUS_ARMOR, ParatrooperArmor(lvl))
else
call UnitSetBonus2(.soldier, BONUS_DAMAGE, .damage)
call UnitSetBonus2(.soldier, BONUS_ARMOR, ParatrooperArmor(lvl))
endif
static if LIBRARY_SetUnitMaxState then
call SetUnitMaxState(.soldier, UNIT_STATE_MAX_LIFE, ParatrooperLife(lvl))
else
call SetUnitMaxState2(.soldier, UNIT_STATE_MAX_LIFE, ParatrooperLife(lvl))
endif
//Linked-list
set .next = 0
set .prev = thistype(0).prev
set .next.prev = this
set .prev.next = this
if .prev == 0 then
call TimerStart(t, TIMEOUT, true, function thistype.pickAll)
endif
return this
endmethod
endstruct
private struct Bullet
private player owner
private unit source
private unit u
private effect bulletModel
private real damage
private boolean lineCollision
private real x
private real y
private real z
private real dx
private real dy
private real dz
private real dxy
private real dxyz
private thistype next
private thistype prev
private static timer t = CreateTimer()
private static group g = CreateGroup()
method destroy takes nothing returns nothing
//Remove from List
set .prev.next = .next
set .next.prev = .prev
if thistype(0).next == 0 then
call PauseTimer(t)
endif
//Clean handles
static if LIBRARY_DummyRecycler then
call DummyAddRecycleTimer(.u, 1.0)
elseif LIBRARY_MissileRecycler then
call RecycleMissile(.u)
else
call UnitApplyTimedLife(.u, 'BTLF', 1.0)
endif
call DestroyEffect(.bulletModel)
set .u = null
set .source = null
set .bulletModel = null
call .deallocate()
endmethod
private static constant real BULLET_COLLISION2 = BULLET_COLLISION*BULLET_COLLISION
//! textmacro AERIAL_AID_BULLET_LOCALS
local real x
local real y
local real z
local real dx
local real dy
local real dz
local real height
local real dist
local real checkDist
local unit u
local unit hit
local boolean withinBound
//line data for line collision
local real xPrev
local real yPrev
local real zPrev
local real radius
local real dxL
local real dyL
local real dzL
local real projection
//! endtextmacro
//! textmacro AERIAL_AID_BULLET_UPDATE
if .lineCollision then
//Store last position
set xPrev = .x
set yPrev = .y
set zPrev = .z
set radius = 0.5*.dxy
endif
//Initially no one is hit
set hit = null
//Update position
set .x = .x + .dx
set .y = .y + .dy
set .z = .z + .dz
call MoveLocation(l, .x, .y)
set height = .z - GetLocationZ(l)
static if LIBRARY_WorldBounds then
set withinBound = .x > WorldBounds.minX and .x < WorldBounds.maxX and .y > WorldBounds.minY and .y < WorldBounds.maxY
else
set withinBound = .x > WorldBounds2.minX and .x < WorldBounds2.maxX and .y > WorldBounds2.minY and .y < WorldBounds2.maxY
endif
if withinBound then
call SetUnitX(.u, .x)
call SetUnitY(.u, .y)
call SetUnitFlyHeight(.u, height, 0)
endif
//Check Collision
if .lineCollision then //when bullet moves to fast
set dxL = .x - xPrev
set dyL = .y - yPrev
set dzL = .z - zPrev
set checkDist = (.dxyz + BULLET_COLLISION)*(.dxyz + BULLET_COLLISION)
call GroupEnumUnitsInRange(g, xPrev + 0.5*.dx, yPrev + 0.5*.dy, radius + 2*BULLET_COLLISION, null)
loop
set u = FirstOfGroup(g)
exitwhen u == null
call GroupRemoveUnit(g, u)
set x = GetUnitX(u)
set y = GetUnitY(u)
call MoveLocation(l, x, y)
set z = GetLocationZ(l) + GetUnitFlyHeight(u)
set dx = x - xPrev
set dy = y - yPrev
set dz = z - zPrev
//Scalar Projection of vector (xPrev -> x, yPrev -> y) on line (xPrev -> x, yPrev -> y)
set projection = (dx*dxL + dy*dyL + dz*dzL)/(.dxyz)
set dist = dx*dx + dy*dy + dz*dz //distance of target to line point1
//dist - projection*projection = 3D distance of the picked unit to the line
if dist - projection*projection <= BULLET_COLLISION2 then
if TargetFilter(u, .owner) then
if dist < checkDist then //if it is closer to the line point1
set checkDist = dist
set hit = u
endif
endif
endif
endloop
else
set checkDist = BULLET_COLLISION2
call GroupEnumUnitsInRange(g, .x, .y, BULLET_COLLISION, null)
loop
set u = FirstOfGroup(g)
exitwhen u == null
call GroupRemoveUnit(g, u)
set x = GetUnitX(u)
set y = GetUnitY(u)
call MoveLocation(l, x, y)
set z = GetLocationZ(l) + GetUnitFlyHeight(u)
set dx = x - .x
set dy = y - .y
set dz = z - .z
//Check if it collides in 3D
set dist = dx*dx + dy*dy + dz*dz //distance of target to bullet
if dist <= checkDist then
if TargetFilter(u, .owner) then
set checkDist = dist
set hit = u
endif
endif
endloop
endif
if hit != null then
call UnitDamageTarget(.source, hit, .damage, false, false, ATTACK_TYPE, DAMAGE_TYPE, WEAPON_TYPE)
call .destroy()
set hit = null
elseif height < 0 or height > 1500 or not withinBound then
call .destroy()
endif
//! endtextmacro
private static method pickAll takes nothing returns nothing
local thistype this = thistype(0).next
//! runtextmacro AERIAL_AID_BULLET_LOCALS()
loop
exitwhen this == 0
//Use textmacro to save a function call per bullet instance
//! runtextmacro AERIAL_AID_BULLET_UPDATE()
set this = .next
endloop
endmethod
static method create takes real x1, real y1, real z1, real x2, real y2, real z2, unit source, player owner, real damage returns thistype
local thistype this = .allocate()
local real dx = x2 - x1
local real dy = y2 - y1
local real dz = z2 - z1
local real facing = Atan2(dy, dx)
local real pitch = Atan(dz/SquareRoot(dx*dx + dy*dy))
static if BULLET_SPEED_3D then
set .dxy = BULLET_SPEED*BULLET_TIMEOUT*Cos(pitch)
else
set .dxy = BULLET_SPEED*BULLET_TIMEOUT
endif
//Initialize positions and speeds
set .x = x1
set .y = y1
set .z = z1
set .dx = dxy*Cos(facing)
set .dy = dxy*Sin(facing)
static if BULLET_SPEED_3D then
set .dz = BULLET_SPEED*BULLET_TIMEOUT*Sin(pitch)
set .dxyz = BULLET_SPEED*BULLET_TIMEOUT
else
set .dz = BULLET_SPEED*BULLET_TIMEOUT*Tan(pitch)
if pitch == 0 then
set .dxyz = .dxy
else
set .dxyz = .dz/Sin(pitch)
endif
endif
set .lineCollision = dxy > BULLET_COLLISION
//Initialize bullet interact properties
set .source = source
set .owner = owner
set .damage = damage
//Create Bullet and set appropriate angle
//Safety precaution, make sure bullet is not outside bounds
static if LIBRARY_WorldBounds then
if x1 > WorldBounds.maxX then
set x1 = WorldBounds.maxX
elseif x1 < WorldBounds.minX then
set x1 = WorldBounds.minX
endif
if y1 > WorldBounds.maxY then
set y1 = WorldBounds.maxY
elseif y1 < WorldBounds.minY then
set y1 = WorldBounds.minY
endif
else
if x1 > WorldBounds2.maxX then
set x1 = WorldBounds2.maxX
elseif x1 < WorldBounds2.minX then
set x1 = WorldBounds2.minX
endif
if y1 > WorldBounds2.maxY then
set y1 = WorldBounds2.maxY
elseif y1 < WorldBounds2.minY then
set y1 = WorldBounds2.minY
endif
endif
call MoveLocation(l, x1, y1)
static if LIBRARY_DummyRecycler then
set .u = GetRecycledDummy(x1, y1, z1 - GetLocationZ(l), facing*bj_RADTODEG)
elseif LIBRARY_MissileRecycler then
set .u = GetRecycledMissile(x1, y1, z1 - GetLocationZ(l), facing*bj_RADTODEG)
else
set .u = CreateUnit(DUMMY_OWNER, DUMMY_ID, x1, y1, facing*bj_RADTODEG)
call SetUnitFlyHeight(.u, z1 - GetLocationZ(l), 0)
endif
set .bulletModel = AddSpecialEffectTarget(BULLET_MODEL, .u, "origin")
call SetUnitAnimationByIndex(.u, R2I(pitch*bj_RADTODEG + 90.5))
//Linked-list
set .next = 0
set .prev = thistype(0).prev
set .next.prev = this
set .prev.next = this
if .prev == 0 then
call TimerStart(t, BULLET_TIMEOUT, true, function thistype.pickAll)
endif
return this
endmethod
endstruct
private struct Gun
private effect targetSfx
readonly unit target
readonly real dx
readonly real dy
readonly thistype next
readonly thistype prev
method destroy takes nothing returns nothing
//Remove from List
set .prev.next = .next
set .next.prev = .prev
if .target != null then
call DestroyEffect(.targetSfx)
endif
set .targetSfx = null
set .target = null
call .deallocate()
endmethod
method update takes unit newTarget returns nothing
if newTarget != .target then
if .target != null then
call DestroyEffect(.targetSfx)
endif
if newTarget != null then
set .targetSfx = AddSpecialEffectTarget(TARGET_MODEL, newTarget, TARGET_ATTACHMENT)
endif
endif
set .target = newTarget
endmethod
static method create takes thistype head, real dx, real dy returns thistype
local thistype this = .allocate()
set .dx = dx
set .dy = dy
set .target = null
set .next = head.next
set .prev = head
set .next.prev = this
set .prev.next = this
return this
endmethod
static method head takes nothing returns thistype
local thistype this = .allocate()
set .next = 0
set .prev = 0
return this
endmethod
endstruct
//The self-sorting list, using an Insertion Sort algorithm
private struct List
private real value
readonly unit unit
readonly thistype next
readonly thistype prev
method destroy takes nothing returns nothing
//Remove List
set .prev.next = .next
set .next.prev = .prev
set .value = 0
set .next = 0
set .unit = null
call .deallocate()
endmethod
static method empty takes nothing returns nothing
local thistype this = thistype(0).next
loop
exitwhen this == 0
call .destroy()
set this = thistype(0).next
endloop
endmethod
static method add takes unit u, real hp, integer count returns nothing
local thistype node = thistype(0).next //head node is thistype(0)
local boolean insert = true
local thistype this
static if PRIORITIZE_HERO then
local boolean isHero = IsUnitType(u, UNIT_TYPE_HERO)
endif
//Insert in the current list
loop
exitwhen node == 0
static if PRIORITIZE_HERO then
if isHero then
exitwhen hp < node.value or not IsUnitType(node.unit, UNIT_TYPE_HERO)
else
exitwhen hp < node.value and not IsUnitType(node.unit, UNIT_TYPE_HERO)
endif
else
exitwhen hp < node.value
endif
set count = count - 1
//If it will not get included in the count anyway, end the insertion and do not insert it in the list
if count == 0 then
set insert = false
exitwhen true
endif
set node = node.next
endloop
//Insert 'this' before 'node'
if insert then
set this = .allocate()
set .unit = u
set .value = hp
set .next = node
set .prev = node.prev
set .next.prev = this
set .prev.next = this
endif
endmethod
endstruct
private struct Effect
private lightning laser
private real x1
private real y1
private real x2
private real y2
private thistype next
private thistype prev
private method destroy takes nothing returns nothing
//Remove from List
set .prev.next = .next
set .next.prev = .prev
//Clean ups
call DestroyLightning(.laser)
set .laser = null
call .deallocate()
endmethod
method destroyAll takes nothing returns nothing
local thistype node = .next
loop
exitwhen node == 0
call node.destroy()
set node = node.next
endloop
call .destroy()
endmethod
//Used on head instances
method updateHead takes real dx, real dy returns nothing
local thistype node = .next
loop
exitwhen node == 0
set node.x1 = node.x1 + dx
set node.y1 = node.y1 + dy
set node.x2 = node.x2 + dx
set node.y2 = node.y2 + dy
call MoveLightningEx(node.laser, false, node.x1, node.y1, 0, node.x2, node.y2, 0)
set node = node.next
endloop
endmethod
static method create takes thistype head, real x1, real y1, real x2, real y2, player p returns thistype
local thistype this = .allocate()
set .laser = AddLightning(LASER_CODE, false, x1, y1, x2, y2)
static if not VISIBLE_LASER then
if IsPlayerEnemy(GetLocalPlayer(), p) then
call SetLightningColor(.laser, 0, 0, 0, 0)
endif
endif
set .x1 = x1
set .y1 = y1
set .x2 = x2
set .y2 = y2
set .next = head.next
set .prev = head
set .next.prev = this
set .prev.next = this
return this
endmethod
private static constant real PLANE_SIGHT_HALF_ANGLE_RAD = 0.5*PLANE_SIGHT_ANGLE*bj_DEGTORAD
static method head takes real x, real y, real angle, real radius, player owner returns thistype
local thistype this = .allocate()
local real dx = Cos(angle + PLANE_SIGHT_HALF_ANGLE_RAD)
local real dy = Sin(angle + PLANE_SIGHT_HALF_ANGLE_RAD)
local real limit = angle + PLANE_SIGHT_HALF_ANGLE_RAD
local real tempAngle
local real tempX
local real tempY
set .next = 0
set .prev = 0
//Create AREA_SFX radius
call Effect.create(this, x + MIN_HIT_DISTANCE*dx, y + MIN_HIT_DISTANCE*dy, x + radius*dx, y + radius*dy, owner)
set dx = Cos(angle - PLANE_SIGHT_HALF_ANGLE_RAD)
set dy = Sin(angle - PLANE_SIGHT_HALF_ANGLE_RAD)
call Effect.create(this, x + MIN_HIT_DISTANCE*dx, y + MIN_HIT_DISTANCE*dy, x + radius*dx, y + radius*dy, owner)
//Create AREA_SFX curve
//! textmacro AERIAL_AID_AREA_SFX_CURVE takes RADIUS
set tempAngle = angle - PLANE_SIGHT_HALF_ANGLE_RAD
set tempX = x + $RADIUS$*Cos(tempAngle)
set tempY = y + $RADIUS$*Sin(tempAngle)
loop
set tempAngle = tempAngle + LASER_DISTANCE/$RADIUS$
exitwhen tempAngle > limit
set dx = x + $RADIUS$*Cos(tempAngle)
set dy = y + $RADIUS$*Sin(tempAngle)
call Effect.create(this, tempX, tempY, dx, dy, owner)
set tempX = dx
set tempY = dy
endloop
call Effect.create(this, tempX, tempY, x + $RADIUS$*Cos(limit), y + $RADIUS$*Sin(limit), owner)
//! endtextmacro
//! runtextmacro AERIAL_AID_AREA_SFX_CURVE("MIN_HIT_DISTANCE")
//! runtextmacro AERIAL_AID_AREA_SFX_CURVE("radius")
return this
endmethod
endstruct
private struct Plane
//Spell Mechanics
private unit caster
private unit plane
private player owner
private integer lvl
private boolean toggled
private boolean outside
private real duration
//Gun-related
private Gun gunHead
private real radius
private integer gunCount
private real gunDamage
private real gunFireTime
private real gunCtr //counter for gun firing rate
//Paratrooper-related
private real soldierSpawnRate
private real soldierCtr //counter for paratrooper deployment
private boolean deploy
private integer spawnCtr //number of paratrooper deployed
//Visual Effect related
private Effect sfxHead
//Movement
private real x //Contains the location of the Plane, the Plane is not meant
private real y //to be moved by Moving effects such as Swap, Hook, Knockback, etc.
private real z
private real angle
private real dx
private real dy
private real dz
private thistype next
private thistype prev
private static timer t = CreateTimer()
private static group g = CreateGroup()
static if LIBRARY_Table then
private static Table tb
private static Table tbCount
else
private static hashtable hash = InitHashtable()
endif
static if not LIBRARY_BonusMod then
private static constant integer BONUS_ARMOR = 0
endif
private method destroy takes nothing returns nothing
local integer id = GetHandleId(.caster)
//Remove from List
set .prev.next = .next
set .next.prev = .prev
if thistype(0).next == 0 then
call PauseTimer(t)
endif
//Table Clean-up
static if LIBRARY_Table then
set tbCount[id] = tbCount[id] - 1
if tbCount[id] == 0 then
call tb.remove(id)
call tbCount.remove(id)
call UnitRemoveAbility(.caster, TOGGLE_ID)
call UnitRemoveAbility(.caster, SPELL_ID_DEPLOY)
endif
else
call SaveInteger(hash, id, 1, LoadInteger(hash, id, 1) - 1)
if LoadInteger(hash, id, 1) == 0 then
call FlushChildHashtable(hash, id)
call UnitRemoveAbility(.caster, TOGGLE_ID)
call UnitRemoveAbility(.caster, SPELL_ID_DEPLOY)
endif
endif
//Destroy Handles
if UnitAlive(.plane) then
call RemoveUnit(.plane)
endif
//For perfect clean-up
set .caster = null
set .plane = null
//Recycle Index
call .deallocate()
endmethod
//Avoid recalculating them everytime
private static constant real PLANE_SIGHT_HALF_ANGLE_RAD = 0.5*PLANE_SIGHT_ANGLE*bj_DEGTORAD
private static constant real SLOPE_CHECK = MIN_HIT_DISTANCE/PLANE_HEIGHT
private method update takes nothing returns nothing
local Gun gun = .gunHead.next
local boolean withinPlay //within Playable Area
local List list
local unit u
local real x
local real y
local real z
local real dx
local real dy
local real angle
local real diff
local real height
//Movement
set .x = .x + .dx
set .y = .y + .dy
//Safety check
set withinPlay = .x < playMaxX and .x > playMinX and .y < playMaxY and .y > playMinY
if withinPlay then
call SetUnitX(.plane, .x)
call SetUnitY(.plane, .y)
endif
//Hide/Unhide the Plane when leaving/entering playable map area
if .outside then
if withinPlay then
set .outside = false
call ShowUnit(.plane, true)
endif
else
if not withinPlay then
set .outside = true
call ShowUnit(.plane, false)
endif
endif
if .duration > 0 and UnitAlive(.plane) then
//Change Plane Height
if .z > PLANE_HEIGHT then
set .z = .z + .dz
if .z < PLANE_HEIGHT then
set .z = PLANE_HEIGHT
endif
call SetUnitFlyHeight(.plane, .z, 0)
endif
//Will only do some action if it has stable height
if .z == PLANE_HEIGHT then
//Create SFX Laser Border when the Plane is active, it will only happen once
if .sfxHead == 0 then
set .sfxHead = Effect.head(.x, .y, .angle, .radius, .owner)
else
//Update SFX
call .sfxHead.updateHead(.dx, .dy)
endif
set .duration = .duration - TIMEOUT
//Targeting
call GroupEnumUnitsInRange(g, .x, .y, .radius, null)
call MoveLocation(l, .x, .y)
set z = GetLocationZ(l) + PLANE_HEIGHT
loop
set u = FirstOfGroup(g)
exitwhen u == null
call GroupRemoveUnit(g, u)
if TargetFilter(u, .owner) then
set x = GetUnitX(u)
set y = GetUnitY(u)
set dx = x - .x
set dy = y - .y
set angle = Atan2(dy, dx)
set diff = RAbsBJ(angle - .angle)
//Check if it is within range of gun angle
if RMinBJ(diff, 2*bj_PI - diff) <= PLANE_SIGHT_HALF_ANGLE_RAD then
//Then check if it is within range of gun pitch
call MoveLocation(l, x, y)
//GetUnitFlyHeight(u) + GetLocationZ(l) = absolute z of the target
//recycle variable 'real angle' as the check distance
set angle = SLOPE_CHECK*(z - GetUnitFlyHeight(u) - GetLocationZ(l)) //recycled real-type variable
if dx*dx + dy*dy >= angle*angle then
call List.add(u, GetWidgetLife(u), .gunCount)
endif
endif
endif
endloop
//Update Gun Targets
set list = List(0).next
loop
exitwhen gun == 0
call gun.update(list.unit)
set list = list.next
//If there are more guns than targets, go back to the first target so that all guns are active
if list == 0 then
set list = List(0).next
endif
set gun = gun.next
endloop
//Empty the list
call List.empty()
//Are the guns ready to fire?
set .gunCtr = .gunCtr + TIMEOUT
if .gunCtr >= .gunFireTime then
set .gunCtr = .gunCtr - .gunFireTime
set gun = .gunHead.next
loop
exitwhen gun == 0 or gun.target == null
set x = GetUnitX(gun.target)
set y = GetUnitY(gun.target)
call MoveLocation(l, x, y)
call Bullet.create(.x + gun.dx, .y + gun.dy, z, x, y, GetUnitFlyHeight(gun.target) + GetLocationZ(l), .plane, .owner, .gunDamage)
set gun = gun.next
endloop
endif
//Are paratroopers commanded to deploy?
if .deploy and .spawnCtr < MaxSpawnCount(.lvl) then
set .soldierCtr = .soldierCtr + TIMEOUT
if .soldierCtr >= .soldierSpawnRate then
set .soldierCtr = .soldierCtr - .soldierSpawnRate
if withinPlay then
call Paratrooper.create(.owner, .lvl, .x, .y, .z)
set .spawnCtr = .spawnCtr + 1
endif
endif
endif
endif
else
//Destroy Guns
if .gunHead != 0 then
loop
exitwhen gun == 0
call gun.destroy()
set gun = gun.next
endloop
call .gunHead.destroy()
set .gunHead = 0
endif
//Destroy Effects
if .sfxHead != 0 then
call .sfxHead.destroyAll()
set .sfxHead = 0
endif
//Make the plane rise again until it is no longer seen in the screen
set .z = .z - .dz
if .z > PLANE_HEIGHT_INITIAL or not UnitAlive(.plane) then
call .destroy()
else
call SetUnitFlyHeight(.plane, .z, 0)
endif
endif
endmethod
private static method pickAll takes nothing returns nothing
local thistype this = thistype(0).next
loop
exitwhen this == 0
call .update()
set this = .next
endloop
endmethod
//Delayed toggle
private static method toggle takes nothing returns nothing
local timer delay = GetExpiredTimer()
static if LIBRARY_Table then
local thistype this = tb[GetHandleId(delay)]
else
local thistype this = LoadInteger(hash, GetHandleId(delay), 0)
endif
set .toggled = true
call UnitAddAbility(.caster, TOGGLE_ID)
call UnitAddAbility(.caster, SPELL_ID_DEPLOY)
call DestroyTimer(delay)
set delay = null
endmethod
private static method onCast takes nothing returns nothing
local thistype this = .allocate()
local timer delay = CreateTimer()
local integer i = 1
local integer id
local real x
local real y
local real cos
local real sin
local real spd
local real gunX
local real gunY
//Spell Data and Initializations
set .caster = GetTriggerUnit()
set .owner = GetTriggerPlayer()
set .deploy = false
set .sfxHead = 0
set .lvl = GetUnitAbilityLevel(.caster, SPELL_ID)
set .spawnCtr = 0
set .gunHead = Gun.head()
set .gunCtr = 0
set .gunFireTime = GunFiringRate(.lvl)
set .gunDamage = GunDamage(.lvl)
set .soldierCtr = 0
set .soldierSpawnRate = SpawnRate(.lvl)
set .duration = Duration(.lvl)
set .radius = GunRadius(.lvl)
set x = GetSpellTargetX()
set y = GetSpellTargetY()
set .angle = Atan2(y - GetUnitY(.caster), x - GetUnitX(.caster))
set cos = Cos(.angle)
set sin = Sin(.angle)
set .gunCount = GunCount(.lvl)
loop
exitwhen i > .gunCount
set gunX = GunOffsetX(i, .gunCount)
set gunY = GunOffsetY(i, .gunCount)
call Gun.create(.gunHead, gunX*cos - gunY*sin, gunY*cos + gunX*sin)
set i = i + 1
endloop
//Movement Data
set spd = PlaneSpeed(.lvl)*TIMEOUT
set .dx = spd*cos
set .dy = spd*sin
set .dz = -PLANE_FALL_SPEED*TIMEOUT
//Calculate the spawn loc based on duration and target location with an offset for the rise/fall of the plane
set .x = x - (0.5*.duration + (PLANE_HEIGHT_INITIAL - PLANE_HEIGHT)/PLANE_FALL_SPEED)*PlaneSpeed(.lvl)*cos
set .y = y - (0.5*.duration + (PLANE_HEIGHT_INITIAL - PLANE_HEIGHT)/PLANE_FALL_SPEED)*PlaneSpeed(.lvl)*sin
set .z = PLANE_HEIGHT_INITIAL
if .x > playMaxX or .x < playMinX or .y > playMaxY or .y < playMinY then
//Safety precaution, make sure plane is not outside bounds to prevent fatal error
//recycle 'x' and 'y' as the spawn loc, because .x and .y must not be overwritten
set x = .x
set y = .y
static if LIBRARY_WorldBounds then
if x > WorldBounds.maxX then
set x = WorldBounds.maxX
elseif x < WorldBounds.minX then
set x = WorldBounds.minX
endif
if y > WorldBounds.maxY then
set y = WorldBounds.maxY
elseif y < WorldBounds.minY then
set y = WorldBounds.minY
endif
else
if x > WorldBounds2.maxX then
set x = WorldBounds2.maxX
elseif x < WorldBounds2.minX then
set x = WorldBounds2.minX
endif
if y > WorldBounds2.maxY then
set y = WorldBounds2.maxY
elseif y < WorldBounds2.minY then
set y = WorldBounds2.minY
endif
endif
set .outside = true
set .plane = CreateUnit(.owner, PLANE_ID, x, y, .angle*bj_RADTODEG)
call ShowUnit(.plane, false)
else
set .outside = false
set .plane = CreateUnit(.owner, PLANE_ID, .x, .y, .angle*bj_RADTODEG)
endif
call SetUnitFlyHeight(.plane, .z, 0)
static if not CONTROLLABLE then
call SetUnitTurnSpeed(.plane, 0.0)
endif
//Stats
static if LIBRARY_BonusMod then
call UnitSetBonus(.plane, BONUS_ARMOR, PlaneArmor(lvl))
else
call UnitSetBonus2(.plane, BONUS_ARMOR, PlaneArmor(lvl))
endif
static if LIBRARY_SetUnitMaxState then
call SetUnitMaxState(.plane, UNIT_STATE_MAX_LIFE, PlaneLife(lvl))
else
call SetUnitMaxState2(.plane, UNIT_STATE_MAX_LIFE, PlaneLife(lvl))
endif
//Store caster handle id for onDeploy data retrieval
set id = GetHandleId(.caster)
static if LIBRARY_Table then
set tb[id] = this
set tbCount[id] = tbCount[id] + 1
set tb[GetHandleId(delay)] = this
else
call SaveInteger(hash, id, 0, this)
call SaveInteger(hash, id, 1, LoadInteger(hash, id, 1) + 1)
call SaveInteger(hash, GetHandleId(delay), 0, this)
endif
//Toggle Ability
call TimerStart(delay, 0., false, function thistype.toggle)
//Linked-list
set .next = 0
set .prev = thistype(0).prev
set .next.prev = this
set .prev.next = this
if .prev == 0 then
call TimerStart(t, TIMEOUT, true, function thistype.pickAll)
endif
endmethod
private static method onDeploy takes nothing returns nothing
static if LIBRARY_Table then
local thistype this = tb[GetHandleId(GetTriggerUnit())]
else
local thistype this = LoadInteger(hash, GetHandleId(GetTriggerUnit()), 0)
endif
set .deploy = true
//Toggle Ability
set .toggled = false
call UnitRemoveAbility(.caster, TOGGLE_ID)
call UnitRemoveAbility(.caster, SPELL_ID_DEPLOY)
endmethod
static if not LIBRARY_SpellEffectEvent then
private static method castCondition takes nothing returns boolean
local integer spell = GetSpellAbilityId()
if spell == SPELL_ID then
call thistype.onCast()
elseif spell == SPELL_ID_DEPLOY then
call thistype.onDeploy()
endif
return false
endmethod
endif
private static method onInit takes nothing returns nothing
local integer i = 0
static if PRELOAD then
local unit u1 = CreateUnit(DUMMY_OWNER, PARATROOPER_AIR_ID, 0, 0, 0)
local unit u2 = CreateUnit(DUMMY_OWNER, PLANE_ID, 0, 0, 0)
endif
static if LIBRARY_SpellEffectEvent then
call RegisterSpellEffectEvent(SPELL_ID, function thistype.onCast)
call RegisterSpellEffectEvent(SPELL_ID_DEPLOY, function thistype.onDeploy)
else
local trigger t = CreateTrigger()
call TriggerRegisterAnyUnitEventBJ(t, EVENT_PLAYER_UNIT_SPELL_EFFECT)
call TriggerAddCondition(t, Condition(function thistype.castCondition))
endif
static if LIBRARY_Table then
set tb = Table.create()
set tbCount = Table.create()
endif
static if not LIBRARY_BonusMod then
call BonusMod_Init()
endif
static if not LIBRARY_SetUnitMaxState then
call SetUnitMaxState_Init()
endif
//For toggling from Aerial Aid -> Deploy Paratroopers and vice versa
loop
exitwhen i == bj_MAX_PLAYER_SLOTS
call SetPlayerAbilityAvailable(Player(i), TOGGLE_ID, false)
set i = i + 1
endloop
//Playable Map bounds for non-locust units
set playMinX = GetRectMinX(bj_mapInitialPlayableArea)
set playMaxX = GetRectMaxX(bj_mapInitialPlayableArea)
set playMinY = GetRectMinY(bj_mapInitialPlayableArea)
set playMaxY = GetRectMaxY(bj_mapInitialPlayableArea)
static if PRELOAD then
call UnitAddAbility(u1, TRANSFORM_ID)
call UnitAddAbility(u2, SPELL_ID_DEPLOY)
call UnitAddAbility(u2, SPELL_ID)
call RemoveUnit(u1)
call RemoveUnit(u2)
set u1 = null
set u2 = null
endif
endmethod
endstruct
endscope