From 1a62e0f7ab41b3acc5ae9fcc81b4dc3d2476c169 Mon Sep 17 00:00:00 2001 From: Clank-o-matic <898+clank-o-matic@noreply.octowow.st> Date: Fri, 18 Sep 2026 09:12:31 +0000 Subject: [PATCH] Upload files to "/" --- Core.lua | 1366 +++++++++++++++++++++++++++++++++++++++++++++++++++ OSHS.toc | 9 + Options.lua | 143 ++++++ README.md | 12 +- 4 files changed, 1528 insertions(+), 2 deletions(-) create mode 100644 Core.lua create mode 100644 OSHS.toc create mode 100644 Options.lua diff --git a/Core.lua b/Core.lua new file mode 100644 index 0000000..eb2c92e --- /dev/null +++ b/Core.lua @@ -0,0 +1,1366 @@ +local ADDON_NAME = "OSHS" + +OSHS = CreateFrame("Frame", "OSHSFrame", UIParent) +local HS = OSHS + +local defaults = { + enabled = true, + showDamage = true, + showDamageTaken = true, + showInterrupts = true, + showHealing = true, + showMisses = true, + colorBySchool = true, + critColor = true, + hideBlizzardDamage = false, + useTargetFrame = true, + scale = .9, + textSize = 22, + duration = 1.25, + offsetX = 0, + offsetY = -14, + horizontalSpacing = 23, + verticalSpacing = 19, + showStatusEffects = true, + statusShowIcons = true, + statusTextSize = 17, + statusDuration = 1.5, + statusRiseDistance = 45, + statusAnchorLocked = true, + statusAnchorPoint = "CENTER", + statusAnchorX = -120, + statusAnchorY = -10, + optionsPoint = "CENTER", + optionsX = 0, + optionsY = 0, +} + +local damageEvents = { + "CHAT_MSG_COMBAT_SELF_HITS", + "CHAT_MSG_COMBAT_SELF_MISSES", + "CHAT_MSG_COMBAT_PET_HITS", + "CHAT_MSG_COMBAT_PET_MISSES", + "CHAT_MSG_SPELL_SELF_DAMAGE", + "CHAT_MSG_SPELL_PERIODIC_CREATURE_DAMAGE", + "CHAT_MSG_SPELL_PERIODIC_HOSTILEPLAYER_DAMAGE", + "CHAT_MSG_SPELL_PET_DAMAGE", +} + +local incomingEvents = { + CHAT_MSG_COMBAT_CREATURE_VS_SELF_HITS = true, + CHAT_MSG_COMBAT_CREATURE_VS_SELF_MISSES = true, + CHAT_MSG_COMBAT_HOSTILEPLAYER_HITS = true, + CHAT_MSG_COMBAT_HOSTILEPLAYER_MISSES = true, + CHAT_MSG_SPELL_CREATURE_VS_SELF_DAMAGE = true, + CHAT_MSG_SPELL_HOSTILEPLAYER_DAMAGE = true, + CHAT_MSG_SPELL_PERIODIC_SELF_DAMAGE = true, +} + +local interruptOnlyEvents = { + CHAT_MSG_SPELL_FRIENDLYPLAYER_DAMAGE = true, + CHAT_MSG_SPELL_PARTY_DAMAGE = true, + CHAT_MSG_SPELL_CREATURE_VS_CREATURE_DAMAGE = true, +} + +local periodicDamageEvents = { + CHAT_MSG_SPELL_PERIODIC_CREATURE_DAMAGE = true, + CHAT_MSG_SPELL_PERIODIC_HOSTILEPLAYER_DAMAGE = true, +} + +local petDamageEvents = { + CHAT_MSG_COMBAT_PET_HITS = true, + CHAT_MSG_COMBAT_PET_MISSES = true, + CHAT_MSG_SPELL_PET_DAMAGE = true, +} + +local healEvents = { + "CHAT_MSG_SPELL_SELF_BUFF", + "CHAT_MSG_SPELL_PERIODIC_SELF_BUFFS", + "CHAT_MSG_SPELL_PERIODIC_FRIENDLYPLAYER_BUFFS", + "CHAT_MSG_SPELL_PERIODIC_PARTY_BUFFS", + "CHAT_MSG_SPELL_PERIODIC_CREATURE_BUFFS", +} + +local schoolTextures = { + physical = "Interface\\AddOns\\OSHS\\Assets\\Hitsplat-Physical.tga", + holy = "Interface\\AddOns\\OSHS\\Assets\\Hitsplat-Holy.tga", + fire = "Interface\\AddOns\\OSHS\\Assets\\Hitsplat-Fire.tga", + nature = "Interface\\AddOns\\OSHS\\Assets\\Hitsplat-Nature.tga", + frost = "Interface\\AddOns\\OSHS\\Assets\\Hitsplat-Frost.tga", + shadow = "Interface\\AddOns\\OSHS\\Assets\\Hitsplat-Shadow.tga", + arcane = "Interface\\AddOns\\OSHS\\Assets\\Hitsplat-Arcane.tga", +} + +local plates, active, pool = {}, {}, {} +local interruptActive, interruptPool = {}, {} +local statusActive, statusPool, statusByKey = {}, {}, {} +local spellIcons = {} +local recentTargetGUIDs = {} +local pendingInterrupt +local lastWorldChildren, scanElapsed = 0, 0 +local knownBossNames = {} +local nampowerHealsActive = GetNampowerVersion and true or false +local nampowerAurasActive = GetNampowerVersion and true or false +local nampowerSpellDamageActive = false +local nampowerAutoAttackActive = false +local nampowerControlledSpellActive = false +local nampowerControlledAutoActive = false +local nampowerIncomingSpellActive = false +local nampowerIncomingAutoActive = false +HS.hasClassicAPI = C_NamePlate and C_NamePlate.GetNamePlateForUnit and UnitGUID +HS.hasNampower = GetNampowerVersion and true or false + +local function applyDefaults() + if not OSHSDB then OSHSDB = {} end + local key, value + for key, value in pairs(defaults) do + if OSHSDB[key] == nil then OSHSDB[key] = value end + end + HS.db = OSHSDB +end + +function HS:ResetSettings() + if self.db and self.db.nativeDamageBeforeOSHS ~= nil and SetCVar then + SetCVar("CombatDamage", self.db.nativeDamageBeforeOSHS) + end + OSHSDB = {} + applyDefaults() + self:ApplyNativeTextSetting() + if self.PositionStatusAnchor then self:PositionStatusAnchor() end + if self.SetStatusAnchorLocked then self:SetStatusAnchorLocked(self.db.statusAnchorLocked) end +end + +function HS:ApplyNativeTextSetting() + if not self.db or not SetCVar then return end + if self.db.hideBlizzardDamage then + if self.db.nativeDamageBeforeOSHS == nil then + self.db.nativeDamageBeforeOSHS = tonumber(GetCVar("CombatDamage")) or 1 + end + SetCVar("CombatDamage", 0) + elseif self.db.nativeDamageBeforeOSHS ~= nil then + SetCVar("CombatDamage", self.db.nativeDamageBeforeOSHS) + self.db.nativeDamageBeforeOSHS = nil + end +end + +local function schoolKey(school) + local value = string.lower(tostring(school or "")) + if string.find(value, "holy") then return "holy" end + if string.find(value, "fire") then return "fire" end + if string.find(value, "nature") or string.find(value, "poison") then return "nature" end + if string.find(value, "frost") then return "frost" end + if string.find(value, "shadow") then return "shadow" end + if string.find(value, "arcane") then return "arcane" end + return "physical" +end + +local function trimName(name) + if not name then return nil end + name = string.gsub(name, "^%s+", "") + name = string.gsub(name, "%s+$", "") + return name +end + +local function findNameRegion(frame) + local regions = { frame:GetRegions() } + local i, region, label + for i = 1, table.getn(regions) do + region = regions[i] + if region and region.GetObjectType and region:GetObjectType() == "FontString" then + label = region:GetText() + if label and label ~= "" and not tonumber(label) then return region end + end + end +end + +local function considerPlate(frame) + if not frame or plates[frame] then return end + local bar = frame:GetChildren() + if not bar or not bar.GetObjectType or bar:GetObjectType() ~= "StatusBar" then return end + local nameRegion = findNameRegion(frame) + if nameRegion then plates[frame] = { frame = frame, nameRegion = nameRegion } end +end + +local function visualPlateAnchor(frame) + if not frame then return nil end + if frame.nameplate then return frame.nameplate.health or frame.nameplate end + if frame.parent and frame.health and frame.platename then return frame.health end + return frame +end + +function HS:ScanNameplates() + local count = WorldFrame:GetNumChildren() + if count == lastWorldChildren then return end + local children = { WorldFrame:GetChildren() } + local i + for i = 1, table.getn(children) do considerPlate(children[i]) end + lastWorldChildren = count +end + +local function plateForName(name) + name = trimName(name) + if not name then return nil end + if HS.hasClassicAPI then + if UnitExists("target") and UnitName("target") == name then + local targetPlate = C_NamePlate.GetNamePlateForUnit("target") + if targetPlate then return visualPlateAnchor(targetPlate) end + end + local i, token, frame + for i = 1, 40 do + token = "nameplate" .. i + if UnitExists(token) and UnitName(token) == name then + frame = C_NamePlate.GetNamePlateForUnit(token) + if frame then return visualPlateAnchor(frame) end + end + end + end + local data + for _, data in pairs(plates) do + if data.frame:IsShown() and trimName(data.nameRegion:GetText()) == name then + return visualPlateAnchor(data.frame) + end + end +end + +local function plateForGUID(guid) + if not guid then return nil end + if C_NamePlate and C_NamePlate.GetNamePlateForGUID then + local ok, frame = pcall(C_NamePlate.GetNamePlateForGUID, guid) + if ok and frame then return visualPlateAnchor(frame) end + end + if UnitTokenFromGUID and C_NamePlate and C_NamePlate.GetNamePlateForUnit then + local ok, token = pcall(UnitTokenFromGUID, guid) + if ok and token then + local frameOK, frame = pcall(C_NamePlate.GetNamePlateForUnit, token) + if frameOK and frame then return visualPlateAnchor(frame) end + end + end +end + +local function rememberCurrentBoss() + if not UnitExists("target") then return end + local name = UnitName("target") + local classification = UnitClassification and UnitClassification("target") + local level = UnitLevel and UnitLevel("target") + if name and (classification == "worldboss" or level == -1) then knownBossNames[name] = true end +end + +local function targetFrameForName(name) + if not HS.db.useTargetFrame or not name or not UnitExists("target") or UnitName("target") ~= name then return nil end + rememberCurrentBoss() + if pfUI and pfUI.uf and pfUI.uf.target and pfUI.uf.target:IsShown() then + return pfUI.uf.target.health or pfUI.uf.target + end + if TargetFrameHealthBar and TargetFrameHealthBar:IsShown() then return TargetFrameHealthBar end + if TargetFrame and TargetFrame:IsShown() then return TargetFrame end +end + +local function anchorIsVisible(anchor) + if not anchor then return false end + if anchor.IsVisible then return anchor:IsVisible() end + return anchor:IsShown() +end + +local function acquireSplat() + local item = table.remove(pool) + if item then item:Show(); return item end + item = CreateFrame("Frame", nil, UIParent) + item:SetWidth(180); item:SetHeight(52); item:SetFrameStrata("HIGH") + item.splat = item:CreateTexture(nil, "ARTWORK") + item.splat:SetPoint("CENTER", item, "CENTER", 0, 0) + item.text = item:CreateFontString(nil, "OVERLAY") + item.text:SetPoint("CENTER", item, "CENTER", 0, 0) + return item +end + +local function releaseSplat(item) + item:Hide(); item:ClearAllPoints() + item.anchor, item.slot, item.usesNameplate = nil, nil, nil + table.insert(pool, item) +end + +local function removeActive(index) + local item = table.remove(active, index) + if item then releaseSplat(item) end +end + +local function acquireInterrupt() + local item = table.remove(interruptPool) + if item then item:Show(); return item end + item = CreateFrame("Frame", nil, UIParent) + item:SetWidth(240); item:SetHeight(34); item:SetFrameStrata("HIGH") + item.text = item:CreateFontString(nil, "OVERLAY") + item.text:SetPoint("CENTER", item, "CENTER", 0, 0) + return item +end + +local function releaseInterrupt(item) + item:Hide(); item:ClearAllPoints() + item.anchor = nil + table.insert(interruptPool, item) +end + +function HS:ShowPlateMessage(targetName, targetGUID, message, red, green, blue) + if not self.db.enabled then return end + local anchor + if targetGUID then anchor = plateForGUID(targetGUID) + end + if not anchorIsVisible(anchor) and targetName and UnitExists("target") and trimName(UnitName("target")) == trimName(targetName) then + if C_NamePlate and C_NamePlate.GetNamePlateForUnit then + local ok, frame = pcall(C_NamePlate.GetNamePlateForUnit, "target") + if ok and frame then anchor = visualPlateAnchor(frame) end + end + if not anchorIsVisible(anchor) and UnitNameplate then + local ok, frame = pcall(UnitNameplate, "target") + if ok and frame then anchor = visualPlateAnchor(frame) end + end + end + if not anchorIsVisible(anchor) then anchor = plateForName(targetName) end + if not anchorIsVisible(anchor) then return end + + local i, item + for i = table.getn(interruptActive), 1, -1 do + if interruptActive[i].anchor == anchor then + releaseInterrupt(table.remove(interruptActive, i)) + end + end + + item = acquireInterrupt() + local scale = tonumber(self.db.scale) or .9 + local textSize = math.max(12, (tonumber(self.db.textSize) or 22) * scale) + item.text:SetFont("Interface\\AddOns\\OSHS\\Assets\\runescape.ttf", math.max(1, textSize - 1), "OUTLINE") + if item.text.SetTextHeight then item.text:SetTextHeight(textSize) end + item.text:SetText(message) + item.text:SetTextColor(red or 1, green or 1, blue or 1) + item:SetAlpha(1) + item.anchor = anchor + item.started = GetTime() + item.duration = math.max(.8, tonumber(self.db.duration) or 1.25) + item:SetPoint("BOTTOM", anchor, "TOP", tonumber(self.db.offsetX) or 0, 8) + table.insert(interruptActive, item) + return true +end + +function HS:ShowInterrupt(targetName, targetGUID) + if not self.db.showInterrupts then return end + return self:ShowPlateMessage(targetName, targetGUID, "Interrupted!", 1, .86, .12) +end + +function HS:ShowEnemyImmune(targetName, targetGUID) + if not self.db.showMisses then return end + return self:ShowPlateMessage(targetName, targetGUID, "Immune!", .7, .65, 1) +end + +HS.DamageFallback = CreateFrame("Frame", "OSHSDamageAnchor", UIParent) +HS.DamageFallback:SetWidth(180); HS.DamageFallback:SetHeight(20) +HS.DamageFallback:SetPoint("CENTER", UIParent, "CENTER", 0, 105) + +HS.HealFallback = CreateFrame("Frame", "OSHSHealAnchor", UIParent) +HS.HealFallback:SetWidth(180); HS.HealFallback:SetHeight(20) +HS.HealFallback:SetPoint("CENTER", UIParent, "CENTER", 0, -55) + +HS.TakenFallback = CreateFrame("Frame", "OSHSTakenAnchor", UIParent) +HS.TakenFallback:SetWidth(180); HS.TakenFallback:SetHeight(20) +HS.TakenFallback:SetPoint("CENTER", UIParent, "CENTER", 0, -35) + +HS.BossFallback = CreateFrame("Frame", "OSHSBossAnchor", UIParent) +HS.BossFallback:SetWidth(220); HS.BossFallback:SetHeight(20) +HS.BossFallback:SetPoint("CENTER", UIParent, "CENTER", 0, 175) + +local statusAnchorBackdrop = { + bgFile = "Interface\\Buttons\\WHITE8X8", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 12, + insets = { left = 3, right = 3, top = 3, bottom = 3 }, +} + +HS.StatusAnchor = CreateFrame("Frame", "OSHSStatusAnchor", UIParent) +HS.StatusAnchor:SetWidth(230); HS.StatusAnchor:SetHeight(28) +HS.StatusAnchor:SetPoint("CENTER", UIParent, "CENTER", -120, -10) +HS.StatusAnchor:SetFrameStrata("DIALOG") +HS.StatusAnchor:SetMovable(true); HS.StatusAnchor:RegisterForDrag("LeftButton") +HS.StatusAnchor:SetBackdrop(statusAnchorBackdrop) +HS.StatusAnchor.label = HS.StatusAnchor:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall") +HS.StatusAnchor.label:SetPoint("CENTER", HS.StatusAnchor, "CENTER", 0, 0) +HS.StatusAnchor.label:SetText("OSHS status effects - drag to move") +HS.StatusAnchor:SetScript("OnDragStart", function() this:StartMoving() end) +HS.StatusAnchor:SetScript("OnDragStop", function() + this:StopMovingOrSizing() + local point, _, _, x, y = this:GetPoint(1) + HS.db.statusAnchorPoint = point or "CENTER" + HS.db.statusAnchorX = x or 0 + HS.db.statusAnchorY = y or 0 +end) + +function HS:PositionStatusAnchor() + if not self.db then return end + self.StatusAnchor:ClearAllPoints() + local point = self.db.statusAnchorPoint or "CENTER" + self.StatusAnchor:SetPoint(point, UIParent, point, self.db.statusAnchorX or -120, self.db.statusAnchorY or -10) +end + +function HS:SetStatusAnchorLocked(locked) + self.db.statusAnchorLocked = locked and true or false + self.StatusAnchor:EnableMouse(not self.db.statusAnchorLocked) + if self.db.statusAnchorLocked then + self.StatusAnchor:SetBackdropColor(0, 0, 0, 0) + self.StatusAnchor:SetBackdropBorderColor(0, 0, 0, 0) + self.StatusAnchor.label:Hide() + else + self.StatusAnchor:SetBackdropColor(.35, .08, .45, .75) + self.StatusAnchor:SetBackdropBorderColor(.85, .45, 1, 1) + self.StatusAnchor.label:Show() + end + if self.UpdateStatusAnchorButton then self:UpdateStatusAnchorButton() end +end + +local function acquireStatusItem() + local item = table.remove(statusPool) + if item then item:Show(); return item end + item = CreateFrame("Frame", nil, UIParent) + item:SetWidth(300); item:SetHeight(28); item:SetFrameStrata("HIGH") + item.icon = item:CreateTexture(nil, "ARTWORK") + item.icon:SetPoint("RIGHT", item, "CENTER", -3, 0) + item.icon:SetTexCoord(.08, .92, .08, .92) + item.text = item:CreateFontString(nil, "OVERLAY") + item.text:SetPoint("LEFT", item, "CENTER", 3, 0) + item.text:SetJustifyH("LEFT") + return item +end + + +local function releaseStatusItem(item) + if item.key and statusByKey[item.key] == item then statusByKey[item.key] = nil end + item:Hide(); item:ClearAllPoints(); item.key = nil + table.insert(statusPool, item) +end + +function HS:ShowStatusEffect(message, r, g, b, texture, dedupeKey) + if not self.db.enabled or not self.db.showStatusEffects or not message or message == "" then return end + local now = GetTime() + local existing = dedupeKey and statusByKey[dedupeKey] + if existing and now - existing.started <= .3 then + existing.text:SetText(message); existing.text:SetTextColor(r or 1, g or 1, b or 1) + if self.db.statusShowIcons and texture then existing.icon:SetTexture(texture); existing.icon:Show() end + existing.started = now; existing.duration = self.db.statusDuration + return + end + + if table.getn(statusActive) >= 4 then + local oldest = table.remove(statusActive, 1) + releaseStatusItem(oldest) + end + local item = acquireStatusItem() + local size = tonumber(self.db.statusTextSize) or 17 + item.text:SetFont("Fonts\\FRIZQT__.TTF", math.max(1, size - 1), "OUTLINE") + if item.text.SetTextHeight then item.text:SetTextHeight(size) end + item.text:SetText(message); item.text:SetTextColor(r or 1, g or 1, b or 1) + if self.db.statusShowIcons and texture then + item.icon:SetTexture(texture); item.icon:SetWidth(size); item.icon:SetHeight(size); item.icon:Show() + else + item.icon:Hide() + end + item:SetAlpha(1); item.started = now; item.duration = tonumber(self.db.statusDuration) or 1.5 + item.key = dedupeKey + if dedupeKey then statusByKey[dedupeKey] = item end + table.insert(statusActive, item) +end + +function HS:ShowSplat(targetName, amount, isCrit, isHeal, school, anchorHint, allowFallback, targetGUID, isTaken) + if not self.db.enabled then return end + if isHeal and not self.db.showHealing then return end + if isTaken and not self.db.showDamageTaken then return end + if not isHeal and tonumber(amount) and not self.db.showDamage then return end + if (not tonumber(amount) or tonumber(amount) == 0) and not self.db.showMisses then return end + + local plateAnchor + if not isTaken then + if anchorHint then + plateAnchor = anchorHint + elseif targetGUID then + plateAnchor = plateForGUID(targetGUID) + else + plateAnchor = plateForName(targetName) + end + end + if not anchorIsVisible(plateAnchor) then plateAnchor = nil end + local targetAnchor = not isTaken and not isHeal and not plateAnchor and targetFrameForName(targetName) + local bossAnchor = not isTaken and not isHeal and not plateAnchor and not targetAnchor and knownBossNames[targetName] and self.BossFallback + local fallback = isTaken and self.TakenFallback or (isHeal and self.HealFallback or self.DamageFallback) + local anchor = plateAnchor or targetAnchor or bossAnchor or (allowFallback ~= false and fallback) + if not anchor then return end + + local numeric = tonumber(amount) + local amountText = tostring(numeric or 0) + local scale = tonumber(self.db.scale) or .9 + local textSize = math.max(10, (tonumber(self.db.textSize) or 22) * .82 * scale) + local item = acquireSplat() + item.text:SetFont("Interface\\AddOns\\OSHS\\Assets\\runescape.ttf", math.max(1, textSize - 1), "OUTLINE") + if item.text.SetTextHeight then item.text:SetTextHeight(textSize) end + item.text:SetText(amountText) + if isCrit and self.db.critColor then item.text:SetTextColor(1, .88, .3) else item.text:SetTextColor(1, 1, 1) end + + local texture = schoolTextures[self.db.colorBySchool and schoolKey(school) or "physical"] + if isHeal then texture = "Interface\\AddOns\\OSHS\\Assets\\Hitsplat-Heal.tga" + elseif not numeric or numeric == 0 then texture = "Interface\\AddOns\\OSHS\\Assets\\Hitsplat-Miss.tga" end + item.splat:SetTexture(texture) + item.splat:SetVertexColor(1, 1, 1) + item.splat:SetWidth(math.max(42, 26 + string.len(amountText) * 7) * scale) + item.splat:SetHeight(42 * scale) + item:SetAlpha(1) + item.anchor = anchor + item.usesNameplate = plateAnchor and true or false + + local occupied = {} + local oldestIndex, oldestTime + local now = GetTime() + local occupiedCount = 0 + local i, other + for i = 1, table.getn(active) do + other = active[i] + if other.anchor == anchor and other.slot then + if not occupied[other.slot] then + occupied[other.slot] = true + occupiedCount = occupiedCount + 1 + end + if not oldestTime or other.started < oldestTime then oldestTime, oldestIndex = other.started, i end + end + end + + local slot + if occupiedCount < 4 then + for i = 1, 4 do if not occupied[i] then slot = i; break end end + elseif oldestIndex then + slot = active[oldestIndex].slot + removeActive(oldestIndex) + end + slot = slot or 1 + item.slot = slot + item.started = now + item.duration = tonumber(self.db.duration) or 1.25 + + local x = tonumber(self.db.offsetX) or 0 + local y = tonumber(self.db.offsetY) or -14 + local h = (tonumber(self.db.horizontalSpacing) or 23) * scale + local v = (tonumber(self.db.verticalSpacing) or 19) * scale + if slot == 2 then x, y = x - h, y - v + elseif slot == 3 then x, y = x + h, y - v + elseif slot == 4 then y = y - (2 * v) end + item.offsetX, item.offsetY = x, y + if plateAnchor then item:SetPoint("TOP", anchor, "BOTTOM", x, y) + else item:SetPoint("CENTER", anchor, "CENTER", x, y) end + table.insert(active, item) +end + +function HS:Preview() + local anchor = UnitName("target") or "Target" + self:ShowSplat(anchor, 42, false, false, "Physical") + self:ShowSplat(anchor, 73, true, false, "Fire") + self:ShowSplat(anchor, 18, false, false, "Frost") + self:ShowSplat(anchor, MISS or "Miss", false, false, nil) +end + +local function parsePetDamage(message, eventName) + local source, spell, target, amount, school + local meleeEvent = eventName == "CHAT_MSG_COMBAT_PET_HITS" + local cmatch = pfUI and pfUI.api and pfUI.api.cmatch + if cmatch then + local patterns = { + { SPELLLOGCRITSCHOOLOTHEROTHER, true, false, true }, { SPELLLOGSCHOOLOTHEROTHER, false, false, true }, + { SPELLLOGCRITOTHEROTHER, true, false, false }, { SPELLLOGOTHEROTHER, false, false, false }, + } + local i, p + for i = 1, table.getn(patterns) do + p = patterns[i] + if p[1] then + if p[4] then source, spell, target, amount, school = cmatch(message, p[1]) + else source, spell, target, amount = cmatch(message, p[1]); school = nil end + if source then return target, amount, p[2], school end + end + end + local meleePatterns = { + { COMBATHITCRITSCHOOLOTHEROTHER, true, true }, { COMBATHITSCHOOLOTHEROTHER, false, true }, + { COMBATHITCRITOTHEROTHER, true, false }, { COMBATHITOTHEROTHER, false, false }, + } + for i = 1, table.getn(meleePatterns) do + p = meleePatterns[i] + if p[1] then + if p[3] then source, target, amount, school = cmatch(message, p[1]) + else source, target, amount = cmatch(message, p[1]); school = "Physical" end + if source then return target, amount, p[2], school end + end + end + end + _, _, source, spell, target, amount, school = string.find(message, "^(.+)'s (.+) crits (.+) for (%d+) (%a+) damage") + if source then return target, amount, true, school end + _, _, source, spell, target, amount, school = string.find(message, "^(.+)'s (.+) hits (.+) for (%d+) (%a+) damage") + if source then return target, amount, false, school end + _, _, source, spell, target, amount = string.find(message, "^(.+)'s (.+) crits (.+) for (%d+)") + if source then return target, amount, true end + _, _, source, spell, target, amount = string.find(message, "^(.+)'s (.+) hits (.+) for (%d+)") + if source then return target, amount, false end + _, _, source, target, amount, school = string.find(message, "^(.+) crits (.+) for (%d+) (%a+) damage") + if source then return target, amount, true, school end + _, _, source, target, amount, school = string.find(message, "^(.+) hits (.+) for (%d+) (%a+) damage") + if source then return target, amount, false, school end + _, _, source, target, amount = string.find(message, "^(.+) crits (.+) for (%d+)") + if source then return target, amount, true, meleeEvent and "Physical" or nil end + _, _, source, target, amount = string.find(message, "^(.+) hits (.+) for (%d+)") + if source then return target, amount, false, meleeEvent and "Physical" or nil end +end + +local function parseDamage(message) + local spell, target, amount, school, source + local cmatch = pfUI and pfUI.api and pfUI.api.cmatch + if cmatch then + if SPELLLOGCRITSCHOOLSELFOTHER then spell, target, amount, school = cmatch(message, SPELLLOGCRITSCHOOLSELFOTHER); if spell then return target, amount, true, school end end + if SPELLLOGSCHOOLSELFOTHER then spell, target, amount, school = cmatch(message, SPELLLOGSCHOOLSELFOTHER); if spell then return target, amount, false, school end end + if SPELLLOGCRITSELFOTHER then spell, target, amount = cmatch(message, SPELLLOGCRITSELFOTHER); if spell then return target, amount, true end end + if SPELLLOGSELFOTHER then spell, target, amount = cmatch(message, SPELLLOGSELFOTHER); if spell then return target, amount, false end end + if COMBATHITCRITSCHOOLSELFOTHER then target, amount, school = cmatch(message, COMBATHITCRITSCHOOLSELFOTHER); if target then return target, amount, true, school end end + if COMBATHITSCHOOLSELFOTHER then target, amount, school = cmatch(message, COMBATHITSCHOOLSELFOTHER); if target then return target, amount, false, school end end + if COMBATHITCRITSELFOTHER then target, amount = cmatch(message, COMBATHITCRITSELFOTHER); if target then return target, amount, true, "Physical" end end + if COMBATHITSELFOTHER then target, amount = cmatch(message, COMBATHITSELFOTHER); if target then return target, amount, false, "Physical" end end + if PERIODICAURADAMAGESELFOTHER then target, amount, school, spell = cmatch(message, PERIODICAURADAMAGESELFOTHER); if target then return target, amount, false, school end end + end + _, _, spell, target, amount, school = string.find(message, "^Your (.+) crits (.+) for (%d+) (%a+) damage") + if spell then return target, amount, true, school end + _, _, spell, target, amount, school = string.find(message, "^Your (.+) hits (.+) for (%d+) (%a+) damage") + if spell then return target, amount, false, school end + _, _, spell, target, amount = string.find(message, "^Your (.+) crits (.+) for (%d+)") + if spell then return target, amount, true end + _, _, spell, target, amount = string.find(message, "^Your (.+) hits (.+) for (%d+)") + if spell then return target, amount, false end + _, _, target, amount, spell = string.find(message, "^(.+) suffers (%d+) .- damage from your (.+)%.") + if target then return target, amount, false end + _, _, target, amount = string.find(message, "^You crit (.+) for (%d+)") + if target then return target, amount, true, "Physical" end + _, _, target, amount = string.find(message, "^You hit (.+) for (%d+)") + if target then return target, amount, false, "Physical" end + _, _, target, amount = string.find(message, "^Your pet crits (.+) for (%d+)") + if target then return target, amount, true, "Physical" end + _, _, target, amount = string.find(message, "^Your pet hits (.+) for (%d+)") + if target then return target, amount, false, "Physical" end + _, _, source, spell, target, amount, school = string.find(message, "^(.+)'s (.+) crits (.+) for (%d+) (%a+) damage") + if source and (source == UnitName("pet") or string.find(string.lower(source), "totem")) then return target, amount, true, school end + _, _, source, spell, target, amount, school = string.find(message, "^(.+)'s (.+) hits (.+) for (%d+) (%a+) damage") + if source and (source == UnitName("pet") or string.find(string.lower(source), "totem")) then return target, amount, false, school end + _, _, source, spell, target, amount = string.find(message, "^(.+)'s (.+) crits (.+) for (%d+)") + if source and (source == UnitName("pet") or string.find(string.lower(source), "totem")) then return target, amount, true end + _, _, source, spell, target, amount = string.find(message, "^(.+)'s (.+) hits (.+) for (%d+)") + if source and (source == UnitName("pet") or string.find(string.lower(source), "totem")) then return target, amount, false end +end + +local function parsePeriodicDamage(message) + local target, amount, school, spell + local cmatch = pfUI and pfUI.api and pfUI.api.cmatch + if cmatch and PERIODICAURADAMAGESELFOTHER then + target, amount, school, spell = cmatch(message, PERIODICAURADAMAGESELFOTHER) + if target then return target, amount, school end + end + _, _, target, amount, school, spell = string.find(message, "^(.+) suffers (%d+) (%a+) damage from your (.+)%.") + if target then return target, amount, school end + _, _, target, amount, spell = string.find(message, "^(.+) suffers (%d+) damage from your (.+)%.") + if target then return target, amount end +end + +local function parseHeal(message) + local spell, target, amount + local cmatch = pfUI and pfUI.api and pfUI.api.cmatch + if cmatch then + if HEALEDCRITSELFSELF then spell, amount = cmatch(message, HEALEDCRITSELFSELF); if spell then return UnitName("player"), amount, true end end + if HEALEDSELFSELF then spell, amount = cmatch(message, HEALEDSELFSELF); if spell then return UnitName("player"), amount, false end end + if HEALEDCRITSELFOTHER then spell, target, amount = cmatch(message, HEALEDCRITSELFOTHER); if spell then return target, amount, true end end + if HEALEDSELFOTHER then spell, target, amount = cmatch(message, HEALEDSELFOTHER); if spell then return target, amount, false end end + if PERIODICAURAHEALSELFSELF then amount, spell = cmatch(message, PERIODICAURAHEALSELFSELF); if amount then return UnitName("player"), amount, false end end + if PERIODICAURAHEALSELFOTHER then target, amount, spell = cmatch(message, PERIODICAURAHEALSELFOTHER); if target then return target, amount, false end end + end + _, _, spell, amount = string.find(message, "^Your (.+) critically heals you for (%d+)") + if spell then return UnitName("player"), amount, true end + _, _, spell, amount = string.find(message, "^Your (.+) heals you for (%d+)") + if spell then return UnitName("player"), amount, false end + _, _, spell, target, amount = string.find(message, "^Your (.+) critically heals (.+) for (%d+)") + if spell then return target, amount, true end + _, _, spell, target, amount = string.find(message, "^Your (.+) heals (.+) for (%d+)") + if spell then return target, amount, false end +end + +local function parseMiss(message) + local spell, target + local cmatch = pfUI and pfUI.api and pfUI.api.cmatch + if cmatch then + local patterns = { + { SPELLMISSSELFOTHER, true, MISS or "Miss" }, { SPELLDODGEDSELFOTHER, true, DODGE or "Dodge" }, + { SPELLPARRIEDSELFOTHER, true, PARRY or "Parry" }, { SPELLBLOCKEDSELFOTHER, true, BLOCK or "Block" }, + { SPELLRESISTSELFOTHER, true, RESIST or "Resist" }, { SPELLLOGABSORBSELFOTHER, true, ABSORB or "Absorb" }, + { SPELLIMMUNESELFOTHER, true, IMMUNE or "Immune" }, { SPELLREFLECTSELFOTHER, true, REFLECT or "Reflect" }, + { SPELLEVADEDSELFOTHER, true, EVADE or "Evade" }, { MISSEDSELFOTHER, false, MISS or "Miss" }, + { VSDODGESELFOTHER, false, DODGE or "Dodge" }, { VSPARRYSELFOTHER, false, PARRY or "Parry" }, + { VSBLOCKSELFOTHER, false, BLOCK or "Block" }, { VSIMMUNESELFOTHER, false, IMMUNE or "Immune" }, + { VSABSORBSELFOTHER, false, ABSORB or "Absorb" }, { VSEVADESELFOTHER, false, EVADE or "Evade" }, + } + local i, p + for i = 1, table.getn(patterns) do + p = patterns[i] + if p[1] then + if p[2] then spell, target = cmatch(message, p[1]) else target = cmatch(message, p[1]) end + if target then return target, p[3] end + end + end + end + _, _, spell, target = string.find(message, "^Your (.+) missed (.+)%.") + if spell then return target, MISS or "Miss" end + _, _, spell, target = string.find(message, "^Your (.+) was resisted by (.+)%.") + if spell then return target, RESIST or "Resist" end + _, _, spell, target = string.find(message, "^Your (.+) failed%. (.+) is immune%.") + if spell then return target, IMMUNE or "Immune" end + _, _, target, spell = string.find(message, "^(.+) is immune to your (.+)%.") + if target then return target, IMMUNE or "Immune" end + _, _, target = string.find(message, "^You miss (.+)%.") + if target then return target, MISS or "Miss" end + _, _, target = string.find(message, "^(.+) dodges") + if target then return target, DODGE or "Dodge" end + _, _, target = string.find(message, "^(.+) parries") + return target, target and (PARRY or "Parry") +end + +local function parsePetMiss(message) + local source, spell, target + local cmatch = pfUI and pfUI.api and pfUI.api.cmatch + if cmatch then + local patterns = { + { SPELLMISSOTHEROTHER, true, MISS or "Miss" }, { SPELLDODGEDOTHEROTHER, true, DODGE or "Dodge" }, + { SPELLPARRIEDOTHEROTHER, true, PARRY or "Parry" }, { SPELLBLOCKEDOTHEROTHER, true, BLOCK or "Block" }, + { SPELLRESISTOTHEROTHER, true, RESIST or "Resist" }, { SPELLLOGABSORBOTHEROTHER, true, ABSORB or "Absorb" }, + { SPELLIMMUNEOTHEROTHER, true, IMMUNE or "Immune" }, { SPELLEVADEDOTHEROTHER, true, EVADE or "Evade" }, + { MISSEDOTHEROTHER, false, MISS or "Miss" }, { VSDODGEOTHEROTHER, false, DODGE or "Dodge" }, + { VSPARRYOTHEROTHER, false, PARRY or "Parry" }, { VSBLOCKOTHEROTHER, false, BLOCK or "Block" }, + { VSABSORBOTHEROTHER, false, ABSORB or "Absorb" }, { VSIMMUNEOTHEROTHER, false, IMMUNE or "Immune" }, + { VSEVADEOTHEROTHER, false, EVADE or "Evade" }, + } + local i, p + for i = 1, table.getn(patterns) do + p = patterns[i] + if p[1] then + if p[2] then source, spell, target = cmatch(message, p[1]) else source, target = cmatch(message, p[1]) end + if source then return target, p[3] end + end + end + end + _, _, source, spell, target = string.find(message, "^(.+)'s (.+) missed (.+)%.") + if source then return target, MISS or "Miss" end + _, _, source, spell, target = string.find(message, "^(.+)'s (.+) failed%. (.+) is immune%.") + if source then return target, IMMUNE or "Immune" end + _, _, source, target = string.find(message, "^(.+) misses (.+)%.") + if source then return target end + _, _, source, target = string.find(message, "^(.+) attacks%. (.+) dodges%.") + if source then return target end + _, _, source, target = string.find(message, "^(.+) attacks%. (.+) parries%.") + if source then return target end + _, _, source, target = string.find(message, "^(.+) attacks%. (.+) blocks%.") + if source then return target end + _, _, target, source = string.find(message, "^(.+) dodges (.+)%.") + if target then return target end + _, _, target, source = string.find(message, "^(.+) parries (.+)%.") + if target then return target end + _, _, target, source = string.find(message, "^(.+) blocks (.+)%.") + return target, target and (BLOCK or "Block") +end + +local function isImmuneResult(result) + if not result then return false end + return string.lower(tostring(result)) == string.lower(tostring(IMMUNE or "Immune")) +end + +local function parseInterrupt(message, allowControlled) + local source, target, spell + local cmatch = pfUI and pfUI.api and pfUI.api.cmatch + if cmatch and SPELLINTERRUPTSELFOTHER then + target, spell = cmatch(message, SPELLINTERRUPTSELFOTHER) + if target then return target end + end + _, _, target, spell = string.find(message, "^You interrupt (.+)'s (.-)[%.!]$") + if target then return target end + local ability + _, _, ability, target, spell = string.find(message, "^Your (.+) interrupts (.+)'s (.-)[%.!]$") + if target then return target end + if string.find(string.lower(message), "interrupt", 1, true) and UnitExists("target") then + return UnitName("target") + end + if not allowControlled then return end + if cmatch and SPELLINTERRUPTOTHEROTHER then + source, target, spell = cmatch(message, SPELLINTERRUPTOTHEROTHER) + else + _, _, source, target, spell = string.find(message, "^(.+) interrupts (.+)'s (.-)[%.!]$") + end + if source and (source == UnitName("pet") or string.find(string.lower(source), "totem")) then return target end +end + +local function interruptGUIDForName(name) + name = trimName(name) + if name and UnitExists("target") and trimName(UnitName("target")) == name and UnitGUID then + return UnitGUID("target") + end + local recent = name and recentTargetGUIDs[name] + if recent and GetTime() - recent.time <= 1 then return recent.guid end +end + +local function parseIncomingAvoidance(message) + local source, spell + local cmatch = pfUI and pfUI.api and pfUI.api.cmatch + if cmatch then + local patterns = { + { SPELLMISSOTHERSELF, true }, { SPELLDODGEDOTHERSELF, true }, { SPELLPARRIEDOTHERSELF, true }, + { SPELLBLOCKEDOTHERSELF, true }, { SPELLRESISTOTHERSELF, true }, { SPELLLOGABSORBOTHERSELF, true }, + { SPELLIMMUNEOTHERSELF, true }, { SPELLEVADEDOTHERSELF, true }, { SPELLREFLECTOTHERSELF, true }, + { MISSEDOTHERSELF, false }, { VSDODGEOTHERSELF, false }, { VSPARRYOTHERSELF, false }, + { VSBLOCKOTHERSELF, false }, { VSABSORBOTHERSELF, false }, { VSIMMUNEOTHERSELF, false }, { VSEVADEOTHERSELF, false }, + } + local i, p + for i = 1, table.getn(patterns) do + p = patterns[i] + if p[1] then + if p[2] then source, spell = cmatch(message, p[1]) else source = cmatch(message, p[1]) end + if source then return source end + end + end + end + _, _, source = string.find(message, "^(.+)'s .+ was blocked%.") + if source then return source end + _, _, source = string.find(message, "^(.+)'s .+ missed you%.") + if source then return source end + _, _, source = string.find(message, "^(.+) attacks%. You block%.") + if source then return source end + _, _, source = string.find(message, "^(.+) attacks%. You dodge%.") + if source then return source end + _, _, source = string.find(message, "^(.+) attacks%. You parry%.") + if source then return source end + _, _, source = string.find(message, "^(.+) misses you%.") + if source then return source end + _, _, source = string.find(message, "^You block (.+)'s attack%.") + if source then return source end + _, _, source = string.find(message, "^You dodge (.+)'s attack%.") + if source then return source end + _, _, source = string.find(message, "^You parry (.+)'s attack%.") + return source +end + +local function parseIncoming(message) + local source, spell, amount, school + local cmatch = pfUI and pfUI.api and pfUI.api.cmatch + if cmatch then + if SPELLLOGCRITSCHOOLOTHERSELF then source, spell, amount, school = cmatch(message, SPELLLOGCRITSCHOOLOTHERSELF); if source then return source, amount, true, school end end + if SPELLLOGSCHOOLOTHERSELF then source, spell, amount, school = cmatch(message, SPELLLOGSCHOOLOTHERSELF); if source then return source, amount, false, school end end + if SPELLLOGCRITOTHERSELF then source, spell, amount = cmatch(message, SPELLLOGCRITOTHERSELF); if source then return source, amount, true end end + if SPELLLOGOTHERSELF then source, spell, amount = cmatch(message, SPELLLOGOTHERSELF); if source then return source, amount, false end end + if COMBATHITCRITSCHOOLOTHERSELF then source, amount, school = cmatch(message, COMBATHITCRITSCHOOLOTHERSELF); if source then return source, amount, true, school end end + if COMBATHITSCHOOLOTHERSELF then source, amount, school = cmatch(message, COMBATHITSCHOOLOTHERSELF); if source then return source, amount, false, school end end + if COMBATHITCRITOTHERSELF then source, amount = cmatch(message, COMBATHITCRITOTHERSELF); if source then return source, amount, true, "Physical" end end + if COMBATHITOTHERSELF then source, amount = cmatch(message, COMBATHITOTHERSELF); if source then return source, amount, false, "Physical" end end + if PERIODICAURADAMAGEOTHERSELF then amount, school, source, spell = cmatch(message, PERIODICAURADAMAGEOTHERSELF); if amount then return source, amount, false, school end end + if PERIODICAURADAMAGESELFSELF then amount, school, spell = cmatch(message, PERIODICAURADAMAGESELFSELF); if amount then return UnitName("player"), amount, false, school end end + end + _, _, source, spell, amount, school = string.find(message, "^(.+)'s (.+) hits you for (%d+) (%a+) damage") + if source then return source, amount, false, school end + _, _, source, spell, amount, school = string.find(message, "^(.+)'s (.+) crits you for (%d+) (%a+) damage") + if source then return source, amount, true, school end + _, _, source, spell, amount = string.find(message, "^(.+)'s (.+) hits you for (%d+)") + if source then return source, amount, false end + _, _, source, spell, amount = string.find(message, "^(.+)'s (.+) crits you for (%d+)") + if source then return source, amount, true end + _, _, source, amount = string.find(message, "^(.+) hits you for (%d+)") + if source then return source, amount, false, "Physical" end + _, _, source, amount = string.find(message, "^(.+) crits you for (%d+)") + if source then return source, amount, true, "Physical" end + _, _, amount, school, source, spell = string.find(message, "^You suffer (%d+) (%a+) damage from (.+)'s (.+)%.") + if amount then return source, amount, false, school end + _, _, amount, source, spell = string.find(message, "^You suffer (%d+) damage from (.+)'s (.+)%.") + if amount then return source, amount, false end + _, _, amount, school, spell = string.find(message, "^You suffer (%d+) (%a+) damage from your (.+)%.") + if amount then return UnitName("player"), amount, false, school end +end + +function HS:HandleCombatMessage(eventName, message) + if not self.db or not message then return end + local target, amount, crit, school, result + if eventName == "CHAT_MSG_SPELL_SELF_DAMAGE" or eventName == "CHAT_MSG_SPELL_PET_DAMAGE" or interruptOnlyEvents[eventName] then + target = parseInterrupt(message, eventName == "CHAT_MSG_SPELL_PET_DAMAGE") + if target then self:ShowInterrupt(target, interruptGUIDForName(target)); return end + if interruptOnlyEvents[eventName] then return end + end + if incomingEvents[eventName] then + local isMelee = string.find(eventName, "COMBAT_") and true or false + if (isMelee and nampowerIncomingAutoActive) or (not isMelee and nampowerIncomingSpellActive) then return end + target = parseIncomingAvoidance(message) + if target then self:ShowSplat(target, 0, false, false, nil, nil, true, nil, true); return end + target, amount, crit, school = parseIncoming(message) + if target or amount then self:ShowSplat(target, amount, crit, false, school, nil, true, nil, true) end + elseif periodicDamageEvents[eventName] then + if nampowerSpellDamageActive then return end + target, amount, school = parsePeriodicDamage(message) + if target then self:ShowSplat(target, amount, false, false, school) end + elseif petDamageEvents[eventName] then + if (eventName == "CHAT_MSG_SPELL_PET_DAMAGE" and nampowerControlledSpellActive) or + (eventName ~= "CHAT_MSG_SPELL_PET_DAMAGE" and nampowerControlledAutoActive) then return end + target, amount, crit, school = parsePetDamage(message, eventName) + if target then self:ShowSplat(target, amount, crit, false, school) + else + target, result = parsePetMiss(message) + if target and (not isImmuneResult(result) or not self:ShowEnemyImmune(target)) then + self:ShowSplat(target, 0, false, false) + end + end + elseif string.find(eventName, "BUFF") then + if not nampowerHealsActive then target, amount, crit = parseHeal(message) end + if target then self:ShowSplat(target, amount, crit, true) end + else + if (eventName == "CHAT_MSG_SPELL_SELF_DAMAGE" and nampowerSpellDamageActive) or + (string.find(eventName, "COMBAT_SELF") and nampowerAutoAttackActive) then return end + target, amount, crit, school = parseDamage(message) + if target then self:ShowSplat(target, amount, crit, false, school) + else + target, result = parseMiss(message) + if target and (not isImmuneResult(result) or not self:ShowEnemyImmune(target)) then + self:ShowSplat(target, 0, false, false) + end + end + end +end + +local function unitNameForGUID(guid) + if not guid or not UnitGUID then return nil end + local units = { "player", "target", "pet", "mouseover" } + local i, unit + for i = 1, 4 do table.insert(units, "party" .. i) end + for i = 1, 40 do table.insert(units, "raid" .. i); table.insert(units, "nameplate" .. i) end + for i = 1, table.getn(units) do + unit = units[i] + if UnitExists(unit) and UnitGUID(unit) == guid then return UnitName(unit) end + end + if GetUnitField then + local ok, name = pcall(GetUnitField, guid, "name") + if ok and name then return name end + end +end + +local function isPlayerOwnedGUID(guid) + if not guid or not UnitGUID then return false end + local playerGUID = UnitGUID("player") + if not playerGUID then return false end + if guid == playerGUID then return true end + if UnitExists("pet") and UnitGUID("pet") == guid then return true end + if UnitOwnerGUID then + local ok, ownerGUID = pcall(UnitOwnerGUID, guid) + if ok and ownerGUID == playerGUID then return true end + end + return false +end + +local nampowerSchools = { + [0] = "Physical", [1] = "Holy", [2] = "Fire", [3] = "Nature", + [4] = "Frost", [5] = "Shadow", [6] = "Arcane", +} + +local function autoAttackCrit(hitInfo) + hitInfo = tonumber(hitInfo) or 0 + return bit and bit.band and bit.band(hitInfo, 128) ~= 0 +end + +local function spellNameFromID(spellID) + if not spellID then return nil end + if GetSpellRecField then + local ok, name = pcall(GetSpellRecField, spellID, "name") + if ok and name then return name end + end + if GetSpellInfo then + local ok, name = pcall(GetSpellInfo, spellID) + if ok and name then return name end + end +end + +local interruptSpellNames = { + ["kick"] = true, ["pummel"] = true, ["shield bash"] = true, + ["counterspell"] = true, ["earth shock"] = true, + ["feral charge"] = true, ["spell lock"] = true, +} + +local function spellHasInterruptEffect(spellID) + spellID = tonumber(spellID) + if not spellID then return false end + if GetSpellRecField then + local fields = { "effect1", "effect2", "effect3", "Effect1", "Effect2", "Effect3" } + local i, ok, value + for i = 1, table.getn(fields) do + ok, value = pcall(GetSpellRecField, spellID, fields[i]) + if ok and tonumber(value) == 68 then return true end + end + end + local name = spellNameFromID(spellID) + return name and interruptSpellNames[string.lower(name)] and true or false +end + +local function spellTextureFromID(spellID) + if not spellID then return nil end + if pfUI and pfUI.libdebuff_GetSpellIcon then + local ok, texture = pcall(pfUI.libdebuff_GetSpellIcon, spellID) + if ok and texture then return texture end + end + if GetSpellRecField and GetSpellIconTexture then + local ok, iconID = pcall(GetSpellRecField, spellID, "spellIconID") + if ok and iconID then + local textureOK, texture = pcall(GetSpellIconTexture, iconID) + if textureOK and texture then + if not string.find(texture, "\\") then texture = "Interface\\Icons\\" .. texture end + return texture + end + end + end +end + +local function buildSpellIcons() + spellIcons = {} + if not GetSpellName then return end + local i, name, texture + i = 1 + while true do + name = GetSpellName(i, BOOKTYPE_SPELL or "spell") + if not name then break end + texture = GetSpellTexture(i, BOOKTYPE_SPELL or "spell") + if texture and not spellIcons[name] then spellIcons[name] = texture end + i = i + 1 + end + if HasPetSpells and HasPetSpells() then + i = 1 + while true do + name = GetSpellName(i, BOOKTYPE_PET or "pet") + if not name then break end + texture = GetSpellTexture(i, BOOKTYPE_PET or "pet") + if texture and not spellIcons[name] then spellIcons[name] = texture end + i = i + 1 + end + end +end + +local function parseStatus(message, faded) + local effect + local cmatch = pfUI and pfUI.api and pfUI.api.cmatch + if cmatch then + if faded and AURAREMOVEDSELF then + effect = cmatch(message, AURAREMOVEDSELF) + if effect then return effect, "fade" end + elseif not faded and AURAADDEDSELFHARMFUL then + effect = cmatch(message, AURAADDEDSELFHARMFUL) + if effect then return effect, "debuff" end + end + if not faded and AURAADDEDSELFHELPFUL then + effect = cmatch(message, AURAADDEDSELFHELPFUL) + if effect then return effect, "buff" end + end + end + if faded then + _, _, effect = string.find(message, "^(.+) fades from you%.") + if effect then return effect, "fade" end + else + if string.find(message, "^You gain %d+ ") then return end + _, _, effect = string.find(message, "^You gain (.+)%.") + if effect then return effect, "buff" end + _, _, effect = string.find(message, "^You are afflicted by (.+)%.") + if effect then return effect, "debuff" end + end +end + +function HS:HandleStatusMessage(message, faded) + if not message or nampowerAurasActive then return end + local effect, statusType = parseStatus(message, faded) + if not effect then return end + local key = "aura:" .. string.lower(effect) + if statusType == "debuff" then + self:ShowStatusEffect("- " .. effect, 1, .3, .45, spellIcons[effect] or "Interface\\Icons\\Spell_Shadow_CurseOfTounges", key) + elseif statusType == "fade" then + self:ShowStatusEffect("-" .. effect, .65, .65, .65, spellIcons[effect], key) + else + self:ShowStatusEffect("+ " .. effect, .45, 1, .55, spellIcons[effect] or "Interface\\Icons\\Spell_Holy_MagicalSentry", key) + end +end + +HS:RegisterEvent("VARIABLES_LOADED") +HS:RegisterEvent("PLAYER_ENTERING_WORLD") +HS:RegisterEvent("PLAYER_TARGET_CHANGED") +HS:RegisterEvent("LEARNED_SPELL_IN_TAB") +HS:RegisterEvent("SPELLS_CHANGED") +HS:RegisterEvent("CHAT_MSG_SPELL_AURA_GONE_SELF") +HS:RegisterEvent("CHAT_MSG_SPELL_PERIODIC_SELF_DAMAGE") +pcall(function() HS:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED") end) +local _, eventName +for _, eventName in ipairs(damageEvents) do HS:RegisterEvent(eventName) end +for _, eventName in ipairs(healEvents) do HS:RegisterEvent(eventName) end +for eventName in pairs(incomingEvents) do HS:RegisterEvent(eventName) end +for eventName in pairs(interruptOnlyEvents) do HS:RegisterEvent(eventName) end +if HS.hasNampower then + HS:RegisterEvent("SPELL_CAST_EVENT") + HS:RegisterEvent("SPELL_FAILED_OTHER") + HS:RegisterEvent("SPELL_DAMAGE_EVENT_SELF") + HS:RegisterEvent("SPELL_DAMAGE_EVENT_OTHER") + HS:RegisterEvent("AUTO_ATTACK_SELF") + HS:RegisterEvent("AUTO_ATTACK_OTHER") + HS:RegisterEvent("SPELL_MISS_SELF") + HS:RegisterEvent("SPELL_MISS_OTHER") + HS:RegisterEvent("SPELL_HEAL_BY_SELF") + HS:RegisterEvent("BUFF_ADDED_SELF") + HS:RegisterEvent("BUFF_REMOVED_SELF") + HS:RegisterEvent("DEBUFF_ADDED_SELF") + HS:RegisterEvent("DEBUFF_REMOVED_SELF") +end + +HS:SetScript("OnEvent", function() + if event == "VARIABLES_LOADED" then + applyDefaults(); HS:PositionStatusAnchor(); HS:SetStatusAnchorLocked(HS.db.statusAnchorLocked) + elseif event == "PLAYER_ENTERING_WORLD" then + applyDefaults(); buildSpellIcons(); HS:ScanNameplates(); HS:ApplyNativeTextSetting(); HS:PositionStatusAnchor(); HS:SetStatusAnchorLocked(HS.db.statusAnchorLocked) + if HS.hasNampower and SetCVar then + SetCVar("NP_EnableAutoAttackEvents", "1") + SetCVar("NP_EnableSpellHealEvents", "1") + end + if DEFAULT_CHAT_FRAME then + DEFAULT_CHAT_FRAME:AddMessage("|cffd58cffOSHS:|r loaded. Type |cffffffff/oshs|r for settings.") + end + elseif event == "PLAYER_TARGET_CHANGED" then + rememberCurrentBoss() + elseif event == "LEARNED_SPELL_IN_TAB" or event == "SPELLS_CHANGED" then + buildSpellIcons() + elseif event == "SPELL_CAST_EVENT" then + if tonumber(arg1) == 1 and spellHasInterruptEffect(arg2) then + local targetGUID = arg4 + if not targetGUID or targetGUID == "0x0000000000000000" or targetGUID == "0x000000000" then + targetGUID = UnitExists("target") and UnitGUID and UnitGUID("target") + end + pendingInterrupt = { guid = targetGUID, time = GetTime() } + end + elseif event == "SPELL_FAILED_OTHER" then + if pendingInterrupt and GetTime() - pendingInterrupt.time <= 1.25 and + (not pendingInterrupt.guid or pendingInterrupt.guid == arg1) then + local targetGUID = pendingInterrupt.guid or arg1 + HS:ShowInterrupt(unitNameForGUID(targetGUID), targetGUID) + pendingInterrupt = nil + end + elseif event == "COMBAT_LOG_EVENT_UNFILTERED" then + if arg2 == "SPELL_INTERRUPT" or arg3 == "SPELL_INTERRUPT" then + local playerGUID = UnitGUID and UnitGUID("player") + local sourceGUID, targetGUID, targetName = arg4, arg7, arg8 + if sourceGUID and (sourceGUID == playerGUID or isPlayerOwnedGUID(sourceGUID)) then + HS:ShowInterrupt(targetName or unitNameForGUID(targetGUID), targetGUID) + end + end + elseif event == "SPELL_DAMAGE_EVENT_SELF" then + local playerGUID = UnitGUID and UnitGUID("player") + local targetGUID, casterGUID = arg1, arg2 + local targetName = unitNameForGUID(targetGUID) + if targetGUID and targetGUID == playerGUID then + nampowerIncomingSpellActive = true + HS:ShowSplat(UnitName("player"), arg4, (tonumber(arg6) or 0) ~= 0, false, nampowerSchools[tonumber(arg7)], nil, true, nil, true) + else + if casterGUID == playerGUID then nampowerSpellDamageActive = true + else nampowerControlledSpellActive = true end + if targetName and targetGUID then + recentTargetGUIDs[trimName(targetName)] = { guid = targetGUID, time = GetTime() } + end + HS:ShowSplat(targetName, arg4, (tonumber(arg6) or 0) ~= 0, false, nampowerSchools[tonumber(arg7)], nil, false, targetGUID) + end + elseif event == "SPELL_DAMAGE_EVENT_OTHER" then + local playerGUID = UnitGUID and UnitGUID("player") + if arg1 and arg1 == playerGUID then + nampowerIncomingSpellActive = true + HS:ShowSplat(UnitName("player"), arg4, (tonumber(arg6) or 0) ~= 0, false, nampowerSchools[tonumber(arg7)], nil, true, nil, true) + elseif isPlayerOwnedGUID(arg2) then + local targetGUID = arg1 + local targetName = unitNameForGUID(targetGUID) + nampowerControlledSpellActive = true + if targetName and targetGUID then + recentTargetGUIDs[trimName(targetName)] = { guid = targetGUID, time = GetTime() } + end + HS:ShowSplat(targetName, arg4, (tonumber(arg6) or 0) ~= 0, false, nampowerSchools[tonumber(arg7)], nil, false, targetGUID) + end + elseif event == "AUTO_ATTACK_SELF" then + local casterGUID, targetGUID = arg1, arg2 + local targetName = unitNameForGUID(targetGUID) + local playerGUID = UnitGUID and UnitGUID("player") + if casterGUID == playerGUID then nampowerAutoAttackActive = true + else nampowerControlledAutoActive = true end + if targetName and targetGUID then + recentTargetGUIDs[trimName(targetName)] = { guid = targetGUID, time = GetTime() } + end + local victimState = tonumber(arg5) + if victimState == 4 then HS:ShowInterrupt(targetName, targetGUID) end + if victimState == 7 then + if not HS:ShowEnemyImmune(targetName, targetGUID) then + HS:ShowSplat(targetName, 0, false, false, nil, nil, false, targetGUID) + end + elseif victimState == 1 or victimState == 4 then + HS:ShowSplat(targetName, arg3, autoAttackCrit(arg4), false, "Physical", nil, false, targetGUID) + else + HS:ShowSplat(targetName, 0, false, false, nil, nil, false, targetGUID) + end + elseif event == "AUTO_ATTACK_OTHER" then + local playerGUID = UnitGUID and UnitGUID("player") + if arg2 and arg2 == playerGUID then + nampowerIncomingAutoActive = true + if tonumber(arg5) == 1 or tonumber(arg5) == 4 then + HS:ShowSplat(UnitName("player"), arg3, autoAttackCrit(arg4), false, "Physical", nil, true, nil, true) + else + HS:ShowSplat(UnitName("player"), 0, false, false, nil, nil, true, nil, true) + end + elseif isPlayerOwnedGUID(arg1) then + local targetGUID = arg2 + local targetName = unitNameForGUID(targetGUID) + nampowerControlledAutoActive = true + if targetName and targetGUID then + recentTargetGUIDs[trimName(targetName)] = { guid = targetGUID, time = GetTime() } + end + local victimState = tonumber(arg5) + if victimState == 4 then HS:ShowInterrupt(targetName, targetGUID) end + if victimState == 7 then + if not HS:ShowEnemyImmune(targetName, targetGUID) then + HS:ShowSplat(targetName, 0, false, false, nil, nil, false, targetGUID) + end + elseif victimState == 1 or victimState == 4 then + HS:ShowSplat(targetName, arg3, autoAttackCrit(arg4), false, "Physical", nil, false, targetGUID) + else + HS:ShowSplat(targetName, 0, false, false, nil, nil, false, targetGUID) + end + end + elseif event == "SPELL_MISS_SELF" then + local casterGUID, targetGUID = arg1, arg2 + local targetName = unitNameForGUID(targetGUID) + local missInfo = tonumber(arg4) + local playerGUID = UnitGUID and UnitGUID("player") + if casterGUID == playerGUID then nampowerSpellDamageActive = true + else nampowerControlledSpellActive = true end + if missInfo == 7 or missInfo == 8 then + if not HS:ShowEnemyImmune(targetName, targetGUID) then + HS:ShowSplat(targetName, 0, false, false, nil, nil, false, targetGUID) + end + else + HS:ShowSplat(targetName, 0, false, false, nil, nil, false, targetGUID) + end + elseif event == "SPELL_MISS_OTHER" then + local playerGUID = UnitGUID and UnitGUID("player") + if arg2 and arg2 == playerGUID then + nampowerIncomingSpellActive = true + HS:ShowSplat(UnitName("player"), 0, false, false, nil, nil, true, nil, true) + elseif isPlayerOwnedGUID(arg1) then + local targetGUID = arg2 + local targetName = unitNameForGUID(targetGUID) + local missInfo = tonumber(arg4) + nampowerControlledSpellActive = true + if missInfo == 7 or missInfo == 8 then + if not HS:ShowEnemyImmune(targetName, targetGUID) then + HS:ShowSplat(targetName, 0, false, false, nil, nil, false, targetGUID) + end + else + HS:ShowSplat(targetName, 0, false, false, nil, nil, false, targetGUID) + end + end + elseif event == "SPELL_HEAL_BY_SELF" then + nampowerHealsActive = true + local playerGUID = UnitGUID and UnitGUID("player") + local targetName = unitNameForGUID(arg1) or (arg1 == playerGUID and UnitName("player")) or "Friendly Player" + HS:ShowSplat(targetName, arg4, arg5 == 1, true, nil, nil, true, arg1) + elseif event == "BUFF_ADDED_SELF" or event == "BUFF_REMOVED_SELF" or event == "DEBUFF_ADDED_SELF" or event == "DEBUFF_REMOVED_SELF" then + nampowerAurasActive = true + if HS.db.showStatusEffects then + local spellName = spellNameFromID(arg3) or "Status Effect" + local texture = spellTextureFromID(arg3) + local stacks = tonumber(arg4) or 0 + local state = tonumber(arg7) or 0 + local stackText = stacks > 1 and " x" .. stacks or "" + local auraKey = "aura:" .. string.lower(spellName) + if state == 2 then return + elseif string.find(event, "REMOVED") then + HS:ShowStatusEffect("-" .. spellName, .65, .65, .65, texture, auraKey) + elseif string.find(event, "DEBUFF") then + HS:ShowStatusEffect("- " .. spellName .. stackText, 1, .3, .45, texture or "Interface\\Icons\\Spell_Shadow_CurseOfTounges", auraKey) + else + HS:ShowStatusEffect("+ " .. spellName .. stackText, .45, 1, .55, texture or "Interface\\Icons\\Spell_Holy_MagicalSentry", auraKey) + end + end + elseif event == "CHAT_MSG_SPELL_AURA_GONE_SELF" then + HS:HandleStatusMessage(arg1, true) + else + HS:HandleCombatMessage(event, arg1) + if event == "CHAT_MSG_SPELL_SELF_BUFF" or event == "CHAT_MSG_SPELL_PERIODIC_SELF_BUFFS" or event == "CHAT_MSG_SPELL_PERIODIC_SELF_DAMAGE" then + HS:HandleStatusMessage(arg1, false) + end + end +end) + +HS:SetScript("OnUpdate", function() + scanElapsed = scanElapsed + (arg1 or 0) + if scanElapsed > .25 then HS:ScanNameplates(); scanElapsed = 0 end + local now = GetTime() + local i, item + for i = table.getn(active), 1, -1 do + item = active[i] + if (item.usesNameplate and not anchorIsVisible(item.anchor)) or now - item.started >= item.duration then + removeActive(i) + else + item:SetAlpha(1) + end + end + for i = table.getn(interruptActive), 1, -1 do + item = interruptActive[i] + local progress = (now - item.started) / item.duration + if not anchorIsVisible(item.anchor) or progress >= 1 then + table.remove(interruptActive, i); releaseInterrupt(item) + elseif progress > .65 then + item:SetAlpha(1 - ((progress - .65) / .35)) + else + item:SetAlpha(1) + end + end + for i = table.getn(statusActive), 1, -1 do + item = statusActive[i] + local progress = (now - item.started) / item.duration + if progress >= 1 then + table.remove(statusActive, i); releaseStatusItem(item) + else + item:SetAlpha(1 - progress) + end + end + local spacing = (tonumber(HS.db.statusTextSize) or 17) + 4 + local rise = tonumber(HS.db.statusRiseDistance) or 45 + for i = 1, table.getn(statusActive) do + item = statusActive[i] + local progress = (now - item.started) / item.duration + item:ClearAllPoints() + item:SetPoint("BOTTOM", HS.StatusAnchor, "TOP", 0, 4 + (i - 1) * spacing + progress * rise) + end +end) + +SLASH_OSHS1 = "/oshs" +SLASH_OSHS2 = "/hitsplats" +SlashCmdList.OSHS = function(msg) + msg = string.lower(msg or "") + if msg == "test" then HS:Preview() + elseif msg == "interrupt" then + local targetName = UnitExists("target") and UnitName("target") + local targetGUID = targetName and UnitGUID and UnitGUID("target") + if not targetName then + DEFAULT_CHAT_FRAME:AddMessage("|cffff5555OSHS:|r target an enemy with a visible nameplate first.") + elseif not HS:ShowInterrupt(targetName, targetGUID) then + DEFAULT_CHAT_FRAME:AddMessage("|cffff5555OSHS:|r no visible nameplate found for " .. targetName .. ".") + end + elseif msg == "immune" then + local targetName = UnitExists("target") and UnitName("target") + local targetGUID = targetName and UnitGUID and UnitGUID("target") + if not targetName then + DEFAULT_CHAT_FRAME:AddMessage("|cffff5555OSHS:|r target an enemy with a visible nameplate first.") + elseif not HS:ShowEnemyImmune(targetName, targetGUID) then + DEFAULT_CHAT_FRAME:AddMessage("|cffff5555OSHS:|r no visible nameplate found for " .. targetName .. ".") + end + elseif msg == "reset" then + HS:ResetSettings() + if HS.RefreshOptions then HS:RefreshOptions() end + DEFAULT_CHAT_FRAME:AddMessage("|cffd58cffOSHS:|r settings reset.") + elseif HS.ToggleOptions then HS:ToggleOptions() + else DEFAULT_CHAT_FRAME:AddMessage("|cffff5555OSHS:|r options failed to load.") end +end diff --git a/OSHS.toc b/OSHS.toc new file mode 100644 index 0000000..31a4606 --- /dev/null +++ b/OSHS.toc @@ -0,0 +1,9 @@ +## Interface: 11200 +## Title: OSRS Hit Splashes +## Notes: Old School RuneScape-style combat hit splashes for OctoWoW +## Version: 1.2.6 +## SavedVariables: OSHSDB +## OptionalDeps: pfUI, !!!ClassicAPI + +Core.lua +Options.lua diff --git a/Options.lua b/Options.lua new file mode 100644 index 0000000..77195c9 --- /dev/null +++ b/Options.lua @@ -0,0 +1,143 @@ +local HS = OSHS + +local panel = CreateFrame("Frame", "OSHSOptions", UIParent) +panel:SetWidth(500); panel:SetHeight(620) +panel:SetPoint("CENTER", UIParent, "CENTER", 0, 0) +panel:SetFrameStrata("DIALOG") +panel:SetBackdrop({ bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background", edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border", tile = true, tileSize = 32, edgeSize = 32, insets = { left = 11, right = 12, top = 12, bottom = 11 } }) +panel:Hide(); panel:SetMovable(true); panel:EnableMouse(true); panel:RegisterForDrag("LeftButton") +panel:SetScript("OnDragStart", function() this:StartMoving() end) +panel:SetScript("OnDragStop", function() + this:StopMovingOrSizing() + local point, _, _, x, y = this:GetPoint(1) + HS.db.optionsPoint, HS.db.optionsX, HS.db.optionsY = point or "CENTER", x or 0, y or 0 +end) + +local title = panel:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge") +title:SetPoint("TOP", panel, "TOP", 0, -18); title:SetText("OSRS Hit Splashes") +local subtitle = panel:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall") +subtitle:SetPoint("TOP", title, "BOTTOM", 0, -7); subtitle:SetText("/oshs test previews the current style") +local close = CreateFrame("Button", nil, panel, "UIPanelCloseButton") +close:SetPoint("TOPRIGHT", panel, "TOPRIGHT", -5, -5) + +local checks = {} +local function addCheck(label, key, x, y, callback) + local button = CreateFrame("CheckButton", nil, panel, "UICheckButtonTemplate") + button:SetPoint("TOPLEFT", panel, "TOPLEFT", x, y) + button.text = button:CreateFontString(nil, "OVERLAY", "GameFontNormal") + button.text:SetPoint("LEFT", button, "RIGHT", 2, 0); button.text:SetText(label) + button.key = key + button:SetScript("OnClick", function() + HS.db[key] = this:GetChecked() and true or false + if callback then callback() end + end) + table.insert(checks, button) +end + +addCheck("Enable hitsplats", "enabled", 22, -62) +addCheck("Show damage", "showDamage", 178, -62) +addCheck("Show healing", "showHealing", 334, -62) +addCheck("Show misses as 0", "showMisses", 22, -94) +addCheck("Damage-school splats", "colorBySchool", 178, -94) +addCheck("Gold critical numbers", "critColor", 334, -94) +addCheck("Show damage taken", "showDamageTaken", 22, -126) +addCheck("Target-frame fallback", "useTargetFrame", 178, -126) +addCheck("Hide native damage", "hideBlizzardDamage", 350, -126, function() HS:ApplyNativeTextSetting() end) +addCheck("Show interrupt messages", "showInterrupts", 22, -154) + +local sliders, sliderCount = {}, 0 +local function formatValue(value, step) + if step >= 1 then return tostring(math.floor(value + .5)) end + return string.format("%.2f", value) +end +local function addSlider(label, key, minValue, maxValue, step, x, y) + sliderCount = sliderCount + 1 + local slider = CreateFrame("Slider", "OSHSSlider" .. sliderCount, panel, "OptionsSliderTemplate") + slider:SetWidth(205); slider:SetPoint("TOPLEFT", panel, "TOPLEFT", x, y) + slider:SetMinMaxValues(minValue, maxValue); slider:SetValueStep(step) + getglobal(slider:GetName() .. "Low"):SetText(tostring(minValue)) + getglobal(slider:GetName() .. "High"):SetText(tostring(maxValue)) + slider.label = getglobal(slider:GetName() .. "Text") + slider.key, slider.baseLabel, slider.step = key, label, step + slider:SetScript("OnValueChanged", function() + local value = this:GetValue() + if step >= 1 then value = math.floor(value + .5) else value = math.floor(value * 20 + .5) / 20 end + HS.db[key] = value + this.label:SetText(label .. ": " .. formatValue(value, step)) + end) + table.insert(sliders, slider) +end + +addSlider("Overall scale", "scale", .65, 1.5, .05, 25, -199) +addSlider("Text size", "textSize", 12, 34, 1, 270, -199) +addSlider("Hold duration", "duration", .5, 3, .05, 25, -259) +addSlider("Vertical offset", "offsetY", -100, 50, 1, 270, -259) +addSlider("Horizontal offset", "offsetX", -100, 100, 1, 25, -319) +addSlider("Side spacing", "horizontalSpacing", 10, 50, 1, 270, -319) +addSlider("Row spacing", "verticalSpacing", 10, 50, 1, 25, -379) + +local statusTitle = panel:CreateFontString(nil, "OVERLAY", "GameFontNormal") +statusTitle:SetPoint("TOPLEFT", panel, "TOPLEFT", 22, -420) +statusTitle:SetText("Scrolling status effects") +addCheck("Show buff, debuff, and fade messages", "showStatusEffects", 22, -438) +addCheck("Show status icons", "statusShowIcons", 292, -438) +addSlider("Status text size", "statusTextSize", 12, 28, 1, 25, -486) +addSlider("Status duration", "statusDuration", .5, 4, .05, 270, -486) +addSlider("Status rise", "statusRiseDistance", 0, 100, 5, 25, -546) + +local preview = CreateFrame("Button", nil, panel, "UIPanelButtonTemplate") +preview:SetWidth(105); preview:SetHeight(24); preview:SetPoint("BOTTOMLEFT", panel, "BOTTOMLEFT", 22, 14); preview:SetText("Hitsplat test") +preview:SetScript("OnClick", function() HS:Preview() end) +local statusPreview = CreateFrame("Button", nil, panel, "UIPanelButtonTemplate") +statusPreview:SetWidth(105); statusPreview:SetHeight(24); statusPreview:SetPoint("LEFT", preview, "RIGHT", 10, 0); statusPreview:SetText("Status test") +statusPreview:SetScript("OnClick", function() + HS:ShowStatusEffect("+ Arcane Intellect", .45, 1, .55, "Interface\\Icons\\Spell_Holy_MagicalSentry", "preview:buff") + HS:ShowStatusEffect("- Poison", 1, .3, .45, "Interface\\Icons\\Spell_Nature_CorrosiveBreath", "preview:debuff") + HS:ShowStatusEffect("-Blessing faded", .65, .65, .65, nil, "preview:fade") +end) +local anchorButton = CreateFrame("Button", nil, panel, "UIPanelButtonTemplate") +anchorButton:SetWidth(130); anchorButton:SetHeight(24); anchorButton:SetPoint("LEFT", statusPreview, "RIGHT", 10, 0) +anchorButton:SetScript("OnClick", function() HS:SetStatusAnchorLocked(not HS.db.statusAnchorLocked) end) +local reset = CreateFrame("Button", nil, panel, "UIPanelButtonTemplate") +reset:SetWidth(85); reset:SetHeight(24); reset:SetPoint("LEFT", anchorButton, "RIGHT", 10, 0); reset:SetText("Reset") +reset:SetScript("OnClick", function() HS:ResetSettings(); HS:RefreshOptions(); HS:Preview() end) + +function HS:UpdateStatusAnchorButton() + anchorButton:SetText(self.db.statusAnchorLocked and "Move anchor" or "Lock anchor") +end + +function HS:RefreshOptions() + local _, check, slider + for _, check in ipairs(checks) do check:SetChecked(self.db[check.key]) end + for _, slider in ipairs(sliders) do + slider:SetValue(self.db[slider.key]) + slider.label:SetText(slider.baseLabel .. ": " .. formatValue(self.db[slider.key], slider.step)) + end + self:UpdateStatusAnchorButton() +end + +panel:SetScript("OnShow", function() + panel:ClearAllPoints() + panel:SetPoint(HS.db.optionsPoint or "CENTER", UIParent, HS.db.optionsPoint or "CENTER", HS.db.optionsX or 0, HS.db.optionsY or 0) + HS:RefreshOptions() +end) + +function HS:ToggleOptions() + if panel:IsShown() then panel:Hide() else panel:Show() end +end + +function HS:ApplyPfUISkin() + if panel.pfUISkinned or not pfUI or not pfUI.api then return end + local api = pfUI.api + if not api.CreateBackdrop or not api.SkinButton then return end + panel:SetBackdrop(nil); api.CreateBackdrop(panel, nil, true, .88) + if api.CreateBackdropShadow then api.CreateBackdropShadow(panel) end + if api.SkinCloseButton then api.SkinCloseButton(close, panel.backdrop, -4, -4) end + local _, widget + if api.SkinCheckbox then for _, widget in ipairs(checks) do api.SkinCheckbox(widget) end end + api.SkinButton(preview); api.SkinButton(statusPreview); api.SkinButton(anchorButton); api.SkinButton(reset) + if api.SkinSlider then for _, widget in ipairs(sliders) do api.SkinSlider(widget) end end + panel.pfUISkinned = true +end + +if pfUI then pcall(function() HS:ApplyPfUISkin() end) end diff --git a/README.md b/README.md index 710f026..4be1b76 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,11 @@ -# OldSchool-HitSplashes +# OSRS Hit Splashes -OSRS themed/styled hitsplashes \ No newline at end of file +OSHS is a focused Octo WoW addon that displays Old School RuneScape-style hit splashes plus an optional scrolling buff/debuff status stream. It excludes CleanDamage's general floating-combat-text, sound, resource, combo-point, and warning modules. + +It supports outgoing player damage and DoTs; pet and totem damage; incoming damage; direct and periodic healing; critical hits; avoided attacks as blue zero splats; `Immune!` results above enemy nameplates; optional yellow interrupt notifications; damage-school artwork; default and pfUI nameplates; ClassicAPI nameplate tokens; and Nampower combat events when available. Successful interrupts are detected through the exact combat-log event when available, Nampower's interrupted victim state or correlated enemy cast failure, and the Vanilla chat line as fallback. With Nampower and ClassicAPI, AoE results are routed by GUID so same-named enemies receive hits on their own nameplates. Controlled-unit ownership is resolved through the caster's owner GUID, covering ordinary pets, guardians, and totems such as Magma Totem. + +Each target can hold four simultaneous splats. Existing splats remain for their configured duration while fewer than four are visible; only a fifth incoming result replaces the oldest occupied slot. + +Use `/oshs` or `/hitsplats` to open settings. Use `/oshs test` for a hitsplat preview, `/oshs interrupt` or `/oshs immune` to preview those messages on your current target's visible nameplate, and `/oshs reset` to restore defaults. + +The settings window controls outgoing and incoming damage visibility, interrupt messages, damage-school artwork, critical-number coloring, native Blizzard damage suppression, target-frame fallback, overall scale, text size, duration, offsets, and cluster spacing. It also controls the scrolling buff, debuff, and fade notification stream, including its icons, text size, duration, rise distance, and movable screen anchor. \ No newline at end of file