diff --git a/scripts/subtlety.lua b/scripts/subtlety.lua index 538cd3e..502c949 100644 --- a/scripts/subtlety.lua +++ b/scripts/subtlety.lua @@ -37,6 +37,7 @@ local CrimsonVial = Bastion.SpellBook:GetSpell(185311) local Shiv = Bastion.SpellBook:GetSpell(5938) local KidneyShot = Bastion.SpellBook:GetSpell(408) local InstantPoison = Bastion.SpellBook:GetSpell(315584) +local Sanguine = Bastion.SpellBook:GetSpell(326509) local AtrophicPosion = Bastion.SpellBook:GetSpell(381637) local Evasion = Bastion.SpellBook:GetSpell(5277) local TricksOfTheTrade = Bastion.SpellBook:GetSpell(57934) @@ -58,10 +59,23 @@ local BlackPowder = Bastion.SpellBook:GetSpell(319175) local SecretTechnique = Bastion.SpellBook:GetSpell(280719) local DarkBrew = Bastion.SpellBook:GetSpell(310454) local Premeditation = Bastion.SpellBook:GetSpell(343173) +local DanseMacabre = Bastion.SpellBook:GetSpell(393969) local IrideusFragment = Bastion.ItemBook:GetItem(193743) local Healthstone = Bastion.ItemBook:GetItem(5512) local WindscarWhetstone = Bastion.ItemBook:GetItem(137486) +local DarkMoonRime = Bastion.ItemBook:GetItem(198477) + +local RimeCards = { + One = Bastion.SpellBook:GetSpell(382844), + Two = Bastion.SpellBook:GetSpell(382845), + Three = Bastion.SpellBook:GetSpell(382846), + Four = Bastion.SpellBook:GetSpell(382847), + Five = Bastion.SpellBook:GetSpell(382848), + Six = Bastion.SpellBook:GetSpell(382849), + Seven = Bastion.SpellBook:GetSpell(382850), + Eight = Bastion.SpellBook:GetSpell(382851), +} local PurgeTarget = Bastion.UnitManager:CreateCustomUnit('purge', function(unit) local purge = nil @@ -93,7 +107,7 @@ local PurgeTarget = Bastion.UnitManager:CreateCustomUnit('purge', function(unit) end) local KickTarget = Bastion.UnitManager:CreateCustomUnit('kick', function(unit) - local purge = nil + local kick = nil Bastion.UnitManager:EnumEnemies(function(unit) if unit:IsDead() then @@ -108,17 +122,17 @@ local KickTarget = Bastion.UnitManager:CreateCustomUnit('kick', function(unit) return false end - if Player:InMelee(unit) and unit:IsInterruptible(5) and Player:IsFacing(unit) then - purge = unit + if Player:InMelee(unit) and Player:IsFacing(unit) and Bastion.MythicPlusUtils:CastingCriticalKick(unit, 5) then + kick = unit return true end end) - if purge == nil then - purge = None + if kick == nil then + kick = None end - return purge + return kick end) local Tank = Bastion.UnitManager:CreateCustomUnit('tank', function(unit) @@ -176,6 +190,7 @@ local RuptureTarget = Bastion.UnitManager:CreateCustomUnit('rupture', function() not unit:GetAuras():FindMy(Rupture):IsUp() or unit:GetAuras():FindMy(Rupture):GetRemainingTime() < 6 ) + and unit:TimeToDie() > 12 then target = unit return true @@ -212,6 +227,7 @@ SpecialAPL:AddSpell( ( Player:GetComboPoints(Target) >= 4 and (Player:GetAuras():FindMy(Broadside):IsUp() or Player:GetAuras():FindMy(Opportunity):IsUp()))) + and not Target:GetAuras():Find(Sanguine):IsUp() end):SetTarget(KickTarget) ) @@ -310,12 +326,29 @@ SpecialAPL:AddItem( end):SetTarget(Player) ) +SpecialAPL:AddItem( + DarkMoonRime:UsableIf(function(self) + return Target:Exists() and Player:InMelee(Target) and self:IsEquippedAndUsable() and + not Player:IsCastingOrChanneling() and (Player:GetMeleeAttackers() > 2 or Target:IsBoss()) and + (Player:GetAuras():FindMy(RimeCards.One):IsUp() or + Player:GetAuras():FindMy(RimeCards.Two):IsUp() or + Player:GetAuras():FindMy(RimeCards.Three):IsUp() or + Player:GetAuras():FindMy(RimeCards.Four):IsUp() or + Player:GetAuras():FindMy(RimeCards.Five):IsUp() or + Player:GetAuras():FindMy(RimeCards.Six):IsUp() or + Player:GetAuras():FindMy(RimeCards.Seven):IsUp() or + Player:GetAuras():FindMy(RimeCards.Eight):IsUp() + ) + end):SetTarget(Target) +) + -- Use Shadowstrike during Shadow Dance. SpecialAPL:AddSpell( Shadowstrike:CastableIf(function(self) return Target:Exists() and Player:InMelee(Target) and self:IsKnownAndUsable() and - not Player:IsCastingOrChanneling() and Player:GetAuras():FindMy(Premeditation):IsUp() + not Player:IsCastingOrChanneling() and Player:GetAuras():FindMy(Premeditation):IsUp() and + Player:GetEnemies(10) <= 3 end):SetTarget(Target) ) @@ -333,7 +366,11 @@ DefaultAPL:AddSpell( return Target:Exists() and Player:InMelee(Target) and self:IsKnownAndUsable() and not Player:IsCastingOrChanneling() - end):SetTarget(Player) + end):SetTarget(Player):OnCast(function() + SpellCancelQueuedSpell() + ShurikenTornado:Cast(Target) + SpellCancelQueuedSpell() + end) ) -- Use Shadow Blades on cooldown. @@ -375,12 +412,17 @@ DefaultAPL:AddSpell( DefaultAPL:AddSpell( ShadowDance:CastableIf(function(self) return Target:Exists() and Player:InMelee(Target) and - self:IsKnownAndUsable() and - not Player:IsCastingOrChanneling() - end):SetTarget(Player) + self:IsKnownAndUsable() and Gloomblade:IsKnownAndUsable() and + not Player:IsCastingOrChanneling() and Player:GetComboPoints(Target) <= 2 + end):SetTarget(Player):OnCast(function() + SpellCancelQueuedSpell() + Gloomblade:Cast(Target) -- We want to cast gloomblade immediately with shadow dance to trigger 1 stack of danse macabre + SpellCancelQueuedSpell() + end) ) -- Use Thistle Tea when low on energy. +-- actions.cds+=/thistle_tea,if=cooldown.symbols_of_death.remains>=3&!buff.thistle_tea.up&(energy.deficit>=100|cooldown.thistle_tea.charges_fractional>=2.75&buff.shadow_dance.up)|buff.shadow_dance.remains>=4&!buff.thistle_tea.up&spell_targets.shuriken_storm>=3|!buff.thistle_tea.up&fight_remains<=(6*cooldown.thistle_tea.charges) DefaultAPL:AddSpell( ThistleTea:CastableIf(function(self) return Target:Exists() and Player:InMelee(Target) and @@ -389,6 +431,7 @@ DefaultAPL:AddSpell( Player:GetPowerDeficit() >= 100 and ThistleTea:GetTimeSinceLastCast() >= 3 end):SetTarget(Player) + ) -- Use Finishing moves with 6 or more combo points (5 or more during Shadow Dance) with the following priority: @@ -419,7 +462,7 @@ DefaultAPL:AddSpell( Player:GetAuras():FindMy(ShadowDanceAura):IsUp())) and ( not Target:GetAuras():FindMy(Rupture):IsUp() or Target:GetAuras():FindMy(Rupture):GetRemainingTime() < 6 - ) + ) and not Player:GetAuras():FindMy(ShadowDanceAura):IsUp() end):SetTarget(Target) ) @@ -431,7 +474,7 @@ DefaultAPL:AddSpell( not Player:IsCastingOrChanneling() and (Player:GetComboPoints(Target) >= 6 or (Player:GetComboPoints(Target) >= 5 and - Player:GetAuras():FindMy(ShadowDanceAura):IsUp())) + Player:GetAuras():FindMy(ShadowDanceAura):IsUp())) and Target:GetAuras():FindMy(Rupture):IsUp() end):SetTarget(Target) ) @@ -486,7 +529,11 @@ AOEAPL:AddSpell( return Target:Exists() and Player:InMelee(Target) and self:IsKnownAndUsable() and not Player:IsCastingOrChanneling() - end):SetTarget(Player) + end):SetTarget(Player):OnCast(function() + SpellCancelQueuedSpell() + ShurikenTornado:Cast(Target) + SpellCancelQueuedSpell() + end) ) -- Use Shadow Blades on cooldown. @@ -528,9 +575,13 @@ AOEAPL:AddSpell( AOEAPL:AddSpell( ShadowDance:CastableIf(function(self) return Target:Exists() and Player:InMelee(Target) and - self:IsKnownAndUsable() and - not Player:IsCastingOrChanneling() - end):SetTarget(Player) + self:IsKnownAndUsable() and Gloomblade:IsKnownAndUsable() and + not Player:IsCastingOrChanneling() and Player:GetComboPoints(Target) <= 2 + end):SetTarget(Player):OnCast(function() + SpellCancelQueuedSpell() + Gloomblade:Cast(Target) -- We want to cast gloomblade immediately with shadow dance to trigger 1 stack of danse macabre + SpellCancelQueuedSpell() + end) ) -- Use Thistle Tea with Shadow Dance. @@ -539,7 +590,7 @@ AOEAPL:AddSpell( return Target:Exists() and Player:InMelee(Target) and self:IsKnownAndUsable() and not Player:IsCastingOrChanneling() and - Player:GetAuras():FindMy(ShadowDanceAura):IsUp() and Player:GetPowerDeficit() >= 70 and + Player:GetPowerDeficit() >= 100 and ThistleTea:GetTimeSinceLastCast() >= 3 end):SetTarget(Player) ) @@ -556,6 +607,7 @@ AOEAPL:AddSpell( not Player:GetAuras():FindMy(SliceAndDice):IsUp() or Player:GetAuras():FindMy(SliceAndDice):GetRemainingTime() < 6 ) + and Player:GetEnemies(10) < 6 end):SetTarget(Target) ) @@ -569,6 +621,9 @@ AOEAPL:AddSpell( not Target:GetAuras():FindMy(Rupture):IsUp() or Target:GetAuras():FindMy(Rupture):GetRemainingTime() < 6 ) + and not Player:GetAuras():FindMy(ShadowDanceAura):IsUp() + and not Player:GetAuras():FindMy(SymbolsOfDeath):IsUp() + and not Player:GetAuras():FindMy(ThistleTea):IsUp() end):SetTarget(Target) ) @@ -582,6 +637,7 @@ AOEAPL:AddSpell( not RuptureTarget:GetAuras():FindMy(Rupture):IsUp() or RuptureTarget:GetAuras():FindMy(Rupture):GetRemainingTime() < 6 ) + and not Player:GetAuras():FindMy(ShadowDanceAura):IsUp() end):SetTarget(RuptureTarget) ) @@ -590,7 +646,9 @@ AOEAPL:AddSpell( return Target:Exists() and Player:InMelee(Target) and self:IsKnownAndUsable() and not Player:IsCastingOrChanneling() and - (Player:GetComboPoints(Target) >= 5) + (Player:GetComboPoints(Target) >= 6 or + (Player:GetComboPoints(Target) >= 5 and + Player:GetAuras():FindMy(ShadowDanceAura):IsUp())) and Target:GetAuras():FindMy(Rupture):IsUp() end):SetTarget(Target) ) @@ -601,8 +659,8 @@ AOEAPL:AddSpell( self:IsKnownAndUsable() and not Player:IsCastingOrChanneling() and (Player:GetComboPoints(Target) >= 5) and - (Player:GetMeleeAttackers() >= 3 or - (Player:GetMeleeAttackers() >= 2 and + (Player:GetEnemies(10) >= 3 or + (Player:GetEnemies(10) >= 2 and DarkBrew:IsKnown())) end):SetTarget(Target) ) @@ -659,10 +717,9 @@ AOEAPL:AddSpell( end):SetTarget(Player) ) - SubModulue:Sync(function() SpecialAPL:Execute() - if Player:GetMeleeAttackers() > 1 then + if Player:GetEnemies(10) >= 2 then AOEAPL:Execute() else DefaultAPL:Execute() diff --git a/src/List/List.lua b/src/List/List.lua index 7b583a6..bc03ebe 100644 --- a/src/List/List.lua +++ b/src/List/List.lua @@ -3,9 +3,9 @@ local Tinkr, Bastion = ... local List = {} List.__index = List -function List:New() +function List:New(from) local self = setmetatable({}, List) - self._list = {} + self._list = from or {} return self end diff --git a/src/MythicPlusUtils/MythicPlusUtils.lua b/src/MythicPlusUtils/MythicPlusUtils.lua index f0a5fa9..4dac63f 100644 --- a/src/MythicPlusUtils/MythicPlusUtils.lua +++ b/src/MythicPlusUtils/MythicPlusUtils.lua @@ -11,6 +11,119 @@ function MythicPlusUtils:New() local self = setmetatable({}, MythicPlusUtils) self.random = math.random(1000000, 9999999) + self.kickList = { + + -- Algeth'ar Academy + [388392] = true, -- Monotonous Lecture + [396812] = true, -- Mystic Blast + [377389] = true, -- Call of the Flock + [396640] = true, -- Healing Touch + [387843] = true, -- Astral Bomb + [387955] = true, -- Celestial Shield + [387910] = true, -- Astral Whirlwind + + -- Azure Vault + -- [375602] = true, -- Erratic Growth + [387564] = true, -- Mystic Vapors + -- [386546] = true, -- Waking Bane + [389804] = true, -- Heavy Tome + [377488] = true, -- Icy Bindings + + -- Brackenhide + [382249] = true, -- Earth Bolt + [367500] = true, -- Hideous Cackle + [377950] = true, -- Greater Healing Rapids + [385029] = true, -- Screech + [373804] = true, -- Touch of Decay + [381770] = true, -- Gushing Ooze + [374544] = true, -- Burst of Decay + + -- Halls of Infusion + [374066] = true, -- Earth Shield + [374339] = true, -- Demoralizing Shout + [374045] = true, -- Expulse + [374080] = true, -- Blasting Gust + [389443] = true, -- Purifying Blast + [395694] = true, -- Elemental Focus + [374563] = true, -- Dazzle + [385141] = true, -- Thunderstorm + [374706] = true, -- Pyretic Burst + [375384] = true, -- Rumbling Earth + [375950] = true, -- Ice Shards + [377348] = true, -- Tidal Divergence + [377402] = true, -- Aqueous Barrier + [387618] = true, -- Infuse + + -- Neltharus + [378282] = true, -- Molten Core + [372615] = true, -- Ember Reach + [395427] = true, -- Burning Roar + [372538] = true, -- Melt + [384161] = true, -- Mote of Combustion + [382795] = true, -- Molten Barrier + + -- Nokhud + [384365] = true, -- Disruptive Shout + [386024] = true, -- Tempest + [387411] = true, -- Death Bolt Volley + [387606] = true, -- Dominate + [376725] = true, -- Storm Bolt + [384808] = true, -- Guardian Wind + [383823] = true, -- Rally the Clan (CC to interrupt) + [387135] = true, -- Arcing Strike (CC to interrupt) + [373395] = true, -- Bloodcurdling Shout + + -- Ruby Life Pools + [373017] = true, -- Roaring Blaze + [392398] = true, -- Crackling Detonation + [392451] = true, -- Flashfire + [385310] = true, -- Lightning Bolt + [375602] = true, -- Erratic Growth + -- [386546] = true, -- Waking Bane + -- [387564] = true, -- Mystic Vapors + [373932] = true, -- Illusionary Bolt + [386546] = true, -- Waking Bane + + -- Uldaman + [369675] = true, -- Chain Lightning + [369674] = true, -- Stone Spike + [369823] = true, -- Spiked Carapace + [369603] = true, -- Defensive Bulwark + [369399] = true, -- Stone Bolt + [369400] = true, -- Earthen Ward + + -- Court of Stars + [211401] = true, -- Drifting Embers + [211464] = true, -- Fel Detonation + [207980] = true, -- Disintegration Beam + [208165] = true, -- Withering Soul + [207881] = true, -- Infernal Eruption + + -- Halls of Valor + [198595] = true, -- Thunderous Bolt + [198959] = true, -- Etch + [192288] = true, -- Searing Light + [199726] = true, -- Unruly Yell + [198750] = true, -- Surge + + -- Shadowmoon Burial Grounds + [152818] = true, -- Shadow Mend + [153153] = true, -- Dark Communion (CC to interrupt) + [156776] = true, -- Rending Voidlash + [156722] = true, -- Void Bolt + [398206] = true, -- Death Blast + [156718] = true, -- Necrotic Burst + [153524] = true, -- Plague Spit + + -- Temple of the Jade Serpent + [397888] = true, -- Hydrolance + [114646] = true, -- Haunting Gaze + [395859] = true, -- Haunting Scream + [396073] = true, -- Cat Nap + [397914] = true, -- Defiling Mist + + [315584] = true + } Bastion.EventManager:RegisterWoWEvent('UNIT_AURA', function(unit, auras) if not self.debuffLogging then @@ -41,8 +154,17 @@ function MythicPlusUtils:ToggleDebuffLogging() self.debuffLogging = not self.debuffLogging end -function MythicPlusUtils:HasCriticalDispel(unit) +function MythicPlusUtils:CastingCriticalKick(unit, percent) + local castingSpell = unit:GetCastingOrChannelingSpell() + + if castingSpell then + local spellID = castingSpell:GetID() + if self.kickList[spellID] and unit:IsInterruptibleAt(percent) then + return true + end + end + return false end return MythicPlusUtils diff --git a/src/Spell/Spell.lua b/src/Spell/Spell.lua index b97a781..4cf98a8 100644 --- a/src/Spell/Spell.lua +++ b/src/Spell/Spell.lua @@ -252,6 +252,25 @@ function Spell:GetCharges() return GetSpellCharges(self:GetID()) end +function Spell:GetChargesFractional() + local charges, maxCharges, start, duration = GetSpellCharges(self:GetID()) + + if charges == maxCharges then + return maxCharges + end + + if charges == 0 then + return 0 + end + + local timeSinceStart = GetTime() - start + local timeLeft = duration - timeSinceStart + local timePerCharge = duration / maxCharges + local chargesFractional = charges + (timeLeft / timePerCharge) + + return chargesFractional +end + -- Get the spells charges remaining function Spell:GetChargesRemaining() local charges, maxCharges, start, duration = GetSpellCharges(self:GetID()) diff --git a/src/Unit/Unit.lua b/src/Unit/Unit.lua index ffae779..bd9e4d0 100644 --- a/src/Unit/Unit.lua +++ b/src/Unit/Unit.lua @@ -4,7 +4,7 @@ local Tinkr, Bastion = ... local Unit = { cache = nil, aura_table = nil, - unit = nil + unit = nil, } function Unit:__index(k) @@ -28,10 +28,11 @@ end -- Constructor function Unit:New(unit) - local self = setmetatable({}, Unit) - self.unit = unit - self.cache = Bastion.Cache:New() - self.aura_table = Bastion.AuraTable:New(self) + local self = setmetatable({}, Unit) + self.unit = unit + self.cache = Bastion.Cache:New() + self.aura_table = Bastion.AuraTable:New(self) + self.regression_history = {} return self end @@ -281,6 +282,22 @@ function Unit:IsCasting() return UnitCastingInfo(self.unit) ~= nil end +-- Get Casting or channeling spell +function Unit:GetCastingOrChannelingSpell() + local name, text, texture, startTimeMS, endTimeMS, isTradeSkill, castID, notInterruptible, spellId = UnitCastingInfo(self + .unit) + + if not name then + name, text, texture, startTimeMS, endTimeMS, isTradeSkill, notInterruptible, spellId = UnitChannelInfo(self.unit) + end + + if name then + return Bastion.SpellBook:GetSpell(spellId) + end + + return nil +end + -- Check if the unit is channeling a spell function Unit:IsChanneling() return UnitChannelInfo(self.unit) ~= nil @@ -296,10 +313,25 @@ function Unit:CanAttack(unit) return UnitCanAttack(self.unit, unit.unit) end --- Check if unit is interruptible -function Unit:IsInterruptible(percent) - local percent = percent or math.random(2, 5) +function Unit:GetChannelOrCastPercentComplete() + local name, text, texture, startTimeMS, endTimeMS, isTradeSkill, castID, notInterruptible, spellId = UnitCastingInfo(self + .unit) + + if not name then + name, text, texture, startTimeMS, endTimeMS, isTradeSkill, notInterruptible, spellId = UnitChannelInfo(self.unit) + end + if name and startTimeMS and endTimeMS then + local start = startTimeMS / 1000 + local finish = endTimeMS / 1000 + local current = GetTime() + print(((current - start) / (finish - start)) * 100) + return ((current - start) / (finish - start)) * 100 + end + return 0 +end + +function Unit:IsInterruptible() local name, text, texture, startTimeMS, endTimeMS, isTradeSkill, castID, notInterruptible, spellId = UnitCastingInfo(self .unit) @@ -307,13 +339,26 @@ function Unit:IsInterruptible(percent) name, text, texture, startTimeMS, endTimeMS, isTradeSkill, notInterruptible, spellId = UnitChannelInfo(self.unit) end - if name and startTimeMS and endTimeMS and not notInterruptible then - local castTimeRemaining = endTimeMS / 1000 - GetTime() - local castTimeTotal = (endTimeMS - startTimeMS) / 1000 - if castTimeTotal > 0 and castTimeRemaining / castTimeTotal * 100 >= percent then - return true - end + if name then + return not notInterruptible + end + + return false +end + +-- Check if unit is interruptible +function Unit:IsInterruptibleAt(percent) + if not self:IsInterruptible() then + return false + end + + local percent = percent or math.random(2, 5) + + local castPercent = self:GetChannelOrCastPercentComplete() + if castPercent >= percent then + return true end + return false end @@ -375,8 +420,12 @@ function Unit:IsMoving() return GetUnitSpeed(self.unit) > 0 end -function Unit:GetComboPoints(unit) - return GetComboPoints(self.unit, unit.unit) +function Unit:IsMovingAtAll() + return ObjectMovementFlag(self.unit) ~= 0 +end + +function Unit:GetComboPoints() + return UnitPower(self.unit, 4) end -- IsUnit @@ -470,4 +519,88 @@ function Unit:IsInParty() return UnitInParty(self.unit) end +-- Linear regression between time and percent to something +function Unit:LinearRegression(time, percent) + local x = time + local y = percent + + local n = #x + local sum_x = 0 + local sum_y = 0 + local sum_xy = 0 + local sum_xx = 0 + local sum_yy = 0 + + for i = 1, n do + sum_x = sum_x + x[i] + sum_y = sum_y + y[i] + sum_xy = sum_xy + x[i] * y[i] + sum_xx = sum_xx + x[i] * x[i] + sum_yy = sum_yy + y[i] * y[i] + end + + local slope = (n * sum_xy - sum_x * sum_y) / (n * sum_xx - sum_x * sum_x) + local intercept = (sum_y - slope * sum_x) / n + + return slope, intercept +end + +-- Use linear regression to get the health percent at a given time in the future +function Unit:PredictHealth(time) + local x = {} + local y = {} + + if #self.regression_history > 10 then + table.remove(self.regression_history, 1) + end + + table.insert(self.regression_history, { time = GetTime(), percent = self:GetHP() }) + + for i = 1, #self.regression_history do + local entry = self.regression_history[i] + table.insert(x, entry.time) + table.insert(y, entry.percent) + end + + local slope, intercept = self:LinearRegression(x, y) + return slope * time + intercept +end + +-- Use linear regression to guess the time until a given health percent +function Unit:PredictTime(percent) + local x = {} + local y = {} + + if #self.regression_history > 10 then + table.remove(self.regression_history, 1) + end + + table.insert(self.regression_history, { time = GetTime(), percent = self:GetHP() }) + + for i = 1, #self.regression_history do + local entry = self.regression_history[i] + table.insert(x, entry.time) + table.insert(y, entry.percent) + end + + local slope, intercept = self:LinearRegression(x, y) + return (percent - intercept) / slope +end + +-- Time until death +function Unit:TimeToDie() + if self:IsDead() then + self.regression_history = {} + return 0 + end + + local timeto = GetTime() - self:PredictTime(0) + + if timeto ~= timeto or timeto == math.huge or timeto < 0 then + return 0 + end + + return timeto +end + return Unit