scope AerialAid/*
*************************** Aerial Aid aplha version *****************************
by Flux
SPELL DESCRIPTION:
Calls an unmanned aerial vehicle (UAV) containing your army of robotic minions.
The UAV is equuipped with 2/3/4 guns which fires at enemy units in its field of
sight targeting heroes and units with low hitpoints. Upon activation, 'Aerial Aid'
is replaced by the sub-skill 'Deploy Paratroopers' which commands your robotic army
to parachute from the UAV.
SPELL NOTES (Default Configuration):
- Heroes have higher priority than units with lower hitpoints.
- The target location will be the midpoint of the line connecting
the spawn location and expire location.
- The UAV will reach the target point when half of the UAV duration has
elapsed.
- UAV guns can only hit units within the field of sight of the UAV. The
field of sight has a conic shape and cannot hit units directly below it.
- The falling entrance and rising exit of the UAV is not part of the
spell duration, and the UAV is not active during that time.
To do list (Order by Priority):
- Fix Object Editor Data (including creating my own screenshot icon for this contest)
- Use a better lightning model for the target something that looks like a laser.
- Create SFX where the Plane will go, something like a runway light SFX.
- Add Snipe Target SFX.
- Instead of a new unit-type per level, consider using BonusMod.
In the end, it will require 1 troope fly-type, 1 troop ground-type, 1 transform ability
and 1 uav-unitType in all levels.
- Fix World Bounds stuffs.
- Instead of a gun with instant gun shot, consider using a missile projectile.
CREDITS:
Bribe - MissileRecycler, SpellEffectEvent, Table
NatDis - Dwarven Air Force DF 35 Fighter
Fingolfin - Parachute Model
Tranquil - Snipe Target Model
Magtheridon96 - RegisterPlayerUnitEvent
*/
native UnitAlive takes unit u returns boolean
globals
//Rawcodes
private constant integer SPELL_ID = 'Aaer'
private constant integer SPELL_ID_DEPLOY = 'Adpy'
private constant integer HIDE_ID = 'Ahde'
//Dummy Unit Properties
private constant integer DUMMY_ID = 'dumi'
private constant player DUMMY_OWNER = Player(14)
//Periodic Timing of the Spell
private constant real TIMEOUT = 0.03125
//Visual Effects
private constant string PARACHUTE_MODEL = "war3mapImported\\Parachute.mdl"
private constant string TARGET_MODEL = "war3mapImported\\SniperTarget.mdl"
private constant string TARGET_ATTACHMENT = "overhead"
private constant string LASER_CODE = "DRAL"
//Attack and Damage types
private constant attacktype ATTACK_TYPE = ATTACK_TYPE_NORMAL
private constant damagetype DAMAGE_TYPE = DAMAGE_TYPE_MAGIC
private constant weapontype WEAPON_TYPE = WEAPON_TYPE_WHOKNOWS
//************************************************************************************
//**************************** PLANE CONFIGURATION ***********************************
//************************************************************************************
private constant real PLANE_HEIGHT_INITIAL = 1200
private constant real PLANE_HEIGHT = 600
private constant real PLANE_FALL_SPEED = 700
//************************************************************************************
//***************************** GUN CONFIGURATION ************************************
//************************************************************************************
//Minumum distance from the plane to hit
//It won't hit units having an azimuthal distance less than MIN_HIT_DISTANCE to the plane.
private constant real MIN_HIT_DISTANCE = 450
//Conic Radius for the Plane Gun (degrees)
private constant real PLANE_SIGHT_ANGLE = 45
//Plane Gun will prioritize targeting hero
private constant boolean PRIORITIZE_HERO = true
//************************************************************************************
//************************ PARATROOPER CONFIGURATION *********************************
//************************************************************************************
//Is the number of paratroopers that can spawn limited per spell?
private constant boolean LIMIT_SPAWN = true
//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 = true
//When the current height of the Paratrooper is less than PARACHUTE_ACTIVATE, it will
//activate its
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 ************************************
//************************************************************************************
//The rawcode of the Plane per level
private function PlaneId takes integer lvl returns integer
return 'uav0' + lvl //uav1, uav2, uav3
endfunction
//How fast the Plane moves (per second)
private function PlaneSpeed takes integer lvl returns real
return 350.0 + 50.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 900.
endfunction
//How fast the gun fires
private function GunFiringRate takes integer lvl returns real
return 0.2 //Every 0.2 second, each gun will fire
endfunction
//Damage per gun bullet
private function GunDamage takes integer lvl returns real
return 3.0 + 2.0*lvl //5, 7, 9
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 *******************************
//************************************************************************************
//ParatrooperGroups will spawnrate (second)
private function SpawnRate takes integer lvl returns real
return 0.5 + 0.0*lvl //Every 0.5 second, a ParatrooperGroup will spawn
endfunction
static if LIMIT_SPAWN then
//The maximum number of paratroopers the spell can spawn
private function MaxSpawnCount takes integer lvl returns integer
return 4 + 2*lvl //6, 8, 10
endfunction
endif
//The rawcode of the Flying Paratrooper per level
private function ParatrooperOnAirId takes integer lvl returns integer
return 'trp3' + lvl //trp4, trp5, trp6
endfunction
//Inorder to make it realistic, Paratroopers are first considered flying units and
//when it reaches GND_HEIGHT, it will now be considered as a ground unit.
//This is the rawcode of the transform ability needed for the fly->ground transformation
private function TransformAbility takes integer lvl returns integer
return 'Atf0' + lvl //Atf1, Atf2, Atf3
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
globals
private location l = Location(0, 0)
endglobals
private struct Paratrooper
private unit robot
private unit parachute
private effect parachuteModel
private boolean activated
private integer transform
//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
//Calls when the robot landed
private method destroy takes nothing returns nothing
//Remove from List
set .prev.next = .next
set .next.prev = .prev
//Remove Parachute
call DestroyEffect(.parachuteModel)
static if LIBRARY_DummyRecycler then
call AddRecycleTimer(.parachute, 3.0)
elseif LIBRARY_MissileRecycler then
call RecycleMissile(.parachute)
else
//Enough time for the parachute to play death animation
call UnitApplyTimedLife(.parachute, 'BTLF', 2)
endif
set .parachuteModel = null
set .robot = null
set .parachute = null
call .deallocate()
endmethod
//! textmacro 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
else
set .dz = .dz + GRAVITY
if .z < PARACHUTE_ACTIVATE then
set .activated = true
//Create the Parachute
static if LIBRARY_DummyRecycler then
set .parachute = GetRecycledUnit(GetUnitX(.robot), GetUnitY(.robot), false, GetRandomReal(0, 360))
elseif LIBRARY_MissileRecycler then
set .parachute = GetRecycledMissile(GetUnitX(.robot), GetUnitY(.robot), 0, GetRandomReal(0, 360))
else
set .parachute = CreateUnit(DUMMY_OWNER, DUMMY_ID, GetUnitX(.robot), GetUnitY(.robot), GetRandomReal(0, 360))
endif
set .parachuteModel = AddSpecialEffectTarget(PARACHUTE_MODEL, .parachute, "origin")
call SetUnitFlyHeight(.parachute, .z, 0)
endif
endif
if .z <= 0 then
call SetUnitFlyHeight(.parachute, .z, 0)
//For realism effect
if .dz < -SPEED_DAMAGE then
call KillUnit(.robot)
endif
call .destroy()
else
call SetUnitFlyHeight(.robot, .z, 0)
if .activated then
call SetUnitFlyHeight(.parachute, .z, 0)
endif
if .z < GND_HEIGHT and .transform != 0 then
call UnitAddAbility(.robot, .transform)
call UnitRemoveAbility(.robot, .transform)
set .transform = 0
endif
endif
//! endtextmacro
private static method pickAll takes nothing returns nothing
local thistype this = thistype(0).next
loop
exitwhen this == 0
//! runtextmacro 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 .transform = TransformAbility(lvl)
set .activated = false
set .z = z
set .dz = 0
//Create the Robot
set .robot = CreateUnit(owner, ParatrooperOnAirId(lvl), x, y, GetRandomReal(0, 360))
static if APPLY_TIMED_LIFE then
call UnitApplyTimedLife(.robot, 'BTLF', TimedLife(lvl))
endif
call SetUnitFlyHeight(.robot, .z, 0)
static if not CAN_BE_HIT then
call UnitAddAbility(.robot, 'Avul')
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
//Missile launched by the Gun
private struct Missile
endstruct
private struct Gun
private lightning laser
private boolean hidden
private effect targetSfx
readonly unit target
private real dx
private 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 .laser != null then
call DestroyLightning(.laser)
set .laser = null
endif
set .target = null
call .deallocate()
endmethod
method update takes unit newTarget, real x, real y, real z returns nothing
local real targetX
local real targetY
if newTarget == null then
if not .hidden then
set .hidden = true
call SetLightningColor(.laser, 0, 0, 0, 0)
endif
else
if .hidden then
set .hidden = false
call SetLightningColor(.laser, 1, 1, 1, 1)
endif
set targetX = GetUnitX(newTarget)
set targetY = GetUnitY(newTarget)
call MoveLocation(l, targetX, targetY)
call MoveLightningEx(.laser, false, x + .dx, y + .dy, z, targetX, targetY, GetLocationZ(l))
endif
set .target = newTarget
endmethod
static method head takes nothing returns thistype
local thistype this = .allocate()
set .next = 0
set .prev = 0
return this
endmethod
static method create takes thistype head, real dx, real dy returns thistype
local thistype this = .allocate()
set .laser = AddLightning(LASER_CODE, false, 0, 0, 1, 1)
set .hidden = true
set .dx = dx
set .dy = dy
call SetLightningColor(.laser, 0, 0, 0, 0)
set .next = head.next
set .prev = head
set .next.prev = this
set .prev.next = this
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 Plane
//Spell Mechanics
private unit caster
private unit plane
private player owner
private effect planeModel
private integer lvl
private boolean toggled
//Gun-related
private Gun gunHead
private real radius
private integer gunCount
private real gunDamage
private real gunFireTime
//Paratrooper-related
private real robotSpawnRate
private boolean deploy
static if LIMIT_SPAWN then
private integer spawnCtr //number of paratrooper deployed
endif
//Timing
private real gunCtr //counter for gun firing rate
private real robotCtr //counter for paratrooper deployment
private real duration
//Movement
private real x
private real y
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
else
private static hashtable hash = InitHashtable()
endif
private method destroy takes nothing returns nothing
//Remove from List
set .prev.next = .next
set .next.prev = .prev
//Destroy Handles
call DestroyEffect(.planeModel)
call RemoveUnit(.plane)
//For perfect clean-up
set .caster = null
set .planeModel = null
set .plane = null
//Recycle Index
call .deallocate()
endmethod
//! textmacro UAV_UPDATE
set gun = .gunHead.next
//Movement
set .x = .x + .dx
set .y = .y + .dy
call SetUnitX(.plane, .x)
call SetUnitY(.plane, .y)
if .duration > 0 then
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
set .duration = .duration - TIMEOUT
//Targeting
call GroupEnumUnitsInRange(g, .x, .y, .radius, null)
loop
set u = FirstOfGroup(g)
exitwhen u == null
call GroupRemoveUnit(g, u)
if TargetFilter(u, .owner) then
set diffX = GetUnitX(u) - .x
set diffY = GetUnitY(u) - .y
//If greater than minimum distance
if diffX*diffX + diffY*diffY >= MIN_HIT_DISTANCE*MIN_HIT_DISTANCE then
//if within angle of sight
if RAbsBJ(.angle - Atan2(diffY, diffX)) <= PLANE_SIGHT_ANGLE*bj_DEGTORAD then
//Add the Picked Unit to the Self-Sorting List
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, .x, .y, .z)
set list = list.next
//No idle gun if there are less targets
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?
set .gunCtr = .gunCtr + TIMEOUT
if .gunCtr >= .gunFireTime then
set .gunCtr = .gunCtr - .gunFireTime
set gun = .gunHead.next
loop
exitwhen gun == 0 or gun.target == null
call UnitDamageTarget(.caster, gun.target, .gunDamage, false, false, ATTACK_TYPE, DAMAGE_TYPE, WEAPON_TYPE)
set gun = gun.next
endloop
endif
//Are paratroopers commanded to deploy?
if .deploy then
set .robotCtr = .robotCtr + TIMEOUT
if .robotCtr >= .robotSpawnRate then
set .robotCtr = .robotCtr - .robotSpawnRate
static if LIMIT_SPAWN then
if .spawnCtr < MaxSpawnCount(.lvl) then
call Paratrooper.create(.owner, .lvl, .x, .y, .z)
set .spawnCtr = .spawnCtr + 1
endif
else
call Paratrooper.create(.owner, .lvl, .x, .y, .z)
endif
endif
endif
endif
else
//Toggle Ability
if .toggled then
set .toggled = false
call UnitRemoveAbility(.caster, HIDE_ID)
call UnitRemoveAbility(.caster, SPELL_ID_DEPLOY)
endif
//Destroy Guns
if .gunHead != 0 then
loop
exitwhen gun == 0
call gun.destroy()
set gun = .gunHead.next
endloop
call .gunHead.destroy()
set .gunHead = 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 then
call .destroy()
else
call SetUnitFlyHeight(.plane, .z, 0)
endif
endif
//! endtextmacro
private static method pickAll takes nothing returns nothing
local thistype this = thistype(0).next
//update local variables
local unit u
local real diffX
local real diffY
local Gun gun
local List list
loop
exitwhen this == 0
//! runtextmacro UAV_UPDATE()
set this = .next
endloop
endmethod
private static method onCast takes nothing returns boolean
local thistype this = .allocate()
local integer i = 1
local real x
local real y
local real cos
local real sin
local real spd
local real gunX
local real gunY
//Spell Initialization
set .caster = GetTriggerUnit()
set .owner = GetTriggerPlayer()
set .deploy = false
set .lvl = GetUnitAbilityLevel(.caster, SPELL_ID)
static if LIMIT_SPAWN then
set .spawnCtr = 0
endif
set .gunHead = Gun.head()
set .gunCtr = 0
set .gunFireTime = GunFiringRate(.lvl)
set .gunDamage = GunDamage(.lvl)
set .robotCtr = 0
set .robotSpawnRate = 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
set spd = PlaneSpeed(.lvl)*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
set .dx = spd*cos
set .dy = spd*sin
set .dz = -PLANE_FALL_SPEED*TIMEOUT
set .plane = CreateUnit(.owner, PlaneId(.lvl), .x, .y, angle*bj_RADTODEG)
call UnitRemoveAbility(.plane, 'Amov')
call SetUnitFlyHeight(.plane, .z, 0)
//Store caster handle id for onDeploy data retrieval
static if LIBRARY_Table then
set tb[GetHandleId(.caster)] = this
else
call SaveInteger(hash, GetHandleId(.caster), 0, this)
endif
//Toggle Ability
set .toggled = true
call UnitAddAbility(.caster, HIDE_ID)
call UnitAddAbility(.caster, SPELL_ID_DEPLOY)
//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 false
endmethod
private static method onDeploy takes nothing returns boolean
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, HIDE_ID)
call UnitRemoveAbility(.caster, SPELL_ID_DEPLOY)
return false
endmethod
static if not LIBRARY_SpellEffectEvent then
private static method castCondition takes nothing returns boolean
return (GetSpellAbilityId() == SPELL_ID and thistype.onCast() )
endmethod
endif
static if not LIBRARY_SpellEffectEvent then
private static method releaseCondition takes nothing returns boolean
return (GetSpellAbilityId() == SPELL_ID_DEPLOY and thistype.onDeploy() )
endmethod
endif
private static method onInit takes nothing returns nothing
local integer i = 0
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))
call TriggerAddCondition(t, Condition(function thistype.releaseCondition))
endif
static if LIBRARY_Table then
set tb = Table.create()
endif
loop
exitwhen i == bj_MAX_PLAYER_SLOTS
call SetPlayerAbilityAvailable(Player(i), HIDE_ID, false)
set i = i + 1
endloop
endmethod
endstruct
endscope