library FrozenOrb
globals
private constant integer ABILITY_ID = 'A000'
private constant integer DUMMY_ID = 'e000'
private constant boolean APPLY_BUFF = true
private constant integer BUFF_ID = 'A001' // must be based on Slow Aura('Aasl')
private constant real DT = 1.0 / 32.0
// We divide the unit circle into this many slices so that we can pick random directions
// for ice bolts to travel in.
private constant integer MAX_DIRECTIONS = 16
// The offset height for the orb and the frost bolts.
private constant real Z_OFFSET = 80.0
// Every this amount of seconds the orb spawns <frost_bolt_count_on_orb_spawn> many frost bolts.
private constant real ORB_FROST_BOLT_SPAWN_DELAY = 0.75
// When the orb explodes it spawns ice bolts that follow a curved path.
private constant real FROST_BOLT_CURVE_ANGLE = 90.0 // in degrees
private constant real FROST_BOLT_CURVE_SPEED = 180.0
// UnitDamageTarget arguments
private constant boolean MELEE_ATTACK = false
private constant boolean RANGE_ATTACK = true
private constant attacktype ATTACK_TYPE = ATTACK_TYPE_NORMAL // spell
private constant damagetype DAMAGE_TYPE = DAMAGE_TYPE_UNIVERSAL // ignore armor value
private constant weapontype WEAPON_TYPE = WEAPON_TYPE_WHOKNOWS
private constant boolean ENABLE_UNIT_FREEZING = true
private constant boolean REDUCE_MOVE_SPEED = true
private constant real MOVE_SPEED_REDUCTION = 0.50 // 50%
private constant boolean CHANGE_VERTEX_COLOR_ON_FREEZE = true
endglobals
globals
private real MIN_X
private real MAX_X
private real MIN_Y
private real MAX_Y
endglobals
private function set_bounds takes nothing returns nothing
local rect r = GetPlayableMapRect()
set MIN_X = GetRectMinX(r)
set MAX_X = GetRectMaxX(r)
set MIN_Y = GetRectMinY(r)
set MAX_Y = GetRectMaxY(r)
endfunction
native UnitAlive takes unit u returns boolean
private function targets_allowed takes FrozenOrb instance, unit u returns boolean
local integer ut = GetUnitTypeId(u)
if ut == 'nwen' then
// wendigos are immune to cold =)
return false
endif
return UnitAlive(u) and IsUnitEnemy(u, instance.caster_owner)
endfunction
private function frost_bolt_damage takes FrozenOrb frost_bolt returns real
return 25.0 + 5.0 * frost_bolt.ability_level // 30.0, 35.0, 40.0
endfunction
private function orb_speed takes FrozenOrb orb returns real
// can use orb.ability_level for level specific speed
return 275.0
endfunction
private function frost_bolt_speed takes FrozenOrb frost_bolt returns real
// can use frost_bolt.ability_level for level specific speed
return 600.0
endfunction
private function orb_max_distance takes FrozenOrb orb returns real
return 900.0
endfunction
private function frost_bolt_max_distance takes FrozenOrb frost_bolt returns real
return 1000.0
endfunction
private function frost_bolt_count_on_orb_spawn takes FrozenOrb orb returns integer
return 3
endfunction
private function frost_bolt_count_on_orb_explode takes FrozenOrb orb returns integer
return MAX_DIRECTIONS // must be <= MAX_DIRECTIONS
endfunction
private function freeze_duration takes FrozenOrb instance returns real
return 5.0
endfunction
globals
private location loc = Location(0.0, 0.0)
endglobals
private function GetUnitZ takes unit u returns real
call MoveLocation(loc, GetUnitX(u), GetUnitY(u))
return GetLocationZ(loc) + GetUnitFlyHeight(u)
endfunction
private function SetUnitZ takes unit u, real z returns nothing
call MoveLocation(loc, GetUnitX(u), GetUnitY(u))
call SetUnitFlyHeight(u, z - GetLocationZ(loc), 0.0)
endfunction
// NOTE: you can rewrite these functions to use your dummy recycling scripts!
//
globals
private unit created_dummy
endglobals
private function dummy_create takes player p, real x, real y, real z, real facing returns unit
set created_dummy = CreateUnit(p, DUMMY_ID, x, y, facing)
call SetUnitX(created_dummy, x)
call SetUnitY(created_dummy, y)
call SetUnitFacing(created_dummy, facing)
call UnitAddAbility(created_dummy, 'Amrf')
call UnitRemoveAbility(created_dummy, 'Amrf')
call SetUnitZ(created_dummy, z)
call UnitAddAbility(created_dummy, 'Aloc')
call UnitRemoveAbility(created_dummy, 'Aloc')
return created_dummy
endfunction
private function dummy_destroy takes unit u returns nothing
call RemoveUnit(u)
endfunction
private struct Timer extends array
readonly static integer max_count = 0
private static Timer head = 0
private Timer next
private timer t
integer data
real timeout
static method start takes integer user_data, real timeout, code callback returns Timer
local Timer this
if head != 0 then
set this = head
set head = head.next
else
set max_count = max_count + 1
set this = max_count
set this.t = CreateTimer()
endif
set this.next = 0
set this.data = user_data
set this.timeout = timeout
call TimerStart(this.t, this, false, null)
call PauseTimer(this.t)
call TimerStart(this.t, timeout, false, callback)
return this
endmethod
method stop takes nothing returns nothing
call TimerStart(this.t, 0.0, false, null)
set this.next = head
set head = this
endmethod
static method get_expired_data takes nothing returns integer
local Timer t = Timer( R2I(TimerGetRemaining(GetExpiredTimer()) + 0.5) )
local integer data = t.data
call t.stop()
return data
endmethod
endstruct
private struct Sound
static thistype array heads
thistype next
sound s
integer head
boolean m_is_looping
boolean m_is_3d
real m_duration
static string sound_file
static boolean is_looping
static boolean is_3d
static boolean stop_when_out_of_range
static integer fade_in_rate
static integer fade_out_rate
static string eax_setting
static real duration
static method preload_sound takes nothing returns nothing
local Sound snd = Timer.get_expired_data()
if snd.m_is_3d then
call SetSoundPosition(snd.s, 0, 0, 0)
endif
call StartSound(snd.s)
call StopSound(snd.s, /*kill-when-done:*/ false, /*fade-out:*/ false)
set snd.next = heads[snd.head]
set heads[snd.head] = snd
endmethod
static integer /*Sound.*/FROST_BOLT_HIT
static integer /*Sound.*/ORB_MOVING
static integer /*Sound.*/ORB_EXPLOSION
static method onInit takes nothing returns nothing
local Sound snd
local sound s
local integer i
local integer j
set i = 1
loop
exitwhen i > 3
if i == 1 then
set thistype.FROST_BOLT_HIT = 1
set sound_file = "Abilities\\Spells\\Other\\FrostBolt\\FrostBoltLaunch1.wav"
set duration = 1.1
set is_looping = false
set is_3d = true
set stop_when_out_of_range = true
set fade_in_rate = 10
set fade_out_rate = 10
set eax_setting = "MissilesEAX"
elseif i == 2 then
set thistype.ORB_MOVING = 2
set sound_file = "Abilities\\Spells\\Other\\Doom\\DoomTarget.wav"
set duration = 2.261
set is_looping = true
set is_3d = true
set stop_when_out_of_range = true
set fade_in_rate = 10
set fade_out_rate = 10
set eax_setting = "SpellsEAX"
elseif i == 3 then
set thistype.ORB_EXPLOSION = 3
set sound_file = "Abilities\\Spells\\Other\\FrostBolt\\FrostBoltHit1.wav"
set duration = 1.347
set is_looping = false
set is_3d = true
set stop_when_out_of_range = true
set fade_in_rate = 10
set fade_out_rate = 10
set eax_setting = "MissilesEAX"
endif
set j = 1
loop
exitwhen j > 4 // can't have more than 4 of the same sound playing at the same time
set s = CreateSound(sound_file, is_looping, is_3d, stop_when_out_of_range, fade_in_rate, fade_out_rate, eax_setting)
if is_3d then
call SetSoundDistances(s, /*min-dist */ 600, /*max-dist*/ 100000)
call SetSoundConeAngles(s, 0, 0, 0)
call SetSoundConeOrientation(s, 0, 0, 0)
call SetSoundVelocity(s, 0, 0, 0)
endif
set snd = Sound.create()
set snd.s = s
set snd.head = i
set snd.m_duration = duration
set snd.m_is_looping = is_looping
set snd.m_is_3d = is_3d
call Timer.start(snd, 0.0, function thistype.preload_sound)
set j = j + 1
endloop
set i = i + 1
endloop
endmethod
method kill takes nothing returns nothing
if this == 0 then
return
endif
call StopSound(this.s, /*kill-when-done:*/ false, /*fade-out:*/ true)
set this.next = heads[this.head]
set heads[this.head] = this
endmethod
static method kill_when_done takes nothing returns nothing
call thistype(Timer.get_expired_data()).kill()
endmethod
static method get takes integer sid returns thistype
local Sound snd
if heads[sid] == 0 then
return 0
endif
set snd = heads[sid]
set heads[sid] = heads[sid].next
call SetSoundChannel(snd.s, GetRandomInt(0, 11))
if not snd.m_is_looping then
call Timer.start(snd, snd.duration, function thistype.kill_when_done)
endif
return snd
endmethod
method volume takes real v returns thistype
if this != 0 then
call SetSoundVolume(this.s, R2I(v / 100.0 * 127.0))
endif
return this
endmethod
method cutoff takes real c returns thistype
if this != 0 and this.is_3d then
call SetSoundDistanceCutoff(this.s, c)
endif
return this
endmethod
method attach_to_unit takes unit u returns thistype
if this != 0 then
call AttachSoundToUnit(this.s, u)
endif
return this
endmethod
method set_xyz takes real x, real y, real z returns thistype
if this != 0 then
call SetSoundPosition(this.s, x, y, z)
endif
return this
endmethod
method play takes nothing returns thistype
if this != 0 then
call StartSound(this.s)
endif
return this
endmethod
endstruct
private struct FreezeUnit
static group G = CreateGroup()
unit u
effect e
real ms_delta
static method unfreeze takes nothing returns nothing
local thistype this = Timer.get_expired_data()
call GroupRemoveUnit(G, this.u)
static if APPLY_BUFF then
call UnitRemoveAbility(u, BUFF_ID)
endif
static if REDUCE_MOVE_SPEED then
call SetUnitMoveSpeed(u, GetUnitMoveSpeed(u) + this.ms_delta)
endif
call DestroyEffect(this.e)
static if CHANGE_VERTEX_COLOR_ON_FREEZE then
call SetUnitVertexColor(u, 255, 255, 255, 255)
endif
call this.deallocate()
endmethod
static method freeze takes unit u, real ms_reduce_perc, real duration returns nothing
local thistype this
if IsUnitInGroup(u, G) then
return
endif
set this = allocate()
call GroupAddUnit(G, u)
static if APPLY_BUFF then
call UnitAddAbility(u, BUFF_ID)
endif
set this.u = u
static if REDUCE_MOVE_SPEED then
set this.ms_delta = GetUnitDefaultMoveSpeed(u) * ms_reduce_perc
call SetUnitMoveSpeed(u, GetUnitMoveSpeed(u) - this.ms_delta)
endif
set this.e = AddSpecialEffectTarget("Abilities\\Spells\\Other\\FrostDamage\\FrostDamage.mdl", u, "origin")
static if CHANGE_VERTEX_COLOR_ON_FREEZE then
call SetUnitVertexColor(u, 127, 190, 190, 255)
endif
call Timer.start(this, duration, function thistype.unfreeze)
endmethod
endstruct
globals
private real dx
private real dy
private real dz
private real v_len
endglobals
private function normalize_or_z takes real vx, real vy, real vz returns nothing
set v_len = SquareRoot(vx * vx + vy * vy + vz * vz)
if v_len > 0.0 then
set dx = vx / v_len
set dy = vy / v_len
set dz = vz / v_len
else
set dx = 0.0
set dy = 0.0
set dz = 1.0
endif
endfunction
struct FrozenOrb
static thistype array ul // update list
static integer ul_size = 0
static timer ul_tmr = CreateTimer()
static code update_func
static group G = CreateGroup()
static real array dirs
static constant integer dirs_count = MAX_DIRECTIONS
unit caster
player caster_owner
integer ability_level
unit pu1
effect pu1m
// the orb is made of two parts, the rotating "sphere" and the bluish glowing light
unit pu2
effect pu2m
// current position
real px
real py
real pz
// velocity
real dpx
real dpy
real dpz
real speed
real speed_inc
// target point
real tx
real ty
real tz
real dist = 0.0
real max_dist
real facing
real end_facing
real facing_inc
Sound snd = 0
real tick = 0.0
real radius
real damage
real freeze_duration
static method add_to_update_list takes thistype instance returns nothing
set ul_size = ul_size + 1
set ul[ul_size] = instance
if ul_size == 1 then
call TimerStart(ul_tmr, DT, true, update_func)
endif
endmethod
static method remove_from_update_list takes integer i returns nothing
set ul[i] = ul[ul_size]
set ul_size = ul_size - 1
if ul_size == 0 then
call PauseTimer(ul_tmr)
endif
endmethod
method orb_spawn_frost_bolts takes integer n, real curve_angle returns nothing
local thistype orb = this
local thistype fb // frost bolt
local integer i
local integer r
local integer m
local real dir
local real a
set i = 1
set m = dirs_count
loop
exitwhen i > n // n must be <= m
set r = GetRandomInt(1, m)
set dir = dirs[r]
set dirs[r] = dirs[m]
set dirs[m] = dir
set m = m - 1
set fb = allocate()
set fb.caster = orb.caster
set fb.caster_owner = orb.caster_owner
set fb.ability_level = orb.ability_level
set fb.px = orb.px
set fb.py = orb.py
set fb.pz = orb.pz
set fb.speed = frost_bolt_speed(fb.ability_level)
set fb.speed_inc = fb.speed * DT
set a = dir * bj_DEGTORAD
set fb.dpx = Cos(a) * fb.speed_inc
set fb.dpy = Sin(a) * fb.speed_inc
set fb.dpz = 0.0
set fb.max_dist = frost_bolt_max_distance(fb)
// set fb.tx = fb.px + Cos(a) * fb.max_dist
// set fb.ty = fb.py + Sin(a) * fb.max_dist
// set fb.tz = fb.pz
set fb.facing = dir
set fb.end_facing = dir + curve_angle
set fb.facing_inc = -FROST_BOLT_CURVE_SPEED * DT
set fb.radius = 64.0
set fb.damage = frost_bolt_damage(fb)
set fb.freeze_duration = freeze_duration(fb)
set fb.pu1 = dummy_create(fb.caster_owner, fb.px, fb.py, fb.pz, fb.facing)
set fb.pu1m = AddSpecialEffectTarget("Abilities\\Weapons\\LichMissile\\LichMissile.mdl", fb.pu1, "origin")
call SetUnitScale(fb.pu1, 0.70, 0.70, 0.70)
set fb.pu2 = null
set fb.pu2m = null
call add_to_update_list(fb)
set i = i + 1
endloop
endmethod
// method destroy can't have arguments...
method destroy2 takes real pos_backtrack returns nothing
call DestroyEffect(this.pu1m)
call dummy_destroy(this.pu1)
if this.pu2 != null then // orb
call DestroyEffect(this.pu2m)
call dummy_destroy(this.pu2)
call Sound.get(Sound.ORB_EXPLOSION).volume(100).cutoff(3000.0).set_xyz(this.px, this.py, this.pz).play()
set this.px = this.px - Cos(this.facing * bj_DEGTORAD) * pos_backtrack
set this.py = this.py - Sin(this.facing * bj_DEGTORAD) * pos_backtrack
call this.orb_spawn_frost_bolts(frost_bolt_count_on_orb_explode(this), -FROST_BOLT_CURVE_ANGLE)
endif
if this.snd != 0 then
call this.snd.kill()
endif
call this.deallocate()
endmethod
static method update takes nothing returns nothing
local thistype this
local integer i
local boolean is_orb
local real a
local unit u
local boolean has_hit
local real z
set i = 1
loop
exitwhen i > ul_size
set this = ul[i]
set is_orb = this.pu2 != null
set this.dist = this.dist + this.speed_inc
call MoveLocation(loc, this.px, this.py)
if this.dist > this.max_dist /*
*/ or this.px < MIN_X or this.px > MAX_X or this.py < MIN_Y or this.py > MAX_Y /*
*/ or this.pz - GetLocationZ(loc) < 32.0 then
call remove_from_update_list(i)
set i = i - 1
if is_orb then
call this.destroy2(64.0)
else
call this.destroy2(0.0)
endif
else
set this.px = this.px + this.dpx
set this.py = this.py + this.dpy
set this.pz = this.pz + this.dpz
call SetUnitX(this.pu1, this.px)
call SetUnitY(this.pu1, this.py)
call MoveLocation(loc, this.px, this.py)
set z = this.pz - GetLocationZ(loc)
call SetUnitFlyHeight(this.pu1, z, 0.0)
call GroupEnumUnitsInRange(G, this.px, this.py, this.radius, null)
set has_hit = false
loop
set u = FirstOfGroup(G)
exitwhen u == null
call GroupRemoveUnit(G, u)
if targets_allowed(this, u) then
if is_orb then
call UnitDamageTarget(this.caster, u, this.damage, MELEE_ATTACK, RANGE_ATTACK, ATTACK_TYPE, DAMAGE_TYPE, WEAPON_TYPE)
elseif not has_hit then // frost bolt
call Sound.get(Sound.FROST_BOLT_HIT).volume(100).cutoff(3000.0).attach_to_unit(u).play()
call UnitDamageTarget(this.caster, u, this.damage, MELEE_ATTACK, RANGE_ATTACK, ATTACK_TYPE, DAMAGE_TYPE, WEAPON_TYPE)
call remove_from_update_list(i)
set i = i - 1
call this.destroy2(0.0)
set has_hit = true
endif
static if ENABLE_UNIT_FREEZING then
call FreezeUnit.freeze(u, MOVE_SPEED_REDUCTION, this.freeze_duration)
endif
endif
endloop
if is_orb then
call SetUnitX(this.pu2, this.px)
call SetUnitY(this.pu2, this.py)
call SetUnitFlyHeight(this.pu2, z, 0.0)
set this.tick = this.tick + DT
if this.tick > ORB_FROST_BOLT_SPAWN_DELAY then
set this.tick = 0.0
call this.orb_spawn_frost_bolts(frost_bolt_count_on_orb_spawn(this), 0.0)
endif
else // frost bolt
if this.facing > this.end_facing then
set this.facing = this.facing + this.facing_inc
call SetUnitFacing(this.pu1, this.facing)
set a = this.facing * bj_DEGTORAD
set this.dpx = Cos(a) * this.speed_inc
set this.dpy = Sin(a) * this.speed_inc
endif
endif
endif
set i = i + 1
endloop
set u = null
endmethod
static method create_orb takes thistype caster_info returns thistype
local thistype orb = allocate()
set orb.caster = caster_info.caster
set orb.caster_owner = GetOwningPlayer(orb.caster)
set orb.ability_level = caster_info.ability_level
set orb.px = GetUnitX(orb.caster)
set orb.py = GetUnitY(orb.caster)
set orb.pz = GetUnitZ(orb.caster) + Z_OFFSET
set orb.tx = caster_info.tx
set orb.ty = caster_info.ty
set orb.tz = orb.pz
call normalize_or_z(orb.tx - orb.px, orb.ty - orb.py, orb.tz - orb.pz)
set orb.speed = orb_speed(orb.ability_level)
set orb.speed_inc = orb.speed * DT
set orb.dpx = dx * orb.speed * DT
set orb.dpy = dy * orb.speed * DT
set orb.dpz = 0.0
set orb.max_dist = orb_max_distance(orb)
set orb.facing = Atan2(orb.dpy, orb.dpx) * bj_RADTODEG
set orb.end_facing = orb.facing
set orb.pu1 = dummy_create(orb.caster_owner, orb.px, orb.py, orb.pz, orb.facing)
set orb.pu1m = AddSpecialEffectTarget("Abilities\\Spells\\Human\\ManaFlare\\ManaFlareTarget.mdl", orb.pu1, "origin")
call SetUnitScale(orb.pu1, 5.0, 5.0, 5.0)
set orb.pu2 = dummy_create(orb.caster_owner, orb.px, orb.py, orb.pz, orb.facing)
set orb.pu2m = AddSpecialEffectTarget("Abilities\\Weapons\\SpiritOfVengeanceMissile\\SpiritOfVengeanceMissile.mdl", orb.pu2, "origin")
call SetUnitScale(orb.pu2, 4.0, 4.0, 4.0)
set orb.snd = Sound.get(Sound.ORB_MOVING).volume(100).cutoff(3000.0).attach_to_unit(orb.pu1).play()
set orb.radius = 128.0
set orb.damage = 0.333 * frost_bolt_damage(orb) * DT
set orb.freeze_duration = freeze_duration(orb)
call orb.orb_spawn_frost_bolts(frost_bolt_count_on_orb_spawn(orb), 0.0)
call add_to_update_list(orb)
return orb
endmethod
static method reset_time_scale takes nothing returns nothing
local thistype caster_info = Timer.get_expired_data()
call SetUnitTimeScale(caster_info.caster, 1.0)
call caster_info.deallocate()
endmethod
static method on_spell_effect takes nothing returns nothing
local thistype caster_info = allocate()
local unit u = GetTriggerUnit()
set caster_info.caster = u
set caster_info.ability_level = GetUnitAbilityLevel(u, GetSpellAbilityId())
set caster_info.tx = GetSpellTargetX()
set caster_info.ty = GetSpellTargetY()
// Jaina's spell animation is a bit slow, let's make it as if she had a "faster cast rate"
call SetUnitTimeScale(u, 4.0)
call Timer.start(caster_info, 0.4, function thistype.reset_time_scale)
call create_orb(caster_info)
set u = null
endmethod
static method cast takes nothing returns nothing
if GetSpellAbilityId() != ABILITY_ID then
return
endif
call on_spell_effect()
endmethod
private static method onInit takes nothing returns nothing
local trigger t
local real dir
local real step
local integer i
call set_bounds()
set dir = 0.0
set step = 360.0 / dirs_count
set i = 0
loop
exitwhen i > dirs_count
set dirs[i] = dir
set dir = dir + step
set i = i + 1
endloop
set update_func = function thistype.update
set t = CreateTrigger()
call TriggerRegisterAnyUnitEventBJ(t, EVENT_PLAYER_UNIT_SPELL_EFFECT)
call TriggerAddAction(t, function thistype.cast)
endmethod
endstruct
endlibrary