You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

2020 lines
69 KiB

4 years ago
-- WarlockDemonology.lua
-- June 2018
local addon, ns = ...
local Hekili = _G[ addon ]
local class = Hekili.Class
local state = Hekili.State
local PTR = ns.PTR
local FindUnitBuffByID, FindUnitDebuffByID = ns.FindUnitBuffByID, ns.FindUnitDebuffByID
4 years ago
local GetPlayerAuraBySpellID = _G.GetPlayerAuraBySpellID
local ceil = math.ceil
4 years ago
3 years ago
local RC = LibStub( "LibRangeCheck-2.0" )
4 years ago
-- Conduits
-- [-] borne_of_blood
-- [-] carnivorous_stalkers
-- [-] fel_commando
-- [x] tyrants_soul
if UnitClassBase( "player" ) == "WARLOCK" then
local spec = Hekili:NewSpecialization( 266, true )
spec:RegisterResource( Enum.PowerType.SoulShards )
spec:RegisterResource( Enum.PowerType.Mana )
-- Talents
spec:RegisterTalents( {
dreadlash = 19290, -- 264078
bilescourge_bombers = 22048, -- 267211
demonic_strength = 23138, -- 267171
demonic_calling = 22045, -- 205145
power_siphon = 21694, -- 264130
doom = 23158, -- 603
demon_skin = 19280, -- 219272
burning_rush = 19285, -- 111400
dark_pact = 19286, -- 108416
from_the_shadows = 22477, -- 267170
soul_strike = 22042, -- 264057
summon_vilefiend = 23160, -- 264119
vilefiend = 23160, -- 264119
4 years ago
darkfury = 22047, -- 264874
mortal_coil = 19291, -- 6789
howl_of_terror = 23465, -- 5484
soul_conduit = 23147, -- 215941
inner_demons = 23146, -- 267216
grimoire_felguard = 21717, -- 111898
sacrificed_souls = 23161, -- 267214
demonic_consumption = 22479, -- 267215
nether_portal = 23091, -- 267217
} )
-- PvP Talents
spec:RegisterPvpTalents( {
4 years ago
amplify_curse = 3507, -- 328774
bane_of_fragility = 3505, -- 199954
call_fel_lord = 162, -- 212459
call_felhunter = 156, -- 212619
call_observer = 165, -- 201996
casting_circle = 3626, -- 221703
essence_drain = 3625, -- 221711
fel_obelisk = 5400, -- 353601
gateway_mastery = 3506, -- 248855
master_summoner = 1213, -- 212628
nether_ward = 3624, -- 212295
pleasure_through_pain = 158, -- 212618
shadow_rift = 5394, -- 353294
} )
-- Demon Handling
local dreadstalkers = {}
local dreadstalkers_v = {}
local vilefiend = {}
local vilefiend_v = {}
local wild_imps = {}
local wild_imps_v = {}
4 years ago
local malicious_imps = {}
local malicious_imps_v = {}
4 years ago
local demonic_tyrant = {}
local demonic_tyrant_v = {}
local grim_felguard = {}
local grim_felguard_v = {}
local other_demon = {}
local other_demon_v = {}
local imps = {}
local guldan = {}
local guldan_v = {}
local last_summon = {}
local FindUnitBuffByID = ns.FindUnitBuffByID
local shards_for_guldan = 0
local function UpdateShardsForGuldan()
shards_for_guldan = UnitPower( "player", Enum.PowerType.SoulShards )
end
4 years ago
local first_combat_tyrant
4 years ago
-- March 27 APL Update
-- actions.precombat+=/variable,name=first_tyrant_time,op=set,value=12
-- actions.precombat+=/variable,name=first_tyrant_time,op=add,value=action.grimoire_felguard.execute_time,if=talent.grimoire_felguard.enabled
-- actions.precombat+=/variable,name=first_tyrant_time,op=add,value=action.summon_vilefiend.execute_time,if=talent.summon_vilefiend.enabled
-- actions.precombat+=/variable,name=first_tyrant_time,op=add,value=gcd.max,if=talent.grimoire_felguard.enabled|talent.summon_vilefiend.enabled
-- actions.precombat+=/variable,name=first_tyrant_time,op=sub,value=action.summon_demonic_tyrant.execute_time+action.shadow_bolt.execute_time
-- actions.precombat+=/variable,name=first_tyrant_time,op=min,value=10
4 years ago
spec:RegisterStateExpr( "first_tyrant_time", function()
if first_combat_tyrant and combat > 0 then
return first_combat_tyrant - combat
end
4 years ago
4 years ago
-- Tyrant is on CD, we're not starting fresh, skip opener.
if cooldown.summon_demonic_tyrant.true_remains > gcd.max then
return 0
4 years ago
end
local ftt = 12
local bonus_demon = false
if talent.grimoire_felguard.enabled then
ftt = ftt + action.grimoire_felguard.execute_time
bonus_demon = true
end
if talent.summon_vilefiend.enabled then
ftt = ftt + action.summon_vilefiend.execute_time
bonus_demon = true
end
if bonus_demon then
ftt = ftt + gcd.max
end
return min( 10, ftt - ( action.summon_demonic_tyrant.execute_time - action.shadow_bolt.execute_time ) )
4 years ago
end )
spec:RegisterStateExpr( "in_opener", function()
return time < first_tyrant_time
4 years ago
end )
3 years ago
local dreadstalkers_travel_time = 1
spec:RegisterCombatLogEvent( function( _, subtype, _, source, _, _, _, destGUID, _, _, _, spellID, spellName )
4 years ago
if source == state.GUID then
4 years ago
local now = GetTime()
4 years ago
4 years ago
if subtype == "SPELL_SUMMON" then
-- Wild Imp: 104317 (40) and 279910 (20).
4 years ago
if spellID == 104317 or spellID == 279910 then
4 years ago
local dur = ( spellID == 279910 and 20 or 40 )
table.insert( wild_imps, now + dur )
4 years ago
imps[ destGUID ] = {
t = now,
casts = 0,
4 years ago
expires = math.ceil( now + dur ),
max = math.ceil( now + dur )
4 years ago
}
if guldan[ 1 ] then
-- If this imp is impacting within 0.15s of the expected queued imp, remove that imp from the queue.
if abs( now - guldan[ 1 ] ) < 0.15 then
table.remove( guldan, 1 )
end
end
-- Expire missed/lost Gul'dan predictions.
while( guldan[ 1 ] ) do
if guldan[ 1 ] < now then
table.remove( guldan, 1 )
else
break
end
end
-- Grimoire Felguard
-- elseif spellID == 111898 then table.insert( grim_felguard, now + 17 )
-- Demonic Tyrant: 265187, 15 seconds uptime.
elseif spellID == 265187 then table.insert( demonic_tyrant, now + 15 )
-- for i = 1, #dreadstalkers do dreadstalkers[ i ] = dreadstalkers[ i ] + 15 end
-- for i = 1, #vilefiend do vilefiend[ i ] = vilefiend[ i ] + 15 end
-- for i = 1, #grim_felguard do grim_felguard[ i ] = grim_felguard[ i ] + 15 end
for i = 1, #wild_imps do wild_imps[ i ] = wild_imps[ i ] + 15 end
for _, imp in pairs( imps ) do
imp.expires = imp.expires + 15
imp.max = imp.max + 15
end
4 years ago
elseif spellID == 364198 then
-- Tier 28: Malicious Imp
imps[ destGUID ] = {
t = now,
casts = 0,
expires = math.ceil( now + 40 ),
max = math.ceil( now + 40 ),
malicious = true
}
table.insert( malicious_imps, now + 40 )
4 years ago
-- Other Demons, 15 seconds uptime.
-- 267986 - Prince Malchezaar
-- 267987 - Illidari Satyr
-- 267988 - Vicious Hellhound
-- 267989 - Eyes of Gul'dan
-- 267991 - Void Terror
-- 267992 - Bilescourge
-- 267994 - Shivarra
-- 267995 - Wrathguard
-- 267996 - Darkhound
elseif spellID >= 267986 and spellID <= 267996 then table.insert( other_demon, now + 15 ) end
elseif subtype == "SPELL_CAST_START" and spellID == 105174 then
C_Timer.After( 0.25, UpdateShardsForGuldan )
elseif subtype == "SPELL_CAST_SUCCESS" then
-- Implosion.
if spellID == 196277 then
table.wipe( wild_imps )
table.wipe( imps )
-- Power Siphon.
elseif spellID == 264130 then
if wild_imps[1] then table.remove( wild_imps, 1 ) end
if wild_imps[1] then table.remove( wild_imps, 1 ) end
for i = 1, 2 do
local lowest
for id, imp in pairs( imps ) do
if not lowest then lowest = id
elseif imp.expires < imps[ lowest ].expires then
lowest = id
end
end
if lowest then
imps[ lowest ] = nil
end
end
-- Hand of Guldan (queue imps).
elseif spellID == 105174 then
4 years ago
hog_time = now
4 years ago
if shards_for_guldan >= 1 then table.insert( guldan, now + 0.6 ) end
if shards_for_guldan >= 2 then table.insert( guldan, now + 0.8 ) end
if shards_for_guldan >= 3 then table.insert( guldan, now + 1 ) end
4 years ago
--[[ elseif spellID == 265187 and InCombatLockdown() and not first_combat_tyrant then
first_combat_tyrant = now ]]
3 years ago
-- Call Dreadstalkers (use travel time to determine buffer delay for Demonic Cores).
elseif spellID == 104316 then
-- TODO: Come up with a good estimate of the time it takes.
dreadstalkers_travel_time = ( select( 2, RC:GetRange( "target" ) ) or 25 ) / 25
4 years ago
end
end
elseif imps[ source ] and subtype == "SPELL_CAST_SUCCESS" then
4 years ago
local demonic_power = GetPlayerAuraBySpellID( 265273 )
local now = GetTime()
4 years ago
if not demonic_power then
local imp = imps[ source ]
imp.start = now
imp.casts = imp.casts + 1
imp.expires = min( imp.max, now + ( ( ( state.level > 55 and 7 or 6 ) - imp.casts ) * 2 * state.haste ) )
end
end
end )
4 years ago
spec:RegisterEvent( "PLAYER_REGEN_DISABLED", function ()
-- Rethinking this.
-- We'll try to make the opener work if Tyrant will be off CD anywhere from 10-20 seconds into the fight.
-- If it's later, we'll assume we're starting from the middle.
local tyrant, duration = GetSpellCooldown( 265187 )
local gcd, gcd_duration = GetSpellCooldown( 61304 )
tyrant = tyrant + duration
gcd = gcd + gcd_duration
if tyrant > gcd then
first_combat_tyrant = GetTime()
return
end
first_combat_tyrant = GetTime() + 10
end )
spec:RegisterEvent( "PLAYER_REGEN_ENABLED", function ()
first_combat_tyrant = nil
end )
4 years ago
local ExpireDreadstalkers = setfenv( function()
addStack( "demonic_core", nil, set_bonus.tier28_2pc > 0 and 3 or 2 )
end, state )
4 years ago
local wipe = table.wipe
spec:RegisterHook( "reset_precast", function()
local i = 1
for id, imp in pairs( imps ) do
if imp.expires < now then
imps[ id ] = nil
end
end
while( wild_imps[ i ] ) do
if wild_imps[ i ] < now then
table.remove( wild_imps, i )
else
i = i + 1
end
end
wipe( wild_imps_v )
4 years ago
wipe( malicious_imps_v )
for n, t in pairs( imps ) do
if t.malicious then table.insert( malicious_imps_v, t.expires )
else table.insert( wild_imps_v, t.expires ) end
end
4 years ago
table.sort( wild_imps_v )
4 years ago
table.sort( malicious_imps_v )
4 years ago
local difference = #wild_imps_v - GetSpellCount( 196277 )
while difference > 0 do
table.remove( wild_imps_v, 1 )
difference = difference - 1
end
wipe( guldan_v )
for n, t in ipairs( guldan ) do guldan_v[ n ] = t end
i = 1
while( other_demon[ i ] ) do
if other_demon[ i ] < now then
table.remove( other_demon, i )
else
i = i + 1
end
end
wipe( other_demon_v )
for n, t in ipairs( other_demon ) do other_demon_v[ n ] = t end
if #dreadstalkers_v > 0 then wipe( dreadstalkers_v ) end
if #vilefiend_v > 0 then wipe( vilefiend_v ) end
if #grim_felguard_v > 0 then wipe( grim_felguard_v ) end
if #demonic_tyrant_v > 0 then wipe( demonic_tyrant_v ) end
-- Pull major demons from Totem API.
for i = 1, 5 do
local exists, name, summoned, duration, texture = GetTotemInfo( i )
if exists then
local demon, extraTime = nil, 0
4 years ago
-- Grimoire Felguard
if texture == 136216 then
extraTime = action.grimoire_felguard.lastCast % 1
demon = grim_felguard_v
elseif texture == 1616211 then
extraTime = action.summon_vilefiend.lastCast % 1
demon = vilefiend_v
elseif texture == 1378282 then
extraTime = action.call_dreadstalkers.lastCast % 1
demon = dreadstalkers_v
elseif texture == 135002 then
extraTime = action.summon_demonic_tyrant.lastCast % 1
demon = demonic_tyrant_v
end
4 years ago
if demon then
insert( demon, summoned + duration + extraTime )
4 years ago
end
end
if #grim_felguard_v > 1 then table.sort( grim_felguard_v ) end
if #vilefiend_v > 1 then table.sort( vilefiend_v ) end
if #dreadstalkers_v > 1 then table.sort( dreadstalkers_v ) end
if #demonic_tyrant_v > 1 then table.sort( demonic_tyrant_v ) end
end
last_summon.name = nil
last_summon.at = nil
last_summon.count = nil
if demonic_tyrant_v[ 1 ] and demonic_tyrant_v[ 1 ] > query_time then
summonPet( "demonic_tyrant", demonic_tyrant_v[ 1 ] - query_time )
end
if buff.demonic_power.up and buff.demonic_power.remains > pet.demonic_tyrant.remains then
summonPet( "demonic_tyrant", buff.demonic_power.remains )
end
local subjugated, icon, count, debuffType, duration, expirationTime = FindUnitDebuffByID( "pet", 1098 )
if subjugated then
summonPet( "subjugated_demon", expirationTime - now )
else
dismissPet( "subjugated_demon" )
end
if buff.dreadstalkers.up then
3 years ago
state:QueueAuraExpiration( "dreadstalkers", ExpireDreadstalkers, 1 + buff.dreadstalkers.expires + dreadstalkers_travel_time )
end
class.abilities.summon_pet = class.abilities.summon_felguard
4 years ago
first_tyrant_time = nil
4 years ago
if Hekili.ActiveDebug then
4 years ago
Hekili:Debug( " - Dreadstalkers: %d, %.2f\n" ..
4 years ago
" - Vilefiend : %d, %.2f\n" ..
" - Grim Felguard: %d, %.2f\n" ..
" - Wild Imps : %d, %.2f\n" ..
4 years ago
" - Malicious Imp: %d, %.2f\n" ..
4 years ago
"Next Demon Exp. : %.2f",
buff.dreadstalkers.stack, buff.dreadstalkers.remains,
buff.vilefiend.stack, buff.vilefiend.remains,
buff.grimoire_felguard.stack, buff.grimoire_felguard.remains,
buff.wild_imps.stack, buff.wild_imps.remains,
4 years ago
buff.malicious_imps.stack, buff.malicious_imps.remains,
major_demon_remains )
4 years ago
end
end )
spec:RegisterHook( "advance_end", function ()
-- For virtual imps, assume they'll take 0.5s to start casting and then chain cast.
local longevity = 0.5 + ( state.level > 55 and 7 or 6 ) * 2 * state.haste
4 years ago
for i = #guldan_v, 1, -1 do
local imp = guldan_v[i]
if imp <= query_time then
if ( imp + longevity ) > query_time then
insert( wild_imps_v, imp + longevity )
4 years ago
end
remove( guldan_v, i )
end
end
end )
-- Provide a way to confirm if all Hand of Gul'dan imps have landed.
spec:RegisterStateExpr( "spawn_remains", function ()
if #guldan_v > 0 then
return max( 0, guldan_v[ #guldan_v ] - query_time )
end
return 0
end )
spec:RegisterHook( "spend", function( amt, resource )
if resource == "soul_shards" and amt > 0 then
if buff.nether_portal.up then
summon_demon( "other", 15, amt )
end
if legendary.wilfreds_sigil_of_superior_summoning.enabled then
reduceCooldown( "summon_demonic_tyrant", amt * 0.6 )
end
end
end )
spec:RegisterStateFunction( "summon_demon", function( name, duration, count )
local db = other_demon_v
if name == "dreadstalkers" then db = dreadstalkers_v
elseif name == "vilefiend" then db = vilefiend_v
elseif name == "wild_imps" then db = wild_imps_v
4 years ago
elseif name == "malicious_imps" then db = malicious_imps_v
4 years ago
elseif name == "grimoire_felguard" then db = grim_felguard_v
elseif name == "demonic_tyrant" then db = demonic_tyrant_v end
count = count or 1
local expires = query_time + duration
last_summon.name = name
last_summon.at = query_time
last_summon.count = count
for i = 1, count do
table.insert( db, expires )
end
end )
spec:RegisterStateFunction( "extend_demons", function( duration )
duration = duration or 15
4 years ago
for k, v in pairs( dreadstalkers_v ) do dreadstalkers_v [ k ] = v + duration end
for k, v in pairs( vilefiend_v ) do vilefiend_v [ k ] = v + duration end
for k, v in pairs( wild_imps_v ) do wild_imps_v [ k ] = v + duration end
for k, v in pairs( malicious_imps_v) do malicious_imps_v[ k ] = v + duration end
for k, v in pairs( grim_felguard_v ) do grim_felguard_v [ k ] = v + duration end
for k, v in pairs( other_demon_v ) do other_demon_v [ k ] = v + duration end
4 years ago
end )
spec:RegisterStateFunction( "consume_demons", function( name, count )
local db = other_demon_v
if name == "dreadstalkers" then db = dreadstalkers_v
elseif name == "vilefiend" then db = vilefiend_v
elseif name == "wild_imps" then db = wild_imps_v
4 years ago
elseif name == "malicious_imps" then db = malicious_imps_v
4 years ago
elseif name == "grimoire_felguard" then db = grim_felguard_v
elseif name == "demonic_tyrant" then db = demonic_tyrant_v end
if type( count ) == "string" and count == "all" then
table.wipe( db )
-- Wipe queued Guldan imps that should have landed by now.
if name == "wild_imps" then
while( guldan_v[ 1 ] ) do
if guldan_v[ 1 ] < now then table.remove( guldan_v, 1 )
else break end
end
end
return
end
count = count or 0
if count >= #db then
count = count - #db
table.wipe( db )
end
while( count > 0 ) do
if not db[1] then break end
table.remove( db, 1 )
count = count - 1
end
if name == "wild_imps" and count > 0 then
while( count > 0 ) do
if not guldan_v[1] or guldan_v[1] > now then break end
table.remove( guldan_v, 1 )
count = count - 1
end
end
end )
spec:RegisterStateExpr( "soul_shard", function () return soul_shards.current end )
-- How long before you can complete a 3 Soul Shard HoG cast.
spec:RegisterStateExpr( "time_to_hog", function ()
local shards_needed = max( 0, 3 - soul_shards.current )
local cast_time = action.hand_of_guldan.cast_time
if shards_needed > 0 then
local cores = min( shards_needed, buff.demonic_core.stack )
if cores > 0 then
cast_time = cast_time + cores * gcd.execute
shards_needed = shards_needed - cores
end
cast_time = cast_time + shards_needed * action.shadow_bolt.cast_time
end
return cast_time
end )
spec:RegisterStateExpr( "major_demons_active", function ()
return ( buff.grimoire_felguard.up and 1 or 0 ) + ( buff.vilefiend.up and 1 or 0 ) + ( buff.dreadstalkers.up and 1 or 0 )
end )
-- When the next major demon (anything but Wild Imps) expires.
spec:RegisterStateExpr( "major_demon_remains", function ()
4 years ago
local expire = 3600
if buff.grimoire_felguard.up then expire = min( expire, buff.grimoire_felguard.remains ) end
if buff.vilefiend.up then expire = min( expire, buff.vilefiend.remains ) end
if buff.dreadstalkers.up then expire = min( expire, buff.dreadstalkers.remains ) end
if expire == 3600 then return 0 end
return expire
end )
-- New imp forecasting expressions for Demo.
spec:RegisterStateExpr( "incoming_imps", function ()
local n = 0
for i, time in ipairs( guldan_v ) do
if time > query_time then
n = n + 1
end
4 years ago
end
return n
end )
local time_to_n = 0
spec:RegisterStateTable( "query_imp_spawn", setmetatable( {}, {
__index = function( t, k )
if k ~= "remains" then return 0 end
local queued = #guldan_v
if queued == 0 then return 0 end
if time_to_n == 0 or time_to_n >= queued then
return max( 0, guldan_v[ queued ] - query_time )
end
local count = 0
local remains = 0
for i, time in ipairs( guldan_v ) do
if time > query_time then
count = count + 1
remains = time - query_time
if count >= time_to_n then break end
end
end
return remains
end,
} ) )
spec:RegisterStateTable( "time_to_imps", setmetatable( {}, {
__index = function( t, k )
if type( k ) == "number" then
time_to_n = min( #guldan_v, k )
elseif k == "all" then
time_to_n = #guldan_v
else
return 0
4 years ago
end
return query_imp_spawn.remains
4 years ago
end
} ) )
local debugstack = debugstack
spec:RegisterStateTable( "imps_spawned_during", setmetatable( {}, {
__index = function( t, k, v )
local cap = query_time
if type(k) == "number" then cap = cap + ( k / 1000 )
else
if not class.abilities[ k ] then k = "summon_demonic_tyrant" end
cap = cap + action[ k ].cast
end
-- In SimC, k would be a numeric value to be interpreted but I don't see the point.
-- We're only using it for SDT now, and I don't know what else we'd really use it for.
-- So imps_spawned_during.summon_demonic_tyrant would be the syntax I'll use here.
local n = 0
for i, spawn in ipairs( guldan_v ) do
if spawn > cap then break end
if spawn > query_time then n = n + 1 end
end
return n
end,
} ) )
-- Auras
spec:RegisterAuras( {
axe_toss = {
id = 89766,
duration = 4,
max_stack = 1,
},
banish = {
id = 710,
duration = 30,
max_stack = 1,
},
bile_spit = {
id = 267997,
duration = 10,
max_stack = 1,
},
burning_rush = {
id = 111400,
duration = 3600,
max_stack = 1,
},
corruption = {
id = 146739,
duration = 14,
type = "Magic",
max_stack = 1,
},
curse_of_tongues = {
id = 1714,
duration = 60,
type = "Curse",
max_stack = 1,
},
curse_of_weakness = {
id = 702,
duration = 120,
type = "Curse",
max_stack = 1,
},
dark_pact = {
id = 108416,
duration = 20,
max_stack = 1,
},
demonic_calling = {
id = 205146,
duration = 20,
type = "Magic",
max_stack = 1,
},
demonic_circle = {
id = 48018,
duration = 900,
max_stack = 1,
},
demonic_circle_teleport = {
id = 48020,
},
demonic_core = {
id = 264173,
duration = 20,
max_stack = 4,
},
demonic_power = {
id = 265273,
duration = 15,
max_stack = 1,
copy = "tyrant"
},
demonic_strength = {
id = 267171,
duration = 20,
max_stack = 1,
},
doom = {
id = 603,
duration = function () return 20 * haste end,
tick_time = function () return 20 * haste end,
max_stack = 1,
},
drain_life = {
id = 234153,
duration = function () return 5 * haste * ( legendary.claw_of_endereth.enabled and 0.5 or 1 ) end,
tick_time = function () return haste * ( legendary.claw_of_endereth.enabled and 0.5 or 1 ) end,
max_stack = 1,
},
eye_of_kilrogg = {
id = 126,
},
fear = {
id = 118699,
duration = 20,
type = "Magic",
max_stack = 1,
},
fel_domination = {
id = 333889,
duration = 15,
type = "Magic",
max_stack = 1,
},
felstorm = {
id = 89751,
duration = function () return 5 * haste end,
tick_time = function () return 1 * haste end,
max_stack = 1,
generate = function ()
local fs = buff.felstorm
local name, _, count, _, duration, expires, caster = FindUnitBuffByID( "pet", 89751 )
if name then
fs.count = 1
fs.applied = expires - duration
fs.expires = expires
fs.caster = "pet"
return
end
fs.count = 0
fs.applied = 0
fs.expires = 0
fs.caster = "nobody"
end,
},
from_the_shadows = {
id = 270569,
duration = 12,
max_stack = 1,
},
howl_of_terror = {
id = 5484,
duration = 20,
type = "Magic",
max_stack = 1,
},
legion_strike = {
id = 30213,
duration = 6,
max_stack = 1,
},
mortal_coil = {
id = 6789,
duration = 3,
type = "Magic",
max_stack = 1,
},
nether_portal = {
id = 267218,
duration = 15,
max_stack = 1,
},
ritual_of_summoning = {
id = 698,
},
shadowfury = {
id = 30283,
duration = 3,
type = "Magic",
max_stack = 1,
},
soul_leech = {
id = 108366,
duration = 15,
max_stack = 1,
},
soul_link = {
id = 108415,
},
soulstone = {
id = 20707,
duration = 900,
max_stack = 1,
},
subjugate_demon = {
id = 1098,
duration = 300,
max_stack = 1,
},
unending_breath = {
id = 5697,
duration = 600,
max_stack = 1,
},
unending_resolve = {
id = 104773,
duration = 8,
max_stack = 1,
},
dreadstalkers = {
duration = 12,
meta = {
up = function ()
local exp = dreadstalkers_v[ #dreadstalkers_v ]
return exp and exp >= query_time or false
end,
down = function ( t ) return not t.up end,
applied = function () local exp = dreadstalkers_v[ #dreadstalkers_v ]; return exp and ( exp - 12 ) or 0 end,
expires = function () return dreadstalkers_v[ #dreadstalkers_v ] or 0 end,
4 years ago
count = function ()
local c = 0
for i, exp in ipairs( dreadstalkers_v ) do
4 years ago
if exp >= query_time then c = c + ( set_bonus.tier28_2pc > 0 and 3 or 2 ) end
4 years ago
end
return c
end,
}
},
grimoire_felguard = {
duration = 17,
meta = {
up = function () local exp = grim_felguard_v[ #grim_felguard_v ]; return exp and exp >= query_time or false end,
down = function ( t ) return not t.up end,
applied = function () local exp = grim_felguard_v[ #grim_felguard_v ]; return exp and ( exp - 12 ) or 0 end,
expires = function () return grim_felguard_v[ #grim_felguard_v ] or 0 end,
4 years ago
count = function ()
local c = 0
for i, exp in ipairs( grim_felguard_v ) do
if exp > query_time then c = c + 1 end
end
return c
end,
}
},
vilefiend = {
duration = 15,
meta = {
up = function () local exp = vilefiend_v[ #vilefiend_v ]; return exp and exp >= query_time or false end,
down = function ( t ) return not t.up end,
applied = function () local exp = vilefiend_v[ #vilefiend_v ]; return exp and ( exp - 15 ) or 0 end,
expires = function () return vilefiend_v[ #vilefiend_v ] or 0 end,
4 years ago
count = function ()
local c = 0
for i, exp in ipairs( vilefiend_v ) do
if exp > query_time then c = c + 1 end
end
return c
end,
}
},
wild_imps = {
4 years ago
duration = 40,
4 years ago
meta = {
up = function () local exp = wild_imps_v[ #wild_imps_v ]; return exp and exp >= query_time or false end,
down = function ( t ) return not t.up end,
applied = function () local exp = wild_imps_v[ #wild_imps_v ]; return exp and ( exp - 40 ) or 0 end,
expires = function () return wild_imps_v[ #wild_imps_v ] or 0 end,
4 years ago
count = function ()
local c = 0
for i, exp in ipairs( wild_imps_v ) do
if exp > query_time then c = c + 1 end
end
4 years ago
return c
end,
}
},
4 years ago
4 years ago
malicious_imps = {
duration = 40,
meta = {
up = function () local exp = malicious_imps_v[ #malicious_imps_v ]; return exp and exp >= query_time or false end,
down = function ( t ) return not t.up end,
applied = function () local exp = malicious_imps_v[ #malicious_imps_v ]; return exp and ( exp - 40 ) or 0 end,
expires = function () return malicious_imps_v[ #malicious_imps_v ] or 0 end,
4 years ago
count = function ()
local c = 0
for i, exp in ipairs( malicious_imps_v ) do
if exp > query_time then c = c + 1 end
4 years ago
end
return c
end,
}
},
other_demon = {
duration = 20,
meta = {
up = function () local exp = other_demon_v[ #other_demon_v ]; return exp and exp >= query_time or false end,
4 years ago
down = function ( t ) return not t.up end,
applied = function () local exp = other_demon_v[ #other_demon_v ]; return exp and ( exp - 15 ) or 0 end,
expires = function () return other_demon_v[ #other_demon_v ] or 0 end,
4 years ago
count = function ()
local c = 0
for i, exp in ipairs( other_demon_v ) do
if exp > query_time then c = c + 1 end
end
return c
end,
}
},
-- Azerite Powers
forbidden_knowledge = {
id = 279666,
duration = 15,
max_stack = 1,
},
} )
local Glyphed = IsSpellKnownOrOverridesKnown
-- Fel Imp 58959
spec:RegisterPet( "imp",
function() return Glyphed( 112866 ) and 58959 or 416 end,
"summon_imp",
3600 )
-- Voidlord 58960
spec:RegisterPet( "voidwalker",
function() return Glyphed( 112867 ) and 58960 or 1860 end,
"summon_voidwalker",
3600 )
-- Observer 58964
spec:RegisterPet( "felhunter",
function() return Glyphed( 112869 ) and 58964 or 417 end,
"summon_felhunter",
3600 )
-- Fel Succubus 120526
-- Shadow Succubus 120527
-- Shivarra 58963
spec:RegisterPet( "sayaad",
4 years ago
function()
if Glyphed( 240263 ) then return 120526
elseif Glyphed( 240266 ) then return 120527
elseif Glyphed( 112868 ) then return 58963
elseif Glyphed( 365349 ) then return 184600
end
4 years ago
return 1863
end,
"summon_sayaad",
3600,
"incubus", "succubus" )
4 years ago
-- Wrathguard 58965
spec:RegisterPet( "felguard",
function() return Glyphed( 112870 ) and 58965 or 17252 end,
"summon_felguard",
3600 )
spec:RegisterPet( "doomguard",
11859,
"ritual_of_doom",
300 )
4 years ago
--[[ Demonic Tyrant
spec:RegisterPet( "demonic_tyrant",
135002,
"summon_demonic_tyrant",
15 ) ]]
spec:RegisterTotem( "demonic_tyrant", 135002 )
4 years ago
spec:RegisterTotem( "vilefiend", 1616211 )
spec:RegisterTotem( "grimoire_felguard", 136216 )
spec:RegisterTotem( "dreadstalker", 1378282 )
spec:RegisterStateExpr( "extra_shards", function () return 0 end )
4 years ago
spec:RegisterStateExpr( "last_cast_imps", function ()
local count = 0
for i, imp in ipairs( wild_imps_v ) do
if imp - query_time <= 2 * haste then count = count + 1 end
end
return count
end )
spec:RegisterStateExpr( "two_cast_imps", function ()
4 years ago
local count = 0
for i, imp in ipairs( wild_imps_v ) do
if imp - query_time <= 4 * haste then count = count + 1 end
end
return count
end )
-- Tier 28
spec:RegisterSetBonuses( "tier28_2pc", 364436, "tier28_4pc", 3643951 )
-- 2-Set - Ripped From the Portal - Call Dreadstalkers has a 100% chance to summon an additional Dreadstalker.
-- 4-Set - Malicious Imp-Pact - Your Hand of Gul'dan has a 15% chance per Soul Shard to summon a Malicious Imp. When slain, Malicious Imp will either deal (85% of Spell power) Fire damage to all nearby enemies of your Implosion or deal it to your current target.
4 years ago
-- Abilities
spec:RegisterAbilities( {
axe_toss = {
id = 119914,
known = function () return IsSpellKnownOrOverridesKnown( 119914 ) end,
cast = 0,
cooldown = 30,
gcd = "spell",
startsCombat = true,
toggle = "interrupts",
interrupt = true,
debuff = "casting",
readyTime = state.timeToInterrupt,
usable = function () return pet.exists end,
handler = function ()
interrupt()
applyDebuff( "target", "axe_toss", 4 )
end,
copy = 119914
},
banish = {
id = 710,
cast = 1.5,
cooldown = 0,
gcd = "spell",
spend = 0.02,
spendType = "mana",
startsCombat = true,
handler = function ()
applyDebuff( "target", "banish", 30 )
end,
},
bilescourge_bombers = {
id = 267211,
cast = 0,
cooldown = 30,
gcd = "spell",
spend = 2,
spendType = "soul_shards",
talent = "bilescourge_bombers",
startsCombat = true,
handler = function ()
end,
},
burning_rush = {
id = 111400,
cast = 0,
cooldown = 0,
gcd = "spell",
startsCombat = true,
talent = "burning_rush",
handler = function ()
if buff.burning_rush.up then removeBuff( "burning_rush" )
else applyBuff( "burning_rush", 3600 ) end
end,
},
call_felhunter = {
id = 212619,
cast = 0,
cooldown = 24,
gcd = "spell",
spend = 0.01,
spendType = "mana",
startsCombat = true,
texture = 136174,
toggle = "interrupts",
interrupt = true,
pvptalent = "call_felhunter",
debuff = "casting",
readyTime = state.timeToInterrupt,
handler = function ()
interrupt()
end,
},
-- PvP:master_summoner.
call_dreadstalkers = {
id = 104316,
cast = function () if pvptalent.master_summoner.enabled then return 0 end
return buff.demonic_calling.up and 0 or ( ( level > 53 and 1.5 or 2 ) * haste )
end,
cooldown = 20,
gcd = "spell",
spend = function () return buff.demonic_calling.up and 0 or 2 end,
spendType = "soul_shards",
startsCombat = true,
handler = function ()
4 years ago
summon_demon( "dreadstalkers", 12, set_bonus.tier28_2pc > 0 and 3 or 2 )
applyBuff( "dreadstalkers", 12, set_bonus.tier28_2pc > 0 and 3 or 2 )
4 years ago
summonPet( "dreadstalker", 12 )
removeStack( "demonic_calling" )
if talent.from_the_shadows.enabled then applyDebuff( "target", "from_the_shadows" ) end
end,
},
--[[ command_demon = {
id = 119898,
cast = 0,
cooldown = 0,
gcd = "spell",
startsCombat = true,
handler = function ()
if pet.felguard.up then runHandler( "axe_toss" )
elseif pet.felhunter.up then runHandler( "spell_lock" )
elseif pet.voidwalker.up then runHandler( "shadow_bulwark" )
elseif pet.succubus.up then runHandler( "seduction" )
elseif pet.imp.up then runHandler( "singe_magic" ) end
end,
}, ]]
create_healthstone = {
id = 6201,
cast = function () return 3 * haste end,
cooldown = 0,
gcd = "spell",
spend = 0.02,
spendType = "mana",
startsCombat = true,
handler = function ()
end,
},
create_soulwell = {
id = 29893,
cast = function () return 3 * haste end,
cooldown = 120,
gcd = "spell",
spend = 0.05,
spendType = "mana",
startsCombat = true,
handler = function ()
end,
},
dark_pact = {
id = 108416,
cast = 0,
cooldown = 60,
gcd = "spell",
startsCombat = true,
talent = "dark_pact",
handler = function ()
applyBuff( "dark_pact", 20 )
end,
},
demonbolt = {
id = 264178,
cast = function () return ( buff.demonic_core.up and 0 or 4.5 ) * haste end,
cooldown = 0,
gcd = "spell",
spend = 0.02,
spendType = "mana",
startsCombat = true,
handler = function ()
if buff.forbidden_knowledge.up and buff.demonic_core.down then
removeBuff( "forbidden_knowledge" )
end
removeStack( "demonic_core" )
4 years ago
removeStack( "power_siphon" )
4 years ago
removeStack( "decimating_bolt" )
gain( 2, "soul_shards" )
end,
},
demonic_circle = {
id = 48018,
cast = 0.5,
cooldown = 10,
gcd = "spell",
spend = 0.02,
spendType = "mana",
startsCombat = false,
nobuff = "demonic_circle",
handler = function ()
applyBuff( "demonic_circle" )
end,
},
demonic_circle_teleport = {
id = 48020,
cast = 0,
cooldown = 30,
gcd = "spell",
spend = 0.03,
spendType = "mana",
startsCombat = false,
talent = "demonic_circle",
buff = "demonic_circle",
handler = function ()
if conduit.demonic_momentum.enabled then applyBuff( "demonic_momentum" ) end
end,
auras = {
-- Conduit
demonic_momentum = {
id = 339412,
duration = 5,
max_stack = 1
}
}
},
demonic_gateway = {
id = 111771,
cast = function () return legendary.pillars_of_the_dark_portal.enabled and 0 or 2 end,
cooldown = 10,
gcd = "spell",
spend = 0.03,
spendType = "mana",
startsCombat = false,
4 years ago
handler = function ()
end,
},
demonic_strength = {
id = 267171,
cast = 0,
cooldown = 60,
gcd = "spell",
startsCombat = false,
nobuff = "felstorm",
handler = function ()
applyBuff( "demonic_strength" )
end,
},
doom = {
id = 603,
cast = 0,
cooldown = 0,
gcd = "spell",
spend = 0.01,
spendType = "mana",
startsCombat = true,
talent = "doom",
cycle = "doom",
min_ttd = function () return 3 + debuff.doom.duration end,
-- readyTime = function () return IsCycling() and 0 or debuff.doom.remains end,
-- usable = function () return IsCycling() or ( target.time_to_die < 3600 and target.time_to_die > debuff.doom.duration ) end,
handler = function ()
applyDebuff( "target", "doom" )
end,
},
drain_life = {
id = 234153,
cast = function () return 5 * haste * ( legendary.claw_of_endereth.enabled and 0.5 or 1 ) end,
cooldown = 0,
channeled = true,
gcd = "spell",
spend = function () return debuff.soul_rot.up and 0 or 0.03 end,
spendType = "mana",
startsCombat = true,
start = function ()
applyDebuff( "drain_life" )
end,
finish = function ()
if conduit.accrued_vitality.enabled then applyBuff( "accrued_vitality" ) end
end,
},
eye_of_kilrogg = {
id = 126,
cast = function () return 2 * haste end,
cooldown = 0,
gcd = "spell",
spend = 0.03,
spendType = "mana",
startsCombat = true,
handler = function ()
end,
},
fear = {
id = 5782,
cast = function () return 1.7 * haste end,
cooldown = 0,
gcd = "spell",
spend = 0.05,
spendType = "mana",
startsCombat = true,
handler = function ()
applyDebuff( "target", "fear" )
end,
},
fel_domination = {
id = 333889,
cast = 0,
cooldown = function () return 180 + conduit.fel_celerity.mod * 0.001 end,
gcd = "spell",
startsCombat = false,
texture = 237564,
essential = true,
nomounted = true,
nobuff = "grimoire_of_sacrifice",
handler = function ()
applyBuff( "fel_domination" )
end,
},
grimoire_felguard = {
id = 111898,
cast = 0,
cooldown = 120,
gcd = "spell",
spend = 1,
spendType = "soul_shards",
toggle = "cooldowns",
startsCombat = true,
handler = function ()
summon_demon( "grimoire_felguard", 15 )
applyBuff( "grimoire_felguard" )
summonPet( "grimoire_felguard" )
end,
},
hand_of_guldan = {
id = 105174,
cast = function () return 1.5 * haste end,
cooldown = 0,
gcd = "spell",
spend = 1,
spendType = "soul_shards",
startsCombat = true,
-- usable = function () return soul_shards.current >= 3 end,
handler = function ()
extra_shards = min( 2, soul_shards.current )
if Hekili.ActiveDebug then Hekili:Debug( "Extra Shards: %d", extra_shards ) end
spend( extra_shards, "soul_shards" )
insert( guldan_v, query_time + 0.6 )
if extra_shards > 0 then insert( guldan_v, query_time + 0.8 ) end
if extra_shards > 1 then insert( guldan_v, query_time + 1 ) end
end,
},
health_funnel = {
id = 755,
cast = function () return 5 * haste end,
cooldown = 0,
gcd = "spell",
channeled = true,
startsCombat = false,
texture = 607852,
usable = function () return pet.alive and pet.health_pct < 100, "requires injured demon" end,
start = function ()
applyBuff( "health_funnel" )
end,
},
implosion = {
id = 196277,
cast = 0,
cooldown = 0,
gcd = "spell",
spend = 0.02,
spendType = "mana",
startsCombat = true,
texture = 2065588,
velocity = 30,
usable = function ()
if buff.wild_imps.stack < 3 and azerite.explosive_potential.enabled then return false, "too few imps for explosive_potential"
elseif buff.wild_imps.stack < 1 then return false, "no imps available" end
return true
end,
handler = function ()
if azerite.explosive_potential.enabled and buff.wild_imps.stack >= 3 then applyBuff( "explosive_potential" ) end
if legendary.implosive_potential.enabled then
if buff.implosive_potential.up then
stat.haste = stat.haste - 0.01 * buff.implosive_potential.v1
removeBuff( "implosive_potential" )
end
4 years ago
if buff.implosive_potential.down then stat.haste = stat.haste + 0.05 * buff.wild_imps.stack end
applyBuff( "implosive_potential", 12 )
stat.haste = stat.haste + ( active_enemies > 2 and 0.05 or 0.01 ) * buff.wild_imps.stack
buff.implosive_potential.v1 = ( active_enemies > 2 and 5 or 1 ) * buff.wild_imps.stack
4 years ago
end
consume_demons( "wild_imps", "all" )
4 years ago
if buff.malicious_imps.up then
consume_demons( "malicious_imps", "all" )
end
4 years ago
end,
auras = {
implosive_potential = {
id = 337139,
duration = 8,
max_stack = 1
}
}
},
mortal_coil = {
id = 6789,
cast = 0,
cooldown = 45,
gcd = "spell",
spend = 0.02,
spendType = "mana",
startsCombat = true,
texture = 607853,
handler = function ()
applyDebuff( "target", "mortal_coil" )
end,
},
nether_portal = {
id = 267217,
cast = function () return 2.5 * haste end,
cooldown = 180,
gcd = "spell",
spend = 1,
spendType = "soul_shards",
toggle = "cooldowns",
startsCombat = false,
texture = 2065615,
handler = function ()
applyBuff( "nether_portal" )
end,
},
power_siphon = {
id = 264130,
cast = 0,
cooldown = 30,
gcd = "spell",
startsCombat = false,
texture = 236290,
talent = "power_siphon",
readyTime = function ()
if buff.wild_imps.stack >= 2 then return 0 end
local imp_deficit = 2 - buff.wild_imps.stack
for i, imp in ipairs( guldan_v ) do
if imp > query_time then
imp_deficit = imp_deficit - 1
if imp_deficit == 0 then return imp - query_time end
end
end
return 3600
end,
handler = function ()
local num = min( 2, buff.wild_imps.count )
consume_demons( "wild_imps", num )
addStack( "demonic_core", 20, num )
4 years ago
addStack( "power_siphon", 20, num )
4 years ago
end,
4 years ago
auras = {
power_siphon = {
id = 334581,
duration = 20,
max_stack = 2,
generate = function( t )
-- Detect via hidden aura.
local name, _, count, _, duration, expires, caster, _, _, spellID = GetPlayerAuraBySpellID( 334581 )
if name then
t.count = max( 1, count )
t.expires = expires
t.applied = expires - duration
t.caster = caster
return
end
4 years ago
t.count = 0
t.expires = 0
t.applied = 0
t.caster = "nobody"
end
}
}
4 years ago
},
ritual_of_summoning = {
id = 698,
cast = 0,
cooldown = 120,
gcd = "spell",
spend = 0,
spendType = "mana",
startsCombat = true,
texture = 136223,
handler = function ()
end,
},
shadow_bolt = {
id = 686,
cast = function () return 2 * haste end,
cooldown = 0,
gcd = "spell",
spend = 0.02,
spendType = "mana",
startsCombat = true,
texture = 136197,
handler = function ()
gain( 1, "soul_shards" )
if legendary.balespiders_burning_core.enabled then
addStack( "balespiders_burning_core", nil, 1 )
end
end,
auras = {
balespiders_burning_core = {
id = 337161,
duration = 15,
max_stack = 4
}
}
},
shadowfury = {
id = 30283,
cast = function () return 1.5 * haste end,
cooldown = function () return talent.darkfury.enabled and 45 or 60 end,
gcd = "spell",
startsCombat = true,
texture = 607865,
handler = function ()
applyDebuff( "target", "shadowfury" )
end,
},
soul_strike = {
id = 264057,
cast = 0,
cooldown = 10,
gcd = "spell",
startsCombat = true,
texture = 1452864,
usable = function () return pet.felguard.up and pet.alive, "requires living felguard" end,
handler = function ()
gain( 1, "soul_shards" )
end,
},
soulstone = {
id = 20707,
cast = 3,
cooldown = 600,
gcd = "spell",
spend = 0.01,
spendType = "mana",
toggle = "cooldowns",
startsCombat = false,
texture = 136210,
handler = function ()
applyBuff( "soulstone" )
end,
},
subjugate_demon = {
id = 1098,
cast = 3,
cooldown = 0,
gcd = "spell",
spend = 0.02,
spendType = "mana",
startsCombat = false,
texture = 136154,
usable = function () return target.is_demon and target.level < level + 2, "requires demon enemy" end,
handler = function ()
summonPet( "subjugate_demon" )
end,
},
summon_demonic_tyrant = {
id = 265187,
cast = function () return 2 * haste end,
cooldown = function () return ( essence.vision_of_perfection.enabled and 0.87 or 1 ) * 90 end,
gcd = "spell",
spend = 0.02,
spendType = "mana",
toggle = "cooldowns",
startsCombat = true,
texture = 2065628,
readyTime = function ()
if settings.dcon_imps == 0 or buff.wild_imps.stack > settings.dcon_imps then return 0 end
local missing = settings.dcon_imps - buff.wild_imps.stack
if missing <= 0 then return 0 end
if missing > 3 or missing > #guldan_v then return 3600 end
-- Still a little risky, because imps can despawn, too.
for i, time in ipairs( guldan_v ) do
if time > query_time then
missing = missing - 1
if missing <= 0 then return time - query_time end
end
end
return 3600
end,
4 years ago
handler = function ()
summonPet( "demonic_tyrant", 15 )
summon_demon( "demonic_tyrant", 15 )
applyBuff( "demonic_power", 15 )
extend_demons()
4 years ago
if level > 57 or azerite.baleful_invocation.enabled then gain( 5, "soul_shards" ) end
4 years ago
end,
auras = {
-- Conduit
-- Note: Should set up a queued event for this to start when Tyrant finishes.
tyrants_soul = {
id = 339766,
duration = 15,
max_stack = 1
}
}
},
summon_felguard = {
id = 30146,
cast = function () return ( buff.fel_domination.up and 0.5 or 6 ) * haste end,
cooldown = 0,
gcd = "spell",
spend = function () return buff.fel_domination.up and 0 or 1 end,
spendType = "soul_shards",
startsCombat = false,
essential = true,
bind = "summon_pet",
nomounted = true,
usable = function () return not pet.exists, "cannot have an existing pet" end,
handler = function ()
removeBuff( "fel_domination" )
summonPet( "felguard", 3600 )
end,
copy = { "summon_pet", 112870 }
},
summon_vilefiend = {
id = 264119,
cast = function () return 2 * haste end,
cooldown = 45,
gcd = "spell",
spend = 1,
spendType = "soul_shards",
startsCombat = true,
texture = 1616211,
handler = function ()
summon_demon( "vilefiend", 15 )
summonPet( "vilefiend", 15 )
end,
},
unending_breath = {
id = 5697,
cast = 0,
cooldown = 0,
gcd = "spell",
spend = 0.02,
spendType = "mana",
startsCombat = true,
handler = function ()
end,
},
unending_resolve = {
id = 104773,
cast = 0,
cooldown = 180,
gcd = "spell",
spend = 0.02,
spendType = "mana",
toggle = "defensives",
startsCombat = true,
handler = function ()
end,
},
--[[ wartime_ability = {
id = 264739,
cast = 0,
cooldown = 0,
gcd = "spell",
startsCombat = true,
handler = function ()
end,
}, ]]
} )
spec:RegisterOptions( {
enabled = true,
aoe = 3,
nameplates = false,
nameplateRange = 8,
cycle = true,
damage = true,
damageExpiration = 6,
potion = "spectral_intellect",
package = "Demonology",
} )
spec:RegisterSetting( "dcon_imps", 0, {
type = "range",
name = "Wild Imps Required",
desc = "If set above zero, Summon Demonic Tyrant will not be recommended unless the specified number of imps are summoned.\n\n" ..
"This can backfire horribly, letting your Felguard or Vilefiend expire when you could've extended them with Summon Demonic Tyrant.",
min = 0,
max = 10,
step = 1,
width= "full"
} )
4 years ago
3 years ago
spec:RegisterPack( "Demonology", 20220911, [[Hekili:v3ZAtUnUr(BzQurwAEilXrJ9yFJuQn7MlNDTNtQm7D7(PHIIesI1qrQWhYEUsL(TFDdWhaGaGKsA8M8HD9irWgD3Or)cnA904N(LNE0ZjL80xSgzzn6dJhpC8Oj3BD7tpM(YwYtpU1X9zNvWFe6Sb())eztuyuq0QxWh9sqKJhcIKOSyx4Xp6V5hF6Xfz(bPFk8PfkH94jW43sCHV(DV7Phx775ryJLK4YpdhM)RoXbrUpF4ZRtt3M8X3(2v(PRZwm0nAZBt83Kf4K6hf6g7Smf)S7Bp85hjBtjBwqIpmF84RpmhN6dF(WN)X1oHRijF8WNV5W8h3sccom)NbypK(f)TWay6IjaG3qc9omNIflIcspmFBm5g47x4a)T)s4)W)nb(VWKuNq4d9JGjBTtSNRtc8XL4hxeLKqsg8FCyUxu4BGVnAhj21z7H5j4ityG6LOS3etomhb9DiepmpDn85OTKqsmd1(7rFfjMh93UocEUtqa8fEIOvAe(5ixoKQKaYHYU)ommNac(m)WusCC228N9d7I8ba(4F(Mz)xr)vad36S5W8VUMewHHHrW75SiklF2sY2SHIohM)lVedZidu)NbKK1S1J)3iC1jaE3hJc2rNxxNqxsGns5U0LdpgWkXh4rm48lXVWEKdd3(kWz9dxLtw(aH(JriAbeR7ZjsVZgNNl5HiFXpk2pfEweDr(RrXpZ4(FLqrjysCtZa(6lSxcEaiLvJ0(0gGdVdFLOnBbkBHFafQSb)FFfSMdI9EjdF6Xa)K0e62cyTYon2p8zc9l(cD)gj0zraX7P)CXMky3tqajfPq74SfWwl3xCdi2PoXRWxeEThbCee1vowG6iX(op94oh4FaqpCr2YL2jVe6A7cmVhWvEesd98rPdxGiElvGdiR(WwKdZV8W8vUEd348n6FxcOSeInkezN6VbMrqSDWtPWgxjYOe5Ycxeef5zdKv66xsspjS7koSZigvWgRn7aQERguLSz7lXeNa7OyVqCwBhQ2xl2oGIWJT6oExhvqm)X1BzIogeCwf4457KgfNyd68ciBQOV6pIcuxqegMJuBhM8RkyxXhaYaL3w4h6nCPpjWZoAPnWJtsI2Kmm)vomF)Eq3zwib08TIm0J468ckEIVQDItQ7Asq1Gb(upyxjYbz7LTtz7YqmFhrus3ookvLaxfkwIFRWDN2(H78xfftTlunNWeErhqXE0fWYLUqY3sZrYcXaRrmQw7yMDy(TJORXQihzHYkY5c6u3wMERi(bhd9FHHLi6amr5JVxnzprZEraD834qvPHBrK4hg2c1JJfeqC8Sx8In5BoB2cJwA10m1W4dOLz2YlJ9EsmHjSL)AAYb(WDVM8HNj7aNaSJI()ibWt7aF41KKF3RajlSrbLRxefhsSJjGRw(ePTk8dExURjG4p1Zezj)M5Ld42wzqvMkoX77oNOr2Wff(2nCl6TODc1zrrgGBuuGx0xdfhsmzJdqIuPEgMmucrgs(gXnlLGZjHJSlHhZvqBjwq2wdsC8Z69sONAWL)gWUBiYHucBB6T6L3UxdxEzPdQknIyuYaDgO(iqK(Ph5DHfdas1iZ2Iy2hA2ugZTE7nep)06wYAJPtxiEmCbmfCofDN4lJhzYKtZZSryR3bI2R1D6H5JQMe)nGJ4E4S46Kc((hhTDDUBr5QCsiPa)0O3lMmGp(UQjle8we2rSnkg2ez2BdJG89fouNVrsTKS42PRQg9Ahqus)wU(C7rCHOvS9aFg9aFqdEMeNiSL6uXe2O3bYUG)hGuXrHY4MA6YphAwHLZmyjPgMyeV5CXAvS)Mi)yI9ssWQmqSYSVwn4J347ocUy3xRoMzP1RvFpxhkNT(sEB(v)GLXeVeWMZk)a0B2KSTemSCBgGLDtPf(yuQnsI(v5Mzl3(ADAle63luHT1Lfu5oyf(Yaaq1l9Jtkry23YzCLFLLt9QXv9oqKdQ7MrDXoExnWjU6rfy0aEN24v7xk1CCeZdDIyA1kMagvNs6j)GVl4c1PIAQ4kC0sZtFDXS6BefHkZIThzPtg4rRcJ1ykFJ93YaXFHbEqQa9GhtMhDEOzefMgmTQyExD254hq3fZn7ycDTXCfxA)wyphWpwNHUOXp7fghkgPZ3asdcZwAmt4hJhzh4(J9gNv(UsJtEBC0waViPvVBH2h8VcYWWd6MpV6CARHKe0XzHQDO6DYdFxbYaKJnlnuIAtvfUxx4f6NXVhjMQZCRhoDULCiHnZT0yiRtHpFbM4DBwAY5Xp(CEQkuQMXUXwDcrkXcHiQRBhWeskhvvZi5OtahnzOIQigC5XZgcIjzi9ekO7PgvqE52qRz80e9vl(TF)iWTXKD24PfmE4ANq6oVvzbE4bP07ijozZcntCLB5KZwHjTgA5agroDh2b1Wc7d24j(Wo3NCOuEOpiemgcsJRmgIHUY5aZGGkts94cxTCcd9x7Zo7uU800MKirbKWJX3FqDNC1XxIwLZv17KoE0PuCAP)Q1P28EWuEsvczkau7J)jcvdUshTAfWEkKByZaZbJhu4Fni)blHUPcN9MzkJPifFdzRFD80keYSFBpbad5mI5ea7CFadgUXrbrqSXDMcRD2r47kB7QBY2MciR8ajlxOuhiKaBig06es8S36aY3K2fvjMjX7pBBM42jSnkTqWux(inReHpvxiu0N7Wx1yU)GCir6tPxdj79DIh18aEhRzVeiuscxLUM6hV(KwAEjQZoXbePLvtXPBPpvJnkOBkxtZyNN4n0Jr(YtAHSXCnyPp3Mu56CoeY8AuSGlfQGcbcnXWRqAhl)g2SP3YhtMXg0zUXNWsjX4IJoFmLnuKQbh4Lw67cBQXD5jYoSRXFRPuHTIN3jYQybJ(M52xauf2nd4JVtqoykThwBYVSGkaYzsXhzHOMFkfdlnBXw(lsFgI2AN2DJfwGfS8zP3EQcoT1RjNtq8vfDWR2rWVJLXrBSHW9TzjDqYuNhHIq1gKKsxr2IEhc((iaYG3TcWdds0nk0lZpvjS6ol)F3ew17G0XjS(9Hl)Vuc2697Q0)z09i70iBpFs(XLwbmLhvILSxkTaOw816HIKXBP3NfTWK)W50LxXVCRENdKsWmh0edyLcg9g1LkkeMqqLeWcyTnzRVhyRZErwCi90kJIj1pZyTJK31iEAUkNRuuuVnBWD3ycm8fbCN(Rxu0g67PR23K4cIzH9tlpm)N4DB4WChAnQgGF5lSedVJCnwtOW)rqcLwrOoOlGSQBvhxe3UEBX2dT5yO8eR0e0lL20BVtEchxTqiCsVfH(2QtQBMuzsAuKsVrhd4MQugzv5tTUZXz0qwLsI1ESUthuIrYnuJq)Htf6m1YGRTZ(tfkOVr84FygsUu5PniIMNJifVPDlF6ToDbxG0yjDS(L4Zty03QRQGymlygEMiTp9x9txtlh7hl9pbl1AWbLRz)l8)PVh7utWAOiCfMEV01yQ6kRp8RXvewvTV2jHw2ZR9xTMV0PLybn4xeLE0BjPXqL0UBGBZsHmhvlA(iqlC8XExLKkkgP3oulqhJ5oZWiAfQnr2sMqmXSIbtyP)XTv3rHIAHpj)IhKVYNFjdYahtckQJ9kvDmrcxWDcct5nv1ETGbf4YWqCtP0RKDXjn6(eehpsMYwABIm)jADgvH908GCn150wr(Vo0dTubO0tdHs3LzRVb56zL1JAZbKOIPIOQ(4WpbHFds0gSotsHPjmlzyQpj26EBRTUg5l9PE4x3UHAJgIhr9mtM3UPeaI2aKGXnCfgzolvz9rOF9dTZRBHrVRcDwgYrraT6MwQjoL2yQWfCC6ml1zdCt0La0g9f9LOS8RPdelkWMP8uuN1DYk7wGoMtQ59QOspQMURr7DVb(u0Zo5xAOCLkG)HBqpB)AZUWAuJxzC557zlxibgNqg7eoH(g8aLTB4UJD3WaEN3mkZtDXEISfth(4luhesdUG9K6YHVR2)(FqZwC3qp(Rs3QiQndCjLzEt5ng7AAnMewCH(EJh7kMrOU)KqLQO82HTDhPQNXvKFuop)nSsJ2ck7ySWMtUybf2CcFCl9cl
4 years ago
end