library ShadowAttack requires TimerUtils, GroupUtils, xedamage
//===============================================================================================
// shadow attack v1.00 by scorpion182
// requires: TimerUtils, xedamage by Vexorian
// GroupUtils by RisingDusk
//
// optional: BoundSentinel by Vexorian (use it if you don't want your hero stuck outside the map bound)
//
//===============================================================================================
globals
private keyword data
//-----------------------CALIBRATION SECTION-----------------------------------------------------
//spell id
private constant integer SPELL_ID = 'A000'
//shadow trail unit id
private constant integer SHADOW_TRAIL = 'h000'
//shadow number
private constant integer MAX_SHADOW = 30
//attack animation index
private constant integer ANIMATION_INDEX = 2
//walk animation index
private constant integer ATTACK_AN_INDEX = 5
//collision distance
private constant real COLLISION_DIST = 75.
//spell order id
private constant string ORDER_ID = "blizzard"
//image special effect
private constant string IMAGE_FX = "Abilities\\Spells\\Orc\\MirrorImage\\MirrorImageCaster.mdl"
//on-damage effect
private constant string ON_DAMAGE_FX = "Abilities\\Spells\\Other\\Stampede\\StampedeMissileDeath.mdl"
//on-damage effect attachment point
private constant string DAMAGE_ATTCH = "origin"
//preload effect?
private constant boolean PRELOAD_FX = true
endglobals
//do damage each hit
private constant function GetDamage takes integer lvl returns real
return 35. + lvl * 10
endfunction
//attack interval
private constant function GetAttackInterval takes integer lvl returns real
return 2. + lvl * 0
endfunction
//maximum number target per level
private constant function GetTargetNum takes integer lvl returns integer
return 2 + lvl * 1
endfunction
//rotation speed
private constant function GetAngleSpeed takes integer lvl returns real
return .05 + lvl * 0.
endfunction
//caster movement speed
private constant function GetSpeed takes integer lvl returns real
return 800. * XE_ANIMATION_PERIOD + lvl * 0
endfunction
//shadow number
private constant function GetShadowNum takes integer lvl returns integer
return 18 + lvl * 0
endfunction
//area of effect
private constant function GetDistance takes integer lvl returns real
return 450. + lvl * 0
endfunction
//filter the target
private function IsValidTarget takes unit u, data s returns boolean
return not IsUnitType(u, UNIT_TYPE_DEAD) and GetUnitTypeId(u) != 0 and IsUnitType(u,UNIT_TYPE_STRUCTURE)==false and IsUnitEnemy(u,GetOwningPlayer(s.caster))==true and IsUnitVisible(u,GetOwningPlayer(s.caster))==true
endfunction
//damage options must match the target filter
private function DamageOptions takes xedamage spellDamage returns nothing
set spellDamage.dtype=DAMAGE_TYPE_UNIVERSAL
set spellDamage.atype=ATTACK_TYPE_NORMAL
set spellDamage.exception=UNIT_TYPE_STRUCTURE
set spellDamage.visibleOnly=true
endfunction
//----------------------END OF CALIBRATION-------------------------------------------------------
globals
private xedamage xed
private constant real A = 2 * bj_PI
endglobals
private struct shadow
unit array shadow[MAX_SHADOW]
real array angle[MAX_SHADOW]
boolean array show[MAX_SHADOW]
group victim
unit target = null
boolean sets = false
boolean attack = false
boolean ready = false
boolean back = false
boolean inpos = false
real interval = 0
real cap = 0
real ag = 0
integer idx = 0
integer num = 0
integer n = 0
integer pos = 0
unit caster
timer t
integer lvl
real cx
real cy
private static thistype temp
static method create takes unit c, real x, real y returns thistype
local thistype this = thistype.allocate()
local integer i = 0
local real a
set .caster = c
set .t = NewTimer()
set .lvl = GetUnitAbilityLevel(c, SPELL_ID)
set .victim = NewGroup()
set .cap = A
set .cx = x
set .cy = y
loop
exitwhen i == GetShadowNum(.lvl)
set a = i * A / GetShadowNum(.lvl)
set .shadow[i] = CreateUnit(GetOwningPlayer(.caster), SHADOW_TRAIL, x, y, a)
set .angle[i] = a
call SetUnitTimeScale(.shadow[i], 2.)
call SetUnitColor(.shadow[i], PLAYER_COLOR_LIGHT_GRAY)
call SetUnitVertexColor(.shadow[i], 255, 255, 255, 125)
call SetUnitAnimationByIndex(.shadow[i], ANIMATION_INDEX)
call ShowUnit(.shadow[i], false)
set i = i + 1
endloop
call SetTimerData(.t, this)
call TimerStart(.t, XE_ANIMATION_PERIOD, true, function thistype.onLoop)
return this
endmethod
//i don't know why i can't detect locust unit, so i reverse it
static method IsCaster takes nothing returns boolean
local unit u = GetFilterUnit()
if u==temp.caster and not temp.sets then
set temp.pos = temp.idx
set temp.sets = true
call RemoveUnit(temp.shadow[temp.idx])
set temp.shadow[temp.idx] = temp.caster
set temp.show[temp.idx] = true
set temp.num = temp.num + 1
endif
set u = null
return false
endmethod
static method IsTarget takes nothing returns boolean
local unit u = GetFilterUnit()
if temp.target == null and IsValidTarget(u, temp) and not IsUnitInGroup(u, temp.victim) then
set temp.target = u
set temp.n = temp.n + 1
set temp.attack = true
call GroupAddUnit(temp.victim, u)
endif
set u = null
return false
endmethod
static method onLoop takes nothing returns nothing
local thistype this = thistype(GetTimerData(GetExpiredTimer()))
local integer i = 0
local real dx
local real dy
local real dis
local real angle
if GetUnitCurrentOrder(.caster) == OrderId(ORDER_ID) then
set temp = this
if .ready then
if .back then
set .interval = .interval + XE_ANIMATION_PERIOD
if .interval > GetAttackInterval(.lvl) then
set .interval = 0.
set .back = false
set .shadow [.pos] = CreateUnit(GetOwningPlayer(.caster), SHADOW_TRAIL, GetUnitX(.caster), GetUnitY(.caster), GetUnitFacing(.caster))
call SetUnitTimeScale(.shadow[.pos], 2.)
call SetUnitColor(.shadow[.pos], PLAYER_COLOR_LIGHT_GRAY)
call SetUnitVertexColor(.shadow[.pos], 255, 255, 255, 125)
call SetUnitAnimationByIndex(.shadow[.pos], ANIMATION_INDEX)
call UnitAddAbility(.shadow[.pos], 'Aloc')
call GroupClear(.victim)
endif
endif
if not .attack and .target == null and .n < GetTargetNum(.lvl) and not .back then
call GroupEnumUnitsInRange(ENUM_GROUP, .cx, .cy, GetDistance(.lvl), function thistype.IsTarget)
if .n == 0 and .target == null and not .inpos then
set .inpos = true
call RemoveUnit(.shadow[.pos])
set .shadow[.pos] = .caster
set .show[.pos] = true
call SetUnitAnimationByIndex(.caster, ANIMATION_INDEX)
endif
if .n >= 1 and .target == null then
set .ready = false
call SetUnitAnimationByIndex(.caster, ANIMATION_INDEX)
endif
elseif .attack and .target!=null then
set dx = GetUnitX(.target) - GetUnitX(.caster)
set dy = GetUnitY(.target) - GetUnitY(.caster)
set dis = SquareRoot(dx * dx + dy * dy)
set angle = Atan2(dy, dx)
if dis > COLLISION_DIST and IsUnitInRangeXY(.target, .cx, .cy, GetDistance(.lvl)) then
call SetUnitX(.caster, GetUnitX(.caster) + GetSpeed(.lvl) * Cos(angle))
call SetUnitY(.caster, GetUnitY(.caster) + GetSpeed(.lvl) * Sin(angle))
call SetUnitFacing(.caster, angle * bj_RADTODEG)
else
call xed.damageTarget(.caster,.target,GetDamage(.lvl))
set .attack = false
set .target = null
call SetUnitAnimationByIndex(.caster, ATTACK_AN_INDEX)
endif
elseif .n == GetTargetNum (.lvl) then
set .ready = false
call SetUnitAnimationByIndex(.caster, ANIMATION_INDEX)
endif
else
if .num == GetShadowNum(.lvl) then
set dx = GetUnitX(.shadow[.pos]) - GetUnitX(.caster)
set dy = GetUnitY(.shadow[.pos]) - GetUnitY(.caster)
set dis = SquareRoot(dx * dx + dy * dy)
set angle = Atan2(dy, dx)
if dis > 215. then
call SetUnitX(.caster, GetUnitX(.caster) + GetSpeed(.lvl) * Cos(angle))
call SetUnitY(.caster, GetUnitY(.caster) + GetSpeed(.lvl) * Sin(angle))
call SetUnitFacing(.caster, angle * bj_RADTODEG)
else
if not .back then
set .back = true
set .ready = true
set .attack = false
set .inpos = false
set .n = 0
set .target = null
set .pos = .pos - 1
if .pos < 0 then
set .pos = GetShadowNum(.lvl) - 1
endif
call RemoveUnit(.shadow[.pos])
set .shadow[.pos] = .caster
set .show[.pos] = true
call SetUnitAnimationByIndex(.caster, ANIMATION_INDEX)
endif
endif
endif
endif
if .sets and .ag > .cap then
set temp.idx = temp.idx - 1
if temp.idx < 0 then
set temp.idx = GetShadowNum(.lvl) - 1
endif
set .ag = 0
if not .show[.idx] then
set .num = .num + 1
if .num > GetShadowNum(.lvl) then
set .num = GetShadowNum(.lvl)
endif
if .num == GetShadowNum(.lvl) then
set .ready = true
set .shadow [.pos] = CreateUnit(GetOwningPlayer(.caster), SHADOW_TRAIL, GetUnitX(.caster), GetUnitY(.caster), GetUnitFacing(.caster))
call SetUnitTimeScale(.shadow[.pos], 2.)
call SetUnitColor(.shadow[.pos], PLAYER_COLOR_LIGHT_GRAY)
call SetUnitVertexColor(.shadow[.pos], 255, 255, 255, 125)
call SetUnitAnimationByIndex(.shadow[.pos], ANIMATION_INDEX)
call UnitAddAbility(.shadow[.pos], 'Aloc')
endif
set .show[.idx] = true
call ShowUnit(.shadow[.idx], true)
call UnitAddAbility(.shadow[.idx], 'Aloc')
call DestroyEffect(AddSpecialEffectTarget(IMAGE_FX, .shadow[.idx], "origin"))
endif
endif
loop
exitwhen i == GetShadowNum(.lvl)
if not .sets then
call GroupEnumUnitsInRange(ENUM_GROUP, GetUnitX(.shadow[i]), GetUnitY(.shadow[i]), COLLISION_DIST, function thistype.IsCaster)
set .idx = i
endif
set .ag = .ag + GetAngleSpeed(.lvl)
set .angle[i] = .angle[i] + GetAngleSpeed(.lvl)
call SetUnitX(.shadow[i], .cx + GetDistance(.lvl) * Cos(.angle[i]))
call SetUnitY(.shadow[i], .cy + GetDistance(.lvl) * Sin(.angle[i]))
call SetUnitFacing(.shadow[i], .angle[i] * bj_RADTODEG + 90. )
set i = i + 1
endloop
else
call .destroy()
endif
endmethod
private method onDestroy takes nothing returns nothing
local integer i = 0
call ReleaseTimer(.t)
call ReleaseGroup(.victim)
call SetUnitTimeScale(.caster, 1.)
loop
exitwhen i == GetShadowNum(.lvl)
set .show[i] = false
if .shadow[i] != .caster then
call SetUnitExploded(.shadow[i], true)
call KillUnit(.shadow[i])
endif
set i = i + 1
endloop
endmethod
endstruct
private struct data
unit caster
timer t
real tx
real ty
integer lvl
real cx
real cy
static method create takes unit c, real x, real y returns thistype
local thistype this = thistype.allocate()
local real angle = bj_RADTODEG * Atan2(y - GetUnitY(c), x - GetUnitX(c))
set .cx = x
set .cy = y
set .caster = c
set .t = NewTimer()
set .lvl = GetUnitAbilityLevel(c, SPELL_ID)
set .tx = x + GetDistance(.lvl) * Cos(angle)
set .ty = y + GetDistance(.lvl) * Sin(angle)
call SetUnitFacing(.caster, angle)
call SetUnitAnimationByIndex(.caster, ANIMATION_INDEX)
call SetUnitTimeScale(.caster, 2.)
return this
endmethod
private method onDestroy takes nothing returns nothing
call ReleaseTimer(.t)
endmethod
static method onLoop takes nothing returns nothing
local thistype this = thistype(GetTimerData(GetExpiredTimer()))
local real dx = .tx - GetUnitX(.caster)
local real dy = .ty - GetUnitY(.caster)
local real dis = SquareRoot(dx * dx + dy * dy)
local real angle = Atan2(dy, dx)
local shadow s
if dis > COLLISION_DIST and GetUnitCurrentOrder(.caster) == OrderId(ORDER_ID) then
call SetUnitX(.caster, GetUnitX(.caster) + GetSpeed(.lvl) * Cos(angle))
call SetUnitY(.caster, GetUnitY(.caster) + GetSpeed(.lvl) * Sin(angle))
call SetUnitFacing(.caster, angle * bj_RADTODEG)
else
call .destroy()
set s = shadow.create(.caster, .cx, .cy)
endif
endmethod
static method SpellEffect takes nothing returns boolean
local thistype this
local unit u = GetTriggerUnit()
local real x = GetSpellTargetX()
local real y = GetSpellTargetY()
if GetSpellAbilityId() == SPELL_ID then
set this = thistype.create(u, x, y)
call SetTimerData(.t, this)
call TimerStart(.t, XE_ANIMATION_PERIOD, true, function thistype.onLoop)
endif
set u = null
return false
endmethod
static method onInit takes nothing returns nothing
local trigger t = CreateTrigger()
call TriggerRegisterAnyUnitEventBJ(t, EVENT_PLAYER_UNIT_SPELL_EFFECT)
call TriggerAddCondition(t, Condition(function thistype.SpellEffect))
//init xedamage
set xed=xedamage.create()
call DamageOptions(xed)
call xed.useSpecialEffect(ON_DAMAGE_FX, DAMAGE_ATTCH)
//preload fx
static if PRELOAD_FX then
call Preload(IMAGE_FX)
call Preload(ON_DAMAGE_FX)
endif
endmethod
endstruct
endlibrary