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.

2019 lines
69 KiB

-- 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
local GetPlayerAuraBySpellID = _G.GetPlayerAuraBySpellID
local ceil = math.ceil
local RC = LibStub( "LibRangeCheck-2.0" )
-- 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
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( {
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 = {}
local malicious_imps = {}
local malicious_imps_v = {}
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
local first_combat_tyrant
-- 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
spec:RegisterStateExpr( "first_tyrant_time", function()
if first_combat_tyrant and combat > 0 then
return first_combat_tyrant - combat
end
-- Tyrant is on CD, we're not starting fresh, skip opener.
if cooldown.summon_demonic_tyrant.true_remains > gcd.max then
return 0
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 ) )
end )
spec:RegisterStateExpr( "in_opener", function()
return time < first_tyrant_time
end )
local dreadstalkers_travel_time = 1
spec:RegisterCombatLogEvent( function( _, subtype, _, source, _, _, _, destGUID, _, _, _, spellID, spellName )
if source == state.GUID then
local now = GetTime()
if subtype == "SPELL_SUMMON" then
-- Wild Imp: 104317 (40) and 279910 (20).
if spellID == 104317 or spellID == 279910 then
local dur = ( spellID == 279910 and 20 or 40 )
table.insert( wild_imps, now + dur )
imps[ destGUID ] = {
t = now,
casts = 0,
expires = math.ceil( now + dur ),
max = math.ceil( now + dur )
}
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
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 )
-- 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
hog_time = now
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
--[[ elseif spellID == 265187 and InCombatLockdown() and not first_combat_tyrant then
first_combat_tyrant = now ]]
-- 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
end
end
elseif imps[ source ] and subtype == "SPELL_CAST_SUCCESS" then
local demonic_power = GetPlayerAuraBySpellID( 265273 )
local now = GetTime()
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 )
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 )
local ExpireDreadstalkers = setfenv( function()
addStack( "demonic_core", nil, set_bonus.tier28_2pc > 0 and 3 or 2 )
end, state )
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 )
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
table.sort( wild_imps_v )
table.sort( malicious_imps_v )
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
-- 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
if demon then
insert( demon, summoned + duration + extraTime )
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
state:QueueAuraExpiration( "dreadstalkers", ExpireDreadstalkers, 1 + buff.dreadstalkers.expires + dreadstalkers_travel_time )
end
class.abilities.summon_pet = class.abilities.summon_felguard
first_tyrant_time = nil
if Hekili.ActiveDebug then
Hekili:Debug( " - Dreadstalkers: %d, %.2f\n" ..
" - Vilefiend : %d, %.2f\n" ..
" - Grim Felguard: %d, %.2f\n" ..
" - Wild Imps : %d, %.2f\n" ..
" - Malicious Imp: %d, %.2f\n" ..
"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,
buff.malicious_imps.stack, buff.malicious_imps.remains,
major_demon_remains )
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
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 )
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
elseif name == "malicious_imps" then db = malicious_imps_v
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
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
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
elseif name == "malicious_imps" then db = malicious_imps_v
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 ()
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
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
end
return query_imp_spawn.remains
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,
count = function ()
local c = 0
for i, exp in ipairs( dreadstalkers_v ) do
if exp >= query_time then c = c + ( set_bonus.tier28_2pc > 0 and 3 or 2 ) end
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,
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,
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 = {
duration = 40,
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,
count = function ()
local c = 0
for i, exp in ipairs( wild_imps_v ) do
if exp > query_time then c = c + 1 end
end
return c
end,
}
},
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,
count = function ()
local c = 0
for i, exp in ipairs( malicious_imps_v ) do
if exp > query_time then c = c + 1 end
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,
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,
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",
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
return 1863
end,
"summon_sayaad",
3600,
"incubus", "succubus" )
-- 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 )
--[[ Demonic Tyrant
spec:RegisterPet( "demonic_tyrant",
135002,
"summon_demonic_tyrant",
15 ) ]]
spec:RegisterTotem( "demonic_tyrant", 135002 )
spec:RegisterTotem( "vilefiend", 1616211 )
spec:RegisterTotem( "grimoire_felguard", 136216 )
spec:RegisterTotem( "dreadstalker", 1378282 )
spec:RegisterStateExpr( "extra_shards", function () return 0 end )
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 ()
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.
-- 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 ()
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 )
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" )
removeStack( "power_siphon" )
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,
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
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
end
consume_demons( "wild_imps", "all" )
if buff.malicious_imps.up then
consume_demons( "malicious_imps", "all" )
end
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 )
addStack( "power_siphon", 20, num )
end,
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
t.count = 0
t.expires = 0
t.applied = 0
t.caster = "nobody"
end
}
}
},
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,
handler = function ()
summonPet( "demonic_tyrant", 15 )
summon_demon( "demonic_tyrant", 15 )
applyBuff( "demonic_power", 15 )
extend_demons()
if level > 57 or azerite.baleful_invocation.enabled then gain( 5, "soul_shards" ) end
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"
} )
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)nSsJ2ck7ySWMtUybf2CcFCl9clUWrzP1uaaiaFBViqLUtbtvzj3uS5P4kfUGScW(HhM)dUUKTPj4MIWBaEYMLi3Hb9e2LQtGlvU6xEE3OkyNa8a8tLQnN8ii2sKOTAfTz(5HeTHGYSW7GUZMLGj4b(RmgYub3MgyA9IeS4M4fqCwNyNqCJbdvPe31H()ZmcVYsnJinVcGk57CvAUaTPp8IJ2ZxqNKLERXYkXZjvQGf4dru8cKxLgfVb0ujl5vFaP1ljevQkt4sAc6EdnzpO2UAwtzxBwwLaQZdgZvnF9z8IJCsLs9Yru2fvjisTFtLA(z8LUKpPRAHlGx1WP4ZKMgWxXd1QgcQYL8ZZ2yf2FrRlVkjVn4VwFfpqSm8vOpOyG1ZDPKxlkuXuLtsXX2Uu1ukwzYsnVdXsIr63YB29BL4G5ukwfX5eTOZ75zqkoNqz7mMUbZnVHOn1bWRFnUuxIhgVnFz6O9sn76amzaMlJDGvTqhMZYljs7eyiGUbBpHkGKR7vlivy7Rt4rlqcZm8wHI6UiNATPOqGNY)5VA14bWB20KI9RfZ8MiWre)u7TX(jBA)gSgzdIWvXwzELs2e3a)TjK2rVVV8wCZ)QkSxZ5BLuWeGVmzB2wZSIPCxynQLrXvD9ZvyWx0jYLljUOhIPRPnJIyNDe0b6GOVI9ub8CTYRIBMF)yqy)ZmF6Hk455JaXjaZRdzRdkSHjIm0ddtQLOQyBwqH3cfqbBchlHbJAlDc934aXJTs9YvDve5RwAGHkNf4w4yazykwZKj2faxC1sXuw6ql7BOHlz462vonwN00qtVL26EmKKfJAdEj0beyTjHRXEwqCle6bFpMChtDEZ8zDttQXcQ0u6)5zc1qSwVVyBg45HNZgyrVAdsQYVxBFBGAUDj6cZosscjGZPx5NOWWedgqi3r0SSa)bjEh4qufuu8mTbv5e7Ug4EWgyBqKMeM5N(c1vIkWPFiAdKYnkzdOHIRluWWj4p54ZggKcD9hdVtwDDryEGFv(RcrPgjptl(wf6Hlv1h8Y21uNmssaBjb5bwxjeYmHuLjowcaZv2R8LvOInNADIFgTbzdEx)S92SuGd5sOTRKsYw7quOjKbvYcWYbsT78t4GK4xRsJwU7tHURDOLzj3shy029zWP0kWzEyM1KHhZKADMAukQpz88GYYmOSQ0UuseWybiTPUxX0szUylEXWXkFJe)m12LqGgcH6WAtnlZIFrAuctnkkthP0GM0k8dWqWsfTEry9FR7TaHWV6qJvag9V(d)JV8PV8x)4H5hM)lOXz)nyOx5NJYBeSP(Mst2EyqjOYuNS0Ono0uhX6Ltyxy6NHvBqw)JyY2cHzN(ySNGbV1lHPyUV(JSCZ9ZzohM75dcA(yzfbbBg7qVHx9Fl4WF)XFBqj4SoVG72Zf4o85gyHY5b)K4IVPkq7F7nmSK)Rk8)7nsuRmuWT30cLUeivFJoyyDwWezOCmyYTNfmrgkTftS4GXeag)dWn8DieG4j(4VHRGem07(SELwXTuK1w3ifnRozXXjhjYWdJ7oxiZDNbK5DhjmULdgVxcg5X9ucHIpRdhU)iXbEy8HZamgp64acgg1F5V9ZTxvd)nfOBQzo1TunIA53(3UHvVgcIDLYonXzbXGJfi87jglVPOTaratKLQpoMI15X4KLSYVJemNNv6BpvIQXTcLNxx32mutlsxrSZeyAM(QfcChju5vGc)2hZP6VkQGFRgfk7JrXGTu8(w8VFJK2r6jSmlVReKogYPtqSJ3PJKZXPbs2AMAG1APX6gh)eLYqOmEuXr9DyoTlScphIvklDnM3b8eZJw6JNnXF4pKxzaYDAy8bTVBdJJwxhhgFwvxhg)uTopC(x(VQDF4C07m0bIlGKUUqC(ZpxDI4CWDw6gX5WQZDKy537vOReNpfTUZetLkHnpNNI(4WNZ)8Ws7Axn9TILBY1(lNIjFD2OExuwgiQFXQAbr9Zls0X1yoOM2qfE0gqORso0qxLYsQFo)PTJuTQA5y24EAQHJhMA1kuw9bJP(vlR1beDOP1JT5)H761hvqSFp7TKkkIPJgOzbQ6qKRdXlAeKOWQkWwKsEgfwlT9xhTDkewZ10dtE6yRtcqoEE5akhpR3WK4leoQ0llutfdKzq68IpgBNBCOJ2(V15bBYRuT2q)73)AIsjzluYGKoZwEU0vfJuxDqEk4dOxRqsCuBbuzNvrsuw9UmX3v(qRfbrdLAuV(nxCrx1qbfDLHIjA24b1SO0Yo3vjTpTQZDv(DaxqSPDX)KIM0f)3X3uUQGSiNu8OWf5JDSDbGBlUO0t3AnOJ977i8M1C)JINApg6shCrsPVjsPHY7AqVosRp0mojSVQT7MgHuI(Qisl7J)CGfHOMIOGU4RTOs6DrvFu6iMZXwna(sOZCNs11N8iM2rTEw1mP9u6Td4ZxQMUb1VpyP2oxvVZjE26wtfsp6lpjzTHSpsRhHCxGZtjrZJepPs6oDdCpT7DgSFFF9VMXQURx)lmy(y)E6ll1nPgmGNEkVZ7uxQbLs9eAkupyDjqATGbW0yWUPMtht9RrQFq1dnz9q9QgPrylFkFcZsPQ1697P97nP3T8z16RtO5f5E5epwYA6qnTCRr72d5E(r5gZQXn4WlXgTKw4D79NG8ZGM5(8h8HOlbmr5I2jeJFuznRnTvhDe1SpG0K2nmQ2kviV(UlZzW1jnHsW1OQHUAZDMLvZZMbzL(64dwJU5Ul7itvGWv02GkJDoNyq6u7cXDA1vWwkkkbNzJVQ)4lBWv3bQTIn9D9Ap(arwJJvxFyjxLN0uCz)Xxn5Y(A65kZSgmy2uTGD3y9kmLycwNdcKB5wb6uPXxxlpz)E9T7KwtjN0Y5v9VT8DrFqLAsmd6c34FlxUpNmGVBId1sarPhbOHP8UhZd3oI)DKtjHYxX6EfVIOgvLV3ybLpI(tkMFSPcJuBA0E4UEnhVoBTPPgkZdcZiEFnXPI)EAIrTFo60lTLhmB6T96RXV897178z7HFEUvLBXl9AXLRDMvHz5opDY7USQh8rXKmA4Dxv2ttkZsL5EOIwO9WXaTR6pA2FQ)T3urhdgCzXRQQZRCKUjEJcU5r488fY9AfQu73X(EIW22OYljpf3AW6JOh5IhnGbxT64PgCRPP5iD620tlN1)a9KmBvx4OJnHKAoXxOOSopGE7R7XPaDIwdvwJO48P2rrEvqUhgBDAWTVgXJzwvjNxNtsVccqAjgvT(djkP)74voPs3eFQ8NPt74n5VOHMCqXqQoPk(Noq3wUzC5T)OwSCK8KvJoMUR6S0K(VtnmJ2B68w9BtbpQtn2Wm4sUUb7597FxxKJgCvZYldQ5rLqRSGzXIZdlnMgPRsFh7FfDwyv13Z84y8v5M1VuZEdnE5INru(dgkCRcMkFE8I)YgRIbxk4GOqr6xFq6xK3)4D30V0JUl1QfBGg06QAN6U8VRWDatUcWKthdQ9leSEmOFnuyWvJTAcnQqez1ktloNgCkpTJkQ5JNP2SxOumF(nKov(irvo)AtJ1dwJ2VxNgPB5RgIgWUlmYEmG7dAg5VqF6g1H6JV3iMlD6eQ2THxLGCXP1G0x66HBDtBWd2sYu63J3EgsykNbsWLLJGuN0WQ0RnTk97eRbA93lYZu6DRKArbqXFeD3VV6XY)GM27cDmGbdmiWEQKIbkr1XnX5XGQF5BFO0pq9)Q3cutJ)A3QCnVyoUVZ1eWdnP4PQE0eumwBnIARw5pATxZxOJtvpooV5vQ9t43cwbeP25wDSQ)RnfxOFomcjFv)MYIaSnkMMoI3izzVrqYfMA)O7Z)s4PWoT6(Hk(aa)QUuO1Eu5nbT2tkV(NcZe94oN2slNuEAd2FKHnmZcjAtXZRLWwfJrobTQgIH0LAUB80mWmRzVCAmNYvworMOy2QJv8ls1VSesctYxNDtVAnpgR3ba60RR9g)3jOy4(93j4CsCc6sz(T6VtVOYBQpk2WuouecVqH81mu1EB97eueUP(D6nnEP8Bas5XNWCOO6QSi0QrAfeSeGGvxGGsfELruwSJMR6jZ46Ti5vOZ376GzqDmTUyQU(TeLvXaV5260johTykAJjC9tU6AxxFKSCvTGEOk0FMmRt)uqE(w0KEm(9kHK603v6cbco5CEfvRVoPn5X5z2(1Rhm1bAsijgTGRQUvkvFvKFlSsWYRysTEPEhheT4HO1jbrrsxt3nsRS1Sj3XLKI2nJQxKQRvLsJkZwzp14ILGZW5JNEwktf8kuGAKp0J7udbvEoAeoV)M(m4CgUBad61VfPx(iNttxTJgrS8Zix15kRHZy(mHvNAB5vcvfqH20On(UoWvAlNUlWSvC6xxoj6KqhliU2L1hPfMgQyWAhW4XWf1jk2K0IiSnOCrx9GOVekAnrmOkxq6Uxsm5a5FC)hWshwrq)f15zhqYhAns2iNoF2LXXEIF5RY8s9gS(L(QO8aQ)KZpwmG2eRE6)p]] )
end