-- 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 . is the internal identifier for these abilities. i.e. Sunder Armor has = "sunder". This is locale independent. , , and are optional. is localised, and will have the value either nil or "" or one of SPELL_SCHOOL1_CAP, SPELL_SCHOOL2_CAP, etc. 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. is the name of the ability. 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. is the damage done. is the name of the mob you hit. is a boolean, whether the hit was a critical one. Triggers iff it is the boolean value true. 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. 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 "", 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 "")) end end end end --[[ me.possibleoverheal(spellname, spellid, amount, target) Works out the threat from a heal. is the localised name of the spell is the mod's internal name for the spell, if it is special (i.e. affected by talents / sets bonuses), otherwise "". is the healed amount only (no overheal) 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. is the localised name of the spell is the mod's internal name for the spell, if it is special (i.e. affected by talents / sets bonuses), otherwise "". is the healed amount only (no overheal) 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. is the amount of power gained. is "Mana" or "Rage" or "Energy", but the localised versions. 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. is the category to add the threat to 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 ". 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