vanilla-wow-addons – Rev 1
?pathlinks?

-- Add the module to the tree
local mod = klhtm
local me = {}
mod.combat = me
--[[
KTM_Combat.lua
The combat module parses combat log events for damage and abilities done.
]]
-- These are the events we would like to be notified of
me.myevents = { "CHAT_MSG_SPELL_FAILED_LOCALPLAYER", "CHAT_MSG_COMBAT_FRIENDLY_DEATH"}
-- these are kept for debug purposes. We need two because next attack abilities are split into two.
me.lastattack = nil
me.secondlastattack = nil
--[[
This is a record of attacks in the last second while out of combat. We keep this because when you
go into combat by initiating an attack, the +combat event can come after the actual attack.
]]
me.recentattacks = { }
me.onupdate = function()
local timenow = GetTime()
local key
local value
for key, value in me.recentattacks do
if value[1] < timenow - 1 then
me.recentattacks[key] = nil
end
end
end
-- This is a method level temporary variable. Declared at file level because it is a list,
-- and we don't want to keep paging heap memory every time he is created.
me.event =
{
["hits"] = 0,
["damage"] = 0,
["rage"] = 0,
["threat"] = 0,
["name"] = 0,
}
--[[
Special onevent() method that will be called by Core.lua:onevent()
]]
me.onevent = function()
if me.oneventinternal() then
KLHTM_RequestRedraw("self")
end
end
-- Returns non-nil if the event causes our threat to change
me.oneventinternal = function()
local ability
local target
local amount
local damagetype
if event == "CHAT_MSG_SPELL_FAILED_LOCALPLAYER" then
if string.find(arg1, mod.string.get("spell", "sunder")) then
me.retractsundercast()
return true
else
return
end
elseif event == "CHAT_MSG_COMBAT_FRIENDLY_DEATH" then
if arg1 == UNITDIESSELF then -- UNITDIESSELF = "You die."
-- death is a threat wipe
mod.table.resetraidthreat()
return true
end
end
end
--[[
mod.combat.specialattack(abilityid, target, damage, iscrit, spellschool)
This handles any attack from a spell that has special threat properties, i.e. all the spells in mod.data.spells .
<abilityid> is the internal identifier for these abilities. i.e. Sunder Armor has <abilityid> = "sunder". This is locale
independent.
<damage>, <iscrit>, and <spellschool> are optional. <spellschool> is localised, and will have the value either nil
or "" or one of SPELL_SCHOOL1_CAP, SPELL_SCHOOL2_CAP, etc.
<iscrit> is only accepted if it has the boolean value true.
]]
me.specialattack = function(abilityid, target, damage, iscrit, spellschool)
-- 1) check the attack is directed at the master target. If not, ignore.
if mod.boss.targetismaster(target) == nil then
return
end
-- 2) get the player's global threat modifiers (defensive stance, blessing of salvation, etc)
local threatmodifier = mod.my.globalthreat.value
--[[
Now, most attacks can be handled gracefully by the table. However, for abilities that modify your autoattack,
we would prefer to decouple the ability from the autoattack, so we have to handle these cases individually.
]]
-- reset me.event
me.event.hits = 1
me.event.rage = 0
me.event.damage = damage
-- 3) Handle Autoattack modifying abilities separately
if abilityid == "whitedamage" then
-- shaman special: check for rockbiter
if mod.my.mods.shaman.rockbiter > 0 then
-- make a separate event for the rockbiter
local weaponspeed = UnitAttackSpeed("player")
local rockbiterdps = mod.data.rockbiter[mod.my.mods.shaman.rockbiter]
-- note: we are sending the threat value of rockbiter as the damage argument
me.specialattack("rockbiter", target, rockbiterdps * weaponspeed, nil, nil)
-- the above call to me.specialattack will have overwritten some parts of me.event. Set them back!
me.event.damage = damage
end
-- normal behaviour
me.event.threat = damage * threatmodifier
-- Special case: Rockbiter Weapon
elseif abilityid == "rockbiter" then
-- this will only come from a "whitedamage" call to this method (see above)
me.event.threat = me.event.damage * threatmodifier
me.event.damage = 0
-- Special case: Heroic Strike
elseif abilityid == "heroicstrike" then
local preimpaledamage = damage
if iscrit == true then
preimpaledamage = damage / (1 + mod.my.mods.warrior.impale)
end
local addeddamage = mod.my.ability("heroicstrike", "nextattack")
local myaveragedamage = me.averagemainhanddamage()
local whitedamage = preimpaledamage * (myaveragedamage / (addeddamage + myaveragedamage))
-- Now make a separate method call for the autoattack component
me.specialattack("whitedamage", target, whitedamage, nil, nil)
-- The above method will have overwritten some parts of me.event, so change them back
me.event.damage = damage - whitedamage
me.event.threat = (me.event.damage + mod.my.ability("heroicstrike", "threat")) * threatmodifier
me.event.rage = mod.my.ability("heroicstrike", "rage") + whitedamage / (UnitLevel("player") / 2)
-- Special Case: Maul
elseif abilityid == "maul" then
-- same as heroic strike, but a bit different
local presavagefurydamage = damage / (1 + mod.my.mods.druid.savagefury)
local addeddamage = mod.my.ability("maul", "nextattack")
local myaveragedamage = me.averagemainhanddamage()
local whitedamage = presavagefurydamage * (myaveragedamage / (addeddamage + myaveragedamage))
-- Now make a separate method call for the autoattack component
me.specialattack("whitedamage", target, whitedamage, nil, nil)
-- The above method will have overwritten some parts of me.event, so change them back
me.event.damage = damage - whitedamage
me.event.threat = threatmodifier * (damage * mod.my.ability("maul", "multiplier") - whitedamage)
me.event.rage = mod.my.ability("maul", "rage") + whitedamage / (UnitLevel("player") / 2)
-- Default Case: all other abilities
else
-- 1) Check for rage
me.event.rage = mod.my.ability(abilityid, "rage")
local multiplier = mod.my.ability(abilityid, "multiplier")
-- 2) Check for multiplier
if multiplier then
me.event.threat = me.event.damage * multiplier
else
me.event.threat = me.event.damage + mod.my.ability(abilityid, "threat")
end
-- 3) Multiply by global modifiers
me.event.threat = me.event.threat * threatmodifier
end
-- Paladin righteous fury (can affect holy shield)
if mod.my.class == "paladin" then
-- righteous fury
if spellschool == SPELL_SCHOOL1_CAP then -- holy
me.event.threat = me.event.threat * mod.my.mods.paladin.righteousfury
end
-- warlock Nemesis 8/8 (can affect searing pain)
elseif mod.my.class == "warlock" then
-- Nemesis 8/8
if (mod.my.mods.warlock.nemesis == true) and (mod.data.spellmatchesset("Warlock Destruction", abilityid) == true) then
me.event.threat = me.event.threat * 0.8
end
end
-- check for >= 0 threat
if me.event.threat + mod.table.getraidthreat() < 0 then
me.event.threat = - mod.table.getraidthreat()
end
-- relocalise.
if abilityid == "whitedamage" then
me.event.name = mod.string.get("threatsource", "whitedamage")
else
me.event.name = mod.string.get("spell", abilityid)
end
-- Add to data
me.addattacktodata(me.event.name, me.event)
me.addattacktodata(mod.string.get("threatsource", "total"), me.event)
end
-- to work out which part of a next attack ability was from white damage
-- used by nextattack abilities like maul and heroic strike.
me.averagemainhanddamage = function()
local min, max = UnitDamage("player")
return (min + max) / 2
end
--[[
me.normalattack(spellname, damage, target, iscrit, spellschool)
Handles a damage-causing ability with no special threat properties. We often have to make modifiers for gear or talents here.
<spellname> is the name of the ability.
<spellid> is the internal name for "special" spells or abilities, i.e. those we have to look out for because there are specific set bonuses or talents that affect them.
<damage> is the damage done.
<target> is the name of the mob you hit.
<iscrit> is a boolean, whether the hit was a critical one. Triggers iff it is the boolean value true.
<spellschool> is a string, one of SPELL_SCHOOL1_CAP, SPELL_SCHOOL2_CAP, etc, or possibly nil or "".
]]
me.normalattack = function(spellname, spellid, damage, isdot, target, iscrit, spellschool)
-- check the attack is directed at the master target. If not, ignore.
if mod.boss.targetismaster(target) == nil then
return
end
-- threatmodifier includes global things, like defensive stance, tranquil air totem, rogue passive modifier, etc.
local threatmodifier = mod.my.globalthreat.value
-- Special threat mod: priest silent resolve (spells only)
if mod.my.class == "priest" then
-- 1.12: multiplicative threat
if mod.isnewwowversion then
threatmodifier = threatmodifier * (1.0 + mod.my.mods.priest.silentresolve)
else
threatmodifier = threatmodifier + mod.my.mods.priest.silentresolve
end
end
-- Special threat modifiers for mages
if mod.my.class == "mage" then
if spellschool == SPELL_SCHOOL6_CAP then -- arcane
-- 1.12 override check
if mod.isnewwowversion then
threatmodifier = threatmodifier * (1.0 + mod.my.mods.mage.arcanethreat)
else
threatmodifier = threatmodifier + mod.my.mods.mage.arcanethreat
end
elseif spellschool == SPELL_SCHOOL4_CAP then -- frost
-- 1.12 override check
if mod.isnewwowversion then
threatmodifier = threatmodifier * (1.0 + mod.my.mods.mage.frostthreat)
else
threatmodifier = threatmodifier + mod.my.mods.mage.frostthreat
end
elseif spellschool == SPELL_SCHOOL2_CAP then -- fire
-- 1.12 override check
if mod.isnewwowversion then
threatmodifier = threatmodifier * (1.0 + mod.my.mods.mage.firethreat)
else
threatmodifier = threatmodifier + mod.my.mods.mage.firethreat
end
end
end
-- Default values for me.event:
me.event.hits = 1
me.event.rage = 0
me.event.damage = damage
me.event.threat = damage * threatmodifier
-- now get the name:
if spellid == "dot" then
me.event.hits = 0
me.event.name = mod.string.get("threatsource", "dot")
elseif spellid == "damageshield" then
me.event.name = mod.string.get("threatsource", "damageshield")
else
me.event.name = mod.string.get("threatsource", "special")
me.event.threat = damage * threatmodifier
end
-- Now apply class-specific filters
-- warlock
if mod.my.class == "warlock" then
-- Nemesis 8/8
if (mod.my.mods.warlock.nemesis == true) and (mod.data.spellmatchesset("Warlock Destruction", spellid) == true) then
me.event.threat = me.event.threat * 0.8
end
-- Priest
elseif mod.my.class == "priest" then
-- shadow affinity
if spellschool == SPELL_SCHOOL5_CAP then
me.event.threat = me.event.threat * mod.my.mods.priest.shadowaffinity
end
-- holy nova: no threat
if spellid == "holynova" then
me.event.threat = 0
end
-- Mage
elseif mod.my.class == "mage" then
-- netherwind
if mod.my.mods.mage.netherwind == true then
if (spellid == "frostbolt") or (spellid == "scorch") or (spellid == "fireball") then
-- note that this won't trigger off the dot part of fireball, because then spellid will be "dot"
me.event.threat = math.max(0, (me.event.threat - 100))
elseif spellid == "arcanemissiles" then
me.event.threat = math.max(0, (me.event.threat - 20))
end
end
-- Paladin
elseif mod.my.class == "paladin" then
-- righteous fury
if spellschool == SPELL_SCHOOL1_CAP then -- holy
me.event.threat = me.event.threat * mod.my.mods.paladin.righteousfury
end
end
-- special: blood siphon no threat vs Hakkar. This may or may not be correct.
if spellid == "bloodsiphon" then
me.event.threat = 0
end
-- now add me.event to individual and totals
me.addattacktodata(me.event.name, me.event)
me.addattacktodata(mod.string.get("threatsource", "total"), me.event)
end
--[[
me.registertaunt()
Called when you succesfully casts Taunt or Growl on a mob.
<target> is the name of the mob you have taunted.
]]
me.taunt = function(target)
if mod.out.checktrace("info", me, "taunt") then
mod.out.printtrace(string.format("Taunting %s!", target))
end
--[[
OK, new idea. If targettarget has greater threat than you, assume they are the aggro target.
]]
if mod.boss.mastertarget and (target ~= mod.boss.mastertarget) then
-- you taunted a mob that is not the master target
return
end
-- here, you either taunted the master target, or there is no master target
-- check if you are still targetting that mob (likely, if you just taunted it)
if UnitName("target") == target then
-- Check for tt
local targettarget = UnitName("targettarget")
if mod.table.raiddata[targettarget] and (mod.table.raiddata[targettarget] > mod.table.getraidthreat()) then
-- your current target is targetting another player, and they have more threat than you
local gain = mod.table.raiddata[targettarget] - mod.table.getraidthreat()
me.event.hits = 1
me.event.damage = 0
me.event.rage = 0
me.event.threat = gain
me.event.name = mod.string.get("spell", "taunt")
me.addattacktodata(mod.string.get("spell", "taunt"), me.event)
me.addattacktodata(mod.string.get("threatsource", "total"), me.event)
if mod.out.checktrace("info", me, "taunt") then
mod.out.printtrace(string.format("You taunt %s from %s, gaining %d threat.", target, targettarget, gain))
end
return
end
end
-- If that didn't work, use the old code:
if target == mod.boss.mastertarget then
local previoustargetthreat = mod.table.raiddata[mod.boss.mttruetarget]
if (previoustargetthreat ~= nil) and (previoustargetthreat > mod.table.getraidthreat()) then
local tauntgain = previoustargetthreat - mod.table.getraidthreat()
me.event.hits = 1
me.event.damage = 0
me.event.rage = 0
me.event.threat = tauntgain
me.event.name = mod.string.get("spell", "taunt")
me.addattacktodata(mod.string.get("spell", "taunt"), me.event)
me.addattacktodata(mod.string.get("threatsource", "total"), me.event)
if mod.out.checktrace("info", me, "taunt") then
mod.out.printtrace(string.format("You taunt %s from %s, gaining %d threat.", target, mod.boss.mttruetarget or "<nil>", tauntgain))
end
else
if mod.out.checktrace("info", me, "taunt") then
mod.out.printtrace(string.format("You taunt %s from %s, but he had a lower threat, so you gain no threat.", target, mod.boss.mttruetarget or "<nil>"))
end
end
end
end
--[[
me.possibleoverheal(spellname, spellid, amount, target)
Works out the threat from a heal.
<spellname> is the localised name of the spell
<spellid> is the mod's internal name for the spell, if it is special (i.e. affected by talents / sets bonuses), otherwise "".
<amount> is the healed amount only (no overheal)
<target> is the name of the target
Called when you heal someone. Deducts the overhealing from the total, then calls the Heal method
]]
me.possibleoverheal = function(spellname, spellid, amount, target)
-- we can check the target's health, which will be the health before the heal. Then we work out what the
-- heal did, and we can calculate overheal.
local unit = mod.unit.findunitidfromname(target)
if unit == nil then
if mod.out.checktrace("info", me, "healtarget") then
mod.out.printtrace(string.format("Could not find a UnitID for the name %s.", target))
end
-- (and assume there was no overheal)
else
local hpvoid = UnitHealthMax(unit) - UnitHealth(unit)
amount = math.min(amount, hpvoid)
end
me.registerheal(spellname, spellid, amount, target)
end
--[[
me.registerheal(spellname, spellid, amount, target)
Works out the threat from a heal.
<spellname> is the localised name of the spell
<spellid> is the mod's internal name for the spell, if it is special (i.e. affected by talents / sets bonuses), otherwise "".
<amount> is the healed amount only (no overheal)
<target> is the name of the target
]]
me.registerheal = function(spellname, spellid, amount, target)
-- in general, don't count heals towards the master target
if mod.boss.mastertarget and mod.boss.targetismaster(target) then
return
end
me.event.hits = 1
me.event.rage = 0
me.event.damage = amount
local threatmod = mod.my.globalthreat.value
-- Special threat mod: priest silent resolve (spells only)
if mod.my.class == "priest" then
-- 1.12+ multiplicative threat
if mod.isnewwowversion then
threatmod = threatmod * (1 + mod.my.mods.priest.silentresolve)
else
threatmod = threatmod + mod.my.mods.priest.silentresolve
end
end
me.event.threat = amount * threatmod * mod.data.threatconstants.healing
-- class-based healing multipliers
if mod.my.class == "paladin" then
me.event.threat = me.event.threat * mod.my.mods.paladin.healing
elseif mod.my.class == "druid" then
me.event.threat = me.event.threat * mod.my.mods.druid.subtlety
if spellid == "tranquility" then
me.event.threat = me.event.threat * mod.my.mods.druid.tranquilitythreat
end
elseif mod.my.class == "shaman" then
me.event.threat = me.event.threat * mod.my.mods.shaman.healing
end
-- Special: healing abilities which don't cause threat
if spellid == "holynova" then
me.event.threat = 0
elseif spellid == "siphonlife" then
me.event.threat = 0
elseif spellid == "drainlife" then
me.event.threat = 0
elseif spellid == "deathcoil" then
me.event.threat = 0
end
me.addattacktodata(mod.string.get("threatsource", "healing"), me.event)
me.event.damage = 0
me.addattacktodata(mod.string.get("threatsource", "total"), me.event)
end
--[[
me.powergain(amount, powertype)
Calculates the threat from gaining energy / mana / rage.
<amount> is the amount of power gained.
<powertype> is "Mana" or "Rage" or "Energy", but the localised versions.
<spellid> is the spell or effect that caused the power gain.
]]
me.powergain = function(amount, powertype, spellid)
me.event.damage = amount
me.event.hits = 1
me.event.rage = 0
-- 1) Prevent "overheal" for power gain
local maxgain = UnitManaMax("player") - UnitMana("player")
amount = math.min(maxgain, amount)
if powertype == mod.string.get("power", "rage") then
me.event.threat = amount * mod.data.threatconstants.ragegain
elseif powertype == mod.string.get("power", "energy") then
me.event.threat = amount * mod.data.threatconstants.energygain
elseif powertype == mod.string.get("power", "mana") then
me.event.threat = amount * mod.data.threatconstants.managain
else
return
end
-- Special: abilities which don't cause threat
if spellid == "lifetap" then
me.event.threat = 0
end
me.addattacktodata(mod.string.get("threatsource", "powergain"), me.event)
-- now mod it a bit to work into total better
me.event.damage = 0
me.event.hits = 0
me.addattacktodata(mod.string.get("threatsource", "total"), me.event)
end
--[[
me.addattacktodata(name, data)
Once the threat from an attack has been worked, add it.
<name> is the category to add the threat to
<data> is me.event, i think...
Heap Memory will be created when you call this method when out of combat (mostly healing).
From the size of the list (two numbers in it), will be 50 bytes tops each time.
]]
me.addattacktodata = function(name, data)
-- Ignore if charmed
if mod.my.states.playercharmed.value == true then
return
end
if name ~= mod.string.get("threatsource", "total") then
me.secondlastattack = me.lastattack
me.lastattack = data
-- Add this to the recent attacks list, if we are not in combat
if mod.my.states.incombat.value == false then
table.insert(me.recentattacks, {GetTime(), data.threat})
end
end
-- Add a new column to mod.table.mydata, if it does not exist already
if mod.table.mydata[name] == nil then
mod.table.mydata[name] = mod.table.newdatastruct()
end
mod.table.mydata[name].hits = mod.table.mydata[name].hits + data.hits
mod.table.mydata[name].threat = mod.table.mydata[name].threat + data.threat
mod.table.mydata[name].rage = mod.table.mydata[name].rage + data.rage
mod.table.mydata[name].damage = mod.table.mydata[name].damage + data.damage
end
----------------------------------------------------------
-- Handling Sunder --
----------------------------------------------------------
--[[
klhtu.combat.submitsundercast()
When you think you have just cast a sunder, call this method to make sure the addon knows. There's no way to
reliably detect a sunder hit, so we assume it has been cast, then look for failure signs (misses, spell errors)
It's highly recommended to use klhtm.combat.sunder() or KLHTM_Sunder() instead, because it will make sure
that threat is correct when you spam the button, which may not happen otherwise.
]]
me.submitsundercast = function()
-- check for Master Target
if mod.boss.targetismaster(UnitName("target")) == nil then
return
end
me.specialattack("sunder", UnitName("target"), 0)
KLHTM_RequestRedraw("self")
end
-- Call this from a macro, to replace your normal sunder button
me.spellbooksunderindex = 1
-- Kept for compatibility
function KLHTM_Sunder()
me.sunder()
end
--[[
me.sunder()
Casts Sunder Armor if most checks reveal it is castable, and calls me.sumbitsundercast().
This method is safe to be spammed. It will only cast if the global cooldown is off, which means once you try to
cast it, you won't try again unless the server says "failed due to <x>". So it's like full good and stuff.
]]
me.sunder = function()
if mod.my.class ~= "warrior" then
return
end
-- 1) Check if the old position is fine
if GetSpellName(me.spellbooksunderindex, "spell") ~= mod.string.get("spell", "sunder") then
-- get spell number of spells
local spelltabs = GetNumSpellTabs()
local numspells = 0
local i
local temp
for i = 1, spelltabs do
_, _, _, temp = GetSpellTabInfo(i)
numspells = numspells + temp
end
-- 2) locate sunder armor in the spell book
for i = 1, numspells do
if GetSpellName(me.spellbooksunderindex + i, "spell") == mod.string.get("spell", "sunder") then
me.spellbooksunderindex = me.spellbooksunderindex + i
break
elseif i == numspells then
if mod.out.checktrace("warning", me, "sunder") then
mod.out.printtrace("Can't find sunder in your spellbook!")
end
return -- can't find sunder
end
end
end
-- Now we've found sunder. Check the cooldown
if GetSpellCooldown(me.spellbooksunderindex, "spell") ~= 0 then
return
end
-- Test for target
if UnitCanAttack("player", "target") == nil then
return
end
-- Cast
CastSpellByName(mod.string.get("spell", "sunder") .. "()")
me.submitsundercast()
end
--[[
me.retractsundercast()
Called when an attempted cast of Sunder Armor was found to have failed. i.e. we received a message like
"Your sunder armor failed: not enough rage" in the combat log.
]]
me.retractsundercast = function()
-- 1) check for master target
if mod.boss.targetismaster(UnitName("target")) == nil then
return
end
me.event.hits = -1
me.event.damage = 0
me.event.rage = - mod.my.ability("sunder", "rage")
local threatmodifier = mod.my.globalthreat.value
me.event.threat = - mod.my.ability("sunder", "threat") * threatmodifier
me.addattacktodata(mod.string.get("spell", "sunder"), me.event)
me.addattacktodata(mod.string.get("threatsource", "total"), me.event)
KLHTM_RequestRedraw("self")
end
--[[
This code hooks UseAction, intercepts the user casting Sunder Armor, and runs me.sunder() instead.
]]
me.saveduseaction = UseAction
me.newuseaction = function(actionindex, x, y)
-- Check Sunder
if (mod.my.class == "warrior") and actionindex and (GetActionText(actionindex) == nil) and (GetActionTexture(actionindex) == "Interface\\Icons\\Ability_Warrior_Sunder") then
if GetActionCooldown(actionindex) > 0 then
if mod.out.checktrace("info", me, "sunder") then
mod.out.printtrace("Preventing Sunder cast due to cooldown!")
end
return
else
-- Now check we have a decent target. Otherwise press "tab" - this is what the UI does anyway
if UnitCanAttack("player", "target") ~= 1 then
if mod.out.checktrace("info", me, "sunder") then
mod.out.printtrace("Preventing Sunder cast due to invalid target!")
end
TargetNearestEnemy()
return
end
me.submitsundercast()
-- At the bottom of this method the sunder will be cast
end
end
-- Call the original function
me.saveduseaction(actionindex, x, y)
end
UseAction = me.newuseaction