/*
LOST SOULS
by Kricz
Version:
1.01b
Credits:
Vexorian & BruZzl3R_17C
Requires:
TimerUtils, xebasic, xecast, xedamage, xepreload, Table
Discription:
Summons a cricle of gravestones arround the target, damaging enemies
in it. Summons lost souls in several intervals, flying to enemies in
the circle and disease them on impact with one of three negative buffs.
*/
scope LostSouls //needs TimerUtils, xecast, xedamage, xepreload, Table, MyFunctions
globals
/*MAIN SETUP*/
//The Spell Id of the Spell
private constant integer SPELL_ID = 'A000'
//The Order-String of the Spell
private constant string ORDER_STRING = "sacrifice"
//The Number of Levels for the Spell
private constant integer LEVELS = 3
//The Number of the effects, the souls can cast
private constant integer DUMMY_SPELLS = 3
//Shouls all destructibles/trees be destroyed in the spell are on start?
private constant boolean DESTROY_TREES = true
//How much auras (effects) should be created in the middle of the circle?
private constant integer AURA_COUNT = 2
/*END OF MAIN SETUP*/
/*CASTER EFFECT SETUP*/
//Should a effect be created at the hero's location?
private constant boolean CASTER_EFFECT = true
//The effect created on the hero while channeling
private constant string CASTER_EFFECT_MODEL = "Abilities\\Spells\\Undead\\Possession\\PossessionCaster.mdl"
//The heigh of the hero effect
private constant real CASTER_EFFECT_HEIGH = 62.50
//The size of the created effect
private constant real CASTER_EFFECT_SIZE = 3.25
/*END OF CASTER EFFECT SETUP*/
/*SETUP FOR THE STONE CIRCLE*/
//The model of the stones
private constant string GRAVE_STONE_MODEL = "Abilities\\Spells\\Undead\\Graveyard\\GraveMarker.mdl"
//How much gravestones should be in the circle?
private constant integer STONE_COUNT = 11
//The size of the stones
private constant real STONE_SIZE = 1.35
/*END OF STONE CIRCLE SETUP*/
/*SETUP FOR THE SOULS*/
//The model of the souls
private constant string LOST_SOUL_MODEL = "Abilities\\Spells\\Undead\\Possession\\PossessionMissile.mdl"
//The distance a soul must have between the target to damage the target and get destroyed
private constant real DISTANCE = 8.5
//The Z-Value of the souls (high)
private constant real HEIGH = 72.50
//The speed of the souls
private constant real SPEED = 500.00
//How much souls can be alive per spell at the same time?
//If there are MAX_SOULS per cast alive, no new will be created until the other reached their target
private constant integer MAX_SOULS = 13
//The size of the souls
private constant real SOUL_SIZE = 1.3
/*END OF SOULS SETUP*/
//Don't change anything here!
//They are for private using!
private real array DAMAGE[LEVELS]
private real array DURATION[LEVELS]
private real array AOE[LEVELS]
private real array INTERVAL[LEVELS]
private string array MODEL_AURA[AURA_COUNT]
private real array AURA_HEIGH[AURA_COUNT]
private real array AURA_SIZE[AURA_COUNT]
private integer array DUMMY_SPELL_ID[DUMMY_SPELLS]
private string array DUMMY_ORDERS[DUMMY_SPELLS]
private integer tempint = 0
private unit tempunit = null
endglobals
//Setup for the damage dealt over the duration in the circle
private function SETUP_DAMAGE takes nothing returns nothing
set DAMAGE[1] = 200
set DAMAGE[2] = 300
set DAMAGE[3] = 400
endfunction
//Setup for the channel duration
private function SETUP_DURATION takes nothing returns nothing
set DURATION[1] = 5.00
set DURATION[2] = 6.00
set DURATION[3] = 7.00
endfunction
//Setup for the AoE each level
private function SETUP_AOE takes nothing returns nothing
set AOE[1] = 350.00
set AOE[2] = 400.00
set AOE[3] = 450.00
endfunction
//Setup for the Intervals between the summons
private function SETUP_INTERVAL takes nothing returns nothing
set INTERVAL[1] = 0.60
set INTERVAL[2] = 0.45
set INTERVAL[3] = 0.30
endfunction
//Setup for the order-id's for the dummys
private function SETUP_DUMMY_IDS takes nothing returns nothing
set DUMMY_SPELL_ID[0] = 'LSD1'
set DUMMY_SPELL_ID[1] = 'LSD2'
set DUMMY_SPELL_ID[2] = 'LSD3'
endfunction
//Setup for the order-strings for the dummys. Note, that the order strings must have the same array number like the ID's!!
private function SETUP_DUMMY_ORDERS takes nothing returns nothing
set DUMMY_ORDERS[0] = "curse"
set DUMMY_ORDERS[1] = "cripple"
set DUMMY_ORDERS[2] = "silence"
endfunction
//Setup for the Auras things...
private function SETUP_AURAS takes nothing returns nothing
set MODEL_AURA[0] = "Abilities\\Spells\\Human\\FlameStrike\\FlameStrikeTarget.mdl"
set AURA_HEIGH[0] = 2.50
set AURA_SIZE[0] = 1.00
set MODEL_AURA[1] = "Abilities\\Spells\\Undead\\Unsummon\\UnsummonTarget.mdl"
set AURA_HEIGH[1] = 0.00
set AURA_SIZE[1] = 1.80
endfunction
//Setup for the Damage-Options
//! textmacro LS_DAMAGE_OPTIONS
set .dmg.dtype = DAMAGE_TYPE_MAGIC
set .dmg.atype = ATTACK_TYPE_MAGIC
set .dmg.wtype = WEAPON_TYPE_WHOKNOWS
//! endtextmacro
/**************************/
/*END OF USER CONFIGURATION!*/
/**************************/
//Don't change anything below here!!
private function GroupGetRandomUnitEnum takes nothing returns nothing
set tempint = tempint + 1
if ( GetRandomInt(1, tempint) == 1 ) then
set tempunit = GetEnumUnit()
endif
endfunction
private function GroupGetRandomUnit takes group g returns unit
set tempint = 0
set tempunit = null
call ForGroup(g, function GroupGetRandomUnitEnum)
return tempunit
endfunction
private keyword soul
//The "Main-Struct". It countains the caster and all required datas, also the active souls
private struct spell
xefx array stone[STONE_COUNT] //The xefx models for the stone circle
xefx array aura[AURA_COUNT] //The xefx models for the auras
xefx cfx = 0 //The xefx effect created at the hero
unit caster = null //The channeling unit
real px = 0.00 //The X of the cast point
real py = 0.00 //The Y of the cast point
integer lvl = 0 //The level of the spell
timer t = null //The used timer
real ticker = 0.00 //The real counts the time
real counter = 0.00 //This real will summon the souls if it has the value of TICK
integer count = 0 //This counter is used to count actual living souls
boolean summon = true //If this is true, more souls will be summoned (also for debuging)
soul array souls[MAX_SOULS] //The souls, which are currently flying
real dist = 0.00 //I use this to store the distance between target point and caster to check if the caster moves during channeling, which is caused through Wc3 itself:
//If you cast the spell while channeling to a point far away from the caster location, he has to move to the new point.
//While moving, his actual oder isn't "move" but ORDER_STRING, and so, the old spell will be channeled further.
static delegate xecast cast = 0 //Used to cast the spells on impact
static group tempgroup = CreateGroup() //Group to detect units each interval
static xedamage dmg //Used to deal damage
static HandleTable table //Uses to detect, if a unit was channeling when casting again
static boolexpr filter //filter for tempgroup
static spell temp //required by the filter
//This is called, when the caster starts the spell. It creates all datas and attachs them to the table
private static method create takes unit caster, integer level, real x, real y returns spell
local spell this = spell.allocate()
local integer i = 0
local real dx = 0.00
local real dy = 0.00
local real angle = 360 / STONE_COUNT
set .caster = caster
set .lvl = level
set .px = x
set .py = y
set .t = NewTimer()
loop
exitwhen i >= AURA_COUNT
set .aura[i] = xefx.create(.px, .py, 0.00)
set .aura[i].fxpath = MODEL_AURA[i]
set .aura[i].scale = AURA_SIZE[i]
set .aura[i].z = AURA_HEIGH[i]
set i = i + 1
endloop
set i = 0
set .dist = SquareRoot((.px - GetUnitX(.caster)) * (.px - GetUnitX(.caster)) + (.py - GetUnitY(.caster)) * (.py - GetUnitY(.caster)))
set .table[.caster] = this
if DESTROY_TREES then
call .dmg.damageDestructablesAOE(.caster, .px, .py, AOE[.lvl], 1000000.00) //Yay i know, it's ugly^^
endif
loop
set angle = I2R(i) / STONE_COUNT * 2 * bj_PI
set dx = x + AOE[.lvl] * Cos(angle)
set dy = y + AOE[.lvl] * Sin(angle)
set angle = Atan2(y - dy, x - dx)
debug call BJDebugMsg("Creating new stone with index: " + I2S(i) + ", angle: " + R2S(angle))
set .stone[i] = xefx.create(dx, dy, angle)
set .stone[i].xyangle = angle
set .stone[i].fxpath = GRAVE_STONE_MODEL
set .stone[i].scale = STONE_SIZE
set i = i + 1
exitwhen i == STONE_COUNT
endloop
if CASTER_EFFECT then
set .cfx = xefx.create(GetUnitX(.caster), GetUnitY(.caster), 0.00)
set .cfx.fxpath = CASTER_EFFECT_MODEL
set .cfx.scale = CASTER_EFFECT_SIZE
set .cfx.z = CASTER_EFFECT_HEIGH
endif
call SetTimerData(.t, this)
call TimerStart(.t, XE_ANIMATION_PERIOD, true, function spell.periodic)
return this
endmethod
//This method will be called every XE_ANIMATION_PERIOD (that means 0.025 seconds) and moves the souls and checks, if the caster is still channeling
private static method periodic takes nothing returns nothing
local timer t = GetExpiredTimer()
local spell this = spell(GetTimerData(t))
local integer i = .count - 1
local unit u = null
local real newdist = SquareRoot((.px - GetUnitX(.caster)) * (.px - GetUnitX(.caster)) + (.py - GetUnitY(.caster)) * (.py - GetUnitY(.caster)))
set .ticker = .ticker + XE_ANIMATION_PERIOD
set .counter = .counter + XE_ANIMATION_PERIOD
if GetWidgetLife(.caster) <= 0.405 or .counter >= DURATION[.lvl] or GetUnitCurrentOrder(.caster) != OrderId(ORDER_STRING) or newdist != .dist then
if CASTER_EFFECT and .cfx != 0 then
call .cfx.destroy()
endif
if .count <= 0 then
debug call BJDebugMsg("Destroying Datas")
call .destroy()
else
debug call BJDebugMsg("Cannot destroy datas yet, because there are living souls, which require these datas.")
set .summon = false
endif
elseif GetWidgetLife(.caster) > 0.405 and .counter < DURATION[.lvl] and GetUnitCurrentOrder(.caster) == OrderId(ORDER_STRING) and newdist == .dist then
call GroupEnumUnitsInRange(.tempgroup, .px, .py, AOE[.lvl], .filter)
loop
set u = FirstOfGroup(.tempgroup)
exitwhen u == null
call SetUnitExploded(u, true)
call .dmg.damageTarget(.caster, u, DAMAGE[.lvl] / DURATION[.lvl] * XE_ANIMATION_PERIOD)
call SetUnitExploded(u, false)
call GroupRemoveUnit(.tempgroup, u)
set u = null
endloop
call GroupClear(.tempgroup)
if .ticker >= INTERVAL[.lvl] and .summon then
set .ticker = 0.00
call .SummonSoul()
endif
endif
loop
exitwhen i < 0
if not .souls[i].Control() then
debug call BJDebugMsg("A Soul reached its target and will be destroyed.")
set .count = .count - 1
if .count < 0 then
set .count = 0
else
set .souls[i] = .souls[.count]
endif
endif
set i = i - 1
endloop
endmethod
//Checks first, if a new soul can be summoned, if true, it creates one and attaches it to the main-struct
method SummonSoul takes nothing returns nothing
local unit u = null
local integer from = 0
if .count < MAX_SOULS - 1 then
set .temp = this
call GroupEnumUnitsInRange(.tempgroup, .px, .py, AOE[.lvl], .filter)
set u = GroupGetRandomUnit(.tempgroup)
call GroupClear(.tempgroup)
if u != null then
set from = GetRandomInt(0, STONE_COUNT -1)
set .souls[.count] = soul.create(this, from, u)
debug call BJDebugMsg("Registring soul in array number '" + I2S(.count) + "'.")
set .count = .count + 1
set u = null
endif
debug else
debug call BJDebugMsg("Soul Storage is full. Cannot create a new soul.")
endif
endmethod
//When the caster dies or the duration over is, the struct need to be destroyed with this method
method onDestroy takes nothing returns nothing
local integer i = 0
call ReleaseTimer(.t)
loop
exitwhen i >= STONE_COUNT
call .stone[i].destroy()
set i = i + 1
endloop
set i = 0
loop
exitwhen i >= .count
set .souls[i].destroyMe = true
set i = i + 1
endloop
set i = 0
loop
exitwhen i >= AURA_COUNT
call .aura[i].destroy()
set i = i + 1
endloop
if CASTER_EFFECT and .cfx != 0 then
call .cfx.destroy()
endif
call .table.flush(.caster)
endmethod
//This method creates the struct for the casting unit
private static method actions takes nothing returns boolean
local unit caster = GetTriggerUnit()
local integer lvl = GetUnitAbilityLevel(caster, SPELL_ID)
local spell this = spell.table[caster]
local real x = GetSpellTargetX()
local real y = GetSpellTargetY()
if GetSpellAbilityId() == SPELL_ID then
if spell.table.exists(caster) and this != null and this != 0 then
call .destroy()
debug call BJDebugMsg("Already channeling, destroying old datas and creating new ones.")
else
debug call BJDebugMsg("Caster wasn't channeling, creating datas.")
endif
call spell.create(caster, lvl, x, y)
endif
set caster = null
return true
endmethod
//This is the filter used to check for enemy units and so on...
private static method FilterFunc takes nothing returns boolean
return GetFilterUnit() != spell.temp.caster and GetWidgetLife(GetFilterUnit()) > 0.405 and not IsUnitType(GetFilterUnit(), UNIT_TYPE_RESISTANT) and not IsUnitType(GetFilterUnit(), UNIT_TYPE_MAGIC_IMMUNE) and IsUnitType(GetFilterUnit(), UNIT_TYPE_GROUND) and not IsUnitType(GetFilterUnit(), UNIT_TYPE_MECHANICAL) and not IsUnitType(GetFilterUnit(), UNIT_TYPE_STRUCTURE) and IsUnitEnemy(GetFilterUnit(), GetOwningPlayer(spell.temp.caster))
endmethod
private static method onInit takes nothing returns nothing
local trigger t = CreateTrigger()
local integer i = 0
set .table = HandleTable.create()
set .dmg = xedamage.create()
set .cast = xecast.create()
set .filter = Condition(function spell.FilterFunc)
//adding Events and Actions
call TriggerRegisterAnyUnitEventBJ(t, EVENT_PLAYER_UNIT_SPELL_CHANNEL)
call TriggerAddCondition(t, Condition(function spell.actions)) //cause conditions are faster than actions ;)
//call the Setup functions
call SETUP_DAMAGE()
call SETUP_DURATION()
call SETUP_AOE()
call SETUP_DUMMY_IDS()
call SETUP_DUMMY_ORDERS()
call SETUP_INTERVAL()
call SETUP_AURAS()
//! runtextmacro LS_DAMAGE_OPTIONS()
//Preloading dummy-spells and effects/models
call XE_PreloadAbility(SPELL_ID)
set i = 0
loop
call XE_PreloadAbility(DUMMY_SPELL_ID[i])
exitwhen i >= DUMMY_SPELLS - 1
set i = i + 1
endloop
call Preload(GRAVE_STONE_MODEL)
call Preload(LOST_SOUL_MODEL)
set i = 0
loop
exitwhen i == AURA_COUNT
call Preload(MODEL_AURA[i])
set i = i + 1
endloop
set t = null
endmethod
endstruct
//Each soul has it own struct.
private struct soul
delegate xefx model = 0 //the soul itself
delegate spell root = 0 //the struct the souls are from
unit target = null //The target of the soul
real angle = 0.00 //the angle of the soul
boolean destroyMe = false //If true, the missile will be destroyed in the next timer expiration
//creates the soul...
static method create takes spell root, integer from, unit target returns soul
local soul this = soul.allocate()
set .root = root
set .target = target
set .angle = Atan2(GetUnitY(.target) - .stone[from].y, GetUnitX(.target) - .stone[from].x)
set .model = xefx.create(.stone[from].x, .stone[from].y, .angle)
set .z = HEIGH
set .fxpath = LOST_SOUL_MODEL
set .scale = SOUL_SIZE
return this
endmethod
//Will be called every time for every soul, when the periodic method is called and returns if it should be destroy or not
method Control takes nothing returns boolean
local real dx = GetUnitX(.target) - .x
local real dy = GetUnitY(.target) - .y
local real dist = SquareRoot(dx * dx + dy * dy)
if dist > DISTANCE and .target != null and not .destroyMe and GetWidgetLife(.target) > 0.405 then
set .angle = Atan2(GetUnitY(.target) - .y, GetUnitX(.target) - .x)
set .x = .x + ( SPEED * XE_ANIMATION_PERIOD ) * Cos(.angle)
set .y = .y + ( SPEED * XE_ANIMATION_PERIOD ) * Sin(.angle)
set .xyangle = .angle
return true
elseif dist <= DISTANCE or .target == null or .destroyMe or GetWidgetLife(.target) <= 0.405 then
if GetWidgetLife(.target) > 0.405 and .target != null then
call .cast()
endif
call .destroy()
return false
endif
return false
endmethod
//casts the a random dummy spell on impact
method cast takes nothing returns nothing
local integer id = GetRandomInt(0, DUMMY_SPELLS - 1)
set spell.level = .lvl
set spell.orderstring = DUMMY_ORDERS[id]
set spell.abilityid = DUMMY_SPELL_ID[id]
set spell.owningplayer = GetOwningPlayer(.caster)
call spell.castOnTarget(.target)
debug call BJDebugMsg("Ability |cffffcc00" + GetObjectName(DUMMY_SPELL_ID[id]) + "|r was casted on a |cffffcc00" + GetUnitName(.target) + "|r, with level |cffffcc00" + I2S(.lvl) + "|r.")
endmethod
//destroys the souls
method onDestroy takes nothing returns nothing
call .model.destroy()
endmethod
endstruct
endscope