diff --git a/libs/libcast.lua b/libs/libcast.lua new file mode 100644 index 0000000..d333c7e --- /dev/null +++ b/libs/libcast.lua @@ -0,0 +1,410 @@ +local _G = _G or getfenv(0) + +local L = ShaguTweaks.L +local GetExpansion = ShaguTweaks.GetExpansion +local libtipscan = ShaguTweaks.libtipscan +local libspell = ShaguTweaks.libspell +local hooksecurefunc = ShaguTweaks.hooksecurefunc +local cmatch = ShaguTweaks.cmatch + +-- return instantly if we're not on a vanilla client +if GetExpansion() ~= "vanilla" then return end + +local valid_units = {} +valid_units["pet"] = true +valid_units["player"] = true +valid_units["target"] = true +valid_units["mouseover"] = true + +valid_units["pettarget"] = true +valid_units["playertarget"] = true +valid_units["targettarget"] = true +valid_units["mouseovertarget"] = true +valid_units["targettargettarget"] = true + +for i=1,4 do valid_units["party" .. i] = true end +for i=1,4 do valid_units["partypet" .. i] = true end +for i=1,40 do valid_units["raid" .. i] = true end +for i=1,40 do valid_units["raidpet" .. i] = true end + +for i=1,4 do valid_units["party" .. i .. "target"] = true end +for i=1,4 do valid_units["partypet" .. i .. "target"] = true end +for i=1,40 do valid_units["raid" .. i .. "target"] = true end +for i=1,40 do valid_units["raidpet" .. i .. "target"] = true end + +local lastcasttex, lastrank, _ +local scanner = libtipscan:GetScanner("libcast") + +local libcast = CreateFrame("Frame", "ShaguTweaksEnemyCast") +local player = UnitName("player") + +ShaguTweaks.UnitChannelInfo = _G.UnitChannelInfo or function(unit) + -- convert to name if unitstring was given + unit = valid_units[unit] and UnitName(unit) or unit + + local cast, nameSubtext, text, texture, startTime, endTime, isTradeSkill + local db = libcast.db[unit] + + -- clean legacy values + if db and db.cast and db.start + db.casttime / 1000 > GetTime() then + if not db.channel then return end + cast = db.cast + nameSubtext = db.rank + text = "" + texture = db.icon + startTime = db.start * 1000 + endTime = startTime + db.casttime + isTradeSkill = nil + elseif db then + -- remove cast action to the database + db.cast = nil + db.rank = nil + db.start = nil + db.casttime = nil + db.icon = nil + db.channel = nil + end + + return cast, nameSubtext, text, texture, startTime, endTime, isTradeSkill +end + +ShaguTweaks.UnitCastingInfo = _G.UnitCastingInfo or function(unit) + -- convert to name if unitstring was given + unit = valid_units[unit] and UnitName(unit) or unit + + local cast, nameSubtext, text, texture, startTime, endTime, isTradeSkill + local db = libcast.db[unit] + + -- clean legacy values + if db and db.cast and db.start + db.casttime / 1000 > GetTime() then + if db.channel then return end + cast = db.cast + nameSubtext = db.rank or "" + text = "" + texture = db.icon + startTime = db.start * 1000 + endTime = startTime + db.casttime + isTradeSkill = nil + elseif db then + -- remove cast action to the database + db.cast = nil + db.rank = nil + db.start = nil + db.casttime = nil + db.icon = nil + db.channel = nil + end + + return cast, nameSubtext, text, texture, startTime, endTime, isTradeSkill +end + +function libcast:AddAction(mob, spell, channel) + if not mob or not spell then return nil end + + if L["spells"][spell] ~= nil then + local casttime = L["spells"][spell].t + local icon = L["spells"][spell].icon and string.format("%s%s", "Interface\\Icons\\", L["spells"][spell].icon) or nil + + -- add cast action to the database + if not self.db[mob] then self.db[mob] = {} end + self.db[mob].cast = spell + self.db[mob].rank = nil + self.db[mob].start = GetTime() + self.db[mob].casttime = casttime + self.db[mob].icon = icon + self.db[mob].channel = channel + + return true + end + + return nil +end + +function libcast:RemoveAction(mob, spell) + if self.db[mob] and ( L["interrupts"][spell] ~= nil or spell == "INTERRUPT" ) then + + -- remove cast action to the database + self.db[mob].cast = nil + self.db[mob].rank = nil + self.db[mob].start = nil + self.db[mob].casttime = nil + self.db[mob].icon = nil + self.db[mob].channel = nil + end +end + +-- main data +libcast.db = { [player] = {} } + +-- environmental casts +libcast:RegisterEvent("CHAT_MSG_SPELL_SELF_DAMAGE") +libcast:RegisterEvent("CHAT_MSG_SPELL_HOSTILEPLAYER_DAMAGE") +libcast:RegisterEvent("CHAT_MSG_SPELL_HOSTILEPLAYER_BUFF") +libcast:RegisterEvent("CHAT_MSG_SPELL_FRIENDLYPLAYER_DAMAGE") +libcast:RegisterEvent("CHAT_MSG_SPELL_FRIENDLYPLAYER_BUFF") +libcast:RegisterEvent("CHAT_MSG_SPELL_PERIODIC_HOSTILEPLAYER_BUFFS") +libcast:RegisterEvent("CHAT_MSG_SPELL_PERIODIC_FRIENDLYPLAYER_BUFFS") +libcast:RegisterEvent("CHAT_MSG_SPELL_PERIODIC_HOSTILEPLAYER_DAMAGE") +libcast:RegisterEvent("CHAT_MSG_SPELL_PERIODIC_FRIENDLYPLAYER_DAMAGE") +libcast:RegisterEvent("CHAT_MSG_SPELL_PERIODIC_SELF_DAMAGE") +libcast:RegisterEvent("CHAT_MSG_SPELL_PARTY_DAMAGE") +libcast:RegisterEvent("CHAT_MSG_SPELL_PARTY_BUFF") +libcast:RegisterEvent("CHAT_MSG_SPELL_PERIODIC_PARTY_DAMAGE") +libcast:RegisterEvent("CHAT_MSG_SPELL_PERIODIC_PARTY_BUFFS") +libcast:RegisterEvent("CHAT_MSG_SPELL_PERIODIC_CREATURE_DAMAGE") +libcast:RegisterEvent("CHAT_MSG_SPELL_PERIODIC_CREATURE_BUFFS") +libcast:RegisterEvent("CHAT_MSG_SPELL_CREATURE_VS_CREATURE_DAMAGE") +libcast:RegisterEvent("CHAT_MSG_SPELL_CREATURE_VS_CREATURE_BUFF") + +-- player spells +libcast:RegisterEvent("SPELLCAST_START") +libcast:RegisterEvent("SPELLCAST_STOP") +libcast:RegisterEvent("SPELLCAST_FAILED") +libcast:RegisterEvent("SPELLCAST_INTERRUPTED") +libcast:RegisterEvent("SPELLCAST_DELAYED") +libcast:RegisterEvent("SPELLCAST_CHANNEL_START") +libcast:RegisterEvent("SPELLCAST_CHANNEL_STOP") +libcast:RegisterEvent("SPELLCAST_CHANNEL_UPDATE") + +libcast:SetScript("OnEvent", function() + -- Fill database with player casts + if event == "SPELLCAST_START" then + local icon = L["spells"][arg1] and L["spells"][arg1].icon and string.format("%s%s", "Interface\\Icons\\", L["spells"][arg1].icon) or lastcasttex + -- add cast action to the database + this.db[player].cast = arg1 + this.db[player].rank = lastrank + this.db[player].start = GetTime() + this.db[player].casttime = arg2 + this.db[player].icon = icon + this.db[player].channel = nil + if not L["spells"][arg1] or not L["spells"][arg1].icon or not L["spells"][arg1].t then + L["spells"][arg1] = L["spells"][arg1] or { } + L["spells"][arg1].icon = L["spells"][arg1].icon or icon + L["spells"][arg1].t = L["spells"][arg1].t or arg2 + end + lastcasttex, lastrank = nil, nil + elseif event == "SPELLCAST_STOP" or event == "SPELLCAST_FAILED" or event == "SPELLCAST_INTERRUPTED" then + if this.db[player] and not this.db[player].channel then + -- remove cast action to the database + this.db[player].cast = nil + this.db[player].rank = nil + this.db[player].rank = nil + this.db[player].start = nil + this.db[player].casttime = nil + this.db[player].icon = nil + this.db[player].channel = nil + else + lastcasttex, lastrank = nil, nil + end + elseif event == "SPELLCAST_DELAYED" then + if this.db[player].cast then + this.db[player].start = this.db[player].start + arg1/1000 + end + elseif event == "SPELLCAST_CHANNEL_START" then + -- add cast action to the database + this.db[player].cast = arg2 + this.db[player].rank = lastrank + this.db[player].start = GetTime() + this.db[player].casttime = arg1 + this.db[player].icon = L["spells"][arg2] and L["spells"][arg2].icon and string.format("%s%s", "Interface\\Icons\\", L["spells"][arg2].icon) or lastcasttex + this.db[player].channel = true + lastcasttex, lastrank = nil, nil + elseif event == "SPELLCAST_CHANNEL_STOP" then + if this.db[player] and this.db[player].channel then + -- remove cast action to the database + this.db[player].cast = nil + this.db[player].rank = nil + this.db[player].start = nil + this.db[player].casttime = nil + this.db[player].icon = nil + this.db[player].channel = nil + end + elseif event == "SPELLCAST_CHANNEL_UPDATE" then + if this.db[player].cast then + this.db[player].start = -this.db[player].casttime/1000 + GetTime() + arg1/1000 + end + -- Fill database with environmental casts + elseif arg1 then + local mob, spell, _ + + -- (.+) begins to cast (.+). + mob, spell = cmatch(arg1, SPELLCASTOTHERSTART) + if libcast:AddAction(mob, spell) then return end + + -- (.+) begins to perform (.+). + mob, spell = cmatch(arg1, SPELLPERFORMOTHERSTART) + if libcast:AddAction(mob, spell) then return end + + -- (.+) gains (.+). + mob, spell = cmatch(arg1, AURAADDEDOTHERHELPFUL) + if libcast:RemoveAction(mob, spell) then return end + + -- (.+) is afflicted by (.+). + mob, spell = cmatch(arg1, AURAADDEDOTHERHARMFUL) + if libcast:RemoveAction(mob, spell) then return end + + -- Your (.+) hits (.+) for (%d+). + spell, mob = cmatch(arg1, SPELLLOGSELFOTHER) + if libcast:RemoveAction(mob, spell) then return end + + -- Your (.+) crits (.+) for (%d+). + spell, mob = cmatch(arg1, SPELLLOGCRITSELFOTHER) + if libcast:RemoveAction(mob, spell) then return end + + -- (.+)'s (.+) %a hits (.+) for (%d+). + _, spell, mob = cmatch(arg1, SPELLLOGOTHEROTHER) + if libcast:RemoveAction(mob, spell) then return end + + -- (.+)'s (.+) %a crits (.+) for (%d+). + _, spell, mob = cmatch(arg1, SPELLLOGCRITOTHEROTHER) + if libcast:RemoveAction(mob, spell) then return end + + -- You interrupt (.+)'s (.+). + mob, spell = cmatch(arg1, SPELLINTERRUPTSELFOTHER) + if libcast:RemoveAction(mob, spell) then return end + + -- (.+) interrupts (.+)'s (.+). + _, mob, spell = cmatch(arg1, SPELLINTERRUPTOTHEROTHER) + if libcast:RemoveAction(mob, spell) then return end + end +end) + +--[[ Custom Casts + Enable Castbars for spells that don't have a castbar by default + (e.g Multi-Shot and Aimed Shot) +]]-- +local aimedshot = L["customcast"]["AIMEDSHOT"] +local multishot = L["customcast"]["MULTISHOT"] + +libcast.customcast = {} +libcast.customcast[strlower(aimedshot)] = function(begin, duration) + if begin then + local duration = duration or 3000 + + for i=1,32 do + if UnitBuff("player", i) == "Interface\\Icons\\Racial_Troll_Berserk" then + local berserk = 0.3 + if((UnitHealth("player")/UnitHealthMax("player")) >= 0.40) then + berserk = (1.30 - (UnitHealth("player") / UnitHealthMax("player"))) / 3 + end + duration = duration / (1 + berserk) + elseif UnitBuff("player", i) == "Interface\\Icons\\Ability_Hunter_RunningShot" then + duration = duration / 1.4 + elseif UnitBuff("player", i) == "Interface\\Icons\\Ability_Warrior_InnerRage" then + duration = duration / 1.3 + elseif UnitBuff("player", i) == "Interface\\Icons\\Inv_Trinket_Naxxramas04" then + duration = duration / 1.2 + end + end + + local _,_, lag = GetNetStats() + local start = GetTime() + lag/1000 + + -- add cast action to the database + libcast.db[player].cast = aimedshot + libcast.db[player].rank = lastrank + libcast.db[player].start = start + libcast.db[player].casttime = duration + libcast.db[player].icon = "Interface\\Icons\\Inv_spear_07" + libcast.db[player].channel = nil + else + -- remove cast action to the database + libcast.db[player].cast = nil + libcast.db[player].rank = nil + libcast.db[player].start = nil + libcast.db[player].casttime = nil + libcast.db[player].icon = nil + libcast.db[player].channel = nil + end +end + +libcast.customcast[strlower(multishot)] = function(begin, duration) + if begin then + local duration = duration or 500 + + for i=1,32 do + if UnitBuff("player", i) == "Interface\\Icons\\Racial_Troll_Berserk" then + local berserk = 0.3 + if((UnitHealth("player")/UnitHealthMax("player")) >= 0.40) then + berserk = (1.30 - (UnitHealth("player") / UnitHealthMax("player"))) / 3 + end + duration = duration / (1 + berserk) + elseif UnitBuff("player", i) == "Interface\\Icons\\Ability_Hunter_RunningShot" then + duration = duration / 1.4 + elseif UnitBuff("player", i) == "Interface\\Icons\\Ability_Warrior_InnerRage" then + duration = duration / 1.3 + elseif UnitBuff("player", i) == "Interface\\Icons\\Inv_Trinket_Naxxramas04" then + duration = duration / 1.2 + end + end + + local _,_, lag = GetNetStats() + local start = GetTime() + lag/1000 + + -- add cast action to the database + libcast.db[player].cast = multishot + libcast.db[player].rank = lastrank + libcast.db[player].start = start + libcast.db[player].casttime = duration + libcast.db[player].icon = "Interface\\Icons\\Ability_upgrademoonglaive" + libcast.db[player].channel = nil + else + -- remove cast action to the database + libcast.db[player].cast = nil + libcast.db[player].rank = nil + libcast.db[player].start = nil + libcast.db[player].casttime = nil + libcast.db[player].icon = nil + libcast.db[player].channel = nil + end +end + +local function CastCustom(spell) + if not spell then return end + if not ShaguTweaks.UnitCastingInfo(UnitName("player")) then + for custom, func in pairs(libcast.customcast) do + if strfind(strlower(spell), custom) or strlower(spell) == custom then + func(true) + end + end + end +end + +hooksecurefunc("UseContainerItem", function(id, index) + lastcasttex = GetContainerItemInfo(id, index) +end) + +hooksecurefunc("CastSpell", function(id, bookType) + _, lastrank = libspell.GetSpellInfo(id, bookType) + lastcasttex = GetSpellTexture(id, bookType) + + if GetSpellCooldown(id, bookType) ~= 0 then + local spellName = GetSpellName(id, bookType) + CastCustom(spellName) + end +end, true) + +hooksecurefunc("CastSpellByName", function(spell, target) + _, lastrank = libspell.GetSpellInfo(spell) + + for i=1,120 do + -- detect if any cast is ongoing + if IsCurrentAction(i) then + CastCustom(spell) + return + end + end +end, true) + +hooksecurefunc("UseAction", function(slot, target, button) + scanner:SetAction(slot) + local spellName, rank = scanner:Line(1) + + lastcasttex = GetActionTexture(slot) + lastrank = rank + + if GetActionText(slot) or not IsCurrentAction(slot) then return end + CastCustom(spellName) +end, true) + +ShaguTweaks.libcast = libcast diff --git a/libs/libdebuff.lua b/libs/libdebuff.lua new file mode 100644 index 0000000..d810ca6 --- /dev/null +++ b/libs/libdebuff.lua @@ -0,0 +1,271 @@ +local _G = _G or getfenv(0) + +local L = ShaguTweaks.L +local GetExpansion = ShaguTweaks.GetExpansion +local libtipscan = ShaguTweaks.libtipscan +local libspell = ShaguTweaks.libspell +local hooksecurefunc = ShaguTweaks.hooksecurefunc +local QueueFunction = ShaguTweaks.QueueFunction +local cmatch = ShaguTweaks.cmatch + +-- return instantly if we're not on a vanilla client +if GetExpansion() ~= "vanilla" then return end + +-- fix a typo (missing $) in ruRU capture index +if GetLocale() == "ruRU" then + SPELLREFLECTSELFOTHER = gsub(SPELLREFLECTSELFOTHER, "%%2s", "%%2%$s") +end + +local libdebuff = CreateFrame("Frame", "ShaguTweaksDebuffsScanner", UIParent) +local scanner = libtipscan:GetScanner("libdebuff") +local _, class = UnitClass("player") +local lastspell + +function libdebuff:GetDuration(effect, rank) + if L["debuffs"][effect] then + local rank = rank and tonumber((string.gsub(rank, RANK .. " ", ""))) or 0 + local rank = L["debuffs"][effect][rank] and rank or libdebuff:GetMaxRank(effect) + local duration = L["debuffs"][effect][rank] + + if effect == L["dyndebuffs"]["Rupture"] then + -- Rupture: +2 sec per combo point + duration = duration + GetComboPoints()*2 + elseif effect == L["dyndebuffs"]["Kidney Shot"] then + -- Kidney Shot: +1 sec per combo point + duration = duration + GetComboPoints()*1 + elseif effect == L["dyndebuffs"]["Demoralizing Shout"] then + -- Booming Voice: 10% per talent + local _,_,_,_,count = GetTalentInfo(2,1) + if count and count > 0 then duration = duration + ( duration / 100 * (count*10)) end + elseif effect == L["dyndebuffs"]["Shadow Word: Pain"] then + -- Improved Shadow Word: Pain: +3s per talent + local _,_,_,_,count = GetTalentInfo(3,4) + if count and count > 0 then duration = duration + count * 3 end + elseif effect == L["dyndebuffs"]["Frostbolt"] then + -- Permafrost: +1s per talent + local _,_,_,_,count = GetTalentInfo(3,7) + if count and count > 0 then duration = duration + count end + elseif effect == L["dyndebuffs"]["Gouge"] then + -- Improved Gouge: +.5s per talent + local _,_,_,_,count = GetTalentInfo(2,1) + if count and count > 0 then duration = duration + (count*.5) end + end + return duration + else + return 0 + end +end + +function libdebuff:GetMaxRank(effect) + local max = 0 + for id in pairs(L["debuffs"][effect]) do + if id > max then max = id end + end + return max +end + +function libdebuff:UpdateUnits() + TargetDebuffButton_Update() +end + +function libdebuff:AddPending(unit, unitlevel, effect, duration) + if not unit then return end + if not L["debuffs"][effect] then return end + + if duration > 0 and libdebuff.pending[3] ~= effect then + libdebuff.pending[1] = unit + libdebuff.pending[2] = unitlevel or 0 + libdebuff.pending[3] = effect + libdebuff.pending[4] = duration or libdebuff:GetDuration(effect) + end +end + +function libdebuff:RemovePending() + libdebuff.pending[1] = nil + libdebuff.pending[2] = nil + libdebuff.pending[3] = nil + libdebuff.pending[4] = nil +end + +function libdebuff:PersistPending(effect) + if not libdebuff.pending[3] then return end + if libdebuff.pending[3] == effect or ( effect == nil and libdebuff.pending[3] ) then + libdebuff:AddEffect(libdebuff.pending[1], libdebuff.pending[2], libdebuff.pending[3], libdebuff.pending[4]) + libdebuff:RemovePending() + end +end + +function libdebuff:RevertLastAction() + lastspell.start = lastspell.start_old + lastspell.start_old = nil + libdebuff:UpdateUnits() +end + +function libdebuff:AddEffect(unit, unitlevel, effect, duration) + if not unit or not effect then return end + unitlevel = unitlevel or 0 + if not libdebuff.objects[unit] then libdebuff.objects[unit] = {} end + if not libdebuff.objects[unit][unitlevel] then libdebuff.objects[unit][unitlevel] = {} end + if not libdebuff.objects[unit][unitlevel][effect] then libdebuff.objects[unit][unitlevel][effect] = {} end + + -- save current effect as lastspell + lastspell = libdebuff.objects[unit][unitlevel][effect] + + libdebuff.objects[unit][unitlevel][effect].effect = effect + libdebuff.objects[unit][unitlevel][effect].start_old = libdebuff.objects[unit][unitlevel][effect].start + libdebuff.objects[unit][unitlevel][effect].start = GetTime() + libdebuff.objects[unit][unitlevel][effect].duration = duration or libdebuff:GetDuration(effect) + + libdebuff:UpdateUnits() +end + +-- scan for debuff application +libdebuff:RegisterEvent("CHAT_MSG_SPELL_PERIODIC_HOSTILEPLAYER_DAMAGE") +libdebuff:RegisterEvent("CHAT_MSG_SPELL_PERIODIC_CREATURE_DAMAGE") +libdebuff:RegisterEvent("CHAT_MSG_SPELL_FAILED_LOCALPLAYER") +libdebuff:RegisterEvent("CHAT_MSG_SPELL_SELF_DAMAGE") +libdebuff:RegisterEvent("PLAYER_TARGET_CHANGED") +libdebuff:RegisterEvent("SPELLCAST_STOP") +libdebuff:RegisterEvent("UNIT_AURA") + +-- register seal handler +if class == "PALADIN" then + libdebuff:RegisterEvent("CHAT_MSG_COMBAT_SELF_HITS") +end + +-- Remove Pending +libdebuff.rp = { SPELLIMMUNESELFOTHER, IMMUNEDAMAGECLASSSELFOTHER, + SPELLMISSSELFOTHER, SPELLRESISTSELFOTHER, SPELLEVADEDSELFOTHER, + SPELLDODGEDSELFOTHER, SPELLDEFLECTEDSELFOTHER, SPELLREFLECTSELFOTHER, + SPELLPARRIEDSELFOTHER, SPELLLOGABSORBSELFOTHER } + +libdebuff.objects = {} +libdebuff.pending = {} + +-- Gather Data by Events +libdebuff:SetScript("OnEvent", function() + -- paladin seal refresh + if event == "CHAT_MSG_COMBAT_SELF_HITS" then + local hit = cmatch(arg1, COMBATHITSELFOTHER) + local crit = cmatch(arg1, COMBATHITCRITSELFOTHER) + if hit or crit then + for seal in L["judgements"] do + local name = UnitName("target") + local level = UnitLevel("target") + if name and libdebuff.objects[name] then + if level and libdebuff.objects[name][level] and libdebuff.objects[name][level][seal] then + libdebuff:AddEffect(name, level, seal) + elseif libdebuff.objects[name][0] and libdebuff.objects[name][0][seal] then + libdebuff:AddEffect(name, 0, seal) + end + end + end + end + + -- Add Combat Log + elseif event == "CHAT_MSG_SPELL_PERIODIC_HOSTILEPLAYER_DAMAGE" or event == "CHAT_MSG_SPELL_PERIODIC_CREATURE_DAMAGE" then + local unit, effect = cmatch(arg1, AURAADDEDOTHERHARMFUL) + if unit and effect then + local unitlevel = UnitName("target") == unit and UnitLevel("target") or 0 + if not libdebuff.objects[unit] or not libdebuff.objects[unit][unitlevel] or not libdebuff.objects[unit][unitlevel][effect] then + libdebuff:AddEffect(unit, unitlevel, effect) + end + end + + -- Add Missing Buffs by Iteration + elseif ( event == "UNIT_AURA" and arg1 == "target" ) or event == "PLAYER_TARGET_CHANGED" then + for i=1, 16 do + local effect, rank, texture, stacks, dtype, duration, timeleft = libdebuff:UnitDebuff("target", i) + + -- abort when no further debuff was found + if not texture then return end + + if texture and effect and effect ~= "" then + -- don't overwrite existing timers + local unitlevel = UnitLevel("target") or 0 + local unit = UnitName("target") + if not libdebuff.objects[unit] or not libdebuff.objects[unit][unitlevel] or not libdebuff.objects[unit][unitlevel][effect] then + libdebuff:AddEffect(unit, unitlevel, effect) + end + end + end + + -- Update Pending Spells + elseif event == "CHAT_MSG_SPELL_FAILED_LOCALPLAYER" or event == "CHAT_MSG_SPELL_SELF_DAMAGE" then + -- Remove pending spell + for _, msg in pairs(libdebuff.rp) do + local effect = cmatch(arg1, msg) + if effect and libdebuff.pending[3] == effect then + -- instant removal of the pending spell + libdebuff:RemovePending() + return + elseif effect and lastspell and lastspell.start_old and lastspell.effect == effect then + -- late removal of debuffs (e.g hunter arrows as they hit late) + libdebuff:RevertLastAction() + return + end + end + elseif event == "SPELLCAST_STOP" then + QueueFunction(libdebuff.PersistPending) + end +end) + +-- Gather Data by User Actions +hooksecurefunc("CastSpell", function(id, bookType) + local effect = GetSpellName(id, bookType) + local _, rank = libspell.GetSpellInfo(id, bookType) + local duration = libdebuff:GetDuration(effect, rank) + libdebuff:AddPending(UnitName("target"), UnitLevel("target"), effect, duration) +end, true) + +hooksecurefunc("CastSpellByName", function(effect, target) + local _, rank = libspell.GetSpellInfo(effect) + local duration = libdebuff:GetDuration(effect, rank) + libdebuff:AddPending(UnitName("target"), UnitLevel("target"), effect, duration) +end, true) + +hooksecurefunc("UseAction", function(slot, target, button) + if GetActionText(slot) or not IsCurrentAction(slot) then return end + scanner:SetAction(slot) + local effect, rank = scanner:Line(1) + local duration = libdebuff:GetDuration(effect, rank) + libdebuff:AddPending(UnitName("target"), UnitLevel("target"), effect, duration) +end, true) + +function libdebuff:UnitDebuff(unit, id) + local unitname = UnitName(unit) + local unitlevel = UnitLevel(unit) + local texture, stacks, dtype = UnitDebuff(unit, id) + local duration, timeleft = nil, -1 + local rank = nil -- no backport + local effect + + if texture then + scanner:SetUnitDebuff(unit, id) + effect = scanner:Line(1) or "" + end + + if libdebuff.objects[unitname] and libdebuff.objects[unitname][unitlevel] and libdebuff.objects[unitname][unitlevel][effect] then + -- clean up cache + if libdebuff.objects[unitname][unitlevel][effect].duration and libdebuff.objects[unitname][unitlevel][effect].duration + libdebuff.objects[unitname][unitlevel][effect].start < GetTime() then + libdebuff.objects[unitname][unitlevel][effect] = nil + else + duration = libdebuff.objects[unitname][unitlevel][effect].duration + timeleft = duration + libdebuff.objects[unitname][unitlevel][effect].start - GetTime() + end + + -- no level data + elseif libdebuff.objects[unitname] and libdebuff.objects[unitname][0] and libdebuff.objects[unitname][0][effect] then + -- clean up cache + if libdebuff.objects[unitname][0][effect].duration and libdebuff.objects[unitname][0][effect].duration + libdebuff.objects[unitname][0][effect].start < GetTime() then + libdebuff.objects[unitname][0][effect] = nil + else + duration = libdebuff.objects[unitname][0][effect].duration + timeleft = duration + libdebuff.objects[unitname][0][effect].start - GetTime() + end + end + + return effect, rank, texture, stacks, dtype, duration, timeleft +end + +ShaguTweaks.libdebuff = libdebuff diff --git a/libs/libhealth.lua b/libs/libhealth.lua new file mode 100644 index 0000000..83f2bcd --- /dev/null +++ b/libs/libhealth.lua @@ -0,0 +1,109 @@ +local _G = _G or getfenv(0) + +local L = ShaguTweaks.L +local GetExpansion = ShaguTweaks.GetExpansion + +local mobdb = {} +local target, dmg, perc, diff = nil, 0, 0, 0, 0 +local UnitHealth, UnitHealthMax +local libhealth = CreateFrame("Frame") +libhealth.enabled = true +libhealth.reqhit = 4 +libhealth.reqdmg = 5 +libhealth:RegisterEvent("UNIT_HEALTH") +libhealth:RegisterEvent("UNIT_COMBAT") +libhealth:RegisterEvent("PLAYER_TARGET_CHANGED") +libhealth:RegisterEvent("PLAYER_ENTERING_WORLD") +libhealth:SetScript("OnEvent", function() + if event == "PLAYER_ENTERING_WORLD" then + -- create initial health cache tables + ShaguTweaks_cache = ShaguTweaks_cache or {} + ShaguTweaks_cache["libhealth"] = ShaguTweaks_cache["libhealth"] or {} + mobdb = ShaguTweaks_cache["libhealth"] + + -- load enable state and set functions + libhealth.enabled = true + libhealth.reqhit = 2 + libhealth.reqdmg = 10 + end + + -- return as we're not supposed to be here + if not libhealth.enabled then this:UnregisterAllEvents() return end + + -- try to estimate mob health based on combat and health events + if event == "PLAYER_TARGET_CHANGED" then + dmg, perc = 0, _G.UnitHealth("target") + if UnitName("target") and UnitLevel("target") and _G.UnitHealthMax("target") == 100 then + target = string.format("%s:%d",UnitName("target"), UnitLevel("target")) + else + target = nil + end + elseif target and event == "UNIT_COMBAT" and arg1 == "target" then + if arg2 and arg2 == "HEAL" then return end + dmg = dmg + arg4 + elseif target and event == "UNIT_HEALTH" and arg1 == "target" then + diff = perc-_G.UnitHealth("target") + if dmg > 0 and diff > 0 then + mobdb[target] = mobdb[target] or {} + if not mobdb[target][2] or diff > mobdb[target][2] then + mobdb[target][1] = ceil(dmg / diff * 100) + mobdb[target][2] = diff + mobdb[target][3] = mobdb[target][3] and mobdb[target][3] + 1 or 1 + end + end + end +end) + +-- core function to query the generated database +local unit, level, cur, max, dbstring +local function GetUnitHealth(self, unitstr) + unit = UnitName(unitstr) + level = UnitLevel(unitstr) + cur = _G.UnitHealth(unitstr) + max = _G.UnitHealthMax(unitstr) + + -- return raw values if another addon is replacing global API calls + if cur > 100 or max > 100 or max < 100 then + return cur, max, true + end + + -- return raw values if no unit data could be found (unlikely but happened...) + if not unit or not level then + return cur, max + end + + -- query the database for known values + dbstring = string.format("%s:%s", unit, level) + if mobdb[dbstring] and mobdb[dbstring][1] and mobdb[dbstring][2] > libhealth.reqdmg and (not mobdb[dbstring][3] or mobdb[dbstring][3] > libhealth.reqhit) then + return ceil(mobdb[dbstring][1]/100*cur), mobdb[dbstring][1], true + end + + -- return raw values as fallback + return cur, max +end + +local function GetUnitHealthByName(self, unit, level, cur, max) + -- return raw values if another addon is replacing global API calls + if cur > 100 or max > 100 or max < 100 then + return cur, max, true + end + + -- return raw values if no unit data could be found (unlikely but happened...) + if not unit or not level then + return cur, max + end + + -- query the database for known values + dbstring = string.format("%s:%s", unit, level) + if mobdb[dbstring] and mobdb[dbstring][1] and mobdb[dbstring][2] > libhealth.reqdmg and (not mobdb[dbstring][3] or mobdb[dbstring][3] > libhealth.reqhit) then + return ceil(mobdb[dbstring][1]/100*cur), mobdb[dbstring][1], true + end + + -- return raw values as fallback + return cur, max +end + +-- add api calls to global tree +ShaguTweaks.libhealth = libhealth +ShaguTweaks.libhealth.GetUnitHealth = GetUnitHealth +ShaguTweaks.libhealth.GetUnitHealthByName = GetUnitHealthByName diff --git a/libs/libnameplate.lua b/libs/libnameplate.lua new file mode 100644 index 0000000..67fbf47 --- /dev/null +++ b/libs/libnameplate.lua @@ -0,0 +1,71 @@ +local _G = _G or getfenv(0) +local GetExpansion = ShaguTweaks.GetExpansion +local GetUnitData = ShaguTweaks.GetUnitData + +local NAMEPLATE_OBJECTORDER = { "border", "glow", "name", "level", "levelicon", "raidicon" } +if GetExpansion() == "tbc" then + NAMEPLATE_OBJECTORDER = { "border", "castborder", "casticon", "glow", "name", "level", "levelicon", "raidicon" } +end + +local function IsNamePlate(frame) + if frame:GetObjectType() ~= "Button" then return nil end + regions = frame:GetRegions() + + if not regions then return nil end + if not regions.GetObjectType then return nil end + if not regions.GetTexture then return nil end + + if regions:GetObjectType() ~= "Texture" then return nil end + return regions:GetTexture() == "Interface\\Tooltips\\Nameplate-Border" or nil +end + +local registry = {} +local initialized = 0 +local parentcount, childs, plate +ShaguTweaks.libnameplate = CreateFrame("Frame", nil, UIParent) +ShaguTweaks.libnameplate.OnInit = {} +ShaguTweaks.libnameplate.OnShow = {} +ShaguTweaks.libnameplate.OnUpdate = {} +ShaguTweaks.libnameplate:SetScript("OnUpdate", function() + parentcount = WorldFrame:GetNumChildren() + if initialized < parentcount then + childs = { WorldFrame:GetChildren() } + for i = initialized + 1, parentcount do + plate = childs[i] + + if IsNamePlate(plate) and not registry[plate] then + plate.healthbar = plate:GetChildren() + for i, object in pairs({plate:GetRegions()}) do + plate[NAMEPLATE_OBJECTORDER[i]] = object + end + + -- run OnInit functions + for id, func in pairs(ShaguTweaks.libnameplate.OnInit) do + func(plate) + end + + -- register OnUpdate functions + local oldUpdate = plate:GetScript("OnUpdate") + plate:SetScript("OnUpdate", function() + if oldUpdate then oldUpdate() end + for id, func in pairs(ShaguTweaks.libnameplate.OnUpdate) do + func() + end + end) + + -- register OnShow functions + local oldShow = plate:GetScript("OnShow") + plate:SetScript("OnShow", function() + if oldShow then oldUpdate() end + for id, func in pairs(ShaguTweaks.libnameplate.OnShow) do + func() + end + end) + + registry[plate] = plate + end + end + + initialized = parentcount + end +end) diff --git a/libs/libpredict.lua b/libs/libpredict.lua new file mode 100644 index 0000000..17ea202 --- /dev/null +++ b/libs/libpredict.lua @@ -0,0 +1,469 @@ +local _G = _G or getfenv(0) + +local L = ShaguTweaks.L +local GetExpansion = ShaguTweaks.GetExpansion +local libtipscan = ShaguTweaks.libtipscan +local libspell = ShaguTweaks.libspell +local hooksecurefunc = ShaguTweaks.hooksecurefunc +local cmatch = ShaguTweaks.cmatch + +local senttarget +local heals, ress, events = {}, {}, {} + +local PRAYER_OF_HEALING +do -- Prayer of Healing + local locales = { + ["deDE"] = "Gebet der Heilung", + ["enUS"] = "Prayer of Healing", + ["esES"] = "Rezo de curación", + ["frFR"] = "Prière de soins", + ["koKR"] = "치유의 기원", + ["ruRU"] = "Молитва исцеления", + ["zhCN"] = "治疗祷言", + } + + PRAYER_OF_HEALING = locales[GetLocale()] or locales["enUS"] +end + +local libpredict = CreateFrame("Frame") +libpredict:RegisterEvent("UNIT_HEALTH") +libpredict:RegisterEvent("CHAT_MSG_ADDON") +libpredict:RegisterEvent("PLAYER_TARGET_CHANGED") +libpredict:SetScript("OnEvent", function() + if event == "CHAT_MSG_ADDON" and (arg1 == "HealComm" or arg1 == "CTRA") then + this:ParseChatMessage(arg4, arg2, arg1) + elseif event == "UNIT_HEALTH" then + local name = UnitName(arg1) + if ress[name] and not UnitIsDeadOrGhost(arg1) then + ress[UnitName(arg1)] = nil + end + end +end) + +libpredict:SetScript("OnUpdate", function() + -- update on timeout events + for timestamp, targets in pairs(events) do + if GetTime() >= timestamp then + events[timestamp] = nil + end + end +end) + +function libpredict:ParseComm(sender, msg) + local msgtype, target, heal, time + + if msg == "Healstop" or msg == "GrpHealstop" then + msgtype = "Stop" + elseif msg == "Resurrection/stop/" then + msgtype = "RessStop" + elseif msg then + local msgobj = {strsplit("/", msg)} + + if msgobj and msgobj[1] and msgobj[2] then + -- legacy healcomm object + if msgobj[1] == "GrpHealdelay" or msgobj[1] == "Healdelay" then + msgtype, time = "Delay", msgobj[2] + end + + if msgobj[1] and msgobj[1] == "Resurrection" and msgobj[2] then + msgtype, target = "Ress", msgobj[2] + end + + if msgobj[1] == "Heal" and msgobj[2] then + msgtype, target, heal, time = "Heal", msgobj[2], msgobj[3], msgobj[4] + end + + if msgobj[1] == "GrpHeal" and msgobj[2] then + msgtype, target, heal, time = "Heal", {}, msgobj[2], msgobj[3] + for i=4,8 do + if msgobj[i] then table.insert(target, msgobj[i]) end + end + end + elseif select and UnitCastingInfo then + -- latest healcomm + msgtype = tonumber(string.sub(msg, 1, 3)) + if not msgtype then return end + + if msgtype == 0 then + msgtype = "Heal" + heal = tonumber(string.sub(msg, 4, 8)) + target = string.sub(msg,9, -1) + + local starttime = select(5, UnitCastingInfo(sender)) + local endtime = select(6, UnitCastingInfo(sender)) + if not starttime or not endtime then return end + time = endtime - starttime + elseif msgtype == 1 then + msgtype = "Stop" + elseif msgtype == 2 then + msgtype = "Heal" + heal = tonumber(string.sub(msg,4, 8)) + target = {strsplit(":", string.sub(msg,9, -1))} + local starttime = select(5, UnitCastingInfo(sender)) + local endtime = select(6, UnitCastingInfo(sender)) + if not starttime or not endtime then return end + time = endtime - starttime + end + end + end + + return msgtype, target, heal, time +end + +function libpredict:ParseChatMessage(sender, msg, comm) + local msgtype, target, heal, time + + if comm == "HealComm" then + msgtype, target, heal, time = libpredict:ParseComm(sender, msg) + elseif comm == "CTRA" then + local _, _, cmd, ctratarget = string.find(msg, "(%a+)%s?([^#]*)") + if cmd and ctratarget and cmd == "RES" and ctratarget ~= "" and ctratarget ~= UNKNOWN then + msgtype = "Ress" + target = ctratarget + end + end + + if msgtype == "Stop" and sender then + libpredict:HealStop(sender) + return + elseif ( msg == "RessStop" or msg == "RESNO" ) and sender then + libpredict:RessStop(sender) + return + elseif msgtype == "Delay" and time then + libpredict:HealDelay(sender, time) + elseif msgtype == "Heal" and target and heal and time then + if type(target) == "table" then + for _, name in pairs(target) do + libpredict:Heal(sender, name, heal, time) + end + else + libpredict:Heal(sender, target, heal, time) + end + elseif msgtype == "Ress" then + libpredict:Ress(sender, target) + end +end + +function libpredict:AddEvent(time, target) + events[time] = events[time] or {} + table.insert(events[time], target) +end + +function libpredict:Heal(sender, target, amount, duration) + if not sender or not target or not amount or not duration then + return + end + + local timeout = duration/1000 + GetTime() + heals[target] = heals[target] or {} + heals[target][sender] = { amount, timeout } + libpredict:AddEvent(timeout, target) +end + +function libpredict:HealStop(sender) + for ttarget, t in pairs(heals) do + for tsender in pairs(heals[ttarget]) do + if sender == tsender then + heals[ttarget][tsender] = nil + end + end + end +end + +function libpredict:HealDelay(sender, delay) + local delay = delay/1000 + for target, t in pairs(heals) do + for tsender, amount in pairs(heals[target]) do + if sender == tsender then + amount[2] = amount[2] + delay + libpredict:AddEvent(amount[2], target) + end + end + end +end + +function libpredict:Ress(sender, target) + ress[target] = ress[target] or {} + ress[target][sender] = true +end + +function libpredict:RessStop(sender) + for ttarget, t in pairs(ress) do + for tsender in pairs(ress[ttarget]) do + if sender == tsender then + ress[ttarget][tsender] = nil + end + end + end +end + +function libpredict:UnitGetIncomingHeals(unit) + if not unit or not UnitName(unit) then return 0 end + if UnitIsDeadOrGhost(unit) then return 0 end + local name = UnitName(unit) + + local sumheal = 0 + if not heals[name] then + return sumheal + else + for sender, amount in pairs(heals[name]) do + if amount[2] <= GetTime() then + heals[name][sender] = nil + else + sumheal = sumheal + amount[1] + end + end + end + return sumheal +end + +function libpredict:UnitHasIncomingResurrection(unit) + if not unit or not UnitName(unit) then return nil end + local name = UnitName(unit) + + if not ress[name] then + return nil + else + for sender, val in pairs(ress[name]) do + if val == true then + return val + end + end + end + return nil +end + +local spell_queue = { "DUMMY", "DUMMYRank 9", "TARGET" } +local realm = GetRealmName() +local player = UnitName("player") +local cache, gear_string = {}, "" +local resetcache = CreateFrame("Frame") +resetcache:RegisterEvent("PLAYER_ENTERING_WORLD") +resetcache:RegisterEvent("SKILL_LINES_CHANGED") +resetcache:RegisterEvent("UNIT_INVENTORY_CHANGED") +resetcache:SetScript("OnEvent", function() + if event == "PLAYER_ENTERING_WORLD" then + -- load and initialize previous caches of spell amounts + ShaguTweaks_cache = ShaguTweaks_cache or {} + ShaguTweaks_cache["prediction"] = ShaguTweaks_cache["prediction"] or {} + ShaguTweaks_cache["prediction"][realm] = ShaguTweaks_cache["prediction"][realm] or {} + ShaguTweaks_cache["prediction"][realm][player] = ShaguTweaks_cache["prediction"][realm][player] or {} + ShaguTweaks_cache["prediction"][realm][player]["heals"] = ShaguTweaks_cache["prediction"][realm][player]["heals"] or {} + cache = ShaguTweaks_cache["prediction"][realm][player]["heals"] + return + end + + if event == "UNIT_INVENTORY_CHANGED" then + -- skip non-player events + if arg1 and arg1 ~= "player" then return end + + local gear = "" + for id = 1, 18 do + gear = gear .. (GetInventoryItemLink("player",id) or "") + end + + -- abort when inventory didn't change + if gear == gear_string then return end + gear_string = gear + end + + -- flag all cached heals for renewal + for k in pairs(cache) do + if type(cache[k]) == "number" or type(cache[k]) == "string" then + -- migrate old data + local oldval = cache[k] + cache[k] = { [1] = oldval } + end + + -- flag for reset + cache[k][2] = true + end +end) + +local function UpdateCache(spell, heal, crit) + if not spell or not heal then return end + local heal = tonumber(heal) + + if not cache[spell] or cache[spell][2] then + -- skills or equipment changed, save whatever is detected + cache[spell] = cache[spell] or {} + cache[spell][1] = crit and heal*2/3 or heal + cache[spell][2] = crit + elseif not crit and cache[spell][1] < heal then + -- safe the best heal we can get + cache[spell][1] = heal + cache[spell][2] = nil + end +end + +-- Gather Data by User Actions +hooksecurefunc("CastSpell", function(id, bookType) + if not libpredict.sender.enabled then return end + local effect, rank = libspell.GetSpellInfo(id, bookType) + if not effect then return end + spell_queue[1] = effect + spell_queue[2] = effect.. ( rank or "" ) + spell_queue[3] = UnitName("target") and UnitCanAssist("player", "target") and UnitName("target") or UnitName("player") +end, true) + +hooksecurefunc("CastSpellByName", function(effect, target) + if not libpredict.sender.enabled then return end + local effect, rank = libspell.GetSpellInfo(effect) + if not effect then return end + spell_queue[1] = effect + spell_queue[2] = effect.. ( rank or "" ) + spell_queue[3] = UnitName("target") and UnitCanAssist("player", "target") and UnitName("target") or UnitName("player") +end, true) + +local scanner = libtipscan:GetScanner("prediction") +hooksecurefunc("UseAction", function(slot, target, selfcast) + if not libpredict.sender.enabled then return end + if GetActionText(slot) or not IsCurrentAction(slot) then return end + scanner:SetAction(slot) + local effect, rank = scanner:Line(1) + if not effect then return end + spell_queue[1] = effect + spell_queue[2] = effect.. ( rank or "" ) + spell_queue[3] = selfcast and UnitName("player") or UnitName("target") and UnitCanAssist("player", "target") and UnitName("target") or UnitName("player") +end, true) + +libpredict.sender = CreateFrame("Frame", "ShaguTweaksPredictionSender", UIParent) +libpredict.sender.enabled = true +libpredict.sender.SendHealCommMsg = function(self, msg) + SendAddonMessage("HealComm", msg, "RAID") + SendAddonMessage("HealComm", msg, "BATTLEGROUND") +end +libpredict.sender.SendResCommMsg = function(self, msg) + SendAddonMessage("CTRA", msg, "RAID") + SendAddonMessage("CTRA", msg, "BATTLEGROUND") +end + +-- tbc +libpredict.sender:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED") +libpredict.sender:RegisterEvent("UNIT_SPELLCAST_START") +libpredict.sender:RegisterEvent("UNIT_SPELLCAST_STOP") +libpredict.sender:RegisterEvent("UNIT_SPELLCAST_FAILED") +libpredict.sender:RegisterEvent("UNIT_SPELLCAST_INTERRUPTED") +libpredict.sender:RegisterEvent("UNIT_SPELLCAST_SENT") + +-- vanilla +libpredict.sender:RegisterEvent("CHAT_MSG_SPELL_SELF_BUFF") +libpredict.sender:RegisterEvent("SPELLCAST_START") +libpredict.sender:RegisterEvent("SPELLCAST_STOP") +libpredict.sender:RegisterEvent("SPELLCAST_FAILED") +libpredict.sender:RegisterEvent("SPELLCAST_INTERRUPTED") +libpredict.sender:RegisterEvent("SPELLCAST_DELAYED") + +-- force cache updates +libpredict.sender:RegisterEvent("UNIT_INVENTORY_CHANGED") +libpredict.sender:RegisterEvent("SKILL_LINES_CHANGED") + +libpredict.sender:SetScript("OnEvent", function() + if event == "CHAT_MSG_SPELL_SELF_BUFF" then -- vanilla + local spell, _, heal = cmatch(arg1, HEALEDSELFOTHER) -- "Your %s heals %s for %d." + if spell and heal then + if spell == spell_queue[1] then UpdateCache(spell_queue[2], heal) end + return + end + + local spell, heal = cmatch(arg1, HEALEDSELFSELF) -- "Your %s heals you for %d." + if spell and heal then + if spell == spell_queue[1] then UpdateCache(spell_queue[2], heal) end + return + end + + local spell, heal = cmatch(arg1, HEALEDCRITSELFOTHER) -- "Your %s critically heals %s for %d." + if spell and heal then + if spell == spell_queue[1] then UpdateCache(spell_queue[2], heal, true) end + return + end + + local spell, _, heal = cmatch(arg1, HEALEDCRITSELFSELF) -- "Your %s critically heals you for %d." + if spell and heal then + if spell == spell_queue[1] then UpdateCache(spell_queue[2], heal, true) end + return + end + elseif event == "COMBAT_LOG_EVENT_UNFILTERED" and arg2 == "SPELL_HEAL" and arg4 == player then -- tbc + local spell, heal, crit = arg10, arg12, arg13 + if spell and heal and crit then + if spell == spell_queue[1] then UpdateCache(spell_queue[2], heal, true) end + elseif spell and heal then + if spell == spell_queue[1] then UpdateCache(spell_queue[2], heal) end + end + elseif event == "UNIT_SPELLCAST_SENT" and arg4 then -- fix tbc mouseover macros + senttarget = arg4 + elseif strfind(event, "SPELLCAST_START", 1) then + local spell, time = arg1, arg2 + + if strfind(event, "UNIT_", 1) then -- tbc + if arg1 ~= "player" then return end + local spellname, _, _, _, starttime, endtime = UnitCastingInfo("player") + spell, time = spellname, endtime - starttime + end + + if spell_queue[1] == spell and cache[spell_queue[2]] then + local sender = player + local target = senttarget or spell_queue[3] + local amount = cache[spell_queue[2]][1] + local casttime = time + + if spell == PRAYER_OF_HEALING then + target = sender + + for i=1,4 do + if CheckInteractDistance("party"..i, 4) then + libpredict:Heal(player, UnitName("party"..i), amount, casttime) + if GetExpansion() == "vanilla" then -- vanilla + libpredict.sender:SendHealCommMsg("Heal/" .. UnitName("party"..i) .. "/" .. amount .. "/" .. casttime .. "/") + else -- tbc + libpredict.sender:SendHealCommMsg(string.format("002%05d%s", math.min(amount, 99999), UnitName("party"..i))) + end + libpredict.sender.healing = true + end + end + end + + libpredict:Heal(player, target, amount, casttime) + if GetExpansion() == "vanilla" then -- vanilla + libpredict.sender:SendHealCommMsg("Heal/" .. target .. "/" .. amount .. "/" .. casttime .. "/") + else -- tbc + libpredict.sender:SendHealCommMsg(string.format("002%05d%s", math.min(amount, 99999), target)) + end + libpredict.sender.healing = true + + elseif spell_queue[1] == spell and L["resurrections"][spell] then + local target = senttarget or spell_queue[3] + libpredict:Ress(player, target) + libpredict.sender:SendHealCommMsg("Resurrection/" .. target .. "/start/") + libpredict.sender:SendResCommMsg("RES " .. target) + libpredict.sender.resurrecting = true + end + elseif strfind(event, "SPELLCAST_FAILED", 1) or strfind(event, "SPELLCAST_INTERRUPTED", 1) then + if strfind(event, "UNIT_", 1) and arg1 ~= "player" then return end + if libpredict.sender.healing then + libpredict:HealStop(player) + if GetExpansion() == "vanilla" then -- vanilla + libpredict.sender:SendHealCommMsg("HealStop") + else -- tbc + libpredict.sender:SendHealCommMsg("001F") + end + libpredict.sender.healing = nil + elseif libpredict.sender.resurrecting then + local target = senttarget or spell_queue[3] + libpredict:RessStop(player) + libpredict.sender:SendHealCommMsg("Resurrection/stop/") + libpredict.sender:SendResCommMsg("RESNO " .. target) + libpredict.sender.resurrecting = nil + end + elseif event == "SPELLCAST_DELAYED" then + if libpredict.sender.healing then + libpredict:HealDelay(player, arg1) + libpredict.sender:SendHealCommMsg("Healdelay/" .. arg1 .. "/") + end + elseif strfind(event, "SPELLCAST_STOP", 1) then + if strfind(event, "UNIT_", 1) and arg1 ~= "player" then return end + libpredict:HealStop(player) + end +end) + +ShaguTweaks.libpredict = libpredict diff --git a/libs/libspell.lua b/libs/libspell.lua new file mode 100644 index 0000000..197db98 --- /dev/null +++ b/libs/libspell.lua @@ -0,0 +1,130 @@ +local _G = _G or getfenv(0) + +local GetExpansion = ShaguTweaks.GetExpansion +local libtipscan = ShaguTweaks.libtipscan +local hooksecurefunc = ShaguTweaks.hooksecurefunc + +local scanner = libtipscan:GetScanner("libspell") +local libspell = {} + +-- [ GetSpellMaxRank ] +-- Returns the maximum rank of a players spell. +-- 'name' [string] spellname to query +-- return: [string],[number] maximum rank in characters and the number +-- e.g "Rank 1" and "1" +local spellmaxrank = {} +function libspell.GetSpellMaxRank(name) + local cache = spellmaxrank[name] + if cache then return cache[1], cache[2] end + + local rank = { 0, nil} + for i = 1, GetNumSpellTabs() do + local _, _, offset, num = GetSpellTabInfo(i) + local bookType = BOOKTYPE_SPELL + for id = offset + 1, offset + num do + local spellName, spellRank = GetSpellName(id, bookType) + if spellName == name then + if not rank[2] then rank[2] = spellRank end + + local _, _, numRank = string.find(spellRank, " (%d+)$") + if numRank and tonumber(numRank) > rank[1] then + rank = { tonumber(numRank), spellRank} + end + end + end + end + + spellmaxrank[name] = { rank[2], rank[1] } + return rank[2], rank[1] +end + +-- [ GetSpellIndex ] +-- Returns the spellbook index and bookid of the given spell. +-- 'name' [string] spellname to query +-- 'rank' [string] rank to query (optional) +-- return: [number],[string] spell index and spellbook id +local spellindex = {} +function libspell.GetSpellIndex(name, rank) + local cache = spellindex[name..(rank or "")] + if cache then return cache[1], cache[2] end + + if not rank then rank = libspell.GetSpellMaxRank(name) end + + for i = 1, GetNumSpellTabs() do + local _, _, offset, num = GetSpellTabInfo(i) + local bookType = BOOKTYPE_SPELL + for id = offset + 1, offset + num do + local spellName, spellRank = GetSpellName(id, bookType) + if rank and rank == spellRank and name == spellName then + spellindex[name..rank] = { id, bookType } + return id, bookType + elseif not rank and name == spellName then + spellindex[name] = { id, bookType } + return id, bookType + end + end + end + spellindex[name..(rank or "")] = { nil } + return nil +end + +-- [ GetSpellInfo ] +-- Returns several information about a spell. +-- 'index' [string/number] Spellname or Index of a spell in the spellbook +-- 'bookType' [string] Type of spellbook (optional) +-- return: +-- [string] Name of the spell +-- [string] Secondary text associated with the spell +-- (e.g."Rank 5", "Racial", etc.) +-- [string] Path to an icon texture for the spell +-- [number] Casting time of the spell in milliseconds +-- [number] Minimum range from the target required to cast the spell +-- [number] Maximum range from the target at which you can cast the spell +local spellinfo = {} +function libspell.GetSpellInfo(index, bookType) + local cache = spellinfo[index] + if cache then return cache[1], cache[2], cache[3], cache[4], cache[5], cache[6] end + + local name, rank, id + local icon = "" + local castingTime = 0 + local minRange = 0 + local maxRange = 0 + + if type(index) == "string" then + local _, _, sname, srank = string.find(index, '(.+)%((.+)%)') + name = sname or index + rank = srank or libspell.GetSpellMaxRank(name) + id, bookType = libspell.GetSpellIndex(name, rank) + else + name, rank = GetSpellName(index, bookType) + id, bookType = libspell.GetSpellIndex(name, rank) + end + + if name and id then + icon = GetSpellTexture(id, bookType) + end + + if id then + scanner:SetSpell(id, bookType) + local _, sec = scanner:Find(gsub(SPELL_CAST_TIME_SEC, "%%.3g", "%(.+%)")) + local _, min = scanner:Find(gsub(SPELL_CAST_TIME_MIN, "%%.3g", "%(.+%)")) + local _, range = scanner:Find(gsub(SPELL_RANGE, "%%s", "%(.+%)")) + castingTime = (tonumber(sec) or tonumber(min) or 0) * 1000 + if range then + local _, _, min, max = string.find(range, "(.+)-(.+)") + if min and max then + minRange = tonumber(min) + maxRange = tonumber(max) + else + minRange = 0 + maxRange = tonumber(range) + end + end + end + + spellinfo[index] = { name, rank, icon, castingTime, minRange, maxRange } + return name, rank, icon, castingTime, minRange, maxRange +end + +ShaguTweaks.libspell = libspell diff --git a/libs/libtipscan.lua b/libs/libtipscan.lua new file mode 100644 index 0000000..944a319 --- /dev/null +++ b/libs/libtipscan.lua @@ -0,0 +1,170 @@ +local _G = _G or getfenv(0) + +local libtipscan = {} +local baseName = "ShaguTweaksTooltip" +local methods = { + "SetBagItem", "SetAction", "SetAuctionItem", "SetAuctionSellItem", "SetBuybackItem", + "SetCraftItem", "SetCraftSpell", "SetHyperlink", "SetInboxItem", "SetInventoryItem", + "SetLootItem", "SetLootRollItem", "SetMerchantItem", "SetPetAction", "SetPlayerBuff", + "SetQuestItem", "SetQuestLogItem", "SetQuestRewardSpell", "SetSendMailItem", "SetShapeshift", + "SetSpell", "SetTalent", "SetTrackingSpell", "SetTradePlayerItem", "SetTradeSkillItem", "SetTradeTargetItem", + "SetTrainerService", "SetUnit", "SetUnitBuff", "SetUnitDebuff", +} +local extra_methods = { + "Find", "Line", "Text", "List", +} + +local getFontString = function(obj) + local name = obj:GetName() + local r, g, b, color, a + local text, segment + + for i=1, obj:NumLines() do + local left = _G[string.format("%sTextLeft%d",name,i)] + segment = left and left:IsVisible() and left:GetText() + segment = segment and segment ~= "" and segment or nil + if segment then + r, g, b, a = left:GetTextColor() + segment = rgbhex(r,g,b) .. segment .. "|r" + text = text and text .. "\n" .. segment or segment + end + end + return text +end + +local getText = function(obj) + local name = obj:GetName() + local text = {} + for i=1, obj:NumLines() do + local left, right = _G[string.format("%sTextLeft%d",name,i)], _G[string.format("%sTextRight%d",name,i)] + left = left and left:IsVisible() and left:GetText() + right = right and right:IsVisible() and right:GetText() + left = left and left ~= "" and left or nil + right = right and right ~= "" and right or nil + if left or right then + text[i] = {left, right} + end + end + return text +end + +local findText = function(obj, text, exact) + local name = obj:GetName() + for i=1, obj:NumLines() do + local left, right = _G[string.format("%sTextLeft%d",name,i)], _G[string.format("%sTextRight%d",name,i)] + left = left and left:IsVisible() and left:GetText() + right = right and right:IsVisible() and right:GetText() + if exact then + if (left and left == text) or (right and right == text) then + return i, text + end + else + if left then + local found,_,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10 = string.find(left, text) + if found then + return i, a1,a2,a3,a4,a5,a6,a7,a8,a9,a10 + end + end + if right then + local found,_,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10 = string.find(right, text) + if found then + return i, a1,a2,a3,a4,a5,a6,a7,a8,a9,a10 + end + end + end + end +end + +local lineText = function(obj, line) + local name = obj:GetName() + if line <= obj:NumLines() then + local left, right = _G[string.format("%sTextLeft%d",name,line)], _G[string.format("%sTextRight%d",name,line)] + left = left and left:IsVisible() and left:GetText() + right = right and right:IsVisible() and right:GetText() + + if left or right then + return left, right + end + end +end + +local findColor = function(obj, r,g,b) + local name = obj:GetName() + if type(r) == "table" then + r,g,b = r.r or r[1], r.g or r[2], r.b or r[3] + end + for i=1, obj:NumLines() do + local tr, tg, tb + local left, right = _G[string.format("%sTextLeft%d",name,i)], _G[string.format("%sTextRight%d",name,i)] + if left and left:IsVisible() then + tr, tg, tb = left:GetTextColor() + tr, tg, tb = round(tr,1), round(tg,1), round(tb,1) + end + if tr and (tr == r and tg == g and tb == b) then + return i + end + if right and right:IsVisible() then + tr, tg, tb = right:GetTextColor() + tr, tg, tb = round(tr,1), round(tg,1), round(tb,1) + end + if tr and (tr == r and tg == g and tb == b) then + return i + end + end +end + +libtipscan._registry = setmetatable({},{__index = function(t,k) + local v = CreateFrame("GameTooltip", string.format("%s%s",baseName,k), nil, "GameTooltipTemplate") + v:SetOwner(WorldFrame,"ANCHOR_NONE") + v:SetScript("OnHide", function () + this:SetOwner(WorldFrame,"ANCHOR_NONE") + end) + function v:Text() + return getText(self) + end + function v:FontString() + return getFontString(self) + end + function v:Find(text, exact) + return findText(self, text, exact) + end + function v:Color(r,g,b) + return findColor(self,r,g,b) + end + function v:Line(line) + return lineText(self, line) + end + for _,method in ipairs(methods) do + local method = method + local old = v[method] + v[method] = function(v, a1,a2,a3,a4,a5,a6,a7,a8,a9,a10) + v:ClearLines() + return old(v, a1,a2,a3,a4,a5,a6,a7,a8,a9,a10) + end + end + function v:List() + table.sort(methods) + for _,method in ipairs(methods) do + print(method) + end + for _,method in ipairs(extra_methods) do + print(method) + end + end + rawset(t,k,v) + return v +end}) + +function libtipscan:GetScanner(type) + local scanner = self._registry[type] + scanner:ClearLines() + return scanner +end + +function libtipscan:List() + for name, scanner in pairs(self._registry) do + print(name) + end +end + +ShaguTweaks.libtipscan = libtipscan diff --git a/libs/libunitscan.lua b/libs/libunitscan.lua new file mode 100644 index 0000000..4c53776 --- /dev/null +++ b/libs/libunitscan.lua @@ -0,0 +1,145 @@ +local _G = _G or getfenv(0) + +local GetExpansion = ShaguTweaks.GetExpansion +local L = ShaguTweaks.L + +local units = { players = {}, mobs = {} } +local queue = { } + +local libunitscan = CreateFrame("Frame", "ShaguTweaksUnitScan", UIParent) + +function ShaguTweaks.GetUnitData(name, active) + if units["players"][name] then + local ret = units["players"][name] + return ret.class, ret.level, ret.elite, true + elseif units["mobs"][name] then + local ret = units["mobs"][name] + return ret.class, ret.level, ret.elite, nil + elseif active then + queue[name] = true + libunitscan:Show() + end +end + +local function AddData(db, name, class, level, elite) + if not name then return end + units[db][name] = units[db][name] or {} + units[db][name].class = class or units[db][name].class + units[db][name].level = level or units[db][name].level + units[db][name].elite = elite or units[db][name].elite + queue[name] = nil +end + +libunitscan:RegisterEvent("PLAYER_ENTERING_WORLD") +libunitscan:RegisterEvent("FRIENDLIST_UPDATE") +libunitscan:RegisterEvent("GUILD_ROSTER_UPDATE") +libunitscan:RegisterEvent("RAID_ROSTER_UPDATE") +libunitscan:RegisterEvent("PARTY_MEMBERS_CHANGED") +libunitscan:RegisterEvent("PLAYER_TARGET_CHANGED") +libunitscan:RegisterEvent("WHO_LIST_UPDATE") +libunitscan:RegisterEvent("CHAT_MSG_SYSTEM") +libunitscan:RegisterEvent("UPDATE_MOUSEOVER_UNIT") +libunitscan:SetScript("OnEvent", function() + if event == "PLAYER_ENTERING_WORLD" then + -- load database + ShaguTweaks_cache = ShaguTweaks_cache or {} + ShaguTweaks_cache["players"] = ShaguTweaks_cache["players"] or {} + units.players = ShaguTweaks_cache["players"] + + -- update own character details + local name = UnitName("player") + local _, class = UnitClass("player") + local level = UnitLevel("player") + AddData("players", name, class, level) + + elseif event == "FRIENDLIST_UPDATE" then + local name, class, level + for i = 1, GetNumFriends() do + name, level, class = GetFriendInfo(i) + class = L["class"][class] or nil + -- friendlist updates due to friend going off-line return level 0, let's not overwrite good older values + level = level > 0 and level or nil + AddData("players", name, class, level) + end + + elseif event == "GUILD_ROSTER_UPDATE" then + local name, class, level, _ + for i = 1, GetNumGuildMembers() do + name, _, _, level, class = GetGuildRosterInfo(i) + class = L["class"][class] or nil + AddData("players", name, class, level) + end + + elseif event == "RAID_ROSTER_UPDATE" then + local name, class, SubGroup, level, _ + for i = 1, GetNumRaidMembers() do + name, _, SubGroup, level, class = GetRaidRosterInfo(i) + class = L["class"][class] or nil + AddData("players", name, class, level) + end + + elseif event == "PARTY_MEMBERS_CHANGED" then + local name, class, level, unit, _ + for i = 1, GetNumPartyMembers() do + unit = "party" .. i + _, class = UnitClass(unit) + name = UnitName(unit) + level = UnitLevel(unit) + AddData("players", name, class, level) + end + + elseif event == "WHO_LIST_UPDATE" or event == "CHAT_MSG_SYSTEM" then + local name, class, level, _ + for i = 1, GetNumWhoResults() do + name, _, level, _, class, _ = GetWhoInfo(i) + class = L["class"][class] or nil + AddData("players", name, class, level) + end + + elseif event == "UPDATE_MOUSEOVER_UNIT" or event == "PLAYER_TARGET_CHANGED" then + local scan = event == "PLAYER_TARGET_CHANGED" and "target" or "mouseover" + local name, class, level, elite, _ + if UnitIsPlayer(scan) then + _, class = UnitClass(scan) + level = UnitLevel(scan) + name = UnitName(scan) + AddData("players", name, class, level) + else + _, class = UnitClass(scan) + elite = UnitClassification(scan) + level = UnitLevel(scan) + name = UnitName(scan) + AddData("mobs", name, class, level, elite) + end + end +end) + +-- since TargetByName can only be triggered within vanilla, +-- we can't auto-scan targets on further expansions. +if GetExpansion() == "vanilla" then + -- setup sound function switches + local SoundOn = PlaySound + local SoundOff = function() return end + + libunitscan:SetScript("OnUpdate", function() + -- don't scan when another unit is in target + if UnitExists("target") or UnitName("target") then return end + + local name = next(queue) + if name then + -- disable sound + _G.PlaySound = SoundOff + + -- try to target the unknown unit + TargetByName(name, true) + ClearTarget() + + -- enable sound again + _G.PlaySound = SoundOn + + queue[name] = nil + end + + this:Hide() + end) +end